From 9b761ff88bc1bbd0b008e2f211ed400caf781791 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:48:13 +0200 Subject: [PATCH 01/14] start docs for results --- docs/user-guide/Analyze Results/index.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/user-guide/Analyze Results/index.md diff --git a/docs/user-guide/Analyze Results/index.md b/docs/user-guide/Analyze Results/index.md new file mode 100644 index 000000000..1d50a62a7 --- /dev/null +++ b/docs/user-guide/Analyze Results/index.md @@ -0,0 +1,9 @@ +# Results + +The results of the optimization are stored in the `results` attribute of the [`Calculation`][flixopt.calculation.Calculation] object. +Depending on the type of calculation, the results are stored in different formats. For both [`FullCalculation`][flixopt.calculation.FullCalculation] and [`AggregatedCalculation`][flixopt.calculation.AggregatedCalculation], the results are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object. +THis object can be saved to a file and reloaded later. The used flow system is also stored in the results in the form of a xarray.Dataset. A proper FlowSystem can be reconstructed from the dataset using the [`FlowSystem.from_dataset`][flixopt.flow_system.FlowSystem.from_dataset] method. + +## Extracting Results for specific Elements + +In most cases, one wants to extract the results for a specific element, such as a bus or a component. From 0afb1e1d522a473df51461bc75bbc07f6d90eb93 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 13 Apr 2025 17:50:46 +0200 Subject: [PATCH 02/14] Improve results docs --- docs/user-guide/Analyze Results/index.md | 118 ++++++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/docs/user-guide/Analyze Results/index.md b/docs/user-guide/Analyze Results/index.md index 1d50a62a7..3221c5f7e 100644 --- a/docs/user-guide/Analyze Results/index.md +++ b/docs/user-guide/Analyze Results/index.md @@ -2,8 +2,120 @@ The results of the optimization are stored in the `results` attribute of the [`Calculation`][flixopt.calculation.Calculation] object. Depending on the type of calculation, the results are stored in different formats. For both [`FullCalculation`][flixopt.calculation.FullCalculation] and [`AggregatedCalculation`][flixopt.calculation.AggregatedCalculation], the results are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object. -THis object can be saved to a file and reloaded later. The used flow system is also stored in the results in the form of a xarray.Dataset. A proper FlowSystem can be reconstructed from the dataset using the [`FlowSystem.from_dataset`][flixopt.flow_system.FlowSystem.from_dataset] method. +This object can be saved to a file and reloaded later. The used flow system is also stored in the results in the form of a xarray.Dataset. A proper FlowSystem can be reconstructed from the dataset using the [`FlowSystem.from_dataset`][flixopt.flow_system.FlowSystem.from_dataset] method. -## Extracting Results for specific Elements +## General Data handling -In most cases, one wants to extract the results for a specific element, such as a bus or a component. +The results object provides a dictionary-like access to the results of the calculation. You can access any result by subscripting the object with the result's label. +The solution of the optimization is stored in the `solution` attribute of the results object as an [xarray.Dataset](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html). +This dataset contains all solutions for the variables of the optimization. +As the dataset can become a bit large, it is recommended to pass it to the [`fx.results.filter_dataset()`][flixopt.results.filter_dataset] function to select only the variables of interest. +There you can filter out variables that dont have a certain dimension, select a subset of the timesteps or **drop trailing nan values**. +### Dataset handling +Further, here are some of the most commonly used methods to process a dataset: + +- `solution.sel(time=slice('2020-01-01', '2020-01-10'))`: Select a subset of the solution by time +- `solution.isel(time=0)`: Select a subset of the solution by time (by index) +- `solution.sum('time')`: Sum the solution over all timesteps (take care that you might need to multiply by the timesteps_per_hour to get the actual flow_hours) +- `solution.to_dataframe()`: Convert the solution to a pandas.DataFrame (leads to Multiindexes) +- `solution.to_pandas()`: Convert the solution to a pandas.DataFrame or pandas.Series, depending on the number of dimensions +- `solution.resample('D').sum()`: Resample the solution to daily timesteps and sum the values + +For more information on how to use xarray, please refer to the [xarray documentation](https://docs.xarray.dev/en/stable/). + +Instead +```python +results: fx.CalculationResults +da: xarray.Dataarray = results['Boiler(Q_th)|flow_rate'] +``` + +From there you have all the functionality of an [xarray.DataArray](https://docs.xarray.dev/en/stable/generated/xarray.DataArray.html). +Here are some of the most commonly used methods: + +- `da[2]` or `da[2:5]`: Select a subset of the data by index (time) +- `da.loc(time=slice('2020-01-01', '2020-01-10'))`: Select a subset of the data by time +- `da.sel(time='2020-01-01')`: Select a subset of the data by time (single timestamp) +- `da.sel(time=slice('2020-01-01', '2020-01-10'))`: Select a subset of the data by time +- `da.isel(time=0)`: Select a subset of the data by time (by index) +- `da.isel(time=range(0,5))`: Select a subset of the data by time (by index) +- `da.plot()`: Plot the data +- `da.to_dataframe()`: Convert the data to a pandas.DataFrame +- `da.to_netcdf()`: Save the data to a netcdf file + +### Syntax + +```python +result = calculation_results[result_label] + +Where: +- `calculation_results` is a [`CalculationResults`][flixopt.results.CalculationResults] instance +- `result_label` is a string representing the label of the result +- The returned `result` is a pandas.DataFrame +## Accessing Component Results + +The [`CalculationResults`][flixopt.results.CalculationResults] object provides dictionary-like access to individual component results. You can access any component's results by subscripting the object with the component's label. + +### Syntax + +```python +component_result = calculation_results[component_label] +``` + +Where: +- `calculation_results` is a [`CalculationResults`][flixopt.results.CalculationResults] instance +- `component_label` is a string representing the label of the component +- The returned `component_result` is a [`ComponentResults`][flixopt.results.ComponentResults] object + +- The same goes for buses and effects, with corresponding return types. + +### Example + +```python +boiler_results = calculation_results['Boiler'] + +# You can also access nested components in hierarchical models +chp_turbine_results = calculation_results['CHP.Turbine'] +``` + +### Return Value + +The subscript operation returns a [`ComponentResult`][flixopt.results.ComponentResult] object that contains: +- Time series data for all component variables +- Metadata about the component's operation +- Component-specific performance metrics + +### Additional Methods + +You can chain this with other methods to extract specific information: + +```python +# Get the power output time series of a generator +generator_power = calculation_results['Generator'].get_variable('power_output') + +# Get the efficiency of a boiler +boiler_efficiency = calculation_results['Boiler'].get_metric('efficiency') +``` + +### Error Handling + +If the component label doesn't exist, a `KeyError` is raised: + +```python +try: + missing_component = calculation_results['NonExistentComponent'] +except KeyError as e: + print(f"Component not found: {e}") +``` + +### See Also +- [`CalculationResults.get_component()`][flixopt.results.CalculationResults.get_component] - Alternative method for accessing components +- [`ComponentResult`][flixopt.results.ComponentResult] - Documentation for the returned component result object + +## Extracting Results for specific Scenarios + +If the calculation was run with scenarios, the results can be filtered by scenario using the `scenario` keyword argument. + + + + +`calculation.results['Storage'].node_balance().isel(scenario=0, drop=True).to_pandas()` \ No newline at end of file From 75bb6beb75b1277986e425d74d9b7a0d8f69781e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 13 Apr 2025 18:12:05 +0200 Subject: [PATCH 03/14] Improve results docs --- docs/user-guide/Analyze Results/index.md | 243 ++++++++++++++++------- 1 file changed, 168 insertions(+), 75 deletions(-) diff --git a/docs/user-guide/Analyze Results/index.md b/docs/user-guide/Analyze Results/index.md index 3221c5f7e..34fb3d7c7 100644 --- a/docs/user-guide/Analyze Results/index.md +++ b/docs/user-guide/Analyze Results/index.md @@ -1,121 +1,214 @@ -# Results +# Working with FlixOpt Results -The results of the optimization are stored in the `results` attribute of the [`Calculation`][flixopt.calculation.Calculation] object. -Depending on the type of calculation, the results are stored in different formats. For both [`FullCalculation`][flixopt.calculation.FullCalculation] and [`AggregatedCalculation`][flixopt.calculation.AggregatedCalculation], the results are stored in a [`CalculationResults`][flixopt.results.CalculationResults] object. -This object can be saved to a file and reloaded later. The used flow system is also stored in the results in the form of a xarray.Dataset. A proper FlowSystem can be reconstructed from the dataset using the [`FlowSystem.from_dataset`][flixopt.flow_system.FlowSystem.from_dataset] method. +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. -## General Data handling +## Result Objects -The results object provides a dictionary-like access to the results of the calculation. You can access any result by subscripting the object with the result's label. -The solution of the optimization is stored in the `solution` attribute of the results object as an [xarray.Dataset](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html). -This dataset contains all solutions for the variables of the optimization. -As the dataset can become a bit large, it is recommended to pass it to the [`fx.results.filter_dataset()`][flixopt.results.filter_dataset] function to select only the variables of interest. -There you can filter out variables that dont have a certain dimension, select a subset of the timesteps or **drop trailing nan values**. -### Dataset handling -Further, here are some of the most commonly used methods to process a dataset: +Depending on the calculation type, the results are stored in different formats: -- `solution.sel(time=slice('2020-01-01', '2020-01-10'))`: Select a subset of the solution by time -- `solution.isel(time=0)`: Select a subset of the solution by time (by index) -- `solution.sum('time')`: Sum the solution over all timesteps (take care that you might need to multiply by the timesteps_per_hour to get the actual flow_hours) -- `solution.to_dataframe()`: Convert the solution to a pandas.DataFrame (leads to Multiindexes) -- `solution.to_pandas()`: Convert the solution to a pandas.DataFrame or pandas.Series, depending on the number of dimensions -- `solution.resample('D').sum()`: Resample the solution to daily timesteps and sum the values +- 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 -For more information on how to use xarray, please refer to the [xarray documentation](https://docs.xarray.dev/en/stable/). +These result objects can be saved to files and reloaded later for further analysis. + +## Accessing Results + +The results object provides dictionary-like access to components, buses, and effects: -Instead ```python -results: fx.CalculationResults -da: xarray.Dataarray = results['Boiler(Q_th)|flow_rate'] +# Get results for a specific component +boiler_results = calculation_results['Boiler'] + +# Get results for a specific bus +electricity_bus_results = calculation_results['ElectricityBus'] + +# Get results for a specific effect +costs_results = calculation_results['Costs'] ``` -From there you have all the functionality of an [xarray.DataArray](https://docs.xarray.dev/en/stable/generated/xarray.DataArray.html). -Here are some of the most commonly used methods: +Each of these results is an instance of [`ComponentResults`][flixopt.results.ComponentResults], [`BusResults`][flixopt.results.BusResults], or [`EffectResults`][flixopt.results.EffectResults] respectively, providing specialized methods for each type. -- `da[2]` or `da[2:5]`: Select a subset of the data by index (time) -- `da.loc(time=slice('2020-01-01', '2020-01-10'))`: Select a subset of the data by time -- `da.sel(time='2020-01-01')`: Select a subset of the data by time (single timestamp) -- `da.sel(time=slice('2020-01-01', '2020-01-10'))`: Select a subset of the data by time -- `da.isel(time=0)`: Select a subset of the data by time (by index) -- `da.isel(time=range(0,5))`: Select a subset of the data by time (by index) -- `da.plot()`: Plot the data -- `da.to_dataframe()`: Convert the data to a pandas.DataFrame -- `da.to_netcdf()`: Save the data to a netcdf file +## Working with the Solution Dataset -### Syntax +The core of the results is the `solution` attribute, which is an [xarray.Dataset](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html) containing all variables from the optimization. ```python -result = calculation_results[result_label] +# Access the complete solution dataset +solution = calculation_results.solution + +# Access a specific variable directly +flow_rate = calculation_results.solution['Boiler(Q_th)|flow_rate'] -Where: -- `calculation_results` is a [`CalculationResults`][flixopt.results.CalculationResults] instance -- `result_label` is a string representing the label of the result -- The returned `result` is a pandas.DataFrame -## Accessing Component Results +# Or more conveniently through the component +flow_rate = calculation_results['Boiler'].solution['Boiler(Q_th)|flow_rate'] +``` -The [`CalculationResults`][flixopt.results.CalculationResults] object provides dictionary-like access to individual component results. You can access any component's results by subscripting the object with the component's label. +### Filtering the Solution -### Syntax +The solution dataset can become large with many variables. Use the [`filter_solution`][flixopt.results.CalculationResults.filter_solution] method to select only variables of interest: ```python -component_result = calculation_results[component_label] +# Get only time-dependent variables +time_vars = calculation_results.filter_solution(variable_dims='time') + +# Get only scalar variables +scalar_vars = calculation_results.filter_solution(variable_dims='scalar') + +# Filter for a specific component and time range +boiler_jan_2022 = calculation_results.filter_solution( + element='Boiler', + timesteps=pd.date_range('2020-01-01 00:00', '2020-01-01 04:00', freq='H') +) + ``` -Where: -- `calculation_results` is a [`CalculationResults`][flixopt.results.CalculationResults] instance -- `component_label` is a string representing the label of the component -- The returned `component_result` is a [`ComponentResults`][flixopt.results.ComponentResults] object +### Common xarray Operations + +The solution dataset supports all xarray functionality: + +```python +solution: xarray.DataSet = calculation_results.solution +# Select a time range +solution_jan = solution.sel(time=slice('2020-01-01', '2020-01-31')) +solution_3_steps = solution.isel(time=slice(0, 3)) -- The same goes for buses and effects, with corresponding return types. +solution_time_x = solution.sel(time='2020-01-01') -### Example +# Sum over time dimension +total_by_var = solution.sum('time') + +# Resample to daily values +daily_sums = solution.resample(time='D').sum() + +# Convert to pandas DataFrame +solution_df = solution.to_dataframe() # Or solution.to_pandas() to not convert without a multiindex +``` + +## Component Results + +The [`ComponentResults`][flixopt.results.ComponentResults] class provides specialized methods for analyzing component behavior: ```python -boiler_results = calculation_results['Boiler'] +# Get a component's results +storage = calculation_results['Storage'] -# You can also access nested components in hierarchical models -chp_turbine_results = calculation_results['CHP.Turbine'] +# Plot the node balance (inputs and outputs) of a component +storage.plot_node_balance(save='storage_balance.html') + +# For storage components, plot the charge state +storage.plot_charge_state(show=True) + +# Get the node balance as a dataset +balance = storage.node_balance(negate_inputs=True) ``` -### Return Value +### Working with Storage Results + +Storage components have additional methods: -The subscript operation returns a [`ComponentResult`][flixopt.results.ComponentResult] object that contains: -- Time series data for all component variables -- Metadata about the component's operation -- Component-specific performance metrics +```python +# Check if a component is a storage +is_storage = calculation_results['Battery'].is_storage + +# Get the charge state of a storage +charge_state = calculation_results['Battery'].charge_state -### Additional Methods +# Get node balance including charge state +balance_with_charge = calculation_results['Battery'].node_balance_with_charge_state() +``` -You can chain this with other methods to extract specific information: +## Bus Results + +The [`BusResults`][flixopt.results.BusResults] class provides methods for analyzing energy or material flows through buses: + +```python +# Get a bus's results +heat_bus = calculation_results['Fernwärme'] + +# Plot the node balance of a bus +heat_bus.plot_node_balance(show=True) + +# Show a pie chart of flows through the bus +heat_bus.plot_node_balance_pie(lower_percentage_group=2, show=True) +``` + +## Effect Results + +The [`EffectResults`][flixopt.results.EffectResults] class helps analyze effects like costs or emissions: + +```python +# Get an effect's results +costs = calculation_results['Costs'] + +# Get the shares of an effect from a specific element +boiler_costs = costs.get_shares_from('Boiler') +``` + +## Working with Scenarios + +If your calculation included scenarios, you can access scenario-specific results: ```python -# Get the power output time series of a generator -generator_power = calculation_results['Generator'].get_variable('power_output') +# Select specific scenario when plotting +storage.plot_charge_state(scenario='high_demand') + +# Filter node balance for a specific scenario +balance = storage.node_balance() +scenario_balance = balance.sel(scenario='high_demand') + +# View results for a single variable as a DataFrame, with columns represensting scenarios +df_flow_rate = solution['Storage(Q_th)|flow_rate'].to_pandas() +``` + +If plotting without specifiing a scenario, the first scenario is used. -# Get the efficiency of a boiler -boiler_efficiency = calculation_results['Boiler'].get_metric('efficiency') +## Visualization + +The results objects provide several visualization methods: + +```python +# Plot a heatmap of a variable +calculation_results.plot_heatmap( + variable_name='Boiler(Q_th)|flow_rate', + heatmap_timeframes='D', + heatmap_timesteps_per_frame='h', + show=True +) + +# Plot the network graph +calculation_results.plot_network(show=True) ``` -### Error Handling +## Saving and Loading Results -If the component label doesn't exist, a `KeyError` is raised: +Results can be saved to files and loaded later: ```python -try: - missing_component = calculation_results['NonExistentComponent'] -except KeyError as e: - print(f"Component not found: {e}") +# Save results to files +calculation_results.to_file(folder='results', compression=5) + +# Load results from files +loaded_results = fx.results.CalculationResults.from_file('results', 'optimization_run') ``` -### See Also -- [`CalculationResults.get_component()`][flixopt.results.CalculationResults.get_component] - Alternative method for accessing components -- [`ComponentResult`][flixopt.results.ComponentResult] - Documentation for the returned component result object +## Converting to Other Formats -## Extracting Results for specific Scenarios +Results can be converted to various formats for further analysis: -If the calculation was run with scenarios, the results can be filtered by scenario using the `scenario` keyword argument. +```python +# Convert to pandas DataFrame +df = calculation_results['Boiler'].node_balance().to_dataframe() +# Save as CSV +df.to_csv('boiler_results.csv') +# Convert flow rates to flow_hours (kW to kWh) - Multiply rate by duration +flow_hours = calculation_results['Boiler'].node_balance(mode='flow_hours') +``` +## Tips for Working with Large Datasets -`calculation.results['Storage'].node_balance().isel(scenario=0, drop=True).to_pandas()` \ No newline at end of file +- Use `filter_solution()` to limit the variables you're working with +- Select only the time range you need with `sel(time=slice(start, end))` +- Consider using `isel()` instead of `sel()` for faster indexing by position +- For aggregated views, use `resample()` to reduce the data size \ No newline at end of file From 1407e99c711ebc0c33097a5d5bf514d1c6198a23 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:05:21 +0200 Subject: [PATCH 04/14] Start release notes --- docs/release-notes/v2.2.0.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/v2.2.0.md b/docs/release-notes/v2.2.0.md index 3cf7eef8d..239ac9de2 100644 --- a/docs/release-notes/v2.2.0.md +++ b/docs/release-notes/v2.2.0.md @@ -25,10 +25,15 @@ This enables the following use cases: 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. +### Results +* The results now contain a dedicated `FlowResults` class for each Flow. +* dedicated dataarrays for flow_rates, flow_hours, and sizes of flows are availlable. THey can be filtered by start, and and component +* Effects per component can now be easily evaluated through a dedicated dataarray. -## Other new features +### 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 +* Plotting styles can now be changed for all plots. (stacked_bar, line, area) +* ## Improvements @@ -47,7 +52,8 @@ This might occur when scenarios represent years or months, while an investment d ## Deprecations -* Feature X will be removed in v{next_version} +* Renamed `Calculation.active_timesteps` to `Calculation.selected_timesteps` +* ## Dependencies From aad83b5be6dc01f5d6584bed46b241e5601c628d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 8 May 2025 19:09:03 +0200 Subject: [PATCH 05/14] Update effects_per_component() to have a filter option by component. And update filter_dataarray_by_coord to also accept xr.Datasets --- flixopt/results.py | 51 ++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index 41c98be15..382a0da79 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -307,7 +307,10 @@ 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). Args: @@ -320,7 +323,8 @@ 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] + filters = {'component': component} if component is not None else {} + return filter_by_coord(self._effects_per_component[mode], **filters) def flow_rates( self, @@ -351,7 +355,7 @@ def flow_rates( dim=pd.Index(self.flows.keys(), name='flow')) ).rename('flow_rates') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} - return filter_dataarray_by_coord(self._flow_rates, **filters) + return filter_by_coord(self._flow_rates, **filters) def flow_hours( self, @@ -383,7 +387,7 @@ def flow_hours( if self._flow_hours is None: self._flow_hours = (self.flow_rates() * self.hours_per_timestep).rename('flow_hours') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} - return filter_dataarray_by_coord(self._flow_hours, **filters) + return filter_by_coord(self._flow_hours, **filters) def sizes( self, @@ -410,7 +414,7 @@ def sizes( dim=pd.Index(self.flows.keys(), name='flow')) ).rename('flow_sizes') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} - return filter_dataarray_by_coord(self._sizes, **filters) + return filter_by_coord(self._sizes, **filters) def _assign_flow_coords(self, da: xr.DataArray): # Add start and end coordinates @@ -1534,28 +1538,23 @@ def filter_dataset( return filtered_ds -def filter_dataarray_by_coord( - da: xr.DataArray, - **kwargs: Optional[Union[str, List[str]]] -) -> xr.DataArray: - """Filter flows by node and component attributes. +def filter_by_coord( + data: Union[xr.DataArray, xr.Dataset], **kwargs: Optional[Union[str, List[str]]] +) -> Union[xr.DataArray, xr.Dataset]: + """Filter xarray object by coordinate values. - Filters are applied in the order they are specified. All filters must match for an edge to be included. - - To recombine filtered dataarrays, use `xr.concat`. - - xr.concat([res.sizes(start='Fernwärme'), res.sizes(end='Fernwärme')], dim='flow') + Filters are applied in the order they are specified. All filters must match for elements to be included. Args: - da: Flow DataArray with network metadata coordinates. + data: DataArray or Dataset with metadata coordinates. **kwargs: Coord filters as name=value pairs. Returns: - Filtered DataArray with matching edges. + Filtered DataArray or Dataset with matching elements. Raises: AttributeError: If required coordinates are missing. - ValueError: If specified nodes don't exist or no matches found. + ValueError: If specified values don't exist or no matches found. """ # Helper function to process filters def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): @@ -1570,7 +1569,7 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): available = set(array[coord_name].values) missing = [v for v in val_list if v not in available] if missing: - raise ValueError(f"{coord_name.title()} value(s) not found: {missing}") + raise ValueError(f'{coord_name.title()} value(s) not found: {missing}') # Apply filter return array.where( @@ -1580,14 +1579,18 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): # Apply filters from kwargs filters = {k: v for k, v in kwargs.items() if v is not None} + result = data + try: for coord, values in filters.items(): - da = apply_filter(da, coord, values) + result = apply_filter(result, coord, values) except ValueError as e: - raise ValueError(f"No edges match criteria: {filters}") from e + raise ValueError(f'No elements match criteria: {filters}') from e # Verify results exist - if da.size == 0: - raise ValueError(f"No edges match criteria: {filters}") + if isinstance(result, xr.DataArray) and result.size == 0: + raise ValueError(f'No elements match criteria: {filters}') + elif isinstance(result, xr.Dataset) and all(v.size == 0 for v in result.values()): + raise ValueError(f'No elements match criteria: {filters}') - return da + return result From 084245f33dc59e8b76ae4afd084b214f83d7abee Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 8 May 2025 19:28:23 +0200 Subject: [PATCH 06/14] Improving docs --- docs/user-guide/Analyze Results/index.md | 131 ++++++++++++++++++++++- docs/user-guide/index.md | 2 +- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/docs/user-guide/Analyze Results/index.md b/docs/user-guide/Analyze Results/index.md index 34fb3d7c7..dcc6dc61d 100644 --- a/docs/user-guide/Analyze Results/index.md +++ b/docs/user-guide/Analyze Results/index.md @@ -13,7 +13,132 @@ These result objects can be saved to files and reloaded later for further analys ## Accessing Results -The results object provides dictionary-like access to components, buses, and effects: +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) From 277c2bd72da27257680d78871b47a34b22bd57fd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 8 May 2025 19:29:38 +0200 Subject: [PATCH 07/14] Add filter for component in effects_per_component --- flixopt/results.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flixopt/results.py b/flixopt/results.py index 382a0da79..cec1e1596 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -323,6 +323,8 @@ def effects_per_component( raise ValueError(f'Invalid mode {mode}') if self._effects_per_component[mode] is None: self._effects_per_component[mode] = self._create_effects_dataset(mode) + if component is not None: + return self._effects_per_component[mode].sel(component=component, drop=True) filters = {'component': component} if component is not None else {} return filter_by_coord(self._effects_per_component[mode], **filters) From b2ec0db8fa43c08fdb0242d108261547e522426f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 8 May 2025 19:31:28 +0200 Subject: [PATCH 08/14] Revert "Update effects_per_component() to have a filter option by component. And update filter_dataarray_by_coord to also accept xr.Datasets" This reverts commit aad83b5b --- flixopt/results.py | 49 +++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index cec1e1596..5f71580ff 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -323,10 +323,10 @@ def effects_per_component( raise ValueError(f'Invalid mode {mode}') if self._effects_per_component[mode] is None: self._effects_per_component[mode] = self._create_effects_dataset(mode) + ds = self._effects_per_component[mode] if component is not None: - return self._effects_per_component[mode].sel(component=component, drop=True) - filters = {'component': component} if component is not None else {} - return filter_by_coord(self._effects_per_component[mode], **filters) + return ds.sel(component=component, drop=True) + return ds def flow_rates( self, @@ -357,7 +357,7 @@ def flow_rates( dim=pd.Index(self.flows.keys(), name='flow')) ).rename('flow_rates') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} - return filter_by_coord(self._flow_rates, **filters) + return filter_dataarray_by_coord(self._flow_rates, **filters) def flow_hours( self, @@ -389,7 +389,7 @@ def flow_hours( if self._flow_hours is None: self._flow_hours = (self.flow_rates() * self.hours_per_timestep).rename('flow_hours') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} - return filter_by_coord(self._flow_hours, **filters) + return filter_dataarray_by_coord(self._flow_hours, **filters) def sizes( self, @@ -416,7 +416,7 @@ def sizes( dim=pd.Index(self.flows.keys(), name='flow')) ).rename('flow_sizes') filters = {k: v for k, v in {'start': start, 'end': end, 'component': component}.items() if v is not None} - return filter_by_coord(self._sizes, **filters) + return filter_dataarray_by_coord(self._sizes, **filters) def _assign_flow_coords(self, da: xr.DataArray): # Add start and end coordinates @@ -1540,23 +1540,28 @@ def filter_dataset( return filtered_ds -def filter_by_coord( - data: Union[xr.DataArray, xr.Dataset], **kwargs: Optional[Union[str, List[str]]] -) -> Union[xr.DataArray, xr.Dataset]: - """Filter xarray object by coordinate values. +def filter_dataarray_by_coord( + da: xr.DataArray, + **kwargs: Optional[Union[str, List[str]]] +) -> xr.DataArray: + """Filter flows by node and component attributes. + + Filters are applied in the order they are specified. All filters must match for an edge to be included. - Filters are applied in the order they are specified. All filters must match for elements to be included. + To recombine filtered dataarrays, use `xr.concat`. + + xr.concat([res.sizes(start='Fernwärme'), res.sizes(end='Fernwärme')], dim='flow') Args: - data: DataArray or Dataset with metadata coordinates. + da: Flow DataArray with network metadata coordinates. **kwargs: Coord filters as name=value pairs. Returns: - Filtered DataArray or Dataset with matching elements. + Filtered DataArray with matching edges. Raises: AttributeError: If required coordinates are missing. - ValueError: If specified values don't exist or no matches found. + ValueError: If specified nodes don't exist or no matches found. """ # Helper function to process filters def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): @@ -1571,7 +1576,7 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): available = set(array[coord_name].values) missing = [v for v in val_list if v not in available] if missing: - raise ValueError(f'{coord_name.title()} value(s) not found: {missing}') + raise ValueError(f"{coord_name.title()} value(s) not found: {missing}") # Apply filter return array.where( @@ -1581,18 +1586,14 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]): # Apply filters from kwargs filters = {k: v for k, v in kwargs.items() if v is not None} - result = data - try: for coord, values in filters.items(): - result = apply_filter(result, coord, values) + da = apply_filter(da, coord, values) except ValueError as e: - raise ValueError(f'No elements match criteria: {filters}') from e + raise ValueError(f"No edges match criteria: {filters}") from e # Verify results exist - if isinstance(result, xr.DataArray) and result.size == 0: - raise ValueError(f'No elements match criteria: {filters}') - elif isinstance(result, xr.Dataset) and all(v.size == 0 for v in result.values()): - raise ValueError(f'No elements match criteria: {filters}') + if da.size == 0: + raise ValueError(f"No edges match criteria: {filters}") - return result + return da From 329686f9f53c6b5747f0d0c4a9ef20b9a8b8d190 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 8 May 2025 19:33:15 +0200 Subject: [PATCH 09/14] Improve effects_per_component --- flixopt/results.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flixopt/results.py b/flixopt/results.py index 5f71580ff..ffa598294 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -308,13 +308,16 @@ def filter_solution( ) def effects_per_component( - self, - mode: Literal['operation', 'invest', 'total'] = 'total', - component: Optional[Union[str, List[str]]] = None) -> xr.Dataset: + 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. From afc0497270e728048956008d815a823e7fb7e603 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 14 Jun 2025 17:46:28 +0200 Subject: [PATCH 10/14] Moved release notes to CHANGELOG.md --- CHANGELOG.md | 11 +++++++ docs/release-notes/v2.2.0.md | 61 ------------------------------------ 2 files changed, 11 insertions(+), 61 deletions(-) delete mode 100644 docs/release-notes/v2.2.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d692d5e5..aeb839293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +* **Scenarios:** A big addition to flixopt: Scenarios let you model **Uncertainties** or **Multi-Period Transformations** in a single model. +* **Results:** The CalculationResults now contain a dedicated `FlowResults` object for each Flow. +* **Results:** dedicated xr.DataArrays for flow_rates, flow_hours, and sizes of flows are accessible through the CalculationResults +* **Results:** Effects per component can now be easily evaluated through a dedicated xr.DataArrays. +* Balanced storage - Storage charging and discharging sizes can now be forced to be equal when optimizing their size by choosing `balanced=True`. +* Plotting styles can now be changed for all plots. (stacked_bar, line, area) + +### Deprecations +* Renamed `Calculation.active_timesteps` to `Calculation.selected_timesteps` + ## [2.1.2] - 2025-06-14 ### Fixed diff --git a/docs/release-notes/v2.2.0.md b/docs/release-notes/v2.2.0.md deleted file mode 100644 index 239ac9de2..000000000 --- a/docs/release-notes/v2.2.0.md +++ /dev/null @@ -1,61 +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. - -### Results -* The results now contain a dedicated `FlowResults` class for each Flow. -* dedicated dataarrays for flow_rates, flow_hours, and sizes of flows are availlable. THey can be filtered by start, and and component -* Effects per component can now be easily evaluated through a dedicated dataarray. - -### Other new features -* Balanced storage - Storage charging and discharging sizes can now be forced to be equal in when optimizing their size. -* Plotting styles can now be changed for all plots. (stacked_bar, line, area) -* - -## 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 - -* Renamed `Calculation.active_timesteps` to `Calculation.selected_timesteps` -* - -## Dependencies - -* Added dependency X v1.2.3 -* Updated dependency Y to v2.0.0 \ No newline at end of file From 4246e6589d723e049a8a519674cdaf597ecceaa9 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:09:17 +0200 Subject: [PATCH 11/14] Add release notes --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb839293..089d99584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +* Fixed formating issues in the yaml model documentation (line breaks) + +### Breaking Changes: +* Removed `kind` in favor of `style` in plotting functions. +* Renamed `TimeSeries.active_data` to `TimeSeries.selected_data` +* `CalculationResults.flow_system` now returns the restorded FlowSystem instead of the `xr.Dataset`. The data can be found under `flow_system_data`. + ### Added -* **Scenarios:** A big addition to flixopt: Scenarios let you model **Uncertainties** or **Multi-Period Transformations** in a single model. -* **Results:** The CalculationResults now contain a dedicated `FlowResults` object for each Flow. -* **Results:** dedicated xr.DataArrays for flow_rates, flow_hours, and sizes of flows are accessible through the CalculationResults -* **Results:** Effects per component can now be easily evaluated through a dedicated xr.DataArrays. +* **Scenarios:** + * Scenarios can now be used to model uncertainties in the flow system, such as: + * Scenarios are passed to a `FlowSystem`. The total objective effect of each scenario is multiplied by a `scenario_weight`. This forms the objective of the optimization. +* **`CalculationResults`:** + * New dedicated `FlowResults`. + * Dedicated xr.DataArrays for all **flow_rates**, **flow_hours**, and **sizes** of flows are availlable. + * Use `effects_per_component()` to retrieve all effects results for every Component. This includes indirect effects that hove their origin in an element, but are inflicted by another effect (ElementA --> CO2 --> Costs)) * Balanced storage - Storage charging and discharging sizes can now be forced to be equal when optimizing their size by choosing `balanced=True`. * Plotting styles can now be changed for all plots. (stacked_bar, line, area) +* Added plotting style `grouped_bar` +* Support for pandas.Series and pandas.DataFrame when setting parameters of Elements (internally converted to xarray.DataArrays) + +### Changed +* Improved internal Datatypes to make needed data format more obvious: `Scalar` for only scalar values, `TimestepData` for time-indexed data (which might have a scenario dimension), `ScenarioData` for data with a scenario dimension. +* `InvestmentParameters` now have a `investment_scenarios` parameter to define which scenarios to define how to optimize the size across scenarios +* Changed legend location in plots ### Deprecations * Renamed `Calculation.active_timesteps` to `Calculation.selected_timesteps` +* A warning is raised if Results prior ti this version are loaded, as this prevents the FLowResults from being created. + +### Known Issues +* Scenarios are not yet supported in `AggregatedCalculation` and `SegmentedCalculation` + +### Development +* Greatly improved testing by directly asserting for the correctness of the created equations and variables (and their bounds). ## [2.1.2] - 2025-06-14 From eebc894a16a89a05814f52e415dfee525763e54c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:10:08 +0200 Subject: [PATCH 12/14] Added docstring --- flixopt/core.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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) From bdcfa619710107d317d64d3d9f030597e75d50f4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:24:17 +0200 Subject: [PATCH 13/14] Improved CHANGELOG readibility --- CHANGELOG.md | 160 +++++++++++++++++++++++++++------------------------ 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089d99584..5ec7a187b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,117 +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] -### Fixed -* Fixed formating issues in the yaml model documentation (line breaks) - -### Breaking Changes: -* Removed `kind` in favor of `style` in plotting functions. -* Renamed `TimeSeries.active_data` to `TimeSeries.selected_data` -* `CalculationResults.flow_system` now returns the restorded FlowSystem instead of the `xr.Dataset`. The data can be found under `flow_system_data`. +### 🚨 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 -* **Scenarios:** - * Scenarios can now be used to model uncertainties in the flow system, such as: - * Scenarios are passed to a `FlowSystem`. The total objective effect of each scenario is multiplied by a `scenario_weight`. This forms the objective of the optimization. -* **`CalculationResults`:** - * New dedicated `FlowResults`. - * Dedicated xr.DataArrays for all **flow_rates**, **flow_hours**, and **sizes** of flows are availlable. - * Use `effects_per_component()` to retrieve all effects results for every Component. This includes indirect effects that hove their origin in an element, but are inflicted by another effect (ElementA --> CO2 --> Costs)) -* Balanced storage - Storage charging and discharging sizes can now be forced to be equal when optimizing their size by choosing `balanced=True`. -* Plotting styles can now be changed for all plots. (stacked_bar, line, area) -* Added plotting style `grouped_bar` -* Support for pandas.Series and pandas.DataFrame when setting parameters of Elements (internally converted to xarray.DataArrays) +#### 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. -### Changed -* Improved internal Datatypes to make needed data format more obvious: `Scalar` for only scalar values, `TimestepData` for time-indexed data (which might have a scenario dimension), `ScenarioData` for data with a scenario dimension. -* `InvestmentParameters` now have a `investment_scenarios` parameter to define which scenarios to define how to optimize the size across scenarios -* Changed legend location in plots - -### Deprecations -* Renamed `Calculation.active_timesteps` to `Calculation.selected_timesteps` -* A warning is raised if Results prior ti this version are loaded, as this prevents the FLowResults from being created. +### Fixed +* Fixed formatting issues in YAML model documentation (line breaks) ### Known Issues * Scenarios are not yet supported in `AggregatedCalculation` and `SegmentedCalculation` -### Development -* Greatly improved testing by directly asserting for the correctness of the created equations and variables (and their bounds). - ## [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}}$ +* **🐛 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 `_ElementResults.constraints` returning variables instead of constraints ### Changed -- Improved docstring and tests +* Improved docstrings and tests ## [2.1.0] - 2025-04-11 +### 🚨 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 + ### 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 +* **Python 3.13 support** +* Enhanced testing infrastructure leveraging linopy's testing framework +* Logger warnings for `relative_minimum` usage without `on_off_parameters` in Flow ### 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) - -### 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 `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 -- Replace "|" with "__" in filenames when saving figures (Windows compatibility) -- Fixed bug that prevented the load factor from working without InvestmentParameters +* **Windows Compatibility**: Replace "|" with "__" in figure filenames +* Fixed load factor functionality 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 +### 🚨 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 -- 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 +#### 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 size through improved compression +* **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) -- Period concepts in time management (simplified to timesteps) \ No newline at end of file +- **🚨 BREAKING**: Pyomo dependency (replaced by linopy) +- **🚨 BREAKING**: Period concepts in time management (simplified to timesteps) From dae8b27069647afaebbe0862b43a329e1ef6e092 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:31:59 +0200 Subject: [PATCH 14/14] Add emojis --- CHANGELOG.md | 64 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec7a187b..7e0b2d71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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` +### 💥 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 +### ✨ Added #### Major Features * **Scenarios**: Model uncertainties or **Multi-Period Transformations** * Scenarios are passed to a `FlowSystem` with `scenario_weight` multipliers @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **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 +* **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) @@ -37,11 +37,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `grouped_bar` plotting style * Changed default legend location in plots (now on the right side) -### Deprecated +### 🗑️ 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 * Fixed formatting issues in YAML model documentation (line breaks) ### Known Issues @@ -49,8 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.1.2] - 2025-06-14 -### Fixed -* **🐛 Critical Fix**: Storage losses per hour calculation corrected (thanks @brokenwings01) +### 🐛 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` @@ -60,7 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.1.1] - 2025-05-08 -### Fixed +### 🐛 Fixed * Fixed `_ElementResults.constraints` returning variables instead of constraints ### Changed @@ -68,43 +68,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.1.0] - 2025-04-11 -### 🚨 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` +### 💥 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 -### Added +### ✨ 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 -### Fixed +### 🐛 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 -### Fixed +### 🐛 Fixed * **Windows Compatibility**: Replace "|" with "__" in figure filenames * Fixed load factor functionality without InvestmentParameters -### Added +### ✨ Added * Logger warning for `relative_minimum` usage without `on_off_parameters` in Flow ## [2.0.0] - 2025-03-29 -### 🚨 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) +### 💥 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 +### ✨ Added #### Major Features * **Full model serialization**: Save and restore unsolved Models * **Enhanced model documentation**: YAML export with human-readable mathematical formulations @@ -120,11 +120,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `to_netcdf/from_netcdf` methods for FlowSystem and core components * Google Style Docstrings throughout codebase -### Fixed +### 🐛 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) +### 🔥 Removed +* **BREAKING**: Pyomo dependency (replaced by linopy) +* **BREAKING**: Period concepts in time management (simplified to timesteps) \ No newline at end of file