Skip to content

Typed refusals: SparseJacobianUnavailableError and CodegenDeclinedError (#1313) - #1336

Open
Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1313-typed-refusals
Open

Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1313-typed-refusals

Conversation

@Montana

@Montana Montana commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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 except keeps working.

type raised for subclass of
SparseJacobianUnavailableError force_sparse_linear_solver=True on a model with no sparse Jacobian for KLU SimulationError
CodegenDeclinedError codegen=True on a model codegen declines (e.g. functions that read each other, #621) BngsimError, so RuntimeError

The sparse refusal is a C++ type, bngsim::SparseJacobianUnavailable (new include/bngsim/refusals.hpp), thrown where cvode_simulator.cpp refuses. The core's exception translator, which already maps TimeoutError to SimulationTimeout, maps it to the Python type. run(), run_batch's threads and each batch row pass it through untouched, as they do SimulationTimeout.

The codegen decline: _codegen_refusal now returns the exception. A decline is CodegenDeclinedError, a statement about the model. A failed build (a compiler error, a timeout) stays a plain RuntimeError, which says nothing about the model, as the issue asks. The messages are unchanged.

-            throw std::runtime_error(
+            throw SparseJacobianUnavailable(
                 "force_sparse_linear_solver: this model cannot supply a sparse "

The harness (parity_checks/differential/_differential.py) reads both with isinstance instead of their text:

  • _sparse_declined(e) is isinstance(e, SparseJacobianUnavailableError);
  • _compiled catches CodegenDeclinedError.

The baseline's notes (_Declined: codegen=True requested, but codegen declined this model: …) are unchanged.

A bug this found: run_batch ignored the linear-solver flags

The batch test never reached the refusal: run_batch ran. _run_single_batch copied jacobian, the tolerances and codegen onto each row's options, but neither force_dense_linear_solver nor force_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:

                 opts.jacobian = self._jacobian
+                opts.force_dense_linear_solver = self._force_dense_linear_solver
+                opts.force_sparse_linear_solver = self._force_sparse_linear_solver

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:

  • both are exported and subclass what was raised before; CodegenDeclinedError is not a SimulationError;
  • species with no reactions, under force_sparse_linear_solver=True, jacobian="fd", raise SparseJacobianUnavailableError, still a SimulationError;
  • the same through run_batch with 2 threads arrives by its type, not as "Batch simulation 0 failed";
  • a solver failure is not the refusal;
  • the cyclic-function model raises CodegenDeclinedError;
  • a codegen build failure (injected in generate_combined_from_model) is a RuntimeError, not a decline;
  • the harness reads a reworded message of either as the refusal;
  • a batch row takes the solver it was asked for: {1} (KLU) under the auto rule, {0} (dense) under force_dense_linear_solver.

Mutations, each caught:

  • the C++ translator removed (rebuilt): 2 fail;
  • run() wrapping it;
  • the decline untyped;
  • a build failure typed as a decline;
  • the harness by text, for each type;
  • run_batch dropping the flags: 2 fail.

Suites: python/tests and parity_checks/tests give 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 fragments 1313.added.md and 1313.fixed.md.

Conflicts: none. A trial merge with each open differential PR (#1315, #1318, #1319, #1320, #1321, #1322, #1323) is clean.

…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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Typed exceptions for "no sparse Jacobian for KLU" and "codegen declined this model"

1 participant