diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d692d5e5..7e0b2d71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,81 +2,129 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and [gitmoji](https://gitmoji.dev) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### 💥 Breaking Changes +* **💥 BREAKING**: Removed `kind` in favor of `style` in plotting functions +* **💥 BREAKING**: Renamed `TimeSeries.active_data` to `TimeSeries.selected_data` +* **💥 BREAKING**: `CalculationResults.flow_system` now returns the restored FlowSystem instead of the `xr.Dataset`. The data can be found under `flow_system_data` + +### ✨ Added +#### Major Features +* **Scenarios**: Model uncertainties or **Multi-Period Transformations** + * Scenarios are passed to a `FlowSystem` with `scenario_weight` multipliers + * Total objective effect of each scenario forms the optimization objective + * Sizes might be optimized for each scenario separately, globally or only for a subset of all scenarios (See `InvestmentParameters`). +* **Balanced Storage**: Storage charging and discharging sizes can now be forced to be equal when optimizing by choosing `balanced=True` + +#### Results & Analysis +* **New dedicated `FlowResults` class** + * Dedicated xr.DataArrays combining all **flow_rates**, **flow_hours**, or **sizes** of flows + * Use `effects_per_component()` to retrieve all effects results for every Component, including indirect effects (ElementA → CO2 → Costs) + +#### API Improvements +* Support for pandas.Series and pandas.DataFrame when setting Element parameters (internally converted to xarray.DataArrays) +* Improved internal datatypes for clearer data format requirements: + * `Scalar` for scalar values only + * `TimestepData` for time-indexed data (with optional scenario dimension) + * `ScenarioData` for scenario-dimensional data + +#### Plotting & Visualization +* All plotting styles available for both plotly and matplotlib plots: `stacked_bar`, `line`, `area` +* Added `grouped_bar` plotting style +* Changed default legend location in plots (now on the right side) + +### 🗑️ Deprecated +* `Calculation.active_timesteps` → Use `Calculation.selected_timesteps` instead +* ⚠️ Loading Results from prior versions will raise warnings due to FlowResults incompatibility. Some new features cannot be used. + +### 🐛 Fixed +* Fixed formatting issues in YAML model documentation (line breaks) + +### Known Issues +* Scenarios are not yet supported in `AggregatedCalculation` and `SegmentedCalculation` + ## [2.1.2] - 2025-06-14 -### Fixed -- Storage losses per hour where not calculated correctly, as mentioned by @brokenwings01. This might have lead to issues with modeling large losses and long timesteps. - - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ - - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ +### 🐛 Fixed +* **Critical Fix**: Storage losses per hour calculation corrected (thanks @brokenwings01) + * **Impact**: Affects modeling of large losses and long timesteps + * **Old**: `c(t_i) · (1-ċ_rel,loss(t_i)) · Δt_i` + * **Correct**: `c(t_i) · (1-ċ_rel,loss(t_i))^Δt_i` -### Known issues -- Just to mention: Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. +### Known Issues +* Plotly >= 6 may raise errors if "nbformat" is not installed (pinned to <6 for now) ## [2.1.1] - 2025-05-08 -### Fixed -- Fixed bug in the `_ElementResults.constraints` not returning the constraints but rather the variables +### 🐛 Fixed +* Fixed `_ElementResults.constraints` returning variables instead of constraints ### Changed -- Improved docstring and tests +* Improved docstrings and tests ## [2.1.0] - 2025-04-11 -### Added -- Python 3.13 support added -- Logger warning if relative_minimum is used without on_off_parameters in Flow -- Greatly improved internal testing infrastructure by leveraging linopy's testing framework +### 💥 Breaking Changes +* **💥 BREAKING**: Restructured On/Off state modeling for Flows and Components + * **♻️ Variable renaming**: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` + * **♻️ Variable renaming**: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` + * **♻️ Constraint renaming**: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + * Similar pattern applied to all consecutive on/off constraints -### Fixed -- Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters -- Fixed bug that prevented divest effects from working -- Added lower bounds of 0 to two unbounded vars (numerical improvement) +### ✨ Added +* **Python 3.13 support** +* Enhanced testing infrastructure leveraging linopy's testing framework +* Logger warnings for `relative_minimum` usage without `on_off_parameters` in Flow -### Changed -- **BREAKING**: Restructured the modeling of the On/Off state of Flows or Components - - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` - - Similar pattern for all consecutive on/off constraints +### 🐛 Fixed +* Fixed `flow_rate` lower bound issues with optional investments without OnOffParameters +* Fixed divest effects functionality +* Added missing lower bounds of 0 to unbounded variables (numerical stability improvement) ## [2.0.1] - 2025-04-10 -### Added -- Logger warning if relative_minimum is used without on_off_parameters in Flow +### 🐛 Fixed +* **Windows Compatibility**: Replace "|" with "__" in figure filenames +* Fixed load factor functionality without InvestmentParameters -### Fixed -- Replace "|" with "__" in filenames when saving figures (Windows compatibility) -- Fixed bug that prevented the load factor from working without InvestmentParameters +### ✨ Added +* Logger warning for `relative_minimum` usage without `on_off_parameters` in Flow ## [2.0.0] - 2025-03-29 -### Changed -- **BREAKING**: Complete migration from Pyomo to Linopy optimization framework -- **BREAKING**: Redesigned data handling to rely on xarray.Dataset throughout the package -- **BREAKING**: Framework renamed from flixOpt to flixopt (`import flixopt as fx`) -- **BREAKING**: Results handling completely redesigned with new `CalculationResults` class - -### Added -- Full model serialization support - save and restore unsolved Models -- Enhanced model documentation with YAML export containing human-readable mathematical formulations -- Extend flixopt models with native linopy language support -- Full Model Export/Import capabilities via linopy.Model -- Unified solution exploration through `Calculation.results` attribute -- Compression support for result files -- `to_netcdf/from_netcdf` methods for FlowSystem and core components -- xarray integration for TimeSeries with improved datatypes support -- Google Style Docstrings throughout the codebase - -### Fixed -- Improved infeasible model detection and reporting -- Enhanced time series management and serialization -- Reduced file size through improved compression - -### Removed -- **BREAKING**: Pyomo dependency (replaced by linopy) -- Period concepts in time management (simplified to timesteps) \ No newline at end of file +### 💥 Breaking Changes +* **💥 BREAKING**: Complete migration from Pyomo to Linopy optimization framework +* **💥 BREAKING**: Redesigned data handling using xarray.Dataset throughout +* **💥 BREAKING**: Framework renamed from flixOpt to flixopt (`import flixopt as fx`) +* **💥 BREAKING**: Complete redesign of Results handling with new `CalculationResults` class +* **🔥 BREAKING**: Removed Pyomo dependency +* **🔥 BREAKING**: Removed Period concepts (simplified to timesteps) + +### ✨ Added +#### Major Features +* **Full model serialization**: Save and restore unsolved Models +* **Enhanced model documentation**: YAML export with human-readable mathematical formulations +* **Native linopy integration**: Extend flixopt models with linopy language support +* **Model Export/Import**: Full capabilities via linopy.Model + +#### Results & Analysis +* **Unified solution exploration** through `Calculation.results` attribute +* **Compression support** for result files +* **xarray integration** for TimeSeries with improved datatypes support + +#### API Improvements +* `to_netcdf/from_netcdf` methods for FlowSystem and core components +* Google Style Docstrings throughout codebase + +### 🐛 Fixed +* **Improved infeasible model detection and reporting** +* Enhanced time series management and serialization +* Reduced file sizes through better compression + +### 🔥 Removed +* **BREAKING**: Pyomo dependency (replaced by linopy) +* **BREAKING**: Period concepts in time management (simplified to timesteps) \ No newline at end of file diff --git a/docs/release-notes/v2.2.0.md b/docs/release-notes/v2.2.0.md deleted file mode 100644 index 3cf7eef8d..000000000 --- a/docs/release-notes/v2.2.0.md +++ /dev/null @@ -1,55 +0,0 @@ -# Release v2.2.0 - -**Release Date:** YYYY-MM-DD - -## What's New - -### Scenarios -Scenarios are a new feature of flixopt. They can be used to model uncertainties in the flow system, such as: -* Different demand profiles -* Different price forecasts -* Different weather conditions -* Different climate conditions -The might also be used to model an evolving system with multiple investment periods. Each **scenario** might be a new year, a new month, or a new day, with a different set of investment decisions to take. - -The weighted sum of the total objective effect of each scenario is used as the objective of the optimization. - -#### Investments and scenarios -Scenarios allow for more flexibility in investment decisions. -You can decide to allow different investment decisions for each scenario, or to allow a single investment decision for a subset of all scnarios, while not allowing for an invest in others. -This enables the following use cases: -* Find the best investment decision for each scenario individually -* Find the best overall investment decision for possible scenarios (robust decision-making) -* Find the best overall investment decision for a subset of all scenarios - -The last one might be useful if you want to model a system with multiple investment periods, where one investment decision is made for more than one scenario. -This might occur when scenarios represent years or months, while an investment decision influences the system for multiple years or months. - - -## Other new features -* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. -* Feature 2 - Description - -## Improvements - -* Improvement 1 - Description -* Improvement 2 - Description - -## Bug Fixes - -* Fixed issue with X -* Resolved problem with Y - -## Breaking Changes - -* Change 1 - Migration instructions -* Change 2 - Migration instructions - -## Deprecations - -* Feature X will be removed in v{next_version} - -## Dependencies - -* Added dependency X v1.2.3 -* Updated dependency Y to v2.0.0 \ No newline at end of file diff --git a/docs/user-guide/Analyze Results/index.md b/docs/user-guide/Analyze Results/index.md new file mode 100644 index 000000000..dcc6dc61d --- /dev/null +++ b/docs/user-guide/Analyze Results/index.md @@ -0,0 +1,341 @@ +# Working with FlixOpt Results + +The results of an optimization are stored in the `results` attribute of a [`Calculation`][flixopt.calculation.Calculation] object. This documentation provides a comprehensive guide to working with these results effectively. + +## Result Objects + +Depending on the calculation type, the results are stored in different formats: + +- For [`FullCalculation`][flixopt.calculation.FullCalculation] and [`AggregatedCalculation`][flixopt.calculation.AggregatedCalculation]: Results are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object +- For [`SegmentedCalculation`][flixopt.calculation.SegmentedCalculation]: Results are stored in a [`SegmentedCalculationResults`][flixopt.results.SegmentedCalculationResults] object + +These result objects can be saved to files and reloaded later for further analysis. + +## Accessing Results + +There are multile ways of acessing the results of a calculation. One method might be more convenient than the others, depending on your use case. + +### Acess through composed DataArrays + +The results object provides easy access for the most commonly needed results, such as: + +* Flow Rates, through [`CalculationResults.flow_rates()`][flixopt.results.CalculationResults.flow_rates] +* Flow hours, through [`CalculationResults.flow_hours()`][flixopt.results.CalculationResults.flow_hours] +* Flow Sizes, through [`CalculationResults.sizes()`][flixopt.results.CalculationResults.sizes] +* Effects per Component, through [`CalculationResults.effects_per_component()`][flixopt.results.CalculationResults.effects_per_component] + +These datasets can be filtered by start and end node or by component. +And will most likely be converted to pandas DataFrames for exporting or plotting. + +Accessing the flow rates ending at the node "Fernwärme" +```python +# Filter flow_rates by start and end node +calculation_results.flow_rates(end='Fernwärme').to_pandas() +``` +``` +flow Boiler(Q_th) CHP(Q_th) Storage(Q_th_unload) +time +2020-01-01 00:00:00 5.0000 25.000000 -4.574119e-14 +2020-01-01 01:00:00 5.0000 21.666667 -2.286171e-15 +2020-01-01 02:00:00 5.0000 75.000000 1.000000e+01 +2020-01-01 03:00:00 23.8864 75.000000 1.111360e+01 +2020-01-01 04:00:00 35.0000 75.000000 -6.394885e-14 +2020-01-01 05:00:00 5.0000 15.000000 0.000000e+00 +2020-01-01 06:00:00 5.0000 15.000000 0.000000e+00 +2020-01-01 07:00:00 5.0000 15.000000 0.000000e+00 +2020-01-01 08:00:00 5.0000 15.000000 0.000000e+00 +2020-01-01 09:00:00 NaN NaN NaN +``` + +Accessing the flow rates staring at the "Boiler" +```python +calculation_results.flow_rates(start='Boiler').to_pandas() +``` +``` +flow Boiler(Q_th) +time +2020-01-01 00:00:00 5.0000 +2020-01-01 01:00:00 5.0000 +2020-01-01 02:00:00 5.0000 +2020-01-01 03:00:00 23.8864 +2020-01-01 04:00:00 35.0000 +2020-01-01 05:00:00 5.0000 +2020-01-01 06:00:00 5.0000 +2020-01-01 07:00:00 5.0000 +2020-01-01 08:00:00 5.0000 +2020-01-01 09:00:00 NaN +``` + +Accessing all sizes of the "Boiler" +```python +calculation_results.sizes(component='Boiler').to_pandas() +``` +``` +flow +Boiler(Q_fu) 10000000.0 +Boiler(Q_th) 50.0 +Name: flow_sizes, dtype: float64 +``` + +Or acessing the effects per component +```python +# filter effects_per_component by component +calculation_results.effects_per_component(mode='operation', component='Gastarif').to_pandas() +``` +``` + Size: 24B +Dimensions: (component: 1) +Coordinates: + * component (component) object 8B 'Gastarif' +Data variables: + CO2 (component) float64 8B 255.3 + costs (component) float64 8B 85.11 +``` + + + +This will return a `xarray.DataArray` with the flow rates ending at the `Fernwärme` node. +``` +xarray.DataArray 'flow_rates' (time: 10, flow: 3)> Size: 240B +array([[ 5. , 25. , -0. ], + [ 5. , 21.67, -0. ], + [ 5. , 75. , 10. ], + [23.89, 75. , 11.11], + [35. , 75. , -0. ], + [ 5. , 15. , 0. ], + [ 5. , 15. , 0. ], + [ 5. , 15. , 0. ], + [ 5. , 15. , 0. ], + [ nan, nan, nan]]) +Coordinates: + * time (time) datetime64[ns] 80B 2020-01-01 ... 2020-01-01T09:00:00 + * flow (flow) object 24B 'Boiler(Q_th)' ... 'Storage(Q_th_unload)' + start (flow) ![FlixOpt Conceptual Usage](../images/architecture_flixOpt.png) diff --git a/flixopt/core.py b/flixopt/core.py index ea447e652..559968071 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -969,7 +969,18 @@ def __init__( hours_of_last_timestep: Optional[float] = None, hours_of_previous_timesteps: Optional[Union[float, np.ndarray]] = None, ): - """Initialize a TimeSeriesCollection.""" + """ + Initialize a TimeSeriesCollection. + + Args: + timesteps: The timesteps of the model. + scenarios: The scenarios of the model. + hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified + hours_of_previous_timesteps: The duration of previous timesteps. + If None, the first time increment of time_series is used. + This is needed to calculate previous durations (for example consecutive_on_hours). + If you use an array, take care that its long enough to cover all previous values! + """ self._full_timesteps = self._validate_timesteps(timesteps) self._full_scenarios = self._validate_scenarios(scenarios) diff --git a/flixopt/results.py b/flixopt/results.py index bd0abaa5e..1971e12ec 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -306,11 +306,17 @@ def filter_solution( startswith=startswith, ) - def effects_per_component(self, mode: Literal['operation', 'invest', 'total'] = 'total') -> xr.Dataset: + def effects_per_component( + self, + mode: Literal['operation', 'invest', 'total'] = 'total', + component: Optional[Union[str, List[str]]] = None + ) -> xr.Dataset: """Returns a dataset containing effect totals for each components (including their flows). + The effects contain direct as well as indirect effect through shares between effects! Args: mode: Which effects to contain. (operation, invest, total) + component: The component to return the effects for. If None, all components are returned. Returns: An xarray Dataset with an additional component dimension and effects as variables. @@ -319,7 +325,10 @@ def effects_per_component(self, mode: Literal['operation', 'invest', 'total'] = raise ValueError(f'Invalid mode {mode}') if self._effects_per_component[mode] is None: self._effects_per_component[mode] = self._create_effects_dataset(mode) - return self._effects_per_component[mode] + ds = self._effects_per_component[mode] + if component is not None: + return ds.sel(component=component, drop=True) + return ds def flow_rates( self,