Skip to content

Add simulation option 'jacobian_mode' (auto | dd | fd) and a kink guard for the finite-difference Jacobian - #424

Draft
hubertus65 wants to merge 3 commits into
modelon-community:masterfrom
hubertus65:pr-jacobian-mode
Draft

hubertus65 wants to merge 3 commits into
modelon-community:masterfrom
hubertus65:pr-jacobian-mode

Conversation

@hubertus65

Copy link
Copy Markdown

Summary

A new simulation option jacobian_mode ("auto" | "dd" | "fd") choosing how the PyFMI
Jacobian for an ME FMU is formed, and a kink guard that makes the finite-difference path
trustworthy enough to be the automatic choice. Three commits.

commit change
9823fa8 the option: "auto" times one fmi2GetDirectionalDerivative call against one rhs evaluation at initialization and picks the cheaper; scoped to the simulation, so a later manual Jacobian call is unaffected. "dd" is the previous behaviour. The mode actually used is on the algorithm object as jacobian_mode.
f352f04 FMUModelME2.fd_kink_guard: the kink guard on the coloured finite-difference Jacobian
(docs) records in the option help what the guard protects against

Why

On the 12 ME FMUs of this study the Jacobian evaluation is 55–85 % of the solver time, because
one directional-derivative call costs about 2× an rhs evaluation and the colouring is bounded by
the densest Jacobian row. Coloured finite differences cost one rhs per colour group.

model CVode Radau5ODE
226-state plant 164 s → 91 s 529 s → 210 s
141-state vehicle model 37 s → 15 s 379 s → 23 s
small models unchanged unchanged

"auto" declines to use finite differences when the tolerance is tight enough that the
forward-difference truncation error (~sqrt(eps)) would stall Newton (rtol < 1e-7), when the
solver needs an exact Jacobian (Rosenbrock methods), and when a directional derivative is cheap
anyway.

The kink guard, and why the option is not enough without it

Plain coloured finite differences are not safe by default, which is the interesting part of
this PR. On a 318-state combined-cycle plant, two entries per ~200 Jacobians had jump/h ≈ 1e8
— a difference taken across a steam-table region boundary — and that corrupted the Newton matrix
for many steps afterwards: 91 of 308 plant states ended more than 1e-3 off the reference, at
rtol 1e-6 and 5e-7 but not at 2e-7 or 1e-7, so no check at initialization detects it and a tighter
tolerance appears to "fix" it.

The guard: an entry that jumps more than 1e3× against the previous Jacobian flags its colour
group; a flagged group is re-differenced with h/10; entries whose quotient then moves more than
10 % are kinks and take the directional derivative if the FMU has one, otherwise the
smaller-magnitude one-sided difference.

With it, the finite-difference run matches the directional-derivative run on that model (median
deviation 5.0e-6) in 229 s instead of 340 s.

Behaviour change

FMUs whose directional derivatives cost more than an rhs now get finite differences by default.
options["jacobian_mode"] = "dd" restores the previous behaviour per run. FMUs without
directional derivatives are unaffected except that the guard adds one extra Jacobian on the first
evaluation and then only re-differences flagged groups.

Review notes

  • f352f04 touches fmi2.pyx / fmi2.pxd; the rest is fmi_algorithm_drivers.py.
  • Two commits of the original branch that set the default to "dd" and then back to "auto"
    have been squashed out; what remains is one decision with its documentation.
  • Tests: test_jacobian_mode_option (mode selection under every combination of capability,
    solver and tolerance) and the kink-guard tests in tests/test_fmi2.py.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PyrxGeWuwCmAH9LishMW6p

hubertus65 and others added 3 commits September 22, 2026 19:38
…Jacobian

When 'with_jacobian' is in effect and the FMU provides directional derivatives,
PyFMI evaluates the Jacobian with one fmi2GetDirectionalDerivative call per
colour group of the sparsity pattern. On FMUs where one such call costs more
than an rhs evaluation (measured 1.7x on OCT exports of 140-320 state models),
coloured forward differences give the same Jacobian for a third of the cost,
and because the colouring is bounded by the densest Jacobian row, Jacobian
evaluation was 55-85 % of the solver time on those models. Measured with
CVode: absorptionplant (226 states) 164 -> 89 s; Radau5ODE 529 -> 206 s; same
step counts, events and accuracy (deviation <= 4e-4 at rtol 1e-6).

'jacobian_mode' = "auto" (default) times one rhs and one directional-derivative
call at initialization and picks "fd" when the derivative is the more expensive
one, except for RodasODE (a Rosenbrock method needs the exact Jacobian), the
sparse linear solver, and rtol < 1e-7 (forward-difference truncation error
~sqrt(eps) then stalls Newton, measured on a 318-state plant at rtol 1e-8).
"dd" and "fd" force either. Without directional derivatives the mode is
always "fd". The setting is scoped to the simulation: the model's
force_finite_differences attribute is restored afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A forward difference taken across a kink of the model -- a limiter, a region
boundary of a property function -- returns jump/h (1e8 and more) where the
derivative on this side is small. Newton-based integrators converge on the
corrupted iteration matrix and reuse it for many steps. Measured on a 318-
state combined-cycle plant at rtol 1e-6: two such entries in 200 Jacobian
evaluations (d der(pump.s)/d s = -3.2e8 vs 0 at the end of the steam-table
failure zone; d der(drum.p)/d hl = -1.3e8 vs -0.18 at t = 0) moved 91 of 308
plant states more than 1e-3 off the rtol 1e-8 reference.

The guard (FMUModelME2.fd_kink_guard, default True) works per colour group of
_estimate_directional_derivative:
- flag: an entry that grew more than 1e3x relative to the previous Jacobian
  and is not negligible relative to it marks the group as suspect; the first
  evaluation checks every group. Kinks are transient, so this is nearly free.
- confirm: a suspect group is re-differenced with a 10x smaller step; entries
  whose quotient changes by more than 10 % are kinks (a jump scales with 1/h,
  a smooth function does not).
- fix: kink entries take the FMU's directional derivative for the group when
  it has one, else the smaller in magnitude of the forward and the backward
  difference (the side without the jump).
Model variables are restored after each check; statistics in _fd_guard_stats.

With the guard, CVode with the FD Jacobian on the plant matches the DD run's
accuracy (plant median 5.0e-6 vs 9.8e-6, 4 vs 1 states beyond 1e-3, 23
events) at 5458 instead of 14353 steps and 320 instead of 511 Jacobians, each
half the cost: 229 s instead of 340 s. On a 226-state plant and a 141-state
vehicle model the guard changes nothing (accuracy and time within noise).
FMI3's separate finite-difference routine is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyrxGeWuwCmAH9LishMW6p
…tion's help

The default stays "auto" (as introduced); this records what the kink guard
protects against and asserts the default in the option test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyrxGeWuwCmAH9LishMW6p
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.

1 participant