flixopt 3.2.1__tar.gz → 3.3.1__tar.gz

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 (34) hide show
  1. {flixopt-3.2.1 → flixopt-3.3.1}/CHANGELOG.md +70 -31
  2. flixopt-3.3.1/CONTRIBUTE.md +168 -0
  3. {flixopt-3.2.1/flixopt.egg-info → flixopt-3.3.1}/PKG-INFO +1 -1
  4. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/calculation.py +1 -1
  5. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/components.py +10 -0
  6. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/effects.py +23 -27
  7. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/elements.py +54 -1
  8. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/flow_system.py +139 -84
  9. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/interface.py +23 -2
  10. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/io.py +396 -12
  11. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/results.py +48 -22
  12. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/structure.py +366 -48
  13. {flixopt-3.2.1 → flixopt-3.3.1/flixopt.egg-info}/PKG-INFO +1 -1
  14. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt.egg-info/SOURCES.txt +1 -0
  15. {flixopt-3.2.1 → flixopt-3.3.1}/LICENSE +0 -0
  16. {flixopt-3.2.1 → flixopt-3.3.1}/MANIFEST.in +0 -0
  17. {flixopt-3.2.1 → flixopt-3.3.1}/README.md +0 -0
  18. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/__init__.py +0 -0
  19. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/aggregation.py +0 -0
  20. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/color_processing.py +0 -0
  21. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/commons.py +0 -0
  22. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/config.py +0 -0
  23. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/core.py +0 -0
  24. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/features.py +0 -0
  25. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/linear_converters.py +0 -0
  26. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/modeling.py +0 -0
  27. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/network_app.py +0 -0
  28. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/plotting.py +0 -0
  29. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt/solvers.py +0 -0
  30. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt.egg-info/dependency_links.txt +0 -0
  31. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt.egg-info/requires.txt +0 -0
  32. {flixopt-3.2.1 → flixopt-3.3.1}/flixopt.egg-info/top_level.txt +0 -0
  33. {flixopt-3.2.1 → flixopt-3.3.1}/pyproject.toml +0 -0
  34. {flixopt-3.2.1 → flixopt-3.3.1}/setup.cfg +0 -0
@@ -81,6 +81,45 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
81
81
 
82
82
  Until here -->
83
83
 
84
+ ## [3.3.1] - 2025-10-30
85
+
86
+ **Summary**: Small Bugfix and improving readability
87
+
88
+ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
89
+
90
+ ### ♻️ Changed
91
+ - Improved `summary.yaml` to use a compacted list representation for periods and scenarios
92
+
93
+ ### 🐛 Fixed
94
+ - Using `switch_on_total_max` with periods or scenarios failed
95
+
96
+ ### 📝 Docs
97
+ - Add more comprehensive `CONTRIBUTE.md`
98
+ - Improve logical structure in User Guide
99
+
100
+ ---
101
+
102
+ ## [3.3.0] - 2025-10-30
103
+
104
+ **Summary**: Better access to Elements stored in the FLowSystem and better representations (repr)
105
+
106
+ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
107
+
108
+ ### ♻️ Changed
109
+ **Improved repr methods:**
110
+ - **Results classes** (`ComponentResults`, `BusResults`, `FlowResults`, `EffectResults`) now show concise header with key metadata followed by xarray Dataset repr
111
+ - **Element classes** (`Component`, `Bus`, `Flow`, `Effect`, `Storage`) now show one-line summaries with essential information (connections, sizes, capacities, constraints)
112
+
113
+ **Container-based access:**
114
+ - **FlowSystem** now provides dict-like access patterns for all elements
115
+ - Use `flow_system['element_label']`, `flow_system.keys()`, `flow_system.values()`, and `flow_system.items()` for unified element access
116
+ - Specialized containers (`components`, `buses`, `effects`, `flows`) offer type-specific access with helpful error messages
117
+
118
+ ### 🗑️ Deprecated
119
+ - **`FlowSystem.all_elements`** property is deprecated in favor of dict-like interface (`flow_system['label']`, `.keys()`, `.values()`, `.items()`). Will be removed in v4.0.0.
120
+
121
+ ---
122
+
84
123
  ## [3.2.1] - 2025-10-29
85
124
 
86
125
  **Summary**:
@@ -105,21 +144,21 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
105
144
 
106
145
  **Color management:**
107
146
  - **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots
108
- - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})`
109
- - Automatically propagates to all segments in segmented calculations
110
- - Colors persist across all plot calls unless explicitly overridden
147
+ - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})`
148
+ - Automatically propagates to all segments in segmented calculations
149
+ - Colors persist across all plot calls unless explicitly overridden
111
150
  - **Flexible color inputs**: Supports colorscale names (e.g., 'turbo', 'plasma'), color lists, or label-to-color dictionaries
112
151
  - **Cross-backend compatibility**: Seamless color handling for both Plotly and Matplotlib
113
152
 
114
153
  **Plotting customization:**
115
154
  - **Plotting kwargs support**: Pass additional arguments to plotting backends via `px_kwargs`, `plot_kwargs`, and `backend_kwargs` parameters
116
155
  - **New `CONFIG.Plotting` configuration section**:
117
- - `default_show`: Control default plot visibility
118
- - `default_engine`: Choose 'plotly' or 'matplotlib'
119
- - `default_dpi`: Set resolution for saved plots
120
- - `default_facet_cols`: Configure default faceting columns
121
- - `default_sequential_colorscale`: Default for heatmaps (now 'turbo')
122
- - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly')
156
+ - `default_show`: Control default plot visibility
157
+ - `default_engine`: Choose 'plotly' or 'matplotlib'
158
+ - `default_dpi`: Set resolution for saved plots
159
+ - `default_facet_cols`: Configure default faceting columns
160
+ - `default_sequential_colorscale`: Default for heatmaps (now 'turbo')
161
+ - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly')
123
162
 
124
163
  **I/O improvements:**
125
164
  - Centralized JSON/YAML I/O with auto-format detection
@@ -286,12 +325,12 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
286
325
  **API and Behavior Changes:**
287
326
 
288
327
  - **Effect system redesigned** (no deprecation):
289
- - **Terminology changes**: Effect domains renamed for clarity: `operation` → `temporal`, `invest`/`investment` → `periodic`
290
- - **Sharing system**: The old `specific_share_to_other_effects_*` parameters were completely replaced with the new `share_from_temporal` and `share_from_periodic` syntax (see 🔥 Removed section)
328
+ - **Terminology changes**: Effect domains renamed for clarity: `operation` → `temporal`, `invest`/`investment` → `periodic`
329
+ - **Sharing system**: The old `specific_share_to_other_effects_*` parameters were completely replaced with the new `share_from_temporal` and `share_from_periodic` syntax (see 🔥 Removed section)
291
330
  - **FlowSystem independence**: FlowSystems cannot be shared across multiple Calculations anymore. A copy of the FlowSystem is created instead, making every Calculation independent. Each Subcalculation in `SegmentedCalculation` now has its own distinct `FlowSystem` object
292
331
  - **Bus and Effect object assignment**: Direct assignment of Bus/Effect objects is no longer supported. Use labels (strings) instead:
293
- - `Flow.bus` must receive a string label, not a Bus object
294
- - Effect shares must use effect labels (strings) in dictionaries, not Effect objects
332
+ - `Flow.bus` must receive a string label, not a Bus object
333
+ - Effect shares must use effect labels (strings) in dictionaries, not Effect objects
295
334
  - **Logging defaults** (from v2.2.0): Console and file logging are now disabled by default. Enable explicitly with `CONFIG.Logging.console = True` and `CONFIG.apply()`
296
335
 
297
336
  **Class and Method Renaming:**
@@ -305,14 +344,14 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
305
344
 
306
345
  - Investment binary variable: `is_invested` → `invested` in `InvestmentModel`
307
346
  - Switch tracking variables in `OnOffModel`:
308
- - `switch_on` → `switch|on`
309
- - `switch_off` → `switch|off`
310
- - `switch_on_nr` → `switch|count`
347
+ - `switch_on` → `switch|on`
348
+ - `switch_off` → `switch|off`
349
+ - `switch_on_nr` → `switch|count`
311
350
  - Effect submodel variables (following terminology changes):
312
- - `Effect(invest)|total` → `Effect(periodic)`
313
- - `Effect(operation)|total` → `Effect(temporal)`
314
- - `Effect(operation)|total_per_timestep` → `Effect(temporal)|per_timestep`
315
- - `Effect|total` → `Effect`
351
+ - `Effect(invest)|total` → `Effect(periodic)`
352
+ - `Effect(operation)|total` → `Effect(temporal)`
353
+ - `Effect(operation)|total_per_timestep` → `Effect(temporal)|per_timestep`
354
+ - `Effect|total` → `Effect`
316
355
 
317
356
  **Data Structure Changes:**
318
357
 
@@ -533,7 +572,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
533
572
 
534
573
  ### ✨ Added
535
574
  - **Network Visualization**: Added `FlowSystem.start_network_app()` and `FlowSystem.stop_network_app()` to easily visualize the network structure of a flow system in an interactive Dash web app
536
- - *Note: This is still experimental and might change in the future*
575
+ - *Note: This is still experimental and might change in the future*
537
576
 
538
577
  ### ♻️ Changed
539
578
  - **Multi-Flow Support**: `Sink`, `Source`, and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables modeling more use cases with these classes
@@ -575,8 +614,8 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
575
614
 
576
615
  ### 🐛 Fixed
577
616
  - Storage losses per hour were not calculated correctly, as mentioned by @brokenwings01. This might have led to issues when modeling large losses and long timesteps.
578
- - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$
579
- - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$
617
+ - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$
618
+ - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$
580
619
 
581
620
  ### 🚧 Known Issues
582
621
  - Just to mention: Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future.
@@ -601,10 +640,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
601
640
 
602
641
  ### 💥 Breaking Changes
603
642
  - Restructured the modeling of the On/Off state of Flows or Components
604
- - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours`
605
- - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours`
606
- - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1`
607
- - Similar pattern for all consecutive on/off constraints
643
+ - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours`
644
+ - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours`
645
+ - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1`
646
+ - Similar pattern for all consecutive on/off constraints
608
647
 
609
648
  ### 🐛 Fixed
610
649
  - Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters
@@ -650,10 +689,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
650
689
 
651
690
  **Variable Structure:**
652
691
  - Restructured the modeling of the On/Off state of Flows or Components
653
- - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours`
654
- - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours`
655
- - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1`
656
- - Similar pattern for all consecutive on/off constraints
692
+ - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours`
693
+ - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours`
694
+ - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1`
695
+ - Similar pattern for all consecutive on/off constraints
657
696
 
658
697
  ### 🔥 Removed
659
698
  - **Pyomo dependency** (replaced by linopy)
@@ -0,0 +1,168 @@
1
+ # Contributing to FlixOpt
2
+
3
+ We warmly welcome contributions from the community! Whether you're fixing bugs, adding features, improving documentation, or sharing examples, your contributions are valuable.
4
+
5
+ ## Ways to Contribute
6
+
7
+ ### 🐛 Report Issues
8
+ Found a bug or have a feature request? Please [open an issue](https://github.com/flixOpt/flixopt/issues) on GitHub.
9
+
10
+ When reporting issues, please include:
11
+ - A clear description of the problem
12
+ - Steps to reproduce the issue
13
+ - Expected vs. actual behavior
14
+ - Your environment (OS, Python version, FlixOpt version)
15
+ - Minimal code example if applicable
16
+
17
+ ### 💡 Share Examples
18
+ Help others learn FlixOpt by contributing examples:
19
+ - Real-world use cases
20
+ - Tutorial notebooks
21
+ - Integration examples with other tools
22
+ - Add them to the `examples/` directory
23
+
24
+ ### 📖 Improve Documentation
25
+ Documentation improvements are always welcome:
26
+ - Fix typos or clarify existing docs
27
+ - Add missing documentation
28
+ - Translate documentation
29
+ - Improve code comments
30
+
31
+ ### 🔧 Submit Code Contributions
32
+ Ready to contribute code? Great! See the sections below for setup and guidelines.
33
+
34
+ ---
35
+
36
+ ## Development Setup
37
+
38
+ ### Getting Started
39
+ 1. Fork and clone the repository:
40
+ ```bash
41
+ git clone https://github.com/flixOpt/flixopt.git
42
+ cd flixopt
43
+ ```
44
+
45
+ 2. Install development dependencies:
46
+ ```bash
47
+ pip install -e ".[full, dev]"
48
+ ```
49
+
50
+ 3. Set up pre-commit hooks (one-time setup):
51
+ ```bash
52
+ pre-commit install
53
+ ```
54
+
55
+ 4. Verify your setup:
56
+ ```bash
57
+ pytest
58
+ ```
59
+
60
+ ### Working with Documentation
61
+ FlixOpt uses [mkdocs](https://www.mkdocs.org/) to generate documentation.
62
+
63
+ To work on documentation:
64
+ ```bash
65
+ pip install -e ".[docs]"
66
+ mkdocs serve
67
+ ```
68
+ Then navigate to http://127.0.0.1:8000/
69
+
70
+ ---
71
+
72
+ ## Code Quality Standards
73
+
74
+ ### Automated Checks
75
+ We use [Ruff](https://github.com/astral-sh/ruff) for linting and formatting. After the one-time setup above, **code quality checks run automatically on every commit**.
76
+
77
+ ### Manual Checks
78
+ To run checks manually:
79
+ - `ruff check --fix .` - Check and fix linting issues
80
+ - `ruff format .` - Format code
81
+ - `pre-commit run --all-files` - Run all pre-commit checks
82
+
83
+ ### Testing
84
+ All tests are located in the `tests/` directory with a flat structure:
85
+ - `test_component.py` - Component tests
86
+ - `test_flow.py` - Flow tests
87
+ - `test_storage.py` - Storage tests
88
+ - etc.
89
+
90
+ #### Running Tests
91
+ - `pytest` - Run the full test suite (excluding examples by default)
92
+ - `pytest tests/test_component.py` - Run a specific test file
93
+ - `pytest tests/test_component.py::TestClassName` - Run a specific test class
94
+ - `pytest tests/test_component.py::TestClassName::test_method` - Run a specific test
95
+ - `pytest -m slow` - Run only slow tests
96
+ - `pytest -m examples` - Run example tests (normally skipped)
97
+ - `pytest -k "keyword"` - Run tests matching a keyword
98
+
99
+ #### Common Test Patterns
100
+ The `tests/conftest.py` file provides shared fixtures:
101
+ - `solver_fixture` - Parameterized solver fixture (HiGHS, Gurobi)
102
+ - `highs_solver` - HiGHS solver instance
103
+ - Coordinate configuration fixtures for timesteps, periods, scenarios
104
+
105
+ Use these fixtures by adding them as function parameters:
106
+ ```python
107
+ def test_my_feature(solver_fixture):
108
+ # solver_fixture is automatically provided by pytest
109
+ model = fx.FlowSystem(...)
110
+ model.solve(solver_fixture)
111
+ ```
112
+
113
+ #### Testing Guidelines
114
+ - Write tests for all new functionality
115
+ - Ensure all tests pass before submitting a PR
116
+ - Aim for 100% test coverage for new code
117
+ - Use descriptive test names that explain what's being tested
118
+ - Add the `@pytest.mark.slow` decorator for tests that take >5 seconds
119
+
120
+ ### Coding Guidelines
121
+ - Follow [PEP 8](https://pep8.org/) style guidelines
122
+ - Write clear, self-documenting code with helpful comments
123
+ - Include type hints for function signatures
124
+ - Create or update tests for new functionality
125
+ - Aim for 100% test coverage for new code
126
+
127
+ ---
128
+
129
+ ## Workflow
130
+
131
+ ### Branches & Pull Requests
132
+ 1. Create a feature branch from `main`:
133
+ ```bash
134
+ git checkout -b feature/your-feature-name
135
+ ```
136
+
137
+ 2. Make your changes and commit them with clear messages
138
+
139
+ 3. Push your branch and open a Pull Request
140
+
141
+ 4. Ensure all CI checks pass
142
+
143
+ ### Branch Naming
144
+ - Features: `feature/feature-name`
145
+ - Bug fixes: `fix/bug-description`
146
+ - Documentation: `docs/what-changed`
147
+
148
+ ### Commit Messages
149
+ - Use clear, descriptive commit messages
150
+ - Start with a verb (Add, Fix, Update, Remove, etc.)
151
+ - Keep the first line under 72 characters
152
+
153
+ ---
154
+
155
+ ## Releases
156
+
157
+ We follow **Semantic Versioning** (MAJOR.MINOR.PATCH). Releases are created manually from the `main` branch by maintainers.
158
+
159
+ ---
160
+
161
+ ## Questions?
162
+
163
+ If you have questions or need help, feel free to:
164
+ - Open a discussion on GitHub
165
+ - Ask in an issue
166
+ - Reach out to the maintainers
167
+
168
+ Thank you for contributing to FlixOpt!
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 3.2.1
3
+ Version: 3.3.1
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -112,7 +112,7 @@ class Calculation:
112
112
  'periodic': effect.submodel.periodic.total.solution.values,
113
113
  'total': effect.submodel.total.solution.values,
114
114
  }
115
- for effect in self.flow_system.effects
115
+ for effect in sorted(self.flow_system.effects.values(), key=lambda e: e.label_full.upper())
116
116
  },
117
117
  'Invest-Decisions': {
118
118
  'Invested': {
@@ -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
@@ -528,6 +529,15 @@ class Storage(Component):
528
529
  f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.'
529
530
  )
530
531
 
532
+ def __repr__(self) -> str:
533
+ """Return string representation."""
534
+ # Use build_repr_from_init directly to exclude charging and discharging
535
+ return fx_io.build_repr_from_init(
536
+ self,
537
+ excluded_params={'self', 'label', 'charging', 'discharging', 'kwargs'},
538
+ skip_default_size=True,
539
+ ) + fx_io.format_flow_details(self)
540
+
531
541
 
532
542
  @register_class_for_io
533
543
  class Transmission(Component):
@@ -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
@@ -448,13 +449,13 @@ PeriodicEffects = dict[str, Scalar]
448
449
  EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares
449
450
 
450
451
 
451
- class EffectCollection:
452
+ class EffectCollection(ElementContainer[Effect]):
452
453
  """
453
454
  Handling all Effects
454
455
  """
455
456
 
456
457
  def __init__(self, *effects: Effect):
457
- self._effects = {}
458
+ super().__init__(element_type_name='effects')
458
459
  self._standard_effect: Effect | None = None
459
460
  self._objective_effect: Effect | None = None
460
461
 
@@ -474,7 +475,7 @@ class EffectCollection:
474
475
  self.standard_effect = effect
475
476
  if effect.is_objective:
476
477
  self.objective_effect = effect
477
- self._effects[effect.label] = effect
478
+ self.add(effect) # Use the inherited add() method from ElementContainer
478
479
  logger.info(f'Registered new Effect: {effect.label}')
479
480
 
480
481
  def create_effect_values_dict(
@@ -520,10 +521,13 @@ class EffectCollection:
520
521
  # Check circular loops in effects:
521
522
  temporal, periodic = self.calculate_effect_share_factors()
522
523
 
523
- # Validate all referenced sources exist
524
- unknown = {src for src, _ in list(temporal.keys()) + list(periodic.keys()) if src not in self.effects}
524
+ # Validate all referenced effects (both sources and targets) exist
525
+ edges = list(temporal.keys()) + list(periodic.keys())
526
+ unknown_sources = {src for src, _ in edges if src not in self}
527
+ unknown_targets = {tgt for _, tgt in edges if tgt not in self}
528
+ unknown = unknown_sources | unknown_targets
525
529
  if unknown:
526
- raise KeyError(f'Unknown effects used in in effect share mappings: {sorted(unknown)}')
530
+ raise KeyError(f'Unknown effects used in effect share mappings: {sorted(unknown)}')
527
531
 
528
532
  temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal]))
529
533
  periodic_cycles = detect_cycles(tuples_to_adjacency_list([key for key in periodic]))
@@ -552,31 +556,23 @@ class EffectCollection:
552
556
  else:
553
557
  raise KeyError(f'Effect {effect} not found!')
554
558
  try:
555
- return self.effects[effect]
559
+ return super().__getitem__(effect) # Leverage ContainerMixin suggestions
556
560
  except KeyError as e:
557
- raise KeyError(f'Effect "{effect}" not found! Add it to the FlowSystem first!') from e
561
+ # Extract the original message and append context for cleaner output
562
+ original_msg = str(e).strip('\'"')
563
+ raise KeyError(f'{original_msg} Add the effect to the FlowSystem first.') from None
558
564
 
559
- def __iter__(self) -> Iterator[Effect]:
560
- return iter(self._effects.values())
561
-
562
- def __len__(self) -> int:
563
- return len(self._effects)
565
+ def __iter__(self) -> Iterator[str]:
566
+ return iter(self.keys()) # Iterate over keys like a normal dict
564
567
 
565
568
  def __contains__(self, item: str | Effect) -> bool:
566
569
  """Check if the effect exists. Checks for label or object"""
567
570
  if isinstance(item, str):
568
- return item in self.effects # Check if the label exists
571
+ return super().__contains__(item) # Check if the label exists
569
572
  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
573
+ return item.label_full in self and self[item.label_full] is item
574
574
  return False
575
575
 
576
- @property
577
- def effects(self) -> dict[str, Effect]:
578
- return self._effects
579
-
580
576
  @property
581
577
  def standard_effect(self) -> Effect:
582
578
  if self._standard_effect is None:
@@ -611,7 +607,7 @@ class EffectCollection:
611
607
  dict[tuple[str, str], xr.DataArray],
612
608
  ]:
613
609
  shares_periodic = {}
614
- for name, effect in self.effects.items():
610
+ for name, effect in self.items():
615
611
  if effect.share_from_periodic:
616
612
  for source, data in effect.share_from_periodic.items():
617
613
  if source not in shares_periodic:
@@ -620,7 +616,7 @@ class EffectCollection:
620
616
  shares_periodic = calculate_all_conversion_paths(shares_periodic)
621
617
 
622
618
  shares_temporal = {}
623
- for name, effect in self.effects.items():
619
+ for name, effect in self.items():
624
620
  if effect.share_from_temporal:
625
621
  for source, data in effect.share_from_temporal.items():
626
622
  if source not in shares_temporal:
@@ -670,7 +666,7 @@ class EffectCollectionModel(Submodel):
670
666
 
671
667
  def _do_modeling(self):
672
668
  super()._do_modeling()
673
- for effect in self.effects:
669
+ for effect in self.effects.values():
674
670
  effect.create_model(self._model)
675
671
  self.penalty = self.add_submodels(
676
672
  ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'),
@@ -684,7 +680,7 @@ class EffectCollectionModel(Submodel):
684
680
  )
685
681
 
686
682
  def _add_share_between_effects(self):
687
- for target_effect in self.effects:
683
+ for target_effect in self.effects.values():
688
684
  # 1. temporal: <- receiving temporal shares from other effects
689
685
  for source_effect, time_series in target_effect.share_from_temporal.items():
690
686
  target_effect.submodel.temporal.add_share(
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
11
11
  import numpy as np
12
12
  import xarray as xr
13
13
 
14
+ from . import io as fx_io
14
15
  from .config import CONFIG
15
16
  from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser
16
17
  from .features import InvestmentModel, OnOffModel
@@ -86,10 +87,12 @@ class Component(Element):
86
87
  super().__init__(label, meta_data=meta_data)
87
88
  self.inputs: list[Flow] = inputs or []
88
89
  self.outputs: list[Flow] = outputs or []
89
- self._check_unique_flow_labels()
90
90
  self.on_off_parameters = on_off_parameters
91
91
  self.prevent_simultaneous_flows: list[Flow] = prevent_simultaneous_flows or []
92
92
 
93
+ self._check_unique_flow_labels()
94
+ self._connect_flows()
95
+
93
96
  self.flows: dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs}
94
97
 
95
98
  def create_model(self, model: FlowSystemModel) -> ComponentModel:
@@ -115,6 +118,48 @@ class Component(Element):
115
118
  def _plausibility_checks(self) -> None:
116
119
  self._check_unique_flow_labels()
117
120
 
121
+ def _connect_flows(self):
122
+ # Inputs
123
+ for flow in self.inputs:
124
+ if flow.component not in ('UnknownComponent', self.label_full):
125
+ raise ValueError(
126
+ f'Flow "{flow.label}" already assigned to component "{flow.component}". '
127
+ f'Cannot attach to "{self.label_full}".'
128
+ )
129
+ flow.component = self.label_full
130
+ flow.is_input_in_component = True
131
+ # Outputs
132
+ for flow in self.outputs:
133
+ if flow.component not in ('UnknownComponent', self.label_full):
134
+ raise ValueError(
135
+ f'Flow "{flow.label}" already assigned to component "{flow.component}". '
136
+ f'Cannot attach to "{self.label_full}".'
137
+ )
138
+ flow.component = self.label_full
139
+ flow.is_input_in_component = False
140
+
141
+ # Validate prevent_simultaneous_flows: only allow local flows
142
+ if self.prevent_simultaneous_flows:
143
+ # Deduplicate while preserving order
144
+ seen = set()
145
+ self.prevent_simultaneous_flows = [
146
+ f for f in self.prevent_simultaneous_flows if id(f) not in seen and not seen.add(id(f))
147
+ ]
148
+ local = set(self.inputs + self.outputs)
149
+ foreign = [f for f in self.prevent_simultaneous_flows if f not in local]
150
+ if foreign:
151
+ names = ', '.join(f.label_full for f in foreign)
152
+ raise ValueError(
153
+ f'prevent_simultaneous_flows for "{self.label_full}" must reference its own flows. '
154
+ f'Foreign flows detected: {names}'
155
+ )
156
+
157
+ def __repr__(self) -> str:
158
+ """Return string representation with flow information."""
159
+ return fx_io.build_repr_from_init(
160
+ self, excluded_params={'self', 'label', 'inputs', 'outputs', 'kwargs'}, skip_default_size=True
161
+ ) + fx_io.format_flow_details(self)
162
+
118
163
 
119
164
  @register_class_for_io
120
165
  class Bus(Element):
@@ -216,6 +261,10 @@ class Bus(Element):
216
261
  def with_excess(self) -> bool:
217
262
  return False if self.excess_penalty_per_flow_hour is None else True
218
263
 
264
+ def __repr__(self) -> str:
265
+ """Return string representation."""
266
+ return super().__repr__() + fx_io.format_flow_details(self)
267
+
219
268
 
220
269
  @register_class_for_io
221
270
  class Connection:
@@ -493,6 +542,10 @@ class Flow(Element):
493
542
  # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
494
543
  return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
495
544
 
545
+ def _format_invest_params(self, params: InvestParameters) -> str:
546
+ """Format InvestParameters for display."""
547
+ return f'size: {params.format_for_repr()}'
548
+
496
549
 
497
550
  class FlowModel(ElementModel):
498
551
  element: Flow # Type hint