flixopt 3.2.0__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.0 → flixopt-3.3.1}/CHANGELOG.md +84 -31
  2. flixopt-3.3.1/CONTRIBUTE.md +168 -0
  3. {flixopt-3.2.0/flixopt.egg-info → flixopt-3.3.1}/PKG-INFO +2 -2
  4. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/calculation.py +1 -1
  5. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/components.py +10 -0
  6. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/effects.py +23 -27
  7. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/elements.py +54 -1
  8. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/flow_system.py +149 -86
  9. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/interface.py +23 -2
  10. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/io.py +396 -12
  11. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/results.py +48 -22
  12. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/structure.py +366 -48
  13. {flixopt-3.2.0 → flixopt-3.3.1/flixopt.egg-info}/PKG-INFO +2 -2
  14. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt.egg-info/SOURCES.txt +1 -0
  15. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt.egg-info/requires.txt +1 -1
  16. {flixopt-3.2.0 → flixopt-3.3.1}/pyproject.toml +1 -1
  17. {flixopt-3.2.0 → flixopt-3.3.1}/LICENSE +0 -0
  18. {flixopt-3.2.0 → flixopt-3.3.1}/MANIFEST.in +0 -0
  19. {flixopt-3.2.0 → flixopt-3.3.1}/README.md +0 -0
  20. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/__init__.py +0 -0
  21. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/aggregation.py +0 -0
  22. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/color_processing.py +0 -0
  23. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/commons.py +0 -0
  24. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/config.py +0 -0
  25. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/core.py +0 -0
  26. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/features.py +0 -0
  27. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/linear_converters.py +0 -0
  28. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/modeling.py +0 -0
  29. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/network_app.py +0 -0
  30. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/plotting.py +0 -0
  31. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt/solvers.py +0 -0
  32. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt.egg-info/dependency_links.txt +0 -0
  33. {flixopt-3.2.0 → flixopt-3.3.1}/flixopt.egg-info/top_level.txt +0 -0
  34. {flixopt-3.2.0 → flixopt-3.3.1}/setup.cfg +0 -0
@@ -81,6 +81,59 @@ 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
+
123
+ ## [3.2.1] - 2025-10-29
124
+
125
+ **Summary**:
126
+
127
+ 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/).
128
+
129
+ ### 🐛 Fixed
130
+ - Fixed resampling of FlowSystem to reset `hours_of_last_timestep` and `hours_of_previous_timesteps` properly
131
+
132
+ ### 👷 Development
133
+ - Improved issue templates
134
+
135
+ ---
136
+
84
137
  ## [3.2.0] - 2025-10-26
85
138
 
86
139
  **Summary**: Enhanced plotting capabilities with consistent color management, custom plotting kwargs support, and centralized I/O handling.
@@ -91,21 +144,21 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
91
144
 
92
145
  **Color management:**
93
146
  - **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots
94
- - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})`
95
- - Automatically propagates to all segments in segmented calculations
96
- - 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
97
150
  - **Flexible color inputs**: Supports colorscale names (e.g., 'turbo', 'plasma'), color lists, or label-to-color dictionaries
98
151
  - **Cross-backend compatibility**: Seamless color handling for both Plotly and Matplotlib
99
152
 
100
153
  **Plotting customization:**
101
154
  - **Plotting kwargs support**: Pass additional arguments to plotting backends via `px_kwargs`, `plot_kwargs`, and `backend_kwargs` parameters
102
155
  - **New `CONFIG.Plotting` configuration section**:
103
- - `default_show`: Control default plot visibility
104
- - `default_engine`: Choose 'plotly' or 'matplotlib'
105
- - `default_dpi`: Set resolution for saved plots
106
- - `default_facet_cols`: Configure default faceting columns
107
- - `default_sequential_colorscale`: Default for heatmaps (now 'turbo')
108
- - `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')
109
162
 
110
163
  **I/O improvements:**
111
164
  - Centralized JSON/YAML I/O with auto-format detection
@@ -272,12 +325,12 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
272
325
  **API and Behavior Changes:**
273
326
 
274
327
  - **Effect system redesigned** (no deprecation):
275
- - **Terminology changes**: Effect domains renamed for clarity: `operation` → `temporal`, `invest`/`investment` → `periodic`
276
- - **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)
277
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
278
331
  - **Bus and Effect object assignment**: Direct assignment of Bus/Effect objects is no longer supported. Use labels (strings) instead:
279
- - `Flow.bus` must receive a string label, not a Bus object
280
- - 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
281
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()`
282
335
 
283
336
  **Class and Method Renaming:**
@@ -291,14 +344,14 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
291
344
 
292
345
  - Investment binary variable: `is_invested` → `invested` in `InvestmentModel`
293
346
  - Switch tracking variables in `OnOffModel`:
294
- - `switch_on` → `switch|on`
295
- - `switch_off` → `switch|off`
296
- - `switch_on_nr` → `switch|count`
347
+ - `switch_on` → `switch|on`
348
+ - `switch_off` → `switch|off`
349
+ - `switch_on_nr` → `switch|count`
297
350
  - Effect submodel variables (following terminology changes):
298
- - `Effect(invest)|total` → `Effect(periodic)`
299
- - `Effect(operation)|total` → `Effect(temporal)`
300
- - `Effect(operation)|total_per_timestep` → `Effect(temporal)|per_timestep`
301
- - `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`
302
355
 
303
356
  **Data Structure Changes:**
304
357
 
@@ -519,7 +572,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
519
572
 
520
573
  ### ✨ Added
521
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
522
- - *Note: This is still experimental and might change in the future*
575
+ - *Note: This is still experimental and might change in the future*
523
576
 
524
577
  ### ♻️ Changed
525
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
@@ -561,8 +614,8 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
561
614
 
562
615
  ### 🐛 Fixed
563
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.
564
- - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$
565
- - 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}}$
566
619
 
567
620
  ### 🚧 Known Issues
568
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.
@@ -587,10 +640,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
587
640
 
588
641
  ### 💥 Breaking Changes
589
642
  - Restructured the modeling of the On/Off state of Flows or Components
590
- - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours`
591
- - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours`
592
- - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1`
593
- - 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
594
647
 
595
648
  ### 🐛 Fixed
596
649
  - Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters
@@ -636,10 +689,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
636
689
 
637
690
  **Variable Structure:**
638
691
  - Restructured the modeling of the On/Off state of Flows or Components
639
- - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours`
640
- - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours`
641
- - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1`
642
- - 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
643
696
 
644
697
  ### 🔥 Removed
645
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.0
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>
@@ -67,7 +67,7 @@ Requires-Dist: networkx==3.0.0; extra == "dev"
67
67
  Requires-Dist: werkzeug==3.0.0; extra == "dev"
68
68
  Provides-Extra: docs
69
69
  Requires-Dist: mkdocs==1.6.1; extra == "docs"
70
- Requires-Dist: mkdocs-material==9.6.21; extra == "docs"
70
+ Requires-Dist: mkdocs-material==9.6.22; extra == "docs"
71
71
  Requires-Dist: mkdocstrings-python==1.18.2; extra == "docs"
72
72
  Requires-Dist: mkdocs-table-reader-plugin==3.1.0; extra == "docs"
73
73
  Requires-Dist: mkdocs-gen-files==0.5.0; extra == "docs"
@@ -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