diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 395fc766c..4c77a4dbe 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,7 +6,7 @@ on: pull_request: branches: ["*"] workflow_dispatch: - workflow_call: # Allow release.yaml to call this workflow + workflow_call: # Allow release.yaml to call this workflow. concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7544375ff..3bc226e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,14 +51,76 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? -**Summary**: - -If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). +**Summary**: Renamed OnOff terminology to Status terminology for better alignment with PyPSA and unit commitment standards. ### ✨ Added ### 💥 Breaking Changes +**Renamed `OnOffParameters` → `StatusParameters`**: Complete terminology update to align with industry standards (PyPSA, unit commitment). This is a clean breaking change with no backwards compatibility wrapper. + +**Class and Constructor Parameters:** + +| Category | Old Name (OnOffParameters) | New Name (StatusParameters) | Notes | +|----------|---------------------------|----------------------------|-------| +| **Class** | `OnOffParameters` | `StatusParameters` | Main class renamed | +| **Constructor** | `on_variable` | `status` | Model variable parameter | +| **Constructor** | `previous_states` | `previous_status` | Initial state parameter | +| **Parameter** | `effects_per_switch_on` | `effects_per_startup` | Startup costs/impacts | +| **Parameter** | `effects_per_running_hour` | `effects_per_active_hour` | Operating costs/impacts | +| **Parameter** | `on_hours_total_min` | `active_hours_min` | Minimum total operating hours | +| **Parameter** | `on_hours_total_max` | `active_hours_max` | Maximum total operating hours | +| **Parameter** | `consecutive_on_hours_min` | `min_uptime` | UC standard terminology | +| **Parameter** | `consecutive_on_hours_max` | `max_uptime` | UC standard terminology | +| **Parameter** | `consecutive_off_hours_min` | `min_downtime` | UC standard terminology | +| **Parameter** | `consecutive_off_hours_max` | `max_downtime` | UC standard terminology | +| **Parameter** | `switch_on_total_max` | `startup_limit` | Maximum number of startups | +| **Parameter** | `force_switch_on` | `force_startup_tracking` | Force creation of startup variables | + +**Model Classes and Variables:** + +| Category | Old Name (OnOffModel) | New Name (StatusModel) | Notes | +|----------|----------------------|------------------------|-------| +| **Model Class** | `OnOffModel` | `StatusModel` | Feature model class | +| **Variable** | `on` | `status` | Main binary state variable | +| **Variable** | `switch_on` | `startup` | Startup event variable | +| **Variable** | `switch_off` | `shutdown` | Shutdown event variable | +| **Variable** | `switch_on_nr` | `startup_count` | Cumulative startup counter | +| **Variable** | `on_hours_total` | `active_hours` | Total operating hours | +| **Variable** | `consecutive_on_hours` | `uptime` | Consecutive active hours | +| **Variable** | `consecutive_off_hours` | `downtime` | Consecutive inactive hours | +| **Variable** | `off` | `inactive` | Deprecated - use `1 - status` instead | + +**Flow and Component API:** + +| Category | Old Name | New Name | Location | +|----------|----------|----------|----------| +| **Parameter** | `on_off_parameters` | `status_parameters` | `Flow.__init__()` | +| **Parameter** | `on_off_parameters` | `status_parameters` | `Component.__init__()` | +| **Property** | `flow.submodel.on_off` | `flow.submodel.status` | Flow submodel access | +| **Property** | `component.submodel.on_off` | `component.submodel.status` | Component submodel access | + +**Internal Properties:** + +| Old Name | New Name | +|----------|----------| +| `use_switch_on` | `use_startup_tracking` | +| `use_consecutive_on_hours` | `use_uptime_tracking` | +| `use_consecutive_off_hours` | `use_downtime_tracking` | +| `with_on_off` | `with_status` | +| `previous_states` | `previous_status` | + +**Migration Guide**: + +Use find-and-replace to update your code with the mappings above. The functionality is identical - only naming has changed. + +**Important**: This is a complete renaming with no backwards compatibility. The change affects: +- Constructor parameter names +- Model variable names and property access +- Results access patterns + +A partial backwards compatibility wrapper would be misleading, so we opted for a clean breaking change. + ### ♻️ Changed ### 🗑️ Deprecated diff --git a/docs/user-guide/core-concepts.md b/docs/user-guide/core-concepts.md index f165f1e4e..d63f10f27 100644 --- a/docs/user-guide/core-concepts.md +++ b/docs/user-guide/core-concepts.md @@ -28,7 +28,7 @@ Element labels must be unique across all types. See the [`FlowSystem` API refere - Have a `size` which, generally speaking, defines how much energy or material can be moved. Usually measured in MW, kW, m³/h, etc. - Have a `flow_rate`, which defines how fast energy or material is transported. Usually measured in MW, kW, m³/h, etc. -- Have constraints to limit the flow-rate (min/max, total flow hours, on/off etc.) +- Have constraints to limit the flow-rate (min/max, total flow hours, active/inactive status etc.) - Can have fixed profiles (for demands or renewable generation) - Can have [Effects](#effects) associated by their use (costs, emissions, labour, ...) diff --git a/docs/user-guide/mathematical-notation/dimensions.md b/docs/user-guide/mathematical-notation/dimensions.md index e10ef5ffd..2a526e19d 100644 --- a/docs/user-guide/mathematical-notation/dimensions.md +++ b/docs/user-guide/mathematical-notation/dimensions.md @@ -102,7 +102,7 @@ Scenarios within a period are **operationally independent**: - Each scenario has its own operational variables: $p(\text{t}_i, s_1)$ and $p(\text{t}_i, s_2)$ are independent - Scenarios cannot exchange energy, information, or resources - Storage states are separate: $c(\text{t}_i, s_1) \neq c(\text{t}_i, s_2)$ -- Binary states (on/off) are independent: $s(\text{t}_i, s_1)$ vs $s(\text{t}_i, s_2)$ +- Binary states (active/inactive) are independent: $s(\text{t}_i, s_1)$ vs $s(\text{t}_i, s_2)$ Scenarios are connected **only through the objective function** via weights: diff --git a/docs/user-guide/mathematical-notation/effects-penalty-objective.md b/docs/user-guide/mathematical-notation/effects-penalty-objective.md index 1c96f3613..aeab09031 100644 --- a/docs/user-guide/mathematical-notation/effects-penalty-objective.md +++ b/docs/user-guide/mathematical-notation/effects-penalty-objective.md @@ -7,6 +7,7 @@ **Example:** [`Flows`][flixopt.elements.Flow] have an attribute `effects_per_flow_hour` that defines the effect contribution per flow-hour: + - Costs (€/kWh) - Emissions (kg CO₂/kWh) - Primary energy consumption (kWh_primary/kWh) @@ -260,6 +261,7 @@ $$ $$ Where: + - $\mathcal{S}$ is the set of scenarios - $w_s$ is the weight for scenario $s$ (typically scenario probability) - Periodic effects are **shared across scenarios**: $E_{\Omega,\text{per}}$ and $E_{\Phi,\text{per}}$ (same for all $s$) @@ -280,6 +282,7 @@ $$ $$ Where: + - $\mathcal{Y}$ is the set of periods (e.g., years) - $w_y$ is the weight for period $y$ (typically annual discount factor) - Each period $y$ has **independent** periodic and temporal effects (including penalty) @@ -295,6 +298,7 @@ $$ $$ Where: + - $\mathcal{S}$ is the set of scenarios - $\mathcal{Y}$ is the set of periods - $w_y$ is the period weight (for periodic effects) diff --git a/docs/user-guide/mathematical-notation/elements/Flow.md b/docs/user-guide/mathematical-notation/elements/Flow.md index 5914ba911..2cc2e3b6a 100644 --- a/docs/user-guide/mathematical-notation/elements/Flow.md +++ b/docs/user-guide/mathematical-notation/elements/Flow.md @@ -23,8 +23,8 @@ $$ $$ -This mathematical formulation can be extended by using [OnOffParameters](../features/OnOffParameters.md) -to define the on/off state of the Flow, or by using [InvestParameters](../features/InvestParameters.md) +This mathematical formulation can be extended by using [StatusParameters](../features/StatusParameters.md) +to define the active/inactive 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 [StatusParameters](../features/StatusParameters.md) - **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions with [InvestParameters](../features/InvestParameters.md) --- @@ -44,11 +44,12 @@ Flow formulation uses the following modeling patterns: **Python Class:** [`Flow`][flixopt.elements.Flow] **Key Parameters:** + - `size`: Flow size $\text{P}$ (can be fixed or variable with InvestParameters) - `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)) +- `status_parameters`: Optional active/inactive operation (see [StatusParameters](../features/StatusParameters.md)) See the [`Flow`][flixopt.elements.Flow] API documentation for complete parameter list and usage examples. @@ -56,7 +57,7 @@ See the [`Flow`][flixopt.elements.Flow] API documentation for complete parameter ## See Also -- [OnOffParameters](../features/OnOffParameters.md) - Binary on/off operation +- [StatusParameters](../features/StatusParameters.md) - Binary active/inactive 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/elements/Storage.md b/docs/user-guide/mathematical-notation/elements/Storage.md index cd7046592..9ecd4d570 100644 --- a/docs/user-guide/mathematical-notation/elements/Storage.md +++ b/docs/user-guide/mathematical-notation/elements/Storage.md @@ -53,6 +53,7 @@ Storage formulation uses the following modeling patterns: - **[Scaled Bounds](../modeling-patterns/bounds-and-states.md#scaled-bounds)** - For flow rate bounds relative to storage size When combined with investment parameters, storage can use: + - **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions (see [InvestParameters](../features/InvestParameters.md)) --- @@ -62,6 +63,7 @@ When combined with investment parameters, storage can use: **Python Class:** [`Storage`][flixopt.components.Storage] **Key Parameters:** + - `capacity_in_flow_hours`: Storage capacity $\text{C}$ - `relative_loss_per_hour`: Self-discharge rate $\dot{\text{c}}_\text{rel,loss}$ - `initial_charge_state`: Initial charge $c(\text{t}_0)$ diff --git a/docs/user-guide/mathematical-notation/features/InvestParameters.md b/docs/user-guide/mathematical-notation/features/InvestParameters.md index 14fe02c79..d998039c0 100644 --- a/docs/user-guide/mathematical-notation/features/InvestParameters.md +++ b/docs/user-guide/mathematical-notation/features/InvestParameters.md @@ -15,6 +15,7 @@ v_\text{invest} = s_\text{invest} \cdot \text{size}_\text{fixed} $$ With: + - $v_\text{invest}$ being the resulting investment size - $s_\text{invest} \in \{0, 1\}$ being the binary investment decision - $\text{size}_\text{fixed}$ being the predefined component size @@ -34,6 +35,7 @@ s_\text{invest} \cdot \text{size}_\text{min} \leq v_\text{invest} \leq s_\text{i $$ With: + - $v_\text{invest}$ being the investment size variable (continuous) - $s_\text{invest} \in \{0, 1\}$ being the binary investment decision - $\text{size}_\text{min}$ being the minimum investment size (if investing) @@ -80,6 +82,7 @@ E_{e,\text{fix}} = s_\text{invest} \cdot \text{fix}_e $$ With: + - $E_{e,\text{fix}}$ being the fixed contribution to effect $e$ - $\text{fix}_e$ being the fixed effect value (e.g., fixed installation cost) @@ -99,6 +102,7 @@ E_{e,\text{spec}} = v_\text{invest} \cdot \text{spec}_e $$ With: + - $E_{e,\text{spec}}$ being the size-dependent contribution to effect $e$ - $\text{spec}_e$ being the specific effect value per unit size (e.g., €/kW) @@ -123,6 +127,7 @@ v_\text{invest} = \sum_{k=1}^{K} \lambda_k \cdot v_k $$ With: + - $E_{e,\text{pw}}$ being the piecewise contribution to effect $e$ - $\lambda_k$ being the piecewise lambda variables (see [Piecewise](../features/Piecewise.md)) - $r_{e,k}$ being the effect rate at piece $k$ @@ -146,6 +151,7 @@ E_{e,\text{retirement}} = (1 - s_\text{invest}) \cdot \text{retirement}_e $$ With: + - $E_{e,\text{retirement}}$ being the retirement contribution to effect $e$ - $\text{retirement}_e$ being the retirement effect value @@ -210,6 +216,7 @@ $$\label{eq:annualization} $$ With: + - $\text{cost}_\text{capital}$ being the upfront investment cost - $r$ being the discount rate - $n$ being the equipment lifetime in years @@ -226,6 +233,7 @@ $$ **Python Class:** [`InvestParameters`][flixopt.interface.InvestParameters] **Key Parameters:** + - `fixed_size`: For binary investments (mutually exclusive with continuous sizing) - `minimum_size`, `maximum_size`: For continuous sizing - `mandatory`: Whether investment is required (default: `False`) diff --git a/docs/user-guide/mathematical-notation/features/OnOffParameters.md b/docs/user-guide/mathematical-notation/features/OnOffParameters.md index 6bf40fec9..e69de29bb 100644 --- a/docs/user-guide/mathematical-notation/features/OnOffParameters.md +++ b/docs/user-guide/mathematical-notation/features/OnOffParameters.md @@ -1,307 +0,0 @@ -# OnOffParameters - -[`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. - -## Binary State Variable - -Equipment operation is modeled using a binary state variable: - -$$\label{eq:onoff_state} -s(t) \in \{0, 1\} \quad \forall t -$$ - -With: -- $s(t) = 1$: equipment is operating (on state) -- $s(t) = 0$: equipment is shutdown (off state) - -This state variable controls the equipment's operational constraints and modifies flow bounds using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). - ---- - -## State Transitions and Switching - -State transitions are tracked using switch variables (see [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions)): - -$$\label{eq:onoff_transitions} -s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1) \quad \forall t > 0 -$$ - -$$\label{eq:onoff_switch_exclusivity} -s^\text{on}(t) + s^\text{off}(t) \leq 1 \quad \forall t -$$ - -With: -- $s^\text{on}(t) \in \{0, 1\}$: equals 1 when switching from off to on (startup) -- $s^\text{off}(t) \in \{0, 1\}$: equals 1 when switching from on to off (shutdown) - -**Behavior:** -- Off → On: $s^\text{on}(t) = 1, s^\text{off}(t) = 0$ -- On → Off: $s^\text{on}(t) = 0, s^\text{off}(t) = 1$ -- No change: $s^\text{on}(t) = 0, s^\text{off}(t) = 0$ - ---- - -## Effects and Costs - -### Switching Effects - -Effects incurred when equipment starts up: - -$$\label{eq:onoff_switch_effects} -E_{e,\text{switch}} = \sum_{t} s^\text{on}(t) \cdot \text{effect}_{e,\text{switch}} -$$ - -With: -- $\text{effect}_{e,\text{switch}}$ being the effect value per startup event - -**Examples:** -- Startup fuel consumption -- Wear and tear costs -- Labor costs for startup procedures -- Inrush power demands - ---- - -### Running Effects - -Effects incurred while equipment is operating: - -$$\label{eq:onoff_running_effects} -E_{e,\text{run}} = \sum_{t} s(t) \cdot \Delta t \cdot \text{effect}_{e,\text{run}} -$$ - -With: -- $\text{effect}_{e,\text{run}}$ being the effect rate per operating hour -- $\Delta t$ being the time step duration - -**Examples:** -- Fixed operating and maintenance costs -- Auxiliary power consumption -- Consumable materials -- Emissions while running - ---- - -## Operating Hour Constraints - -### Total Operating Hours - -Bounds on total operating time across the planning horizon: - -$$\label{eq:onoff_total_hours} -h_\text{min} \leq \sum_{t} s(t) \cdot \Delta t \leq h_\text{max} -$$ - -With: -- $h_\text{min}$ being the minimum total operating hours -- $h_\text{max}$ being the maximum total operating hours - -**Use cases:** -- Minimum runtime requirements (contracts, maintenance) -- Maximum runtime limits (fuel availability, permits, equipment life) - ---- - -### Consecutive Operating Hours - -**Minimum Consecutive On-Time:** - -Enforces minimum runtime once started using duration tracking (see [Duration Tracking](../modeling-patterns/duration-tracking.md#minimum-duration-constraints)): - -$$\label{eq:onoff_min_on_duration} -d^\text{on}(t) \geq (s(t-1) - s(t)) \cdot h^\text{on}_\text{min} \quad \forall t > 0 -$$ - -With: -- $d^\text{on}(t)$ being the consecutive on-time duration at time $t$ -- $h^\text{on}_\text{min}$ being the minimum required on-time - -**Behavior:** -- When shutting down at time $t$: enforces equipment was on for at least $h^\text{on}_\text{min}$ prior to the switch -- Prevents short cycling and frequent startups - -**Maximum Consecutive On-Time:** - -Limits continuous operation before requiring shutdown: - -$$\label{eq:onoff_max_on_duration} -d^\text{on}(t) \leq h^\text{on}_\text{max} \quad \forall t -$$ - -**Use cases:** -- Mandatory maintenance intervals -- Process batch time limits -- Thermal cycling requirements - ---- - -### Consecutive Shutdown Hours - -**Minimum Consecutive Off-Time:** - -Enforces minimum shutdown duration before restarting: - -$$\label{eq:onoff_min_off_duration} -d^\text{off}(t) \geq (s(t) - s(t-1)) \cdot h^\text{off}_\text{min} \quad \forall t > 0 -$$ - -With: -- $d^\text{off}(t)$ being the consecutive off-time duration at time $t$ -- $h^\text{off}_\text{min}$ being the minimum required off-time - -**Use cases:** -- Cooling periods -- Maintenance requirements -- Process stabilization - -**Maximum Consecutive Off-Time:** - -Limits shutdown duration before mandatory restart: - -$$\label{eq:onoff_max_off_duration} -d^\text{off}(t) \leq h^\text{off}_\text{max} \quad \forall t -$$ - -**Use cases:** -- Equipment preservation requirements -- Process stability needs -- Contractual minimum activity levels - ---- - -## Cycling Limits - -Maximum number of startups across the planning horizon: - -$$\label{eq:onoff_max_switches} -\sum_{t} s^\text{on}(t) \leq n_\text{max} -$$ - -With: -- $n_\text{max}$ being the maximum allowed number of startups - -**Use cases:** -- Preventing excessive equipment wear -- Grid stability requirements -- Operational complexity limits -- Maintenance budget constraints - ---- - -## Integration with Flow Bounds - -OnOffParameters modify flow rate bounds by coupling them to the on/off state. - -**Without OnOffParameters** (continuous operation): -$$ -P \cdot \text{rel}_\text{lower} \leq p(t) \leq P \cdot \text{rel}_\text{upper} -$$ - -**With OnOffParameters** (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} -$$ - -Using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). - -**Behavior:** -- When $s(t) = 0$: flow is forced to zero -- When $s(t) = 1$: flow follows normal bounds - ---- - -## Complete Formulation Summary - -For equipment with OnOffParameters, 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)$ -3. **Switch exclusivity:** $s^\text{on}(t) + s^\text{off}(t) \leq 1$ -4. **Duration tracking:** - - On-duration: $d^\text{on}(t)$ following duration tracking pattern - - Off-duration: $d^\text{off}(t)$ following duration tracking pattern -5. **Minimum on-time:** $d^\text{on}(t) \geq (s(t-1) - s(t)) \cdot h^\text{on}_\text{min}$ -6. **Maximum on-time:** $d^\text{on}(t) \leq h^\text{on}_\text{max}$ -7. **Minimum off-time:** $d^\text{off}(t) \geq (s(t) - s(t-1)) \cdot h^\text{off}_\text{min}$ -8. **Maximum off-time:** $d^\text{off}(t) \leq h^\text{off}_\text{max}$ -9. **Total hours:** $h_\text{min} \leq \sum_t s(t) \cdot \Delta t \leq h_\text{max}$ -10. **Cycling limit:** $\sum_t s^\text{on}(t) \leq n_\text{max}$ -11. **Flow bounds:** $s(t) \cdot P \cdot \text{rel}_\text{lower} \leq p(t) \leq s(t) \cdot P \cdot \text{rel}_\text{upper}$ - ---- - -## Implementation - -**Python Class:** [`OnOffParameters`][flixopt.interface.OnOffParameters] - -**Key Parameters:** -- `effects_per_switch_on`: Costs per startup event -- `effects_per_running_hour`: Costs per hour of operation -- `on_hours_min`, `on_hours_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_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. - -**Mathematical Patterns Used:** -- [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions) - Switch tracking -- [Duration Tracking](../modeling-patterns/duration-tracking.md) - Consecutive time constraints -- [Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state) - Flow control - -**Used in:** -- [`Flow`][flixopt.elements.Flow] - On/off operation for flows -- All components supporting discrete operational states - ---- - -## Examples - -### Power Plant with Startup Costs -```python -power_plant = OnOffParameters( - effects_per_switch_on={'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_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_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_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_min=26, # Weekly testing requirement -) -``` - ---- - -## 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. diff --git a/docs/user-guide/mathematical-notation/features/StatusParameters.md b/docs/user-guide/mathematical-notation/features/StatusParameters.md new file mode 100644 index 000000000..2ec34e3df --- /dev/null +++ b/docs/user-guide/mathematical-notation/features/StatusParameters.md @@ -0,0 +1,317 @@ +# StatusParameters + +[`StatusParameters`][flixopt.interface.StatusParameters] model equipment that operates in discrete active/inactive states rather than continuous operation. This captures realistic operational constraints including startup costs, minimum run times, cycling limitations, and maintenance scheduling. + +## Binary State Variable + +Equipment operation is modeled using a binary state variable: + +$$\label{eq:status_state} +s(t) \in \{0, 1\} \quad \forall t +$$ + +With: + +- $s(t) = 1$: equipment is operating (active state) +- $s(t) = 0$: equipment is shutdown (inactive state) + +This state variable controls the equipment's operational constraints and modifies flow bounds using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +--- + +## State Transitions and Switching + +State transitions are tracked using switch variables (see [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions)): + +$$\label{eq:status_transitions} +s^\text{startup}(t) - s^\text{shutdown}(t) = s(t) - s(t-1) \quad \forall t > 0 +$$ + +$$\label{eq:status_switch_exclusivity} +s^\text{startup}(t) + s^\text{shutdown}(t) \leq 1 \quad \forall t +$$ + +With: + +- $s^\text{startup}(t) \in \{0, 1\}$: equals 1 when switching from inactive to active (startup) +- $s^\text{shutdown}(t) \in \{0, 1\}$: equals 1 when switching from active to inactive (shutdown) + +**Behavior:** +- Inactive → Active: $s^\text{startup}(t) = 1, s^\text{shutdown}(t) = 0$ +- Active → Inactive: $s^\text{startup}(t) = 0, s^\text{shutdown}(t) = 1$ +- No change: $s^\text{startup}(t) = 0, s^\text{shutdown}(t) = 0$ + +--- + +## Effects and Costs + +### Startup Effects + +Effects incurred when equipment starts up: + +$$\label{eq:status_switch_effects} +E_{e,\text{switch}} = \sum_{t} s^\text{startup}(t) \cdot \text{effect}_{e,\text{switch}} +$$ + +With: + +- $\text{effect}_{e,\text{switch}}$ being the effect value per startup event + +**Examples:** +- Startup fuel consumption +- Wear and tear costs +- Labor costs for startup procedures +- Inrush power demands + +--- + +### Running Effects + +Effects incurred while equipment is operating: + +$$\label{eq:status_running_effects} +E_{e,\text{run}} = \sum_{t} s(t) \cdot \Delta t \cdot \text{effect}_{e,\text{run}} +$$ + +With: + +- $\text{effect}_{e,\text{run}}$ being the effect rate per operating hour +- $\Delta t$ being the time step duration + +**Examples:** +- Fixed operating and maintenance costs +- Auxiliary power consumption +- Consumable materials +- Emissions while running + +--- + +## Operating Hour Constraints + +### Total Operating Hours + +Bounds on total operating time across the planning horizon: + +$$\label{eq:status_total_hours} +h_\text{min} \leq \sum_{t} s(t) \cdot \Delta t \leq h_\text{max} +$$ + +With: + +- $h_\text{min}$ being the minimum total operating hours +- $h_\text{max}$ being the maximum total operating hours + +**Use cases:** +- Minimum runtime requirements (contracts, maintenance) +- Maximum runtime limits (fuel availability, permits, equipment life) + +--- + +### Consecutive Operating Hours + +**Minimum Consecutive Uptime:** + +Enforces minimum runtime once started using duration tracking (see [Duration Tracking](../modeling-patterns/duration-tracking.md#minimum-duration-constraints)): + +$$\label{eq:status_min_uptime} +d^\text{uptime}(t) \geq (s(t-1) - s(t)) \cdot h^\text{uptime}_\text{min} \quad \forall t > 0 +$$ + +With: + +- $d^\text{uptime}(t)$ being the consecutive uptime duration at time $t$ +- $h^\text{uptime}_\text{min}$ being the minimum required uptime + +**Behavior:** +- When shutting down at time $t$: enforces equipment was on for at least $h^\text{uptime}_\text{min}$ prior to the switch +- Prevents short cycling and frequent startups + +**Maximum Consecutive Uptime:** + +Limits continuous operation before requiring shutdown: + +$$\label{eq:status_max_uptime} +d^\text{uptime}(t) \leq h^\text{uptime}_\text{max} \quad \forall t +$$ + +**Use cases:** +- Mandatory maintenance intervals +- Process batch time limits +- Thermal cycling requirements + +--- + +### Consecutive Shutdown Hours + +**Minimum Consecutive Downtime:** + +Enforces minimum shutdown duration before restarting: + +$$\label{eq:status_min_downtime} +d^\text{downtime}(t) \geq (s(t) - s(t-1)) \cdot h^\text{downtime}_\text{min} \quad \forall t > 0 +$$ + +With: + +- $d^\text{downtime}(t)$ being the consecutive downtime duration at time $t$ +- $h^\text{downtime}_\text{min}$ being the minimum required downtime + +**Use cases:** +- Cooling periods +- Maintenance requirements +- Process stabilization + +**Maximum Consecutive Downtime:** + +Limits shutdown duration before mandatory restart: + +$$\label{eq:status_max_downtime} +d^\text{downtime}(t) \leq h^\text{downtime}_\text{max} \quad \forall t +$$ + +**Use cases:** +- Equipment preservation requirements +- Process stability needs +- Contractual minimum activity levels + +--- + +## Cycling Limits + +Maximum number of startups across the planning horizon: + +$$\label{eq:status_max_switches} +\sum_{t} s^\text{startup}(t) \leq n_\text{max} +$$ + +With: + +- $n_\text{max}$ being the maximum allowed number of startups + +**Use cases:** +- Preventing excessive equipment wear +- Grid stability requirements +- Operational complexity limits +- Maintenance budget constraints + +--- + +## Integration with Flow Bounds + +StatusParameters modify flow rate bounds by coupling them to the active/inactive state. + +**Without StatusParameters** (continuous operation): +$$ +P \cdot \text{rel}_\text{lower} \leq p(t) \leq P \cdot \text{rel}_\text{upper} +$$ + +**With StatusParameters** (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} +$$ + +Using the **bounds with state** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state). + +**Behavior:** +- When $s(t) = 0$: flow is forced to zero +- When $s(t) = 1$: flow follows normal bounds + +--- + +## Complete Formulation Summary + +For equipment with StatusParameters, the complete constraint system includes: + +1. **State variable:** $s(t) \in \{0, 1\}$ +2. **Switch tracking:** $s^\text{startup}(t) - s^\text{shutdown}(t) = s(t) - s(t-1)$ +3. **Switch exclusivity:** $s^\text{startup}(t) + s^\text{shutdown}(t) \leq 1$ +4. **Duration tracking:** + + - On-duration: $d^\text{uptime}(t)$ following duration tracking pattern + - Off-duration: $d^\text{downtime}(t)$ following duration tracking pattern +5. **Minimum uptime:** $d^\text{uptime}(t) \geq (s(t-1) - s(t)) \cdot h^\text{uptime}_\text{min}$ +6. **Maximum uptime:** $d^\text{uptime}(t) \leq h^\text{uptime}_\text{max}$ +7. **Minimum downtime:** $d^\text{downtime}(t) \geq (s(t) - s(t-1)) \cdot h^\text{downtime}_\text{min}$ +8. **Maximum downtime:** $d^\text{downtime}(t) \leq h^\text{downtime}_\text{max}$ +9. **Total hours:** $h_\text{min} \leq \sum_t s(t) \cdot \Delta t \leq h_\text{max}$ +10. **Cycling limit:** $\sum_t s^\text{startup}(t) \leq n_\text{max}$ +11. **Flow bounds:** $s(t) \cdot P \cdot \max(\varepsilon, \text{rel}_\text{lower}) \leq p(t) \leq s(t) \cdot P \cdot \text{rel}_\text{upper}$ + +--- + +## Implementation + +**Python Class:** [`StatusParameters`][flixopt.interface.StatusParameters] + +**Key Parameters:** + +- `effects_per_startup`: Costs per startup event +- `effects_per_active_hour`: Costs per hour of operation +- `active_hours_min`, `active_hours_max`: Total runtime bounds +- `min_uptime`, `max_uptime`: Consecutive runtime bounds +- `min_downtime`, `max_downtime`: Consecutive shutdown bounds +- `startup_limit`: Maximum number of startups +- `force_startup_tracking`: Create switch variables even without limits (for tracking) + +See the [`StatusParameters`][flixopt.interface.StatusParameters] API documentation for complete parameter list and usage examples. + +**Mathematical Patterns Used:** +- [State Transitions](../modeling-patterns/state-transitions.md#binary-state-transitions) - Switch tracking +- [Duration Tracking](../modeling-patterns/duration-tracking.md) - Consecutive time constraints +- [Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state) - Flow control + +**Used in:** +- [`Flow`][flixopt.elements.Flow] - Active/inactive operation for flows +- All components supporting discrete operational states + +--- + +## Examples + +### Power Plant with Startup Costs +```python +power_plant = StatusParameters( + effects_per_startup={'startup_cost': 25000}, # €25k per startup + effects_per_active_hour={'fixed_om': 125}, # €125/hour while running + min_uptime=8, # Minimum 8-hour run + min_downtime=4, # 4-hour cooling period + active_hours_max=6000, # Annual limit +) +``` + +### Batch Process with Cycling Limits +```python +batch_reactor = StatusParameters( + effects_per_startup={'setup_cost': 1500}, + min_uptime=12, # 12-hour minimum batch + max_uptime=24, # 24-hour maximum batch + min_downtime=6, # Cleaning time + startup_limit=200, # Max 200 batches +) +``` + +### HVAC with Cycle Prevention +```python +hvac = StatusParameters( + effects_per_startup={'compressor_wear': 0.5}, + min_uptime=1, # Prevent short cycling + min_downtime=0.5, # 30-min minimum off + startup_limit=2000, # Limit compressor starts +) +``` + +### Backup Generator with Testing Requirements +```python +backup_gen = StatusParameters( + effects_per_startup={'fuel_priming': 50}, # L diesel + min_uptime=0.5, # 30-min test duration + max_downtime=720, # Test every 30 days + active_hours_min=26, # Weekly testing requirement +) +``` + +--- + +## Notes + +**Time Series Boundary:** The final time period constraints for min_uptime/max and min_downtime/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..4512820f3 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 +- [StatusParameters](features/StatusParameters.md) - Binary active/inactive 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 `status_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** | [StatusParameters](features/StatusParameters.md) | [`StatusParameters`][flixopt.interface.StatusParameters] | | **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] | +| `StatusParameters` | [StatusParameters](features/StatusParameters.md) | [`StatusParameters`][flixopt.interface.StatusParameters] | | `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..18235e50d 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 @@ -11,6 +11,7 @@ $$\label{eq:basic_bounds} $$ With: + - $v$ being the optimization variable - $\text{lower}$ being the lower bound (constant or time-dependent) - $\text{upper}$ being the upper bound (constant or time-dependent) @@ -25,13 +26,14 @@ With: ## Bounds with State -When a variable should only be non-zero if a binary state variable is active (e.g., on/off operation, investment decisions), the bounds are controlled by the state: +When a variable should only be non-zero if a binary state variable is active (e.g., active/inactive operation, investment decisions), the bounds are controlled by the state: $$\label{eq:bounds_with_state} s \cdot \max(\varepsilon, \text{lower}) \leq v \leq s \cdot \text{upper} $$ With: + - $v$ being the optimization variable - $s \in \{0, 1\}$ being the binary state variable - $\text{lower}$ being the lower bound when active @@ -45,7 +47,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 active/inactive operation (see [StatusParameters](../features/StatusParameters.md)) - Investment size decisions (see [InvestParameters](../features/InvestParameters.md)) --- @@ -59,6 +61,7 @@ v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \t $$ With: + - $v$ being the optimization variable (e.g., flow rate) - $v_\text{scale}$ being the scaling variable (e.g., component size) - $\text{rel}_\text{lower}$ being the relative lower bound factor (typically 0) @@ -78,7 +81,7 @@ With: ## Scaled Bounds with State -Combining scaled bounds with binary state control requires a Big-M formulation to handle both the scaling and the on/off behavior: +Combining scaled bounds with binary state control requires a Big-M formulation to handle both the scaling and the active/inactive behavior: $$\label{eq:scaled_bounds_with_state_1} (s - 1) \cdot M_\text{misc} + v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper} @@ -89,6 +92,7 @@ s \cdot M_\text{lower} \leq v \leq s \cdot M_\text{upper} $$ With: + - $v$ being the optimization variable - $v_\text{scale}$ being the scaling variable - $s \in \{0, 1\}$ being the binary state variable @@ -107,8 +111,8 @@ Where $v_\text{scale,max}$ and $v_\text{scale,min}$ are the maximum and minimum **Implementation:** [`BoundingPatterns.scaled_bounds_with_state()`][flixopt.modeling.BoundingPatterns.scaled_bounds_with_state] **Used in:** -- Flow rates with on/off operation and investment sizing -- Components combining [OnOffParameters](../features/OnOffParameters.md) and [InvestParameters](../features/InvestParameters.md) +- Flow rates with active/inactive operation and investment sizing +- Components combining [StatusParameters](../features/StatusParameters.md) and [InvestParameters](../features/InvestParameters.md) --- @@ -127,6 +131,7 @@ $$\label{eq:expression_tracking_bounds} $$ With: + - $v_\text{tracker}$ being the auxiliary tracking variable - $\text{expression}$ being a linear expression of other variables - $\text{lower}, \text{upper}$ being optional bounds on the tracker @@ -149,6 +154,7 @@ $$\label{eq:mutual_exclusivity} $$ With: + - $s_i(t) \in \{0, 1\}$ being binary state variables - $\text{tolerance}$ being the maximum number of simultaneously active states (typically 1) - $t$ being the time index 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..2d6f46ed1 100644 --- a/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md +++ b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md @@ -15,6 +15,7 @@ d(t) \leq s(t) \cdot M \quad \forall t $$ With: + - $d(t)$ being the duration variable (continuous, non-negative) - $s(t) \in \{0, 1\}$ being the binary state variable - $M$ being a sufficiently large constant (big-M) @@ -38,6 +39,7 @@ d(t+1) \geq d(t) + \Delta d(t) + (s(t+1) - 1) \cdot M \quad \forall t $$ With: + - $\Delta d(t)$ being the duration increment for time step $t$ (typically $\Delta t_i$ from the time series) - $M$ being a sufficiently large constant @@ -56,6 +58,7 @@ d(0) = (\Delta d(0) + d_\text{prev}) \cdot s(0) $$ With: + - $d_\text{prev}$ being the duration from before the optimization period - $\Delta d(0)$ being the duration increment for the first time step @@ -89,6 +92,7 @@ d(t) \geq (s(t-1) - s(t)) \cdot d_\text{min}(t-1) \quad \forall t > 0 $$ With: + - $d_\text{min}(t)$ being the required minimum duration at time $t$ **Behavior:** @@ -116,7 +120,7 @@ Ensuring equipment runs for a minimum duration once started: # State: 1 when running, 0 when off # Require at least 2 hours of operation duration = modeling.consecutive_duration_tracking( - state_variable=on_state, + state=on_state, duration_per_step=time_step_hours, minimum_duration=2.0 ) @@ -129,7 +133,7 @@ Tracking time since startup for gradual ramp-up constraints: ```python # Track startup duration startup_duration = modeling.consecutive_duration_tracking( - state_variable=on_state, + state=on_state, duration_per_step=time_step_hours ) # Constrain output based on startup duration @@ -143,7 +147,7 @@ Tracking time in a state before allowing transitions: ```python # Track maintenance duration maintenance_duration = modeling.consecutive_duration_tracking( - state_variable=maintenance_state, + state=maintenance_state, duration_per_step=time_step_hours, minimum_duration=scheduled_maintenance_hours ) @@ -154,6 +158,7 @@ maintenance_duration = modeling.consecutive_duration_tracking( ## Used In This pattern is used in: -- [`OnOffParameters`](../features/OnOffParameters.md) - Minimum on/off times + +- [`StatusParameters`](../features/StatusParameters.md) - Minimum active/inactive 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..ab347eb39 100644 --- a/docs/user-guide/mathematical-notation/modeling-patterns/index.md +++ b/docs/user-guide/mathematical-notation/modeling-patterns/index.md @@ -17,7 +17,7 @@ The modeling patterns are organized into three categories: These patterns define how optimization variables are constrained within bounds: - **Basic Bounds** - Simple upper and lower bounds on variables -- **Bounds with State** - Binary-controlled bounds (on/off states) +- **Bounds with State** - Binary-controlled bounds (active/inactive states) - **Scaled Bounds** - Bounds dependent on another variable (e.g., size) - **Scaled Bounds with State** - Combination of scaling and binary control @@ -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 +- [`StatusParameters`](../features/StatusParameters.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..cf6cfe736 100644 --- a/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md +++ b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md @@ -9,6 +9,7 @@ For a binary state variable $s(t) \in \{0, 1\}$, state transitions track when th ### Switch Variables Two binary variables track the transitions: + - $s^\text{on}(t) \in \{0, 1\}$: equals 1 when switching from off to on - $s^\text{off}(t) \in \{0, 1\}$: equals 1 when switching from on to off @@ -25,6 +26,7 @@ s^\text{on}(0) - s^\text{off}(0) = s(0) - s_\text{prev} $$ With: + - $s(t)$ being the binary state variable - $s_\text{prev}$ being the state before the optimization period - $s^\text{on}(t), s^\text{off}(t)$ being the switch variables @@ -45,8 +47,9 @@ s^\text{on}(t) + s^\text{off}(t) \leq 1 \quad \forall t $$ This ensures: + - At most one switch event per time step -- No simultaneous on/off switching +- No simultaneous active/inactive switching --- @@ -80,6 +83,7 @@ $$\label{eq:continuous_transition_initial} $$ With: + - $v(t)$ being the continuous variable - $v_\text{prev}$ being the value before the optimization period - $\Delta v^\text{max}$ being the maximum allowed change @@ -110,6 +114,7 @@ $$\label{eq:level_evolution} $$ With: + - $\ell(t)$ being the level variable - $\ell_\text{init}$ being the initial level - $\ell^\text{inc}(t)$ being the increase in level at time $t$ (non-negative) @@ -130,6 +135,7 @@ $$\label{eq:decrease_bound} $$ With: + - $\Delta \ell^\text{max}$ being the maximum change per time step - $b^\text{inc}(t), b^\text{dec}(t) \in \{0, 1\}$ being binary control variables @@ -144,6 +150,7 @@ b^\text{inc}(t) + b^\text{dec}(t) \leq 1 \quad \forall t $$ This ensures: + - Level can only increase OR decrease (or stay constant) in each time step - No simultaneous contradictory changes @@ -174,14 +181,14 @@ Track startup and shutdown events to apply costs: ```python # Create switch variables -switch_on, switch_off = modeling.state_transition_bounds( - state_variable=on_state, +startup, shutdown = modeling.state_transition_bounds( + state=on_state, previous_state=previous_on_state ) # Apply costs to switches -startup_cost = switch_on * startup_cost_per_event -shutdown_cost = switch_off * shutdown_cost_per_event +startup_cost = startup * startup_cost_per_event +shutdown_cost = shutdown * shutdown_cost_per_event ``` ### Limited Switching @@ -190,13 +197,13 @@ Restrict the number of state changes: ```python # Track all switches -switch_on, switch_off = modeling.state_transition_bounds( - state_variable=on_state +startup, shutdown = modeling.state_transition_bounds( + state=on_state ) # Limit total switches model.add_constraint( - (switch_on + switch_off).sum() <= max_switches + (startup + shutdown).sum() <= max_switches ) ``` @@ -221,7 +228,8 @@ model.add_constraint(increase.sum() <= max_total_expansion) ## Used In These patterns are used in: -- [`OnOffParameters`](../features/OnOffParameters.md) - Startup/shutdown tracking and costs + +- [`StatusParameters`](../features/StatusParameters.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/docs/user-guide/recipes/index.md b/docs/user-guide/recipes/index.md index 8ac7d1812..0317b2c70 100644 --- a/docs/user-guide/recipes/index.md +++ b/docs/user-guide/recipes/index.md @@ -28,7 +28,7 @@ Unlike full examples, recipes will be focused snippets showing a single concept. - **Data Manipulation** - Common xarray operations for parameterization and analysis - **Investment Optimization** - Size optimization strategies - **Renewable Integration** - Solar, wind capacity optimization -- **On/Off Constraints** - Minimum runtime, startup costs +- **Status Constraints** - Minimum runtime, startup costs - **Large-Scale Problems** - Segmented and aggregated calculations - **Custom Constraints** - Extend models with linopy - **Domain-Specific Patterns** - District heating, microgrids, industrial processes diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 2913f643f..b86c0e9de 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -47,12 +47,12 @@ # --- Define Components --- # 1. Define Boiler Component - # A gas boiler that converts fuel into thermal output, with investment and on-off parameters + # A gas boiler that converts fuel into thermal output, with investment and on-inactive parameters Gaskessel = fx.linear_converters.Boiler( 'Kessel', thermal_efficiency=0.5, # Efficiency ratio - on_off_parameters=fx.OnOffParameters( - effects_per_running_hour={Costs.label: 0, CO2.label: 1000} + status_parameters=fx.StatusParameters( + effects_per_active_hour={Costs.label: 0, CO2.label: 1000} ), # CO2 emissions per hour thermal_flow=fx.Flow( label='Q_th', # Thermal output @@ -69,14 +69,14 @@ relative_maximum=1, # Maximum part load previous_flow_rate=50, # Previous flow rate flow_hours_max=1e6, # Total energy flow limit - on_off_parameters=fx.OnOffParameters( - on_hours_min=0, # Minimum operating hours - on_hours_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_max=1000, # Max number of starts + status_parameters=fx.StatusParameters( + active_hours_min=0, # Minimum operating hours + active_hours_max=1000, # Maximum operating hours + max_uptime=10, # Max consecutive operating hours + min_uptime=np.array([1, 1, 1, 1, 1, 2, 2, 2, 2]), # min consecutive operation hours + max_downtime=10, # Max consecutive inactive hours + effects_per_startup={Costs.label: 0.01}, # Cost per startup + startup_limit=1000, # Max number of starts ), ), fuel_flow=fx.Flow(label='Q_fu', bus='Gas', size=200), @@ -88,7 +88,7 @@ 'BHKW2', thermal_efficiency=0.5, electrical_efficiency=0.4, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + status_parameters=fx.StatusParameters(effects_per_startup={Costs.label: 0.01}), electrical_flow=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60), thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=1e3), fuel_flow=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), + status_parameters=fx.StatusParameters(effects_per_startup={Costs.label: 0.01}), ) # 4. Define Storage Component diff --git a/examples/02_Complex/complex_example_results.py b/examples/02_Complex/complex_example_results.py index 7f1123a26..c4e9bb4f2 100644 --- a/examples/02_Complex/complex_example_results.py +++ b/examples/02_Complex/complex_example_results.py @@ -29,8 +29,8 @@ bus.plot_node_balance(show=False, save=f'results/{bus.label}--balance.html') # --- Plotting internal variables manually --- - results.plot_heatmap('BHKW2(Q_th)|on') - results.plot_heatmap('Kessel(Q_th)|on') + results.plot_heatmap('BHKW2(Q_th)|status') + results.plot_heatmap('Kessel(Q_th)|status') # Dataframes from results: fw_bus = results['Fernwärme'].node_balance().to_dataframe() diff --git a/examples/03_Optimization_modes/example_optimization_modes.py b/examples/03_Optimization_modes/example_optimization_modes.py index d3ae566e4..009c008d9 100644 --- a/examples/03_Optimization_modes/example_optimization_modes.py +++ b/examples/03_Optimization_modes/example_optimization_modes.py @@ -91,7 +91,7 @@ def get_solutions(optimizations: list, variable: str) -> xr.Dataset: size=95, relative_minimum=12 / 95, previous_flow_rate=20, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=1000), + status_parameters=fx.StatusParameters(effects_per_startup=1000), ), ) @@ -100,7 +100,7 @@ def get_solutions(optimizations: list, variable: str) -> xr.Dataset: 'BHKW2', thermal_efficiency=0.58, electrical_efficiency=0.22, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=24000), + status_parameters=fx.StatusParameters(effects_per_startup=24000), electrical_flow=fx.Flow('P_el', bus='Strom', size=200), thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=200), fuel_flow=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 6ae01c4f0..672df5c7f 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -123,7 +123,7 @@ size=50, relative_minimum=0.1, relative_maximum=1, - on_off_parameters=fx.OnOffParameters(), + status_parameters=fx.StatusParameters(), ), fuel_flow=fx.Flow(label='Q_fu', bus='Gas'), ) @@ -135,7 +135,7 @@ thermal_efficiency=0.48, # Realistic thermal efficiency (48%) electrical_efficiency=0.40, # Realistic electrical efficiency (40%) electrical_flow=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, status_parameters=fx.StatusParameters() ), thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), fuel_flow=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 d8f4e87fe..9e102c44f 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -57,16 +57,14 @@ ), relative_minimum=0.2, previous_flow_rate=20, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), + status_parameters=fx.StatusParameters(effects_per_startup=300), ), ), fx.linear_converters.CHP( 'BHKW2', thermal_efficiency=0.58, electrical_efficiency=0.22, - on_off_parameters=fx.OnOffParameters( - effects_per_switch_on=1_000, consecutive_on_hours_min=10, consecutive_off_hours_min=10 - ), + status_parameters=fx.StatusParameters(effects_per_startup=1_000, min_uptime=10, min_downtime=10), electrical_flow=fx.Flow('P_el', bus='Strom'), thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), fuel_flow=fx.Flow( diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 3941cb491..0f8fc73e2 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -31,7 +31,7 @@ from .effects import PENALTY_EFFECT_LABEL, Effect from .elements import Bus, Flow from .flow_system import FlowSystem -from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects +from .interface import InvestParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects, StatusParameters # Import new Optimization classes from .optimization import ClusteredOptimization, Optimization, SegmentedOptimization @@ -60,7 +60,7 @@ 'AggregatedCalculation', 'SegmentedCalculation', 'InvestParameters', - 'OnOffParameters', + 'StatusParameters', 'Piece', 'Piecewise', 'PiecewiseConversion', diff --git a/flixopt/components.py b/flixopt/components.py index 07bc5f204..2d04586cc 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -14,7 +14,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 InvestParameters, PiecewiseConversion, StatusParameters 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 + status_parameters: Information about active and inactive state of LinearConverter. + Component is active/inactive if all connected Flows are active/inactive. This induces a + status variable (binary) in all Flows! If possible, use StatusParameters 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, + status_parameters: StatusParameters | 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, status_parameters, meta_data=meta_data) self.conversion_factors = conversion_factors or [] self.piecewise_conversion = piecewise_conversion @@ -573,8 +573,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. + status_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. @@ -629,7 +629,7 @@ class Transmission(Component): ) ``` - Material conveyor with on/off operation: + Material conveyor with active/inactive status: ```python conveyor_belt = Transmission( @@ -637,10 +637,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_max=10, # Maximum 10 starts per day + status_parameters=StatusParameters( + effects_per_startup={'maintenance': 0.1}, + min_uptime=2, # Minimum 2-hour operation + startup_limit=10, # Maximum 10 starts per period ), ) ``` @@ -654,7 +654,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 on/inactive variables, which increases computational complexity but enables realistic modeling of equipment with standby power consumption. @@ -671,7 +671,7 @@ def __init__( out2: Flow | None = None, relative_losses: Numeric_TPS | None = None, absolute_losses: Numeric_TPS | None = None, - on_off_parameters: OnOffParameters = None, + status_parameters: StatusParameters | None = None, prevent_simultaneous_flows_in_both_directions: bool = True, balanced: bool = False, meta_data: dict | None = None, @@ -680,7 +680,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, + status_parameters=status_parameters, prevent_simultaneous_flows=None if in2 is None or prevent_simultaneous_flows_in_both_directions is False else [in1, in2], @@ -739,8 +739,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.status_parameters is None: + flow.status_parameters = StatusParameters() super().__init__(model, element) @@ -772,8 +772,8 @@ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) short_name=name, ) - if self.element.absolute_losses is not None: - con_transmission.lhs += in_flow.submodel.on_off.on * self.element.absolute_losses + if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses != 0): + con_transmission.lhs += in_flow.submodel.status.status * self.element.absolute_losses return con_transmission @@ -807,7 +807,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 StatusParameters. 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() @@ -819,7 +819,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.status.status if self.status is not None else False, dims=('time', 'period', 'scenario'), ), short_name='PiecewiseConversion', @@ -978,7 +978,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 17730bc98..f12dae4c4 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -14,8 +14,8 @@ from . import io as fx_io from .config import CONFIG, DEPRECATION_REMOVAL_VERSION from .core import PlausibilityError -from .features import InvestmentModel, OnOffModel -from .interface import InvestParameters, OnOffParameters +from .features import InvestmentModel, StatusModel +from .interface import InvestParameters, StatusParameters from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilitiesAbstract from .structure import ( Element, @@ -58,9 +58,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 + status_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 StatusParameters 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 @@ -70,13 +70,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 + - status_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 StatusParameters Component is an abstract base class. In practice, use specialized subclasses: - LinearConverter: Linear input/output relationships @@ -91,14 +91,14 @@ def __init__( label: str, inputs: list[Flow] | None = None, outputs: list[Flow] | None = None, - on_off_parameters: OnOffParameters | None = None, + status_parameters: StatusParameters | 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.status_parameters = status_parameters self.prevent_simultaneous_flows: list[Flow] = prevent_simultaneous_flows or [] self._check_unique_flow_labels() @@ -114,15 +114,15 @@ def create_model(self, model: FlowSystemModel) -> ComponentModel: def _set_flow_system(self, flow_system) -> None: """Propagate flow_system reference to nested Interface objects and flows.""" super()._set_flow_system(flow_system) - if self.on_off_parameters is not None: - self.on_off_parameters._set_flow_system(flow_system) + if self.status_parameters is not None: + self.status_parameters._set_flow_system(flow_system) for flow in self.inputs + self.outputs: flow._set_flow_system(flow_system) def transform_data(self, 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(prefix) + if self.status_parameters is not None: + self.status_parameters.transform_data(prefix) for flow in self.inputs + self.outputs: flow.transform_data() # Flow doesnt need the name_prefix @@ -314,7 +314,7 @@ class Flow(Element): between a Bus and a Component in a specific direction. The flow rate is the primary optimization variable, with constraints and costs defined through various parameters. Flows can have fixed or variable sizes, operational - constraints, and complex on/off behavior. + constraints, and complex on/inactive behavior. Key Concepts: **Flow Rate**: The instantaneous rate of energy/material transfer (optimization variable) [kW, m³/h, kg/h] @@ -324,7 +324,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 + - **StatusParameters**: Used for `status_parameters` when flow has discrete states Mathematical Formulation: See the complete mathematical model in the documentation: @@ -340,7 +340,7 @@ 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. + status_parameters: Binary operation constraints (StatusParameters). Default: None. flow_hours_max: Maximum cumulative flow-hours per period. Alternative to load_factor_max. flow_hours_min: Minimum cumulative flow-hours per period. Alternative to load_factor_min. flow_hours_max_over_periods: Maximum weighted sum of flow-hours across ALL periods. @@ -349,7 +349,7 @@ class Flow(Element): Weighted by FlowSystem period weights. 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 status at model start. Default: None (inactive). meta_data: Additional info stored in results. Python native types only. Examples: @@ -386,13 +386,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_max=200, # Maximum 200 starts per period + status_parameters=StatusParameters( + effects_per_startup={'startup_cost': 100, 'wear': 0.1}, + min_uptime=2, # Must run at least 2 hours + min_downtime=1, # Must stay inactive at least 1 hour + startup_limit=200, # Maximum 200 starts per period ), ) ``` @@ -428,7 +428,7 @@ class Flow(Element): limits across all periods. **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 `status_parameters` for discrete active/inactive behavior. **Fixed Profiles**: Use `fixed_relative_profile` for known exact patterns, `relative_maximum` for upper bounds on optimization variables. @@ -454,7 +454,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, + status_parameters: StatusParameters | None = None, flow_hours_max: Numeric_PS | None = None, flow_hours_min: Numeric_PS | None = None, flow_hours_max_over_periods: Numeric_S | None = None, @@ -479,7 +479,7 @@ def __init__( self.flow_hours_min = flow_hours_min self.flow_hours_max_over_periods = flow_hours_max_over_periods self.flow_hours_min_over_periods = flow_hours_min_over_periods - self.on_off_parameters = on_off_parameters + self.status_parameters = status_parameters self.previous_flow_rate = previous_flow_rate @@ -507,8 +507,8 @@ def create_model(self, model: FlowSystemModel) -> FlowModel: def _set_flow_system(self, flow_system) -> None: """Propagate flow_system reference to nested Interface objects.""" super()._set_flow_system(flow_system) - if self.on_off_parameters is not None: - self.on_off_parameters._set_flow_system(flow_system) + if self.status_parameters is not None: + self.status_parameters._set_flow_system(flow_system) if isinstance(self.size, Interface): self.size._set_flow_system(flow_system) @@ -537,8 +537,8 @@ def transform_data(self, 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(prefix) + if self.status_parameters is not None: + self.status_parameters.transform_data(prefix) if isinstance(self.size, InvestParameters): self.size.transform_data(prefix) else: @@ -558,17 +558,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.status_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 status_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.status_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 status_parameters. ' + f'This prevents the Flow from switching inactive (flow_rate = 0). ' + f'Consider using status_parameters to allow the Flow to be switched active and inactive.' ) if self.previous_flow_rate is not None: @@ -666,18 +666,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_status_model(self): + status = self.add_variables(binary=True, short_name='status', coords=self._model.get_coords()) self.add_submodels( - OnOffModel( + StatusModel( model=self._model, label_of_element=self.label_of_element, - parameters=self.element.on_off_parameters, - on_variable=on, - previous_states=self.previous_states, + parameters=self.element.status_parameters, + status=status, + previous_status=self.previous_status, label_of_model=self.label_of_element, ), - short_name='on_off', + short_name='status', ) def _create_investment_model(self): @@ -693,23 +693,23 @@ def _create_investment_model(self): def _constraint_flow_rate(self): """Create bounding constraints for flow_rate (models already created in _create_variables)""" - if not self.with_investment and not self.with_on_off: + if not self.with_investment and not self.with_status: # 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_status and not self.with_investment: + # Status, but no Investment + self._create_status_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, + state=self.status.status, ) - elif self.with_investment and not self.with_on_off: - # Investment, but no OnOff + elif self.with_investment and not self.with_status: + # Investment, but no Status self._create_investment_model() BoundingPatterns.scaled_bounds( self, @@ -718,10 +718,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_status: + # Investment and Status self._create_investment_model() - self._create_on_off_model() + self._create_status_model() BoundingPatterns.scaled_bounds_with_state( model=self, @@ -729,14 +729,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, + state=self.status.status, ) else: raise Exception('Not valid') @property - def with_on_off(self) -> bool: - return self.element.on_off_parameters is not None + def with_status(self) -> bool: + return self.element.status_parameters is not None @property def with_investment(self) -> bool: @@ -809,9 +809,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_status: if not self.with_investment: - # Basic case without investment and without OnOff + # Basic case without investment and without Status lb = lb_relative * self.element.size elif self.with_investment and self.element.size.mandatory: # With mandatory Investment @@ -825,11 +825,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 status(self) -> StatusModel | None: + """Status feature""" + if 'status' not in self.submodels: return None - return self.submodels['on_off'] + return self.submodels['status'] @property def _investment(self) -> InvestmentModel | None: @@ -838,14 +838,14 @@ 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'] @property - def previous_states(self) -> xr.DataArray | None: - """Previous states of the flow rate""" + def previous_status(self) -> xr.DataArray | None: + """Previous status of the flow rate""" # TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well. previous_flow_rate = self.element.previous_flow_rate if previous_flow_rate is None: @@ -923,7 +923,7 @@ class ComponentModel(ElementModel): element: Component # Type hint def __init__(self, model: FlowSystemModel, element: Component): - self.on_off: OnOffModel | None = None + self.status: StatusModel | None = None super().__init__(model, element) def _do_modeling(self): @@ -932,51 +932,52 @@ def _do_modeling(self): all_flows = self.element.inputs + self.element.outputs - # Set on_off_parameters on flows if needed - if self.element.on_off_parameters: + # Set status_parameters on flows if needed + if self.element.status_parameters: for flow in all_flows: - if flow.on_off_parameters is None: - flow.on_off_parameters = OnOffParameters() + if flow.status_parameters is None: + flow.status_parameters = StatusParameters() 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.status_parameters is None: + flow.status_parameters = StatusParameters() # Create FlowModels (which creates their variables and constraints) for flow in all_flows: self.add_submodels(flow.create_model(self._model), short_name=flow.label) - # Create component on variable and OnOffModel if needed - if self.element.on_off_parameters: - on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords()) + # Create component status variable and StatusModel if needed + if self.element.status_parameters: + status = self.add_variables(binary=True, short_name='status', 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(status == all_flows[0].submodel.status.status, short_name='status') else: - flow_ons = [flow.submodel.on_off.on for flow in all_flows] + flow_statuses = [flow.submodel.status.status 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(status <= sum(flow_statuses) + CONFIG.Modeling.epsilon, short_name='status|ub') self.add_constraints( - on >= sum(flow_ons) / (len(flow_ons) + CONFIG.Modeling.epsilon), short_name='on|lb' + status >= sum(flow_statuses) / (len(flow_statuses) + CONFIG.Modeling.epsilon), + short_name='status|lb', ) - self.on_off = self.add_submodels( - OnOffModel( + self.status = self.add_submodels( + StatusModel( model=self._model, label_of_element=self.label_of_element, - parameters=self.element.on_off_parameters, - on_variable=on, + parameters=self.element.status_parameters, + status=status, label_of_model=self.label_of_element, - previous_states=self.previous_states, + previous_status=self.previous_status, ), - short_name='on_off', + short_name='status', ) if self.element.prevent_simultaneous_flows: # Simultanious Useage --> Only One FLow is On 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.status.status for flow in self.element.prevent_simultaneous_flows], short_name='prevent_simultaneous_use', ) @@ -989,21 +990,21 @@ 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') + def previous_status(self) -> xr.DataArray | None: + """Previous status of the component, derived from its flows""" + if self.element.status_parameters is None: + raise ValueError(f'StatusModel not present in \n{self}\nCant access previous_status') - previous_states = [flow.submodel.on_off._previous_states for flow in self.element.inputs + self.element.outputs] - previous_states = [da for da in previous_states if da is not None] + previous_status = [flow.submodel.status._previous_status for flow in self.element.inputs + self.element.outputs] + previous_status = [da for da in previous_status if da is not None] - if not previous_states: # Empty list + if not previous_status: # Empty list return None - max_len = max(da.sizes['time'] for da in previous_states) + max_len = max(da.sizes['time'] for da in previous_status) - padded_previous_states = [ + padded_previous_status = [ da.assign_coords(time=range(-da.sizes['time'], 0)).reindex(time=range(-max_len, 0), fill_value=0) - for da in previous_states + for da in previous_status ] - return xr.concat(padded_previous_states, dim='flow').any(dim='flow').astype(int) + return xr.concat(padded_previous_status, dim='flow').any(dim='flow').astype(int) diff --git a/flixopt/features.py b/flixopt/features.py index 8c4bf7c70..f4bfdab0f 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -16,15 +16,17 @@ if TYPE_CHECKING: from collections.abc import Collection + import xarray as xr + from .core import FlowSystemDimensions - from .interface import InvestParameters, OnOffParameters, Piecewise + from .interface import InvestParameters, Piecewise, StatusParameters from .types import Numeric_PS, Numeric_TPS class InvestmentModel(Submodel): """ This feature model is used to model the investment of a variable. - It applies the corresponding bounds to the variable and the on/off state of the variable. + It applies the corresponding bounds to the variable and the active/inactive state of the variable. Args: model: The optimization model instance @@ -75,7 +77,7 @@ def _create_variables_and_constraints(self): BoundingPatterns.bounds_with_state( self, variable=self.size, - variable_state=self._variables['invested'], + state=self._variables['invested'], bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) @@ -144,32 +146,33 @@ def invested(self) -> linopy.Variable | None: return self._variables['invested'] -class OnOffModel(Submodel): - """OnOff model using factory patterns""" +class StatusModel(Submodel): + """Status model for equipment with binary active/inactive states""" def __init__( self, model: FlowSystemModel, label_of_element: str, - parameters: OnOffParameters, - on_variable: linopy.Variable, - previous_states: Numeric_TPS | None, + parameters: StatusParameters, + status: linopy.Variable, + previous_status: xr.DataArray | 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 status (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 - previous_states: The previous flow_rates + status: The variable that determines the active state + previous_status: 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._previous_states = previous_states + self.status = status + self._previous_status = previous_status self.parameters = parameters super().__init__(model, label_of_element, label_of_model=label_of_model) @@ -177,92 +180,95 @@ def _do_modeling(self): """Create variables, constraints, and nested submodels""" 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') + # Create a separate binary 'inactive' variable when needed for downtime tracking or explicit use + # When not needed, the expression (1 - self.status) can be used instead + if self.parameters.use_downtime_tracking: + inactive = self.add_variables(binary=True, short_name='inactive', coords=self._model.get_coords()) + self.add_constraints(self.status + 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.status * self._model.hours_per_step).sum('time'), bounds=( - self.parameters.on_hours_min if self.parameters.on_hours_min is not None else 0, - self.parameters.on_hours_max if self.parameters.on_hours_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_min if self.parameters.active_hours_min is not None else 0, + self.parameters.active_hours_max + if self.parameters.active_hours_max is not None + else self._model.hours_per_step.sum('time').max().item(), + ), + short_name='active_hours', 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_tracking: + 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=self.status, + activate=self.startup, + deactivate=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, + previous_state=self._previous_status.isel(time=-1) if self._previous_status is not None else 0, coord='time', ) - if self.parameters.switch_on_max is not None: + if self.parameters.startup_limit is not None: count = self.add_variables( lower=0, - upper=self.parameters.switch_on_max, + upper=self.parameters.startup_limit, 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 (uptime) using existing pattern + if self.parameters.use_uptime_tracking: 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=self.status, + short_name='uptime', + minimum_duration=self.parameters.min_uptime, + maximum_duration=self.parameters.max_uptime, duration_per_step=self.hours_per_step, duration_dim='time', - previous_duration=self._get_previous_on_duration(), + previous_duration=self._get_previous_uptime(), ) - # 6. Consecutive off duration using existing pattern - if self.parameters.use_consecutive_off_hours: + # 6. Consecutive inactive duration (downtime) using existing pattern + if self.parameters.use_downtime_tracking: 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=self.inactive, + short_name='downtime', + minimum_duration=self.parameters.min_downtime, + maximum_duration=self.parameters.max_downtime, duration_per_step=self.hours_per_step, duration_dim='time', - previous_duration=self._get_previous_off_duration(), + previous_duration=self._get_previous_downtime(), ) - # TODO: self._add_effects() def _add_effects(self): """Add operational effects""" - if self.parameters.effects_per_running_hour: + if self.parameters.effects_per_active_hour: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ - effect: self.on * factor * self._model.hours_per_step - for effect, factor in self.parameters.effects_per_running_hour.items() + effect: self.status * factor * self._model.hours_per_step + for effect, factor in self.parameters.effects_per_active_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', ) @@ -270,55 +276,55 @@ 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(self) -> linopy.Variable: + """Total active hours variable""" + return self['active_hours'] @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 (deprecated - use 1 - status expression instead)""" + 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_count(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 uptime(self) -> linopy.Variable | None: + """Consecutive active hours (uptime) variable""" + return self.get('uptime') @property - def consecutive_off_hours(self) -> linopy.Variable | None: - """Consecutive off hours variable""" - return self.get('consecutive_off_hours') + def downtime(self) -> linopy.Variable | None: + """Consecutive inactive hours (downtime) variable""" + return self.get('downtime') - def _get_previous_on_duration(self): - """Get previous on duration. Previously OFF by default, for one timestep""" + def _get_previous_uptime(self): + """Get previous uptime (consecutive active hours). 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: + if self._previous_status is None: return 0 else: - return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states, hours_per_step) + return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_status, hours_per_step) - def _get_previous_off_duration(self): - """Get previous off duration. Previously OFF by default, for one timestep""" + def _get_previous_downtime(self): + """Get previous downtime (consecutive inactive hours). 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: + if self._previous_status is None: return hours_per_step else: - return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states * -1 + 1, hours_per_step) + return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_status * -1 + 1, hours_per_step) class PieceModel(Submodel): diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 52c403396..c0deaa1ca 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -52,7 +52,7 @@ class FlowSystem(Interface, CompositeContainerMixin[Element]): hours_of_last_timestep: Duration of the last timestep. If None, computed from the last time interval. hours_of_previous_timesteps: Duration of previous timesteps. If None, computed from the first time interval. Can be a scalar (all previous timesteps have same duration) or array (different durations). - Used to calculate previous values (e.g., consecutive_on_hours). + Used to calculate previous values (e.g., uptime and downtime). weight_of_last_period: Weight/duration of the last period. If None, computed from the last period interval. Used for calculating sums over periods in multi-period models. scenario_weights: The weights of each scenario. If None, all scenarios have the same weight (normalized to 1). @@ -76,7 +76,7 @@ class FlowSystem(Interface, CompositeContainerMixin[Element]): >>> flow_system = fx.FlowSystem(timesteps) >>> >>> # Add elements to the system - >>> boiler = fx.Component('Boiler', inputs=[heat_flow], on_off_parameters=...) + >>> boiler = fx.Component('Boiler', inputs=[heat_flow], status_parameters=...) >>> heat_bus = fx.Bus('Heat', excess_penalty_per_flow_hour=1e4) >>> costs = fx.Effect('costs', is_objective=True, is_standard=True) >>> flow_system.add_elements(boiler, heat_bus, costs) diff --git a/flixopt/interface.py b/flixopt/interface.py index cfa210f6d..30db4876f 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -1,5 +1,5 @@ """ -This module contains classes to collect Parameters for the Investment and OnOff decisions. +This module contains classes to collect Parameters for the Investment and Status decisions. These are tightly connected to features.py """ @@ -413,7 +413,7 @@ class PiecewiseConversion(Interface): operate in certain ranges (e.g., minimum loads, unstable regions). **Discrete Modes**: Use pieces with identical start/end values to model - equipment with fixed operating points (e.g., on/off, discrete speeds). + equipment with fixed operating points (e.g., on/inactive, discrete speeds). **Efficiency Changes**: Coordinate input and output pieces to reflect changing conversion efficiency across operating ranges. @@ -1006,19 +1006,19 @@ 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 StatusParameters(Interface): + """Define operational constraints and effects for binary status 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. Key Modeling Capabilities: - **Switching Costs**: One-time costs for starting equipment (fuel, wear, labor) - **Runtime Constraints**: Minimum and maximum continuous operation periods - **Cycling Limits**: Maximum number of starts to prevent excessive wear - **Operating Hours**: Total runtime limits and requirements over time horizon + **Startup Costs**: One-time costs for starting equipment (fuel, wear, labor) + **Runtime Constraints**: Minimum and maximum continuous operation periods (uptime/downtime) + **Cycling Limits**: Maximum number of startups to prevent excessive wear + **Operating Hours**: Total active hours limits and requirements over time horizon Typical Equipment Applications: - **Power Plants**: Combined cycle units, steam turbines with startup costs @@ -1029,45 +1029,45 @@ class OnOffParameters(Interface): Mathematical Formulation: See the complete mathematical model in the documentation: - [OnOffParameters](../user-guide/mathematical-notation/features/OnOffParameters.md) + [StatusParameters](../user-guide/mathematical-notation/features/StatusParameters.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 (status=0) to active state (status=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. + effects_per_active_hour: Ongoing costs or impacts while equipment operates + 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_min: Minimum total operating hours per period. + active_hours_min: Minimum total active hours across the entire time horizon per period. Ensures equipment meets minimum utilization requirements or contractual obligations (e.g., power purchase agreements, maintenance schedules). - on_hours_max: Maximum total operating hours per period. + active_hours_max: Maximum total active hours across the entire time horizon per period. 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. + min_uptime: Minimum continuous operating duration once started (unit commitment term). 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. + max_uptime: Maximum continuous operating duration in one campaign (unit commitment term). Models mandatory maintenance intervals, process batch sizes, or equipment thermal limits requiring periodic shutdowns. - consecutive_off_hours_min: Minimum continuous shutdown duration between operations. + min_downtime: Minimum continuous shutdown duration between operations (unit commitment term). Models cooling periods, maintenance requirements, or process constraints that prevent immediate restart after shutdown. - consecutive_off_hours_max: Maximum continuous shutdown duration before mandatory + max_downtime: Maximum continuous shutdown duration before mandatory restart. Models equipment preservation, process stability, or contractual requirements for minimum activity levels. - switch_on_max: Maximum number of startup operations per period. + startup_limit: Maximum number of startup operations across the time horizon per period.. 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_max constraint. Useful for tracking or reporting startup + force_startup_tracking: When True, creates startup variables even without explicit + startup_limit 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 + min_uptime/max_uptime and min_downtime/max_downtime are not enforced, allowing the optimization to end with ongoing campaigns that may be shorter than the specified minimums or longer than maximums. @@ -1075,105 +1075,105 @@ 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 = StatusParameters( + 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 'maintenance_impact': 0.1, # Fractional life consumption }, - effects_per_running_hour={ - 'fixed_om': 125, # Fixed O&M costs while running + effects_per_active_hour={ + 'fixed_om': 125, # Fixed O&M costs while active '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_max=6000, # Annual operating limit + min_uptime=8, # Minimum 8-hour run once started + min_downtime=4, # Minimum 4-hour cooling period + active_hours_max=6000, # Annual operating limit ) ``` Industrial batch process with cycling limits: ```python - batch_reactor = OnOffParameters( - effects_per_switch_on={ + batch_reactor = StatusParameters( + effects_per_startup={ 'setup_cost': 1500, # Labor and materials for startup 'catalyst_consumption': 5, # kg catalyst per batch 'cleaning_chemicals': 200, # L cleaning solution }, - effects_per_running_hour={ + effects_per_active_hour={ 'steam': 2.5, # t/h process steam '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_max=200, # Maximum 200 batches per period - on_hours_max=4000, # Maximum production time + min_uptime=12, # Minimum batch size (12 hours) + max_uptime=24, # Maximum batch size (24 hours) + min_downtime=6, # Cleaning and setup time + startup_limit=200, # Maximum 200 batches per period + active_hours_max=4000, # Maximum production time ) ``` HVAC system with thermostat control and maintenance: ```python - hvac_operation = OnOffParameters( - effects_per_switch_on={ + hvac_operation = StatusParameters( + effects_per_startup={ 'compressor_wear': 0.5, # Hours of compressor life per start 'inrush_current': 15, # kW peak demand on startup }, - effects_per_running_hour={ + effects_per_active_hour={ '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_max=2000, # Limit cycling for compressor life - on_hours_min=2000, # Minimum operation for humidity control - on_hours_max=5000, # Maximum operation for energy budget + min_uptime=1, # Minimum 1-hour run to avoid cycling + min_downtime=0.5, # 30-minute minimum inactive time + startup_limit=2000, # Limit cycling for compressor life + active_hours_min=2000, # Minimum operation for humidity control + active_hours_max=5000, # Maximum operation for energy budget ) ``` Backup generator with testing and maintenance requirements: ```python - backup_generator = OnOffParameters( - effects_per_switch_on={ + backup_generator = StatusParameters( + 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 }, - effects_per_running_hour={ + effects_per_active_hour={ 'fuel_consumption': 180, # L/h diesel consumption '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_max=52, # Weekly testing limit - on_hours_min=26, # Minimum annual testing (0.5h × 52) - on_hours_max=200, # Maximum runtime (emergencies + tests) + min_uptime=0.5, # Minimum test duration (30 min) + max_downtime=720, # Maximum 30 days between tests + startup_limit=52, # Weekly testing limit + active_hours_min=26, # Minimum annual testing (0.5h × 52) + active_hours_max=200, # Maximum runtime (emergencies + tests) ) ``` Peak shaving battery with cycling degradation: ```python - battery_cycling = OnOffParameters( - effects_per_switch_on={ + battery_cycling = StatusParameters( + effects_per_startup={ 'cycle_degradation': 0.01, # % capacity loss per cycle 'inverter_startup': 0.5, # kWh losses during startup }, - effects_per_running_hour={ + effects_per_active_hour={ 'standby_losses': 2, # kW standby consumption '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_max=365, # Daily cycling limit - force_switch_on=True, # Track all cycling events + min_uptime=1, # Minimum discharge duration + max_uptime=4, # Maximum continuous discharge + min_downtime=1, # Minimum rest between cycles + startup_limit=365, # Daily cycling limit + force_startup_tracking=True, # Track all cycling events ) ``` @@ -1189,86 +1189,73 @@ class OnOffParameters(Interface): def __init__( self, - effects_per_switch_on: Effect_TPS | Numeric_TPS | None = None, - effects_per_running_hour: Effect_TPS | Numeric_TPS | None = None, - on_hours_min: Numeric_PS | None = None, - on_hours_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_max: Numeric_PS | None = None, - force_switch_on: bool = False, + effects_per_startup: Effect_TPS | Numeric_TPS | None = None, + effects_per_active_hour: Effect_TPS | Numeric_TPS | None = None, + active_hours_min: Numeric_PS | None = None, + active_hours_max: Numeric_PS | None = None, + min_uptime: Numeric_TPS | None = None, + max_uptime: Numeric_TPS | None = None, + min_downtime: Numeric_TPS | None = None, + max_downtime: Numeric_TPS | None = None, + startup_limit: Numeric_PS | None = None, + force_startup_tracking: bool = False, ): - self.effects_per_switch_on = effects_per_switch_on if effects_per_switch_on 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_min = on_hours_min - self.on_hours_max = on_hours_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_max = switch_on_max - self.force_switch_on: bool = force_switch_on + self.effects_per_startup = effects_per_startup if effects_per_startup is not None else {} + self.effects_per_active_hour = effects_per_active_hour if effects_per_active_hour is not None else {} + self.active_hours_min = active_hours_min + self.active_hours_max = active_hours_max + self.min_uptime = min_uptime + self.max_uptime = max_uptime + self.min_downtime = min_downtime + self.max_downtime = max_downtime + self.startup_limit = startup_limit + self.force_startup_tracking: bool = force_startup_tracking def transform_data(self, name_prefix: str = '') -> None: - self.effects_per_switch_on = self._fit_effect_coords( + self.effects_per_startup = self._fit_effect_coords( prefix=name_prefix, - effect_values=self.effects_per_switch_on, - suffix='per_switch_on', + effect_values=self.effects_per_startup, + suffix='per_startup', ) - self.effects_per_running_hour = self._fit_effect_coords( + self.effects_per_active_hour = self._fit_effect_coords( prefix=name_prefix, - effect_values=self.effects_per_running_hour, - suffix='per_running_hour', + effect_values=self.effects_per_active_hour, + suffix='per_active_hour', ) - self.consecutive_on_hours_min = self._fit_coords( - f'{name_prefix}|consecutive_on_hours_min', self.consecutive_on_hours_min + self.min_uptime = self._fit_coords(f'{name_prefix}|min_uptime', self.min_uptime) + self.max_uptime = self._fit_coords(f'{name_prefix}|max_uptime', self.max_uptime) + self.min_downtime = self._fit_coords(f'{name_prefix}|min_downtime', self.min_downtime) + self.max_downtime = self._fit_coords(f'{name_prefix}|max_downtime', self.max_downtime) + self.active_hours_max = self._fit_coords( + f'{name_prefix}|active_hours_max', self.active_hours_max, dims=['period', 'scenario'] ) - self.consecutive_on_hours_max = self._fit_coords( - f'{name_prefix}|consecutive_on_hours_max', self.consecutive_on_hours_max + self.active_hours_min = self._fit_coords( + f'{name_prefix}|active_hours_min', self.active_hours_min, dims=['period', 'scenario'] ) - self.consecutive_off_hours_min = self._fit_coords( - f'{name_prefix}|consecutive_off_hours_min', self.consecutive_off_hours_min + self.startup_limit = self._fit_coords( + f'{name_prefix}|startup_limit', self.startup_limit, dims=['period', 'scenario'] ) - self.consecutive_off_hours_max = self._fit_coords( - f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max - ) - self.on_hours_max = self._fit_coords( - f'{name_prefix}|on_hours_max', self.on_hours_max, dims=['period', 'scenario'] - ) - self.on_hours_min = self._fit_coords( - f'{name_prefix}|on_hours_min', self.on_hours_min, dims=['period', 'scenario'] - ) - self.switch_on_max = self._fit_coords( - f'{name_prefix}|switch_on_max', self.switch_on_max, dims=['period', 'scenario'] - ) - - @property - def use_off(self) -> bool: - """Proxy: whether OFF variable is required""" - return self.use_consecutive_off_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_uptime_tracking(self) -> bool: + """Determines whether a Variable for uptime (consecutive active hours) is needed or not""" + return any(param is not None for param in [self.min_uptime, self.max_uptime]) @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_downtime_tracking(self) -> bool: + """Determines whether a Variable for downtime (consecutive inactive hours) is needed or not""" + return any(param is not None for param in [self.min_downtime, self.max_downtime]) @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_tracking(self) -> bool: + """Determines whether a variable for startup is needed or not""" + if self.force_startup_tracking: return True return any( self._has_value(param) for param in [ - self.effects_per_switch_on, - self.switch_on_max, + self.effects_per_startup, + self.startup_limit, ] ) diff --git a/flixopt/linear_converters.py b/flixopt/linear_converters.py index 9ca73519e..8326fe6c5 100644 --- a/flixopt/linear_converters.py +++ b/flixopt/linear_converters.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from .elements import Flow - from .interface import OnOffParameters + from .interface import StatusParameters from .types import Numeric_TPS logger = logging.getLogger('flixopt') @@ -35,7 +35,7 @@ class Boiler(LinearConverter): output to fuel input energy content. fuel_flow: Fuel input-flow representing fuel consumption. thermal_flow: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + status_parameters: Parameters defining status, startup and shutdown constraints and effects meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -59,9 +59,9 @@ class Boiler(LinearConverter): thermal_efficiency=seasonal_efficiency_profile, # Time-varying efficiency fuel_flow=biomass_flow, thermal_flow=district_heat_flow, - on_off_parameters=OnOffParameters( - consecutive_on_hours_min=4, # Minimum 4-hour operation - effects_per_switch_on={'startup_fuel': 50}, # Startup fuel penalty + status_parameters=StatusParameters( + min_uptime=4, # Minimum 4-hour operation + effects_per_startup={'startup_fuel': 50}, # Startup fuel penalty ), ) ``` @@ -79,7 +79,7 @@ def __init__( thermal_efficiency: Numeric_TPS | None = None, fuel_flow: Flow | None = None, thermal_flow: Flow | None = None, - on_off_parameters: OnOffParameters | None = None, + status_parameters: StatusParameters | None = None, meta_data: dict | None = None, ): # Validate required parameters @@ -94,7 +94,7 @@ def __init__( label, inputs=[fuel_flow], outputs=[thermal_flow], - on_off_parameters=on_off_parameters, + status_parameters=status_parameters, meta_data=meta_data, ) self.fuel_flow = fuel_flow @@ -128,7 +128,7 @@ class Power2Heat(LinearConverter): electrode boilers or systems with distribution losses. electrical_flow: Electrical input-flow representing electricity consumption. thermal_flow: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + status_parameters: Parameters defining status, startup and shutdown constraints and effects meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -152,9 +152,9 @@ class Power2Heat(LinearConverter): thermal_efficiency=0.95, # 95% efficiency including boiler losses electrical_flow=industrial_electricity, thermal_flow=process_steam_flow, - on_off_parameters=OnOffParameters( - consecutive_on_hours_min=1, # Minimum 1-hour operation - effects_per_switch_on={'startup_cost': 100}, + status_parameters=StatusParameters( + min_uptime=1, # Minimum 1-hour operation + effects_per_startup={'startup_cost': 100}, ), ) ``` @@ -174,7 +174,7 @@ def __init__( thermal_efficiency: Numeric_TPS | None = None, electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, - on_off_parameters: OnOffParameters | None = None, + status_parameters: StatusParameters | None = None, meta_data: dict | None = None, ): # Validate required parameters @@ -189,7 +189,7 @@ def __init__( label, inputs=[electrical_flow], outputs=[thermal_flow], - on_off_parameters=on_off_parameters, + status_parameters=status_parameters, meta_data=meta_data, ) @@ -224,7 +224,7 @@ class HeatPump(LinearConverter): additional energy from the environment. electrical_flow: Electrical input-flow representing electricity consumption. thermal_flow: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + status_parameters: Parameters defining status, startup and shutdown constraints and effects meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -248,9 +248,9 @@ class HeatPump(LinearConverter): cop=temperature_dependent_cop, # Time-varying COP based on ground temp electrical_flow=electricity_flow, thermal_flow=radiant_heating_flow, - on_off_parameters=OnOffParameters( - consecutive_on_hours_min=2, # Avoid frequent cycling - effects_per_running_hour={'maintenance': 0.5}, + status_parameters=StatusParameters( + min_uptime=2, # Avoid frequent cycling + effects_per_active_hour={'maintenance': 0.5}, ), ) ``` @@ -269,7 +269,7 @@ def __init__( cop: Numeric_TPS | None = None, electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, - on_off_parameters: OnOffParameters | None = None, + status_parameters: StatusParameters | None = None, meta_data: dict | None = None, ): # Validate required parameters @@ -285,7 +285,7 @@ def __init__( inputs=[electrical_flow], outputs=[thermal_flow], conversion_factors=[], - on_off_parameters=on_off_parameters, + status_parameters=status_parameters, meta_data=meta_data, ) self.electrical_flow = electrical_flow @@ -319,7 +319,7 @@ class CoolingTower(LinearConverter): of thermal power that must be supplied as electricity for fans and pumps. electrical_flow: Electrical input-flow representing electricity consumption for fans/pumps. thermal_flow: Thermal input-flow representing waste heat to be rejected to environment. - on_off_parameters: Parameters defining binary operation constraints and costs. + status_parameters: Parameters defining status, startup and shutdown constraints and effects meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -343,9 +343,9 @@ class CoolingTower(LinearConverter): specific_electricity_demand=0.015, # 1.5% auxiliary power electrical_flow=auxiliary_electricity, thermal_flow=condenser_waste_heat, - on_off_parameters=OnOffParameters( - consecutive_on_hours_min=4, # Minimum operation time - effects_per_running_hour={'water_consumption': 2.5}, # m³/h + status_parameters=StatusParameters( + min_uptime=4, # Minimum operation time + effects_per_active_hour={'water_consumption': 2.5}, # m³/h ), ) ``` @@ -366,7 +366,7 @@ def __init__( specific_electricity_demand: Numeric_TPS, electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, - on_off_parameters: OnOffParameters | None = None, + status_parameters: StatusParameters | None = None, meta_data: dict | None = None, ): # Validate required parameters @@ -379,7 +379,7 @@ def __init__( label, inputs=[electrical_flow, thermal_flow], outputs=[], - on_off_parameters=on_off_parameters, + status_parameters=status_parameters, meta_data=meta_data, ) @@ -416,7 +416,7 @@ class CHP(LinearConverter): fuel_flow: Fuel input-flow representing fuel consumption. electrical_flow: Electrical output-flow representing electricity generation. thermal_flow: Thermal output-flow representing heat generation. - on_off_parameters: Parameters defining binary operation constraints and costs. + status_parameters: Parameters defining status, startup and shutdown constraints and effects meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -444,10 +444,10 @@ class CHP(LinearConverter): fuel_flow=fuel_gas_flow, electrical_flow=plant_electricity, thermal_flow=process_steam, - on_off_parameters=OnOffParameters( - consecutive_on_hours_min=8, # Minimum 8-hour operation - effects_per_switch_on={'startup_cost': 5000}, - on_hours_max=6000, # Annual operating limit + status_parameters=StatusParameters( + min_uptime=8, # Minimum 8-hour operation + effects_per_startup={'startup_cost': 5000}, + active_hours_max=6000, # Annual operating limit ), ) ``` @@ -470,7 +470,7 @@ def __init__( fuel_flow: Flow | None = None, electrical_flow: Flow | None = None, thermal_flow: Flow | None = None, - on_off_parameters: OnOffParameters | None = None, + status_parameters: StatusParameters | None = None, meta_data: dict | None = None, ): # Validate required parameters @@ -490,7 +490,7 @@ def __init__( inputs=[fuel_flow], outputs=[thermal_flow, electrical_flow], conversion_factors=[{}, {}], - on_off_parameters=on_off_parameters, + status_parameters=status_parameters, meta_data=meta_data, ) @@ -546,7 +546,7 @@ class HeatPumpWithSource(LinearConverter): heat_source_flow: Heat source input-flow representing thermal energy extracted from environment (ground, air, water source). thermal_flow: Thermal output-flow representing useful heat delivered to the application. - on_off_parameters: Parameters defining binary operation constraints and costs. + status_parameters: Parameters defining status, startup and shutdown constraints and effects meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -572,9 +572,9 @@ class HeatPumpWithSource(LinearConverter): electrical_flow=electricity_consumption, heat_source_flow=industrial_heat_extraction, # Heat extracted from a industrial process or waste water thermal_flow=heat_supply, - on_off_parameters=OnOffParameters( - consecutive_on_hours_min=0.5, # 30-minute minimum runtime - effects_per_switch_on={'costs': 1000}, + status_parameters=StatusParameters( + min_uptime=0.5, # 30-minute minimum runtime + effects_per_startup={'costs': 1000}, ), ) ``` @@ -600,7 +600,7 @@ def __init__( electrical_flow: Flow | None = None, heat_source_flow: Flow | None = None, thermal_flow: Flow | None = None, - on_off_parameters: OnOffParameters | None = None, + status_parameters: StatusParameters | None = None, meta_data: dict | None = None, ): # Validate required parameters @@ -617,7 +617,7 @@ def __init__( label, inputs=[electrical_flow, heat_source_flow], outputs=[thermal_flow], - on_off_parameters=on_off_parameters, + status_parameters=status_parameters, meta_data=meta_data, ) self.electrical_flow = electrical_flow diff --git a/flixopt/modeling.py b/flixopt/modeling.py index 01a2c2410..043f0f8fc 100644 --- a/flixopt/modeling.py +++ b/flixopt/modeling.py @@ -59,16 +59,16 @@ def count_consecutive_states( """Count consecutive steps in the final active state of a binary time series. This function counts how many consecutive time steps the series remains "on" - (non-zero) at the end of the time series. If the final state is "off", returns 0. + (non-zero) at the end of the time series. If the final state is "inactive", returns 0. Args: - binary_values: Binary DataArray with values close to 0 (off) or 1 (on). + binary_values: Binary DataArray with values close to 0 (inactive) or 1 (active). dim: Dimension along which to count consecutive states. epsilon: Tolerance for zero detection. Uses CONFIG.Modeling.epsilon if None. Returns: - Sum of values in the final consecutive "on" period. Returns 0.0 if the - final state is "off". + Sum of values in the final consecutive "active" period. Returns 0.0 if the + final state is "inactive". Examples: >>> arr = xr.DataArray([0, 0, 1, 1, 1, 0, 1, 1], dims=['time']) @@ -100,11 +100,11 @@ def count_consecutive_states( if arr.size == 1: return float(arr[0]) if not np.isclose(arr[0], 0, atol=epsilon) else 0.0 - # Return 0 if final state is off + # Return 0 if final state is inactive if np.isclose(arr[-1], 0, atol=epsilon): return 0.0 - # Find the last zero position (treat NaNs as off) + # Find the last zero position (treat NaNs as inactive) arr = np.nan_to_num(arr, nan=0.0) is_zero = np.isclose(arr, 0, atol=epsilon) zero_indices = np.where(is_zero)[0] @@ -123,7 +123,7 @@ def compute_consecutive_hours_in_state( epsilon: float = None, ) -> float: """ - Computes the final consecutive duration in state 'on' (=1) in hours. + Computes the final consecutive duration in state 'active' (=1) in hours. Args: binary_values: Binary DataArray with 'time' dim, or scalar/array @@ -131,7 +131,7 @@ def compute_consecutive_hours_in_state( epsilon: Tolerance for zero detection (uses CONFIG.Modeling.epsilon if None) Returns: - The duration of the final consecutive 'on' period in hours + The duration of the final consecutive 'active' period in hours """ if not isinstance(hours_per_timestep, (int, float)): raise TypeError(f'hours_per_timestep must be a scalar, got {type(hours_per_timestep)}') @@ -159,14 +159,14 @@ def compute_previous_off_duration( previous_values: xr.DataArray, hours_per_step: xr.DataArray | float | int ) -> float: """ - Compute previous consecutive 'off' duration. + Compute previous consecutive 'inactive' duration. Args: previous_values: DataArray with 'time' dimension hours_per_step: Duration of each timestep in hours Returns: - Previous consecutive off duration in hours + Previous consecutive inactive duration in hours """ if previous_values is None or previous_values.size == 0: return 0.0 @@ -199,22 +199,28 @@ class ModelingPrimitives: @staticmethod def expression_tracking_variable( model: Submodel, - tracked_expression, + tracked_expression: linopy.expressions.LinearExpression | linopy.Variable, name: str = None, short_name: str = None, bounds: tuple[xr.DataArray, xr.DataArray] = None, coords: str | list[str] | None = None, ) -> tuple[linopy.Variable, linopy.Constraint]: - """ - Creates variable that equals a given expression. + """Creates a variable constrained to equal a given expression. Mathematical formulation: tracker = expression - lower ≤ tracker ≤ upper (if bounds provided) + lower ≤ tracker ≤ upper (if bounds provided) + + Args: + model: The submodel to add variables and constraints to + tracked_expression: Expression that the tracker variable must equal + name: Full name for the variable and constraint + short_name: Short name for display purposes + bounds: Optional (lower_bound, upper_bound) tuple for the tracker variable + coords: Coordinate dimensions for the variable (None uses all model coords) Returns: - variables: {'tracker': tracker_var} - constraints: {'tracking': constraint} + Tuple of (tracker_variable, tracking_constraint) """ if not isinstance(model, Submodel): raise ValueError('ModelingPrimitives.expression_tracking_variable() can only be used with a Submodel') @@ -238,7 +244,7 @@ def expression_tracking_variable( @staticmethod def consecutive_duration_tracking( model: Submodel, - state_variable: linopy.Variable, + state: linopy.Variable, name: str = None, short_name: str = None, minimum_duration: xr.DataArray | None = None, @@ -247,28 +253,36 @@ def consecutive_duration_tracking( duration_per_step: int | float | xr.DataArray = None, previous_duration: xr.DataArray = 0, ) -> tuple[linopy.Variable, tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]]: - """ - Creates consecutive duration tracking for a binary state variable. + """Creates consecutive duration tracking for a binary state variable. + + Tracks how long a binary state has been continuously active (=1). + Duration resets to 0 when state becomes inactive (=0). Mathematical formulation: - duration[t] ≤ state[t] * M ∀t + duration[t] ≤ state[t] · M ∀t duration[t+1] ≤ duration[t] + duration_per_step[t] ∀t - duration[t+1] ≥ duration[t] + duration_per_step[t] + (state[t+1] - 1) * M ∀t - duration[0] = (duration_per_step[0] + previous_duration) * state[0] + duration[t+1] ≥ duration[t] + duration_per_step[t] + (state[t+1] - 1) · M ∀t + duration[0] = (duration_per_step[0] + previous_duration) · state[0] If minimum_duration provided: - duration[t] ≥ (state[t-1] - state[t]) * minimum_duration[t-1] ∀t > 0 + duration[t] ≥ (state[t-1] - state[t]) · minimum_duration[t-1] ∀t > 0 + + Where M is a big-M value (sum of all duration_per_step + previous_duration). Args: - name: Name of the duration variable - state_variable: Binary state variable to track duration for - minimum_duration: Optional minimum consecutive duration - maximum_duration: Optional maximum consecutive duration - previous_duration: Duration from before first timestep + model: The submodel to add variables and constraints to + state: Binary state variable (1=active, 0=inactive) to track duration for + name: Full name for the duration variable + short_name: Short name for display purposes + minimum_duration: Optional minimum consecutive duration (enforced at state transitions) + maximum_duration: Optional maximum consecutive duration (upper bound on duration variable) + duration_dim: Dimension name to track duration along (default 'time') + duration_per_step: Time increment per step in duration_dim + previous_duration: Initial duration value before first timestep (default 0) Returns: - variables: {'duration': duration_var} - constraints: {'ub': constraint, 'forward': constraint, 'backward': constraint, ...} + Tuple of (duration_variable, constraints_dict) + where constraints_dict contains: 'ub', 'forward', 'backward', 'initial', and optionally 'lb', 'initial_lb' """ if not isinstance(model, Submodel): raise ValueError('ModelingPrimitives.consecutive_duration_tracking() can only be used with a Submodel') @@ -279,7 +293,7 @@ def consecutive_duration_tracking( duration = model.add_variables( lower=0, upper=maximum_duration if maximum_duration is not None else mega, - coords=state_variable.coords, + coords=state.coords, name=name, short_name=short_name, ) @@ -287,7 +301,7 @@ def consecutive_duration_tracking( constraints = {} # Upper bound: duration[t] ≤ state[t] * M - constraints['ub'] = model.add_constraints(duration <= state_variable * mega, name=f'{duration.name}|ub') + constraints['ub'] = model.add_constraints(duration <= state * mega, name=f'{duration.name}|ub') # Forward constraint: duration[t+1] ≤ duration[t] + duration_per_step[t] constraints['forward'] = model.add_constraints( @@ -301,14 +315,14 @@ def consecutive_duration_tracking( duration.isel({duration_dim: slice(1, None)}) >= duration.isel({duration_dim: slice(None, -1)}) + duration_per_step.isel({duration_dim: slice(None, -1)}) - + (state_variable.isel({duration_dim: slice(1, None)}) - 1) * mega, + + (state.isel({duration_dim: slice(1, None)}) - 1) * mega, name=f'{duration.name}|backward', ) # Initial condition: duration[0] = (duration_per_step[0] + previous_duration) * state[0] constraints['initial'] = model.add_constraints( duration.isel({duration_dim: 0}) - == (duration_per_step.isel({duration_dim: 0}) + previous_duration) * state_variable.isel({duration_dim: 0}), + == (duration_per_step.isel({duration_dim: 0}) + previous_duration) * state.isel({duration_dim: 0}), name=f'{duration.name}|initial', ) @@ -316,10 +330,7 @@ def consecutive_duration_tracking( if minimum_duration is not None: constraints['lb'] = model.add_constraints( duration - >= ( - state_variable.isel({duration_dim: slice(None, -1)}) - - state_variable.isel({duration_dim: slice(1, None)}) - ) + >= (state.isel({duration_dim: slice(None, -1)}) - state.isel({duration_dim: slice(1, None)})) * minimum_duration.isel({duration_dim: slice(None, -1)}), name=f'{duration.name}|lb', ) @@ -333,7 +344,7 @@ def consecutive_duration_tracking( min0 = float(minimum_duration.isel({duration_dim: 0}).max().item()) if prev > 0 and prev < min0: constraints['initial_lb'] = model.add_constraints( - state_variable.isel({duration_dim: 0}) == 1, name=f'{duration.name}|initial_lb' + state.isel({duration_dim: 0}) == 1, name=f'{duration.name}|initial_lb' ) variables = {'duration': duration} @@ -347,23 +358,21 @@ def mutual_exclusivity_constraint( tolerance: float = 1, short_name: str = 'mutual_exclusivity', ) -> linopy.Constraint: - """ - Creates mutual exclusivity constraint for binary variables. + """Creates mutual exclusivity constraint for binary variables. - Mathematical formulation: - Σ(binary_vars[i]) ≤ tolerance ∀t + Ensures at most one binary variable can be active (=1) at any time. - Ensures at most one binary variable can be 1 at any time. - Tolerance > 1.0 accounts for binary variable numerical precision. + Mathematical formulation: + Σᵢ binary_vars[i] ≤ tolerance ∀t Args: + model: The submodel to add the constraint to binary_variables: List of binary variables that should be mutually exclusive - tolerance: Upper bound - short_name: Short name of the constraint + tolerance: Upper bound on the sum (default 1, allows slight numerical tolerance) + short_name: Short name for the constraint Returns: - variables: {} (no new variables created) - constraints: {'mutual_exclusivity': constraint} + Mutual exclusivity constraint Raises: AssertionError: If fewer than 2 variables provided or variables aren't binary @@ -396,19 +405,19 @@ def basic_bounds( bounds: tuple[xr.DataArray, xr.DataArray], name: str = None, ) -> list[linopy.constraints.Constraint]: - """Create simple bounds. - variable ∈ [lower_bound, upper_bound] + """Creates simple lower and upper bounds for a variable. - Mathematical Formulation: + Mathematical formulation: lower_bound ≤ variable ≤ upper_bound Args: - model: The optimization model instance + model: The submodel to add constraints to variable: Variable to be bounded bounds: Tuple of (lower_bound, upper_bound) absolute bounds + name: Optional name prefix for constraints Returns: - List containing lower_bound and upper_bound constraints + List of [lower_constraint, upper_constraint] """ if not isinstance(model, Submodel): raise ValueError('BoundingPatterns.basic_bounds() can only be used with a Submodel') @@ -426,29 +435,28 @@ def bounds_with_state( model: Submodel, variable: linopy.Variable, bounds: tuple[xr.DataArray, xr.DataArray], - variable_state: linopy.Variable, + state: linopy.Variable, name: str = None, ) -> list[linopy.Constraint]: - """Constraint a variable to bounds, that can be escaped from to 0 by a binary variable. - variable ∈ {0, [max(ε, lower_bound), upper_bound]} + """Creates bounds controlled by a binary state variable. + + Variable is forced to 0 when state=0, bounded when state=1. - Mathematical Formulation: - - variable_state * max(ε, lower_bound) ≤ variable ≤ variable_state * upper_bound + Mathematical formulation: + state · max(ε, lower_bound) ≤ variable ≤ state · upper_bound - Use Cases: - - Investment decisions - - Unit commitment (on/off states) + Where ε is a small positive number (CONFIG.Modeling.epsilon) ensuring + numerical stability when lower_bound is 0. Args: - model: The optimization model instance + model: The submodel to add constraints to variable: Variable to be bounded - bounds: Tuple of (lower_bound, upper_bound) absolute bounds - variable_state: Binary variable controlling the bounds + bounds: Tuple of (lower_bound, upper_bound) absolute bounds when state=1 + state: Binary variable (0=force variable to 0, 1=allow bounds) + name: Optional name prefix for constraints Returns: - Tuple containing: - - variables (Dict): Empty dict - - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' + List of [lower_constraint, upper_constraint] (or [fix_constraint] if lower=upper) """ if not isinstance(model, Submodel): raise ValueError('BoundingPatterns.bounds_with_state() can only be used with a Submodel') @@ -457,13 +465,13 @@ def bounds_with_state( name = name or f'{variable.name}' if np.allclose(lower_bound, upper_bound, atol=1e-10, equal_nan=True): - fix_constraint = model.add_constraints(variable == variable_state * upper_bound, name=f'{name}|fix') + fix_constraint = model.add_constraints(variable == state * upper_bound, name=f'{name}|fix') return [fix_constraint] epsilon = np.maximum(CONFIG.Modeling.epsilon, lower_bound) - upper_constraint = model.add_constraints(variable <= variable_state * upper_bound, name=f'{name}|ub') - lower_constraint = model.add_constraints(variable >= variable_state * epsilon, name=f'{name}|lb') + upper_constraint = model.add_constraints(variable <= state * upper_bound, name=f'{name}|ub') + lower_constraint = model.add_constraints(variable >= state * epsilon, name=f'{name}|lb') return [lower_constraint, upper_constraint] @@ -475,26 +483,22 @@ def scaled_bounds( relative_bounds: tuple[xr.DataArray, xr.DataArray], name: str = None, ) -> list[linopy.Constraint]: - """Constraint a variable by scaling bounds, dependent on another variable. - variable ∈ [lower_bound * scaling_variable, upper_bound * scaling_variable] + """Creates bounds scaled by another variable. - Mathematical Formulation: - scaling_variable * lower_factor ≤ variable ≤ scaling_variable * upper_factor + Variable is bounded relative to a scaling variable (e.g., flow rate relative to size). - Use Cases: - - Flow rates bounded by equipment capacity - - Production levels scaled by plant size + Mathematical formulation: + scaling_variable · lower_factor ≤ variable ≤ scaling_variable · upper_factor Args: - model: The optimization model instance + model: The submodel to add constraints to variable: Variable to be bounded - scaling_variable: Variable that scales the bound factors - relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable + scaling_variable: Variable that scales the bound factors (e.g., equipment size) + relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling_variable + name: Optional name prefix for constraints Returns: - Tuple containing: - - variables (Dict): Empty dict - - constraints (Dict[str, linopy.Constraint]): 'ub', 'lb' + List of [lower_constraint, upper_constraint] (or [fix_constraint] if lower=upper) """ if not isinstance(model, Submodel): raise ValueError('BoundingPatterns.scaled_bounds() can only be used with a Submodel') @@ -517,33 +521,33 @@ def scaled_bounds_with_state( scaling_variable: linopy.Variable, relative_bounds: tuple[xr.DataArray, xr.DataArray], scaling_bounds: tuple[xr.DataArray, xr.DataArray], - variable_state: linopy.Variable, + state: linopy.Variable, name: str = None, ) -> list[linopy.Constraint]: - """Constraint a variable by scaling bounds with binary state control. + """Creates bounds scaled by a variable and controlled by a binary state. - variable ∈ {0, [max(ε, lower_relative_bound) * scaling_variable, upper_relative_bound * scaling_variable]} + Variable is forced to 0 when state=0, bounded relative to scaling_variable when state=1. - Mathematical Formulation (Big-M): - (variable_state - 1) * M_misc + scaling_variable * rel_lower ≤ variable ≤ scaling_variable * rel_upper - variable_state * big_m_lower ≤ variable ≤ variable_state * big_m_upper + Mathematical formulation (Big-M): + (state - 1) · M_misc + scaling_variable · rel_lower ≤ variable ≤ scaling_variable · rel_upper + state · big_m_lower ≤ variable ≤ state · big_m_upper Where: - M_misc = scaling_max * rel_lower - big_m_upper = scaling_max * rel_upper - big_m_lower = max(ε, scaling_min * rel_lower) + M_misc = scaling_max · rel_lower + big_m_upper = scaling_max · rel_upper + big_m_lower = max(ε, scaling_min · rel_lower) Args: - model: The optimization model instance + model: The submodel to add constraints to variable: Variable to be bounded - scaling_variable: Variable that scales the bound factors - relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling variable - scaling_bounds: Tuple of (scaling_min, scaling_max) bounds of the scaling variable - variable_state: Binary variable for on/off control + scaling_variable: Variable that scales the bound factors (e.g., equipment size) + relative_bounds: Tuple of (lower_factor, upper_factor) relative to scaling_variable + scaling_bounds: Tuple of (scaling_min, scaling_max) bounds of the scaling_variable + state: Binary variable (0=force variable to 0, 1=allow scaled bounds) name: Optional name prefix for constraints Returns: - List[linopy.Constraint]: List of constraint objects + List of [scaling_lower, scaling_upper, binary_lower, binary_upper] constraints """ if not isinstance(model, Submodel): raise ValueError('BoundingPatterns.scaled_bounds_with_state() can only be used with a Submodel') @@ -555,60 +559,69 @@ def scaled_bounds_with_state( big_m_misc = scaling_max * rel_lower scaling_lower = model.add_constraints( - variable >= (variable_state - 1) * big_m_misc + scaling_variable * rel_lower, name=f'{name}|lb2' + variable >= (state - 1) * big_m_misc + scaling_variable * rel_lower, name=f'{name}|lb2' ) scaling_upper = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub2') big_m_upper = rel_upper * scaling_max big_m_lower = np.maximum(CONFIG.Modeling.epsilon, rel_lower * scaling_min) - binary_upper = model.add_constraints(variable_state * big_m_upper >= variable, name=f'{name}|ub1') - binary_lower = model.add_constraints(variable_state * big_m_lower <= variable, name=f'{name}|lb1') + binary_upper = model.add_constraints(state * big_m_upper >= variable, name=f'{name}|ub1') + binary_lower = model.add_constraints(state * big_m_lower <= variable, name=f'{name}|lb1') return [scaling_lower, scaling_upper, binary_lower, binary_upper] @staticmethod def state_transition_bounds( model: Submodel, - state_variable: linopy.Variable, - switch_on: linopy.Variable, - switch_off: linopy.Variable, + state: linopy.Variable, + activate: linopy.Variable, + deactivate: linopy.Variable, name: str, - previous_state=0, + previous_state: float | xr.DataArray = 0, coord: str = 'time', ) -> tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]: - """ - Creates switch-on/off variables with state transition logic. + """Creates state transition constraints for binary state variables. + + Tracks transitions between active (1) and inactive (0) states using + separate binary variables for activation and deactivation events. Mathematical formulation: - switch_on[t] - switch_off[t] = state[t] - state[t-1] ∀t > 0 - switch_on[0] - switch_off[0] = state[0] - previous_state - switch_on[t] + switch_off[t] ≤ 1 ∀t - switch_on[t], switch_off[t] ∈ {0, 1} + activate[t] - deactivate[t] = state[t] - state[t-1] ∀t > 0 + activate[0] - deactivate[0] = state[0] - previous_state + activate[t] + deactivate[t] ≤ 1 ∀t + activate[t], deactivate[t] ∈ {0, 1} + + Args: + model: The submodel to add constraints to + state: Binary state variable (0=inactive, 1=active) + activate: Binary variable for transitions from inactive to active (0→1) + deactivate: Binary variable for transitions from active to inactive (1→0) + name: Base name for constraints + previous_state: State value before first timestep (default 0) + coord: Time dimension name (default 'time') Returns: - variables: {'switch_on': binary_var, 'switch_off': binary_var} - constraints: {'transition': constraint, 'initial': constraint, 'mutex': constraint} + Tuple of (transition_constraint, initial_constraint, mutex_constraint) """ if not isinstance(model, Submodel): - raise ValueError('ModelingPrimitives.state_transition_bounds() can only be used with a Submodel') + raise ValueError('BoundingPatterns.state_transition_bounds() can only be used with a Submodel') # State transition constraints for t > 0 transition = model.add_constraints( - switch_on.isel({coord: slice(1, None)}) - switch_off.isel({coord: slice(1, None)}) - == state_variable.isel({coord: slice(1, None)}) - state_variable.isel({coord: slice(None, -1)}), + activate.isel({coord: slice(1, None)}) - deactivate.isel({coord: slice(1, None)}) + == state.isel({coord: slice(1, None)}) - state.isel({coord: slice(None, -1)}), name=f'{name}|transition', ) # Initial state transition for t = 0 initial = model.add_constraints( - switch_on.isel({coord: 0}) - switch_off.isel({coord: 0}) - == state_variable.isel({coord: 0}) - previous_state, + activate.isel({coord: 0}) - deactivate.isel({coord: 0}) == state.isel({coord: 0}) - previous_state, name=f'{name}|initial', ) - # At most one switch per timestep - mutex = model.add_constraints(switch_on + switch_off <= 1, name=f'{name}|mutex') + # At most one transition per timestep (mutual exclusivity) + mutex = model.add_constraints(activate + deactivate <= 1, name=f'{name}|mutex') return transition, initial, mutex @@ -616,63 +629,66 @@ def state_transition_bounds( def continuous_transition_bounds( model: Submodel, continuous_variable: linopy.Variable, - switch_on: linopy.Variable, - switch_off: linopy.Variable, + activate: linopy.Variable, + deactivate: linopy.Variable, name: str, max_change: float | xr.DataArray, previous_value: float | xr.DataArray = 0.0, coord: str = 'time', ) -> tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint, linopy.Constraint]: - """ - Constrains a continuous variable to only change when switch variables are active. + """Constrains a continuous variable to only change during state transitions. + + Ensures a continuous variable remains constant unless a transition event occurs. + Uses Big-M formulation to enforce change bounds. Mathematical formulation: - -max_change * (switch_on[t] + switch_off[t]) <= continuous[t] - continuous[t-1] <= max_change * (switch_on[t] + switch_off[t]) ∀t > 0 - -max_change * (switch_on[0] + switch_off[0]) <= continuous[0] - previous_value <= max_change * (switch_on[0] + switch_off[0]) - switch_on[t], switch_off[t] ∈ {0, 1} + -max_change · (activate[t] + deactivate[t]) ≤ continuous[t] - continuous[t-1] ≤ max_change · (activate[t] + deactivate[t]) ∀t > 0 + -max_change · (activate[0] + deactivate[0]) ≤ continuous[0] - previous_value ≤ max_change · (activate[0] + deactivate[0]) + activate[t], deactivate[t] ∈ {0, 1} - This ensures the continuous variable can only change when switch_on or switch_off is 1. - When both switches are 0, the variable must stay exactly constant. + Behavior: + - When activate=0 and deactivate=0: variable must stay constant + - When activate=1 or deactivate=1: variable can change within ±max_change Args: model: The submodel to add constraints to - continuous_variable: The continuous variable to constrain - switch_on: Binary variable indicating when changes are allowed (typically transitions to active state) - switch_off: Binary variable indicating when changes are allowed (typically transitions to inactive state) - name: Base name for the constraints - max_change: Maximum possible change in the continuous variable (Big-M value) - previous_value: Initial value of the continuous variable before first period - coord: Coordinate name for time dimension + continuous_variable: Continuous variable to constrain + activate: Binary variable for transitions from inactive to active (0→1) + deactivate: Binary variable for transitions from active to inactive (1→0) + name: Base name for constraints + max_change: Maximum allowed change (Big-M value, should be ≥ actual max change) + previous_value: Initial value before first timestep (default 0.0) + coord: Time dimension name (default 'time') Returns: - Tuple of constraints: (transition_upper, transition_lower, initial_upper, initial_lower) + Tuple of (transition_upper, transition_lower, initial_upper, initial_lower) constraints """ if not isinstance(model, Submodel): raise ValueError('ModelingPrimitives.continuous_transition_bounds() can only be used with a Submodel') - # Transition constraints for t > 0: continuous variable can only change when switches are active + # Transition constraints for t > 0: continuous variable can only change when transitions occur transition_upper = model.add_constraints( continuous_variable.isel({coord: slice(1, None)}) - continuous_variable.isel({coord: slice(None, -1)}) - <= max_change * (switch_on.isel({coord: slice(1, None)}) + switch_off.isel({coord: slice(1, None)})), + <= max_change * (activate.isel({coord: slice(1, None)}) + deactivate.isel({coord: slice(1, None)})), name=f'{name}|transition_ub', ) transition_lower = model.add_constraints( -(continuous_variable.isel({coord: slice(1, None)}) - continuous_variable.isel({coord: slice(None, -1)})) - <= max_change * (switch_on.isel({coord: slice(1, None)}) + switch_off.isel({coord: slice(1, None)})), + <= max_change * (activate.isel({coord: slice(1, None)}) + deactivate.isel({coord: slice(1, None)})), name=f'{name}|transition_lb', ) # Initial constraints for t = 0 initial_upper = model.add_constraints( continuous_variable.isel({coord: 0}) - previous_value - <= max_change * (switch_on.isel({coord: 0}) + switch_off.isel({coord: 0})), + <= max_change * (activate.isel({coord: 0}) + deactivate.isel({coord: 0})), name=f'{name}|initial_ub', ) initial_lower = model.add_constraints( -continuous_variable.isel({coord: 0}) + previous_value - <= max_change * (switch_on.isel({coord: 0}) + switch_off.isel({coord: 0})), + <= max_change * (activate.isel({coord: 0}) + deactivate.isel({coord: 0})), name=f'{name}|initial_lb', ) diff --git a/mkdocs.yml b/mkdocs.yml index 0adba464d..7e86d9720 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,7 +24,7 @@ nav: - LinearConverter: user-guide/mathematical-notation/elements/LinearConverter.md - Features: - InvestParameters: user-guide/mathematical-notation/features/InvestParameters.md - - OnOffParameters: user-guide/mathematical-notation/features/OnOffParameters.md + - StatusParameters: user-guide/mathematical-notation/features/StatusParameters.md - Piecewise: user-guide/mathematical-notation/features/Piecewise.md - Effects, Penalty & Objective: user-guide/mathematical-notation/effects-penalty-objective.md - Modeling Patterns: diff --git a/tests/conftest.py b/tests/conftest.py index b7acee446..11d35f536 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(), + status_parameters=fx.StatusParameters(), ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -149,7 +149,7 @@ def complex(): return fx.linear_converters.Boiler( 'Kessel', thermal_efficiency=0.5, - on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + status_parameters=fx.StatusParameters(effects_per_active_hour={'costs': 0, 'CO2': 1000}), thermal_flow=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_min=0, - on_hours_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_max=1000, + status_parameters=fx.StatusParameters( + active_hours_min=0, + active_hours_max=1000, + max_uptime=10, + min_uptime=1, + max_downtime=10, + effects_per_startup=0.01, + startup_limit=1000, ), flow_hours_max=1e6, ), @@ -187,7 +187,7 @@ def simple(): thermal_efficiency=0.5, electrical_efficiency=0.4, electrical_flow=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, status_parameters=fx.StatusParameters() ), thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), fuel_flow=fx.Flow('Q_fu', bus='Gas'), @@ -200,7 +200,7 @@ def base(): 'KWK', thermal_efficiency=0.5, electrical_efficiency=0.4, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=0.01), + status_parameters=fx.StatusParameters(effects_per_startup=0.01), electrical_flow=fx.Flow('P_el', bus='Strom', size=60, relative_minimum=5 / 60, previous_flow_rate=10), thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=1e3), fuel_flow=fx.Flow('Q_fu', bus='Gas', size=1e3), @@ -224,7 +224,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), + status_parameters=fx.StatusParameters(effects_per_startup=0.01), ) @staticmethod @@ -249,7 +249,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), + status_parameters=fx.StatusParameters(effects_per_startup=0.01), ) @@ -604,14 +604,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), + status_parameters=fx.StatusParameters(effects_per_startup=1000), ), ), fx.linear_converters.CHP( 'BHKW2', thermal_efficiency=0.58, electrical_efficiency=0.22, - on_off_parameters=fx.OnOffParameters(effects_per_switch_on=24000), + status_parameters=fx.StatusParameters(effects_per_startup=24000), electrical_flow=fx.Flow('P_el', bus='Strom'), thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), fuel_flow=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 c33aaf437..41d39b12a 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, status_parameters=fx.StatusParameters() ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -92,18 +92,18 @@ 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)|status', + 'TestComponent(In1)|active_hours', 'TestComponent(Out1)|flow_rate', 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|status', + 'TestComponent(Out1)|active_hours', 'TestComponent(Out2)|flow_rate', 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', + 'TestComponent(Out2)|status', + 'TestComponent(Out2)|active_hours', + 'TestComponent|status', + 'TestComponent|active_hours', }, 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', 'TestComponent(Out1)|total_flow_hours', 'TestComponent(Out1)|flow_rate|lb', 'TestComponent(Out1)|flow_rate|ub', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|active_hours', 'TestComponent(Out2)|total_flow_hours', 'TestComponent(Out2)|flow_rate|lb', 'TestComponent(Out2)|flow_rate|ub', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on|lb', - 'TestComponent|on|ub', - 'TestComponent|on_hours_total', + 'TestComponent(Out2)|active_hours', + 'TestComponent|status|lb', + 'TestComponent|status|ub', + 'TestComponent|active_hours', }, msg='Incorrect constraints', ) @@ -138,36 +138,39 @@ def test_on_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_co model['TestComponent(Out2)|flow_rate'], model.add_variables(lower=0, upper=300 * upper_bound_flow_rate, coords=model.get_coords()), ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) - assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent|status'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal( + model['TestComponent(Out2)|status'], model.add_variables(binary=True, coords=model.get_coords()) + ) assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|lb'], - model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300, + model.variables['TestComponent(Out2)|flow_rate'] + >= model.variables['TestComponent(Out2)|status'] * 0.3 * 300, ) assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] - <= model.variables['TestComponent(Out2)|on'] * 300 * upper_bound_flow_rate, + <= model.variables['TestComponent(Out2)|status'] * 300 * upper_bound_flow_rate, ) assert_conequal( - model.constraints['TestComponent|on|lb'], - model.variables['TestComponent|on'] + model.constraints['TestComponent|status|lb'], + model.variables['TestComponent|status'] >= ( - model.variables['TestComponent(In1)|on'] - + model.variables['TestComponent(Out1)|on'] - + model.variables['TestComponent(Out2)|on'] + model.variables['TestComponent(In1)|status'] + + model.variables['TestComponent(Out1)|status'] + + model.variables['TestComponent(Out2)|status'] ) / (3 + 1e-5), ) assert_conequal( - model.constraints['TestComponent|on|ub'], - model.variables['TestComponent|on'] + model.constraints['TestComponent|status|ub'], + model.variables['TestComponent|status'] <= ( - model.variables['TestComponent(In1)|on'] - + model.variables['TestComponent(Out1)|on'] - + model.variables['TestComponent(Out2)|on'] + model.variables['TestComponent(In1)|status'] + + model.variables['TestComponent(Out1)|status'] + + model.variables['TestComponent(Out2)|status'] ) + 1e-5, ) @@ -180,7 +183,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, status_parameters=fx.StatusParameters() ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -190,10 +193,10 @@ 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|on', - 'TestComponent|on_hours_total', + 'TestComponent(In1)|status', + 'TestComponent(In1)|active_hours', + 'TestComponent|status', + 'TestComponent|active_hours', }, msg='Incorrect variables', ) @@ -204,9 +207,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|on', - 'TestComponent|on_hours_total', + 'TestComponent(In1)|active_hours', + 'TestComponent|status', + 'TestComponent|active_hours', }, msg='Incorrect constraints', ) @@ -214,21 +217,23 @@ def test_on_with_single_flow(self, basic_flow_system_linopy_coords, coords_confi assert_var_equal( model['TestComponent(In1)|flow_rate'], model.add_variables(lower=0, upper=100, coords=model.get_coords()) ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) - assert_var_equal(model['TestComponent(In1)|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent|status'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal( + model['TestComponent(In1)|status'], model.add_variables(binary=True, coords=model.get_coords()) + ) assert_conequal( model.constraints['TestComponent(In1)|flow_rate|lb'], - model.variables['TestComponent(In1)|flow_rate'] >= model.variables['TestComponent(In1)|on'] * 0.1 * 100, + model.variables['TestComponent(In1)|flow_rate'] >= model.variables['TestComponent(In1)|status'] * 0.1 * 100, ) assert_conequal( model.constraints['TestComponent(In1)|flow_rate|ub'], - model.variables['TestComponent(In1)|flow_rate'] <= model.variables['TestComponent(In1)|on'] * 100, + model.variables['TestComponent(In1)|flow_rate'] <= model.variables['TestComponent(In1)|status'] * 100, ) assert_conequal( - model.constraints['TestComponent|on'], - model.variables['TestComponent|on'] == model.variables['TestComponent(In1)|on'], + model.constraints['TestComponent|status'], + model.variables['TestComponent|status'] == model.variables['TestComponent(In1)|status'], ) def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coords, coords_config): @@ -257,7 +262,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, status_parameters=fx.StatusParameters() ) flow_system.add_elements(comp) model = create_linopy_model(flow_system) @@ -267,18 +272,18 @@ 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)|status', + 'TestComponent(In1)|active_hours', 'TestComponent(Out1)|flow_rate', 'TestComponent(Out1)|total_flow_hours', - 'TestComponent(Out1)|on', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|status', + 'TestComponent(Out1)|active_hours', 'TestComponent(Out2)|flow_rate', 'TestComponent(Out2)|total_flow_hours', - 'TestComponent(Out2)|on', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on', - 'TestComponent|on_hours_total', + 'TestComponent(Out2)|status', + 'TestComponent(Out2)|active_hours', + 'TestComponent|status', + 'TestComponent|active_hours', }, msg='Incorrect variables', ) @@ -289,18 +294,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', 'TestComponent(Out1)|total_flow_hours', 'TestComponent(Out1)|flow_rate|lb', 'TestComponent(Out1)|flow_rate|ub', - 'TestComponent(Out1)|on_hours_total', + 'TestComponent(Out1)|active_hours', 'TestComponent(Out2)|total_flow_hours', 'TestComponent(Out2)|flow_rate|lb', 'TestComponent(Out2)|flow_rate|ub', - 'TestComponent(Out2)|on_hours_total', - 'TestComponent|on|lb', - 'TestComponent|on|ub', - 'TestComponent|on_hours_total', + 'TestComponent(Out2)|active_hours', + 'TestComponent|status|lb', + 'TestComponent|status|ub', + 'TestComponent|active_hours', }, msg='Incorrect constraints', ) @@ -313,36 +318,39 @@ def test_previous_states_with_multiple_flows(self, basic_flow_system_linopy_coor model['TestComponent(Out2)|flow_rate'], model.add_variables(lower=0, upper=300 * upper_bound_flow_rate, coords=model.get_coords()), ) - assert_var_equal(model['TestComponent|on'], model.add_variables(binary=True, coords=model.get_coords())) - assert_var_equal(model['TestComponent(Out2)|on'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal(model['TestComponent|status'], model.add_variables(binary=True, coords=model.get_coords())) + assert_var_equal( + model['TestComponent(Out2)|status'], model.add_variables(binary=True, coords=model.get_coords()) + ) assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|lb'], - model.variables['TestComponent(Out2)|flow_rate'] >= model.variables['TestComponent(Out2)|on'] * 0.3 * 300, + model.variables['TestComponent(Out2)|flow_rate'] + >= model.variables['TestComponent(Out2)|status'] * 0.3 * 300, ) assert_conequal( model.constraints['TestComponent(Out2)|flow_rate|ub'], model.variables['TestComponent(Out2)|flow_rate'] - <= model.variables['TestComponent(Out2)|on'] * 300 * upper_bound_flow_rate, + <= model.variables['TestComponent(Out2)|status'] * 300 * upper_bound_flow_rate, ) assert_conequal( - model.constraints['TestComponent|on|lb'], - model.variables['TestComponent|on'] + model.constraints['TestComponent|status|lb'], + model.variables['TestComponent|status'] >= ( - model.variables['TestComponent(In1)|on'] - + model.variables['TestComponent(Out1)|on'] - + model.variables['TestComponent(Out2)|on'] + model.variables['TestComponent(In1)|status'] + + model.variables['TestComponent(Out1)|status'] + + model.variables['TestComponent(Out2)|status'] ) / (3 + 1e-5), ) assert_conequal( - model.constraints['TestComponent|on|ub'], - model.variables['TestComponent|on'] + model.constraints['TestComponent|status|ub'], + model.variables['TestComponent|status'] <= ( - model.variables['TestComponent(In1)|on'] - + model.variables['TestComponent(Out1)|on'] - + model.variables['TestComponent(Out2)|on'] + model.variables['TestComponent(In1)|status'] + + model.variables['TestComponent(Out1)|status'] + + model.variables['TestComponent(Out2)|status'] ) + 1e-5, ) @@ -377,7 +385,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), + status_parameters=fx.StatusParameters(min_uptime=3), ), ] outputs = [ @@ -397,15 +405,15 @@ def test_previous_states_with_multiple_flows_parameterized( 'TestComponent', inputs=inputs, outputs=outputs, - on_off_parameters=fx.OnOffParameters(consecutive_on_hours_min=3), + status_parameters=fx.StatusParameters(min_uptime=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.variables['TestComponent|on'].isel(time=0) * (previous_on_hours + 1), + comp.submodel.constraints['TestComponent|uptime|initial'], + comp.submodel.variables['TestComponent|uptime'].isel(time=0) + == comp.submodel.variables['TestComponent|status'].isel(time=0) * (previous_on_hours + 1), ) @@ -438,9 +446,9 @@ 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.status.status.solution.values, np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), - 'On does not work properly', + 'Status does not work properly', ) assert_almost_equal_numeric( @@ -502,9 +510,9 @@ 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.status.status.solution.values, np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - 'On does not work properly', + 'Status does not work properly', ) assert_almost_equal_numeric( @@ -583,9 +591,9 @@ 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.status.status.solution.values, np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - 'On does not work properly', + 'Status does not work properly', ) assert_almost_equal_numeric( diff --git a/tests/test_flow.py b/tests/test_flow.py index 3017b25dd..01022e53a 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -524,14 +524,14 @@ 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(), + status_parameters=fx.StatusParameters(), ) 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)|status', 'Sink(Wärme)|active_hours'}, msg='Incorrect variables', ) @@ -539,7 +539,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', 'Sink(Wärme)|flow_rate|lb', 'Sink(Wärme)|flow_rate|ub', }, @@ -555,31 +555,35 @@ def test_flow_on(self, basic_flow_system_linopy_coords, coords_config): ), ) - # OnOff + # Status assert_var_equal( - flow.submodel.on_off.on, + flow.submodel.status.status, model.add_variables(binary=True, coords=model.get_coords()), ) + # Upper bound is total hours when active_hours_max is not specified + total_hours = model.hours_per_step.sum('time').max().item() assert_var_equal( - model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), + model.variables['Sink(Wärme)|active_hours'], + model.add_variables(lower=0, upper=total_hours, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb'], - flow.submodel.variables['Sink(Wärme)|flow_rate'] >= flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 100, + flow.submodel.variables['Sink(Wärme)|flow_rate'] + >= flow.submodel.variables['Sink(Wärme)|status'] * 0.2 * 100, ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub'], - flow.submodel.variables['Sink(Wärme)|flow_rate'] <= flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 100, + flow.submodel.variables['Sink(Wärme)|flow_rate'] + <= flow.submodel.variables['Sink(Wärme)|status'] * 0.8 * 100, ) assert_conequal( - model.constraints['Sink(Wärme)|on_hours_total'], - flow.submodel.variables['Sink(Wärme)|on_hours_total'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), + model.constraints['Sink(Wärme)|active_hours'], + flow.submodel.variables['Sink(Wärme)|active_hours'] + == (flow.submodel.variables['Sink(Wärme)|status'] * model.hours_per_step).sum('time'), ) - def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_config): + def test_effects_per_active_hour(self, basic_flow_system_linopy_coords, coords_config): flow_system, coords_config = basic_flow_system_linopy_coords, coords_config timesteps = flow_system.timesteps @@ -589,8 +593,8 @@ 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( - effects_per_running_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} + status_parameters=fx.StatusParameters( + effects_per_active_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow]), fx.Effect('CO2', 't', '')) @@ -602,8 +606,8 @@ 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)|status', + 'Sink(Wärme)|active_hours', }, msg='Incorrect variables', ) @@ -613,7 +617,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', }, msg='Incorrect constraints', ) @@ -621,8 +625,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.status_parameters.effects_per_active_hour['costs'] + co2_per_running_hour = flow.status_parameters.effects_per_active_hour['CO2'] assert costs_per_running_hour.dims == tuple(model.get_coords()) assert co2_per_running_hour.dims == tuple(model.get_coords()) @@ -630,13 +634,13 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ assert_conequal( model.constraints['Sink(Wärme)->costs(temporal)'], model.variables['Sink(Wärme)->costs(temporal)'] - == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * costs_per_running_hour, + == flow.submodel.variables['Sink(Wärme)|status'] * model.hours_per_step * costs_per_running_hour, ) assert_conequal( model.constraints['Sink(Wärme)->CO2(temporal)'], model.variables['Sink(Wärme)->CO2(temporal)'] - == flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, + == flow.submodel.variables['Sink(Wärme)|status'] * model.hours_per_step * co2_per_running_hour, ) def test_consecutive_on_hours(self, basic_flow_system_linopy_coords, coords_config): @@ -647,322 +651,322 @@ 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 + status_parameters=fx.StatusParameters( + min_uptime=2, # Must run for at least 2 hours when turned on + max_uptime=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)|uptime', 'Sink(Wärme)|status'}.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)|uptime|ub', + 'Sink(Wärme)|uptime|forward', + 'Sink(Wärme)|uptime|backward', + 'Sink(Wärme)|uptime|initial', + 'Sink(Wärme)|uptime|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)|uptime|ub', + 'Sink(Wärme)|uptime|forward', + 'Sink(Wärme)|uptime|backward', + 'Sink(Wärme)|uptime|initial', + 'Sink(Wärme)|uptime|lb', }, - msg='Missing consecutive on hours constraints', + msg='Missing uptime constraints', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_on_hours'], + model.variables['Sink(Wärme)|uptime'], 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)|uptime|ub'], + model.variables['Sink(Wärme)|uptime'] <= model.variables['Sink(Wärme)|status'] * 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)|uptime|forward'], + model.variables['Sink(Wärme)|uptime'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|uptime'].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)|uptime|backward'], + model.variables['Sink(Wärme)|uptime'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|uptime'].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, + + (model.variables['Sink(Wärme)|status'].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.variables['Sink(Wärme)|on'].isel(time=0) * model.hours_per_step.isel(time=0), + model.constraints['Sink(Wärme)|uptime|initial'], + model.variables['Sink(Wärme)|uptime'].isel(time=0) + == model.variables['Sink(Wärme)|status'].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)|uptime|lb'], + model.variables['Sink(Wärme)|uptime'] >= ( - model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) + model.variables['Sink(Wärme)|status'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|status'].isel(time=slice(1, None)) ) * 2, ) def test_consecutive_on_hours_previous(self, basic_flow_system_linopy_coords, coords_config): - """Test flow with minimum and maximum consecutive on hours.""" + """Test flow with minimum and maximum uptime.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( '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 + status_parameters=fx.StatusParameters( + min_uptime=2, # Must run for at least 2 hours when active + max_uptime=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 + previous_flow_rate=np.array([10, 20, 30, 0, 20, 20, 30]), # Previously active for 3 steps ) 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)|uptime', 'Sink(Wärme)|status'}.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)|uptime|lb', + 'Sink(Wärme)|uptime|forward', + 'Sink(Wärme)|uptime|backward', + 'Sink(Wärme)|uptime|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)|uptime|lb', + 'Sink(Wärme)|uptime|forward', + 'Sink(Wärme)|uptime|backward', + 'Sink(Wärme)|uptime|initial', }, - msg='Missing consecutive on hours constraints for previous states', + msg='Missing uptime constraints for previous states', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_on_hours'], + model.variables['Sink(Wärme)|uptime'], 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)|uptime|ub'], + model.variables['Sink(Wärme)|uptime'] <= model.variables['Sink(Wärme)|status'] * 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)|uptime|forward'], + model.variables['Sink(Wärme)|uptime'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|uptime'].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)|uptime|backward'], + model.variables['Sink(Wärme)|uptime'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|uptime'].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, + + (model.variables['Sink(Wärme)|status'].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.variables['Sink(Wärme)|on'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 3)), + model.constraints['Sink(Wärme)|uptime|initial'], + model.variables['Sink(Wärme)|uptime'].isel(time=0) + == model.variables['Sink(Wärme)|status'].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)|uptime|lb'], + model.variables['Sink(Wärme)|uptime'] >= ( - model.variables['Sink(Wärme)|on'].isel(time=slice(None, -1)) - - model.variables['Sink(Wärme)|on'].isel(time=slice(1, None)) + model.variables['Sink(Wärme)|status'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|status'].isel(time=slice(1, None)) ) * 2, ) def test_consecutive_off_hours(self, basic_flow_system_linopy_coords, coords_config): - """Test flow with minimum and maximum consecutive off hours.""" + """Test flow with minimum and maximum consecutive inactive hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( '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 + status_parameters=fx.StatusParameters( + min_downtime=4, # Must stay inactive for at least 4 hours when shut down + max_downtime=12, # Can't be inactive 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)|downtime', 'Sink(Wärme)|inactive'}.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)|downtime|ub', + 'Sink(Wärme)|downtime|forward', + 'Sink(Wärme)|downtime|backward', + 'Sink(Wärme)|downtime|initial', + 'Sink(Wärme)|downtime|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)|downtime|ub', + 'Sink(Wärme)|downtime|forward', + 'Sink(Wärme)|downtime|backward', + 'Sink(Wärme)|downtime|initial', + 'Sink(Wärme)|downtime|lb', }, - msg='Missing consecutive off hours constraints', + msg='Missing consecutive inactive hours constraints', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_off_hours'], + model.variables['Sink(Wärme)|downtime'], 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 + mega = model.hours_per_step.sum('time') + model.hours_per_step.isel(time=0) * 1 # previously inactive 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)|downtime|ub'], + model.variables['Sink(Wärme)|downtime'] <= model.variables['Sink(Wärme)|inactive'] * 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)|downtime|forward'], + model.variables['Sink(Wärme)|downtime'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|downtime'].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)|downtime|backward'], + model.variables['Sink(Wärme)|downtime'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|downtime'].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, + + (model.variables['Sink(Wärme)|inactive'].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.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 1)), + model.constraints['Sink(Wärme)|downtime|initial'], + model.variables['Sink(Wärme)|downtime'].isel(time=0) + == model.variables['Sink(Wärme)|inactive'].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)|downtime|lb'], + model.variables['Sink(Wärme)|downtime'] >= ( - model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) + model.variables['Sink(Wärme)|inactive'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|inactive'].isel(time=slice(1, None)) ) * 4, ) def test_consecutive_off_hours_previous(self, basic_flow_system_linopy_coords, coords_config): - """Test flow with minimum and maximum consecutive off hours.""" + """Test flow with minimum and maximum consecutive inactive hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( '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 + status_parameters=fx.StatusParameters( + min_downtime=4, # Must stay inactive for at least 4 hours when shut down + max_downtime=12, # Can't be inactive for more than 12 consecutive hours ), - previous_flow_rate=np.array([10, 20, 30, 0, 20, 0, 0]), # Previously off for 2 steps + previous_flow_rate=np.array([10, 20, 30, 0, 20, 0, 0]), # Previously inactive for 2 steps ) 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)|downtime', 'Sink(Wärme)|inactive'}.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)|downtime|ub', + 'Sink(Wärme)|downtime|forward', + 'Sink(Wärme)|downtime|backward', + 'Sink(Wärme)|downtime|initial', + 'Sink(Wärme)|downtime|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)|downtime|ub', + 'Sink(Wärme)|downtime|forward', + 'Sink(Wärme)|downtime|backward', + 'Sink(Wärme)|downtime|initial', + 'Sink(Wärme)|downtime|lb', }, - msg='Missing consecutive off hours constraints for previous states', + msg='Missing consecutive inactive hours constraints for previous states', ) assert_var_equal( - model.variables['Sink(Wärme)|consecutive_off_hours'], + model.variables['Sink(Wärme)|downtime'], 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)|downtime|ub'], + model.variables['Sink(Wärme)|downtime'] <= model.variables['Sink(Wärme)|inactive'] * 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)|downtime|forward'], + model.variables['Sink(Wärme)|downtime'].isel(time=slice(1, None)) + <= model.variables['Sink(Wärme)|downtime'].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)|downtime|backward'], + model.variables['Sink(Wärme)|downtime'].isel(time=slice(1, None)) + >= model.variables['Sink(Wärme)|downtime'].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, + + (model.variables['Sink(Wärme)|inactive'].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.variables['Sink(Wärme)|off'].isel(time=0) * (model.hours_per_step.isel(time=0) * (1 + 2)), + model.constraints['Sink(Wärme)|downtime|initial'], + model.variables['Sink(Wärme)|downtime'].isel(time=0) + == model.variables['Sink(Wärme)|inactive'].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)|downtime|lb'], + model.variables['Sink(Wärme)|downtime'] >= ( - model.variables['Sink(Wärme)|off'].isel(time=slice(None, -1)) - - model.variables['Sink(Wärme)|off'].isel(time=slice(1, None)) + model.variables['Sink(Wärme)|inactive'].isel(time=slice(None, -1)) + - model.variables['Sink(Wärme)|inactive'].isel(time=slice(1, None)) ) * 4, ) @@ -975,9 +979,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_max=5, # Maximum 5 startups - effects_per_switch_on={'costs': 100}, # 100 EUR startup cost + status_parameters=fx.StatusParameters( + startup_limit=5, # Maximum 5 startups + effects_per_startup={'costs': 100}, # 100 EUR startup cost ), ) @@ -985,7 +989,7 @@ def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_con model = create_linopy_model(flow_system) # Check that variables exist - assert {'Sink(Wärme)|switch|on', 'Sink(Wärme)|switch|off', 'Sink(Wärme)|switch|count'}.issubset( + assert {'Sink(Wärme)|startup', 'Sink(Wärme)|shutdown', 'Sink(Wärme)|startup_count'}.issubset( set(flow.submodel.variables) ) @@ -995,29 +999,29 @@ def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_con 'Sink(Wärme)|switch|transition', 'Sink(Wärme)|switch|initial', 'Sink(Wärme)|switch|mutex', - 'Sink(Wärme)|switch|count', + 'Sink(Wärme)|startup_count', } & set(flow.submodel.constraints), { 'Sink(Wärme)|switch|transition', 'Sink(Wärme)|switch|initial', 'Sink(Wärme)|switch|mutex', - 'Sink(Wärme)|switch|count', + 'Sink(Wärme)|startup_count', }, msg='Missing switch constraints', ) - # Check switch_on_nr variable bounds + # Check startup_count variable bounds assert_var_equal( - flow.submodel.variables['Sink(Wärme)|switch|count'], + flow.submodel.variables['Sink(Wärme)|startup_count'], model.add_variables(lower=0, upper=5, coords=model.get_coords(['period', 'scenario'])), ) - # Verify switch_on_nr constraint (limits number of startups) + # Verify startup_count constraint (limits number of startups) assert_conequal( - model.constraints['Sink(Wärme)|switch|count'], - flow.submodel.variables['Sink(Wärme)|switch|count'] - == flow.submodel.variables['Sink(Wärme)|switch|on'].sum('time'), + model.constraints['Sink(Wärme)|startup_count'], + flow.submodel.variables['Sink(Wärme)|startup_count'] + == flow.submodel.variables['Sink(Wärme)|startup'].sum('time'), ) # Check that startup cost effect constraint exists @@ -1026,20 +1030,20 @@ def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_con # Verify the startup cost effect constraint assert_conequal( model.constraints['Sink(Wärme)->costs(temporal)'], - model.variables['Sink(Wärme)->costs(temporal)'] == flow.submodel.variables['Sink(Wärme)|switch|on'] * 100, + model.variables['Sink(Wärme)->costs(temporal)'] == flow.submodel.variables['Sink(Wärme)|startup'] * 100, ) def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): - """Test flow with limits on total on hours.""" + """Test flow with limits on total active hours.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config flow = fx.Flow( 'Wärme', bus='Fernwärme', size=100, - on_off_parameters=fx.OnOffParameters( - on_hours_min=20, # Minimum 20 hours of operation - on_hours_max=100, # Maximum 100 hours of operation + status_parameters=fx.StatusParameters( + active_hours_min=20, # Minimum 20 hours of operation + active_hours_max=100, # Maximum 100 hours of operation ), ) @@ -1047,22 +1051,22 @@ 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)|status', 'Sink(Wärme)|active_hours'}.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' in model.constraints - # Check on_hours_total variable bounds + # Check active_hours variable bounds assert_var_equal( - flow.submodel.variables['Sink(Wärme)|on_hours_total'], + flow.submodel.variables['Sink(Wärme)|active_hours'], model.add_variables(lower=20, upper=100, coords=model.get_coords(['period', 'scenario'])), ) - # Check on_hours_total constraint + # Check active_hours constraint assert_conequal( - model.constraints['Sink(Wärme)|on_hours_total'], - flow.submodel.variables['Sink(Wärme)|on_hours_total'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), + model.constraints['Sink(Wärme)|active_hours'], + flow.submodel.variables['Sink(Wärme)|active_hours'] + == (flow.submodel.variables['Sink(Wärme)|status'] * model.hours_per_step).sum('time'), ) @@ -1077,7 +1081,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(), + status_parameters=fx.StatusParameters(), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) @@ -1089,8 +1093,8 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|invested', 'Sink(Wärme)|size', - 'Sink(Wärme)|on', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|status', + 'Sink(Wärme)|active_hours', }, msg='Incorrect variables', ) @@ -1099,7 +1103,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', 'Sink(Wärme)|flow_rate|lb1', 'Sink(Wärme)|flow_rate|ub1', 'Sink(Wärme)|size|lb', @@ -1120,14 +1124,16 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c ), ) - # OnOff + # Status assert_var_equal( - flow.submodel.on_off.on, + flow.submodel.status.status, model.add_variables(binary=True, coords=model.get_coords()), ) + # Upper bound is total hours when active_hours_max is not specified + total_hours = model.hours_per_step.sum('time') assert_var_equal( - model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), + model.variables['Sink(Wärme)|active_hours'], + model.add_variables(lower=0, upper=total_hours, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( model.constraints['Sink(Wärme)|size|lb'], @@ -1139,16 +1145,18 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb1'], - flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.submodel.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|status'] * 0.2 * 20 + <= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub1'], - flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|status'] * 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'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), + model.constraints['Sink(Wärme)|active_hours'], + flow.submodel.variables['Sink(Wärme)|active_hours'] + == (flow.submodel.variables['Sink(Wärme)|status'] * model.hours_per_step).sum('time'), ) # Investment @@ -1161,7 +1169,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb2'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|on'] * mega + >= flow.submodel.variables['Sink(Wärme)|status'] * mega + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 - mega, ) @@ -1178,7 +1186,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(), + status_parameters=fx.StatusParameters(), ) flow_system.add_elements(fx.Sink('Sink', inputs=[flow])) model = create_linopy_model(flow_system) @@ -1189,8 +1197,8 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor 'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate', 'Sink(Wärme)|size', - 'Sink(Wärme)|on', - 'Sink(Wärme)|on_hours_total', + 'Sink(Wärme)|status', + 'Sink(Wärme)|active_hours', }, msg='Incorrect variables', ) @@ -1199,7 +1207,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', 'Sink(Wärme)|flow_rate|lb1', 'Sink(Wärme)|flow_rate|ub1', 'Sink(Wärme)|flow_rate|lb2', @@ -1218,27 +1226,31 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor ), ) - # OnOff + # Status assert_var_equal( - flow.submodel.on_off.on, + flow.submodel.status.status, model.add_variables(binary=True, coords=model.get_coords()), ) + # Upper bound is total hours when active_hours_max is not specified + total_hours = model.hours_per_step.sum('time') assert_var_equal( - model.variables['Sink(Wärme)|on_hours_total'], - model.add_variables(lower=0, coords=model.get_coords(['period', 'scenario'])), + model.variables['Sink(Wärme)|active_hours'], + model.add_variables(lower=0, upper=total_hours, coords=model.get_coords(['period', 'scenario'])), ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb1'], - flow.submodel.variables['Sink(Wärme)|on'] * 0.2 * 20 <= flow.submodel.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|status'] * 0.2 * 20 + <= flow.submodel.variables['Sink(Wärme)|flow_rate'], ) assert_conequal( model.constraints['Sink(Wärme)|flow_rate|ub1'], - flow.submodel.variables['Sink(Wärme)|on'] * 0.8 * 200 >= flow.submodel.variables['Sink(Wärme)|flow_rate'], + flow.submodel.variables['Sink(Wärme)|status'] * 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'] - == (flow.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step).sum('time'), + model.constraints['Sink(Wärme)|active_hours'], + flow.submodel.variables['Sink(Wärme)|active_hours'] + == (flow.submodel.variables['Sink(Wärme)|status'] * model.hours_per_step).sum('time'), ) # Investment @@ -1251,7 +1263,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor assert_conequal( model.constraints['Sink(Wärme)|flow_rate|lb2'], flow.submodel.variables['Sink(Wärme)|flow_rate'] - >= flow.submodel.variables['Sink(Wärme)|on'] * mega + >= flow.submodel.variables['Sink(Wärme)|status'] * mega + flow.submodel.variables['Sink(Wärme)|size'] * 0.2 - mega, ) diff --git a/tests/test_functional.py b/tests/test_functional.py index ae01a44f2..4b5c6c686 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -3,7 +3,7 @@ This module defines a set of unit tests for testing the functionality of the `flixopt` framework. The tests focus on verifying the correct behavior of flow systems, including component modeling, -investment optimization, and operational constraints like on-off behavior. +investment optimization, and operational constraints like status behavior. ### Approach: 1. **Setup**: Each test initializes a flow system with a set of predefined elements and parameters. @@ -11,10 +11,10 @@ 3. **Solution**: The models are solved using the `solve_and_load` method, which performs modeling, solves the optimization problem, and loads the results. 4. **Validation**: Results are validated using assertions, primarily `assert_allclose`, to ensure model outputs match expected values with a specified tolerance. -Classes group related test cases by their functional focus: -- Minimal modeling setup (`TestMinimal`) -- Investment behavior (`TestInvestment`) -- On-off operational constraints (`TestOnOff`). +Tests group related cases by their functional focus: +- Minimal modeling setup (`TestMinimal` class) +- Investment behavior (`TestInvestment` class) +- Status operational constraints (functions: `test_startup_shutdown`, `test_consecutive_uptime_downtime`, etc.) """ import numpy as np @@ -338,7 +338,7 @@ def test_on(solver_fixture, time_steps_fixture): 'Boiler', thermal_efficiency=0.5, fuel_flow=fx.Flow('Q_fu', bus='Gas'), - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100, on_off_parameters=fx.OnOffParameters()), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=100, status_parameters=fx.StatusParameters()), ) ) @@ -354,7 +354,7 @@ def test_on(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.thermal_flow.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.status.status.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), + status_parameters=fx.StatusParameters(max_downtime=100), ), ) ) @@ -398,15 +398,15 @@ def test_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.thermal_flow.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.status.status.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.thermal_flow.submodel.on_off.off.solution.values, - 1 - boiler.thermal_flow.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.status.inactive.solution.values, + 1 - boiler.thermal_flow.submodel.status.status.solution.values, rtol=1e-5, atol=1e-10, err_msg='"Boiler__Q_th__off" does not have the right value', @@ -420,8 +420,8 @@ def test_off(solver_fixture, time_steps_fixture): ) -def test_switch_on_off(solver_fixture, time_steps_fixture): - """Tests if the Switch On/Off Variable is correctly created and calculated in a Flow""" +def test_startup_shutdown(solver_fixture, time_steps_fixture): + """Tests if the startup/shutdown Variable is correctly created and calculated in a Flow""" flow_system = flow_system_base(time_steps_fixture) flow_system.add_elements( fx.linear_converters.Boiler( @@ -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), + status_parameters=fx.StatusParameters(force_startup_tracking=True), ), ) ) @@ -449,21 +449,21 @@ def test_switch_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.thermal_flow.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.status.status.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.thermal_flow.submodel.on_off.switch_on.solution.values, + boiler.thermal_flow.submodel.status.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.thermal_flow.submodel.on_off.switch_off.solution.values, + boiler.thermal_flow.submodel.status.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_max=1), + status_parameters=fx.StatusParameters(active_hours_max=1), ), ), fx.linear_converters.Boiler( @@ -513,7 +513,7 @@ def test_on_total_max(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.thermal_flow.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.status.status.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_max=2), + status_parameters=fx.StatusParameters(active_hours_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_min=3), + status_parameters=fx.StatusParameters(active_hours_min=3), ), ), ) @@ -572,7 +572,7 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.thermal_flow.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.status.status.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.thermal_flow.submodel.on_off.on.solution.values), + sum(boiler_backup.thermal_flow.submodel.status.status.solution.values), 3, rtol=1e-5, atol=1e-10, @@ -602,8 +602,8 @@ def test_on_total_bounds(solver_fixture, time_steps_fixture): ) -def test_consecutive_on_off(solver_fixture, time_steps_fixture): - """Tests if the consecutive on/off hours are correctly created and calculated in a Flow""" +def test_consecutive_uptime_downtime(solver_fixture, time_steps_fixture): + """Tests if the consecutive uptime/downtime are correctly created and calculated in a Flow""" flow_system = flow_system_base(time_steps_fixture) flow_system.add_elements( fx.linear_converters.Boiler( @@ -614,7 +614,7 @@ 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), + status_parameters=fx.StatusParameters(max_uptime=2, min_uptime=2), ), ), fx.linear_converters.Boiler( @@ -640,7 +640,7 @@ def test_consecutive_on_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler.thermal_flow.submodel.on_off.on.solution.values, + boiler.thermal_flow.submodel.status.status.solution.values, [1, 1, 0, 1, 1], rtol=1e-5, atol=1e-10, @@ -682,7 +682,7 @@ 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), + status_parameters=fx.StatusParameters(max_downtime=2, min_downtime=2), ), ), ) @@ -703,14 +703,14 @@ def test_consecutive_off(solver_fixture, time_steps_fixture): ) assert_allclose( - boiler_backup.thermal_flow.submodel.on_off.on.solution.values, + boiler_backup.thermal_flow.submodel.status.status.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.thermal_flow.submodel.on_off.off.solution.values, + boiler_backup.thermal_flow.submodel.status.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 02aa792f3..57b911d64 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -134,24 +134,26 @@ def test_linear_converter_multiple_factors(self, basic_flow_system_linopy_coords input_flow1.submodel.flow_rate * 0.2 == output_flow2.submodel.flow_rate * 0.3, ) - def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coords_config): - """Test a LinearConverter with OnOffParameters.""" + def test_linear_converter_with_status(self, basic_flow_system_linopy_coords, coords_config): + """Test a LinearConverter with StatusParameters.""" 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_min=10, on_hours_max=40, effects_per_running_hour={'costs': 5}) + # Create StatusParameters + status_params = fx.StatusParameters( + active_hours_min=10, active_hours_max=40, effects_per_active_hour={'costs': 5} + ) - # Create a linear converter with OnOffParameters + # Create a linear converter with StatusParameters 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, + status_parameters=status_params, ) # Add to flow system @@ -164,15 +166,15 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coo # Create model model = create_linopy_model(flow_system) - # Verify OnOff variables and constraints - assert 'Converter|on' in model.variables - assert 'Converter|on_hours_total' in model.variables + # Verify Status variables and constraints + assert 'Converter|status' in model.variables + assert 'Converter|active_hours' in model.variables - # Check on_hours_total constraint + # Check active_hours constraint assert_conequal( - model.constraints['Converter|on_hours_total'], - model.variables['Converter|on_hours_total'] - == (model.variables['Converter|on'] * model.hours_per_step).sum('time'), + model.constraints['Converter|active_hours'], + model.variables['Converter|active_hours'] + == (model.variables['Converter|status'] * model.hours_per_step).sum('time'), ) # Check conversion constraint @@ -181,11 +183,12 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coo input_flow.submodel.flow_rate * 0.8 == output_flow.submodel.flow_rate * 1.0, ) - # Check on_off effects + # Check status effects assert 'Converter->costs(temporal)' in model.constraints assert_conequal( model.constraints['Converter->costs(temporal)'], - model.variables['Converter->costs(temporal)'] == model.variables['Converter|on'] * model.hours_per_step * 5, + model.variables['Converter->costs(temporal)'] + == model.variables['Converter|status'] * model.hours_per_step * 5, ) def test_linear_converter_multidimensional(self, basic_flow_system_linopy_coords, coords_config): @@ -368,15 +371,15 @@ def test_piecewise_conversion(self, basic_flow_system_linopy_coords, coords_conf assert 'Converter|Converter(input)|flow_rate|single_segment' in model.constraints # The constraint should enforce that the sum of inside_piece variables is limited - # If there's no on_off parameter, the right-hand side should be 1 + # If there's no status parameter, the right-hand side should be 1 assert_conequal( model.constraints['Converter|Converter(input)|flow_rate|single_segment'], sum([model.variables[f'Converter|Piece_{i}|inside_piece'] for i in range(len(piecewise_model.pieces))]) <= 1, ) - def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, coords_config): - """Test a LinearConverter with PiecewiseConversion and OnOffParameters.""" + def test_piecewise_conversion_with_status(self, basic_flow_system_linopy_coords, coords_config): + """Test a LinearConverter with PiecewiseConversion and StatusParameters.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config # Create input and output flows @@ -393,16 +396,18 @@ 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_min=10, on_hours_max=40, effects_per_running_hour={'costs': 5}) + # Create StatusParameters + status_params = fx.StatusParameters( + active_hours_min=10, active_hours_max=40, effects_per_active_hour={'costs': 5} + ) - # Create a linear converter with piecewise conversion and on/off parameters + # Create a linear converter with piecewise conversion and status parameters converter = fx.LinearConverter( label='Converter', inputs=[input_flow], outputs=[output_flow], piecewise_conversion=piecewise_conversion, - on_off_parameters=on_off_params, + status_parameters=status_params, ) # Add to flow system @@ -424,9 +429,9 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, # Check that we have the expected pieces (2 in this case) 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 - assert 'Converter|on' in model.variables + # Verify that the status variable was used as the zero_point for the piecewise model + # When using StatusParameters, the zero_point should be the status variable + assert 'Converter|status' in model.variables assert piecewise_model.zero_point is not None # Should be a variable # Verify that variables were created for each piece @@ -473,21 +478,22 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, assert_conequal( model.constraints['Converter|Converter(input)|flow_rate|single_segment'], sum([model.variables[f'Converter|Piece_{i}|inside_piece'] for i in range(len(piecewise_model.pieces))]) - <= model.variables['Converter|on'], + <= model.variables['Converter|status'], ) - # Also check that the OnOff model is working correctly - assert 'Converter|on_hours_total' in model.constraints + # Also check that the Status model is working correctly + assert 'Converter|active_hours' 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'], + model['Converter|active_hours'] == (model['Converter|status'] * model.hours_per_step).sum('time'), ) # Verify that the costs effect is applied assert 'Converter->costs(temporal)' in model.constraints assert_conequal( model.constraints['Converter->costs(temporal)'], - model.variables['Converter->costs(temporal)'] == model.variables['Converter|on'] * model.hours_per_step * 5, + model.variables['Converter->costs(temporal)'] + == model.variables['Converter|status'] * model.hours_per_step * 5, ) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index bd402cb8c..c952777b2 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -143,7 +143,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: boiler = fx.linear_converters.Boiler( 'Kessel', thermal_efficiency=0.5, - on_off_parameters=fx.OnOffParameters(effects_per_running_hour={'costs': 0, 'CO2': 1000}), + status_parameters=fx.StatusParameters(effects_per_active_hour={'costs': 0, 'CO2': 1000}), thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', @@ -158,14 +158,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_min=0, - on_hours_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_max=1000, + status_parameters=fx.StatusParameters( + active_hours_min=0, + active_hours_max=1000, + max_uptime=10, + min_uptime=1, + max_downtime=10, + effects_per_startup=0.01, + startup_limit=1000, ), flow_hours_max=1e6, ), @@ -231,7 +231,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), + status_parameters=fx.StatusParameters(effects_per_startup=0.01), ) ) diff --git a/tests/test_storage.py b/tests/test_storage.py index 6220ee08a..a5d2c7a19 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -408,8 +408,8 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy_coords, co # Binary variables should exist when preventing simultaneous operation if prevent_simultaneous: binary_vars = { - 'SimultaneousStorage(Q_th_in)|on', - 'SimultaneousStorage(Q_th_out)|on', + 'SimultaneousStorage(Q_th_in)|status', + 'SimultaneousStorage(Q_th_out)|status', } for var_name in binary_vars: assert var_name in model.variables, f'Missing binary variable: {var_name}' @@ -420,7 +420,8 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy_coords, co assert_conequal( model.constraints['SimultaneousStorage|prevent_simultaneous_use'], - model.variables['SimultaneousStorage(Q_th_in)|on'] + model.variables['SimultaneousStorage(Q_th_out)|on'] + model.variables['SimultaneousStorage(Q_th_in)|status'] + + model.variables['SimultaneousStorage(Q_th_out)|status'] <= 1, )