Skip to content

Per-row noise values in a measurement_params file are silently ignored when the column's sigma is column_mean, ave_norm_sos or fix_at, because no formula reads them #958

Description

@wshlavacek

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsilent-incorrectnessCompletes without error, but the reported result is wrong, incomplete, or missing

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions