flixopt 2.2.0rc2__py3-none-any.whl → 3.0.0__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.

Potentially problematic release.


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

Files changed (58) hide show
  1. flixopt/__init__.py +33 -4
  2. flixopt/aggregation.py +60 -80
  3. flixopt/calculation.py +395 -178
  4. flixopt/commons.py +1 -10
  5. flixopt/components.py +939 -448
  6. flixopt/config.py +553 -191
  7. flixopt/core.py +513 -846
  8. flixopt/effects.py +644 -178
  9. flixopt/elements.py +610 -355
  10. flixopt/features.py +394 -966
  11. flixopt/flow_system.py +736 -219
  12. flixopt/interface.py +1104 -302
  13. flixopt/io.py +103 -79
  14. flixopt/linear_converters.py +387 -95
  15. flixopt/modeling.py +759 -0
  16. flixopt/network_app.py +73 -39
  17. flixopt/plotting.py +294 -138
  18. flixopt/results.py +1253 -299
  19. flixopt/solvers.py +25 -21
  20. flixopt/structure.py +938 -396
  21. flixopt/utils.py +38 -12
  22. flixopt-3.0.0.dist-info/METADATA +209 -0
  23. flixopt-3.0.0.dist-info/RECORD +26 -0
  24. flixopt-3.0.0.dist-info/top_level.txt +1 -0
  25. docs/examples/00-Minimal Example.md +0 -5
  26. docs/examples/01-Basic Example.md +0 -5
  27. docs/examples/02-Complex Example.md +0 -10
  28. docs/examples/03-Calculation Modes.md +0 -5
  29. docs/examples/index.md +0 -5
  30. docs/faq/contribute.md +0 -61
  31. docs/faq/index.md +0 -3
  32. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  33. docs/images/architecture_flixOpt.png +0 -0
  34. docs/images/flixopt-icon.svg +0 -1
  35. docs/javascripts/mathjax.js +0 -18
  36. docs/user-guide/Mathematical Notation/Bus.md +0 -33
  37. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  38. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  39. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  40. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  41. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  42. docs/user-guide/Mathematical Notation/index.md +0 -22
  43. docs/user-guide/Mathematical Notation/others.md +0 -3
  44. docs/user-guide/index.md +0 -124
  45. flixopt/config.yaml +0 -10
  46. flixopt-2.2.0rc2.dist-info/METADATA +0 -167
  47. flixopt-2.2.0rc2.dist-info/RECORD +0 -54
  48. flixopt-2.2.0rc2.dist-info/top_level.txt +0 -5
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixOpt_plotting.jpg +0 -0
  52. pics/flixopt-icon.svg +0 -1
  53. pics/pics.pptx +0 -0
  54. scripts/extract_release_notes.py +0 -45
  55. scripts/gen_ref_pages.py +0 -54
  56. tests/ressources/Zeitreihen2020.csv +0 -35137
  57. {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/WHEEL +0 -0
  58. {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.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 Dict, Literal, Optional, Tuple, Union
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
- from .core import TimeSeries
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 = _process_complex_strings(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 any string with newlines
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 to ensure proper parsing
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=yaml.SafeDumper,
78
+ Dumper=CustomDumper,
106
79
  sort_keys=False, # Preserve dictionary order
107
80
  default_flow_style=False, # Use block style for mappings
108
- width=float('inf'), # Don't wrap long lines
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 _process_complex_strings(data):
87
+ def _normalize_complex_data(data):
114
88
  """
115
- Process dictionary data recursively with comprehensive string normalization.
116
- Handles various types of strings and special formatting.
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: The data to process (dict, list, str, or other)
95
+ data: Any data type (dict, list, str, or primitive)
120
96
 
121
97
  Returns:
122
- Processed data with normalized strings
98
+ Data with all strings normalized according to defined rules
123
99
  """
124
100
  if isinstance(data, dict):
125
- return {k: _process_complex_strings(v) for k, v in data.items()}
101
+ return {key: _normalize_complex_data(value) for key, value in data.items()}
102
+
126
103
  elif isinstance(data, list):
127
- return [_process_complex_strings(item) for item in data]
104
+ return [_normalize_complex_data(item) for item in data]
105
+
128
106
  elif isinstance(data, str):
129
- # Step 1: Normalize line endings to \n
130
- normalized = data.replace('\r\n', '\n').replace('\r', '\n')
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
- # Step 2: Handle escaped newlines with robust regex
133
- normalized = re.sub(r'(?<!\\)\\n', '\n', normalized)
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
- # Step 3: Handle unnecessary double backslashes
136
- normalized = re.sub(r'\\\\(n)', r'\\\1', normalized)
126
+ # Convert escaped newlines to actual newlines (avoiding double-backslashes)
127
+ text = re.sub(r'(?<!\\)\\n', '\n', text)
137
128
 
138
- # Step 4: Ensure proper formatting of "[time: N]:\n---------"
139
- normalized = re.sub(r'(\[time: \d+\]):\s*\\?n', r'\1:\n', normalized)
129
+ # Normalize double backslashes before specific escape sequences
130
+ text = re.sub(r'\\\\([rtn])', r'\\\1', text)
140
131
 
141
- # Step 5: Ensure "Constraint `...`" patterns are properly formatted
142
- normalized = re.sub(r'Constraint `([^`]+)`\\?n', r'Constraint `\1`\n', normalized)
132
+ # Standardize constraint headers format
133
+ text = re.sub(r'Constraint\s*`([^`]+)`\s*(?:\\n|[\s\n]*)', r'Constraint `\1`\n', text)
143
134
 
144
- return normalized
145
- else:
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
- def document_linopy_model(model: linopy.Model, path: pathlib.Path = None) -> Dict[str, str]:
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: Union[str, pathlib.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 the attrs as a json string in the 'attrs' attribute.
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('netCDF4') is not None:
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 "netcdf4".'
229
- 'Install netcdf4 via `pip install netcdf4`.'
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': 5} for data_var in ds.data_vars},
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: Union[str, pathlib.Path]) -> xr.Dataset:
251
+ def load_dataset_from_netcdf(path: str | pathlib.Path) -> xr.Dataset:
242
252
  """
243
- Load a dataset from a netcdf file. Load the attrs from the 'attrs' attribute.
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
- ds.attrs = json.loads(ds.attrs['attrs'])
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) -> Dict[str, pathlib.Path]:
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: Optional[str] = None, new_folder: Optional[pathlib.Path] = None) -> None:
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