commit 1bdbaa95728981e6b3bad42b171427e40c72b574 Author: Zachery Aaron Shores-Chmielewski Date: Sat Jul 25 11:09:19 2026 +0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84892d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.venv/ +.loop/ +**/__pycache__ +python/src/foam_stepper.egg-info/ + +# foreign +OpenFOAM-14/ +ThirdParty-14/ +airfrans/ + +# no idea if this is some `make` residue or not but has some object files so we exclude it +pythonStepper/ diff --git a/notebooks/airfrans_equation_first_hand_simulation.ipynb b/notebooks/airfrans_equation_first_hand_simulation.ipynb new file mode 100644 index 0000000..69fd820 --- /dev/null +++ b/notebooks/airfrans_equation_first_hand_simulation.ipynb @@ -0,0 +1,262 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f61d8fbc", + "metadata": {}, + "source": [ + "# AirfRANS finite-volume hand simulation\n", + "\n", + "Purpose: learn the computation, not the Python stepper or OpenFOAM object graph.\n", + "\n", + "This notebook reduces the AirfRANS/OpenFOAM momentum loop to the smallest useful stencil:\n", + "\n", + "1. one internal face shared by two cells;\n", + "2. one boundary face on one of those cells.\n", + "\n", + "Repo grounding:\n", + "\n", + "- AirfRANS case scale: $U_\\infty=(93.009,6.161,0)$ m/s, $\\nu=1.56\\times10^{-5}$ m²/s, fields `U`, `p`, `phi`, `nut`, `k`, `omega`.\n", + "- OpenFOAM v14 momentum predictor source: `OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C`.\n", + "- The real predictor assembles\n", + "\n", + "$$\n", + "\\texttt{fvm::ddt(U)} + \\texttt{fvm::div(phi,U)} + \\texttt{momentumTransport->divDevSigma(U)}\n", + "= \\texttt{fvModels().source(U)}\n", + "$$\n", + "\n", + "then solves\n", + "\n", + "$$\n", + "\\texttt{UEqn == -fvc::grad(p)}.\n", + "$$\n", + "\n", + "For the hand calculation, keep one velocity component, ignore time/MRF/source/non-orthogonal details, and replace the viscous/turbulent stress term by scalar diffusion with\n", + "\n", + "$$\n", + "\\Gamma = \\nu + \\nu_t.\n", + "$$\n", + "\n", + "For a cell $P$, use the residual convention\n", + "\n", + "$$\n", + "R_P =\n", + "\\sum_f \\underbrace{\\phi_f U_f}_{\\text{convection leaving cell}}\n", + "-\n", + "\\sum_f \\underbrace{\\Gamma_f \\nabla U_f\\cdot S_f}_{\\text{diffusion leaving cell}}\n", + "+\n", + "\\sum_f \\underbrace{p_f S_{f,x}}_{\\text{pressure-gradient contribution}}.\n", + "$$\n", + "\n", + "The equation wants $R_P=0$. A face loop is bookkeeping for these three numbers.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "176670ce", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:22:08.071527Z", + "iopub.status.busy": "2026-07-24T06:22:08.071431Z", + "iopub.status.idle": "2026-07-24T06:22:08.076755Z", + "shell.execute_reply": "2026-07-24T06:22:08.076272Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "internal face: P -> N\n", + "AirfRANS anchor: U_inf=(93.009, 6.161, 0.0) m/s, nu=1.560e-05 m^2/s\n", + "face flux phi_f = 91\n", + "upwind U_f = 92\n", + "\n", + "convection owner P 8372 | neighbour N -8372\n", + "diffusion owner P 0.0400312 | neighbour N -0.0400312\n", + "pressure owner P 0.15 | neighbour N -0.15\n", + "residual owner P 8372.19 | neighbour N -8372.19\n", + "\n", + "conservation check: R_P + R_N = 0\n" + ] + } + ], + "source": [ + "import math\n", + "\n", + "# Real AirfRANS scale, kept only to anchor the toy numbers.\n", + "U_INF = (93.00914503999995, 6.161356013756317, 0.0)\n", + "NU = 1.56e-5\n", + "\n", + "# Toy two-cell state. P is the OpenFOAM owner, N is the neighbour.\n", + "# Face area vector S points from P to N.\n", + "U_P = 92.0 # x-velocity in owner cell P [m/s]\n", + "U_N = 90.0 # x-velocity in neighbour cell N [m/s]\n", + "p_P = 0.20 # incompressible pressure p/rho in P [m^2/s^2]\n", + "p_N = 0.10 # incompressible pressure p/rho in N [m^2/s^2]\n", + "S = 1.0 # face area vector x-component, owner-outward [m^2]\n", + "d = 1.0 # owner-to-neighbour center distance [m]\n", + "Gamma = NU + 2e-2 # nu + toy turbulent viscosity [m^2/s]\n", + "\n", + "# 1) Face flux: surfaceScalarField phi = U_f · S_f.\n", + "U_for_phi = 0.5 * (U_P + U_N)\n", + "phi_f = U_for_phi * S\n", + "\n", + "# 2) Convection: upwind momentum carried through the face.\n", + "U_f = U_P if phi_f >= 0 else U_N\n", + "convection_P = phi_f * U_f\n", + "convection_N = -convection_P\n", + "\n", + "# 3) Diffusion: - Gamma * grad(U) · S.\n", + "grad_U_dot_S_P = (U_N - U_P) / d * S\n", + "diffusion_P = -Gamma * grad_U_dot_S_P\n", + "diffusion_N = -diffusion_P\n", + "\n", + "# 4) Pressure-gradient contribution: p_f * S.\n", + "p_f = 0.5 * (p_P + p_N)\n", + "pressure_P = p_f * S\n", + "pressure_N = -pressure_P\n", + "\n", + "# 5) Residual contribution from this single internal face.\n", + "R_P_internal = convection_P + diffusion_P + pressure_P\n", + "R_N_internal = convection_N + diffusion_N + pressure_N\n", + "\n", + "assert math.isclose(R_P_internal + R_N_internal, 0.0, abs_tol=1e-12)\n", + "\n", + "def row(label, owner, neighbour):\n", + " print(f\"{label:<13} owner P {owner:>12.6g} | neighbour N {neighbour:>12.6g}\")\n", + "\n", + "print(\"internal face: P -> N\")\n", + "print(f\"AirfRANS anchor: U_inf=({U_INF[0]:.3f}, {U_INF[1]:.3f}, {U_INF[2]:.1f}) m/s, nu={NU:.3e} m^2/s\")\n", + "print(f\"face flux phi_f = {phi_f:.6g}\")\n", + "print(f\"upwind U_f = {U_f:.6g}\")\n", + "print()\n", + "row(\"convection\", convection_P, convection_N)\n", + "row(\"diffusion\", diffusion_P, diffusion_N)\n", + "row(\"pressure\", pressure_P, pressure_N)\n", + "row(\"residual\", R_P_internal, R_N_internal)\n", + "print()\n", + "print(f\"conservation check: R_P + R_N = {R_P_internal + R_N_internal:.6g}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7325d390", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:22:08.077905Z", + "iopub.status.busy": "2026-07-24T06:22:08.077807Z", + "iopub.status.idle": "2026-07-24T06:22:08.081575Z", + "shell.execute_reply": "2026-07-24T06:22:08.080994Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "boundary face on P: noSlip wall\n", + "patch algebra: U_b=0, p_b=p_P, phi_b=0\n", + "\n", + "convection owner P 0\n", + "diffusion owner P 3.68287\n", + "pressure owner P -0.2\n", + "wall total owner P 3.48287\n", + "\n", + "R_P before boundary = 8372.19\n", + "R_P after boundary = 8375.67\n", + "R_N unchanged = -8372.19\n" + ] + } + ], + "source": [ + "# Add one boundary face on the left side of owner cell P.\n", + "# This mimics the airfoil wall idea: noSlip velocity, zeroGradient pressure, no normal flux.\n", + "\n", + "U_wall = 0.0 # noSlip\n", + "p_wall = p_P # zeroGradient pressure -> boundary value equals owner value\n", + "S_wall = -1.0 # owner-outward area vector points left\n", + "area_wall = 1.0\n", + "d_wall = 0.5 # distance from cell center to wall face\n", + "phi_wall = 0.0 # impermeable wall\n", + "\n", + "# Convection through an impermeable wall is zero.\n", + "convection_wall_P = phi_wall * U_wall\n", + "\n", + "# Boundary diffusion uses the patch value instead of a neighbour value.\n", + "grad_U_dot_S_wall = (U_wall - U_P) / d_wall * area_wall\n", + "diffusion_wall_P = -Gamma * grad_U_dot_S_wall\n", + "\n", + "# Pressure term uses the boundary pressure and the boundary area vector.\n", + "pressure_wall_P = p_wall * S_wall\n", + "\n", + "R_wall_P = convection_wall_P + diffusion_wall_P + pressure_wall_P\n", + "R_P_after_wall = R_P_internal + R_wall_P\n", + "R_N_after_wall = R_N_internal\n", + "\n", + "assert R_N_after_wall == R_N_internal\n", + "assert diffusion_wall_P > 0\n", + "\n", + "def one(label, value):\n", + " print(f\"{label:<13} owner P {value:>12.6g}\")\n", + "\n", + "print(\"boundary face on P: noSlip wall\")\n", + "print(\"patch algebra: U_b=0, p_b=p_P, phi_b=0\")\n", + "print()\n", + "one(\"convection\", convection_wall_P)\n", + "one(\"diffusion\", diffusion_wall_P)\n", + "one(\"pressure\", pressure_wall_P)\n", + "one(\"wall total\", R_wall_P)\n", + "print()\n", + "print(f\"R_P before boundary = {R_P_internal:.6g}\")\n", + "print(f\"R_P after boundary = {R_P_after_wall:.6g}\")\n", + "print(f\"R_N unchanged = {R_N_after_wall:.6g}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "ec2cb6d6", + "metadata": {}, + "source": [ + "## What this teaches\n", + "\n", + "The OpenFOAM loop shape is now visible:\n", + "\n", + "| notebook arithmetic | OpenFOAM idea |\n", + "|---|---|\n", + "| `convection_*` | `fvm::div(phi,U)` moves momentum with face flux |\n", + "| `diffusion_*` | `momentumTransport->divDevSigma(U)` / diffusion uses face area and cell spacing |\n", + "| `pressure_*` | `-fvc::grad(p)` appears as face pressure times area vector |\n", + "| internal equal/opposite signs | owner/neighbour addressing conserves internal face transfers |\n", + "| wall-only contribution | a boundary patch replaces the missing neighbour with boundary-condition algebra |\n", + "\n", + "This is not yet SIMPLE, turbulence, pressure correction, or a real airfoil mesh. It is the smallest executable picture of what every finite-volume face loop contributes to the residual.\n", + "\n", + "Good use: change one value at a time — `U_N`, `p_N`, `Gamma`, `U_wall`, or `phi_wall` — predict the sign of the residual change, then rerun.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/airfrans_openfoam_algorithm_first.ipynb b/notebooks/airfrans_openfoam_algorithm_first.ipynb new file mode 100644 index 0000000..5f66b79 --- /dev/null +++ b/notebooks/airfrans_openfoam_algorithm_first.ipynb @@ -0,0 +1,814 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "232f64cf", + "metadata": {}, + "source": [ + "# AirfRANS in OpenFOAM: algorithm-first, array-visible\n", + "\n", + "Purpose: understand what OpenFOAM is doing in the AirfRANS steady RANS solve, without drowning in C++ framework structure.\n", + "\n", + "Pattern used throughout:\n", + "\n", + "```text\n", + "OpenFOAM phase → algorithmic meaning → equations used → arrays touched → parallelization shape → small numeric probe\n", + "```\n", + "\n", + "This is not a generic CFD derivation. It follows the repo's real OpenFOAM v14 `incompressibleFluid` path used to run a migrated AirfRANS `kOmegaSST` case.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7a6f30ae", + "metadata": {}, + "source": [ + "## 0. Mental model\n", + "\n", + "OpenFOAM is doing a finite-volume SIMPLE solve.\n", + "\n", + "```text\n", + "cells store unknowns\n", + "faces move flux\n", + "matrices encode neighbour coupling\n", + "pressure repairs continuity\n", + "turbulence updates effective viscosity\n", + "```\n", + "\n", + "Core arrays:\n", + "\n", + "| array | meaning |\n", + "|---|---|\n", + "| `V[c]` | cell volume |\n", + "| `C[c,3]` | cell centre |\n", + "| `Sf[f,3]` | oriented face area vector |\n", + "| `owner[f]`, `neighbour[f]` | face-to-cell topology |\n", + "| `phi[f]` | active solve face volume flux; empty front/back faces are bookkeeping for 2D |\n", + "| `U[c,3]` | mean velocity |\n", + "| `p[c]` | kinematic pressure |\n", + "| `k[c]`, `omega[c]`, `nut[c]` | SST turbulence state |\n", + "| `diag`, `upper`, `lower`, `source` | sparse finite-volume matrix storage |\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e6138eb2", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:07.329068Z", + "iopub.status.busy": "2026-07-24T06:27:07.328767Z", + "iopub.status.idle": "2026-07-24T06:27:09.064496Z", + "shell.execute_reply": "2026-07-24T06:27:09.063766Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'simulation': 'airFoil2D_SST_93.213_3.79_0.418_0.0_9.665', 'cells': 283400, 'internal_face_flux_entries': 565419, 'sparse_offdiag_entries': 565419, 'boundary_patches': ['aerofoil', 'freestream', 'frontAndBack'], 'U_shape': (283400, 3), 'p_shape': (283400,), 'phi_internal_shape': (565419,)}\n" + ] + } + ], + "source": [ + "# Fresh-kernel setup. Keep framework setup quarantined here.\n", + "from pathlib import Path\n", + "from contextlib import contextmanager\n", + "import os, sys, shutil, json, re\n", + "\n", + "# Automation can inject an incompatible PYTHONPATH. Drop it before compiled imports.\n", + "pythonpath = os.environ.pop(\"PYTHONPATH\", \"\")\n", + "for entry in pythonpath.split(os.pathsep):\n", + " if entry:\n", + " while entry in sys.path:\n", + " sys.path.remove(entry)\n", + "\n", + "import numpy as np\n", + "\n", + "\n", + "def find_repo_root(start):\n", + " start = Path(start).resolve()\n", + " for p in [start, *start.parents]:\n", + " if (p / \"pyproject.toml\").exists() and (p / \"OpenFOAM-14\").exists():\n", + " return p\n", + " raise RuntimeError(\"repo root not found\")\n", + "\n", + "\n", + "@contextmanager\n", + "def quiet_native_output():\n", + " # Suppress OpenFOAM banner/solver logs; this notebook prints selected facts instead.\n", + " sys.stdout.flush(); sys.stderr.flush()\n", + " devnull = os.open(os.devnull, os.O_WRONLY)\n", + " saved_out = os.dup(1)\n", + " saved_err = os.dup(2)\n", + " try:\n", + " os.dup2(devnull, 1)\n", + " os.dup2(devnull, 2)\n", + " yield\n", + " finally:\n", + " os.dup2(saved_out, 1)\n", + " os.dup2(saved_err, 2)\n", + " os.close(saved_out)\n", + " os.close(saved_err)\n", + " os.close(devnull)\n", + "\n", + "\n", + "def q(call, *args, **kwargs):\n", + " with quiet_native_output():\n", + " return call(*args, **kwargs)\n", + "\n", + "\n", + "ROOT = find_repo_root(Path.cwd())\n", + "os.chdir(ROOT)\n", + "sys.path.insert(0, str(ROOT / \"scripts\"))\n", + "\n", + "from openfoam_env import apply_openfoam_env\n", + "from prepare_airfrans_stepper_case import DEFAULT_SOURCE, prepare_case\n", + "\n", + "apply_openfoam_env()\n", + "import foam_stepper as foam\n", + "\n", + "WORK = ROOT / \"tmp\" / \"airfrans_openfoam_algorithm_first_notebook\"\n", + "CASE = WORK / \"airfrans_v14\"\n", + "SOURCE = DEFAULT_SOURCE\n", + "\n", + "# Rebuild the migrated case for every full notebook execution.\n", + "if CASE.exists():\n", + " shutil.rmtree(CASE)\n", + "WORK.mkdir(parents=True, exist_ok=True)\n", + "meta = prepare_case(SOURCE, CASE, end_time=1)\n", + "\n", + "with quiet_native_output():\n", + " stepper = foam.Case(CASE).make_stepper()\n", + " mesh = stepper.mesh()\n", + " fields = stepper.fields()\n", + "\n", + "# Plain arrays used by the rest of the notebook.\n", + "V = np.asarray(mesh.V)\n", + "C = np.asarray(mesh.C)\n", + "Cf = np.asarray(mesh.Cf)\n", + "Sf = np.asarray(mesh.Sf)\n", + "magSf = np.asarray(mesh.magSf)\n", + "owner = np.asarray(mesh.owner, dtype=np.int64)\n", + "neighbour = np.asarray(mesh.neighbour, dtype=np.int64)\n", + "patches = list(mesh.boundary)\n", + "\n", + "U0 = np.asarray(fields[\"U\"].internal)\n", + "p0 = np.asarray(fields[\"p\"].internal)\n", + "phi0 = np.asarray(fields[\"phi\"].internal)\n", + "k0 = np.asarray(fields[\"k\"].internal)\n", + "omega0 = np.asarray(fields[\"omega\"].internal)\n", + "nut0 = np.asarray(fields[\"nut\"].internal)\n", + "phi_field0 = fields[\"phi\"]\n", + "\n", + "print({\n", + " \"simulation\": meta.simulation,\n", + " \"cells\": int(V.size),\n", + " \"internal_face_flux_entries\": int(phi0.size),\n", + " \"sparse_offdiag_entries\": int(neighbour.size),\n", + " \"boundary_patches\": [patch.name for patch in patches],\n", + " \"U_shape\": U0.shape,\n", + " \"p_shape\": p0.shape,\n", + " \"phi_internal_shape\": phi0.shape,\n", + "})\n" + ] + }, + { + "cell_type": "markdown", + "id": "763c6a18", + "metadata": {}, + "source": [ + "## 1. Case contract: what algorithm OpenFOAM has been asked to run\n", + "\n", + "Before thinking about kernels, read the dictionaries as the numerical contract:\n", + "\n", + "- `controlDict`: steady run controls and selected solver;\n", + "- `fvSchemes`: discrete operators, interpolation, gradients, divergence, laplacians;\n", + "- `fvSolution`: linear solvers, SIMPLE controls, relaxation;\n", + "- initial fields: boundary conditions and starting values;\n", + "- `momentumTransport`: RAS model choice, here `kOmegaSST`.\n", + "\n", + "`frontAndBack` is an OpenFOAM `empty` patch. It matters for declaring the case 2D, but it does not contribute active face-flux work in the solve view used below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eadd8926", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:09.065740Z", + "iopub.status.busy": "2026-07-24T06:27:09.065631Z", + "iopub.status.idle": "2026-07-24T06:27:09.069646Z", + "shell.execute_reply": "2026-07-24T06:27:09.069229Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "case\n", + "{'solver': 'incompressibleFluid', 'endTime': '1', 'deltaT': '1', 'RAS_model': 'kOmegaSST', 'freestream_speed': 93.213, 'alpha_deg': 3.79, 'Re': 5975192.3}\n", + "\n", + "patches\n", + "{'name': 'aerofoil', 'type': 'wall', 'active_faces_in_stepper_view': 1026}\n", + "{'name': 'freestream', 'type': 'patch', 'active_faces_in_stepper_view': 1736}\n", + "{'name': 'frontAndBack', 'type': 'empty', 'active_faces_in_stepper_view': 0}\n", + "frontAndBack is an empty 2D patch; it has no active solve-flux faces here.\n", + "\n", + "numerics\n", + "{'has_SIMPLE_block': True, 'has_div_phi_U_scheme': True, 'has_laplacian_schemes': True, 'has_relaxationFactors': True}\n" + ] + } + ], + "source": [ + "def assignment(text, key, default=None):\n", + " m = re.search(rf\"^\\s*{re.escape(key)}\\s+([^;]+);\", text, flags=re.MULTILINE)\n", + " return default if m is None else m.group(1).strip()\n", + "\n", + "\n", + "def contains_line(text, needle):\n", + " return any(needle in line for line in text.splitlines())\n", + "\n", + "control = (CASE / \"system/controlDict\").read_text(errors=\"replace\")\n", + "schemes = (CASE / \"system/fvSchemes\").read_text(errors=\"replace\")\n", + "solution = (CASE / \"system/fvSolution\").read_text(errors=\"replace\")\n", + "transport = (CASE / \"constant/momentumTransport\").read_text(errors=\"replace\")\n", + "\n", + "patch_rows = []\n", + "for patch in patches:\n", + " patch_rows.append((patch.name, patch.type, int(patch.size), int(patch.start)))\n", + "\n", + "print(\"case\")\n", + "print({\n", + " \"solver\": assignment(control, \"solver\"),\n", + " \"endTime\": assignment(control, \"endTime\"),\n", + " \"deltaT\": assignment(control, \"deltaT\"),\n", + " \"RAS_model\": assignment(transport, \"model\"),\n", + " \"freestream_speed\": meta.u_inf,\n", + " \"alpha_deg\": round(meta.alpha_deg, 6),\n", + " \"Re\": round(meta.reynolds, 1),\n", + "})\n", + "\n", + "print(\"\\npatches\")\n", + "for row in patch_rows:\n", + " print({\"name\": row[0], \"type\": row[1], \"active_faces_in_stepper_view\": row[2]})\n", + "print(\"frontAndBack is an empty 2D patch; it has no active solve-flux faces here.\")\n", + "\n", + "print(\"\\nnumerics\")\n", + "print({\n", + " \"has_SIMPLE_block\": \"SIMPLE\" in solution,\n", + " \"has_div_phi_U_scheme\": contains_line(schemes, \"div(phi,U)\"),\n", + " \"has_laplacian_schemes\": \"laplacianSchemes\" in schemes,\n", + " \"has_relaxationFactors\": \"relaxationFactors\" in solution,\n", + "})\n" + ] + }, + { + "cell_type": "markdown", + "id": "943203bb", + "metadata": {}, + "source": [ + "## 2. The actual OpenFOAM solve order\n", + "\n", + "For this repo's OpenFOAM v14 path, `simpleFoam` maps to `foamRun -solver incompressibleFluid`.\n", + "The relevant algorithmic order is:\n", + "\n", + "```text\n", + "pre_solve\n", + "advance_time\n", + "begin SIMPLE/PIMPLE iteration\n", + " fv_models_correct\n", + " pre_predictor\n", + " momentum_transport_predictor\n", + " assemble momentum terms\n", + " assemble UEqn\n", + " relax UEqn\n", + " constrain UEqn\n", + " solve momentum predictor\n", + " compute pressure inputs: rAU, HbyA, phiHbyA\n", + " assemble pEqn\n", + " solve pEqn\n", + " correct phi, p, U\n", + " momentum_transport_corrector # k-omega SST: omega, k, nut\n", + "end iteration\n", + "post_solve\n", + "```\n", + "\n", + "Parallel intuition:\n", + "\n", + "```text\n", + "assembly is mostly face/cell parallel\n", + "linear solves are global iterative kernels\n", + "boundary conditions are patch kernels\n", + "residuals/convergence are reductions\n", + "phase boundaries are synchronization points\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d13855ca", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:09.070871Z", + "iopub.status.busy": "2026-07-24T06:27:09.070736Z", + "iopub.status.idle": "2026-07-24T06:27:09.080021Z", + "shell.execute_reply": "2026-07-24T06:27:09.079575Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'initial_continuity_residual': {'Linf': 0.061028931972296335, 'L1': 20.391531263432356}, 'note': 'Signed internal owner/neighbour flux plus boundary patch flux into each cell.'}\n" + ] + } + ], + "source": [ + "def continuity_residual_from_phi(phi_field, owner, neighbour, patches, n_cells):\n", + " \"\"\"Signed flux sum per cell, including boundary patch fluxes.\"\"\"\n", + " internal_phi = np.asarray(phi_field.internal)\n", + " r = np.zeros(n_cells, dtype=internal_phi.dtype)\n", + "\n", + " np.add.at(r, owner, internal_phi[:len(owner)])\n", + " np.add.at(r, neighbour, -internal_phi[:len(neighbour)])\n", + "\n", + " for patch in patches:\n", + " values = np.asarray(phi_field.boundary[patch.name].values).reshape(-1)\n", + " if values.size == 0:\n", + " continue\n", + " face_cells = np.asarray(patch.face_cells, dtype=np.int64)[:values.size]\n", + " np.add.at(r, face_cells, values)\n", + " return r\n", + "\n", + "\n", + "def norm_report(x):\n", + " x = np.asarray(x)\n", + " return {\n", + " \"Linf\": float(np.max(np.abs(x))) if x.size else 0.0,\n", + " \"L1\": float(np.sum(np.abs(x))) if x.size else 0.0,\n", + " }\n", + "\n", + "mass0 = continuity_residual_from_phi(phi_field0, owner, neighbour, patches, len(V))\n", + "print({\n", + " \"initial_continuity_residual\": norm_report(mass0),\n", + " \"note\": \"Signed internal owner/neighbour flux plus boundary patch flux into each cell.\",\n", + "})\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "3f5ddd40", + "metadata": {}, + "source": [ + "## 3. Momentum predictor\n", + "\n", + "OpenFOAM operation, stripped to computation:\n", + "\n", + "```text\n", + "UEqn = ddt(U) + div(phi, U) + turbulent_stress_divergence(U, nut) == sources\n", + "relax UEqn\n", + "apply matrix/boundary constraints\n", + "solve UEqn == -grad(p)\n", + "```\n", + "\n", + "Sparse-row view:\n", + "\n", + "$$\n", + "A_c U_c + \\sum_{n\\in N(c)} A_{cn}U_n = b_c - V_c(\\nabla p)_c\n", + "$$\n", + "\n", + "Arrays touched:\n", + "\n", + "```text\n", + "read: U, p, phi, nut, V, Sf, owner, neighbour, boundary fields, fvSchemes, fvSolution\n", + "write: UEqn.diag, UEqn.upper/lower, UEqn.source, possibly U\n", + "```\n", + "\n", + "Parallel shape:\n", + "\n", + "```text\n", + "per-face work: convection/diffusion coupling across owner-neighbour\n", + "per-cell work: diagonal/source accumulation and explicit terms\n", + "solver work: sparse matrix-vector iterations plus reductions\n", + "hazard: face contributions scatter into two cells unless accumulation is organized carefully\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "57ce4919", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:09.081243Z", + "iopub.status.busy": "2026-07-24T06:27:09.081153Z", + "iopub.status.idle": "2026-07-24T06:27:09.322109Z", + "shell.execute_reply": "2026-07-24T06:27:09.321878Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'phases_reached': ['pre_solve', 'advance_time', 'begin_pimple_iteration', 'fv_models_correct', 'pre_predictor', 'momentum_transport_predict', 'assemble_momentum_terms', 'assemble_UEqn'], 'momentum_terms': ['assemble_momentum_ddt', 'assemble_momentum_div_phi_U', 'assemble_momentum_divDevSigma', 'assemble_momentum_sources', 'assemble_momentum_MRF_DDt', 'assemble_momentum_pressure_rhs_grad_p'], 'diag': (283400,), 'upper': (565419,), 'lower': (565419,), 'source': (283400, 3), 'H': (283400, 3), 'diag_first_5': [4.5674015979225775, 4.426112149473814, 4.2788585852295125, 4.134855363049338, 3.9941324145628894]}\n" + ] + } + ], + "source": [ + "phase_names = []\n", + "for method in [\n", + " stepper.pre_solve,\n", + " stepper.advance_time,\n", + " stepper.begin_pimple_iteration,\n", + " stepper.fv_models_correct,\n", + " stepper.pre_predictor,\n", + " stepper.momentum_transport_predictor,\n", + "]:\n", + " result = q(method)\n", + " phase_names.append(result.name)\n", + "\n", + "terms = q(stepper.assemble_momentum_terms)\n", + "UEqn_result = q(stepper.assemble_momentum_matrix)\n", + "UEqn = UEqn_result.outputs[\"UEqn\"]\n", + "phase_names.extend([terms.name, UEqn_result.name])\n", + "\n", + "A_U = np.asarray(UEqn.diag)\n", + "upper_U = None if UEqn.upper is None else np.asarray(UEqn.upper)\n", + "lower_U = None if UEqn.lower is None else np.asarray(UEqn.lower)\n", + "b_U = np.asarray(UEqn.source)\n", + "H_U = None if UEqn.H is None else np.asarray(UEqn.H.internal)\n", + "\n", + "print({\n", + " \"phases_reached\": phase_names,\n", + " \"momentum_terms\": [t[\"name\"] for t in terms.outputs[\"terms\"]],\n", + " \"diag\": A_U.shape,\n", + " \"upper\": None if upper_U is None else upper_U.shape,\n", + " \"lower\": None if lower_U is None else lower_U.shape,\n", + " \"source\": b_U.shape,\n", + " \"H\": None if H_U is None else H_U.shape,\n", + " \"diag_first_5\": A_U[:5].tolist(),\n", + "})\n" + ] + }, + { + "cell_type": "markdown", + "id": "63757c15", + "metadata": {}, + "source": [ + "## 4. One face as kernel anatomy, not serial control flow\n", + "\n", + "The one-face view is useful only as a stencil exemplar.\n", + "It should read as:\n", + "\n", + "```text\n", + "this is one work item shape; many faces run in parallel\n", + "```\n", + "\n", + "For an internal face `f`, a face kernel typically reads both adjacent cells and writes contributions to both matrix rows:\n", + "\n", + "```text\n", + "o = owner[f]\n", + "n = neighbour[f]\n", + "read: U[o], U[n], C[o], C[n], Sf[f], phi[f], nut[o], nut[n]\n", + "write: row contribution for o and row contribution for n\n", + "```\n", + "\n", + "Race risk appears if many faces scatter-add into the same cell row at once.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "39d467a5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:09.323799Z", + "iopub.status.busy": "2026-07-24T06:27:09.323707Z", + "iopub.status.idle": "2026-07-24T06:27:09.325983Z", + "shell.execute_reply": "2026-07-24T06:27:09.325697Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'face': 282709, 'owner_cell': 141545, 'neighbour_cell': 141546, 'Sf': [-0.009744034299999982, -0.0027874465999999654, 0.0], 'magSf': 0.010134893338729693, 'phi_before': -0.9234587503553671, 'U_owner_before': [93.00914503999995, 6.161356013756317, 0.0], 'U_neighbour_before': [93.00914503999995, 6.161356013756317, 0.0], 'center_distance': 0.001573663286642463}\n" + ] + } + ], + "source": [ + "f = int(len(neighbour) // 2)\n", + "o = int(owner[f])\n", + "n = int(neighbour[f])\n", + "\n", + "face_probe = {\n", + " \"face\": f,\n", + " \"owner_cell\": o,\n", + " \"neighbour_cell\": n,\n", + " \"Sf\": Sf[f].tolist(),\n", + " \"magSf\": float(magSf[f]),\n", + " \"phi_before\": float(phi0[f]),\n", + " \"U_owner_before\": U0[o].tolist(),\n", + " \"U_neighbour_before\": U0[n].tolist(),\n", + " \"center_distance\": float(np.linalg.norm(C[n] - C[o])),\n", + "}\n", + "print(face_probe)\n" + ] + }, + { + "cell_type": "markdown", + "id": "906e541d", + "metadata": {}, + "source": [ + "## 5. Pressure correction\n", + "\n", + "OpenFOAM operation, stripped to computation:\n", + "\n", + "```text\n", + "rAU = 1 / UEqn.A\n", + "HbyA = constrained(rAU * UEqn.H)\n", + "phiHbyA = flux(HbyA) + time/mesh correction\n", + "apply pressure boundary consistency\n", + "pEqn = laplacian(rAU, p) == div(phiHbyA)\n", + "solve pEqn\n", + "phi = phiHbyA - pEqn.flux()\n", + "p.relax()\n", + "U = HbyA - rAU * grad(p)\n", + "correct U boundary conditions\n", + "```\n", + "\n", + "Why this exists:\n", + "\n", + "```text\n", + "momentum predicts U\n", + "predicted U may violate continuity\n", + "pressure solve computes a flux correction\n", + "corrected phi is the flux field OpenFOAM uses for continuity-error accounting\n", + "U is then corrected to be consistent with that pressure/flux repair\n", + "```\n", + "\n", + "Parallel shape:\n", + "\n", + "```text\n", + "per-cell: rAU, HbyA\n", + "per-face: interpolate HbyA/rAU and compute phiHbyA\n", + "solver: pressure Laplacian iterations are global\n", + "per-face: corrected phi\n", + "per-cell: corrected U\n", + "reduction: continuity errors\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "958d0625", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:09.327451Z", + "iopub.status.busy": "2026-07-24T06:27:09.327360Z", + "iopub.status.idle": "2026-07-24T06:27:09.655600Z", + "shell.execute_reply": "2026-07-24T06:27:09.655213Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'matrix_steps': ['relax_UEqn', 'constrain_UEqn', 'solve_UEqn'], 'pressure_steps': ['compute_pressure_inputs', 'assemble_pEqn'], 'rAU': (283400,), 'HbyA': (283400, 3), 'phiHbyA': (565419,), 'p_diag': (283400,), 'p_upper': (565419,), 'p_source': (283400,), 'rAU_first_5': [0.00026755862048551105, 0.00024708843305873933, 0.00022836470465003848, 0.00021125319224392247, 0.00019560221178537135]}\n" + ] + } + ], + "source": [ + "relax_result = q(stepper.relax_matrix)\n", + "constrain_result = q(stepper.constrain_matrix)\n", + "momentum_solve = q(stepper.solve_momentum)\n", + "pressure_inputs = q(stepper.compute_pressure_inputs)\n", + "pEqn_result = q(stepper.assemble_pressure_matrix)\n", + "pEqn = pEqn_result.outputs[\"pEqn\"]\n", + "\n", + "rAU = np.asarray(pressure_inputs.outputs[\"rAU\"].internal)\n", + "HbyA = np.asarray(pressure_inputs.outputs[\"HbyA\"].internal)\n", + "phiHbyA = np.asarray(pressure_inputs.outputs[\"phiHbyA\"].internal)\n", + "A_p = np.asarray(pEqn.diag)\n", + "upper_p = None if pEqn.upper is None else np.asarray(pEqn.upper)\n", + "b_p = np.asarray(pEqn.source)\n", + "\n", + "print({\n", + " \"matrix_steps\": [relax_result.name, constrain_result.name, momentum_solve.name],\n", + " \"pressure_steps\": [pressure_inputs.name, pEqn_result.name],\n", + " \"rAU\": rAU.shape,\n", + " \"HbyA\": HbyA.shape,\n", + " \"phiHbyA\": phiHbyA.shape,\n", + " \"p_diag\": A_p.shape,\n", + " \"p_upper\": None if upper_p is None else upper_p.shape,\n", + " \"p_source\": b_p.shape,\n", + " \"rAU_first_5\": rAU[:5].tolist(),\n", + "})\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ab30847e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:09.656945Z", + "iopub.status.busy": "2026-07-24T06:27:09.656826Z", + "iopub.status.idle": "2026-07-24T06:27:12.376771Z", + "shell.execute_reply": "2026-07-24T06:27:12.376091Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'pressure_steps': ['solve_pEqn', 'correct_velocity_pressure_flux'], 'changed_fields': ['p', 'U', 'phi'], 'delta_U_Linf': 142.80037348998195, 'delta_p_Linf': 139306.85715654117, 'delta_phi_Linf': 0.12004663628376555, 'continuity_after_pressure': {'Linf': 0.026893021480056893, 'L1': 1.0664099985737665}}\n" + ] + } + ], + "source": [ + "pressure_solve = q(stepper.solve_pressure)\n", + "correct_result = q(stepper.correct_velocity_pressure_flux)\n", + "\n", + "fields_after_pressure = stepper.fields()\n", + "U_after_pressure = np.asarray(fields_after_pressure[\"U\"].internal)\n", + "p_after_pressure = np.asarray(fields_after_pressure[\"p\"].internal)\n", + "phi_after_pressure_field = fields_after_pressure[\"phi\"]\n", + "phi_after_pressure = np.asarray(phi_after_pressure_field.internal)\n", + "\n", + "mass_after_pressure = continuity_residual_from_phi(phi_after_pressure_field, owner, neighbour, patches, len(V))\n", + "\n", + "print({\n", + " \"pressure_steps\": [pressure_solve.name, correct_result.name],\n", + " \"changed_fields\": correct_result.changed_fields,\n", + " \"delta_U_Linf\": float(np.max(np.abs(U_after_pressure - U0))),\n", + " \"delta_p_Linf\": float(np.max(np.abs(p_after_pressure - p0))),\n", + " \"delta_phi_Linf\": float(np.max(np.abs(phi_after_pressure - phi0))),\n", + " \"continuity_after_pressure\": norm_report(mass_after_pressure),\n", + "})\n" + ] + }, + { + "cell_type": "markdown", + "id": "4c384cfc", + "metadata": {}, + "source": [ + "## 6. Turbulence corrector: k-omega SST\n", + "\n", + "OpenFOAM's `kOmegaSST` corrector does this computationally:\n", + "\n", + "```text\n", + "compute grad(U)\n", + "compute strain magnitude S2\n", + "compute production G from nut and grad(U)\n", + "update omega wall coefficients\n", + "compute SST blending functions F1, F2/F23\n", + "assemble and solve omega equation\n", + "bound omega\n", + "assemble and solve k equation\n", + "bound k and omega\n", + "update nut = a1*k / max(a1*omega, b1*F2*sqrt(S2))\n", + "apply nut boundary conditions/constraints\n", + "```\n", + "\n", + "For parallelization:\n", + "\n", + "```text\n", + "grad(U): face/cell stencil\n", + "production/S2: per-cell\n", + "omega matrix: face/cell assembly + sparse solve\n", + "k matrix: face/cell assembly + sparse solve\n", + "nut update: embarrassingly per-cell, plus patch kernels for wall functions\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "d8e88122", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-24T06:27:12.378174Z", + "iopub.status.busy": "2026-07-24T06:27:12.378067Z", + "iopub.status.idle": "2026-07-24T06:27:12.634993Z", + "shell.execute_reply": "2026-07-24T06:27:12.634592Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'final_steps': ['momentum_transport_correct', 'end_pimple_iteration', 'post_solve'], 'turbulence_changed_fields': ['viscosity', 'momentumTransport'], 'delta_k_Linf': 2.9020790511255652e-08, 'delta_omega_Linf': 1718225865.8661606, 'delta_nut_Linf': 3.1199999716434326e-09, 'final_continuity_residual': {'Linf': 0.026893021480056893, 'L1': 1.0664099985737665}, 'omega_delta_note': 'Linf is wall-dominated because omega wall coefficients update strongly.'}\n" + ] + } + ], + "source": [ + "turbulence_result = q(stepper.momentum_transport_corrector)\n", + "end_result = q(stepper.end_pimple_iteration)\n", + "post_result = q(stepper.post_solve, write=False)\n", + "\n", + "fields1 = stepper.fields()\n", + "U1 = np.asarray(fields1[\"U\"].internal)\n", + "p1 = np.asarray(fields1[\"p\"].internal)\n", + "phi_field1 = fields1[\"phi\"]\n", + "phi1 = np.asarray(phi_field1.internal)\n", + "k1 = np.asarray(fields1[\"k\"].internal)\n", + "omega1 = np.asarray(fields1[\"omega\"].internal)\n", + "nut1 = np.asarray(fields1[\"nut\"].internal)\n", + "\n", + "mass1 = continuity_residual_from_phi(phi_field1, owner, neighbour, patches, len(V))\n", + "\n", + "print({\n", + " \"final_steps\": [turbulence_result.name, end_result.name, post_result.name],\n", + " \"turbulence_changed_fields\": turbulence_result.changed_fields,\n", + " \"delta_k_Linf\": float(np.max(np.abs(k1 - k0))),\n", + " \"delta_omega_Linf\": float(np.max(np.abs(omega1 - omega0))),\n", + " \"delta_nut_Linf\": float(np.max(np.abs(nut1 - nut0))),\n", + " \"final_continuity_residual\": norm_report(mass1),\n", + " \"omega_delta_note\": \"Linf is wall-dominated because omega wall coefficients update strongly.\",\n", + "})\n" + ] + }, + { + "cell_type": "markdown", + "id": "a78169fd", + "metadata": {}, + "source": [ + "## 7. Parallelization map\n", + "\n", + "| OpenFOAM algorithm phase | natural work items | writes | synchronization / hazard |\n", + "|---|---|---|---|\n", + "| face flux/interpolation | faces | `phi[f]`, face temporaries | reads owner/neighbour cells |\n", + "| continuity residual | faces or cells | residual per cell | face scatter needs atomics or two-pass accumulation |\n", + "| momentum assembly | faces + cells | sparse matrix rows | owner/neighbour scatter or row-wise gather |\n", + "| matrix relaxation/constraints | cells + patches | matrix rows, boundary coeffs | patch-specific logic |\n", + "| momentum solve | sparse rows | `U` iterations | global reductions each Krylov iteration |\n", + "| pressure-input build | cells + faces | `rAU`, `HbyA`, `phiHbyA` | interpolation crosses faces |\n", + "| pressure assembly/solve | faces + sparse rows | pressure matrix, `p` iterations | global solve/reductions |\n", + "| flux/velocity correction | faces + cells | `phi`, `U` | pressure field must be solved first |\n", + "| SST corrector | cells + faces + patches | `omega`, `k`, `nut` | two scalar solves, wall-function patches |\n", + "\n", + "Main lesson for GPU work:\n", + "\n", + "```text\n", + "OpenFOAM's loop is phase-serial.\n", + "Inside each phase, most assembly/update work is data-parallel.\n", + "The sparse solvers and reductions are the major global synchronization points.\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "3737d955", + "metadata": {}, + "source": [ + "## 8. What this notebook should let you explain\n", + "\n", + "After working through it, you should be able to say:\n", + "\n", + "1. which OpenFOAM phase builds the momentum matrix;\n", + "2. why pressure correction exists;\n", + "3. why `phi` is central to continuity;\n", + "4. where boundary conditions alter matrices/fields;\n", + "5. which parts are face-parallel, cell-parallel, patch-parallel, or globally synchronized;\n", + "6. why a one-face/one-cell probe is a kernel stencil example, not the serial algorithm.\n", + "\n", + "If a future section cannot be described as `phase → arrays → kernel shape`, it is probably framework spandrel and should be cut.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/airfrans_stepper_full_detail_learning_tool.ipynb b/notebooks/airfrans_stepper_full_detail_learning_tool.ipynb new file mode 100644 index 0000000..b1fa7b6 --- /dev/null +++ b/notebooks/airfrans_stepper_full_detail_learning_tool.ipynb @@ -0,0 +1,7597 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "full-detail-00", + "metadata": {}, + "source": [ + "# AirfRANS RANS simulation full-detail microscope\n", + "\n", + "This notebook is a full-detail companion to `airfrans_stepper_learning_tool.ipynb` for the same real AirfRANS OpenFOAM case:\n", + "\n", + "`airFoil2D_SST_93.213_3.79_0.418_0.0_9.665`\n", + "\n", + "The goal is to expose the simulation without silently hiding large structures:\n", + "\n", + "1. identify the physical problem encoded by the AirfRANS case;\n", + "2. inspect how OpenFOAM stores mesh topology, patches, fields, and boundary conditions;\n", + "3. display full tensor shapes, dtypes, entity counts, and truncation policy every time values are too large for a notebook cell;\n", + "4. visualize every large field or mesh object through histograms, binned spatial maps, and sparse-graph density plots so every entity contributes even when raw value text is truncated;\n", + "5. step through one steady SIMPLE iteration with the Python OpenFOAM stepper;\n", + "6. inspect each intermediate object: `MeshView`, `FieldView`, `PatchFieldView`, `MatrixView`, `SolveResult`, and `TransformResult`;\n", + "7. connect each object back to finite-volume RANS mechanics.\n", + "\n", + "Output contract for this full-detail edition: raw value displays may truncate values to keep cells readable, but every display keeps the exact shape, dtype, size, and entity count. Visualizations aggregate complete arrays unless explicitly labeled as a selected-row view for ragged OpenFOAM lists.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "full-detail-01", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:17.543442Z", + "iopub.status.busy": "2026-07-22T20:00:17.543233Z", + "iopub.status.idle": "2026-07-22T20:00:17.550312Z", + "shell.execute_reply": "2026-07-22T20:00:17.549370Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cwd=/home/aaron/data/openFOAM-RANS-to-GPU/notebooks\n", + "repo=/home/aaron/data/openFOAM-RANS-to-GPU\n", + "kernel_python=/home/aaron/data/openFOAM-RANS-to-GPU/.venv/bin/python\n", + "project_python=/home/aaron/data/openFOAM-RANS-to-GPU/.venv/bin/python\n", + "raw_case=/home/aaron/data/airfrans/data/raw/OF_dataset/airFoil2D_SST_93.213_3.79_0.418_0.0_9.665\n", + "raw_case_exists=True\n" + ] + } + ], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import re\n", + "import shutil\n", + "import subprocess\n", + "import sys\n", + "\n", + "\n", + "def find_repo_root(start: Path) -> Path:\n", + " \"\"\"Find the repo even when Jupyter starts in ./notebooks.\"\"\"\n", + " for candidate in (start.resolve(), *start.resolve().parents):\n", + " if (candidate / \"pyproject.toml\").exists() and (candidate / \"OpenFOAM-14\").exists() and (candidate / \"scripts\").exists():\n", + " return candidate\n", + " raise RuntimeError(f\"Could not find openFOAM-RANS-to-GPU repo root from {start}\")\n", + "\n", + "\n", + "def drop_ambient_pythonpath() -> None:\n", + " \"\"\"Avoid accidentally importing packages from an external PYTHONPATH.\"\"\"\n", + " pythonpath = os.environ.pop(\"PYTHONPATH\", \"\")\n", + " for entry in pythonpath.split(os.pathsep):\n", + " if not entry:\n", + " continue\n", + " while entry in sys.path:\n", + " sys.path.remove(entry)\n", + "\n", + "\n", + "ROOT = find_repo_root(Path.cwd())\n", + "PYTHON = ROOT / \".venv/bin/python\"\n", + "if not PYTHON.exists():\n", + " PYTHON = Path(sys.executable)\n", + "SCRIPTS = ROOT / \"scripts\"\n", + "if str(SCRIPTS) not in sys.path:\n", + " sys.path.insert(0, str(SCRIPTS))\n", + "\n", + "drop_ambient_pythonpath()\n", + "\n", + "RAW_CASE = ROOT.parent / \"airfrans/data/raw/OF_dataset/airFoil2D_SST_93.213_3.79_0.418_0.0_9.665\"\n", + "WORK = ROOT / \"tmp/airfrans_stepper_full_detail_learning_tool\"\n", + "CASE = WORK / \"split_case\"\n", + "HIGH_LEVEL_CASE = WORK / \"high_level_case\"\n", + "\n", + "print(f\"cwd={Path.cwd()}\")\n", + "print(f\"repo={ROOT}\")\n", + "print(f\"kernel_python={sys.executable}\")\n", + "print(f\"project_python={PYTHON}\")\n", + "print(f\"raw_case={RAW_CASE}\")\n", + "print(f\"raw_case_exists={RAW_CASE.exists()}\")\n", + "assert RAW_CASE.exists(), RAW_CASE\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-02", + "metadata": {}, + "source": [ + "## Notebook map\n", + "\n", + "The cells are ordered like the solver:\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[Raw AirfRANS case] --> B[v14 migrated working case]\n", + " B --> C[MeshView and boundary patches]\n", + " C --> D[FieldRegistryView: U, p, phi, nut, k, omega]\n", + " D --> E[preSolve and SIMPLE loop]\n", + " E --> F[Momentum terms and UEqn]\n", + " F --> G[Solve U]\n", + " G --> H[Pressure inputs and pEqn]\n", + " H --> I[Solve p and update phi]\n", + " I --> J[Correct U, p, phi]\n", + " J --> K[Correct turbulence: k, omega, nut]\n", + " K --> L[Parity sanity check]\n", + "```\n", + "\n", + "The key idea: OpenFOAM advances the simulation by repeatedly transforming named fields and matrices. The stepper lets Python stop after each transformation and inspect the exact native OpenFOAM objects.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "full-detail-03", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:17.552136Z", + "iopub.status.busy": "2026-07-22T20:00:17.551999Z", + "iopub.status.idle": "2026-07-22T20:00:18.009382Z", + "shell.execute_reply": "2026-07-22T20:00:18.008990Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "foam_stepper version=0.2.0\n" + ] + } + ], + "source": [ + "try:\n", + " import json\n", + " import math\n", + " from dataclasses import asdict\n", + "\n", + " import numpy as np\n", + " from IPython.display import Markdown, display\n", + "\n", + " from prepare_airfrans_stepper_case import (\n", + " assignment,\n", + " prepare_case,\n", + " read_text,\n", + " vector_assignment,\n", + " )\n", + " from openfoam_env import apply_openfoam_env\n", + "\n", + " apply_openfoam_env()\n", + " import foam_stepper as foam\n", + "except Exception as exc:\n", + " raise RuntimeError(\n", + " \"Notebook setup failed. Use the repo .venv kernel if possible: \"\n", + " f\"{PYTHON}. The first cell resolved paths correctly, but the current \"\n", + " \"kernel could not import the numerical/OpenFOAM bindings.\"\n", + " ) from exc\n", + "\n", + "\n", + "def fmt(value, digits: int = 4) -> str:\n", + " if value is None:\n", + " return \"\"\n", + " if isinstance(value, (bool, np.bool_)):\n", + " return str(bool(value))\n", + " if isinstance(value, (int, np.integer)):\n", + " return str(int(value))\n", + " if isinstance(value, (float, np.floating)):\n", + " if not np.isfinite(value):\n", + " return str(value)\n", + " return f\"{float(value):.{digits}g}\"\n", + " return str(value)\n", + "\n", + "\n", + "def md_table(headers, rows) -> Markdown:\n", + " def clean(value):\n", + " return fmt(value).replace(\"|\", \"\\\\|\").replace(\"\\n\", \"
\")\n", + " clean_headers = [clean(header) for header in headers]\n", + " lines = [\"| \" + \" | \".join(clean_headers) + \" |\", \"| \" + \" | \".join([\"---\"] * len(clean_headers)) + \" |\"]\n", + " for row in rows:\n", + " lines.append(\"| \" + \" | \".join(clean(value) for value in row) + \" |\")\n", + " return Markdown(\"\\n\".join(lines))\n", + "\n", + "\n", + "def show_table(title, headers, rows):\n", + " display(Markdown(f\"**{title}**\"))\n", + " display(md_table(headers, rows))\n", + "\n", + "\n", + "def arr_stats(array) -> dict[str, object]:\n", + " arr = np.asarray(array)\n", + " if arr.size == 0:\n", + " return {\"shape\": tuple(arr.shape), \"min\": None, \"max\": None, \"mean\": None, \"l2\": 0.0}\n", + " flat = arr.astype(float, copy=False).reshape(-1)\n", + " return {\n", + " \"shape\": tuple(arr.shape),\n", + " \"min\": float(np.nanmin(flat)),\n", + " \"max\": float(np.nanmax(flat)),\n", + " \"mean\": float(np.nanmean(flat)),\n", + " \"l2\": float(np.linalg.norm(flat)),\n", + " }\n", + "\n", + "\n", + "def stats_row(name, array):\n", + " stats = arr_stats(array)\n", + " return [name, stats[\"shape\"], stats[\"min\"], stats[\"max\"], stats[\"mean\"], stats[\"l2\"]]\n", + "\n", + "\n", + "def source_text(result) -> str:\n", + " source = result.source\n", + " if not source.file:\n", + " return \"native wrapper / OpenFOAM runtime\"\n", + " lines = \"\" if source.lines is None else f\":{source.lines[0]}-{source.lines[1]}\"\n", + " return f\"{source.file}{lines}::{source.function}\"\n", + "\n", + "\n", + "def transform_row(result):\n", + " return [\n", + " result.name,\n", + " result.phase,\n", + " \", \".join(result.changed_fields) or \"—\",\n", + " \", \".join(result.inputs.keys()) or \"—\",\n", + " \", \".join(result.outputs.keys()) or \"—\",\n", + " source_text(result),\n", + " ]\n", + "\n", + "\n", + "def field_summary_row(field):\n", + " stats = arr_stats(field.internal)\n", + " return [field.name, field.kind, field.dimensions, field.entity_kind, field.entity_count, stats[\"shape\"], stats[\"min\"], stats[\"max\"], stats[\"mean\"]]\n", + "\n", + "\n", + "def boundary_field_rows(field):\n", + " rows = []\n", + " for patch_name, patch_field in field.boundary.items():\n", + " stats = arr_stats(patch_field.values)\n", + " rows.append([\n", + " field.name,\n", + " patch_name,\n", + " patch_field.type,\n", + " stats[\"shape\"],\n", + " stats[\"min\"],\n", + " stats[\"max\"],\n", + " patch_field.fixes_value,\n", + " patch_field.assignable,\n", + " patch_field.coupled,\n", + " ])\n", + " return rows\n", + "\n", + "\n", + "def matrix_summary_row(label, matrix):\n", + " diag = arr_stats(matrix.diag)\n", + " source = arr_stats(matrix.source)\n", + " upper_shape = None if matrix.upper is None else tuple(np.asarray(matrix.upper).shape)\n", + " lower_shape = None if matrix.lower is None else tuple(np.asarray(matrix.lower).shape)\n", + " return [\n", + " label,\n", + " matrix.field_name,\n", + " matrix.value_rank,\n", + " matrix.dimensions,\n", + " diag[\"shape\"],\n", + " diag[\"min\"],\n", + " diag[\"max\"],\n", + " source[\"shape\"],\n", + " source[\"l2\"],\n", + " upper_shape,\n", + " lower_shape,\n", + " matrix.diagonal,\n", + " matrix.symmetric,\n", + " matrix.asymmetric,\n", + " ]\n", + "\n", + "\n", + "def field_snapshot(registry, names=(\"U\", \"p\", \"phi\", \"nut\", \"k\", \"omega\")):\n", + " return {name: np.array(registry[name].internal, copy=True) for name in names if name in registry}\n", + "\n", + "\n", + "def delta_rows(before, after, names=None):\n", + " rows = []\n", + " names = names or sorted(set(before) & set(after))\n", + " for name in names:\n", + " if name not in before or name not in after:\n", + " continue\n", + " diff = np.asarray(after[name]) - np.asarray(before[name])\n", + " stats = arr_stats(diff)\n", + " rows.append([name, stats[\"shape\"], stats[\"min\"], stats[\"max\"], stats[\"mean\"], stats[\"l2\"], float(np.max(np.abs(diff))) if diff.size else 0.0])\n", + " return rows\n", + "\n", + "\n", + "def matrix_delta_row(label, before, after):\n", + " diag = np.asarray(after.diag) - np.asarray(before.diag)\n", + " source = np.asarray(after.source) - np.asarray(before.source)\n", + " return [\n", + " label,\n", + " float(np.max(np.abs(diag))) if diag.size else 0.0,\n", + " float(np.linalg.norm(diag.reshape(-1))) if diag.size else 0.0,\n", + " float(np.max(np.abs(source))) if source.size else 0.0,\n", + " float(np.linalg.norm(source.reshape(-1))) if source.size else 0.0,\n", + " ]\n", + "\n", + "\n", + "def solve_result_row(result):\n", + " return [\n", + " result.field_name,\n", + " result.solver_name,\n", + " result.initial_residual,\n", + " result.final_residual,\n", + " result.n_iterations,\n", + " result.converged,\n", + " result.singular,\n", + " ]\n", + "\n", + "print(f\"foam_stepper version={foam.version()}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-04", + "metadata": {}, + "source": [ + "## Full-detail output policy\n", + "\n", + "Large OpenFOAM arrays can be hundreds of thousands of rows long. This notebook does not pretend those arrays are small:\n", + "\n", + "- every raw-array panel prints exact `shape`, `dtype`, `ndim`, and `size` before any value text;\n", + "- value text uses NumPy edge truncation only when a cell would flood the notebook;\n", + "- ragged mesh lists show exact row counts and selected rows with their full row lengths;\n", + "- charts and heatmaps are computed from the full input arrays, not from the displayed edge values;\n", + "- spatial heatmaps use cell or face centres as coordinates, so large tensors can be seen as geometry instead of scrollback.\n", + "\n", + "Tune `FULL_DETAIL_MAX_VALUES`, `FULL_DETAIL_EDGE_ITEMS`, and `FULL_DETAIL_GRID` below if you want denser text or finer binned plots.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "full-detail-05", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import HTML, SVG\n", + "import html\n", + "\n", + "FULL_DETAIL_MAX_VALUES = 256\n", + "FULL_DETAIL_EDGE_ITEMS = 4\n", + "FULL_DETAIL_ROW_ITEMS = 48\n", + "FULL_DETAIL_GRID = 96\n", + "\n", + "np.set_printoptions(\n", + " threshold=FULL_DETAIL_MAX_VALUES,\n", + " edgeitems=FULL_DETAIL_EDGE_ITEMS,\n", + " linewidth=180,\n", + " suppress=False,\n", + ")\n", + "\n", + "\n", + "def _html(value) -> str:\n", + " return html.escape(str(value), quote=False)\n", + "\n", + "\n", + "def shape_text(shape) -> str:\n", + " shape = tuple(int(dim) for dim in shape)\n", + " if not shape:\n", + " return \"scalar\"\n", + " return \" × \".join(f\"{dim:,}\" for dim in shape)\n", + "\n", + "\n", + "def exact_array_meta(array) -> dict[str, object]:\n", + " arr = np.asarray(array)\n", + " return {\n", + " \"shape\": tuple(int(dim) for dim in arr.shape),\n", + " \"shape text\": shape_text(arr.shape),\n", + " \"ndim\": int(arr.ndim),\n", + " \"dtype\": str(arr.dtype),\n", + " \"size\": int(arr.size),\n", + " }\n", + "\n", + "\n", + "def array_panel(name, array, *, max_values=FULL_DETAIL_MAX_VALUES, edgeitems=FULL_DETAIL_EDGE_ITEMS, open=True):\n", + " \"\"\"Display exact array metadata plus truncated value text when needed.\"\"\"\n", + " arr = np.asarray(array)\n", + " meta = exact_array_meta(arr)\n", + " status = \"complete value text\"\n", + " if arr.size > max_values:\n", + " status = (\n", + " f\"TRUNCATED value text; exact shape retained; \"\n", + " f\"size={arr.size:,}; threshold={max_values:,}; edgeitems={edgeitems}\"\n", + " )\n", + " try:\n", + " text = np.array2string(arr, threshold=max_values, edgeitems=edgeitems, max_line_width=180)\n", + " except Exception as exc:\n", + " text = f\"\\nrepr={arr!r}\"\n", + " attr = \" open\" if open else \"\"\n", + " summary = (\n", + " f\"{_html(name)} — shape={_html(meta['shape'])} \"\n", + " f\"({meta['shape text']}), dtype={_html(meta['dtype'])}, size={meta['size']:,}, {status}\"\n", + " )\n", + " display(HTML(\n", + " f\"\"\n", + " f\"{summary}\"\n", + " f\"
\"\n",
+    "        f\"{_html(text)}
\"\n", + " ))\n", + "\n", + "\n", + "def safe_array_panel(name, producer, *, open=False):\n", + " \"\"\"Show an array if an optional view exists; otherwise show the missing reason.\"\"\"\n", + " try:\n", + " value = producer()\n", + " except Exception as exc:\n", + " display(Markdown(f\"`{name}` unavailable: `{type(exc).__name__}: {exc}`\"))\n", + " return None\n", + " if value is None:\n", + " display(Markdown(f\"`{name}` is `None`.\"))\n", + " return None\n", + " array_panel(name, value, open=open)\n", + " return value\n", + "\n", + "\n", + "def selected_indices(n: int, edge: int = 4) -> list[int]:\n", + " n = int(n)\n", + " if n <= 0:\n", + " return []\n", + " if n <= 2 * edge + 3:\n", + " return list(range(n))\n", + " middle = n // 2\n", + " picks = set(range(edge))\n", + " picks.update([middle - 1, middle, middle + 1])\n", + " picks.update(range(max(0, n - edge), n))\n", + " return sorted(i for i in picks if 0 <= i < n)\n", + "\n", + "\n", + "def truncate_sequence(values, *, max_items=FULL_DETAIL_ROW_ITEMS):\n", + " values = list(values)\n", + " if len(values) <= max_items:\n", + " return values, False\n", + " edge = max(1, max_items // 3)\n", + " return values[:edge] + [\"...\"] + values[-edge:], True\n", + "\n", + "\n", + "def ragged_panel(name, ragged, n_rows: int, *, edge=4, max_items=FULL_DETAIL_ROW_ITEMS, open=True):\n", + " \"\"\"Display exact row count and selected ragged rows without pretending the list is rectangular.\"\"\"\n", + " n_rows = int(n_rows)\n", + " rows = []\n", + " for i in selected_indices(n_rows, edge=edge):\n", + " row = [int(x) for x in ragged.row(i)]\n", + " shown, truncated = truncate_sequence(row, max_items=max_items)\n", + " rows.append((i, len(row), shown, truncated))\n", + " lines = [\n", + " f\"{name}: ragged OpenFOAM label list\",\n", + " f\"exact row count={n_rows:,}\",\n", + " f\"selected rows={len(rows):,}; selected row values keep exact row length; long rows use ellipsis only in the middle\",\n", + " \"\",\n", + " ]\n", + " for i, length, shown, truncated in rows:\n", + " suffix = \" (row value text truncated)\" if truncated else \"\"\n", + " lines.append(f\"row {i:,}: length={length:,}{suffix}\")\n", + " lines.append(\" \" + repr(shown))\n", + " attr = \" open\" if open else \"\"\n", + " display(HTML(\n", + " f\"\"\n", + " f\"{_html(name)} — ragged rows={n_rows:,}\"\n", + " f\"
\"\n",
+    "        f\"{_html(chr(10).join(lines))}
\"\n", + " ))\n", + "\n", + "\n", + "_PALETTE = np.array(\n", + " [\n", + " [68, 1, 84],\n", + " [59, 82, 139],\n", + " [33, 145, 140],\n", + " [94, 201, 98],\n", + " [253, 231, 37],\n", + " ],\n", + " dtype=float,\n", + ")\n", + "\n", + "\n", + "def color_map(t) -> str:\n", + " t = float(np.clip(t, 0.0, 1.0))\n", + " pos = t * (len(_PALETTE) - 1)\n", + " i = int(np.floor(pos))\n", + " if i >= len(_PALETTE) - 1:\n", + " rgb = _PALETTE[-1]\n", + " else:\n", + " frac = pos - i\n", + " rgb = (1.0 - frac) * _PALETTE[i] + frac * _PALETTE[i + 1]\n", + " r, g, b = [int(round(x)) for x in rgb]\n", + " return f\"rgb({r},{g},{b})\"\n", + "\n", + "\n", + "def _finite_flat(data):\n", + " arr = np.asarray(data, dtype=float).reshape(-1)\n", + " return arr[np.isfinite(arr)]\n", + "\n", + "\n", + "def histogram_svg(title, data, *, bins=80, width=760, height=230, log_y=False):\n", + " values = _finite_flat(data)\n", + " if values.size == 0:\n", + " display(Markdown(f\"**{title}**: no finite values.\"))\n", + " return\n", + " lo = float(values.min())\n", + " hi = float(values.max())\n", + " if lo == hi:\n", + " pad = 0.5 if lo == 0 else abs(lo) * 0.01\n", + " lo -= pad\n", + " hi += pad\n", + " counts, edges = np.histogram(values, bins=bins, range=(lo, hi))\n", + " y_values = np.log1p(counts) if log_y else counts.astype(float)\n", + " y_max = float(y_values.max()) if y_values.size else 1.0\n", + " y_max = y_max if y_max > 0 else 1.0\n", + " left, right, top, bottom = 70, 18, 38, 48\n", + " plot_w = width - left - right\n", + " plot_h = height - top - bottom\n", + " bar_w = plot_w / len(counts)\n", + " rects = []\n", + " for i, y in enumerate(y_values):\n", + " h = 0.0 if y_max == 0 else (float(y) / y_max) * plot_h\n", + " x = left + i * bar_w\n", + " y0 = top + plot_h - h\n", + " rects.append(f\"\")\n", + " mean = float(values.mean())\n", + " std = float(values.std())\n", + " label = \"log1p(count)\" if log_y else \"count\"\n", + " svg = f\"\"\"\n", + " \n", + " \n", + " {_html(title)}\n", + " n={values.size:,} finite, min={lo:.6g}, max={hi:.6g}, mean={mean:.6g}, std={std:.6g}\n", + " x=value, y={label}, bins={bins}\n", + " \n", + " \n", + " {''.join(rects)}\n", + " \n", + " \"\"\"\n", + " display(SVG(svg))\n", + "\n", + "\n", + "def bar_chart_svg(title, labels, values, *, width=760, height=None):\n", + " labels = [str(x) for x in labels]\n", + " values = np.asarray(values, dtype=float)\n", + " if height is None:\n", + " height = max(220, 48 + 30 * len(labels))\n", + " if values.size == 0:\n", + " display(Markdown(f\"**{title}**: no values.\"))\n", + " return\n", + " vmax = float(values.max()) if float(values.max()) > 0 else 1.0\n", + " left, right, top, row_h = 170, 30, 34, 26\n", + " plot_w = width - left - right\n", + " rows = []\n", + " for i, (label, value) in enumerate(zip(labels, values)):\n", + " y = top + i * row_h\n", + " w = plot_w * float(value) / vmax\n", + " rows.append(\n", + " f\"{_html(label)}\"\n", + " f\"\"\n", + " f\"{value:,.0f}\"\n", + " )\n", + " svg = f\"\"\"\n", + " \n", + " \n", + " {_html(title)}\n", + " {''.join(rows)}\n", + " \n", + " \"\"\"\n", + " display(SVG(svg))\n", + "\n", + "\n", + "def density_svg(title, x, y, values=None, *, grid=FULL_DETAIL_GRID, width=700, height=560, statistic=\"mean\", log_counts=True):\n", + " x = np.asarray(x, dtype=float).reshape(-1)\n", + " y = np.asarray(y, dtype=float).reshape(-1)\n", + " if x.size != y.size:\n", + " raise ValueError(f\"x/y length mismatch: {x.size} != {y.size}\")\n", + " mask = np.isfinite(x) & np.isfinite(y)\n", + " v = None\n", + " if values is not None:\n", + " v = np.asarray(values, dtype=float).reshape(-1)\n", + " if v.size != x.size:\n", + " raise ValueError(f\"values length mismatch: {v.size} != {x.size}\")\n", + " mask &= np.isfinite(v)\n", + " v = v[mask]\n", + " x = x[mask]\n", + " y = y[mask]\n", + " if x.size == 0:\n", + " display(Markdown(f\"**{title}**: no finite coordinates.\"))\n", + " return\n", + " x_range = (float(x.min()), float(x.max()))\n", + " y_range = (float(y.min()), float(y.max()))\n", + " if x_range[0] == x_range[1]:\n", + " x_range = (x_range[0] - 0.5, x_range[1] + 0.5)\n", + " if y_range[0] == y_range[1]:\n", + " y_range = (y_range[0] - 0.5, y_range[1] + 0.5)\n", + " counts, x_edges, y_edges = np.histogram2d(x, y, bins=grid, range=[x_range, y_range])\n", + " if v is None:\n", + " Z = np.log1p(counts) if log_counts else counts\n", + " occupied = counts > 0\n", + " legend = \"log1p(count)\" if log_counts else \"count\"\n", + " raw_min = float(counts[occupied].min()) if np.any(occupied) else 0.0\n", + " raw_max = float(counts.max()) if counts.size else 0.0\n", + " else:\n", + " sums, _, _ = np.histogram2d(x, y, bins=grid, range=[x_range, y_range], weights=v)\n", + " with np.errstate(invalid=\"ignore\", divide=\"ignore\"):\n", + " Z = sums / counts\n", + " occupied = counts > 0\n", + " legend = statistic\n", + " raw_min = float(np.nanmin(Z[occupied])) if np.any(occupied) else 0.0\n", + " raw_max = float(np.nanmax(Z[occupied])) if np.any(occupied) else 0.0\n", + " if not np.any(occupied):\n", + " display(Markdown(f\"**{title}**: no occupied bins.\"))\n", + " return\n", + " z_values = Z[occupied]\n", + " z_min = float(np.nanmin(z_values))\n", + " z_max = float(np.nanmax(z_values))\n", + " if z_min == z_max:\n", + " denom = 1.0\n", + " else:\n", + " denom = z_max - z_min\n", + " left, right, top, bottom = 72, 18, 38, 58\n", + " plot_w = width - left - right\n", + " plot_h = height - top - bottom\n", + " cell_w = plot_w / grid\n", + " cell_h = plot_h / grid\n", + " rects = []\n", + " for ix, iy in np.argwhere(occupied):\n", + " z = float(Z[ix, iy])\n", + " t = 0.5 if z_min == z_max else (z - z_min) / denom\n", + " px = left + ix * cell_w\n", + " py = top + (grid - 1 - iy) * cell_h\n", + " rects.append(\n", + " f\"\")\n", + " svg = f\"\"\"\n", + " \n", + " \n", + " {_html(title)}\n", + " \n", + " {''.join(rects)}\n", + " points={x.size:,}; bins={grid}×{grid}; color={_html(legend)}; occupied bins={int(occupied.sum()):,}\n", + " x=[{x_range[0]:.6g}, {x_range[1]:.6g}], y=[{y_range[0]:.6g}, {y_range[1]:.6g}]\n", + " raw color range=[{raw_min:.6g}, {raw_max:.6g}]\n", + " \n", + " \"\"\"\n", + " display(SVG(svg))\n", + "\n", + "\n", + "def entity_scalar_values(array, n_entities: int):\n", + " arr = np.asarray(array)\n", + " if arr.shape[0] != int(n_entities):\n", + " raise ValueError(f\"first dimension {arr.shape[0]} does not match entity count {n_entities}\")\n", + " if arr.ndim == 1:\n", + " return arr.astype(float, copy=False)\n", + " flat = arr.reshape((int(n_entities), -1)).astype(float, copy=False)\n", + " if flat.shape[1] == 1:\n", + " return flat[:, 0]\n", + " return np.linalg.norm(flat, axis=1)\n", + "\n", + "\n", + "def show_field_histograms(field, *, bins=80):\n", + " arr = np.asarray(field.internal)\n", + " array_panel(f\"{field.name}.internal\", arr, open=False)\n", + " if arr.ndim == 1:\n", + " histogram_svg(f\"{field.name} internal values\", arr, bins=bins)\n", + " return\n", + " flat = arr.reshape((arr.shape[0], -1))\n", + " for j in range(min(flat.shape[1], 6)):\n", + " histogram_svg(f\"{field.name} component {j} internal values\", flat[:, j], bins=bins)\n", + " if flat.shape[1] > 1:\n", + " histogram_svg(f\"{field.name} internal vector/tensor magnitude\", np.linalg.norm(flat, axis=1), bins=bins)\n", + "\n", + "\n", + "def show_cell_field_map(mesh, field, *, title=None, grid=FULL_DETAIL_GRID):\n", + " coords = np.asarray(mesh.C)\n", + " values = entity_scalar_values(field.internal, mesh.n_cells)\n", + " density_svg(title or f\"{field.name} on cell centres\", coords[:, 0], coords[:, 1], values, grid=grid, statistic=\"bin mean\")\n", + "\n", + "\n", + "def show_face_field_map(mesh, field, *, title=None, grid=FULL_DETAIL_GRID):\n", + " coords = np.asarray(mesh.Cf[:mesh.n_internal_faces])\n", + " values = entity_scalar_values(field.internal, mesh.n_internal_faces)\n", + " density_svg(title or f\"{field.name} on internal face centres\", coords[:, 0], coords[:, 1], values, grid=grid, statistic=\"bin mean\")\n", + "\n", + "\n", + "def show_delta_map(mesh, name, before, after, *, grid=FULL_DETAIL_GRID):\n", + " diff = np.asarray(after[name]) - np.asarray(before[name])\n", + " values = entity_scalar_values(diff, diff.shape[0])\n", + " coords = np.asarray(mesh.C if diff.shape[0] == mesh.n_cells else mesh.Cf[:diff.shape[0]])\n", + " density_svg(f\"|Δ{name}| spatial distribution\", coords[:, 0], coords[:, 1], np.abs(values), grid=grid, statistic=\"bin mean |Δ|\")\n", + "\n", + "\n", + "def show_matrix_arrays(label, matrix, *, open_arrays=False):\n", + " array_panel(f\"{label}.diag\", matrix.diag, open=open_arrays)\n", + " array_panel(f\"{label}.source\", matrix.source, open=open_arrays)\n", + " if matrix.upper is not None:\n", + " array_panel(f\"{label}.upper\", matrix.upper, open=open_arrays)\n", + " if matrix.lower is not None:\n", + " array_panel(f\"{label}.lower\", matrix.lower, open=open_arrays)\n", + " histogram_svg(f\"{label}.diag coefficient distribution\", matrix.diag, bins=90, log_y=True)\n", + " histogram_svg(f\"{label}.source distribution\", matrix.source, bins=90, log_y=True)\n", + " if matrix.upper is not None:\n", + " histogram_svg(f\"{label}.upper coefficient distribution\", matrix.upper, bins=90, log_y=True)\n", + " if matrix.lower is not None:\n", + " histogram_svg(f\"{label}.lower coefficient distribution\", matrix.lower, bins=90, log_y=True)\n", + "\n", + "\n", + "def show_ldu_density(mesh, title=\"LDU owner-neighbour sparse graph density\", *, grid=FULL_DETAIL_GRID):\n", + " density_svg(title, np.asarray(mesh.owner), np.asarray(mesh.neighbour), grid=grid, statistic=\"count\", log_counts=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-06", + "metadata": {}, + "source": [ + "## The Python objects this notebook will use\n", + "\n", + "The stepper facade turns native OpenFOAM objects into small Python views. These are not reimplementations of OpenFOAM; they are snapshots of OpenFOAM-owned state.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "full-detail-07", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:18.010909Z", + "iopub.status.busy": "2026-07-22T20:00:18.010785Z", + "iopub.status.idle": "2026-07-22T20:00:18.015422Z", + "shell.execute_reply": "2026-07-22T20:00:18.014909Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Stepper data model**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Object | What it represents | Why it matters for the simulation |\n", + "| --- | --- | --- |\n", + "| MeshView | points, faces, owner/neighbour addressing, cell volumes, patch geometry | the finite-volume control-volume graph |\n", + "| PatchView | one boundary patch: wall, farfield, empty plane | where boundary conditions attach to mesh faces |\n", + "| FieldRegistryView | mapping of registered fields | what OpenFOAM currently knows: U, p, phi, turbulence fields |\n", + "| FieldView | internal field array plus boundary patch fields | cell or face values being solved/updated |\n", + "| PatchFieldView | boundary-condition type, values, coefficients | how wall/freestream/empty constraints enter equations |\n", + "| MatrixView | fvMatrix/lduMatrix diag, upper, lower, source | the sparse linear system OpenFOAM solves |\n", + "| SolveResult | solver residuals, iteration count, convergence flag | how hard the linear solve was |\n", + "| TransformResult | one named OpenFOAM operation with inputs, outputs, source location | the breadcrumb for every state transition |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "show_table(\n", + " \"Stepper data model\",\n", + " [\"Object\", \"What it represents\", \"Why it matters for the simulation\"],\n", + " [\n", + " [\"MeshView\", \"points, faces, owner/neighbour addressing, cell volumes, patch geometry\", \"the finite-volume control-volume graph\"],\n", + " [\"PatchView\", \"one boundary patch: wall, farfield, empty plane\", \"where boundary conditions attach to mesh faces\"],\n", + " [\"FieldRegistryView\", \"mapping of registered fields\", \"what OpenFOAM currently knows: U, p, phi, turbulence fields\"],\n", + " [\"FieldView\", \"internal field array plus boundary patch fields\", \"cell or face values being solved/updated\"],\n", + " [\"PatchFieldView\", \"boundary-condition type, values, coefficients\", \"how wall/freestream/empty constraints enter equations\"],\n", + " [\"MatrixView\", \"fvMatrix/lduMatrix diag, upper, lower, source\", \"the sparse linear system OpenFOAM solves\"],\n", + " [\"SolveResult\", \"solver residuals, iteration count, convergence flag\", \"how hard the linear solve was\"],\n", + " [\"TransformResult\", \"one named OpenFOAM operation with inputs, outputs, source location\", \"the breadcrumb for every state transition\"],\n", + " ],\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-08", + "metadata": {}, + "source": [ + "## 1. Prepare the real AirfRANS case\n", + "\n", + "The sibling AirfRANS raw case is OpenFOAM.com v2112 syntax. This repo runs OpenFOAM Foundation v14. The helper copies the raw case into `tmp/` and migrates only the dictionaries needed for a one-iteration microscope run:\n", + "\n", + "- `controlDict`: uses `solver incompressibleFluid`, one time step, no function objects;\n", + "- `physicalProperties`: carries `rho` and molecular `nu`;\n", + "- `momentumTransport`: selects `RAS { model kOmegaSST; turbulence on; }`;\n", + "- mesh and initial fields are copied unchanged.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "full-detail-09", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:18.016773Z", + "iopub.status.busy": "2026-07-22T20:00:18.016585Z", + "iopub.status.idle": "2026-07-22T20:00:18.031003Z", + "shell.execute_reply": "2026-07-22T20:00:18.030338Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Selected case metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Quantity | Value | Meaning |\n", + "| --- | --- | --- |\n", + "| simulation | airFoil2D_SST_93.213_3.79_0.418_0.0_9.665 | AirfRANS raw OpenFOAM case directory |\n", + "| Uinf | 93.21 | freestream speed magnitude [m/s] |\n", + "| velocity | (93.00914503999995, 6.161356013756317, 0.0) | initial/freestream velocity vector [m/s] |\n", + "| nu | 1.56e-05 | kinematic viscosity [m^2/s] |\n", + "| rhoInf | 1.204 | reference density [kg/m^3] |\n", + "| Re | 5.975e+06 | Re = Uinf * chord / nu; AirfRANS chord is 1 m |\n", + "| Mach | 0.2693 | Uinf / 346.1, archived diagnostic |\n", + "| angle | 3.79 | angle from dragDir |\n", + "| raw endTime | 20000 | original AirfRANS run length |\n", + "| notebook endTime | 1 | one iteration for inspection |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "working_case=/home/aaron/data/openFOAM-RANS-to-GPU/tmp/airfrans_stepper_full_detail_learning_tool/split_case\n" + ] + } + ], + "source": [ + "if WORK.exists():\n", + " shutil.rmtree(WORK)\n", + "WORK.mkdir(parents=True)\n", + "\n", + "meta = prepare_case(RAW_CASE, CASE, end_time=1)\n", + "meta_dict = asdict(meta)\n", + "\n", + "show_table(\n", + " \"Selected case metadata\",\n", + " [\"Quantity\", \"Value\", \"Meaning\"],\n", + " [\n", + " [\"simulation\", meta.simulation, \"AirfRANS raw OpenFOAM case directory\"],\n", + " [\"Uinf\", meta.u_inf, \"freestream speed magnitude [m/s]\"],\n", + " [\"velocity\", meta.velocity, \"initial/freestream velocity vector [m/s]\"],\n", + " [\"nu\", meta.nu, \"kinematic viscosity [m^2/s]\"],\n", + " [\"rhoInf\", meta.rho_inf, \"reference density [kg/m^3]\"],\n", + " [\"Re\", meta.reynolds, \"Re = Uinf * chord / nu; AirfRANS chord is 1 m\"],\n", + " [\"Mach\", meta.mach, \"Uinf / 346.1, archived diagnostic\"],\n", + " [\"angle\", meta.alpha_deg, \"angle from dragDir\"],\n", + " [\"raw endTime\", meta.source_end_time, \"original AirfRANS run length\"],\n", + " [\"notebook endTime\", meta.migrated_end_time, \"one iteration for inspection\"],\n", + " ],\n", + ")\n", + "print(f\"working_case={CASE}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-10", + "metadata": {}, + "source": [ + "## 2. What the raw OpenFOAM dictionaries say\n", + "\n", + "These dictionaries are the simulation contract. They encode the solver, numerical schemes, SIMPLE controls, turbulence model, force directions, and output objects used by AirfRANS.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "full-detail-11", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:18.032543Z", + "iopub.status.busy": "2026-07-22T20:00:18.032363Z", + "iopub.status.idle": "2026-07-22T20:00:18.039450Z", + "shell.execute_reply": "2026-07-22T20:00:18.038878Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Raw dictionary facts**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Dictionary | Entry | Value | Interpretation |\n", + "| --- | --- | --- | --- |\n", + "| controlDict | application | simpleFoam | original OpenFOAM.com solver |\n", + "| controlDict | Uinf | 93.213 | freestream magnitude |\n", + "| controlDict | endTime | 20000 | original run has 20k steady iterations |\n", + "| controlDict | liftDir | (-0.06609975018244577, 0.9978130200723071, 0.0) | force projection direction |\n", + "| controlDict | dragDir | (0.9978130200723071, 0.06609975018244577, 0.0) | force projection direction |\n", + "| transportProperties | nu | 1.56e-5 | molecular kinematic viscosity |\n", + "| turbulenceProperties | simulationType | RAS | RAS turbulence closure active |\n", + "| fvSolution | SIMPLE.consistent | yes | SIMPLEC-style pressure/velocity coupling |\n", + "| fvSolution | SIMPLE.nNonOrthogonalCorrectors | 3 | four pressure solves in this v14 path: initial + 3 correctors |\n", + "| fvSchemes | ddtSchemes.default | steadyState | pseudo-time iteration, not physical transient integration |\n", + "| fvSchemes | div(phi,U) | bounded Gauss linearUpwind grad(U) | convective momentum flux discretization |\n", + "| fvSchemes | wallDist.method | meshWave | wall distance for k-omega SST near-wall handling |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "raw_control = read_text(RAW_CASE / \"system/controlDict\")\n", + "raw_solution = read_text(RAW_CASE / \"system/fvSolution\")\n", + "raw_schemes = read_text(RAW_CASE / \"system/fvSchemes\")\n", + "raw_turbulence = read_text(RAW_CASE / \"constant/turbulenceProperties\")\n", + "raw_transport = read_text(RAW_CASE / \"constant/transportProperties\")\n", + "\n", + "show_table(\n", + " \"Raw dictionary facts\",\n", + " [\"Dictionary\", \"Entry\", \"Value\", \"Interpretation\"],\n", + " [\n", + " [\"controlDict\", \"application\", assignment(raw_control, \"application\"), \"original OpenFOAM.com solver\"],\n", + " [\"controlDict\", \"Uinf\", assignment(raw_control, \"Uinf\"), \"freestream magnitude\"],\n", + " [\"controlDict\", \"endTime\", assignment(raw_control, \"endTime\"), \"original run has 20k steady iterations\"],\n", + " [\"controlDict\", \"liftDir\", vector_assignment(raw_control, \"liftDir\"), \"force projection direction\"],\n", + " [\"controlDict\", \"dragDir\", vector_assignment(raw_control, \"dragDir\"), \"force projection direction\"],\n", + " [\"transportProperties\", \"nu\", assignment(raw_transport, \"nu\"), \"molecular kinematic viscosity\"],\n", + " [\"turbulenceProperties\", \"simulationType\", assignment(raw_turbulence, \"simulationType\"), \"RAS turbulence closure active\"],\n", + " [\"fvSolution\", \"SIMPLE.consistent\", \"yes\", \"SIMPLEC-style pressure/velocity coupling\"],\n", + " [\"fvSolution\", \"SIMPLE.nNonOrthogonalCorrectors\", \"3\", \"four pressure solves in this v14 path: initial + 3 correctors\"],\n", + " [\"fvSchemes\", \"ddtSchemes.default\", \"steadyState\", \"pseudo-time iteration, not physical transient integration\"],\n", + " [\"fvSchemes\", \"div(phi,U)\", \"bounded Gauss linearUpwind grad(U)\", \"convective momentum flux discretization\"],\n", + " [\"fvSchemes\", \"wallDist.method\", \"meshWave\", \"wall distance for k-omega SST near-wall handling\"],\n", + " ],\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-12", + "metadata": {}, + "source": [ + "## 3. Construct the OpenFOAM stepper and inspect the finite-volume mesh\n", + "\n", + "A finite-volume mesh is a graph:\n", + "\n", + "- cells are control volumes;\n", + "- faces connect cells or lie on boundary patches;\n", + "- each internal face has an owner cell and a neighbour cell;\n", + "- each boundary face belongs to exactly one patch;\n", + "- fields live either on cells (`vol*`) or faces (`surface*`).\n", + "\n", + "The AirfRANS case is a 2D airfoil extrusion. The `frontAndBack` patch is `empty`, which tells OpenFOAM the third direction is not solved as a real 3D thickness.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "full-detail-13", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:18.041099Z", + "iopub.status.busy": "2026-07-22T20:00:18.040919Z", + "iopub.status.idle": "2026-07-22T20:00:19.340747Z", + "shell.execute_reply": "2026-07-22T20:00:19.340216Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/*---------------------------------------------------------------------------*\\\n", + " ========= |\n", + " \\\\ / F ield | OpenFOAM: The Open Source CFD Toolbox\n", + " \\\\ / O peration | Website: https://openfoam.org\n", + " \\\\ / A nd | Version: 14\n", + " \\\\/ M anipulation |\n", + "\\*---------------------------------------------------------------------------*/\n", + "Build : 14-0db507470b5e\n", + "Exec : foam_stepper -case /home/aaron/data/openFOAM-RANS-to-GPU/tmp/airfrans_stepper_full_detail_learning_tool/split_case -noFunctionObjects -solver incompressibleFluid\n", + "Date : Jul 23 2026\n", + "Time : 11:44:59\n", + "Host : \"devuan-hpz\"\n", + "PID : 28708\n", + "I/O : uncollated\n", + "Case : /home/aaron/data/openFOAM-RANS-to-GPU/tmp/airfrans_stepper_full_detail_learning_tool/split_case\n", + "nProcs : 1\n", + "fileModificationChecking : Monitoring run-time modified files using timeStampMaster (fileModificationSkew 10)\n", + "allowSystemOperations : Allowing user-supplied system call operations\n", + "\n", + "// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //\n", + "\n", + "Selecting viscosity model constant\n", + "\n", + "Selecting momentum transport model type RAS\n", + " Selecting RAS turbulence model kOmegaSST\n", + " bounding k, min: 0 max: 3.6353069999999998e-09 average: 3.6353069999910849e-09\n", + "\n", + "Selecting patchDistMethod meshWave\n", + "\n", + "SIMPLE: Convergence criteria found\n", + " p: tolerance 0\n", + " U: tolerance 0\n", + " nuTilda: tolerance 0\n", + " k: tolerance 0\n", + " omega: tolerance 0\n", + " h: tolerance 0\n", + "\n", + "\n", + "SIMPLE: Operating solver in steady-state mode with 1 outer corrector\n", + "SIMPLE: Operating solver in SIMPLE mode\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/markdown": [ + "**Initial solver state**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Key | Value |\n", + "| --- | --- |\n", + "| case_path | /home/aaron/data/openFOAM-RANS-to-GPU/tmp/airfrans_stepper_full_detail_learning_tool/split_case |\n", + "| solver_name | incompressibleFluid |\n", + "| time_name | 0 |\n", + "| time_index | 0 |\n", + "| time_value | 0 |\n", + "| delta_t | 1 |\n", + "| pimple_iteration_open | False |\n", + "| has_momentum_matrix | False |\n", + "| has_pressure_inputs | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Mesh counts**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Quantity | Value | Meaning |\n", + "| --- | --- | --- |\n", + "| points | 569562 | mesh vertices |\n", + "| faces | 1134981 | internal + boundary faces |\n", + "| internal faces | 565419 | faces with owner and neighbour cells |\n", + "| cells | 283400 | finite-volume control volumes |\n", + "| ldu owner size | (565419,) | owner addressing for lower/upper matrix coefficients |\n", + "| ldu neighbour size | (565419,) | neighbour addressing for lower/upper matrix coefficients |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Boundary patches from MeshView**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | type | index | start | size | coupled | constraint | min area | max area | mean area |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| aerofoil | wall | 0 | 565419 | 1026 | False | False | 5.949e-06 | 0.0076 | 0.001976 |\n", + "| freestream | patch | 1 | 566445 | 1736 | False | False | 0.0001 | 11.11 | 0.8227 |\n", + "| frontAndBack | empty | 2 | 568181 | 0 | False | True | | | |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "case = foam.Case(CASE)\n", + "stepper = case.make_stepper()\n", + "state0 = stepper.state()\n", + "mesh = stepper.mesh()\n", + "fields = stepper.fields()\n", + "\n", + "show_table(\"Initial solver state\", [\"Key\", \"Value\"], list(state0.items()))\n", + "show_table(\n", + " \"Mesh counts\",\n", + " [\"Quantity\", \"Value\", \"Meaning\"],\n", + " [\n", + " [\"points\", mesh.n_points, \"mesh vertices\"],\n", + " [\"faces\", mesh.n_faces, \"internal + boundary faces\"],\n", + " [\"internal faces\", mesh.n_internal_faces, \"faces with owner and neighbour cells\"],\n", + " [\"cells\", mesh.n_cells, \"finite-volume control volumes\"],\n", + " [\"ldu owner size\", np.asarray(mesh.owner).shape, \"owner addressing for lower/upper matrix coefficients\"],\n", + " [\"ldu neighbour size\", np.asarray(mesh.neighbour).shape, \"neighbour addressing for lower/upper matrix coefficients\"],\n", + " ],\n", + ")\n", + "\n", + "patch_rows = []\n", + "for patch in mesh.boundary:\n", + " mag_stats = arr_stats(patch.magSf)\n", + " patch_rows.append([\n", + " patch.name,\n", + " patch.type,\n", + " patch.index,\n", + " patch.start,\n", + " patch.size,\n", + " patch.coupled,\n", + " patch.constraint,\n", + " mag_stats[\"min\"],\n", + " mag_stats[\"max\"],\n", + " mag_stats[\"mean\"],\n", + " ])\n", + "show_table(\"Boundary patches from MeshView\", [\"name\", \"type\", \"index\", \"start\", \"size\", \"coupled\", \"constraint\", \"min area\", \"max area\", \"mean area\"], patch_rows)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-14", + "metadata": {}, + "source": [ + "### Full-shape mesh arrays and geometry visualizations\n", + "\n", + "The tables above give counts. The panels below expose the arrays behind those counts. Text panels may truncate values, but they preserve exact shapes and sizes. The charts use the complete coordinate, volume, face-area, and addressing arrays.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "full-detail-15", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
mesh.points — shape=(569562, 3) (569,562 × 3), dtype=float64, size=1,708,686, TRUNCATED value text; exact shape retained; size=1,708,686; threshold=256; edgeitems=4
[[ 200.           13.15385029    0.        ]\n",
+       " [ 186.1155879    12.23609412    0.        ]\n",
+       " [ 173.1998558    11.38236745    0.        ]\n",
+       " [ 161.1852212    10.5882031     0.        ]\n",
+       " ...\n",
+       " [-199.3376715   -16.12037607    1.        ]\n",
+       " [-199.6203798   -12.17269963    1.        ]\n",
+       " [-199.8270894    -8.16952639    1.        ]\n",
+       " [-199.9546882    -4.11165538    1.        ]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.C cell centres — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[ 193.10515307   12.69805363    0.5       ]\n",
+       " [ 179.70132112   11.81206564    0.5       ]\n",
+       " [ 167.23269792   10.98789461    0.5       ]\n",
+       " [ 155.63378164   10.22121091    0.5       ]\n",
+       " ...\n",
+       " [-192.71874543  -13.56339867    0.5       ]\n",
+       " [-192.92770495   -9.75145316    0.5       ]\n",
+       " [-193.06140841   -5.88666928    0.5       ]\n",
+       " [-193.11681557   -1.96987964    0.5       ]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.Cf face centres — shape=(565419, 3) (565,419 × 3), dtype=float64, size=1,696,257, TRUNCATED value text; exact shape retained; size=1,696,257; threshold=256; edgeitems=4
[[ 186.1155888    12.23604618    0.5       ]\n",
+       " [ 193.05779485   12.69487427    0.5       ]\n",
+       " [ 193.05779395   12.6949722     0.5       ]\n",
+       " [ 173.1998576    11.38232148    0.5       ]\n",
+       " ...\n",
+       " [-192.4947848   -15.44731136    0.5       ]\n",
+       " [-192.7404734   -11.66449536    0.5       ]\n",
+       " [-192.9124636    -7.82846933    0.5       ]\n",
+       " [-193.0077474    -3.94000887    0.5       ]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.Sf face area vectors — shape=(565419, 3) (565,419 × 3), dtype=float64, size=1,696,257, TRUNCATED value text; exact shape retained; size=1,696,257; threshold=256; edgeitems=4
[[-9.58700000e-05 -1.79999998e-06  0.00000000e+00]\n",
+       " [ 9.17752040e-01 -1.38844103e+01  0.00000000e+00]\n",
+       " [-9.17756170e-01  1.38844121e+01 -4.44089210e-16]\n",
+       " [-9.19400000e-05 -3.60000001e-06  0.00000000e+00]\n",
+       " ...\n",
+       " [-1.34612941e+00  1.36857734e+01  0.00000000e+00]\n",
+       " [-1.01640854e+00  1.37598128e+01  4.44089210e-16]\n",
+       " [-6.82114115e-01  1.38292516e+01  0.00000000e+00]\n",
+       " [-3.43293021e-01  1.38938816e+01  0.00000000e+00]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.V cell volumes — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[1.36059579e-03 1.21515680e-03 1.08571142e-03 9.70557105e-04 ... 5.22010257e+01 5.30622492e+01 5.39490060e+01 5.48621082e+01]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.owner internal-face owner cells — shape=(565419,) (565,419), dtype=int64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[     0      0      0      1 ... 283395 283396 283397 283398]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.neighbour internal-face neighbour cells — shape=(565419,) (565,419), dtype=int64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[     1    137  29866      2 ... 283396 283397 283398 283399]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.faces point labels per face — ragged rows=1,134,981
mesh.faces point labels per face: ragged OpenFOAM label list\n",
+       "exact row count=1,134,981\n",
+       "selected rows=11; selected row values keep exact row length; long rows use ellipsis only in the middle\n",
+       "\n",
+       "row 0: length=4\n",
+       "  [1, 139, 30361, 30223]\n",
+       "row 1: length=4\n",
+       "  [138, 30360, 30361, 139]\n",
+       "row 2: length=4\n",
+       "  [0, 1, 30223, 30222]\n",
+       "row 3: length=4\n",
+       "  [2, 140, 30362, 30224]\n",
+       "row 567,489: length=4\n",
+       "  [173873, 227309, 227310, 173874]\n",
+       "row 567,490: length=4\n",
+       "  [173874, 227310, 227311, 173875]\n",
+       "row 567,491: length=4\n",
+       "  [173875, 227311, 227312, 173876]\n",
+       "row 1,134,977: length=4\n",
+       "  [59890, 59891, 60029, 60028]\n",
+       "row 1,134,978: length=4\n",
+       "  [60028, 60029, 60167, 60166]\n",
+       "row 1,134,979: length=4\n",
+       "  [60166, 60167, 60305, 60304]\n",
+       "row 1,134,980: length=4\n",
+       "  [60304, 60305, 60443, 60442]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
mesh.cells face labels per cell — ragged rows=283,400
mesh.cells face labels per cell: ragged OpenFOAM label list\n",
+       "exact row count=283,400\n",
+       "selected rows=11; selected row values keep exact row length; long rows use ellipsis only in the middle\n",
+       "\n",
+       "row 0: length=6\n",
+       "  [0, 1, 2, 567201, 821715, 1105115]\n",
+       "row 1: length=6\n",
+       "  [3, 4, 5, 821933, 1105333, 0]\n",
+       "row 2: length=6\n",
+       "  [6, 7, 8, 822151, 1105551, 3]\n",
+       "row 3: length=6\n",
+       "  [9, 10, 11, 822369, 1105769, 6]\n",
+       "row 141,699: length=6\n",
+       "  [283017, 283018, 818126, 1101526, 282530, 283015]\n",
+       "row 141,700: length=6\n",
+       "  [283019, 283020, 818344, 1101744, 282532, 283017]\n",
+       "row 141,701: length=6\n",
+       "  [283021, 283022, 818562, 1101962, 282534, 283019]\n",
+       "row 283,396: length=6\n",
+       "  [565416, 566979, 767868, 1051268, 565145, 565415]\n",
+       "row 283,397: length=6\n",
+       "  [565417, 566980, 768086, 1051486, 565147, 565416]\n",
+       "row 283,398: length=6\n",
+       "  [565418, 566981, 768304, 1051704, 565149, 565417]\n",
+       "row 283,399: length=6\n",
+       "  [566982, 768522, 1051922, 448621, 565150, 565418]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " Boundary patch sizes\n", + " aerofoil1,026freestream1,736frontAndBack0\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " Cell-volume distribution V\n", + " n=283,400 finite, min=1.27546e-11, max=57.9661, mean=0.503898, std=2.81004\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " Face-area magnitude distribution |Sf|\n", + " n=565,419 finite, min=1.72865e-06, max=17.5268, mean=0.636312, std=2.03571\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " All cell centres: occupancy density\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=log1p(count); occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[1, 170642]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " All face centres: occupancy density\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=log1p(count); occupied bins=7,291\n", + " x=[-193.023, 197.582], y=[-194.361, 195.068]\n", + " raw color range=[1, 340226]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " LDU owner-neighbour sparse graph density\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=log1p(count); occupied bins=360\n", + " x=[0, 283398], y=[1, 283399]\n", + " raw color range=[1, 5749]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
patch aerofoil.Cf face centres — shape=(0, 3) (0 × 3), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
patch aerofoil.Sf face area vectors — shape=(0, 3) (0 × 3), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**patch aerofoil: |Sf|**: no finite values." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**patch aerofoil: face-centre occupancy**: no finite coordinates." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
patch freestream.Cf face centres — shape=(0, 3) (0 × 3), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
patch freestream.Sf face area vectors — shape=(0, 3) (0 × 3), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**patch freestream: |Sf|**: no finite values." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**patch freestream: face-centre occupancy**: no finite coordinates." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
patch frontAndBack.Cf face centres — shape=(0, 3) (0 × 3), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
patch frontAndBack.Sf face area vectors — shape=(0, 3) (0 × 3), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**patch frontAndBack: |Sf|**: no finite values." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**patch frontAndBack: face-centre occupancy**: no finite coordinates." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "safe_array_panel(\"mesh.points\", lambda: mesh.points, open=False)\n", + "array_panel(\"mesh.C cell centres\", mesh.C, open=False)\n", + "array_panel(\"mesh.Cf face centres\", mesh.Cf, open=False)\n", + "array_panel(\"mesh.Sf face area vectors\", mesh.Sf, open=False)\n", + "array_panel(\"mesh.V cell volumes\", mesh.V, open=False)\n", + "array_panel(\"mesh.owner internal-face owner cells\", mesh.owner, open=False)\n", + "array_panel(\"mesh.neighbour internal-face neighbour cells\", mesh.neighbour, open=False)\n", + "ragged_panel(\"mesh.faces point labels per face\", mesh.faces, mesh.n_faces, open=False)\n", + "ragged_panel(\"mesh.cells face labels per cell\", mesh.cells, mesh.n_cells, open=False)\n", + "\n", + "bar_chart_svg(\n", + " \"Boundary patch sizes\",\n", + " [patch.name for patch in mesh.boundary],\n", + " [patch.size for patch in mesh.boundary],\n", + ")\n", + "histogram_svg(\"Cell-volume distribution V\", mesh.V, bins=90, log_y=True)\n", + "histogram_svg(\"Face-area magnitude distribution |Sf|\", np.linalg.norm(np.asarray(mesh.Sf), axis=1), bins=90, log_y=True)\n", + "density_svg(\"All cell centres: occupancy density\", np.asarray(mesh.C)[:, 0], np.asarray(mesh.C)[:, 1], grid=FULL_DETAIL_GRID, log_counts=True)\n", + "density_svg(\"All face centres: occupancy density\", np.asarray(mesh.Cf)[:, 0], np.asarray(mesh.Cf)[:, 1], grid=FULL_DETAIL_GRID, log_counts=True)\n", + "show_ldu_density(mesh)\n", + "\n", + "for patch in mesh.boundary:\n", + " start = int(patch.start)\n", + " stop = start + int(patch.size)\n", + " patch_cf = np.asarray(mesh.Cf[start:stop])\n", + " patch_sf = np.asarray(mesh.Sf[start:stop])\n", + " array_panel(f\"patch {patch.name}.Cf face centres\", patch_cf, open=False)\n", + " array_panel(f\"patch {patch.name}.Sf face area vectors\", patch_sf, open=False)\n", + " histogram_svg(f\"patch {patch.name}: |Sf|\", np.linalg.norm(patch_sf, axis=1), bins=60, log_y=True)\n", + " density_svg(f\"patch {patch.name}: face-centre occupancy\", patch_cf[:, 0], patch_cf[:, 1], grid=64, log_counts=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-16", + "metadata": {}, + "source": [ + "### Mesh topology samples\n", + "\n", + "The arrays below are the core OpenFOAM finite-volume addressing:\n", + "\n", + "- `mesh.faces` is ragged: each face has a variable number of point labels;\n", + "- `mesh.owner[f]` is the cell on one side of internal face `f`;\n", + "- `mesh.neighbour[f]` is the cell on the other side;\n", + "- the same addressing is later reused by `MatrixView.upper` and `MatrixView.lower`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "full-detail-17", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.342450Z", + "iopub.status.busy": "2026-07-22T20:00:19.342284Z", + "iopub.status.idle": "2026-07-22T20:00:19.351503Z", + "shell.execute_reply": "2026-07-22T20:00:19.350958Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Internal face samples**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| face | owner | neighbour | n points | point labels sample | face centre Cf | area vector Sf |\n", + "| --- | --- | --- | --- | --- | --- | --- |\n", + "| 0 | 0 | 1 | 4 | (1, 139, 30361, 30223) | (186.11558879999995, 12.236046185, 0.49999999999999994) | (-9.587000000088608e-05, -1.7999999784024112e-06, 0.0) |\n", + "| 1 | 0 | 137 | 4 | (138, 30360, 30361, 139) | (193.05779484999996, 12.694874269999998, 0.5) | (0.9177520399999999, -13.884410300000013, 0.0) |\n", + "| 2 | 0 | 29866 | 4 | (0, 1, 30223, 30222) | (193.05779395, 12.694972204999997, 0.49999999999999994) | (-0.9177561699999988, 13.884412099999992, -4.440892098500626e-16) |\n", + "| 565418 | 283398 | 283399 | 4 | (510601, 510869, 569561, 569293) | (-193.0077474, -3.9400088735, 0.5) | (-0.3432930209999996, 13.893881599999986, 0.0) |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Cell samples**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| cell | n faces | face labels sample | cell centre C | volume V |\n", + "| --- | --- | --- | --- | --- |\n", + "| 0 | 6 | (0, 1, 2, 567201, 821715, 1105115) | (193.10515306703107, 12.698053626715934, 0.4999999999999795) | 0.001361 |\n", + "| 1 | 6 | (3, 4, 5, 821933, 1105333, 0) | (179.70132111689765, 11.812065637500481, 0.49999999999999994) | 0.001215 |\n", + "| 141700 | 6 | (283019, 283020, 818344, 1101744, 282532, 283017) | (0.4132102425057547, -0.18515406274195162, 0.5) | 6.852e-05 |\n", + "| 283399 | 6 | (566982, 768522, 1051922, 448621, 565150, 565418) | (-193.1168155735373, -1.9698796355742973, 0.5) | 54.86 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "face_indices = [0, 1, 2, mesh.n_internal_faces - 1]\n", + "face_rows = []\n", + "for face_i in face_indices:\n", + " points = mesh.faces.row(face_i)\n", + " face_rows.append([\n", + " face_i,\n", + " int(mesh.owner[face_i]),\n", + " int(mesh.neighbour[face_i]),\n", + " len(points),\n", + " tuple(int(x) for x in points[:8]),\n", + " tuple(float(x) for x in mesh.Cf[face_i]),\n", + " tuple(float(x) for x in mesh.Sf[face_i]),\n", + " ])\n", + "show_table(\"Internal face samples\", [\"face\", \"owner\", \"neighbour\", \"n points\", \"point labels sample\", \"face centre Cf\", \"area vector Sf\"], face_rows)\n", + "\n", + "cell_rows = []\n", + "for cell_i in [0, 1, mesh.n_cells // 2, mesh.n_cells - 1]:\n", + " faces_i = mesh.cells.row(cell_i)\n", + " cell_rows.append([\n", + " cell_i,\n", + " len(faces_i),\n", + " tuple(int(x) for x in faces_i[:10]),\n", + " tuple(float(x) for x in mesh.C[cell_i]),\n", + " float(mesh.V[cell_i]),\n", + " ])\n", + "show_table(\"Cell samples\", [\"cell\", \"n faces\", \"face labels sample\", \"cell centre C\", \"volume V\"], cell_rows)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-18", + "metadata": {}, + "source": [ + "## 4. Inspect registered fields and boundary conditions\n", + "\n", + "The initial state contains six important fields:\n", + "\n", + "- `U`: velocity, a cell vector field;\n", + "- `p`: kinematic pressure-like pressure used by incompressible OpenFOAM, a cell scalar field;\n", + "- `phi`: face flux, a surface scalar field on internal faces plus boundary patches;\n", + "- `nut`: turbulent viscosity from the RAS model;\n", + "- `k`: turbulent kinetic energy;\n", + "- `omega`: specific dissipation rate for k-omega SST.\n", + "\n", + "The internal arrays are what the linear systems update. The boundary patch fields tell OpenFOAM how to close the equations at the wall, freestream, and empty extrusion planes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "full-detail-19", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.353167Z", + "iopub.status.busy": "2026-07-22T20:00:19.352859Z", + "iopub.status.idle": "2026-07-22T20:00:19.399841Z", + "shell.execute_reply": "2026-07-22T20:00:19.399456Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Initial field registry**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | kind | dimensions | entity | count | shape | min | max | mean |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| p | volScalar | [0 2 -2 0 0 0 0] | cell | 283400 | (283400,) | 0 | 0 | 0 |\n", + "| U | volVector | [0 1 -1 0 0 0 0] | cell | 283400 | (283400, 3) | 0 | 93.01 | 33.06 |\n", + "| phi | surfaceScalar | [0 3 -1 0 0 0 0] | internal_face | 565419 | (565419,) | -1632 | 53.68 | -41.24 |\n", + "| nut | volScalar | [0 2 -1 0 0 0 0] | cell | 283400 | (283400,) | 1.574e-17 | 3.12e-09 | 3.109e-09 |\n", + "| k | volScalar | [0 2 -2 0 0 0 0] | cell | 283400 | (283400,) | 3.635e-09 | 3.635e-09 | 3.635e-09 |\n", + "| omega | volScalar | [0 0 -1 0 0 0 0] | cell | 283400 | (283400,) | 1.165 | 1.165 | 1.165 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Boundary-condition views**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | patch | BC type | values shape | min | max | fixes value | assignable | coupled |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| p | aerofoil | zeroGradient | (1026,) | 0 | 0 | False | True | False |\n", + "| p | freestream | freestreamPressure | (1736,) | 0 | 0 | True | False | False |\n", + "| p | frontAndBack | empty | (0,) | | | False | True | False |\n", + "| U | aerofoil | noSlip | (1026, 3) | 0 | 0 | True | False | False |\n", + "| U | freestream | freestreamVelocity | (1736, 3) | 0 | 93.01 | True | False | False |\n", + "| U | frontAndBack | empty | (0, 3) | | | False | True | False |\n", + "| phi | aerofoil | calculated | (1026,) | 0 | 0 | False | True | False |\n", + "| phi | freestream | calculated | (1736,) | -382.7 | 1034 | False | True | False |\n", + "| phi | frontAndBack | empty | (0,) | | | False | True | False |\n", + "| nut | aerofoil | nutLowReWallFunction | (1026,) | 0 | 0 | True | False | False |\n", + "| nut | freestream | freestream | (1736,) | 3.12e-09 | 1.56e-05 | True | True | False |\n", + "| nut | frontAndBack | empty | (0,) | | | False | True | False |\n", + "| k | aerofoil | fixedValue | (1026,) | 3.635e-09 | 3.635e-09 | True | False | False |\n", + "| k | freestream | freestream | (1736,) | 3.635e-09 | 3.635e-09 | True | True | False |\n", + "| k | frontAndBack | empty | (0,) | | | False | True | False |\n", + "| omega | aerofoil | omegaWallFunction | (1026,) | 1.165 | 1.165 | True | False | False |\n", + "| omega | freestream | freestream | (1736,) | 1.165 | 1.165 | True | True | False |\n", + "| omega | frontAndBack | empty | (0,) | | | False | True | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "show_table(\n", + " \"Initial field registry\",\n", + " [\"name\", \"kind\", \"dimensions\", \"entity\", \"count\", \"shape\", \"min\", \"max\", \"mean\"],\n", + " [field_summary_row(fields[name]) for name in fields],\n", + ")\n", + "\n", + "boundary_rows = []\n", + "for name in fields:\n", + " boundary_rows.extend(boundary_field_rows(fields[name]))\n", + "show_table(\n", + " \"Boundary-condition views\",\n", + " [\"field\", \"patch\", \"BC type\", \"values shape\", \"min\", \"max\", \"fixes value\", \"assignable\", \"coupled\"],\n", + " boundary_rows,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-20", + "metadata": {}, + "source": [ + "### Full-shape field and boundary-condition arrays\n", + "\n", + "This replaces the small-sample field dump from the original notebook. Each internal field and each boundary patch field is shown with exact shape metadata. Histograms and spatial maps use the complete arrays.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "full-detail-21", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.410758Z", + "iopub.status.busy": "2026-07-22T20:00:19.410543Z", + "iopub.status.idle": "2026-07-22T20:00:19.422292Z", + "shell.execute_reply": "2026-07-22T20:00:19.420159Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### Field `U`: `volVector` on `cell`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**U exact field metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| dimensions | [0 1 -1 0 0 0 0] |\n", + "| entity_kind | cell |\n", + "| entity_count | 283400 |\n", + "| internal_shape | (283400, 3) |\n", + "| internal_dtype | float64 |\n", + "| boundary_patch_count | 3 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
U.internal — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " ...\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " U component 0 internal values\n", + " n=283,400 finite, min=92.0791, max=93.9392, mean=93.0091, std=1.42109e-14\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " U component 1 internal values\n", + " n=283,400 finite, min=6.09974, max=6.22297, mean=6.16136, std=1.77636e-15\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " U component 2 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " U internal vector/tensor magnitude\n", + " n=283,400 finite, min=92.2809, max=94.1451, mean=93.213, std=2.84217e-14\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " U: complete internal field mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[93.213, 93.213]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**U boundary patch `aerofoil` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | noSlip |\n", + "| values shape | (1026, 3) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | False |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
U.boundary['aerofoil'].values — shape=(1026, 3) (1,026 × 3), dtype=float64, size=3,078, TRUNCATED value text; exact shape retained; size=3,078; threshold=256; edgeitems=4
[[0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " ...\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " U boundary `aerofoil` values\n", + " n=3,078 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**U boundary patch `freestream` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | freestreamVelocity |\n", + "| values shape | (1736, 3) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | False |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
U.boundary['freestream'].values — shape=(1736, 3) (1,736 × 3), dtype=float64, size=5,208, TRUNCATED value text; exact shape retained; size=5,208; threshold=256; edgeitems=4
[[93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " ...\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " U boundary `freestream` values\n", + " n=5,208 finite, min=0, max=93.0091, mean=33.0568, std=42.4672\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**U boundary patch `frontAndBack` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | empty |\n", + "| values shape | (0, 3) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
U.boundary['frontAndBack'].values — shape=(0, 3) (0 × 3), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Field `p`: `volScalar` on `cell`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**p exact field metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| dimensions | [0 2 -2 0 0 0 0] |\n", + "| entity_kind | cell |\n", + "| entity_count | 283400 |\n", + "| internal_shape | (283400,) |\n", + "| internal_dtype | float64 |\n", + "| boundary_patch_count | 3 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
p.internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[0. 0. 0. 0. ... 0. 0. 0. 0.]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " p internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " p: complete internal field mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0, 0]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**p boundary patch `aerofoil` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | zeroGradient |\n", + "| values shape | (1026,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
p.boundary['aerofoil'].values — shape=(1026,) (1,026), dtype=float64, size=1,026, TRUNCATED value text; exact shape retained; size=1,026; threshold=256; edgeitems=4
[0. 0. 0. 0. ... 0. 0. 0. 0.]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " p boundary `aerofoil` values\n", + " n=1,026 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**p boundary patch `freestream` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | freestreamPressure |\n", + "| values shape | (1736,) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | False |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
p.boundary['freestream'].values — shape=(1736,) (1,736), dtype=float64, size=1,736, TRUNCATED value text; exact shape retained; size=1,736; threshold=256; edgeitems=4
[0. 0. 0. 0. ... 0. 0. 0. 0.]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " p boundary `freestream` values\n", + " n=1,736 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**p boundary patch `frontAndBack` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | empty |\n", + "| values shape | (0,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
p.boundary['frontAndBack'].values — shape=(0,) (0), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Field `phi`: `surfaceScalar` on `internal_face`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**phi exact field metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| dimensions | [0 3 -1 0 0 0 0] |\n", + "| entity_kind | internal_face |\n", + "| entity_count | 565419 |\n", + "| internal_shape | (565419,) |\n", + "| internal_dtype | float64 |\n", + "| boundary_patch_count | 3 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
phi.internal — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-8.92787718e-03 -1.87462300e-01  1.87089263e-01 -8.57344168e-03 ... -4.08794233e+01 -9.75618397e+00  2.17640919e+01  5.36757606e+01]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " phi internal values\n", + " n=565,419 finite, min=-1631.95, max=53.6758, mean=-41.2404, std=161.171\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " phi: complete internal field mapped on internal face centres\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=bin mean; occupied bins=7,291\n", + " x=[-193.023, 197.582], y=[-194.361, 195.068]\n", + " raw color range=[-1631.79, 53.6758]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**phi boundary patch `aerofoil` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | calculated |\n", + "| values shape | (1026,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
phi.boundary['aerofoil'].values — shape=(1026,) (1,026), dtype=float64, size=1,026, TRUNCATED value text; exact shape retained; size=1,026; threshold=256; edgeitems=4
[ 0.  0.  0.  0. ...  0. -0.  0. -0.]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " phi boundary `aerofoil` values\n", + " n=1,026 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**phi boundary patch `freestream` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | calculated |\n", + "| values shape | (1736,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
phi.boundary['freestream'].values — shape=(1736,) (1,736), dtype=float64, size=1,736, TRUNCATED value text; exact shape retained; size=1,736; threshold=256; edgeitems=4
[ 0.83994191  0.84201829  0.84387795  0.8455505  ... -0.90038459 -0.87774364 -0.85567199 -0.83415537]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " phi boundary `freestream` values\n", + " n=1,736 finite, min=-382.701, max=1033.56, mean=2.09561e-15, std=123.688\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**phi boundary patch `frontAndBack` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | empty |\n", + "| values shape | (0,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
phi.boundary['frontAndBack'].values — shape=(0,) (0), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Field `nut`: `volScalar` on `cell`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**nut exact field metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| dimensions | [0 2 -1 0 0 0 0] |\n", + "| entity_kind | cell |\n", + "| entity_count | 283400 |\n", + "| internal_shape | (283400,) |\n", + "| internal_dtype | float64 |\n", + "| boundary_patch_count | 3 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
nut.internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[3.12e-09 3.12e-09 3.12e-09 3.12e-09 ... 3.12e-09 3.12e-09 3.12e-09 3.12e-09]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " nut internal values\n", + " n=283,400 finite, min=1.57427e-17, max=3.12e-09, mean=3.1087e-09, std=1.87388e-10\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " nut: complete internal field mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[3.10124e-09, 3.12e-09]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**nut boundary patch `aerofoil` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | nutLowReWallFunction |\n", + "| values shape | (1026,) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | False |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
nut.boundary['aerofoil'].values — shape=(1026,) (1,026), dtype=float64, size=1,026, TRUNCATED value text; exact shape retained; size=1,026; threshold=256; edgeitems=4
[0. 0. 0. 0. ... 0. 0. 0. 0.]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " nut boundary `aerofoil` values\n", + " n=1,026 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**nut boundary patch `freestream` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | freestream |\n", + "| values shape | (1736,) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
nut.boundary['freestream'].values — shape=(1736,) (1,736), dtype=float64, size=1,736, TRUNCATED value text; exact shape retained; size=1,736; threshold=256; edgeitems=4
[3.12e-09 3.12e-09 3.12e-09 3.12e-09 ... 1.56e-05 1.56e-05 1.56e-05 1.56e-05]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " nut boundary `freestream` values\n", + " n=1,736 finite, min=3.12e-09, max=1.56e-05, mean=7.68476e-06, std=7.79757e-06\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**nut boundary patch `frontAndBack` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | empty |\n", + "| values shape | (0,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
nut.boundary['frontAndBack'].values — shape=(0,) (0), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Field `k`: `volScalar` on `cell`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**k exact field metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| dimensions | [0 2 -2 0 0 0 0] |\n", + "| entity_kind | cell |\n", + "| entity_count | 283400 |\n", + "| internal_shape | (283400,) |\n", + "| internal_dtype | float64 |\n", + "| boundary_patch_count | 3 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
k.internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[3.635307e-09 3.635307e-09 3.635307e-09 3.635307e-09 ... 3.635307e-09 3.635307e-09 3.635307e-09 3.635307e-09]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " k internal values\n", + " n=283,400 finite, min=3.59895e-09, max=3.67166e-09, mean=3.63531e-09, std=1.24077e-24\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " k: complete internal field mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[3.63531e-09, 3.63531e-09]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**k boundary patch `aerofoil` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | fixedValue |\n", + "| values shape | (1026,) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | False |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
k.boundary['aerofoil'].values — shape=(1026,) (1,026), dtype=float64, size=1,026, TRUNCATED value text; exact shape retained; size=1,026; threshold=256; edgeitems=4
[3.635307e-09 3.635307e-09 3.635307e-09 3.635307e-09 ... 3.635307e-09 3.635307e-09 3.635307e-09 3.635307e-09]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " k boundary `aerofoil` values\n", + " n=1,026 finite, min=3.59895e-09, max=3.67166e-09, mean=3.63531e-09, std=8.27181e-25\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**k boundary patch `freestream` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | freestream |\n", + "| values shape | (1736,) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
k.boundary['freestream'].values — shape=(1736,) (1,736), dtype=float64, size=1,736, TRUNCATED value text; exact shape retained; size=1,736; threshold=256; edgeitems=4
[3.635307e-09 3.635307e-09 3.635307e-09 3.635307e-09 ... 3.635307e-09 3.635307e-09 3.635307e-09 3.635307e-09]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " k boundary `freestream` values\n", + " n=1,736 finite, min=3.59895e-09, max=3.67166e-09, mean=3.63531e-09, std=4.1359e-25\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**k boundary patch `frontAndBack` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | empty |\n", + "| values shape | (0,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
k.boundary['frontAndBack'].values — shape=(0,) (0), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Field `omega`: `volScalar` on `cell`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**omega exact field metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| dimensions | [0 0 -1 0 0 0 0] |\n", + "| entity_kind | cell |\n", + "| entity_count | 283400 |\n", + "| internal_shape | (283400,) |\n", + "| internal_dtype | float64 |\n", + "| boundary_patch_count | 3 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
omega.internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[1.1651625 1.1651625 1.1651625 1.1651625 ... 1.1651625 1.1651625 1.1651625 1.1651625]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " omega internal values\n", + " n=283,400 finite, min=1.15351, max=1.17681, mean=1.16516, std=2.22045e-16\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " omega: complete internal field mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[1.16516, 1.16516]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**omega boundary patch `aerofoil` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | omegaWallFunction |\n", + "| values shape | (1026,) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | False |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
omega.boundary['aerofoil'].values — shape=(1026,) (1,026), dtype=float64, size=1,026, TRUNCATED value text; exact shape retained; size=1,026; threshold=256; edgeitems=4
[1.1651625 1.1651625 1.1651625 1.1651625 ... 1.1651625 1.1651625 1.1651625 1.1651625]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " omega boundary `aerofoil` values\n", + " n=1,026 finite, min=1.15351, max=1.17681, mean=1.16516, std=2.22045e-16\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**omega boundary patch `freestream` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | freestream |\n", + "| values shape | (1736,) |\n", + "| values dtype | float64 |\n", + "| fixes value | True |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
omega.boundary['freestream'].values — shape=(1736,) (1,736), dtype=float64, size=1,736, TRUNCATED value text; exact shape retained; size=1,736; threshold=256; edgeitems=4
[1.1651625 1.1651625 1.1651625 1.1651625 ... 1.1651625 1.1651625 1.1651625 1.1651625]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " omega boundary `freestream` values\n", + " n=1,736 finite, min=1.15351, max=1.17681, mean=1.16516, std=2.22045e-16\n", + " x=value, y=log1p(count), bins=60\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**omega boundary patch `frontAndBack` metadata**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| property | value |\n", + "| --- | --- |\n", + "| BC type | empty |\n", + "| values shape | (0,) |\n", + "| values dtype | float64 |\n", + "| fixes value | False |\n", + "| assignable | True |\n", + "| coupled | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
omega.boundary['frontAndBack'].values — shape=(0,) (0), dtype=float64, size=0, complete value text
[]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for name in [\"U\", \"p\", \"phi\", \"nut\", \"k\", \"omega\"]:\n", + " field = fields[name]\n", + " display(Markdown(f\"#### Field `{name}`: `{field.kind}` on `{field.entity_kind}`\"))\n", + " show_table(\n", + " f\"{name} exact field metadata\",\n", + " [\"property\", \"value\"],\n", + " [\n", + " [\"dimensions\", field.dimensions],\n", + " [\"entity_kind\", field.entity_kind],\n", + " [\"entity_count\", field.entity_count],\n", + " [\"internal_shape\", np.asarray(field.internal).shape],\n", + " [\"internal_dtype\", np.asarray(field.internal).dtype],\n", + " [\"boundary_patch_count\", len(field.boundary)],\n", + " ],\n", + " )\n", + " show_field_histograms(field)\n", + " if field.entity_kind == \"cell\" or np.asarray(field.internal).shape[0] == mesh.n_cells:\n", + " show_cell_field_map(mesh, field, title=f\"{name}: complete internal field mapped on cell centres\")\n", + " elif field.entity_kind == \"face\" or np.asarray(field.internal).shape[0] == mesh.n_internal_faces:\n", + " show_face_field_map(mesh, field, title=f\"{name}: complete internal field mapped on internal face centres\")\n", + " for patch_name, patch_field in field.boundary.items():\n", + " values = np.asarray(patch_field.values)\n", + " show_table(\n", + " f\"{name} boundary patch `{patch_name}` metadata\",\n", + " [\"property\", \"value\"],\n", + " [\n", + " [\"BC type\", patch_field.type],\n", + " [\"values shape\", values.shape],\n", + " [\"values dtype\", values.dtype],\n", + " [\"fixes value\", patch_field.fixes_value],\n", + " [\"assignable\", patch_field.assignable],\n", + " [\"coupled\", patch_field.coupled],\n", + " ],\n", + " )\n", + " array_panel(f\"{name}.boundary[{patch_name!r}].values\", values, open=False)\n", + " if values.size:\n", + " histogram_svg(f\"{name} boundary `{patch_name}` values\", values, bins=60, log_y=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-22", + "metadata": {}, + "source": [ + "## 5. The equations behind the objects\n", + "\n", + "For this steady incompressible RANS run, the solver is seeking a fixed point of the discretized equations, not integrating real physical time.\n", + "\n", + "Continuity enforces mass conservation:\n", + "\n", + "$$\\nabla \\cdot U = 0$$\n", + "\n", + "OpenFOAM stores the face-normal volume flux as `phi`. The momentum equation, after RANS modelling and finite-volume discretization, becomes a sparse linear system for velocity:\n", + "\n", + "$$A_U U = H(U, \\phi, \\nu_\\mathrm{eff}) - \\nabla p$$\n", + "\n", + "where `nuEff = nu + nut` is affected by the k-omega SST turbulence model. SIMPLE/SIMPLEC alternates between:\n", + "\n", + "1. assemble/solve a momentum predictor for `U`;\n", + "2. assemble/solve a pressure equation for `p` so corrected `phi` satisfies continuity;\n", + "3. correct `U`, `p`, and `phi`;\n", + "4. update turbulence quantities `k`, `omega`, and `nut`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "full-detail-23", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.425612Z", + "iopub.status.busy": "2026-07-22T20:00:19.425445Z", + "iopub.status.idle": "2026-07-22T20:00:19.438238Z", + "shell.execute_reply": "2026-07-22T20:00:19.431531Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**Split-step execution plan**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Step | Phase | What to watch |\n", + "| --- | --- | --- |\n", + "| pre_solve | time | OpenFOAM preSolve hook: mesh/field housekeeping before the loop |\n", + "| advance_time | time | increment pseudo-time from 0 to 1 |\n", + "| begin_pimple_iteration | pimple | enter the SIMPLE/PIMPLE outer corrector |\n", + "| fv_models_correct | pimple | apply fvModels if present; none are active here |\n", + "| pre_predictor | pimple | module hook before momentum/turbulence predictor |\n", + "| momentum_transport_predictor | momentum | predict turbulence/momentum-transport state if enabled |\n", + "| assemble_momentum_terms | momentum | split the pieces that will become UEqn |\n", + "| assemble_UEqn | momentum | build the sparse fvVectorMatrix for U |\n", + "| relax_UEqn | momentum | apply equation relaxation from fvSolution |\n", + "| constrain_UEqn | momentum | apply constraints/boundary equation changes |\n", + "| solve_UEqn | momentum | linear solve for U components |\n", + "| compute_pressure_inputs | pressure | compute rAU, HbyA, phiHbyA, rAtU |\n", + "| assemble_pEqn | pressure | build the sparse fvScalarMatrix for p |\n", + "| solve_pEqn | pressure | pressure solves and flux update |\n", + "| correct_velocity_pressure_flux | pressure | final SIMPLEC correction of U, p, phi |\n", + "| momentum_transport_correct | momentum | solve/correct k, omega, nut |\n", + "| end_pimple_iteration | pimple | leave the outer corrector |\n", + "| post_solve | time | postSolve hook; optional write disabled here |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "step_plan = [\n", + " [\"pre_solve\", \"time\", \"OpenFOAM preSolve hook: mesh/field housekeeping before the loop\"],\n", + " [\"advance_time\", \"time\", \"increment pseudo-time from 0 to 1\"],\n", + " [\"begin_pimple_iteration\", \"pimple\", \"enter the SIMPLE/PIMPLE outer corrector\"],\n", + " [\"fv_models_correct\", \"pimple\", \"apply fvModels if present; none are active here\"],\n", + " [\"pre_predictor\", \"pimple\", \"module hook before momentum/turbulence predictor\"],\n", + " [\"momentum_transport_predictor\", \"momentum\", \"predict turbulence/momentum-transport state if enabled\"],\n", + " [\"assemble_momentum_terms\", \"momentum\", \"split the pieces that will become UEqn\"],\n", + " [\"assemble_UEqn\", \"momentum\", \"build the sparse fvVectorMatrix for U\"],\n", + " [\"relax_UEqn\", \"momentum\", \"apply equation relaxation from fvSolution\"],\n", + " [\"constrain_UEqn\", \"momentum\", \"apply constraints/boundary equation changes\"],\n", + " [\"solve_UEqn\", \"momentum\", \"linear solve for U components\"],\n", + " [\"compute_pressure_inputs\", \"pressure\", \"compute rAU, HbyA, phiHbyA, rAtU\"],\n", + " [\"assemble_pEqn\", \"pressure\", \"build the sparse fvScalarMatrix for p\"],\n", + " [\"solve_pEqn\", \"pressure\", \"pressure solves and flux update\"],\n", + " [\"correct_velocity_pressure_flux\", \"pressure\", \"final SIMPLEC correction of U, p, phi\"],\n", + " [\"momentum_transport_correct\", \"momentum\", \"solve/correct k, omega, nut\"],\n", + " [\"end_pimple_iteration\", \"pimple\", \"leave the outer corrector\"],\n", + " [\"post_solve\", \"time\", \"postSolve hook; optional write disabled here\"],\n", + "]\n", + "show_table(\"Split-step execution plan\", [\"Step\", \"Phase\", \"What to watch\"], step_plan)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-24", + "metadata": {}, + "source": [ + "## 6. Start the split-step walk\n", + "\n", + "`TransformResult` is the central learning object. Every step returns:\n", + "\n", + "- `name` and `phase`;\n", + "- `inputs`: fields/matrices consumed by that step;\n", + "- `outputs`: fields/matrices/results produced by that step;\n", + "- `changed_fields`: high-level names that changed;\n", + "- `source`: OpenFOAM source file/function/line provenance when known.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "full-detail-25", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.443811Z", + "iopub.status.busy": "2026-07-22T20:00:19.443591Z", + "iopub.status.idle": "2026-07-22T20:00:19.482622Z", + "shell.execute_reply": "2026-07-22T20:00:19.481846Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial split-step snapshot captured for: ['U', 'k', 'nut', 'omega', 'p', 'phi']\n" + ] + } + ], + "source": [ + "transform_results = []\n", + "initial_snapshot = field_snapshot(stepper.fields())\n", + "\n", + "\n", + "def record(result):\n", + " transform_results.append(result)\n", + " show_table(\n", + " f\"TransformResult: {result.name}\",\n", + " [\"name\", \"phase\", \"changed\", \"inputs\", \"outputs\", \"source\"],\n", + " [transform_row(result)],\n", + " )\n", + " return result\n", + "\n", + "print(\"Initial split-step snapshot captured for:\", sorted(initial_snapshot))\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-26", + "metadata": {}, + "source": [ + "### Time and SIMPLE loop setup\n", + "\n", + "These steps move OpenFOAM from case time `0` to pseudo-time `1` and open the outer SIMPLE/PIMPLE corrector. No velocity/pressure solve has happened yet.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "full-detail-27", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.484224Z", + "iopub.status.busy": "2026-07-22T20:00:19.484011Z", + "iopub.status.idle": "2026-07-22T20:00:19.496615Z", + "shell.execute_reply": "2026-07-22T20:00:19.496029Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**TransformResult: pre_solve**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| pre_solve | time | mesh, Uf | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | OpenFOAM-14/applications/modules/incompressibleFluid/incompressibleFluid.C:165-201::preSolve |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: advance_time**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| advance_time | time | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: begin_pimple_iteration**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| begin_pimple_iteration | pimple | — | — | active, state | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: fv_models_correct**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| fv_models_correct | pimple | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: pre_predictor**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| pre_predictor | pimple | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: momentum_transport_predict**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| momentum_transport_predict | momentum | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | OpenFOAM-14/applications/modules/incompressibleFluid/incompressibleFluid.C:208-211::momentumTransportPredictor |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**State after loop setup**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Key | Value |\n", + "| --- | --- |\n", + "| case_path | /home/aaron/data/openFOAM-RANS-to-GPU/tmp/airfrans_stepper_full_detail_learning_tool/split_case |\n", + "| solver_name | incompressibleFluid |\n", + "| time_name | 1 |\n", + "| time_index | 1 |\n", + "| time_value | 1 |\n", + "| delta_t | 1 |\n", + "| pimple_iteration_open | True |\n", + "| has_momentum_matrix | False |\n", + "| has_pressure_inputs | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for label, fn in [\n", + " (\"pre_solve\", stepper.pre_solve),\n", + " (\"advance_time\", stepper.advance_time),\n", + " (\"begin_pimple_iteration\", stepper.begin_pimple_iteration),\n", + " (\"fv_models_correct\", stepper.fv_models_correct),\n", + " (\"pre_predictor\", stepper.pre_predictor),\n", + " (\"momentum_transport_predictor\", stepper.momentum_transport_predictor),\n", + "]:\n", + " record(fn())\n", + "\n", + "show_table(\"State after loop setup\", [\"Key\", \"Value\"], list(stepper.state().items()))\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-28", + "metadata": {}, + "source": [ + "## 7. Momentum equation terms\n", + "\n", + "Before building the complete velocity matrix, the wrapper asks OpenFOAM for the pieces of the momentum equation. This is the best place to learn what `UEqn` is made of.\n", + "\n", + "The implicit pieces become matrix coefficients. The explicit pieces become source-like fields.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "full-detail-29", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.498401Z", + "iopub.status.busy": "2026-07-22T20:00:19.498073Z", + "iopub.status.idle": "2026-07-22T20:00:19.699758Z", + "shell.execute_reply": "2026-07-22T20:00:19.699342Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**TransformResult: assemble_momentum_terms**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| assemble_momentum_terms | momentum | — | p, U, phi, nut, k, omega | terms, source | OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C:36-53::Foam::solvers::incompressibleFluid::momentumPredictor |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Momentum-term decomposition**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | OpenFOAM expression | implicit | inputs | payload |\n", + "| --- | --- | --- | --- | --- |\n", + "| assemble_momentum_ddt | fvm::ddt(U) | True | U, phi, p | diag (283400,), source (283400, 3), upper None |\n", + "| assemble_momentum_div_phi_U | fvm::div(phi,U) | True | U, phi, p | diag (283400,), source (283400, 3), upper (565419,) |\n", + "| assemble_momentum_divDevSigma | momentumTransport->divDevSigma(U) | True | U, phi, p | diag (283400,), source (283400, 3), upper (565419,) |\n", + "| assemble_momentum_sources | fvModels().source(U) | True | U, phi, p | diag (283400,), source (283400, 3), upper None |\n", + "| assemble_momentum_MRF_DDt | MRF.DDt(U) | False | U | field MRFZoneList:DDt, shape (283400, 3), l2 0 |\n", + "| assemble_momentum_pressure_rhs_grad_p | -fvc::grad(p) | False | p | field grad(p), shape (283400, 3), l2 0 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "terms_result = record(stepper.assemble_momentum_terms())\n", + "terms = terms_result.outputs[\"terms\"]\n", + "term_rows = []\n", + "for term in terms:\n", + " if \"matrix\" in term:\n", + " matrix = term[\"matrix\"]\n", + " diag = arr_stats(matrix.diag)\n", + " source = arr_stats(matrix.source)\n", + " upper_shape = None if matrix.upper is None else tuple(np.asarray(matrix.upper).shape)\n", + " payload = f\"diag {diag['shape']}, source {source['shape']}, upper {upper_shape}\"\n", + " else:\n", + " field = term[\"field\"]\n", + " stats = arr_stats(field.internal)\n", + " payload = f\"field {field.name}, shape {stats['shape']}, l2 {fmt(stats['l2'])}\"\n", + " term_rows.append([\n", + " term[\"name\"],\n", + " term[\"kind\"],\n", + " term[\"implicit\"],\n", + " \", \".join(term[\"inputs\"]),\n", + " payload,\n", + " ])\n", + "show_table(\"Momentum-term decomposition\", [\"name\", \"OpenFOAM expression\", \"implicit\", \"inputs\", \"payload\"], term_rows)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-30", + "metadata": {}, + "source": [ + "### Full-shape momentum-term payloads\n", + "\n", + "Each term below is either a full matrix payload or a field payload. The raw panels keep complete shape metadata; histograms consume all coefficients or field values.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "full-detail-31", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### Momentum term `assemble_momentum_ddt` — `fvm::ddt(U)`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_ddt.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[0. 0. 0. 0. ... 0. 0. 0. 0.]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_ddt.source — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " ...\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_ddt.diag coefficient distribution\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_ddt.source distribution\n", + " n=850,200 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Momentum term `assemble_momentum_div_phi_U` — `fvm::div(phi,U)`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_div_phi_U.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[1.87089263e-01 1.82963849e-01 1.70467373e-01 1.58836418e-01 ... 3.78668061e+02 3.73605332e+02 3.99969387e+02 4.36376495e+02]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_div_phi_U.source — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[-7.05168044e-13 -4.34221328e-15  0.00000000e+00]\n",
+       " [-1.27219059e-13 -4.18512726e-14  0.00000000e+00]\n",
+       " [ 6.58276063e-13  4.17470685e-14  0.00000000e+00]\n",
+       " [ 4.47464866e-14  3.70092311e-14  0.00000000e+00]\n",
+       " ...\n",
+       " [ 2.60823561e-12 -1.30219992e-13  0.00000000e+00]\n",
+       " [ 2.62017246e-13  2.56321219e-14  0.00000000e+00]\n",
+       " [ 1.78859210e-12  1.15501967e-13  0.00000000e+00]\n",
+       " [ 6.98628865e-13 -1.22370729e-13  0.00000000e+00]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_div_phi_U.upper — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-8.92787718e-03 -1.87462300e-01  0.00000000e+00 -8.57344168e-03 ... -4.08794233e+01 -9.75618397e+00  0.00000000e+00  0.00000000e+00]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_div_phi_U.lower — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[  0.           0.          -0.18708926   0.         ...   0.           0.         -21.76409186 -53.67576057]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_div_phi_U.diag coefficient distribution\n", + " n=283,400 finite, min=-7.43849e-15, max=1639.56, mean=82.3761, std=224.185\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_div_phi_U.source distribution\n", + " n=850,200 finite, min=-2.83813, max=2.83813, mean=1.29282e-19, std=0.0453185\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_div_phi_U.upper coefficient distribution\n", + " n=565,419 finite, min=-1631.95, max=0, mean=-41.2645, std=161.165\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_div_phi_U.lower coefficient distribution\n", + " n=565,419 finite, min=-53.6758, max=0, mean=-0.0240981, std=0.278605\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Momentum term `assemble_momentum_divDevSigma` — `momentumTransport->divDevSigma(U)`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_divDevSigma.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[4.38031234e+00 4.24314830e+00 4.10839121e+00 3.97601895e+00 ... 1.17597557e-04 1.16564118e-04 1.15567290e-04 1.14997131e-04]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_divDevSigma.source — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[ 3.44131100e-18 -4.72770839e-16  4.64136331e-19]\n",
+       " [-6.78603060e-19 -2.03523451e-17 -4.88457210e-19]\n",
+       " [ 5.18358783e-17  3.43732527e-16 -9.50705393e-19]\n",
+       " [-1.83101493e-17  5.34586731e-16  5.21049275e-19]\n",
+       " ...\n",
+       " [-3.00758949e-20  2.36330044e-20 -3.69113144e-20]\n",
+       " [-4.27221240e-20  1.11161689e-19  6.85057839e-20]\n",
+       " [ 1.85703851e-20 -3.45231393e-19 -1.88776643e-22]\n",
+       " [ 9.30099529e-21  1.46540804e-19 -1.26750121e-21]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_divDevSigma.upper — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-1.11501221e-10 -2.16066526e+00 -2.21964708e+00 -1.14931800e-10 ... -5.69477417e-05 -5.64008384e-05 -5.58727140e-05 -5.53628040e-05]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_divDevSigma.diag coefficient distribution\n", + " n=283,400 finite, min=4.22094e-05, max=4.38031, mean=0.0174517, std=0.138364\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_divDevSigma.source distribution\n", + " n=850,200 finite, min=-0.23184, max=0.10648, mean=-0.000101892, std=0.00384959\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_divDevSigma.upper coefficient distribution\n", + " n=565,419 finite, min=-2.21965, max=-1.11501e-10, mean=-0.00437359, std=0.0491913\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Momentum term `assemble_momentum_sources` — `fvModels().source(U)`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_sources.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[0. 0. 0. 0. ... 0. 0. 0. 0.]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
term assemble_momentum_sources.source — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " ...\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_sources.diag coefficient distribution\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_sources.source distribution\n", + " n=850,200 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Momentum term `assemble_momentum_MRF_DDt` — `MRF.DDt(U)`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
MRFZoneList:DDt.internal — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " ...\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " MRFZoneList:DDt component 0 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " MRFZoneList:DDt component 1 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " MRFZoneList:DDt component 2 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " MRFZoneList:DDt internal vector/tensor magnitude\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_MRF_DDt: field mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0, 0]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Momentum term `assemble_momentum_pressure_rhs_grad_p` — `-fvc::grad(p)`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
grad(p).internal — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " ...\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]\n",
+       " [0. 0. 0.]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " grad(p) component 0 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " grad(p) component 1 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " grad(p) component 2 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " grad(p) internal vector/tensor magnitude\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " term assemble_momentum_pressure_rhs_grad_p: field mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0, 0]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for term in terms:\n", + " display(Markdown(f\"#### Momentum term `{term['name']}` — `{term['kind']}`\"))\n", + " if \"matrix\" in term:\n", + " show_matrix_arrays(f\"term {term['name']}\", term[\"matrix\"], open_arrays=False)\n", + " else:\n", + " field = term[\"field\"]\n", + " show_field_histograms(field)\n", + " if np.asarray(field.internal).shape[0] == mesh.n_cells:\n", + " show_cell_field_map(mesh, field, title=f\"term {term['name']}: field mapped on cell centres\")\n", + " elif np.asarray(field.internal).shape[0] == mesh.n_internal_faces:\n", + " show_face_field_map(mesh, field, title=f\"term {term['name']}: field mapped on internal face centres\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-32", + "metadata": {}, + "source": [ + "## 8. Assemble, relax, and constrain `UEqn`\n", + "\n", + "`UEqn` is a `MatrixView` over an OpenFOAM `fvVectorMatrix`:\n", + "\n", + "- `diag`: one diagonal coefficient per cell;\n", + "- `upper`/`lower`: off-diagonal coefficients for internal owner-neighbour faces;\n", + "- `source`: vector right-hand side, one 3-vector per cell;\n", + "- boundary coefficient lists carry patch contributions.\n", + "\n", + "Relaxation and constraints mutate the matrix before the linear solver sees it.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "full-detail-33", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:19.706933Z", + "iopub.status.busy": "2026-07-22T20:00:19.706754Z", + "iopub.status.idle": "2026-07-22T20:00:20.047584Z", + "shell.execute_reply": "2026-07-22T20:00:20.046319Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**TransformResult: assemble_UEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| assemble_UEqn | momentum | — | p, U, phi, nut, k, omega | UEqn | OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C:36-44::momentumPredictor |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: relax_UEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| relax_UEqn | momentum | — | — | UEqn | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: constrain_UEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| constrain_UEqn | momentum | — | — | UEqn | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**UEqn MatrixView summaries**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| label | field | rank | dimensions | diag shape | diag min | diag max | source shape | source l2 | upper | lower | diagonal | symmetric | asymmetric |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| assembled | U | vector | [0 4 -2 0 0 0 0] | (283400,) | 4.221e-05 | 1640 | (283400, 3) | 42.07 | (565419,) | (565419,) | False | False | True |\n", + "| relaxed | U | vector | [0 4 -2 0 0 0 0] | (283400,) | 0.0006976 | 1829 | (283400, 3) | 1.329e+06 | (565419,) | (565419,) | False | False | True |\n", + "| constrained | U | vector | [0 4 -2 0 0 0 0] | (283400,) | 0.0006976 | 1829 | (283400, 3) | 1.329e+06 | (565419,) | (565419,) | False | False | True |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**How relaxation/constraints changed UEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| change | diag max_abs | diag l2 | source max_abs | source l2 |\n", + "| --- | --- | --- | --- | --- |\n", + "| relaxed - assembled | 199.4 | 1.426e+04 | 1.855e+04 | 1.329e+06 |\n", + "| constrained - relaxed | 0 | 0 | 0 | 0 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "UEqn_assembled_result = record(stepper.assemble_momentum_matrix())\n", + "UEqn_assembled = UEqn_assembled_result.outputs[\"UEqn\"]\n", + "UEqn_relaxed_result = record(stepper.relax_matrix())\n", + "UEqn_relaxed = UEqn_relaxed_result.outputs[\"UEqn\"]\n", + "UEqn_constrained_result = record(stepper.constrain_matrix())\n", + "UEqn_constrained = UEqn_constrained_result.outputs[\"UEqn\"]\n", + "\n", + "show_table(\n", + " \"UEqn MatrixView summaries\",\n", + " [\"label\", \"field\", \"rank\", \"dimensions\", \"diag shape\", \"diag min\", \"diag max\", \"source shape\", \"source l2\", \"upper\", \"lower\", \"diagonal\", \"symmetric\", \"asymmetric\"],\n", + " [\n", + " matrix_summary_row(\"assembled\", UEqn_assembled),\n", + " matrix_summary_row(\"relaxed\", UEqn_relaxed),\n", + " matrix_summary_row(\"constrained\", UEqn_constrained),\n", + " ],\n", + ")\n", + "show_table(\n", + " \"How relaxation/constraints changed UEqn\",\n", + " [\"change\", \"diag max_abs\", \"diag l2\", \"source max_abs\", \"source l2\"],\n", + " [\n", + " matrix_delta_row(\"relaxed - assembled\", UEqn_assembled, UEqn_relaxed),\n", + " matrix_delta_row(\"constrained - relaxed\", UEqn_relaxed, UEqn_constrained),\n", + " ],\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-34", + "metadata": {}, + "source": [ + "### Full-shape `UEqn` coefficient arrays and sparse graph view\n", + "\n", + "The `UEqn` panels expose diagonal, source, and off-diagonal arrays before and after relaxation/constraints. The sparse-graph density plot bins the full owner-neighbour addressing used by `upper` and `lower` coefficients.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "full-detail-35", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### UEqn assembled" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn assembled.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[  4.5674016    4.42611215   4.27885859   4.13485536 ... 378.66817863 373.60544841 399.9695021  436.37660966]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn assembled.source — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[-7.05164602e-13 -4.81498412e-15  4.64136331e-19]\n",
+       " [-1.27219738e-13 -4.18716250e-14 -4.88457210e-19]\n",
+       " [ 6.58327899e-13  4.20908010e-14 -9.50705393e-19]\n",
+       " [ 4.47281764e-14  3.75438178e-14  5.21049275e-19]\n",
+       " ...\n",
+       " [ 2.60823558e-12 -1.30219968e-13 -3.69113144e-20]\n",
+       " [ 2.62017204e-13  2.56322331e-14  6.85057839e-20]\n",
+       " [ 1.78859212e-12  1.15501622e-13 -1.88776643e-22]\n",
+       " [ 6.98628874e-13 -1.22370583e-13 -1.26750121e-21]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn assembled.upper — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-8.92787729e-03 -2.34812756e+00 -2.21964708e+00 -8.57344179e-03 ... -4.08794802e+01 -9.75624037e+00 -5.58727140e-05 -5.53628040e-05]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn assembled.lower — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-1.11501221e-10 -2.16066526e+00 -2.40673634e+00 -1.14931800e-10 ... -5.69477417e-05 -5.64008384e-05 -2.17641477e+01 -5.36758159e+01]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn assembled.diag coefficient distribution\n", + " n=283,400 finite, min=4.22094e-05, max=1639.56, mean=82.3935, std=224.179\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn assembled.source distribution\n", + " n=850,200 finite, min=-2.84873, max=2.83447, mean=-0.000101892, std=0.0456304\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn assembled.upper coefficient distribution\n", + " n=565,419 finite, min=-1631.95, max=-1.43895e-07, mean=-41.2689, std=161.164\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn assembled.lower coefficient distribution\n", + " n=565,419 finite, min=-53.6758, max=-1.11501e-10, mean=-0.0284717, std=0.283\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### UEqn relaxed" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn relaxed.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[  5.07593427   4.91790239   4.75428732   4.59428374 ... 420.7479423  415.15891126 444.65102908 485.47654639]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn relaxed.source — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[ 4.72981890e+01  3.13325084e+00  4.64136331e-19]\n",
+       " [ 4.57409897e+01  3.03009475e+00 -4.88457210e-19]\n",
+       " [ 4.42192199e+01  2.92928568e+00 -9.50705393e-19]\n",
+       " [ 4.27310402e+01  2.83070177e+00  5.21049275e-19]\n",
+       " ...\n",
+       " [ 3.91380284e+03  2.59268405e+02 -3.69113144e-20]\n",
+       " [ 3.86485205e+03  2.56025678e+02  6.85057839e-20]\n",
+       " [ 4.15579062e+03  2.75298795e+02 -1.88776643e-22]\n",
+       " [ 4.56674314e+03  3.02522190e+02 -1.26750121e-21]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn relaxed.upper — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-8.92787729e-03 -2.34812756e+00 -2.21964708e+00 -8.57344179e-03 ... -4.08794802e+01 -9.75624037e+00 -5.58727140e-05 -5.53628040e-05]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn relaxed.lower — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-1.11501221e-10 -2.16066526e+00 -2.40673634e+00 -1.14931800e-10 ... -5.69477417e-05 -5.64008384e-05 -2.17641477e+01 -5.36758159e+01]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn relaxed.diag coefficient distribution\n", + " n=283,400 finite, min=0.000697607, max=1828.85, mean=91.5997, std=249.292\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn relaxed.source distribution\n", + " n=850,200 finite, min=-1.62897, max=18549.2, mean=304.325, std=1408.6\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn relaxed.upper coefficient distribution\n", + " n=565,419 finite, min=-1631.95, max=-1.43895e-07, mean=-41.2689, std=161.164\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn relaxed.lower coefficient distribution\n", + " n=565,419 finite, min=-53.6758, max=-1.11501e-10, mean=-0.0284717, std=0.283\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### UEqn constrained" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn constrained.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[  5.07593427   4.91790239   4.75428732   4.59428374 ... 420.7479423  415.15891126 444.65102908 485.47654639]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn constrained.source — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[ 4.72981890e+01  3.13325084e+00  4.64136331e-19]\n",
+       " [ 4.57409897e+01  3.03009475e+00 -4.88457210e-19]\n",
+       " [ 4.42192199e+01  2.92928568e+00 -9.50705393e-19]\n",
+       " [ 4.27310402e+01  2.83070177e+00  5.21049275e-19]\n",
+       " ...\n",
+       " [ 3.91380284e+03  2.59268405e+02 -3.69113144e-20]\n",
+       " [ 3.86485205e+03  2.56025678e+02  6.85057839e-20]\n",
+       " [ 4.15579062e+03  2.75298795e+02 -1.88776643e-22]\n",
+       " [ 4.56674314e+03  3.02522190e+02 -1.26750121e-21]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn constrained.upper — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-8.92787729e-03 -2.34812756e+00 -2.21964708e+00 -8.57344179e-03 ... -4.08794802e+01 -9.75624037e+00 -5.58727140e-05 -5.53628040e-05]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
UEqn constrained.lower — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-1.11501221e-10 -2.16066526e+00 -2.40673634e+00 -1.14931800e-10 ... -5.69477417e-05 -5.64008384e-05 -2.17641477e+01 -5.36758159e+01]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn constrained.diag coefficient distribution\n", + " n=283,400 finite, min=0.000697607, max=1828.85, mean=91.5997, std=249.292\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn constrained.source distribution\n", + " n=850,200 finite, min=-1.62897, max=18549.2, mean=304.325, std=1408.6\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn constrained.upper coefficient distribution\n", + " n=565,419 finite, min=-1631.95, max=-1.43895e-07, mean=-41.2689, std=161.164\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn constrained.lower coefficient distribution\n", + " n=565,419 finite, min=-53.6758, max=-1.11501e-10, mean=-0.0284717, std=0.283\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " UEqn uses this complete owner-neighbour sparse graph\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=log1p(count); occupied bins=360\n", + " x=[0, 283398], y=[1, 283399]\n", + " raw color range=[1, 5749]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for label, matrix in [\n", + " (\"UEqn assembled\", UEqn_assembled),\n", + " (\"UEqn relaxed\", UEqn_relaxed),\n", + " (\"UEqn constrained\", UEqn_constrained),\n", + "]:\n", + " display(Markdown(f\"#### {label}\"))\n", + " show_matrix_arrays(label, matrix, open_arrays=False)\n", + "show_ldu_density(mesh, \"UEqn uses this complete owner-neighbour sparse graph\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-36", + "metadata": {}, + "source": [ + "## 9. Solve the momentum predictor\n", + "\n", + "The velocity solve updates `U` but it does not by itself guarantee continuity. It is a momentum prediction using the current pressure field. The following pressure correction will adjust `p`, `phi`, and `U` to enforce mass conservation more tightly.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "full-detail-37", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:20.066676Z", + "iopub.status.busy": "2026-07-22T20:00:20.066493Z", + "iopub.status.idle": "2026-07-22T20:00:20.363147Z", + "shell.execute_reply": "2026-07-22T20:00:20.362713Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DILUPBiCGStab: Solving for Ux, Initial residual = 0.99999991606780358, Final residual = 6.4677780734704781e-09, No Iterations 11\n", + "DILUPBiCGStab: Solving for Uy, Initial residual = 0.99999968448032772, Final residual = 7.4359526559359641e-09, No Iterations 11\n" + ] + }, + { + "data": { + "text/markdown": [ + "**TransformResult: solve_UEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| solve_UEqn | momentum | U | U, UEqn | performance, matrix_before, field_before, field_after | OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C:50-55::momentumPredictor |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Momentum linear-solver performance**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | solver | initial residual | final residual | iterations | converged | singular |\n", + "| --- | --- | --- | --- | --- | --- | --- |\n", + "| U | DILUPBiCGStab | (0.9999999160678036, 0.9999996844803277, 0.0) | (6.467778073470478e-09, 7.435952655935964e-09, 0.0) | (11, 11, 0) | False | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Field deltas caused by solve_UEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | shape | min Δ | max Δ | mean Δ | l2 Δ | max \\|Δ\\| |\n", + "| --- | --- | --- | --- | --- | --- | --- |\n", + "| U | (283400, 3) | -107.7 | 1.559 | -0.2652 | 2940 | 107.7 |\n", + "| p | (283400,) | 0 | 0 | 0 | 0 | 0 |\n", + "| phi | (565419,) | 0 | 0 | 0 | 0 | 0 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pre_momentum_snapshot = field_snapshot(stepper.fields(), names=(\"U\", \"p\", \"phi\"))\n", + "solve_U_result = record(stepper.solve_momentum())\n", + "post_momentum_snapshot = field_snapshot(stepper.fields(), names=(\"U\", \"p\", \"phi\"))\n", + "\n", + "show_table(\n", + " \"Momentum linear-solver performance\",\n", + " [\"field\", \"solver\", \"initial residual\", \"final residual\", \"iterations\", \"converged\", \"singular\"],\n", + " [solve_result_row(solve_U_result.outputs[\"performance\"])],\n", + ")\n", + "show_table(\n", + " \"Field deltas caused by solve_UEqn\",\n", + " [\"field\", \"shape\", \"min Δ\", \"max Δ\", \"mean Δ\", \"l2 Δ\", \"max |Δ|\"],\n", + " delta_rows(pre_momentum_snapshot, post_momentum_snapshot, names=(\"U\", \"p\", \"phi\")),\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-38", + "metadata": {}, + "source": [ + "## 10. Pressure-correction inputs\n", + "\n", + "SIMPLE pressure correction uses quantities derived from the momentum matrix:\n", + "\n", + "- `rAU`: reciprocal diagonal of the velocity matrix;\n", + "- `HbyA`: non-pressure part of the predicted velocity;\n", + "- `phiHbyA`: face flux implied by `HbyA`;\n", + "- `rAtU`: SIMPLEC-consistent reciprocal coefficient.\n", + "\n", + "These are fields, not just temporary scalars, so we can inspect their shapes and magnitudes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "full-detail-39", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:20.371643Z", + "iopub.status.busy": "2026-07-22T20:00:20.371435Z", + "iopub.status.idle": "2026-07-22T20:00:20.492873Z", + "shell.execute_reply": "2026-07-22T20:00:20.491123Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**TransformResult: compute_pressure_inputs**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| compute_pressure_inputs | pressure | — | — | rAU, HbyA, phiHbyA, rAtU, adjusted_phi_for_pressure_reference, consistent, source | OpenFOAM-14/applications/modules/incompressibleFluid/correctPressure.C:47-81::Foam::solvers::incompressibleFluid::correctPressure |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Pressure-input fields**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | kind | dimensions | entity | count | shape | min | max | mean |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| (1\\|A(U)) | volScalar | [0 0 1 0 0 0 0] | cell | 283400 | (283400,) | 1.388e-08 | 0.1278 | 0.002038 |\n", + "| HbyA | volVector | [0 1 -1 0 0 0 0] | cell | 283400 | (283400, 3) | -14.68 | 93.01 | 32.79 |\n", + "| phiHbyA | surfaceScalar | [0 3 -1 0 0 0 0] | internal_face | 565419 | (565419,) | -1632 | 53.68 | -41.24 |\n", + "| (1\\|max(((1\\|(1\\|A(U)))-H(1)),(0.1\\|(1\\|A(U))))) | volScalar | [0 0 1 0 0 0 0] | cell | 283400 | (283400,) | 6.007e-08 | 1.191 | 0.02022 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Pressure-input switches**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| key | value | meaning |\n", + "| --- | --- | --- |\n", + "| consistent | True | SIMPLEC correction path is active |\n", + "| adjusted_phi_for_pressure_reference | False | whether flux was adjusted for pressure reference |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pressure_inputs_result = record(stepper.compute_pressure_inputs())\n", + "pressure_inputs = pressure_inputs_result.outputs\n", + "pressure_input_fields = [key for key in (\"rAU\", \"HbyA\", \"phiHbyA\", \"rAtU\") if key in pressure_inputs]\n", + "show_table(\n", + " \"Pressure-input fields\",\n", + " [\"name\", \"kind\", \"dimensions\", \"entity\", \"count\", \"shape\", \"min\", \"max\", \"mean\"],\n", + " [field_summary_row(pressure_inputs[name]) for name in pressure_input_fields],\n", + ")\n", + "show_table(\n", + " \"Pressure-input switches\",\n", + " [\"key\", \"value\", \"meaning\"],\n", + " [\n", + " [\"consistent\", pressure_inputs.get(\"consistent\"), \"SIMPLEC correction path is active\"],\n", + " [\"adjusted_phi_for_pressure_reference\", pressure_inputs.get(\"adjusted_phi_for_pressure_reference\"), \"whether flux was adjusted for pressure reference\"],\n", + " ],\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-40", + "metadata": {}, + "source": [ + "### Full-shape pressure-input arrays and maps\n", + "\n", + "`rAU`, `HbyA`, `phiHbyA`, and `rAtU` are full OpenFOAM fields. The plots below show their distributions and geometry-aligned binned maps.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "full-detail-41", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### Pressure input `rAU`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
(1|A(U)).internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[0.00026756 0.00024709 0.00022836 0.00021125 ... 0.12406799 0.12781799 0.12135996 0.11307441]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " (1|A(U)) internal values\n", + " n=283,400 finite, min=1.38844e-08, max=0.127818, mean=0.00203795, std=0.00809275\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " rAU: complete pressure input mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[2.86833e-05, 0.127818]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Pressure input `HbyA`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
HbyA.internal — shape=(283400, 3) (283,400 × 3), dtype=float64, size=850,200, TRUNCATED value text; exact shape retained; size=850,200; threshold=256; edgeitems=4
[[93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " ...\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]\n",
+       " [93.00914504  6.16135601  0.        ]]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " HbyA component 0 internal values\n", + " n=283,400 finite, min=-14.6831, max=93.0091, mean=92.2819, std=5.44834\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " HbyA component 1 internal values\n", + " n=283,400 finite, min=-4.63575, max=7.72001, mean=6.09312, std=0.534427\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " HbyA component 2 internal values\n", + " n=283,400 finite, min=-0.5, max=0.5, mean=0, std=0\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " HbyA internal vector/tensor magnitude\n", + " n=283,400 finite, min=2.35378, max=93.213, mean=92.4988, std=5.19794\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " HbyA: complete pressure input mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[92.0268, 93.213]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Pressure input `phiHbyA`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
phiHbyA.internal — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[-8.92787718e-03 -1.87462300e-01  1.87089263e-01 -8.57344168e-03 ... -4.08794233e+01 -9.75618397e+00  2.17640919e+01  5.36757606e+01]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " phiHbyA internal values\n", + " n=565,419 finite, min=-1631.95, max=53.6758, mean=-41.2405, std=161.171\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " phiHbyA: complete pressure input mapped on internal face centres\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=bin mean; occupied bins=7,291\n", + " x=[-193.023, 197.582], y=[-194.361, 195.068]\n", + " raw color range=[-1631.79, 53.6758]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Pressure input `rAtU`" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
(1|max(((1|(1|A(U)))-H(1)),(0.1|(1|A(U))))).internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[0.00267559 0.00247088 0.00228365 0.00211253 ... 0.12701319 0.12781802 0.12760753 0.12713983]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " (1|max(((1|(1|A(U)))-H(1)),(0.1|(1|A(U))))) internal values\n", + " n=283,400 finite, min=6.00735e-08, max=1.19133, mean=0.0202164, std=0.0802763\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " rAtU: complete pressure input mapped on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0.000286831, 1.19133]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for name in pressure_input_fields:\n", + " field = pressure_inputs[name]\n", + " display(Markdown(f\"#### Pressure input `{name}`\"))\n", + " show_field_histograms(field)\n", + " if np.asarray(field.internal).shape[0] == mesh.n_cells:\n", + " show_cell_field_map(mesh, field, title=f\"{name}: complete pressure input mapped on cell centres\")\n", + " elif np.asarray(field.internal).shape[0] == mesh.n_internal_faces:\n", + " show_face_field_map(mesh, field, title=f\"{name}: complete pressure input mapped on internal face centres\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-42", + "metadata": {}, + "source": [ + "## 11. Assemble and solve `pEqn`\n", + "\n", + "The pressure equation is a scalar sparse matrix. Solving it updates pressure and the face flux `phi`. In this case `nNonOrthogonalCorrectors = 3`, so OpenFOAM performs multiple pressure solves inside `solve_pressure()`; the returned `SolveResult` is the final one.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "full-detail-43", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:20.500348Z", + "iopub.status.busy": "2026-07-22T20:00:20.500108Z", + "iopub.status.idle": "2026-07-22T20:00:23.601973Z", + "shell.execute_reply": "2026-07-22T20:00:23.601600Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**TransformResult: assemble_pEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| assemble_pEqn | pressure | — | — | pEqn | OpenFOAM-14/applications/modules/incompressibleFluid/correctPressure.C:83-101::correctPressure |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**pEqn MatrixView summary**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| label | field | rank | dimensions | diag shape | diag min | diag max | source shape | source l2 | upper | lower | diagonal | symmetric | asymmetric |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| assembled | p | scalar | [0 3 -1 0 0 0 0] | (283400,) | -8244 | -4.016e-07 | (283400,) | 0.4615 | (565419,) | | False | True | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DICPCG: Solving for p, Initial residual = 1, Final residual = 0.030890897351242643, No Iterations 300\n" + ] + }, + { + "data": { + "text/markdown": [ + "**TransformResult: solve_pEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DICPCG: Solving for p, Initial residual = 7.2784937401803668e-05, Final residual = 9.4967147297221812e-07, No Iterations 199\n", + "DICPCG: Solving for p, Initial residual = 1.9461086386417007e-05, Final residual = 9.8677378665869984e-07, No Iterations 160\n", + "DICPCG: Solving for p, Initial residual = 8.7397651610059266e-06, Final residual = 9.9577380572382355e-07, No Iterations 125\n" + ] + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| solve_pEqn | pressure | p, phi | p, pEqn | performance, matrix_before, field_before, p, phi | OpenFOAM-14/applications/modules/incompressibleFluid/correctPressure.C:103-108::correctPressure |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Pressure linear-solver performance, final non-orthogonal solve**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | solver | initial residual | final residual | iterations | converged | singular |\n", + "| --- | --- | --- | --- | --- | --- | --- |\n", + "| p | DICPCG | 8.74e-06 | 9.958e-07 | 125 | True | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Field deltas caused by solve_pEqn**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | shape | min Δ | max Δ | mean Δ | l2 Δ | max \\|Δ\\| |\n", + "| --- | --- | --- | --- | --- | --- | --- |\n", + "| U | (283400, 3) | 0 | 0 | 0 | 0 | 0 |\n", + "| p | (283400,) | -2.074e+04 | 1.393e+05 | 7713 | 1.433e+07 | 1.393e+05 |\n", + "| phi | (565419,) | -0.12 | 0.07011 | -0.001635 | 16.37 | 0.12 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pEqn_result = record(stepper.assemble_pressure_matrix())\n", + "pEqn = pEqn_result.outputs[\"pEqn\"]\n", + "show_table(\n", + " \"pEqn MatrixView summary\",\n", + " [\"label\", \"field\", \"rank\", \"dimensions\", \"diag shape\", \"diag min\", \"diag max\", \"source shape\", \"source l2\", \"upper\", \"lower\", \"diagonal\", \"symmetric\", \"asymmetric\"],\n", + " [matrix_summary_row(\"assembled\", pEqn)],\n", + ")\n", + "\n", + "pre_pressure_snapshot = field_snapshot(stepper.fields(), names=(\"U\", \"p\", \"phi\"))\n", + "solve_p_result = record(stepper.solve_pressure())\n", + "post_pressure_snapshot = field_snapshot(stepper.fields(), names=(\"U\", \"p\", \"phi\"))\n", + "\n", + "show_table(\n", + " \"Pressure linear-solver performance, final non-orthogonal solve\",\n", + " [\"field\", \"solver\", \"initial residual\", \"final residual\", \"iterations\", \"converged\", \"singular\"],\n", + " [solve_result_row(solve_p_result.outputs[\"performance\"])],\n", + ")\n", + "show_table(\n", + " \"Field deltas caused by solve_pEqn\",\n", + " [\"field\", \"shape\", \"min Δ\", \"max Δ\", \"mean Δ\", \"l2 Δ\", \"max |Δ|\"],\n", + " delta_rows(pre_pressure_snapshot, post_pressure_snapshot, names=(\"U\", \"p\", \"phi\")),\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-44", + "metadata": {}, + "source": [ + "### Full-shape `pEqn` arrays\n", + "\n", + "The pressure equation is scalar, but its diagonal/off-diagonal/source arrays still span the full cell graph.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "full-detail-45", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
pEqn assembled.diag — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[-768.51401632 -687.69317782 -615.60662007 -551.29512049 ...   -1.10414391   -1.10656942   -1.09019334   -1.07202383]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
pEqn assembled.source — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[ 1.02180417e-13 -7.38853423e-14 -5.16253706e-15  2.05391260e-15 ...  0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
pEqn assembled.upper — shape=(565419,) (565,419), dtype=float64, size=565,419, TRUNCATED value text; exact shape retained; size=565,419; threshold=256; edgeitems=4
[1.83571656e-08 3.87796037e+02 3.80717980e+02 1.74812696e-08 ... 4.60832668e-01 4.60561132e-01 4.57325218e-01 4.51951588e-01]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " pEqn assembled.diag coefficient distribution\n", + " n=283,400 finite, min=-8243.87, max=-4.01646e-07, mean=-25.5789, std=238.124\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " pEqn assembled.source distribution\n", + " n=283,400 finite, min=-0.031998, max=0.0169861, mean=1.13639e-17, std=0.000866946\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " pEqn assembled.upper coefficient distribution\n", + " n=565,419 finite, min=3.33494e-11, max=4122.53, mean=6.41034, std=84.5465\n", + " x=value, y=log1p(count), bins=90\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " pEqn uses this complete owner-neighbour sparse graph\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=log1p(count); occupied bins=360\n", + " x=[0, 283398], y=[1, 283399]\n", + " raw color range=[1, 5749]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |ΔU| spatial distribution\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean |Δ|; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0, 0]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |Δp| spatial distribution\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean |Δ|; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0.000362124, 18846.4]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |Δphi| spatial distribution\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=bin mean |Δ|; occupied bins=7,291\n", + " x=[-193.023, 197.582], y=[-194.361, 195.068]\n", + " raw color range=[7.31074e-07, 0.0709497]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "show_matrix_arrays(\"pEqn assembled\", pEqn, open_arrays=False)\n", + "show_ldu_density(mesh, \"pEqn uses this complete owner-neighbour sparse graph\")\n", + "for name in (\"U\", \"p\", \"phi\"):\n", + " show_delta_map(mesh, name, pre_pressure_snapshot, post_pressure_snapshot)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-46", + "metadata": {}, + "source": [ + "## 12. Correct velocity, pressure, and flux\n", + "\n", + "After solving pressure, OpenFOAM performs the final pressure/velocity/flux correction. This is where `U`, `p`, and `phi` are made consistent with the pressure equation products.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "full-detail-47", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:23.608612Z", + "iopub.status.busy": "2026-07-22T20:00:23.608438Z", + "iopub.status.idle": "2026-07-22T20:00:23.673668Z", + "shell.execute_reply": "2026-07-22T20:00:23.672454Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "time step continuity errors : sum local = 7.4676177118375265e-06, global = 9.9407679212193635e-07\n" + ] + }, + { + "data": { + "text/markdown": [ + "**TransformResult: correct_velocity_pressure_flux**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| correct_velocity_pressure_flux | pressure | p, U, phi | — | p, U, phi, source | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Field deltas caused by correct_velocity_pressure_flux**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | shape | min Δ | max Δ | mean Δ | l2 Δ | max \\|Δ\\| |\n", + "| --- | --- | --- | --- | --- | --- | --- |\n", + "| U | (283400, 3) | -142.8 | 59.18 | -2.593 | 1.342e+04 | 142.8 |\n", + "| p | (283400,) | 0 | 0 | 0 | 0 | 0 |\n", + "| phi | (565419,) | 0 | 0 | 0 | 0 | 0 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Boundary flux after correction**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| patch | phi BC type | shape | sum(phi) | max \\|phi\\| |\n", + "| --- | --- | --- | --- | --- |\n", + "| aerofoil | calculated | (1026,) | 0 | 0 |\n", + "| freestream | calculated | (1736,) | 0.142 | 1034 |\n", + "| frontAndBack | empty | (0,) | 0 | 0 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pre_correction_snapshot = field_snapshot(stepper.fields(), names=(\"U\", \"p\", \"phi\"))\n", + "correction_result = record(stepper.correct_velocity_pressure_flux())\n", + "post_correction_snapshot = field_snapshot(stepper.fields(), names=(\"U\", \"p\", \"phi\"))\n", + "\n", + "show_table(\n", + " \"Field deltas caused by correct_velocity_pressure_flux\",\n", + " [\"field\", \"shape\", \"min Δ\", \"max Δ\", \"mean Δ\", \"l2 Δ\", \"max |Δ|\"],\n", + " delta_rows(pre_correction_snapshot, post_correction_snapshot, names=(\"U\", \"p\", \"phi\")),\n", + ")\n", + "\n", + "corrected_fields = stepper.fields()\n", + "flux_rows = []\n", + "for patch_name, patch_field in corrected_fields[\"phi\"].boundary.items():\n", + " values = np.asarray(patch_field.values)\n", + " flux_rows.append([patch_name, patch_field.type, values.shape, float(values.sum()) if values.size else 0.0, float(np.max(np.abs(values))) if values.size else 0.0])\n", + "show_table(\"Boundary flux after correction\", [\"patch\", \"phi BC type\", \"shape\", \"sum(phi)\", \"max |phi|\"], flux_rows)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-48", + "metadata": {}, + "source": [ + "### Spatial view of the final pressure/velocity/flux correction\n", + "\n", + "These heatmaps use the complete delta arrays from `correct_velocity_pressure_flux()` and bin them by cell or face centre.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "full-detail-49", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |ΔU| spatial distribution\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean |Δ|; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[3.80655e-05, 19.7646]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |Δp| spatial distribution\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean |Δ|; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0, 0]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |Δphi| spatial distribution\n", + " \n", + " \n", + " points=565,419; bins=96×96; color=bin mean |Δ|; occupied bins=7,291\n", + " x=[-193.023, 197.582], y=[-194.361, 195.068]\n", + " raw color range=[0, 0]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for name in (\"U\", \"p\", \"phi\"):\n", + " show_delta_map(mesh, name, pre_correction_snapshot, post_correction_snapshot)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-50", + "metadata": {}, + "source": [ + "## 13. Correct the turbulence model\n", + "\n", + "The k-omega SST model updates turbulence state after the pressure/velocity correction. Here the wrapper exposes the resulting fields (`nut`, `k`, `omega`) rather than splitting the turbulence equations into their own matrices. That is still enough to see how turbulence state changes during one iteration.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "full-detail-51", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:23.675514Z", + "iopub.status.busy": "2026-07-22T20:00:23.675348Z", + "iopub.status.idle": "2026-07-22T20:00:24.126363Z", + "shell.execute_reply": "2026-07-22T20:00:24.119094Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DILUPBiCGStab: Solving for omega, Initial residual = " + ] + }, + { + "data": { + "text/markdown": [ + "**TransformResult: momentum_transport_correct**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.00071906933244685197, Final residual = 1.3458793712123469e-09, No Iterations 3\n", + "DILUPBiCGStab: Solving for k, Initial residual = 0.99999999999345446, Final residual = 6.2532997654966409e-09, No Iterations 7\n" + ] + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| momentum_transport_correct | momentum | viscosity, momentumTransport | — | p, U, phi, nut, k, omega | OpenFOAM-14/applications/modules/incompressibleFluid/incompressibleFluid.C:233-237::momentumTransportCorrector |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Field deltas caused by momentum_transport_correct**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | shape | min Δ | max Δ | mean Δ | l2 Δ | max \\|Δ\\| |\n", + "| --- | --- | --- | --- | --- | --- | --- |\n", + "| nut | (283400,) | -3.12e-09 | 1.597e-10 | -1.502e-09 | 1.144e-06 | 3.12e-09 |\n", + "| k | (283400,) | -2.202e-09 | 2.902e-08 | 1.588e-09 | 3.252e-06 | 2.902e-08 |\n", + "| omega | (283400,) | -0.06781 | 1.718e+09 | 1.314e+07 | 5.498e+10 | 1.718e+09 |\n", + "| U | (283400, 3) | 0 | 0 | 0 | 0 | 0 |\n", + "| p | (283400,) | 0 | 0 | 0 | 0 | 0 |\n", + "| phi | (565419,) | 0 | 0 | 0 | 0 | 0 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Turbulence fields after correction**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | kind | dimensions | entity | count | shape | min | max | mean |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| nut | volScalar | [0 2 -1 0 0 0 0] | cell | 283400 | (283400,) | 9.192e-19 | 3.28e-09 | 1.607e-09 |\n", + "| k | volScalar | [0 2 -2 0 0 0 0] | cell | 283400 | (283400,) | 1.434e-09 | 3.266e-08 | 5.224e-09 |\n", + "| omega | volScalar | [0 0 -1 0 0 0 0] | cell | 283400 | (283400,) | 1.097 | 1.718e+09 | 1.314e+07 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pre_turbulence_snapshot = field_snapshot(stepper.fields(), names=(\"nut\", \"k\", \"omega\", \"U\", \"p\", \"phi\"))\n", + "turbulence_result = record(stepper.momentum_transport_corrector())\n", + "post_turbulence_snapshot = field_snapshot(stepper.fields(), names=(\"nut\", \"k\", \"omega\", \"U\", \"p\", \"phi\"))\n", + "\n", + "show_table(\n", + " \"Field deltas caused by momentum_transport_correct\",\n", + " [\"field\", \"shape\", \"min Δ\", \"max Δ\", \"mean Δ\", \"l2 Δ\", \"max |Δ|\"],\n", + " delta_rows(pre_turbulence_snapshot, post_turbulence_snapshot, names=(\"nut\", \"k\", \"omega\", \"U\", \"p\", \"phi\")),\n", + ")\n", + "show_table(\n", + " \"Turbulence fields after correction\",\n", + " [\"name\", \"kind\", \"dimensions\", \"entity\", \"count\", \"shape\", \"min\", \"max\", \"mean\"],\n", + " [field_summary_row(stepper.fields()[name]) for name in (\"nut\", \"k\", \"omega\")],\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-52", + "metadata": {}, + "source": [ + "### Full-shape turbulence-correction outputs\n", + "\n", + "The turbulence corrector updates `nut`, `k`, and `omega`. The raw panels and maps below expose the complete post-correction fields and complete one-iteration deltas.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "full-detail-53", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### Turbulence field `nut` after correction" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
nut.internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[3.12299427e-09 3.12135019e-09 3.12274815e-09 3.12226472e-09 ... 3.11722210e-09 3.11720802e-09 3.11721107e-09 3.11721779e-09]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " nut internal values\n", + " n=283,400 finite, min=9.19177e-19, max=3.2797e-09, mean=1.60719e-09, std=1.53864e-09\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " nut: post-correction field on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[6.06727e-10, 3.13788e-09]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |Δnut| spatial distribution\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean |Δ|; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[3.99598e-14, 2.49499e-09]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Turbulence field `k` after correction" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
k.internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[3.64046932e-09 3.63712021e-09 3.64010971e-09 3.63917170e-09 ... 3.58815537e-09 3.58786899e-09 3.58793063e-09 3.58806572e-09]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " k internal values\n", + " n=283,400 finite, min=1.43373e-09, max=3.26561e-08, mean=5.22354e-09, std=5.89869e-09\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " k: post-correction field on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[3.41962e-09, 6.28036e-09]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |Δk| spatial distribution\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean |Δ|; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[5.45733e-13, 2.6708e-09]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "#### Turbulence field `omega` after correction" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
omega.internal — shape=(283400,) (283,400), dtype=float64, size=283,400, TRUNCATED value text; exact shape retained; size=283,400; threshold=256; edgeitems=4
[1.16569837 1.1652394  1.16567508 1.16555514 ... 1.15107466 1.15098799 1.15100664 1.15104749]
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " omega internal values\n", + " n=283,400 finite, min=1.09735, max=1.71823e+09, mean=1.31364e+07, std=1.02433e+08\n", + " x=value, y=count, bins=80\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " omega: post-correction field on cell centres\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[1.09838, 2.18167e+07]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/svg+xml": [ + "\n", + " \n", + " |Δomega| spatial distribution\n", + " \n", + " \n", + " points=283,400; bins=96×96; color=bin mean |Δ|; occupied bins=4,847\n", + " x=[-193.117, 197.702], y=[-194.298, 195.038]\n", + " raw color range=[0.000175349, 2.18167e+07]\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for name in (\"nut\", \"k\", \"omega\"):\n", + " field = stepper.fields()[name]\n", + " display(Markdown(f\"#### Turbulence field `{name}` after correction\"))\n", + " show_field_histograms(field)\n", + " show_cell_field_map(mesh, field, title=f\"{name}: post-correction field on cell centres\")\n", + " show_delta_map(mesh, name, pre_turbulence_snapshot, post_turbulence_snapshot)\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-54", + "metadata": {}, + "source": [ + "## 14. Close the iteration and view the whole transform graph\n", + "\n", + "The final housekeeping steps close the SIMPLE/PIMPLE iteration and call `postSolve(write=False)`. The table then shows the whole path this notebook executed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "full-detail-55", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:24.139309Z", + "iopub.status.busy": "2026-07-22T20:00:24.139042Z", + "iopub.status.idle": "2026-07-22T20:00:24.209193Z", + "shell.execute_reply": "2026-07-22T20:00:24.208452Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**TransformResult: end_pimple_iteration**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| end_pimple_iteration | pimple | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**TransformResult: post_solve**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| post_solve | time | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Complete executed transform graph**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| name | phase | changed | inputs | outputs | source |\n", + "| --- | --- | --- | --- | --- | --- |\n", + "| pre_solve | time | mesh, Uf | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | OpenFOAM-14/applications/modules/incompressibleFluid/incompressibleFluid.C:165-201::preSolve |\n", + "| advance_time | time | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |\n", + "| begin_pimple_iteration | pimple | — | — | active, state | native wrapper / OpenFOAM runtime |\n", + "| fv_models_correct | pimple | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |\n", + "| pre_predictor | pimple | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |\n", + "| momentum_transport_predict | momentum | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | OpenFOAM-14/applications/modules/incompressibleFluid/incompressibleFluid.C:208-211::momentumTransportPredictor |\n", + "| assemble_momentum_terms | momentum | — | p, U, phi, nut, k, omega | terms, source | OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C:36-53::Foam::solvers::incompressibleFluid::momentumPredictor |\n", + "| assemble_UEqn | momentum | — | p, U, phi, nut, k, omega | UEqn | OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C:36-44::momentumPredictor |\n", + "| relax_UEqn | momentum | — | — | UEqn | native wrapper / OpenFOAM runtime |\n", + "| constrain_UEqn | momentum | — | — | UEqn | native wrapper / OpenFOAM runtime |\n", + "| solve_UEqn | momentum | U | U, UEqn | performance, matrix_before, field_before, field_after | OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C:50-55::momentumPredictor |\n", + "| compute_pressure_inputs | pressure | — | — | rAU, HbyA, phiHbyA, rAtU, adjusted_phi_for_pressure_reference, consistent, source | OpenFOAM-14/applications/modules/incompressibleFluid/correctPressure.C:47-81::Foam::solvers::incompressibleFluid::correctPressure |\n", + "| assemble_pEqn | pressure | — | — | pEqn | OpenFOAM-14/applications/modules/incompressibleFluid/correctPressure.C:83-101::correctPressure |\n", + "| solve_pEqn | pressure | p, phi | p, pEqn | performance, matrix_before, field_before, p, phi | OpenFOAM-14/applications/modules/incompressibleFluid/correctPressure.C:103-108::correctPressure |\n", + "| correct_velocity_pressure_flux | pressure | p, U, phi | — | p, U, phi, source | native wrapper / OpenFOAM runtime |\n", + "| momentum_transport_correct | momentum | viscosity, momentumTransport | — | p, U, phi, nut, k, omega | OpenFOAM-14/applications/modules/incompressibleFluid/incompressibleFluid.C:233-237::momentumTransportCorrector |\n", + "| end_pimple_iteration | pimple | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |\n", + "| post_solve | time | — | — | case_path, solver_name, time_name, time_index, time_value, delta_t, pimple_iteration_open, has_momentum_matrix, has_pressure_inputs | native wrapper / OpenFOAM runtime |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Final solver state**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| Key | Value |\n", + "| --- | --- |\n", + "| case_path | /home/aaron/data/openFOAM-RANS-to-GPU/tmp/airfrans_stepper_full_detail_learning_tool/split_case |\n", + "| solver_name | incompressibleFluid |\n", + "| time_name | 1 |\n", + "| time_index | 1 |\n", + "| time_value | 1 |\n", + "| delta_t | 1 |\n", + "| pimple_iteration_open | False |\n", + "| has_momentum_matrix | False |\n", + "| has_pressure_inputs | False |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "record(stepper.end_pimple_iteration())\n", + "record(stepper.post_solve(write=False))\n", + "\n", + "show_table(\n", + " \"Complete executed transform graph\",\n", + " [\"name\", \"phase\", \"changed\", \"inputs\", \"outputs\", \"source\"],\n", + " [transform_row(result) for result in transform_results],\n", + ")\n", + "show_table(\"Final solver state\", [\"Key\", \"Value\"], list(stepper.state().items()))\n" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-56", + "metadata": {}, + "source": [ + "## 15. Sanity check: split walk versus high-level `run_one_pimple_iteration()`\n", + "\n", + "The educational walk above called each step explicitly. The production-style convenience call `run_one_pimple_iteration()` should produce the same `U`, `p`, and `phi` for one iteration because both paths call the same OpenFOAM operations in the same order.\n", + "\n", + "The high-level call is run in a fresh Python subprocess to avoid OpenFOAM global `jobInfo` warnings from constructing a second independent case inside the same process. The resulting arrays are saved to `tmp/` and loaded back for comparison." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "full-detail-57", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-22T20:00:24.210779Z", + "iopub.status.busy": "2026-07-22T20:00:24.210628Z", + "iopub.status.idle": "2026-07-22T20:00:30.292997Z", + "shell.execute_reply": "2026-07-22T20:00:30.292487Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "subprocess returncode= 0\n", + "high-level subprocess log tail:\n", + "_full_detail_learning_tool/high_level_case\n", + "nProcs : 1\n", + "fileModificationChecking : Monitoring run-time modified files using timeStampMaster (fileModificationSkew 10)\n", + "allowSystemOperations : Allowing user-supplied system call operations\n", + "\n", + "// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //\n", + "\n", + "Selecting viscosity model constant\n", + "\n", + "Selecting momentum transport model type RAS\n", + " Selecting RAS turbulence model kOmegaSST\n", + " bounding k, min: 0 max: 3.6353069999999998e-09 average: 3.6353069999910849e-09\n", + "\n", + "Selecting patchDistMethod meshWave\n", + "\n", + "SIMPLE: Convergence criteria found\n", + " p: tolerance 0\n", + " U: tolerance 0\n", + " nuTilda: tolerance 0\n", + " k: tolerance 0\n", + " omega: tolerance 0\n", + " h: tolerance 0\n", + "\n", + "\n", + "SIMPLE: Operating solver in steady-state mode with 1 outer corrector\n", + "SIMPLE: Operating solver in SIMPLE mode\n", + "\n", + "\n", + "DILUPBiCGStab: Solving for Ux, Initial residual = 0.99999991606780358, Final residual = 6.4677780734704781e-09, No Iterations 11\n", + "DILUPBiCGStab: Solving for Uy, Initial residual = 0.99999968448032772, Final residual = 7.4359526559359641e-09, No Iterations 11\n", + "DICPCG: Solving for p, Initial residual = 1, Final residual = 0.030890897351242643, No Iterations 300\n", + "DICPCG: Solving for p, Initial residual = 7.2784937401803668e-05, Final residual = 9.4967147297221812e-07, No Iterations 199\n", + "DICPCG: Solving for p, Initial residual = 1.9461086386417007e-05, Final residual = 9.8677378665869984e-07, No Iterations 160\n", + "DICPCG: Solving for p, Initial residual = 8.7397651610059266e-06, Final residual = 9.9577380572382355e-07, No Iterations 125\n", + "time step continuity errors : sum local = 7.4676177118375265e-06, global = 9.9407679212193635e-07, cumulative = 9.9407679212193635e-07\n", + "DILUPBiCGStab: Solving for omega, Initial residual = 0.00071906933244685197, Final residual = 1.3458793712123469e-09, No Iterations 3\n", + "DILUPBiCGStab: Solving for k, Initial residual = 0.99999999999345446, Final residual = 6.2532997654966409e-09, No Iterations 7\n", + "\n" + ] + }, + { + "data": { + "text/markdown": [ + "**Split-step parity against run_one_pimple_iteration**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| field | split shape | run_one shape | max \\|diff\\| | allclose |\n", + "| --- | --- | --- | --- | --- |\n", + "| U | (283400, 3) | (283400, 3) | 0 | True |\n", + "| p | (283400,) | (283400,) | 0 | True |\n", + "| phi | (565419,) | (565419,) | 0 | True |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "run_one graph:\n", + "['pre_solve', 'advance_time', 'begin_pimple_iteration', 'fv_models_correct', 'pre_predictor', 'momentum_transport_predict', 'assemble_UEqn', 'relax_UEqn', 'constrain_UEqn', 'solve_UEqn', 'constrain_U_after_momentum', 'compute_pressure_inputs', 'assemble_pEqn', 'solve_pEqn', 'update_phi_from_pEqn_flux', 'correct_velocity_pressure_flux', 'momentum_transport_correct', 'end_pimple_iteration', 'post_solve']\n" + ] + } + ], + "source": [ + "high_level_npz = WORK / \"high_level_fields.npz\"\n", + "subprocess_code = f\"\"\"\n", + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "os.environ.pop('PYTHONPATH', None)\n", + "root = Path({str(ROOT)!r})\n", + "sys.path.insert(0, str(root / 'scripts'))\n", + "from prepare_airfrans_stepper_case import prepare_case\n", + "from openfoam_env import apply_openfoam_env\n", + "apply_openfoam_env()\n", + "import numpy as np\n", + "import foam_stepper as foam\n", + "raw_case = Path({str(RAW_CASE)!r})\n", + "high_level_case = Path({str(HIGH_LEVEL_CASE)!r})\n", + "out = Path({str(high_level_npz)!r})\n", + "prepare_case(raw_case, high_level_case, end_time=1)\n", + "result = foam.Case(high_level_case).make_stepper().run_one_pimple_iteration()\n", + "fields = result.outputs['fields']\n", + "np.savez(\n", + " out,\n", + " U=np.asarray(fields['U'].internal),\n", + " p=np.asarray(fields['p'].internal),\n", + " phi=np.asarray(fields['phi'].internal),\n", + " graph=np.asarray([node.name for node in result.outputs['graph']], dtype=object),\n", + ")\n", + "\"\"\"\n", + "completed = subprocess.run([str(PYTHON), \"-c\", subprocess_code], cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=600)\n", + "print(\"subprocess returncode=\", completed.returncode)\n", + "if completed.returncode != 0:\n", + " print(completed.stdout[-8000:])\n", + " raise SystemExit(completed.returncode)\n", + "print(\"high-level subprocess log tail:\")\n", + "print(completed.stdout[-2000:])\n", + "\n", + "high = np.load(high_level_npz, allow_pickle=True)\n", + "split_final_fields = stepper.fields()\n", + "\n", + "parity_rows = []\n", + "for name in (\"U\", \"p\", \"phi\"):\n", + " split_arr = np.asarray(split_final_fields[name].internal)\n", + " high_arr = np.asarray(high[name])\n", + " diff = split_arr - high_arr\n", + " parity_rows.append([\n", + " name,\n", + " split_arr.shape,\n", + " high_arr.shape,\n", + " float(np.max(np.abs(diff))) if diff.size else 0.0,\n", + " bool(np.allclose(split_arr, high_arr, rtol=1e-8, atol=1e-8)),\n", + " ])\n", + "show_table(\"Split-step parity against run_one_pimple_iteration\", [\"field\", \"split shape\", \"run_one shape\", \"max |diff|\", \"allclose\"], parity_rows)\n", + "\n", + "run_one_graph = list(high[\"graph\"])\n", + "print(\"run_one graph:\")\n", + "print(run_one_graph)\n", + "assert all(row[-1] for row in parity_rows)" + ] + }, + { + "cell_type": "markdown", + "id": "full-detail-58", + "metadata": {}, + "source": [ + "## What this one-iteration microscope teaches\n", + "\n", + "The AirfRANS simulation is not a black box once viewed through these intermediate objects:\n", + "\n", + "- The **mesh** is a finite-volume graph: cells, faces, owner/neighbour addressing, and boundary patches.\n", + "- The **fields** are typed OpenFOAM fields with dimensions, entity locations, and boundary conditions.\n", + "- The **momentum predictor** creates a vector sparse matrix `UEqn` from convection, turbulence stress, sources, and pressure-gradient terms.\n", + "- **Relaxation and constraints** alter matrix coefficients before solving.\n", + "- The **velocity solve** updates `U` but does not complete incompressibility.\n", + "- The **pressure correction** builds scalar matrix `pEqn`, updates `p` and `phi`, then corrects `U`/`p`/`phi` consistently.\n", + "- The **k-omega SST corrector** updates `k`, `omega`, and `nut`, changing the effective viscosity used by future momentum equations.\n", + "- `TransformResult.source` ties many Python-visible transitions back to specific OpenFOAM source files and functions.\n", + "\n", + "A next notebook can go deeper by exposing the turbulence equations as separate `MatrixView` objects, matching the already exposed momentum and pressure matrices.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e05b6be --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "openfoam-rans-to-gpu" +version = "0.1.0" +description = "OpenFOAM RANS-to-GPU experiments and Python stepper tooling" +requires-python = ">=3.12" +dependencies = [ + "foam-stepper", + "quadrants>=1.1.3", + "numpy>=2.0", + "pybind11>=2.13", +] + +[dependency-groups] +dev = [ + "ipykernel>=6.29", + "jupyterlab>=4.2", + "nbclient>=0.10", + "nbformat>=5.10", +] + +[tool.uv] +package = false + +[tool.uv.sources] +foam-stepper = { path = "python", editable = true } diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..046c26e --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "foam-stepper" +version = "0.2.0" +description = "Python-driven OpenFOAM observability stepper" +requires-python = ">=3.12" +dependencies = [ + "numpy>=2.0", +] + +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/python/src/foam_stepper/__init__.py b/python/src/foam_stepper/__init__.py new file mode 100644 index 0000000..2718b8d --- /dev/null +++ b/python/src/foam_stepper/__init__.py @@ -0,0 +1,205 @@ +"""Python-driven OpenFOAM observability stepper.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from ._runtime import configure_openfoam_environment + +configure_openfoam_environment() + +from . import _foam_stepper as _native +from .types import ( + PatchFieldView, + PatchView, + RaggedIntArray, + FieldRegistryView, + FieldView, + MatrixView, + MeshView, + SolveResult, + SourceLocation, + TransformResult, + _wrap_value, +) + +OpenFoamError = _native.OpenFoamError + + +def version() -> str: + return _native.version() + + +class Case: + """OpenFOAM case handle that creates Python-controlled steppers.""" + + def __init__(self, path: str | Path, solver: str = "incompressibleFluid", time: str | None = None, libs: list[str] | None = None): + if time is not None: + raise NotImplementedError("Explicit time selection is not implemented yet; OpenFOAM selects startTime") + if libs: + raise NotImplementedError("Additional library loading is not implemented yet") + self._path = str(path) + self._solver = solver + self._native = _native.Case(self._path, solver) + + @property + def path(self) -> str: + return self._path + + @property + def solver_name(self) -> str: + return self._solver + + def times(self) -> list[str]: + root = Path(self._path) + times: list[tuple[float, str]] = [] + for child in root.iterdir(): + if child.is_dir() and _looks_like_time(child.name): + times.append((float(child.name), child.name)) + return [name for _, name in sorted(times)] + + def control_dict(self) -> str: + return self.make_stepper().control_dict() + + def fv_schemes(self) -> str: + return self.make_stepper().fv_schemes() + + def fv_solution(self) -> str: + return self.make_stepper().fv_solution() + + def make_stepper(self) -> "SimpleStepper": + return SimpleStepper(self._native.make_stepper()) + + +class SimpleStepper: + """Python facade over the C++ OpenFOAM observability stepper.""" + + def __init__(self, native: Any): + self._native = native + + @property + def case_path(self) -> str: + return self._native.case_path + + @property + def solver_name(self) -> str: + return self._native.solver_name + + def state(self) -> dict[str, Any]: + return dict(self._native.state()) + + def control_dict(self) -> str: + return self._native.control_dict() + + def fv_schemes(self) -> str: + return self._native.fv_schemes() + + def fv_solution(self) -> str: + return self._native.fv_solution() + + def mesh(self) -> MeshView: + return MeshView.from_dict(self._native.mesh()) + + def fields(self) -> FieldRegistryView: + return FieldRegistryView.from_dict(self._native.fields()) + + def pre_solve(self) -> TransformResult: + return _transform(self._native.pre_solve()) + + def advance_time(self) -> TransformResult: + return _transform(self._native.advance_time()) + + def begin_pimple_iteration(self) -> TransformResult: + return _transform(self._native.begin_pimple_iteration()) + + def end_pimple_iteration(self) -> TransformResult: + return _transform(self._native.end_pimple_iteration()) + + def post_solve(self, write: bool = False) -> TransformResult: + return _transform(self._native.post_solve(write)) + + def fv_models_correct(self) -> TransformResult: + return _transform(self._native.fv_models_correct()) + + def pre_predictor(self) -> TransformResult: + return _transform(self._native.pre_predictor()) + + def momentum_transport_predictor(self) -> TransformResult: + return _transform(self._native.momentum_transport_predictor()) + + def momentum_transport_corrector(self) -> TransformResult: + return _transform(self._native.momentum_transport_corrector()) + + def assemble_momentum_terms(self) -> TransformResult: + return _transform(self._native.assemble_momentum_terms()) + + def assemble_momentum_matrix(self, terms: Any | None = None) -> TransformResult: + if terms is not None: + # C++ recomputes the OpenFOAM expression to preserve native invariants. + pass + return _transform(self._native.assemble_momentum_matrix()) + + def relax_matrix(self, matrix: Any | None = None) -> TransformResult: + return _transform(self._native.relax_matrix()) + + def constrain_matrix(self, matrix: Any | None = None) -> TransformResult: + return _transform(self._native.constrain_matrix()) + + def solve_momentum(self, matrix: Any | None = None) -> TransformResult: + return _transform(self._native.solve_momentum()) + + def compute_pressure_inputs(self, UEqn: Any | None = None) -> TransformResult: + return _transform(self._native.compute_pressure_inputs()) + + def assemble_pressure_matrix(self, inputs: Any | None = None) -> TransformResult: + return _transform(self._native.assemble_pressure_matrix()) + + def solve_pressure(self, matrix: Any | None = None) -> TransformResult: + return _transform(self._native.solve_pressure()) + + def correct_velocity_pressure_flux(self, inputs: Any | None = None, pEqn: Any | None = None) -> TransformResult: + return _transform(self._native.correct_velocity_pressure_flux()) + + def run_one_pimple_iteration(self) -> TransformResult: + return _transform(self._native.run_one_pimple_iteration()) + + def run_until(self, max_steps: int | None = None, max_iterations: int | None = None) -> list[TransformResult]: + limit = max_steps if max_steps is not None else max_iterations + results: list[TransformResult] = [] + while limit is None or len(results) < limit: + result = self.run_one_pimple_iteration() + results.append(result) + if not result.outputs.get("time_loop_active", False): + break + return results + + +def _transform(raw: Any) -> TransformResult: + return TransformResult.from_dict(raw) + + +def _looks_like_time(name: str) -> bool: + try: + float(name) + except ValueError: + return False + return True + + +__all__ = [ + "Case", + "SimpleStepper", + "OpenFoamError", + "PatchFieldView", + "PatchView", + "RaggedIntArray", + "FieldRegistryView", + "FieldView", + "MatrixView", + "MeshView", + "SolveResult", + "SourceLocation", + "TransformResult", + "version", +] diff --git a/python/src/foam_stepper/_foam_stepper.so b/python/src/foam_stepper/_foam_stepper.so new file mode 100755 index 0000000..317b645 Binary files /dev/null and b/python/src/foam_stepper/_foam_stepper.so differ diff --git a/python/src/foam_stepper/_runtime.py b/python/src/foam_stepper/_runtime.py new file mode 100644 index 0000000..b485e40 --- /dev/null +++ b/python/src/foam_stepper/_runtime.py @@ -0,0 +1,70 @@ +"""Runtime OpenFOAM environment setup for the Python facade.""" + +from __future__ import annotations + +import os +import shlex +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +OPENFOAM_ROOT = ROOT / "OpenFOAM-14" +_ENV_SENTINEL = "_FOAM_STEPPER_OPENFOAM_ENV" + + +def _capture_openfoam_env() -> dict[str, str] | None: + bashrc = OPENFOAM_ROOT / "etc/bashrc" + if not bashrc.exists(): + return None + + script = " ".join( + [ + "set +u;", + "source", + shlex.quote(str(bashrc)), + "WM_MPLIB=Dummy", + "ParaView_TYPE=none", + "SCOTCH_TYPE=none", + "ZOLTAN_TYPE=none;", + "set -u;", + "unset FOAM_SIGFPE;", + "unset PYTHONPATH;", + "env -0", + ] + ) + completed = subprocess.run( + ["bash", "-c", script], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={key: value for key, value in os.environ.items() if key not in {"PYTHONPATH", "FOAM_SIGFPE"}}, + ) + + env: dict[str, str] = {} + for item in completed.stdout.decode().split("\0"): + if not item or "=" not in item: + continue + key, value = item.split("=", 1) + env[key] = value + env.pop("PYTHONPATH", None) + env.pop("FOAM_SIGFPE", None) + env[_ENV_SENTINEL] = str(OPENFOAM_ROOT) + return env + + +def configure_openfoam_environment() -> dict[str, str]: + """Apply the repository OpenFOAM runtime environment if available.""" + + if os.environ.get(_ENV_SENTINEL) == str(OPENFOAM_ROOT): + os.environ.pop("PYTHONPATH", None) + os.environ.pop("FOAM_SIGFPE", None) + return dict(os.environ) + + env = _capture_openfoam_env() + if env is None: + return dict(os.environ) + + os.environ.update(env) + os.environ.pop("PYTHONPATH", None) + os.environ.pop("FOAM_SIGFPE", None) + return env diff --git a/python/src/foam_stepper/types.py b/python/src/foam_stepper/types.py new file mode 100644 index 0000000..7fbb8d8 --- /dev/null +++ b/python/src/foam_stepper/types.py @@ -0,0 +1,399 @@ +"""Typed Python facade objects for OpenFOAM observability snapshots.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable, Iterator, Mapping + +import numpy as np + + +@dataclass(frozen=True) +class RaggedIntArray: + offsets: np.ndarray + values: np.ndarray + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "RaggedIntArray": + return cls(offsets=data["offsets"], values=data["values"]) + + def row(self, index: int) -> np.ndarray: + return self.values[self.offsets[index] : self.offsets[index + 1]] + + def to_dict(self) -> dict[str, Any]: + return {"offsets": self.offsets, "values": self.values} + + +@dataclass(frozen=True) +class SourceLocation: + file: str + function: str + lines: tuple[int, int] | None = None + + @classmethod + def from_dict(cls, data: Mapping[str, Any] | None) -> "SourceLocation": + if not data: + return cls(file="", function="", lines=None) + lines = data.get("lines") + return cls( + file=str(data.get("file", "")), + function=str(data.get("function", "")), + lines=tuple(lines) if lines is not None else None, + ) + + def to_dict(self) -> dict[str, Any]: + return {"file": self.file, "function": self.function, "lines": self.lines} + + +@dataclass(frozen=True) +class PatchView: + name: str + type: str + index: int + start: int + size: int + coupled: bool + constraint: bool + face_cells: np.ndarray + face_indices: np.ndarray + Cf: np.ndarray + Sf: np.ndarray + magSf: np.ndarray + raw: Mapping[str, Any] + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "PatchView": + return cls( + name=data["name"], + type=data["type"], + index=int(data["index"]), + start=int(data["start"]), + size=int(data["size"]), + coupled=bool(data["coupled"]), + constraint=bool(data["constraint"]), + face_cells=data["face_cells"], + face_indices=data["face_indices"], + Cf=data["Cf"], + Sf=data["Sf"], + magSf=data["magSf"], + raw=data, + ) + + def to_dict(self) -> Mapping[str, Any]: + return self.raw + + +@dataclass(frozen=True) +class PatchFieldView: + name: str + type: str + values: np.ndarray + fixes_value: bool + assignable: bool + coupled: bool + updated: bool + patch_internal: np.ndarray | None + value_internal_coeffs: np.ndarray | None + value_boundary_coeffs: np.ndarray | None + gradient_internal_coeffs: np.ndarray | None + gradient_boundary_coeffs: np.ndarray | None + raw: Mapping[str, Any] + + @classmethod + def from_dict(cls, name: str, data: Mapping[str, Any]) -> "PatchFieldView": + return cls( + name=name, + type=data["type"], + values=data["values"], + fixes_value=bool(data["fixes_value"]), + assignable=bool(data["assignable"]), + coupled=bool(data["coupled"]), + updated=bool(data["updated"]), + patch_internal=data.get("patch_internal"), + value_internal_coeffs=data.get("value_internal_coeffs"), + value_boundary_coeffs=data.get("value_boundary_coeffs"), + gradient_internal_coeffs=data.get("gradient_internal_coeffs"), + gradient_boundary_coeffs=data.get("gradient_boundary_coeffs"), + raw=data, + ) + + def to_dict(self) -> Mapping[str, Any]: + return self.raw + + +@dataclass(frozen=True) +class FieldView: + name: str + kind: str + dimensions: str + entity_kind: str + entity_count: int + internal: np.ndarray + boundary: Mapping[str, PatchFieldView] + raw: Mapping[str, Any] + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "FieldView": + boundary = { + name: PatchFieldView.from_dict(name, patch) + for name, patch in data.get("boundary", {}).items() + } + return cls( + name=data["name"], + kind=data["kind"], + dimensions=data["dimensions"], + entity_kind=data["entity_kind"], + entity_count=int(data["entity_count"]), + internal=data["internal"], + boundary=boundary, + raw=data, + ) + + def to_numpy(self, copy: bool = True) -> np.ndarray: + return np.array(self.internal, copy=copy) + + def to_dict(self, copy: bool = False) -> Mapping[str, Any]: + if not copy: + return self.raw + out = dict(self.raw) + out["internal"] = np.array(self.internal, copy=True) + return out + + +@dataclass(frozen=True) +class FieldRegistryView(Mapping[str, FieldView]): + fields: Mapping[str, FieldView] + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "FieldRegistryView": + return cls({name: FieldView.from_dict(value) for name, value in data.items()}) + + def __getitem__(self, key: str) -> FieldView: + return self.fields[key] + + def __iter__(self) -> Iterator[str]: + return iter(self.fields) + + def __len__(self) -> int: + return len(self.fields) + + def __getattr__(self, key: str) -> FieldView: + try: + return self.fields[key] + except KeyError as exc: + raise AttributeError(key) from exc + + def to_dict(self) -> dict[str, Mapping[str, Any]]: + return {name: field.to_dict() for name, field in self.fields.items()} + + +@dataclass(frozen=True) +class MeshView: + n_points: int + n_faces: int + n_internal_faces: int + n_cells: int + points: np.ndarray + faces: RaggedIntArray + cells: RaggedIntArray + owner: np.ndarray + neighbour: np.ndarray + boundary: list[PatchView] + V: np.ndarray + C: np.ndarray + Cf: np.ndarray + Sf: np.ndarray + magSf: np.ndarray + ldu: Mapping[str, Any] + raw: Mapping[str, Any] + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "MeshView": + return cls( + n_points=int(data["n_points"]), + n_faces=int(data["n_faces"]), + n_internal_faces=int(data["n_internal_faces"]), + n_cells=int(data["n_cells"]), + points=data["points"], + faces=RaggedIntArray.from_dict(data["faces"]), + cells=RaggedIntArray.from_dict(data["cells"]), + owner=data["owner"], + neighbour=data["neighbour"], + boundary=[PatchView.from_dict(patch) for patch in data["boundary"]], + V=data["V"], + C=data["C"], + Cf=data["Cf"], + Sf=data["Sf"], + magSf=data["magSf"], + ldu=data["ldu"], + raw=data, + ) + + def patch(self, name: str) -> PatchView: + for patch in self.boundary: + if patch.name == name: + return patch + raise KeyError(name) + + def to_dict(self) -> Mapping[str, Any]: + return self.raw + + +@dataclass(frozen=True) +class MatrixView: + name: str + field_name: str + psi: FieldView + value_rank: str + dimensions: str + has_diag: bool + has_upper: bool + has_lower: bool + diagonal: bool + symmetric: bool + asymmetric: bool + diag: np.ndarray + upper: np.ndarray | None + lower: np.ndarray | None + source: np.ndarray + internal_coeffs: list[np.ndarray] + boundary_coeffs: list[np.ndarray] + face_flux_correction: FieldView | None + raw: Mapping[str, Any] + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "MatrixView": + face_flux = data.get("face_flux_correction") + return cls( + name=data["name"], + field_name=data["field_name"], + psi=FieldView.from_dict(data["psi"]), + value_rank=data["value_rank"], + dimensions=data["dimensions"], + has_diag=bool(data["has_diag"]), + has_upper=bool(data["has_upper"]), + has_lower=bool(data["has_lower"]), + diagonal=bool(data["diagonal"]), + symmetric=bool(data["symmetric"]), + asymmetric=bool(data["asymmetric"]), + diag=data["diag"], + upper=data.get("upper"), + lower=data.get("lower"), + source=data["source"], + internal_coeffs=list(data.get("internal_coeffs", [])), + boundary_coeffs=list(data.get("boundary_coeffs", [])), + face_flux_correction=( + FieldView.from_dict(face_flux) + if isinstance(face_flux, Mapping) and "kind" in face_flux + else None + ), + raw=data, + ) + + @property + def A(self) -> FieldView | None: + value = self.raw.get("A") + return FieldView.from_dict(value) if isinstance(value, Mapping) else None + + @property + def H(self) -> FieldView | None: + value = self.raw.get("H") + return FieldView.from_dict(value) if isinstance(value, Mapping) else None + + @property + def H1(self) -> FieldView | None: + value = self.raw.get("H1") + return FieldView.from_dict(value) if isinstance(value, Mapping) else None + + @property + def flux(self) -> FieldView | None: + value = self.raw.get("flux") + return FieldView.from_dict(value) if isinstance(value, Mapping) else None + + def residual(self) -> np.ndarray | None: + return self.raw.get("residual") + + def D(self) -> np.ndarray | None: + return self.raw.get("D") + + def DD(self) -> np.ndarray | None: + return self.raw.get("DD") + + def to_dict(self) -> Mapping[str, Any]: + return self.raw + + +@dataclass(frozen=True) +class SolveResult: + solver_name: str + field_name: str + initial_residual: Any + final_residual: Any + n_iterations: Any + converged: bool + singular: bool + raw: Mapping[str, Any] + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "SolveResult": + return cls( + solver_name=data["solver_name"], + field_name=data["field_name"], + initial_residual=data["initial_residual"], + final_residual=data["final_residual"], + n_iterations=data["n_iterations"], + converged=bool(data["converged"]), + singular=bool(data["singular"]), + raw=data, + ) + + +@dataclass(frozen=True) +class TransformResult: + name: str + phase: str + source: SourceLocation + inputs: Mapping[str, Any] + outputs: Mapping[str, Any] + changed_fields: list[str] + metadata: Mapping[str, Any] + raw: Mapping[str, Any] + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "TransformResult": + return cls( + name=data["name"], + phase=data["phase"], + source=SourceLocation.from_dict(data.get("source")), + inputs=_wrap_mapping(data.get("inputs", {})), + outputs=_wrap_mapping(data.get("outputs", {})), + changed_fields=list(data.get("changed_fields", [])), + metadata=dict(data.get("metadata", {})), + raw=data, + ) + + def to_dict(self) -> Mapping[str, Any]: + return self.raw + + +def _wrap_mapping(data: Mapping[str, Any]) -> dict[str, Any]: + return {key: _wrap_value(value) for key, value in data.items()} + + +def _wrap_value(value: Any) -> Any: + if isinstance(value, Mapping): + if {"n_cells", "owner", "neighbour"}.issubset(value): + return MeshView.from_dict(value) + if "kind" in value and "internal" in value: + return FieldView.from_dict(value) + if "value_rank" in value and "diag" in value: + return MatrixView.from_dict(value) + if {"solver_name", "field_name", "initial_residual"}.issubset(value): + return SolveResult.from_dict(value) + if {"name", "phase", "outputs"}.issubset(value): + return TransformResult.from_dict(value) + return _wrap_mapping(value) + if isinstance(value, list): + return [_wrap_value(item) for item in value] + return value diff --git a/scripts/build_openfoam_airfrans_subset.sh b/scripts/build_openfoam_airfrans_subset.sh new file mode 100755 index 0000000..73d8163 --- /dev/null +++ b/scripts/build_openfoam_airfrans_subset.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build the OpenFOAM Foundation v14 serial subset needed for AirfRANS-style data generation. +# Tools built: foamRun/incompressibleFluid (simpleFoam replacement), simpleFoam wrapper, +# gmshToFoam, checkMesh, and foamToVTK. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +OPENFOAM_REPO_URL="${OPENFOAM_REPO_URL:-https://github.com/OpenFOAM/OpenFOAM-14.git}" +THIRDPARTY_REPO_URL="${THIRDPARTY_REPO_URL:-https://github.com/OpenFOAM/ThirdParty-14.git}" +AIRFRANS_REPO_URL="${AIRFRANS_REPO_URL:-https://github.com/Extrality/AirfRANS.git}" +JOBS="${JOBS:-$(nproc)}" + +clone_if_missing() { + local url="$1" + local dir="$2" + + if [[ -d "${ROOT_DIR}/${dir}/.git" ]]; then + printf 'using existing %s\n' "${dir}" + else + git clone --depth 1 "${url}" "${ROOT_DIR}/${dir}" + fi +} + +clone_if_missing "${OPENFOAM_REPO_URL}" OpenFOAM-14 +clone_if_missing "${THIRDPARTY_REPO_URL}" ThirdParty-14 +clone_if_missing "${AIRFRANS_REPO_URL}" airfrans + +# Dummy MPI keeps this subset serial and avoids requiring mpicc/OpenMPI for p1. +# Use SYSTEMOPENMPI instead only after installing a system MPI development package. +# OpenFOAM's bashrc probes optional shell variables that may be unset. +# shellcheck disable=SC1091 +set +u +source "${ROOT_DIR}/OpenFOAM-14/etc/bashrc" \ + WM_MPLIB=Dummy \ + ParaView_TYPE=none \ + SCOTCH_TYPE=none \ + ZOLTAN_TYPE=none +set -u + +cd "${ROOT_DIR}/OpenFOAM-14" + +(cd wmake/src && make) + +src/Pstream/Allwmake -j "${JOBS}" +src/OSspecific/${WM_OSTYPE:-POSIX}/Allwmake -j "${JOBS}" +wmake -j "${JOBS}" libso src/OpenFOAM + +for target in \ + src/fileFormats \ + src/surfMesh \ + src/triSurface \ + src/meshTools \ + src/finiteVolume \ + src/ODE \ + src/physicalProperties \ + src/tracking \ + src/lagrangian/basic \ + src/generic/genericPatches \ + src/generic/genericFields \ + src/generic/genericFvPatches \ + src/generic/genericFvFields \ + src/sampling \ + src/meshCheck \ + src/mesh/extrudeModel \ + src/polyTopoChange \ + src/conversion \ + src/thermophysicalModels/specie \ + src/thermophysicalModels/thermophysicalProperties \ + src/thermophysicalModels/basic \ + src/thermophysicalModels/multicomponentThermo \ + src/thermophysicalModels/solidThermo \ + src/MomentumTransportModels/momentumTransportModels \ + src/MomentumTransportModels/incompressible \ + src/MomentumTransportModels/compressible \ + src/ThermophysicalTransportModels/thermophysicalTransportModel \ + src/ThermophysicalTransportModels/fluid \ + src/ThermophysicalTransportModels/fluidThermo \ + src/fvConstraints \ + src/fvModels/general \ + applications/modules/basicFluidSolver \ + applications/modules/incompressibleFluid + do + wmake -j "${JOBS}" libso "${target}" + done + +for app in \ + applications/solvers/foamRun \ + applications/utilities/mesh/conversion/gmshToFoam \ + applications/utilities/mesh/manipulation/checkMesh + do + wmake -j "${JOBS}" "${app}" + done + +applications/utilities/postProcessing/dataConversion/foamToVTK/Allwmake -j "${JOBS}" + +foamRun -help >/dev/null +foamRun -solver incompressibleFluid -help >/dev/null +gmshToFoam -help >/dev/null +checkMesh -help >/dev/null +foamToVTK -help >/dev/null +simpleFoam -help >/dev/null + +printf 'OpenFOAM v14 AirfRANS subset built and smoke-checked.\n' diff --git a/scripts/build_python_stepper.sh b/scripts/build_python_stepper.sh new file mode 100755 index 0000000..f912c26 --- /dev/null +++ b/scripts/build_python_stepper.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +PYTHON_BIN="${PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}" +JOBS="${JOBS:-$(nproc)}" + +if [[ ! -x "${PYTHON_BIN}" ]]; then + echo "Python interpreter not found: ${PYTHON_BIN}" >&2 + echo "Run: uv sync --dev" >&2 + exit 1 +fi + +# shellcheck disable=SC1091 +set +u +source "${ROOT_DIR}/OpenFOAM-14/etc/bashrc" \ + WM_MPLIB=Dummy \ + ParaView_TYPE=none \ + SCOTCH_TYPE=none \ + ZOLTAN_TYPE=none +set -u + +export FOAM_USER_LIBBIN="${ROOT_DIR}/python/src/foam_stepper" +export FOAM_USER_APPBIN="${ROOT_DIR}/pythonStepper/bin" +mkdir -p "${FOAM_USER_LIBBIN}" "${FOAM_USER_APPBIN}" + +export PYBIND11_INCLUDES="$("${PYTHON_BIN}" -m pybind11 --includes)" +PYTHON_LIBDIR="$("${PYTHON_BIN}" -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR") or "")')" +PYTHON_LINK_LIBS="$("${PYTHON_BIN}" -c 'import sysconfig; print(" ".join(v for v in [sysconfig.get_config_var("LIBDIR") and "-L" + sysconfig.get_config_var("LIBDIR"), sysconfig.get_config_var("LDLIBRARY") and "-l" + sysconfig.get_config_var("LDLIBRARY").removeprefix("lib").removesuffix(".so"), sysconfig.get_config_var("LIBS"), sysconfig.get_config_var("SYSLIBS")] if v))')" +RPATH_DIRS=("${FOAM_LIBBIN}" "${FOAM_LIBBIN}/${FOAM_MPI}" "${FOAM_EXT_LIBBIN}") +if [[ -n "${PYTHON_LIBDIR}" ]]; then + RPATH_DIRS+=("${PYTHON_LIBDIR}") +fi +RPATH_LIBS="-Wl,--disable-new-dtags" +for lib_dir in "${RPATH_DIRS[@]}"; do + if [[ -d "${lib_dir}" ]]; then + RPATH_LIBS="${RPATH_LIBS} -Wl,-rpath,${lib_dir}" + fi +done +export PYTHON_LIBS="${PYTHON_LINK_LIBS} ${RPATH_LIBS}" + +rm -f "${FOAM_USER_LIBBIN}/_foam_stepper.so" + +wmake -j "${JOBS}" libso "${ROOT_DIR}/pythonStepper/pyfoam_stepper" + +printf 'Built %s/_foam_stepper.so\n' "${FOAM_USER_LIBBIN}" diff --git a/scripts/fuzz_python_stepper.py b/scripts/fuzz_python_stepper.py new file mode 100755 index 0000000..877fbf1 --- /dev/null +++ b/scripts/fuzz_python_stepper.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +"""Seeded equivalence fuzzer for the Python-driven OpenFOAM stepper.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import os +import random +import shutil +import subprocess +import sys +from dataclasses import asdict, dataclass +from pathlib import Path + + +def _drop_ambient_pythonpath() -> None: + pythonpath = os.environ.pop("PYTHONPATH", "") + for entry in pythonpath.split(os.pathsep): + if not entry: + continue + while entry in sys.path: + sys.path.remove(entry) + + +_drop_ambient_pythonpath() + +import numpy as np + + +def _load_openfoam_env_helpers(): + try: + from openfoam_env import apply_openfoam_env, openfoam_env + except ModuleNotFoundError: + spec = importlib.util.spec_from_file_location("openfoam_env", Path(__file__).with_name("openfoam_env.py")) + if spec is None or spec.loader is None: + raise + module = importlib.util.module_from_spec(spec) + sys.modules["openfoam_env"] = module + spec.loader.exec_module(module) + return module.apply_openfoam_env, module.openfoam_env + return apply_openfoam_env, openfoam_env + + +apply_openfoam_env, openfoam_env = _load_openfoam_env_helpers() + +ROOT = Path(__file__).resolve().parents[1] +TUTORIAL = ROOT / "OpenFOAM-14/tutorials/incompressibleFluid/venturiTube" +DEFAULT_WORK = ROOT / "tmp/python_stepper_fuzz" +DEFAULT_SEEDS = 10 +DEFAULT_ATOL = 1e-10 +DEFAULT_RTOL = 1e-10 +FIELDS = ("U", "p", "phi") + + +@dataclass(frozen=True) +class CaseVariant: + seed: int + diameter: float + dia_cells: int + ven_cells: int + in_cells: int + out_cells: int + box_cells: int + rad_cells: int + out_grading: float + u_inlet: float + nu: float + p_relax: float + u_relax: float + p_tolerance: float + u_tolerance: float + n_non_orthogonal_correctors: int + + +def scalar(value: float) -> str: + return f"{value:.17g}" + + +def variant_for_seed(seed: int) -> CaseVariant: + rng = random.Random(seed) + return CaseVariant( + seed=seed, + diameter=rng.choice([0.05, 0.075, 0.1, 0.15, 0.2]), + dia_cells=rng.choice([4, 6, 8]), + ven_cells=rng.choice([2, 4]), + in_cells=rng.choice([6, 8, 10]), + out_cells=rng.choice([8, 10, 12]), + box_cells=rng.choice([2, 3, 4]), + rad_cells=rng.choice([4, 6, 8]), + out_grading=rng.choice([0.5, 0.75, 1.0, 1.25]), + u_inlet=rng.choice([0.05, 0.1, 0.2, 0.4]), + nu=rng.choice([2e-5, 4e-5, 8e-5, 1.6e-4]), + p_relax=rng.choice([0.2, 0.3, 0.5, 0.7]), + u_relax=rng.choice([0.5, 0.7, 0.9]), + p_tolerance=rng.choice([1e-6, 1e-7]), + u_tolerance=rng.choice([1e-7, 1e-8]), + n_non_orthogonal_correctors=rng.choice([0, 1]), + ) + + +def replace_required(path: Path, text: str, old: str, new: str) -> str: + count = text.count(old) + if count == 0: + raise AssertionError(f"{path}: missing literal {old!r}") + if count != 1: + raise AssertionError(f"{path}: expected one literal {old!r}, found {count}") + return text.replace(old, new, 1) + + +def patch_file(path: Path, replacements: list[tuple[str, str]]) -> None: + text = path.read_text() + for old, new in replacements: + text = replace_required(path, text, old, new) + path.write_text(text) + + +def patch_block_mesh(case: Path, variant: CaseVariant) -> None: + patch_file( + case / "system/blockMeshDict", + [ + ("diameter 0.1;", f"diameter {scalar(variant.diameter)};"), + ("diaCells 16;", f"diaCells {variant.dia_cells};"), + ("venCells 8;", f"venCells {variant.ven_cells};"), + ("inCells 20;", f"inCells {variant.in_cells};"), + ("outCells 40;", f"outCells {variant.out_cells};"), + ("boxCells 8;", f"boxCells {variant.box_cells};"), + ("radCells 16;", f"radCells {variant.rad_cells};"), + ("outGrading 0.5;", f"outGrading {scalar(variant.out_grading)};"), + ], + ) + + +def patch_initial_fields(case: Path, variant: CaseVariant) -> None: + patch_file(case / "0/U", [("Uinlet 0.2;", f"Uinlet {scalar(variant.u_inlet)};")]) + + +def patch_physical_properties(case: Path, variant: CaseVariant) -> None: + patch_file(case / "constant/physicalProperties", [("nu 4e-05;", f"nu {scalar(variant.nu)};")]) + + +def patch_fv_solution(case: Path, variant: CaseVariant) -> None: + patch_file( + case / "system/fvSolution", + [ + (" tolerance 1e-6;", f" tolerance {scalar(variant.p_tolerance)};"), + (" tolerance 1e-7;", f" tolerance {scalar(variant.u_tolerance)};"), + ( + " nNonOrthogonalCorrectors 0;", + f" nNonOrthogonalCorrectors {variant.n_non_orthogonal_correctors};", + ), + (" p 0.3;", f" p {scalar(variant.p_relax)};"), + (" U 0.7;", f" U {scalar(variant.u_relax)};"), + ], + ) + + +def patch_control_dict_for_seed(case: Path) -> None: + patch_file( + case / "system/controlDict", + [ + ("startFrom latestTime;", "startFrom startTime;"), + ("endTime 1000;", "endTime 1;"), + ("writeInterval 50;", "writeInterval 1;"), + ("writePrecision 8;", "writePrecision 17;"), + ], + ) + + +def patch_control_dict_to_latest(case: Path) -> None: + patch_file(case / "system/controlDict", [("startFrom startTime;", "startFrom latestTime;")]) + + +def run(cmd: list[str], *, log_path: Path | None = None) -> None: + env = openfoam_env() + if log_path is None: + subprocess.run(cmd, cwd=ROOT, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, env=env) + return + + with log_path.open("w") as log: + subprocess.run(cmd, cwd=ROOT, check=True, stdout=log, stderr=subprocess.STDOUT, env=env) + + +def prepare_case(dst: Path, variant: CaseVariant) -> None: + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(TUTORIAL, dst, ignore=shutil.ignore_patterns("processor*", "postProcessing", "*.log")) + for orig in (dst / "0").glob("*.orig"): + shutil.copyfile(orig, orig.with_suffix("")) + + patch_block_mesh(dst, variant) + patch_initial_fields(dst, variant) + patch_physical_properties(dst, variant) + patch_fv_solution(dst, variant) + patch_control_dict_for_seed(dst) + + run(["blockMesh", "-case", str(dst)]) + run(["createZones", "-case", str(dst)]) + + +def prepare_seed(seed_dir: Path, variant: CaseVariant) -> dict[str, Path]: + if seed_dir.exists(): + shutil.rmtree(seed_dir) + outputs = seed_dir / "outputs" + outputs.mkdir(parents=True) + (seed_dir / "variant.json").write_text(json.dumps(asdict(variant), indent=2, sort_keys=True) + "\n") + + cases = { + "foam": seed_dir / "foam_case", + "run_one": seed_dir / "python_run_one_case", + "run_split": seed_dir / "python_split_case", + } + for case in cases.values(): + prepare_case(case, variant) + return cases + + +def save_npz(out: Path, *, U: np.ndarray, p: np.ndarray, phi: np.ndarray) -> None: + out.parent.mkdir(parents=True, exist_ok=True) + np.savez_compressed(out, U=np.asarray(U), p=np.asarray(p), phi=np.asarray(phi)) + + +def child_load_fields(case: Path, out: Path) -> None: + import foam_stepper as foam + + fields = foam.Case(case).make_stepper().fields() + save_npz(out, U=fields.U.internal, p=fields.p.internal, phi=fields.phi.internal) + + +def child_run_one(case: Path, out: Path) -> None: + import foam_stepper as foam + + result = foam.Case(case).make_stepper().run_one_pimple_iteration() + fields = result.outputs["fields"] + save_npz(out, U=fields["U"].internal, p=fields["p"].internal, phi=fields["phi"].internal) + + +def child_run_split(case: Path, out: Path) -> None: + import foam_stepper as foam + + stepper = foam.Case(case).make_stepper() + stepper.pre_solve() + stepper.advance_time() + begin = stepper.begin_pimple_iteration() + assert begin.outputs["active"] is True + stepper.fv_models_correct() + stepper.pre_predictor() + stepper.momentum_transport_predictor() + stepper.assemble_momentum_matrix() + stepper.relax_matrix() + stepper.constrain_matrix() + stepper.solve_momentum() + stepper.compute_pressure_inputs() + stepper.assemble_pressure_matrix() + stepper.solve_pressure() + stepper.correct_velocity_pressure_flux() + stepper.momentum_transport_corrector() + stepper.end_pimple_iteration() + stepper.post_solve(write=False) + fields = stepper.fields() + save_npz(out, U=fields.U.internal, p=fields.p.internal, phi=fields.phi.internal) + + +def load_npz(path: Path) -> dict[str, np.ndarray]: + with np.load(path) as data: + return {field: data[field] for field in FIELDS} + + +def failure_message( + *, + seed: int, + variant: CaseVariant, + seed_dir: Path, + path_label: str, + field: str, + baseline_path: Path, + actual_path: Path, + expected_shape: tuple[int, ...], + actual_shape: tuple[int, ...], + max_abs: float, + max_rel: float, +) -> str: + return ( + f"fuzz equivalence failed seed={seed} path={path_label} field={field}\n" + f"variant={json.dumps(asdict(variant), sort_keys=True)}\n" + f"variant_json={seed_dir / 'variant.json'}\n" + f"foamRun_log={seed_dir / 'foamRun.log'}\n" + f"baseline_output={baseline_path}\n" + f"actual_output={actual_path}\n" + f"shape_pair=actual{actual_shape} expected{expected_shape}\n" + f"max_abs={max_abs:.17g}\n" + f"max_rel={max_rel:.17g}" + ) + + +def compare_field( + *, + seed: int, + variant: CaseVariant, + seed_dir: Path, + path_label: str, + field: str, + expected: np.ndarray, + actual: np.ndarray, + baseline_path: Path, + actual_path: Path, + atol: float, + rtol: float, +) -> float: + if actual.shape != expected.shape: + raise AssertionError( + failure_message( + seed=seed, + variant=variant, + seed_dir=seed_dir, + path_label=path_label, + field=field, + baseline_path=baseline_path, + actual_path=actual_path, + expected_shape=expected.shape, + actual_shape=actual.shape, + max_abs=float("nan"), + max_rel=float("nan"), + ) + ) + + abs_diff = np.abs(actual - expected) + max_abs = float(np.max(abs_diff)) if abs_diff.size else 0.0 + max_rel = float(np.max(abs_diff / np.maximum(np.abs(expected), atol))) if abs_diff.size else 0.0 + if not np.allclose(actual, expected, rtol=rtol, atol=atol): + raise AssertionError( + failure_message( + seed=seed, + variant=variant, + seed_dir=seed_dir, + path_label=path_label, + field=field, + baseline_path=baseline_path, + actual_path=actual_path, + expected_shape=expected.shape, + actual_shape=actual.shape, + max_abs=max_abs, + max_rel=max_rel, + ) + ) + return max_abs + + +def compare_outputs( + *, + seed: int, + variant: CaseVariant, + seed_dir: Path, + baseline_path: Path, + actual_paths: dict[str, Path], + atol: float, + rtol: float, +) -> dict[str, dict[str, float]]: + baseline = load_npz(baseline_path) + summary: dict[str, dict[str, float]] = {} + for path_label, actual_path in actual_paths.items(): + actual = load_npz(actual_path) + summary[path_label] = {} + for field in FIELDS: + summary[path_label][field] = compare_field( + seed=seed, + variant=variant, + seed_dir=seed_dir, + path_label=path_label, + field=field, + expected=baseline[field], + actual=actual[field], + baseline_path=baseline_path, + actual_path=actual_path, + atol=atol, + rtol=rtol, + ) + return summary + + +def run_child(child: str, case: Path, out: Path) -> None: + subprocess.run( + [sys.executable, str(Path(__file__).resolve()), "--child", child, "--case", str(case), "--out", str(out)], + cwd=ROOT, + check=True, + env=openfoam_env(), + ) + + +def run_seed(seed: int, variant: CaseVariant, work_dir: Path, *, atol: float, rtol: float) -> Path: + seed_dir = work_dir / f"seed_{seed}" + cases = prepare_seed(seed_dir, variant) + outputs = seed_dir / "outputs" + baseline_path = outputs / "baseline.npz" + run_one_path = outputs / "run_one.npz" + run_split_path = outputs / "run_split.npz" + + run( + ["foamRun", "-case", str(cases["foam"]), "-solver", "incompressibleFluid", "-noFunctionObjects"], + log_path=seed_dir / "foamRun.log", + ) + patch_control_dict_to_latest(cases["foam"]) + run_child("load-fields", cases["foam"], baseline_path) + run_child("run-one", cases["run_one"], run_one_path) + run_child("run-split", cases["run_split"], run_split_path) + + summary = compare_outputs( + seed=seed, + variant=variant, + seed_dir=seed_dir, + baseline_path=baseline_path, + actual_paths={"run_one": run_one_path, "run_split": run_split_path}, + atol=atol, + rtol=rtol, + ) + cells = int(load_npz(baseline_path)["U"].shape[0]) + print( + f"seed={seed} cells={cells} " + f"run_one: U={summary['run_one']['U']:.3e} p={summary['run_one']['p']:.3e} phi={summary['run_one']['phi']:.3e} " + f"split: U={summary['run_split']['U']:.3e} p={summary['run_split']['p']:.3e} phi={summary['run_split']['phi']:.3e}", + flush=True, + ) + return seed_dir + + +def run_parent(args: argparse.Namespace) -> None: + if args.seeds < 1: + raise SystemExit("--seeds must be at least 1") + + args.work_dir.mkdir(parents=True, exist_ok=True) + for seed in range(args.seed_start, args.seed_start + args.seeds): + variant = variant_for_seed(seed) + seed_dir = run_seed(seed, variant, args.work_dir, atol=args.atol, rtol=args.rtol) + if not args.keep_passing: + shutil.rmtree(seed_dir) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--seeds", type=int, default=DEFAULT_SEEDS) + parser.add_argument("--seed-start", type=int, default=0) + parser.add_argument("--work-dir", type=Path, default=DEFAULT_WORK) + parser.add_argument("--atol", type=float, default=DEFAULT_ATOL) + parser.add_argument("--rtol", type=float, default=DEFAULT_RTOL) + parser.add_argument("--keep-passing", action="store_true") + parser.add_argument("--child", choices=("load-fields", "run-one", "run-split")) + parser.add_argument("--case", type=Path) + parser.add_argument("--out", type=Path) + args = parser.parse_args() + + if args.child is not None and (args.case is None or args.out is None): + parser.error("--child requires --case and --out") + if args.child is None and (args.case is not None or args.out is not None): + parser.error("--case and --out are only valid with --child") + return args + + +def main() -> None: + args = parse_args() + if args.child == "load-fields": + apply_openfoam_env() + child_load_fields(args.case, args.out) + elif args.child == "run-one": + apply_openfoam_env() + child_run_one(args.case, args.out) + elif args.child == "run-split": + apply_openfoam_env() + child_run_split(args.case, args.out) + else: + run_parent(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/fuzz_python_stepper.sh b/scripts/fuzz_python_stepper.sh new file mode 100755 index 0000000..1301f1b --- /dev/null +++ b/scripts/fuzz_python_stepper.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +JOBS="${JOBS:-$(nproc)}" +PYTHON_BIN="${PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}" + +if [[ ! -x "${PYTHON_BIN}" ]]; then + uv sync --dev +fi + +if ! PYTHONPATH= "${PYTHON_BIN}" -c 'import pybind11, numpy' >/dev/null 2>&1; then + uv sync --dev +fi + +"${ROOT_DIR}/scripts/build_openfoam_airfrans_subset.sh" >/dev/null + +set +u +source "${ROOT_DIR}/OpenFOAM-14/etc/bashrc" \ + WM_MPLIB=Dummy \ + ParaView_TYPE=none \ + SCOTCH_TYPE=none \ + ZOLTAN_TYPE=none +set -u +unset FOAM_SIGFPE + +( + cd "${ROOT_DIR}/OpenFOAM-14" + wmake -j "${JOBS}" libso src/mesh/blockMesh >/dev/null + wmake -j "${JOBS}" applications/utilities/mesh/generation/blockMesh >/dev/null + wmake -j "${JOBS}" applications/utilities/mesh/manipulation/createZones >/dev/null +) + +"${ROOT_DIR}/scripts/build_python_stepper.sh" >/dev/null + +"${PYTHON_BIN}" "${ROOT_DIR}/scripts/fuzz_python_stepper.py" "$@" diff --git a/scripts/openfoam_env.py b/scripts/openfoam_env.py new file mode 100644 index 0000000..5e39494 --- /dev/null +++ b/scripts/openfoam_env.py @@ -0,0 +1,73 @@ +"""Internal OpenFOAM environment resolution for Python scripts. + +This keeps user-facing commands rooted in the project .venv while still giving +OpenFOAM subprocesses the environment normally produced by etc/bashrc. +""" + +from __future__ import annotations + +import os +import shlex +import subprocess +from functools import lru_cache +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +@lru_cache(maxsize=1) +def openfoam_env() -> dict[str, str]: + """Return a sanitized environment with OpenFOAM v14 sourced. + + The returned environment intentionally removes ambient PYTHONPATH. The + project package is installed into .venv by `uv sync --dev`, so PYTHONPATH is + unnecessary and can accidentally shadow the venv's binary wheels. + """ + + bashrc = ROOT / "OpenFOAM-14/etc/bashrc" + if not bashrc.exists(): + raise RuntimeError(f"OpenFOAM bashrc not found: {bashrc}") + + command = " ".join( + [ + "set +u;", + "source", + shlex.quote(str(bashrc)), + "WM_MPLIB=Dummy", + "ParaView_TYPE=none", + "SCOTCH_TYPE=none", + "ZOLTAN_TYPE=none", + ">/dev/null;", + "set -u;", + "unset FOAM_SIGFPE;", + "unset PYTHONPATH;", + "env -0", + ] + ) + completed = subprocess.run( + ["bash", "-lc", command], + cwd=ROOT, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={key: value for key, value in os.environ.items() if key not in {"PYTHONPATH", "FOAM_SIGFPE"}}, + ) + env: dict[str, str] = {} + for item in completed.stdout.decode().split("\0"): + if not item: + continue + key, _, value = item.partition("=") + env[key] = value + env.pop("PYTHONPATH", None) + env.pop("FOAM_SIGFPE", None) + return env + + +def apply_openfoam_env() -> dict[str, str]: + """Apply and return the sanitized OpenFOAM environment to this process.""" + + env = openfoam_env() + os.environ.update(env) + os.environ.pop("PYTHONPATH", None) + os.environ.pop("FOAM_SIGFPE", None) + return env diff --git a/scripts/prepare_airfrans_stepper_case.py b/scripts/prepare_airfrans_stepper_case.py new file mode 100755 index 0000000..612a40d --- /dev/null +++ b/scripts/prepare_airfrans_stepper_case.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +"""Prepare a v14-compatible AirfRANS raw OpenFOAM case for the Python stepper. + +The source raw data lives in the sibling ../airfrans repo and must not be +modified. This helper copies only the files needed for one local v14 +OpenFOAM/stepper iteration: mesh, initial fields, fvSchemes, and fvSolution. +""" + +from __future__ import annotations + +import argparse +import json +import math +import re +import shutil +from dataclasses import asdict, dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +RAW_ROOT = ROOT.parent / "airfrans/data/raw/OF_dataset" +DEFAULT_SIMULATION = "airFoil2D_SST_93.213_3.79_0.418_0.0_9.665" +DEFAULT_SOURCE = RAW_ROOT / DEFAULT_SIMULATION +DEFAULT_DEST = ROOT / "tmp/airfrans_stepper_case" / f"{DEFAULT_SIMULATION}_v14" + +FLOAT_RE = re.compile(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?") + + +@dataclass(frozen=True) +class AirfransCaseMetadata: + simulation: str + source: str + u_inf: float + velocity: tuple[float, float, float] + nu: float + rho_inf: float + reynolds: float + mach: float + alpha_deg: float + drag_dir: tuple[float, float, float] + lift_dir: tuple[float, float, float] + source_end_time: int + migrated_end_time: int + + +def read_text(path: Path) -> str: + return path.read_text(errors="replace") + + +def assignment(text: str, key: str) -> str: + match = re.search(rf"^\s*{re.escape(key)}\s+([^;]+);", text, flags=re.MULTILINE) + if not match: + raise ValueError(f"missing assignment {key!r}") + return match.group(1).strip() + + +def vector_assignment(text: str, key: str) -> tuple[float, float, float]: + values = [float(value) for value in FLOAT_RE.findall(assignment(text, key))] + if len(values) != 3: + raise ValueError(f"expected 3-vector for {key!r}, got {values!r}") + return (values[0], values[1], values[2]) + + +def copy_required_file(src_root: Path, dst_root: Path, relative: str) -> None: + src = src_root / relative + dst = dst_root / relative + if not src.exists(): + raise FileNotFoundError(src) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + + +def copy_required_tree(src_root: Path, dst_root: Path, relative: str) -> None: + src = src_root / relative + dst = dst_root / relative + if not src.exists(): + raise FileNotFoundError(src) + if dst.exists(): + shutil.rmtree(dst) + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(src, dst) + + +def header(class_name: str, location: str, object_name: str) -> str: + location_line = f' location "{location}";\n' if location else "" + return f"""/*--------------------------------*- C++ -*----------------------------------*\\ + ========= | + \\ / F ield | OpenFOAM: The Open Source CFD Toolbox + \\ / O peration | Website: https://openfoam.org + \\ / A nd | Version: 14 + \\/ M anipulation | +\\*---------------------------------------------------------------------------*/ +FoamFile +{{ + format ascii; + class {class_name}; +{location_line} object {object_name}; +}} +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // + +""" + + +def write_control_dict(dst: Path, *, u_inf: float, end_time: int) -> None: + content = header("dictionary", "system", "controlDict") + f"""Uinf {u_inf:.17g}; + +solver incompressibleFluid; + +startFrom startTime; + +startTime 0; + +stopAt endTime; + +endTime {end_time}; + +deltaT 1; + +writeControl timeStep; + +writeInterval {end_time}; + +purgeWrite 0; + +writeFormat ascii; + +writePrecision 17; + +writeCompression off; + +timeFormat general; + +timePrecision 6; + +runTimeModifiable true; + +// Function objects are intentionally disabled for first-step parity; run +// foamRun/foam_stepper with -noFunctionObjects and parse archived raw force +// outputs separately in the notebook. + +// ************************************************************************* // +""" + (dst / "system/controlDict").write_text(content) + + +def write_physical_properties(dst: Path, *, nu: float, rho_inf: float) -> None: + content = header("dictionary", "constant", "physicalProperties") + f"""viscosityModel constant; + +rho {rho_inf:.17g}; + +nu {nu:.17g}; + +// ************************************************************************* // +""" + (dst / "constant/physicalProperties").write_text(content) + + +def write_momentum_transport(dst: Path) -> None: + content = header("dictionary", "constant", "momentumTransport") + """simulationType RAS; + +RAS +{ + model kOmegaSST; + turbulence on; +} + +// ************************************************************************* // +""" + (dst / "constant/momentumTransport").write_text(content) + + +def metadata_from_source(src: Path, migrated_end_time: int) -> AirfransCaseMetadata: + control = read_text(src / "system/controlDict") + transport = read_text(src / "constant/transportProperties") + u_field = read_text(src / "0/U") + + u_inf = float(assignment(control, "Uinf")) + velocity = vector_assignment(u_field, "field") + nu = float(assignment(transport, "nu")) + rho_inf = float(assignment(control, "rhoInf")) + drag_dir = vector_assignment(control, "dragDir") + lift_dir = vector_assignment(control, "liftDir") + source_end_time = int(float(assignment(control, "endTime"))) + alpha_deg = math.degrees(math.atan2(drag_dir[1], drag_dir[0])) + + return AirfransCaseMetadata( + simulation=src.name, + source=str(src), + u_inf=u_inf, + velocity=velocity, + nu=nu, + rho_inf=rho_inf, + reynolds=u_inf / nu, + mach=u_inf / 346.1, + alpha_deg=alpha_deg, + drag_dir=drag_dir, + lift_dir=lift_dir, + source_end_time=source_end_time, + migrated_end_time=migrated_end_time, + ) + + +def prepare_case(src: Path, dst: Path, *, end_time: int = 1) -> AirfransCaseMetadata: + src = src.resolve() + dst = dst.resolve() + if not src.exists(): + raise FileNotFoundError(src) + + required = [ + "system/fvSchemes", + "system/fvSolution", + "system/blockMeshDict", + "0/U", + "0/p", + "0/nut", + "0/k", + "0/omega", + "constant/transportProperties", + "constant/turbulenceProperties", + "constant/polyMesh/boundary", + "constant/polyMesh/points.gz", + "constant/polyMesh/faces.gz", + "constant/polyMesh/owner.gz", + "constant/polyMesh/neighbour.gz", + ] + missing = [relative for relative in required if not (src / relative).exists()] + if missing: + raise FileNotFoundError(f"missing required AirfRANS files: {missing}") + + if dst.exists(): + shutil.rmtree(dst) + dst.mkdir(parents=True) + + copy_required_tree(src, dst, "constant/polyMesh") + for relative in [ + "system/fvSchemes", + "system/fvSolution", + "system/blockMeshDict", + "0/U", + "0/p", + "0/nut", + "0/k", + "0/omega", + "constant/transportProperties", + "constant/turbulenceProperties", + ]: + copy_required_file(src, dst, relative) + + meta = metadata_from_source(src, end_time) + write_control_dict(dst, u_inf=meta.u_inf, end_time=end_time) + write_physical_properties(dst, nu=meta.nu, rho_inf=meta.rho_inf) + write_momentum_transport(dst) + + # Keep the original dictionaries for audit without letting v14 select them. + shutil.move(dst / "constant/transportProperties", dst / "constant/transportProperties.v2112") + shutil.move(dst / "constant/turbulenceProperties", dst / "constant/turbulenceProperties.v2112") + + (dst / "airfrans_case_metadata.json").write_text(json.dumps(asdict(meta), indent=2, sort_keys=True) + "\n") + return meta + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE) + parser.add_argument("--dest", type=Path, default=DEFAULT_DEST) + parser.add_argument("--end-time", type=int, default=1) + args = parser.parse_args() + + meta = prepare_case(args.source, args.dest, end_time=args.end_time) + print(json.dumps(asdict(meta), indent=2, sort_keys=True)) + print(f"prepared={args.dest}") + + +if __name__ == "__main__": + main() diff --git a/scripts/quadrants_hello_world.py b/scripts/quadrants_hello_world.py new file mode 100755 index 0000000..68f33bc --- /dev/null +++ b/scripts/quadrants_hello_world.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Minimal Quadrants CUDA kernel smoke run.""" + +from __future__ import annotations + +import argparse + +import numpy as np +import quadrants as qd + + +@qd.kernel +def axpy(n: int, x: qd.types.NDArray[qd.f32, 1], y: qd.types.NDArray[qd.f32, 1]) -> None: + for i in range(n): + y[i] = 2.0 * x[i] + 1.0 + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--arch", default="cuda", choices=("cuda", "gpu", "cpu", "x64")) + args = parser.parse_args() + + qd.init(arch=getattr(qd, args.arch)) + + x_np = np.arange(8, dtype=np.float32) + expected = 2.0 * x_np + 1.0 + + x = qd.ndarray(qd.f32, shape=x_np.shape) + y = qd.ndarray(qd.f32, shape=x_np.shape) + x.from_numpy(x_np) + + axpy(x_np.size, x, y) + qd.sync() + + actual = y.to_numpy() + if not np.array_equal(actual, expected): + raise SystemExit(f"Quadrants hello kernel mismatch: {actual} != {expected}") + + print(f"quadrants hello world ok on arch={args.arch}: {actual}") + + +if __name__ == "__main__": + main() diff --git a/scripts/verify_airfrans_stepper.py b/scripts/verify_airfrans_stepper.py new file mode 100755 index 0000000..54c8cdb --- /dev/null +++ b/scripts/verify_airfrans_stepper.py @@ -0,0 +1,988 @@ +#!/usr/bin/env python3 +"""Verifier harness for the migrated AirfRANS OpenFOAM stepper case.""" + +from __future__ import annotations + +import argparse +import dataclasses +import hashlib +import json +import math +import os +import shutil +import subprocess +import sys +import time +import traceback +from collections.abc import Mapping +from pathlib import Path +from typing import Any + + +def _drop_ambient_pythonpath() -> None: + pythonpath = os.environ.pop("PYTHONPATH", "") + for entry in pythonpath.split(os.pathsep): + if not entry: + continue + while entry in sys.path: + sys.path.remove(entry) + + +_drop_ambient_pythonpath() + +import numpy as np + +from openfoam_env import apply_openfoam_env, openfoam_env +from prepare_airfrans_stepper_case import DEFAULT_SOURCE, prepare_case + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_WORK = ROOT / "tmp/airfrans_stepper_verify" +PRIMARY_FIELDS = ("U", "p", "phi") +TURBULENCE_FIELDS = ("nut", "k", "omega") +REQUIRED_FIELDS = PRIMARY_FIELDS + TURBULENCE_FIELDS + +LOADING_FAILURE = "loading_or_parsing_failure" +ORACLE_FAILURE = "openfoam_oracle_failure" +STEPPER_FAILURE = "stepper_execution_failure" +COMPARISON_FAILURE = "numerical_comparison_failure" +INTERNAL_FAILURE = "internal_harness_failure" + +EXIT_CODES = { + LOADING_FAILURE: 2, + ORACLE_FAILURE: 3, + STEPPER_FAILURE: 4, + COMPARISON_FAILURE: 5, + INTERNAL_FAILURE: 6, +} + + +class HarnessError(Exception): + """Categorized verifier failure that can be serialized into the report.""" + + def __init__( + self, + category: str, + step: str, + message: str, + *, + details: Mapping[str, Any] | None = None, + cause: BaseException | None = None, + ) -> None: + super().__init__(message) + self.category = category + self.step = step + self.details = dict(details or {}) + self.cause = cause + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "category": self.category, + "step": self.step, + "message": str(self), + "details": self.details, + } + if self.cause is not None: + out["cause"] = { + "type": type(self.cause).__name__, + "message": str(self.cause), + } + return json_ready(out) + + +def json_ready(value: Any) -> Any: + """Convert report values into strict JSON-compatible data.""" + + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return json_ready(dataclasses.asdict(value)) + if isinstance(value, Path): + return str(value) + if isinstance(value, np.ndarray): + return array_stats(value) + if isinstance(value, np.generic): + return json_ready(value.item()) + if isinstance(value, float): + return value if math.isfinite(value) else None + if isinstance(value, (str, int, bool)) or value is None: + return value + if isinstance(value, Mapping): + return {str(key): json_ready(item) for key, item in value.items()} + if isinstance(value, (list, tuple, set)): + return [json_ready(item) for item in value] + return repr(value) + + +def array_shape(array: Any | None) -> list[int] | None: + if array is None: + return None + return [int(dim) for dim in np.asarray(array).shape] + + +def finite_float(value: Any) -> float | None: + try: + number = float(value) + except (TypeError, ValueError): + return None + return number if math.isfinite(number) else None + + +def array_stats(array: Any) -> dict[str, Any]: + arr = np.asarray(array) + out: dict[str, Any] = { + "shape": array_shape(arr), + "dtype": str(arr.dtype), + "size": int(arr.size), + } + if arr.size == 0 or not np.issubdtype(arr.dtype, np.number): + return out + + finite = np.isfinite(arr) + out["finite_count"] = int(np.count_nonzero(finite)) + out["nonfinite_count"] = int(arr.size - out["finite_count"]) + if out["finite_count"]: + finite_values = arr[finite] + out.update( + { + "min": finite_float(np.min(finite_values)), + "max": finite_float(np.max(finite_values)), + "mean": finite_float(np.mean(finite_values)), + } + ) + return out + + +def value_at(array: np.ndarray, index: tuple[int, ...]) -> Any: + value = array[index] + if isinstance(value, np.generic): + return json_ready(value.item()) + if isinstance(value, np.ndarray): + return json_ready(value.tolist()) + return json_ready(value) + + +def entity_value_at(array: np.ndarray, entity_index: int | None) -> Any: + if entity_index is None or array.ndim == 0: + return None + value = array[entity_index] + if isinstance(value, np.generic): + return json_ready(value.item()) + if isinstance(value, np.ndarray): + return json_ready(value.tolist()) + return json_ready(value) + + +def update_hash_text(digest: "hashlib._Hash", value: str) -> None: + encoded = value.encode("utf-8") + digest.update(len(encoded).to_bytes(8, "little")) + digest.update(encoded) + + +def update_hash_array(digest: "hashlib._Hash", label: str, array: Any) -> None: + arr = np.ascontiguousarray(np.asarray(array)) + update_hash_text(digest, label) + update_hash_text(digest, str(arr.dtype)) + update_hash_text(digest, repr(tuple(int(dim) for dim in arr.shape))) + digest.update(arr.tobytes()) + + +def source_summary(source: Any) -> dict[str, Any]: + return { + "file": getattr(source, "file", ""), + "function": getattr(source, "function", ""), + "lines": json_ready(getattr(source, "lines", None)), + } + + +def boundary_field_summary(patch: Any) -> dict[str, Any]: + return { + "type": getattr(patch, "type", ""), + "values_shape": array_shape(getattr(patch, "values", None)), + "fixes_value": bool(getattr(patch, "fixes_value", False)), + "assignable": bool(getattr(patch, "assignable", False)), + "coupled": bool(getattr(patch, "coupled", False)), + "updated": bool(getattr(patch, "updated", False)), + "patch_internal_shape": array_shape(getattr(patch, "patch_internal", None)), + "value_internal_coeffs_shape": array_shape(getattr(patch, "value_internal_coeffs", None)), + "value_boundary_coeffs_shape": array_shape(getattr(patch, "value_boundary_coeffs", None)), + "gradient_internal_coeffs_shape": array_shape(getattr(patch, "gradient_internal_coeffs", None)), + "gradient_boundary_coeffs_shape": array_shape(getattr(patch, "gradient_boundary_coeffs", None)), + } + + +def field_summary(field: Any) -> dict[str, Any]: + boundary = getattr(field, "boundary", {}) + return { + "name": getattr(field, "name", ""), + "kind": getattr(field, "kind", ""), + "dimensions": getattr(field, "dimensions", ""), + "entity_kind": getattr(field, "entity_kind", ""), + "entity_count": int(getattr(field, "entity_count", 0)), + "internal": array_stats(getattr(field, "internal")), + "boundary": {name: boundary_field_summary(patch) for name, patch in boundary.items()}, + } + + +def solve_summary(performance: Any) -> dict[str, Any]: + return { + "solver_name": getattr(performance, "solver_name", ""), + "field_name": getattr(performance, "field_name", ""), + "initial_residual": json_ready(getattr(performance, "initial_residual", None)), + "final_residual": json_ready(getattr(performance, "final_residual", None)), + "n_iterations": json_ready(getattr(performance, "n_iterations", None)), + "converged": bool(getattr(performance, "converged", False)), + "singular": bool(getattr(performance, "singular", False)), + } + + +def matrix_summary(matrix: Any) -> dict[str, Any]: + derived: dict[str, Any] = {} + for name in ("A", "H", "H1", "flux", "face_flux_correction"): + value = getattr(matrix, name, None) + if callable(value): + value = value() + if value is not None: + derived[name] = field_summary(value) + + for name in ("residual", "D", "DD"): + value = getattr(matrix, name, None) + if callable(value): + value = value() + if value is not None: + derived[name] = array_stats(value) + + return { + "name": getattr(matrix, "name", ""), + "field_name": getattr(matrix, "field_name", ""), + "value_rank": getattr(matrix, "value_rank", ""), + "dimensions": getattr(matrix, "dimensions", ""), + "has_diag": bool(getattr(matrix, "has_diag", False)), + "has_upper": bool(getattr(matrix, "has_upper", False)), + "has_lower": bool(getattr(matrix, "has_lower", False)), + "diagonal": bool(getattr(matrix, "diagonal", False)), + "symmetric": bool(getattr(matrix, "symmetric", False)), + "asymmetric": bool(getattr(matrix, "asymmetric", False)), + "diag": array_stats(getattr(matrix, "diag")), + "upper": None if getattr(matrix, "upper", None) is None else array_stats(getattr(matrix, "upper")), + "lower": None if getattr(matrix, "lower", None) is None else array_stats(getattr(matrix, "lower")), + "source": array_stats(getattr(matrix, "source")), + "psi": field_summary(getattr(matrix, "psi")), + "internal_coeff_shapes": [array_shape(item) for item in getattr(matrix, "internal_coeffs", [])], + "boundary_coeff_shapes": [array_shape(item) for item in getattr(matrix, "boundary_coeffs", [])], + "derived": derived, + } + + +def summarize_value(value: Any) -> Any: + if hasattr(value, "diag") and hasattr(value, "field_name") and hasattr(value, "source"): + return matrix_summary(value) + if hasattr(value, "internal") and hasattr(value, "entity_kind") and hasattr(value, "boundary"): + return field_summary(value) + if hasattr(value, "solver_name") and hasattr(value, "initial_residual"): + return solve_summary(value) + if hasattr(value, "name") and hasattr(value, "phase") and hasattr(value, "outputs"): + return transform_summary(value) + if isinstance(value, Mapping): + return {str(key): summarize_value(item) for key, item in value.items()} + if isinstance(value, list): + return [summarize_value(item) for item in value] + if isinstance(value, tuple): + return [summarize_value(item) for item in value] + return json_ready(value) + + +def transform_summary(result: Any) -> dict[str, Any]: + return { + "name": getattr(result, "name", ""), + "phase": getattr(result, "phase", ""), + "changed_fields": json_ready(getattr(result, "changed_fields", [])), + "source": source_summary(getattr(result, "source", None)), + "metadata": json_ready(getattr(result, "metadata", {})), + "outputs": summarize_value(getattr(result, "outputs", {})), + } + + +def selected_field_summaries(fields: Mapping[str, Any]) -> dict[str, Any]: + return {name: field_summary(fields[name]) for name in REQUIRED_FIELDS if name in fields} + + +def field_dict_to_mapping(fields: Any) -> dict[str, Any]: + if isinstance(fields, Mapping): + return dict(fields) + if hasattr(fields, "fields") and isinstance(fields.fields, Mapping): + return dict(fields.fields) + return {name: getattr(fields, name) for name in REQUIRED_FIELDS if hasattr(fields, name)} + + +def mesh_patch_table(mesh: Any) -> list[dict[str, Any]]: + patches = [] + for patch in getattr(mesh, "boundary", []): + patches.append( + { + "index": int(getattr(patch, "index", 0)), + "name": getattr(patch, "name", ""), + "type": getattr(patch, "type", ""), + "start": int(getattr(patch, "start", 0)), + "size": int(getattr(patch, "size", 0)), + "coupled": bool(getattr(patch, "coupled", False)), + "constraint": bool(getattr(patch, "constraint", False)), + } + ) + return patches + + +def mesh_topology_digest(mesh: Any) -> str: + digest = hashlib.sha256() + for name in ("n_points", "n_faces", "n_internal_faces", "n_cells"): + update_hash_text(digest, f"{name}={int(getattr(mesh, name))}") + update_hash_array(digest, "faces.offsets", mesh.faces.offsets) + update_hash_array(digest, "faces.values", mesh.faces.values) + update_hash_array(digest, "cells.offsets", mesh.cells.offsets) + update_hash_array(digest, "cells.values", mesh.cells.values) + update_hash_array(digest, "owner", mesh.owner) + update_hash_array(digest, "neighbour", mesh.neighbour) + ldu = getattr(mesh, "ldu", {}) + if isinstance(ldu, Mapping): + if "lower_addr" in ldu: + update_hash_array(digest, "ldu.lower_addr", ldu["lower_addr"]) + if "upper_addr" in ldu: + update_hash_array(digest, "ldu.upper_addr", ldu["upper_addr"]) + for entry in ldu.get("patch_addr", []): + update_hash_text(digest, f"ldu.patch_index={entry.get('patch_index')}") + update_hash_array(digest, "ldu.patch_addr", entry.get("addr", [])) + for patch in mesh_patch_table(mesh): + update_hash_text(digest, json.dumps(patch, sort_keys=True)) + return digest.hexdigest() + + +def mesh_geometry_digest(mesh: Any) -> str: + digest = hashlib.sha256() + for name in ("points", "V", "C", "Cf", "Sf", "magSf"): + update_hash_array(digest, name, getattr(mesh, name)) + return digest.hexdigest() + + +def mesh_identity(mesh: Any) -> dict[str, Any]: + return { + "n_points": int(mesh.n_points), + "n_faces": int(mesh.n_faces), + "n_internal_faces": int(mesh.n_internal_faces), + "n_cells": int(mesh.n_cells), + "patches": mesh_patch_table(mesh), + "topology_sha256": mesh_topology_digest(mesh), + "geometry_sha256": mesh_geometry_digest(mesh), + } + + +def compare_mesh_identity(mode: str, actual: Mapping[str, Any], expected: Mapping[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]: + keys = ("n_points", "n_faces", "n_internal_faces", "n_cells", "topology_sha256", "geometry_sha256", "patches") + differences = { + key: {"actual": actual.get(key), "expected": expected.get(key)} + for key in keys + if actual.get(key) != expected.get(key) + } + report = {"matches_oracle": not differences, "differences": differences} + if not differences: + return report, [] + return report, [{"mode": mode, "kind": "mesh_identity", "differences": differences}] + + +def field_compare_report(name: str, actual_field: Any, expected_field: Any, *, rtol: float, atol: float) -> tuple[dict[str, Any], dict[str, Any] | None]: + actual = np.asarray(actual_field.internal) + expected = np.asarray(expected_field.internal) + report: dict[str, Any] = { + "field": name, + "actual_shape": array_shape(actual), + "expected_shape": array_shape(expected), + "actual_entity_kind": getattr(actual_field, "entity_kind", ""), + "expected_entity_kind": getattr(expected_field, "entity_kind", ""), + "rtol": rtol, + "atol": atol, + } + + if actual.shape != expected.shape: + report.update({"allclose": False, "reason": "shape_mismatch"}) + return report, {"field": name, "reason": "shape_mismatch", **report} + + if actual.size == 0: + report.update( + { + "allclose": True, + "max_abs": 0.0, + "mean_abs": 0.0, + "location": None, + "actual_at_max": None, + "expected_at_max": None, + } + ) + return report, None + + diff = np.abs(actual - expected) + finite = np.isfinite(diff) + if not np.all(finite): + flat_index = int(np.flatnonzero(~finite)[0]) + max_abs: float | None = None + else: + flat_index = int(np.argmax(diff)) + max_abs = finite_float(diff.reshape(-1)[flat_index]) + + max_index = tuple(int(item) for item in np.unravel_index(flat_index, diff.shape)) + entity_index = max_index[0] if max_index else None + component_index = list(max_index[1:]) if len(max_index) > 1 else None + actual_at_max = value_at(actual, max_index) + expected_at_max = value_at(expected, max_index) + tolerance_at_max = None + if isinstance(expected_at_max, (int, float)): + tolerance_at_max = atol + rtol * abs(float(expected_at_max)) + + finite_diff = diff[finite] + allclose = bool(np.allclose(actual, expected, rtol=rtol, atol=atol, equal_nan=False)) + report.update( + { + "allclose": allclose, + "max_abs": max_abs, + "mean_abs": finite_float(np.mean(finite_diff)) if finite_diff.size else None, + "location": { + "array_index": list(max_index), + "entity_kind": getattr(actual_field, "entity_kind", ""), + "entity_index": entity_index, + "component_index": component_index, + }, + "actual_at_max": actual_at_max, + "expected_at_max": expected_at_max, + "actual_entity_at_max": entity_value_at(actual, entity_index), + "expected_entity_at_max": entity_value_at(expected, entity_index), + "tolerance_at_max": finite_float(tolerance_at_max), + "nonfinite_error_count": int(diff.size - np.count_nonzero(finite)), + } + ) + if allclose: + return report, None + report["reason"] = "value_mismatch" + return report, {"field": name, "reason": "value_mismatch", **report} + + +def compare_fields( + mode: str, + actual: Mapping[str, Any], + expected: Mapping[str, Any], + *, + rtol: float, + atol: float, +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + report: dict[str, Any] = {} + mismatches: list[dict[str, Any]] = [] + for name in REQUIRED_FIELDS: + if name not in actual or name not in expected: + missing = { + "field": name, + "reason": "missing_field", + "missing_actual": name not in actual, + "missing_expected": name not in expected, + "actual_fields": sorted(actual), + "expected_fields": sorted(expected), + } + report[name] = {"field": name, "allclose": False, **missing} + mismatches.append({"mode": mode, **missing}) + continue + field_report, mismatch = field_compare_report(name, actual[name], expected[name], rtol=rtol, atol=atol) + report[name] = field_report + if mismatch is not None: + mismatches.append({"mode": mode, **mismatch}) + return report, mismatches + + +def run_openfoam_command(cmd: list[str], *, log_path: Path, timeout: int) -> dict[str, Any]: + started = time.monotonic() + env = openfoam_env() + log_path.parent.mkdir(parents=True, exist_ok=True) + try: + with log_path.open("w") as log: + subprocess.run(cmd, cwd=ROOT, env=env, check=True, stdout=log, stderr=subprocess.STDOUT, timeout=timeout) + except subprocess.TimeoutExpired as exc: + raise HarnessError( + ORACLE_FAILURE, + Path(cmd[0]).name, + f"OpenFOAM command timed out after {timeout}s: {' '.join(cmd)}", + details={"cmd": cmd, "timeout_seconds": timeout, "log_path": log_path}, + cause=exc, + ) from exc + except subprocess.CalledProcessError as exc: + raise HarnessError( + ORACLE_FAILURE, + Path(cmd[0]).name, + f"OpenFOAM command failed with exit code {exc.returncode}: {' '.join(cmd)}", + details={"cmd": cmd, "returncode": exc.returncode, "log_path": log_path}, + cause=exc, + ) from exc + return { + "cmd": cmd, + "returncode": 0, + "log_path": log_path, + "duration_seconds": round(time.monotonic() - started, 6), + "timeout_seconds": timeout, + } + + +def patch_start_from_latest(case: Path) -> None: + path = case / "system/controlDict" + text = path.read_text() + old = "startFrom startTime;" + new = "startFrom latestTime;" + if old not in text: + raise AssertionError(f"{path}: missing {old!r}") + path.write_text(text.replace(old, new, 1)) + + +def import_foam() -> Any: + apply_openfoam_env() + import foam_stepper as foam + + return foam + + +def prepare_work_cases(source: Path, work: Path, *, include_split: bool) -> dict[str, Any]: + if work.exists(): + shutil.rmtree(work) + work.mkdir(parents=True) + oracle = work / "oracle_case" + run_one = work / "run_one_case" + split = work / "split_case" if include_split else None + + oracle_meta = prepare_case(source, oracle, end_time=1) + run_one_meta = prepare_case(source, run_one, end_time=1) + split_meta = prepare_case(source, split, end_time=1) if split is not None else None + + return { + "oracle_case": oracle, + "run_one_case": run_one, + "split_case": split, + "metadata": { + "oracle": oracle_meta, + "run_one": run_one_meta, + "split": split_meta, + }, + } + + +def make_stepper(foam: Any, case: Path, label: str) -> Any: + try: + return foam.Case(case).make_stepper() + except Exception as exc: + raise HarnessError( + LOADING_FAILURE, + f"load_{label}_case", + f"failed to load {label} case with foam_stepper: {case}", + details={"case": case}, + cause=exc, + ) from exc + + +def read_mesh_identity(stepper: Any, label: str) -> dict[str, Any]: + try: + return mesh_identity(stepper.mesh()) + except Exception as exc: + raise HarnessError( + LOADING_FAILURE, + f"read_{label}_mesh", + f"failed to read {label} mesh identity", + details={"case": getattr(stepper, "case_path", "")}, + cause=exc, + ) from exc + + +def read_fields(stepper: Any, label: str) -> dict[str, Any]: + try: + return field_dict_to_mapping(stepper.fields()) + except Exception as exc: + raise HarnessError( + LOADING_FAILURE, + f"read_{label}_fields", + f"failed to read {label} fields", + details={"case": getattr(stepper, "case_path", "")}, + cause=exc, + ) from exc + + +def checked_step(stages: list[dict[str, Any]], name: str, fn: Any) -> Any: + try: + result = fn() + except Exception as exc: + raise HarnessError( + STEPPER_FAILURE, + name, + f"split-step stage failed: {name}", + cause=exc, + ) from exc + stages.append(transform_summary(result)) + return result + + +def run_split_iteration(stepper: Any) -> dict[str, Any]: + stages: list[dict[str, Any]] = [] + checked_step(stages, "pre_solve", stepper.pre_solve) + checked_step(stages, "advance_time", stepper.advance_time) + begin = checked_step(stages, "begin_pimple_iteration", stepper.begin_pimple_iteration) + if begin.outputs.get("active") is not True: + raise HarnessError( + STEPPER_FAILURE, + "begin_pimple_iteration", + "split-step PIMPLE iteration was inactive", + details={"outputs": summarize_value(begin.outputs)}, + ) + + checked_step(stages, "fv_models_correct", stepper.fv_models_correct) + checked_step(stages, "pre_predictor", stepper.pre_predictor) + checked_step(stages, "momentum_transport_predict", stepper.momentum_transport_predictor) + terms = checked_step(stages, "assemble_momentum_terms", stepper.assemble_momentum_terms) + UEqn = checked_step(stages, "assemble_UEqn", stepper.assemble_momentum_matrix) + checked_step(stages, "relax_UEqn", stepper.relax_matrix) + checked_step(stages, "constrain_UEqn", stepper.constrain_matrix) + checked_step(stages, "solve_UEqn", stepper.solve_momentum) + checked_step(stages, "compute_pressure_inputs", stepper.compute_pressure_inputs) + pEqn = checked_step(stages, "assemble_pEqn", stepper.assemble_pressure_matrix) + checked_step(stages, "solve_pEqn", stepper.solve_pressure) + checked_step(stages, "correct_velocity_pressure_flux", stepper.correct_velocity_pressure_flux) + checked_step(stages, "momentum_transport_correct", stepper.momentum_transport_corrector) + checked_step(stages, "end_pimple_iteration", stepper.end_pimple_iteration) + checked_step(stages, "post_solve", lambda: stepper.post_solve(write=False)) + + try: + fields = field_dict_to_mapping(stepper.fields()) + except Exception as exc: + raise HarnessError( + STEPPER_FAILURE, + "split_fields", + "failed to read split-step fields after execution", + cause=exc, + ) from exc + + momentum_terms = [term.get("name", "") for term in terms.outputs.get("terms", [])] + UEqn_matrix = UEqn.outputs["UEqn"] + pEqn_matrix = pEqn.outputs["pEqn"] + return { + "fields": fields, + "stages": stages, + "graph": [stage["name"] for stage in stages], + "momentum_terms": momentum_terms, + "UEqn": matrix_summary(UEqn_matrix), + "pEqn": matrix_summary(pEqn_matrix), + } + + +def visible_turbulence_fields(fields: Mapping[str, Any]) -> list[str]: + return [name for name in TURBULENCE_FIELDS if name in fields] + + +def base_report(args: argparse.Namespace) -> dict[str, Any]: + report_path = args.report if args.report is not None else args.work / "verifier_report.json" + return { + "harness": { + "name": "airfrans_stepper_verifier", + "spec": "VERIFIER_HARNESS_SPEC.md", + "schema_version": 1, + }, + "status": "running", + "failure": None, + "paths": { + "root": ROOT, + "source": args.source, + "work": args.work, + "report": report_path, + }, + "source_policy": "raw AirfRANS source is read-only; all solver runs use prepared work-directory copies", + "tolerances": { + "rtol": args.rtol, + "atol": args.atol, + "policy": "CPU stepper parity with the repository OpenFOAM oracle must pass np.allclose for every required field.", + }, + "required_fields": { + "primary": list(PRIMARY_FIELDS), + "turbulence": list(TURBULENCE_FIELDS), + "all": list(REQUIRED_FIELDS), + }, + "commands": [], + "case_preparation": {}, + "mesh_identity": {}, + "modes": { + "run_one": {"enabled": True}, + "split": {"enabled": not args.skip_split}, + }, + "tracked_turbulence_fields": {}, + "comparison_mismatches": [], + } + + +def run_harness(args: argparse.Namespace, report: dict[str, Any]) -> None: + try: + prepared = prepare_work_cases(args.source, args.work, include_split=not args.skip_split) + except Exception as exc: + raise HarnessError( + LOADING_FAILURE, + "prepare_cases", + "failed to prepare reproducible AirfRANS work cases", + details={"source": args.source, "work": args.work}, + cause=exc, + ) from exc + + oracle_case = prepared["oracle_case"] + run_one_case = prepared["run_one_case"] + split_case = prepared["split_case"] + report["case_preparation"] = json_ready(prepared) + + report["commands"].append( + run_openfoam_command(["checkMesh", "-case", str(oracle_case), "-constant"], log_path=args.work / "checkMesh.log", timeout=120) + ) + report["commands"].append( + run_openfoam_command( + ["foamRun", "-case", str(oracle_case), "-solver", "incompressibleFluid", "-noFunctionObjects"], + log_path=args.work / "foamRun_oracle.log", + timeout=600, + ) + ) + + try: + patch_start_from_latest(oracle_case) + except Exception as exc: + raise HarnessError( + LOADING_FAILURE, + "select_oracle_latest_time", + "failed to configure oracle case to load latestTime output", + details={"case": oracle_case}, + cause=exc, + ) from exc + + try: + foam = import_foam() + except Exception as exc: + raise HarnessError( + LOADING_FAILURE, + "import_foam_stepper", + "failed to import foam_stepper in the repository OpenFOAM environment", + cause=exc, + ) from exc + + oracle_stepper = make_stepper(foam, oracle_case, "oracle") + oracle_mesh = read_mesh_identity(oracle_stepper, "oracle") + oracle_fields = read_fields(oracle_stepper, "oracle") + report["mesh_identity"]["oracle"] = oracle_mesh + report["modes"]["oracle"] = { + "case": oracle_case, + "fields": selected_field_summaries(oracle_fields), + } + report["tracked_turbulence_fields"]["oracle"] = visible_turbulence_fields(oracle_fields) + + run_one_stepper = make_stepper(foam, run_one_case, "run_one") + run_one_mesh = read_mesh_identity(run_one_stepper, "run_one") + report["mesh_identity"]["run_one"] = run_one_mesh + mesh_report, mesh_mismatches = compare_mesh_identity("run_one", run_one_mesh, oracle_mesh) + report["modes"]["run_one"]["mesh_comparison"] = mesh_report + + try: + run_one_result = run_one_stepper.run_one_pimple_iteration() + run_one_fields = field_dict_to_mapping(run_one_result.outputs["fields"]) + except Exception as exc: + raise HarnessError( + STEPPER_FAILURE, + "run_one_pimple_iteration", + "full one-iteration stepper execution failed", + details={"case": run_one_case}, + cause=exc, + ) from exc + + run_one_comparisons, run_one_mismatches = compare_fields( + "run_one", + run_one_fields, + oracle_fields, + rtol=args.rtol, + atol=args.atol, + ) + report["modes"]["run_one"].update( + { + "case": run_one_case, + "stage": transform_summary(run_one_result), + "graph": [entry.name for entry in run_one_result.outputs.get("graph", [])], + "fields": selected_field_summaries(run_one_fields), + "comparisons": run_one_comparisons, + } + ) + report["tracked_turbulence_fields"]["run_one"] = visible_turbulence_fields(run_one_fields) + + mismatches = mesh_mismatches + run_one_mismatches + + if split_case is not None: + split_stepper = make_stepper(foam, split_case, "split") + split_mesh = read_mesh_identity(split_stepper, "split") + report["mesh_identity"]["split"] = split_mesh + split_mesh_report, split_mesh_mismatches = compare_mesh_identity("split", split_mesh, oracle_mesh) + report["modes"]["split"]["mesh_comparison"] = split_mesh_report + + split_result = run_split_iteration(split_stepper) + split_fields = split_result["fields"] + split_comparisons, split_mismatches = compare_fields( + "split", + split_fields, + oracle_fields, + rtol=args.rtol, + atol=args.atol, + ) + report["modes"]["split"].update( + { + "case": split_case, + "graph": split_result["graph"], + "stages": split_result["stages"], + "momentum_terms": split_result["momentum_terms"], + "UEqn": split_result["UEqn"], + "pEqn": split_result["pEqn"], + "fields": selected_field_summaries(split_fields), + "comparisons": split_comparisons, + } + ) + report["tracked_turbulence_fields"]["split"] = visible_turbulence_fields(split_fields) + mismatches.extend(split_mesh_mismatches) + mismatches.extend(split_mismatches) + + report["comparison_mismatches"] = json_ready(mismatches) + if mismatches: + raise HarnessError( + COMPARISON_FAILURE, + "compare_oracle_stepper_outputs", + f"{len(mismatches)} verifier comparison mismatch(es) observed", + details={"mismatches": mismatches}, + ) + + +def write_report(report: Mapping[str, Any], path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(json_ready(report), indent=2, sort_keys=True, allow_nan=False) + "\n") + + +def fmt_sci(value: Any) -> str: + if value is None: + return "none" + try: + return f"{float(value):.3e}" + except (TypeError, ValueError): + return str(value) + + +def format_location(location: Mapping[str, Any] | None) -> str: + if not location: + return "none" + component = location.get("component_index") + component_text = "" if component is None else f" component={component}" + return f"{location.get('entity_kind')}[{location.get('entity_index')}]{component_text}" + + +def print_human_summary(report: Mapping[str, Any]) -> None: + status = report.get("status") + print(f"airfrans_stepper_verification {status}") + print(f"source={report['paths']['source']}") + print(f"work={report['paths']['work']}") + print(f"report={report['paths']['report']}") + print(f"rtol={report['tolerances']['rtol']} atol={report['tolerances']['atol']}") + + if status != "passed": + failure = report.get("failure") or {} + print(f"failure_category={failure.get('category')}") + print(f"failure_step={failure.get('step')}") + print(f"failure_message={failure.get('message')}") + return + + oracle_mesh = report["mesh_identity"]["oracle"] + patch_names = [patch["name"] for patch in oracle_mesh["patches"]] + print(f"oracle_case={report['case_preparation']['oracle_case']}") + print(f"run_one_case={report['modes']['run_one']['case']}") + if report["modes"].get("split", {}).get("enabled"): + print(f"split_case={report['modes']['split']['case']}") + print(f"mesh_cells={oracle_mesh['n_cells']}") + print(f"mesh_internal_faces={oracle_mesh['n_internal_faces']}") + print(f"mesh_patches={patch_names}") + print(f"mesh_topology_sha256={oracle_mesh['topology_sha256']}") + + for mode in ("run_one", "split"): + mode_report = report["modes"].get(mode, {}) + if not mode_report.get("enabled", False): + continue + for field_name in REQUIRED_FIELDS: + data = mode_report.get("comparisons", {}).get(field_name) + if not data: + continue + print( + f"mode={mode} field={field_name} actual_shape={tuple(data['actual_shape'])} " + f"expected_shape={tuple(data['expected_shape'])} max_abs={fmt_sci(data.get('max_abs'))} " + f"location={format_location(data.get('location'))} allclose={data['allclose']}" + ) + graph = mode_report.get("graph") + if graph: + print(f"{mode}_graph={graph}") + + split_report = report["modes"].get("split", {}) + if split_report.get("enabled") and "UEqn" in split_report and "pEqn" in split_report: + print(f"split_momentum_terms={split_report.get('momentum_terms')}") + print(f"split_UEqn_diag_shape={tuple(split_report['UEqn']['diag']['shape'])}") + print(f"split_pEqn_diag_shape={tuple(split_report['pEqn']['diag']['shape'])}") + + print(f"visible_turbulence_fields={report['tracked_turbulence_fields']}") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE) + parser.add_argument("--work", type=Path, default=DEFAULT_WORK) + parser.add_argument("--report", type=Path, default=None, help="JSON report path; defaults to WORK/verifier_report.json") + parser.add_argument("--rtol", type=float, default=1e-8) + parser.add_argument("--atol", type=float, default=1e-8) + parser.add_argument("--skip-split", action="store_true", help="Skip the explicit split-step parity check for local debugging") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + args.source = args.source.resolve() + args.work = args.work.resolve() + if args.report is None: + args.report = args.work / "verifier_report.json" + else: + args.report = args.report.resolve() + + report = base_report(args) + exit_code = 0 + try: + run_harness(args, report) + report["status"] = "passed" + except HarnessError as exc: + report["status"] = "failed" + report["failure"] = exc.to_dict() + exit_code = EXIT_CODES.get(exc.category, EXIT_CODES[INTERNAL_FAILURE]) + except Exception as exc: # pragma: no cover - keeps CLI failures categorized in the report. + report["status"] = "failed" + report["failure"] = json_ready( + { + "category": INTERNAL_FAILURE, + "step": "run_harness", + "message": str(exc), + "cause": {"type": type(exc).__name__, "message": str(exc)}, + "traceback": traceback.format_exc(), + } + ) + exit_code = EXIT_CODES[INTERNAL_FAILURE] + + try: + write_report(report, args.report) + except Exception as exc: + report["status"] = "failed" + report["failure"] = json_ready( + { + "category": INTERNAL_FAILURE, + "step": "write_report", + "message": f"failed to write verifier report: {args.report}", + "cause": {"type": type(exc).__name__, "message": str(exc)}, + } + ) + exit_code = EXIT_CODES[INTERNAL_FAILURE] + + print_human_summary(json_ready(report)) + return exit_code + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_python_stepper.py b/scripts/verify_python_stepper.py new file mode 100755 index 0000000..93855b0 --- /dev/null +++ b/scripts/verify_python_stepper.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Scenario checks for the Python-driven OpenFOAM observability stepper.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from pathlib import Path + + +def _drop_ambient_pythonpath() -> None: + pythonpath = os.environ.pop("PYTHONPATH", "") + for entry in pythonpath.split(os.pathsep): + if not entry: + continue + while entry in sys.path: + sys.path.remove(entry) + + +_drop_ambient_pythonpath() + +import numpy as np + +from openfoam_env import apply_openfoam_env, openfoam_env + +ROOT = Path(__file__).resolve().parents[1] +TUTORIAL = ROOT / "OpenFOAM-14/tutorials/incompressibleFluid/venturiTube" +WORK = ROOT / "tmp/python_stepper_verify" +LOAD_CASE = WORK / "load_case" +TRANSFORM_CASE = WORK / "transform_case" +PY_EQ_CASE = WORK / "python_equivalence_case" +FOAM_EQ_CASE = WORK / "foam_equivalence_case" + + +def run(cmd: list[str]) -> None: + subprocess.run(cmd, cwd=ROOT, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, env=openfoam_env()) + + +def patch_control_dict(case: Path, *, latest: bool = False) -> None: + path = case / "system/controlDict" + text = path.read_text() + replacements = { + "startFrom startTime;": "startFrom latestTime;" if latest else "startFrom startTime;", + "endTime 1000;": "endTime 1;", + "writeInterval 50;": "writeInterval 1;", + } + for old, new in replacements.items(): + text = text.replace(old, new) + path.write_text(text) + + +def prepare_case(dst: Path) -> None: + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(TUTORIAL, dst, ignore=shutil.ignore_patterns("processor*", "postProcessing", "*.log")) + for orig in (dst / "0").glob("*.orig"): + shutil.copyfile(orig, orig.with_suffix("")) + patch_control_dict(dst) + run(["blockMesh", "-case", str(dst)]) + run(["createZones", "-case", str(dst)]) + +def import_foam(): + apply_openfoam_env() + import foam_stepper as foam + + return foam + + + +def load_scenario() -> None: + foam = import_foam() + case = foam.Case(LOAD_CASE) + stepper = case.make_stepper() + mesh = stepper.mesh() + fields = stepper.fields() + assert mesh.n_cells == 57600 + assert mesh.n_internal_faces == fields.phi.internal.shape[0] + assert fields.U.internal.shape == (mesh.n_cells, 3) + assert fields.p.internal.shape == (mesh.n_cells,) + assert fields.phi.internal.shape == (mesh.n_internal_faces,) + assert {patch.name for patch in mesh.boundary} == {"inlet", "outlet", "walls"} + + try: + stepper.assemble_pressure_matrix() + except foam.OpenFoamError as exc: + assert "Momentum matrix is not assembled" in str(exc) + else: + raise AssertionError("assemble_pressure_matrix succeeded before momentum assembly") + + +def transformation_scenario() -> None: + foam = import_foam() + stepper = foam.Case(TRANSFORM_CASE).make_stepper() + stepper.pre_solve() + stepper.advance_time() + begin = stepper.begin_pimple_iteration() + assert begin.outputs["active"] is True + + terms = stepper.assemble_momentum_terms() + names = {term["name"] for term in terms.outputs["terms"]} + assert { + "assemble_momentum_ddt", + "assemble_momentum_div_phi_U", + "assemble_momentum_MRF_DDt", + "assemble_momentum_divDevSigma", + "assemble_momentum_sources", + "assemble_momentum_pressure_rhs_grad_p", + }.issubset(names) + + UEqn = stepper.assemble_momentum_matrix().outputs["UEqn"] + assert UEqn.diag.shape == (57600,) + assert UEqn.psi.internal.shape == (57600, 3) + assert UEqn.source.shape == (57600, 3) + assert UEqn.H is not None + + stepper.relax_matrix() + stepper.constrain_matrix() + momentum = stepper.solve_momentum() + assert momentum.outputs["performance"].field_name == "U" + + pressure_inputs = stepper.compute_pressure_inputs() + assert pressure_inputs.outputs["rAU"].internal.shape == (57600,) + assert pressure_inputs.outputs["HbyA"].internal.shape == (57600, 3) + assert pressure_inputs.outputs["phiHbyA"].internal.shape == (170624,) + + pEqn = stepper.assemble_pressure_matrix().outputs["pEqn"] + assert pEqn.diag.shape == (57600,) + assert pEqn.psi.internal.shape == (57600,) + assert pEqn.flux is not None + + pressure = stepper.solve_pressure() + assert pressure.name == "solve_pEqn" + assert pressure.outputs["performance"].field_name == "p" + assert pressure.outputs["p"].internal.shape == (57600,) + assert pressure.outputs["phi"].internal.shape == (170624,) + + correction = stepper.correct_velocity_pressure_flux() + assert correction.outputs["U"].internal.shape == (57600, 3) + assert correction.outputs["phi"].internal.shape == (170624,) + + +def equivalence_scenario() -> None: + foam = import_foam() + run(["foamRun", "-case", str(FOAM_EQ_CASE), "-solver", "incompressibleFluid", "-noFunctionObjects"]) + patch_control_dict(FOAM_EQ_CASE, latest=True) + baseline = foam.Case(FOAM_EQ_CASE).make_stepper().fields() + + result = foam.Case(PY_EQ_CASE).make_stepper().run_one_pimple_iteration() + actual = result.outputs["fields"] + + comparisons = { + "U": (actual["U"].internal, baseline.U.internal), + "p": (actual["p"].internal, baseline.p.internal), + "phi": (actual["phi"].internal, baseline.phi.internal), + } + for name, (lhs, rhs) in comparisons.items(): + if lhs.shape != rhs.shape: + raise AssertionError(f"{name} shape mismatch: {lhs.shape} != {rhs.shape}") + max_abs = float(np.max(np.abs(lhs - rhs))) + if not np.allclose(lhs, rhs, rtol=1e-7, atol=1e-7): + raise AssertionError(f"{name} mismatch: max_abs={max_abs}") + print(f"equivalence {name}: max_abs={max_abs:.3e}") + + graph = result.outputs["graph"] + graph_names = {entry.name for entry in graph} + assert {"pre_solve", "advance_time", "assemble_UEqn", "solve_UEqn", "assemble_pEqn", "solve_pEqn"}.issubset(graph_names) + + +def main() -> None: + if len(sys.argv) == 2: + scenarios = { + "load": load_scenario, + "transformation": transformation_scenario, + "equivalence": equivalence_scenario, + } + scenarios[sys.argv[1]]() + return + + if WORK.exists(): + shutil.rmtree(WORK) + WORK.mkdir(parents=True) + for case_dir in (LOAD_CASE, TRANSFORM_CASE, PY_EQ_CASE, FOAM_EQ_CASE): + prepare_case(case_dir) + + for scenario in ("load", "transformation", "equivalence"): + subprocess.run([sys.executable, __file__, scenario], cwd=ROOT, check=True, env=openfoam_env()) + + print("python stepper verification passed") + + +if __name__ == "__main__": + main() diff --git a/scripts/verify_python_stepper.sh b/scripts/verify_python_stepper.sh new file mode 100755 index 0000000..c45f4a0 --- /dev/null +++ b/scripts/verify_python_stepper.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +JOBS="${JOBS:-$(nproc)}" +PYTHON_BIN="${PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}" + +if [[ ! -x "${PYTHON_BIN}" ]]; then + uv sync --dev +fi + +if ! PYTHONPATH= "${PYTHON_BIN}" -c 'import pybind11, numpy' >/dev/null 2>&1; then + uv sync --dev +fi + +"${ROOT_DIR}/scripts/build_openfoam_airfrans_subset.sh" >/dev/null + +# blockMesh/createZones are verification fixtures for deterministic tutorial cases. +set +u +source "${ROOT_DIR}/OpenFOAM-14/etc/bashrc" \ + WM_MPLIB=Dummy \ + ParaView_TYPE=none \ + SCOTCH_TYPE=none \ + ZOLTAN_TYPE=none +set -u + +# OpenFOAM's SIGFPE trap is useful for solver binaries but unsafe for Python's +# own floating-point formatting/comparison code inside the verification process. +unset FOAM_SIGFPE + +( + cd "${ROOT_DIR}/OpenFOAM-14" + wmake -j "${JOBS}" libso src/mesh/blockMesh >/dev/null + wmake -j "${JOBS}" applications/utilities/mesh/generation/blockMesh >/dev/null + wmake -j "${JOBS}" applications/utilities/mesh/manipulation/createZones >/dev/null +) + +"${ROOT_DIR}/scripts/build_python_stepper.sh" >/dev/null + +"${PYTHON_BIN}" "${ROOT_DIR}/scripts/verify_python_stepper.py" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..002e489 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1801 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, +] + +[[package]] +name = "async-lru" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "bleach" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/27f65b805d7fe005c44e1a36b9183ecdfbcdbf9d3e721a5115d461ecc7ee/debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9", size = 5336340, upload-time = "2026-06-01T19:30:54.047Z" }, + { url = "https://files.pythonhosted.org/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c", size = 5374751, upload-time = "2026-06-01T19:30:55.891Z" }, + { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, + { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "foam-stepper" +version = "0.2.0" +source = { editable = "python" } +dependencies = [ + { name = "numpy" }, +] + +[package.metadata] +requires-dist = [{ name = "numpy", specifier = ">=2.0" }] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio2" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, +] + +[[package]] +name = "ipython" +version = "9.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "json5" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278, upload-time = "2026-06-19T20:08:27.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570, upload-time = "2026-06-19T20:08:26.748Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-builder" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/61/47f7ae054f5cd3983c10e1d65a6eb7fcd4b87ebb1056e190ef7d63ff4f19/jupyter_builder-1.1.1.tar.gz", hash = "sha256:1a13977912b08deda77fce2c803940131c27cf77a27ed64b9ffca25aa0ed7e6c", size = 971667, upload-time = "2026-07-17T13:14:47.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/cc/f6a12de1c890ea5dd2816c5c76d5ac6d3ed52db3c37f78691328207d13b9/jupyter_builder-1.1.1-py3-none-any.whl", hash = "sha256:f9c14bc55c0488a073f62af12d468936fcf9ecb7e9dd802f6f9c33de46ad70db", size = 913264, upload-time = "2026-07-17T13:14:45.857Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-builder" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/7f/51c0c856ab286bdaf5709cf61ed13584ed9d4bee906479707da45b11b353/jupyterlab-4.6.2.tar.gz", hash = "sha256:e18ce8b34f3de350e93cd5b2c4f3ae884cbe266eb76bf5d6825a4ed34c13bcff", size = 28183650, upload-time = "2026-07-21T12:05:24.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/1f/e39b248c76bb3736bc05a9491a6aa1315414c32dea12f548ecc1b24c758e/jupyterlab-4.6.2-py3-none-any.whl", hash = "sha256:5964447036629adfcd3fc0969effc1da6f47d2cbd0a60b2c2eea7c31be0ec6a8", size = 17166703, upload-time = "2026-07-21T12:05:19.818Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426, upload-time = "2026-07-22T05:22:30.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" }, +] + +[[package]] +name = "nbclient" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio2" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +] + +[[package]] +name = "openfoam-rans-to-gpu" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "foam-stepper" }, + { name = "numpy" }, + { name = "pybind11" }, + { name = "quadrants" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ipykernel" }, + { name = "jupyterlab" }, + { name = "nbclient" }, + { name = "nbformat" }, +] + +[package.metadata] +requires-dist = [ + { name = "foam-stepper", editable = "python" }, + { name = "numpy", specifier = ">=2.0" }, + { name = "pybind11", specifier = ">=2.13" }, + { name = "quadrants", specifier = ">=1.1.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "ipykernel", specifier = ">=6.29" }, + { name = "jupyterlab", specifier = ">=4.2" }, + { name = "nbclient", specifier = ">=0.10" }, + { name = "nbformat", specifier = ">=5.10" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pybind11" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/f0/35145a3c3baffeef55d4b8324caa33abaa8fa56ab345ecd4b2211d09163e/pybind11-3.0.4.tar.gz", hash = "sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c", size = 589533, upload-time = "2026-04-19T03:08:15.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/06/c3a23c9a0263b136c519f033a58d4641e73065fefc7754e9667ec206d992/pybind11-3.0.4-py3-none-any.whl", hash = "sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63", size = 314166, upload-time = "2026-04-19T03:08:14.091Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/2d27f30c59a67be7025b2d7858c8c2d282b74d66544b2384730b82de74fd/pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b", size = 16223484, upload-time = "2026-06-11T00:11:58.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/34/942cc95ca4e26489875aa8a95192766247a687379ec29543eebe73ec945f/pywinpty-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:d62946adf14b15b54c0b8d785f93fe18b04da23f4ad59e2e8c4612646e9abd23", size = 2090915, upload-time = "2026-06-10T23:43:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/5b9053004844139ea8bd86209c57ade12b134b2782f383a095784c8531ec/pywinpty-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:e9391c05fbfa7a992a97e831fc6849887b4014a614192e3d984a7ca59592b376", size = 815934, upload-time = "2026-06-10T23:41:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f4/2a464b9893cceb3b3f416356e94fdc3e1bca9476993927e4e6d99fe95382/pywinpty-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:48db1b0ad9d0a1b81dcaaa7163a99a7808deaceb0c1b2344716dc1fc090c3c4c", size = 2090471, upload-time = "2026-06-10T23:42:11.071Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2c/a138491a0afbdb50eb79395577bd326d4b0fbde7209417d1a8087ff2493a/pywinpty-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:2c6008fb2d3774b48693b2fcb7f2cc317ade9dc581289a964ffeeaf81307c9b5", size = 815518, upload-time = "2026-06-10T23:42:02.363Z" }, + { url = "https://files.pythonhosted.org/packages/6f/15/54400049a380582acd1282665c70fcf11e0bd3713679aca78e24c3aae738/pywinpty-3.0.5-cp313-cp313t-win_amd64.whl", hash = "sha256:22ce1b780d89821cc52daf6eac0708af22d93d000ce9c7c07e37489db8594598", size = 2089920, upload-time = "2026-06-10T23:44:13.395Z" }, + { url = "https://files.pythonhosted.org/packages/94/0c/6f24f3c0799f502259b24bdf841a99ad2b0d59df5c2525b4e2a286d14be2/pywinpty-3.0.5-cp313-cp313t-win_arm64.whl", hash = "sha256:9c2919a81bc5cfb09b86fc5a002112b2de95ca4304a07413cbeeb746a1307a5c", size = 814520, upload-time = "2026-06-10T23:43:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/f3cd1b1e5fc56517f54452c49f92049e7dd9ffc8a63de22a495581f50d04/pywinpty-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:03bb3c16d691d9242267201830bcd0e64a9b663170e9042bc84b210da9de15ac", size = 2090663, upload-time = "2026-06-10T23:43:59.845Z" }, + { url = "https://files.pythonhosted.org/packages/9d/dd/96d6cbfc6d9ddab5c1c2f92c26545ae8997446a2ba7ee2024cd43c81f49b/pywinpty-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:89c5c6ef08997a3b4b277b214a35fe15cab4dd6d119f0140aa71df5b1168fdbc", size = 815700, upload-time = "2026-06-10T23:40:50.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/36/d98087bce0acaa4cce7f196103cfa7be3f63ce65f52473bb3e38784ae5d9/pywinpty-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7b566165e0c5fdd6abe167a5ac8b954be6a843eb55a85946576d6bc1dea03d6d", size = 2090093, upload-time = "2026-06-10T23:40:58.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/fd/fe2b0db922ba052ce3976a08f3fc05d0c05047c8b4ebb6102e832b8ef563/pywinpty-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:24366280a8aa677323da87bec729cb3ea3b35367386cece0978bdc6e4695c690", size = 814517, upload-time = "2026-06-10T23:42:34.946Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, +] + +[[package]] +name = "quadrants" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "colorama" }, + { name = "dill" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "rich" }, + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/55/4931a0f599726aa53ffb3ffed4b997374dde62d49aaf52cec975bae8aaba/quadrants-1.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7655ce9471e016924cdca7e7e6556c711414ad3776173966b5a2deccadf61552", size = 26664368, upload-time = "2026-07-17T21:39:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a0/48ffbf7daace082d13b3ca56934329d992235c18366ee5ab70aac3a2ba73/quadrants-1.1.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:08a248df81d35ee5f70075e8defe7d5a1891d04e7aaffd51d0c1f9049853ce73", size = 38273767, upload-time = "2026-07-17T21:43:40.905Z" }, + { url = "https://files.pythonhosted.org/packages/40/d7/a01a36662187ed0be39280d6992f9dd443eac4be2b62de91cfbf9328470f/quadrants-1.1.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:273983d55683f15471a9909a94a5cd8c20db0ff2335dff446a8d92d87aee9efd", size = 40951378, upload-time = "2026-07-17T21:43:49.962Z" }, + { url = "https://files.pythonhosted.org/packages/cf/32/e2decde7e8f545edcd18dcc25b63acdb641895cb59ffbf010189a301c185/quadrants-1.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:2578b90ec3b26fb111412f421708282b5b16bda6a89f99a458c3792730503cc3", size = 30971283, upload-time = "2026-07-17T21:13:00.538Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/bd25caf6bb258384bd0b88d0e783021246e461427fa078e212aa106f4d9e/quadrants-1.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:55dadb45183bfa17879d456903a0419a1854aecaf80a3c6cb94c28b9b8c3de53", size = 26665102, upload-time = "2026-07-17T21:39:22.103Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a1/29338af40c1e4187a28c00c087660d8c64715e00c0d7e0512817de17af03/quadrants-1.1.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:1a4b598e594ed79fa7dbb2ca71ab340855b5cf5440bdd319a156a972bdc4e78b", size = 38268994, upload-time = "2026-07-17T21:43:40.84Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/15f899bcb6bb798073cd40d888dde658398aa67364307d3b2be356f0b332/quadrants-1.1.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:500cb173aec4c06adaaae051f27bd41556bac02c9ca0429685c098e36911ebd8", size = 40952032, upload-time = "2026-07-17T21:43:41.683Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9b/db1d1ff8f72653c7904901b2699ca4d9462928b281d5782e8acc93dcde60/quadrants-1.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:e8ec9bfa27856017e941753f375d62fd400c7d4ff13faed523986f3aa6bcd26b", size = 30970206, upload-time = "2026-07-17T21:12:53.139Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, +] + +[[package]] +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, +] + +[[package]] +name = "traitlets" +version = "5.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +]