flixopt 1.0.12__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (73) hide show
  1. docs/examples/00-Minimal Example.md +5 -0
  2. docs/examples/01-Basic Example.md +5 -0
  3. docs/examples/02-Complex Example.md +10 -0
  4. docs/examples/03-Calculation Modes.md +5 -0
  5. docs/examples/index.md +5 -0
  6. docs/faq/contribute.md +49 -0
  7. docs/faq/index.md +3 -0
  8. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  9. docs/images/architecture_flixOpt.png +0 -0
  10. docs/images/flixopt-icon.svg +1 -0
  11. docs/javascripts/mathjax.js +18 -0
  12. docs/release-notes/_template.txt +32 -0
  13. docs/release-notes/index.md +7 -0
  14. docs/release-notes/v2.0.0.md +93 -0
  15. docs/release-notes/v2.0.1.md +12 -0
  16. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  17. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  18. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  19. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  20. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  21. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  22. docs/user-guide/Mathematical Notation/index.md +22 -0
  23. docs/user-guide/Mathematical Notation/others.md +3 -0
  24. docs/user-guide/index.md +124 -0
  25. {flixOpt → flixopt}/__init__.py +5 -2
  26. {flixOpt → flixopt}/aggregation.py +113 -140
  27. flixopt/calculation.py +455 -0
  28. {flixOpt → flixopt}/commons.py +7 -4
  29. flixopt/components.py +630 -0
  30. {flixOpt → flixopt}/config.py +9 -8
  31. {flixOpt → flixopt}/config.yaml +3 -3
  32. flixopt/core.py +970 -0
  33. flixopt/effects.py +386 -0
  34. flixopt/elements.py +534 -0
  35. flixopt/features.py +1042 -0
  36. flixopt/flow_system.py +409 -0
  37. flixopt/interface.py +265 -0
  38. flixopt/io.py +308 -0
  39. flixopt/linear_converters.py +331 -0
  40. flixopt/plotting.py +1340 -0
  41. flixopt/results.py +898 -0
  42. flixopt/solvers.py +77 -0
  43. flixopt/structure.py +630 -0
  44. flixopt/utils.py +62 -0
  45. flixopt-2.0.1.dist-info/METADATA +145 -0
  46. flixopt-2.0.1.dist-info/RECORD +57 -0
  47. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
  48. flixopt-2.0.1.dist-info/top_level.txt +6 -0
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixopt-icon.svg +1 -0
  52. pics/pics.pptx +0 -0
  53. scripts/gen_ref_pages.py +54 -0
  54. site/release-notes/_template.txt +32 -0
  55. flixOpt/calculation.py +0 -629
  56. flixOpt/components.py +0 -614
  57. flixOpt/core.py +0 -182
  58. flixOpt/effects.py +0 -410
  59. flixOpt/elements.py +0 -489
  60. flixOpt/features.py +0 -942
  61. flixOpt/flow_system.py +0 -351
  62. flixOpt/interface.py +0 -203
  63. flixOpt/linear_converters.py +0 -325
  64. flixOpt/math_modeling.py +0 -1145
  65. flixOpt/plotting.py +0 -712
  66. flixOpt/results.py +0 -563
  67. flixOpt/solvers.py +0 -21
  68. flixOpt/structure.py +0 -733
  69. flixOpt/utils.py +0 -134
  70. flixopt-1.0.12.dist-info/METADATA +0 -174
  71. flixopt-1.0.12.dist-info/RECORD +0 -29
  72. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  73. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info/licenses}/LICENSE +0 -0
flixOpt/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