Skip to content

Add unit tests for the fitting math in fit.py - #70

Merged
jruffio merged 3 commits into
jruffio:mainfrom
mperrin:test_fitting_math
Aug 12, 2026
Merged

Add unit tests for the fitting math in fit.py#70
jruffio merged 3 commits into
jruffio:mainfrom
mperrin:test_fitting_math

Conversation

@mperrin

@mperrin mperrin commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

This pull request was authored mostly by Claude Opus 5 via GitHub Copilot, instructed and supervised by me @mperrin.

Add Test Suite for fit.py Mathematics

Summary

Adds breads/tests/test_fit.py, a suite of 54 tests covering fitfm, log_prob, combined_log_prob, nlog_prob, and the _get_lsq_fit helper. The tests use small analytic mock datasets (a straight line, a Gaussian on a constant background, a quadratic) with known true parameters, and check the numerical results against independently computed reference values. The functions under test are never mocked.

Coverage includes parameter recovery, uncertainty calibration, chi-square behaviour, the analytic log-probability expressions of Ruffio+2019 Eq. 36 and its H0 counterpart, the regularization branch, degenerate and invalid inputs, and the wrapper functions.

The forward models used as fixtures follow the conventions of breads.fm.template.templatefm and the tutorial in docs/source/framework/breads_simple_fit_tutorial.ipynb.

Current status: 52 pass, 2 fail (due to apparent bugs found during development of these tests) (see below). The 5 pre-existing tests in breads/tests/ are unaffected.

breads/tests/  ->  2 failed, 57 passed in 6.7s

Bug found: linear parameter uncertainties are under-inflated

fitfm computes the parameter covariance as

covphi = noise_scaling * iMTM          # breads/fit.py, line 140

where noise_scaling = sqrt(rchi2). The parameter covariance scales with the variance of the noise, so the correct rescaling of the covariance matrix is noise_scaling ** 2 * iMTM, i.e. rchi2 * iMTM. As implemented, scale_noise=True inflates the returned uncertainties by rchi2 ** 0.25 instead of the conventional rchi2 ** 0.5.

The practical effect: whenever rchi2 > 1, the error bars reported by fitfm are too small. In a test case where the quoted noise is 3x smaller than the true scatter, the reported uncertainty is ~74% too small:

Quantity Value
Empirical scatter of fitted amplitude (300 realizations) 0.1875
Mean uncertainty reported by fitfm 0.1078
Ratio 1.74 (≈ sqrt(3))

Two tests assert the conventionally correct behaviour and therefore fail until this is addressed:

  • test_uncertainties_scaled_by_noise_scaling_factor — returns [0.0945, 0.0354], correct
    value is [0.1562, 0.0586]
  • test_uncertainties_are_statistically_calibrated — Monte-Carlo calibration check above

A passing positive control, test_uncertainties_calibrated_when_noise_correctly_specified, exercises the same Monte-Carlo machinery on the unaffected scale_noise=False path, demonstrating that the test harness itself is sound and that the two failures point at the code rather than the tests.

The proposed one-line fix is covphi = noise_scaling ** 2 * iMTM. This PR does not apply that fix, so that the change can be reviewed as a deliberate decision.


Other behaviours documented (in comments on passing tests, not asserted as failures)

  1. _get_lsq_fit divides chi2 twice when N_data is None. In that branch the code
    computes chi2 = nansum(residuals**2) / N_data and then rchi2 = chi2 / N_data. The
    returned chi2 is therefore already a reduced chi squared, and rchi2 (and hence
    noise_scaling) is divided by N_data a second time. The N_data-supplied branch is
    correct; both paths are tested side by side.

    Consequence: fitfm calls _get_lsq_fit with N_data=None at line 106, so the chi2
    entering the log-probability expression is really chi2 / N_data. This does not shift the
    location of the log-probability maximum, so grid searches are unaffected, but it does
    suppress differences in log_prob. For a 111-sigma companion where the true Δchi2 is
    12946 (log-evidence gain ~6500), log_prob - log_prob_H0 reports only 21.2. Any
    absolute significance or Bayes factor derived from log_prob - log_prob_H0 is therefore
    off by roughly a factor of N_data. The rchi2 returned by fitfm is unaffected,
    because it is recomputed independently from residuals[:N_data].

  2. H1 and H0 use inconsistent noise scaling. fitfm never passes noise_scaling to
    _compute_H0, which therefore always uses its default of 1, while the H1 branch uses the
    fitted value. With scale_noise=True the two hypotheses are not on the same footing: on
    pure background data containing no companion at all, this yields
    log_prob - log_prob_H0 = +7.8, spuriously favouring a companion, purely from the
    mismatched -((Nd - Np) / 2) * ln(noise_scaling**2) term.

    test_bayes_factor_large_with_signal_and_small_without is therefore written with
    scale_noise=False, where both branches are consistent. It then matches an analytic
    decomposition (Occam term + Δchi2 / 2 N_data) to 1e-8 and cleanly separates signal
    (+29.1) from background (-3.1).

  3. raise Warning(...) is a hard error, not a soft warning. Warning is an exception
    class, so the two guards at lines 57 and 65 raise rather than warn. Consequences: (a) any
    forward model with a single linear parameter must pass computeH0=False explicitly —
    including the Gaussian example in the breads tutorial, whose cell 26 raises as written;
    (b) the bounds argument is effectively unusable, since any finite bound raises.
    @mperrin notes, it may be the case that we do want this to raise an exception; I don't see how it makes sense to have computeH0=True for a case with only a single linear parameter. Should this be a different more specific exception class instead of Warning, maybe ValueError or RuntimeError? Should we update the tutorial notebook to set computeH0=False in the cell that currently errors?

  4. combined_log_prob silently ignores its own bounds argument. The implementation
    hard-codes bounds=None in the per-dataset log_prob call, so a caller-supplied bounds
    tuple has no effect.

  5. combined_log_prob applies the non-linear prior once per data object. The prior is
    forwarded into each per-dataset log_prob call, so with N data objects it is counted N
    times, i.e. effectively raised to the Nth power.


Docstring correction

The only change to fit.py in this PR: the fitfm docstring described the third return value as s2: noise scaling factor, when it is in fact the reduced chi squared. Corrected to
rchi2: Reduced chi squared of the best fit. Equal to 1 by definition if scale_noise is False.


Test case table

Type key: + positive path (tests something works as intended), negative path (tests something errors as expected for the given inputs), bound boundary condition, math mathematical identity or statistical property.

# Test Type Verifies Status
Parameter recovery
1 test_linear_model_recovers_tutorial_values + Tutorial dataset yields the weighted least-squares line (2.19, −0.35) pass
2 test_noiseless_gaussian_exact_recovery + Noise-free Gaussian+background: amplitude and background exact, rchi2 ≈ 0 pass
3 test_polynomial_three_linear_parameters_exact_recovery + Three-column design matrix solved exactly pass
4 test_linear_parameters_match_normal_equations + linparas == inv(MᵀM) Mᵀd for the noise-normalized system pass
5 test_noisy_gaussian_recovers_truth_within_uncertainty + Noisy data: fitted parameters within 3σ of truth pass
Uncertainties
6 test_uncertainties_unscaled_match_inverse_normal_matrix + scale_noise=False gives sqrt(diag(inv(MᵀM))) pass
7 test_uncertainties_scaled_by_noise_scaling_factor + scale_noise=True should inflate errors by sqrt(rchi2) fail (bug)
8 test_uncertainties_are_statistically_calibrated math 300 Monte-Carlo realizations: reported error should match empirical scatter fail (bug)
9 test_uncertainties_calibrated_when_noise_correctly_specified math Positive control on the bug-free scale_noise=False path pass
10 test_linear_scaling_invariance bound Scaling data and noise by c scales parameters and errors by c pass
11 test_heteroscedastic_noise_is_correctly_weighted + Per-point σ used as inverse weights, not merely an overall scale pass
Chi-square
12 test_rchi2_near_unity_for_correct_noise_model + rchi2 ≈ 1 for a well-specified noise vector pass
13 test_rchi2_scales_as_square_of_noise_underestimate math σ understated by k inflates rchi2 by k² pass
14 test_rchi2_is_unity_when_scale_noise_false bound rchi2 hard-set to 1 in that branch pass
15 test_get_lsq_fit_with_explicit_n_data_returns_correct_chi2 + N_data supplied: correct chi2, rchi2, noise scaling; prior rows excluded pass
16 test_get_lsq_fit_with_none_n_data_divides_chi2_twice bound N_data=None: documents the extra division (behaviour 1 above) pass
Log probability and hypothesis testing
17 test_log_prob_matches_analytic_expression math Reproduces log(Eq. 36) of Ruffio+2019 as coded pass
18 test_log_prob_H0_matches_analytic_expression math H0 branch reproduced by hand from M[:, 1:] pass
19 test_log_prob_peaks_at_true_nonlinear_parameter math Grid over μ: argmax at the true centre, noiseless and noisy pass
20 test_log_prob_peak_width_scales_with_noise_level math Curvature at the peak: doubling the noise doubles the width pass
21 test_bayes_factor_large_with_signal_and_small_without +/− Evidence gain matches its analytic decomposition; separates signal from background pass
22 test_marginalize_noise_scaling_matches_analytic_expression math Student-t style branch matches its analytic form; same linear solution pass
23 test_marginalize_noise_scaling_peaks_at_true_nonlinear_parameter math Marginalized likelihood also peaks at the truth pass
Regularization (four-output forward models)
24 test_four_output_fm_without_regularization_matches_three_output + An extra_outputs dict lacking the key is inert pass
25 test_strong_regularization_pulls_parameter_toward_prior math Tiny s_reg forces the background to its prior value pass
26 test_weak_regularization_recovers_unregularized_solution math Huge s_reg reproduces the unregularized answer pass
27 test_regularization_strength_interpolates_between_limits math Estimate moves monotonically from prior to free solution as s_reg grows pass
28 test_regularization_nan_entries_leave_parameter_unconstrained bound All-NaN s_reg adds no rows; identical to unregularized fit pass
29 test_regularization_does_not_constrain_the_companion_amplitude + A NaN prior on the first column leaves the companion free pass
30 test_regularization_with_scale_noise_false_runs_and_differs + Both noise-scaling paths execute and give finite, distinct results pass
31 test_regularization_with_marginalize_noise_scaling_raises Incompatible combination is rejected pass
Degenerate, invalid and boundary inputs
32 test_wrong_number_of_fm_outputs_raises_value_error A forward model returning 2 values raises ValueError pass
33 test_empty_data_returns_invalid_outputs bound Empty data short-circuits to -inf, -inf, inf, nan[], nan[] pass
34 test_first_column_all_zero_returns_invalid_outputs Zero companion column: "companion cannot be fitted" path pass
35 test_all_zero_columns_are_dropped_and_returned_as_nan bound Dead column reported as NaN; other parameters unchanged pass
36 test_singular_design_matrix_returns_invalid_outputs Duplicate columns: inversion failure trapped, sentinels returned pass
37 test_single_linear_parameter_with_computeH0_raises One-parameter model with computeH0=True raises Warning pass
38 test_single_linear_parameter_with_computeH0_false_works bound Fits successfully; log_prob_H0 is NaN pass
39 test_finite_bounds_raise_warning Any finite bound raises Warning pass
40 test_infinite_bounds_equivalent_to_none bound Explicit infinite bounds identical to bounds=None pass
41 test_bounds_argument_is_not_mutated bound Caller-supplied bounds are copied, not modified in place pass
Wrapper functions
42 test_log_prob_matches_fitfm_first_output + Wrapper returns fitfm(..., computeH0=False)[0] pass
43 test_log_prob_respects_scale_noise_flag + scale_noise forwarded through to fitfm pass
44 test_log_prob_adds_nonlinear_prior + Prior added additively and called with the right arguments pass
45 test_log_prob_prior_can_veto_a_parameter bound A -inf prior drives the total to -inf pass
46 test_log_prob_returns_neg_inf_when_fm_raises Exceptions swallowed and reported as -inf pass
47 test_log_prob_handles_single_linear_parameter_without_raising bound computeH0=False internally, so one-parameter models return finite values pass
48 test_nlog_prob_is_negative_of_log_prob + Sign inversion, with and without a prior pass
49 test_nlog_prob_minimized_at_true_parameter math Minimum over the μ grid at the truth pass
50 test_combined_log_prob_sums_individual_log_probs + Sum over data objects pass
51 test_combined_log_prob_single_dataset_matches_log_prob + One-element list degenerates to log_prob pass
52 test_combined_log_prob_applies_prior_once_per_dataset bound Prior counted N times (behaviour 5 above) pass
53 test_combined_log_prob_peaks_at_true_parameter math Combining two datasets still peaks at the truth pass
54 test_combined_log_prob_ignores_its_bounds_argument bound bounds never forwarded (behaviour 4 above) pass

Running the tests

pytest breads/tests/test_fit.py -v

Note that CI (ci_test_workflow.yml) will report a failure on this branch until the covphi scaling is addressed, since tests 7 and 8 are intended to flag that bug.

Add breads/tests/test_fit.py, a suite of 54 tests covering fitfm, log_prob,
combined_log_prob, nlog_prob and the _get_lsq_fit helper. The tests use small
analytic mock datasets (a line, a Gaussian on a constant background, a
quadratic) with known true parameters, and check the numerical results against
independently computed reference values.

Coverage includes parameter recovery, uncertainty calibration, chi-square
behaviour, the analytic log-probability expressions of Ruffio+2019 Eq. 36 and
its H0 counterpart, the regularization branch, degenerate and invalid inputs,
and the wrapper functions.

Two tests fail deliberately, flagging a bug in the error bars: fitfm computes
covphi = noise_scaling * iMTM, but the parameter covariance scales with the
noise variance, so it should be noise_scaling**2 * iMTM. As implemented,
scale_noise=True inflates uncertainties by rchi2**0.25 instead of rchi2**0.5,
under-reporting them by ~74% in a case where the quoted noise is 3x too small.
The failing tests assert the conventionally correct behaviour:

  - test_uncertainties_scaled_by_noise_scaling_factor
  - test_uncertainties_are_statistically_calibrated

A passing positive control, test_uncertainties_calibrated_when_noise_correctly
_specified, exercises the same Monte-Carlo machinery on the unaffected
scale_noise=False path to show the test harness itself is sound.

Several other behaviours are documented in comments on passing tests rather
than asserted as failures: the double division of chi2 in _get_lsq_fit when
N_data is None (which suppresses log_prob differences by a factor of N_data);
_compute_H0 always using noise_scaling=1 while the H1 branch uses the fitted
value; combined_log_prob ignoring its own bounds argument and applying the
non-linear prior once per data object; and the use of `raise Warning(...)`,
which makes those two guards hard errors rather than soft warnings.

Also correct the fitfm docstring, which described the third return value as
"s2: noise scaling factor" when it is in fact the reduced chi squared.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mperrin

mperrin commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

FYI. I had Claude Fable 5 review this PR developed by Claude Opus 5, following the recommended pattern of "have the higher-capability model review code generated by a model with lower capability". That review correctly noted the two failing tests, and suggested they be marked as @pytest.mark.xfail since they are expected to fail, for now. It also verified some other parts of this PR and did not find any problems. Here are some of its outputs:

Finding: the two intentional failures will keep CI permanently red

test_uncertainties_scaled_by_noise_scaling_factor and test_uncertainties_are_statistically_calibrated are plain failing tests. Since ci_test_workflow.yml runs bare pytest on every push/PR to main, CI stays red until the covphi bug is fixed — masking any new regressions in the meantime.
Suggested fix: mark both with
@pytest.mark.xfail(strict=True, reason="covphi = noise_scaling * iMTM should be noise_scaling**2 * iMTM")
This keeps the bug loudly documented, keeps CI green and meaningful, and — because of strict=True — the suite fails the moment the bug is fixed (XPASS → failure), forcing deliberate removal of the marker.

What was verified as sound

  • Reference math independently re-derived — analytic log-prob, H0, Bayes-factor decomposition, and both get_lsq_fit chi² paths are correct and non-tautological (including the intentional replication of the N_data=None double-division convention).
  • Tolerance margins are comfortable — e.g., peak-width test off by 4e-10 vs 1e-3 tolerance; MC calibration ratio 1.036 vs ±0.15 window; no flakiness risk from RNG or platform drift.
  • No fixture leakage — fitfm doesn't mutate the Instrument; the singular-matrix test is deterministic.
  • Aside from the two intended failures: 52/54 pass.

@jruffio

jruffio commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Looking at the issues:
0/ For the covariance and noise scaling, it turns out I did find this issue over the last 2 months and I had fixed it in jb-dev (but not main, sorry!).
This line

In general, I have refactored fitfm() in jb-dev compared to main to try to improve clarity a bit.

1/ Same thing for N_data division issu in _get_lsq_fit, it is already fixed in jb-dev.

2/ I have removed the H0 mode entirely since it is no longer really used. It was some heritage from early BREADS.

3/ The condition with the raise warning has also been removed in jb-dev at this time.

4/ and 5/ Notifying @ben-sappey for combined_log_prob since I believe you added it. We can think about how to implement this better, but I also wondering about letting the user figure it out TBD

@jruffio

jruffio commented Aug 11, 2026

Copy link
Copy Markdown
Owner

What I can do later is to rebase this branch into jb-dev and see if those tests pass there.

@mperrin

mperrin commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

For now, since these are pretty easy fixes, we could also just manually copy those specific fixes from jb-dev to main. Yes? That would likely be faster.

@jruffio

jruffio commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Thanks for those fixes, did you check that AI is happy with the new updates and the tests run through? happy to merge this if so.

@mperrin

mperrin commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Still getting some additional errors - debugging now

The covphi error-bar scaling and the _get_lsq_fit(N_data=None) double
division that the suite originally flagged were fixed upstream (commit
27b3e95, merged via d07c905). Update the five affected reference
computations so they assert the corrected behavior:

- _get_lsq_fit(N_data=None) now returns the plain sum of squared
  residuals as chi2; test renamed to
  test_get_lsq_fit_with_none_n_data_returns_textbook_chi2.
- log_prob, log_prob_H0, Bayes-factor and marginalize references use the
  full chi2 (no extra /N_data). Bayes-factor thresholds updated to the
  now-unsuppressed likelihood ratio.

All 54 tests pass; the two intentionally-failing covphi tests now pass
naturally against the fixed code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mperrin

mperrin commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Tests and doc strings slightly updated to take into account the upstream fixes of the identified issues in #71 . All tests now pass.

Here's an updated version of the Copilot-generated PR summary:

Add Test Suite for fit.py Mathematics

Summary

Adds breads/tests/test_fit.py, a suite of 54 tests covering fitfm, log_prob, combined_log_prob, nlog_prob, and the _get_lsq_fit helper. The tests use small analytic mock datasets (a straight line, a Gaussian on a constant background, a quadratic) with known true parameters, and check the numerical results against independently computed reference values. The functions under test are never mocked.

Coverage includes parameter recovery, uncertainty calibration, chi-square behaviour, the analytic log-probability expressions of Ruffio+2019 Eq. 36 and its H0 counterpart, the regularization branch, degenerate and invalid inputs, and the wrapper functions.

The forward models used as fixtures follow the conventions of breads.fm.template.templatefm and the tutorial in docs/source/framework/breads_simple_fit_tutorial.ipynb.

Current status: all 54 tests pass. The 5 pre-existing tests in breads/tests/ are
unaffected.

breads/tests/  ->  59 passed

Two bugs found and fixed

While developing this suite, two mathematical bugs in fit.py were identified. Both were already corrected on the unmerged jb-dev branch; those fixes have since been backported to main and merged into this branch (commit 27b3e95, via d07c905). The tests assert the corrected behaviour.

1. Linear-parameter uncertainties were under-inflated

fitfm computed the parameter covariance as

covphi = noise_scaling * iMTM          # buggy

where noise_scaling = sqrt(rchi2). The parameter covariance scales with the variance of the noise, so the correct rescaling is noise_scaling ** 2 * iMTM, i.e. rchi2 * iMTM. As originally implemented, scale_noise=True inflated the returned uncertainties by rchi2 ** 0.25 instead of the conventional rchi2 ** 0.5, so whenever rchi2 > 1 the error bars were too small (e.g. ~74% too small when the quoted noise was 3x smaller than the true scatter). Fixed to covphi = noise_scaling ** 2 * iMTM.

Tests test_uncertainties_scaled_by_noise_scaling_factor and test_uncertainties_are_statistically_calibrated (a 300-realization Monte-Carlo calibration check) assert the conventionally correct behaviour and now pass against the fixed code. A positive control, test_uncertainties_calibrated_when_noise_correctly_specified, exercises the same Monte-Carlo machinery on the scale_noise=False path.

2. _get_lsq_fit divided chi2 twice when N_data is None

In the N_data is None branch the code computed chi2 = nansum(residuals**2) / N_data and then rchi2 = chi2 / N_data, so the returned chi2 was already a reduced chi squared and rchi2 (hence noise_scaling) was divided by N_data a second time. Because fitfm calls _get_lsq_fit with N_data=None, the chi2 entering the log-probability expression was suppressed by a factor of N_data; this did not move the location of the log-probability
maximum (so grid searches were unaffected) but it distorted absolute log_prob values and any Bayes factor derived from log_prob - log_prob_H0. Fixed so chi2 is the plain sum of squared residuals, with rchi2 = chi2 / N_data following.

test_get_lsq_fit_with_none_n_data_returns_textbook_chi2 asserts the corrected behaviour and checks it against the explicitly-supplied-N_data branch. The log-probability, H0, Bayes-factor and marginalization tests reproduce the analytic expressions using the full chi2.


Other behaviours documented (in comments on passing tests, not fixed here)

  1. H1 and H0 use inconsistent noise scaling. fitfm never passes noise_scaling to
    _compute_H0, which therefore always uses its default of 1, while the H1 branch uses the
    fitted value. With scale_noise=True the two hypotheses are not on the same footing, so
    the -((Nd - Np) / 2) * ln(noise_scaling**2) term can spuriously favour a companion even
    on pure background data. test_bayes_factor_large_with_signal_and_small_without is
    therefore written with scale_noise=False, where both branches are consistent; it then
    matches an analytic decomposition (Occam term + Δchi2 / 2) to 1e-8 and cleanly separates a
    strong signal from background.

  2. raise Warning(...) is a hard error, not a soft warning. Warning is an exception
    class, so the guards raise rather than warn. Consequences: (a) any forward model with a
    single linear parameter must pass computeH0=False explicitly — including the Gaussian
    example in the breads tutorial, whose cell 26 raises as written; (b) the bounds argument
    is effectively unusable, since any finite bound raises.

  3. combined_log_prob silently ignores its own bounds argument. The implementation
    hard-codes bounds=None in the per-dataset log_prob call, so a caller-supplied bounds
    tuple has no effect.

  4. combined_log_prob applies the non-linear prior once per data object. The prior is
    forwarded into each per-dataset log_prob call, so with N data objects it is counted N
    times, i.e. effectively raised to the Nth power.


Docstring correction

The only direct change to fit.py in this PR: the fitfm docstring described the third return value as s2: noise scaling factor, when it is in fact the reduced chi squared. Corrected to rchi2: Reduced chi squared of the best fit. Equal to 1 by definition if scale_noise is False.


Test case table

Type key: + positive path, negative path, bound boundary condition,
math mathematical identity or statistical property.

# Test Type Verifies Status
Parameter recovery
1 test_linear_model_recovers_tutorial_values + Tutorial dataset yields the weighted least-squares line (2.19, −0.35) pass
2 test_noiseless_gaussian_exact_recovery + Noise-free Gaussian+background: amplitude and background exact, rchi2 ≈ 0 pass
3 test_polynomial_three_linear_parameters_exact_recovery + Three-column design matrix solved exactly pass
4 test_linear_parameters_match_normal_equations + linparas == inv(MᵀM) Mᵀd for the noise-normalized system pass
5 test_noisy_gaussian_recovers_truth_within_uncertainty + Noisy data: fitted parameters within 3σ of truth pass
Uncertainties
6 test_uncertainties_unscaled_match_inverse_normal_matrix + scale_noise=False gives sqrt(diag(inv(MᵀM))) pass
7 test_uncertainties_scaled_by_noise_scaling_factor + scale_noise=True inflates errors by sqrt(rchi2) (bug #1, now fixed) pass
8 test_uncertainties_are_statistically_calibrated math 300 Monte-Carlo realizations: reported error matches empirical scatter (bug #1) pass
9 test_uncertainties_calibrated_when_noise_correctly_specified math Positive control on the scale_noise=False path pass
10 test_linear_scaling_invariance bound Scaling data and noise by c scales parameters and errors by c pass
11 test_heteroscedastic_noise_is_correctly_weighted + Per-point σ used as inverse weights, not merely an overall scale pass
Chi-square
12 test_rchi2_near_unity_for_correct_noise_model + rchi2 ≈ 1 for a well-specified noise vector pass
13 test_rchi2_scales_as_square_of_noise_underestimate math σ understated by k inflates rchi2 by k² pass
14 test_rchi2_is_unity_when_scale_noise_false bound rchi2 hard-set to 1 in that branch pass
15 test_get_lsq_fit_with_explicit_n_data_returns_correct_chi2 + N_data supplied: correct chi2, rchi2, noise scaling; prior rows excluded pass
16 test_get_lsq_fit_with_none_n_data_returns_textbook_chi2 bound N_data=None: chi2 is the plain sum of squares (bug #2, now fixed) pass
Log probability and hypothesis testing
17 test_log_prob_matches_analytic_expression math Reproduces log(Eq. 36) of Ruffio+2019 pass
18 test_log_prob_H0_matches_analytic_expression math H0 branch reproduced by hand from M[:, 1:] pass
19 test_log_prob_peaks_at_true_nonlinear_parameter math Grid over μ: argmax at the true centre, noiseless and noisy pass
20 test_log_prob_peak_width_scales_with_noise_level math Curvature at the peak: doubling the noise doubles the width pass
21 test_bayes_factor_large_with_signal_and_small_without +/− Evidence gain matches its analytic decomposition; separates signal from background pass
22 test_marginalize_noise_scaling_matches_analytic_expression math Student-t style branch matches its analytic form; same linear solution pass
23 test_marginalize_noise_scaling_peaks_at_true_nonlinear_parameter math Marginalized likelihood also peaks at the truth pass
Regularization (four-output forward models)
24 test_four_output_fm_without_regularization_matches_three_output + An extra_outputs dict lacking the key is inert pass
25 test_strong_regularization_pulls_parameter_toward_prior math Tiny s_reg forces the background to its prior value pass
26 test_weak_regularization_recovers_unregularized_solution math Huge s_reg reproduces the unregularized answer pass
27 test_regularization_strength_interpolates_between_limits math Estimate moves monotonically from prior to free solution as s_reg grows pass
28 test_regularization_nan_entries_leave_parameter_unconstrained bound All-NaN s_reg adds no rows; identical to unregularized fit pass
29 test_regularization_does_not_constrain_the_companion_amplitude + A NaN prior on the first column leaves the companion free pass
30 test_regularization_with_scale_noise_false_runs_and_differs + Both noise-scaling paths execute and give finite, distinct results pass
31 test_regularization_with_marginalize_noise_scaling_raises Incompatible combination is rejected pass
Degenerate, invalid and boundary inputs
32 test_wrong_number_of_fm_outputs_raises_value_error A forward model returning 2 values raises ValueError pass
33 test_empty_data_returns_invalid_outputs bound Empty data short-circuits to -inf, -inf, inf, nan[], nan[] pass
34 test_first_column_all_zero_returns_invalid_outputs Zero companion column: "companion cannot be fitted" path pass
35 test_all_zero_columns_are_dropped_and_returned_as_nan bound Dead column reported as NaN; other parameters unchanged pass
36 test_singular_design_matrix_returns_invalid_outputs Duplicate columns: inversion failure trapped, sentinels returned pass
37 test_single_linear_parameter_with_computeH0_raises One-parameter model with computeH0=True raises Warning pass
38 test_single_linear_parameter_with_computeH0_false_works bound Fits successfully; log_prob_H0 is NaN pass
39 test_finite_bounds_raise_warning Any finite bound raises Warning pass
40 test_infinite_bounds_equivalent_to_none bound Explicit infinite bounds identical to bounds=None pass
41 test_bounds_argument_is_not_mutated bound Caller-supplied bounds are copied, not modified in place pass
Wrapper functions
42 test_log_prob_matches_fitfm_first_output + Wrapper returns fitfm(..., computeH0=False)[0] pass
43 test_log_prob_respects_scale_noise_flag + scale_noise forwarded through to fitfm pass
44 test_log_prob_adds_nonlinear_prior + Prior added additively and called with the right arguments pass
45 test_log_prob_prior_can_veto_a_parameter bound A -inf prior drives the total to -inf pass
46 test_log_prob_returns_neg_inf_when_fm_raises Exceptions swallowed and reported as -inf pass
47 test_log_prob_handles_single_linear_parameter_without_raising bound computeH0=False internally, so one-parameter models return finite values pass
48 test_nlog_prob_is_negative_of_log_prob + Sign inversion, with and without a prior pass
49 test_nlog_prob_minimized_at_true_parameter math Minimum over the μ grid at the truth pass
50 test_combined_log_prob_sums_individual_log_probs + Sum over data objects pass
51 test_combined_log_prob_single_dataset_matches_log_prob + One-element list degenerates to log_prob pass
52 test_combined_log_prob_applies_prior_once_per_dataset bound Prior counted N times (behaviour 4 above) pass
53 test_combined_log_prob_peaks_at_true_parameter math Combining two datasets still peaks at the truth pass
54 test_combined_log_prob_ignores_its_bounds_argument bound bounds never forwarded (behaviour 3 above) pass

Running the tests

pytest breads/tests/test_fit.py -v

All 54 tests pass on the current branch (both bugs are fixed), so CI
(ci_test_workflow.yml) is green.

@jruffio
jruffio merged commit 48546bb into jruffio:main Aug 12, 2026
3 checks passed
@jruffio

jruffio commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Thanks Marshall!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants