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.
- docs/examples/00-Minimal Example.md +5 -0
- docs/examples/01-Basic Example.md +5 -0
- docs/examples/02-Complex Example.md +10 -0
- docs/examples/03-Calculation Modes.md +5 -0
- docs/examples/index.md +5 -0
- docs/faq/contribute.md +49 -0
- docs/faq/index.md +3 -0
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +1 -0
- docs/javascripts/mathjax.js +18 -0
- docs/release-notes/_template.txt +32 -0
- docs/release-notes/index.md +7 -0
- docs/release-notes/v2.0.0.md +93 -0
- docs/release-notes/v2.0.1.md +12 -0
- docs/user-guide/Mathematical Notation/Bus.md +33 -0
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
- docs/user-guide/Mathematical Notation/Flow.md +26 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
- docs/user-guide/Mathematical Notation/Storage.md +44 -0
- docs/user-guide/Mathematical Notation/index.md +22 -0
- docs/user-guide/Mathematical Notation/others.md +3 -0
- docs/user-guide/index.md +124 -0
- {flixOpt → flixopt}/__init__.py +5 -2
- {flixOpt → flixopt}/aggregation.py +113 -140
- flixopt/calculation.py +455 -0
- {flixOpt → flixopt}/commons.py +7 -4
- flixopt/components.py +630 -0
- {flixOpt → flixopt}/config.py +9 -8
- {flixOpt → flixopt}/config.yaml +3 -3
- flixopt/core.py +970 -0
- flixopt/effects.py +386 -0
- flixopt/elements.py +534 -0
- flixopt/features.py +1042 -0
- flixopt/flow_system.py +409 -0
- flixopt/interface.py +265 -0
- flixopt/io.py +308 -0
- flixopt/linear_converters.py +331 -0
- flixopt/plotting.py +1340 -0
- flixopt/results.py +898 -0
- flixopt/solvers.py +77 -0
- flixopt/structure.py +630 -0
- flixopt/utils.py +62 -0
- flixopt-2.0.1.dist-info/METADATA +145 -0
- flixopt-2.0.1.dist-info/RECORD +57 -0
- {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
- flixopt-2.0.1.dist-info/top_level.txt +6 -0
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixopt-icon.svg +1 -0
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +54 -0
- site/release-notes/_template.txt +32 -0
- flixOpt/calculation.py +0 -629
- flixOpt/components.py +0 -614
- flixOpt/core.py +0 -182
- flixOpt/effects.py +0 -410
- flixOpt/elements.py +0 -489
- flixOpt/features.py +0 -942
- flixOpt/flow_system.py +0 -351
- flixOpt/interface.py +0 -203
- flixOpt/linear_converters.py +0 -325
- flixOpt/math_modeling.py +0 -1145
- flixOpt/plotting.py +0 -712
- flixOpt/results.py +0 -563
- flixOpt/solvers.py +0 -21
- flixOpt/structure.py +0 -733
- flixOpt/utils.py +0 -134
- flixopt-1.0.12.dist-info/METADATA +0 -174
- flixopt-1.0.12.dist-info/RECORD +0 -29
- flixopt-1.0.12.dist-info/top_level.txt +0 -3
- {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()
|