Part of #407 (PEtab v2 two-adapter umbrella; exporter-first pivot, ADR-0025).
Builds on #420 (the BnglModel adapter — now dogfooded: pybnf[tests] pins the Step B
libpetab-python fork branch, so Problem.from_yaml validates language: bngl problems at
MODEL level natively, all 18 default_validation_tasks, in-process and in CI).
Goal
The next exporter chunk after chunk 1 (single BNGL model, one time-course .exp): export a
PyBNF job with multiple experimental conditions to PEtab v2 conditions.tsv +
experiments.tsv, and lift the dose-response boundary. Chunk 1 emits everything with an
empty experimentId ("model as is"); this chunk gives experiments and conditions real
structure.
This is also where the BnglModel methods #420/ADR-0026 left only trivially exercised finally
get real requirements: get_valid_ids_for_condition_table (= params ∪ compartments) and
is_state_variable (= seed species), via CheckValidConditionTargets,
CheckValidParameterInConditionOrParameterTable, and CheckInitialChangeSymbols.
The mapping (recon; verify against the cited file:line)
- PyBNF mutant → PEtab condition. Grammar
parse.py:142: mutant = <base> <name> <var op val>... : f1.exp, ..., op ∈ {= * / + -}. config.py:613 _load_mutants → Mutation(var, op, val) (pset.py:1351) + MutationSet (pset.py:1401), attached via model.add_mutant (pset.py:415). Each mutation → one conditions.tsv row (conditionId, targetId=var, targetValue). = is an absolute set; * / + - are relative to the base value.
- PyBNF suffix → PEtab experiment.
model.suffixes = (sim_type, prefix) tuples (pset.py:446, get_suffixes pset.py:408/846); each suffix is one simulation output = one timecourse. The base time_course suffix → the base experiment; a mutant's .exp is named <base_suffix><mutant_name>.exp (config.py:628-635), so experimentId derives from suffix+mutant and the experiment references that mutant's condition.
- Dose-response → conditions + measurements. PyBNF
param_scan (ParamScan, pset.py:1280: param/min/max/step/time/logspace/suffix); the .exp independent axis is the swept param (not time). Each scan point → a condition setting param=value; measurements at the scan's fixed time. Currently rejected at measurements.py:69.
- Boundaries to lift:
export.py ~99-136 / 207; measurements.py:69.
Key design decisions to settle (design-first / ADR before code)
- Mutating a parameter that is also a free/fit parameter (the crux). PyBNF mutants routinely modify fit params (e.g.
examples/yeast_cell_cycle/yeast.conf), but PEtab's CheckValidParameterInConditionOrParameterTable errors if an id is in both the parameter table and a condition target. Decide the PEtab-idiomatic mapping (condition-specific override via the mapping table? per-condition placeholder parameters? restrict the chunk to non-fit-param mutations and defer the rest?). Gates the whole chunk.
- Relative operators
* / + -: PEtab targetValue is an expression; decide how to express a scaling/offset of the base value (expression referencing the base nominal vs precomputed absolute vs defer non-=).
- Scope: mutants and dose-response both this chunk, or mutants first + dose-response as an immediate follow-on (they share the conditions/experiments machinery; dose-response adds the swept-axis pivot).
- Fixture/oracle: a synthetic minimal multi-condition conf (extend
examples/demo) is the recommended gradeable fixture — the real mutant example (yeast) uses .prop/.con constraint files, not quantitative .exp data.
Acceptance
export_job on a multi-condition fixture writes conditions.tsv + experiments.tsv and lifts the relevant NotImplementedErrors.
- The exported problem loads via
Problem.from_yaml (native BnglModel) and passes all 18 default_validation_tasks with zero errors — genuinely exercising the condition/experiment cross-checks above.
- Strong table oracle: condition/experiment cells map exactly to the PyBNF mutations/suffixes; the measurement pivot stays exact.
- Remaining deferred boundaries still raise
NotImplementedError (no silent mis-export).
- ruff clean; petab test tier green; mirrors
tests/test_petab_export.py.
Refs: ADR-0025 (exporter-first), ADR-0026 (BnglModel; the methods this chunk exercises for real), #407 (umbrella), #420 (adapter/dogfooding).
Part of #407 (PEtab v2 two-adapter umbrella; exporter-first pivot, ADR-0025).
Builds on #420 (the BnglModel adapter — now dogfooded:
pybnf[tests]pins the Step Blibpetab-python fork branch, so
Problem.from_yamlvalidateslanguage: bnglproblems atMODEL level natively, all 18
default_validation_tasks, in-process and in CI).Goal
The next exporter chunk after chunk 1 (single BNGL model, one time-course
.exp): export aPyBNF job with multiple experimental conditions to PEtab v2
conditions.tsv+experiments.tsv, and lift the dose-response boundary. Chunk 1 emits everything with anempty
experimentId("model as is"); this chunk gives experiments and conditions realstructure.
This is also where the BnglModel methods #420/ADR-0026 left only trivially exercised finally
get real requirements:
get_valid_ids_for_condition_table(= params ∪ compartments) andis_state_variable(= seed species), viaCheckValidConditionTargets,CheckValidParameterInConditionOrParameterTable, andCheckInitialChangeSymbols.The mapping (recon; verify against the cited
file:line)parse.py:142:mutant = <base> <name> <var op val>... : f1.exp, ..., op ∈{= * / + -}.config.py:613_load_mutants→Mutation(var, op, val)(pset.py:1351) +MutationSet(pset.py:1401), attached viamodel.add_mutant(pset.py:415). Each mutation → oneconditions.tsvrow (conditionId, targetId=var, targetValue).=is an absolute set;* / + -are relative to the base value.model.suffixes=(sim_type, prefix)tuples (pset.py:446,get_suffixespset.py:408/846); each suffix is one simulation output = one timecourse. The basetime_coursesuffix → the base experiment; a mutant's.expis named<base_suffix><mutant_name>.exp(config.py:628-635), soexperimentIdderives from suffix+mutant and the experiment references that mutant's condition.param_scan(ParamScan,pset.py:1280:param/min/max/step/time/logspace/suffix); the.expindependent axis is the swept param (not time). Each scan point → a condition settingparam=value; measurements at the scan's fixedtime. Currently rejected atmeasurements.py:69.export.py~99-136 / 207;measurements.py:69.Key design decisions to settle (design-first / ADR before code)
examples/yeast_cell_cycle/yeast.conf), but PEtab'sCheckValidParameterInConditionOrParameterTableerrors if an id is in both the parameter table and a condition target. Decide the PEtab-idiomatic mapping (condition-specific override via the mapping table? per-condition placeholder parameters? restrict the chunk to non-fit-param mutations and defer the rest?). Gates the whole chunk.* / + -: PEtabtargetValueis an expression; decide how to express a scaling/offset of the base value (expression referencing the base nominal vs precomputed absolute vs defer non-=).examples/demo) is the recommended gradeable fixture — the real mutant example (yeast) uses.prop/.conconstraint files, not quantitative.expdata.Acceptance
export_jobon a multi-condition fixture writesconditions.tsv+experiments.tsvand lifts the relevantNotImplementedErrors.Problem.from_yaml(native BnglModel) and passes all 18default_validation_taskswith zero errors — genuinely exercising the condition/experiment cross-checks above.NotImplementedError(no silent mis-export).tests/test_petab_export.py.Refs: ADR-0025 (exporter-first), ADR-0026 (BnglModel; the methods this chunk exercises for real), #407 (umbrella), #420 (adapter/dogfooding).