Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

PEtab export ignores the global noise_location = mean key, so a mean-centered lnnormal fit is exported as PEtab's median-centered log-normal with no refusal

Open Beginner friendly
#898 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
85/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
backend

Research direction

Start in pybnf/petab/export.py at _resolve_noise and compare its handling with the existing refusal in _reduce_noise_spec. Read the related cases in tests/test_petab_export.py and add coverage for a global noise_location = mean with objective or whole-fit lnnormal. Done means the export refuses this configuration consistently while existing median-centered exports remain unchanged.

Written by the indexing model from the issue text.

Description

bug silent-incorrectness

_resolve_noise (pybnf/petab/export.py:877-893) takes the whole-fit location only from the parsed noise spec. For objective = <token> that spec comes from _OBJECTIVE_DESUGAR (pybnf/objective.py:2808-2820), which always returns None for the location (lnnormal is at line 2813). A whole-fit noise_model = ... line without a location field also carries None. The exporter's only mean-centering refusal is in _reduce_noise_spec (export.py:1022), and it fires only when location == 'mean' appears in the spec itself. Nothing under pybnf/petab/ reads conf['noise_location']. The fitter does read it: Configuration._load_obj_func (pybnf/config.py:2986-2996) calls obj.set_default_location(location) (pybnf/objective.py:1198-1206). On a log scale this subtracts the moment correction, so PyBNF fits a mean-centered likelihood while the exported problem declares PEtab's median-centered log-normal.

The two spellings of the same thing are handled differently. A location = mean field on a whole-fit noise_model line is refused for every family: tests/test_petab_export.py:2274 checks this on a gaussian line, and an lnnormal line is refused the same way. The documented whole-fit equivalent, noise_location = mean (docs/config_keys.rst:435), is dropped without a warning.

Failure scenario

An edition-2 job with objective = lnnormal and noise_location = mean, exported with pybnf.petab.export.export_job. The export completes, and every file it writes is byte-identical to the export of the same job without noise_location. For the same simulation, PyBNF scores the two jobs differently (0.5752 vs 0.1348 below), but a PEtab tool reading either export scores the median version. At fixed sigma, a mean-centered lnnormal shifts the fitted prediction by a factor of exp(sigma^2/2). A PEtab tool fitting the export therefore estimates different parameters from the ones PyBNF estimates, and nothing reports the change of likelihood.

Reproduction

Copy examples/demo/parabola_v2.bngl into an empty directory and add these files.

positive.exp:

# time y y_SD
11 5.0 0.5
12 8.0 0.5
13 12.0 0.5
14 17.0 0.5
15 22.0 0.5

median.conf:

edition = 2
job_type = de
population_size = 8
max_iterations = 5
model: parabola_v2.bngl
experiment: positive, data: positive.exp
objective = lnnormal
uniform_var = v1 0.1 10
uniform_var = v2 0.1 10
uniform_var = v3 0.1 10

mean.conf is median.conf plus one line:

noise_location = mean

repro.py:

import filecmp, os
import numpy as np
from pybnf.parse import load_config
from pybnf.data import Data
from pybnf.petab.export import export_job

# A fixed simulation of y at the data times (x = t - 10, y = 0.5 x^2 + x + 3).
t = np.arange(11.0, 16.0); x = t - 10; f = 0.5 * x**2 + x + 3
sim = Data(); sim.cols = {'time': 0, 'x': 1, 'y': 2}; sim.headers = {0: 'time', 1: 'x', 2: 'y'}
sim.data = np.column_stack([t, x, f])
exp = Data(file_name='positive.exp')
yobs, s = exp.data[:, 1], 0.5

for tag, shift in (('median', 0.0), ('mean', s**2 / 2)):
    conf = load_config(f'{tag}.conf')
    ref = np.sum(0.5 * ((np.log(yobs) - (np.log(f) - shift)) / s) ** 2)
    print(f'[{tag}] PyBNF objective = {conf.obj.evaluate(sim, exp):.6f}   by hand = {ref:.6f}')
    export_job(f'{tag}.conf', f'out_{tag}')

names = sorted(os.listdir('out_median'))
print('exports byte-identical:',
      all(filecmp.cmp(f'out_median/{n}', f'out_mean/{n}', shallow=False) for n in names))
print(open('out_mean/observables.tsv').read())

Run it with BNGPATH pointing at a BioNetGen install (load_config requires one): python repro.py. Output:

[median] PyBNF objective = 0.134830   by hand = 0.134830
[mean] PyBNF objective = 0.575182   by hand = 0.575182
exports byte-identical: True
observableId	observableFormula	noiseFormula	noiseDistribution	noisePlaceholders
func_y	y	noiseParameter1_func_y	log-normal	noiseParameter1_func_y

The fitter builds the mean-centered objective, and it matches the hand formula with mu = ln f - sigma^2/2. The export of the mean job is identical to the export of the median job. The correct behaviour is the refusal the spec-level form already gets: "the whole-fit noise model is mean-centered (location = mean); PEtab v2 takes the prediction as the distribution median ...".

ADR-0031 calls mean "the explicit, native-only opt-in" and says the exporter always emits median. That sentence says what PEtab can represent. It is not a reason to write a mean-centered fit as a median one. The exporter's own refusal of the spec-level form cites ADR-0031, and a median-centered export of a mean-centered fit has a different optimum.

I also checked a whole-fit noise_model = lnnormal, sigma = fix_at 0.5 line with noise_location = mean. The fitter's noise model is mean-centered, and the export again writes func_y y 0.5 log-normal without a refusal. The same line with , location = mean added is refused.

Reachability

Any edition-2 job that sets noise_location = mean and uses a log-scale family the exporter accepts, i.e. lnnormal, by objective = lnnormal or by a whole-fit noise_model = lnnormal, ... line. The linear gaussian and laplace families give the same numbers, because mean equals median there. lognormal (log10) and neg_bin are refused for other reasons before the location matters. Per-observable noise_model <obs> = ... overrides are not affected: set_default_location changes only the class-default noise model, and the exporter already refuses a location = mean field on an override. The earlier export silent-drop fixes (#719, #733, #736, #738) covered the structural config entries. noise_location is a plain global key (accepted at pybnf/parse.py:192), so it was not covered by that work.

The fix is to have _resolve_noise raise the same NotImplementedError when conf.get('noise_location') == 'mean'.

Where

  • pybnf/petab/export.py:877-893 (_resolve_noise: the location comes only from the spec)
  • pybnf/petab/export.py:1022 (the only mean-centering refusal, spec-level)
  • pybnf/objective.py:2808-2820 (_OBJECTIVE_DESUGAR: location always None)
  • pybnf/config.py:2986-2996 (the fitter applies noise_location)
  • pybnf/objective.py:1198-1206 (set_default_location)
  • docs/config_keys.rst:435 (noise_location documented)
  • docs/adr/0031-median-is-the-universal-centering-default-objective-surface-is-edition-gated.md (mean is the native-only opt-in)

Related: #899, #901, #896, #894, #738.

Found in a whole-codebase audit for silently wrong results (2026-09-23); the reproduction above was re-run independently of the original finding.

Dominant language
Python
Stars
25
Forks
25
Avg merge
2h 26m
Merged PRs (30d)
83

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from lanl/PyBNF

All issues in lanl/PyBNF

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.