Add one line installer and pulsatile case - #6
Open
Eleven7825 wants to merge 77 commits into
Open
Eleven7825 wants to merge 77 commits into
Eleven7825 wants to merge 77 commits into
Conversation
Eleven7825
commented
Feb 19, 2026
Contributor
- Add a one line installer for the svFSGe
- Add a pulsatile case using mean wss
- Add converted pulsatile Q waveform (pulsatile_flow.dat) from VMR source .flow files, converted from cm/s to mm/s to match kg/mm/s unit system - Update steady_full.xml inlet BC to use Unsteady time dependence with the pulsatile temporal values file - Add static BC file copy step in svfsi.py setup_files so pulsatile_flow.dat is staged into the run directory automatically - Include source .flow files from Vascular Model Repository Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Increase Number_of_time_steps from 10 to 960 (10 cardiac cycles at dt=0.01s) - Add run_fluid_only.py script for standalone fluid simulation without FSG coupling, handles mesh/BC file staging and runs svFSI directly Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Container named 'fsg-dev' now persists after exit - Rerunning the script reconnects to existing container - Uses 'sleep infinity' + 'docker exec' pattern for persistence - Updated pip install to use requirements.txt (includes meshio) - Shows container management commands on completion Users can now: - Exit and reconnect without losing setup - Stop/start the container as needed - See the container in 'docker ps -a' Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Instead of requiring requirements.txt, the script now has the package list hardcoded. This makes it work even if requirements.txt is missing or the repo state is inconsistent. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Creates a bootstrap script that can be downloaded and run via curl. Handles repo cloning and Docker setup in one command. Usage: curl -fsSL https://raw.githubusercontent.com/Eleven7825/svFSGe/master/scripts/install.sh | bash Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Prominently featured at the top of Quickstart section - Explains prerequisites and what the command does - Updated Quick Setup Script section to mention persistent container Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add setup_singularity.sh for automated Singularity environment setup - Add install_singularity.sh for one-command installation on HPC - Add run_simulation.sh helper script for easy simulation execution - Update README.md with HPC installation instructions - Support for read-only containers with user-space Python package installation - Include verification step to ensure all dependencies are properly installed
…ive VTU copying - Copies folder structure and all files except VTU/BIN from pulsatile/steady/gr_restart - Copies only last 400 pulsatile VTU files to minimize transfer size - Copies only last 10 steady VTU files - Excludes all gr_restart VTU files completely - Uses SSH ControlMaster for single authentication - Strips carriage returns from remote SSH output for proper path handling
…from the json file
…be read from the json file" This reverts commit 7c671a3.
…_full_coarse.json
Mirror the existing nmax=2 standard-FSI CI test for the neural-operator
direct backend so the NN branch is gated before merge.
- in_sim/partitioned_test_n660.json: nloads=2 test config, neural_operator
direct backend pointing at vendored assets.
- test_assets/n660/: self-contained NN deps (wss/pressure .pt, SVD basis,
cylinder.vtk template, shearStressNN_coeff model module) so CI needs
nothing outside the repo (torch is already in simvascular/solver:latest).
- test_reference/n660_nmax_2/: reference baseline (reference_results.json +
tube_002.vtu); iterations [2, 18, 36], reproducible bit-for-bit.
- test-fsg.yml: run both configs in one Docker build, copy to fixed
test_output/{standard,n660} dirs, compare each via compare_results.py.
- .gitignore: allow test_assets/**/*.vtk past the *.vt* rule.
- post.py: force headless Agg backend; close figures to avoid leaks.
- svfsi.py: optional set_gr_growth (tau_ratio_floor) G&R stabilization.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The base simvascular/solver:latest image has no torch, so CI failed at `from neural_operator import NeuralOperator` (fsg.py top-level) for BOTH the standard and n660 tests. - fsg.py: import NeuralOperator lazily inside __init__, only when a neural_operator surrogate is enabled. The standard FSI path now runs with no torch installed. - test-fsg.yml: install the CPU torch wheel before the n660 test only. Verified in a pristine simvascular/solver:latest container: `import fsg` succeeds without torch, and NeuralOperator loads the vendored test_assets after the CPU torch install. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a partitioned continuation driver that traverses growth limit points by solving for the growth-load factor lambda instead of prescribing it, on top of the new svFSI explicit-restart capability. - fsg.py: _run_arclength() — single-loop Crisfield spherical arc-length in (interface displacement d, growth-load factor lambda), solving the partitioned NN-FSG equilibrium R(d,lambda)=solid(NN(d),lambda)-d=0 with a spherical constraint so a step can be re-evaluated at a different lambda without compounding G&R history. Opt-in via JSON "arc_length". - svfsi.py: pass --restart-in/--restart-out to the solid solver so each arc-length trial re-solves a load step from an exact checkpoint (no compounding); read the most-recently-written gr_restart output when the arc-length resets decouple solver cTS from the coupling counter. - neural_operator.py, post.py: supporting changes (WSS handling; headless plotting / arc-length convergence output). Requires the matching svFSI --restart-in/--restart-out support (svMultiPhysics FSGe branch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The n660 comparison uses a very tight tolerance (Displacement rtol 1e-8, Velocity 1e-6), but the neural-operator (torch CPU) prediction is not bit-reproducible across machines: the committed reference was generated on a workstation, so it passes there but fails on the GitHub runner (the standard FSI test, which has no NN, passes on the runner — confirming the solid solver is consistent and this is NN cross-machine drift). Add a workflow_dispatch input `regenerate_n660` that re-generates the reference from the runner's own n660 output and commits it back (curated convergence.error + tube_002.vtu, matching the documented reference format), then skips the strict comparison for that run. Normal push/PR runs still compare strictly against the committed reference. Trigger it once from the Actions tab to bless a runner-generated baseline; subsequent CI runs on the same runner class then compare against a matching reference. Adds `permissions: contents: write` (used only by the regenerate path). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntinuation, explicit restart # Conflicts: # svfsi.py
fsg.py/svfsi.py: - New "weak" coupling method: one fluid + one solid solve per load step (no sub-iteration, no ramp), with Aitken Delta^2 relaxation of the interface displacement built from consecutive load-step residuals (history spans the outer loop, not sub-iterations). - New "linesearch" coupling method: weak coupling with an intra-step stimulus homotopy (adaptive alpha back-off on solid solve failure). - svFSI restart timeout handling (solve_timeout) so an element-inversion MPI_Abort can't hang a run; self-contained per-run failure-case capture (last-good tube + solver crash-state VTU) instead of a shared pool. - save_tube() helper consolidating tube_*.vtu archiving. CI: - New weak-coupling regression test (in_sim/partitioned_test_weak_n4.json, nloads=4 out of the 80-step production config, for CI runtime) using the same reference-comparison mechanism as the existing standard/n660 tests (scripts/compare_results.py: error-norm + VTU field comparison), plus a matching workflow_dispatch regenerate_weak path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g to nloads
fsg.py: "coup": {"tol_final": ...} tightens the coupling tolerance for only
the last load step (no effect unless set). Used to check whether the
vanilla/weak-coupling comparison gap was an artifact of insufficient vanilla
convergence -- it wasn't (results move <5% under tol_final=1e-8 vs the
default 1e-3, so the ~90-110% stress-field discrepancy against weak coupling
is real, not under-convergence).
svfsi.py: set_gr_timing() now unconditionally calibrates the solid XML's
<n_t_pre>/<n_t_end> G&R load-ramp window to this run's nloads (n_t_pre=1,
n_t_end=nloads+1) instead of relying on whatever the XML happened to have.
A mismatched window silently saturates the load ramp early and
over-integrates G&R history for every step past that point.
test_reference/vanilla_tolfinal_n10/: final converged geometry (tube_010.vtu)
from an iqn_ils run with tol_final=1e-8, held as a comparison reference (not
wired into the CI test suite).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
set_gr_timing() unconditionally recalibrated every run's G&R XML
<n_t_pre>/<n_t_end> window to this run's nloads. Applied to the standard
CI config (nloads=2), this caused a genuine solid-solve failure at t=2
that did not occur before ("disp simulation failed" -- CI run 30951624439).
Reverting until the calibration logic is fixed; fsg.py's tol_final and the
vanilla_tolfinal_n10 reference are unaffected and kept.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ad curve partitioned_test_weak_n4.json previously used a self-scaling tanh gr_load profile that independently ramps 0->1 within its own nloads=4, so it did not represent the actual production trajectory (partitioned_test_weak_ramp80.json, where step 4 of 80 only reaches ~10% load). Switched to the literal first 5 entries of that 80-step curve, so this is now a true "first 4 of 80" prefix. Validated: displacement/velocity/pressure/strain/WSS/GR at step 4 match the real 80-step run's own step 4 to floating-point precision (Displacement max diff 3.6e-11). Regenerated reference_results.json/tube_004.vtu accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds coup.method="uber_robin": the real load curve compressed to N=10 steps (configurable), each sub-iterated to a fixed point at that step's fixed load target with Aitken Delta^2 relaxation reset every step -- unlike _run_weak, which advances the load on every solve with no inner sub-iteration, and unlike the historical static/aitken loop, whose sub-iteration budget and tolerance are fixed constants rather than graded (cheap+loose for intermediate steps, expensive+tight for the final step, since only the final state is later compared against any accuracy reference). Populates self.err["disp"]/self.p["coup"]["omega"]["disp"] in the same per-step list format the static/aitken methods use, plus a richer per-sub-iteration record (uber_robin_history.json, written incrementally so a mid-run kill still leaves a scoreable partial record) and an intermediate-state VTU per sub-iteration under sub_iterations/, not just the converged per-step states. A dedicated plot_uber_robin_history() renders the per-step omega reset and step-varying tolerance explicitly, since the generic plot_convergence() assumes a single global tolerance this method doesn't have. Validated against the original AlphaEvolve run: reproduces the exact same call structure (25 total fluid solves: 1 prestress + 9 steps x 2 sub-iterations + 1 final step x 6 sub-iterations), matching fluid_solves=25 from the source experiment exactly. See docs/coupling_algorithms.tex Section 4 for the algorithm derivation this implements line-for-line (Algorithm 4). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_run_weak and _run_uber_robin each had their own copy-pasted _fluid_solve closure hardcoding n=0 for the neural-operator surrogate call. Harmless for _run_weak (no real sub-iteration concept there, n=0 is correct), but wrong for _run_uber_robin: _neural_operator_step's n drives _wss_relax_beta's designed ramp-up (more WSS damping early in a step's sub-iteration, relaxing as n grows), and the historical coup_step_relax/ coup_step_iqn_ils already thread the real n through correctly -- only the two newer per-step-loop methods didn't. Extracted a single _fluid_solve(self, i_f, t, n=0) method; _run_weak calls it with the same default (unchanged behavior, confirmed via svfsi.py:step() -- n is only used for name=="solid", never "mesh" or "fluid", so this is a no-op there), _run_uber_robin now passes its real sub-iteration index. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Existing time_average/amplitude modes reduce the raw vector components across the cardiac cycle and defer the norm to Solution.add(), which for a reversing WSS vector understates the stimulus. The new "magnitude" mode computes mean_k(||v_k(p)||) directly (classical TAWSS) and packs the scalar into the z-component so the existing downstream norm recovers it exactly, following the same convention neural_operator.py uses.
extract_pulsatile_amplitude previously computed componentwise max(v)-min(v) then deferred the norm downstream, giving ||max_k(v_k)-min_k(v_k)|| -- a range vector that can mix components from different timesteps, not a real WSS amplitude. Now computes max_k(||v_k(p)||) - min_k(||v_k(p)||) directly, matching the magnitude-first convention used by extract_pulsatile_magnitude (real TAWSS). These are now the two well-defined WSS reduction modes; time_average remains available for velo/press, which are kept as actual vectors (not reduced through a norm) and are unrelated to this fix.
sys.path.insert(0, "/home/shiyi/TAA_CFD_pipeline") + the subsequent import assumed a machine-local active-learning pipeline that isn't present on every machine this repo runs on (e.g. the container's home is /home/ubuntu, not /home/shiyi). Any load-step failure that reached this handler crashed on the missing module instead of saving even the diagnostic snapshot. Guard the import and skip only the geometry dump when it's unavailable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJYZxAoJMqkM5oVahM2rxj
The CI n660 comparison (rtol=1e-8, near bit-exact) flaked non-directionally across otherwise-identical commits: verified two CI runs of the SAME commit (byte-identical git tree) gave different VTU comparison results, isolated to the neural-operator step only (the "direct" LDDMM backend used in that test is pure KD-tree + SVD projection, fully deterministic -- the NN forward pass was the only remaining nondeterministic piece). torch's default multi-threaded CPU matmul reduces across threads in a runtime- scheduling-dependent order, so identical weights + inputs can still give ULP-level differences from run to run. Single-threaded matmul has a uniquely defined reduction order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJYZxAoJMqkM5oVahM2rxj
…oducibility" This reverts commit ea38cb8.
The strict 1e-8 rtol shared with the standard (non-ML) FSI comparison was too tight for this pipeline: torch CPU inference is only bit-reproducible within a single fixed (docker image, torch version, CPU microarch) environment, not across whatever GitHub Actions' unpinned `docker pull ...:latest` / unpinned `pip install torch` resolve to over time. Verified empirically before making this change: downloaded the actual output VTUs from two separate CI runs of byte-identical code (one before, one after an earlier, now-reverted single-thread-pinning attempt) and diffed them at full float64 precision -- bit-for-bit identical to each other, both differing from the committed reference VTU by the same 2.674496439251939e-06. That rules out run-to-run non-determinism (the prior, incorrect theory) and confirms this is a persistent, deterministic environment mismatch against a reference that hasn't been regenerated since 2026-07-06. 1e-5 comfortably covers the observed ~2.7e-6 gap while still catching a real regression; --displacement-rtol/--velocity-rtol are now optional per-invocation overrides in compare_results.py so the standard FSI test keeps its original 1e-8. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJYZxAoJMqkM5oVahM2rxj
…nfigs Empirical sweep (fixed final coarse-mesh geometry, standalone svFSI runs) showed the magnitude WSS reduction stays accurate to <0.4% down to 10 steps/cycle, and amplitude stays accurate to <0.2% down to 50 steps/cycle, against the previous 200-steps/cycle baseline -- a large compute saving with negligible accuracy cost. Updated the shared pulsatile_full.xml template's Number_of_new_time_steps/Time_step_size to match the new default steps_per_cycle=50 in all pulsatile configs (both must move together, since svFSI's stopTS = max(Number_of_time_steps, currentStep + Number_of_new_time_steps) silently breaks the per-call step accounting if they diverge). Added two coarse-mesh, 50-steps/cycle, iqn_ils configs for a Bouchet comparison run: default tol=1e-3 and a tightened tol=1e-5 variant.
Was overriding coup.tol globally; the codebase already supports an opt-in coup.tol_final that only applies on the last load step (t == nloads), which is what was actually wanted.
extract_pulsatile_amplitude and extract_pulsatile_magnitude unconditionally treated every field as a 3-component vector (np.linalg.norm(..., axis=2)), but partitioned_pulsatile.json's pre-existing "press": "amplitude" setting feeds a scalar field, causing AxisError: axis 2 is out of bounds for array of dimension 2. Only "wss" is norm-recovered downstream (Solution.add), so the magnitude-first + z-component-packing logic is now scoped to that field specifically; any other field (scalar like press, or a vector consumed as-is like velo) falls back to a plain axis=0 reduction, matching the pre-existing behavior those configs already relied on.
Both jobs inherited "amplitude" for wss/press from the base partitioned_pulsatile.json. For a clean comparison of the tol vs tol_final effect specifically, switch all three fields (wss, velo, press) to time_average so the field-reduction choice isn't a confounding variable.
gr_full_restart.xml hardcodes n_t_pre=1/n_t_end=11 for the solid solver's internal G&R pseudo-time ramp (f_time = (sgr-n_t_pre)/ (n_t_end-n_t_pre)), spanning exactly 10 load steps -- nothing in fsg.py/svfsi.py patches these to track the JSON's "nloads" the way Number_of_time_steps is patched for the pulsatile fluid XML. With nloads=20, the internal ramp saturates at Python's t=10 (halfway) and then extrapolates past f_time=1 for t=11..20 (up to f_time=2.0 at t=20), which can flip material terms like the insult knockdown (KsKi *= 1 - f_time*f_axi*f_cir*phi_e_hm) negative near the insult peak -- a plausible direct cause of the "Negative Jacobian" element- inversion crashes both jobs hit (job 1 crashed at t=11, exactly the first step past where the internal ramp already saturated). Setting nloads=10 restores sync with the solid XML's hardcoded window.
svFSI's fluid solver can now compute a WSS/Velocity/Pressure time-domain reduction on the fly (a repeatable <Add_reduction> XML block per field), instead of the current Python path of scanning every per-step VTU file and reducing them here. This adds the Python-side consumption of that output: - _read_field_reduction_vtu(field, array_name): shared helper that reads one <Add_reduction> block's <field>_reduction.vtu output, keyed by a 0-based GlobalNodeID. - extract_wss_from_accumulator(): reads wss_reduction.vtu, packs the scalar into the z-component of an (N,3) array to match extract_ pulsatile_amplitude/_magnitude's existing downstream-compatible convention (Solution.add / np.linalg.norm(sol, axis=1)). - extract_velocity_from_accumulator() / extract_pressure_from_ accumulator(): read velocity_reduction.vtu / pressure_reduction.vtu as-is (no z-packing needed -- already matches extract_pulsatile_ time_average's own return shape for these two fields). - Three independent opt-in flags in pulsatile_config (each default false): wss_reduction_from_accumulator, velocity_reduction_from_ accumulator, pressure_reduction_from_accumulator. extract_pulsatile_ data() checks each per field and skips the corresponding per-step-VTU scan/reduce entirely when set. Validated against real C++ output from a 3-field <Add_reduction> run (WSS/face/magnitude, Velocity/volume/componentwise, Pressure/volume/ componentwise): all three match an independently-computed reference from the same run's per-step VTUs exactly, and the missing-file error path raises a clear FileNotFoundError naming the expected XML config. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The function unconditionally ravel()'d WSS_reduction before z-packing, assuming the array is always 1D (magnitude mode). When Reduction_mode= componentwise is used instead (as needed to match extract_pulsatile_ time_average's own "wss" semantics -- a raw (N,3) vector, no magnitude), the array is actually (N,3), and ravel()+z-packing silently produced a garbage (3N,3) array with no error at that point. This is the likely root cause of the G&R load-step-3 divergence seen in the accumulator-based pulsatile comparison run on precision: garbage WSS values reaching props[:,6]/the G&R stimulus via sol[map_int]. Now branches on shape: componentwise (N,3) is returned as-is; magnitude (1D) keeps the original ravel+z-pack behavior. Verified against the real extract_pulsatile_time_average on real run data -- both code paths now produce exact (max_abs_diff=0.0) matches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.