Repository navigation
Conversation
…or (lanl#1313) force_sparse_linear_solver on a model with no sparse Jacobian for KLU, and codegen=True on a model codegen declines, were a SimulationError and a RuntimeError a caller told from a crash only by their text: the path-agreement harness matched both, and a reworded message turned its SKIP or UNSUPPORTED into an EXCEPTION. - bngsim::SparseJacobianUnavailable (include/bngsim/refusals.hpp), thrown where cvode_simulator.cpp refuses, translated by the core to SparseJacobianUnavailableError(SimulationError), and passed through run(), run_batch's threads and each batch row untouched. - _codegen_refusal returns the exception: CodegenDeclinedError(BngsimError) for a decline, a plain RuntimeError for a failed build, which says nothing about the model. - Both exported; each subclasses what was raised before. - The harness reads both with isinstance. run_batch dropped force_dense_linear_solver and force_sparse_linear_solver from every row, which ran whatever the auto rule picked (KLU, on a 60-species chain asked for dense); run() honoured both. Found because the batch path never reached the refusal; each row now takes both.
|
Thanks. We reviewed this independently, Opus 5.5 and Fable 5.1, each on our own build of 44c55fe merged onto main c0877fa (clean), on macOS arm64 with KLU and MIR on. Your merge of main this morning, 52b4911, has the same tree (b48ae3a3), so all of this holds for the current head. The change does what it says:
We found no defect in the diff. There is one merge-order point with #1292, one pre-existing gap on a line this PR edits, and some low items. What we checked comes first, so it needn't be redone. What we checked
1. Land #1292 first, or with thisWithout #1292 the new type also fires when the caller set no flag. On main + this PR, a 60-species model with no reactions, run with
This is #1296. The size and density rule sends the empty pattern to KLU, and #1292 adds Fable built main + this PR + #1292 (its head f66d1e4; the merge is clean):
Each of us reproduced the unflagged case with our own script. 2. A threaded
|
| path | type at the caller |
|---|---|
run() |
SimulationTimeout |
run_batch, sequential |
SimulationTimeout |
run_batch, 2 threads |
SimulationError("Batch simulation 0 failed: Simulation exceeded wall-clock timeout…"), with SimulationTimeout as its cause |
Main does the same, so this PR didn't cause it. But it's the line this PR edits, and the body says the threads pass the refusal through "as they do SimulationTimeout", which they don't. Adding the one name to the tuple fixes it, and a test with num_processors=2 and a tiny timeout would pin it.
That tuple now exists three times, in run(), the thread pool and the row, each with its own members. This gap is the drift. One module-level tuple of the typed exceptions that pass through, used in all three, would keep the next type from reaching two of the three.
Low
- The dense assertion reads the built-in LU only. Under
BNGSIM_LAPACK_DENSE=1,kinds(force_dense_linear_solver=True)is{2}, the adaptive BLAS solver, so the test fails. No workflow sets the variable, andtest_engine_choice_accessors.py:315already counts 2 as dense.assert 1 not in kinds(force_dense_linear_solver=True)says what the test means: not KLU. - The third branch of
_codegen_refusalis unpinned. If "codegen built nothing and recorded no reason" is typed asCodegenDeclinedError, the new file still passes 8/8, and no test names that text. - The harness still reads a third refusal by text.
_differential.py:_declinedclassifies The issue #952 hold check refuses a sensitivity run at a bend: a continuous narrow pulse (max/abs or if()) is refused at rtol <= 1e-10, blamed on a jump it does not have #1229's run-time sensitivity refusal by"forward sensitivity" in str(e)on aSimulationError. It's outside Typed exceptions for "no sparse Jacobian for KLU" and "codegen declined this model" #1313's two cases, but it has the same fragility. An issue, or a line in the body saying it's left, would keep it tracked. - The deprecated
bngsim.prepare_codegenstill raises a decline as a plainRuntimeError(_codegen.py:13039, "codegen declined {net_path}: …"). It's public, in__all__. Typing it, or saying the type coverscodegen=Trueonly, would close the gap. - Two comments.
- The translator's header comment (
_bngsim_core.cpp:870–879) still namesTimeoutErroras the one exception translated in C++. CodegenDeclinedErrorand the private_codegen.CodegenDeclined(aRuntimeError, also caught in_jax_rhs.py) are unrelated classes. A line in the new docstring would save a reader from guessing.
- The translator's header comment (
Not checked:
- Linux and Windows beyond CI. No Windows leg runs the new file, so the translator has only run on macOS (here) and Linux (CI).
- A shared-library build of the core. The Python build forces static.
Fixes #1313.
What it adds
Two refusals bngsim makes by name now have types of their own. Each subclasses what was raised before, so an existing
exceptkeeps working.SparseJacobianUnavailableErrorforce_sparse_linear_solver=Trueon a model with no sparse Jacobian for KLUSimulationErrorCodegenDeclinedErrorcodegen=Trueon a model codegen declines (e.g. functions that read each other, #621)BngsimError, soRuntimeErrorThe sparse refusal is a C++ type,
bngsim::SparseJacobianUnavailable(newinclude/bngsim/refusals.hpp), thrown wherecvode_simulator.cpprefuses. The core's exception translator, which already mapsTimeoutErrortoSimulationTimeout, maps it to the Python type.run(),run_batch's threads and each batch row pass it through untouched, as they doSimulationTimeout.The codegen decline:
_codegen_refusalnow returns the exception. A decline isCodegenDeclinedError, a statement about the model. A failed build (a compiler error, a timeout) stays a plainRuntimeError, which says nothing about the model, as the issue asks. The messages are unchanged.The harness (
parity_checks/differential/_differential.py) reads both withisinstanceinstead of their text:_sparse_declined(e)isisinstance(e, SparseJacobianUnavailableError);_compiledcatchesCodegenDeclinedError.The baseline's notes (
_Declined: codegen=True requested, but codegen declined this model: …) are unchanged.A bug this found:
run_batchignored the linear-solver flagsThe batch test never reached the refusal:
run_batchran._run_single_batchcopiedjacobian, the tolerances and codegen onto each row's options, but neitherforce_dense_linear_solvernorforce_sparse_linear_solver. So every batch row ran whatever the auto rule picked. On a 60-species chain, a Simulator asked for the dense solver ran KLU in every row.run()always honoured both, and the steady-state batch already copies them. Each row now takes both:That has its own changelog fragment (
1313.fixed.md).Tests (
python/tests/test_typed_refusals.py)The positive tests fail on main because neither type exists. With the change:
CodegenDeclinedErroris not aSimulationError;force_sparse_linear_solver=True, jacobian="fd", raiseSparseJacobianUnavailableError, still aSimulationError;run_batchwith 2 threads arrives by its type, not as "Batch simulation 0 failed";CodegenDeclinedError;generate_combined_from_model) is aRuntimeError, not a decline;{1}(KLU) under the auto rule,{0}(dense) underforce_dense_linear_solver.Mutations, each caught:
run()wrapping it;run_batchdropping the flags: 2 fail.Suites:
python/testsandparity_checks/testsgive 17,325 passed, 171 skipped, 1 xfailed. clang-format 17.0.6 (the pinned hook) and ruff 0.14.0 are clean. The core stub is unchanged, since no binding changed.Docs: both types are in
docs/reference/api.md's exception list; changelog fragments1313.added.mdand1313.fixed.md.Conflicts: none. A trial merge with each open differential PR (#1315, #1318, #1319, #1320, #1321, #1322, #1323) is clean.