Skip to content

Gradient path: add a discrete-events pre-flight gate (refuse cleanly, don't fail mid-run) #461

Description

@wshlavacek

Summary

The gradient optimizers (#386) refuse an unsupported fit with a clean pre-flight PybnfError that points the user at a metaheuristic fit_type. Today that happens in three places (pybnf/algorithms/optimizers/gradient_base.py), plus a per-evaluation objective gate:

  • edition_require_edition_2 (before any model is built);
  • backend_require_sensitivity_backend (after model build: the model must expose bngsim's forward-sensitivity hooks);
  • capability — deferred to _setup_gradient_path's apply_routing (the bngsim build must have the output_sensitivities feature);
  • objective — a per-evaluation GradientNotSupportedPybnfError for an objective the assembly cannot differentiate.

#386's deliverable list called for one more — "Capability / gradient-availability / events gate … Refuse to start … when the model has discrete events (BNGsim raises on sensitivity requests there)" — and that one was never implemented. This issue tracks it (it is the discrete-events half of #386 deliverable 5, the remaining unchecked gate).

The gap

A model with discrete events (a state-dependent discrete change in the dynamics) has no smooth forward sensitivity, so bngsim raises when sensitivities are requested for it. Because there is no pre-flight gate, such a model does not get the clean "use a metaheuristic fit_type" refusal the other gates give — instead it fails mid-run, at _setup_gradient_path / the first sensitivity-bearing simulate(), with a backend-level error. The requirement is documented (docs/gradient_fitting.rst: "requires a deterministic ODE simulation of a reaction network … run with method=>\"ode\"") but it is only documented, not enforced up front.

What to build

A new gate in GradientOptimizer (gradient_base.py) — e.g. _require_differentiable_dynamics — fired as early as it can be (after model build, next to _require_sensitivity_backend), that detects a discrete-event model and raises a PybnfError carrying the shared _FALLBACK_HINT. Mirror the existing gate pattern exactly:

def _require_sensitivity_backend(self):
    for model in self.model_list:
        if not hasattr(model, 'enable_output_sensitivities'):
            raise PybnfError("Gradient-based fitting (fit_type = %s) requires the bngsim "
                             "backend's forward sensitivities, but model '%s' ..." % (...),
                             _FALLBACK_HINT)

Detection mechanism — TBD / needs confirmation. I did not pin down how a discrete event is represented at the PyBNF/bngsim model layer (an events block? a model attribute exposed on the BngsimModel? something only the bngsim engine knows?). The implementer should confirm where the signal lives; the gate may need a small bngsim-side hook (a model.has_discrete_events / capability query) so the refusal can be made before the run rather than caught from a backend exception. If a pre-flight signal genuinely isn't available, the fallback is to catch the bngsim sensitivity-request exception at _setup_gradient_path and re-raise it as the same clean PybnfError + fallback hint (a late but still actionable gate).

Adjacent (decide in scope)

Non-ODE simulation methods — SSA (stochastic) and NFsim (network-free) — are likewise non-differentiable, and there is no runtime gate for them on the gradient path either (only the documented method=>\"ode\" requirement). Decide whether this gate should be the unified "smooth deterministic ODE" check covering both the events case and the non-ODE-method case, or events-only with the method case tracked separately.

Done criteria

  • A discrete-event model selected with fit_type = trf / lbfgs is refused before the run (or at worst at path activation) with an actionable PybnfError pointing at a metaheuristic fit_type — never a raw backend traceback.
  • A test mirroring the existing gate tests (test_trf_refuses_when_bngsim_lacks_output_sensitivities / test_trf_refuses_non_least_squares_objective_pointing_at_lbfgs) covering the events refusal.
  • gradient_base.py's gate comment block (currently lists three gates) updated to four; docs/gradient_fitting.rst notes the ODE/no-events requirement is enforced, not just documented.
  • Decide and record the scope re: SSA / NFsim (unified non-differentiable-dynamics gate vs events-only).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions