Simulator.steady_state() and steady_state_batch() run the interpreted RHS
unconditionally, and their sensitivity matrix is built entirely from finite
differences. Both are invisible from Python: nothing warns, and the result comes
back looking like every other sensitivity result.
Evidence
1. The codegen field exists, is exposed, and is read by nobody.
SteadyStateOptions carries std::string codegen_so_path (include/bngsim/types.hpp:782),
and pybind exposes it. But:
Simulator.steady_state() builds a SteadyStateOptions and sets tol,
max_time, method, rtol, atol, max_steps, jacobian and
sensitivity_params — never codegen_so_path, even when the Simulator holds a
.so in self._codegen_so_path. steady_state_batch()'s _run_one does the
same.
src/steady_state.cpp contains exactly one occurrence of the string "codegen",
a comment at line 947. The option is never read on the C++ side either.
So a Simulator built with codegen=True solves for steady state on the same
interpreted path as one built with codegen=False. There is also no
codegen_c_source field at all, so the MIR JIT backend is not even representable
here.
2. dY_ss/dp is finite-differenced twice over.
compute_ss_sensitivity (src/steady_state.cpp:703) forms dY_ss/dp = -J⁻¹·(∂f/∂p) as:
J by finite differences — n_species + 1 interpreted RHS evaluations, one per
column, with the comment "We use finite differences (works for all rate law
types)" (lines 731–753). The model's complete analytical Jacobian is not
consulted, even though the newton path's KINSOL polish in this same file uses
it, and jacobian="auto" selects it everywhere else.
∂f/∂p by finite differences — one more RHS evaluation per parameter
(lines 755–781), by mutating the parameter in place.
Both use a fixed sqrt(eps) step. On a 1281-species model that is ~1300
interpreted RHS evaluations to assemble one Jacobian, and the codegen analytical
∂f/∂p — the same derivative the CVODES sensitivity RHS emits — is not used.
Why it matters
GH #214 retired the interpreted finite-difference sensitivity path for the
time-course solve on the grounds that its ~sqrt(eps) noise cannot support tight
tolerances, and made Simulator(..., sensitivity_params=...) refuse rather than
degrade. Issue #62 is the same defect surviving in compute_all_sensitivities.
The steady-state path is the third instance, and the only one where the
finite-difference derivative is the design, not a fallback: it will not be
fixed by the #62 change.
The docs are half-right about this. docs/user-guide/steady-state.md:142 says
∂f/∂p is computed by finite differences, but describes J as "the Jacobian at
steady state", which reads as the analytical one the rest of the library uses.
Suggested fix
Three separable pieces, in increasing order of work:
- Pass
self._codegen_so_path through in steady_state() and
steady_state_batch(), and honor it in steady_state.cpp (and add a
codegen_c_source field for the JIT backend). This alone speeds the state
solve up and gives the sensitivity code a compiled RHS to difference.
- Use the analytical Jacobian for
J when analytical_jacobian_complete is set,
falling back to finite differences otherwise — the same rule jacobian="auto"
already applies, and the same matrix the newton polish already uses.
- Use the codegen analytical
∂f/∂p when available, which is what makes the
result consistent with the GH #214 policy for the time-course path.
Until (2) and (3) land, the docs should say plainly that both factors are finite
differences.
Simulator.steady_state()andsteady_state_batch()run the interpreted RHSunconditionally, and their sensitivity matrix is built entirely from finite
differences. Both are invisible from Python: nothing warns, and the result comes
back looking like every other sensitivity result.
Evidence
1. The codegen field exists, is exposed, and is read by nobody.
SteadyStateOptionscarriesstd::string codegen_so_path(include/bngsim/types.hpp:782),and pybind exposes it. But:
Simulator.steady_state()builds aSteadyStateOptionsand setstol,max_time,method,rtol,atol,max_steps,jacobianandsensitivity_params— nevercodegen_so_path, even when the Simulator holds a.soinself._codegen_so_path.steady_state_batch()'s_run_onedoes thesame.
src/steady_state.cppcontains exactly one occurrence of the string "codegen",a comment at line 947. The option is never read on the C++ side either.
So a Simulator built with
codegen=Truesolves for steady state on the sameinterpreted path as one built with
codegen=False. There is also nocodegen_c_sourcefield at all, so the MIR JIT backend is not even representablehere.
2.
dY_ss/dpis finite-differenced twice over.compute_ss_sensitivity(src/steady_state.cpp:703) formsdY_ss/dp = -J⁻¹·(∂f/∂p)as:Jby finite differences —n_species + 1interpreted RHS evaluations, one percolumn, with the comment "We use finite differences (works for all rate law
types)" (lines 731–753). The model's complete analytical Jacobian is not
consulted, even though the
newtonpath's KINSOL polish in this same file usesit, and
jacobian="auto"selects it everywhere else.∂f/∂pby finite differences — one more RHS evaluation per parameter(lines 755–781), by mutating the parameter in place.
Both use a fixed
sqrt(eps)step. On a 1281-species model that is ~1300interpreted RHS evaluations to assemble one Jacobian, and the codegen analytical
∂f/∂p— the same derivative the CVODES sensitivity RHS emits — is not used.Why it matters
GH #214 retired the interpreted finite-difference sensitivity path for the
time-course solve on the grounds that its ~sqrt(eps) noise cannot support tight
tolerances, and made
Simulator(..., sensitivity_params=...)refuse rather thandegrade. Issue #62 is the same defect surviving in
compute_all_sensitivities.The steady-state path is the third instance, and the only one where the
finite-difference derivative is the design, not a fallback: it will not be
fixed by the #62 change.
The docs are half-right about this.
docs/user-guide/steady-state.md:142says∂f/∂pis computed by finite differences, but describesJas "the Jacobian atsteady state", which reads as the analytical one the rest of the library uses.
Suggested fix
Three separable pieces, in increasing order of work:
self._codegen_so_paththrough insteady_state()andsteady_state_batch(), and honor it insteady_state.cpp(and add acodegen_c_sourcefield for the JIT backend). This alone speeds the statesolve up and gives the sensitivity code a compiled RHS to difference.
Jwhenanalytical_jacobian_completeis set,falling back to finite differences otherwise — the same rule
jacobian="auto"already applies, and the same matrix the
newtonpolish already uses.∂f/∂pwhen available, which is what makes theresult consistent with the GH #214 policy for the time-course path.
Until (2) and (3) land, the docs should say plainly that both factors are finite
differences.