flixopt 3.2.1__py3-none-any.whl → 3.4.0__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.
- flixopt/calculation.py +105 -39
- flixopt/components.py +16 -0
- flixopt/config.py +120 -0
- flixopt/effects.py +28 -28
- flixopt/elements.py +58 -1
- flixopt/flow_system.py +141 -84
- flixopt/interface.py +23 -2
- flixopt/io.py +506 -4
- flixopt/results.py +52 -24
- flixopt/solvers.py +12 -4
- flixopt/structure.py +369 -49
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/METADATA +3 -2
- flixopt-3.4.0.dist-info/RECORD +26 -0
- flixopt-3.2.1.dist-info/RECORD +0 -26
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/WHEEL +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/top_level.txt +0 -0
flixopt/calculation.py
CHANGED
|
@@ -13,13 +13,14 @@ from __future__ import annotations
|
|
|
13
13
|
import logging
|
|
14
14
|
import math
|
|
15
15
|
import pathlib
|
|
16
|
+
import sys
|
|
16
17
|
import timeit
|
|
17
18
|
import warnings
|
|
18
19
|
from collections import Counter
|
|
19
20
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
20
21
|
|
|
21
22
|
import numpy as np
|
|
22
|
-
import
|
|
23
|
+
from tqdm import tqdm
|
|
23
24
|
|
|
24
25
|
from . import io as fx_io
|
|
25
26
|
from .aggregation import Aggregation, AggregationModel, AggregationParameters
|
|
@@ -53,6 +54,8 @@ class Calculation:
|
|
|
53
54
|
active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead.
|
|
54
55
|
"""
|
|
55
56
|
|
|
57
|
+
model: FlowSystemModel | None
|
|
58
|
+
|
|
56
59
|
def __init__(
|
|
57
60
|
self,
|
|
58
61
|
name: str,
|
|
@@ -87,7 +90,7 @@ class Calculation:
|
|
|
87
90
|
flow_system._used_in_calculation = True
|
|
88
91
|
|
|
89
92
|
self.flow_system = flow_system
|
|
90
|
-
self.model
|
|
93
|
+
self.model = None
|
|
91
94
|
|
|
92
95
|
self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0}
|
|
93
96
|
self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder)
|
|
@@ -112,7 +115,7 @@ class Calculation:
|
|
|
112
115
|
'periodic': effect.submodel.periodic.total.solution.values,
|
|
113
116
|
'total': effect.submodel.total.solution.values,
|
|
114
117
|
}
|
|
115
|
-
for effect in self.flow_system.effects
|
|
118
|
+
for effect in sorted(self.flow_system.effects.values(), key=lambda e: e.label_full.upper())
|
|
116
119
|
},
|
|
117
120
|
'Invest-Decisions': {
|
|
118
121
|
'Invested': {
|
|
@@ -225,7 +228,7 @@ class FullCalculation(Calculation):
|
|
|
225
228
|
return self
|
|
226
229
|
|
|
227
230
|
def solve(
|
|
228
|
-
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool =
|
|
231
|
+
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool | None = None
|
|
229
232
|
) -> FullCalculation:
|
|
230
233
|
t_start = timeit.default_timer()
|
|
231
234
|
|
|
@@ -235,6 +238,8 @@ class FullCalculation(Calculation):
|
|
|
235
238
|
**solver.options,
|
|
236
239
|
)
|
|
237
240
|
self.durations['solving'] = round(timeit.default_timer() - t_start, 2)
|
|
241
|
+
logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.')
|
|
242
|
+
logger.info(f'Model status after solve: {self.model.status}')
|
|
238
243
|
|
|
239
244
|
if self.model.status == 'warning':
|
|
240
245
|
# Save the model and the flow_system to file in case of infeasibility
|
|
@@ -248,15 +253,13 @@ class FullCalculation(Calculation):
|
|
|
248
253
|
)
|
|
249
254
|
|
|
250
255
|
# Log the formatted output
|
|
251
|
-
if log_main_results
|
|
256
|
+
should_log = log_main_results if log_main_results is not None else CONFIG.Solving.log_main_results
|
|
257
|
+
if should_log:
|
|
252
258
|
logger.info(
|
|
253
259
|
f'{" Main Results ":#^80}\n'
|
|
254
|
-
+
|
|
260
|
+
+ fx_io.format_yaml_string(
|
|
255
261
|
self.main_results,
|
|
256
|
-
|
|
257
|
-
sort_keys=False,
|
|
258
|
-
allow_unicode=True,
|
|
259
|
-
indent=4,
|
|
262
|
+
compact_numeric_lists=True,
|
|
260
263
|
)
|
|
261
264
|
)
|
|
262
265
|
|
|
@@ -366,7 +369,7 @@ class AggregatedCalculation(FullCalculation):
|
|
|
366
369
|
)
|
|
367
370
|
|
|
368
371
|
self.aggregation.cluster()
|
|
369
|
-
self.aggregation.plot(show=
|
|
372
|
+
self.aggregation.plot(show=CONFIG.Plotting.default_show, save=self.folder / 'aggregation.html')
|
|
370
373
|
if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
|
|
371
374
|
ds = self.flow_system.to_dataset()
|
|
372
375
|
for name, series in self.aggregation.aggregated_data.items():
|
|
@@ -567,48 +570,111 @@ class SegmentedCalculation(Calculation):
|
|
|
567
570
|
f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):'
|
|
568
571
|
)
|
|
569
572
|
|
|
573
|
+
def _solve_single_segment(
|
|
574
|
+
self,
|
|
575
|
+
i: int,
|
|
576
|
+
calculation: FullCalculation,
|
|
577
|
+
solver: _Solver,
|
|
578
|
+
log_file: pathlib.Path | None,
|
|
579
|
+
log_main_results: bool,
|
|
580
|
+
suppress_output: bool,
|
|
581
|
+
) -> None:
|
|
582
|
+
"""Solve a single segment calculation."""
|
|
583
|
+
if i > 0 and self.nr_of_previous_values > 0:
|
|
584
|
+
self._transfer_start_values(i)
|
|
585
|
+
|
|
586
|
+
calculation.do_modeling()
|
|
587
|
+
|
|
588
|
+
# Warn about Investments, but only in first run
|
|
589
|
+
if i == 0:
|
|
590
|
+
invest_elements = [
|
|
591
|
+
model.label_full
|
|
592
|
+
for component in calculation.flow_system.components.values()
|
|
593
|
+
for model in component.submodel.all_submodels
|
|
594
|
+
if isinstance(model, InvestmentModel)
|
|
595
|
+
]
|
|
596
|
+
if invest_elements:
|
|
597
|
+
logger.critical(
|
|
598
|
+
f'Investments are not supported in Segmented Calculation! '
|
|
599
|
+
f'Following InvestmentModels were found: {invest_elements}'
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
log_path = pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log'
|
|
603
|
+
|
|
604
|
+
if suppress_output:
|
|
605
|
+
with fx_io.suppress_output():
|
|
606
|
+
calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)
|
|
607
|
+
else:
|
|
608
|
+
calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)
|
|
609
|
+
|
|
570
610
|
def do_modeling_and_solve(
|
|
571
|
-
self,
|
|
611
|
+
self,
|
|
612
|
+
solver: _Solver,
|
|
613
|
+
log_file: pathlib.Path | None = None,
|
|
614
|
+
log_main_results: bool = False,
|
|
615
|
+
show_individual_solves: bool = False,
|
|
572
616
|
) -> SegmentedCalculation:
|
|
617
|
+
"""Model and solve all segments of the segmented calculation.
|
|
618
|
+
|
|
619
|
+
This method creates sub-calculations for each time segment, then iteratively
|
|
620
|
+
models and solves each segment. It supports two output modes: a progress bar
|
|
621
|
+
for compact output, or detailed individual solve information.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
solver: The solver instance to use for optimization (e.g., Gurobi, HiGHS).
|
|
625
|
+
log_file: Optional path to the solver log file. If None, defaults to
|
|
626
|
+
folder/name.log.
|
|
627
|
+
log_main_results: Whether to log main results (objective, effects, etc.)
|
|
628
|
+
after each segment solve. Defaults to False.
|
|
629
|
+
show_individual_solves: If True, shows detailed output for each segment
|
|
630
|
+
solve with logger messages. If False (default), shows a compact progress
|
|
631
|
+
bar with suppressed solver output for cleaner display.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Self, for method chaining.
|
|
635
|
+
|
|
636
|
+
Note:
|
|
637
|
+
The method automatically transfers all start values between segments to ensure
|
|
638
|
+
continuity of storage states and flow rates across segment boundaries.
|
|
639
|
+
"""
|
|
573
640
|
logger.info(f'{"":#^80}')
|
|
574
641
|
logger.info(f'{" Segmented Solving ":#^80}')
|
|
575
642
|
self._create_sub_calculations()
|
|
576
643
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
644
|
+
if show_individual_solves:
|
|
645
|
+
# Path 1: Show individual solves with detailed output
|
|
646
|
+
for i, calculation in enumerate(self.sub_calculations):
|
|
647
|
+
logger.info(
|
|
648
|
+
f'Solving segment {i + 1}/{len(self.sub_calculations)}: '
|
|
649
|
+
f'{calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}'
|
|
650
|
+
)
|
|
651
|
+
self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=False)
|
|
652
|
+
else:
|
|
653
|
+
# Path 2: Show only progress bar with suppressed output
|
|
654
|
+
progress_bar = tqdm(
|
|
655
|
+
enumerate(self.sub_calculations),
|
|
656
|
+
total=len(self.sub_calculations),
|
|
657
|
+
desc='Solving segments',
|
|
658
|
+
unit='segment',
|
|
659
|
+
file=sys.stdout,
|
|
660
|
+
disable=not CONFIG.Solving.log_to_console,
|
|
581
661
|
)
|
|
582
662
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
# Warn about Investments, but only in fist run
|
|
589
|
-
if i == 0:
|
|
590
|
-
invest_elements = [
|
|
591
|
-
model.label_full
|
|
592
|
-
for component in calculation.flow_system.components.values()
|
|
593
|
-
for model in component.submodel.all_submodels
|
|
594
|
-
if isinstance(model, InvestmentModel)
|
|
595
|
-
]
|
|
596
|
-
if invest_elements:
|
|
597
|
-
logger.critical(
|
|
598
|
-
f'Investments are not supported in Segmented Calculation! '
|
|
599
|
-
f'Following InvestmentModels were found: {invest_elements}'
|
|
663
|
+
try:
|
|
664
|
+
for i, calculation in progress_bar:
|
|
665
|
+
progress_bar.set_description(
|
|
666
|
+
f'Solving ({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]})'
|
|
600
667
|
)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
|
|
605
|
-
log_main_results=log_main_results,
|
|
606
|
-
)
|
|
668
|
+
self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=True)
|
|
669
|
+
finally:
|
|
670
|
+
progress_bar.close()
|
|
607
671
|
|
|
608
672
|
for calc in self.sub_calculations:
|
|
609
673
|
for key, value in calc.durations.items():
|
|
610
674
|
self.durations[key] += value
|
|
611
675
|
|
|
676
|
+
logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.')
|
|
677
|
+
|
|
612
678
|
self.results = SegmentedCalculationResults.from_calculation(self)
|
|
613
679
|
|
|
614
680
|
return self
|
flixopt/components.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Literal
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import xarray as xr
|
|
13
13
|
|
|
14
|
+
from . import io as fx_io
|
|
14
15
|
from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser
|
|
15
16
|
from .elements import Component, ComponentModel, Flow
|
|
16
17
|
from .features import InvestmentModel, PiecewiseModel
|
|
@@ -160,6 +161,8 @@ class LinearConverter(Component):
|
|
|
160
161
|
|
|
161
162
|
"""
|
|
162
163
|
|
|
164
|
+
submodel: LinearConverterModel | None
|
|
165
|
+
|
|
163
166
|
def __init__(
|
|
164
167
|
self,
|
|
165
168
|
label: str,
|
|
@@ -376,6 +379,8 @@ class Storage(Component):
|
|
|
376
379
|
With flow rates in m3/h, the charge state is therefore in m3.
|
|
377
380
|
"""
|
|
378
381
|
|
|
382
|
+
submodel: StorageModel | None
|
|
383
|
+
|
|
379
384
|
def __init__(
|
|
380
385
|
self,
|
|
381
386
|
label: str,
|
|
@@ -528,6 +533,15 @@ class Storage(Component):
|
|
|
528
533
|
f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.'
|
|
529
534
|
)
|
|
530
535
|
|
|
536
|
+
def __repr__(self) -> str:
|
|
537
|
+
"""Return string representation."""
|
|
538
|
+
# Use build_repr_from_init directly to exclude charging and discharging
|
|
539
|
+
return fx_io.build_repr_from_init(
|
|
540
|
+
self,
|
|
541
|
+
excluded_params={'self', 'label', 'charging', 'discharging', 'kwargs'},
|
|
542
|
+
skip_default_size=True,
|
|
543
|
+
) + fx_io.format_flow_details(self)
|
|
544
|
+
|
|
531
545
|
|
|
532
546
|
@register_class_for_io
|
|
533
547
|
class Transmission(Component):
|
|
@@ -640,6 +654,8 @@ class Transmission(Component):
|
|
|
640
654
|
|
|
641
655
|
"""
|
|
642
656
|
|
|
657
|
+
submodel: TransmissionModel | None
|
|
658
|
+
|
|
643
659
|
def __init__(
|
|
644
660
|
self,
|
|
645
661
|
label: str,
|
flixopt/config.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
import warnings
|
|
6
7
|
from logging.handlers import RotatingFileHandler
|
|
@@ -63,6 +64,14 @@ _DEFAULTS = MappingProxyType(
|
|
|
63
64
|
'default_qualitative_colorscale': 'plotly',
|
|
64
65
|
}
|
|
65
66
|
),
|
|
67
|
+
'solving': MappingProxyType(
|
|
68
|
+
{
|
|
69
|
+
'mip_gap': 0.01,
|
|
70
|
+
'time_limit_seconds': 300,
|
|
71
|
+
'log_to_console': True,
|
|
72
|
+
'log_main_results': True,
|
|
73
|
+
}
|
|
74
|
+
),
|
|
66
75
|
}
|
|
67
76
|
)
|
|
68
77
|
|
|
@@ -75,6 +84,8 @@ class CONFIG:
|
|
|
75
84
|
Attributes:
|
|
76
85
|
Logging: Logging configuration.
|
|
77
86
|
Modeling: Optimization modeling parameters.
|
|
87
|
+
Solving: Solver configuration and default parameters.
|
|
88
|
+
Plotting: Plotting configuration.
|
|
78
89
|
config_name: Configuration name.
|
|
79
90
|
|
|
80
91
|
Examples:
|
|
@@ -91,6 +102,9 @@ class CONFIG:
|
|
|
91
102
|
level: DEBUG
|
|
92
103
|
console: true
|
|
93
104
|
file: app.log
|
|
105
|
+
solving:
|
|
106
|
+
mip_gap: 0.001
|
|
107
|
+
time_limit_seconds: 600
|
|
94
108
|
```
|
|
95
109
|
"""
|
|
96
110
|
|
|
@@ -194,6 +208,30 @@ class CONFIG:
|
|
|
194
208
|
epsilon: float = _DEFAULTS['modeling']['epsilon']
|
|
195
209
|
big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
|
|
196
210
|
|
|
211
|
+
class Solving:
|
|
212
|
+
"""Solver configuration and default parameters.
|
|
213
|
+
|
|
214
|
+
Attributes:
|
|
215
|
+
mip_gap: Default MIP gap tolerance for solver convergence.
|
|
216
|
+
time_limit_seconds: Default time limit in seconds for solver runs.
|
|
217
|
+
log_to_console: Whether solver should output to console.
|
|
218
|
+
log_main_results: Whether to log main results after solving.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
```python
|
|
222
|
+
# Set tighter convergence and longer timeout
|
|
223
|
+
CONFIG.Solving.mip_gap = 0.001
|
|
224
|
+
CONFIG.Solving.time_limit_seconds = 600
|
|
225
|
+
CONFIG.Solving.log_to_console = False
|
|
226
|
+
CONFIG.apply()
|
|
227
|
+
```
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
mip_gap: float = _DEFAULTS['solving']['mip_gap']
|
|
231
|
+
time_limit_seconds: int = _DEFAULTS['solving']['time_limit_seconds']
|
|
232
|
+
log_to_console: bool = _DEFAULTS['solving']['log_to_console']
|
|
233
|
+
log_main_results: bool = _DEFAULTS['solving']['log_main_results']
|
|
234
|
+
|
|
197
235
|
class Plotting:
|
|
198
236
|
"""Plotting configuration.
|
|
199
237
|
|
|
@@ -246,6 +284,12 @@ class CONFIG:
|
|
|
246
284
|
for key, value in _DEFAULTS['modeling'].items():
|
|
247
285
|
setattr(cls.Modeling, key, value)
|
|
248
286
|
|
|
287
|
+
for key, value in _DEFAULTS['solving'].items():
|
|
288
|
+
setattr(cls.Solving, key, value)
|
|
289
|
+
|
|
290
|
+
for key, value in _DEFAULTS['plotting'].items():
|
|
291
|
+
setattr(cls.Plotting, key, value)
|
|
292
|
+
|
|
249
293
|
cls.config_name = _DEFAULTS['config_name']
|
|
250
294
|
cls.apply()
|
|
251
295
|
|
|
@@ -329,6 +373,12 @@ class CONFIG:
|
|
|
329
373
|
elif key == 'modeling' and isinstance(value, dict):
|
|
330
374
|
for nested_key, nested_value in value.items():
|
|
331
375
|
setattr(cls.Modeling, nested_key, nested_value)
|
|
376
|
+
elif key == 'solving' and isinstance(value, dict):
|
|
377
|
+
for nested_key, nested_value in value.items():
|
|
378
|
+
setattr(cls.Solving, nested_key, nested_value)
|
|
379
|
+
elif key == 'plotting' and isinstance(value, dict):
|
|
380
|
+
for nested_key, nested_value in value.items():
|
|
381
|
+
setattr(cls.Plotting, nested_key, nested_value)
|
|
332
382
|
elif hasattr(cls, key):
|
|
333
383
|
setattr(cls, key, value)
|
|
334
384
|
|
|
@@ -366,6 +416,12 @@ class CONFIG:
|
|
|
366
416
|
'epsilon': cls.Modeling.epsilon,
|
|
367
417
|
'big_binary_bound': cls.Modeling.big_binary_bound,
|
|
368
418
|
},
|
|
419
|
+
'solving': {
|
|
420
|
+
'mip_gap': cls.Solving.mip_gap,
|
|
421
|
+
'time_limit_seconds': cls.Solving.time_limit_seconds,
|
|
422
|
+
'log_to_console': cls.Solving.log_to_console,
|
|
423
|
+
'log_main_results': cls.Solving.log_main_results,
|
|
424
|
+
},
|
|
369
425
|
'plotting': {
|
|
370
426
|
'default_show': cls.Plotting.default_show,
|
|
371
427
|
'default_engine': cls.Plotting.default_engine,
|
|
@@ -376,6 +432,70 @@ class CONFIG:
|
|
|
376
432
|
},
|
|
377
433
|
}
|
|
378
434
|
|
|
435
|
+
@classmethod
|
|
436
|
+
def silent(cls) -> type[CONFIG]:
|
|
437
|
+
"""Configure for silent operation.
|
|
438
|
+
|
|
439
|
+
Disables console logging, solver output, and result logging
|
|
440
|
+
for clean production runs. Does not show plots. Automatically calls apply().
|
|
441
|
+
"""
|
|
442
|
+
cls.Logging.console = False
|
|
443
|
+
cls.Plotting.default_show = False
|
|
444
|
+
cls.Logging.file = None
|
|
445
|
+
cls.Solving.log_to_console = False
|
|
446
|
+
cls.Solving.log_main_results = False
|
|
447
|
+
cls.apply()
|
|
448
|
+
return cls
|
|
449
|
+
|
|
450
|
+
@classmethod
|
|
451
|
+
def debug(cls) -> type[CONFIG]:
|
|
452
|
+
"""Configure for debug mode with verbose output.
|
|
453
|
+
|
|
454
|
+
Enables console logging at DEBUG level and all solver output for
|
|
455
|
+
troubleshooting. Automatically calls apply().
|
|
456
|
+
"""
|
|
457
|
+
cls.Logging.console = True
|
|
458
|
+
cls.Logging.level = 'DEBUG'
|
|
459
|
+
cls.Solving.log_to_console = True
|
|
460
|
+
cls.Solving.log_main_results = True
|
|
461
|
+
cls.apply()
|
|
462
|
+
return cls
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
def exploring(cls) -> type[CONFIG]:
|
|
466
|
+
"""Configure for exploring flixopt
|
|
467
|
+
|
|
468
|
+
Enables console logging at INFO level and all solver output.
|
|
469
|
+
Also enables browser plotting for plotly with showing plots per default
|
|
470
|
+
"""
|
|
471
|
+
cls.Logging.console = True
|
|
472
|
+
cls.Logging.level = 'INFO'
|
|
473
|
+
cls.Solving.log_to_console = True
|
|
474
|
+
cls.Solving.log_main_results = True
|
|
475
|
+
cls.browser_plotting()
|
|
476
|
+
cls.apply()
|
|
477
|
+
return cls
|
|
478
|
+
|
|
479
|
+
@classmethod
|
|
480
|
+
def browser_plotting(cls) -> type[CONFIG]:
|
|
481
|
+
"""Configure for interactive usage with plotly to open plots in browser.
|
|
482
|
+
|
|
483
|
+
Sets plotly.io.renderers.default = 'browser'. Useful for running examples
|
|
484
|
+
and viewing interactive plots. Does NOT modify CONFIG.Plotting settings.
|
|
485
|
+
|
|
486
|
+
Respects FLIXOPT_CI environment variable if set.
|
|
487
|
+
"""
|
|
488
|
+
cls.Plotting.default_show = True
|
|
489
|
+
cls.apply()
|
|
490
|
+
|
|
491
|
+
# Only set to True if environment variable hasn't overridden it
|
|
492
|
+
if 'FLIXOPT_CI' not in os.environ:
|
|
493
|
+
import plotly.io as pio
|
|
494
|
+
|
|
495
|
+
pio.renderers.default = 'browser'
|
|
496
|
+
|
|
497
|
+
return cls
|
|
498
|
+
|
|
379
499
|
|
|
380
500
|
class MultilineFormatter(logging.Formatter):
|
|
381
501
|
"""Formatter that handles multi-line messages with consistent prefixes.
|
flixopt/effects.py
CHANGED
|
@@ -16,9 +16,10 @@ import linopy
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
import xarray as xr
|
|
18
18
|
|
|
19
|
+
from . import io as fx_io
|
|
19
20
|
from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser
|
|
20
21
|
from .features import ShareAllocationModel
|
|
21
|
-
from .structure import Element, ElementModel, FlowSystemModel, Submodel, register_class_for_io
|
|
22
|
+
from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
24
25
|
from collections.abc import Iterator
|
|
@@ -159,6 +160,8 @@ class Effect(Element):
|
|
|
159
160
|
|
|
160
161
|
"""
|
|
161
162
|
|
|
163
|
+
submodel: EffectModel | None
|
|
164
|
+
|
|
162
165
|
def __init__(
|
|
163
166
|
self,
|
|
164
167
|
label: str,
|
|
@@ -448,17 +451,19 @@ PeriodicEffects = dict[str, Scalar]
|
|
|
448
451
|
EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares
|
|
449
452
|
|
|
450
453
|
|
|
451
|
-
class EffectCollection:
|
|
454
|
+
class EffectCollection(ElementContainer[Effect]):
|
|
452
455
|
"""
|
|
453
456
|
Handling all Effects
|
|
454
457
|
"""
|
|
455
458
|
|
|
459
|
+
submodel: EffectCollectionModel | None
|
|
460
|
+
|
|
456
461
|
def __init__(self, *effects: Effect):
|
|
457
|
-
|
|
462
|
+
super().__init__(element_type_name='effects')
|
|
458
463
|
self._standard_effect: Effect | None = None
|
|
459
464
|
self._objective_effect: Effect | None = None
|
|
460
465
|
|
|
461
|
-
self.submodel
|
|
466
|
+
self.submodel = None
|
|
462
467
|
self.add_effects(*effects)
|
|
463
468
|
|
|
464
469
|
def create_model(self, model: FlowSystemModel) -> EffectCollectionModel:
|
|
@@ -474,7 +479,7 @@ class EffectCollection:
|
|
|
474
479
|
self.standard_effect = effect
|
|
475
480
|
if effect.is_objective:
|
|
476
481
|
self.objective_effect = effect
|
|
477
|
-
self.
|
|
482
|
+
self.add(effect) # Use the inherited add() method from ElementContainer
|
|
478
483
|
logger.info(f'Registered new Effect: {effect.label}')
|
|
479
484
|
|
|
480
485
|
def create_effect_values_dict(
|
|
@@ -520,10 +525,13 @@ class EffectCollection:
|
|
|
520
525
|
# Check circular loops in effects:
|
|
521
526
|
temporal, periodic = self.calculate_effect_share_factors()
|
|
522
527
|
|
|
523
|
-
# Validate all referenced sources exist
|
|
524
|
-
|
|
528
|
+
# Validate all referenced effects (both sources and targets) exist
|
|
529
|
+
edges = list(temporal.keys()) + list(periodic.keys())
|
|
530
|
+
unknown_sources = {src for src, _ in edges if src not in self}
|
|
531
|
+
unknown_targets = {tgt for _, tgt in edges if tgt not in self}
|
|
532
|
+
unknown = unknown_sources | unknown_targets
|
|
525
533
|
if unknown:
|
|
526
|
-
raise KeyError(f'Unknown effects used in
|
|
534
|
+
raise KeyError(f'Unknown effects used in effect share mappings: {sorted(unknown)}')
|
|
527
535
|
|
|
528
536
|
temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal]))
|
|
529
537
|
periodic_cycles = detect_cycles(tuples_to_adjacency_list([key for key in periodic]))
|
|
@@ -552,31 +560,23 @@ class EffectCollection:
|
|
|
552
560
|
else:
|
|
553
561
|
raise KeyError(f'Effect {effect} not found!')
|
|
554
562
|
try:
|
|
555
|
-
return
|
|
563
|
+
return super().__getitem__(effect) # Leverage ContainerMixin suggestions
|
|
556
564
|
except KeyError as e:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return iter(self._effects.values())
|
|
565
|
+
# Extract the original message and append context for cleaner output
|
|
566
|
+
original_msg = str(e).strip('\'"')
|
|
567
|
+
raise KeyError(f'{original_msg} Add the effect to the FlowSystem first.') from None
|
|
561
568
|
|
|
562
|
-
def
|
|
563
|
-
return
|
|
569
|
+
def __iter__(self) -> Iterator[str]:
|
|
570
|
+
return iter(self.keys()) # Iterate over keys like a normal dict
|
|
564
571
|
|
|
565
572
|
def __contains__(self, item: str | Effect) -> bool:
|
|
566
573
|
"""Check if the effect exists. Checks for label or object"""
|
|
567
574
|
if isinstance(item, str):
|
|
568
|
-
return item
|
|
575
|
+
return super().__contains__(item) # Check if the label exists
|
|
569
576
|
elif isinstance(item, Effect):
|
|
570
|
-
|
|
571
|
-
return True
|
|
572
|
-
if item in self.effects.values(): # Check if the object exists
|
|
573
|
-
return True
|
|
577
|
+
return item.label_full in self and self[item.label_full] is item
|
|
574
578
|
return False
|
|
575
579
|
|
|
576
|
-
@property
|
|
577
|
-
def effects(self) -> dict[str, Effect]:
|
|
578
|
-
return self._effects
|
|
579
|
-
|
|
580
580
|
@property
|
|
581
581
|
def standard_effect(self) -> Effect:
|
|
582
582
|
if self._standard_effect is None:
|
|
@@ -611,7 +611,7 @@ class EffectCollection:
|
|
|
611
611
|
dict[tuple[str, str], xr.DataArray],
|
|
612
612
|
]:
|
|
613
613
|
shares_periodic = {}
|
|
614
|
-
for name, effect in self.
|
|
614
|
+
for name, effect in self.items():
|
|
615
615
|
if effect.share_from_periodic:
|
|
616
616
|
for source, data in effect.share_from_periodic.items():
|
|
617
617
|
if source not in shares_periodic:
|
|
@@ -620,7 +620,7 @@ class EffectCollection:
|
|
|
620
620
|
shares_periodic = calculate_all_conversion_paths(shares_periodic)
|
|
621
621
|
|
|
622
622
|
shares_temporal = {}
|
|
623
|
-
for name, effect in self.
|
|
623
|
+
for name, effect in self.items():
|
|
624
624
|
if effect.share_from_temporal:
|
|
625
625
|
for source, data in effect.share_from_temporal.items():
|
|
626
626
|
if source not in shares_temporal:
|
|
@@ -670,7 +670,7 @@ class EffectCollectionModel(Submodel):
|
|
|
670
670
|
|
|
671
671
|
def _do_modeling(self):
|
|
672
672
|
super()._do_modeling()
|
|
673
|
-
for effect in self.effects:
|
|
673
|
+
for effect in self.effects.values():
|
|
674
674
|
effect.create_model(self._model)
|
|
675
675
|
self.penalty = self.add_submodels(
|
|
676
676
|
ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'),
|
|
@@ -684,7 +684,7 @@ class EffectCollectionModel(Submodel):
|
|
|
684
684
|
)
|
|
685
685
|
|
|
686
686
|
def _add_share_between_effects(self):
|
|
687
|
-
for target_effect in self.effects:
|
|
687
|
+
for target_effect in self.effects.values():
|
|
688
688
|
# 1. temporal: <- receiving temporal shares from other effects
|
|
689
689
|
for source_effect, time_series in target_effect.share_from_temporal.items():
|
|
690
690
|
target_effect.submodel.temporal.add_share(
|