From 6e09f4600cfd8a22ee0763c60aa2a498ca4be1bd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 02:52:29 +0100 Subject: [PATCH 01/11] Empty --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From dc0c3661b025ad099b730753ec59bc531ea45020 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 03:13:16 +0100 Subject: [PATCH 02/11] Recreate workflow file --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4c77a4dbe..395fc766c 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 }} From f5ffff866f1f10c127941db59354a8362a9a8234 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 03:19:42 +0100 Subject: [PATCH 03/11] Try renaming the workflow temporarily --- .github/workflows/release.yaml | 2 +- .github/workflows/{test.yaml => tests.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{test.yaml => tests.yaml} (100%) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8d0d0de1f..4f1edd3e8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -80,7 +80,7 @@ jobs: name: Run tests needs: [check-preparation] if: needs.check-preparation.outputs.prepared == 'true' - uses: ./.github/workflows/test.yaml + uses: ./.github/workflows/tests.yaml build: name: Build package diff --git a/.github/workflows/test.yaml b/.github/workflows/tests.yaml similarity index 100% rename from .github/workflows/test.yaml rename to .github/workflows/tests.yaml From 658d410c5cd231a0c5eba613a433b279edcda1fe Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 03:26:19 +0100 Subject: [PATCH 04/11] Fix workflow to always run on all branches --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 395fc766c..8fea07529 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - branches: ["*"] + branches: ["**"] workflow_dispatch: workflow_call: # Allow release.yaml to call this workflow From 4af7ee4ef7704bdd6816f1180f7400b19d59a518 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 03:28:36 +0100 Subject: [PATCH 05/11] Feature/rename on off to status (#500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Perfect! Here's the **final complete renaming table with Option A**: ## Parameters Class (`OnOffParameters` → `StatusParameters`) | Current Name | Recommended Name | Rationale | |--------------|------------------|-----------| | `OnOffParameters` | **`StatusParameters`** | Aligns with PyPSA, clearer semantics | | `effects_per_switch_on` | **`effects_per_startup`** | Standard UC terminology | | `effects_per_running_hour` | **`effects_per_active_hour`** | Clear, concise, matches "active" state | | `on_hours_total_min` | **`active_hours_min`** | Total (not consecutive) active hours | | `on_hours_total_max` | **`active_hours_max`** | Total (not consecutive) active hours | | `consecutive_on_hours_min` | **`min_uptime`** | Standard UC term (consecutive) | | `consecutive_on_hours_max` | **`max_uptime`** | Standard UC term (consecutive) | | `consecutive_off_hours_min` | **`min_downtime`** | Standard UC term (consecutive) | | `consecutive_off_hours_max` | **`max_downtime`** | Standard UC term (consecutive) | | `switch_on_total_max` | **`startup_limit`** | Clearer intent, matches "startup" | | `force_switch_on` | **`force_startup_tracking`** | More explicit about what is forced | ## Model Class (`OnOffModel` → `StatusModel`) ### Class Name | Current Name | Recommended Name | |--------------|------------------| | `OnOffModel` | **`StatusModel`** | ### Constructor Parameters | Current Name | Recommended Name | Rationale | |--------------|------------------|-----------| | `on_variable` | **`status`** | Aligns with PyPSA and literature | | `previous_states` | **`previous_status`** | Consistency with status variable | ### Variables (short_name in add_variables/expression_tracking_variable) | Current Name | Recommended Name | Type | Notes | |--------------|------------------|------|-------| | `self.on` | **`self.status`** | Input variable | Main binary state variable | | `'off'` | **Remove variable** | Binary variable | Replace with expression `1 - status` | | `'switch\|on'` | **`'startup'`** | Binary variable | Startup event indicator | | `'switch\|off'` | **`'shutdown'`** | Binary variable | Shutdown event indicator | | `'switch\|count'` | **`'startup_count'`** | Integer variable | Number of startups | | `'on_hours_total'` | **`'active_hours'`** | Continuous variable | Total active duration | | `'consecutive_on_hours'` | **`'uptime'`** | Continuous variable | Consecutive active hours | | `'consecutive_off_hours'` | **`'downtime'`** | Continuous variable | Consecutive inactive hours | ### Properties | Current Name | Recommended Name | Returns | Meaning | |--------------|------------------|---------|---------| | `on_hours_total` | **`active_hours`** | `linopy.Variable` | Total active hours | | `off` | **Remove property** | — | Use `1 - status` expression | | `switch_on` | **`startup`** | `linopy.Variable \| None` | Startup events | | `switch_off` | **`shutdown`** | `linopy.Variable \| None` | Shutdown events | | `switch_on_nr` | **`startup_count`** | `linopy.Variable \| None` | Number of startups | | `consecutive_on_hours` | **`uptime`** | `linopy.Variable \| None` | Consecutive active hours | | `consecutive_off_hours` | **`downtime`** | `linopy.Variable \| None` | Consecutive inactive hours | ### Internal Methods | Current Name | Recommended Name | |--------------|------------------| | `_get_previous_on_duration()` | **`_get_previous_uptime()`** | | `_get_previous_off_duration()` | **`_get_previous_downtime()`** | ### Internal Properties/Flags (in parameters) | Current Name | Recommended Name | |--------------|------------------| | `use_off` | **Remove** (use expression instead) | | `use_switch_on` | **`use_startup_tracking`** | | `use_consecutive_on_hours` | **`use_uptime_tracking`** | | `use_consecutive_off_hours` | **`use_downtime_tracking`** | ## Constraint Names (short_name in add_constraints) | Current Name | Recommended Name | |--------------|------------------| | `'complementary'` | **Remove** (no off variable) | | `'on_hours_total'` | **`'active_hours'`** | | `'switch\|on'`, `'switch\|off'` | **`'startup'`, `'shutdown'`** | | `'switch\|count'` | **`'startup_count'`** | | `'consecutive_on_hours'` | **`'uptime'`** | | `'consecutive_off_hours'` | **`'downtime'`** | ## Complete Terminology Summary (Option A) **State:** - `status` (binary): 1 = active, 0 = inactive **Events:** - `startup` (binary): transition from inactive to active - `shutdown` (binary): transition from active to inactive **Durations:** - `active_hours` (continuous): **total** hours in active state across time horizon - `uptime` (continuous): **consecutive** hours currently active (UC standard) - `downtime` (continuous): **consecutive** hours currently inactive (UC standard) **Parameter Bounds:** - `active_hours_min/max`: limits on **total** active hours - `min_uptime/max_uptime`: limits on **consecutive** active hours (UC standard) - `min_downtime/max_downtime`: limits on **consecutive** inactive hours (UC standard) - `startup_limit`: maximum number of startup events **Effects:** - `effects_per_startup`: costs/impacts per startup event - `effects_per_active_hour`: costs/impacts per active hour This aligns perfectly with PyPSA and the unit commitment literature! 🎯 * Refactor tests and examples * Refactor tests and examples * Update CHANGELOG.md * Python Docstrings Updated: 1. interface.py - Module docstring now references "Status decisions" 2. components.py - Updated all docstrings: - status_parameters parameter descriptions - Example code updated with new parameter names (effects_per_startup, min_uptime, startup_limit) - Fixed incorrect "OnOff feature" docstring to "Investment feature" - Updated TODO comment to reference StatusParameters 3. linear_converters.py - All docstrings updated: - Import statement updated to StatusParameters - All parameter descriptions updated - All example code updated with new terminology 4. flow_system.py - Updated references from "consecutive_on_hours" to "uptime and downtime" and on_off_parameters to status_parameters 5. modeling.py - Updated docstring from "switch-on/off variables" to "state transition constraints for binary switching variables" Documentation Markdown Files Updated: 1. Flow.md - All references updated: - Links to StatusParameters - "on/off state" → "active/inactive state" - Parameter names updated 2. StatusParameters.md (renamed from OnOffParameters.md) - Comprehensive updates: - Title changed to "StatusParameters" - All terminology updated: on/off → active/inactive - Mathematical notation updated: s^on/s^off → s^startup/s^shutdown - Duration variables: d^on/d^off → d^uptime/d^downtime - Parameter names updated in all examples - All Python code examples updated with new API 3. Other modeling pattern docs - Updated all references to StatusParameters and active/inactive terminology 4. mkdocs.yml - Navigation updated to reference StatusParameters.md All docstrings and documentation now consistently use the new Status terminology aligned with PyPSA and unit commitment standards! * Update remaining mentions of old parameters * ⏺ Perfect! I've addressed all the actionable review comments: Changes Made: 1. Fixed error message in modeling.py - Corrected ModelingPrimitives.state_transition_bounds() → BoundingPatterns.state_transition_bounds() in error message (flixopt/modeling.py:591) 2. Fixed Transmission type hint (flixopt/components.py:667) - Changed status_parameters: StatusParameters = None → status_parameters: StatusParameters | None = None 3. Fixed absolute_losses=0 edge case (flixopt/components.py:768) - Added np.any(self.element.absolute_losses != 0) check in create_transmission_equation to match the initialization logic - This prevents AttributeError when absolute_losses is explicitly set to 0 4. Updated test assertion messages (tests/test_component.py) - Changed "On does not work properly" → "Status does not work properly" 5. Fixed effects_per_startup type (examples/02_Complex/complex_example.py) - Changed scalar effects_per_startup=0.01 → dict effects_per_startup={Costs.label: 0.01} in all 3 occurrences - Now consistent with the StatusParameters API which expects a dict mapping effect names to values 6. Updated test_functional.py docstring - Removed reference to non-existent TestStatus class - Updated to accurately describe the status-related test functions 7. Consistent unbounded upper bounds (flixopt/features.py:191) - Changed np.inf → None for unbounded active_hours_max - Now consistent with FlowModel's total_flow_hours pattern All changes maintain backward compatibility and align with the codebase's existing patterns. The documentation in index.md was already correct (BoundingPatterns is the right class for state_transition_bounds). * Changes Made: 1. CHANGELOG.md - Fixed parameter rename documentation (lines 89-90) - Changed incorrect status_parameters → status_parameters - To correct: on_off_parameters → status_parameters 2. CHANGELOG.md - Removed duplicate logger warning (line 803 in v2.1.0) - Removed duplicate entry that was already documented in v2.0.1 - Fixed v2.0.1 entry to say on_off_parameters (the name at that time) 3. StatusParameters.md - Aligned flow bounds formulation (line 229) - Updated summary to include max(ε, rel_lower) like the main text - Now consistent: s(t) · P · max(ε, rel_lower) ≤ p(t) ≤ s(t) · P · rel_upper 4. features.py - Narrowed previous_status type hint (line 155) - Changed from Numeric_TPS | None to xr.DataArray | None - Added import xarray as xr (line 12) - This accurately reflects that _get_previous_uptime() and _get_previous_downtime() use xarray APIs All changes are verified to compile correctly and maintain consistency with the codebase patterns! * Fixed Issues 1. Constraint naming in tests (tests/test_component.py:126-127, 158, 168, 338, 348): - Updated test expectations from 'TestComponent|on|lb' and 'TestComponent|on|ub' to 'TestComponent|status|lb' and 'TestComponent|status|ub' to match the actual constraint names 2. Added 'off' property to StatusModel (flixopt/features.py:284-287): - Added a new property that returns 1 - self.status for backward compatibility with tests expecting an off attribute 3. Fixed deprecated parameter name (tests/test_functional.py:435): - Changed force_switch_on=True to force_startup_tracking=True in StatusParameters 4. Fixed property name (tests/test_functional.py:466): - Changed switch_off to shutdown to match the actual property name in StatusModel * Delete mistakingly added files * Delete mistakingly added files * Final touches * Final touches * Replace off with inactive * Rename low level parameetrs as well: switch_on -> activate switch_off -> deactivate * Rename low level parameetrs as well: switch_on -> activate switch_off -> deactivate state_variable -> state * Rename low level parameetrs as well: switch_on -> activate switch_off -> deactivate state_variable -> state * Docstring Improvements Summary ✅ All Parameters Now Documented Each primitive now has complete parameter documentation with: - Clear description of what each parameter does - Type expectations - Default values where applicable ✅ Focused on Math & Parameters Removed: - Excessive examples at low level - Use case lists that belong at higher levels Enhanced: - Mathematical formulations (using proper · symbol for multiplication) - Clear behavior descriptions - Precise return value documentation Updated Functions: ModelingPrimitives: 1. expression_tracking_variable - All 6 parameters documented - Clear math formulation 2. consecutive_duration_tracking - All 9 parameters documented - Explained Big-M value - Clear what constraints are returned 3. mutual_exclusivity_constraint - All 4 parameters documented - Simplified, focused on math BoundingPatterns: 4. basic_bounds - All 4 parameters documented - Concise formulation 5. bounds_with_state - All 5 parameters documented - Explained epsilon (ε) usage 6. scaled_bounds - All 5 parameters documented - Clear scaling relationship 7. scaled_bounds_with_state - All 7 parameters documented - Explained Big-M formulation 8. state_transition_bounds - All 7 parameters documented - Removed verbose examples, kept math focus 9. continuous_transition_bounds - All 8 parameters documented - Clear Big-M constraint explanation Result ✅ All parameters documented ✅ Math-focused docstrings ✅ Consistent format across all primitives ✅ Tests still passing The modeling primitives now have professional, complete documentation! * Update docs * Add missing type hints * Fix bullet points * Fix bullet points * Re-apply changes from main * Bugfix: Usage of old on_off_parameters * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Fix typos * Improve flagging of wether to create inactive varaible * Improve default upper bound of active_hours * Bugfix self._model.hours_per_step.sum('time').item() with scenarios/periods * Fix test * FIx names * pdate the test assertions to expect upper=total_hours instead of upper=inf when active_hours_max is not specified * Empty * Trigger CI * Fix test * Triggger CI * Summary of Fixes 1. Return type annotation for consecutive_duration_tracking (flixopt/modeling.py:255): - Changed from tuple[linopy.Variable, tuple[linopy.Constraint, linopy.Constraint, linopy.Constraint]] - To tuple[dict[str, linopy.Variable], dict[str, linopy.Constraint]] to match the actual return value 2. Clarified inactive property docstring (flixopt/features.py:284-291): - Replaced the confusing "deprecated" note with a clear explanation that: - The variable is only created when downtime tracking is enabled - Users should prefer 1 - status expression for general use 3. Fixed _get_previous_uptime docstring (flixopt/features.py:318-322): - Clarified that it returns 0 when no previous status is provided (assumes previously inactive) 4. Fixed _get_previous_downtime docstring (flixopt/features.py:329-333): - Clarified that it returns one timestep duration when no previous status is provided (assumes previously inactive) 5. No action needed for effects_per_startup without use_startup_tracking: - Verified that use_startup_tracking already returns True when effects_per_startup has values (line 1258 in interface.py), so this is already handled correctly 6. Test fixes (tests/test_flow.py): - Updated three test assertions to use model.hours_per_step.sum('time') as the expected upper bound for active_hours when active_hours_max is not specified * Trigger CI --- .github/workflows/tests.yaml | 2 +- CHANGELOG.md | 68 ++- docs/user-guide/core-concepts.md | 2 +- .../mathematical-notation/dimensions.md | 2 +- .../effects-penalty-objective.md | 4 + .../mathematical-notation/elements/Flow.md | 11 +- .../mathematical-notation/elements/Storage.md | 2 + .../features/InvestParameters.md | 8 + .../features/OnOffParameters.md | 307 ------------- .../features/StatusParameters.md | 317 +++++++++++++ .../user-guide/mathematical-notation/index.md | 8 +- .../modeling-patterns/bounds-and-states.md | 16 +- .../modeling-patterns/duration-tracking.md | 13 +- .../modeling-patterns/index.md | 4 +- .../modeling-patterns/state-transitions.md | 26 +- docs/user-guide/recipes/index.md | 2 +- examples/02_Complex/complex_example.py | 26 +- .../02_Complex/complex_example_results.py | 4 +- .../example_optimization_modes.py | 4 +- examples/04_Scenarios/scenario_example.py | 4 +- .../two_stage_optimization.py | 6 +- flixopt/__init__.py | 4 +- flixopt/components.py | 46 +- flixopt/elements.py | 201 +++++---- flixopt/features.py | 181 ++++---- flixopt/flow_system.py | 4 +- flixopt/interface.py | 233 +++++----- flixopt/linear_converters.py | 76 ++-- flixopt/modeling.py | 304 +++++++------ mkdocs.yml | 2 +- tests/conftest.py | 32 +- tests/test_component.py | 174 +++---- tests/test_flow.py | 426 +++++++++--------- tests/test_functional.py | 60 +-- tests/test_linear_converter.py | 68 +-- tests/test_scenarios.py | 20 +- tests/test_storage.py | 7 +- 37 files changed, 1413 insertions(+), 1261 deletions(-) create mode 100644 docs/user-guide/mathematical-notation/features/StatusParameters.md diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8fea07529..5fa245f49 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.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..653a2fb92 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,66 @@ 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. + + Note: + Only created when downtime tracking is enabled (min_downtime or max_downtime set). + For general use, prefer the expression `1 - status` instead of this variable. + """ + return self.get('inactive') @property - def switch_on(self) -> linopy.Variable | None: - """Switch on variable""" - return self.get('switch|on') + def startup(self) -> linopy.Variable | None: + """Startup variable""" + return self.get('startup') @property - def switch_off(self) -> linopy.Variable | None: - """Switch off variable""" - return self.get('switch|off') + def shutdown(self) -> linopy.Variable | None: + """Shutdown variable""" + return self.get('shutdown') @property - def switch_on_nr(self) -> linopy.Variable | None: - """Number of switch-ons variable""" - return self.get('switch|count') + def startup_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). + + Returns 0 if no previous status is provided (assumes previously inactive). + """ 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). + + Returns one timestep duration if no previous status is provided (assumes previously inactive). + """ 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..6b81a0a4a 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, @@ -246,29 +252,37 @@ def consecutive_duration_tracking( duration_dim: str = 'time', 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. + ) -> tuple[dict[str, linopy.Variable], dict[str, linopy.Constraint]]: + """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..0a1a03341 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') 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, ) From 80b3a882375ed9ea30c8ea082785dc41a9a83fca Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:21:47 +0100 Subject: [PATCH 06/11] =?UTF-8?q?=E2=8F=BA=20All=20deprecated=20items=20ha?= =?UTF-8?q?ve=20been=20removed.=20Here's=20a=20summary=20of=20what=20was?= =?UTF-8?q?=20done:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary of Changes for v5.0.0 Removed Modules - calculation.py: Entire module deleted (contained deprecated Calculation, FullCalculation, AggregatedCalculation, SegmentedCalculation classes) Removed from clustering.py - Aggregation class alias → Use Clustering - AggregationParameters class alias → Use ClusteringParameters - AggregationModel class alias → Use ClusteringModel Removed from core.py - aggregation_group parameter in TimeSeriesData.from_dataarray() → Use clustering_group - aggregation_weight parameter in TimeSeriesData.from_dataarray() → Use clustering_weight Removed from optimization.py - active_timesteps parameter and property from all optimization classes → Use flow_system.sel(time=...) or flow_system.isel(time=...) - _perform_aggregation() method → Use _perform_clustering() - calculate_aggregation_weights() method → Use calculate_clustering_weights() - Backward compatibility for aggregation_group/aggregation_weight attrs in calculate_clustering_weights() Removed from config.py - change_logging_level() function → Use CONFIG.Logging.enable_console() - DEPRECATION_REMOVAL_VERSION constant (no longer needed) Removed from flow_system.py - .all_elements property → Use dict-like interface - .weights property (getter/setter) → Use .scenario_weights - weights kwarg in __init__ → Use scenario_weights Removed from elements.py and flow_system.py - Support for passing Bus objects directly to Flow → Pass bus label string instead Removed from results.py - CalculationResults class → Use Results - SegmentedCalculationResults class → Use SegmentedResults - flow_system parameter in Results.__init__ → Use flow_system_data - indexer parameter in multiple methods → Use select - heatmap_timeframes, heatmap_timesteps_per_frame parameters → Use reshape_time - color_map parameter → Use colors Removed from effects.py - Support for Effect objects in EffectValues → Use effect label strings Removed from structure.py - _handle_deprecated_kwarg() helper method (no longer needed) Removed from __init__.py - Exports for deprecated classes (FullCalculation, AggregatedCalculation, SegmentedCalculation, AggregationParameters, change_logging_level) --- flixopt/__init__.py | 16 +-- flixopt/calculation.py | 177 -------------------------- flixopt/clustering.py | 39 ------ flixopt/config.py | 25 +--- flixopt/core.py | 35 +----- flixopt/effects.py | 23 ++-- flixopt/elements.py | 18 +-- flixopt/flow_system.py | 77 +----------- flixopt/optimization.py | 87 +------------ flixopt/results.py | 270 +--------------------------------------- flixopt/structure.py | 74 ----------- 11 files changed, 25 insertions(+), 816 deletions(-) delete mode 100644 flixopt/calculation.py diff --git a/flixopt/__init__.py b/flixopt/__init__.py index 0f8fc73e2..8874811b3 100644 --- a/flixopt/__init__.py +++ b/flixopt/__init__.py @@ -14,10 +14,7 @@ # Import commonly used classes and functions from . import linear_converters, plotting, results, solvers - -# Import old Calculation classes for backwards compatibility (deprecated) -from .calculation import AggregatedCalculation, FullCalculation, SegmentedCalculation -from .clustering import AggregationParameters, ClusteringParameters # AggregationParameters is deprecated +from .clustering import ClusteringParameters from .components import ( LinearConverter, Sink, @@ -26,20 +23,17 @@ Storage, Transmission, ) -from .config import CONFIG, change_logging_level +from .config import CONFIG from .core import TimeSeriesData from .effects import PENALTY_EFFECT_LABEL, Effect from .elements import Bus, Flow from .flow_system import FlowSystem from .interface import InvestParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects, StatusParameters - -# Import new Optimization classes from .optimization import ClusteredOptimization, Optimization, SegmentedOptimization __all__ = [ 'TimeSeriesData', 'CONFIG', - 'change_logging_level', 'Flow', 'Bus', 'Effect', @@ -51,14 +45,9 @@ 'LinearConverter', 'Transmission', 'FlowSystem', - # New Optimization classes (preferred) 'Optimization', 'ClusteredOptimization', 'SegmentedOptimization', - # Old Calculation classes (deprecated, for backwards compatibility) - 'FullCalculation', - 'AggregatedCalculation', - 'SegmentedCalculation', 'InvestParameters', 'StatusParameters', 'Piece', @@ -66,7 +55,6 @@ 'PiecewiseConversion', 'PiecewiseEffects', 'ClusteringParameters', - 'AggregationParameters', # Deprecated, use ClusteringParameters 'plotting', 'results', 'linear_converters', diff --git a/flixopt/calculation.py b/flixopt/calculation.py deleted file mode 100644 index 1211c6763..000000000 --- a/flixopt/calculation.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -This module provides backwards-compatible aliases for the renamed Optimization classes. - -DEPRECATED: This module is deprecated. Use the optimization module instead. -The following classes have been renamed: - - Calculation -> Optimization - - FullCalculation -> Optimization (now the standard, no "Full" prefix) - - AggregatedCalculation -> ClusteredOptimization - - SegmentedCalculation -> SegmentedOptimization - -Import from flixopt.optimization or use the new names from flixopt directly. -""" - -from __future__ import annotations - -import logging -import warnings -from typing import TYPE_CHECKING - -from .config import DEPRECATION_REMOVAL_VERSION -from .optimization import ( - ClusteredOptimization as _ClusteredOptimization, -) -from .optimization import ( - Optimization as _Optimization, -) -from .optimization import ( - SegmentedOptimization as _SegmentedOptimization, -) - -if TYPE_CHECKING: - import pathlib - from typing import Annotated - - import pandas as pd - - from .clustering import AggregationParameters - from .elements import Component - from .flow_system import FlowSystem - -logger = logging.getLogger('flixopt') - - -def _deprecation_warning(old_name: str, new_name: str): - """Issue a deprecation warning for renamed classes.""" - warnings.warn( - f'{old_name} is deprecated and will be removed in v{DEPRECATION_REMOVAL_VERSION}. Use {new_name} instead.', - DeprecationWarning, - stacklevel=3, - ) - - -class Calculation(_Optimization): - """ - DEPRECATED: Use Optimization instead. - - class for defined way of solving a flow_system optimization - - Args: - name: name of calculation - flow_system: flow_system which should be calculated - folder: folder where results should be saved. If None, then the current working directory is used. - normalize_weights: Whether to automatically normalize the weights of scenarios to sum up to 1 when solving. - active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead. - """ - - def __init__( - self, - name: str, - flow_system: FlowSystem, - active_timesteps: Annotated[ - pd.DatetimeIndex | None, - 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', - ] = None, - folder: pathlib.Path | None = None, - normalize_weights: bool = True, - ): - _deprecation_warning('Calculation', 'Optimization') - super().__init__(name, flow_system, active_timesteps, folder, normalize_weights) - - -class FullCalculation(_Optimization): - """ - DEPRECATED: Use Optimization instead (the "Full" prefix has been removed). - - FullCalculation solves the complete optimization problem using all time steps. - - This is the most comprehensive calculation type that considers every time step - in the optimization, providing the most accurate but computationally intensive solution. - - Args: - name: name of calculation - flow_system: flow_system which should be calculated - folder: folder where results should be saved. If None, then the current working directory is used. - normalize_weights: Whether to automatically normalize the weights of scenarios to sum up to 1 when solving. - active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead. - """ - - def __init__( - self, - name: str, - flow_system: FlowSystem, - active_timesteps: Annotated[ - pd.DatetimeIndex | None, - 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', - ] = None, - folder: pathlib.Path | None = None, - normalize_weights: bool = True, - ): - _deprecation_warning('FullCalculation', 'Optimization') - super().__init__(name, flow_system, active_timesteps, folder, normalize_weights) - - -class AggregatedCalculation(_ClusteredOptimization): - """ - DEPRECATED: Use ClusteredOptimization instead. - - AggregatedCalculation reduces computational complexity by clustering time series into typical periods. - - This calculation approach aggregates time series data using clustering techniques (tsam) to identify - representative time periods, significantly reducing computation time while maintaining solution accuracy. - - Args: - name: Name of the calculation - flow_system: FlowSystem to be optimized - aggregation_parameters: Parameters for aggregation. See AggregationParameters class documentation - components_to_clusterize: list of Components to perform aggregation on. If None, all components are aggregated. - This equalizes variables in the components according to the typical periods computed in the aggregation - active_timesteps: DatetimeIndex of timesteps to use for optimization. If None, all timesteps are used - folder: Folder where results should be saved. If None, current working directory is used - """ - - def __init__( - self, - name: str, - flow_system: FlowSystem, - aggregation_parameters: AggregationParameters, - components_to_clusterize: list[Component] | None = None, - active_timesteps: Annotated[ - pd.DatetimeIndex | None, - 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', - ] = None, - folder: pathlib.Path | None = None, - ): - _deprecation_warning('AggregatedCalculation', 'ClusteredOptimization') - super().__init__(name, flow_system, aggregation_parameters, components_to_clusterize, active_timesteps, folder) - - -class SegmentedCalculation(_SegmentedOptimization): - """ - DEPRECATED: Use SegmentedOptimization instead. - - Solve large optimization problems by dividing time horizon into (overlapping) segments. - - Args: - name: Unique identifier for the calculation, used in result files and logging. - flow_system: The FlowSystem to optimize, containing all components, flows, and buses. - timesteps_per_segment: Number of timesteps in each segment (excluding overlap). - overlap_timesteps: Number of additional timesteps added to each segment. - nr_of_previous_values: Number of previous timestep values to transfer between segments for initialization. - folder: Directory for saving results. Defaults to current working directory + 'results'. - """ - - def __init__( - self, - name: str, - flow_system: FlowSystem, - timesteps_per_segment: int, - overlap_timesteps: int, - nr_of_previous_values: int = 1, - folder: pathlib.Path | None = None, - ): - _deprecation_warning('SegmentedCalculation', 'SegmentedOptimization') - super().__init__(name, flow_system, timesteps_per_segment, overlap_timesteps, nr_of_previous_values, folder) - - -__all__ = ['Calculation', 'FullCalculation', 'AggregatedCalculation', 'SegmentedCalculation'] diff --git a/flixopt/clustering.py b/flixopt/clustering.py index 2fbd65318..1c6f7511b 100644 --- a/flixopt/clustering.py +++ b/flixopt/clustering.py @@ -9,13 +9,10 @@ import logging import pathlib import timeit -import warnings as _warnings from typing import TYPE_CHECKING import numpy as np -from .config import DEPRECATION_REMOVAL_VERSION - try: import tsam.timeseriesaggregation as tsam @@ -401,39 +398,3 @@ def _equate_indices(self, variable: linopy.Variable, indices: tuple[np.ndarray, var_k0.sum(dim='time') + var_k1.sum(dim='time') <= limit, short_name=f'limit_corrections|{variable.name}', ) - - -# ===== Deprecated aliases for backward compatibility ===== - - -def _create_deprecation_warning(old_name: str, new_name: str): - """Helper to create a deprecation warning""" - _warnings.warn( - f"'{old_name}' is deprecated and will be removed in v{DEPRECATION_REMOVAL_VERSION}. Use '{new_name}' instead.", - DeprecationWarning, - stacklevel=3, - ) - - -class Aggregation(Clustering): - """Deprecated: Use Clustering instead.""" - - def __init__(self, *args, **kwargs): - _create_deprecation_warning('Aggregation', 'Clustering') - super().__init__(*args, **kwargs) - - -class AggregationParameters(ClusteringParameters): - """Deprecated: Use ClusteringParameters instead.""" - - def __init__(self, *args, **kwargs): - _create_deprecation_warning('AggregationParameters', 'ClusteringParameters') - super().__init__(*args, **kwargs) - - -class AggregationModel(ClusteringModel): - """Deprecated: Use ClusteringModel instead.""" - - def __init__(self, *args, **kwargs): - _create_deprecation_warning('AggregationModel', 'ClusteringModel') - super().__init__(*args, **kwargs) diff --git a/flixopt/config.py b/flixopt/config.py index dbe2bf3c5..60d3cbb96 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -20,7 +20,7 @@ COLORLOG_AVAILABLE = False escape_codes = None -__all__ = ['CONFIG', 'change_logging_level', 'MultilineFormatter', 'SUCCESS_LEVEL'] +__all__ = ['CONFIG', 'MultilineFormatter', 'SUCCESS_LEVEL'] if COLORLOG_AVAILABLE: __all__.append('ColoredMultilineFormatter') @@ -29,9 +29,6 @@ SUCCESS_LEVEL = 25 logging.addLevelName(SUCCESS_LEVEL, 'SUCCESS') -# Deprecation removal version - update this when planning the next major version -DEPRECATION_REMOVAL_VERSION = '5.0.0' - class MultilineFormatter(logging.Formatter): """Custom formatter that handles multi-line messages with box-style borders. @@ -808,23 +805,3 @@ def _apply_config_dict(cls, config_dict: dict) -> None: elif hasattr(cls, key) and key != 'logging': # Skip 'logging' as it requires special handling via CONFIG.Logging methods setattr(cls, key, value) - - -def change_logging_level(level_name: str | int) -> None: - """Change the logging level for the flixopt logger. - - Args: - level_name: The logging level to set (DEBUG, INFO, WARNING, ERROR, CRITICAL or logging constant). - - Examples: - >>> change_logging_level('DEBUG') # deprecated - >>> # Use this instead: - >>> CONFIG.Logging.enable_console('DEBUG') - """ - warnings.warn( - f'change_logging_level is deprecated and will be removed in version {DEPRECATION_REMOVAL_VERSION} ' - 'Use CONFIG.Logging.enable_console(level) instead.', - DeprecationWarning, - stacklevel=2, - ) - CONFIG.Logging.enable_console(level_name) diff --git a/flixopt/core.py b/flixopt/core.py index f2c2c049a..a14aa6654 100644 --- a/flixopt/core.py +++ b/flixopt/core.py @@ -4,7 +4,6 @@ """ import logging -import warnings from itertools import permutations from typing import Any, Literal @@ -12,7 +11,6 @@ import pandas as pd import xarray as xr -from .config import DEPRECATION_REMOVAL_VERSION from .types import NumericOrBool logger = logging.getLogger('flixopt') @@ -101,40 +99,11 @@ def from_dataarray( da: xr.DataArray, clustering_group: str | None = None, clustering_weight: float | None = None, - aggregation_group: str | None = None, - aggregation_weight: float | None = None, ): """Create TimeSeriesData from DataArray, extracting metadata from attrs.""" - # Handle deprecated parameters - if aggregation_group is not None: - warnings.warn( - f'aggregation_group is deprecated, use clustering_group instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - if clustering_group is None: - clustering_group = aggregation_group - if aggregation_weight is not None: - warnings.warn( - f'aggregation_weight is deprecated, use clustering_weight instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - if clustering_weight is None: - clustering_weight = aggregation_weight - - # Get clustering metadata from attrs or parameters (try both old and new attrs keys for backward compat) - final_clustering_group = ( - clustering_group - if clustering_group is not None - else da.attrs.get('clustering_group', da.attrs.get('aggregation_group')) - ) + final_clustering_group = clustering_group if clustering_group is not None else da.attrs.get('clustering_group') final_clustering_weight = ( - clustering_weight - if clustering_weight is not None - else da.attrs.get('clustering_weight', da.attrs.get('aggregation_weight')) + clustering_weight if clustering_weight is not None else da.attrs.get('clustering_weight') ) return cls(da, clustering_group=final_clustering_group, clustering_weight=final_clustering_weight) diff --git a/flixopt/effects.py b/flixopt/effects.py index 124504c67..9df7c2ce5 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -8,7 +8,6 @@ from __future__ import annotations import logging -import warnings from collections import deque from typing import TYPE_CHECKING, Literal @@ -16,7 +15,6 @@ import numpy as np import xarray as xr -from .config import DEPRECATION_REMOVAL_VERSION from .core import PlausibilityError from .features import ShareAllocationModel from .structure import Element, ElementContainer, ElementModel, FlowSystemModel, Submodel, register_class_for_io @@ -468,21 +466,16 @@ def create_effect_values_dict(self, effect_values_user: Numeric_TPS | Effect_TPS Note: a standard effect must be defined when passing scalars or None labels. """ - def get_effect_label(eff: Effect | str) -> str: - """Temporary function to get the label of an effect and warn for deprecation""" + def get_effect_label(eff: str | None) -> str: + """Get the label of an effect""" + if eff is None: + return self.standard_effect.label if isinstance(eff, Effect): - warnings.warn( - f'The use of effect objects when specifying EffectValues is deprecated. ' - f'Use the label of the effect instead. Used effect: {eff.label_full}. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - UserWarning, - stacklevel=2, + raise TypeError( + f'Effect objects are no longer accepted when specifying EffectValues. ' + f'Use the label string instead. Got: {eff.label_full}' ) - return eff.label - elif eff is None: - return self.standard_effect.label - else: - return eff + return eff if effect_values_user is None: return None diff --git a/flixopt/elements.py b/flixopt/elements.py index f12dae4c4..ae31f34c6 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -5,14 +5,13 @@ from __future__ import annotations import logging -import warnings from typing import TYPE_CHECKING import numpy as np import xarray as xr from . import io as fx_io -from .config import CONFIG, DEPRECATION_REMOVAL_VERSION +from .config import CONFIG from .core import PlausibilityError from .features import InvestmentModel, StatusModel from .interface import InvestParameters, StatusParameters @@ -486,18 +485,11 @@ def __init__( self.component: str = 'UnknownComponent' self.is_input_in_component: bool | None = None if isinstance(bus, Bus): - self.bus = bus.label_full - warnings.warn( - f'Bus {bus.label} is passed as a Bus object to {self.label}. This is deprecated and will be removed ' - f'in the future. Add the Bus to the FlowSystem instead and pass its label to the Flow. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - UserWarning, - stacklevel=1, + raise TypeError( + f'Bus {bus.label} is passed as a Bus object to Flow {self.label}. ' + f'This is no longer supported. Add the Bus to the FlowSystem and pass its label (string) to the Flow.' ) - self._bus_object = bus - else: - self.bus = bus - self._bus_object = None + self.bus = bus def create_model(self, model: FlowSystemModel) -> FlowModel: self._plausibility_checks() diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index c0deaa1ca..98d4c5c0b 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -15,7 +15,7 @@ import xarray as xr from . import io as fx_io -from .config import CONFIG, DEPRECATION_REMOVAL_VERSION +from .config import CONFIG from .core import ( ConversionError, DataConverter, @@ -142,9 +142,6 @@ class FlowSystem(Interface, CompositeContainerMixin[Element]): (components, buses, effects, flows) to find the element with the matching label. - Element labels must be unique across all container types. Attempting to add elements with duplicate labels will raise an error, ensuring each label maps to exactly one element. - - The `.all_elements` property is deprecated. Use the dict-like interface instead: - `flow_system['element']`, `'element' in flow_system`, `flow_system.keys()`, - `flow_system.values()`, or `flow_system.items()`. - Direct container access (`.components`, `.buses`, `.effects`, `.flows`) is useful when you need type-specific filtering or operations. - The `.flows` container is automatically populated from all component inputs and outputs. @@ -166,18 +163,7 @@ def __init__( scenario_weights: Numeric_S | None = None, scenario_independent_sizes: bool | list[str] = True, scenario_independent_flow_rates: bool | list[str] = False, - **kwargs, ): - scenario_weights = self._handle_deprecated_kwarg( - kwargs, - 'weights', - 'scenario_weights', - scenario_weights, - check_conflict=True, - additional_warning_message='This might lead to later errors if your custom weights used the period dimension.', - ) - self._validate_kwargs(kwargs) - self.timesteps = self._validate_timesteps(timesteps) # Compute all time-related metadata using shared helper @@ -990,18 +976,6 @@ def _connect_network(self): flow.component = component.label_full flow.is_input_in_component = True if flow in component.inputs else False - # Add Bus if not already added (deprecated) - if flow._bus_object is not None and flow._bus_object.label_full not in self.buses: - warnings.warn( - f'The Bus {flow._bus_object.label_full} was added to the FlowSystem from {flow.label_full}.' - f'This is deprecated and will be removed in the future. ' - f'Please pass the Bus.label to the Flow and the Bus to the FlowSystem instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=1, - ) - self._add_buses(flow._bus_object) - # Connect Buses bus = self.buses.get(flow.bus) if bus is None: @@ -1093,29 +1067,6 @@ def flows(self) -> ElementContainer[Flow]: self._flows_cache = ElementContainer(flows, element_type_name='flows', truncate_repr=10) return self._flows_cache - @property - def all_elements(self) -> dict[str, Element]: - """ - Get all elements as a dictionary. - - .. deprecated:: 3.2.0 - Use dict-like interface instead: `flow_system['element']`, `'element' in flow_system`, - `flow_system.keys()`, `flow_system.values()`, or `flow_system.items()`. - This property will be removed in v4.0.0. - - Returns: - Dictionary mapping element labels to element objects. - """ - warnings.warn( - "The 'all_elements' property is deprecated. Use dict-like interface instead: " - "flow_system['element'], 'element' in flow_system, flow_system.keys(), " - 'flow_system.values(), or flow_system.items(). ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - return {**self.components, **self.effects, **self.flows, **self.buses} - @property def coords(self) -> dict[FlowSystemDimensions, pd.Index]: active_coords = {'time': self.timesteps} @@ -1163,32 +1114,6 @@ def scenario_weights(self, value: Numeric_S | None) -> None: self._scenario_weights = self.fit_to_model_coords('scenario_weights', value, dims=['scenario']) - @property - def weights(self) -> Numeric_S | None: - warnings.warn( - f'FlowSystem.weights is deprecated. Use FlowSystem.scenario_weights instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - return self.scenario_weights - - @weights.setter - def weights(self, value: Numeric_S) -> None: - """ - Set weights (deprecated - sets scenario_weights). - - Args: - value: Scenario weights to set - """ - warnings.warn( - f'Setting FlowSystem.weights is deprecated. Set FlowSystem.scenario_weights instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - self.scenario_weights = value # Use the scenario_weights setter - def _validate_scenario_parameter(self, value: bool | list[str], param_name: str, element_type: str) -> None: """ Validate scenario parameter value. diff --git a/flixopt/optimization.py b/flixopt/optimization.py index e537029d7..1de1bbc49 100644 --- a/flixopt/optimization.py +++ b/flixopt/optimization.py @@ -15,9 +15,8 @@ import pathlib import sys import timeit -import warnings from collections import Counter -from typing import TYPE_CHECKING, Annotated, Any, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable import numpy as np from tqdm import tqdm @@ -26,7 +25,7 @@ from .clustering import Clustering, ClusteringModel, ClusteringParameters from .components import Storage from .config import CONFIG, SUCCESS_LEVEL -from .core import DEPRECATION_REMOVAL_VERSION, DataConverter, TimeSeriesData, drop_constant_arrays +from .core import DataConverter, TimeSeriesData, drop_constant_arrays from .effects import PENALTY_EFFECT_LABEL from .features import InvestmentModel from .flow_system import FlowSystem @@ -85,7 +84,6 @@ def _initialize_optimization_common( obj: Any, name: str, flow_system: FlowSystem, - active_timesteps: pd.DatetimeIndex | None = None, folder: pathlib.Path | None = None, normalize_weights: bool = True, ) -> None: @@ -99,7 +97,6 @@ def _initialize_optimization_common( obj: The optimization object being initialized name: Name of the optimization flow_system: FlowSystem to optimize - active_timesteps: DEPRECATED. Use flow_system.sel(time=...) instead folder: Directory for saving results normalize_weights: Whether to normalize scenario weights """ @@ -112,17 +109,6 @@ def _initialize_optimization_common( ) flow_system = flow_system.copy() - if active_timesteps is not None: - warnings.warn( - f"The 'active_timesteps' parameter is deprecated and will be removed in v{DEPRECATION_REMOVAL_VERSION}. " - 'Use flow_system.sel(time=timesteps) or flow_system.isel(time=indices) before passing ' - 'the FlowSystem to the Optimization instead.', - DeprecationWarning, - stacklevel=2, - ) - flow_system = flow_system.sel(time=active_timesteps) - - obj._active_timesteps = active_timesteps # deprecated obj.normalize_weights = normalize_weights flow_system._used_in_optimization = True @@ -155,7 +141,6 @@ class Optimization: flow_system: flow_system which should be optimized folder: folder where results should be saved. If None, then the current working directory is used. normalize_weights: Whether to automatically normalize the weights of scenarios to sum up to 1 when solving. - active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead. Examples: Basic usage: @@ -182,10 +167,6 @@ def __init__( self, name: str, flow_system: FlowSystem, - active_timesteps: Annotated[ - pd.DatetimeIndex | None, - 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', - ] = None, folder: pathlib.Path | None = None, normalize_weights: bool = True, ): @@ -193,7 +174,6 @@ def __init__( self, name=name, flow_system=flow_system, - active_timesteps=active_timesteps, folder=folder, normalize_weights=normalize_weights, ) @@ -360,16 +340,6 @@ def summary(self): 'Config': CONFIG.to_dict(), } - @property - def active_timesteps(self) -> pd.DatetimeIndex | None: - warnings.warn( - f'active_timesteps is deprecated. Use flow_system.sel(time=...) or flow_system.isel(time=...) instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - return self._active_timesteps - @property def modeled(self) -> bool: return True if self.model is not None else False @@ -393,7 +363,6 @@ class ClusteredOptimization(Optimization): clustering_parameters: Parameters for clustering. See ClusteringParameters class documentation components_to_clusterize: list of Components to perform aggregation on. If None, all components are aggregated. This equalizes variables in the components according to the typical periods computed in the aggregation - active_timesteps: DatetimeIndex of timesteps to use for optimization. If None, all timesteps are used folder: Folder where results should be saved. If None, current working directory is used normalize_weights: Whether to automatically normalize the weights of scenarios to sum up to 1 when solving @@ -408,10 +377,6 @@ def __init__( flow_system: FlowSystem, clustering_parameters: ClusteringParameters, components_to_clusterize: list[Component] | None = None, - active_timesteps: Annotated[ - pd.DatetimeIndex | None, - 'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead', - ] = None, folder: pathlib.Path | None = None, normalize_weights: bool = True, ): @@ -422,7 +387,6 @@ def __init__( super().__init__( name=name, flow_system=flow_system, - active_timesteps=active_timesteps, folder=folder, normalize_weights=normalize_weights, ) @@ -503,26 +467,10 @@ def _perform_clustering(self): self.flow_system.connect_and_transform() self.durations['clustering'] = round(timeit.default_timer() - t_start_agg, 2) - def _perform_aggregation(self): - """Deprecated: Use _perform_clustering instead.""" - warnings.warn( - f'_perform_aggregation is deprecated, use _perform_clustering instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - return self._perform_clustering() - @classmethod def calculate_clustering_weights(cls, ds: xr.Dataset) -> dict[str, float]: """Calculate weights for all datavars in the dataset. Weights are pulled from the attrs of the datavars.""" - - # Support both old and new attr names for backward compatibility - groups = [ - da.attrs.get('clustering_group', da.attrs.get('aggregation_group')) - for da in ds.data_vars.values() - if 'clustering_group' in da.attrs or 'aggregation_group' in da.attrs - ] + groups = [da.attrs.get('clustering_group') for da in ds.data_vars.values() if 'clustering_group' in da.attrs] group_counts = Counter(groups) # Calculate weight for each group (1/count) @@ -530,31 +478,18 @@ def calculate_clustering_weights(cls, ds: xr.Dataset) -> dict[str, float]: weights = {} for name, da in ds.data_vars.items(): - # Try both old and new attr names - clustering_group = da.attrs.get('clustering_group', da.attrs.get('aggregation_group')) + clustering_group = da.attrs.get('clustering_group') group_weight = group_weights.get(clustering_group) if group_weight is not None: weights[name] = group_weight else: - # Try both old and new attr names for weight - weights[name] = da.attrs.get('clustering_weight', da.attrs.get('aggregation_weight', 1)) + weights[name] = da.attrs.get('clustering_weight', 1) if np.all(np.isclose(list(weights.values()), 1, atol=1e-6)): logger.info('All Clustering weights were set to 1') return weights - @classmethod - def calculate_aggregation_weights(cls, ds: xr.Dataset) -> dict[str, float]: - """Deprecated: Use calculate_clustering_weights instead.""" - warnings.warn( - f'calculate_aggregation_weights is deprecated, use calculate_clustering_weights instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - return cls.calculate_clustering_weights(ds) - class SegmentedOptimization: """Solve large optimization problems by dividing time horizon into (overlapping) segments. @@ -673,7 +608,6 @@ class SegmentedOptimization: durations: dict[str, float] model: None # SegmentedOptimization doesn't use a single model normalize_weights: bool - _active_timesteps: pd.DatetimeIndex | None def __init__( self, @@ -688,7 +622,6 @@ def __init__( self, name=name, flow_system=flow_system, - active_timesteps=None, folder=folder, ) self.timesteps_per_segment = timesteps_per_segment @@ -977,13 +910,3 @@ def summary(self): 'Durations': self.durations, 'Config': CONFIG.to_dict(), } - - @property - def active_timesteps(self) -> pd.DatetimeIndex | None: - warnings.warn( - f'active_timesteps is deprecated. Use flow_system.sel(time=...) or flow_system.isel(time=...) instead. ' - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - return self._active_timesteps diff --git a/flixopt/results.py b/flixopt/results.py index 6b9a1c580..f3d0c19a9 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -15,7 +15,7 @@ from . import io as fx_io from . import plotting from .color_processing import process_colors -from .config import CONFIG, DEPRECATION_REMOVAL_VERSION, SUCCESS_LEVEL +from .config import CONFIG, SUCCESS_LEVEL from .flow_system import FlowSystem from .structure import CompositeContainerMixin, ResultsContainer @@ -209,7 +209,6 @@ def __init__( summary: dict, folder: pathlib.Path | None = None, model: linopy.Model | None = None, - **kwargs, # To accept old "flow_system" parameter ): """Initialize Results with optimization data. Usually, this class is instantiated by an Optimization object via `Results.from_optimization()` @@ -222,28 +221,7 @@ def __init__( summary: Optimization metadata. folder: Results storage folder. model: Linopy optimization model. - Deprecated: - flow_system: Use flow_system_data instead. - - Note: - The legacy alias `CalculationResults` is deprecated. Use `Results` instead. """ - # Handle potential old "flow_system" parameter for backward compatibility - if 'flow_system' in kwargs and flow_system_data is None: - flow_system_data = kwargs.pop('flow_system') - warnings.warn( - "The 'flow_system' parameter is deprecated. Use 'flow_system_data' instead. " - "Access is now via '.flow_system_data', while '.flow_system' returns the restored FlowSystem. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - - # Validate that flow_system_data is provided - if flow_system_data is None: - raise TypeError( - "flow_system_data is required (or use deprecated 'flow_system' for backward compatibility)." - ) self.solution = solution self.flow_system_data = flow_system_data @@ -909,11 +887,6 @@ def plot_heatmap( | Literal['auto'] | None = 'auto', fill: Literal['ffill', 'bfill'] | None = 'ffill', - # Deprecated parameters (kept for backwards compatibility) - indexer: dict[FlowSystemDimensions, Any] | None = None, - heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, - heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, - color_map: str | None = None, **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """ @@ -1030,10 +1003,6 @@ def plot_heatmap( facet_cols=facet_cols, reshape_time=reshape_time, fill=fill, - indexer=indexer, - heatmap_timeframes=heatmap_timeframes, - heatmap_timesteps_per_frame=heatmap_timesteps_per_frame, - color_map=color_map, **plot_kwargs, ) @@ -1122,39 +1091,6 @@ def to_file( logger.log(SUCCESS_LEVEL, f'Saved optimization results "{name}" to {paths.model_documentation.parent}') -class CalculationResults(Results): - """DEPRECATED: Use Results instead. - - Backwards-compatible alias for Results class. - All functionality is inherited from Results. - """ - - def __init__(self, *args, **kwargs): - # Only warn if directly instantiating CalculationResults (not subclasses) - if self.__class__.__name__ == 'CalculationResults': - warnings.warn( - f'CalculationResults is deprecated and will be removed in v{DEPRECATION_REMOVAL_VERSION}. Use Results instead.', - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - @classmethod - def from_calculation(cls, calculation: Optimization) -> CalculationResults: - """Create CalculationResults from a Calculation object. - - DEPRECATED: Use Results.from_optimization() instead. - Backwards-compatible method that redirects to from_optimization(). - - Args: - calculation: Calculation object with solved model. - - Returns: - CalculationResults: New instance with extracted results. - """ - return cls.from_optimization(calculation) - - class _ElementResults: def __init__(self, results: Results, label: str, variables: list[str], constraints: list[str]): self._results = results @@ -1265,8 +1201,6 @@ def plot_node_balance( facet_by: str | list[str] | None = 'scenario', animate_by: str | None = 'period', facet_cols: int | None = None, - # Deprecated parameter (kept for backwards compatibility) - indexer: dict[FlowSystemDimensions, Any] | None = None, **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """ @@ -1367,22 +1301,6 @@ def plot_node_balance( >>> fig.update_layout(template='plotly_dark', width=1200, height=600) >>> fig.show() """ - # Handle deprecated indexer parameter - if indexer is not None: - # Check for conflict with new parameter - if select is not None: - raise ValueError( - "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." - ) - - warnings.warn( - f"The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - select = indexer - if engine not in {'plotly', 'matplotlib'}: raise ValueError(f'Engine "{engine}" not supported. Use one of ["plotly", "matplotlib"]') @@ -1450,8 +1368,6 @@ def plot_node_balance_pie( show: bool | None = None, engine: plotting.PlottingEngine = 'plotly', select: dict[FlowSystemDimensions, Any] | None = None, - # Deprecated parameter (kept for backwards compatibility) - indexer: dict[FlowSystemDimensions, Any] | None = None, **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, list[plt.Axes]]: """Plot pie chart of flow hours distribution. @@ -1501,22 +1417,6 @@ def plot_node_balance_pie( >>> results['Bus'].plot_node_balance_pie(save='figure.png', dpi=600) """ - # Handle deprecated indexer parameter - if indexer is not None: - # Check for conflict with new parameter - if select is not None: - raise ValueError( - "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." - ) - - warnings.warn( - f"The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - select = indexer - # Extract dpi for export_figure dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi @@ -1624,8 +1524,6 @@ def node_balance( unit_type: Literal['flow_rate', 'flow_hours'] = 'flow_rate', drop_suffix: bool = False, select: dict[FlowSystemDimensions, Any] | None = None, - # Deprecated parameter (kept for backwards compatibility) - indexer: dict[FlowSystemDimensions, Any] | None = None, ) -> xr.Dataset: """ Returns a dataset with the node balance of the Component or Bus. @@ -1640,22 +1538,6 @@ def node_balance( drop_suffix: Whether to drop the suffix from the variable names. select: Optional data selection dict. Supports single values, lists, slices, and index arrays. """ - # Handle deprecated indexer parameter - if indexer is not None: - # Check for conflict with new parameter - if select is not None: - raise ValueError( - "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." - ) - - warnings.warn( - f"The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - select = indexer - ds = self.solution[self.inputs + self.outputs] ds = sanitize_dataset( @@ -1716,8 +1598,6 @@ def plot_charge_state( facet_by: str | list[str] | None = 'scenario', animate_by: str | None = 'period', facet_cols: int | None = None, - # Deprecated parameter (kept for backwards compatibility) - indexer: dict[FlowSystemDimensions, Any] | None = None, **plot_kwargs: Any, ) -> plotly.graph_objs.Figure: """Plot storage charge state over time, combined with the node balance with optional faceting and animation. @@ -1786,22 +1666,6 @@ def plot_charge_state( >>> results['Storage'].plot_charge_state(save='storage.png', dpi=600) """ - # Handle deprecated indexer parameter - if indexer is not None: - # Check for conflict with new parameter - if select is not None: - raise ValueError( - "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." - ) - - warnings.warn( - f"The 'indexer' parameter is deprecated and will be removed in a future version. Use 'select' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - select = indexer - # Extract dpi for export_figure dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi @@ -2280,10 +2144,6 @@ def plot_heatmap( animate_by: str | None = None, facet_cols: int | None = None, fill: Literal['ffill', 'bfill'] | None = 'ffill', - # Deprecated parameters (kept for backwards compatibility) - heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, - heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, - color_map: str | None = None, **plot_kwargs: Any, ) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]: """Plot heatmap of variable solution across segments. @@ -2302,9 +2162,6 @@ def plot_heatmap( animate_by: Dimension to animate over (Plotly only). facet_cols: Number of columns in the facet grid layout. fill: Method to fill missing values: 'ffill' or 'bfill'. - heatmap_timeframes: (Deprecated) Use reshape_time instead. - heatmap_timesteps_per_frame: (Deprecated) Use reshape_time instead. - color_map: (Deprecated) Use colors instead. **plot_kwargs: Additional plotting customization options. Common options: @@ -2320,41 +2177,6 @@ def plot_heatmap( Returns: Figure object. """ - # Handle deprecated parameters - if heatmap_timeframes is not None or heatmap_timesteps_per_frame is not None: - # Check for conflict with new parameter - if reshape_time != 'auto': # Check if user explicitly set reshape_time - raise ValueError( - "Cannot use both deprecated parameters 'heatmap_timeframes'/'heatmap_timesteps_per_frame' " - "and new parameter 'reshape_time'. Use only 'reshape_time'." - ) - - warnings.warn( - "The 'heatmap_timeframes' and 'heatmap_timesteps_per_frame' parameters are deprecated. " - f"Use 'reshape_time=(timeframes, timesteps_per_frame)' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - # Override reshape_time if old parameters provided - if heatmap_timeframes is not None and heatmap_timesteps_per_frame is not None: - reshape_time = (heatmap_timeframes, heatmap_timesteps_per_frame) - - if color_map is not None: - # Check for conflict with new parameter - if colors is not None: # Check if user explicitly set colors - raise ValueError( - "Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'." - ) - - warnings.warn( - f"The 'color_map' parameter is deprecated. Use 'colors' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - colors = color_map - return plot_heatmap( data=self.solution_without_overlap(variable_name), name=variable_name, @@ -2412,40 +2234,6 @@ def to_file( logger.info(f'Saved optimization "{name}" to {path}') -class SegmentedCalculationResults(SegmentedResults): - """DEPRECATED: Use SegmentedResults instead. - - Backwards-compatible alias for SegmentedResults class. - All functionality is inherited from SegmentedResults. - """ - - def __init__(self, *args, **kwargs): - # Only warn if directly instantiating SegmentedCalculationResults (not subclasses) - if self.__class__.__name__ == 'SegmentedCalculationResults': - warnings.warn( - f'SegmentedCalculationResults is deprecated and will be removed in v{DEPRECATION_REMOVAL_VERSION}. ' - 'Use SegmentedResults instead.', - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - @classmethod - def from_calculation(cls, calculation: SegmentedOptimization) -> SegmentedCalculationResults: - """Create SegmentedCalculationResults from a SegmentedCalculation object. - - DEPRECATED: Use SegmentedResults.from_optimization() instead. - Backwards-compatible method that redirects to from_optimization(). - - Args: - calculation: SegmentedCalculation object with solved model. - - Returns: - SegmentedCalculationResults: New instance with extracted results. - """ - return cls.from_optimization(calculation) - - def plot_heatmap( data: xr.DataArray | xr.Dataset, name: str | None = None, @@ -2462,11 +2250,6 @@ def plot_heatmap( | Literal['auto'] | None = 'auto', fill: Literal['ffill', 'bfill'] | None = 'ffill', - # Deprecated parameters (kept for backwards compatibility) - indexer: dict[str, Any] | None = None, - heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None, - heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None, - color_map: str | None = None, **plot_kwargs: Any, ): """Plot heatmap visualization with support for multi-variable, faceting, and animation. @@ -2515,57 +2298,6 @@ def plot_heatmap( >>> plot_heatmap(dataset, animate_by='variable', reshape_time=('D', 'h')) """ - # Handle deprecated heatmap time parameters - if heatmap_timeframes is not None or heatmap_timesteps_per_frame is not None: - # Check for conflict with new parameter - if reshape_time != 'auto': # User explicitly set reshape_time - raise ValueError( - "Cannot use both deprecated parameters 'heatmap_timeframes'/'heatmap_timesteps_per_frame' " - "and new parameter 'reshape_time'. Use only 'reshape_time'." - ) - - warnings.warn( - "The 'heatmap_timeframes' and 'heatmap_timesteps_per_frame' parameters are deprecated. " - "Use 'reshape_time=(timeframes, timesteps_per_frame)' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - # Override reshape_time if both old parameters provided - if heatmap_timeframes is not None and heatmap_timesteps_per_frame is not None: - reshape_time = (heatmap_timeframes, heatmap_timesteps_per_frame) - - # Handle deprecated color_map parameter - if color_map is not None: - if colors is not None: # User explicitly set colors - raise ValueError( - "Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'." - ) - - warnings.warn( - f"The 'color_map' parameter is deprecated. Use 'colors' instead." - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - colors = color_map - - # Handle deprecated indexer parameter - if indexer is not None: - # Check for conflict with new parameter - if select is not None: # User explicitly set select - raise ValueError( - "Cannot use both deprecated parameter 'indexer' and new parameter 'select'. Use only 'select'." - ) - - warnings.warn( - f"The 'indexer' parameter is deprecated. Use 'select' instead. " - f'Will be removed in v{DEPRECATION_REMOVAL_VERSION}.', - DeprecationWarning, - stacklevel=2, - ) - select = indexer - # Convert Dataset to DataArray with 'variable' dimension if isinstance(data, xr.Dataset): # Extract all data variables from the Dataset diff --git a/flixopt/structure.py b/flixopt/structure.py index 62067e2ba..1e52e521e 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -24,7 +24,6 @@ import xarray as xr from . import io as fx_io -from .config import DEPRECATION_REMOVAL_VERSION from .core import FlowSystemDimensions, TimeSeriesData, get_dataarray_stats if TYPE_CHECKING: # for type checking and preventing circular imports @@ -514,79 +513,6 @@ def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> tuple[An else: return self._serialize_to_basic_types(obj), extracted_arrays - def _handle_deprecated_kwarg( - self, - kwargs: dict, - old_name: str, - new_name: str, - current_value: Any = None, - transform: callable = None, - check_conflict: bool = True, - additional_warning_message: str = '', - ) -> Any: - """ - Handle a deprecated keyword argument by issuing a warning and returning the appropriate value. - - This centralizes the deprecation pattern used across multiple classes (Source, Sink, InvestParameters, etc.). - - Args: - kwargs: Dictionary of keyword arguments to check and modify - old_name: Name of the deprecated parameter - new_name: Name of the replacement parameter - current_value: Current value of the new parameter (if already set) - transform: Optional callable to transform the old value before returning (e.g., lambda x: [x] to wrap in list) - check_conflict: Whether to check if both old and new parameters are specified (default: True). - Note: For parameters with non-None default values (e.g., bool parameters with default=False), - set check_conflict=False since we cannot distinguish between an explicit value and the default. - additional_warning_message: Add a custom message which gets appended with a line break to the default warning. - - Returns: - The value to use (either from old parameter or current_value) - - Raises: - ValueError: If both old and new parameters are specified and check_conflict is True - - Example: - # For parameters where None is the default (conflict checking works): - value = self._handle_deprecated_kwarg(kwargs, 'old_param', 'new_param', current_value) - - # For parameters with non-None defaults (disable conflict checking): - mandatory = self._handle_deprecated_kwarg( - kwargs, 'optional', 'mandatory', mandatory, - transform=lambda x: not x, - check_conflict=False # Cannot detect if mandatory was explicitly passed - ) - """ - import warnings - - old_value = kwargs.pop(old_name, None) - if old_value is not None: - # Build base warning message - base_warning = f'The use of the "{old_name}" argument is deprecated. Use the "{new_name}" argument instead. Will be removed in v{DEPRECATION_REMOVAL_VERSION}.' - - # Append additional message on a new line if provided - if additional_warning_message: - # Normalize whitespace: strip leading/trailing whitespace - extra_msg = additional_warning_message.strip() - if extra_msg: - base_warning += '\n' + extra_msg - - warnings.warn( - base_warning, - DeprecationWarning, - stacklevel=3, # Stack: this method -> __init__ -> caller - ) - # Check for conflicts: only raise error if both were explicitly provided - if check_conflict and current_value is not None: - raise ValueError(f'Either {old_name} or {new_name} can be specified, but not both.') - - # Apply transformation if provided - if transform is not None: - return transform(old_value) - return old_value - - return current_value - def _validate_kwargs(self, kwargs: dict, class_name: str = None) -> None: """ Validate that no unexpected keyword arguments are present in kwargs. From 5017a59cceff6d86f58c2b20bd0d07c043bfc19c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:25:36 +0100 Subject: [PATCH 07/11] Re add handle deprectaed kwarg method --- flixopt/config.py | 3 ++ flixopt/structure.py | 74 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/flixopt/config.py b/flixopt/config.py index 60d3cbb96..f090430b0 100644 --- a/flixopt/config.py +++ b/flixopt/config.py @@ -29,6 +29,9 @@ SUCCESS_LEVEL = 25 logging.addLevelName(SUCCESS_LEVEL, 'SUCCESS') +# Deprecation removal version - update this when planning the next major version +DEPRECATION_REMOVAL_VERSION = '6.0.0' + class MultilineFormatter(logging.Formatter): """Custom formatter that handles multi-line messages with box-style borders. diff --git a/flixopt/structure.py b/flixopt/structure.py index 1e52e521e..62067e2ba 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -24,6 +24,7 @@ import xarray as xr from . import io as fx_io +from .config import DEPRECATION_REMOVAL_VERSION from .core import FlowSystemDimensions, TimeSeriesData, get_dataarray_stats if TYPE_CHECKING: # for type checking and preventing circular imports @@ -513,6 +514,79 @@ def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> tuple[An else: return self._serialize_to_basic_types(obj), extracted_arrays + def _handle_deprecated_kwarg( + self, + kwargs: dict, + old_name: str, + new_name: str, + current_value: Any = None, + transform: callable = None, + check_conflict: bool = True, + additional_warning_message: str = '', + ) -> Any: + """ + Handle a deprecated keyword argument by issuing a warning and returning the appropriate value. + + This centralizes the deprecation pattern used across multiple classes (Source, Sink, InvestParameters, etc.). + + Args: + kwargs: Dictionary of keyword arguments to check and modify + old_name: Name of the deprecated parameter + new_name: Name of the replacement parameter + current_value: Current value of the new parameter (if already set) + transform: Optional callable to transform the old value before returning (e.g., lambda x: [x] to wrap in list) + check_conflict: Whether to check if both old and new parameters are specified (default: True). + Note: For parameters with non-None default values (e.g., bool parameters with default=False), + set check_conflict=False since we cannot distinguish between an explicit value and the default. + additional_warning_message: Add a custom message which gets appended with a line break to the default warning. + + Returns: + The value to use (either from old parameter or current_value) + + Raises: + ValueError: If both old and new parameters are specified and check_conflict is True + + Example: + # For parameters where None is the default (conflict checking works): + value = self._handle_deprecated_kwarg(kwargs, 'old_param', 'new_param', current_value) + + # For parameters with non-None defaults (disable conflict checking): + mandatory = self._handle_deprecated_kwarg( + kwargs, 'optional', 'mandatory', mandatory, + transform=lambda x: not x, + check_conflict=False # Cannot detect if mandatory was explicitly passed + ) + """ + import warnings + + old_value = kwargs.pop(old_name, None) + if old_value is not None: + # Build base warning message + base_warning = f'The use of the "{old_name}" argument is deprecated. Use the "{new_name}" argument instead. Will be removed in v{DEPRECATION_REMOVAL_VERSION}.' + + # Append additional message on a new line if provided + if additional_warning_message: + # Normalize whitespace: strip leading/trailing whitespace + extra_msg = additional_warning_message.strip() + if extra_msg: + base_warning += '\n' + extra_msg + + warnings.warn( + base_warning, + DeprecationWarning, + stacklevel=3, # Stack: this method -> __init__ -> caller + ) + # Check for conflicts: only raise error if both were explicitly provided + if check_conflict and current_value is not None: + raise ValueError(f'Either {old_name} or {new_name} can be specified, but not both.') + + # Apply transformation if provided + if transform is not None: + return transform(old_value) + return old_value + + return current_value + def _validate_kwargs(self, kwargs: dict, class_name: str = None) -> None: """ Validate that no unexpected keyword arguments are present in kwargs. From 90d5e9ca6bc43ccca6aef76aac6802392a1300c0 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:25:56 +0100 Subject: [PATCH 08/11] Update CHANGELOG.md --- CHANGELOG.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc226e40..6d87e290f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp ## [Unreleased] - ????-??-?? -**Summary**: Renamed OnOff terminology to Status terminology for better alignment with PyPSA and unit commitment standards. +**Summary**: Renamed OnOff terminology to Status terminology for better alignment with PyPSA and unit commitment standards. **All deprecated items from v4.x have been removed.** ### ✨ Added @@ -127,6 +127,44 @@ A partial backwards compatibility wrapper would be misleading, so we opted for a ### 🔥 Removed +**Modules removed:** +- `calculation.py` module - Use `optimization.py` instead + +**Classes removed:** +- `Calculation`, `FullCalculation` → Use `Optimization` +- `AggregatedCalculation` → Use `ClusteredOptimization` +- `SegmentedCalculation` → Use `SegmentedOptimization` +- `Aggregation` → Use `Clustering` +- `AggregationParameters` → Use `ClusteringParameters` +- `AggregationModel` → Use `ClusteringModel` +- `CalculationResults` → Use `Results` +- `SegmentedCalculationResults` → Use `SegmentedResults` + +**Functions removed:** +- `change_logging_level()` → Use `CONFIG.Logging.enable_console()` + +**Methods removed:** +- `Optimization._perform_aggregation()` → Use `_perform_clustering()` +- `Optimization.calculate_aggregation_weights()` → Use `calculate_clustering_weights()` + +**Parameters removed:** +- `Optimization.active_timesteps` → Use `flow_system.sel(time=...)` or `flow_system.isel(time=...)` +- `TimeSeriesData.from_dataarray()`: `aggregation_group` → Use `clustering_group` +- `TimeSeriesData.from_dataarray()`: `aggregation_weight` → Use `clustering_weight` +- `FlowSystem.weights` → Use `scenario_weights` +- `Results.__init__()`: `flow_system` → Use `flow_system_data` +- `Results` plotting methods: `indexer` → Use `select` +- `Results.plot_heatmap()`: `heatmap_timeframes`, `heatmap_timesteps_per_frame` → Use `reshape_time` +- `Results.plot_heatmap()`: `color_map` → Use `colors` + +**Properties removed:** +- `FlowSystem.all_elements` → Use dict-like interface (`flow_system['label']`, `.keys()`, `.values()`, `.items()`) +- `FlowSystem.weights` → Use `scenario_weights` + +**Features removed:** +- Passing `Bus` objects directly to `Flow` → Pass bus label string instead and add Bus to FlowSystem +- Using `Effect` objects in `EffectValues` → Use effect label strings instead + **Deprecated parameters removed** (all were deprecated in v4.0.0 or earlier): **TimeSeriesData:** From 802ca7fa341244114270745569eaad0f1acea3c6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 05:31:55 +0100 Subject: [PATCH 09/11] Update test against the deprecated stuff --- tests/test_config.py | 7 ------- tests/test_integration.py | 6 +++--- tests/test_scenarios.py | 14 +++++++------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 9c4f423ee..94d626af2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -211,13 +211,6 @@ def test_attribute_modification(self): assert CONFIG.Modeling.big == 12345678 assert CONFIG.Solving.mip_gap == 0.001 - def test_change_logging_level_deprecated(self): - """Test deprecated change_logging_level function.""" - from flixopt import change_logging_level - - with pytest.warns(DeprecationWarning, match='change_logging_level is deprecated'): - change_logging_level('INFO') - def test_exception_logging(self, capfd): """Test that exceptions are properly logged with tracebacks.""" CONFIG.Logging.enable_console('INFO') diff --git a/tests/test_integration.py b/tests/test_integration.py index 6ac1e0467..35b2fa641 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -267,13 +267,13 @@ def modeling_calculation(self, request, flow_system_long, highs_solver): calc.do_modeling() calc.solve(highs_solver) elif modeling_type == 'segmented': - calc = fx.SegmentedCalculation('segModel', flow_system, timesteps_per_segment=96, overlap_timesteps=1) + calc = fx.SegmentedOptimization('segModel', flow_system, timesteps_per_segment=96, overlap_timesteps=1) calc.do_modeling_and_solve(highs_solver) elif modeling_type == 'aggregated': - calc = fx.AggregatedCalculation( + calc = fx.ClusteredOptimization( 'aggModel', flow_system, - fx.AggregationParameters( + fx.ClusteringParameters( hours_per_period=6, nr_of_periods=4, fix_storage_flows=False, diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index c952777b2..a5eb3d6a2 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -345,7 +345,7 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): assert flow_system.scenarios.equals(flow_system_full.scenarios[0:2]) - np.testing.assert_allclose(flow_system.weights.values, flow_system_full.weights[0:2]) + np.testing.assert_allclose(flow_system.scenario_weights.values, flow_system_full.scenario_weights[0:2]) calc = fx.Optimization(flow_system=flow_system, name='test_scenarios_selection', normalize_weights=False) calc.do_modeling() @@ -357,8 +357,8 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): np.testing.assert_allclose( calc.results.objective, ( - (calc.results.solution['costs'] * flow_system.weights).sum() - + (calc.results.solution['Penalty'] * flow_system.weights).sum() + (calc.results.solution['costs'] * flow_system.scenario_weights).sum() + + (calc.results.solution['Penalty'] * flow_system.scenario_weights).sum() ).item(), ) ## Account for rounding errors @@ -752,8 +752,8 @@ def test_weights_io_persistence(): fs_loaded = fx.FlowSystem.from_dataset(ds) # Verify weights persisted correctly - np.testing.assert_allclose(fs_loaded.weights.values, fs_original.weights.values) - assert fs_loaded.weights.dims == fs_original.weights.dims + np.testing.assert_allclose(fs_loaded.scenario_weights.values, fs_original.scenario_weights.values) + assert fs_loaded.scenario_weights.dims == fs_original.scenario_weights.dims def test_weights_selection(): @@ -788,7 +788,7 @@ def test_weights_selection(): # Verify weights are correctly sliced assert fs_subset.scenarios.equals(pd.Index(['base', 'high'], name='scenario')) - np.testing.assert_allclose(fs_subset.weights.values, custom_scenario_weights[[0, 2]]) + np.testing.assert_allclose(fs_subset.scenario_weights.values, custom_scenario_weights[[0, 2]]) # Verify weights are 1D with just scenario dimension (no period dimension) - assert fs_subset.weights.dims == ('scenario',) + assert fs_subset.scenario_weights.dims == ('scenario',) From 1c4511aa8bc349cc1c6a16310bab74bacf6afd5d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 05:51:56 +0100 Subject: [PATCH 10/11] Feature/excess rename (#501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * | File | Changes | |-------------------------|----------------------------------------------------------------------------------| | flixopt/elements.py | Renamed attributes excess_input → virtual_supply, excess_output → virtual_demand | | flixopt/optimization.py | Updated attribute access and result keys | | tests/test_bus.py | Updated variable name strings in assertions | | docs/.../Bus.md | Updated description of φ symbols | The variable names in the optimization model are now: - {BusName}|virtual_supply (was excess_input) - {BusName}|virtual_demand (was excess_output) * Renamed excess_penalty_per_flow_hour → imbalance_penalty_per_flow_hour * rename excess_penalty to imbalance_penalty * Change default to None * Added self._validate_kwargs(kwargs) to catch typos and unexpected arguments * Renamed with_excess → allows_imbalance * Fix docstring * 1. docs/user-guide/mathematical-notation/elements/Bus.md - Fixed three typos: - "a imbalance_penalty_per_flow_hour" → "an imbalance_penalty_per_flow_hour" - "usefull" → "useful" - "ifeasiblity" → "infeasibility" 2. tests/test_bus.py - Updated comments to use the new imbalance terminology instead of the old "excess" terminology 3. flixopt/elements.py (BusModel) - Improved code clarity: - Changed eq_bus_balance.lhs -= -self.virtual_supply + self.virtual_demand to the more readable eq_bus_balance.lhs += self.virtual_supply - self.virtual_demand - Added a comment explaining the equation: # Σ(inflows) + virtual_supply = Σ(outflows) + virtual_demand - Combined the two separate add_share_to_effects calls into a single call with the combined expression (self.virtual_supply + self.virtual_demand) * imbalance_penalty All 12 bus tests pass with these changes. --- CHANGELOG.md | 8 ++ .../effects-penalty-objective.md | 2 +- .../mathematical-notation/elements/Bus.md | 8 +- examples/02_Complex/complex_example.py | 8 +- .../example_optimization_modes.py | 10 +-- flixopt/elements.py | 76 ++++++++++--------- flixopt/flow_system.py | 2 +- flixopt/optimization.py | 10 +-- tests/test_bus.py | 25 +++--- tests/test_functional.py | 4 +- 10 files changed, 82 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d87e290f..7e181ee9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,10 +121,18 @@ Use find-and-replace to update your code with the mappings above. The functional A partial backwards compatibility wrapper would be misleading, so we opted for a clean breaking change. +- `Bus.imbalance_penalty_per_flow_hour` now defaults to `None` (strict balance) instead of `1e5` + ### ♻️ Changed +- Renamed `BusModel.excess_input` → `virtual_supply` and `BusModel.excess_output` → `virtual_demand` for clearer semantics +- Renamed `Bus.excess_penalty_per_flow_hour` → `imbalance_penalty_per_flow_hour` +- Renamed `Bus.with_excess` → `allows_imbalance` + ### 🗑️ Deprecated +- `Bus.excess_penalty_per_flow_hour` → use `imbalance_penalty_per_flow_hour` + ### 🔥 Removed **Modules removed:** diff --git a/docs/user-guide/mathematical-notation/effects-penalty-objective.md b/docs/user-guide/mathematical-notation/effects-penalty-objective.md index aeab09031..fd8a97a1d 100644 --- a/docs/user-guide/mathematical-notation/effects-penalty-objective.md +++ b/docs/user-guide/mathematical-notation/effects-penalty-objective.md @@ -197,7 +197,7 @@ Where: - $s_{l \rightarrow \Phi, \text{per}}$ is the periodic penalty share from element $l$ - $s_{l \rightarrow \Phi, \text{temp}}(\text{t}_i)$ is the temporal penalty share from element $l$ at timestep $\text{t}_i$ -**Primary usage:** Penalties occur in [Buses](elements/Bus.md) via the `excess_penalty_per_flow_hour` parameter, which allows nodal imbalances at a high cost, and in time series aggregation to allow period flexibility. +**Primary usage:** Penalties occur in [Buses](elements/Bus.md) via the `imbalance_penalty_per_flow_hour` parameter, which allows nodal imbalances at a high cost, and in time series aggregation to allow period flexibility. **Key properties:** - Penalty shares are added via `add_share_to_effects(name, expressions={fx.PENALTY_EFFECT_LABEL: ...}, target='temporal'/'periodic')` diff --git a/docs/user-guide/mathematical-notation/elements/Bus.md b/docs/user-guide/mathematical-notation/elements/Bus.md index bfe57d234..5028e8ef7 100644 --- a/docs/user-guide/mathematical-notation/elements/Bus.md +++ b/docs/user-guide/mathematical-notation/elements/Bus.md @@ -5,8 +5,8 @@ $$ \label{eq:bus_balance} \sum_{f_\text{out} \in \mathcal{F}_\text{out}} p_{f_\text{out}}(\text{t}_i) $$ -Optionally, a Bus can have a `excess_penalty_per_flow_hour` parameter, which allows to penaltize the balance for missing or excess flow-rates. -This is usefull as it handles a possible ifeasiblity gently. +Optionally, a Bus can have an `imbalance_penalty_per_flow_hour` parameter, which allows to penalize the balance for missing or excess flow-rates. +This is useful as it handles a possible infeasibility gently. This changes the balance to @@ -27,10 +27,10 @@ With: - $\mathcal{F}_\text{in}$ and $\mathcal{F}_\text{out}$ being the set of all incoming and outgoing flows - $p_{f_\text{in}}(\text{t}_i)$ and $p_{f_\text{out}}(\text{t}_i)$ being the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively -- $\phi_\text{in}(\text{t}_i)$ and $\phi_\text{out}(\text{t}_i)$ being the missing or excess flow-rate at time $\text{t}_i$, respectively +- $\phi_\text{in}(\text{t}_i)$ and $\phi_\text{out}(\text{t}_i)$ being the virtual supply and virtual demand at time $\text{t}_i$, respectively - $\text{t}_i$ being the time step - $s_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty term -- $\text a_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty coefficient (`excess_penalty_per_flow_hour`) +- $\text a_{b \rightarrow \Phi}(\text{t}_i)$ being the penalty coefficient (`imbalance_penalty_per_flow_hour`) --- diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index b86c0e9de..3806fde40 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -13,7 +13,7 @@ # --- Experiment Options --- # Configure options for testing various parameters and behaviors check_penalty = False - excess_penalty = 1e5 + imbalance_penalty = 1e5 use_chp_with_piecewise_conversion = True time_indices = None # Define specific time steps for custom optimizations, or use the entire series @@ -34,9 +34,9 @@ # --- Define Energy Buses --- # Represent node balances (inputs=outputs) for the different energy carriers (electricity, heat, gas) in the system flow_system.add_elements( - fx.Bus('Strom', excess_penalty_per_flow_hour=excess_penalty), - fx.Bus('Fernwärme', excess_penalty_per_flow_hour=excess_penalty), - fx.Bus('Gas', excess_penalty_per_flow_hour=excess_penalty), + fx.Bus('Strom', imbalance_penalty_per_flow_hour=imbalance_penalty), + fx.Bus('Fernwärme', imbalance_penalty_per_flow_hour=imbalance_penalty), + fx.Bus('Gas', imbalance_penalty_per_flow_hour=imbalance_penalty), ) # --- Define Effects --- diff --git a/examples/03_Optimization_modes/example_optimization_modes.py b/examples/03_Optimization_modes/example_optimization_modes.py index 009c008d9..8f26d84b4 100644 --- a/examples/03_Optimization_modes/example_optimization_modes.py +++ b/examples/03_Optimization_modes/example_optimization_modes.py @@ -41,7 +41,7 @@ def get_solutions(optimizations: list, variable: str) -> xr.Dataset: penalty_of_period_freedom=0, ) keep_extreme_periods = True - excess_penalty = 1e5 # or set to None if not needed + imbalance_penalty = 1e5 # or set to None if not needed # Data Import data_import = pd.read_csv( @@ -67,10 +67,10 @@ def get_solutions(optimizations: list, variable: str) -> xr.Dataset: flow_system = fx.FlowSystem(timesteps) flow_system.add_elements( - fx.Bus('Strom', excess_penalty_per_flow_hour=excess_penalty), - fx.Bus('Fernwärme', excess_penalty_per_flow_hour=excess_penalty), - fx.Bus('Gas', excess_penalty_per_flow_hour=excess_penalty), - fx.Bus('Kohle', excess_penalty_per_flow_hour=excess_penalty), + fx.Bus('Strom', imbalance_penalty_per_flow_hour=imbalance_penalty), + fx.Bus('Fernwärme', imbalance_penalty_per_flow_hour=imbalance_penalty), + fx.Bus('Gas', imbalance_penalty_per_flow_hour=imbalance_penalty), + fx.Bus('Kohle', imbalance_penalty_per_flow_hour=imbalance_penalty), ) # Effects diff --git a/flixopt/elements.py b/flixopt/elements.py index ae31f34c6..9ca938b62 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -195,9 +195,9 @@ class Bus(Element): Args: label: The label of the Element. Used to identify it in the FlowSystem. - excess_penalty_per_flow_hour: Penalty costs for bus balance violations. - When None, no excess/deficit is allowed (hard constraint). When set to a - value > 0, allows bus imbalances at penalty cost. Default is 1e5 (high penalty). + imbalance_penalty_per_flow_hour: Penalty costs for bus balance violations. + When None (default), no imbalance is allowed (hard constraint). When set to a + value > 0, allows bus imbalances at penalty cost. meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. @@ -207,7 +207,7 @@ class Bus(Element): ```python electricity_bus = Bus( label='main_electrical_bus', - excess_penalty_per_flow_hour=None, # No imbalance allowed + imbalance_penalty_per_flow_hour=None, # No imbalance allowed ) ``` @@ -216,7 +216,7 @@ class Bus(Element): ```python heat_network = Bus( label='district_heating_network', - excess_penalty_per_flow_hour=1000, # €1000/MWh penalty for imbalance + imbalance_penalty_per_flow_hour=1000, # €1000/MWh penalty for imbalance ) ``` @@ -225,14 +225,14 @@ class Bus(Element): ```python material_hub = Bus( label='material_processing_hub', - excess_penalty_per_flow_hour=waste_disposal_costs, # Time series + imbalance_penalty_per_flow_hour=waste_disposal_costs, # Time series ) ``` Note: - The bus balance equation enforced is: Σ(inflows) = Σ(outflows) + excess - deficit + The bus balance equation enforced is: Σ(inflows) + virtual_supply = Σ(outflows) + virtual_demand - When excess_penalty_per_flow_hour is None, excess and deficit are forced to zero. + When imbalance_penalty_per_flow_hour is None, virtual_supply and virtual_demand are forced to zero. When a penalty cost is specified, the optimization can choose to violate the balance if economically beneficial, paying the penalty. The penalty is added to the objective directly. @@ -246,11 +246,16 @@ class Bus(Element): def __init__( self, label: str, - excess_penalty_per_flow_hour: Numeric_TPS | None = 1e5, + imbalance_penalty_per_flow_hour: Numeric_TPS | None = None, meta_data: dict | None = None, + **kwargs, ): super().__init__(label, meta_data=meta_data) - self.excess_penalty_per_flow_hour = excess_penalty_per_flow_hour + imbalance_penalty_per_flow_hour = self._handle_deprecated_kwarg( + kwargs, 'excess_penalty_per_flow_hour', 'imbalance_penalty_per_flow_hour', imbalance_penalty_per_flow_hour + ) + self._validate_kwargs(kwargs) + self.imbalance_penalty_per_flow_hour = imbalance_penalty_per_flow_hour self.inputs: list[Flow] = [] self.outputs: list[Flow] = [] @@ -267,16 +272,16 @@ def _set_flow_system(self, flow_system) -> None: def transform_data(self, name_prefix: str = '') -> None: prefix = '|'.join(filter(None, [name_prefix, self.label_full])) - self.excess_penalty_per_flow_hour = self._fit_coords( - f'{prefix}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour + self.imbalance_penalty_per_flow_hour = self._fit_coords( + f'{prefix}|imbalance_penalty_per_flow_hour', self.imbalance_penalty_per_flow_hour ) def _plausibility_checks(self) -> None: - if self.excess_penalty_per_flow_hour is not None: - zero_penalty = np.all(np.equal(self.excess_penalty_per_flow_hour, 0)) + if self.imbalance_penalty_per_flow_hour is not None: + zero_penalty = np.all(np.equal(self.imbalance_penalty_per_flow_hour, 0)) if zero_penalty: logger.warning( - f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.' + f'In Bus {self.label_full}, the imbalance_penalty_per_flow_hour is 0. Use "None" or a value > 0.' ) if len(self.inputs) == 0 and len(self.outputs) == 0: raise ValueError( @@ -284,8 +289,8 @@ def _plausibility_checks(self) -> None: ) @property - def with_excess(self) -> bool: - return False if self.excess_penalty_per_flow_hour is None else True + def allows_imbalance(self) -> bool: + return self.imbalance_penalty_per_flow_hour is not None def __repr__(self) -> str: """Return string representation.""" @@ -856,8 +861,8 @@ class BusModel(ElementModel): element: Bus # Type hint def __init__(self, model: FlowSystemModel, element: Bus): - self.excess_input: linopy.Variable | None = None - self.excess_output: linopy.Variable | None = None + self.virtual_supply: linopy.Variable | None = None + self.virtual_demand: linopy.Variable | None = None super().__init__(model, element) def _do_modeling(self): @@ -870,39 +875,38 @@ def _do_modeling(self): outputs = sum([flow.submodel.flow_rate for flow in self.element.outputs]) eq_bus_balance = self.add_constraints(inputs == outputs, short_name='balance') - # Add excess to balance and penalty if needed - if self.element.with_excess: - excess_penalty = np.multiply(self._model.hours_per_step, self.element.excess_penalty_per_flow_hour) + # Add virtual supply/demand to balance and penalty if needed + if self.element.allows_imbalance: + imbalance_penalty = np.multiply(self._model.hours_per_step, self.element.imbalance_penalty_per_flow_hour) - self.excess_input = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_input') + self.virtual_supply = self.add_variables( + lower=0, coords=self._model.get_coords(), short_name='virtual_supply' + ) - self.excess_output = self.add_variables( - lower=0, coords=self._model.get_coords(), short_name='excess_output' + self.virtual_demand = self.add_variables( + lower=0, coords=self._model.get_coords(), short_name='virtual_demand' ) - eq_bus_balance.lhs -= -self.excess_input + self.excess_output + # Σ(inflows) + virtual_supply = Σ(outflows) + virtual_demand + eq_bus_balance.lhs += self.virtual_supply - self.virtual_demand # Add penalty shares as temporal effects (time-dependent) from .effects import PENALTY_EFFECT_LABEL + total_imbalance_penalty = (self.virtual_supply + self.virtual_demand) * imbalance_penalty self._model.effects.add_share_to_effects( name=self.label_of_element, - expressions={PENALTY_EFFECT_LABEL: self.excess_input * excess_penalty}, - target='temporal', - ) - self._model.effects.add_share_to_effects( - name=self.label_of_element, - expressions={PENALTY_EFFECT_LABEL: self.excess_output * excess_penalty}, + expressions={PENALTY_EFFECT_LABEL: total_imbalance_penalty}, target='temporal', ) def results_structure(self): inputs = [flow.submodel.flow_rate.name for flow in self.element.inputs] outputs = [flow.submodel.flow_rate.name for flow in self.element.outputs] - if self.excess_input is not None: - inputs.append(self.excess_input.name) - if self.excess_output is not None: - outputs.append(self.excess_output.name) + if self.virtual_supply is not None: + inputs.append(self.virtual_supply.name) + if self.virtual_demand is not None: + outputs.append(self.virtual_demand.name) return { **super().results_structure(), 'inputs': inputs, diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py index 98d4c5c0b..9015de3e4 100644 --- a/flixopt/flow_system.py +++ b/flixopt/flow_system.py @@ -77,7 +77,7 @@ class FlowSystem(Interface, CompositeContainerMixin[Element]): >>> >>> # Add elements to the system >>> boiler = fx.Component('Boiler', inputs=[heat_flow], status_parameters=...) - >>> heat_bus = fx.Bus('Heat', excess_penalty_per_flow_hour=1e4) + >>> heat_bus = fx.Bus('Heat', imbalance_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/optimization.py b/flixopt/optimization.py index 1de1bbc49..529975df7 100644 --- a/flixopt/optimization.py +++ b/flixopt/optimization.py @@ -309,15 +309,15 @@ def main_results(self) -> dict[str, int | float | dict]: 'Buses with excess': [ { bus.label_full: { - 'input': bus.submodel.excess_input.solution.sum('time'), - 'output': bus.submodel.excess_output.solution.sum('time'), + 'virtual_supply': bus.submodel.virtual_supply.solution.sum('time'), + 'virtual_demand': bus.submodel.virtual_demand.solution.sum('time'), } } for bus in self.flow_system.buses.values() - if bus.with_excess + if bus.allows_imbalance and ( - bus.submodel.excess_input.solution.sum().item() > 1e-3 - or bus.submodel.excess_output.solution.sum().item() > 1e-3 + bus.submodel.virtual_supply.solution.sum().item() > 1e-3 + or bus.submodel.virtual_demand.solution.sum().item() > 1e-3 ) ], } diff --git a/tests/test_bus.py b/tests/test_bus.py index f1497a0ec..cc49a2073 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -9,7 +9,7 @@ class TestBusModel: def test_bus(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) + bus = fx.Bus('TestBus', imbalance_penalty_per_flow_hour=None) flow_system.add_elements( bus, fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), @@ -28,7 +28,7 @@ def test_bus(self, basic_flow_system_linopy_coords, coords_config): def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - bus = fx.Bus('TestBus') + bus = fx.Bus('TestBus', imbalance_penalty_per_flow_hour=1e5) flow_system.add_elements( bus, fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), @@ -37,26 +37,26 @@ def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): model = create_linopy_model(flow_system) assert set(bus.submodel.variables) == { - 'TestBus|excess_input', - 'TestBus|excess_output', + 'TestBus|virtual_supply', + 'TestBus|virtual_demand', 'WärmelastTest(Q_th_Last)|flow_rate', 'GastarifTest(Q_Gas)|flow_rate', } assert set(bus.submodel.constraints) == {'TestBus|balance'} assert_var_equal( - model.variables['TestBus|excess_input'], model.add_variables(lower=0, coords=model.get_coords()) + model.variables['TestBus|virtual_supply'], model.add_variables(lower=0, coords=model.get_coords()) ) assert_var_equal( - model.variables['TestBus|excess_output'], model.add_variables(lower=0, coords=model.get_coords()) + model.variables['TestBus|virtual_demand'], model.add_variables(lower=0, coords=model.get_coords()) ) assert_conequal( model.constraints['TestBus|balance'], model.variables['GastarifTest(Q_Gas)|flow_rate'] - model.variables['WärmelastTest(Q_th_Last)|flow_rate'] - + model.variables['TestBus|excess_input'] - - model.variables['TestBus|excess_output'] + + model.variables['TestBus|virtual_supply'] + - model.variables['TestBus|virtual_demand'] == 0, ) @@ -65,8 +65,7 @@ def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): assert 'TestBus->Penalty(temporal)' in model.constraints assert 'TestBus->Penalty(temporal)' in model.variables - # The penalty share should equal the excess times the penalty cost - # Note: Each excess (input and output) creates its own share constraint, so we have two + # The penalty share should equal the imbalance (virtual_supply + virtual_demand) times the penalty cost # Let's verify the total penalty contribution by checking the effect's temporal model penalty_effect = flow_system.effects.penalty_effect assert penalty_effect.submodel is not None @@ -75,14 +74,14 @@ def test_bus_penalty(self, basic_flow_system_linopy_coords, coords_config): assert_conequal( model.constraints['TestBus->Penalty(temporal)'], model.variables['TestBus->Penalty(temporal)'] - == model.variables['TestBus|excess_input'] * 1e5 * model.hours_per_step - + model.variables['TestBus|excess_output'] * 1e5 * model.hours_per_step, + == model.variables['TestBus|virtual_supply'] * 1e5 * model.hours_per_step + + model.variables['TestBus|virtual_demand'] * 1e5 * model.hours_per_step, ) def test_bus_with_coords(self, basic_flow_system_linopy_coords, coords_config): """Test bus behavior across different coordinate configurations.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config - bus = fx.Bus('TestBus', excess_penalty_per_flow_hour=None) + bus = fx.Bus('TestBus', imbalance_penalty_per_flow_hour=None) flow_system.add_elements( bus, fx.Sink('WärmelastTest', inputs=[fx.Flow('Q_th_Last', 'TestBus')]), diff --git a/tests/test_functional.py b/tests/test_functional.py index 4b5c6c686..f351deef5 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -66,8 +66,8 @@ def flow_system_base(timesteps: pd.DatetimeIndex) -> fx.FlowSystem: flow_system = fx.FlowSystem(timesteps) flow_system.add_elements( - fx.Bus('Fernwärme', excess_penalty_per_flow_hour=None), - fx.Bus('Gas', excess_penalty_per_flow_hour=None), + fx.Bus('Fernwärme', imbalance_penalty_per_flow_hour=None), + fx.Bus('Gas', imbalance_penalty_per_flow_hour=None), ) flow_system.add_elements(fx.Effect('costs', '€', 'Kosten', is_standard=True, is_objective=True)) flow_system.add_elements( From e1cb20e5d8092dc3fcaaecb61006aa5bd1e48eac Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 30 Nov 2025 06:07:13 +0100 Subject: [PATCH 11/11] Feature/docs improvement (#481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Flow.md - Fully restructured with: - Tab-based organization (Core / Advanced / Patterns / Examples) - Collapsible definition blocks - Links to both Flow and FlowModel classes - Updated docstrings with absolute URLs 2. Bus.md - Restructured with tab organization and dual class linking 3. Storage.md - Restructured with comprehensive examples and dual class linking 4. LinearConverter.md - Restructured with detailed examples including specialized converters 5. InvestParameters.md - Restructured with clear separation of core vs. advanced features * Improve organization * Improve organization * Improve organization by using tables * Improve organization by using tables and use eqref * Add symbol to parameter mapping * Changed to inline math * Use propre constraints with numbering * Move parameters into separate tab * Reorder parameters * : Use the columns "symbol" and "python name" in the variables tab * Update Bus, Storage, and LinearConverter.md * Update InvestParameters and OnOffParameters.md * Update Piecewise.md * Compact effects-penalty-objective.md * Allow toc level 3 * Add toc to homepage * Replace ustom css with mkdocs material stuff * Revert some * Revert some * Remove layout css rule * Show toc on homepage * FIx broken link * Add edit uri * Hide bottom part * Hide bottom part * Restructure docs * Show navigation in home * Add Changelog fromating * THighten CHANGELOG.md * Simplify users.md * Simplify models.md * Shorten citing.md * Shorten support.md * Update CHANGELOG.md * Simplify installation.md * Simplify quick-start.md * Updated FullCalculation → Optimization in documentation Fixed mkdocs.yml navigation Fixed broken link in support.md * Fixed solver calls in docs * Move files and restructure * Delete old docs script * Improve docs structure * Imrpove Optimization Modes * Imrpove Optimization Modes * Rewrite the core concepts to be user facing * Reorganize Mathematical Notation * 1. Minimal variable names — Changed from words to symbols: - penalty_rate → $c_\phi$ - relative_min → $p_{rel}^{min}$ - flow_hours → $h_f$ - loss → $\dot{c}_{loss}$ - etc. 2. Tabs for conditional constraints — Used === "Tab Name" syntax for: - Bus.md: "Without Excess (Strict)" vs "With Excess (Soft)" - Flow.md: "Standard (No On/Off)" vs "With On/Off" vs "Fixed Profile" - Storage.md: "Fixed Initial" vs "Cyclic" vs "Final Bounds" - LinearConverter.md: "Single Input/Output" vs "Multiple Outputs" vs "COP > 1" vs "Time-Varying" - Effects.md: "Temporal (Operational)" vs "Periodic (Investment)" vs "Total" 3. Corrected Flow constraints — Clarified that: - Without on/off parameters: flow cannot be zero if relative_minimum > 0 - With on/off parameters: flow can be zero (when off) OR within bounds (when on) 4. Cleaner structure — Removed redundant content, focused on essential formulas and examples * The Flow.md now has four tabs for capacity bounds: 1. Fixed Size — Standard bounds without on/off 2. Fixed Size + On/Off — Can be zero when off 3. Variable Size — Investment decision on capacity 4. Variable Size + On/Off — Both investment and on/off, with big-M linearization for the bilinear term $s(t) \cdot P$ * InvestParameters.md: - Story-driven intro with real-world examples - Core concept: "Size as a Variable" - Tabs for: Binary (Fixed Size) | Continuous (Size Range) | Mandatory - Tabs for effects: Fixed | Specific | Retirement | Piecewise - Minimal variable names: $P$, $s_{inv}$, $c_{fix}$, $c_{spec}$, etc. - Cost annualization formula OnOffParameters.md: - Story-driven intro with real-world examples - Core concept: "Binary State" with flow bound modification - Tabs for state transitions: Switch Detection | Startup Costs | Running Costs - Tabs for duration constraints: Min Run Time | Min Off Time | Max Run Time | Total Hours | Max Startups - Minimal variable names: $s(t)$, $s^{on}(t)$, $s^{off}(t)$, $T_{on}^{min}$, etc. Piecewise.md: - Story-driven intro with ASCII diagram - Core concept: Linear segments with weighted combinations - Tabs for constraints: Single Piece Active | With Zero Point - Tabs for piece patterns: Continuous (Touching) | Gap (Forbidden Region) | Zero Point - Minimal variable names: $\beta_k$, $\lambda_0$, $\lambda_1$, etc. - Practical examples for heat pumps, boilers, and investment * Make OnOffParameters better * Piecewise.md: - Replaced the useless ASCII diagram with a more informative one showing: - Actual axis labels (input/output) - Numeric values on axes - Two pieces with their connection point labeled - Clear visual of how pieces connect at (50, 45) - Shows the start/end points notation * Add plotly chart * Add custom javascript * Remove charts plugin * Add missing docs file * Fix quick start * Delete model.md * Update citation * Update license.md * Simplify faq, support and troubleshooting.md * Remove old workflow * 1. Renamed OnOffParameters.md → StatusParameters.md 2. Updated all terminology: - on_off_parameters → status_parameters - OnOffParameters → StatusParameters - effects_per_switch_on → effects_per_startup - effects_per_running_hour → effects_per_active_hour - consecutive_on_hours_min → min_uptime - consecutive_on_hours_max → max_uptime - consecutive_off_hours_min → min_downtime - on_hours_min/max → active_hours_min/max - switch_on_max → startup_limit - switch_on/switch_off → startup/shutdown - "on/off" language → "active/inactive" language 3. Updated references in Flow.md, LinearConverter.md, and effects-penalty-objective.md * Remove Modeling patterns from docs * Simplify docs * Improve LinearConverter.md * Improve Flow.md * Improve effects-penalty-objective.md * Improve InvestParameters.md * Add durtaion constraints * Update Piecewise stuff * Update Piecewise stuff * Update Piecewise stuff * Combine effects and dimensions into one tab * The dimension examples now correctly show how to assign them to FlowSystem using pd.Index: * Update effects-and-dimensions.md * Update effects-and-dimensions.md * updated all reference tables across all Mathematical Notation pages to be consistent * updated all reference tables across all Mathematical Notation pages to be consistent * updated all reference tables across all Mathematical Notation pages to be consistent --- .gitignore | 4 + CHANGELOG.md | 20 +- README.md | 2 +- docs/getting-started.md | 65 --- docs/home/citing.md | 29 ++ docs/home/installation.md | 91 ++++ docs/home/license.md | 43 ++ docs/home/quick-start.md | 132 ++++++ docs/home/users.md | 27 ++ docs/index.md | 113 +++-- docs/javascripts/plotly-instant.js | 30 ++ docs/roadmap.md | 2 +- docs/stylesheets/extra.css | 115 +---- docs/user-guide/building-models/index.md | 20 + docs/user-guide/core-concepts.md | 263 ++++++----- docs/user-guide/faq.md | 34 ++ docs/user-guide/index.md | 83 ++++ .../mathematical-notation/dimensions.md | 316 ------------- .../effects-and-dimensions.md | 415 ++++++++++++++++++ .../effects-penalty-objective.md | 337 -------------- .../mathematical-notation/elements/Bus.md | 69 +-- .../mathematical-notation/elements/Flow.md | 146 ++++-- .../elements/LinearConverter.md | 159 +++++-- .../mathematical-notation/elements/Storage.md | 149 ++++--- .../features/InvestParameters.md | 363 +++++---------- .../features/OnOffParameters.md | 0 .../features/Piecewise.md | 162 +++++-- .../features/StatusParameters.md | 355 ++++----------- .../user-guide/mathematical-notation/index.md | 160 +++---- .../modeling-patterns/bounds-and-states.md | 171 -------- .../modeling-patterns/duration-tracking.md | 164 ------- .../modeling-patterns/index.md | 54 --- .../modeling-patterns/state-transitions.md | 235 ---------- .../mathematical-notation/others.md | 3 - docs/user-guide/optimization/index.md | 195 ++++++++ docs/user-guide/results/index.md | 18 + docs/user-guide/support.md | 23 + docs/user-guide/troubleshooting.md | 61 +++ flixopt/components.py | 35 +- flixopt/effects.py | 25 +- flixopt/elements.py | 24 +- flixopt/features.py | 29 +- flixopt/interface.py | 16 +- mkdocs.yml | 77 +++- pyproject.toml | 1 + scripts/extract_changelog.py | 151 ------- scripts/format_changelog.py | 82 ++++ scripts/gen_ref_pages.py | 2 +- 48 files changed, 2413 insertions(+), 2657 deletions(-) delete mode 100644 docs/getting-started.md create mode 100644 docs/home/citing.md create mode 100644 docs/home/installation.md create mode 100644 docs/home/license.md create mode 100644 docs/home/quick-start.md create mode 100644 docs/home/users.md create mode 100644 docs/javascripts/plotly-instant.js create mode 100644 docs/user-guide/building-models/index.md create mode 100644 docs/user-guide/faq.md create mode 100644 docs/user-guide/index.md delete mode 100644 docs/user-guide/mathematical-notation/dimensions.md create mode 100644 docs/user-guide/mathematical-notation/effects-and-dimensions.md delete mode 100644 docs/user-guide/mathematical-notation/effects-penalty-objective.md delete mode 100644 docs/user-guide/mathematical-notation/features/OnOffParameters.md delete mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md delete mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md delete mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/index.md delete mode 100644 docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md delete mode 100644 docs/user-guide/mathematical-notation/others.md create mode 100644 docs/user-guide/optimization/index.md create mode 100644 docs/user-guide/results/index.md create mode 100644 docs/user-guide/support.md create mode 100644 docs/user-guide/troubleshooting.md delete mode 100644 scripts/extract_changelog.py create mode 100644 scripts/format_changelog.py diff --git a/.gitignore b/.gitignore index cc2179b07..169c1a587 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ venv/ .DS_Store lib/ temp-plot.html +.cache +site/ +*.egg-info +uv.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e181ee9a..914ef2666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ This contains all commits, PRs, and contributors. Therefore, the Changelog should focus on the user-facing changes. Please remove all irrelevant sections before releasing. -Please keep the format of the changelog consistent with the other releases, so the extraction for mkdocs works. +Please keep the format of the changelog consistent: ## [VERSION] - YYYY-MM-DD --- ## [Template] - ????-??-?? @@ -49,11 +49,11 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp --- -## [Unreleased] - ????-??-?? +Until here --> -**Summary**: Renamed OnOff terminology to Status terminology for better alignment with PyPSA and unit commitment standards. **All deprecated items from v4.x have been removed.** +## [Upcoming] -### ✨ Added +**Summary**: Renamed OnOff terminology to Status terminology for better alignment with PyPSA and unit commitment standards. **All deprecated items from v4.x have been removed.** ### 💥 Breaking Changes @@ -212,22 +212,14 @@ A partial backwards compatibility wrapper would be misleading, so we opted for a - Flow parameters: `Q_fu` → use `fuel_flow`, `P_el` → use `electrical_flow`, `Q_th` → use `thermal_flow`, `Q_ab` → use `heat_source_flow` - Efficiency parameters: `eta` → use `thermal_efficiency`, `eta_th` → use `thermal_efficiency`, `eta_el` → use `electrical_efficiency`, `COP` → use `cop` -### 🐛 Fixed - -### 🔒 Security - -### 📦 Dependencies ### 📝 Docs +- Improve documentation from the ground up -### 👷 Development - -### 🚧 Known Issues +This is not yet publicly released! --- -Until here --> - ## [4.3.5] - 2025-11-29 **Summary**: Fix zenodo again diff --git a/README.md b/README.md index 6d049819d..339a40b41 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ boiler = fx.Boiler("Boiler", eta=0.9, ...) ### Key Features **Multi-criteria optimization:** Model costs, emissions, resource use - any custom metric. Optimize single objectives or use weighted combinations and ε-constraints. -→ [Effects documentation](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-penalty-objective/) +→ [Effects documentation](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-and-dimensions/) **Performance at any scale:** Choose optimization modes without changing your model - Optimization, SegmentedOptimization, or ClusteredOptimization (using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam)). → [Optimization modes](https://flixopt.github.io/flixopt/latest/api-reference/optimization/) diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 0cdd2a5a7..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,65 +0,0 @@ -# Getting Started with FlixOpt - -This guide will help you install FlixOpt, understand its basic concepts, and run your first optimization model. - -## Installation - -### Basic Installation - -Install FlixOpt directly into your environment using pip: - -```bash -pip install flixopt -``` - -This provides the core functionality with the HiGHS solver included. - -### Full Installation - -For all features including interactive network visualizations and time series aggregation: - -```bash -pip install "flixopt[full]" -``` - -## Logging - -FlixOpt uses Python's standard logging module with optional colored output via [colorlog](https://github.com/borntyping/python-colorlog). Logging is silent by default but can be easily configured. - -```python -from flixopt import CONFIG - -# Enable colored console logging -CONFIG.Logging.enable_console('INFO') - -# Or use a preset configuration for exploring -CONFIG.exploring() -``` - -For advanced logging configuration, you can use Python's standard logging module directly: - -```python -import logging -logging.basicConfig(level=logging.DEBUG) -``` - -For more details on logging configuration, see the [`CONFIG.Logging`][flixopt.config.CONFIG.Logging] documentation. - -## Basic Workflow - -Working with FlixOpt follows a general pattern: - -1. **Create a [`FlowSystem`][flixopt.flow_system.FlowSystem]** with a time series -2. **Define [`Effects`][flixopt.effects.Effect]** (costs, emissions, etc.) -3. **Define [`Buses`][flixopt.elements.Bus]** as connection points in your system -4. **Add [`Components`][flixopt.components]** like converters, storage, sources/sinks with their Flows -5. **Run [`Optimizations`][flixopt.optimization]** to optimize your system -6. **Analyze [`Results`][flixopt.results]** using built-in or external visualization tools - -## Next Steps - -Now that you've installed FlixOpt and understand the basic workflow, you can: - -- Learn about the [core concepts of flixopt](user-guide/core-concepts.md) -- Explore some [examples](examples/index.md) -- Check the [API reference](api-reference/index.md) for detailed documentation diff --git a/docs/home/citing.md b/docs/home/citing.md new file mode 100644 index 000000000..6fd1a6020 --- /dev/null +++ b/docs/home/citing.md @@ -0,0 +1,29 @@ +# Citing flixOpt + +If you use flixOpt in your research, please cite it. + +## Citation + +When referencing flixOpt in academic publications, please use look here: [flixopt citation](https://zenodo.org/records/17756895) + +## Publications + +If you've published research using flixOpt, please let us know! We'd love to feature it here. + +### List of Publications + +*Coming soon: A list of academic publications that have used flixOpt* + +## Contributing Back + +If flixOpt helped your research: + +- Share your model as an example +- Report issues or contribute code +- Improve documentation + +See the [Contributing Guide](../contribute.md). + +## License + +flixOpt is released under the MIT License. See [License](license.md) for details. diff --git a/docs/home/installation.md b/docs/home/installation.md new file mode 100644 index 000000000..afb24172b --- /dev/null +++ b/docs/home/installation.md @@ -0,0 +1,91 @@ +# Installation + +This guide covers installing flixOpt and its dependencies. + + +## Basic Installation + +Install flixOpt directly into your environment using pip: + +```bash +pip install flixopt +``` + +This provides the core functionality with the HiGHS solver included. + +## Full Installation + +For all features including interactive network visualizations and time series aggregation: + +```bash +pip install "flixopt[full]" +``` + +## Development Installation + +If you want to contribute to flixOpt or work with the latest development version: + +```bash +git clone https://github.com/flixOpt/flixopt.git +cd flixopt +pip install -e ".[full,dev,docs]" +``` + +## Solver Installation + +### HiGHS (Included) + +The HiGHS solver is included with flixOpt and works out of the box. No additional installation is required. + +### Gurobi (Optional) + +For academic use, Gurobi offers free licenses: + +1. Register for an academic license at [gurobi.com](https://www.gurobi.com/academia/) +2. Install Gurobi: + ```bash + pip install gurobipy + ``` +3. Activate your license following Gurobi's instructions + +## Verification + +Verify your installation by running: + +```python +import flixopt +print(flixopt.__version__) +``` + +## Logging Configuration + +flixOpt uses Python's standard logging module with optional colored output via [colorlog](https://github.com/borntyping/python-colorlog). Logging is silent by default but can be easily configured: + +```python +from flixopt import CONFIG + +# Enable colored console logging +CONFIG.Logging.enable_console('INFO') + +# Or use a preset configuration for exploring +CONFIG.exploring() +``` + +Since flixOpt uses Python's standard logging, you can also configure it directly: + +```python +import logging + +# Get the flixopt logger and configure it +logger = logging.getLogger('flixopt') +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) +``` + +For more details on logging configuration, see the [`CONFIG.Logging`][flixopt.config.CONFIG.Logging] documentation. + +## Next Steps + +- Follow the [Quick Start](quick-start.md) guide +- Explore the [Minimal Example](../examples/00-Minimal Example.md) +- Read about [Core Concepts](../user-guide/core-concepts.md) diff --git a/docs/home/license.md b/docs/home/license.md new file mode 100644 index 000000000..d00755a0b --- /dev/null +++ b/docs/home/license.md @@ -0,0 +1,43 @@ +# License + +flixOpt is released under the MIT License. + +## MIT License + +``` +MIT License + +Copyright (c) 2022 Chair of Building Energy Systems and Heat Supply - TU Dresden + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## What This Means + +The MIT License is a permissive open-source license that allows you to: + +✅ **Use** flixOpt for any purpose, including commercial applications +✅ **Modify** the source code to fit your needs +✅ **Distribute** copies of flixOpt +✅ **Sublicense** under different terms +✅ **Use privately** without making your modifications public + +## Contributing + +By contributing to flixOpt, you agree that your contributions will be licensed under the MIT License. See our [Contributing Guide](../contribute.md) for more information. diff --git a/docs/home/quick-start.md b/docs/home/quick-start.md new file mode 100644 index 000000000..b0bdef7da --- /dev/null +++ b/docs/home/quick-start.md @@ -0,0 +1,132 @@ +# Quick Start + +Get up and running with flixOpt in 5 minutes! This guide walks you through creating and solving your first energy system optimization. + +## Installation + +First, install flixOpt: + +```bash +pip install "flixopt[full]" +``` + +## Your First Model + +Let's create a simple energy system with a generator, demand, and battery storage. + +### 1. Import flixOpt + +```python +import flixopt as fx +import numpy as np +import pandas as pd +``` + +### 2. Define your time horizon + +```python +# 24h period with hourly timesteps +timesteps = pd.date_range('2024-01-01', periods=24, freq='h') +``` + +### 2. Set Up the Flow System + +```python +# Create the flow system +flow_system = fx.FlowSystem(timesteps) + +# Define an effect to minimize (costs) +costs = fx.Effect('costs', 'EUR', 'Minimize total system costs', is_objective=True) +flow_system.add_elements(costs) +``` + +### 4. Add Components + +```python +# Electricity bus +electricity_bus = fx.Bus('electricity') + +# Solar generator with time-varying output +solar_profile = np.array([0, 0, 0, 0, 0, 0, 0.2, 0.5, 0.8, 1.0, + 1.0, 0.9, 0.8, 0.7, 0.5, 0.3, 0.1, 0, + 0, 0, 0, 0, 0, 0]) + +solar = fx.Source( + 'solar', + outputs=[fx.Flow( + 'power', + bus='electricity', + size=100, # 100 kW capacity + relative_maximum=solar_profile + ) +]) + +# Demand +demand_profile = np.array([30, 25, 20, 20, 25, 35, 50, 70, 80, 75, + 70, 65, 60, 65, 70, 80, 90, 95, 85, 70, + 60, 50, 40, 35]) + +demand = fx.Sink('demand', inputs=[ + fx.Flow('consumption', + bus='electricity', + size=1, + fixed_relative_profile=demand_profile) +]) + +# Battery storage +battery = fx.Storage( + 'battery', + charging=fx.Flow('charge', bus='electricity', size=50), + discharging=fx.Flow('discharge', bus='electricity', size=50), + capacity_in_flow_hours=100, # 100 kWh capacity + initial_charge_state=50, # Start at 50% + eta_charge=0.95, + eta_discharge=0.95, +) + +# Add all components to system +flow_system.add_elements(solar, demand, battery, electricity_bus) +``` + +### 5. Run Optimization + +```python +# Create and run optimization +optimization = fx.Optimization('solar_battery_optimization', flow_system) +optimization.solve(fx.solvers.HighsSolver()) +``` + +### 6. Save Results + +```python +# This includes the modeled FlowSystem. SO you can restore both results and inputs +optimization.results.to_file() +``` + +## What's Next? + +Now that you've created your first model, you can: + +- **Learn the concepts** - Read the [Core Concepts](../user-guide/core-concepts.md) guide +- **Explore examples** - Check out more [Examples](../examples/index.md) +- **Deep dive** - Study the [Mathematical Formulation](../user-guide/mathematical-notation/index.md) +- **Build complex models** - Use [Recipes](../user-guide/recipes/index.md) for common patterns + +## Common Workflow + +Most flixOpt projects follow this pattern: + +1. **Define time series** - Set up the temporal resolution +2. **Create flow system** - Initialize with time series and effects +3. **Add buses** - Define connection points +4. **Add components** - Create generators, storage, converters, loads +5. **Run optimization** - Solve the optimization +6. **Save Results** - For later analysis. Or only extract needed data + +## Tips + +- Start simple and add complexity incrementally +- Use meaningful names for components and flows +- Check solver status before analyzing results +- Enable logging during development for debugging +- Visualize results to verify model behavior diff --git a/docs/home/users.md b/docs/home/users.md new file mode 100644 index 000000000..d27f99576 --- /dev/null +++ b/docs/home/users.md @@ -0,0 +1,27 @@ +# Who Uses flixOpt? + +flixOpt is developed and used primarily in academic research for energy system optimization. + +## Primary Users + +- **Researchers** - Energy system modeling and optimization studies +- **Students** - Master's and PhD thesis projects +- **Engineers** - Feasibility studies and system planning + +## Typical Applications + +- Dispatch optimization with renewable integration +- Capacity expansion planning +- Battery and thermal storage sizing +- District heating network optimization +- Combined heat and power (CHP) systems +- Multi-energy systems and sector coupling + +## Get Involved + +Using flixOpt in your research? Consider: + +- [Citing flixOpt](citing.md) in your publications +- Sharing your model as an example +- Contributing to the codebase +- Joining [discussions](https://github.com/flixOpt/flixopt/discussions) diff --git a/docs/index.md b/docs/index.md index 3467bb394..70fd15bf4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,5 @@ --- title: Home -hide: - - navigation - - toc ---
Model, optimize, and analyze complex energy systems with a powerful Python framework designed for flexibility and performance.
@@ -25,36 +22,44 @@ hide: ## :material-map-marker-path: Quick Navigation -New to FlixOpt? Start here with installation and your first model
- - - -Explore real-world examples from simple to complex systems
- - - -Detailed documentation of all classes, methods, and parameters
- - - -Common patterns and best practices for modeling energy systems
- - - -Understand the mathematical formulations behind the framework
- - - -See what's coming next and contribute to the future of FlixOpt
- +