diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d2b76fb..da5460c43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,7 +136,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Enhanced documentation in `flixopt/types.py` with comprehensive examples and dimension explanation table - Clarified Effect type docstrings - Effect types are dicts, but single numeric values work through union types - Added clarifying comments in `effects.py` explaining parameter handling and transformation -- Improved OnOffParameters attribute documentation +- Improved ActivityParameters attribute documentation - Updated getting-started guide with loguru examples - Updated `config.py` docstrings for loguru integration @@ -215,7 +215,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Improved `summary.yaml` to use a compacted list representation for periods and scenarios ### 🐛 Fixed -- Using `switch_on_total_max` with periods or scenarios failed +- Using `startup_total_max` with periods or scenarios failed ### 📝 Docs - Add more comprehensive `CONTRIBUTE.md` @@ -467,7 +467,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Variable Renaming in Results:** - Investment binary variable: `is_invested` → `invested` in `InvestmentModel` -- Switch tracking variables in `OnOffModel`: +- Switch tracking variables in `ActivityModel`: - `switch_on` → `switch|on` - `switch_off` → `switch|off` - `switch_on_nr` → `switch|count` @@ -759,18 +759,18 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ### ✨ Added - Python 3.13 support added -- Logger warning if relative_minimum is used without on_off_parameters in Flow +- Logger warning if relative_minimum is used without activity_parameters in Flow - Greatly improved internal testing infrastructure by leveraging linopy's testing framework ### 💥 Breaking Changes - Restructured the modeling of the On/Off state of Flows or Components - - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + - Variable renaming: `...|consecutive_active_hours` → `...|ConsecutiveOn|hours` + - Variable renaming: `...|consecutive_inactive_hours` → `...|ConsecutiveOff|hours` + - Constraint renaming: `...|consecutive_active_hours_con1` → `...|ConsecutiveOn|con1` - Similar pattern for all consecutive on/off constraints ### 🐛 Fixed -- Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters +- Fixed the lower bound of `flow_rate` when using optional investments without ActivityParameters - Fixed bug that prevented divest effects from working - Added lower bounds of 0 to two unbounded vars (numerical improvement) @@ -779,7 +779,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ## [2.0.1] - 2025-04-10 ### ✨ Added -- Logger warning if relative_minimum is used without on_off_parameters in Flow +- Logger warning if relative_minimum is used without activity_parameters in Flow ### 🐛 Fixed - Replace "|" with "__" in filenames when saving figures (Windows compatibility) @@ -813,9 +813,9 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Variable Structure:** - Restructured the modeling of the On/Off state of Flows or Components - - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` + - Variable renaming: `...|consecutive_active_hours` → `...|ConsecutiveOn|hours` + - Variable renaming: `...|consecutive_inactive_hours` → `...|ConsecutiveOff|hours` + - Constraint renaming: `...|consecutive_active_hours_con1` → `...|ConsecutiveOn|con1` - Similar pattern for all consecutive on/off constraints ### 🔥 Removed diff --git a/docs/user-guide/mathematical-notation/elements/Flow.md b/docs/user-guide/mathematical-notation/elements/Flow.md index 5914ba911..07e76afb2 100644 --- a/docs/user-guide/mathematical-notation/elements/Flow.md +++ b/docs/user-guide/mathematical-notation/elements/Flow.md @@ -23,7 +23,7 @@ $$ $$ -This mathematical formulation can be extended by using [OnOffParameters](../features/OnOffParameters.md) +This mathematical formulation can be extended by using [ActivityParameters](../features/ActivityParameters.md) to define the on/off state of the Flow, or by using [InvestParameters](../features/InvestParameters.md) to change the size of the Flow from a constant to an optimization variable. @@ -34,7 +34,7 @@ to change the size of the Flow from a constant to an optimization variable. Flow formulation uses the following modeling patterns: - **[Scaled Bounds](../modeling-patterns/bounds-and-states.md#scaled-bounds)** - Basic flow rate bounds (equation $\eqref{eq:flow_rate}$) -- **[Scaled Bounds with State](../modeling-patterns/bounds-and-states.md#scaled-bounds-with-state)** - When combined with [OnOffParameters](../features/OnOffParameters.md) +- **[Scaled Bounds with State](../modeling-patterns/bounds-and-states.md#scaled-bounds-with-state)** - When combined with [ActivityParameters](../features/ActivityParameters.md) - **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions with [InvestParameters](../features/InvestParameters.md) --- @@ -48,7 +48,7 @@ Flow formulation uses the following modeling patterns: - `relative_minimum`, `relative_maximum`: Relative bounds $\text{p}^{\text{L}}_{\text{rel}}, \text{p}^{\text{U}}_{\text{rel}}$ - `effects_per_flow_hour`: Operational effects (costs, emissions, etc.) - `invest_parameters`: Optional investment modeling (see [InvestParameters](../features/InvestParameters.md)) -- `on_off_parameters`: Optional on/off operation (see [OnOffParameters](../features/OnOffParameters.md)) +- `activity_parameters`: Optional on/off operation (see [ActivityParameters](../features/ActivityParameters.md)) See the [`Flow`][flixopt.elements.Flow] API documentation for complete parameter list and usage examples. @@ -56,7 +56,7 @@ See the [`Flow`][flixopt.elements.Flow] API documentation for complete parameter ## See Also -- [OnOffParameters](../features/OnOffParameters.md) - Binary on/off operation +- [ActivityParameters](../features/ActivityParameters.md) - Binary on/off operation - [InvestParameters](../features/InvestParameters.md) - Variable flow sizing - [Bus](../elements/Bus.md) - Flow balance constraints - [LinearConverter](../elements/LinearConverter.md) - Flow ratio constraints diff --git a/docs/user-guide/mathematical-notation/features/OnOffParameters.md b/docs/user-guide/mathematical-notation/features/OnOffParameters.md index 4ec6a9726..e0bae6e4a 100644 --- a/docs/user-guide/mathematical-notation/features/OnOffParameters.md +++ b/docs/user-guide/mathematical-notation/features/OnOffParameters.md @@ -1,6 +1,6 @@ -# OnOffParameters +# ActivityParameters -[`OnOffParameters`][flixopt.interface.OnOffParameters] model equipment that operates in discrete on/off states rather than continuous operation. This captures realistic operational constraints including startup costs, minimum run times, cycling limitations, and maintenance scheduling. +[`ActivityParameters`][flixopt.interface.ActivityParameters] model equipment that operates in discrete on/off states rather than continuous operation. This captures realistic operational constraints including startup costs, minimum run times, cycling limitations, and maintenance scheduling. ## Binary State Variable @@ -190,14 +190,14 @@ With: ## Integration with Flow Bounds -OnOffParameters modify flow rate bounds by coupling them to the on/off state. +ActivityParameters modify flow rate bounds by coupling them to the on/off state. -**Without OnOffParameters** (continuous operation): +**Without ActivityParameters** (continuous operation): $$ P \cdot \text{rel}_\text{lower} \leq p(t) \leq P \cdot \text{rel}_\text{upper} $$ -**With OnOffParameters** (binary operation): +**With ActivityParameters** (binary operation): $$ s(t) \cdot P \cdot \max(\varepsilon, \text{rel}_\text{lower}) \leq p(t) \leq s(t) \cdot P \cdot \text{rel}_\text{upper} $$ @@ -212,7 +212,7 @@ Using the **bounds with state** pattern from [Bounds and States](../modeling-pat ## Complete Formulation Summary -For equipment with OnOffParameters, the complete constraint system includes: +For equipment with ActivityParameters, the complete constraint system includes: 1. **State variable:** $s(t) \in \{0, 1\}$ 2. **Switch tracking:** $s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1)$ @@ -232,18 +232,18 @@ For equipment with OnOffParameters, the complete constraint system includes: ## Implementation -**Python Class:** [`OnOffParameters`][flixopt.interface.OnOffParameters] +**Python Class:** [`ActivityParameters`][flixopt.interface.ActivityParameters] **Key Parameters:** -- `effects_per_switch_on`: Costs per startup event +- `effects_per_startup`: Costs per startup event - `effects_per_running_hour`: Costs per hour of operation -- `on_hours_total_min`, `on_hours_total_max`: Total runtime bounds -- `consecutive_on_hours_min`, `consecutive_on_hours_max`: Consecutive runtime bounds -- `consecutive_off_hours_min`, `consecutive_off_hours_max`: Consecutive shutdown bounds -- `switch_on_total_max`: Maximum number of startups +- `active_hours_total_min`, `active_hours_total_max`: Total runtime bounds +- `consecutive_active_hours_min`, `consecutive_active_hours_max`: Consecutive runtime bounds +- `consecutive_inactive_hours_min`, `consecutive_inactive_hours_max`: Consecutive shutdown bounds +- `startup_total_max`: Maximum number of startups - `force_switch_on`: Create switch variables even without limits (for tracking) -See the [`OnOffParameters`][flixopt.interface.OnOffParameters] API documentation for complete parameter list and usage examples. +See the [`ActivityParameters`][flixopt.interface.ActivityParameters] API documentation for complete parameter list and usage examples. **Mathematical Patterns Used:** - [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions) - Switch tracking @@ -260,43 +260,43 @@ See the [`OnOffParameters`][flixopt.interface.OnOffParameters] API documentation ### Power Plant with Startup Costs ```python -power_plant = OnOffParameters( - effects_per_switch_on={'startup_cost': 25000}, # €25k per startup +power_plant = ActivityParameters( + effects_per_startup={'startup_cost': 25000}, # €25k per startup effects_per_running_hour={'fixed_om': 125}, # €125/hour while running - consecutive_on_hours_min=8, # Minimum 8-hour run - consecutive_off_hours_min=4, # 4-hour cooling period - on_hours_total_max=6000, # Annual limit + consecutive_active_hours_min=8, # Minimum 8-hour run + consecutive_inactive_hours_min=4, # 4-hour cooling period + active_hours_total_max=6000, # Annual limit ) ``` ### Batch Process with Cycling Limits ```python -batch_reactor = OnOffParameters( - effects_per_switch_on={'setup_cost': 1500}, - consecutive_on_hours_min=12, # 12-hour minimum batch - consecutive_on_hours_max=24, # 24-hour maximum batch - consecutive_off_hours_min=6, # Cleaning time - switch_on_total_max=200, # Max 200 batches +batch_reactor = ActivityParameters( + effects_per_startup={'setup_cost': 1500}, + consecutive_active_hours_min=12, # 12-hour minimum batch + consecutive_active_hours_max=24, # 24-hour maximum batch + consecutive_inactive_hours_min=6, # Cleaning time + startup_total_max=200, # Max 200 batches ) ``` ### HVAC with Cycle Prevention ```python -hvac = OnOffParameters( - effects_per_switch_on={'compressor_wear': 0.5}, - consecutive_on_hours_min=1, # Prevent short cycling - consecutive_off_hours_min=0.5, # 30-min minimum off - switch_on_total_max=2000, # Limit compressor starts +hvac = ActivityParameters( + effects_per_startup={'compressor_wear': 0.5}, + consecutive_active_hours_min=1, # Prevent short cycling + consecutive_inactive_hours_min=0.5, # 30-min minimum off + startup_total_max=2000, # Limit compressor starts ) ``` ### Backup Generator with Testing Requirements ```python -backup_gen = OnOffParameters( - effects_per_switch_on={'fuel_priming': 50}, # L diesel - consecutive_on_hours_min=0.5, # 30-min test duration - consecutive_off_hours_max=720, # Test every 30 days - on_hours_total_min=26, # Weekly testing requirement +backup_gen = ActivityParameters( + effects_per_startup={'fuel_priming': 50}, # L diesel + consecutive_active_hours_min=0.5, # 30-min test duration + consecutive_inactive_hours_max=720, # Test every 30 days + active_hours_total_min=26, # Weekly testing requirement ) ``` @@ -304,4 +304,4 @@ backup_gen = OnOffParameters( ## Notes -**Time Series Boundary:** The final time period constraints for consecutive_on_hours_min/max and consecutive_off_hours_min/max are not enforced at the end of the planning horizon. This allows optimization to end with ongoing campaigns that may be shorter/longer than specified, as they extend beyond the modeled period. +**Time Series Boundary:** The final time period constraints for consecutive_active_hours_min/max and consecutive_inactive_hours_min/max are not enforced at the end of the planning horizon. This allows optimization to end with ongoing campaigns that may be shorter/longer than specified, as they extend beyond the modeled period. diff --git a/docs/user-guide/mathematical-notation/index.md b/docs/user-guide/mathematical-notation/index.md index 27e7b7e9a..9da6d0099 100644 --- a/docs/user-guide/mathematical-notation/index.md +++ b/docs/user-guide/mathematical-notation/index.md @@ -56,10 +56,10 @@ Mathematical formulations for core FlixOpt elements (corresponding to [`flixopt. Mathematical formulations for optional features (corresponding to parameters in FlixOpt classes): - [InvestParameters](features/InvestParameters.md) - Investment decision modeling -- [OnOffParameters](features/OnOffParameters.md) - Binary on/off operation +- [ActivityParameters](features/ActivityParameters.md) - Binary on/off operation - [Piecewise](features/Piecewise.md) - Piecewise linear approximations -**User API:** When you pass `invest_parameters` or `on_off_parameters` to a `Flow` or component, these formulations are applied. +**User API:** When you pass `invest_parameters` or `activity_parameters` to a `Flow` or component, these formulations are applied. ### System-Level - [Effects, Penalty & Objective](effects-penalty-objective.md) - Cost allocation and objective function @@ -97,7 +97,7 @@ Mathematical formulations for optional features (corresponding to parameters in | Concept | Documentation | Python Class | |---------|---------------|--------------| | **Binary investment** | [InvestParameters](features/InvestParameters.md) | [`InvestParameters`][flixopt.interface.InvestParameters] | -| **On/off operation** | [OnOffParameters](features/OnOffParameters.md) | [`OnOffParameters`][flixopt.interface.OnOffParameters] | +| **On/off operation** | [ActivityParameters](features/ActivityParameters.md) | [`ActivityParameters`][flixopt.interface.ActivityParameters] | | **Piecewise segments** | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] | ### Modeling Patterns Cross-Reference @@ -119,5 +119,5 @@ Mathematical formulations for optional features (corresponding to parameters in | `Storage` | [Storage](elements/Storage.md) | [`Storage`][flixopt.components.Storage] | | `LinearConverter` | [LinearConverter](elements/LinearConverter.md) | [`LinearConverter`][flixopt.components.LinearConverter] | | `InvestParameters` | [InvestParameters](features/InvestParameters.md) | [`InvestParameters`][flixopt.interface.InvestParameters] | -| `OnOffParameters` | [OnOffParameters](features/OnOffParameters.md) | [`OnOffParameters`][flixopt.interface.OnOffParameters] | +| `ActivityParameters` | [ActivityParameters](features/ActivityParameters.md) | [`ActivityParameters`][flixopt.interface.ActivityParameters] | | `Piecewise` | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] | diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md b/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md index d5821948f..e50eb7328 100644 --- a/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md +++ b/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md @@ -45,7 +45,7 @@ With: **Implementation:** [`BoundingPatterns.bounds_with_state()`][flixopt.modeling.BoundingPatterns.bounds_with_state] **Used in:** -- Flow rates with on/off operation (see [OnOffParameters](../features/OnOffParameters.md)) +- Flow rates with on/off operation (see [ActivityParameters](../features/ActivityParameters.md)) - Investment size decisions (see [InvestParameters](../features/InvestParameters.md)) --- @@ -108,7 +108,7 @@ Where $v_\text{scale,max}$ and $v_\text{scale,min}$ are the maximum and minimum **Used in:** - Flow rates with on/off operation and investment sizing -- Components combining [OnOffParameters](../features/OnOffParameters.md) and [InvestParameters](../features/InvestParameters.md) +- Components combining [ActivityParameters](../features/ActivityParameters.md) and [InvestParameters](../features/InvestParameters.md) --- diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md index 5d430d28c..c419fac2c 100644 --- a/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md +++ b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md @@ -154,6 +154,6 @@ maintenance_duration = modeling.consecutive_duration_tracking( ## Used In This pattern is used in: -- [`OnOffParameters`](../features/OnOffParameters.md) - Minimum on/off times +- [`ActivityParameters`](../features/ActivityParameters.md) - Minimum on/off times - Operating mode constraints with minimum durations - Startup/shutdown sequence modeling diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/index.md b/docs/user-guide/mathematical-notation/modeling-patterns/index.md index 15ff8dbd2..1fb5c70f1 100644 --- a/docs/user-guide/mathematical-notation/modeling-patterns/index.md +++ b/docs/user-guide/mathematical-notation/modeling-patterns/index.md @@ -43,7 +43,7 @@ These patterns are used throughout FlixOpt components: - [`Flow`][flixopt.elements.Flow] uses **scaled bounds with state** for flow rate constraints - [`Storage`][flixopt.components.Storage] uses **basic bounds** for charge state -- [`OnOffParameters`](../features/OnOffParameters.md) uses **state transitions** for startup/shutdown +- [`ActivityParameters`](../features/ActivityParameters.md) uses **state transitions** for startup/shutdown - [`InvestParameters`](../features/InvestParameters.md) uses **bounds with state** for investment decisions ## Implementation diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md index dc75a8008..2af60a042 100644 --- a/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md +++ b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md @@ -221,7 +221,7 @@ model.add_constraint(increase.sum() <= max_total_expansion) ## Used In These patterns are used in: -- [`OnOffParameters`](../features/OnOffParameters.md) - Startup/shutdown tracking and costs +- [`ActivityParameters`](../features/ActivityParameters.md) - Startup/shutdown tracking and costs - Operating mode switching with transition costs - Investment planning with staged capacity additions - Inventory management with controlled stock changes diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 3ff5b251c..3c9c4ddda 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -51,7 +51,7 @@ Gaskessel = fx.linear_converters.Boiler( 'Kessel', eta=0.5, # Efficiency ratio - on_off_parameters=fx.OnOffParameters( + activity_parameters=fx.ActivityParameters( effects_per_running_hour={Costs.label: 0, CO2.label: 1000} ), # CO2 emissions per hour Q_th=fx.Flow( @@ -69,14 +69,14 @@ relative_maximum=1, # Maximum part load previous_flow_rate=50, # Previous flow rate flow_hours_total_max=1e6, # Total energy flow limit - on_off_parameters=fx.OnOffParameters( - on_hours_total_min=0, # Minimum operating hours - on_hours_total_max=1000, # Maximum operating hours - consecutive_on_hours_max=10, # Max consecutive operating hours - consecutive_on_hours_min=np.array([1, 1, 1, 1, 1, 2, 2, 2, 2]), # min consecutive operation hours - consecutive_off_hours_max=10, # Max consecutive off hours - effects_per_switch_on=0.01, # Cost per switch-on - switch_on_total_max=1000, # Max number of starts + activity_parameters=fx.ActivityParameters( + active_hours_total_min=0, # Minimum operating hours + active_hours_total_max=1000, # Maximum operating hours + consecutive_active_hours_max=10, # Max consecutive operating hours + consecutive_active_hours_min=np.array([1, 1, 1, 1, 1, 2, 2, 2, 2]), # min consecutive operation hours + consecutive_inactive_hours_max=10, # Max consecutive off hours + effects_per_startup=0.01, # Cost per switch-on + startup_total_max=1000, # Max number of starts ), ), Q_fu=fx.Flow(label='Q_fu', bus='Gas', size=200), @@ -88,7 +88,7 @@ 'BHKW2', eta_th=0.5, eta_el=0.4, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + activity_parameters=fx.ActivityParameters(effects_per_startup=0.01), P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3, previous_flow_rate=20), # The CHP was ON previously @@ -112,7 +112,7 @@ inputs=[Q_fu], outputs=[P_el, Q_th], piecewise_conversion=piecewise_conversion, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + activity_parameters=fx.ActivityParameters(effects_per_startup=0.01), ) # 4. Define Storage Component diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index e339c1c24..46dc9b5e9 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -79,7 +79,7 @@ size=95, relative_minimum=12 / 95, previous_flow_rate=20, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=1000), + activity_parameters=fx.ActivityParameters(effects_per_startup=1000), ), ) @@ -88,7 +88,7 @@ 'BHKW2', eta_th=0.58, eta_el=0.22, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=24000), + activity_parameters=fx.ActivityParameters(effects_per_startup=24000), P_el=fx.Flow('P_el', bus='Strom', size=200), Q_th=fx.Flow('Q_th', bus='Fernwärme', size=200), Q_fu=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288, previous_flow_rate=100), diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index bf4f24617..781e8d802 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -121,7 +121,7 @@ size=50, relative_minimum=0.1, relative_maximum=1, - on_off_parameters=fx.OnOffParameters(), + activity_parameters=fx.ActivityParameters(), ), Q_fu=fx.Flow(label='Q_fu', bus='Gas'), ) @@ -132,7 +132,9 @@ label='CHP', eta_th=0.48, # Realistic thermal efficiency (48%) eta_el=0.40, # Realistic electrical efficiency (40%) - P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters()), + P_el=fx.Flow( + 'P_el', bus='Strom', size=60, relative_minimum=5 / 60, activity_parameters=fx.ActivityParameters() + ), Q_th=fx.Flow('Q_th', bus='Fernwärme'), Q_fu=fx.Flow('Q_fu', bus='Gas'), ) diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 7354cb877..5b40d3f69 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -55,15 +55,15 @@ ), relative_minimum=0.2, previous_flow_rate=20, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), + activity_parameters=fx.ActivityParameters(effects_per_startup=300), ), ), fx.linear_converters.CHP( 'BHKW2', eta_th=0.58, eta_el=0.22, - on_off_parameters=fx.OnOffParameters( - effects_per_switch_on=1_000, consecutive_on_hours_min=10, consecutive_off_hours_min=10 + activity_parameters=fx.ActivityParameters( + effects_per_startup=1_000, consecutive_active_hours_min=10, consecutive_inactive_hours_min=10 ), P_el=fx.Flow('P_el', bus='Strom'), Q_th=fx.Flow('Q_th', bus='Fernwärme'), diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 6f0dbfe5d..e01213b2e 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -28,7 +28,7 @@ from .effects import Effect from .elements import Bus, Flow from .flow_system import FlowSystem -from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects +from .interface import ActivityParameters, InvestParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects __all__ = [ 'TimeSeriesData', @@ -48,7 +48,7 @@ 'SegmentedCalculation', 'AggregatedCalculation', 'InvestParameters', - 'OnOffParameters', + 'ActivityParameters', 'Piece', 'Piecewise', 'PiecewiseConversion', diff --git a/flixopt/components.py b/flixopt/components.py index c51b4b7d2..211137975 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -15,7 +15,7 @@ from .core import PlausibilityError from .elements import Component, ComponentModel, Flow from .features import InvestmentModel, PiecewiseModel -from .interface import InvestParameters, OnOffParameters, PiecewiseConversion +from .interface import ActivityParameters, InvestParameters, PiecewiseConversion from .modeling import BoundingPatterns from .structure import FlowSystemModel, register_class_for_io @@ -48,9 +48,9 @@ class LinearConverter(Component): label: The label of the Element. Used to identify it in the FlowSystem. inputs: list of input Flows that feed into the converter. outputs: list of output Flows that are produced by the converter. - on_off_parameters: Information about on and off state of LinearConverter. - Component is On/Off if all connected Flows are On/Off. This induces an - On-Variable (binary) in all Flows! If possible, use OnOffParameters in a + activity_parameters: Information about active and inactive state of LinearConverter. + Component is active/inactive if all connected Flows are active/inactive. This induces an + active-Variable (binary) in all Flows! If possible, use ActivityParameters in a single Flow instead to keep the number of binary variables low. conversion_factors: Linear relationships between flows expressed as a list of dictionaries. Each dictionary maps flow labels to their coefficients in one @@ -167,12 +167,12 @@ def __init__( label: str, inputs: list[Flow], outputs: list[Flow], - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, conversion_factors: list[dict[str, Numeric_TPS]] | None = None, piecewise_conversion: PiecewiseConversion | None = None, meta_data: dict | None = None, ): - super().__init__(label, inputs, outputs, on_off_parameters, meta_data=meta_data) + super().__init__(label, inputs, outputs, activity_parameters, meta_data=meta_data) self.conversion_factors = conversion_factors or [] self.piecewise_conversion = piecewise_conversion @@ -566,8 +566,8 @@ class Transmission(Component): relative_losses: Proportional losses as fraction of throughput (e.g., 0.02 for 2% loss). Applied as: output = input × (1 - relative_losses) absolute_losses: Fixed losses that occur when transmission is active. - Automatically creates binary variables for on/off states. - on_off_parameters: Parameters defining binary operation constraints and costs. + Automatically creates binary variables for active/inactive states. + activity_parameters: Parameters defining binary operation constraints and costs. prevent_simultaneous_flows_in_both_directions: If True, prevents simultaneous flow in both directions. Increases binary variables but reflects physical reality for most transmission systems. Default is True. @@ -622,7 +622,7 @@ class Transmission(Component): ) ``` - Material conveyor with on/off operation: + Material conveyor with active/inactive operation: ```python conveyor_belt = Transmission( @@ -630,10 +630,10 @@ class Transmission(Component): in1=loading_station, out1=unloading_station, absolute_losses=25, # 25 kW motor power when running - on_off_parameters=OnOffParameters( - effects_per_switch_on={'maintenance': 0.1}, - consecutive_on_hours_min=2, # Minimum 2-hour operation - switch_on_total_max=10, # Maximum 10 starts per day + activity_parameters=ActivityParameters( + effects_per_startup={'maintenance': 0.1}, + consecutive_active_hours_min=2, # Minimum 2-hour operation + startup_total_max=10, # Maximum 10 starts per day ), ) ``` @@ -647,7 +647,7 @@ class Transmission(Component): When using InvestParameters on in1, the capacity automatically applies to in2 to maintain consistent bidirectional capacity without additional investment variables. - Absolute losses force the creation of binary on/off variables, which increases + Absolute losses force the creation of binary active/inactive variables, which increases computational complexity but enables realistic modeling of equipment with standby power consumption. @@ -664,7 +664,7 @@ def __init__( out2: Flow | None = None, relative_losses: Numeric_TPS | None = None, absolute_losses: Numeric_TPS | None = None, - on_off_parameters: OnOffParameters = None, + activity_parameters: ActivityParameters = None, prevent_simultaneous_flows_in_both_directions: bool = True, balanced: bool = False, meta_data: dict | None = None, @@ -673,7 +673,7 @@ def __init__( label, inputs=[flow for flow in (in1, in2) if flow is not None], outputs=[flow for flow in (out1, out2) if flow is not None], - on_off_parameters=on_off_parameters, + activity_parameters=activity_parameters, prevent_simultaneous_flows=None if in2 is None or prevent_simultaneous_flows_in_both_directions is False else [in1, in2], @@ -732,8 +732,8 @@ class TransmissionModel(ComponentModel): def __init__(self, model: FlowSystemModel, element: Transmission): if (element.absolute_losses is not None) and np.any(element.absolute_losses != 0): for flow in element.inputs + element.outputs: - if flow.on_off_parameters is None: - flow.on_off_parameters = OnOffParameters() + if flow.activity_parameters is None: + flow.activity_parameters = ActivityParameters() super().__init__(model, element) @@ -758,7 +758,7 @@ def _do_modeling(self): def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) -> linopy.Constraint: """Creates an Equation for the Transmission efficiency and adds it to the model""" - # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t)) + # eq: out(t) + active(t)*loss_abs(t) = in(t)*(1 - loss_rel(t)) rel_losses = 0 if self.element.relative_losses is None else self.element.relative_losses con_transmission = self.add_constraints( out_flow.submodel.flow_rate == in_flow.submodel.flow_rate * (1 - rel_losses), @@ -766,7 +766,7 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) ) if self.element.absolute_losses is not None: - con_transmission.lhs += in_flow.submodel.on_off.on * self.element.absolute_losses + con_transmission.lhs += in_flow.submodel.active_inactive.active * self.element.absolute_losses return con_transmission @@ -798,7 +798,7 @@ def _do_modeling(self): ) else: - # TODO: Improve Inclusion of OnOffParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself + # TODO: Improve Inclusion of ActivityParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself piecewise_conversion = { self.element.flows[flow].submodel.flow_rate.name: piecewise for flow, piecewise in self.element.piecewise_conversion.items() @@ -810,7 +810,7 @@ def _do_modeling(self): label_of_element=self.label_of_element, label_of_model=f'{self.label_of_element}', piecewise_variables=piecewise_conversion, - zero_point=self.on_off.on if self.on_off is not None else False, + zero_point=self.active_inactive.active if self.active_inactive is not None else False, dims=('time', 'period', 'scenario'), ), short_name='PiecewiseConversion', @@ -964,7 +964,7 @@ def _investment(self) -> InvestmentModel | None: @property def investment(self) -> InvestmentModel | None: - """OnOff feature""" + """Investment feature""" if 'investment' not in self.submodels: return None return self.submodels['investment'] diff --git a/flixopt/elements.py b/flixopt/elements.py index f47002b3a..2bb88ceba 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -14,8 +14,8 @@ from . import io as fx_io from .config import CONFIG from .core import PlausibilityError -from .features import InvestmentModel, OnOffModel -from .interface import InvestParameters, OnOffParameters +from .features import ActivityModel, InvestmentModel +from .interface import ActivityParameters, InvestParameters from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilitiesAbstract from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io @@ -56,9 +56,9 @@ class Component(Element): energy/material consumption by the component. outputs: list of output Flows leaving the component. These represent energy/material production by the component. - on_off_parameters: Defines binary operation constraints and costs when the - component has discrete on/off states. Creates binary variables for all - connected Flows. For better performance, prefer defining OnOffParameters + activity_parameters: Defines binary operation constraints and costs when the + component has discrete active/inactive states. Creates binary variables for all + connected Flows. For better performance, prefer defining ActivityParameters on individual Flows when possible. prevent_simultaneous_flows: list of Flows that cannot be active simultaneously. Creates binary variables to enforce mutual exclusivity. Use sparingly as @@ -68,13 +68,13 @@ class Component(Element): Note: Component operational state is determined by its connected Flows: - - Component is "on" if ANY of its Flows is active (flow_rate > 0) - - Component is "off" only when ALL Flows are inactive (flow_rate = 0) + - Component is "active" if ANY of its Flows is active (flow_rate > 0) + - Component is "inactive" only when ALL Flows are inactive (flow_rate = 0) Binary variables and constraints: - - on_off_parameters creates binary variables for ALL connected Flows + - activity_parameters creates binary variables for ALL connected Flows - prevent_simultaneous_flows creates binary variables for specified Flows - - For better computational performance, prefer Flow-level OnOffParameters + - For better computational performance, prefer Flow-level ActivityParameters Component is an abstract base class. In practice, use specialized subclasses: - LinearConverter: Linear input/output relationships @@ -89,14 +89,14 @@ def __init__( label: str, inputs: list[Flow] | None = None, outputs: list[Flow] | None = None, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, prevent_simultaneous_flows: list[Flow] | None = None, meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) self.inputs: list[Flow] = inputs or [] self.outputs: list[Flow] = outputs or [] - self.on_off_parameters = on_off_parameters + self.activity_parameters = activity_parameters self.prevent_simultaneous_flows: list[Flow] = prevent_simultaneous_flows or [] self._check_unique_flow_labels() @@ -111,8 +111,8 @@ def create_model(self, model: FlowSystemModel) -> ComponentModel: def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: prefix = '|'.join(filter(None, [name_prefix, self.label_full])) - if self.on_off_parameters is not None: - self.on_off_parameters.transform_data(flow_system, prefix) + if self.activity_parameters is not None: + self.activity_parameters.transform_data(flow_system, prefix) for flow in self.inputs + self.outputs: flow.transform_data(flow_system) # Flow doesnt need the name_prefix @@ -308,7 +308,7 @@ class Flow(Element): Integration with Parameter Classes: - **InvestParameters**: Used for `size` when flow Size is an investment decision - - **OnOffParameters**: Used for `on_off_parameters` when flow has discrete states + - **ActivityParameters**: Used for `activity_parameters` when flow has discrete states Mathematical Formulation: See the complete mathematical model in the documentation: @@ -324,12 +324,12 @@ class Flow(Element): load_factor_max: Maximum average utilization (0-1). Default: 1. effects_per_flow_hour: Operational costs/impacts per flow-hour. Dict mapping effect names to values (e.g., {'cost': 45, 'CO2': 0.8}). - on_off_parameters: Binary operation constraints (OnOffParameters). Default: None. + activity_parameters: Binary operation constraints (ActivityParameters). Default: None. flow_hours_total_max: Maximum cumulative flow-hours. Alternative to load_factor_max. flow_hours_total_min: Minimum cumulative flow-hours. Alternative to load_factor_min. fixed_relative_profile: Predetermined pattern as fraction of size. Flow rate = size × fixed_relative_profile(t). - previous_flow_rate: Initial flow state for on/off dynamics. Default: None (off). + previous_flow_rate: Initial flow state for active/inactive dynamics. Default: None (inactive). meta_data: Additional info stored in results. Python native types only. Examples: @@ -366,13 +366,13 @@ class Flow(Element): label='heat_output', bus='heating_network', size=50, # 50 kW thermal - relative_minimum=0.3, # Minimum 15 kW output when on + relative_minimum=0.3, # Minimum 15 kW output when active effects_per_flow_hour={'electricity_cost': 25, 'maintenance': 2}, - on_off_parameters=OnOffParameters( - effects_per_switch_on={'startup_cost': 100, 'wear': 0.1}, - consecutive_on_hours_min=2, # Must run at least 2 hours - consecutive_off_hours_min=1, # Must stay off at least 1 hour - switch_on_total_max=200, # Maximum 200 starts per period + activity_parameters=ActivityParameters( + effects_per_startup={'startup_cost': 100, 'wear': 0.1}, + consecutive_active_hours_min=2, # Must run at least 2 hours + consecutive_inactive_hours_min=1, # Must stay inactive at least 1 hour + startup_total_max=200, # Maximum 200 starts per period ), ) ``` @@ -407,7 +407,7 @@ class Flow(Element): `load_factor_min/max` for utilization-based constraints. **Relative Bounds**: Set `relative_minimum > 0` only when equipment cannot - operate below that level. Use `on_off_parameters` for discrete on/off behavior. + operate below that level. Use `activity_parameters` for discrete active/inactive behavior. **Fixed Profiles**: Use `fixed_relative_profile` for known exact patterns, `relative_maximum` for upper bounds on optimization variables. @@ -433,7 +433,7 @@ def __init__( relative_minimum: Numeric_TPS = 0, relative_maximum: Numeric_TPS = 1, effects_per_flow_hour: Effect_TPS | Numeric_TPS | None = None, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, flow_hours_total_max: Numeric_PS | None = None, flow_hours_total_min: Numeric_PS | None = None, load_factor_min: Numeric_PS | None = None, @@ -453,7 +453,7 @@ def __init__( self.effects_per_flow_hour = effects_per_flow_hour if effects_per_flow_hour is not None else {} self.flow_hours_total_max = flow_hours_total_max self.flow_hours_total_min = flow_hours_total_min - self.on_off_parameters = on_off_parameters + self.activity_parameters = activity_parameters self.previous_flow_rate = previous_flow_rate @@ -500,8 +500,8 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None f'{prefix}|load_factor_min', self.load_factor_min, dims=['period', 'scenario'] ) - if self.on_off_parameters is not None: - self.on_off_parameters.transform_data(flow_system, prefix) + if self.activity_parameters is not None: + self.activity_parameters.transform_data(flow_system, prefix) if isinstance(self.size, InvestParameters): self.size.transform_data(flow_system, prefix) else: @@ -521,17 +521,17 @@ def _plausibility_checks(self) -> None: f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.' ) - if self.fixed_relative_profile is not None and self.on_off_parameters is not None: + if self.fixed_relative_profile is not None and self.activity_parameters is not None: logger.warning( - f'Flow {self.label_full} has both a fixed_relative_profile and an on_off_parameters.' - f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.' + f'Flow {self.label_full} has both a fixed_relative_profile and activity_parameters.' + f'This will allow the flow to be switched active and inactive, effectively differing from the fixed_flow_rate.' ) - if np.any(self.relative_minimum > 0) and self.on_off_parameters is None: + if np.any(self.relative_minimum > 0) and self.activity_parameters is None: logger.warning( - f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. ' - f'This prevents the Flow from switching off (flow_rate = 0). ' - f'Consider using on_off_parameters to allow the Flow to be switched on and off.' + f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no activity_parameters. ' + f'This prevents the Flow from switching inactive (flow_rate = 0). ' + f'Consider using activity_parameters to allow the Flow to be switched active and inactive.' ) if self.previous_flow_rate is not None: @@ -597,18 +597,18 @@ def _do_modeling(self): # Effects self._create_shares() - def _create_on_off_model(self): - on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) + def _create_active_inactive_model(self): + active = self.add_variables(binary=True, short_name='active', coords=self._model.get_coords()) self.add_submodels( - OnOffModel( + ActivityModel( model=self._model, label_of_element=self.label_of_element, - parameters=self.element.on_off_parameters, - on_variable=on, + parameters=self.element.activity_parameters, + active_variable=active, previous_states=self.previous_states, label_of_model=self.label_of_element, ), - short_name='on_off', + short_name='active_inactive', ) def _create_investment_model(self): @@ -623,23 +623,23 @@ def _create_investment_model(self): ) def _constraint_flow_rate(self): - if not self.with_investment and not self.with_on_off: + if not self.with_investment and not self.with_active_inactive: # Most basic case. Already covered by direct variable bounds pass - elif self.with_on_off and not self.with_investment: - # OnOff, but no Investment - self._create_on_off_model() + elif self.with_active_inactive and not self.with_investment: + # ActiveInactive, but no Investment + self._create_active_inactive_model() bounds = self.relative_flow_rate_bounds BoundingPatterns.bounds_with_state( self, variable=self.flow_rate, bounds=(bounds[0] * self.element.size, bounds[1] * self.element.size), - variable_state=self.on_off.on, + variable_state=self.active_inactive.active, ) - elif self.with_investment and not self.with_on_off: - # Investment, but no OnOff + elif self.with_investment and not self.with_active_inactive: + # Investment, but no ActiveInactive self._create_investment_model() BoundingPatterns.scaled_bounds( self, @@ -648,10 +648,10 @@ def _constraint_flow_rate(self): relative_bounds=self.relative_flow_rate_bounds, ) - elif self.with_investment and self.with_on_off: - # Investment and OnOff + elif self.with_investment and self.with_active_inactive: + # Investment and ActiveInactive self._create_investment_model() - self._create_on_off_model() + self._create_active_inactive_model() BoundingPatterns.scaled_bounds_with_state( model=self, @@ -659,14 +659,14 @@ def _constraint_flow_rate(self): scaling_variable=self._investment.size, relative_bounds=self.relative_flow_rate_bounds, scaling_bounds=(self.element.size.minimum_or_fixed_size, self.element.size.maximum_or_fixed_size), - variable_state=self.on_off.on, + variable_state=self.active_inactive.active, ) else: raise Exception('Not valid') @property - def with_on_off(self) -> bool: - return self.element.on_off_parameters is not None + def with_active_inactive(self) -> bool: + return self.element.activity_parameters is not None @property def with_investment(self) -> bool: @@ -739,9 +739,9 @@ def absolute_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: lb_relative, ub_relative = self.relative_flow_rate_bounds lb = 0 - if not self.with_on_off: + if not self.with_active_inactive: if not self.with_investment: - # Basic case without investment and without OnOff + # Basic case without investment and without ActiveInactive lb = lb_relative * self.element.size elif self.with_investment and self.element.size.mandatory: # With mandatory Investment @@ -755,11 +755,11 @@ def absolute_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: return lb, ub @property - def on_off(self) -> OnOffModel | None: - """OnOff feature""" - if 'on_off' not in self.submodels: + def active_inactive(self) -> ActivityModel | None: + """ActiveInactive feature""" + if 'active_inactive' not in self.submodels: return None - return self.submodels['on_off'] + return self.submodels['active_inactive'] @property def _investment(self) -> InvestmentModel | None: @@ -768,7 +768,7 @@ def _investment(self) -> InvestmentModel | None: @property def investment(self) -> InvestmentModel | None: - """OnOff feature""" + """Investment feature""" if 'investment' not in self.submodels: return None return self.submodels['investment'] @@ -841,55 +841,57 @@ class ComponentModel(ElementModel): element: Component # Type hint def __init__(self, model: FlowSystemModel, element: Component): - self.on_off: OnOffModel | None = None + self.active_inactive: ActivityModel | None = None super().__init__(model, element) def _do_modeling(self): """Initiates all FlowModels""" super()._do_modeling() all_flows = self.element.inputs + self.element.outputs - if self.element.on_off_parameters: + if self.element.activity_parameters: for flow in all_flows: - if flow.on_off_parameters is None: - flow.on_off_parameters = OnOffParameters() + if flow.activity_parameters is None: + flow.activity_parameters = ActivityParameters() if self.element.prevent_simultaneous_flows: for flow in self.element.prevent_simultaneous_flows: - if flow.on_off_parameters is None: - flow.on_off_parameters = OnOffParameters() + if flow.activity_parameters is None: + flow.activity_parameters = ActivityParameters() for flow in all_flows: self.add_submodels(flow.create_model(self._model), short_name=flow.label) - if self.element.on_off_parameters: - on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) + if self.element.activity_parameters: + active = self.add_variables(binary=True, short_name='active', coords=self._model.get_coords()) if len(all_flows) == 1: - self.add_constraints(on == all_flows[0].submodel.on_off.on, short_name='on') + self.add_constraints(active == all_flows[0].submodel.active_inactive.active, short_name='active') else: - flow_ons = [flow.submodel.on_off.on for flow in all_flows] + flow_actives = [flow.submodel.active_inactive.active for flow in all_flows] # TODO: Is the EPSILON even necessary? - self.add_constraints(on <= sum(flow_ons) + CONFIG.Modeling.epsilon, short_name='on|ub') + self.add_constraints(active <= sum(flow_actives) + CONFIG.Modeling.epsilon, short_name='active|ub') self.add_constraints( - on >= sum(flow_ons) / (len(flow_ons) + CONFIG.Modeling.epsilon), short_name='on|lb' + active >= sum(flow_actives) / (len(flow_actives) + CONFIG.Modeling.epsilon), short_name='active|lb' ) - self.on_off = self.add_submodels( - OnOffModel( + self.active_inactive = self.add_submodels( + ActivityModel( model=self._model, label_of_element=self.label_of_element, - parameters=self.element.on_off_parameters, - on_variable=on, + parameters=self.element.activity_parameters, + active_variable=active, label_of_model=self.label_of_element, previous_states=self.previous_states, ), - short_name='on_off', + short_name='active_inactive', ) if self.element.prevent_simultaneous_flows: - # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow + # Simultaneous Usage --> Only One Flow is active at a time, but needs a Binary for every flow ModelingPrimitives.mutual_exclusivity_constraint( self, - binary_variables=[flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows], + binary_variables=[ + flow.submodel.active_inactive.active for flow in self.element.prevent_simultaneous_flows + ], short_name='prevent_simultaneous_use', ) @@ -904,10 +906,12 @@ def results_structure(self): @property def previous_states(self) -> xr.DataArray | None: """Previous state of the component, derived from its flows""" - if self.element.on_off_parameters is None: - raise ValueError(f'OnOffModel not present in \n{self}\nCant access previous_states') + if self.element.activity_parameters is None: + raise ValueError(f'ActivityModel not present in \n{self}\nCant access previous_states') - previous_states = [flow.submodel.on_off._previous_states for flow in self.element.inputs + self.element.outputs] + previous_states = [ + flow.submodel.active_inactive._previous_states for flow in self.element.inputs + self.element.outputs + ] previous_states = [da for da in previous_states if da is not None] if not previous_states: # Empty list diff --git a/flixopt/features.py b/flixopt/features.py index fd9796ba1..bae79bb34 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from .core import FlowSystemDimensions - from .interface import InvestParameters, OnOffParameters, Piecewise + from .interface import ActivityParameters, InvestParameters, Piecewise from .types import Numeric_PS, Numeric_TPS @@ -142,31 +142,31 @@ def invested(self) -> linopy.Variable | None: return self._variables['invested'] -class OnOffModel(Submodel): - """OnOff model using factory patterns""" +class ActivityModel(Submodel): + """Active/Inactive state model using factory patterns""" def __init__( self, model: FlowSystemModel, label_of_element: str, - parameters: OnOffParameters, - on_variable: linopy.Variable, + parameters: ActivityParameters, + active_variable: linopy.Variable, previous_states: Numeric_TPS | None, label_of_model: str | None = None, ): """ - This feature model is used to model the on/off state of flow_rate(s). It does not matter of the flow_rates are - bounded by a size variable or by a hard bound. THe used bound here is the absolute highest/lowest bound! + This feature model is used to model the active/inactive state of flow_rate(s). It does not matter if the flow_rates are + bounded by a size variable or by a hard bound. The used bound here is the absolute highest/lowest bound! Args: model: The optimization model instance label_of_element: The label of the parent (Element). Used to construct the full label of the model. parameters: The parameters of the feature model. - on_variable: The variable that determines the on state + active_variable: The variable that determines the active state previous_states: The previous flow_rates label_of_model: The label of the model. This is needed to construct the full label of the model. """ - self.on = on_variable + self.active = active_variable self._previous_states = previous_states self.parameters = parameters super().__init__(model, label_of_element, label_of_model=label_of_model) @@ -174,70 +174,72 @@ def __init__( def _do_modeling(self): super()._do_modeling() - if self.parameters.use_off: - off = self.add_variables(binary=True, short_name='off', coords=self._model.get_coords()) - self.add_constraints(self.on + off == 1, short_name='complementary') + if self.parameters.use_inactive: + inactive = self.add_variables(binary=True, short_name='inactive', coords=self._model.get_coords()) + self.add_constraints(self.active + inactive == 1, short_name='complementary') # 3. Total duration tracking using existing pattern ModelingPrimitives.expression_tracking_variable( self, - tracked_expression=(self.on * self._model.hours_per_step).sum('time'), + tracked_expression=(self.active * self._model.hours_per_step).sum('time'), bounds=( - self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0, - self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf, - ), # TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration()) - short_name='on_hours_total', + self.parameters.active_hours_total_min if self.parameters.active_hours_total_min is not None else 0, + self.parameters.active_hours_total_max + if self.parameters.active_hours_total_max is not None + else np.inf, + ), # TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_active_duration()) + short_name='active_hours_total', coords=['period', 'scenario'], ) # 4. Switch tracking using existing pattern - if self.parameters.use_switch_on: - self.add_variables(binary=True, short_name='switch|on', coords=self.get_coords()) - self.add_variables(binary=True, short_name='switch|off', coords=self.get_coords()) + if self.parameters.use_startup: + self.add_variables(binary=True, short_name='startup', coords=self.get_coords()) + self.add_variables(binary=True, short_name='shutdown', coords=self.get_coords()) BoundingPatterns.state_transition_bounds( self, - state_variable=self.on, - switch_on=self.switch_on, - switch_off=self.switch_off, + state_variable=self.active, + switch_on=self.startup, + switch_off=self.shutdown, name=f'{self.label_of_model}|switch', previous_state=self._previous_states.isel(time=-1) if self._previous_states is not None else 0, coord='time', ) - if self.parameters.switch_on_total_max is not None: + if self.parameters.startup_total_max is not None: count = self.add_variables( lower=0, - upper=self.parameters.switch_on_total_max, + upper=self.parameters.startup_total_max, coords=self._model.get_coords(('period', 'scenario')), - short_name='switch|count', + short_name='startup_count', ) - self.add_constraints(count == self.switch_on.sum('time'), short_name='switch|count') + self.add_constraints(count == self.startup.sum('time'), short_name='startup_count') - # 5. Consecutive on duration using existing pattern - if self.parameters.use_consecutive_on_hours: + # 5. Consecutive active duration using existing pattern + if self.parameters.use_consecutive_active_hours: ModelingPrimitives.consecutive_duration_tracking( self, - state_variable=self.on, - short_name='consecutive_on_hours', - minimum_duration=self.parameters.consecutive_on_hours_min, - maximum_duration=self.parameters.consecutive_on_hours_max, + state_variable=self.active, + short_name='consecutive_active_hours', + minimum_duration=self.parameters.consecutive_active_hours_min, + maximum_duration=self.parameters.consecutive_active_hours_max, duration_per_step=self.hours_per_step, duration_dim='time', - previous_duration=self._get_previous_on_duration(), + previous_duration=self._get_previous_active_duration(), ) - # 6. Consecutive off duration using existing pattern - if self.parameters.use_consecutive_off_hours: + # 6. Consecutive inactive duration using existing pattern + if self.parameters.use_consecutive_inactive_hours: ModelingPrimitives.consecutive_duration_tracking( self, - state_variable=self.off, - short_name='consecutive_off_hours', - minimum_duration=self.parameters.consecutive_off_hours_min, - maximum_duration=self.parameters.consecutive_off_hours_max, + state_variable=self.inactive, + short_name='consecutive_inactive_hours', + minimum_duration=self.parameters.consecutive_inactive_hours_min, + maximum_duration=self.parameters.consecutive_inactive_hours_max, duration_per_step=self.hours_per_step, duration_dim='time', - previous_duration=self._get_previous_off_duration(), + previous_duration=self._get_previous_inactive_duration(), ) # TODO: @@ -249,17 +251,17 @@ def _add_effects(self): self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: self.on * factor * self._model.hours_per_step + effect: self.active * factor * self._model.hours_per_step for effect, factor in self.parameters.effects_per_running_hour.items() }, target='temporal', ) - if self.parameters.effects_per_switch_on: + if self.parameters.effects_per_startup: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: self.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items() + effect: self.startup * factor for effect, factor in self.parameters.effects_per_startup.items() }, target='temporal', ) @@ -267,50 +269,50 @@ def _add_effects(self): # Properties access variables from Submodel's tracking system @property - def on_hours_total(self) -> linopy.Variable: - """Total on hours variable""" - return self['on_hours_total'] + def active_hours_total(self) -> linopy.Variable: + """Total active hours variable""" + return self['active_hours_total'] @property - def off(self) -> linopy.Variable | None: - """Binary off state variable""" - return self.get('off') + def inactive(self) -> linopy.Variable | None: + """Binary inactive state variable""" + return self.get('inactive') @property - def switch_on(self) -> linopy.Variable | None: - """Switch on variable""" - return self.get('switch|on') + def startup(self) -> linopy.Variable | None: + """Startup variable""" + return self.get('startup') @property - def switch_off(self) -> linopy.Variable | None: - """Switch off variable""" - return self.get('switch|off') + def shutdown(self) -> linopy.Variable | None: + """Shutdown variable""" + return self.get('shutdown') @property - def switch_on_nr(self) -> linopy.Variable | None: - """Number of switch-ons variable""" - return self.get('switch|count') + def startup_nr(self) -> linopy.Variable | None: + """Number of startups variable""" + return self.get('startup_count') @property - def consecutive_on_hours(self) -> linopy.Variable | None: - """Consecutive on hours variable""" - return self.get('consecutive_on_hours') + def consecutive_active_hours(self) -> linopy.Variable | None: + """Consecutive active hours variable""" + return self.get('consecutive_active_hours') @property - def consecutive_off_hours(self) -> linopy.Variable | None: - """Consecutive off hours variable""" - return self.get('consecutive_off_hours') + def consecutive_inactive_hours(self) -> linopy.Variable | None: + """Consecutive inactive hours variable""" + return self.get('consecutive_inactive_hours') - def _get_previous_on_duration(self): - """Get previous on duration. Previously OFF by default, for one timestep""" + def _get_previous_active_duration(self): + """Get previous active duration. Previously inactive by default, for one timestep""" hours_per_step = self._model.hours_per_step.isel(time=0).min().item() if self._previous_states is None: return 0 else: return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states, hours_per_step) - def _get_previous_off_duration(self): - """Get previous off duration. Previously OFF by default, for one timestep""" + def _get_previous_inactive_duration(self): + """Get previous inactive duration. Previously inactive by default, for one timestep""" hours_per_step = self._model.hours_per_step.isel(time=0).min().item() if self._previous_states is None: return hours_per_step diff --git a/flixopt/interface.py b/flixopt/interface.py index f67f501ba..acc76ece7 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1077,10 +1077,10 @@ def compute_linked_periods(first_period: int, last_period: int, periods: pd.Inde @register_class_for_io -class OnOffParameters(Interface): - """Define operational constraints and effects for binary on/off equipment behavior. +class ActivityParameters(Interface): + """Define operational constraints and effects for binary active/inactive equipment behavior. - This class models equipment that operates in discrete states (on/off) rather than + This class models equipment that operates in discrete states (active/inactive) rather than continuous operation, capturing realistic operational constraints and associated costs. It handles complex equipment behavior including startup costs, minimum run times, cycling limitations, and maintenance scheduling requirements. @@ -1100,45 +1100,45 @@ class OnOffParameters(Interface): Mathematical Formulation: See the complete mathematical model in the documentation: - [OnOffParameters](../user-guide/mathematical-notation/features/OnOffParameters.md) + [ActivityParameters](../user-guide/mathematical-notation/features/ActivityParameters.md) Args: - effects_per_switch_on: Costs or impacts incurred for each transition from - off state (var_on=0) to on state (var_on=1). Represents startup costs, + effects_per_startup: Costs or impacts incurred for each transition from + inactive state (var_active=0) to active state (var_active=1). Represents startup costs, wear and tear, or other switching impacts. Dictionary mapping effect names to values (e.g., {'cost': 500, 'maintenance_hours': 2}). effects_per_running_hour: Ongoing costs or impacts while equipment operates - in the on state. Includes fuel costs, labor, consumables, or emissions. + in the active state. Includes fuel costs, labor, consumables, or emissions. Dictionary mapping effect names to hourly values (e.g., {'fuel_cost': 45}). - on_hours_total_min: Minimum total operating hours across the entire time horizon. + active_hours_total_min: Minimum total operating hours across the entire time horizon. Ensures equipment meets minimum utilization requirements or contractual obligations (e.g., power purchase agreements, maintenance schedules). - on_hours_total_max: Maximum total operating hours across the entire time horizon. + active_hours_total_max: Maximum total operating hours across the entire time horizon. Limits equipment usage due to maintenance schedules, fuel availability, environmental permits, or equipment lifetime constraints. - consecutive_on_hours_min: Minimum continuous operating duration once started. + consecutive_active_hours_min: Minimum continuous operating duration once started. Models minimum run times due to thermal constraints, process stability, or efficiency considerations. Can be time-varying to reflect different constraints across the planning horizon. - consecutive_on_hours_max: Maximum continuous operating duration in one campaign. + consecutive_active_hours_max: Maximum continuous operating duration in one campaign. Models mandatory maintenance intervals, process batch sizes, or equipment thermal limits requiring periodic shutdowns. - consecutive_off_hours_min: Minimum continuous shutdown duration between operations. + consecutive_inactive_hours_min: Minimum continuous shutdown duration between operations. Models cooling periods, maintenance requirements, or process constraints that prevent immediate restart after shutdown. - consecutive_off_hours_max: Maximum continuous shutdown duration before mandatory + consecutive_inactive_hours_max: Maximum continuous shutdown duration before mandatory restart. Models equipment preservation, process stability, or contractual requirements for minimum activity levels. - switch_on_total_max: Maximum number of startup operations across the time horizon. + startup_total_max: Maximum number of startup operations across the time horizon. Limits equipment cycling to reduce wear, maintenance costs, or comply with operational constraints (e.g., grid stability requirements). - force_switch_on: When True, creates switch-on variables even without explicit - switch_on_total_max constraint. Useful for tracking or reporting startup + force_startup: When True, creates startup variables even without explicit + startup_total_max constraint. Useful for tracking or reporting startup events without enforcing limits. Note: **Time Series Boundary Handling**: The final time period constraints for - consecutive_on_hours_min/max and consecutive_off_hours_min/max are not + consecutive_active_hours_min/max and consecutive_inactive_hours_min/max are not enforced, allowing the optimization to end with ongoing campaigns that may be shorter than the specified minimums or longer than maximums. @@ -1146,8 +1146,8 @@ class OnOffParameters(Interface): Combined cycle power plant with startup costs and minimum run time: ```python - power_plant_operation = OnOffParameters( - effects_per_switch_on={ + power_plant_operation = ActivityParameters( + effects_per_startup={ 'startup_cost': 25000, # €25,000 per startup 'startup_fuel': 150, # GJ natural gas for startup 'startup_time': 4, # Hours to reach full output @@ -1157,17 +1157,17 @@ class OnOffParameters(Interface): 'fixed_om': 125, # Fixed O&M costs while running 'auxiliary_power': 2.5, # MW parasitic loads }, - consecutive_on_hours_min=8, # Minimum 8-hour run once started - consecutive_off_hours_min=4, # Minimum 4-hour cooling period - on_hours_total_max=6000, # Annual operating limit + consecutive_active_hours_min=8, # Minimum 8-hour run once started + consecutive_inactive_hours_min=4, # Minimum 4-hour cooling period + active_hours_total_max=6000, # Annual operating limit ) ``` Industrial batch process with cycling limits: ```python - batch_reactor = OnOffParameters( - effects_per_switch_on={ + batch_reactor = ActivityParameters( + effects_per_startup={ 'setup_cost': 1500, # Labor and materials for startup 'catalyst_consumption': 5, # kg catalyst per batch 'cleaning_chemicals': 200, # L cleaning solution @@ -1177,19 +1177,19 @@ class OnOffParameters(Interface): 'electricity': 150, # kWh electrical load 'cooling_water': 50, # m³/h cooling water }, - consecutive_on_hours_min=12, # Minimum batch size (12 hours) - consecutive_on_hours_max=24, # Maximum batch size (24 hours) - consecutive_off_hours_min=6, # Cleaning and setup time - switch_on_total_max=200, # Maximum 200 batches per period - on_hours_total_max=4000, # Maximum production time + consecutive_active_hours_min=12, # Minimum batch size (12 hours) + consecutive_active_hours_max=24, # Maximum batch size (24 hours) + consecutive_inactive_hours_min=6, # Cleaning and setup time + startup_total_max=200, # Maximum 200 batches per period + active_hours_total_max=4000, # Maximum production time ) ``` HVAC system with thermostat control and maintenance: ```python - hvac_operation = OnOffParameters( - effects_per_switch_on={ + hvac_operation = ActivityParameters( + effects_per_startup={ 'compressor_wear': 0.5, # Hours of compressor life per start 'inrush_current': 15, # kW peak demand on startup }, @@ -1197,19 +1197,19 @@ class OnOffParameters(Interface): 'electricity': 25, # kW electrical consumption 'maintenance': 0.12, # €/hour maintenance reserve }, - consecutive_on_hours_min=1, # Minimum 1-hour run to avoid cycling - consecutive_off_hours_min=0.5, # 30-minute minimum off time - switch_on_total_max=2000, # Limit cycling for compressor life - on_hours_total_min=2000, # Minimum operation for humidity control - on_hours_total_max=5000, # Maximum operation for energy budget + consecutive_active_hours_min=1, # Minimum 1-hour run to avoid cycling + consecutive_inactive_hours_min=0.5, # 30-minute minimum off time + startup_total_max=2000, # Limit cycling for compressor life + active_hours_total_min=2000, # Minimum operation for humidity control + active_hours_total_max=5000, # Maximum operation for energy budget ) ``` Backup generator with testing and maintenance requirements: ```python - backup_generator = OnOffParameters( - effects_per_switch_on={ + backup_generator = ActivityParameters( + effects_per_startup={ 'fuel_priming': 50, # L diesel for system priming 'wear_factor': 1.0, # Start cycles impact on maintenance 'testing_labor': 2, # Hours technician time per test @@ -1219,19 +1219,19 @@ class OnOffParameters(Interface): 'emissions_permit': 15, # € emissions allowance cost 'noise_penalty': 25, # € noise compliance cost }, - consecutive_on_hours_min=0.5, # Minimum test duration (30 min) - consecutive_off_hours_max=720, # Maximum 30 days between tests - switch_on_total_max=52, # Weekly testing limit - on_hours_total_min=26, # Minimum annual testing (0.5h × 52) - on_hours_total_max=200, # Maximum runtime (emergencies + tests) + consecutive_active_hours_min=0.5, # Minimum test duration (30 min) + consecutive_inactive_hours_max=720, # Maximum 30 days between tests + startup_total_max=52, # Weekly testing limit + active_hours_total_min=26, # Minimum annual testing (0.5h × 52) + active_hours_total_max=200, # Maximum runtime (emergencies + tests) ) ``` Peak shaving battery with cycling degradation: ```python - battery_cycling = OnOffParameters( - effects_per_switch_on={ + battery_cycling = ActivityParameters( + effects_per_startup={ 'cycle_degradation': 0.01, # % capacity loss per cycle 'inverter_startup': 0.5, # kWh losses during startup }, @@ -1240,11 +1240,11 @@ class OnOffParameters(Interface): 'cooling': 5, # kW thermal management 'inverter_losses': 8, # kW conversion losses }, - consecutive_on_hours_min=1, # Minimum discharge duration - consecutive_on_hours_max=4, # Maximum continuous discharge - consecutive_off_hours_min=1, # Minimum rest between cycles - switch_on_total_max=365, # Daily cycling limit - force_switch_on=True, # Track all cycling events + consecutive_active_hours_min=1, # Minimum discharge duration + consecutive_active_hours_max=4, # Maximum continuous discharge + consecutive_inactive_hours_min=1, # Minimum rest between cycles + startup_total_max=365, # Daily cycling limit + force_startup=True, # Track all cycling events ) ``` @@ -1260,82 +1260,86 @@ class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: Effect_TPS | Numeric_TPS | None = None, + effects_per_startup: Effect_TPS | Numeric_TPS | None = None, effects_per_running_hour: Effect_TPS | Numeric_TPS | None = None, - on_hours_total_min: Numeric_PS | None = None, - on_hours_total_max: Numeric_PS | None = None, - consecutive_on_hours_min: Numeric_TPS | None = None, - consecutive_on_hours_max: Numeric_TPS | None = None, - consecutive_off_hours_min: Numeric_TPS | None = None, - consecutive_off_hours_max: Numeric_TPS | None = None, - switch_on_total_max: Numeric_PS | None = None, - force_switch_on: bool = False, + active_hours_total_min: Numeric_PS | None = None, + active_hours_total_max: Numeric_PS | None = None, + consecutive_active_hours_min: Numeric_TPS | None = None, + consecutive_active_hours_max: Numeric_TPS | None = None, + consecutive_inactive_hours_min: Numeric_TPS | None = None, + consecutive_inactive_hours_max: Numeric_TPS | None = None, + startup_total_max: Numeric_PS | None = None, + force_startup: bool = False, ): - self.effects_per_switch_on = effects_per_switch_on if effects_per_switch_on is not None else {} + self.effects_per_startup = effects_per_startup if effects_per_startup is not None else {} self.effects_per_running_hour = effects_per_running_hour if effects_per_running_hour is not None else {} - self.on_hours_total_min = on_hours_total_min - self.on_hours_total_max = on_hours_total_max - self.consecutive_on_hours_min = consecutive_on_hours_min - self.consecutive_on_hours_max = consecutive_on_hours_max - self.consecutive_off_hours_min = consecutive_off_hours_min - self.consecutive_off_hours_max = consecutive_off_hours_max - self.switch_on_total_max = switch_on_total_max - self.force_switch_on: bool = force_switch_on + self.active_hours_total_min = active_hours_total_min + self.active_hours_total_max = active_hours_total_max + self.consecutive_active_hours_min = consecutive_active_hours_min + self.consecutive_active_hours_max = consecutive_active_hours_max + self.consecutive_inactive_hours_min = consecutive_inactive_hours_min + self.consecutive_inactive_hours_max = consecutive_inactive_hours_max + self.startup_total_max = startup_total_max + self.force_startup: bool = force_startup def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: - self.effects_per_switch_on = flow_system.fit_effects_to_model_coords( - name_prefix, self.effects_per_switch_on, 'per_switch_on' + self.effects_per_startup = flow_system.fit_effects_to_model_coords( + name_prefix, self.effects_per_startup, 'per_startup' ) self.effects_per_running_hour = flow_system.fit_effects_to_model_coords( name_prefix, self.effects_per_running_hour, 'per_running_hour' ) - self.consecutive_on_hours_min = flow_system.fit_to_model_coords( - f'{name_prefix}|consecutive_on_hours_min', self.consecutive_on_hours_min + self.consecutive_active_hours_min = flow_system.fit_to_model_coords( + f'{name_prefix}|consecutive_active_hours_min', self.consecutive_active_hours_min ) - self.consecutive_on_hours_max = flow_system.fit_to_model_coords( - f'{name_prefix}|consecutive_on_hours_max', self.consecutive_on_hours_max + self.consecutive_active_hours_max = flow_system.fit_to_model_coords( + f'{name_prefix}|consecutive_active_hours_max', self.consecutive_active_hours_max ) - self.consecutive_off_hours_min = flow_system.fit_to_model_coords( - f'{name_prefix}|consecutive_off_hours_min', self.consecutive_off_hours_min + self.consecutive_inactive_hours_min = flow_system.fit_to_model_coords( + f'{name_prefix}|consecutive_inactive_hours_min', self.consecutive_inactive_hours_min ) - self.consecutive_off_hours_max = flow_system.fit_to_model_coords( - f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max + self.consecutive_inactive_hours_max = flow_system.fit_to_model_coords( + f'{name_prefix}|consecutive_inactive_hours_max', self.consecutive_inactive_hours_max ) - self.on_hours_total_max = flow_system.fit_to_model_coords( - f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, dims=['period', 'scenario'] + self.active_hours_total_max = flow_system.fit_to_model_coords( + f'{name_prefix}|active_hours_total_max', self.active_hours_total_max, dims=['period', 'scenario'] ) - self.on_hours_total_min = flow_system.fit_to_model_coords( - f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, dims=['period', 'scenario'] + self.active_hours_total_min = flow_system.fit_to_model_coords( + f'{name_prefix}|active_hours_total_min', self.active_hours_total_min, dims=['period', 'scenario'] ) - self.switch_on_total_max = flow_system.fit_to_model_coords( - f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, dims=['period', 'scenario'] + self.startup_total_max = flow_system.fit_to_model_coords( + f'{name_prefix}|startup_total_max', self.startup_total_max, dims=['period', 'scenario'] ) @property - def use_off(self) -> bool: - """Proxy: whether OFF variable is required""" - return self.use_consecutive_off_hours + def use_inactive(self) -> bool: + """Proxy: whether inactive variable is required""" + return self.use_consecutive_inactive_hours @property - def use_consecutive_on_hours(self) -> bool: - """Determines whether a Variable for consecutive on hours is needed or not""" - return any(param is not None for param in [self.consecutive_on_hours_min, self.consecutive_on_hours_max]) + def use_consecutive_active_hours(self) -> bool: + """Determines whether a Variable for consecutive active hours is needed or not""" + return any( + param is not None for param in [self.consecutive_active_hours_min, self.consecutive_active_hours_max] + ) @property - def use_consecutive_off_hours(self) -> bool: - """Determines whether a Variable for consecutive off hours is needed or not""" - return any(param is not None for param in [self.consecutive_off_hours_min, self.consecutive_off_hours_max]) + def use_consecutive_inactive_hours(self) -> bool: + """Determines whether a Variable for consecutive inactive hours is needed or not""" + return any( + param is not None for param in [self.consecutive_inactive_hours_min, self.consecutive_inactive_hours_max] + ) @property - def use_switch_on(self) -> bool: - """Determines whether a variable for switch_on is needed or not""" - if self.force_switch_on: + def use_startup(self) -> bool: + """Determines whether a variable for startup is needed or not""" + if self.force_startup: return True return any( self._has_value(param) for param in [ - self.effects_per_switch_on, - self.switch_on_total_max, + self.effects_per_startup, + self.startup_total_max, ] ) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 8f02e4f70..2d07ebc7a 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from .elements import Flow - from .interface import OnOffParameters + from .interface import ActivityParameters from .types import Numeric_TPS @@ -34,7 +34,7 @@ class Boiler(LinearConverter): output to fuel input energy content. Q_fu: Fuel input-flow representing fuel consumption. Q_th: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + activity_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -58,7 +58,7 @@ class Boiler(LinearConverter): eta=seasonal_efficiency_profile, # Time-varying efficiency Q_fu=biomass_flow, Q_th=district_heat_flow, - on_off_parameters=OnOffParameters( + activity_parameters=ActivityParameters( consecutive_on_hours_min=4, # Minimum 4-hour operation effects_per_switch_on={'startup_fuel': 50}, # Startup fuel penalty ), @@ -78,7 +78,7 @@ def __init__( eta: Numeric_TPS, Q_fu: Flow, Q_th: Flow, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, meta_data: dict | None = None, ): super().__init__( @@ -86,7 +86,7 @@ def __init__( inputs=[Q_fu], outputs=[Q_th], conversion_factors=[{Q_fu.label: eta, Q_th.label: 1}], - on_off_parameters=on_off_parameters, + activity_parameters=activity_parameters, meta_data=meta_data, ) self.Q_fu = Q_fu @@ -119,7 +119,7 @@ class Power2Heat(LinearConverter): electrode boilers or systems with distribution losses. P_el: Electrical input-flow representing electricity consumption. Q_th: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + activity_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -143,7 +143,7 @@ class Power2Heat(LinearConverter): eta=0.95, # 95% efficiency including boiler losses P_el=industrial_electricity, Q_th=process_steam_flow, - on_off_parameters=OnOffParameters( + activity_parameters=ActivityParameters( consecutive_on_hours_min=1, # Minimum 1-hour operation effects_per_switch_on={'startup_cost': 100}, ), @@ -165,7 +165,7 @@ def __init__( eta: Numeric_TPS, P_el: Flow, Q_th: Flow, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, meta_data: dict | None = None, ): super().__init__( @@ -173,7 +173,7 @@ def __init__( inputs=[P_el], outputs=[Q_th], conversion_factors=[{P_el.label: eta, Q_th.label: 1}], - on_off_parameters=on_off_parameters, + activity_parameters=activity_parameters, meta_data=meta_data, ) @@ -207,7 +207,7 @@ class HeatPump(LinearConverter): additional energy from the environment. P_el: Electrical input-flow representing electricity consumption. Q_th: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + activity_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -231,7 +231,7 @@ class HeatPump(LinearConverter): COP=temperature_dependent_cop, # Time-varying COP based on ground temp P_el=electricity_flow, Q_th=radiant_heating_flow, - on_off_parameters=OnOffParameters( + activity_parameters=ActivityParameters( consecutive_on_hours_min=2, # Avoid frequent cycling effects_per_running_hour={'maintenance': 0.5}, ), @@ -252,7 +252,7 @@ def __init__( COP: Numeric_TPS, P_el: Flow, Q_th: Flow, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, meta_data: dict | None = None, ): super().__init__( @@ -260,7 +260,7 @@ def __init__( inputs=[P_el], outputs=[Q_th], conversion_factors=[{P_el.label: COP, Q_th.label: 1}], - on_off_parameters=on_off_parameters, + activity_parameters=activity_parameters, meta_data=meta_data, ) self.P_el = P_el @@ -294,7 +294,7 @@ class CoolingTower(LinearConverter): of thermal power that must be supplied as electricity for fans and pumps. P_el: Electrical input-flow representing electricity consumption for fans/pumps. Q_th: Thermal input-flow representing waste heat to be rejected to environment. - on_off_parameters: Parameters defining binary operation constraints and costs. + activity_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -318,7 +318,7 @@ class CoolingTower(LinearConverter): specific_electricity_demand=0.015, # 1.5% auxiliary power P_el=auxiliary_electricity, Q_th=condenser_waste_heat, - on_off_parameters=OnOffParameters( + activity_parameters=ActivityParameters( consecutive_on_hours_min=4, # Minimum operation time effects_per_running_hour={'water_consumption': 2.5}, # m³/h ), @@ -341,7 +341,7 @@ def __init__( specific_electricity_demand: Numeric_TPS, P_el: Flow, Q_th: Flow, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, meta_data: dict | None = None, ): super().__init__( @@ -349,7 +349,7 @@ def __init__( inputs=[P_el, Q_th], outputs=[], conversion_factors=[{P_el.label: -1, Q_th.label: specific_electricity_demand}], - on_off_parameters=on_off_parameters, + activity_parameters=activity_parameters, meta_data=meta_data, ) @@ -387,7 +387,7 @@ class CHP(LinearConverter): Q_fu: Fuel input-flow representing fuel consumption. P_el: Electrical output-flow representing electricity generation. Q_th: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + activity_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -415,7 +415,7 @@ class CHP(LinearConverter): Q_fu=fuel_gas_flow, P_el=plant_electricity, Q_th=process_steam, - on_off_parameters=OnOffParameters( + activity_parameters=ActivityParameters( consecutive_on_hours_min=8, # Minimum 8-hour operation effects_per_switch_on={'startup_cost': 5000}, on_hours_total_max=6000, # Annual operating limit @@ -441,7 +441,7 @@ def __init__( Q_fu: Flow, P_el: Flow, Q_th: Flow, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, meta_data: dict | None = None, ): heat = {Q_fu.label: eta_th, Q_th.label: 1} @@ -452,7 +452,7 @@ def __init__( inputs=[Q_fu], outputs=[Q_th, P_el], conversion_factors=[heat, electricity], - on_off_parameters=on_off_parameters, + activity_parameters=activity_parameters, meta_data=meta_data, ) @@ -500,7 +500,7 @@ class HeatPumpWithSource(LinearConverter): Q_ab: Heat source input-flow representing thermal energy extracted from environment (ground, air, water source). Q_th: Thermal output-flow representing useful heat delivered to the application. - on_off_parameters: Parameters defining binary operation constraints and costs. + activity_parameters: Parameters defining binary operation constraints and costs. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -526,7 +526,7 @@ class HeatPumpWithSource(LinearConverter): P_el=electricity_consumption, Q_ab=industrial_heat_extraction, # Heat extracted from a industrial process or waste water Q_th=heat_supply, - on_off_parameters=OnOffParameters( + activity_parameters=ActivityParameters( consecutive_on_hours_min=0.5, # 30-minute minimum runtime effects_per_switch_on={'costs': 1000}, ), @@ -554,7 +554,7 @@ def __init__( P_el: Flow, Q_ab: Flow, Q_th: Flow, - on_off_parameters: OnOffParameters | None = None, + activity_parameters: ActivityParameters | None = None, meta_data: dict | None = None, ): super().__init__( @@ -562,7 +562,7 @@ def __init__( inputs=[P_el, Q_ab], outputs=[Q_th], conversion_factors=[{P_el.label: COP, Q_th.label: 1}, {Q_ab.label: COP / (COP - 1), Q_th.label: 1}], - on_off_parameters=on_off_parameters, + activity_parameters=activity_parameters, meta_data=meta_data, ) self.P_el = P_el diff --git a/tests/conftest.py b/tests/conftest.py index bd940b843..0eaffb4d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,7 +138,7 @@ def simple(): size=50, relative_minimum=5 / 50, relative_maximum=1, - on_off_parameters=fx.OnOffParameters(), + activity_parameters=fx.ActivityParameters(), ), Q_fu=fx.Flow('Q_fu', bus='Gas'), ) @@ -149,7 +149,7 @@ def complex(): return fx.linear_converters.Boiler( 'Kessel', eta=0.5, - on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + activity_parameters=fx.ActivityParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), Q_th=fx.Flow( 'Q_th', bus='Fernwärme', @@ -164,14 +164,14 @@ def complex(): mandatory=True, effects_of_investment_per_size={'costs': 10, 'PE': 2}, ), - on_off_parameters=fx.OnOffParameters( - on_hours_total_min=0, - on_hours_total_max=1000, - consecutive_on_hours_max=10, - consecutive_on_hours_min=1, - consecutive_off_hours_max=10, - effects_per_switch_on=0.01, - switch_on_total_max=1000, + activity_parameters=fx.ActivityParameters( + active_hours_total_min=0, + active_hours_total_max=1000, + consecutive_active_hours_max=10, + consecutive_active_hours_min=1, + consecutive_inactive_hours_max=10, + effects_per_startup=0.01, + startup_total_max=1000, ), flow_hours_total_max=1e6, ), @@ -187,7 +187,11 @@ def simple(): eta_th=0.5, eta_el=0.4, P_el=fx.Flow( - 'P_el', bus='Strom', size=60, relative_minimum=5 / 60, on_off_parameters=fx.OnOffParameters() + 'P_el', + bus='Strom', + size=60, + relative_minimum=5 / 60, + activity_parameters=fx.ActivityParameters(), ), Q_th=fx.Flow('Q_th', bus='Fernwärme'), Q_fu=fx.Flow('Q_fu', bus='Gas'), @@ -200,7 +204,7 @@ def base(): 'KWK', eta_th=0.5, eta_el=0.4, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + activity_parameters=fx.ActivityParameters(effects_per_startup=0.01), P_el=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), Q_th=fx.Flow('Q_th', bus='Fernwärme', size=1e3), Q_fu=fx.Flow('Q_fu', bus='Gas', size=1e3), @@ -224,7 +228,7 @@ def piecewise(): 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), } ), - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + activity_parameters=fx.ActivityParameters(effects_per_startup=0.01), ) @staticmethod @@ -249,7 +253,7 @@ def segments(timesteps_length): 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), } ), - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + activity_parameters=fx.ActivityParameters(effects_per_startup=0.01), ) @@ -604,14 +608,14 @@ def flow_system_long(): size=95, relative_minimum=12 / 95, previous_flow_rate=0, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=1000), + activity_parameters=fx.ActivityParameters(effects_per_startup=1000), ), ), fx.linear_converters.CHP( 'BHKW2', eta_th=0.58, eta_el=0.22, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=24000), + activity_parameters=fx.ActivityParameters(effects_per_startup=24000), P_el=fx.Flow('P_el', bus='Strom'), Q_th=fx.Flow('Q_th', bus='Fernwärme'), Q_fu=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), diff --git a/tests/test_component.py b/tests/test_component.py index be1eecf3b..795789137 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -82,7 +82,7 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_co fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.3, relative_maximum=ub_out2, size=300), ] comp = flixopt.elements.Component( - 'TestComponent', inputs=inputs, outputs=outputs, on_off_parameters=fx.OnOffParameters() + 'TestComponent', inputs=inputs, outputs=outputs, activity_parameters=fx.ActivityParameters() ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -93,17 +93,17 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_co 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', + 'TestComponent(In1)|active_hours_total', 'TestComponent(Out1)|flow_rate', 'TestComponent(Out1)|total_flow_hours', 'TestComponent(Out1)|on', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|active_hours_total', 'TestComponent(Out2)|flow_rate', 'TestComponent(Out2)|total_flow_hours', 'TestComponent(Out2)|on', - 'TestComponent(Out2)|on_hours_total', + 'TestComponent(Out2)|active_hours_total', 'TestComponent|on', - 'TestComponent|on_hours_total', + 'TestComponent|active_hours_total', }, msg='Incorrect variables', ) @@ -114,18 +114,18 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_co 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|flow_rate|lb', 'TestComponent(In1)|flow_rate|ub', - 'TestComponent(In1)|on_hours_total', + 'TestComponent(In1)|active_hours_total', 'TestComponent(Out1)|total_flow_hours', 'TestComponent(Out1)|flow_rate|lb', 'TestComponent(Out1)|flow_rate|ub', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|active_hours_total', 'TestComponent(Out2)|total_flow_hours', 'TestComponent(Out2)|flow_rate|lb', 'TestComponent(Out2)|flow_rate|ub', - 'TestComponent(Out2)|on_hours_total', + 'TestComponent(Out2)|active_hours_total', 'TestComponent|on|lb', 'TestComponent|on|ub', - 'TestComponent|on_hours_total', + 'TestComponent|active_hours_total', }, msg='Incorrect constraints', ) @@ -180,7 +180,7 @@ def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_confi ] outputs = [] comp = flixopt.elements.Component( - 'TestComponent', inputs=inputs, outputs=outputs, on_off_parameters=fx.OnOffParameters() + 'TestComponent', inputs=inputs, outputs=outputs, activity_parameters=fx.ActivityParameters() ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -191,9 +191,9 @@ def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_confi 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', + 'TestComponent(In1)|active_hours_total', 'TestComponent|on', - 'TestComponent|on_hours_total', + 'TestComponent|active_hours_total', }, msg='Incorrect variables', ) @@ -204,9 +204,9 @@ def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_confi 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|flow_rate|lb', 'TestComponent(In1)|flow_rate|ub', - 'TestComponent(In1)|on_hours_total', + 'TestComponent(In1)|active_hours_total', 'TestComponent|on', - 'TestComponent|on_hours_total', + 'TestComponent|active_hours_total', }, msg='Incorrect constraints', ) @@ -257,7 +257,7 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor ), ] comp = flixopt.elements.Component( - 'TestComponent', inputs=inputs, outputs=outputs, on_off_parameters=fx.OnOffParameters() + 'TestComponent', inputs=inputs, outputs=outputs, activity_parameters=fx.ActivityParameters() ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -268,17 +268,17 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor 'TestComponent(In1)|flow_rate', 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|on', - 'TestComponent(In1)|on_hours_total', + 'TestComponent(In1)|active_hours_total', 'TestComponent(Out1)|flow_rate', 'TestComponent(Out1)|total_flow_hours', 'TestComponent(Out1)|on', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|active_hours_total', 'TestComponent(Out2)|flow_rate', 'TestComponent(Out2)|total_flow_hours', 'TestComponent(Out2)|on', - 'TestComponent(Out2)|on_hours_total', + 'TestComponent(Out2)|active_hours_total', 'TestComponent|on', - 'TestComponent|on_hours_total', + 'TestComponent|active_hours_total', }, msg='Incorrect variables', ) @@ -289,18 +289,18 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor 'TestComponent(In1)|total_flow_hours', 'TestComponent(In1)|flow_rate|lb', 'TestComponent(In1)|flow_rate|ub', - 'TestComponent(In1)|on_hours_total', + 'TestComponent(In1)|active_hours_total', 'TestComponent(Out1)|total_flow_hours', 'TestComponent(Out1)|flow_rate|lb', 'TestComponent(Out1)|flow_rate|ub', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|active_hours_total', 'TestComponent(Out2)|total_flow_hours', 'TestComponent(Out2)|flow_rate|lb', 'TestComponent(Out2)|flow_rate|ub', - 'TestComponent(Out2)|on_hours_total', + 'TestComponent(Out2)|active_hours_total', 'TestComponent|on|lb', 'TestComponent|on|ub', - 'TestComponent|on_hours_total', + 'TestComponent|active_hours_total', }, msg='Incorrect constraints', ) @@ -377,7 +377,7 @@ def test_previous_states_with_multiple_flows_parameterized( relative_minimum=np.ones(10) * 0.1, size=100, previous_flow_rate=in1_previous_flow_rate, - on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), + activity_parameters=fx.ActivityParameters(consecutive_active_hours_min=3), ), ] outputs = [ @@ -397,14 +397,14 @@ def test_previous_states_with_multiple_flows_parameterized( 'TestComponent', inputs=inputs, outputs=outputs, - on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), + activity_parameters=fx.ActivityParameters(consecutive_active_hours_min=3), ) flow_system.add_elements(comp) create_linopy_model(flow_system) assert_conequal( - comp.submodel.constraints['TestComponent|consecutive_on_hours|initial'], - comp.submodel.variables['TestComponent|consecutive_on_hours'].isel(time=0) + comp.submodel.constraints['TestComponent|consecutive_active_hours|initial'], + comp.submodel.variables['TestComponent|consecutive_active_hours'].isel(time=0) == comp.submodel.variables['TestComponent|on'].isel(time=0) * (previous_on_hours + 1), ) @@ -435,7 +435,7 @@ def test_transmission_basic(self, basic_flow_system, highs_solver): # Assertions assert_almost_equal_numeric( - transmission.in1.submodel.on_off.on.solution.values, + transmission.in1.submodel.active_inactive.active.solution.values, np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), 'On does not work properly', ) @@ -496,7 +496,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): # Assertions assert_almost_equal_numeric( - transmission.in1.submodel.on_off.on.solution.values, + transmission.in1.submodel.active_inactive.active.solution.values, np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), 'On does not work properly', ) @@ -574,7 +574,7 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): # Assertions assert_almost_equal_numeric( - transmission.in1.submodel.on_off.on.solution.values, + transmission.in1.submodel.active_inactive.active.solution.values, np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), 'On does not work properly', ) diff --git a/tests/test_flow.py b/tests/test_flow.py index 8a011939f..cc11d7aae 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -524,14 +524,19 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): size=100, relative_minimum=0.2, relative_maximum=0.8, - on_off_parameters=fx.OnOffParameters(), + activity_parameters=fx.ActivityParameters(), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) assert_sets_equal( set(flow.submodel.variables), - {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}, + { + 'Sink(Wärme)|total_flow_hours', + 'Sink(Wärme)|flow_rate', + 'Sink(Wärme)|on', + 'Sink(Wärme)|active_hours_total', + }, msg='Incorrect variables', ) @@ -539,7 +544,7 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): set(flow.submodel.constraints), { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|active_hours_total', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', }, @@ -557,11 +562,11 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): # OnOff assert_var_equal( - flow.submodel.on_off.on, + flow.submodel.active_inactive.active, model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( - model.variables['Sink(Wärme)|on_hours_total'], + model.variables['Sink(Wärme)|active_hours_total'], model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( @@ -574,8 +579,8 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): ) assert_conequal( - model.constraints['Sink(Wärme)|on_hours_total'], - flow.submodel.variables['Sink(Wärme)|on_hours_total'] + model.constraints['Sink(Wärme)|active_hours_total'], + flow.submodel.variables['Sink(Wärme)|active_hours_total'] == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) @@ -589,7 +594,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ flow = fx.Flow( 'Wärme', bus='Fernwärme', - on_off_parameters=fx.OnOffParameters( + activity_parameters=fx.ActivityParameters( effects_per_running_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), ) @@ -603,7 +608,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|on', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|active_hours_total', }, msg='Incorrect variables', ) @@ -613,7 +618,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|active_hours_total', }, msg='Incorrect constraints', ) @@ -621,8 +626,8 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ assert 'Sink(Wärme)->costs(temporal)' in set(costs.submodel.constraints) assert 'Sink(Wärme)->CO2(temporal)' in set(co2.submodel.constraints) - costs_per_running_hour = flow.on_off_parameters.effects_per_running_hour['costs'] - co2_per_running_hour = flow.on_off_parameters.effects_per_running_hour['CO2'] + costs_per_running_hour = flow.activity_parameters.effects_per_running_hour['costs'] + co2_per_running_hour = flow.activity_parameters.effects_per_running_hour['CO2'] assert costs_per_running_hour.dims == tuple(model.get_coords()) assert co2_per_running_hour.dims == tuple(model.get_coords()) @@ -639,7 +644,7 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, ) - def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_config): + def test_consecutive_active_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config @@ -647,73 +652,73 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_conf 'Wärme', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters( - consecutive_on_hours_min=2, # Must run for at least 2 hours when turned on - consecutive_on_hours_max=8, # Can't run more than 8 consecutive hours + activity_parameters=fx.ActivityParameters( + consecutive_active_hours_min=2, # Must run for at least 2 hours when turned on + consecutive_active_hours_max=8, # Can't run more than 8 consecutive hours ), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) + assert {'Sink(Wärme)|consecutive_active_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) assert_sets_equal( { - 'Sink(Wärme)|consecutive_on_hours|ub', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', - 'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_active_hours|ub', + 'Sink(Wärme)|consecutive_active_hours|forward', + 'Sink(Wärme)|consecutive_active_hours|backward', + 'Sink(Wärme)|consecutive_active_hours|initial', + 'Sink(Wärme)|consecutive_active_hours|lb', } & set(flow.submodel.constraints), { - 'Sink(Wärme)|consecutive_on_hours|ub', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', - 'Sink(Wärme)|consecutive_on_hours|lb', + 'Sink(Wärme)|consecutive_active_hours|ub', + 'Sink(Wärme)|consecutive_active_hours|forward', + 'Sink(Wärme)|consecutive_active_hours|backward', + 'Sink(Wärme)|consecutive_active_hours|initial', + 'Sink(Wärme)|consecutive_active_hours|lb', }, msg='Missing consecutive on hours constraints', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_on_hours'], + model.variables['Sink(Wärme)|consecutive_active_hours'], model.add_variables(lower=0, upper=8, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], - model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega, + model.constraints['Sink(Wärme)|consecutive_active_hours|ub'], + model.variables['Sink(Wärme)|consecutive_active_hours'] <= model.variables['Sink(Wärme)|on'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], - model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_active_hours|forward'], + model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|backward'], - model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_active_hours|backward'], + model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|initial'], - model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_active_hours|initial'], + model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=0) == model.variables['Sink(Wärme)|on'].isel(time=0) * model.hours_per_step.isel(time=0), ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], - model.variables['Sink(Wärme)|consecutive_on_hours'] + model.constraints['Sink(Wärme)|consecutive_active_hours|lb'], + model.variables['Sink(Wärme)|consecutive_active_hours'] >= ( model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) @@ -721,7 +726,7 @@ def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_conf * 2, ) - def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, coords_config): + def test_consecutive_active_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive on hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config @@ -729,9 +734,9 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, co 'Wärme', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters( - consecutive_on_hours_min=2, # Must run for at least 2 hours when turned on - consecutive_on_hours_max=8, # Can't run more than 8 consecutive hours + activity_parameters=fx.ActivityParameters( + consecutive_active_hours_min=2, # Must run for at least 2 hours when turned on + consecutive_active_hours_max=8, # Can't run more than 8 consecutive hours ), previous_flow_rate=np.array([10, 20, 30, 0, 20, 20, 30]), # Previously on for 3 steps ) @@ -739,62 +744,62 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, co flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_on_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) + assert {'Sink(Wärme)|consecutive_active_hours', 'Sink(Wärme)|on'}.issubset(set(flow.submodel.variables)) assert_sets_equal( { - 'Sink(Wärme)|consecutive_on_hours|lb', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_active_hours|lb', + 'Sink(Wärme)|consecutive_active_hours|forward', + 'Sink(Wärme)|consecutive_active_hours|backward', + 'Sink(Wärme)|consecutive_active_hours|initial', } & set(flow.submodel.constraints), { - 'Sink(Wärme)|consecutive_on_hours|lb', - 'Sink(Wärme)|consecutive_on_hours|forward', - 'Sink(Wärme)|consecutive_on_hours|backward', - 'Sink(Wärme)|consecutive_on_hours|initial', + 'Sink(Wärme)|consecutive_active_hours|lb', + 'Sink(Wärme)|consecutive_active_hours|forward', + 'Sink(Wärme)|consecutive_active_hours|backward', + 'Sink(Wärme)|consecutive_active_hours|initial', }, msg='Missing consecutive on hours constraints for previous states', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_on_hours'], + model.variables['Sink(Wärme)|consecutive_active_hours'], model.add_variables(lower=0, upper=8, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 3 assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|ub'], - model.variables['Sink(Wärme)|consecutive_on_hours'] <= model.variables['Sink(Wärme)|on'] * mega, + model.constraints['Sink(Wärme)|consecutive_active_hours|ub'], + model.variables['Sink(Wärme)|consecutive_active_hours'] <= model.variables['Sink(Wärme)|on'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|forward'], - model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_active_hours|forward'], + model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|backward'], - model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_active_hours|backward'], + model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|initial'], - model.variables['Sink(Wärme)|consecutive_on_hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_active_hours|initial'], + model.variables['Sink(Wärme)|consecutive_active_hours'].isel(time=0) == model.variables['Sink(Wärme)|on'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 3)), ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_on_hours|lb'], - model.variables['Sink(Wärme)|consecutive_on_hours'] + model.constraints['Sink(Wärme)|consecutive_active_hours|lb'], + model.variables['Sink(Wärme)|consecutive_active_hours'] >= ( model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) @@ -802,7 +807,7 @@ def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, co * 2, ) - def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_config): + def test_consecutive_inactive_hours(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config @@ -810,73 +815,73 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_con 'Wärme', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters( - consecutive_off_hours_min=4, # Must stay off for at least 4 hours when shut down - consecutive_off_hours_max=12, # Can't be off for more than 12 consecutive hours + activity_parameters=fx.ActivityParameters( + consecutive_inactive_hours_min=4, # Must stay off for at least 4 hours when shut down + consecutive_inactive_hours_max=12, # Can't be off for more than 12 consecutive hours ), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) + assert {'Sink(Wärme)|consecutive_inactive_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) assert_sets_equal( { - 'Sink(Wärme)|consecutive_off_hours|ub', - 'Sink(Wärme)|consecutive_off_hours|forward', - 'Sink(Wärme)|consecutive_off_hours|backward', - 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb', + 'Sink(Wärme)|consecutive_inactive_hours|ub', + 'Sink(Wärme)|consecutive_inactive_hours|forward', + 'Sink(Wärme)|consecutive_inactive_hours|backward', + 'Sink(Wärme)|consecutive_inactive_hours|initial', + 'Sink(Wärme)|consecutive_inactive_hours|lb', } & set(flow.submodel.constraints), { - 'Sink(Wärme)|consecutive_off_hours|ub', - 'Sink(Wärme)|consecutive_off_hours|forward', - 'Sink(Wärme)|consecutive_off_hours|backward', - 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb', + 'Sink(Wärme)|consecutive_inactive_hours|ub', + 'Sink(Wärme)|consecutive_inactive_hours|forward', + 'Sink(Wärme)|consecutive_inactive_hours|backward', + 'Sink(Wärme)|consecutive_inactive_hours|initial', + 'Sink(Wärme)|consecutive_inactive_hours|lb', }, msg='Missing consecutive off hours constraints', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_off_hours'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'], model.add_variables(lower=0, upper=12, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 1 # previously off for 1h assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], - model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega, + model.constraints['Sink(Wärme)|consecutive_inactive_hours|ub'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'] <= model.variables['Sink(Wärme)|off'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], - model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_inactive_hours|forward'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|backward'], - model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_inactive_hours|backward'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|initial'], - model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_inactive_hours|initial'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=0) == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 1)), ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], - model.variables['Sink(Wärme)|consecutive_off_hours'] + model.constraints['Sink(Wärme)|consecutive_inactive_hours|lb'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'] >= ( model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) @@ -884,7 +889,7 @@ def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_con * 4, ) - def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, coords_config): + def test_consecutive_inactive_hours_previous(self, basic_flow_system_linopy_coords, coords_config): """Test flow with minimum and maximum consecutive off hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config @@ -892,9 +897,9 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, c 'Wärme', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters( - consecutive_off_hours_min=4, # Must stay off for at least 4 hours when shut down - consecutive_off_hours_max=12, # Can't be off for more than 12 consecutive hours + activity_parameters=fx.ActivityParameters( + consecutive_inactive_hours_min=4, # Must stay off for at least 4 hours when shut down + consecutive_inactive_hours_max=12, # Can't be off for more than 12 consecutive hours ), previous_flow_rate=np.array([10, 20, 30, 0, 20, 0, 0]), # Previously off for 2 steps ) @@ -902,64 +907,64 @@ def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, c flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) - assert {'Sink(Wärme)|consecutive_off_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) + assert {'Sink(Wärme)|consecutive_inactive_hours', 'Sink(Wärme)|off'}.issubset(set(flow.submodel.variables)) assert_sets_equal( { - 'Sink(Wärme)|consecutive_off_hours|ub', - 'Sink(Wärme)|consecutive_off_hours|forward', - 'Sink(Wärme)|consecutive_off_hours|backward', - 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb', + 'Sink(Wärme)|consecutive_inactive_hours|ub', + 'Sink(Wärme)|consecutive_inactive_hours|forward', + 'Sink(Wärme)|consecutive_inactive_hours|backward', + 'Sink(Wärme)|consecutive_inactive_hours|initial', + 'Sink(Wärme)|consecutive_inactive_hours|lb', } & set(flow.submodel.constraints), { - 'Sink(Wärme)|consecutive_off_hours|ub', - 'Sink(Wärme)|consecutive_off_hours|forward', - 'Sink(Wärme)|consecutive_off_hours|backward', - 'Sink(Wärme)|consecutive_off_hours|initial', - 'Sink(Wärme)|consecutive_off_hours|lb', + 'Sink(Wärme)|consecutive_inactive_hours|ub', + 'Sink(Wärme)|consecutive_inactive_hours|forward', + 'Sink(Wärme)|consecutive_inactive_hours|backward', + 'Sink(Wärme)|consecutive_inactive_hours|initial', + 'Sink(Wärme)|consecutive_inactive_hours|lb', }, msg='Missing consecutive off hours constraints for previous states', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_off_hours'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'], model.add_variables(lower=0, upper=12, coords=model.get_coords()), ) mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 2 assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|ub'], - model.variables['Sink(Wärme)|consecutive_off_hours'] <= model.variables['Sink(Wärme)|off'] * mega, + model.constraints['Sink(Wärme)|consecutive_inactive_hours|ub'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'] <= model.variables['Sink(Wärme)|off'] * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|forward'], - model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) - <= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_inactive_hours|forward'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)), ) # eq: duration(t) >= duration(t - 1) + dt(t) + (On(t) - 1) * BIG assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|backward'], - model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(1, None)) - >= model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=slice(None, -1)) + model.constraints['Sink(Wärme)|consecutive_inactive_hours|backward'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=slice(None, -1)) + model.hours_per_step.isel(time=slice(None, -1)) + (model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) - 1) * mega, ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|initial'], - model.variables['Sink(Wärme)|consecutive_off_hours'].isel(time=0) + model.constraints['Sink(Wärme)|consecutive_inactive_hours|initial'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'].isel(time=0) == model.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 2)), ) assert_conequal( - model.constraints['Sink(Wärme)|consecutive_off_hours|lb'], - model.variables['Sink(Wärme)|consecutive_off_hours'] + model.constraints['Sink(Wärme)|consecutive_inactive_hours|lb'], + model.variables['Sink(Wärme)|consecutive_inactive_hours'] >= ( model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) @@ -975,9 +980,9 @@ def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_con 'Wärme', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters( - switch_on_total_max=5, # Maximum 5 startups - effects_per_switch_on={'costs': 100}, # 100 EUR startup cost + activity_parameters=fx.ActivityParameters( + startup_total_max=5, # Maximum 5 startups + effects_per_startup={'costs': 100}, # 100 EUR startup cost ), ) @@ -1037,9 +1042,9 @@ def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): 'Wärme', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters( - on_hours_total_min=20, # Minimum 20 hours of operation - on_hours_total_max=100, # Maximum 100 hours of operation + activity_parameters=fx.ActivityParameters( + active_hours_total_min=20, # Minimum 20 hours of operation + active_hours_total_max=100, # Maximum 100 hours of operation ), ) @@ -1047,21 +1052,21 @@ def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): model = create_linopy_model(flow_system) # Check that variables exist - assert {'Sink(Wärme)|on', 'Sink(Wärme)|on_hours_total'}.issubset(set(flow.submodel.variables)) + assert {'Sink(Wärme)|on', 'Sink(Wärme)|active_hours_total'}.issubset(set(flow.submodel.variables)) # Check that constraints exist - assert 'Sink(Wärme)|on_hours_total' in model.constraints + assert 'Sink(Wärme)|active_hours_total' in model.constraints - # Check on_hours_total variable bounds + # Check active_hours_total variable bounds assert_var_equal( - flow.submodel.variables['Sink(Wärme)|on_hours_total'], + flow.submodel.variables['Sink(Wärme)|active_hours_total'], model.add_variables(lower=20, upper=100, coords=model.get_coords(['period', 'scenario'])), ) - # Check on_hours_total constraint + # Check active_hours_total constraint assert_conequal( - model.constraints['Sink(Wärme)|on_hours_total'], - flow.submodel.variables['Sink(Wärme)|on_hours_total'] + model.constraints['Sink(Wärme)|active_hours_total'], + flow.submodel.variables['Sink(Wärme)|active_hours_total'] == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) @@ -1077,7 +1082,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=False), relative_minimum=0.2, relative_maximum=0.8, - on_off_parameters=fx.OnOffParameters(), + activity_parameters=fx.ActivityParameters(), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) @@ -1090,7 +1095,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c 'Sink(Wärme)|invested', 'Sink(Wärme)|size', 'Sink(Wärme)|on', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|active_hours_total', }, msg='Incorrect variables', ) @@ -1099,7 +1104,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c set(flow.submodel.constraints), { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|active_hours_total', 'Sink(Wärme)|flow_rate|lb1', 'Sink(Wärme)|flow_rate|ub1', 'Sink(Wärme)|size|lb', @@ -1122,11 +1127,11 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c # OnOff assert_var_equal( - flow.submodel.on_off.on, + flow.submodel.active_inactive.active, model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( - model.variables['Sink(Wärme)|on_hours_total'], + model.variables['Sink(Wärme)|active_hours_total'], model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( @@ -1146,8 +1151,8 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( - model.constraints['Sink(Wärme)|on_hours_total'], - flow.submodel.variables['Sink(Wärme)|on_hours_total'] + model.constraints['Sink(Wärme)|active_hours_total'], + flow.submodel.variables['Sink(Wärme)|active_hours_total'] == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) @@ -1178,7 +1183,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=True), relative_minimum=0.2, relative_maximum=0.8, - on_off_parameters=fx.OnOffParameters(), + activity_parameters=fx.ActivityParameters(), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) @@ -1190,7 +1195,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', 'Sink(Wärme)|on', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|active_hours_total', }, msg='Incorrect variables', ) @@ -1199,7 +1204,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor set(flow.submodel.constraints), { 'Sink(Wärme)|total_flow_hours', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|active_hours_total', 'Sink(Wärme)|flow_rate|lb1', 'Sink(Wärme)|flow_rate|ub1', 'Sink(Wärme)|flow_rate|lb2', @@ -1220,11 +1225,11 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor # OnOff assert_var_equal( - flow.submodel.on_off.on, + flow.submodel.active_inactive.active, model.add_variables(binary=True, coords=model.get_coords()), ) assert_var_equal( - model.variables['Sink(Wärme)|on_hours_total'], + model.variables['Sink(Wärme)|active_hours_total'], model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( @@ -1236,8 +1241,8 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( - model.constraints['Sink(Wärme)|on_hours_total'], - flow.submodel.variables['Sink(Wärme)|on_hours_total'] + model.constraints['Sink(Wärme)|active_hours_total'], + flow.submodel.variables['Sink(Wärme)|active_hours_total'] == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), ) diff --git a/tests/test_functional.py b/tests/test_functional.py index a83bf112f..e52a1abf9 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -338,7 +338,7 @@ def test_on(solver_fixture, time_steps_fixture): 'Boiler', 0.5, Q_fu=fx.Flow('Q_fu', bus='Gas'), - Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100, on_off_parameters=fx.OnOffParameters()), + Q_th=fx.Flow('Q_th', bus='Fernwärme', size=100, activity_parameters=fx.ActivityParameters()), ) ) @@ -354,7 +354,7 @@ def test_on(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.Q_th.submodel.active_inactive.active.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, @@ -381,7 +381,7 @@ def test_off(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters(consecutive_off_hours_max=100), + activity_parameters=fx.ActivityParameters(consecutive_inactive_hours_max=100), ), ) ) @@ -398,15 +398,15 @@ def test_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.Q_th.submodel.active_inactive.active.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.on_off.off.solution.values, - 1 - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.Q_th.submodel.active_inactive.inactive.solution.values, + 1 - boiler.Q_th.submodel.active_inactive.active.solution.values, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__off" does not have the right value', @@ -432,7 +432,7 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters(force_switch_on=True), + activity_parameters=fx.ActivityParameters(force_switch_on=True), ), ) ) @@ -449,21 +449,21 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.Q_th.submodel.active_inactive.active.solution.values, [0, 1, 1, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.on_off.switch_on.solution.values, + boiler.Q_th.submodel.active_inactive.startup.solution.values, [0, 1, 0, 0, 1], rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__switch_on" does not have the right value', ) assert_allclose( - boiler.Q_th.submodel.on_off.switch_off.solution.values, + boiler.Q_th.submodel.active_inactive.shutdown.solution.values, [0, 0, 0, 1, 0], rtol=1e-5, atol=1e-10, @@ -490,7 +490,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters(on_hours_total_max=1), + activity_parameters=fx.ActivityParameters(active_hours_total_max=1), ), ), fx.linear_converters.Boiler( @@ -513,7 +513,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.Q_th.submodel.active_inactive.active.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, @@ -540,7 +540,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters(on_hours_total_max=2), + activity_parameters=fx.ActivityParameters(active_hours_total_max=2), ), ), fx.linear_converters.Boiler( @@ -551,7 +551,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters(on_hours_total_min=3), + activity_parameters=fx.ActivityParameters(active_hours_total_min=3), ), ), ) @@ -572,7 +572,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.Q_th.submodel.active_inactive.active.solution.values, [0, 0, 1, 0, 1], rtol=1e-5, atol=1e-10, @@ -587,7 +587,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - sum(boiler_backup.Q_th.submodel.on_off.on.solution.values), + sum(boiler_backup.Q_th.submodel.active_inactive.active.solution.values), 3, rtol=1e-5, atol=1e-10, @@ -614,7 +614,9 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters(consecutive_on_hours_max=2, consecutive_on_hours_min=2), + activity_parameters=fx.ActivityParameters( + consecutive_active_hours_max=2, consecutive_active_hours_min=2 + ), ), ), fx.linear_converters.Boiler( @@ -640,7 +642,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.Q_th.submodel.on_off.on.solution.values, + boiler.Q_th.submodel.active_inactive.active.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, @@ -682,7 +684,9 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): bus='Fernwärme', size=100, previous_flow_rate=np.array([20]), # Otherwise its Off before the start - on_off_parameters=fx.OnOffParameters(consecutive_off_hours_max=2, consecutive_off_hours_min=2), + activity_parameters=fx.ActivityParameters( + consecutive_inactive_hours_max=2, consecutive_inactive_hours_min=2 + ), ), ), ) @@ -703,14 +707,14 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.Q_th.submodel.on_off.on.solution.values, + boiler_backup.Q_th.submodel.active_inactive.active.solution.values, [0, 0, 1, 0, 0], rtol=1e-5, atol=1e-10, err_msg='"Boiler_backup__Q_th__on" does not have the right value', ) assert_allclose( - boiler_backup.Q_th.submodel.on_off.off.solution.values, + boiler_backup.Q_th.submodel.active_inactive.inactive.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index 1884c8d72..258568594 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -135,25 +135,25 @@ def test_linear_converter_multiple_factors(self, basic_flow_system_linopy_coords ) def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coords_config): - """Test a LinearConverter with OnOffParameters.""" + """Test a LinearConverter with ActivityParameters.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows input_flow = fx.Flow('input', bus='input_bus', size=100) output_flow = fx.Flow('output', bus='output_bus', size=100) - # Create OnOffParameters - on_off_params = fx.OnOffParameters( - on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'costs': 5} + # Create ActivityParameters + on_off_params = fx.ActivityParameters( + active_hours_total_min=10, active_hours_total_max=40, effects_per_running_hour={'costs': 5} ) - # Create a linear converter with OnOffParameters + # Create a linear converter with ActivityParameters converter = fx.LinearConverter( label='Converter', inputs=[input_flow], outputs=[output_flow], conversion_factors=[{input_flow.label: 0.8, output_flow.label: 1.0}], - on_off_parameters=on_off_params, + activity_parameters=on_off_params, ) # Add to flow system @@ -168,12 +168,12 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coo # Verify OnOff variables and constraints assert 'Converter|on' in model.variables - assert 'Converter|on_hours_total' in model.variables + assert 'Converter|active_hours_total' in model.variables - # Check on_hours_total constraint + # Check active_hours_total constraint assert_conequal( - model.constraints['Converter|on_hours_total'], - model.variables['Converter|on_hours_total'] + model.constraints['Converter|active_hours_total'], + model.variables['Converter|active_hours_total'] == (model.variables['Converter|on'] * model.hours_per_step).sum('time'), ) @@ -378,7 +378,7 @@ def test_piecewise_conversion(self, basic_flow_system_linopy_coords, coords_conf ) def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, coords_config): - """Test a LinearConverter with PiecewiseConversion and OnOffParameters.""" + """Test a LinearConverter with PiecewiseConversion and ActivityParameters.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows @@ -395,9 +395,9 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, {input_flow.label: fx.Piecewise(input_pieces), output_flow.label: fx.Piecewise(output_pieces)} ) - # Create OnOffParameters - on_off_params = fx.OnOffParameters( - on_hours_total_min=10, on_hours_total_max=40, effects_per_running_hour={'costs': 5} + # Create ActivityParameters + on_off_params = fx.ActivityParameters( + active_hours_total_min=10, active_hours_total_max=40, effects_per_running_hour={'costs': 5} ) # Create a linear converter with piecewise conversion and on/off parameters @@ -406,7 +406,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, inputs=[input_flow], outputs=[output_flow], piecewise_conversion=piecewise_conversion, - on_off_parameters=on_off_params, + activity_parameters=on_off_params, ) # Add to flow system @@ -429,7 +429,7 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, assert len(piecewise_model.pieces) == 2 # Verify that the on variable was used as the zero_point for the piecewise model - # When using OnOffParameters, the zero_point should be the on variable + # When using ActivityParameters, the zero_point should be the on variable assert 'Converter|on' in model.variables assert piecewise_model.zero_point is not None # Should be a variable @@ -481,10 +481,10 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, ) # Also check that the OnOff model is working correctly - assert 'Converter|on_hours_total' in model.constraints + assert 'Converter|active_hours_total' in model.constraints assert_conequal( - model.constraints['Converter|on_hours_total'], - model['Converter|on_hours_total'] == (model['Converter|on'] * model.hours_per_step).sum('time'), + model.constraints['Converter|active_hours_total'], + model['Converter|active_hours_total'] == (model['Converter|on'] * model.hours_per_step).sum('time'), ) # Verify that the costs effect is applied diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 928eb88c6..ce5ca9d2a 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -140,7 +140,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: boiler = fx.linear_converters.Boiler( 'Kessel', eta=0.5, - on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + activity_parameters=fx.ActivityParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), Q_th=fx.Flow( 'Q_th', bus='Fernwärme', @@ -155,14 +155,14 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: mandatory=True, effects_of_investment_per_size={'costs': 10, 'PE': 2}, ), - on_off_parameters=fx.OnOffParameters( - on_hours_total_min=0, - on_hours_total_max=1000, - consecutive_on_hours_max=10, - consecutive_on_hours_min=1, - consecutive_off_hours_max=10, - effects_per_switch_on=0.01, - switch_on_total_max=1000, + activity_parameters=fx.ActivityParameters( + active_hours_total_min=0, + active_hours_total_max=1000, + consecutive_active_hours_max=10, + consecutive_active_hours_min=1, + consecutive_inactive_hours_max=10, + effects_per_startup=0.01, + startup_total_max=1000, ), flow_hours_total_max=1e6, ), @@ -228,7 +228,7 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> 'Q_fu': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]), } ), - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + activity_parameters=fx.ActivityParameters(effects_per_startup=0.01), ) )