Skip to content

feat(stats): add StatsAccessor.summary() headline KPI table#188

Open
Shaan-alpha wants to merge 3 commits into
FBumann:mainfrom
Shaan-alpha:feat/stats-summary
Open

feat(stats): add StatsAccessor.summary() headline KPI table#188
Shaan-alpha wants to merge 3 commits into
FBumann:mainfrom
Shaan-alpha:feat/stats-summary

Conversation

@Shaan-alpha
Copy link
Copy Markdown

@Shaan-alpha Shaan-alpha commented May 31, 2026

Fixes #186. Adds summary() to StatsAccessor which returns an xr.Dataset containing the objective value, effect_totals, and per-flow full_load_hours.

Summary by CodeRabbit

  • New Features
    • Summary statistics now available from optimization results, providing key performance indicators including objective value, effect totals, and full load hours for analysis and reporting.

@github-actions github-actions Bot added the type:feat New feature or request label May 31, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

Warning

Review limit reached

@Shaan-alpha, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 57 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1477dec-1905-448a-af33-cdda793985a7

📥 Commits

Reviewing files that changed from the base of the PR and between 7b7b63d and 169e748.

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

Walkthrough

This PR adds a new summary() method to StatsAccessor that returns a compact xarray.Dataset of headline KPIs: objective value, per-effect totals, and per-flow full-load hours. A test validates the implementation against a minimal optimization run.

Changes

Stats summary KPI table

Layer / File(s) Summary
Summary method implementation
src/fluxopt/stats.py
StatsAccessor.summary() assembles a dataset with objective from the result, conditionally adds effect_totals when present, and computes full_load_hours by dividing total flow hours by combined per-flow sizes (filled from result if missing), then masks non-finite values before returning.
Summary method test
tests/test_stats_summary.py
Test runs a small optimization and validates that stats.summary() returns a dataset containing objective, effect_totals with a cost key, and full_load_hours with grid(elec) and non-negative finite values.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A summary so neat,
Headlines condensed and sweet,
Objective, effects, FLH,
All in one dataset's harmony—
Hop along, let KPIs meet!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is minimal but addresses the core objective; however, it lacks required template sections like Type of Change, Testing checkbox, and Checklist items. Complete the PR description template by adding Type of Change, Testing verification section, and Checklist confirmation items for clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a summary() method to StatsAccessor that returns headline KPIs.
Linked Issues check ✅ Passed The PR fully implements issue #186 requirements: summary() returns an xr.Dataset with objective, effect_totals, and per-flow full_load_hours, composing existing stats without new math.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing StatsAccessor.summary() and its corresponding end-to-end test; no unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test_stats_quick.py (1)

1-13: ⚡ Quick win

Make this a real test that asserts the acceptance criterion.

This file is named test_* but contains only module-level code with print() and no assertions, so pytest collects/executes the build+solve at import time without verifying anything. The linked issue's acceptance criterion requires confirming the summary covers objective, effect totals, and full-load hours — assert those explicitly inside a test function.

💚 Proposed test skeleton
import numpy as np
import xarray as xr

from fluxopt import FlowSystem, Flow, Effect


def test_stats_summary_quickstart():
    time = xr.DataArray(np.arange(2), dims=['time'], coords={'time': np.arange(2)})
    sys = FlowSystem(time=time, dt=1.0)
    sys.build(flows=[Flow(id='f1', size=10)], effects=[Effect(id='cost')])
    res = sys.solve()

    summary = res.stats.summary()

    assert 'objective' in summary
    assert 'effect_totals' in summary
    assert 'full_load_hours' in summary
    assert 'f1' in summary['full_load_hours'].coords['flow'].values
🤖 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 `@test_stats_quick.py` around lines 1 - 13, Convert the module-level script
into a pytest test function (e.g., test_stats_summary_quickstart) that builds
FlowSystem, calls solve(), and asserts the acceptance criteria rather than
printing; specifically use FlowSystem, Flow(id='f1'), Effect(id='cost'), call
res = sys.solve() and summary = res.stats.summary(), then assert that
'objective', 'effect_totals', and 'full_load_hours' are present in summary and
that the flow id 'f1' appears in
summary['full_load_hours'].coords['flow'].values.
🤖 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.

Nitpick comments:
In `@test_stats_quick.py`:
- Around line 1-13: Convert the module-level script into a pytest test function
(e.g., test_stats_summary_quickstart) that builds FlowSystem, calls solve(), and
asserts the acceptance criteria rather than printing; specifically use
FlowSystem, Flow(id='f1'), Effect(id='cost'), call res = sys.solve() and summary
= res.stats.summary(), then assert that 'objective', 'effect_totals', and
'full_load_hours' are present in summary and that the flow id 'f1' appears in
summary['full_load_hours'].coords['flow'].values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ff618c7-4f40-4321-868f-5cf2d50170df

📥 Commits

Reviewing files that changed from the base of the PR and between 0a3966a and 470ea6a.

📒 Files selected for processing (2)
  • src/fluxopt/stats.py
  • test_stats_quick.py

@Shaan-alpha Shaan-alpha force-pushed the feat/stats-summary branch from 470ea6a to 9723917 Compare May 31, 2026 19:40
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

14-17: ⚡ Quick win

Strengthen assertions to validate KPI content, not just variable presence.

These checks can pass even if variables are empty/misaligned. Please assert minimal semantics too (e.g., objective is scalar/non-null, effect_totals contains cost, and full_load_hours for f1 is finite/non-negative) so this test actually guards the acceptance criteria.

Suggested assertion upgrade
     assert 'objective' in summary
     assert 'effect_totals' in summary
     assert 'full_load_hours' in summary
     assert 'f1' in summary['full_load_hours'].coords['flow'].values
+    assert np.isfinite(summary['objective'].item())
+    assert 'cost' in summary['effect_totals'].coords['effect'].values
+    assert np.isfinite(summary['full_load_hours'].sel(flow='f1').item())
+    assert summary['full_load_hours'].sel(flow='f1').item() >= 0
🤖 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_stats.py` around lines 14 - 17, The current assertions only check
key presence; strengthen them by validating content semantics: assert that
summary['objective'] is non-null and scalar/finite, that
summary['effect_totals'] contains a 'cost' entry which is finite and
non-negative, and that summary['full_load_hours'] for flow 'f1' (accessed via
summary['full_load_hours'].coords['flow'].values and the corresponding data
array) contains at least one value that is finite and >= 0; update the test in
tests/test_stats.py to add these non-emptiness/finite/non-negative checks around
the existing keys ('objective', 'effect_totals', 'full_load_hours') and the 'f1'
flow to ensure the KPIs are meaningful.
🤖 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.

Nitpick comments:
In `@tests/test_stats.py`:
- Around line 14-17: The current assertions only check key presence; strengthen
them by validating content semantics: assert that summary['objective'] is
non-null and scalar/finite, that summary['effect_totals'] contains a 'cost'
entry which is finite and non-negative, and that summary['full_load_hours'] for
flow 'f1' (accessed via summary['full_load_hours'].coords['flow'].values and the
corresponding data array) contains at least one value that is finite and >= 0;
update the test in tests/test_stats.py to add these
non-emptiness/finite/non-negative checks around the existing keys ('objective',
'effect_totals', 'full_load_hours') and the 'f1' flow to ensure the KPIs are
meaningful.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60277c2b-86f3-4916-82c0-a26777057e31

📥 Commits

Reviewing files that changed from the base of the PR and between 470ea6a and 9723917.

📒 Files selected for processing (2)
  • src/fluxopt/stats.py
  • tests/test_stats.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/fluxopt/stats.py

@Shaan-alpha
Copy link
Copy Markdown
Author

@coderabbitai resolve Feedback addressed! I've converted the module-level script into a proper pytest test function with assertions as suggested.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

@Shaan-alpha Great work converting the module-level script into a proper test_stats_summary_quickstart() pytest function with assertions! The test structure now follows best practices and the assertions ensure the summary output is validated correctly.

(。◕‿◕。)🐇

✅ Actions performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

summary() raised `TypeError: len() of unsized object` whenever the result
had no invested sizes, because Result.sizes is an empty 0-d DataArray in
that (very common) case. Guard on the `flow` dim instead of len(), so
fixed-size systems work too.

Replace the placeholder test with a real end-to-end one: the previous test
called a non-existent FlowSystem(time=..., dt=...) / build(flows=, effects=)
API and was never executed (the CI has no pytest job), so it hid the bug
above. The new test builds a minimal system via optimize() and asserts the
KPIs carry content, not just keys. Renamed to test_stats_summary.py to avoid
a basename clash with tests/math/test_stats.py that broke full-suite
collection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shaan-alpha
Copy link
Copy Markdown
Author

Shaan-alpha commented Jun 1, 2026

Pushed 7b7b63d, which goes a bit further than the assertion nitpick.

While strengthening the test I found summary() actually crashes on the common case (no investment sizing): Result.sizes is an empty 0-d DataArray then, and len(self._result.sizes) raises TypeError: len() of unsized object. The original test didn't catch it because it called a non-existent FlowSystem(time=..., dt=...) / build(flows=, effects=) API and was never executed — there's no pytest job in CI, so it stayed green.

Changes in this commit:

  • fix: guard on the flow dim instead of len() in summary(), so fixed-size systems work.
  • test: rewrote it as a real end-to-end test via optimize() that asserts the KPIs carry content (objective finite, cost in effect_totals, full-load hours finite and non-negative), per the earlier suggestion. This also exercises the no-sizing path, so it doubles as the regression test for the fix above.
  • collision: renamed to tests/test_stats_summary.py — the old tests/test_stats.py shared a basename with tests/math/test_stats.py, which broke pytest collection for the whole suite.

Verified locally: full suite is 470 passed, 227 skipped, ruff check clean.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

29-29: ⚡ Quick win

effect_totals assertion only checks the key, not the value.

The comment above says the KPIs should "carry meaningful content, not just keys," but unlike objective (Line 28) and full_load_hours (Line 34), this assertion only verifies the cost coordinate label exists. A regression returning NaN/missing for the cost total would still pass. Add a finiteness check on the selected value to match intent.

💚 Proposed assertion to verify content
     assert 'cost' in summary['effect_totals'].coords['effect'].values
+    assert np.isfinite(summary['effect_totals'].sel(effect='cost').item())
🤖 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_stats_summary.py` at line 29, The test currently only asserts the
'cost' coordinate exists in summary['effect_totals'] but not that its value is
meaningful; update the test to also assert the selected cost total is
finite/non-NaN (e.g., use numpy.isfinite or pandas.notna) by selecting the value
via summary['effect_totals'].sel(effect='cost') (or equivalent indexing used
elsewhere in the file) and asserting that the returned scalar is finite/not-NaN
to match the checks applied to 'objective' and 'full_load_hours'.
🤖 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.

Nitpick comments:
In `@tests/test_stats_summary.py`:
- Line 29: The test currently only asserts the 'cost' coordinate exists in
summary['effect_totals'] but not that its value is meaningful; update the test
to also assert the selected cost total is finite/non-NaN (e.g., use
numpy.isfinite or pandas.notna) by selecting the value via
summary['effect_totals'].sel(effect='cost') (or equivalent indexing used
elsewhere in the file) and asserting that the returned scalar is finite/not-NaN
to match the checks applied to 'objective' and 'full_load_hours'.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f081032-a7ac-4ffb-bf0a-e61749895127

📥 Commits

Reviewing files that changed from the base of the PR and between 9723917 and 7b7b63d.

📒 Files selected for processing (2)
  • src/fluxopt/stats.py
  • tests/test_stats_summary.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/fluxopt/stats.py

Match the finiteness checks already applied to objective and full_load_hours,
so a NaN/missing cost total fails instead of passing on the key alone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shaan-alpha
Copy link
Copy Markdown
Author

Shaan-alpha commented Jun 1, 2026

Added in 169e748assert np.isfinite(summary['effect_totals'].sel(effect='cost').item()), so the cost total now gets the same finiteness check as objective and full_load_hours. Test still green locally.

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

Labels

type:feat New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(stats): summary() headline KPI table

1 participant