fix(petab): refuse a time_error fit on export instead of writing it as an exact-time one (#738) - #739
Merged
Merged
Conversation
…s an exact-time one (#738) A `time_error` clause on a `noise_model` line (ADR-0112, #587) says the reported measurement times are not exact: the objective integrates each observation's density over a prior on its true sampling time. `ploop` stores it under its own structural key, `('time_error', observable)`, and `config.py` reads that key to swap the whole per-point objective for a `MarginalizedTimeObjective`. The exporter had no awareness of the clause whatsoever -- `grep -n time_error pybnf/petab/export.py` was empty -- so a job carrying it exported with no warning and no exception, and the emitted problem held the measurements at their nominal times as though those were exact. Measured end to end on the demo job: `MarginalizedTimeObjective` before the round trip, `LikelihoodObjective` after, with the re-imported conf containing no `time_error` substring at all. That is a different statistical model, reached silently. Its sibling in the very same `noise_model` field grammar was guarded all along. `cumulative` is stored under the same kind of key by the same `nm_field` alternation, and `_reject_cumulative` refuses it with reasoning that transfers verbatim: exporting "would silently drop it and emit a problem that scores the raw cumulative columns, a different objective. Refuse instead (the project's fail-loud-over-silently-wrong stance)". That function's twin for `time_error` was never written. It is now, beside it, called from `export_job` between `_reject_cumulative` and `_reject_normalization`. Like the `u` flag of #736 and unlike #719 and #733, this is a real PEtab boundary rather than an unwritten mapping: a `measurements` row carries one exact `time` and has no field for a distribution over it. So the fix is a refusal naming the boundary, not a richer export. The refusal keys on the `('time_error', observable)` key rather than on the whole-fit shape that is reachable today, because the exporter reads the raw config while `_maybe_marginalize_time` is what defers the per-observable form; it names the observables when it finds them and says the fit when the clause is whole-fit. The `sigma_t` scale source rides on the same key and goes with it. Walking the whole `nm_field` alternation, `time_error`/`sigma_t` was the last gap of this shape in the objective/noise surface: `cumulative` and a mean-centred `location` were already refused, and `prediction_formula`, `formula` and the noise source all export and round-trip under test (the `predsigma_v2` and `scaling_v2` fixtures). Tests: three, of which two fail against the old code. The whole-fit clause is refused; a per-observable clause is refused too, which pins that the guard reads the config key rather than the currently-reachable shape; and a control exports the same fixture with the time clause removed, so the refusal cannot pass for some other property of the conf. The boundary is added to the two lists that enumerate them: the module docstring's in-code list, and the PEtab page, which now names the two refusals a user is most likely to meet on a job that otherwise looks perfectly exportable. Signed-off-by: Bill Hlavacek <hlavacek@lanl.gov>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #738. Fourth and last of the export-drops-part-of-a-declaration family, after #719
(PR #734), #733 (PR #735) and #736 (PR #737).
What was wrong
A
time_errorclause on anoise_modelline (ADR-0112, #587) says the reported measurementtimes are not exact: the objective integrates each observation's density over a prior on
its true sampling time.
ploopstores it under its own structural key,('time_error', observable), andconfig.pyreads that key to swap the whole per-pointobjective for a
MarginalizedTimeObjective.The exporter had no awareness of the clause whatsoever —
grep -n time_error pybnf/petab/export.pywas empty. So the job exported with no warning and no exception, and theemitted problem held the measurements at their nominal times as though exact. Measured end to
end on the demo job:
A different statistical model, reached in silence.
Why this is an oversight rather than a decision
Its sibling in the very same
noise_modelfield grammar was guarded all along.cumulativeis stored under the same kind of key by the samenm_fieldalternation, and_reject_cumulativerefuses it with reasoning that transfers verbatim:That function's twin for
time_errorwas never written. It is now, beside it, called fromexport_jobbetween_reject_cumulativeand_reject_normalization.Walking the whole
nm_fieldalternation, this was the last gap of its shape in theobjective/noise surface:
cumulativelocation(mean-centred)prediction_formula,formula, noise sourcepredsigma_v2,scaling_v2)time_error/sigma_tWhy a refusal, not a richer export
Like #736 and unlike #719 and #733, this is a real PEtab boundary rather than an unwritten
mapping: a
measurementsrow carries one exacttimeand has no field for a distribution overit. There is nothing to write the clause as.
The guard keys on the
('time_error', observable)config key rather than on the whole-fitshape that happens to be reachable today — the exporter reads the raw config, while it is
_maybe_marginalize_timethat defers the per-observable form. It names the observables when itfinds them and says "this fit" when the clause is whole-fit.
sigma_trides on the same key andgoes with it.
Tests
Three, of which two fail against the old code.
currently-reachable shape.
for some other property of the conf.
Full suite green: 5165 passed, 25 skipped.
ruff@0.15.14clean; the-W --keep-goingSphinx build succeeds.
Docs
The boundary is added to both lists that enumerate them: the
export.pymodule docstring'sin-code list, and
docs/petab.rst, which now names the two refusals a user is most likely tomeet on a job that otherwise looks perfectly exportable (this one and the
Utag from #736).No ADR amendment: the refuse-over-silently-wrong stance is already established by ADR-0025 and
by
_reject_cumulative, so there is no new decision here to record.