Skip to content

fix: dimension-aware effect-total consistency check#745

Merged
FBumann merged 2 commits into
mainfrom
worktree-fix-effects-validation-transpose
Jul 23, 2026
Merged

fix: dimension-aware effect-total consistency check#745
FBumann merged 2 commits into
mainfrom
worktree-fix-effects-validation-transpose

Conversation

@FBumann

@FBumann FBumann commented Jul 23, 2026

Copy link
Copy Markdown
Member

Problem

The post-solve consistency check in _create_effects_dataset compares ds[effect].sum(...) against solution[label] using np.allclose on the raw .values:

if not np.allclose(computed.fillna(0).values, found.fillna(0).values, equal_nan=True):
    logger.critical(...)   # meant to be a soft warning

When the two arrays carry the same dims in a different order — e.g. a clustered system expanded back yields (scenario, period) while the computed total is (period, scenario) — numpy can't broadcast (3,2) against (2,3) and raises ValueError before the comparison produces a bool. A soft-warning path becomes a hard crash.

Fix

Compare dimension-aware: warn on a genuine dimension-set mismatch, otherwise found.transpose(*computed.dims) before the np.allclose. Applied to both copies of the check (StatisticsAccessor._create_effects_dataset and Results._create_effects_dataset).

Swept the codebase for the same raw-.values pattern: modeling._xr_allclose (uses xr.broadcast) and io.py constant-detection are already alignment-safe; remaining np.array_equal/np.isclose uses are on 1-D coords or scalars.

Tests

Adds tests/test_effects_dataset_validation.py (3 params: temporal/periodic/total). Verified the tests reproduce the exact ValueError: operands could not be broadcast together with shapes (3,2) (2,3) without the fix and pass with it.

Release note

Also being shipped to the 7.2.x line as 7.2.3. This PR forward-ports the fix to main.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation of effect totals when dataset dimensions differ in order.
    • Added clearer error reporting for mismatched dimensions.
    • Prevented valid results from failing validation due to transposed dimensions.
  • Tests
    • Added coverage for effect datasets across temporal, periodic, and total modes.

The post-solve validation in _create_effects_dataset compared
ds[effect].sum(...) against solution[label] via np.allclose on the raw
.values. When the two arrays carry the same dims in a different order --
e.g. a clustered system expanded back yields (scenario, period) while the
computed total is (period, scenario) -- numpy cannot broadcast (3,2) against
(2,3) and raises ValueError, turning a soft warning into a hard crash.

Align the two label-aware before comparing: warn on a genuine dimension-set
mismatch, otherwise transpose solution[label] to the computed dim order.
Applies to both copies of the check (StatisticsAccessor and Results).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@FBumann, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 59a2fe83-6baf-4b96-a577-bdf38a551277

📥 Commits

Reviewing files that changed from the base of the PR and between aeebdb3 and 11ae5dc.

📒 Files selected for processing (1)
  • tests/test_clustering/test_clustered_roundtrip.py
📝 Walkthrough

Walkthrough

Effect-total validation now checks dimension sets and aligns dimension order before comparing computed and stored values. New parametrized regression tests cover transposed solution dimensions across temporal, periodic, and total dataset modes.

Changes

Effects dataset validation

Layer / File(s) Summary
Align effect-total validation
flixopt/results.py, flixopt/statistics_accessor.py
Validation detects dimension mismatches, aligns arrays by dimension order, and compares values with NaN handling.
Test transposed solution dimensions
tests/test_effects_dataset_validation.py
A solved multi-period, multi-scenario system tests transposed costs dimensions across temporal, periodic, and total modes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • flixOpt/flixopt#430: Earlier dimension-aware handling in effects dataset creation.
  • flixOpt/flixopt#506: Earlier solution storage and plotting/statistics accessor work used by this validation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main fix in the PR.
Description check ✅ Passed It covers the problem, fix, tests, and release note, but it does not follow the repository's required template headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-effects-validation-transpose

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_effects_dataset_validation.py (1)

36-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the legacy Results validator too.

This regression test exercises only fs.stats._create_effects_dataset, while flixopt/results.py contains a separately changed implementation. Add an equivalent case for Results._create_effects_dataset so that path cannot regress unnoticed.

🤖 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_effects_dataset_validation.py` around lines 36 - 53, Extend
test_effects_dataset_tolerates_transposed_solution_dims to also construct the
legacy Results path and invoke Results._create_effects_dataset with the
transposed solution dimensions. Assert that the resulting dataset contains
costs, matching the existing fs.stats validation coverage while reusing the same
mode and reordered-dimension scenario.
🤖 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 `@flixopt/statistics_accessor.py`:
- Around line 938-947: In the comparison paths around the statistics accessor
dimension check and the corresponding results comparison, align the found array
to computed.dims, compare computed.sizes with the aligned array’s sizes, and log
a size mismatch before calling np.allclose. Apply this in both
flixopt/statistics_accessor.py:938-947 and flixopt/results.py:969-978,
preserving the existing dimension-mismatch and value-comparison behavior while
preventing incompatible shapes from reaching np.allclose.

---

Nitpick comments:
In `@tests/test_effects_dataset_validation.py`:
- Around line 36-53: Extend
test_effects_dataset_tolerates_transposed_solution_dims to also construct the
legacy Results path and invoke Results._create_effects_dataset with the
transposed solution dimensions. Assert that the resulting dataset contains
costs, matching the existing fs.stats validation coverage while reusing the same
mode and reordered-dimension scenario.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fd62b10-3696-45ed-a216-695d4ff1901c

📥 Commits

Reviewing files that changed from the base of the PR and between 6962e5e and aeebdb3.

📒 Files selected for processing (3)
  • flixopt/results.py
  • flixopt/statistics_accessor.py
  • tests/test_effects_dataset_validation.py

Comment on lines +938 to +947
if set(computed.dims) != set(found.dims):
logger.critical(
f'Results for {effect}({mode}) in effects_dataset doesnt match {label}: '
f'dimension mismatch {computed.dims=} vs {found.dims=}'
)
elif not np.allclose(
computed.fillna(0).values,
found.transpose(*computed.dims).fillna(0).values,
equal_nan=True,
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '^(flixopt/statistics_accessor.py|flixopt/results.py)$' || true

echo "== stats_accessor relevant lines =="
if [ -f flixopt/statistics_accessor.py ]; then
  sed -n '900,970p' flixopt/statistics_accessor.py | cat -n | sed 's/^/stats_accessor: /'
fi

echo "== results relevant lines =="
if [ -f flixopt/results.py ]; then
  sed -n '930,990p' flixopt/results.py | cat -n | sed 's/^/results: /'
fi

echo "== search comparison helper occurrences =="
rg -n "computed\.dims|found\.transpose|np\.allclose|fillna\(0\)\.values|sizes" flixopt/statistics_accessor.py flixopt/results.py || true

echo "== probe xarray dimension-name equality vs size broadcasting with numpy allclose =="
python3 - <<'PY'
import numpy as np
try:
    import xarray as xr
    a = xr.DataArray(np.arange(3*4), dims=['x','y'], coords={'x':[1,2,3],'y':[1,2,3,4]}).fillna(np.nan)
    b = xr.DataArray(np.arange(3*5), dims=['x','y'], coords={'x':[1,2,3],'y':[1,2,3,4,5]}).fillna(np.nan)
    print("xarray available", xr.__version__)
    print("dims equal sets:", set(a.dims) == set(b.dims))
    try:
        np.allclose(a.fillna(0).values, b.transpose(*a.dims).fillna(0).values, equal_nan=True)
        print("allclose returned")
    except Exception as e:
        print("allclose exception:", type(e).__name__, str(e))
except Exception as e:
    print("xarray probe unavailable:", type(e).__name__, str(e))
PY

Repository: flixOpt/flixopt

Length of output: 13925


🌐 Web query:

NumPy allclose ValueError arrays not same shape documentation

💡 Result:

The function numpy.allclose does not raise a ValueError when provided with arrays of different shapes [1][2]. Instead, it uses standard NumPy broadcasting rules to perform the comparison [1][3]. Because of this, numpy.allclose can return True for arrays with different shapes if they are broadcast-compatible and their elements satisfy the tolerance criteria [1][4]. If your goal is to require that both the shapes and the values match, you should use numpy.testing.assert_allclose instead [5][3]. Unlike numpy.allclose, numpy.testing.assert_allclose explicitly checks that the shapes of the two inputs are the same and raises an AssertionError if they do not match [5][3].

Citations:


🌐 Web query:

NumPy ufunc broadcasting rules incompatible shapes ValueError shape

💡 Result:

The ValueError: operands could not be broadcast together with shapes is raised when NumPy attempts to perform element-wise operations (via universal functions or ufuncs) on arrays that do not satisfy the required broadcasting compatibility rules [1][2][3]. Broadcasting Rules To determine if two arrays are compatible, NumPy compares their shapes element-wise, starting from the trailing (rightmost) dimension and moving backward toward the leftmost dimension [1][4][5]. Two dimensions are compatible if: 1. They are equal, or 2. One of them is 1 [1][6][5]. If the arrays have a different number of dimensions, the shape of the array with fewer dimensions is prepended with 1s until the number of dimensions matches [6][5]. If at any point the trailing dimensions are unequal and neither is 1, NumPy raises the ValueError [1][5]. Common Causes and How to Resolve This error typically occurs when the dimensions do not align for the intended element-wise operation [7][8]. To fix it, you must align the shapes by: 1. Adding dimensions: Use np.newaxis or None to convert a 1D array of shape (N,) into a column vector of shape (N, 1), which can then broadcast across a 2D array [8][5]. 2. Reshaping: Explicitly use.reshape to match the required dimensions [8][5]. 3. Reordering: If dimensions are swapped (e.g., trying to broadcast a (N, 1) array where a (1, N) is expected), use.transpose or.moveaxis to reorder them [8]. 4. Verifying Operation: If you intended to perform matrix multiplication instead of element-wise broadcasting, use the @ operator or np.matmul instead of standard arithmetic operators [7][8]. Top Results: [1], [8], [5]

Citations:


🌐 Web query:

NumPy allclose implementation calls subtract absolute less than

💡 Result:

The NumPy implementation of allclose uses the following element-wise comparison to determine if two arrays are close [1][2]: absolute(a - b) <= (atol + rtol * absolute(b)) This implementation computes the absolute difference between input arrays a and b and compares it against the sum of the absolute tolerance (atol) and the scaled relative tolerance (rtol * absolute(b)) [1]. Because this equation uses the absolute value of b, it is asymmetric, meaning allclose(a, b) can yield a different result than allclose(b, a) in some cases [1][2]. This logic aligns with the core requirement of checking if the difference between two values is less than or equal to a combined absolute and relative threshold [1][3]. The function is designed to handle floating-point comparisons where absolute tolerance provides a floor for small values, while relative tolerance scales with the magnitude of the reference value b [1][4].

Citations:


Guard incompatible comparison sizes instead of relying on np.allclose.

These checks only ensure dimension names match; if computed and solution[label] have the same dim set but incompatible sizes, np.allclose(..., equal_nan=True) will raise for incompatible shapes and for nan=True implementations before logging. Align found, compare computed.sizes with the aligned array, and log size mismatches before the value comparison in both affected paths.

📍 Affects 2 files
  • flixopt/statistics_accessor.py#L938-L947 (this comment)
  • flixopt/results.py#L969-L978
🤖 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 `@flixopt/statistics_accessor.py` around lines 938 - 947, In the comparison
paths around the statistics accessor dimension check and the corresponding
results comparison, align the found array to computed.dims, compare
computed.sizes with the aligned array’s sizes, and log a size mismatch before
calling np.allclose. Apply this in both flixopt/statistics_accessor.py:938-947
and flixopt/results.py:969-978, preserving the existing dimension-mismatch and
value-comparison behavior while preventing incompatible shapes from reaching
np.allclose.

Curated from property-based fuzzing that ran save -> load -> expand over random
combinations of periods (0-3), scenarios (0-3), storage (all initial-charge modes),
converters and optimize on/off, same-version and cross-version (7.0.0/7.1.0 -> 7.2.3).
No round-trip failures were found; these 24 cases pin the representative dimension
layouts so the path stays intact, and assert the effect-total validation does not
crash on any layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@FBumann
FBumann merged commit 2b2720c into main Jul 23, 2026
11 checks passed
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