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/core.py
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module contains the core functionality of the flixOpt framework.
|
|
3
|
-
It provides Datatypes, logging functionality, and some functions to transform data structures.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import inspect
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Any, Dict, List, Optional, Union
|
|
9
|
-
|
|
10
|
-
import numpy as np
|
|
11
|
-
|
|
12
|
-
from . import utils
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger('flixOpt')
|
|
15
|
-
|
|
16
|
-
Skalar = Union[int, float] # Datatype
|
|
17
|
-
Numeric = Union[int, float, np.ndarray] # Datatype
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TimeSeriesData:
|
|
21
|
-
# TODO: Move to Interface.py
|
|
22
|
-
def __init__(self, data: Numeric, agg_group: Optional[str] = None, agg_weight: Optional[float] = None):
|
|
23
|
-
"""
|
|
24
|
-
timeseries class for transmit timeseries AND special characteristics of timeseries,
|
|
25
|
-
i.g. to define weights needed in calculation_type 'aggregated'
|
|
26
|
-
EXAMPLE solar:
|
|
27
|
-
you have several solar timeseries. These should not be overweighted
|
|
28
|
-
compared to the remaining timeseries (i.g. heat load, price)!
|
|
29
|
-
fixed_relative_profile_solar1 = TimeSeriesData(sol_array_1, type = 'solar')
|
|
30
|
-
fixed_relative_profile_solar2 = TimeSeriesData(sol_array_2, type = 'solar')
|
|
31
|
-
fixed_relative_profile_solar3 = TimeSeriesData(sol_array_3, type = 'solar')
|
|
32
|
-
--> this 3 series of same type share one weight, i.e. internally assigned each weight = 1/3
|
|
33
|
-
(instead of standard weight = 1)
|
|
34
|
-
|
|
35
|
-
Parameters
|
|
36
|
-
----------
|
|
37
|
-
data : Union[int, float, np.ndarray]
|
|
38
|
-
The timeseries data, which can be a scalar, array, or numpy array.
|
|
39
|
-
agg_group : str, optional
|
|
40
|
-
The group this TimeSeriesData is a part of. agg_weight is split between members of a group. Default is None.
|
|
41
|
-
agg_weight : float, optional
|
|
42
|
-
The weight for calculation_type 'aggregated', should be between 0 and 1. Default is None.
|
|
43
|
-
|
|
44
|
-
Raises
|
|
45
|
-
------
|
|
46
|
-
Exception
|
|
47
|
-
If both agg_group and agg_weight are set, an exception is raised.
|
|
48
|
-
"""
|
|
49
|
-
self.data = data
|
|
50
|
-
self.agg_group = agg_group
|
|
51
|
-
self.agg_weight = agg_weight
|
|
52
|
-
if (agg_group is not None) and (agg_weight is not None):
|
|
53
|
-
raise Exception('Either <agg_group> or explicit <agg_weigth> can be used. Not both!')
|
|
54
|
-
self.label: Optional[str] = None
|
|
55
|
-
|
|
56
|
-
def __repr__(self):
|
|
57
|
-
# Get the constructor arguments and their current values
|
|
58
|
-
init_signature = inspect.signature(self.__init__)
|
|
59
|
-
init_args = init_signature.parameters
|
|
60
|
-
|
|
61
|
-
# Create a dictionary with argument names and their values
|
|
62
|
-
args_str = ', '.join(f'{name}={repr(getattr(self, name, None))}' for name in init_args if name != 'self')
|
|
63
|
-
return f'{self.__class__.__name__}({args_str})'
|
|
64
|
-
|
|
65
|
-
def __str__(self):
|
|
66
|
-
return str(self.data)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Numeric_TS = Union[
|
|
70
|
-
Skalar, np.ndarray, TimeSeriesData
|
|
71
|
-
] # TODO: This is not really correct throughozt the codebase. Sometimes its used for TimeSeries aswell?
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class TimeSeries:
|
|
75
|
-
"""
|
|
76
|
-
Class for data that applies to time series, stored as vector (np.ndarray) or scalar.
|
|
77
|
-
|
|
78
|
-
This class represents a vector or scalar value that makes the handling of time series easier.
|
|
79
|
-
It supports various operations such as activation of specific time indices, setting explicit active data, and
|
|
80
|
-
aggregation weight management.
|
|
81
|
-
|
|
82
|
-
Attributes
|
|
83
|
-
----------
|
|
84
|
-
label : str
|
|
85
|
-
The label for the time series.
|
|
86
|
-
data : Optional[Numeric]
|
|
87
|
-
The actual data for the time series. Can be None.
|
|
88
|
-
aggregated_data : Optional[Numeric]
|
|
89
|
-
aggregated_data to use instead of data if provided.
|
|
90
|
-
active_indices : Optional[np.ndarray]
|
|
91
|
-
Indices of the time steps to activate.
|
|
92
|
-
aggregation_weight : float
|
|
93
|
-
Weight for aggregation method, between 0 and 1, normally 1.
|
|
94
|
-
aggregation_group : str
|
|
95
|
-
Group for calculating the aggregation weigth for aggregation method.
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
def __init__(self, label: str, data: Optional[Numeric_TS]):
|
|
99
|
-
self.label: str = label
|
|
100
|
-
if isinstance(data, TimeSeriesData):
|
|
101
|
-
self.data = self.make_scalar_if_possible(data.data)
|
|
102
|
-
self.aggregation_weight, self.aggregation_group = data.agg_weight, data.agg_group
|
|
103
|
-
data.label = self.label # Connecting User_time_series to real Time_series
|
|
104
|
-
else:
|
|
105
|
-
self.data = self.make_scalar_if_possible(data)
|
|
106
|
-
self.aggregation_weight, self.aggregation_group = None, None
|
|
107
|
-
|
|
108
|
-
self.active_indices: Optional[Union[range, List[int]]] = None
|
|
109
|
-
self.aggregated_data: Optional[Numeric] = None
|
|
110
|
-
|
|
111
|
-
def activate_indices(self, indices: Optional[Union[range, List[int]]], aggregated_data: Optional[Numeric] = None):
|
|
112
|
-
self.active_indices = indices
|
|
113
|
-
|
|
114
|
-
if aggregated_data is not None:
|
|
115
|
-
assert len(aggregated_data) == len(self.active_indices) or len(aggregated_data) == 1, (
|
|
116
|
-
f'The aggregated_data has the wrong length for TimeSeries {self.label}. '
|
|
117
|
-
f'Length should be: {len(self.active_indices)} or 1, but is {len(aggregated_data)}'
|
|
118
|
-
)
|
|
119
|
-
self.aggregated_data = self.make_scalar_if_possible(aggregated_data)
|
|
120
|
-
|
|
121
|
-
def clear_indices_and_aggregated_data(self):
|
|
122
|
-
self.active_indices = None
|
|
123
|
-
self.aggregated_data = None
|
|
124
|
-
|
|
125
|
-
@property
|
|
126
|
-
def active_data(self) -> Numeric:
|
|
127
|
-
if self.aggregated_data is not None: # Aggregated data is always active, if present
|
|
128
|
-
return self.aggregated_data
|
|
129
|
-
|
|
130
|
-
indices_not_applicable = np.isscalar(self.data) or (self.data is None) or (self.active_indices is None)
|
|
131
|
-
if indices_not_applicable:
|
|
132
|
-
return self.data
|
|
133
|
-
else:
|
|
134
|
-
return self.data[self.active_indices]
|
|
135
|
-
|
|
136
|
-
@property
|
|
137
|
-
def active_data_vector(self) -> np.ndarray:
|
|
138
|
-
# Always returns the active data as a vector.
|
|
139
|
-
return utils.as_vector(self.active_data, len(self.active_indices))
|
|
140
|
-
|
|
141
|
-
@property
|
|
142
|
-
def is_scalar(self) -> bool:
|
|
143
|
-
return np.isscalar(self.data)
|
|
144
|
-
|
|
145
|
-
@property
|
|
146
|
-
def is_array(self) -> bool:
|
|
147
|
-
return not self.is_scalar and self.data is not None
|
|
148
|
-
|
|
149
|
-
def __repr__(self):
|
|
150
|
-
# Retrieve all attributes and their values
|
|
151
|
-
attrs = vars(self)
|
|
152
|
-
# Format each attribute as 'key=value'
|
|
153
|
-
attrs_str = ', '.join(f'{key}={value!r}' for key, value in attrs.items())
|
|
154
|
-
# Format the output as 'ClassName(attr1=value1, attr2=value2, ...)'
|
|
155
|
-
return f'{self.__class__.__name__}({attrs_str})'
|
|
156
|
-
|
|
157
|
-
def __str__(self):
|
|
158
|
-
return str(self.active_data)
|
|
159
|
-
|
|
160
|
-
@staticmethod
|
|
161
|
-
def make_scalar_if_possible(data: Optional[Numeric]) -> Optional[Numeric]:
|
|
162
|
-
"""
|
|
163
|
-
Convert an array to a scalar if all values are equal, or return the array as-is.
|
|
164
|
-
Can Return None if the passed data is None
|
|
165
|
-
|
|
166
|
-
Parameters
|
|
167
|
-
----------
|
|
168
|
-
data : Numeric, None
|
|
169
|
-
The data to process.
|
|
170
|
-
|
|
171
|
-
Returns
|
|
172
|
-
-------
|
|
173
|
-
Numeric
|
|
174
|
-
A scalar if all values in the array are equal, otherwise the array itself. None, if the passed value is None
|
|
175
|
-
"""
|
|
176
|
-
# TODO: Should this really return None Values?
|
|
177
|
-
if np.isscalar(data) or data is None:
|
|
178
|
-
return data
|
|
179
|
-
data = np.array(data)
|
|
180
|
-
if np.all(data == data[0]):
|
|
181
|
-
return data[0]
|
|
182
|
-
return data
|
flixOpt/effects.py
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module contains the effects of the flixOpt framework.
|
|
3
|
-
Furthermore, it contains the EffectCollection, which is used to collect all effects of a system.
|
|
4
|
-
Different Datatypes are used to represent the effects with assigned values by the user,
|
|
5
|
-
which are then transformed into the internal data structure.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
from typing import Dict, Literal, Optional, Union
|
|
10
|
-
|
|
11
|
-
import numpy as np
|
|
12
|
-
|
|
13
|
-
from .core import Numeric, Numeric_TS, Skalar, TimeSeries
|
|
14
|
-
from .features import ShareAllocationModel
|
|
15
|
-
from .math_modeling import Equation, Variable
|
|
16
|
-
from .structure import Element, ElementModel, SystemModel, _create_time_series
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger('flixOpt')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class Effect(Element):
|
|
22
|
-
"""
|
|
23
|
-
Effect, i.g. costs, CO2 emissions, area, ...
|
|
24
|
-
Components, FLows, and so on can contribute to an Effect. One Effect is chosen as the Objective of the Optimization
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
label: str,
|
|
30
|
-
unit: str,
|
|
31
|
-
description: str,
|
|
32
|
-
meta_data: Optional[Dict] = None,
|
|
33
|
-
is_standard: bool = False,
|
|
34
|
-
is_objective: bool = False,
|
|
35
|
-
specific_share_to_other_effects_operation: 'EffectValues' = None,
|
|
36
|
-
specific_share_to_other_effects_invest: 'EffectValuesInvest' = None,
|
|
37
|
-
minimum_operation: Optional[Skalar] = None,
|
|
38
|
-
maximum_operation: Optional[Skalar] = None,
|
|
39
|
-
minimum_invest: Optional[Skalar] = None,
|
|
40
|
-
maximum_invest: Optional[Skalar] = None,
|
|
41
|
-
minimum_operation_per_hour: Optional[Numeric_TS] = None,
|
|
42
|
-
maximum_operation_per_hour: Optional[Numeric_TS] = None,
|
|
43
|
-
minimum_total: Optional[Skalar] = None,
|
|
44
|
-
maximum_total: Optional[Skalar] = None,
|
|
45
|
-
):
|
|
46
|
-
"""
|
|
47
|
-
Parameters
|
|
48
|
-
----------
|
|
49
|
-
label : str
|
|
50
|
-
name
|
|
51
|
-
unit : str
|
|
52
|
-
unit of effect, i.g. €, kg_CO2, kWh_primaryEnergy
|
|
53
|
-
description : str
|
|
54
|
-
long name
|
|
55
|
-
meta_data : Optional[Dict]
|
|
56
|
-
used to store more information about the element. Is not used internally, but saved in the results
|
|
57
|
-
is_standard : boolean, optional
|
|
58
|
-
true, if Standard-Effect (for direct input of value without effect (alternatively to dict)) , else false
|
|
59
|
-
is_objective : boolean, optional
|
|
60
|
-
true, if optimization target
|
|
61
|
-
specific_share_to_other_effects_operation : {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
|
|
62
|
-
share to other effects (only operation)
|
|
63
|
-
specific_share_to_other_effects_invest : {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
|
|
64
|
-
share to other effects (only invest).
|
|
65
|
-
minimum_operation : scalar, optional
|
|
66
|
-
minimal sum (only operation) of the effect
|
|
67
|
-
maximum_operation : scalar, optional
|
|
68
|
-
maximal sum (nur operation) of the effect.
|
|
69
|
-
minimum_operation_per_hour : scalar or TS
|
|
70
|
-
maximum value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
|
|
71
|
-
maximum_operation_per_hour : scalar or TS
|
|
72
|
-
minimum value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
|
|
73
|
-
minimum_invest : scalar, optional
|
|
74
|
-
minimal sum (only invest) of the effect
|
|
75
|
-
maximum_invest : scalar, optional
|
|
76
|
-
maximal sum (only invest) of the effect
|
|
77
|
-
minimum_total : sclalar, optional
|
|
78
|
-
min sum of effect (invest+operation).
|
|
79
|
-
maximum_total : scalar, optional
|
|
80
|
-
max sum of effect (invest+operation).
|
|
81
|
-
**kwargs : TYPE
|
|
82
|
-
DESCRIPTION.
|
|
83
|
-
|
|
84
|
-
Returns
|
|
85
|
-
-------
|
|
86
|
-
None.
|
|
87
|
-
|
|
88
|
-
"""
|
|
89
|
-
super().__init__(label, meta_data=meta_data)
|
|
90
|
-
self.label = label
|
|
91
|
-
self.unit = unit
|
|
92
|
-
self.description = description
|
|
93
|
-
self.is_standard = is_standard
|
|
94
|
-
self.is_objective = is_objective
|
|
95
|
-
self.specific_share_to_other_effects_operation: Union[EffectValues, EffectTimeSeries] = (
|
|
96
|
-
specific_share_to_other_effects_operation or {}
|
|
97
|
-
)
|
|
98
|
-
self.specific_share_to_other_effects_invest: Union[EffectValuesInvest, EffectDictInvest] = (
|
|
99
|
-
specific_share_to_other_effects_invest or {}
|
|
100
|
-
)
|
|
101
|
-
self.minimum_operation = minimum_operation
|
|
102
|
-
self.maximum_operation = maximum_operation
|
|
103
|
-
self.minimum_operation_per_hour: Numeric_TS = minimum_operation_per_hour
|
|
104
|
-
self.maximum_operation_per_hour: Numeric_TS = maximum_operation_per_hour
|
|
105
|
-
self.minimum_invest = minimum_invest
|
|
106
|
-
self.maximum_invest = maximum_invest
|
|
107
|
-
self.minimum_total = minimum_total
|
|
108
|
-
self.maximum_total = maximum_total
|
|
109
|
-
|
|
110
|
-
self._plausibility_checks()
|
|
111
|
-
|
|
112
|
-
def _plausibility_checks(self) -> None:
|
|
113
|
-
# Check circular loops in effects: (Effekte fügen sich gegenseitig Shares hinzu):
|
|
114
|
-
# TODO: Improve checks!! Only most basic case covered...
|
|
115
|
-
|
|
116
|
-
def error_str(effect_label: str, share_ffect_label: str):
|
|
117
|
-
return (
|
|
118
|
-
f' {effect_label} -> has share in: {share_ffect_label}\n'
|
|
119
|
-
f' {share_ffect_label} -> has share in: {effect_label}'
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
# Effekt darf nicht selber als Share in seinen ShareEffekten auftauchen:
|
|
123
|
-
# operation:
|
|
124
|
-
for target_effect in self.specific_share_to_other_effects_operation.keys():
|
|
125
|
-
assert self not in target_effect.specific_share_to_other_effects_operation.keys(), (
|
|
126
|
-
f'Error: circular operation-shares \n{error_str(target_effect.label, target_effect.label)}'
|
|
127
|
-
)
|
|
128
|
-
# invest:
|
|
129
|
-
for target_effect in self.specific_share_to_other_effects_invest.keys():
|
|
130
|
-
assert self not in target_effect.specific_share_to_other_effects_invest.keys(), (
|
|
131
|
-
f'Error: circular invest-shares \n{error_str(target_effect.label, target_effect.label)}'
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
def transform_data(self):
|
|
135
|
-
self.minimum_operation_per_hour = _create_time_series(
|
|
136
|
-
'minimum_operation_per_hour', self.minimum_operation_per_hour, self
|
|
137
|
-
)
|
|
138
|
-
self.maximum_operation_per_hour = _create_time_series(
|
|
139
|
-
'maximum_operation_per_hour', self.maximum_operation_per_hour, self
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
self.specific_share_to_other_effects_operation = effect_values_to_time_series(
|
|
143
|
-
'specific_share_to_other_effects_operation', self.specific_share_to_other_effects_operation, self
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
def create_model(self) -> 'EffectModel':
|
|
147
|
-
self.model = EffectModel(self)
|
|
148
|
-
return self.model
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class EffectModel(ElementModel):
|
|
152
|
-
def __init__(self, element: Effect):
|
|
153
|
-
super().__init__(element)
|
|
154
|
-
self.element: Effect
|
|
155
|
-
self.invest = ShareAllocationModel(
|
|
156
|
-
self.element, 'invest', False, total_max=self.element.maximum_invest, total_min=self.element.minimum_invest
|
|
157
|
-
)
|
|
158
|
-
self.operation = ShareAllocationModel(
|
|
159
|
-
self.element,
|
|
160
|
-
'operation',
|
|
161
|
-
True,
|
|
162
|
-
total_max=self.element.maximum_operation,
|
|
163
|
-
total_min=self.element.minimum_operation,
|
|
164
|
-
min_per_hour=self.element.minimum_operation_per_hour.active_data
|
|
165
|
-
if self.element.minimum_operation_per_hour is not None
|
|
166
|
-
else None,
|
|
167
|
-
max_per_hour=self.element.maximum_operation_per_hour.active_data
|
|
168
|
-
if self.element.maximum_operation_per_hour is not None
|
|
169
|
-
else None,
|
|
170
|
-
)
|
|
171
|
-
self.all = ShareAllocationModel(
|
|
172
|
-
self.element, 'all', False, total_max=self.element.maximum_total, total_min=self.element.minimum_total
|
|
173
|
-
)
|
|
174
|
-
self.sub_models.extend([self.invest, self.operation, self.all])
|
|
175
|
-
|
|
176
|
-
def do_modeling(self, system_model: SystemModel):
|
|
177
|
-
for model in self.sub_models:
|
|
178
|
-
model.do_modeling(system_model)
|
|
179
|
-
|
|
180
|
-
self.all.add_share(system_model, 'operation', self.operation.sum, 1)
|
|
181
|
-
self.all.add_share(system_model, 'invest', self.invest.sum, 1)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
EffectDict = Dict[Optional['Effect'], Numeric]
|
|
185
|
-
EffectDictInvest = Dict[Optional['Effect'], Skalar]
|
|
186
|
-
|
|
187
|
-
EffectValues = Optional[Union[Numeric_TS, EffectDict]] # Datatype for User Input
|
|
188
|
-
EffectValuesInvest = Optional[Union[Skalar, EffectDictInvest]] # Datatype for User Input
|
|
189
|
-
|
|
190
|
-
EffectTimeSeries = Dict[Optional['Effect'], TimeSeries] # Final Internal Data Structure
|
|
191
|
-
ElementTimeSeries = Dict[Optional[Element], TimeSeries] # Final Internal Data Structure
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def nested_values_to_time_series(
|
|
195
|
-
nested_values: Dict[Element, Numeric_TS], label_suffix: str, parent_element: Element
|
|
196
|
-
) -> ElementTimeSeries:
|
|
197
|
-
"""
|
|
198
|
-
Creates TimeSeries from nested values, which are a Dict of Elements to values.
|
|
199
|
-
The resulting label of the TimeSeries is the label of the parent_element, followed by the label of the element in
|
|
200
|
-
the nested_values and the label_suffix.
|
|
201
|
-
"""
|
|
202
|
-
return {
|
|
203
|
-
element: _create_time_series(f'{element.label}_{label_suffix}', value, parent_element)
|
|
204
|
-
for element, value in nested_values.items()
|
|
205
|
-
if element is not None
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def effect_values_to_time_series(
|
|
210
|
-
label_suffix: str, nested_values: EffectValues, parent_element: Element
|
|
211
|
-
) -> Optional[EffectTimeSeries]:
|
|
212
|
-
"""
|
|
213
|
-
Creates TimeSeries from EffectValues. The resulting label of the TimeSeries is the label of the parent_element,
|
|
214
|
-
followed by the label of the Effect in the nested_values and the label_suffix.
|
|
215
|
-
If the key in the EffectValues is None, the alias 'Standart_Effect' is used
|
|
216
|
-
"""
|
|
217
|
-
nested_values = as_effect_dict(nested_values)
|
|
218
|
-
if nested_values is None:
|
|
219
|
-
return None
|
|
220
|
-
else:
|
|
221
|
-
standard_value = nested_values.pop(None, None)
|
|
222
|
-
transformed_values = nested_values_to_time_series(nested_values, label_suffix, parent_element)
|
|
223
|
-
if standard_value is not None:
|
|
224
|
-
transformed_values[None] = _create_time_series(
|
|
225
|
-
f'Standard_Effect_{label_suffix}', standard_value, parent_element
|
|
226
|
-
)
|
|
227
|
-
return transformed_values
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def as_effect_dict(effect_values: EffectValues) -> Optional[EffectDict]:
|
|
231
|
-
"""
|
|
232
|
-
Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.
|
|
233
|
-
|
|
234
|
-
Examples
|
|
235
|
-
--------
|
|
236
|
-
costs = 20 -> {None: 20}
|
|
237
|
-
costs = None -> None
|
|
238
|
-
costs = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3}
|
|
239
|
-
|
|
240
|
-
Parameters
|
|
241
|
-
----------
|
|
242
|
-
effect_values : None, int, float, TimeSeries, or dict
|
|
243
|
-
The effect values to convert, either a scalar, TimeSeries, or a dictionary.
|
|
244
|
-
|
|
245
|
-
Returns
|
|
246
|
-
-------
|
|
247
|
-
dict or None
|
|
248
|
-
A dictionary with None or Effect as the key, or None if input is None.
|
|
249
|
-
"""
|
|
250
|
-
return (
|
|
251
|
-
effect_values
|
|
252
|
-
if isinstance(effect_values, dict)
|
|
253
|
-
else {None: effect_values}
|
|
254
|
-
if effect_values is not None
|
|
255
|
-
else None
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def effect_values_from_effect_time_series(effect_time_series: EffectTimeSeries) -> Dict[Optional[Effect], Numeric]:
|
|
260
|
-
return {effect: time_series.active_data for effect, time_series in effect_time_series.items()}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
class EffectCollection:
|
|
264
|
-
"""
|
|
265
|
-
Handling all Effects
|
|
266
|
-
"""
|
|
267
|
-
|
|
268
|
-
def __init__(self, label: str):
|
|
269
|
-
self.label = label
|
|
270
|
-
self.model: Optional[EffectCollectionModel] = None
|
|
271
|
-
self.effects: Dict[str, Effect] = {}
|
|
272
|
-
|
|
273
|
-
def create_model(self, system_model: SystemModel) -> 'EffectCollectionModel':
|
|
274
|
-
self.model = EffectCollectionModel(self, system_model)
|
|
275
|
-
return self.model
|
|
276
|
-
|
|
277
|
-
def add_effect(self, effect: 'Effect') -> None:
|
|
278
|
-
if effect.is_standard and self.standard_effect is not None:
|
|
279
|
-
raise Exception(f'A standard-effect already exists! ({self.standard_effect.label=})')
|
|
280
|
-
if effect.is_objective and self.objective_effect is not None:
|
|
281
|
-
raise Exception(f'A objective-effect already exists! ({self.objective_effect.label=})')
|
|
282
|
-
if effect in self.effects.values():
|
|
283
|
-
raise Exception(f'Effect already added! ({effect.label=})')
|
|
284
|
-
if effect.label in self.effects:
|
|
285
|
-
raise Exception(f'Effect with label "{effect.label=}" already added!')
|
|
286
|
-
self.effects[effect.label] = effect
|
|
287
|
-
|
|
288
|
-
@property
|
|
289
|
-
def standard_effect(self) -> Optional[Effect]:
|
|
290
|
-
for effect in self.effects.values():
|
|
291
|
-
if effect.is_standard:
|
|
292
|
-
return effect
|
|
293
|
-
|
|
294
|
-
@property
|
|
295
|
-
def objective_effect(self) -> Optional[Effect]:
|
|
296
|
-
for effect in self.effects.values():
|
|
297
|
-
if effect.is_objective:
|
|
298
|
-
return effect
|
|
299
|
-
|
|
300
|
-
@property
|
|
301
|
-
def label_full(self):
|
|
302
|
-
return self.label
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
class EffectCollectionModel(ElementModel):
|
|
306
|
-
# TODO: Maybe all EffectModels should be sub_models of this Model? Including Objective and Penalty?
|
|
307
|
-
def __init__(self, element: EffectCollection, system_model: SystemModel):
|
|
308
|
-
super().__init__(element)
|
|
309
|
-
self.element = element
|
|
310
|
-
self._system_model = system_model
|
|
311
|
-
self._effect_models: Dict[Effect, EffectModel] = {}
|
|
312
|
-
self.penalty: Optional[ShareAllocationModel] = None
|
|
313
|
-
self.objective: Optional[Equation] = None
|
|
314
|
-
|
|
315
|
-
def do_modeling(self, system_model: SystemModel):
|
|
316
|
-
self._effect_models = {effect: effect.create_model() for effect in self.element.effects.values()}
|
|
317
|
-
self.penalty = ShareAllocationModel(self.element, 'penalty', False)
|
|
318
|
-
self.sub_models.extend(list(self._effect_models.values()) + [self.penalty])
|
|
319
|
-
for model in self.sub_models:
|
|
320
|
-
model.do_modeling(system_model)
|
|
321
|
-
|
|
322
|
-
self.add_share_between_effects()
|
|
323
|
-
|
|
324
|
-
self.objective = Equation('OBJECTIVE', 'OBJECTIVE', is_objective=True)
|
|
325
|
-
self.objective.add_summand(self._objective_effect_model.operation.sum, 1)
|
|
326
|
-
self.objective.add_summand(self._objective_effect_model.invest.sum, 1)
|
|
327
|
-
self.objective.add_summand(self.penalty.sum, 1)
|
|
328
|
-
|
|
329
|
-
@property
|
|
330
|
-
def _objective_effect_model(self) -> EffectModel:
|
|
331
|
-
return self._effect_models[self.element.objective_effect]
|
|
332
|
-
|
|
333
|
-
def _add_share_to_effects(
|
|
334
|
-
self,
|
|
335
|
-
name: str,
|
|
336
|
-
element: Element,
|
|
337
|
-
target: Literal['operation', 'invest'],
|
|
338
|
-
effect_values: EffectDict,
|
|
339
|
-
factor: Numeric,
|
|
340
|
-
variable: Optional[Variable] = None,
|
|
341
|
-
) -> None:
|
|
342
|
-
# an alle Effects, die einen Wert haben, anhängen:
|
|
343
|
-
for effect, value in effect_values.items():
|
|
344
|
-
if effect is None: # Falls None, dann Standard-effekt nutzen:
|
|
345
|
-
effect = self.element.standard_effect
|
|
346
|
-
assert effect in self.element.effects.values(), f'Effect {effect.label} was used but not added to model!'
|
|
347
|
-
|
|
348
|
-
if target == 'operation':
|
|
349
|
-
model = self._effect_models[effect].operation
|
|
350
|
-
elif target == 'invest':
|
|
351
|
-
model = self._effect_models[effect].invest
|
|
352
|
-
else:
|
|
353
|
-
raise ValueError(f'Target {target} not supported!')
|
|
354
|
-
|
|
355
|
-
name_of_share = f'{element.label_full}__{name}'
|
|
356
|
-
total_factor = np.multiply(value, factor)
|
|
357
|
-
model.add_share(self._system_model, name_of_share, variable, total_factor)
|
|
358
|
-
|
|
359
|
-
def add_share_to_invest(
|
|
360
|
-
self,
|
|
361
|
-
name: str,
|
|
362
|
-
element: Element,
|
|
363
|
-
effect_values: EffectDictInvest,
|
|
364
|
-
factor: Numeric,
|
|
365
|
-
variable: Optional[Variable] = None,
|
|
366
|
-
) -> None:
|
|
367
|
-
# TODO: Add checks
|
|
368
|
-
self._add_share_to_effects(name, element, 'invest', effect_values, factor, variable)
|
|
369
|
-
|
|
370
|
-
def add_share_to_operation(
|
|
371
|
-
self,
|
|
372
|
-
name: str,
|
|
373
|
-
element: Element,
|
|
374
|
-
effect_values: EffectTimeSeries,
|
|
375
|
-
factor: Numeric,
|
|
376
|
-
variable: Optional[Variable] = None,
|
|
377
|
-
) -> None:
|
|
378
|
-
# TODO: Add checks
|
|
379
|
-
self._add_share_to_effects(
|
|
380
|
-
name, element, 'operation', effect_values_from_effect_time_series(effect_values), factor, variable
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
def add_share_to_penalty(
|
|
384
|
-
self,
|
|
385
|
-
name: Optional[str],
|
|
386
|
-
variable: Variable,
|
|
387
|
-
factor: Numeric,
|
|
388
|
-
) -> None:
|
|
389
|
-
assert variable is not None, 'A Variable must be passed to add a share to penalty! Else its a constant Penalty!'
|
|
390
|
-
self.penalty.add_share(self._system_model, name, variable, factor, True)
|
|
391
|
-
|
|
392
|
-
def add_share_between_effects(self):
|
|
393
|
-
for origin_effect in self.element.effects.values():
|
|
394
|
-
# 1. operation: -> hier sind es Zeitreihen (share_TS)
|
|
395
|
-
for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items():
|
|
396
|
-
target_model = self._effect_models[target_effect].operation
|
|
397
|
-
origin_model = self._effect_models[origin_effect].operation
|
|
398
|
-
target_model.add_share(
|
|
399
|
-
self._system_model,
|
|
400
|
-
f'{origin_effect.label_full}_operation',
|
|
401
|
-
origin_model.sum_TS,
|
|
402
|
-
time_series.active_data,
|
|
403
|
-
)
|
|
404
|
-
# 2. invest: -> hier ist es Skalar (share)
|
|
405
|
-
for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items():
|
|
406
|
-
target_model = self._effect_models[target_effect].invest
|
|
407
|
-
origin_model = self._effect_models[origin_effect].invest
|
|
408
|
-
target_model.add_share(
|
|
409
|
-
self._system_model, f'{origin_effect.label_full}_invest', origin_model.sum, factor
|
|
410
|
-
)
|