flixopt 2.2.0rc2__py3-none-any.whl → 3.0.1__py3-none-any.whl
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.
- flixopt/__init__.py +33 -4
- flixopt/aggregation.py +60 -80
- flixopt/calculation.py +403 -182
- flixopt/commons.py +1 -10
- flixopt/components.py +939 -448
- flixopt/config.py +553 -191
- flixopt/core.py +513 -846
- flixopt/effects.py +644 -178
- flixopt/elements.py +610 -355
- flixopt/features.py +394 -966
- flixopt/flow_system.py +736 -219
- flixopt/interface.py +1104 -302
- flixopt/io.py +103 -79
- flixopt/linear_converters.py +387 -95
- flixopt/modeling.py +757 -0
- flixopt/network_app.py +73 -39
- flixopt/plotting.py +294 -138
- flixopt/results.py +1254 -300
- flixopt/solvers.py +25 -21
- flixopt/structure.py +938 -396
- flixopt/utils.py +36 -12
- flixopt-3.0.1.dist-info/METADATA +209 -0
- flixopt-3.0.1.dist-info/RECORD +26 -0
- flixopt-3.0.1.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -61
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.2.0rc2.dist-info/METADATA +0 -167
- flixopt-2.2.0rc2.dist-info/RECORD +0 -54
- flixopt-2.2.0rc2.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/extract_release_notes.py +0 -45
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.1.dist-info}/WHEEL +0 -0
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.1.dist-info}/licenses/LICENSE +0 -0
flixopt/io.py
CHANGED
|
@@ -1,56 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import importlib.util
|
|
2
4
|
import json
|
|
3
5
|
import logging
|
|
4
6
|
import pathlib
|
|
5
7
|
import re
|
|
6
8
|
from dataclasses import dataclass
|
|
7
|
-
from typing import
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
10
|
|
|
9
|
-
import linopy
|
|
10
11
|
import xarray as xr
|
|
11
12
|
import yaml
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import linopy
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger('flixopt')
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
def replace_timeseries(obj, mode: Literal['name', 'stats', 'data'] = 'name'):
|
|
19
|
-
"""Recursively replaces TimeSeries objects with their names prefixed by '::::'."""
|
|
20
|
-
if isinstance(obj, dict):
|
|
21
|
-
return {k: replace_timeseries(v, mode) for k, v in obj.items()}
|
|
22
|
-
elif isinstance(obj, list):
|
|
23
|
-
return [replace_timeseries(v, mode) for v in obj]
|
|
24
|
-
elif isinstance(obj, TimeSeries): # Adjust this based on the actual class
|
|
25
|
-
if obj.all_equal:
|
|
26
|
-
return obj.active_data.values[0].item()
|
|
27
|
-
elif mode == 'name':
|
|
28
|
-
return f'::::{obj.name}'
|
|
29
|
-
elif mode == 'stats':
|
|
30
|
-
return obj.stats
|
|
31
|
-
elif mode == 'data':
|
|
32
|
-
return obj
|
|
33
|
-
else:
|
|
34
|
-
raise ValueError(f'Invalid mode {mode}')
|
|
35
|
-
else:
|
|
36
|
-
return obj
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def insert_dataarray(obj, ds: xr.Dataset):
|
|
40
|
-
"""Recursively inserts TimeSeries objects into a dataset."""
|
|
41
|
-
if isinstance(obj, dict):
|
|
42
|
-
return {k: insert_dataarray(v, ds) for k, v in obj.items()}
|
|
43
|
-
elif isinstance(obj, list):
|
|
44
|
-
return [insert_dataarray(v, ds) for v in obj]
|
|
45
|
-
elif isinstance(obj, str) and obj.startswith('::::'):
|
|
46
|
-
da = ds[obj[4:]]
|
|
47
|
-
if da.isel(time=-1).isnull():
|
|
48
|
-
return da.isel(time=slice(0, -1))
|
|
49
|
-
return da
|
|
50
|
-
else:
|
|
51
|
-
return obj
|
|
52
|
-
|
|
53
|
-
|
|
54
20
|
def remove_none_and_empty(obj):
|
|
55
21
|
"""Recursively removes None and empty dicts and lists values from a dictionary or list."""
|
|
56
22
|
|
|
@@ -79,15 +45,17 @@ def _save_to_yaml(data, output_file='formatted_output.yaml'):
|
|
|
79
45
|
output_file (str): Path to output YAML file
|
|
80
46
|
"""
|
|
81
47
|
# Process strings to normalize all newlines and handle special patterns
|
|
82
|
-
processed_data =
|
|
48
|
+
processed_data = _normalize_complex_data(data)
|
|
83
49
|
|
|
84
50
|
# Define a custom representer for strings
|
|
85
51
|
def represent_str(dumper, data):
|
|
86
|
-
# Use literal block style (|) for
|
|
52
|
+
# Use literal block style (|) for multi-line strings
|
|
87
53
|
if '\n' in data:
|
|
54
|
+
# Clean up formatting for literal block style
|
|
55
|
+
data = data.strip() # Remove leading/trailing whitespace
|
|
88
56
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
|
|
89
57
|
|
|
90
|
-
# Use quoted style for strings with special characters
|
|
58
|
+
# Use quoted style for strings with special characters
|
|
91
59
|
elif any(char in data for char in ':`{}[]#,&*!|>%@'):
|
|
92
60
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='"')
|
|
93
61
|
|
|
@@ -97,56 +65,83 @@ def _save_to_yaml(data, output_file='formatted_output.yaml'):
|
|
|
97
65
|
# Add the string representer to SafeDumper
|
|
98
66
|
yaml.add_representer(str, represent_str, Dumper=yaml.SafeDumper)
|
|
99
67
|
|
|
68
|
+
# Configure dumper options for better formatting
|
|
69
|
+
class CustomDumper(yaml.SafeDumper):
|
|
70
|
+
def increase_indent(self, flow=False, indentless=False):
|
|
71
|
+
return super().increase_indent(flow, False)
|
|
72
|
+
|
|
100
73
|
# Write to file with settings that ensure proper formatting
|
|
101
74
|
with open(output_file, 'w', encoding='utf-8') as file:
|
|
102
75
|
yaml.dump(
|
|
103
76
|
processed_data,
|
|
104
77
|
file,
|
|
105
|
-
Dumper=
|
|
78
|
+
Dumper=CustomDumper,
|
|
106
79
|
sort_keys=False, # Preserve dictionary order
|
|
107
80
|
default_flow_style=False, # Use block style for mappings
|
|
108
|
-
width=
|
|
81
|
+
width=1000, # Set a reasonable line width
|
|
109
82
|
allow_unicode=True, # Support Unicode characters
|
|
83
|
+
indent=2, # Set consistent indentation
|
|
110
84
|
)
|
|
111
85
|
|
|
112
86
|
|
|
113
|
-
def
|
|
87
|
+
def _normalize_complex_data(data):
|
|
114
88
|
"""
|
|
115
|
-
|
|
116
|
-
|
|
89
|
+
Recursively normalize strings in complex data structures.
|
|
90
|
+
|
|
91
|
+
Handles dictionaries, lists, and strings, applying various text normalization
|
|
92
|
+
rules while preserving important formatting elements.
|
|
117
93
|
|
|
118
94
|
Args:
|
|
119
|
-
data:
|
|
95
|
+
data: Any data type (dict, list, str, or primitive)
|
|
120
96
|
|
|
121
97
|
Returns:
|
|
122
|
-
|
|
98
|
+
Data with all strings normalized according to defined rules
|
|
123
99
|
"""
|
|
124
100
|
if isinstance(data, dict):
|
|
125
|
-
return {
|
|
101
|
+
return {key: _normalize_complex_data(value) for key, value in data.items()}
|
|
102
|
+
|
|
126
103
|
elif isinstance(data, list):
|
|
127
|
-
return [
|
|
104
|
+
return [_normalize_complex_data(item) for item in data]
|
|
105
|
+
|
|
128
106
|
elif isinstance(data, str):
|
|
129
|
-
|
|
130
|
-
|
|
107
|
+
return _normalize_string_content(data)
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _normalize_string_content(text):
|
|
114
|
+
"""
|
|
115
|
+
Apply comprehensive string normalization rules.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
text: The string to normalize
|
|
131
119
|
|
|
132
|
-
|
|
133
|
-
|
|
120
|
+
Returns:
|
|
121
|
+
Normalized string with standardized formatting
|
|
122
|
+
"""
|
|
123
|
+
# Standardize line endings
|
|
124
|
+
text = text.replace('\r\n', '\n').replace('\r', '\n')
|
|
134
125
|
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
# Convert escaped newlines to actual newlines (avoiding double-backslashes)
|
|
127
|
+
text = re.sub(r'(?<!\\)\\n', '\n', text)
|
|
137
128
|
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
# Normalize double backslashes before specific escape sequences
|
|
130
|
+
text = re.sub(r'\\\\([rtn])', r'\\\1', text)
|
|
140
131
|
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
# Standardize constraint headers format
|
|
133
|
+
text = re.sub(r'Constraint\s*`([^`]+)`\s*(?:\\n|[\s\n]*)', r'Constraint `\1`\n', text)
|
|
143
134
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return data
|
|
135
|
+
# Clean up ellipsis patterns
|
|
136
|
+
text = re.sub(r'[\t ]*(\.\.\.)', r'\1', text)
|
|
147
137
|
|
|
138
|
+
# Limit consecutive newlines (max 2)
|
|
139
|
+
text = re.sub(r'\n{3,}', '\n\n', text)
|
|
148
140
|
|
|
149
|
-
|
|
141
|
+
return text.strip()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def document_linopy_model(model: linopy.Model, path: pathlib.Path | None = None) -> dict[str, str]:
|
|
150
145
|
"""
|
|
151
146
|
Convert all model variables and constraints to a structured string representation.
|
|
152
147
|
This can take multiple seconds for large models.
|
|
@@ -195,18 +190,19 @@ def document_linopy_model(model: linopy.Model, path: pathlib.Path = None) -> Dic
|
|
|
195
190
|
if path is not None:
|
|
196
191
|
if path.suffix not in ['.yaml', '.yml']:
|
|
197
192
|
raise ValueError(f'Invalid file extension for path {path}. Only .yaml and .yml are supported')
|
|
198
|
-
_save_to_yaml(documentation, path)
|
|
193
|
+
_save_to_yaml(documentation, str(path))
|
|
199
194
|
|
|
200
195
|
return documentation
|
|
201
196
|
|
|
202
197
|
|
|
203
198
|
def save_dataset_to_netcdf(
|
|
204
199
|
ds: xr.Dataset,
|
|
205
|
-
path:
|
|
200
|
+
path: str | pathlib.Path,
|
|
206
201
|
compression: int = 0,
|
|
202
|
+
engine: Literal['netcdf4', 'scipy', 'h5netcdf'] = 'h5netcdf',
|
|
207
203
|
) -> None:
|
|
208
204
|
"""
|
|
209
|
-
Save a dataset to a netcdf file. Store
|
|
205
|
+
Save a dataset to a netcdf file. Store all attrs as JSON strings in 'attrs' attributes.
|
|
210
206
|
|
|
211
207
|
Args:
|
|
212
208
|
ds: Dataset to save.
|
|
@@ -216,40 +212,68 @@ def save_dataset_to_netcdf(
|
|
|
216
212
|
Raises:
|
|
217
213
|
ValueError: If the path has an invalid file extension.
|
|
218
214
|
"""
|
|
215
|
+
path = pathlib.Path(path)
|
|
219
216
|
if path.suffix not in ['.nc', '.nc4']:
|
|
220
217
|
raise ValueError(f'Invalid file extension for path {path}. Only .nc and .nc4 are supported')
|
|
221
218
|
|
|
222
219
|
apply_encoding = False
|
|
223
220
|
if compression != 0:
|
|
224
|
-
if importlib.util.find_spec(
|
|
221
|
+
if importlib.util.find_spec(engine) is not None:
|
|
225
222
|
apply_encoding = True
|
|
226
223
|
else:
|
|
227
224
|
logger.warning(
|
|
228
|
-
'Dataset was exported without compression due to missing dependency "
|
|
229
|
-
'Install
|
|
225
|
+
f'Dataset was exported without compression due to missing dependency "{engine}".'
|
|
226
|
+
f'Install {engine} via `pip install {engine}`.'
|
|
230
227
|
)
|
|
228
|
+
|
|
231
229
|
ds = ds.copy(deep=True)
|
|
232
230
|
ds.attrs = {'attrs': json.dumps(ds.attrs)}
|
|
231
|
+
|
|
232
|
+
# Convert all DataArray attrs to JSON strings
|
|
233
|
+
for var_name, data_var in ds.data_vars.items():
|
|
234
|
+
if data_var.attrs: # Only if there are attrs
|
|
235
|
+
ds[var_name].attrs = {'attrs': json.dumps(data_var.attrs)}
|
|
236
|
+
|
|
237
|
+
# Also handle coordinate attrs if they exist
|
|
238
|
+
for coord_name, coord_var in ds.coords.items():
|
|
239
|
+
if hasattr(coord_var, 'attrs') and coord_var.attrs:
|
|
240
|
+
ds[coord_name].attrs = {'attrs': json.dumps(coord_var.attrs)}
|
|
241
|
+
|
|
233
242
|
ds.to_netcdf(
|
|
234
243
|
path,
|
|
235
244
|
encoding=None
|
|
236
245
|
if not apply_encoding
|
|
237
|
-
else {data_var: {'zlib': True, 'complevel':
|
|
246
|
+
else {data_var: {'zlib': True, 'complevel': compression} for data_var in ds.data_vars},
|
|
247
|
+
engine=engine,
|
|
238
248
|
)
|
|
239
249
|
|
|
240
250
|
|
|
241
|
-
def load_dataset_from_netcdf(path:
|
|
251
|
+
def load_dataset_from_netcdf(path: str | pathlib.Path) -> xr.Dataset:
|
|
242
252
|
"""
|
|
243
|
-
Load a dataset from a netcdf file. Load
|
|
253
|
+
Load a dataset from a netcdf file. Load all attrs from 'attrs' attributes.
|
|
244
254
|
|
|
245
255
|
Args:
|
|
246
256
|
path: Path to load the dataset from.
|
|
247
257
|
|
|
248
258
|
Returns:
|
|
249
|
-
Dataset: Loaded dataset.
|
|
259
|
+
Dataset: Loaded dataset with restored attrs.
|
|
250
260
|
"""
|
|
251
|
-
ds = xr.load_dataset(path)
|
|
252
|
-
|
|
261
|
+
ds = xr.load_dataset(str(path), engine='h5netcdf')
|
|
262
|
+
|
|
263
|
+
# Restore Dataset attrs
|
|
264
|
+
if 'attrs' in ds.attrs:
|
|
265
|
+
ds.attrs = json.loads(ds.attrs['attrs'])
|
|
266
|
+
|
|
267
|
+
# Restore DataArray attrs
|
|
268
|
+
for var_name, data_var in ds.data_vars.items():
|
|
269
|
+
if 'attrs' in data_var.attrs:
|
|
270
|
+
ds[var_name].attrs = json.loads(data_var.attrs['attrs'])
|
|
271
|
+
|
|
272
|
+
# Restore coordinate attrs
|
|
273
|
+
for coord_name, coord_var in ds.coords.items():
|
|
274
|
+
if hasattr(coord_var, 'attrs') and 'attrs' in coord_var.attrs:
|
|
275
|
+
ds[coord_name].attrs = json.loads(coord_var.attrs['attrs'])
|
|
276
|
+
|
|
253
277
|
return ds
|
|
254
278
|
|
|
255
279
|
|
|
@@ -273,7 +297,7 @@ class CalculationResultsPaths:
|
|
|
273
297
|
self.flow_system = self.folder / f'{self.name}--flow_system.nc4'
|
|
274
298
|
self.model_documentation = self.folder / f'{self.name}--model_documentation.yaml'
|
|
275
299
|
|
|
276
|
-
def all_paths(self) ->
|
|
300
|
+
def all_paths(self) -> dict[str, pathlib.Path]:
|
|
277
301
|
"""Return a dictionary of all paths."""
|
|
278
302
|
return {
|
|
279
303
|
'linopy_model': self.linopy_model,
|
|
@@ -297,7 +321,7 @@ class CalculationResultsPaths:
|
|
|
297
321
|
f'Folder {self.folder} and its parent do not exist. Please create them first.'
|
|
298
322
|
) from e
|
|
299
323
|
|
|
300
|
-
def update(self, new_name:
|
|
324
|
+
def update(self, new_name: str | None = None, new_folder: pathlib.Path | None = None) -> None:
|
|
301
325
|
"""Update name and/or folder and refresh all paths."""
|
|
302
326
|
if new_name is not None:
|
|
303
327
|
self.name = new_name
|