An experiment's measurement_params: file gives per-row values for a named placeholder, such as a per-row noise sigma noiseParameter1_obs_Obs_A. PyBNF uses those values only when the column's noise model or measurement formula names that placeholder. With sigma = column_mean, objective = ave_norm_sos or sigma = fix_at 1, PyBNF loads the file, checks that every row has a value, and then never reads it. The fit is identical with or without the file, and nothing is printed. In the example, the file gives an outlier a sigma of 1000 so that it barely counts. The fit ignores this and returns k = 0.474, not the k = 0.500 that honours the file (which is also the true value). PyBNF should refuse the job at load and name the experiment, the column and the placeholder that nothing reads. libpetab's lint already rejects the same combination in PyBNF's own export of this job.
Example
decay.bngl: one species, A() 100, rule A() -> 0 k, observable Molecules Obs_A A().
meas.exp is 100·e^(−0.5t) at t = 0, 0.5, …, 5, except that the last point is an outlier (30 instead of 8.21):
# time Obs_A
0 100
0.5 77.88007831
...
4.5 10.53992246
5 30
mp.tsv gives every row sigma 1, except the outlier, which gets 1000:
column time placeholder token
Obs_A 0 noiseParameter1_obs_Obs_A 1
...
Obs_A 4.5 noiseParameter1_obs_Obs_A 1
Obs_A 5 noiseParameter1_obs_Obs_A 1000
V1.conf:
edition = 2
job_type = de
model: decay.bngl
noise_model = normal, sigma = column_mean
experiment: meas, data: meas.exp, measurement_params: mp.tsv
uniform_var = k 0.05 3.0
population_size = 16
max_iterations = 40
random_seed = 3
Results of pybnf -c <conf>. Each variant changes only the noise line, or whether the measurement_params: field is present:
| noise model |
measurement_params |
best k |
objective |
sigma = column_mean |
mp.tsv |
0.47442 |
0.137838 |
sigma = column_mean |
none |
0.47442 |
0.137838 |
objective = ave_norm_sos |
mp.tsv |
0.47442 |
0.137838 |
sigma = fix_at 1 |
mp.tsv |
0.47442 |
225.553 |
sigma = fix_at 1 |
the same file with observableParameter1_obs_Obs_A |
0.47442 |
225.553 |
sigma = formula noiseParameter1_obs_Obs_A |
mp.tsv |
0.49999 |
6.908 |
Oracle (scipy minimize_scalar on Σ((100·e^(−kt) − y)/σ)², k in [0.05, 3]): σ = column mean (40.452), or any constant, gives k = 0.47432, and σ from mp.tsv gives k = 0.50000. numpy gives 0.5·Σ((100·e^(−kt) − y)/40.452)² = 0.137838 at PyBNF's k, which matches the first three rows.
libpetab agrees that the file has nothing to bind to. export_job('V1.conf', 'pet') succeeds. It writes noiseFormula 40.452 and the values 1…1000 into the noiseParameters column, and petab.v2.lint.lint_problem then reports, once per row: No placeholders have been specified in the noise model for observable obs_Obs_A, but a parameter ID or multiple overrides were specified in the noiseParameters column.
The reverse mismatch is not checked at load either. With sigma = formula noiseParameter1_Obs_A, a placeholder that mp.tsv does not bind, the conf loads, and every evaluation logs The per-measurement binding table for 'Obs_A' has no value for placeholder 'noiseParameter1_Obs_A' at data row 0. After 100 of them the run stops with Error: Your simulations are failing to run. Logs from failed simulations are saved in the FailedSimLogs directory.
Why
_attach_measurement_params (pybnf/config.py:2285-2326) attaches every column and placeholder the file names. It checks only that each data row has a value (config.py:2362). It does not check that the column is scored or that anything on that column names the placeholder. Only the per-row noise source (PerMeasurementFormulaSigma, pybnf/noise/source.py:297) and the per-row measurement model (PerMeasurementModel, pybnf/measurement/base.py:188) look the file up. ColumnMeanSigma.value (noise/source.py:643-649) returns the column mean, and ConstantSigma and the legacy ave_norm_sos never consult the file. In the reverse case, the lookup happens only while scoring (noise/source.py:440-449). Its error is caught by run_simulation (pybnf/algorithms/core.py:407-414) and counted as a failed simulation.
Possible fix
At load, once the noise sources and measurement formulas are built, cross-check each experiment's file both ways. Every (column, placeholder) pair in the file must be read by that column's noise source or measurement formula, and every placeholder that a column's formula names must be bound for every experiment that scores the column. Refuse otherwise, naming the experiment, the column and the placeholder. export_job should apply the same check, so that it does not write a problem libpetab rejects.
Related: #818
Found while fixing the PEtab issues #892–#908 (2026-09-25); reproduced on main at cd2be67.
An experiment's
measurement_params:file gives per-row values for a named placeholder, such as a per-row noise sigmanoiseParameter1_obs_Obs_A. PyBNF uses those values only when the column's noise model or measurement formula names that placeholder. Withsigma = column_mean,objective = ave_norm_sosorsigma = fix_at 1, PyBNF loads the file, checks that every row has a value, and then never reads it. The fit is identical with or without the file, and nothing is printed. In the example, the file gives an outlier a sigma of 1000 so that it barely counts. The fit ignores this and returns k = 0.474, not the k = 0.500 that honours the file (which is also the true value). PyBNF should refuse the job at load and name the experiment, the column and the placeholder that nothing reads. libpetab's lint already rejects the same combination in PyBNF's own export of this job.Example
decay.bngl: one species,A() 100, ruleA() -> 0 k, observableMolecules Obs_A A().meas.expis 100·e^(−0.5t) at t = 0, 0.5, …, 5, except that the last point is an outlier (30 instead of 8.21):mp.tsvgives every row sigma 1, except the outlier, which gets 1000:V1.conf:Results of
pybnf -c <conf>. Each variant changes only the noise line, or whether themeasurement_params:field is present:sigma = column_meansigma = column_meanobjective = ave_norm_sossigma = fix_at 1sigma = fix_at 1observableParameter1_obs_Obs_Asigma = formula noiseParameter1_obs_Obs_AOracle (scipy
minimize_scalaron Σ((100·e^(−kt) − y)/σ)², k in [0.05, 3]): σ = column mean (40.452), or any constant, gives k = 0.47432, and σ frommp.tsvgives k = 0.50000. numpy gives 0.5·Σ((100·e^(−kt) − y)/40.452)² = 0.137838 at PyBNF's k, which matches the first three rows.libpetab agrees that the file has nothing to bind to.
export_job('V1.conf', 'pet')succeeds. It writes noiseFormula40.452and the values 1…1000 into thenoiseParameterscolumn, andpetab.v2.lint.lint_problemthen reports, once per row:No placeholders have been specified in the noise model for observable obs_Obs_A, but a parameter ID or multiple overrides were specified in the noiseParameters column.The reverse mismatch is not checked at load either. With
sigma = formula noiseParameter1_Obs_A, a placeholder thatmp.tsvdoes not bind, the conf loads, and every evaluation logsThe per-measurement binding table for 'Obs_A' has no value for placeholder 'noiseParameter1_Obs_A' at data row 0. After 100 of them the run stops withError: Your simulations are failing to run. Logs from failed simulations are saved in the FailedSimLogs directory.Why
_attach_measurement_params(pybnf/config.py:2285-2326) attaches every column and placeholder the file names. It checks only that each data row has a value (config.py:2362). It does not check that the column is scored or that anything on that column names the placeholder. Only the per-row noise source (PerMeasurementFormulaSigma, pybnf/noise/source.py:297) and the per-row measurement model (PerMeasurementModel, pybnf/measurement/base.py:188) look the file up.ColumnMeanSigma.value(noise/source.py:643-649) returns the column mean, andConstantSigmaand the legacyave_norm_sosnever consult the file. In the reverse case, the lookup happens only while scoring (noise/source.py:440-449). Its error is caught byrun_simulation(pybnf/algorithms/core.py:407-414) and counted as a failed simulation.Possible fix
At load, once the noise sources and measurement formulas are built, cross-check each experiment's file both ways. Every (column, placeholder) pair in the file must be read by that column's noise source or measurement formula, and every placeholder that a column's formula names must be bound for every experiment that scores the column. Refuse otherwise, naming the experiment, the column and the placeholder.
export_jobshould apply the same check, so that it does not write a problem libpetab rejects.Related: #818
Found while fixing the PEtab issues #892–#908 (2026-09-25); reproduced on main at cd2be67.