flixopt 1.0.12__py3-none-any.whl → 2.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.

Potentially problematic release.


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

Files changed (73) hide show
  1. docs/examples/00-Minimal Example.md +5 -0
  2. docs/examples/01-Basic Example.md +5 -0
  3. docs/examples/02-Complex Example.md +10 -0
  4. docs/examples/03-Calculation Modes.md +5 -0
  5. docs/examples/index.md +5 -0
  6. docs/faq/contribute.md +49 -0
  7. docs/faq/index.md +3 -0
  8. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  9. docs/images/architecture_flixOpt.png +0 -0
  10. docs/images/flixopt-icon.svg +1 -0
  11. docs/javascripts/mathjax.js +18 -0
  12. docs/release-notes/_template.txt +32 -0
  13. docs/release-notes/index.md +7 -0
  14. docs/release-notes/v2.0.0.md +93 -0
  15. docs/release-notes/v2.0.1.md +12 -0
  16. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  17. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  18. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  19. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  20. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  21. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  22. docs/user-guide/Mathematical Notation/index.md +22 -0
  23. docs/user-guide/Mathematical Notation/others.md +3 -0
  24. docs/user-guide/index.md +124 -0
  25. {flixOpt → flixopt}/__init__.py +5 -2
  26. {flixOpt → flixopt}/aggregation.py +113 -140
  27. flixopt/calculation.py +455 -0
  28. {flixOpt → flixopt}/commons.py +7 -4
  29. flixopt/components.py +630 -0
  30. {flixOpt → flixopt}/config.py +9 -8
  31. {flixOpt → flixopt}/config.yaml +3 -3
  32. flixopt/core.py +970 -0
  33. flixopt/effects.py +386 -0
  34. flixopt/elements.py +534 -0
  35. flixopt/features.py +1042 -0
  36. flixopt/flow_system.py +409 -0
  37. flixopt/interface.py +265 -0
  38. flixopt/io.py +308 -0
  39. flixopt/linear_converters.py +331 -0
  40. flixopt/plotting.py +1340 -0
  41. flixopt/results.py +898 -0
  42. flixopt/solvers.py +77 -0
  43. flixopt/structure.py +630 -0
  44. flixopt/utils.py +62 -0
  45. flixopt-2.0.1.dist-info/METADATA +145 -0
  46. flixopt-2.0.1.dist-info/RECORD +57 -0
  47. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
  48. flixopt-2.0.1.dist-info/top_level.txt +6 -0
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixopt-icon.svg +1 -0
  52. pics/pics.pptx +0 -0
  53. scripts/gen_ref_pages.py +54 -0
  54. site/release-notes/_template.txt +32 -0
  55. flixOpt/calculation.py +0 -629
  56. flixOpt/components.py +0 -614
  57. flixOpt/core.py +0 -182
  58. flixOpt/effects.py +0 -410
  59. flixOpt/elements.py +0 -489
  60. flixOpt/features.py +0 -942
  61. flixOpt/flow_system.py +0 -351
  62. flixOpt/interface.py +0 -203
  63. flixOpt/linear_converters.py +0 -325
  64. flixOpt/math_modeling.py +0 -1145
  65. flixOpt/plotting.py +0 -712
  66. flixOpt/results.py +0 -563
  67. flixOpt/solvers.py +0 -21
  68. flixOpt/structure.py +0 -733
  69. flixOpt/utils.py +0 -134
  70. flixopt-1.0.12.dist-info/METADATA +0 -174
  71. flixopt-1.0.12.dist-info/RECORD +0 -29
  72. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  73. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info/licenses}/LICENSE +0 -0
flixOpt/structure.py DELETED
@@ -1,733 +0,0 @@
1
- """
2
- This module contains the core structure of the flixOpt framework.
3
- These classes are not directly used by the end user, but are used by other modules.
4
- """
5
-
6
- import inspect
7
- import json
8
- import logging
9
- import pathlib
10
- from datetime import datetime
11
- from io import StringIO
12
- from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union
13
-
14
- import numpy as np
15
- from rich.console import Console
16
- from rich.pretty import Pretty
17
-
18
- from . import utils
19
- from .config import CONFIG
20
- from .core import Numeric, Numeric_TS, Skalar, TimeSeries, TimeSeriesData
21
- from .math_modeling import Equation, Inequation, MathModel, Solver, Variable, VariableTS
22
-
23
- if TYPE_CHECKING: # for type checking and preventing circular imports
24
- from .elements import BusModel, ComponentModel
25
- from .flow_system import FlowSystem
26
-
27
- logger = logging.getLogger('flixOpt')
28
-
29
-
30
- class SystemModel(MathModel):
31
- """
32
- Hier kommen die ModellingLanguage-spezifischen Sachen rein
33
- """
34
-
35
- def __init__(
36
- self,
37
- label: str,
38
- modeling_language: Literal['pyomo', 'cvxpy'],
39
- flow_system: 'FlowSystem',
40
- time_indices: Optional[Union[List[int], range]],
41
- ):
42
- super().__init__(label, modeling_language)
43
- self.flow_system = flow_system
44
- # Zeitdaten generieren:
45
- self.time_series, self.time_series_with_end, self.dt_in_hours, self.dt_in_hours_total = (
46
- flow_system.get_time_data_from_indices(time_indices)
47
- )
48
- self.previous_dt_in_hours = flow_system.previous_dt_in_hours
49
- self.nr_of_time_steps = len(self.time_series)
50
- self.indices = range(self.nr_of_time_steps)
51
-
52
- self.effect_collection_model = flow_system.effect_collection.create_model(self)
53
- self.component_models: List['ComponentModel'] = []
54
- self.bus_models: List['BusModel'] = []
55
- self.other_models: List[ElementModel] = []
56
-
57
- def do_modeling(self):
58
- self.effect_collection_model.do_modeling(self)
59
- self.component_models = [component.create_model() for component in self.flow_system.components.values()]
60
- self.bus_models = [bus.create_model() for bus in self.flow_system.buses.values()]
61
- for component_model in self.component_models:
62
- component_model.do_modeling(self)
63
- for bus_model in self.bus_models: # Buses after Components, because FlowModels are created in ComponentModels
64
- bus_model.do_modeling(self)
65
-
66
- def solve(self, solver: Solver, excess_threshold: Union[int, float] = 0.1):
67
- """
68
- Parameters
69
- ----------
70
- solver : Solver
71
- An Instance of the class Solver. Choose from flixOpt.solvers
72
- excess_threshold : float, positive!
73
- threshold for excess: If sum(Excess)>excess_threshold a warning is raised, that an excess occurs
74
- """
75
-
76
- logger.info(f'{" starting solving ":#^80}')
77
- logger.info(f'{self.describe_size()}')
78
-
79
- super().solve(solver)
80
-
81
- logger.info(f'Termination message: "{self.solver.termination_message}"')
82
-
83
- logger.info(f'{" finished solving ":#^80}')
84
- logger.info(f'{" Main Results ":#^80}')
85
- for effect_name, effect_results in self.main_results['Effects'].items():
86
- logger.info(
87
- f'{effect_name}:\n'
88
- f' {"operation":<15}: {effect_results["operation"]:>10.2f}\n'
89
- f' {"invest":<15}: {effect_results["invest"]:>10.2f}\n'
90
- f' {"sum":<15}: {effect_results["sum"]:>10.2f}'
91
- )
92
-
93
- logger.info(
94
- # f'{"SUM":<15}: ...todo...\n'
95
- f'{"Penalty":<17}: {self.main_results["penalty"]:>10.2f}\n'
96
- f'{"":-^80}\n'
97
- f'{"Objective":<17}: {self.main_results["Objective"]:>10.2f}\n'
98
- f'{"":-^80}'
99
- )
100
-
101
- logger.info('Investment Decisions:')
102
- logger.info(
103
- utils.apply_formating(
104
- data_dict={
105
- **self.main_results['Invest-Decisions']['invested'],
106
- **self.main_results['Invest-Decisions']['not invested'],
107
- },
108
- key_format='<30',
109
- indent=2,
110
- sort_by='value',
111
- )
112
- )
113
-
114
- for bus in self.main_results['buses with excess']:
115
- logger.warning(f'A penalty occurred in Bus "{bus}"!')
116
-
117
- if self.main_results['penalty'] > 10:
118
- logger.warning(f'A total penalty of {self.main_results["penalty"]} occurred.This might distort the results')
119
- logger.info(f'{" End of Main Results ":#^80}')
120
-
121
- def description_of_variables(self, structured: bool = True) -> Dict[str, Union[str, List[str]]]:
122
- return {
123
- 'Components': {
124
- label: comp.model.description_of_variables(structured)
125
- for label, comp in self.flow_system.components.items()
126
- },
127
- 'Buses': {
128
- label: bus.model.description_of_variables(structured) for label, bus in self.flow_system.buses.items()
129
- },
130
- 'Effects': self.flow_system.effect_collection.model.description_of_variables(structured),
131
- 'Others': {model.element.label: model.description_of_variables(structured) for model in self.other_models},
132
- }
133
-
134
- def description_of_constraints(self, structured: bool = True) -> Dict[str, Union[str, List[str]]]:
135
- return {
136
- 'Components': {
137
- label: comp.model.description_of_constraints(structured)
138
- for label, comp in self.flow_system.components.items()
139
- },
140
- 'Buses': {
141
- label: bus.model.description_of_constraints(structured) for label, bus in self.flow_system.buses.items()
142
- },
143
- 'Objective': self.objective.description(),
144
- 'Effects': self.flow_system.effect_collection.model.description_of_constraints(structured),
145
- 'Others': {
146
- model.element.label: model.description_of_constraints(structured) for model in self.other_models
147
- },
148
- }
149
-
150
- def results(self):
151
- return {
152
- 'Components': {model.element.label: model.results() for model in self.component_models},
153
- 'Effects': self.effect_collection_model.results(),
154
- 'Buses': {model.element.label: model.results() for model in self.bus_models},
155
- 'Others': {model.element.label: model.results() for model in self.other_models},
156
- 'Objective': self.result_of_objective,
157
- 'Time': self.time_series_with_end,
158
- 'Time intervals in hours': self.dt_in_hours,
159
- }
160
-
161
- @property
162
- def main_results(self) -> Dict[str, Union[Skalar, Dict]]:
163
- main_results = {}
164
- effect_results = {}
165
- main_results['Effects'] = effect_results
166
- for effect in self.flow_system.effect_collection.effects.values():
167
- effect_results[f'{effect.label} [{effect.unit}]'] = {
168
- 'operation': float(effect.model.operation.sum.result),
169
- 'invest': float(effect.model.invest.sum.result),
170
- 'sum': float(effect.model.all.sum.result),
171
- }
172
- main_results['penalty'] = float(self.effect_collection_model.penalty.sum.result)
173
- main_results['Objective'] = self.result_of_objective
174
- main_results['lower bound'] = self.solver.best_bound
175
- buses_with_excess = []
176
- main_results['buses with excess'] = buses_with_excess
177
- for bus in self.flow_system.buses.values():
178
- if bus.with_excess:
179
- if np.sum(bus.model.excess_input.result) > 1e-3 or np.sum(bus.model.excess_output.result) > 1e-3:
180
- buses_with_excess.append(bus.label)
181
-
182
- invest_decisions = {'invested': {}, 'not invested': {}}
183
- main_results['Invest-Decisions'] = invest_decisions
184
- from flixOpt.features import InvestmentModel
185
-
186
- for sub_model in self.sub_models:
187
- if isinstance(sub_model, InvestmentModel):
188
- invested_size = float(sub_model.size.result) # bei np.floats Probleme bei Speichern
189
- if invested_size > 1e-3:
190
- invest_decisions['invested'][sub_model.element.label_full] = invested_size
191
- else:
192
- invest_decisions['not invested'][sub_model.element.label_full] = invested_size
193
-
194
- return main_results
195
-
196
- @property
197
- def infos(self) -> Dict:
198
- infos = super().infos
199
- infos['Constraints'] = self.description_of_constraints()
200
- infos['Variables'] = self.description_of_variables()
201
- infos['Main Results'] = self.main_results
202
- infos['Config'] = CONFIG.to_dict()
203
- return infos
204
-
205
- @property
206
- def all_variables(self) -> Dict[str, Variable]:
207
- all_vars = {}
208
- for model in self.sub_models:
209
- for label, variable in model.variables.items():
210
- if label in all_vars:
211
- raise KeyError(f'Duplicate Variable found in SystemModel:{model=} {label=}; {variable=}')
212
- all_vars[label] = variable
213
- return all_vars
214
-
215
- @property
216
- def all_constraints(self) -> Dict[str, Union[Equation, Inequation]]:
217
- all_constr = {}
218
- for model in self.sub_models:
219
- for label, constr in model.constraints.items():
220
- if label in all_constr:
221
- raise KeyError(f'Duplicate Constraint found in SystemModel: {label=}; {constr=}')
222
- else:
223
- all_constr[label] = constr
224
- return all_constr
225
-
226
- @property
227
- def all_equations(self) -> Dict[str, Equation]:
228
- return {key: value for key, value in self.all_constraints.items() if isinstance(value, Equation)}
229
-
230
- @property
231
- def all_inequations(self) -> Dict[str, Inequation]:
232
- return {key: value for key, value in self.all_constraints.items() if isinstance(value, Inequation)}
233
-
234
- @property
235
- def sub_models(self) -> List['ElementModel']:
236
- direct_models = [self.effect_collection_model] + self.component_models + self.bus_models + self.other_models
237
- sub_models = [sub_model for direct_model in direct_models for sub_model in direct_model.all_sub_models]
238
- return direct_models + sub_models
239
-
240
- @property
241
- def variables(self) -> List[Variable]:
242
- """Needed for Mother class"""
243
- return list(self.all_variables.values())
244
-
245
- @property
246
- def equations(self) -> List[Equation]:
247
- """Needed for Mother class"""
248
- return list(self.all_equations.values())
249
-
250
- @property
251
- def inequations(self) -> List[Inequation]:
252
- """Needed for Mother class"""
253
- return list(self.all_inequations.values())
254
-
255
- @property
256
- def objective(self) -> Equation:
257
- return self.effect_collection_model.objective
258
-
259
-
260
- class Interface:
261
- """
262
- This class is used to collect arguments about a Model.
263
- """
264
-
265
- def transform_data(self):
266
- raise NotImplementedError('Every Interface needs a transform_data() method')
267
-
268
- def infos(self, use_numpy=True, use_element_label=False) -> Dict:
269
- """
270
- Generate a dictionary representation of the object's constructor arguments.
271
- Excludes default values and empty dictionaries and lists.
272
- Converts data to be compatible with JSON.
273
-
274
- Parameters:
275
- -----------
276
- use_numpy bool:
277
- Whether to convert NumPy arrays to lists. Defaults to True.
278
- If True, numeric numpy arrays (`np.ndarray`) are preserved as-is.
279
- If False, they are converted to lists.
280
- use_element_label bool:
281
- Whether to use the element label instead of the infos of the element. Defaults to False.
282
- Note that Elements used as keys in dictionaries are always converted to their labels.
283
-
284
- Returns:
285
- Dict: A dictionary representation of the object's constructor arguments.
286
-
287
- """
288
- # Get the constructor arguments and their default values
289
- init_params = sorted(
290
- inspect.signature(self.__init__).parameters.items(),
291
- key=lambda x: (x[0].lower() != 'label', x[0].lower()), # Prioritize 'label'
292
- )
293
- # Build a dict of attribute=value pairs, excluding defaults
294
- details = {'class': ':'.join([cls.__name__ for cls in self.__class__.__mro__])}
295
- for name, param in init_params:
296
- if name == 'self':
297
- continue
298
- value, default = getattr(self, name, None), param.default
299
- # Ignore default values and empty dicts and list
300
- if np.all(value == default) or (isinstance(value, (dict, list)) and not value):
301
- continue
302
- details[name] = copy_and_convert_datatypes(value, use_numpy, use_element_label)
303
- return details
304
-
305
- def to_json(self, path: Union[str, pathlib.Path]):
306
- """
307
- Saves the element to a json file.
308
- This not meant to be reloaded and recreate the object, but rather used to document or compare the object.
309
-
310
- Parameters:
311
- -----------
312
- path : Union[str, pathlib.Path]
313
- The path to the json file.
314
- """
315
- data = get_compact_representation(self.infos(use_numpy=True, use_element_label=True))
316
- with open(path, 'w', encoding='utf-8') as f:
317
- json.dump(data, f, indent=4, ensure_ascii=False)
318
-
319
- def __repr__(self):
320
- # Get the constructor arguments and their current values
321
- init_signature = inspect.signature(self.__init__)
322
- init_args = init_signature.parameters
323
-
324
- # Create a dictionary with argument names and their values
325
- args_str = ', '.join(f'{name}={repr(getattr(self, name, None))}' for name in init_args if name != 'self')
326
- return f'{self.__class__.__name__}({args_str})'
327
-
328
- def __str__(self):
329
- return get_str_representation(self.infos(use_numpy=True, use_element_label=True))
330
-
331
-
332
- class Element(Interface):
333
- """Basic Element of flixOpt"""
334
-
335
- def __init__(self, label: str, meta_data: Dict = None):
336
- """
337
- Parameters
338
- ----------
339
- label : str
340
- label of the element
341
- meta_data : Optional[Dict]
342
- used to store more information about the element. Is not used internally, but saved in the results
343
- """
344
- if not utils.label_is_valid(label):
345
- logger.critical(
346
- f"'{label}' cannot be used as a label. Leading or Trailing '_' and '__' are reserved. "
347
- f'Use any other symbol instead'
348
- )
349
- self.label = label
350
- self.meta_data = meta_data if meta_data is not None else {}
351
- self.used_time_series: List[TimeSeries] = [] # Used for better access
352
- self.model: Optional[ElementModel] = None
353
-
354
- def _plausibility_checks(self) -> None:
355
- """This function is used to do some basic plausibility checks for each Element during initialization"""
356
- raise NotImplementedError('Every Element needs a _plausibility_checks() method')
357
-
358
- def create_model(self) -> None:
359
- raise NotImplementedError('Every Element needs a create_model() method')
360
-
361
- @property
362
- def label_full(self) -> str:
363
- return self.label
364
-
365
-
366
- class ElementModel:
367
- """Interface to create the mathematical Models for Elements"""
368
-
369
- def __init__(self, element: Element, label: Optional[str] = None):
370
- logger.debug(f'Created {self.__class__.__name__} for {element.label_full}')
371
- self.element = element
372
- self.variables = {}
373
- self.constraints = {}
374
- self.sub_models = []
375
- self._label = label
376
-
377
- def add_variables(self, *variables: Variable) -> None:
378
- for variable in variables:
379
- if variable.label not in self.variables.keys():
380
- self.variables[variable.label] = variable
381
- elif variable in self.variables.values():
382
- raise Exception(f'Variable "{variable.label}" already exists')
383
- else:
384
- raise Exception(f'A Variable with the label "{variable.label}" already exists')
385
-
386
- def add_constraints(self, *constraints: Union[Equation, Inequation]) -> None:
387
- for constraint in constraints:
388
- if constraint.label not in self.constraints.keys():
389
- self.constraints[constraint.label] = constraint
390
- else:
391
- raise Exception(f'Constraint "{constraint.label}" already exists')
392
-
393
- def description_of_variables(self, structured: bool = True) -> Union[Dict[str, Union[List[str], Dict]], List[str]]:
394
- if structured:
395
- # Gather descriptions of this model's variables
396
- descriptions = {'_self': [var.description() for var in self.variables.values()]}
397
-
398
- # Recursively gather descriptions from sub-models
399
- for sub_model in self.sub_models:
400
- descriptions[sub_model.label] = sub_model.description_of_variables(structured=structured)
401
-
402
- return descriptions
403
- else:
404
- return [var.description() for var in self.all_variables.values()]
405
-
406
- def description_of_constraints(self, structured: bool = True) -> Union[Dict[str, str], List[str]]:
407
- if structured:
408
- # Gather descriptions of this model's variables
409
- descriptions = {'_self': [constr.description() for constr in self.constraints.values()]}
410
-
411
- # Recursively gather descriptions from sub-models
412
- for sub_model in self.sub_models:
413
- descriptions[sub_model.label] = sub_model.description_of_constraints(structured=structured)
414
-
415
- return descriptions
416
- else:
417
- return [eq.description() for eq in self.all_equations.values()]
418
-
419
- @property
420
- def overview_of_model_size(self) -> Dict[str, int]:
421
- all_vars, all_eqs, all_ineqs = self.all_variables, self.all_equations, self.all_inequations
422
- return {
423
- 'no of Euations': len(all_eqs),
424
- 'no of Equations single': sum(eq.nr_of_single_equations for eq in all_eqs.values()),
425
- 'no of Inequations': len(all_ineqs),
426
- 'no of Inequations single': sum(ineq.nr_of_single_equations for ineq in all_ineqs.values()),
427
- 'no of Variables': len(all_vars),
428
- 'no of Variables single': sum(var.length for var in all_vars.values()),
429
- }
430
-
431
- @property
432
- def inequations(self) -> Dict[str, Inequation]:
433
- return {name: ineq for name, ineq in self.constraints.items() if isinstance(ineq, Inequation)}
434
-
435
- @property
436
- def equations(self) -> Dict[str, Equation]:
437
- return {name: eq for name, eq in self.constraints.items() if isinstance(eq, Equation)}
438
-
439
- @property
440
- def all_variables(self) -> Dict[str, Variable]:
441
- all_vars = self.variables.copy()
442
- for sub_model in self.sub_models:
443
- for key, value in sub_model.all_variables.items():
444
- if key in all_vars:
445
- raise KeyError(f"Duplicate key found: '{key}' in both main model and submodel!")
446
- all_vars[key] = value
447
- return all_vars
448
-
449
- @property
450
- def all_constraints(self) -> Dict[str, Union[Equation, Inequation]]:
451
- all_constr = self.constraints.copy()
452
- for sub_model in self.sub_models:
453
- for key, value in sub_model.all_constraints.items():
454
- if key in all_constr:
455
- raise KeyError(f"Duplicate key found: '{key}' in both main model and submodel!")
456
- all_constr[key] = value
457
- return all_constr
458
-
459
- @property
460
- def all_equations(self) -> Dict[str, Equation]:
461
- all_eqs = self.equations.copy()
462
- for sub_model in self.sub_models:
463
- for key, value in sub_model.all_equations.items():
464
- if key in all_eqs:
465
- raise KeyError(f"Duplicate key found: '{key}' in both main model and submodel!")
466
- all_eqs[key] = value
467
- return all_eqs
468
-
469
- @property
470
- def all_inequations(self) -> Dict[str, Inequation]:
471
- all_ineqs = self.inequations.copy()
472
- for sub_model in self.sub_models:
473
- for key in sub_model.all_inequations:
474
- if key in all_ineqs:
475
- raise KeyError(f"Duplicate key found: '{key}' in both main model and submodel!")
476
- return all_ineqs
477
-
478
- @property
479
- def all_sub_models(self) -> List['ElementModel']:
480
- all_subs = []
481
- to_process = self.sub_models.copy()
482
- for model in to_process:
483
- all_subs.append(model)
484
- to_process.extend(model.sub_models)
485
- return all_subs
486
-
487
- def results(self) -> Dict:
488
- return {
489
- **{variable.label_short: variable.result for variable in self.variables.values()},
490
- **{model.label: model.results() for model in self.sub_models},
491
- }
492
-
493
- @property
494
- def label_full(self) -> str:
495
- return f'{self.element.label_full}__{self._label}' if self._label else self.element.label_full
496
-
497
- @property
498
- def label(self):
499
- return self._label or self.element.label
500
-
501
-
502
- def _create_time_series(
503
- label: str, data: Optional[Union[Numeric_TS, TimeSeries]], element: Element
504
- ) -> Optional[TimeSeries]:
505
- """Creates a TimeSeries from Numeric Data and adds it to the list of time_series of an Element.
506
- If the data already is a TimeSeries, nothing happens and the TimeSeries gets cleaned and returned"""
507
- if data is None:
508
- return None
509
- elif isinstance(data, TimeSeries):
510
- data.clear_indices_and_aggregated_data()
511
- return data
512
- else:
513
- time_series = TimeSeries(label=f'{element.label_full}__{label}', data=data)
514
- element.used_time_series.append(time_series)
515
- return time_series
516
-
517
-
518
- def create_equation(
519
- label: str, element_model: ElementModel, eq_type: Literal['eq', 'ineq'] = 'eq'
520
- ) -> Union[Equation, Inequation]:
521
- """Creates an Equation and adds it to the model of the Element"""
522
- if eq_type == 'eq':
523
- constr = Equation(f'{element_model.label_full}_{label}', label)
524
- elif eq_type == 'ineq':
525
- constr = Inequation(f'{element_model.label_full}_{label}', label)
526
- element_model.add_constraints(constr)
527
- return constr
528
-
529
-
530
- def create_variable(
531
- label: str,
532
- element_model: ElementModel,
533
- length: int,
534
- is_binary: bool = False,
535
- fixed_value: Optional[Numeric] = None,
536
- lower_bound: Optional[Numeric] = None,
537
- upper_bound: Optional[Numeric] = None,
538
- previous_values: Optional[Numeric] = None,
539
- avoid_use_of_variable_ts: bool = False,
540
- ) -> VariableTS:
541
- """Creates a VariableTS and adds it to the model of the Element"""
542
- variable_label = f'{element_model.label_full}_{label}'
543
- if length > 1 and not avoid_use_of_variable_ts:
544
- var = VariableTS(
545
- variable_label, length, label, is_binary, fixed_value, lower_bound, upper_bound, previous_values
546
- )
547
- logger.debug(f'Created VariableTS "{variable_label}": [{length}]')
548
- else:
549
- var = Variable(variable_label, length, label, is_binary, fixed_value, lower_bound, upper_bound)
550
- logger.debug(f'Created Variable "{variable_label}": [{length}]')
551
- element_model.add_variables(var)
552
- return var
553
-
554
-
555
- def copy_and_convert_datatypes(data: Any, use_numpy: bool = True, use_element_label: bool = False) -> Any:
556
- """
557
- Converts values in a nested data structure into JSON-compatible types while preserving or transforming numpy arrays
558
- and custom `Element` objects based on the specified options.
559
-
560
- The function handles various data types and transforms them into a consistent, readable format:
561
- - Primitive types (`int`, `float`, `str`, `bool`, `None`) are returned as-is.
562
- - Numpy scalars are converted to their corresponding Python scalar types.
563
- - Collections (`list`, `tuple`, `set`, `dict`) are recursively processed to ensure all elements are compatible.
564
- - Numpy arrays are preserved or converted to lists, depending on `use_numpy`.
565
- - Custom `Element` objects can be represented either by their `label` or their initialization parameters as a dictionary.
566
- - Timestamps (`datetime`) are converted to ISO 8601 strings.
567
-
568
- Parameters
569
- ----------
570
- data : Any
571
- The input data to process, which may be deeply nested and contain a mix of types.
572
- use_numpy : bool, optional
573
- If `True`, numeric numpy arrays (`np.ndarray`) are preserved as-is. If `False`, they are converted to lists.
574
- Default is `True`.
575
- use_element_label : bool, optional
576
- If `True`, `Element` objects are represented by their `label`. If `False`, they are converted into a dictionary
577
- based on their initialization parameters. Default is `False`.
578
-
579
- Returns
580
- -------
581
- Any
582
- A transformed version of the input data, containing only JSON-compatible types:
583
- - `int`, `float`, `str`, `bool`, `None`
584
- - `list`, `dict`
585
- - `np.ndarray` (if `use_numpy=True`. This is NOT JSON-compatible)
586
-
587
- Raises
588
- ------
589
- TypeError
590
- If the data cannot be converted to the specified types.
591
-
592
- Examples
593
- --------
594
- >>> copy_and_convert_datatypes({'a': np.array([1, 2, 3]), 'b': Element(label='example')})
595
- {'a': array([1, 2, 3]), 'b': {'class': 'Element', 'label': 'example'}}
596
-
597
- >>> copy_and_convert_datatypes({'a': np.array([1, 2, 3]), 'b': Element(label='example')}, use_numpy=False)
598
- {'a': [1, 2, 3], 'b': {'class': 'Element', 'label': 'example'}}
599
-
600
- Notes
601
- -----
602
- - The function gracefully handles unexpected types by issuing a warning and returning a deep copy of the data.
603
- - Empty collections (lists, dictionaries) and default parameter values in `Element` objects are omitted from the output.
604
- - Numpy arrays with non-numeric data types are automatically converted to lists.
605
- """
606
- if isinstance(data, np.integer): # This must be checked before checking for regular int and float!
607
- return int(data)
608
- elif isinstance(data, np.floating):
609
- return float(data)
610
-
611
- elif isinstance(data, (int, float, str, bool, type(None))):
612
- return data
613
- elif isinstance(data, datetime):
614
- return data.isoformat()
615
-
616
- elif isinstance(data, (tuple, set)):
617
- return copy_and_convert_datatypes([item for item in data], use_numpy, use_element_label)
618
- elif isinstance(data, dict):
619
- return {
620
- copy_and_convert_datatypes(key, use_numpy, use_element_label=True): copy_and_convert_datatypes(
621
- value, use_numpy, use_element_label
622
- )
623
- for key, value in data.items()
624
- }
625
- elif isinstance(data, list): # Shorten arrays/lists to be readable
626
- if use_numpy and all([isinstance(value, (int, float)) for value in data]):
627
- return np.array([item for item in data])
628
- else:
629
- return [copy_and_convert_datatypes(item, use_numpy, use_element_label) for item in data]
630
-
631
- elif isinstance(data, np.ndarray):
632
- if not use_numpy:
633
- return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label)
634
- elif use_numpy and np.issubdtype(data.dtype, np.number):
635
- return data
636
- else:
637
- logger.critical(
638
- f'An np.array with non-numeric content was found: {data=}.It will be converted to a list instead'
639
- )
640
- return copy_and_convert_datatypes(data.tolist(), use_numpy, use_element_label)
641
-
642
- elif isinstance(data, TimeSeries):
643
- return copy_and_convert_datatypes(data.active_data, use_numpy, use_element_label)
644
- elif isinstance(data, TimeSeriesData):
645
- return copy_and_convert_datatypes(data.data, use_numpy, use_element_label)
646
-
647
- elif isinstance(data, Interface):
648
- if use_element_label and isinstance(data, Element):
649
- return data.label
650
- return data.infos(use_numpy, use_element_label)
651
- else:
652
- raise TypeError(f'copy_and_convert_datatypes() did get unexpected data of type "{type(data)}": {data=}')
653
-
654
-
655
- def get_compact_representation(data: Any, array_threshold: int = 50, decimals: int = 2) -> Dict:
656
- """
657
- Generate a compact json serializable representation of deeply nested data.
658
- Numpy arrays are statistically described if they exceed a threshold and converted to lists.
659
-
660
- Args:
661
- data (Any): The data to format and represent.
662
- array_threshold (int): Maximum length of NumPy arrays to display. Longer arrays are statistically described.
663
- decimals (int): Number of decimal places in which to describe the arrays.
664
-
665
- Returns:
666
- Dict: A dictionary representation of the data
667
- """
668
-
669
- def format_np_array_if_found(value: Any) -> Any:
670
- """Recursively processes the data, formatting NumPy arrays."""
671
- if isinstance(value, (int, float, str, bool, type(None))):
672
- return value
673
- elif isinstance(value, np.ndarray):
674
- return describe_numpy_arrays(value)
675
- elif isinstance(value, dict):
676
- return {format_np_array_if_found(k): format_np_array_if_found(v) for k, v in value.items()}
677
- elif isinstance(value, (list, tuple, set)):
678
- return [format_np_array_if_found(v) for v in value]
679
- else:
680
- logger.warning(
681
- f'Unexpected value found when trying to format numpy array numpy array: {type(value)=}; {value=}'
682
- )
683
- return value
684
-
685
- def describe_numpy_arrays(arr: np.ndarray) -> Union[str, List]:
686
- """Shortens NumPy arrays if they exceed the specified length."""
687
-
688
- def normalized_center_of_mass(array: Any) -> float:
689
- # position in array (0 bis 1 normiert)
690
- positions = np.linspace(0, 1, len(array)) # weights w_i
691
- # mass center
692
- if np.sum(array) == 0:
693
- return np.nan
694
- else:
695
- return np.sum(positions * array) / np.sum(array)
696
-
697
- if arr.size > array_threshold: # Calculate basic statistics
698
- fmt = f'.{decimals}f'
699
- return (
700
- f'Array (min={np.min(arr):{fmt}}, max={np.max(arr):{fmt}}, mean={np.mean(arr):{fmt}}, '
701
- f'median={np.median(arr):{fmt}}, std={np.std(arr):{fmt}}, len={len(arr)}, '
702
- f'center={normalized_center_of_mass(arr):{fmt}})'
703
- )
704
- else:
705
- return np.around(arr, decimals=decimals).tolist()
706
-
707
- # Process the data to handle NumPy arrays
708
- formatted_data = format_np_array_if_found(copy_and_convert_datatypes(data, use_numpy=True))
709
-
710
- return formatted_data
711
-
712
-
713
- def get_str_representation(data: Any, array_threshold: int = 50, decimals: int = 2) -> str:
714
- """
715
- Generate a string representation of deeply nested data using `rich.print`.
716
- NumPy arrays are shortened to the specified length and converted to strings.
717
-
718
- Args:
719
- data (Any): The data to format and represent.
720
- array_threshold (int): Maximum length of NumPy arrays to display. Longer arrays are statistically described.
721
- decimals (int): Number of decimal places in which to describe the arrays.
722
-
723
- Returns:
724
- str: The formatted string representation of the data.
725
- """
726
-
727
- formatted_data = get_compact_representation(data, array_threshold, decimals)
728
-
729
- # Use Rich to format and print the data
730
- with StringIO() as output_buffer:
731
- console = Console(file=output_buffer, width=1000) # Adjust width as needed
732
- console.print(Pretty(formatted_data, expand_all=True, indent_guides=True))
733
- return output_buffer.getvalue()