Skip to content

sensitivity: refuse a parameter that sits on a kink of a rate law (#1332) - #1380

Open
Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1332-sensitivity-on-a-kink
Open

Montana wants to merge 1 commit into
lanl:mainfrom
Montana:fix/1332-sensitivity-on-a-kink

Conversation

@Montana

@Montana Montana commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Closes #1332.

What was wrong

A rate law that bends in a parameter (abs, min, max, clamp, if()), at a value that puts it exactly on the bend, has no derivative in that parameter. The model is 0 -> X at r(), X -> 0 at k1 = 0.21, so X* = r/k1. On main c0877fa, at the steady state and in a run to t = 100 alike:

r() at dX*/dq returned one-sided slopes
10*abs(q) + s q = 0 0 −47.62 and +47.62
10*min(q, 1) + s q = 1 23.81 47.62 and 0

Both come from the compiled ∂f/∂p (sens_dfdp_source "codegen"). It differentiates each branch and takes sign(0) = 0 and half at a tie (_kink_bindings in _jacobian.py). Neither number is a derivative, and no error was raised. dX*/ds was right throughout.

The fix

New python/bngsim/_parameter_kinks.py. parameters_on_a_kink(core, params) works as follows:

A bend is reported when:

  • abs(u): u == 0 exactly, and ±u differ in their slope in p.
  • min/max: the extreme is attained by two or more arguments whose slopes in p differ. A tie of one slope has a derivative and is left alone.
  • clamp(lo, x, hi): x is on an edge, with the two sides' slopes differing.
  • if(): a condition's two sides are equal and their difference moves with p. Such a switch is a jump or a corner. This is conservative for an if() whose branches happen to agree in value and slope at the boundary.

Exact zero is deliberate. It is precisely where the compiled derivative returns neither side's slope. A bend an ulp away gives the one-sided derivative of the function the engine evaluates.

Simulator._raise_if_param_on_a_kink raises SensitivityUnsupportedError, naming each parameter, reaction and bend:

Forward sensitivity is not supported where a parameter sits on a kink of a rate law, where the law has no derivative in it: 'q' in reaction 1 (r): abs(q), on its switch at q = 0. Its slopes on the two sides differ, and the column would be one side's, or neither (issue #1332). Move the parameter off the switch, or difference the run at p +/- h on each side.

It is asked at the values each solve uses, since a set_param can put a parameter on a kink after the Simulator is built:

Tests (test_sensitivity_parameter_on_a_kink.py, 15)

test on main
steady state on a kink: abs at 0, min and max ties at 1, if(q > 0, …) at 0 (4) fail: values returned
run on a kink, the same 4 fail
set_param onto the kink after the Simulator is built fail
a run_batch row on the kink, the model itself off it fail
compute_all_sensitivities on the kink fail
controls: off the kink, dX*/dq = 10/k1 to 1e-8; s, which the bend does not move, at q = 0 pass
controls: a tie of one slope, min(q, q*(s - 0.3)) at q = 1, exactly 1.0 = 1.0; a bend over the state, abs(Xo - q), left to the envelope pass

Mutations

mutant fails
K2: any tie is a kink, even of one slope the one-slope tie control
K3: near the switch counts as on it the off-the-kink control
K4: run() does not ask 4 run tests and the set_param test
K5: batch rows do not ask the batch-row test
K6: compute_all_sensitivities does not ask its test
K1: drop the "the bend reads p" fast path none: equivalent, since a bend p does not read has slope 0 on both sides, which the slope test already passes

K2 first survived on a control that sympy simplified away (min(q, q) is q), and then on one whose tie missed by an ulp (q*s/1.3). The control in the PR is an exact tie.

Measured

Full suite (macOS arm64, main c0877fa + this branch): 16,430 passed, 0 failed, 137 skipped. No existing sensitivity test is refused, and the suite's wall time is unchanged (8 min). The suite ran before test_compute_all_sensitivities_on_the_kink_is_refused was added; that file passes 15 of 15. pre-commit run --all-files (new files staged) and sphinx -W with docs/requirements-ci.txt are clean.

Not in this PR

…nl#1332)

abs, min, max, clamp and if() over parameters alone, at values that put
them exactly on their switch, have no derivative in a parameter that
moves it. The compiled dfdp took sign(0) = 0 and the average at a tie:
10*abs(q) + s at q = 0 gave dX*/dq = 0 for one-sided slopes of +-47.62,
and 10*min(q, 1) + s at q = 1 gave 23.81 for 47.62 or 0, with no error.
Such a column is refused (SensitivityUnsupportedError, naming the
parameter, reaction and bend), at the values of each run, steady state,
compute_all_sensitivities call and batch row.

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.

Forward sensitivity at a parameter that sits on a kink of a rate law returns one side's slope or neither (dX*/dq = 0 for ±47.6), with no error

1 participant