Skip to content

fix: make optimizer post-processing robust to correlations without uncertainties#2708

Open
henryiii wants to merge 1 commit into
mainfrom
fix/optimize-postprocess
Open

fix: make optimizer post-processing robust to correlations without uncertainties#2708
henryiii wants to merge 1 commit into
mainfrom
fix/optimize-postprocess

Conversation

@henryiii

@henryiii henryiii commented Jun 12, 2026

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Part of the code review in #2706.

Summary

Two correctness fixes in src/pyhf/optimize/mixins.py:

  • _internal_postprocess: num_fixed_pars was only defined inside the if uncertainties is not None: branch but used unconditionally in the if correlations is not None: branch. A fit result carrying corr without unc therefore raised an UnboundLocalError. The assignment is now hoisted to right after fitted_pars is computed so both branches can use it.
  • minimize: par_names = pdf.config.par_names was mutated in place (par_names[index] = None). pyhf's own property returns a fresh list, but duck-typed configs returning a stored list would be corrupted. A defensive copy (list(...)) is now made when par_names is not None.

Test plan

  • Added test_postprocess_correlations_without_uncertainties in tests/test_optim.py, which exercises _internal_postprocess with an OptimizeResult that has corr but no unc. It fails with UnboundLocalError on main and passes with this change.
  • pytest tests/test_optim.py -q: 73 passed, 5 skipped.
  • prek -a (ruff lint/format): clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed parameter name handling in optimization to prevent unintended side effects from downstream processing.
    • Improved robustness of optimization result postprocessing.
  • Tests

    • Added regression test for edge case with correlation data but no uncertainties.

…certainties

Hoist `num_fixed_pars` in `_internal_postprocess` so the correlations branch
no longer raises an UnboundLocalError when a fit result carries `corr` without
`unc`. Also make a defensive copy of `par_names` in `minimize` so duck-typed
configs returning a stored list are not corrupted by the in-place mutation.

Assisted-by: ClaudeCode:claude-opus-4.8
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes two defensive issues in OptimizerMixin. The _internal_postprocess method now computes fixed parameter counts earlier after stitching fitted parameters, and a regression test ensures correlations are preserved when uncertainties are absent. The minimize method defensively copies the provided par_names to prevent downstream mutations from affecting stored configuration values.

Changes

Optimizer defensive fixes and postprocessing repair

Layer / File(s) Summary
_internal_postprocess fixed parameter computation and regression test
src/pyhf/optimize/mixins.py, tests/test_optim.py
num_fixed_pars computation moves earlier in _internal_postprocess after stitching, and a regression test validates that postprocessing preserves correlations when uncertainties are absent.
Defensive par_names copying in minimize
src/pyhf/optimize/mixins.py
minimize now copies par_names to a mutable list when provided, preventing later in-place mutations from affecting the original duck-typed or stored configuration object.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and accurately describes the primary purpose: fixing optimizer post-processing to handle correlations without uncertainties, which aligns with both the code changes (fixing UnboundLocalError in _internal_postprocess) and the new test case.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@henryiii henryiii marked this pull request as ready for review June 12, 2026 17:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_optim.py`:
- Around line 604-623: Update
test_postprocess_correlations_without_uncertainties to also cover num_fixed_pars
> 0 by replacing the passthrough stitch_pars with one that actually stitches in
fixed parameters (use _make_stitch_pars and _TensorViewer or manually create a
stitch_pars that inserts fixed values) so that
OptimizerMixin._internal_postprocess is exercised when correlation stitching
must add rows/columns; call optimizer._internal_postprocess with the new
stitch_pars and add assertions that result.corr has the expected larger shape
(and that the fixed-parameter rows/columns are zeros or the expected values) to
validate the stitching logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3200d03d-d191-4165-8e18-4e7a73660f05

📥 Commits

Reviewing files that changed from the base of the PR and between 0e54a51 and e64ef49.

📒 Files selected for processing (2)
  • src/pyhf/optimize/mixins.py
  • tests/test_optim.py

Comment thread tests/test_optim.py
Comment on lines +604 to +623
def test_postprocess_correlations_without_uncertainties():
# Regression test: _internal_postprocess must not raise when a fit result
# carries correlations but no uncertainties (num_fixed_pars was previously
# only defined inside the uncertainties branch).
pyhf.set_backend("numpy")

fitresult = OptimizeResult(
x=np.asarray([1.0, 2.0]),
fun=0.0,
corr=np.asarray([[1.0, 0.5], [0.5, 1.0]]),
)

def stitch_pars(pars, stitch_with=None): # noqa: ARG001
return pars

optimizer = OptimizerMixin()
result = optimizer._internal_postprocess(fitresult, stitch_pars)

assert result.unc is None
assert np.allclose(pyhf.tensorlib.tolist(result.corr), [[1.0, 0.5], [0.5, 1.0]])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider testing with num_fixed_pars > 0 for more thorough coverage.

The regression test correctly validates that _internal_postprocess no longer raises UnboundLocalError when correlations exist without uncertainties. However, the passthrough stitch_pars means num_fixed_pars = 0, so the correlation stitching logic (lines 105-115 in mixins.py) runs but doesn't add rows/columns.

A more comprehensive test would use a stitch_pars that actually adds fixed parameters, ensuring the correlation stitching logic works correctly with the hoisted num_fixed_pars computation.

📝 Example enhancement
def test_postprocess_correlations_without_uncertainties():
    # ... existing setup ...
    
    # Test with fixed parameters
    from pyhf.optimize.common import _make_stitch_pars
    from pyhf.tensor.common import _TensorViewer
    
    fixed_idx = [0]
    variable_idx = [1, 2]
    tv = _TensorViewer([fixed_idx, variable_idx])
    stitch_pars = _make_stitch_pars(tv, [1.5])
    
    fitresult = OptimizeResult(
        x=np.asarray([2.0, 3.0]),  # variable params only
        fun=0.0,
        corr=np.asarray([[1.0, 0.5], [0.5, 1.0]]),  # 2x2 for variable params
    )
    
    result = optimizer._internal_postprocess(fitresult, stitch_pars)
    
    # Should stitch in zeros for fixed param: result shape (3, 3)
    assert result.corr.shape == (3, 3)
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 616-616: Missing return type annotation for private function stitch_pars

(ANN202)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_optim.py` around lines 604 - 623, Update
test_postprocess_correlations_without_uncertainties to also cover num_fixed_pars
> 0 by replacing the passthrough stitch_pars with one that actually stitches in
fixed parameters (use _make_stitch_pars and _TensorViewer or manually create a
stitch_pars that inserts fixed values) so that
OptimizerMixin._internal_postprocess is exercised when correlation stitching
must add rows/columns; call optimizer._internal_postprocess with the new
stitch_pars and add assertions that result.corr has the expected larger shape
(and that the fixed-parameter rows/columns are zeros or the expected values) to
validate the stitching logic.

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.

1 participant