flixopt 3.1.1__tar.gz → 3.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flixopt might be problematic. Click here for more details.

Files changed (34) hide show
  1. {flixopt-3.1.1 → flixopt-3.2.1}/CHANGELOG.md +73 -1
  2. {flixopt-3.1.1/flixopt.egg-info → flixopt-3.2.1}/PKG-INFO +3 -3
  3. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/aggregation.py +13 -4
  4. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/calculation.py +2 -3
  5. flixopt-3.2.1/flixopt/color_processing.py +261 -0
  6. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/config.py +59 -4
  7. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/flow_system.py +15 -5
  8. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/interface.py +2 -1
  9. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/io.py +239 -22
  10. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/plotting.py +583 -789
  11. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/results.py +445 -56
  12. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/structure.py +1 -3
  13. {flixopt-3.1.1 → flixopt-3.2.1/flixopt.egg-info}/PKG-INFO +3 -3
  14. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt.egg-info/SOURCES.txt +1 -1
  15. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt.egg-info/requires.txt +2 -2
  16. {flixopt-3.1.1 → flixopt-3.2.1}/pyproject.toml +2 -2
  17. flixopt-3.1.1/flixopt/utils.py +0 -86
  18. {flixopt-3.1.1 → flixopt-3.2.1}/LICENSE +0 -0
  19. {flixopt-3.1.1 → flixopt-3.2.1}/MANIFEST.in +0 -0
  20. {flixopt-3.1.1 → flixopt-3.2.1}/README.md +0 -0
  21. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/__init__.py +0 -0
  22. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/commons.py +0 -0
  23. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/components.py +0 -0
  24. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/core.py +0 -0
  25. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/effects.py +0 -0
  26. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/elements.py +0 -0
  27. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/features.py +0 -0
  28. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/linear_converters.py +0 -0
  29. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/modeling.py +0 -0
  30. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/network_app.py +0 -0
  31. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt/solvers.py +0 -0
  32. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt.egg-info/dependency_links.txt +0 -0
  33. {flixopt-3.1.1 → flixopt-3.2.1}/flixopt.egg-info/top_level.txt +0 -0
  34. {flixopt-3.1.1 → flixopt-3.2.1}/setup.cfg +0 -0
@@ -21,8 +21,9 @@ Please keep the format of the changelog consistent with the other releases, so t
21
21
 
22
22
  ## [Template] - ????-??-??
23
23
 
24
- If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
24
+ **Summary**:
25
25
 
26
+ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
26
27
 
27
28
  ### ✨ Added
28
29
 
@@ -50,6 +51,8 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
50
51
 
51
52
  ## [Unreleased] - ????-??-??
52
53
 
54
+ **Summary**:
55
+
53
56
  If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
54
57
 
55
58
  ### ✨ Added
@@ -78,6 +81,75 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
78
81
 
79
82
  Until here -->
80
83
 
84
+ ## [3.2.1] - 2025-10-29
85
+
86
+ **Summary**:
87
+
88
+ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
89
+
90
+ ### 🐛 Fixed
91
+ - Fixed resampling of FlowSystem to reset `hours_of_last_timestep` and `hours_of_previous_timesteps` properly
92
+
93
+ ### 👷 Development
94
+ - Improved issue templates
95
+
96
+ ---
97
+
98
+ ## [3.2.0] - 2025-10-26
99
+
100
+ **Summary**: Enhanced plotting capabilities with consistent color management, custom plotting kwargs support, and centralized I/O handling.
101
+
102
+ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
103
+
104
+ ### ✨ Added
105
+
106
+ **Color management:**
107
+ - **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots
108
+ - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})`
109
+ - Automatically propagates to all segments in segmented calculations
110
+ - Colors persist across all plot calls unless explicitly overridden
111
+ - **Flexible color inputs**: Supports colorscale names (e.g., 'turbo', 'plasma'), color lists, or label-to-color dictionaries
112
+ - **Cross-backend compatibility**: Seamless color handling for both Plotly and Matplotlib
113
+
114
+ **Plotting customization:**
115
+ - **Plotting kwargs support**: Pass additional arguments to plotting backends via `px_kwargs`, `plot_kwargs`, and `backend_kwargs` parameters
116
+ - **New `CONFIG.Plotting` configuration section**:
117
+ - `default_show`: Control default plot visibility
118
+ - `default_engine`: Choose 'plotly' or 'matplotlib'
119
+ - `default_dpi`: Set resolution for saved plots
120
+ - `default_facet_cols`: Configure default faceting columns
121
+ - `default_sequential_colorscale`: Default for heatmaps (now 'turbo')
122
+ - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly')
123
+
124
+ **I/O improvements:**
125
+ - Centralized JSON/YAML I/O with auto-format detection
126
+ - Enhanced NetCDF handling with consistent engine usage
127
+ - Better numeric formatting in YAML exports
128
+
129
+ ### ♻️ Changed
130
+ - **Default colorscale**: Changed from 'viridis' to 'turbo' for better perceptual uniformity
131
+ - **Color terminology**: Standardized from "colormap" to "colorscale" throughout for Plotly consistency
132
+ - **Plotting internals**: Now use `xr.Dataset` as primary data type (DataFrames automatically converted)
133
+ - **NetCDF engine**: Switched back to netcdf4 engine following xarray updates and performance benchmarks
134
+
135
+ ### 🔥 Removed
136
+ - Removed unused `plotting.pie_with_plotly()` method
137
+
138
+ ### 🐛 Fixed
139
+ - Improved error messages when using `engine='matplotlib'` with multidimensional data
140
+ - Better dimension validation in `results.plot_heatmap()`
141
+
142
+ ### 📝 Docs
143
+ - Enhanced examples demonstrating `setup_colors()` usage
144
+ - Updated terminology from "colormap" to "colorscale" in docstrings
145
+
146
+ ### 👷 Development
147
+ - Fixed concurrency issue in CI
148
+ - Centralized color processing logic into dedicated module
149
+ - Refactored to function-based color handling for simpler API
150
+
151
+ ---
152
+
81
153
  ## [3.1.1] - 2025-10-20
82
154
  **Summary**: Fixed a bug when acessing the `effects_per_component` dataset in results without periodic effects.
83
155
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 3.1.1
3
+ Version: 3.2.1
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -24,7 +24,7 @@ Requires-Dist: numpy<3,>=1.21.5
24
24
  Requires-Dist: pandas<3,>=2.0.0
25
25
  Requires-Dist: xarray<2026.0,>=2024.2.0
26
26
  Requires-Dist: linopy<0.6,>=0.5.1
27
- Requires-Dist: h5netcdf<2,>=1.0.0
27
+ Requires-Dist: netcdf4<2,>=1.6.1
28
28
  Requires-Dist: pyyaml<7,>=6.0.0
29
29
  Requires-Dist: rich<15,>=13.0.0
30
30
  Requires-Dist: tomli<3,>=2.0.1; python_version < "3.11"
@@ -67,7 +67,7 @@ Requires-Dist: networkx==3.0.0; extra == "dev"
67
67
  Requires-Dist: werkzeug==3.0.0; extra == "dev"
68
68
  Provides-Extra: docs
69
69
  Requires-Dist: mkdocs==1.6.1; extra == "docs"
70
- Requires-Dist: mkdocs-material==9.6.21; extra == "docs"
70
+ Requires-Dist: mkdocs-material==9.6.22; extra == "docs"
71
71
  Requires-Dist: mkdocstrings-python==1.18.2; extra == "docs"
72
72
  Requires-Dist: mkdocs-table-reader-plugin==3.1.0; extra == "docs"
73
73
  Requires-Dist: mkdocs-gen-files==0.5.0; extra == "docs"
@@ -20,7 +20,9 @@ try:
20
20
  except ImportError:
21
21
  TSAM_AVAILABLE = False
22
22
 
23
+ from .color_processing import process_colors
23
24
  from .components import Storage
25
+ from .config import CONFIG
24
26
  from .structure import (
25
27
  FlowSystemModel,
26
28
  Submodel,
@@ -141,7 +143,7 @@ class Aggregation:
141
143
  def use_extreme_periods(self):
142
144
  return self.time_series_for_high_peaks or self.time_series_for_low_peaks
143
145
 
144
- def plot(self, colormap: str = 'viridis', show: bool = True, save: pathlib.Path | None = None) -> go.Figure:
146
+ def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Path | None = None) -> go.Figure:
145
147
  from . import plotting
146
148
 
147
149
  df_org = self.original_data.copy().rename(
@@ -150,13 +152,20 @@ class Aggregation:
150
152
  df_agg = self.aggregated_data.copy().rename(
151
153
  columns={col: f'Aggregated - {col}' for col in self.aggregated_data.columns}
152
154
  )
153
- fig = plotting.with_plotly(df_org, 'line', colors=colormap)
155
+ colors = list(
156
+ process_colors(colormap or CONFIG.Plotting.default_qualitative_colorscale, list(df_org.columns)).values()
157
+ )
158
+ fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colors, xlabel='Time in h')
154
159
  for trace in fig.data:
155
160
  trace.update(dict(line=dict(dash='dash')))
156
- fig = plotting.with_plotly(df_agg, 'line', colors=colormap, fig=fig)
161
+ fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colors, xlabel='Time in h')
162
+ for trace in fig2.data:
163
+ fig.add_trace(trace)
157
164
 
158
165
  fig.update_layout(
159
- title='Original vs Aggregated Data (original = ---)', xaxis_title='Index', yaxis_title='Value'
166
+ title='Original vs Aggregated Data (original = ---)',
167
+ xaxis_title='Time in h',
168
+ yaxis_title='Value',
160
169
  )
161
170
 
162
171
  plotting.export_figure(
@@ -22,7 +22,6 @@ import numpy as np
22
22
  import yaml
23
23
 
24
24
  from . import io as fx_io
25
- from . import utils as utils
26
25
  from .aggregation import Aggregation, AggregationModel, AggregationParameters
27
26
  from .components import Storage
28
27
  from .config import CONFIG
@@ -144,7 +143,7 @@ class Calculation:
144
143
  ],
145
144
  }
146
145
 
147
- return utils.round_nested_floats(main_results)
146
+ return fx_io.round_nested_floats(main_results)
148
147
 
149
148
  @property
150
149
  def summary(self):
@@ -253,7 +252,7 @@ class FullCalculation(Calculation):
253
252
  logger.info(
254
253
  f'{" Main Results ":#^80}\n'
255
254
  + yaml.dump(
256
- utils.round_nested_floats(self.main_results),
255
+ self.main_results,
257
256
  default_flow_style=False,
258
257
  sort_keys=False,
259
258
  allow_unicode=True,
@@ -0,0 +1,261 @@
1
+ """Simplified color handling for visualization.
2
+
3
+ This module provides clean color processing that transforms various input formats
4
+ into a label-to-color mapping dictionary, without needing to know about the plotting engine.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ import matplotlib.colors as mcolors
12
+ import matplotlib.pyplot as plt
13
+ import plotly.express as px
14
+ from plotly.exceptions import PlotlyError
15
+
16
+ logger = logging.getLogger('flixopt')
17
+
18
+
19
+ def _rgb_string_to_hex(color: str) -> str:
20
+ """Convert Plotly RGB/RGBA string format to hex.
21
+
22
+ Args:
23
+ color: Color in format 'rgb(R, G, B)', 'rgba(R, G, B, A)' or already in hex
24
+
25
+ Returns:
26
+ Color in hex format '#RRGGBB'
27
+ """
28
+ color = color.strip()
29
+
30
+ # If already hex, return as-is
31
+ if color.startswith('#'):
32
+ return color
33
+
34
+ # Try to parse rgb() or rgba()
35
+ try:
36
+ if color.startswith('rgb('):
37
+ # Extract RGB values from 'rgb(R, G, B)' format
38
+ rgb_str = color[4:-1] # Remove 'rgb(' and ')'
39
+ elif color.startswith('rgba('):
40
+ # Extract RGBA values from 'rgba(R, G, B, A)' format
41
+ rgb_str = color[5:-1] # Remove 'rgba(' and ')'
42
+ else:
43
+ return color
44
+
45
+ # Split on commas and parse first three components
46
+ components = rgb_str.split(',')
47
+ if len(components) < 3:
48
+ return color
49
+
50
+ # Parse and clamp the first three components
51
+ r = max(0, min(255, int(round(float(components[0].strip())))))
52
+ g = max(0, min(255, int(round(float(components[1].strip())))))
53
+ b = max(0, min(255, int(round(float(components[2].strip())))))
54
+
55
+ return f'#{r:02x}{g:02x}{b:02x}'
56
+ except (ValueError, IndexError):
57
+ # If parsing fails, return original
58
+ return color
59
+
60
+
61
+ def process_colors(
62
+ colors: None | str | list[str] | dict[str, str],
63
+ labels: list[str],
64
+ default_colorscale: str = 'turbo',
65
+ ) -> dict[str, str]:
66
+ """Process color input and return a label-to-color mapping.
67
+
68
+ This function takes flexible color input and always returns a dictionary
69
+ mapping each label to a specific color string. The plotting engine can then
70
+ use this mapping as needed.
71
+
72
+ Args:
73
+ colors: Color specification in one of four formats:
74
+ - None: Use the default colorscale
75
+ - str: Name of a colorscale (e.g., 'turbo', 'plasma', 'Set1', 'portland')
76
+ - list[str]: List of color strings (hex, named colors, etc.)
77
+ - dict[str, str]: Direct label-to-color mapping
78
+ labels: List of labels that need colors assigned
79
+ default_colorscale: Fallback colorscale name if requested scale not found
80
+
81
+ Returns:
82
+ Dictionary mapping each label to a color string
83
+
84
+ Examples:
85
+ >>> # Using None - applies default colorscale
86
+ >>> process_colors(None, ['A', 'B', 'C'])
87
+ {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'}
88
+
89
+ >>> # Using a colorscale name
90
+ >>> process_colors('plasma', ['A', 'B', 'C'])
91
+ {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'}
92
+
93
+ >>> # Using a list of colors
94
+ >>> process_colors(['red', 'blue', 'green'], ['A', 'B', 'C'])
95
+ {'A': 'red', 'B': 'blue', 'C': 'green'}
96
+
97
+ >>> # Using a pre-made mapping
98
+ >>> process_colors({'A': 'red', 'B': 'blue'}, ['A', 'B', 'C'])
99
+ {'A': 'red', 'B': 'blue', 'C': '#0d0887'} # C gets color from default scale
100
+ """
101
+ if not labels:
102
+ return {}
103
+
104
+ # Case 1: Already a mapping dictionary
105
+ if isinstance(colors, dict):
106
+ return _fill_missing_colors(colors, labels, default_colorscale)
107
+
108
+ # Case 2: None or colorscale name (string)
109
+ if colors is None or isinstance(colors, str):
110
+ colorscale_name = colors if colors is not None else default_colorscale
111
+ color_list = _get_colors_from_scale(colorscale_name, len(labels), default_colorscale)
112
+ return dict(zip(labels, color_list, strict=False))
113
+
114
+ # Case 3: List of colors
115
+ if isinstance(colors, list):
116
+ if len(colors) == 0:
117
+ logger.warning(f'Empty color list provided. Using {default_colorscale} instead.')
118
+ color_list = _get_colors_from_scale(default_colorscale, len(labels), default_colorscale)
119
+ return dict(zip(labels, color_list, strict=False))
120
+
121
+ if len(colors) < len(labels):
122
+ logger.debug(
123
+ f'Not enough colors provided ({len(colors)}) for all labels ({len(labels)}). Colors will cycle.'
124
+ )
125
+
126
+ # Cycle through colors if we don't have enough
127
+ return {label: colors[i % len(colors)] for i, label in enumerate(labels)}
128
+
129
+ raise TypeError(f'colors must be None, str, list, or dict, got {type(colors)}')
130
+
131
+
132
+ def _fill_missing_colors(
133
+ color_mapping: dict[str, str],
134
+ labels: list[str],
135
+ default_colorscale: str,
136
+ ) -> dict[str, str]:
137
+ """Fill in missing labels in a color mapping using a colorscale.
138
+
139
+ Args:
140
+ color_mapping: Partial label-to-color mapping
141
+ labels: All labels that need colors
142
+ default_colorscale: Colorscale to use for missing labels
143
+
144
+ Returns:
145
+ Complete label-to-color mapping
146
+ """
147
+ missing_labels = [label for label in labels if label not in color_mapping]
148
+
149
+ if not missing_labels:
150
+ return color_mapping.copy()
151
+
152
+ # Log warning about missing labels
153
+ logger.debug(f'Labels missing colors: {missing_labels}. Using {default_colorscale} for these.')
154
+
155
+ # Get colors for missing labels
156
+ missing_colors = _get_colors_from_scale(default_colorscale, len(missing_labels), default_colorscale)
157
+
158
+ # Combine existing and new colors
159
+ result = color_mapping.copy()
160
+ result.update(dict(zip(missing_labels, missing_colors, strict=False)))
161
+ return result
162
+
163
+
164
+ def _get_colors_from_scale(
165
+ colorscale_name: str,
166
+ num_colors: int,
167
+ fallback_scale: str,
168
+ ) -> list[str]:
169
+ """Extract a list of colors from a named colorscale.
170
+
171
+ Tries to get colors from the named scale (Plotly first, then Matplotlib),
172
+ falls back to the fallback scale if not found.
173
+
174
+ Args:
175
+ colorscale_name: Name of the colorscale to try
176
+ num_colors: Number of colors needed
177
+ fallback_scale: Fallback colorscale name if first fails
178
+
179
+ Returns:
180
+ List of color strings (hex format)
181
+ """
182
+ # Try to get the requested colorscale
183
+ colors = _try_get_colorscale(colorscale_name, num_colors)
184
+
185
+ if colors is not None:
186
+ return colors
187
+
188
+ # Fallback to default
189
+ logger.warning(f"Colorscale '{colorscale_name}' not found. Using '{fallback_scale}' instead.")
190
+
191
+ colors = _try_get_colorscale(fallback_scale, num_colors)
192
+
193
+ if colors is not None:
194
+ return colors
195
+
196
+ # Ultimate fallback: just use basic colors
197
+ logger.warning(f"Fallback colorscale '{fallback_scale}' also not found. Using basic colors.")
198
+ basic_colors = [
199
+ '#1f77b4',
200
+ '#ff7f0e',
201
+ '#2ca02c',
202
+ '#d62728',
203
+ '#9467bd',
204
+ '#8c564b',
205
+ '#e377c2',
206
+ '#7f7f7f',
207
+ '#bcbd22',
208
+ '#17becf',
209
+ ]
210
+ return [basic_colors[i % len(basic_colors)] for i in range(num_colors)]
211
+
212
+
213
+ def _try_get_colorscale(colorscale_name: str, num_colors: int) -> list[str] | None:
214
+ """Try to get colors from Plotly or Matplotlib colorscales.
215
+
216
+ Tries Plotly colorscales first (both qualitative and sequential),
217
+ then falls back to Matplotlib colorscales.
218
+
219
+ Args:
220
+ colorscale_name: Name of the colorscale
221
+ num_colors: Number of colors needed
222
+
223
+ Returns:
224
+ List of color strings (hex format) if successful, None if colorscale not found
225
+ """
226
+ # First try Plotly qualitative (discrete) color sequences
227
+ colorscale_title = colorscale_name.title()
228
+ if hasattr(px.colors.qualitative, colorscale_title):
229
+ color_list = getattr(px.colors.qualitative, colorscale_title)
230
+ # Convert to hex format for matplotlib compatibility
231
+ return [_rgb_string_to_hex(color_list[i % len(color_list)]) for i in range(num_colors)]
232
+
233
+ # Then try Plotly sequential/continuous colorscales
234
+ try:
235
+ colorscale = px.colors.get_colorscale(colorscale_name)
236
+ # Sample evenly from the colorscale
237
+ if num_colors == 1:
238
+ sample_points = [0.5]
239
+ else:
240
+ sample_points = [i / (num_colors - 1) for i in range(num_colors)]
241
+ colors = px.colors.sample_colorscale(colorscale, sample_points)
242
+ # Convert to hex format for matplotlib compatibility
243
+ return [_rgb_string_to_hex(c) for c in colors]
244
+ except (PlotlyError, ValueError):
245
+ pass
246
+
247
+ # Finally try Matplotlib colorscales
248
+ try:
249
+ cmap = plt.get_cmap(colorscale_name)
250
+
251
+ # Sample evenly from the colorscale
252
+ if num_colors == 1:
253
+ colors = [cmap(0.5)]
254
+ else:
255
+ colors = [cmap(i / (num_colors - 1)) for i in range(num_colors)]
256
+
257
+ # Convert RGBA tuples to hex strings
258
+ return [mcolors.rgb2hex(color[:3]) for color in colors]
259
+
260
+ except (ValueError, KeyError):
261
+ return None
@@ -8,7 +8,6 @@ from pathlib import Path
8
8
  from types import MappingProxyType
9
9
  from typing import Literal
10
10
 
11
- import yaml
12
11
  from rich.console import Console
13
12
  from rich.logging import RichHandler
14
13
  from rich.style import Style
@@ -54,6 +53,16 @@ _DEFAULTS = MappingProxyType(
54
53
  'big_binary_bound': 100_000,
55
54
  }
56
55
  ),
56
+ 'plotting': MappingProxyType(
57
+ {
58
+ 'default_show': True,
59
+ 'default_engine': 'plotly',
60
+ 'default_dpi': 300,
61
+ 'default_facet_cols': 3,
62
+ 'default_sequential_colorscale': 'turbo',
63
+ 'default_qualitative_colorscale': 'plotly',
64
+ }
65
+ ),
57
66
  }
58
67
  )
59
68
 
@@ -185,6 +194,42 @@ class CONFIG:
185
194
  epsilon: float = _DEFAULTS['modeling']['epsilon']
186
195
  big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
187
196
 
197
+ class Plotting:
198
+ """Plotting configuration.
199
+
200
+ Configure backends via environment variables:
201
+ - Matplotlib: Set `MPLBACKEND` environment variable (e.g., 'Agg', 'TkAgg')
202
+ - Plotly: Set `PLOTLY_RENDERER` or use `plotly.io.renderers.default`
203
+
204
+ Attributes:
205
+ default_show: Default value for the `show` parameter in plot methods.
206
+ default_engine: Default plotting engine.
207
+ default_dpi: Default DPI for saved plots.
208
+ default_facet_cols: Default number of columns for faceted plots.
209
+ default_sequential_colorscale: Default colorscale for heatmaps and continuous data.
210
+ default_qualitative_colorscale: Default colormap for categorical plots (bar/line/area charts).
211
+
212
+ Examples:
213
+ ```python
214
+ # Set consistent theming
215
+ CONFIG.Plotting.plotly_template = 'plotly_dark'
216
+ CONFIG.apply()
217
+
218
+ # Configure default export and color settings
219
+ CONFIG.Plotting.default_dpi = 600
220
+ CONFIG.Plotting.default_sequential_colorscale = 'plasma'
221
+ CONFIG.Plotting.default_qualitative_colorscale = 'Dark24'
222
+ CONFIG.apply()
223
+ ```
224
+ """
225
+
226
+ default_show: bool = _DEFAULTS['plotting']['default_show']
227
+ default_engine: Literal['plotly', 'matplotlib'] = _DEFAULTS['plotting']['default_engine']
228
+ default_dpi: int = _DEFAULTS['plotting']['default_dpi']
229
+ default_facet_cols: int = _DEFAULTS['plotting']['default_facet_cols']
230
+ default_sequential_colorscale: str = _DEFAULTS['plotting']['default_sequential_colorscale']
231
+ default_qualitative_colorscale: str = _DEFAULTS['plotting']['default_qualitative_colorscale']
232
+
188
233
  config_name: str = _DEFAULTS['config_name']
189
234
 
190
235
  @classmethod
@@ -253,13 +298,15 @@ class CONFIG:
253
298
  Raises:
254
299
  FileNotFoundError: If the config file does not exist.
255
300
  """
301
+ # Import here to avoid circular import
302
+ from . import io as fx_io
303
+
256
304
  config_path = Path(config_file)
257
305
  if not config_path.exists():
258
306
  raise FileNotFoundError(f'Config file not found: {config_file}')
259
307
 
260
- with config_path.open() as file:
261
- config_dict = yaml.safe_load(file) or {}
262
- cls._apply_config_dict(config_dict)
308
+ config_dict = fx_io.load_yaml(config_path)
309
+ cls._apply_config_dict(config_dict)
263
310
 
264
311
  cls.apply()
265
312
 
@@ -319,6 +366,14 @@ class CONFIG:
319
366
  'epsilon': cls.Modeling.epsilon,
320
367
  'big_binary_bound': cls.Modeling.big_binary_bound,
321
368
  },
369
+ 'plotting': {
370
+ 'default_show': cls.Plotting.default_show,
371
+ 'default_engine': cls.Plotting.default_engine,
372
+ 'default_dpi': cls.Plotting.default_dpi,
373
+ 'default_facet_cols': cls.Plotting.default_facet_cols,
374
+ 'default_sequential_colorscale': cls.Plotting.default_sequential_colorscale,
375
+ 'default_qualitative_colorscale': cls.Plotting.default_qualitative_colorscale,
376
+ },
322
377
  }
323
378
 
324
379
 
@@ -4,7 +4,6 @@ This module contains the FlowSystem class, which is used to collect instances of
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- import json
8
7
  import logging
9
8
  import warnings
10
9
  from typing import TYPE_CHECKING, Any, Literal, Optional
@@ -13,6 +12,7 @@ import numpy as np
13
12
  import pandas as pd
14
13
  import xarray as xr
15
14
 
15
+ from .config import CONFIG
16
16
  from .core import (
17
17
  ConversionError,
18
18
  DataConverter,
@@ -80,7 +80,7 @@ class FlowSystem(Interface):
80
80
  timesteps: pd.DatetimeIndex,
81
81
  periods: pd.Index | None = None,
82
82
  scenarios: pd.Index | None = None,
83
- hours_of_last_timestep: float | None = None,
83
+ hours_of_last_timestep: int | float | None = None,
84
84
  hours_of_previous_timesteps: int | float | np.ndarray | None = None,
85
85
  weights: PeriodicDataUser | None = None,
86
86
  scenario_independent_sizes: bool | list[str] = True,
@@ -484,7 +484,7 @@ class FlowSystem(Interface):
484
484
  | list[
485
485
  Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
486
486
  ] = True,
487
- show: bool = False,
487
+ show: bool | None = None,
488
488
  ) -> pyvis.network.Network | None:
489
489
  """
490
490
  Visualizes the network structure of a FlowSystem using PyVis, saving it as an interactive HTML file.
@@ -514,7 +514,9 @@ class FlowSystem(Interface):
514
514
  from . import plotting
515
515
 
516
516
  node_infos, edge_infos = self.network_infos()
517
- return plotting.plot_network(node_infos, edge_infos, path, controls, show)
517
+ return plotting.plot_network(
518
+ node_infos, edge_infos, path, controls, show if show is not None else CONFIG.Plotting.default_show
519
+ )
518
520
 
519
521
  def start_network_app(self):
520
522
  """Visualizes the network structure of a FlowSystem using Dash, Cytoscape, and networkx.
@@ -927,6 +929,8 @@ class FlowSystem(Interface):
927
929
  self,
928
930
  time: str,
929
931
  method: Literal['mean', 'sum', 'max', 'min', 'first', 'last', 'std', 'var', 'median', 'count'] = 'mean',
932
+ hours_of_last_timestep: int | float | None = None,
933
+ hours_of_previous_timesteps: int | float | np.ndarray | None = None,
930
934
  **kwargs: Any,
931
935
  ) -> FlowSystem:
932
936
  """
@@ -936,10 +940,12 @@ class FlowSystem(Interface):
936
940
  Args:
937
941
  time: Resampling frequency (e.g., '3h', '2D', '1M')
938
942
  method: Resampling method. Recommended: 'mean', 'first', 'last', 'max', 'min'
943
+ hours_of_last_timestep: New duration of the last time step. Defaults to the last time interval of the new timesteps
944
+ hours_of_previous_timesteps: New duration of the previous timestep. Defaults to the first time increment of the new timesteps
939
945
  **kwargs: Additional arguments passed to xarray.resample()
940
946
 
941
947
  Returns:
942
- FlowSystem: New FlowSystem with resampled data
948
+ FlowSystem: New resampled FlowSystem
943
949
  """
944
950
  if not self.connected_and_transformed:
945
951
  self.connect_and_transform()
@@ -973,6 +979,10 @@ class FlowSystem(Interface):
973
979
  else:
974
980
  resampled_dataset = resampled_time_data
975
981
 
982
+ # Let FlowSystem recalculate or use explicitly set value
983
+ resampled_dataset.attrs['hours_of_last_timestep'] = hours_of_last_timestep
984
+ resampled_dataset.attrs['hours_of_previous_timesteps'] = hours_of_previous_timesteps
985
+
976
986
  return self.__class__.from_dataset(resampled_dataset)
977
987
 
978
988
  @property
@@ -712,6 +712,8 @@ class InvestParameters(Interface):
712
712
  Combinable with effects_of_investment and effects_of_investment_per_size.
713
713
  effects_of_retirement: Costs incurred if NOT investing (demolition, penalties).
714
714
  Dict: {'effect_name': value}.
715
+ linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods.
716
+ For convenience, pass a tuple containing the first and last period (2025, 2039), linking them and those in between
715
717
 
716
718
  Deprecated Args:
717
719
  fix_effects: **Deprecated**. Use `effects_of_investment` instead.
@@ -724,7 +726,6 @@ class InvestParameters(Interface):
724
726
  Will be removed in version 4.0.
725
727
  optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`.
726
728
  Will be removed in version 4.0.
727
- linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods.
728
729
 
729
730
  Cost Annualization Requirements:
730
731
  All cost values must be properly weighted to match the optimization model's time horizon.