Reject step sizes that are spuriously self-consistent in Consistency - #77
Reject step sizes that are spuriously self-consistent in Consistency#77dweindl wants to merge 2 commits into
Conversation
`Consistency` checked whether the requested methods (e.g. forward/backward/central) agreed with each other at each step size, then blended every self-consistent size's mean into the final value with one final blanket tolerance check. A step size can become small enough that all methods sample points within the target function's floating-point noise floor and become correlated (affected by the same rounding/cancellation error) -- spuriously self-consistent, yet biased away from the true derivative. That biased size was still blended into the average while `success` reported `True`. Symmetrically, a large step size can also be self-consistently wrong due to higher-order/truncation effects. Add an order-independent iterative outlier-rejection pass (`Consistency._reject_outliers`) over the self-consistent step sizes' means: repeatedly drop the single worst-deviating candidate, relative to the median and a robust (MAD-based) spread of the rest, until nothing looks anomalous. This intentionally does not treat step size magnitude as a proxy for trustworthiness, and does not reuse the user's own `rtol` as the cross-size threshold (a fixed relative tolerance loose enough for the per-size check is too loose to catch this kind of bias no matter where it's applied; the point of using a spread estimated from the trusted candidates themselves is that it tightens automatically as they agree more closely). This changes `Consistency()`'s default behavior -- intentionally, since today's default is the bug. Two new tunable parameters, `trend_n_sigma` (default 5.0) and `min_trend_samples` (default 3), gate this behavior; below `min_trend_samples` self-consistent sizes, behavior is unchanged. Documented limitation (see updated class docstring): like any purely data-driven consensus check, this has a ~50% breakdown point -- if close to half the self-consistent step sizes are corrupted, it cannot reliably tell which subset is trustworthy. Still a strict improvement over today's ~0% effective breakdown point, where a single corrupted-but-self-consistent size already breaks the check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Hm, we could alternatively name this Regarding Weber, did you already check that appropriate simulation tolerances are used? I am not against a more robust |
For me it doesn't matter much whether this goes to an extra class or not, or whether it's opt-in in
I can't easily reproduce it to test much more. I am almost certain there would be some tolerance where this specific problem does not occur. However, my strong preference would be having a robust solution in fiddy that can deal with a given function instead of tuning fiddy hyperparameters for every single model and parameter vector. Without that, I don't see how for example AMICI-dev/AMICI#2093 could ever be implemented with fiddy. |
|
Thanks. The current Instead of introducing MAD, could we just take all FDs from self-consistent step sizes, and then take the largest subset of those FDs that are within rtol/atol of each other, then average those? This would be more aligned with the pre-existing check, re-using the same rtol/atol from the user. Also a warning should be emitted if a step size appears consistent but is then rejected IMO? |
Per review feedback: revert Consistency to its original behavior (no cross-step-size rejection), and move the MAD-based outlier rejection added in the previous commit into a new RobustConsistency(Consistency) subclass instead of changing Consistency's default behavior. This is non-breaking for any existing Consistency() caller, and lets the more robust checker be adopted explicitly (e.g. by AMICI) without a default-behavior debate. Also addresses the reviewer's request to warn when a step size is rejected: RobustConsistency.method now emits a UserWarning (with the count of rejected step sizes) whenever _reject_outliers drops one or more otherwise self-consistent step sizes. Shared logic (grouping results by step size, the within-size self-consistency check) is factored into Consistency._self_consistent_means so both classes reuse it without duplication. tests/test_success.py updated accordingly: the regression/robustness tests now target RobustConsistency; added a test asserting RobustConsistency's warning fires (with the right count) exactly when a step size is actually rejected, and not otherwise. Also added a test documenting a related, non-bug property found while re-testing: a wide, noise-free step-size range can legitimately narrow down to just the smallest/most-precise steps, since larger (but not wrong, just less precise) steps can look like outliers next to a cluster already near machine precision -- the blended value stays accurate regardless. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
For the issue I am addressing, this wouldn't help. Everything would be accepted. Or at least again require further fiddling around with the proper rtol/atol values. My feeling is that the MAD would be more robust. I followed your earlier suggestion and added it via a separate class for now.
Added. |
I don't see why... rtol/atol are the precision to which the user wants to compute the derivative, right? Hence, collecting and averaging all derivatives that are within this rtol/atol of each other should only keep approximately "true"/"reproducible across step size" gradient values, up to the user-chosen rtol/atol? I don't see why If you just want to get something merged without digging into it too much then fine for me since it's now a separate class, I can simply review the current state if you like. |
|
Thanks for pushing on this — let me try to answer with actual numbers rather than just intuition. Concrete data, from the synthetic repro I added in With I don't think this is really about picking a better number, though — I think it's structural. Worth being precise about scope too: I don't think fiddy's consistency check should try to assert final gradient accuracy — it structurally can't, it has no ground truth. That job is already Given it's a separate, opt-in class now, happy for you to just review the current state if that's easier — but wanted to give you the actual numbers first since you asked. 🤖 Generated with Claude Code |
|
Then rather implement some |
Well, again, my plan was to not tune tolerances for every single model. If you don't think this is helpful to have in fiddy, I will move it to amici. |
|
Ah, by tune I thought you meant manually tune. What is the issue with automatic relative step sizes? I see the benefit of checking for consistency across step sizes, but I don't yet see the logic in having two different concepts of what consistency means: one for same-size, and one for across-sizes. The only reason seems to be that rtol is too relaxed. |
…5 gradient check fiddy's `Consistency` checker only verifies that forward/backward/central methods agree with each other at a given step size, then blends every self-consistent size into the final value. A step size can become small enough that all methods sample points within the target function's floating-point noise floor and become spuriously self-consistent while biased away from the true derivative -- the confirmed root cause of the intermittent test_benchmark_gradient[Weber_BMC2015-*-unscaled] failures (AMICI-dev#3078), affecting parameter a32, which is orders of magnitude smaller than the model's other free parameters. This was originally proposed upstream as ICB-DCM/fiddy#77, which added a `RobustConsistency` subclass performing iterative, order-independent, MAD-based outlier rejection across self-consistent step sizes. After review feedback, that PR was closed in favor of implementing it directly in AMICI, since fiddy's maintainer preferred not to add a parameter-magnitude-calibrating statistic to a general-purpose library. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ICB-DCM/fiddy#77 was closed after review feedback; the fix for the flaky Weber_BMC2015 gradient check (AMICI-dev#3078) is now implemented directly in AMICI (RobustConsistency in amici.adapters.fiddy) instead, so the benchmark CI job no longer needs to depend on an unmerged fiddy branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Consistency(the defaultsuccess_checkerforget_derivative) checked whether the requested methods (e.g. forward/backward/central) agreed with each other at each step size, then blended every self-consistent size's mean into the final value, with one final blanket tolerance check across those means.The bug: this never compares a step size's estimate against the results from other step sizes. A step size can become small enough that all methods sample points within the target function's floating-point noise floor and become correlated (affected by the same rounding/cancellation error) — spuriously self-consistent, yet biased away from the true derivative. That biased size still got blended into the average while
successreportedTrue. Symmetrically, a large step size can also be self-consistently wrong due to higher-order/truncation effects.This is the confirmed root cause of a long-standing intermittent CI failure in AMICI's PEtab benchmark gradient test (
test_benchmark_gradient[Weber_BMC2015-*-unscaled]) — see AMICI-dev/AMICI#3078. That test usesfiddy.get_derivativewithConsistencyto finite-difference-check an analytically computed gradient for a model parameter (a32) several orders of magnitude smaller than the model's other free parameters. Two independent CI runs, 9 days apart, on identical code/commit, produced a bit-identical wrong finite-difference value (860.83vs. the stable, cross-checked analytic value~872.68); other CI runs on the same commit passed. The FD computation for that parameter was on a knife-edge where a negligible amount of run-to-run numerical noise was enough to flip a step size's self-consistency check and pull a biased estimate into the blend — a fragility ofConsistency's algorithm, not of AMICI's simulation. A minimal, deterministic, AMICI-free repro of the same mechanism is nowtest_consistency_rejects_rounding_noise_dominated_step_sizesin this PR.Fix
Add an order-independent iterative outlier-rejection pass (
Consistency._reject_outliers) over the self-consistent step sizes' means: repeatedly drop the single worst-deviating candidate — relative to the median and a robust (MAD-based) spread of the rest — until nothing looks anomalous.Deliberately does not:
rtolas the cross-size threshold: a relative tolerance loose enough to make sense for the per-size check is too loose to catch this kind of bias no matter where it's applied. A spread estimated from the trusted candidates themselves tightens automatically as they agree more closely, regardless of how loose the user's ownrtolis.This changes
Consistency()'s default behavior, intentionally — today's default is the bug. Two new parameters,trend_n_sigma(default5.0) andmin_trend_samples(default3), gate this; belowmin_trend_samplesself-consistent sizes, behavior is unchanged from before.Documented limitation (see the updated class docstring): like any purely data-driven consensus check, this has a ~50% breakdown point — if close to half (or more) of the self-consistent step sizes are corrupted, it cannot reliably tell which subset is trustworthy. That's a fundamental property of median/MAD-based statistics, not something engineerable away from data alone. Still a strict improvement over today's ~0% effective breakdown point, where a single corrupted-but-self-consistent size already breaks the check.
Test plan
pytest tests/— full suite passes (98 tests, including newtests/test_success.py)pre-commit run --all-files— ruff lint/format clean_reject_outlierscovering: below-threshold no-op, no-outlier no-op, single/multiple outlier removal, order-independence,trend_n_sigmatuning, vector-valued outputs (drop whole candidate on any-element outlier), and a documented NaN-candidate edge caseexclude_params_unscaledworkaround forWeber_BMC2015intests/benchmark_models/test_petab_benchmark.py🤖 Generated with Claude Code