Skip to content

Conversation

@FBumann
Copy link
Member

@FBumann FBumann commented Jan 2, 2026

Summary

Simplify the weights API by always normalizing scenario weights when set on FlowSystem. This removes the need for the normalize_weights parameter and ensures consistent behavior across all access paths.

Changes

Breaking Changes

  • Scenario weights are now automatically normalized to sum to 1 when set
  • Subset selections (.sel(), .transform.sel()) re-normalize weights

API Changes

  • Deprecated normalize_weights parameter in create_model(), build_model(), optimize()
  • FlowSystem.weights now returns dict[str, xr.DataArray] (no more float fallbacks)
  • FlowSystemDimensions type now includes 'cluster'

Bug Fixes

  • Fixed temporal_weight vs sum_temporal() implementation inconsistency
  • Fixed model.weights['scenario'] vs model.scenario_weights divergence

Internal

  • Removed normalize_weights from FlowSystemModel constructor
  • Added _unit_weight() helper for consistent unit weight creation
  • Simplified FlowSystemModel.scenario_weights to delegate to FlowSystem

  Changes made:

  1. elements.py - Flow tracking:
  # Before:
  flow_hours = self.flow_rate * self._model.timestep_duration
  weighted_flow_hours = flow_hours * self._model.cluster_weight
  tracked_expression=weighted_flow_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.flow_rate)
  2. elements.py - Load factor total hours:
  # Before:
  total_hours = (self._model.timestep_duration * self._model.cluster_weight).sum(self._model.temporal_dims)

  # After:
  total_hours = self._model.weights.temporal.sum(self._model.weights.temporal_dims)
  3. features.py - Status tracking:
  # Before:
  active_hours = self.status * self._model.timestep_duration
  weighted_active_hours = active_hours * self._model.cluster_weight
  tracked_expression=weighted_active_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.status)
  4. features.py - Temporal effects summing (only needs cluster weight since already per-timestep):
  # Before:
  weighted_per_timestep = self.total_per_timestep * self._model.cluster_weight
  temporal_dims = [d for d in self.total_per_timestep.dims if d not in ('period', 'scenario')]

  # After:
  weighted_per_timestep = self.total_per_timestep * self._model.weights.cluster
  self._eq_total.lhs -= weighted_per_timestep.sum(dim=self._model.weights.temporal_dims)
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 2, 2026

📝 Walkthrough

Walkthrough

TimeSeriesWeights is replaced with a new Weights class that provides a generalized weighting API initialized from FlowSystem. Temporal weighting now uses computed properties and a unified sum_temporal method. All modules using weights are updated to leverage this new interface, eliminating manual weight calculations.

Changes

Cohort / File(s) Summary
Public API Updates
flixopt/__init__.py, flixopt/flow_system.py
TimeSeriesWeights removed from exports and replaced with Weights. FlowSystem.weights property updated to return Weights instead of TimeSeriesWeights.
Weights System Refactor
flixopt/structure.py
New Weights class replaces TimeSeriesWeights dataclass. Initialized from FlowSystem instead of inline fields. Introduces computed temporal property (time × cluster), temporal_dims property, and sum_temporal(data) method for unified weighting and aggregation.
Weights Usage Integration
flixopt/elements.py, flixopt/features.py, flixopt/statistics_accessor.py
Manual temporal weighting calculations replaced with model.weights.sum_temporal() and model.weights properties. Cluster weight and timestep duration access now delegated to the Weights abstraction.
Test Updates
tests/test_clustering/test_integration.py
Imports and assertions updated to reference Weights instead of TimeSeriesWeights. Tests validate new Weights API (time, cluster, temporal_dims properties and sum_temporal method).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #352: Modifies FlowSystem/structure weights implementation and FlowSystemModel.weights behavior, directly aligned with this PR's core refactor.
  • PR #475: Reworks FlowSystem weight handling by introducing scenario_weights/period_weights mechanisms, complementary to this Weights class redesign.
  • PR #506: Updates FlowSystem properties and StatisticsAccessor usage of weights, overlapping with the weights API migration in this PR.

Poem

🐰 With whiskers twitch and code so bright,
We've spun the weights into new light—
TimeSeriesWeights now fade away,
Weights class blooms to save the day! 🌟
Temporal sums, unified and true,
Hopping forward—refactored anew!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title mentions 'normalize scenario weights' but the changeset focuses on refactoring the weights API from TimeSeriesWeights to Weights class, integrating it across multiple modules, and delegating temporal weighting to model.weights. Update the title to reflect the main change: 'Refactor weights API: introduce unified Weights class and integrate across modules' or similar.
Description check ❓ Inconclusive The PR description contains significant content but shows a major inconsistency between the stated objectives and the summary provided. Clarify which changes are actually in scope: either normalize scenario weights automatically (as stated in Summary/Breaking Changes), or refactor weights API to use Weights class (as shown in raw_summary and commit messages). Update description to match the actual implementation.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 96.67% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

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.

@FBumann
Copy link
Member Author

FBumann commented Jan 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
flixopt/statistics_accessor.py (1)

848-852: Use of self._fs.weights.cluster correctly aligns with new weights API

Multiplying temporal shares by weights.cluster and then summing over all non-period/scenario dims preserves the previous semantics while handling both clustered (DataArray) and non‑clustered (scalar 1.0) systems cleanly. No issues seen here.

flixopt/__init__.py (1)

34-35: Public export switches from TimeSeriesWeights to Weights – consider transition plan

Exposing Weights via __all__ is consistent with the new unified weights API, but any external code doing from flixopt import TimeSeriesWeights will now break unless you keep a compatibility alias somewhere.

If you want a softer migration path, consider re‑exporting an alias (e.g. TimeSeriesWeights = Weights) or documenting this as a breaking change in the release notes.

Also applies to: 36-62

flixopt/features.py (1)

199-214: StatusModel active‑hours tracking correctly switches to model.weights

Computing:

  • total_hours from self._model.weights.temporal.sum(self._model.weights.temporal_dims), and
  • tracked_expression as self._model.weights.sum_temporal(self.status)

gives a consistent, weights‑aware definition of total/active hours that automatically handles clustered and non‑clustered systems. The fallback upper bound total_hours.max().item() is conservative but safe (it will never cut off feasible solutions since it’s ≥ any period/scenario total).

No functional issues here.

flixopt/structure.py (1)

92-100: Consider exposing scenario_weights through a public accessor.

The scenario property accesses self._fs._scenario_weights, a private attribute. While this works, it creates a dependency on internal implementation details. If FlowSystem has or could add a public scenario_weights property (similar to period_weights), using that would be more robust.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e38cc2 and 0cad8de.

📒 Files selected for processing (7)
  • flixopt/__init__.py
  • flixopt/elements.py
  • flixopt/features.py
  • flixopt/flow_system.py
  • flixopt/statistics_accessor.py
  • flixopt/structure.py
  • tests/test_clustering/test_integration.py
🧰 Additional context used
🧬 Code graph analysis (7)
flixopt/statistics_accessor.py (3)
flixopt/structure.py (2)
  • weights (329-335)
  • cluster (74-78)
flixopt/flow_system.py (1)
  • weights (2009-2021)
flixopt/transform_accessor.py (1)
  • cluster (577-933)
flixopt/elements.py (2)
flixopt/structure.py (5)
  • weights (329-335)
  • sum_temporal (102-119)
  • temporal (81-83)
  • temporal_dims (86-90)
  • temporal_dims (318-326)
flixopt/flow_system.py (1)
  • weights (2009-2021)
flixopt/__init__.py (1)
flixopt/structure.py (1)
  • Weights (46-119)
flixopt/flow_system.py (1)
flixopt/structure.py (2)
  • Weights (46-119)
  • weights (329-335)
flixopt/features.py (3)
flixopt/structure.py (6)
  • weights (329-335)
  • temporal (81-83)
  • temporal_dims (86-90)
  • temporal_dims (318-326)
  • sum_temporal (102-119)
  • cluster (74-78)
flixopt/flow_system.py (1)
  • weights (2009-2021)
flixopt/modeling.py (2)
  • ModelingPrimitives (196-395)
  • expression_tracking_variable (200-242)
flixopt/structure.py (1)
flixopt/flow_system.py (1)
  • weights (2009-2021)
tests/test_clustering/test_integration.py (2)
flixopt/flow_system.py (2)
  • FlowSystem (56-2377)
  • weights (2009-2021)
flixopt/structure.py (11)
  • Weights (46-119)
  • weights (329-335)
  • time (69-71)
  • cluster (74-78)
  • temporal_dims (86-90)
  • temporal_dims (318-326)
  • temporal (81-83)
  • sum_temporal (102-119)
  • timestep_duration (298-300)
  • timestep_duration (1830-1831)
  • cluster_weight (307-315)
🔇 Additional comments (9)
flixopt/elements.py (1)

680-688: FlowModel now correctly delegates temporal aggregation to model.weights

Using self._model.weights.sum_temporal(self.flow_rate) for total_flow_hours and self._model.weights.temporal.sum(self._model.weights.temporal_dims) for total_hours keeps the previous behavior (timestep × cluster weighting, then summing over temporal dims) while centralizing the logic in the Weights abstraction. This should behave correctly for both clustered and non‑clustered systems and avoids duplicating weighting formulas.

Also applies to: 840-854

flixopt/features.py (1)

628-632: ShareAllocationModel now uses unified cluster/temporal weighting

Replacing direct use of self._model.cluster_weight with:

weighted_per_timestep = self.total_per_timestep * self._model.weights.cluster
self._eq_total.lhs -= weighted_per_timestep.sum(dim=self._model.weights.temporal_dims)

keeps the previous semantics (time × cluster aggregation to total) while routing everything through the Weights API. Broadcasting and summation dimensions match the intended temporal aggregation.

tests/test_clustering/test_integration.py (3)

8-8: LGTM!

Import updated correctly to use the new Weights class from the public API.


11-64: LGTM!

The TestWeights class provides good coverage for the new Weights API surface:

  • Creation via FlowSystem.weights property
  • Individual property accessors (time, cluster, temporal, temporal_dims)
  • The sum_temporal method with a clear rate-to-total conversion test

The use of xr.testing.assert_equal for DataArray comparisons is appropriate.


67-106: LGTM!

The updated tests correctly validate the new Weights API behavior:

  • Instance type checking with Weights
  • Temporal calculation verification (timestep_duration × cluster_weight)
  • Proper use of xr.testing.assert_equal for DataArray comparison

The test_weights_with_cluster_weight test appropriately validates per-timestep weighting without full clustering enabled.

flixopt/structure.py (4)

46-66: LGTM!

The Weights class provides a clean, unified interface for temporal weighting. The delegation pattern to FlowSystem attributes keeps the implementation simple and ensures consistency with the source data.


68-90: LGTM!

The properties are well-designed:

  • time and cluster delegate cleanly to FlowSystem attributes
  • temporal is a computed property that correctly multiplies time × cluster
  • temporal_dims dynamically returns the appropriate dimensions based on clustering status

102-119: LGTM!

The sum_temporal method correctly implements the rate-to-total conversion pattern by:

  1. Multiplying by the combined temporal weight (time × cluster)
  2. Summing over the appropriate temporal dimensions

The docstring clearly explains the use case with good examples.


328-335: LGTM!

The property correctly provides access to the unified weighting system. Creating a new Weights instance on each access is acceptable since the class is lightweight (only stores a reference to the FlowSystem).

Comment on lines 41 to 45
from .clustering import Clustering
from .solvers import _Solver
from .structure import TimeSeriesWeights
from .structure import Weights
from .types import Effect_TPS, Numeric_S, Numeric_TPS, NumericOrBool

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

FlowSystem.weights correctly exposes the new Weights API, but docstring is stale

The property implementation:

from .structure import Weights
return Weights(self)

is the right way to surface the unified weighting system without introducing import cycles.

However, the example in the docstring still shows weights.timestep, while the Weights class exposes time, cluster, temporal, etc. To avoid confusion, the example should be updated, e.g.:

-    >>> weights = flow_system.weights
-    >>> flow_hours = flow_rate * weights.timestep
-    >>> total = weights.sum_temporal(flow_hours)
+    >>> weights = flow_system.weights
+    >>> flow_hours = flow_rate * weights.time
+    >>> total = weights.sum_temporal(flow_hours)

Also applies to: 2009-2021

🤖 Prompt for AI Agents
In flixopt/flow_system.py around lines 41 to 45, the FlowSystem.weights property
correctly returns Weights(self) but the docstring example still references the
old weights.timestep API; update the docstring to use the current Weights
attributes (e.g., weights.time, weights.cluster, weights.temporal) and show
usage with the unified Weights instance returned by FlowSystem.weights; also
scan and update other docstring/examples in the file (and related files from
2009-2021) to replace any remaining weights.timestep references with the new
attribute names to avoid confusion.

  Changes made:

  1. elements.py - Flow tracking:
  # Before:
  flow_hours = self.flow_rate * self._model.timestep_duration
  weighted_flow_hours = flow_hours * self._model.cluster_weight
  tracked_expression=weighted_flow_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.flow_rate)
  2. elements.py - Load factor total hours:
  # Before:
  total_hours = (self._model.timestep_duration * self._model.cluster_weight).sum(self._model.temporal_dims)

  # After:
  total_hours = self._model.weights.temporal.sum(self._model.weights.temporal_dims)
  3. features.py - Status tracking:
  # Before:
  active_hours = self.status * self._model.timestep_duration
  weighted_active_hours = active_hours * self._model.cluster_weight
  tracked_expression=weighted_active_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.status)
  4. features.py - Temporal effects summing (only needs cluster weight since already per-timestep):
  # Before:
  weighted_per_timestep = self.total_per_timestep * self._model.cluster_weight
  temporal_dims = [d for d in self.total_per_timestep.dims if d not in ('period', 'scenario')]

  # After:
  weighted_per_timestep = self.total_per_timestep * self._model.weights.cluster
  self._eq_total.lhs -= weighted_per_timestep.sum(dim=self._model.weights.temporal_dims)
@FBumann FBumann changed the title Feature/sel+isel+cluster io+weights Refactor weights API: always normalize scenario weights Jan 3, 2026
@FBumann FBumann merged commit cb01345 into feature/sel+isel+cluster-io Jan 3, 2026
8 checks passed
FBumann added a commit that referenced this pull request Jan 3, 2026
#549)

* Enable selecting a single period/scenario

* No selected_* tracking

* Clustering IO

* Clustering IO

* Improve IO

* Improve validation

* Fix cluster weight IO

* Fix cluster weights stuff

* Fix cluster weights stuff

* Refactor weights API: always normalize scenario weights (#547)

* Add weights class

* Add weights class

* The Weights API is now used in the modeling equations:

  Changes made:

  1. elements.py - Flow tracking:
  # Before:
  flow_hours = self.flow_rate * self._model.timestep_duration
  weighted_flow_hours = flow_hours * self._model.cluster_weight
  tracked_expression=weighted_flow_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.flow_rate)
  2. elements.py - Load factor total hours:
  # Before:
  total_hours = (self._model.timestep_duration * self._model.cluster_weight).sum(self._model.temporal_dims)

  # After:
  total_hours = self._model.weights.temporal.sum(self._model.weights.temporal_dims)
  3. features.py - Status tracking:
  # Before:
  active_hours = self.status * self._model.timestep_duration
  weighted_active_hours = active_hours * self._model.cluster_weight
  tracked_expression=weighted_active_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.status)
  4. features.py - Temporal effects summing (only needs cluster weight since already per-timestep):
  # Before:
  weighted_per_timestep = self.total_per_timestep * self._model.cluster_weight
  temporal_dims = [d for d in self.total_per_timestep.dims if d not in ('period', 'scenario')]

  # After:
  weighted_per_timestep = self.total_per_timestep * self._model.weights.cluster
  self._eq_total.lhs -= weighted_per_timestep.sum(dim=self._model.weights.temporal_dims)

* The Weights API is now used in the modeling equations:

  Changes made:

  1. elements.py - Flow tracking:
  # Before:
  flow_hours = self.flow_rate * self._model.timestep_duration
  weighted_flow_hours = flow_hours * self._model.cluster_weight
  tracked_expression=weighted_flow_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.flow_rate)
  2. elements.py - Load factor total hours:
  # Before:
  total_hours = (self._model.timestep_duration * self._model.cluster_weight).sum(self._model.temporal_dims)

  # After:
  total_hours = self._model.weights.temporal.sum(self._model.weights.temporal_dims)
  3. features.py - Status tracking:
  # Before:
  active_hours = self.status * self._model.timestep_duration
  weighted_active_hours = active_hours * self._model.cluster_weight
  tracked_expression=weighted_active_hours.sum(self._model.temporal_dims)

  # After:
  tracked_expression=self._model.weights.sum_temporal(self.status)
  4. features.py - Temporal effects summing (only needs cluster weight since already per-timestep):
  # Before:
  weighted_per_timestep = self.total_per_timestep * self._model.cluster_weight
  temporal_dims = [d for d in self.total_per_timestep.dims if d not in ('period', 'scenario')]

  # After:
  weighted_per_timestep = self.total_per_timestep * self._model.weights.cluster
  self._eq_total.lhs -= weighted_per_timestep.sum(dim=self._model.weights.temporal_dims)

* Minor fixes in test

* Improve weighting system and normalization of scenrio weights

* Update CHANGELOG.md

* 1. ClusterStructure.n_clusters naming - Added explicit rename (matching n_representatives pattern) to avoid "None" variable names in serialized datasets
  2. original_timesteps validation - Added explicit KeyError with actionable message when original_time coordinate is missing
  3. active_hours bounds simplified - Passing total_hours DataArray directly instead of .max().item() fallback, allowing proper per-(period, scenario) bounds
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