-
Notifications
You must be signed in to change notification settings - Fork 9
Fix/expand flow system rework #591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
1. Fixed _interpolate_charge_state_segmented Bug (Critical) File: flixopt/transform_accessor.py:1965-2043 The function was incorrectly decoding timestep_mapping using timesteps_per_cluster. For segmented systems, timestep_mapping encodes cluster * n_segments + segment_idx, so this produced wrong cluster indices. Fix: Compute original period index and position directly from timestep indices instead of decoding from timestep_mapping. 2. Implemented EXPAND_FIRST_TIMESTEP for Segmented Systems Only File: flixopt/transform_accessor.py:2045-2100 Added _expand_first_timestep_only() method that places startup/shutdown events at the first timestep of each segment (not cluster). Key design decisions: - Segmented systems: Events placed at first timestep of each segment (timing within segment is lost) - Non-segmented systems: Normal expansion preserves timing within cluster (no special handling needed) 3. Added Tests - test_startup_shutdown_first_timestep_only: Verifies segmented systems place events at segment boundaries - test_startup_timing_preserved_non_segmented: Verifies non-segmented systems preserve timing within clusters 4. Updated Docstrings Clarified in expand() docstring that: - Binary events in segmented systems go to first timestep of each segment - Non-segmented systems preserve timing via normal expansion
1. Fixed _interpolate_charge_state_segmented Bug (Critical) File: flixopt/transform_accessor.py:1965-2043 The function was incorrectly decoding timestep_mapping using timesteps_per_cluster. For segmented systems, timestep_mapping encodes cluster * n_segments + segment_idx, so this produced wrong cluster indices. Fix: Compute original period index and position directly from timestep indices instead of decoding from timestep_mapping. 2. Implemented EXPAND_FIRST_TIMESTEP for Segmented Systems Only File: flixopt/transform_accessor.py:2045-2100 Added _expand_first_timestep_only() method that places startup/shutdown events at the first timestep of each segment (not cluster). Key design decisions: - Segmented systems: Events placed at first timestep of each segment (timing within segment is lost) - Non-segmented systems: Normal expansion preserves timing within cluster (no special handling needed) 3. Added Tests - test_startup_shutdown_first_timestep_only: Verifies segmented systems place events at segment boundaries - test_startup_timing_preserved_non_segmented: Verifies non-segmented systems preserve timing within clusters 4. Updated Docstrings Clarified in expand() docstring that: - Binary events in segmented systems go to first timestep of each segment - Non-segmented systems preserve timing via normal expansion
…scenario) dimensions: 1. For each missing (period_label, scenario_label) key, it selects the specific period/scenario slice from the original variable using .sel(..., drop=True) 2. Then it slices and reshapes that specific slice 3. All slices in filled_slices now have consistent dimensions ['cluster', 'time'] + other_dims without 'period' or 'scenario' coordinates This ensures all DataArrays being concatenated have the same structure. Can you try running your clustering code again?
New Modules Created
1. flixopt/clustering/iteration.py - Shared iteration infrastructure:
- DimSliceContext dataclass for (period, scenario) slice context
- DimInfo dataclass for dimension metadata
- iter_dim_slices() generator for standardized iteration
- iter_dim_slices_simple() convenience function
2. flixopt/clustering/aggregation.py - Aggregation helpers:
- combine_slices_to_dataarray() - Unified slice combination function
- build_typical_dataarrays() - Build typical periods DataArrays
- build_segment_durations() - Build segment duration DataArray
- build_cluster_weights() - Build cluster weight DataArray
- build_clustering_metrics() - Build metrics Dataset
- calculate_clustering_weights() - Calculate clustering weights from data
- build_cluster_config_with_weights() - Merge weights into ClusterConfig
- accuracy_to_dataframe() - Convert tsam AccuracyMetrics to DataFrame
3. flixopt/clustering/expansion.py - Expansion helpers:
- VariableExpansionHandler class - Handles variable-specific expansion logic
- interpolate_charge_state_segmented() - Interpolate state variables in segments
- expand_first_timestep_only() - Expand binary events to first timestep
- build_segment_total_varnames() - Build segment total variable names
- append_final_state() - Append final state value to expanded data
4. Updated flixopt/clustering/intercluster_helpers.py:
- Added combine_intercluster_charge_states() - Combine charge_state with SOC_boundary
- Added apply_soc_decay() - Apply self-discharge decay to SOC values
Refactored flixopt/transform_accessor.py
- cluster() method: Now uses DimInfo and iter_dim_slices() for standardized iteration
- expand() method: Uses VariableExpansionHandler and extracted helper functions
- _build_reduced_flow_system(): Signature changed to use DimInfo instead of separate periods/scenarios lists
- _build_reduced_dataset(): Updated to use DimInfo and combine_slices_to_dataarray()
- apply_clustering(): Uses DimInfo for cleaner key conversion
Removed from transform_accessor.py
- _calculate_clustering_weights() → moved to clustering/aggregation.py
- _build_cluster_config_with_weights() → moved to clustering/aggregation.py
- _accuracy_to_dataframe() → moved to clustering/aggregation.py
- _build_cluster_weight_da() → moved to clustering/aggregation.py
- _build_typical_das() → moved to clustering/aggregation.py
- _build_segment_durations_da() → moved to clustering/aggregation.py
- _build_clustering_metrics() → moved to clustering/aggregation.py
- _combine_slices_to_dataarray_generic() → replaced by unified combine_slices_to_dataarray()
- _combine_slices_to_dataarray_2d() → replaced by unified combine_slices_to_dataarray()
- _combine_intercluster_charge_states() → moved to clustering/intercluster_helpers.py
- _apply_soc_decay() → moved to clustering/intercluster_helpers.py
- _build_segment_total_varnames() → moved to clustering/expansion.py
- _interpolate_charge_state_segmented() → moved to clustering/expansion.py
- _expand_first_timestep_only() → moved to clustering/expansion.py
Updated flixopt/clustering/__init__.py
- Exports all new helpers from the new modules
Tests
- All 102 tests in test_cluster_reduce_expand.py pass
- All 43 tests in test_clustering_io.py pass
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughModularizes clustering, expansion, and inter-cluster operations by extracting implementations from transform_accessor into dedicated clustering submodules; adds DimInfo for multi-dimensional (period/scenario) handling; re-exports new public APIs via flixopt.clustering.init; updates consumers to use the new helpers. (49 words) Changes
Sequence Diagram(s)sequenceDiagram
participant TA as transform_accessor
participant DIM as clustering.iteration::DimInfo / iter_dim_slices
participant TSAM as tsam.aggregate
participant AGG as clustering.aggregation
participant EXP as clustering.expansion::VariableExpansionHandler
participant ICC as clustering.intercluster_helpers
TA->>DIM: build DimInfo from flow_system
TA->>DIM: iterate slices via iter_dim_slices
loop per (period,scenario)
TA->>TSAM: call aggregate on selected data
TSAM-->>TA: Aggregation results (per-slice)
TA->>AGG: combine_slices_to_dataarray / build_typical_dataarrays
AGG-->>TA: reduced DataArrays/Datasets
end
TA->>EXP: decide expansion paths via VariableExpansionHandler
EXP-->>TA: expanded per-variable DataArrays
TA->>ICC: combine_intercluster_charge_states (apply_soc_decay if needed)
ICC-->>TA: SOC-reconstructed solution
TA->>TA: assemble final expanded FlowSystem / dataset
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
flixopt/transform_accessor.py (2)
376-387: Update_build_cluster_assignments_dato use the unifiedcombine_slices_to_dataarrayfunction.The method calls
self._combine_slices_to_dataarray_generic(), which no longer exists—it was unified intocombine_slices_to_dataarray()(see comment at line 1371-1372). This will raiseAttributeErrorwhen invoked.Proposed fix
- return self._combine_slices_to_dataarray_generic( - cluster_assignments_slices, ['original_cluster'], periods, scenarios, 'cluster_assignments' - ) + dim_info = DimInfo(periods=periods, scenarios=scenarios) + return combine_slices_to_dataarray( + slices=cluster_assignments_slices, + dim_info=dim_info, + base_dims=['original_cluster'], + name='cluster_assignments', + )
1320-1351: Add explicit period/scenario validation before callingclustering.results.apply().The docstring promises a
ValueErrorif clustering dimensions don't match, but currently validation is implicit via xarray's.sel(), which raisesKeyErrorwith an unclear message. Add a preemptive guard that validatesclustering.coords(e.g., checking that all periods/scenarios inclusteringexist in the dataset) before line 1340, with a clear error message matching the docstring's contract.
🤖 Fix all issues with AI agents
In `@flixopt/clustering/intercluster_helpers.py`:
- Around line 277-335: The code assumes loss_value has a .values attribute which
fails for scalar returns from _scalar_safe_reduce; update apply_soc_decay to
handle both scalars and DataArray: after loss_value = _scalar_safe_reduce(...),
normalize it by wrapping scalars into an xarray/DataArray or use numpy
conversion (e.g., loss_arr = np.asarray(loss_value)) and replace the check if
not np.any(loss_value.values > 0) with if not np.any(loss_arr > 0); then use
loss_arr (or xr.DataArray(loss_value) if you prefer) for the decay calculation
(decay_da = (1 - loss_arr) ** time_within_period_da) so both scalar floats and
DataArrays from storage.relative_loss_per_hour work without AttributeError.
In `@flixopt/transform_accessor.py`:
- Around line 1164-1176: The current check uses is_multi_dimensional =
dim_info.has_periods or dim_info.has_scenarios which wrongly blocks
extremes.method 'new_cluster'/'append' even for a single period/scenario slice;
change the guard to detect multiple period/scenario combinations instead (e.g.,
compute total_slices from dim_info — using available attributes like
dim_info.periods or dim_info.n_periods and dim_info.scenarios or
dim_info.n_scenarios — and treat multi-dimensional only when total_slices > 1),
then only raise the ValueError for extremes.method in ('new_cluster', 'append')
when total_slices > 1. Ensure you reference the same variables used in the diff
(dim_info, extremes, extreme_method) when updating the condition.
🧹 Nitpick comments (3)
flixopt/clustering/aggregation.py (1)
98-100: Preserve fullbase_dimsordering after concatenation.Only the first base dimension is forced to the front, so other base dims (e.g.,
time) can end up afterperiod/scenario, contradicting the stated order. Consider transposing with all base dims up front.♻️ Proposed change
- result = result.transpose(base_dims[0], ...) + result = result.transpose(*base_dims, ...)tests/test_cluster_reduce_expand.py (2)
841-870: Align thenew_clusterexpectation with the assertion.The description says
new_clustershould increasen_clusters, but the assertion allows no increase. Consider tightening the assertion or updating the wording to match the intended behavior.💡 Possible tweak
- assert fs_clustered.clustering.n_clusters >= 2 + assert fs_clustered.clustering.n_clusters > 2
908-923: Test name/docstring doesn’t match the ExtremeConfig method used.The test is named for
new_cluster, but it configuresmethod='append'. Consider renaming the test/docstring or switching the method to keep intent unambiguous.💡 Possible rename
- def test_extremes_new_cluster_with_segments(self, solver_fixture, timesteps_8_days): - """Test that method='new_cluster' works correctly with segmentation.""" + def test_extremes_append_with_segments(self, solver_fixture, timesteps_8_days): + """Test that method='append' works correctly with segmentation."""
… to handle both scalar and DataArray returns from _scalar_safe_reduce using np.asarray().
2. flixopt/transform_accessor.py: Fixed the multi-dimensional check to only block extremes.method='new_cluster'/'append' when there are actually multiple slices (total_slices
> 1), not just when periods or scenarios exist.
3. flixopt/clustering/aggregation.py: Fixed combine_slices_to_dataarray to transpose with all base dims in order (*base_dims, ...) instead of just the first one.
4. tests/test_cluster_reduce_expand.py:
- Updated test_extremes_new_cluster_increases_n_clusters docstring to clarify that tsam may or may not add clusters (the >= 2 assertion was actually correct)
- Renamed test_extremes_new_cluster_with_segments to test_extremes_append_with_segments and updated docstring to match the actual method='append' used
…m-rework # Conflicts: # flixopt/transform_accessor.py # tests/test_cluster_reduce_expand.py
Description
Brief description of the changes in this PR.
Type of Change
Related Issues
Closes #(issue number)
Testing
Checklist
Summary by CodeRabbit
New Features
Refactor
Tests
Chores
✏️ Tip: You can customize this high-level summary in your review settings.