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/math_modeling.py
DELETED
|
@@ -1,1145 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module contains the mathematical core of the flixOpt framework.
|
|
3
|
-
THe module is designed to be used by other modules than flixOpt itself.
|
|
4
|
-
It holds all necessary classes and functions to create a mathematical model, consisting of Varaibles and constraints,
|
|
5
|
-
and translate it into a ModelingLanguage like Pyomo, and the solve it through a solver.
|
|
6
|
-
Multiple solvers are supported.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
import re
|
|
11
|
-
import timeit
|
|
12
|
-
from abc import ABC, abstractmethod
|
|
13
|
-
from typing import Any, Dict, List, Literal, Optional, Union
|
|
14
|
-
|
|
15
|
-
import numpy as np
|
|
16
|
-
import pyomo.environ as pyo
|
|
17
|
-
|
|
18
|
-
from . import utils
|
|
19
|
-
from .core import Numeric
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger('flixOpt')
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Variable:
|
|
25
|
-
"""
|
|
26
|
-
Variable class
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
label: str,
|
|
32
|
-
length: int,
|
|
33
|
-
label_short: Optional[str] = None,
|
|
34
|
-
is_binary: bool = False,
|
|
35
|
-
fixed_value: Optional[Numeric] = None,
|
|
36
|
-
lower_bound: Optional[Numeric] = None,
|
|
37
|
-
upper_bound: Optional[Numeric] = None,
|
|
38
|
-
):
|
|
39
|
-
"""
|
|
40
|
-
label: full label of the variable
|
|
41
|
-
label_short: short label of the variable
|
|
42
|
-
|
|
43
|
-
# TODO: Allow for None values in fixed_value. If None, the index gets not fixed!
|
|
44
|
-
"""
|
|
45
|
-
self.label = label
|
|
46
|
-
self.label_short = label_short or label
|
|
47
|
-
self.length = length
|
|
48
|
-
self.is_binary = is_binary
|
|
49
|
-
self.fixed_value = fixed_value
|
|
50
|
-
self.lower_bound = lower_bound
|
|
51
|
-
self.upper_bound = upper_bound
|
|
52
|
-
|
|
53
|
-
self.indices = range(self.length)
|
|
54
|
-
self.fixed = False
|
|
55
|
-
|
|
56
|
-
self.result = None # Ergebnis-Speicher
|
|
57
|
-
|
|
58
|
-
if self.fixed_value is not None: # Check if value is within bounds, element-wise
|
|
59
|
-
above = self.lower_bound is None or np.all(np.asarray(self.fixed_value) >= np.asarray(self.lower_bound))
|
|
60
|
-
below = self.upper_bound is None or np.all(np.asarray(self.fixed_value) <= np.asarray(self.upper_bound))
|
|
61
|
-
if not (above and below):
|
|
62
|
-
raise Exception(
|
|
63
|
-
f'Fixed value of Variable {self.label} not inside set bounds:'
|
|
64
|
-
f'\n{self.fixed_value=};\n{self.lower_bound=};\n{self.upper_bound=}'
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
# Mark as fixed
|
|
68
|
-
self.fixed = True
|
|
69
|
-
|
|
70
|
-
logger.debug('Variable created: ' + self.label)
|
|
71
|
-
|
|
72
|
-
def description(self, max_length_ts=60) -> str:
|
|
73
|
-
bin_type = 'bin' if self.is_binary else ' '
|
|
74
|
-
|
|
75
|
-
header = f'Var {bin_type} x {self.length:<6} "{self.label}"'
|
|
76
|
-
if self.fixed:
|
|
77
|
-
description = f'{header:<40}: fixed={str(self.fixed_value)[:max_length_ts]:<10}'
|
|
78
|
-
else:
|
|
79
|
-
description = (
|
|
80
|
-
f'{header:<40}: min={str(self.lower_bound)[:max_length_ts]:<10}, '
|
|
81
|
-
f'max={str(self.upper_bound)[:max_length_ts]:<10}'
|
|
82
|
-
)
|
|
83
|
-
return description
|
|
84
|
-
|
|
85
|
-
def reset_result(self):
|
|
86
|
-
self.result = None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class VariableTS(Variable):
|
|
90
|
-
"""
|
|
91
|
-
Timeseries-Variable, optionally with previous_values. class for Variables that are related by time
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
def __init__(
|
|
95
|
-
self,
|
|
96
|
-
label: str,
|
|
97
|
-
length: int,
|
|
98
|
-
label_short: Optional[str] = None,
|
|
99
|
-
is_binary: bool = False,
|
|
100
|
-
fixed_value: Optional[Numeric] = None,
|
|
101
|
-
lower_bound: Optional[Numeric] = None,
|
|
102
|
-
upper_bound: Optional[Numeric] = None,
|
|
103
|
-
previous_values: Optional[Numeric] = None,
|
|
104
|
-
):
|
|
105
|
-
assert length > 1, 'length is one, that seems not right for VariableTS'
|
|
106
|
-
super().__init__(
|
|
107
|
-
label,
|
|
108
|
-
length,
|
|
109
|
-
label_short,
|
|
110
|
-
is_binary=is_binary,
|
|
111
|
-
fixed_value=fixed_value,
|
|
112
|
-
lower_bound=lower_bound,
|
|
113
|
-
upper_bound=upper_bound,
|
|
114
|
-
)
|
|
115
|
-
self.previous_values = previous_values
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class _Constraint:
|
|
119
|
-
"""
|
|
120
|
-
Abstract Class for Constraints. Use Child classes!
|
|
121
|
-
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
def __init__(self, label: str, label_short: Optional[str] = None):
|
|
125
|
-
"""
|
|
126
|
-
Equation of the form: ∑(<summands>) = <constant> type: 'eq'
|
|
127
|
-
Equation of the form: ∑(<summands>) <= <constant> type: 'ineq'
|
|
128
|
-
Equation of the form: ∑(<summands>) = <constant> type: 'objective'
|
|
129
|
-
|
|
130
|
-
Parameters
|
|
131
|
-
----------
|
|
132
|
-
label: full label of the variable
|
|
133
|
-
label_short: short label of the variable. If None, the the full label is used
|
|
134
|
-
"""
|
|
135
|
-
self.label = label
|
|
136
|
-
self.label_short = label_short or label
|
|
137
|
-
self.summands: List[SumOfSummand] = []
|
|
138
|
-
self.parts_of_constant: List[Numeric] = []
|
|
139
|
-
self.constant: Numeric = 0 # Total of right side
|
|
140
|
-
|
|
141
|
-
self.length = 1 # Anzahl der Gleichungen
|
|
142
|
-
|
|
143
|
-
logger.debug(f'Equation created: {self.label}')
|
|
144
|
-
|
|
145
|
-
def add_summand(
|
|
146
|
-
self,
|
|
147
|
-
variable: Variable,
|
|
148
|
-
factor: Numeric,
|
|
149
|
-
indices_of_variable: Optional[Union[int, np.ndarray, range, List[int]]] = None,
|
|
150
|
-
as_sum: bool = False,
|
|
151
|
-
) -> None:
|
|
152
|
-
"""
|
|
153
|
-
Adds a summand to the left side of the equation.
|
|
154
|
-
|
|
155
|
-
This method creates a summand from the given variable and factor, optionally summing over all given indices.
|
|
156
|
-
The summand is then added to the summands of the equation, which represent the left side.
|
|
157
|
-
|
|
158
|
-
Parameters:
|
|
159
|
-
-----------
|
|
160
|
-
variable : Variable
|
|
161
|
-
The variable to be used in the summand.
|
|
162
|
-
factor : Numeric
|
|
163
|
-
The factor by which the variable is multiplied.
|
|
164
|
-
indices_of_variable : Optional[Numeric], optional
|
|
165
|
-
Specific indices of the variable to be used. If not provided, all indices are used.
|
|
166
|
-
as_sum : bool, optional
|
|
167
|
-
If True, the summand is treated as a sum over all indices of the variable.
|
|
168
|
-
|
|
169
|
-
Raises:
|
|
170
|
-
-------
|
|
171
|
-
TypeError
|
|
172
|
-
If the provided variable is not an instance of the Variable class.
|
|
173
|
-
ValueError
|
|
174
|
-
If the variable is None and as_sum is True.
|
|
175
|
-
ValueError
|
|
176
|
-
If the length doesnt match the Equation's length.
|
|
177
|
-
"""
|
|
178
|
-
# TODO: Functionality to create A Sum of Summand over a specified range of indices? For Limiting stuff per one year...?
|
|
179
|
-
if not isinstance(variable, Variable):
|
|
180
|
-
raise TypeError(f'Error in Equation "{self.label}": no variable given (variable = "{variable}")')
|
|
181
|
-
if variable is None and as_sum:
|
|
182
|
-
raise ValueError(f'Error in Equation "{self.label}": Variable can not be None and be summed up!')
|
|
183
|
-
|
|
184
|
-
if np.isscalar(indices_of_variable): # Wenn nur ein Wert, dann Liste mit einem Eintrag drausmachen:
|
|
185
|
-
indices_of_variable = [indices_of_variable]
|
|
186
|
-
|
|
187
|
-
if as_sum:
|
|
188
|
-
summand = SumOfSummand(variable, factor, indices=indices_of_variable)
|
|
189
|
-
else:
|
|
190
|
-
summand = Summand(variable, factor, indices=indices_of_variable)
|
|
191
|
-
|
|
192
|
-
try:
|
|
193
|
-
self._update_length(summand.length) # Check Variablen-Länge:
|
|
194
|
-
except ValueError as e:
|
|
195
|
-
raise ValueError(
|
|
196
|
-
f'Length of Summand with variable "{variable.label}" does not fit equation "{self.label}": {e}'
|
|
197
|
-
) from e
|
|
198
|
-
self.summands.append(summand)
|
|
199
|
-
|
|
200
|
-
def add_constant(self, value: Numeric) -> None:
|
|
201
|
-
"""
|
|
202
|
-
Adds a constant value to the rigth side of the equation
|
|
203
|
-
|
|
204
|
-
Parameters
|
|
205
|
-
----------
|
|
206
|
-
value : float or array
|
|
207
|
-
constant-value of equation [A*x = constant] or [A*x <= constant]
|
|
208
|
-
|
|
209
|
-
Returns
|
|
210
|
-
-------
|
|
211
|
-
None.
|
|
212
|
-
|
|
213
|
-
Raises:
|
|
214
|
-
-------
|
|
215
|
-
ValueError
|
|
216
|
-
If the length doesnt match the Equation's length.
|
|
217
|
-
|
|
218
|
-
"""
|
|
219
|
-
self.constant = np.add(self.constant, value) # Adding to current constant
|
|
220
|
-
self.parts_of_constant.append(value) # Adding to parts of constants
|
|
221
|
-
|
|
222
|
-
length = 1 if np.isscalar(self.constant) else len(self.constant)
|
|
223
|
-
try:
|
|
224
|
-
self._update_length(length)
|
|
225
|
-
except ValueError as e:
|
|
226
|
-
raise ValueError(f'Length of Constant {value=} does not fit: {e}') from e
|
|
227
|
-
|
|
228
|
-
def description(self, at_index: int = 0) -> str:
|
|
229
|
-
raise NotImplementedError('Not implemented for Abstract class <_Constraint>')
|
|
230
|
-
|
|
231
|
-
def _update_length(self, new_length: int) -> None:
|
|
232
|
-
"""
|
|
233
|
-
Passes if the new_length is 1, the current length is 1 or new_length matches the existing length of the Equation
|
|
234
|
-
"""
|
|
235
|
-
if self.length == 1: # First Summand sets length
|
|
236
|
-
self.length = new_length
|
|
237
|
-
elif new_length == 1 or new_length == self.length: # Length 1 is always possible
|
|
238
|
-
pass
|
|
239
|
-
else:
|
|
240
|
-
raise ValueError(
|
|
241
|
-
f'The length of the new element {new_length=} doesnt match the existing '
|
|
242
|
-
f'length of the Equation {self.length=}!'
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
@property
|
|
246
|
-
def constant_vector(self) -> Numeric:
|
|
247
|
-
return utils.as_vector(self.constant, self.length)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
class Equation(_Constraint):
|
|
251
|
-
"""
|
|
252
|
-
Equation of the form: ∑(<summands>) = <constant>
|
|
253
|
-
Can be the Objective of a MathModel.
|
|
254
|
-
|
|
255
|
-
Parameters
|
|
256
|
-
----------
|
|
257
|
-
label : str
|
|
258
|
-
Full label of the variable.
|
|
259
|
-
label_short : str, optional
|
|
260
|
-
Short label of the variable. If None, the full label is used.
|
|
261
|
-
is_objective : bool, optional
|
|
262
|
-
Indicates if this equation is the objective of the model (default is False).
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
def __init__(self, label, label_short=None, is_objective=False):
|
|
266
|
-
super().__init__(label, label_short)
|
|
267
|
-
self.is_objective = is_objective
|
|
268
|
-
|
|
269
|
-
def description(self, at_index: int = 0) -> str:
|
|
270
|
-
equation_nr = min(at_index, self.length - 1)
|
|
271
|
-
|
|
272
|
-
# Name and index as str
|
|
273
|
-
if self.is_objective == 'objective':
|
|
274
|
-
name, index_str = 'OBJ', ''
|
|
275
|
-
else:
|
|
276
|
-
name, index_str = f'EQ {self.label}', f'[{equation_nr + 1}/{self.length}]'
|
|
277
|
-
|
|
278
|
-
# Summands:
|
|
279
|
-
summand_strings = [summand.description(at_index) for summand in self.summands]
|
|
280
|
-
all_summands_string = ' + '.join(summand_strings)
|
|
281
|
-
|
|
282
|
-
constant = self.constant_vector[equation_nr]
|
|
283
|
-
|
|
284
|
-
# String formating
|
|
285
|
-
header_width = 30
|
|
286
|
-
header = f'{name:<{header_width - len(index_str) - 1}} {index_str}'
|
|
287
|
-
return f'{header:<{header_width}}: {constant:>8} = {all_summands_string}'
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
class Inequation(_Constraint):
|
|
291
|
-
"""
|
|
292
|
-
Equation of the form: <constant> >= ∑(<summands>)
|
|
293
|
-
|
|
294
|
-
Parameters
|
|
295
|
-
----------
|
|
296
|
-
label: full label of the variable
|
|
297
|
-
label_short: short label of the variable. If None, the full label is used
|
|
298
|
-
"""
|
|
299
|
-
|
|
300
|
-
def __init__(self, label, label_short=None):
|
|
301
|
-
super().__init__(label, label_short)
|
|
302
|
-
|
|
303
|
-
def description(self, at_index: int = 0) -> str:
|
|
304
|
-
equation_nr = min(at_index, self.length - 1)
|
|
305
|
-
|
|
306
|
-
# Name and index as str
|
|
307
|
-
name, index_str = f'INEQ {self.label}', f'[{equation_nr + 1}/{self.length}]'
|
|
308
|
-
|
|
309
|
-
# Summands:
|
|
310
|
-
summand_strings = [summand.description(at_index) for summand in self.summands]
|
|
311
|
-
all_summands_string = ' + '.join(summand_strings)
|
|
312
|
-
|
|
313
|
-
constant = self.constant_vector[equation_nr]
|
|
314
|
-
|
|
315
|
-
# String formating
|
|
316
|
-
header_width = 30
|
|
317
|
-
header = f'{name:<{header_width - len(index_str) - 1}} {index_str}'
|
|
318
|
-
return f'{header:<{header_width}}: {constant:>8} >= {all_summands_string}'
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
class Summand:
|
|
322
|
-
"""
|
|
323
|
-
Represents a part of a Constraint , consisting of a variable (or a time-series variable) and a factor.
|
|
324
|
-
|
|
325
|
-
Parameters
|
|
326
|
-
----------
|
|
327
|
-
variable : Variable
|
|
328
|
-
The variable associated with this summand.
|
|
329
|
-
factor : Numeric
|
|
330
|
-
The factor by which the variable is multiplied in the equation.
|
|
331
|
-
indices : int, np.ndarray, range, List[int], optional
|
|
332
|
-
Specifies which indices of the variable to use. If None, all indices of the variable are used.
|
|
333
|
-
"""
|
|
334
|
-
|
|
335
|
-
def __init__(
|
|
336
|
-
self, variable: Variable, factor: Numeric, indices: Optional[Union[int, np.ndarray, range, List[int]]] = None
|
|
337
|
-
): # indices_of_variable default : alle
|
|
338
|
-
self.variable = variable
|
|
339
|
-
self.factor = factor
|
|
340
|
-
self.indices = indices if indices is not None else variable.indices # wenn nicht definiert, dann alle Indexe
|
|
341
|
-
|
|
342
|
-
self.length = self._check_length() # Länge ermitteln:
|
|
343
|
-
|
|
344
|
-
self.factor_vec = utils.as_vector(factor, self.length) # Faktor als Vektor:
|
|
345
|
-
|
|
346
|
-
def description(self, at_index=0):
|
|
347
|
-
i = 0 if self.length == 1 else at_index
|
|
348
|
-
index = self.indices[i]
|
|
349
|
-
factor = self.factor_vec[i]
|
|
350
|
-
factor_str = f'{factor:.6}' if isinstance(factor, (float, np.floating)) else str(factor)
|
|
351
|
-
return f'{factor_str} * {self.variable.label}[{index}]'
|
|
352
|
-
|
|
353
|
-
def _check_length(self):
|
|
354
|
-
"""
|
|
355
|
-
Determines and returns the length of the summand by comparing the lengths of the factor and the variable indices.
|
|
356
|
-
Sets the attribute .length to this value.
|
|
357
|
-
|
|
358
|
-
Returns:
|
|
359
|
-
--------
|
|
360
|
-
int
|
|
361
|
-
The length of the summand, which is the length of the indices if they match the length of the factor,
|
|
362
|
-
or the length of the longer one if one of them is a scalar.
|
|
363
|
-
|
|
364
|
-
Raises:
|
|
365
|
-
-------
|
|
366
|
-
Exception
|
|
367
|
-
If the lengths of the factor and the variable indices do not match and neither is a scalar.
|
|
368
|
-
"""
|
|
369
|
-
length_of_factor = 1 if np.isscalar(self.factor) else len(self.factor)
|
|
370
|
-
length_of_indices = len(self.indices)
|
|
371
|
-
if length_of_indices == length_of_factor:
|
|
372
|
-
return length_of_indices
|
|
373
|
-
elif length_of_factor == 1:
|
|
374
|
-
return length_of_indices
|
|
375
|
-
elif length_of_indices == 1:
|
|
376
|
-
return length_of_factor
|
|
377
|
-
else:
|
|
378
|
-
raise Exception(
|
|
379
|
-
f'Variable {self.variable.label} (length={length_of_indices}) und '
|
|
380
|
-
f'Faktor (length={length_of_factor}) müssen gleiche Länge haben oder Skalar sein'
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
class SumOfSummand(Summand):
|
|
385
|
-
"""
|
|
386
|
-
Represents a part of an Equation that sums all components of a regular Summand over specified indices.
|
|
387
|
-
|
|
388
|
-
Parameters
|
|
389
|
-
----------
|
|
390
|
-
variable : Variable
|
|
391
|
-
The variable associated with this summand.
|
|
392
|
-
factor : Numeric
|
|
393
|
-
The factor by which the variable is multiplied.
|
|
394
|
-
indices : int, np.ndarray, range, List[int], optional
|
|
395
|
-
Specifies which indices of the variable to use for the sum. If None, all indices are summed.
|
|
396
|
-
"""
|
|
397
|
-
|
|
398
|
-
def __init__(
|
|
399
|
-
self, variable: Variable, factor: Numeric, indices: Optional[Union[int, np.ndarray, range, List[int]]] = None
|
|
400
|
-
): # indices_of_variable default : alle
|
|
401
|
-
super().__init__(variable, factor, indices)
|
|
402
|
-
self.length = 1
|
|
403
|
-
|
|
404
|
-
def description(self, at_index=0):
|
|
405
|
-
index = self.indices[at_index]
|
|
406
|
-
factor = self.factor_vec[0]
|
|
407
|
-
factor_str = str(factor) if isinstance(factor, int) else f'{factor:.6}'
|
|
408
|
-
single_summand_str = f'{factor_str} * {self.variable.label}[{index}]'
|
|
409
|
-
return f'∑({("..+" if index > 0 else "")}{single_summand_str}{("+.." if index < self.variable.length else "")})'
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
class MathModel:
|
|
413
|
-
"""
|
|
414
|
-
A mathematical model for defining equations and constraints of the form:
|
|
415
|
-
|
|
416
|
-
a1 * x1 + a2 + x2 = y
|
|
417
|
-
and
|
|
418
|
-
a1 * x1 + a2 + x2 <= y
|
|
419
|
-
|
|
420
|
-
where 'a1', 'a2' and y can be vectors or scalars, while 'x1' and 'x2' are variables with an appropriate length.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
This class provides methods to add variables, equations, and inequality constraints to the model and supports
|
|
424
|
-
translation to a specified modeling language like pyomo.
|
|
425
|
-
|
|
426
|
-
The expression 'a1 * x1' is referred to as a 'Summand'. Supported summand formats are:
|
|
427
|
-
- 'Variable[j] * Factor[i]' : Multiplication of vector variables and vector factors.
|
|
428
|
-
- 'Variable[j] * Factor' : Vector variable with scalar factor.
|
|
429
|
-
- 'Variable * Factor' : Scalar variable with scalar factor.
|
|
430
|
-
- 'Factor' : Scalar constant.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
Parameters
|
|
434
|
-
----------
|
|
435
|
-
label : str
|
|
436
|
-
A descriptive label for the model.
|
|
437
|
-
modeling_language : {'pyomo', 'cvxpy'}, optional
|
|
438
|
-
Specifies the modeling language used for translation (default is 'pyomo').
|
|
439
|
-
|
|
440
|
-
Attributes
|
|
441
|
-
----------
|
|
442
|
-
label : str
|
|
443
|
-
The label assigned to the model.
|
|
444
|
-
modeling_language : str
|
|
445
|
-
The modeling language to which the model will be translated.
|
|
446
|
-
epsilon : float
|
|
447
|
-
Small tolerance value used in model calculations, defaulting to `1e-5`.
|
|
448
|
-
solver : Optional[Solver]
|
|
449
|
-
The solver instance assigned to solve the model.
|
|
450
|
-
model : Optional[ModelingLanguage]
|
|
451
|
-
The model instance in the specified modeling language.
|
|
452
|
-
_variables : List[Variable]
|
|
453
|
-
List of variables added to the model.
|
|
454
|
-
_constraints : List[Union[Equation, Inequation]]
|
|
455
|
-
List of equations and inequality constraints in the model.
|
|
456
|
-
_objective : Optional[Equation]
|
|
457
|
-
The objective function, if defined as an equation.
|
|
458
|
-
duration : dict
|
|
459
|
-
Dictionary tracking the time taken for translation and solving steps.
|
|
460
|
-
|
|
461
|
-
Methods
|
|
462
|
-
-------
|
|
463
|
-
add(*args)
|
|
464
|
-
Adds variables, equations, or inequations to the model.
|
|
465
|
-
describe_size()
|
|
466
|
-
Provides a summary of the number of equations, inequations, and variables.
|
|
467
|
-
translate_to_modeling_language()
|
|
468
|
-
Translates the model to the specified modeling language.
|
|
469
|
-
solve(solver)
|
|
470
|
-
Solves the model using the specified solver instance.
|
|
471
|
-
results()
|
|
472
|
-
Returns a dictionary of variable results after solving.
|
|
473
|
-
"""
|
|
474
|
-
|
|
475
|
-
def __init__(self, label: str, modeling_language: Literal['pyomo', 'cvxpy'] = 'pyomo'):
|
|
476
|
-
self._infos = {}
|
|
477
|
-
self.label = label
|
|
478
|
-
self.modeling_language: str = modeling_language
|
|
479
|
-
|
|
480
|
-
self.solver: Optional[Solver] = None
|
|
481
|
-
self.model: Optional[ModelingLanguage] = None
|
|
482
|
-
|
|
483
|
-
self._variables: List[Variable] = []
|
|
484
|
-
self._constraints: List[Union[Equation, Inequation]] = []
|
|
485
|
-
self._objective: Optional[Equation] = None
|
|
486
|
-
self.result_of_objective: Optional[float] = None
|
|
487
|
-
|
|
488
|
-
self.duration = {}
|
|
489
|
-
|
|
490
|
-
def add(self, *args: Union[Variable, Equation, Inequation]) -> None:
|
|
491
|
-
if not isinstance(args, list):
|
|
492
|
-
args = list(args)
|
|
493
|
-
for arg in args:
|
|
494
|
-
if isinstance(arg, Variable):
|
|
495
|
-
self._variables.append(arg)
|
|
496
|
-
elif isinstance(arg, (Equation, Inequation)):
|
|
497
|
-
if isinstance(arg, Equation) and arg.is_objective:
|
|
498
|
-
self._objective = arg
|
|
499
|
-
else:
|
|
500
|
-
self._constraints.append(arg)
|
|
501
|
-
else:
|
|
502
|
-
raise Exception(f'{arg} cant be added this way!')
|
|
503
|
-
|
|
504
|
-
def describe_size(self) -> str:
|
|
505
|
-
return (
|
|
506
|
-
f'No. of Equations (single): {self.nr_of_equations} ({self.nr_of_single_equations})\n'
|
|
507
|
-
f'No. of Inequations (single): {self.nr_of_inequations} ({self.nr_of_single_inequations})\n'
|
|
508
|
-
f'No. of Variables (single): {self.nr_of_variables} ({self.nr_of_single_variables})'
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
def translate_to_modeling_language(self) -> None:
|
|
512
|
-
t_start = timeit.default_timer()
|
|
513
|
-
if self.modeling_language == 'pyomo':
|
|
514
|
-
self.model = PyomoModel()
|
|
515
|
-
self.model.translate_model(self)
|
|
516
|
-
else:
|
|
517
|
-
raise NotImplementedError('Modeling Language cvxpy is not yet implemented')
|
|
518
|
-
self.duration['Translation'] = round(timeit.default_timer() - t_start, 2)
|
|
519
|
-
|
|
520
|
-
def solve(self, solver: 'Solver') -> None:
|
|
521
|
-
self.solver = solver
|
|
522
|
-
t_start = timeit.default_timer()
|
|
523
|
-
for variable in self.variables:
|
|
524
|
-
variable.reset_result() # altes Ergebnis löschen (falls vorhanden)
|
|
525
|
-
self.model.solve(self, solver)
|
|
526
|
-
self.duration['Solving'] = round(timeit.default_timer() - t_start, 2)
|
|
527
|
-
|
|
528
|
-
def results(self) -> Dict[str, Numeric]:
|
|
529
|
-
return {variable.label: variable.result for variable in self.variables}
|
|
530
|
-
|
|
531
|
-
@property
|
|
532
|
-
def infos(self) -> Dict:
|
|
533
|
-
return {
|
|
534
|
-
'Solver': repr(self.solver),
|
|
535
|
-
'Model Size': {
|
|
536
|
-
'No. of Eqs.': self.nr_of_equations,
|
|
537
|
-
'No. of Eqs. (single)': self.nr_of_single_equations,
|
|
538
|
-
'No. of Ineqs.': self.nr_of_inequations,
|
|
539
|
-
'No. of Ineqs. (single)': self.nr_of_single_inequations,
|
|
540
|
-
'No. of Vars.': self.nr_of_variables,
|
|
541
|
-
'No. of Vars. (single)': self.nr_of_single_variables,
|
|
542
|
-
'No. of Vars. (TS)': len(self.ts_variables),
|
|
543
|
-
},
|
|
544
|
-
'Solver Log': self.solver.log.infos if isinstance(self.solver.log, SolverLog) else self.solver.log,
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
@property
|
|
548
|
-
def variables(self) -> List[Variable]:
|
|
549
|
-
return self._variables
|
|
550
|
-
|
|
551
|
-
@property
|
|
552
|
-
def equations(self) -> List[Equation]:
|
|
553
|
-
return [eq for eq in self._constraints if isinstance(eq, Equation)]
|
|
554
|
-
|
|
555
|
-
@property
|
|
556
|
-
def inequations(self):
|
|
557
|
-
return [eq for eq in self._constraints if isinstance(eq, Inequation)]
|
|
558
|
-
|
|
559
|
-
@property
|
|
560
|
-
def objective(self) -> Equation:
|
|
561
|
-
return self._objective
|
|
562
|
-
|
|
563
|
-
@property
|
|
564
|
-
def ts_variables(self) -> List[VariableTS]:
|
|
565
|
-
return [variable for variable in self.variables if isinstance(variable, VariableTS)]
|
|
566
|
-
|
|
567
|
-
@property
|
|
568
|
-
def nr_of_variables(self) -> int:
|
|
569
|
-
return len(self.variables)
|
|
570
|
-
|
|
571
|
-
@property
|
|
572
|
-
def nr_of_constraints(self) -> int:
|
|
573
|
-
return len(self._constraints)
|
|
574
|
-
|
|
575
|
-
@property
|
|
576
|
-
def nr_of_equations(self) -> int:
|
|
577
|
-
return len(self.equations)
|
|
578
|
-
|
|
579
|
-
@property
|
|
580
|
-
def nr_of_inequations(self) -> int:
|
|
581
|
-
return len(self.inequations)
|
|
582
|
-
|
|
583
|
-
@property
|
|
584
|
-
def nr_of_single_variables(self) -> int:
|
|
585
|
-
return sum([var.length for var in self.variables])
|
|
586
|
-
|
|
587
|
-
@property
|
|
588
|
-
def nr_of_single_equations(self) -> int:
|
|
589
|
-
return sum([eq.length for eq in self.equations])
|
|
590
|
-
|
|
591
|
-
@property
|
|
592
|
-
def nr_of_single_inequations(self) -> int:
|
|
593
|
-
return sum([eq.length for eq in self.inequations])
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
class SolverLog:
|
|
597
|
-
"""
|
|
598
|
-
Parses and holds solver log information for specific solvers.
|
|
599
|
-
|
|
600
|
-
Attributes:
|
|
601
|
-
solver_name (str): Name of the solver (e.g., 'gurobi', 'cbc').
|
|
602
|
-
log (str): Content of the log file.
|
|
603
|
-
presolved_rows (Optional[int]): Number of rows after presolving.
|
|
604
|
-
presolved_cols (Optional[int]): Number of columns after presolving.
|
|
605
|
-
presolved_nonzeros (Optional[int]): Number of nonzeros after presolving.
|
|
606
|
-
presolved_continuous (Optional[int]): Number of continuous variables after presolving.
|
|
607
|
-
presolved_integer (Optional[int]): Number of integer variables after presolving.
|
|
608
|
-
presolved_binary (Optional[int]): Number of binary variables after presolving.
|
|
609
|
-
"""
|
|
610
|
-
|
|
611
|
-
def __init__(self, solver_name: str, filename: str):
|
|
612
|
-
with open(filename, 'r') as file:
|
|
613
|
-
self.log = file.read()
|
|
614
|
-
|
|
615
|
-
self.solver_name = solver_name
|
|
616
|
-
|
|
617
|
-
self.presolved_rows = None
|
|
618
|
-
self.presolved_cols = None
|
|
619
|
-
self.presolved_nonzeros = None
|
|
620
|
-
|
|
621
|
-
self.presolved_continuous = None
|
|
622
|
-
self.presolved_integer = None
|
|
623
|
-
self.presolved_binary = None
|
|
624
|
-
self.parse_infos()
|
|
625
|
-
|
|
626
|
-
@property
|
|
627
|
-
def infos(self) -> Dict[str, Dict[str, int]]:
|
|
628
|
-
return {
|
|
629
|
-
'presolved': {
|
|
630
|
-
'cols': self.presolved_cols,
|
|
631
|
-
'continuous': self.presolved_continuous,
|
|
632
|
-
'integer': self.presolved_integer,
|
|
633
|
-
'binary': self.presolved_binary,
|
|
634
|
-
'rows': self.presolved_rows,
|
|
635
|
-
'nonzeros': self.presolved_nonzeros,
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
# Suche infos aus log:
|
|
640
|
-
def parse_infos(self):
|
|
641
|
-
if self.solver_name == 'gurobi':
|
|
642
|
-
# string-Schnipsel 1:
|
|
643
|
-
"""
|
|
644
|
-
Optimize a model with 285 rows, 292 columns and 878 nonzeros
|
|
645
|
-
Model fingerprint: 0x1756ffd1
|
|
646
|
-
Variable types: 202 continuous, 90 integer (90 binary)
|
|
647
|
-
"""
|
|
648
|
-
# string-Schnipsel 2:
|
|
649
|
-
"""
|
|
650
|
-
Presolve removed 154 rows and 172 columns
|
|
651
|
-
Presolve time: 0.00s
|
|
652
|
-
Presolved: 131 rows, 120 columns, 339 nonzeros
|
|
653
|
-
Variable types: 53 continuous, 67 integer (67 binary)
|
|
654
|
-
"""
|
|
655
|
-
# string: Presolved: 131 rows, 120 columns, 339 nonzeros\n
|
|
656
|
-
match = re.search(
|
|
657
|
-
r'Presolved: (\d+) rows, (\d+) columns, (\d+) nonzeros\n'
|
|
658
|
-
r'Variable types: (\d+) continuous, (\d+) integer \((\d+) binary\)',
|
|
659
|
-
self.log,
|
|
660
|
-
)
|
|
661
|
-
if match:
|
|
662
|
-
# string: Presolved: 131 rows, 120 columns, 339 nonzeros\n
|
|
663
|
-
self.presolved_rows = int(match.group(1))
|
|
664
|
-
self.presolved_cols = int(match.group(2))
|
|
665
|
-
self.presolved_nonzeros = int(match.group(3))
|
|
666
|
-
# string: Variable types: 53 continuous, 67 integer (67 binary)
|
|
667
|
-
self.presolved_continuous = int(match.group(4))
|
|
668
|
-
self.presolved_integer = int(match.group(5))
|
|
669
|
-
self.presolved_binary = int(match.group(6))
|
|
670
|
-
|
|
671
|
-
elif self.solver_name == 'cbc':
|
|
672
|
-
# string: Presolve 1623 (-1079) rows, 1430 (-1078) columns and 4296 (-3306) elements
|
|
673
|
-
match = re.search(r'Presolve (\d+) \((-?\d+)\) rows, (\d+) \((-?\d+)\) columns and (\d+)', self.log)
|
|
674
|
-
if match is not None:
|
|
675
|
-
self.presolved_rows = int(match.group(1))
|
|
676
|
-
self.presolved_cols = int(match.group(3))
|
|
677
|
-
self.presolved_nonzeros = int(match.group(5))
|
|
678
|
-
|
|
679
|
-
# string: Presolved problem has 862 integers (862 of which binary)
|
|
680
|
-
match = re.search(r'Presolved problem has (\d+) integers \((\d+) of which binary\)', self.log)
|
|
681
|
-
if match is not None:
|
|
682
|
-
self.presolved_integer = int(match.group(1))
|
|
683
|
-
self.presolved_binary = int(match.group(2))
|
|
684
|
-
self.presolved_continuous = self.presolved_cols - self.presolved_integer
|
|
685
|
-
|
|
686
|
-
elif self.solver_name == 'glpk':
|
|
687
|
-
logger.warning(f'{"":#^80}\n')
|
|
688
|
-
logger.warning(f'{" No solver-log parsing implemented for glpk yet! ":#^80}\n')
|
|
689
|
-
else:
|
|
690
|
-
raise Exception('SolverLog.parse_infos() is not defined for solver ' + self.solver_name)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
class Solver(ABC):
|
|
694
|
-
"""
|
|
695
|
-
Abstract base class for solvers.
|
|
696
|
-
|
|
697
|
-
Attributes:
|
|
698
|
-
mip_gap (float): Solver's mip gap setting. The MIP gap describes the accepted (MILP) objective,
|
|
699
|
-
and the lower bound, which is the theoretically optimal solution (LP)
|
|
700
|
-
solver_output_to_console (bool): Whether to display solver output.
|
|
701
|
-
logfile_name (str): Filename for saving the solver log.
|
|
702
|
-
objective (Optional[float]): Objective value from the solution.
|
|
703
|
-
best_bound (Optional[float]): Best bound from the solver.
|
|
704
|
-
termination_message (Optional[str]): Solver's termination message.
|
|
705
|
-
"""
|
|
706
|
-
|
|
707
|
-
def __init__(
|
|
708
|
-
self,
|
|
709
|
-
mip_gap: float,
|
|
710
|
-
solver_output_to_console: bool,
|
|
711
|
-
logfile_name: str,
|
|
712
|
-
):
|
|
713
|
-
self.mip_gap = mip_gap
|
|
714
|
-
self.solver_output_to_console = solver_output_to_console
|
|
715
|
-
self.logfile_name = logfile_name
|
|
716
|
-
|
|
717
|
-
self.objective: Optional[float] = None
|
|
718
|
-
self.best_bound: Optional[float] = None
|
|
719
|
-
self.termination_message: Optional[str] = None
|
|
720
|
-
self.log: Optional[str, SolverLog] = None
|
|
721
|
-
|
|
722
|
-
self._solver = None
|
|
723
|
-
self._results: Optional[float, str] = None
|
|
724
|
-
|
|
725
|
-
@abstractmethod
|
|
726
|
-
def solve(self, modeling_language: 'ModelingLanguage'):
|
|
727
|
-
raise NotImplementedError(' Solving is not possible with this Abstract class')
|
|
728
|
-
|
|
729
|
-
def __repr__(self):
|
|
730
|
-
return (
|
|
731
|
-
f'{self.__class__.__name__}('
|
|
732
|
-
f'mip_gap={self.mip_gap}, '
|
|
733
|
-
f'solver_output_to_console={self.solver_output_to_console}, '
|
|
734
|
-
f"logfile_name='{self.logfile_name}', "
|
|
735
|
-
f'objective={self.objective!r}, '
|
|
736
|
-
f'best_bound={self.best_bound!r}, '
|
|
737
|
-
f'termination_message={self.termination_message!r})'
|
|
738
|
-
)
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
class GurobiSolver(Solver):
|
|
742
|
-
"""
|
|
743
|
-
Solver implementation for Gurobi.
|
|
744
|
-
Also Look in class Solver for more details
|
|
745
|
-
|
|
746
|
-
Attributes:
|
|
747
|
-
time_limit_seconds (int): Time limit for the solver. After this time, the solver takes the currently
|
|
748
|
-
best solution, ignoring the mip_gap.
|
|
749
|
-
"""
|
|
750
|
-
|
|
751
|
-
def __init__(
|
|
752
|
-
self,
|
|
753
|
-
mip_gap: float = 0.01,
|
|
754
|
-
time_limit_seconds: int = 300,
|
|
755
|
-
logfile_name: str = 'gurobi.log',
|
|
756
|
-
solver_output_to_console: bool = True,
|
|
757
|
-
):
|
|
758
|
-
super().__init__(mip_gap, solver_output_to_console, logfile_name)
|
|
759
|
-
self.time_limit_seconds = time_limit_seconds
|
|
760
|
-
|
|
761
|
-
def solve(self, modeling_language: 'ModelingLanguage'):
|
|
762
|
-
if isinstance(modeling_language, PyomoModel):
|
|
763
|
-
self._solver = pyo.SolverFactory('gurobi')
|
|
764
|
-
self._results = self._solver.solve(
|
|
765
|
-
modeling_language.model,
|
|
766
|
-
tee=self.solver_output_to_console,
|
|
767
|
-
keepfiles=True,
|
|
768
|
-
logfile=self.logfile_name,
|
|
769
|
-
options={'mipgap': self.mip_gap, 'TimeLimit': self.time_limit_seconds},
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
self.objective = modeling_language.model.objective.expr()
|
|
773
|
-
self.termination_message = self._results.solver.termination_message
|
|
774
|
-
self.best_bound = self._results.problem.lower_bound
|
|
775
|
-
|
|
776
|
-
from pyomo.opt import SolverStatus, TerminationCondition
|
|
777
|
-
|
|
778
|
-
if not (
|
|
779
|
-
self._results.solver.status == SolverStatus.ok
|
|
780
|
-
and self._results.solver.termination_condition == TerminationCondition.optimal
|
|
781
|
-
):
|
|
782
|
-
logger.warning(
|
|
783
|
-
f'Solver ended with status {self._results.solver.status} and '
|
|
784
|
-
f'termination condition {self._results.solver.termination_condition}'
|
|
785
|
-
)
|
|
786
|
-
try:
|
|
787
|
-
self.log = SolverLog('gurobi', self.logfile_name)
|
|
788
|
-
except Exception as e:
|
|
789
|
-
self.log = None
|
|
790
|
-
logger.warning(f'SolverLog could not be loaded. {e}')
|
|
791
|
-
|
|
792
|
-
try:
|
|
793
|
-
import gurobi_logtools
|
|
794
|
-
|
|
795
|
-
self.log = gurobi_logtools.get_dataframe([str(self.logfile_name)]).T.to_dict()[0]
|
|
796
|
-
except ImportError:
|
|
797
|
-
logger.info(
|
|
798
|
-
'Evaluationg the gurobi log after the solve was not possible, due to a missing dependency '
|
|
799
|
-
'"gurobi_logtools". For further details of the solving process, '
|
|
800
|
-
'install the dependency via "pip install gurobi_logtools".'
|
|
801
|
-
)
|
|
802
|
-
else:
|
|
803
|
-
raise NotImplementedError('Only Pyomo is implemented for GUROBI solver.')
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
class CplexSolver(Solver):
|
|
807
|
-
"""
|
|
808
|
-
Solver implementation for CPLEX.
|
|
809
|
-
Also Look in class Solver for more details
|
|
810
|
-
|
|
811
|
-
Attributes:
|
|
812
|
-
time_limit_seconds (int): Time limit for the solver. After this time, the solver takes the currently
|
|
813
|
-
best solution, ignoring the mip_gap.
|
|
814
|
-
"""
|
|
815
|
-
|
|
816
|
-
def __init__(
|
|
817
|
-
self,
|
|
818
|
-
mip_gap: float = 0.01,
|
|
819
|
-
time_limit_seconds: int = 300,
|
|
820
|
-
logfile_name: str = 'cplex.log',
|
|
821
|
-
solver_output_to_console: bool = True,
|
|
822
|
-
):
|
|
823
|
-
super().__init__(mip_gap, solver_output_to_console, logfile_name)
|
|
824
|
-
self.time_limit_seconds = time_limit_seconds
|
|
825
|
-
|
|
826
|
-
def solve(self, modeling_language: 'ModelingLanguage'):
|
|
827
|
-
if isinstance(modeling_language, PyomoModel):
|
|
828
|
-
self._solver = pyo.SolverFactory('cplex')
|
|
829
|
-
self._results = self._solver.solve(
|
|
830
|
-
modeling_language.model,
|
|
831
|
-
tee=self.solver_output_to_console,
|
|
832
|
-
keepfiles=True,
|
|
833
|
-
logfile=self.logfile_name,
|
|
834
|
-
options={'mipgap': self.mip_gap, 'timelimit': self.time_limit_seconds},
|
|
835
|
-
)
|
|
836
|
-
|
|
837
|
-
self.objective = modeling_language.model.objective.expr()
|
|
838
|
-
self.termination_message: Optional[str] = f'Not Implemented for {self.__class__.__name__} yet'
|
|
839
|
-
self.best_bound = self._results['Problem'][0]['Lower bound']
|
|
840
|
-
self.log = f'Not Implemented for {self.__class__.__name__} yet'
|
|
841
|
-
else:
|
|
842
|
-
raise NotImplementedError('Only Pyomo is implemented for CPLEX solver.')
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
class HighsSolver(Solver):
|
|
846
|
-
"""
|
|
847
|
-
Solver implementation for HIGHS.
|
|
848
|
-
Also Look in class Solver for more details
|
|
849
|
-
|
|
850
|
-
Attributes:
|
|
851
|
-
time_limit_seconds (int): Time limit for the solver. After this time, the solver takes the currently
|
|
852
|
-
best solution, ignoring the mip_gap.
|
|
853
|
-
threads (int): Number of threads to use for the solver.
|
|
854
|
-
"""
|
|
855
|
-
|
|
856
|
-
def __init__(
|
|
857
|
-
self,
|
|
858
|
-
mip_gap: float = 0.01,
|
|
859
|
-
time_limit_seconds: int = 300,
|
|
860
|
-
logfile_name: str = 'highs.log',
|
|
861
|
-
solver_output_to_console: bool = True,
|
|
862
|
-
threads: int = 4,
|
|
863
|
-
):
|
|
864
|
-
super().__init__(mip_gap, solver_output_to_console, logfile_name)
|
|
865
|
-
self.time_limit_seconds = time_limit_seconds
|
|
866
|
-
self.threads = threads
|
|
867
|
-
|
|
868
|
-
def solve(self, modeling_language: 'ModelingLanguage'):
|
|
869
|
-
if isinstance(modeling_language, PyomoModel):
|
|
870
|
-
from pyomo.contrib import appsi
|
|
871
|
-
|
|
872
|
-
self._solver = appsi.solvers.Highs()
|
|
873
|
-
self._solver.highs_options = {
|
|
874
|
-
'mip_rel_gap': self.mip_gap,
|
|
875
|
-
'time_limit': self.time_limit_seconds,
|
|
876
|
-
'log_file': str(self.logfile_name),
|
|
877
|
-
# "log_to_console": self.solver_output_to_console,
|
|
878
|
-
'threads': self.threads,
|
|
879
|
-
'parallel': 'on',
|
|
880
|
-
'presolve': 'on',
|
|
881
|
-
'output_flag': True,
|
|
882
|
-
}
|
|
883
|
-
self._solver.config.stream_solver = True
|
|
884
|
-
|
|
885
|
-
self._results = self._solver.solve(
|
|
886
|
-
modeling_language.model
|
|
887
|
-
) # HiGHS writes logs to stdout/stderr, so we capture them here
|
|
888
|
-
|
|
889
|
-
self.objective = modeling_language.model.objective.expr()
|
|
890
|
-
self.termination_message: Optional[str] = self._results.termination_condition.name
|
|
891
|
-
if not self.termination_message == 'optimal':
|
|
892
|
-
logger.warning(f'Solution is not optimal. Termination Message: "{self.termination_message}"')
|
|
893
|
-
self.best_bound = self._results.best_objective_bound
|
|
894
|
-
self.log = f'Not Implemented for {self.__class__.__name__} yet'
|
|
895
|
-
else:
|
|
896
|
-
raise NotImplementedError('Only Pyomo is implemented for HIGHS solver.')
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
class CbcSolver(Solver):
|
|
900
|
-
"""
|
|
901
|
-
Solver implementation for CBC.
|
|
902
|
-
Also Look in class Solver for more details
|
|
903
|
-
|
|
904
|
-
Attributes:
|
|
905
|
-
time_limit_seconds (int): Time limit for the solver. After this time, the solver takes the currently
|
|
906
|
-
best solution, ignoring the mip_gap.
|
|
907
|
-
"""
|
|
908
|
-
|
|
909
|
-
def __init__(
|
|
910
|
-
self,
|
|
911
|
-
mip_gap: float = 0.01,
|
|
912
|
-
time_limit_seconds: int = 300,
|
|
913
|
-
logfile_name: str = 'cbc.log',
|
|
914
|
-
solver_output_to_console: bool = True,
|
|
915
|
-
):
|
|
916
|
-
super().__init__(mip_gap, solver_output_to_console, logfile_name)
|
|
917
|
-
self.time_limit_seconds = time_limit_seconds
|
|
918
|
-
|
|
919
|
-
def solve(self, modeling_language: 'ModelingLanguage'):
|
|
920
|
-
if isinstance(modeling_language, PyomoModel):
|
|
921
|
-
self._solver = pyo.SolverFactory('cbc')
|
|
922
|
-
self._results = self._solver.solve(
|
|
923
|
-
modeling_language.model,
|
|
924
|
-
tee=self.solver_output_to_console,
|
|
925
|
-
keepfiles=True,
|
|
926
|
-
logfile=self.logfile_name,
|
|
927
|
-
options={'ratio': self.mip_gap, 'sec': self.time_limit_seconds},
|
|
928
|
-
)
|
|
929
|
-
self.objective = modeling_language.model.objective.expr()
|
|
930
|
-
self.termination_message: Optional[str] = f'Not Implemented for {self.__class__.__name__} yet'
|
|
931
|
-
self.best_bound = self._results['Problem'][0]['Lower bound']
|
|
932
|
-
self.log = f'Not Implemented for {self.__class__.__name__} yet'
|
|
933
|
-
else:
|
|
934
|
-
raise NotImplementedError('Only Pyomo is implemented for Cbc solver.')
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
class GlpkSolver(Solver):
|
|
938
|
-
"""Solver implementation for Glpk. Also Look in class Solver for more details"""
|
|
939
|
-
|
|
940
|
-
def __init__(
|
|
941
|
-
self,
|
|
942
|
-
mip_gap: float = 0.01,
|
|
943
|
-
logfile_name: str = 'glpk.log',
|
|
944
|
-
solver_output_to_console: bool = True,
|
|
945
|
-
):
|
|
946
|
-
super().__init__(mip_gap, solver_output_to_console, logfile_name)
|
|
947
|
-
|
|
948
|
-
def solve(self, modeling_language: 'ModelingLanguage'):
|
|
949
|
-
if isinstance(modeling_language, PyomoModel):
|
|
950
|
-
self._solver = pyo.SolverFactory('glpk')
|
|
951
|
-
self._results = self._solver.solve(
|
|
952
|
-
modeling_language.model,
|
|
953
|
-
tee=self.solver_output_to_console,
|
|
954
|
-
keepfiles=True,
|
|
955
|
-
logfile=self.logfile_name,
|
|
956
|
-
options={'mipgap': self.mip_gap},
|
|
957
|
-
)
|
|
958
|
-
|
|
959
|
-
self.objective = modeling_language.model.objective.expr()
|
|
960
|
-
self.termination_message = self._results['Solver'][0]['Status']
|
|
961
|
-
self.best_bound = self._results['Problem'][0]['Lower bound']
|
|
962
|
-
try:
|
|
963
|
-
self.log = SolverLog('glpk', self.logfile_name)
|
|
964
|
-
except Exception as e:
|
|
965
|
-
self.log = None
|
|
966
|
-
logger.warning(f'SolverLog could not be loaded. {e}')
|
|
967
|
-
else:
|
|
968
|
-
raise NotImplementedError('Only Pyomo is implemented for Cbc solver.')
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
class ModelingLanguage(ABC):
|
|
972
|
-
"""
|
|
973
|
-
Abstract base class for modeling languages.
|
|
974
|
-
|
|
975
|
-
Methods:
|
|
976
|
-
translate_model(model): Translates a math model into a solveable form.
|
|
977
|
-
"""
|
|
978
|
-
|
|
979
|
-
@abstractmethod
|
|
980
|
-
def translate_model(self, model: MathModel):
|
|
981
|
-
raise NotImplementedError
|
|
982
|
-
|
|
983
|
-
def solve(self, math_model: MathModel, solver: Solver):
|
|
984
|
-
raise NotImplementedError
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
class PyomoModel(ModelingLanguage):
|
|
988
|
-
"""
|
|
989
|
-
Pyomo-based modeling language for constructing and solving optimization models.
|
|
990
|
-
Translates a MathModel into a PyomoModel.
|
|
991
|
-
|
|
992
|
-
Attributes:
|
|
993
|
-
model: Pyomo model instance.
|
|
994
|
-
mapping (dict): Maps variables and equations to Pyomo components.
|
|
995
|
-
_counter (int): Counter for naming Pyomo components.
|
|
996
|
-
"""
|
|
997
|
-
|
|
998
|
-
def __init__(self):
|
|
999
|
-
logger.debug('Loaded pyomo modules')
|
|
1000
|
-
|
|
1001
|
-
self.model = pyo.ConcreteModel(name='(Minimalbeispiel)')
|
|
1002
|
-
|
|
1003
|
-
self.mapping: Dict[Union[Variable, Equation], Any] = {} # Mapping to Pyomo Units
|
|
1004
|
-
self._counter = 0
|
|
1005
|
-
|
|
1006
|
-
def solve(self, math_model: MathModel, solver: Solver):
|
|
1007
|
-
if self._counter == 0:
|
|
1008
|
-
raise Exception(' First, call .translate_model(). Else PyomoModel cant solve()')
|
|
1009
|
-
solver.solve(self)
|
|
1010
|
-
|
|
1011
|
-
# write results
|
|
1012
|
-
math_model.result_of_objective = self.model.objective.expr()
|
|
1013
|
-
for variable in math_model.variables:
|
|
1014
|
-
raw_results = self.mapping[variable].get_values().values() # .values() of dict, because {0:0.1, 1:0.3,...}
|
|
1015
|
-
if variable.is_binary:
|
|
1016
|
-
dtype = np.int8 # geht das vielleicht noch kleiner ???
|
|
1017
|
-
else:
|
|
1018
|
-
dtype = float
|
|
1019
|
-
# transform to np-array (fromiter() is 5-7x faster than np.array(list(...)) )
|
|
1020
|
-
result = np.fromiter(raw_results, dtype=dtype)
|
|
1021
|
-
# Falls skalar:
|
|
1022
|
-
if len(result) == 1:
|
|
1023
|
-
variable.result = result[0]
|
|
1024
|
-
else:
|
|
1025
|
-
variable.result = result
|
|
1026
|
-
|
|
1027
|
-
def translate_model(self, math_model: MathModel):
|
|
1028
|
-
for variable in math_model.variables: # Variablen erstellen
|
|
1029
|
-
logger.debug(f'VAR {variable.label} gets translated to Pyomo')
|
|
1030
|
-
self.translate_variable(variable)
|
|
1031
|
-
for eq in math_model.equations: # Gleichungen erstellen
|
|
1032
|
-
logger.debug(f'EQ {eq.label} gets translated to Pyomo')
|
|
1033
|
-
self.translate_equation(eq)
|
|
1034
|
-
for ineq in math_model.inequations: # Ungleichungen erstellen:
|
|
1035
|
-
logger.debug(f'INEQ {ineq.label} gets translated to Pyomo')
|
|
1036
|
-
self.translate_inequation(ineq)
|
|
1037
|
-
|
|
1038
|
-
obj = math_model.objective
|
|
1039
|
-
logger.debug(f'{obj.label} gets translated to Pyomo')
|
|
1040
|
-
self.translate_objective(obj)
|
|
1041
|
-
|
|
1042
|
-
def translate_variable(self, variable: Variable):
|
|
1043
|
-
assert isinstance(variable, Variable), 'Wrong type of variable'
|
|
1044
|
-
|
|
1045
|
-
if variable.is_binary:
|
|
1046
|
-
pyomo_comp = pyo.Var(variable.indices, domain=pyo.Binary)
|
|
1047
|
-
else:
|
|
1048
|
-
pyomo_comp = pyo.Var(variable.indices, within=pyo.Reals)
|
|
1049
|
-
self.mapping[variable] = pyomo_comp
|
|
1050
|
-
|
|
1051
|
-
# Register in pyomo-model:
|
|
1052
|
-
self._register_pyomo_comp(pyomo_comp, variable)
|
|
1053
|
-
|
|
1054
|
-
lower_bound_vector = utils.as_vector(variable.lower_bound, variable.length)
|
|
1055
|
-
upper_bound_vector = utils.as_vector(variable.upper_bound, variable.length)
|
|
1056
|
-
fixed_value_vector = utils.as_vector(variable.fixed_value, variable.length)
|
|
1057
|
-
for i in variable.indices:
|
|
1058
|
-
# Wenn Vorgabe-Wert vorhanden:
|
|
1059
|
-
if variable.fixed and (fixed_value_vector[i] is not None):
|
|
1060
|
-
# Fixieren:
|
|
1061
|
-
pyomo_comp[i].value = fixed_value_vector[i]
|
|
1062
|
-
pyomo_comp[i].fix()
|
|
1063
|
-
else:
|
|
1064
|
-
# Boundaries:
|
|
1065
|
-
pyomo_comp[i].setlb(lower_bound_vector[i]) # min
|
|
1066
|
-
pyomo_comp[i].setub(upper_bound_vector[i]) # max
|
|
1067
|
-
|
|
1068
|
-
def translate_equation(self, equation: Equation):
|
|
1069
|
-
if not isinstance(equation, Equation):
|
|
1070
|
-
raise TypeError(f'Wrong Class: {equation.__class__.__name__}')
|
|
1071
|
-
|
|
1072
|
-
# constant_vector hier erneut erstellen, da Anz. Glg. vorher noch nicht bekannt:
|
|
1073
|
-
constant_vector = equation.constant_vector
|
|
1074
|
-
|
|
1075
|
-
def linear_sum_pyomo_rule(model, i):
|
|
1076
|
-
"""This function is needed for pyomoy internal construction of Constraints."""
|
|
1077
|
-
lhs = 0
|
|
1078
|
-
summand: Summand
|
|
1079
|
-
for summand in equation.summands:
|
|
1080
|
-
lhs += self._summand_math_expression(summand, i) # i-te Gleichung (wenn Skalar, dann wird i ignoriert)
|
|
1081
|
-
rhs = constant_vector[i]
|
|
1082
|
-
return lhs == rhs
|
|
1083
|
-
|
|
1084
|
-
pyomo_comp = pyo.Constraint(range(equation.length), rule=linear_sum_pyomo_rule) # Nebenbedingung erstellen
|
|
1085
|
-
|
|
1086
|
-
self._register_pyomo_comp(pyomo_comp, equation)
|
|
1087
|
-
|
|
1088
|
-
def translate_inequation(self, inequation: Inequation):
|
|
1089
|
-
if not isinstance(inequation, Inequation):
|
|
1090
|
-
raise TypeError(f'Wrong Class: {inequation.__class__.__name__}')
|
|
1091
|
-
|
|
1092
|
-
# constant_vector hier erneut erstellen, da Anz. Glg. vorher noch nicht bekannt:
|
|
1093
|
-
constant_vector = inequation.constant_vector
|
|
1094
|
-
|
|
1095
|
-
def linear_sum_pyomo_rule(model, i):
|
|
1096
|
-
"""This function is needed for pyomoy internal construction of Constraints."""
|
|
1097
|
-
lhs = 0
|
|
1098
|
-
summand: Summand
|
|
1099
|
-
for summand in inequation.summands:
|
|
1100
|
-
lhs += self._summand_math_expression(summand, i) # i-te Gleichung (wenn Skalar, dann wird i ignoriert)
|
|
1101
|
-
rhs = constant_vector[i]
|
|
1102
|
-
|
|
1103
|
-
return lhs <= rhs
|
|
1104
|
-
|
|
1105
|
-
pyomo_comp = pyo.Constraint(range(inequation.length), rule=linear_sum_pyomo_rule) # Nebenbedingung erstellen
|
|
1106
|
-
|
|
1107
|
-
self._register_pyomo_comp(pyomo_comp, inequation)
|
|
1108
|
-
|
|
1109
|
-
def translate_objective(self, objective: Equation):
|
|
1110
|
-
if not isinstance(objective, Equation):
|
|
1111
|
-
raise TypeError(f'Class {objective.__class__.__name__} Can not be the objective!')
|
|
1112
|
-
if not objective.is_objective:
|
|
1113
|
-
raise TypeError(
|
|
1114
|
-
f'Objective Equation is not marked as objective, {objective.is_objective=}, '
|
|
1115
|
-
f'but was sent to translate to objective!'
|
|
1116
|
-
)
|
|
1117
|
-
if objective.length != 1:
|
|
1118
|
-
raise Exception('Length of Objective must be 0')
|
|
1119
|
-
|
|
1120
|
-
def _rule_linear_sum_skalar(model):
|
|
1121
|
-
skalar = 0
|
|
1122
|
-
for summand in objective.summands:
|
|
1123
|
-
skalar += self._summand_math_expression(summand)
|
|
1124
|
-
return skalar
|
|
1125
|
-
|
|
1126
|
-
self.model.objective = pyo.Objective(rule=_rule_linear_sum_skalar, sense=pyo.minimize)
|
|
1127
|
-
self.mapping[objective] = self.model.objective
|
|
1128
|
-
|
|
1129
|
-
def _summand_math_expression(self, summand: Summand, at_index: int = 0) -> 'pyo.Expression':
|
|
1130
|
-
pyomo_variable = self.mapping[summand.variable]
|
|
1131
|
-
if isinstance(summand, SumOfSummand):
|
|
1132
|
-
return sum(pyomo_variable[summand.indices[j]] * summand.factor_vec[j] for j in summand.indices)
|
|
1133
|
-
|
|
1134
|
-
# Ausdruck für i-te Gleichung (falls Skalar, dann immer gleicher Ausdruck ausgegeben)
|
|
1135
|
-
if summand.length == 1:
|
|
1136
|
-
# ignore argument at_index, because Skalar is used for every single equation
|
|
1137
|
-
return pyomo_variable[summand.indices[0]] * summand.factor_vec[0]
|
|
1138
|
-
if len(summand.indices) == 1:
|
|
1139
|
-
return pyomo_variable[summand.indices[0]] * summand.factor_vec[at_index]
|
|
1140
|
-
return pyomo_variable[summand.indices[at_index]] * summand.factor_vec[at_index]
|
|
1141
|
-
|
|
1142
|
-
def _register_pyomo_comp(self, pyomo_comp, part: Union[Variable, Equation, Inequation]) -> None:
|
|
1143
|
-
self._counter += 1 # Counter to guarantee unique names
|
|
1144
|
-
self.model.add_component(f'{part.label}__{self._counter}', pyomo_comp)
|
|
1145
|
-
self.mapping[part] = pyomo_comp
|