From ebb77ca52e4ffaece3552b64ee9bd725bae47698 Mon Sep 17 00:00:00 2001 From: James Le Houx <37665786+jameslehoux@users.noreply.github.com> Date: Sat, 13 Jun 2026 08:53:26 +0000 Subject: [PATCH 1/3] docs: refresh CLAUDE.md to match current architecture - Replace outdated Fortran-kernel references with native C++ kernels (TortuosityKernels.H); the .F90/*_F.H files no longer exist - Add TortuosityMLMG matrix-free solver and TortuositySolverBase abstraction - Add homogenization (DeffTensor), microstructure modules (SSA, PSD, connected components, through-thickness, REV study) to the file reference - Document the Python/pybind11 layer and pure-Python fallback - Update data-flow and test tables (MLMG, synthetic, Diffusion integration, validation, pytest) --- CLAUDE.md | 109 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 30 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8ae9365a..6d986863 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,15 +23,19 @@ for linear solves. └────────────────┘ └────────────────┘ └──────────────────┘ TiffReader.H/cpp TortuosityHypre ResultsJSON.H - HDF5Reader.H/cpp EffDiffusivityHypre → BPX, BattINFO - RawReader.H/cpp VolumeFraction → JSON + text output - DatReader.H/cpp PercolationCheck - TortuosityDirect + HDF5Reader.H/cpp TortuosityMLMG → BPX, BattINFO + RawReader.H/cpp EffDiffusivityHypre → JSON + text output + DatReader.H/cpp VolumeFraction + PercolationCheck │ ┌──────────▼──────────┐ - │ Fortran Kernels │ ← Computational hot path - │ (*_F.H ↔ *.F90) │ Matrix fill, flux calc + │ C++ Kernels │ ← Computational hot path + │ (TortuosityKernels │ Matrix fill, flux calc + │ .H, AMReX GPU) │ (AMReX ParallelFor/Array4) └─────────────────────┘ + +A Python layer (`python/`) wraps the C++ libraries via pybind11 and adds a +NumPy-native high-level API plus a pure-Python SciPy/CuPy fallback. ``` ### Module Relationships @@ -41,17 +45,28 @@ MultiFab). All readers produce the same output type — a phase-labeled voxel grid. The readers are independent of the physics layer. **Physics solvers** (`src/props/`) operate on `iMultiFab` phase data: -- `TortuosityHypre` — Solves ∇·(D∇φ) = 0 with Dirichlet BCs to get τ +- `TortuosityHypre` — Solves ∇·(D∇φ) = 0 with Dirichlet BCs to get τ, using + HYPRE structured-grid Krylov solvers + SMG/PFMG multigrid preconditioners +- `TortuosityMLMG` — Same PDE via AMReX's matrix-free MLMG geometric multigrid + (embedded boundary for inactive cells); ~3× less memory, fastest on GPU - `EffectiveDiffusivityHypre` — Solves cell problem ∇·(D∇χ) = -∇·(Dê) for the effective diffusivity tensor via homogenization - `PercolationCheck` — Flood-fill connectivity check (no solver needed) - `VolumeFraction` — Phase counting with MPI reduction - `TortuosityDirect` — Legacy iterative solver (Forward Euler, not HYPRE) -**Fortran interop** — The HYPRE matrix fill and flux calculations are in -Fortran 90 for performance. C interface headers (`*_F.H`) bridge C++ ↔ Fortran -with explicit documentation of index convention differences (C: 0-based, -Fortran: 1-based). +`TortuosityHypre`, `TortuosityMLMG`, and `TortuosityDirect` share +`TortuositySolverBase`, which factors out the solver-independent work: +building the diffusion-coefficient field, removing isolated cells, the +flood-fill activity mask, and post-solve boundary-flux integration → +conservation check → τ. Each backend just implements `solve()`. + +**Native C++ kernels** — The HYPRE/MLMG matrix fill and flux calculations were +historically Fortran 90 but have been **migrated to native C++** +(`TortuosityKernels.H`, using AMReX `ParallelFor`/`Array4`) so the hot path +runs on CPU and GPU (CUDA/HIP) from a single source. There are no `.F90` / +`*_F.H` files in the tree; CMake still lists Fortran as a language for AMReX +compatibility only. **Configuration** is via AMReX `ParmParse` (text `inputs` files). Key types: - `PhysicsConfig.H` — Maps solver output to physical quantities (diffusion, @@ -60,14 +75,20 @@ Fortran: 1-based). ### Key Data Flow +`Diffusion.cpp` selects one of four modes via the `calculation_method` / +`dry_run` / `rev.do_study` inputs: `dry_run` (percolation + volume fraction +only), REV study, `homogenization` (D_eff tensor), or `flow_through` +(tortuosity). + ``` -TIFF/HDF5/RAW file +TIFF/HDF5/RAW/DAT file → Reader.threshold() → iMultiFab (phase IDs: 0, 1, ...) → PercolationCheck (is phase connected inlet→outlet?) → VolumeFraction (what fraction is this phase?) - → TortuosityHypre or EffDiffusivityHypre - → Fortran kernel fills HYPRE matrix (harmonic mean face coefficients) - → HYPRE solve (FlexGMRES, PCG, etc.) + → TortuosityHypre / TortuosityMLMG (flow_through) + or EffectiveDiffusivityHypre + DeffTensor (homogenization) + → C++ kernel fills system (harmonic mean face coefficients) + → solve (HYPRE FlexGMRES/PCG/… or AMReX MLMG V-cycle) → Flux integration → D_eff, tortuosity → ResultsJSON → results.json + results.txt ``` @@ -79,8 +100,9 @@ Inter-cell face diffusivities use the harmonic mean of adjacent cell values: ``` D_face = 2 * D_left * D_right / (D_left + D_right) ``` -This is physically correct for series resistance and appears in both -`TortuosityHypreFill.F90` and `EffDiffFillMtx.F90`. +This is physically correct for series resistance and appears in the C++ +matrix-fill paths of `TortuosityHypre`, `TortuosityMLMG`, and +`EffectiveDiffusivityHypre`. ### Tortuosity Definition ``` @@ -138,22 +160,43 @@ make -j$(nproc) && ctest --output-on-failure | File | Purpose | |------|---------| | `Diffusion.cpp` | **Main application entry point** — orchestrates full pipeline | -| `Tortuosity.H` | Base class + enums (Direction, CellType, SolverType) | -| `TortuosityHypre.H/cpp` | HYPRE-based tortuosity solver (primary solver) | +| `Tortuosity.H` | Base interface + enums (Direction, CellType, SolverType) | +| `TortuositySolverBase.H/cpp` | Shared solver setup + flux/τ post-processing (base class) | +| `TortuosityHypre.H/cpp` | HYPRE structured-grid tortuosity solver | +| `TortuosityMLMG.H/cpp` | Matrix-free AMReX MLMG tortuosity solver (EB, GPU-native) | | `TortuosityDirect.H/cpp` | Legacy iterative tortuosity solver (Forward Euler) | -| `EffectiveDiffusivityHypre.H/cpp` | Effective diffusivity tensor via homogenization | +| `TortuosityKernels.H` | C++ hot-path kernels (isolated-cell removal, face flux, flux integration) | +| `HypreStructSolver.H/cpp` | HYPRE lifecycle/infrastructure shared by HYPRE solvers | +| `EffectiveDiffusivityHypre.H/cpp` | Cell-problem solver for χ fields (homogenization) | +| `DeffTensor.H/cpp` | Assembles 3×3 D_eff tensor from χ gradients | | `VolumeFraction.H/cpp` | Phase volume fraction calculator | | `PercolationCheck.H/cpp` | Flood-fill percolation connectivity check | +| `FloodFill.H/cpp` | GPU-parallel flood-fill (used by percolation/components/mask) | +| `ConnectedComponents.H/cpp` | Labels contiguous regions of a phase | +| `SpecificSurfaceArea.H/cpp` | Interface area via Cauchy–Crofton stereology | +| `ParticleSizeDistribution.H/cpp` | Equivalent-sphere radii from components | +| `ThroughThicknessProfile.H/cpp` | Per-slice volume fraction along a direction | +| `REVStudy.H/cpp` | Representative Elementary Volume convergence study | | `PhysicsConfig.H` | Physics type mapping (diffusion ↔ conductivity ↔ thermal) | +| `SolverConfig.H` | String↔enum parsing (solver type, direction) | +| `BoundaryCondition.H` | BC enums/abstraction (Dirichlet/Neumann/Periodic) | +| `MacroGeometry.H` | Electrode thickness / cross-section / volume helpers | | `ResultsJSON.H` | Structured JSON output (BPX/BattINFO compatible) | -### Source — Fortran Kernels (`src/props/`) +> **Note:** The HYPRE/MLMG matrix-fill and flux kernels were historically +> Fortran 90 (`*_F.H` ↔ `*.F90`) but have been migrated to native C++ in +> `TortuosityKernels.H` (AMReX `ParallelFor`/`Array4`, CPU+GPU). No Fortran +> source files remain in the repository. + +### Source — Python layer (`python/`) | File | Purpose | |------|---------| -| `TortuosityHypreFill_F.H` / `.F90` | HYPRE matrix fill for tortuosity (7-pt stencil) | -| `EffDiffFillMtx_F.H` / `.F90` | HYPRE matrix fill for effective diffusivity cell problem | -| `Tortuosity_filcc_F.H` / `.F90` | Cell type ID, ghost cell fill, initial conditions | -| `Tortuosity_poisson_3d_F.H` / `.F90` | Flux calculation and Forward Euler update (legacy solver) | +| `openimpala/facade.py` | High-level NumPy-native API (tortuosity, volume_fraction, …) | +| `openimpala/session.py` | `Session` context manager — AMReX init/finalize (re-entrant) | +| `openimpala/_solver.py` | Pure-Python SciPy/CuPy fallback backend (no compiled `_core`) | +| `openimpala/cli.py` | `openimpala` CLI entry point | +| `bindings/*.cpp` | pybind11 `_core` module (module/io/props/solvers/config/enums) | +| `bindings/VoxelImage.H` | NumPy → AMReX `iMultiFab` bridge struct | ### Tests (`tests/`) | File | Purpose | @@ -162,12 +205,15 @@ make -j$(nproc) && ctest --output-on-failure | `tEffectiveDiffusivity.cpp` | D_eff tensor on real TIFF data | | `tVolumeFraction.cpp` | Volume fraction validation | | `tPercolationCheck.cpp` | Flood-fill connectivity check | +| `tTortuosityMLMG.cpp` | Matrix-free MLMG solver (directional/geometry variants) | | `tMultiPhaseTransport.cpp` | Synthetic geometry tests (analytical validation) | -| `tTiffReader.cpp` | TIFF I/O correctness | -| `tHDF5Reader.cpp` | HDF5 I/O correctness | -| `tRawReader.cpp` | RAW binary I/O correctness | -| `tests/unit/` | Catch2 unit tests (PhysicsConfig, ResultsJSON) | +| `tSynthetic*.cpp` | Synthetic VF / percolation / D_eff / microstructure / REV tests | +| `tTiffReader.cpp` / `tHDF5Reader.cpp` / `tRawReader.cpp` / `tDatReader.cpp` | I/O correctness | +| `tests/inputs/tDiffusion_*.inputs` | End-to-end `Diffusion` integration tests (dry-run, tiff, hdf5, REV, …) | +| `tests/unit/` | Catch2 unit tests (PhysicsConfig, ResultsJSON, MacroGeometry) | +| `tests/validation/` | V&V scripts (Hashin–Shtrikman bounds, sphere packing, Berea sandstone) | | `tests/benchmarks/` | Python scripts for generating benchmark datasets | +| `python/tests/` | pytest suite for the Python API and bindings | ### Regression Benchmarks Three CTest benchmarks with exact analytical solutions on discrete grids: @@ -182,10 +228,13 @@ Three CTest benchmarks with exact analytical solutions on discrete grids: - **LibTIFF** — TIFF image reading - **nlohmann/json** — JSON output (fetched via CMake FetchContent) - **Catch2 v3** — Test framework (fetched via CMake FetchContent) +- **pybind11** — C++ ↔ Python bindings for the `_core` extension (Python wheels) +- **NumPy / SciPy** (+ optional **CuPy**) — Python API and pure-Python fallback solver ## Code Style - C++17, 100-column limit, 4-space indent (LLVM-based via `.clang-format`) - All code in `namespace OpenImpala` - Headers use `#ifndef` include guards (not `#pragma once`) - Doxygen `@file` / `@brief` / `@param` comments on all public APIs -- Fortran files are NOT processed by clang-format or clang-tidy +- Hot-path kernels are native C++ (AMReX `ParallelFor`/`Array4`) for CPU+GPU; + there are no Fortran source files (the historical `.F90` kernels were ported) From c098f4456bd6e0b7f0e936f5cea4edff899717a8 Mon Sep 17 00:00:00 2001 From: James Le Houx <37665786+jameslehoux@users.noreply.github.com> Date: Sat, 13 Jun 2026 08:53:26 +0000 Subject: [PATCH 2/3] notebooks: isolate solver bake-off combos in subprocesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The profiling notebook's solver bake-off runs every HYPRE config including standalone SMG/PFMG. On a GPU build, standalone SMG drives many full cyclic-reduction V-cycles to tolerance and can abort at the C++ level (CUDA OOM / HYPRE kernel error). That hard-aborts the process, killing the Jupyter kernel with no catchable exception and wiping df_solvers plus every downstream section. Run each combo in its own short-lived subprocess so a hard crash only takes down that child — the parent records it as a FAILED row (decoding the signal, e.g. SIGABRT/SIGKILL, as a likely OOM) and continues. The crashed child's CUDA context is reclaimed on exit. Adds a per-combo timeout and a robustness note in the section markdown. All 10 combos are retained for completeness. --- notebooks/profiling_and_tuning.ipynb | 129 ++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 3 deletions(-) diff --git a/notebooks/profiling_and_tuning.ipynb b/notebooks/profiling_and_tuning.ipynb index 047b437e..571e6aba 100644 --- a/notebooks/profiling_and_tuning.ipynb +++ b/notebooks/profiling_and_tuning.ipynb @@ -120,14 +120,137 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## 3. Solver bake-off on porous media\n\nThis is the headline diagnostic. All available HYPRE Krylov methods, all HYPRE multigrid preconditioners, and the matrix-free AMReX MLMG are run on the *same* porespy structure at 64³. Wall time and iteration count are reported side-by-side; solvers that fail (multigrid stalls on masked rows, etc.) are flagged in red.\n\n**The default solver (`solver=\"auto\"`, since v4.2.20) is MLMG** — it scales near-linearly (see §4) and has the lowest memory footprint of any option. The bake-off below confirms it's also the fastest on this geometry. The runner-up among HYPRE configurations is `flexgmres+pfmg`; `pcg+smg` is the safe fallback if MLMG ever stalls." + "source": [ + "## 3. Solver bake-off on porous media\n", + "\n", + "This is the headline diagnostic. All available HYPRE Krylov methods, all HYPRE multigrid preconditioners, and the matrix-free AMReX MLMG are run on the *same* porespy structure at 64³. Wall time and iteration count are reported side-by-side; solvers that fail (multigrid stalls on masked rows, etc.) are flagged in red.\n", + "\n", + "**The default solver (`solver=\"auto\"`, since v4.2.20) is MLMG** — it scales near-linearly (see §4) and has the lowest memory footprint of any option. The bake-off below confirms it's also the fastest on this geometry. The runner-up among HYPRE configurations is `flexgmres+pfmg`; `pcg+smg` is the safe fallback if MLMG ever stalls.\n", + "\n", + "> **Robustness note.** Standalone structured-multigrid solvers (`smg`, `pfmg`) can abort at the C++ level on a GPU build — a CUDA out-of-memory or HYPRE kernel error that would otherwise kill the notebook kernel and wipe every result. Each combo below therefore runs in its own subprocess, so a hard crash is recorded as a red `FAILED` bar instead of taking the session down with it." + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "import sys\n\n# Sanity: make sure the porespy datasets cell (§2) has run. The restructure\n# moved cells around; if you opened the notebook and jumped here directly,\n# `data_medium` won't exist yet and the whole loop raises NameError before\n# any print fires — looking exactly like \"no output\".\nif \"data_medium\" not in dir():\n raise RuntimeError(\n \"data_medium not defined — re-run §2 (Synthetic datasets) first.\"\n )\n\n# Compare Krylov and multigrid choices on 64^3.\n# Each entry is (label, solver, preconditioner). A preconditioner of None means\n# the solver ignores it (standalone SMG/PFMG/Jacobi, or MLMG). `mlmg` is the\n# AMReX-native matrix-free path and the library default since v4.3.0.\ncombos = [\n (\"pcg\", \"pcg\", None),\n (\"pcg+pfmg\", \"pcg\", \"pfmg\"),\n (\"pcg+smg\", \"pcg\", \"smg\"),\n (\"flexgmres+pfmg\", \"flexgmres\", \"pfmg\"),\n (\"gmres\", \"gmres\", None),\n (\"gmres+pfmg\", \"gmres\", \"pfmg\"),\n (\"bicgstab\", \"bicgstab\", None),\n (\"smg\", \"smg\", None),\n (\"pfmg\", \"pfmg\", None),\n (\"mlmg\", \"mlmg\", None),\n]\n\nprint(f\"Running bake-off on {data_medium.shape[0]}³ porespy data... \"\n f\"({len(combos)} solver/preconditioner combinations)\", flush=True)\n\nrecords = []\nfor label, s, pc in combos:\n kwargs = dict(phase=0, direction=\"z\", solver=s, max_grid_size=32, verbose=0)\n if pc is not None:\n kwargs[\"preconditioner\"] = pc\n\n # Flush before each call: a C++-level AMReX::Abort or CUDA OOM kills\n # the kernel without unwinding Python, so anything we haven't printed\n # already is lost. Live progress also makes it obvious *which* solver\n # killed the cell if one of them does.\n sys.stdout.flush()\n\n try:\n t0 = time.perf_counter()\n res = oi.tortuosity(data_medium, **kwargs)\n dt = time.perf_counter() - t0\n records.append({\"label\": label, \"solver\": s, \"precond\": pc, \"t\": dt,\n \"iters\": res.iterations, \"tau\": res.tortuosity,\n \"ok\": res.solver_converged})\n print(f\" {label:18s} t={dt:6.2f}s iters={res.iterations:4d} \"\n f\"tau={res.tortuosity:.4f}\", flush=True)\n except TypeError as e:\n if \"preconditioner\" in str(e) and pc is not None:\n print(f\" {label:18s} SKIP — wheel predates preconditioner plumbing\",\n flush=True)\n records.append({\"label\": label, \"solver\": s, \"precond\": pc,\n \"t\": np.nan, \"iters\": np.nan, \"tau\": np.nan, \"ok\": False})\n continue\n raise\n except Exception as e:\n records.append({\"label\": label, \"solver\": s, \"precond\": pc,\n \"t\": np.nan, \"iters\": np.nan, \"tau\": np.nan, \"ok\": False})\n print(f\" {label:18s} FAILED — {e}\", flush=True)\n\ndf_solvers = pd.DataFrame(records)\ndf_solvers" + "source": [ + "import sys, os, json, subprocess, tempfile\n", + "\n", + "# Sanity: make sure the porespy datasets cell (§2) has run. The restructure\n", + "# moved cells around; if you opened the notebook and jumped here directly,\n", + "# `data_medium` won't exist yet and the whole loop raises NameError before\n", + "# any print fires — looking exactly like \"no output\".\n", + "if \"data_medium\" not in dir():\n", + " raise RuntimeError(\n", + " \"data_medium not defined — re-run §2 (Synthetic datasets) first.\"\n", + " )\n", + "\n", + "# Compare Krylov and multigrid choices on 64^3.\n", + "# Each entry is (label, solver, preconditioner). A preconditioner of None means\n", + "# the solver ignores it (standalone SMG/PFMG/Jacobi, or MLMG). `mlmg` is the\n", + "# AMReX-native matrix-free path and the library default since v4.3.0.\n", + "combos = [\n", + " (\"pcg\", \"pcg\", None),\n", + " (\"pcg+pfmg\", \"pcg\", \"pfmg\"),\n", + " (\"pcg+smg\", \"pcg\", \"smg\"),\n", + " (\"flexgmres+pfmg\", \"flexgmres\", \"pfmg\"),\n", + " (\"gmres\", \"gmres\", None),\n", + " (\"gmres+pfmg\", \"gmres\", \"pfmg\"),\n", + " (\"bicgstab\", \"bicgstab\", None),\n", + " (\"smg\", \"smg\", None),\n", + " (\"pfmg\", \"pfmg\", None),\n", + " (\"mlmg\", \"mlmg\", None),\n", + "]\n", + "\n", + "# --- Crash isolation ---------------------------------------------------------\n", + "# Some HYPRE configurations — notably *standalone* SMG/PFMG on a GPU build —\n", + "# can abort at the C++ level (CUDA out-of-memory or a HYPRE kernel error).\n", + "# That aborts the whole process, killing the Python kernel with no traceback\n", + "# and no way to catch it from the `except` below; df_solvers and every later\n", + "# section die with it.\n", + "#\n", + "# To stay robust we run each combo in its own short-lived subprocess. A hard\n", + "# abort then only takes down that child — the parent records it as FAILED and\n", + "# moves on, and the crashed child's CUDA context is reclaimed cleanly on exit.\n", + "# The cost is one fresh AMReX/HYPRE init per combo (a few seconds); cheap\n", + "# insurance for a diagnostic that deliberately probes failing solvers.\n", + "_data_path = os.path.join(tempfile.gettempdir(), \"oi_bakeoff_data.npy\")\n", + "np.save(_data_path, data_medium)\n", + "\n", + "_CHILD = r\"\"\"\n", + "import sys, json, time, numpy as np, openimpala as oi\n", + "data = np.load(sys.argv[1])\n", + "solver, precond, mgs = sys.argv[2], sys.argv[3], int(sys.argv[4])\n", + "kwargs = dict(phase=0, direction=\"z\", solver=solver, max_grid_size=mgs, verbose=0)\n", + "if precond != \"none\":\n", + " kwargs[\"preconditioner\"] = precond\n", + "with oi.Session():\n", + " t0 = time.perf_counter()\n", + " res = oi.tortuosity(data, **kwargs)\n", + " dt = time.perf_counter() - t0\n", + "print(\"RESULT \" + json.dumps({\n", + " \"t\": dt, \"iters\": int(res.iterations),\n", + " \"tau\": float(res.tortuosity), \"ok\": bool(res.solver_converged),\n", + "}))\n", + "\"\"\"\n", + "\n", + "\n", + "def run_combo(solver, precond, max_grid_size=32, timeout=300):\n", + " \"\"\"Run one solver in an isolated process. A crash, timeout, or\n", + " non-convergence all come back as ok=False instead of killing this kernel.\"\"\"\n", + " try:\n", + " proc = subprocess.run(\n", + " [sys.executable, \"-c\", _CHILD, _data_path, solver,\n", + " precond or \"none\", str(max_grid_size)],\n", + " capture_output=True, text=True, timeout=timeout,\n", + " )\n", + " except subprocess.TimeoutExpired:\n", + " return {\"t\": np.nan, \"iters\": np.nan, \"tau\": np.nan, \"ok\": False,\n", + " \"error\": f\"timeout (>{timeout}s)\"}\n", + "\n", + " for line in proc.stdout.splitlines():\n", + " if line.startswith(\"RESULT \"):\n", + " r = json.loads(line[len(\"RESULT \"):])\n", + " r[\"error\"] = None\n", + " return r\n", + "\n", + " # No RESULT line → the child died before printing. Decode why.\n", + " if proc.returncode < 0:\n", + " import signal\n", + " try:\n", + " sig = signal.Signals(-proc.returncode).name\n", + " except ValueError:\n", + " sig = f\"signal {-proc.returncode}\"\n", + " reason = f\"crashed ({sig}) — likely CUDA OOM / HYPRE abort\"\n", + " else:\n", + " tail = (proc.stderr.strip().splitlines() or [\"no stderr\"])[-1]\n", + " reason = f\"exit {proc.returncode}: {tail[:120]}\"\n", + " return {\"t\": np.nan, \"iters\": np.nan, \"tau\": np.nan, \"ok\": False,\n", + " \"error\": reason}\n", + "\n", + "\n", + "print(f\"Running bake-off on {data_medium.shape[0]}³ porespy data... \"\n", + " f\"({len(combos)} solver/preconditioner combinations, each isolated)\",\n", + " flush=True)\n", + "\n", + "records = []\n", + "for label, s, pc in combos:\n", + " sys.stdout.flush()\n", + " r = run_combo(s, pc, max_grid_size=32)\n", + " records.append({\"label\": label, \"solver\": s, \"precond\": pc,\n", + " \"t\": r[\"t\"], \"iters\": r[\"iters\"], \"tau\": r[\"tau\"],\n", + " \"ok\": r[\"ok\"]})\n", + " if r[\"ok\"]:\n", + " print(f\" {label:18s} t={r['t']:6.2f}s iters={int(r['iters']):4d} \"\n", + " f\"tau={r['tau']:.4f}\", flush=True)\n", + " else:\n", + " print(f\" {label:18s} FAILED — {r['error']}\", flush=True)\n", + "\n", + "df_solvers = pd.DataFrame(records)\n", + "df_solvers\n" + ] }, { "cell_type": "code", @@ -486,4 +609,4 @@ ] } ] -} \ No newline at end of file +} From 6a2c4c85aefc543462df8d596c8e0c299100286b Mon Sep 17 00:00:00 2001 From: James Le Houx <37665786+jameslehoux@users.noreply.github.com> Date: Sat, 13 Jun 2026 08:53:55 +0000 Subject: [PATCH 3/3] chore: add SessionStart hook to set git commit identity Claude Code on the web runs in ephemeral containers that clone the repo fresh with no git identity configured. This SessionStart hook sets the local user.name/user.email on every session so commits are attributed to James Le Houx's GitHub account (via the GitHub noreply email, which maps reliably to the account). Idempotent and repo-scoped. --- .claude/hooks/session-start.sh | 13 +++++++++++++ .claude/settings.json | 14 ++++++++++++++ 2 files changed, 27 insertions(+) create mode 100755 .claude/hooks/session-start.sh create mode 100644 .claude/settings.json diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 00000000..945bc4b5 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# SessionStart hook: attribute commits made in Claude Code (especially the +# ephemeral "on the web" containers, which clone the repo fresh and have no +# git identity configured) to James Le Houx's GitHub account. +# +# Uses GitHub's noreply email, which reliably maps to the account for commit +# attribution. Scoped to this repo (--local) and idempotent. +set -euo pipefail + +git config --local user.name "James Le Houx" +git config --local user.email "37665786+jameslehoux@users.noreply.github.com" + +echo "git identity set: James Le Houx <37665786+jameslehoux@users.noreply.github.com>" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..e06b0338 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +}