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 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 yaml
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: FlowSystemModel | None = None
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 = True
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
- + yaml.dump(
260
+ + fx_io.format_yaml_string(
255
261
  self.main_results,
256
- default_flow_style=False,
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=True, save=self.folder / 'aggregation.html')
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, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False
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
- for i, calculation in enumerate(self.sub_calculations):
578
- logger.info(
579
- f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] '
580
- f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):'
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
- 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 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
- calculation.solve(
603
- solver,
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
- self._effects = {}
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: EffectCollectionModel | None = None
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._effects[effect.label] = effect
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
- unknown = {src for src, _ in list(temporal.keys()) + list(periodic.keys()) if src not in self.effects}
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 in effect share mappings: {sorted(unknown)}')
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 self.effects[effect]
563
+ return super().__getitem__(effect) # Leverage ContainerMixin suggestions
556
564
  except KeyError as e:
557
- raise KeyError(f'Effect "{effect}" not found! Add it to the FlowSystem first!') from e
558
-
559
- def __iter__(self) -> Iterator[Effect]:
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 __len__(self) -> int:
563
- return len(self._effects)
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 in self.effects # Check if the label exists
575
+ return super().__contains__(item) # Check if the label exists
569
576
  elif isinstance(item, Effect):
570
- if item.label_full in self.effects:
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.effects.items():
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.effects.items():
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(