Model, optimize, and analyze complex energy systems with a powerful Python framework designed for flexibility and performance.
-
- 🚀 Getting Started
- New to FlixOpt? Start here with installation and your first model
-
-
-
- 💡 Examples Gallery
- Explore real-world examples from simple to complex systems
-
-
-
- 📚 API Reference
- Detailed documentation of all classes, methods, and parameters
-
-
-
- 📖 Recipes
- Common patterns and best practices for modeling energy systems
-
-
-
- ∫ Mathematical Notation
- Understand the mathematical formulations behind the framework
-
-
-
- 🛣️ Roadmap
- See what's coming next and contribute to the future of FlixOpt
-
+
+
+- :rocket: **[Getting Started](home/installation/)**
+
+ ---
+
+ New to FlixOpt? Start here with installation and your first model
+
+- :bulb: **[Examples Gallery](examples/)**
+
+ ---
+
+ Explore real-world examples from simple to complex systems
+
+- :books: **[API Reference](api-reference/)**
+
+ ---
+
+ Detailed documentation of all classes, methods and parameters
+
+- :book: **[Recipes](user-guide/recipes/)**
+
+ ---
+
+ Common patterns and best practices for modeling energy systems
+
+- :material-math-integral: **[Mathematical Notation](user-guide/mathematical-notation/)**
+
+ ---
+
+ Understand the mathematical formulations behind the framework
+
+- :material-road: **[Roadmap](roadmap/)**
+
+ ---
+
+ See what's coming next and contribute to the future of FlixOpt
+
## 🏗️ Framework Architecture
@@ -77,43 +82,31 @@ hide:
## :material-account-group: Community & Support
-
-
-
-
-:fontawesome-brands-github:{ .feature-icon }
+
-### GitHub
+- :fontawesome-brands-github: **GitHub**
-Report issues, request features, and contribute to the codebase
+ ---
-[Visit Repository →](https://github.com/flixOpt/flixopt){target="_blank" rel="noopener noreferrer"}
+ Report issues, request features, and contribute to the codebase
-
-
-
+ [Visit Repository →](https://github.com/flixOpt/flixopt){target="_blank" rel="noopener noreferrer"}
-:material-forum:{ .feature-icon }
+- :material-forum: **Discussions**
-### Discussions
+ ---
-Ask questions and share your projects with the community
+ Ask questions and share your projects with the community
-[Join Discussion →](https://github.com/flixOpt/flixopt/discussions){target="_blank" rel="noopener noreferrer"}
-
-
+ [Join Discussion →](https://github.com/flixOpt/flixopt/discussions){target="_blank" rel="noopener noreferrer"}
-
+- :material-book-open-page-variant: **Contributing**
-:material-book-open-page-variant:{ .feature-icon }
+ ---
-### Contributing
+ Help improve FlixOpt by contributing code, docs, or examples
-Help improve FlixOpt by contributing code, docs, or examples
-
-[Learn How →](contribute/){target="_blank" rel="noopener noreferrer"}
-
-
+ [Learn How →](contribute/){target="_blank" rel="noopener noreferrer"}
@@ -132,7 +125,7 @@ Help improve FlixOpt by contributing code, docs, or examples
Ready to optimize your energy system?
- ▶️ Start Building
+ ▶️ Start Building
diff --git a/docs/javascripts/plotly-instant.js b/docs/javascripts/plotly-instant.js
new file mode 100644
index 000000000..c6dd2766c
--- /dev/null
+++ b/docs/javascripts/plotly-instant.js
@@ -0,0 +1,30 @@
+// Re-initialize Plotly charts on MkDocs Material instant navigation
+document.addEventListener('DOMContentLoaded', function() {
+ initPlotlyCharts();
+});
+
+// Hook into Material's instant navigation
+if (typeof document$ !== 'undefined') {
+ document$.subscribe(function() {
+ initPlotlyCharts();
+ });
+}
+
+function initPlotlyCharts() {
+ const charts = document.querySelectorAll('div.mkdocs-plotly');
+ charts.forEach(function(chart) {
+ // Skip if already initialized (has children)
+ if (chart.children.length > 0) return;
+
+ try {
+ const plotData = JSON.parse(chart.textContent);
+ chart.textContent = '';
+ const data = plotData.data || [];
+ const layout = plotData.layout || {};
+ const config = plotData.config || {responsive: true};
+ Plotly.newPlot(chart, data, layout, config);
+ } catch (e) {
+ console.error('Failed to initialize Plotly chart:', e);
+ }
+ });
+}
diff --git a/docs/roadmap.md b/docs/roadmap.md
index fbad1043c..13233f014 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -18,7 +18,7 @@ We believe optimization modeling should be **approachable for beginners** yet **
## 🔮 Medium-term (6-12 months)
- **Modeling to Generate Alternatives (MGA)** - Built-in support for exploring near-optimal solution spaces to produce more robust, diverse solutions under uncertainty. See [PyPSA](https://docs.pypsa.org/latest/user-guide/optimization/modelling-to-generate-alternatives/) and [Calliope](https://calliope.readthedocs.io/en/latest/examples/modes/) for reference implementations
-- **Advanced stochastic optimization** - Build sophisticated new `Calculation` classes to perform different stochastic optimization approaches, like PyPSA's [two-stage stochastic programming and risk preferences with Conditional Value-at-Risk (CVaR)](https://docs.pypsa.org/latest/user-guide/optimization/stochastic/)
+- **Advanced stochastic optimization** - Build sophisticated new `Optimization` classes to perform different stochastic optimization approaches, like PyPSA's [two-stage stochastic programming and risk preferences with Conditional Value-at-Risk (CVaR)](https://docs.pypsa.org/latest/user-guide/optimization/stochastic/)
- **Enhanced component library** - More pre-built, domain-specific components (sector coupling, hydrogen systems, thermal networks, demand-side management)
## 🌟 Long-term (12+ months)
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
index 2992267b6..78946b9ad 100644
--- a/docs/stylesheets/extra.css
+++ b/docs/stylesheets/extra.css
@@ -505,12 +505,16 @@
Home Page Inline Styles (moved from docs/index.md)
========================================================================= */
+/* Center home page content when navigation is hidden */
+/* Remove this rule as it conflicts with TOC layout */
+
.hero-section {
text-align: center;
padding: 4rem 2rem 3rem 2rem;
background: linear-gradient(135deg, rgba(0, 150, 136, 0.1) 0%, rgba(0, 121, 107, 0.1) 100%);
border-radius: 1rem;
- margin-bottom: 3rem;
+ margin: 0 auto 3rem auto;
+ max-width: 1200px;
}
.hero-section h1 {
@@ -558,47 +562,6 @@
margin-top: 2rem;
}
-.feature-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 2rem;
- margin: 3rem 0;
-}
-
-.feature-card {
- padding: 2rem;
- border-radius: 0.75rem;
- background: var(--md-code-bg-color);
- border: 1px solid var(--md-default-fg-color--lightest);
- transition: all 0.3s ease;
- text-align: center;
-}
-
-.feature-card:hover {
- transform: translateY(-4px);
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
- border-color: var(--md-primary-fg-color);
-}
-
-.feature-icon {
- font-size: 3rem;
- margin-bottom: 1rem;
- display: block;
-}
-
-.feature-card h3 {
- margin-top: 0;
- margin-bottom: 0.5rem;
- font-size: 1.25rem;
-}
-
-.feature-card p {
- color: var(--md-default-fg-color--light);
- margin: 0;
- font-size: 0.95rem;
- line-height: 1.6;
-}
-
.stats-banner {
display: flex;
justify-content: space-around;
@@ -631,45 +594,14 @@
}
.architecture-section {
- margin: 4rem 0;
+ margin: 4rem auto;
padding: 2rem;
background: var(--md-code-bg-color);
border-radius: 0.75rem;
+ max-width: 1200px;
}
-.quick-links {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 1.5rem;
- margin: 3rem 0;
-}
-
-.quick-link-card {
- padding: 1.5rem;
- border-left: 4px solid var(--md-primary-fg-color);
- background: var(--md-code-bg-color);
- border-radius: 0.5rem;
- transition: all 0.2s ease;
- text-decoration: none;
- display: block;
-}
-
-.quick-link-card:hover {
- background: var(--md-default-fg-color--lightest);
- transform: translateX(4px);
-}
-.quick-link-card h3 {
- margin: 0 0 0.5rem 0;
- font-size: 1.1rem;
- color: var(--md-primary-fg-color);
-}
-
-.quick-link-card p {
- margin: 0;
- color: var(--md-default-fg-color--light);
- font-size: 0.9rem;
-}
@media screen and (max-width: 768px) {
.hero-section h1 {
@@ -685,10 +617,6 @@
align-items: stretch;
}
- .feature-grid {
- grid-template-columns: 1fr;
- }
-
.stats-banner {
flex-direction: column;
}
@@ -839,27 +767,26 @@ button:focus-visible {
Footer Alignment Fix
========================================================================= */
-/* Align footer with content width */
-.md-footer-meta__inner,
-.md-footer__inner {
- max-width: 1300px;
- margin: 0 auto;
- padding-left: 1.2rem;
- padding-right: 1.2rem;
+/* Hide the social media footer section */
+.md-footer-meta {
+ display: none;
+}
+
+/* Footer navigation content matches page width */
+.md-footer .md-grid {
+ max-width: 1300px !important;
+ padding-left: 1.2rem !important;
+ padding-right: 1.2rem !important;
}
@media screen and (min-width: 76.25em) {
- .md-footer-meta__inner,
- .md-footer__inner {
- padding-left: 1rem;
- padding-right: 1rem;
+ .md-footer .md-grid {
+ padding-left: 1rem !important;
}
}
@media screen and (min-width: 100em) {
- .md-footer-meta__inner,
- .md-footer__inner {
- padding-left: 2rem;
- padding-right: 2rem;
+ .md-footer .md-grid {
+ padding-left: 2rem !important;
}
}
diff --git a/docs/user-guide/building-models/index.md b/docs/user-guide/building-models/index.md
new file mode 100644
index 000000000..27808ea56
--- /dev/null
+++ b/docs/user-guide/building-models/index.md
@@ -0,0 +1,20 @@
+# Building Models
+
+!!! note "Under Development"
+ This section is being expanded with detailed tutorials.
+
+Learn how to construct FlowSystem models step by step:
+
+- Defining time horizons and dimensions
+- Creating buses and flows
+- Adding components (Sources, Sinks, Converters, Storage)
+- Configuring effects and objectives
+- Using advanced features (Investment, On/Off, Piecewise)
+
+## Getting Started
+
+For now, see:
+
+- **[Core Concepts](../core-concepts.md)** - Understand the fundamental building blocks
+- **[Examples](../../examples/index.md)** - Working code you can learn from
+- **[Mathematical Notation](../mathematical-notation/index.md)** - Detailed specifications of each element
diff --git a/docs/user-guide/core-concepts.md b/docs/user-guide/core-concepts.md
index f165f1e4e..3bccb554c 100644
--- a/docs/user-guide/core-concepts.md
+++ b/docs/user-guide/core-concepts.md
@@ -1,155 +1,196 @@
-# Core concepts of flixopt
+# Core Concepts
-FlixOpt is built around a set of core concepts that work together to represent and optimize **any system involving flows and conversions** - whether that's energy systems, material flows, supply chains, water networks, or production processes.
+This page introduces the fundamental concepts of flixOpt through practical scenarios. Understanding these concepts will help you model any system involving flows and conversions.
-This page provides a high-level overview of these concepts and how they interact.
+## The Big Picture
-## Main building blocks
+Imagine you're managing a district heating system. You have:
-### FlowSystem
+- A **gas boiler** that burns natural gas to produce heat
+- A **heat pump** that uses electricity to extract heat from the environment
+- A **thermal storage tank** to buffer heat production and demand
+- **Buildings** that need heat throughout the day
+- Access to the **gas grid** and **electricity grid**
-The [`FlowSystem`][flixopt.flow_system.FlowSystem] is the central organizing unit in FlixOpt.
-Every FlixOpt model starts with creating a FlowSystem. It:
+Your goal: **minimize total operating costs** while meeting all heat demands.
-- Defines the timesteps for the optimization
-- Contains and connects [components](#components), [buses](#buses), and [flows](#flows)
-- Manages the [effects](#effects) (objectives and constraints)
+This is exactly the kind of problem flixOpt solves. Let's see how each concept maps to this scenario.
-FlowSystem provides two ways to access elements:
+## Buses: Where Things Connect
-- **Dict-like interface**: Access any element by label: `flow_system['Boiler']`, `'Boiler' in flow_system`, `flow_system.keys()`
-- **Direct containers**: Access type-specific containers: `flow_system.components`, `flow_system.buses`, `flow_system.effects`, `flow_system.flows`
+A [`Bus`][flixopt.elements.Bus] is a connection point where energy or material flows meet. Think of it as a junction or hub.
-Element labels must be unique across all types. See the [`FlowSystem` API reference][flixopt.flow_system.FlowSystem] for detailed examples and usage patterns.
+!!! example "In our heating system"
+ - **Heat Bus** — where heat from the boiler, heat pump, and storage meets the building demand
+ - **Gas Bus** — connection to the gas grid
+ - **Electricity Bus** — connection to the power grid
-### Flows
+**The key rule:** At every bus, **inputs must equal outputs** at each timestep.
-[`Flow`][flixopt.elements.Flow] objects represent the movement of energy or material between a [Bus](#buses) and a [Component](#components) in a predefined direction.
+$$\sum inputs = \sum outputs$$
-- 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.)
-- Can have fixed profiles (for demands or renewable generation)
-- Can have [Effects](#effects) associated by their use (costs, emissions, labour, ...)
+This balance constraint is what makes your model physically meaningful — energy can't appear or disappear.
-#### Flow Hours
-While the **Flow Rate** defines the rate in which energy or material is transported, the **Flow Hours** define the amount of energy or material that is transported.
-Its defined by the flow_rate times the duration of the timestep in hours.
+## Flows: What Moves Between Elements
-Examples:
+A [`Flow`][flixopt.elements.Flow] represents the movement of energy or material. Every flow connects a component to a bus, with a defined direction.
-| Flow Rate | Timestep | Flow Hours |
-|-----------|----------|------------|
-| 10 (MW) | 1 hour | 10 (MWh) |
-| 10 (MW) | 6 minutes | 0.1 (MWh) |
-| 10 (kg/h) | 1 hour | 10 (kg) |
+!!! example "In our heating system"
+ - Heat flowing **from** the boiler **to** the Heat Bus
+ - Gas flowing **from** the Gas Bus **to** the boiler
+ - Heat flowing **from** the Heat Bus **to** the buildings
-### Buses
+Flows have:
-[`Bus`][flixopt.elements.Bus] objects represent nodes or connection points in a FlowSystem. They:
+- A **size** (capacity) — *"This boiler can deliver up to 500 kW"*
+- A **flow rate** — *"Right now it's running at 300 kW"*
-- Balance incoming and outgoing flows
-- Can represent physical networks like heat, electricity, or gas
-- Handle infeasible balances gently by allowing the balance to be closed in return for a big Penalty (optional)
+## Components: The Equipment
-### Components
+[`Components`][flixopt.elements.Component] are the physical (or logical) elements that transform, store, or transfer flows.
-[`Component`][flixopt.elements.Component] objects usually represent physical entities in your system that interact with [`Flows`][flixopt.elements.Flow]. The generic component types work across all domains:
+### Converters — Transform One Thing Into Another
-- [`LinearConverters`][flixopt.components.LinearConverter] - Converts input flows to output flows with (piecewise) linear relationships
- - *Energy: boilers, heat pumps, turbines*
- - *Manufacturing: assembly lines, processing equipment*
- - *Chemistry: reactors, separators*
-- [`Storages`][flixopt.components.Storage] - Stores energy or material over time
- - *Energy: batteries, thermal storage, gas storage*
- - *Logistics: warehouses, buffer inventory*
- - *Water: reservoirs, tanks*
-- [`Sources`][flixopt.components.Source] / [`Sinks`][flixopt.components.Sink] / [`SourceAndSinks`][flixopt.components.SourceAndSink] - Produce or consume flows
- - *Energy: demands, renewable generation*
- - *Manufacturing: raw material supply, product demand*
- - *Supply chain: suppliers, customers*
-- [`Transmissions`][flixopt.components.Transmission] - Moves flows between locations with possible losses
- - *Energy: pipelines, power lines*
- - *Logistics: transport routes*
- - *Water: distribution networks*
+A [`LinearConverter`][flixopt.components.LinearConverter] takes inputs and produces outputs with a defined efficiency.
-**Pre-built specialized components** for energy systems include [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc. These can serve as blueprints for custom domain-specific components.
+!!! example "In our heating system"
+ - **Gas Boiler**: Gas → Heat (η = 90%)
+ - **Heat Pump**: Electricity → Heat (COP = 3.5)
-### Effects
+The conversion relationship:
-[`Effect`][flixopt.effects.Effect] objects represent impacts or metrics related to your system. While commonly used to allocate costs, they're completely flexible:
+$$output = \eta \cdot input$$
-**Energy systems:**
-- Costs (investment, operation)
-- Emissions (CO₂, NOx, etc.)
-- Primary energy consumption
+### Storages — Save for Later
-**Other domains:**
-- Production time, labor hours (manufacturing)
-- Water consumption, wastewater (process industries)
-- Transport distance, vehicle utilization (logistics)
-- Space consumption
-- Any custom metric relevant to your domain
+A [`Storage`][flixopt.components.Storage] accumulates and releases energy or material over time.
-These can be freely defined and crosslink to each other (`CO₂` ──[specific CO₂-costs]─→ `Costs`).
-One effect is designated as the **optimization objective** (typically Costs), while others can be constrained.
-This approach allows for multi-criteria optimization using both:
+!!! example "In our heating system"
+ - **Thermal Tank**: Store excess heat during cheap hours, use it during expensive hours
- - **Weighted Sum Method**: Optimize a theoretical Effect which other Effects crosslink to
- - **ε-constraint method**: Constrain effects to specific limits
+The storage tracks its state over time:
-### Optimization
+$$charge(t+1) = charge(t) + charging - discharging$$
-A [`FlowSystem`][flixopt.flow_system.FlowSystem] can be converted to a Model and optimized by creating an [`Optimization`][flixopt.optimization.Optimization] from it.
+### Sources & Sinks — System Boundaries
-FlixOpt offers different optimization modes:
+[`Sources`][flixopt.components.Source] and [`Sinks`][flixopt.components.Sink] connect your system to the outside world.
-- [`Optimization`][flixopt.optimization.Optimization] - Solves the entire problem at once
-- [`SegmentedOptimization`][flixopt.optimization.SegmentedOptimization] - Solves the problem in segments (with optional overlap), improving performance for large problems
-- [`ClusteredOptimization`][flixopt.optimization.ClusteredOptimization] - Uses typical periods to reduce computational requirements
+!!! example "In our heating system"
+ - **Gas Source**: Buy gas from the grid at market prices
+ - **Electricity Source**: Buy power at time-varying prices
+ - **Heat Sink**: The building demand that must be met
-### Results
+## Effects: What You're Tracking
-The results of an optimization are stored in a [`Results`][flixopt.results.Results] object.
-This object contains the solutions of the optimization as well as all information about the [`Optimization`][flixopt.optimization.Optimization] and the [`FlowSystem`][flixopt.flow_system.FlowSystem] it was created from.
-The solution is stored as an `xarray.Dataset`, but can be accessed through their associated Component, Bus or Effect.
+An [`Effect`][flixopt.effects.Effect] represents any metric you want to track or optimize. One effect is your **objective** (what you minimize or maximize), others can be **constraints**.
-This [`Results`][flixopt.results.Results] object can be saved to file and reloaded from file, allowing you to analyze the results anytime after the solve.
+!!! example "In our heating system"
+ - **Costs** (objective) — minimize total operating costs
+ - **CO₂ Emissions** (constraint) — stay below 1000 tonnes/year
+ - **Gas Consumption** (tracking) — report total gas used
-## How These Concepts Work Together
+Effects can be linked: *"Each kg of CO₂ costs €80 in emissions trading"* — this creates a connection from the CO₂ effect to the Costs effect.
-The process of working with FlixOpt can be divided into 3 steps:
+## FlowSystem: Putting It All Together
-1. Create a [`FlowSystem`][flixopt.flow_system.FlowSystem], containing all the elements and data of your system
- - Define the time horizon of your system (and optionally your periods and scenarios, see [Dimensions](mathematical-notation/dimensions.md)))
- - Add [`Effects`][flixopt.effects.Effect] to represent costs, emissions, etc.
- - Add [`Buses`][flixopt.elements.Bus] as connection points in your system and [`Sinks`][flixopt.components.Sink] & [`Sources`][flixopt.components.Source] as connections to the outer world (markets, power grid, ...)
- - Add [`Components`][flixopt.components] like [`Boilers`][flixopt.linear_converters.Boiler], [`HeatPumps`][flixopt.linear_converters.HeatPump], [`CHPs`][flixopt.linear_converters.CHP], etc.
- - Add
- - [`FlowSystems`][flixopt.flow_system.FlowSystem] can also be loaded from a netCDF file*
-2. Translate the model to a mathematical optimization problem
- - Create an [`Optimization`][flixopt.optimization.Optimization] from your FlowSystem and choose a Solver
- - ...The Optimization is translated internally to a mathematical optimization problem...
- - ...and solved by the chosen solver.
-3. Analyze the results
- - The results are stored in a [`Results`][flixopt.results.Results] object
- - This object can be saved to file and reloaded from file, retaining all information about the optimization
- - As it contains the used [`FlowSystem`][flixopt.flow_system.FlowSystem], it fully documents all assumptions taken to create the results.
+The [`FlowSystem`][flixopt.flow_system.FlowSystem] is your complete model. It contains all buses, components, flows, and effects, plus the **time definition** for your optimization.
+
+```python
+import flixopt as fx
+
+# Define timesteps (e.g., hourly for one week)
+timesteps = pd.date_range('2024-01-01', periods=168, freq='h')
+
+# Create the system
+flow_system = fx.FlowSystem(timesteps)
+
+# Add elements
+flow_system.add_elements(heat_bus, gas_bus, electricity_bus)
+flow_system.add_elements(boiler, heat_pump, storage)
+flow_system.add_elements(costs_effect, co2_effect)
+```
+
+## The Workflow: Model → Optimize → Analyze
+
+Working with flixOpt follows three steps:
+
+```mermaid
+graph LR
+ A[1. Build FlowSystem] --> B[2. Run Optimization]
+ B --> C[3. Analyze Results]
+```
+
+### 1. Build Your Model
+
+Define your system structure, parameters, and time series data.
+
+### 2. Run the Optimization
+
+Create an [`Optimization`][flixopt.optimization.Optimization] and solve it:
+
+```python
+optimization = fx.Optimization('my_model', flow_system)
+results = optimization.solve(fx.solvers.HighsSolver())
+```
+
+### 3. Analyze Results
+
+The [`Results`][flixopt.results.Results] object contains all solution data:
+
+```python
+# Access component results
+boiler_output = results['Boiler'].node_balance()
+
+# Get total costs
+total_costs = results.solution['Costs']
+```

Conceptual Usage and IO operations of FlixOpt
-## Advanced Usage
-As flixopt is build on [linopy](https://github.com/PyPSA/linopy), any model created with FlixOpt can be extended or modified using the great [linopy API](https://linopy.readthedocs.io/en/latest/api.html).
-This allows to adjust your model to very specific requirements without loosing the convenience of FlixOpt.
-
-
-
-
-
-
-
-
-
+## Quick Reference
+
+| Concept | What It Represents | Real-World Example |
+|---------|-------------------|-------------------|
+| **Bus** | Connection point | Heat network, electrical grid |
+| **Flow** | Energy/material movement | Heat delivery, gas consumption |
+| **LinearConverter** | Transformation equipment | Boiler, heat pump, turbine |
+| **Storage** | Time-shifting capability | Battery, thermal tank, warehouse |
+| **Source/Sink** | System boundary | Grid connection, demand |
+| **Effect** | Metric to track/optimize | Costs, emissions, energy use |
+| **FlowSystem** | Complete model | Your entire system |
+
+## Beyond Energy Systems
+
+While our example used a heating system, flixOpt works for any flow-based optimization:
+
+| Domain | Buses | Components | Effects |
+|--------|-------|------------|---------|
+| **District Heating** | Heat, Gas, Electricity | Boilers, CHPs, Heat Pumps | Costs, CO₂ |
+| **Manufacturing** | Raw Materials, Products | Machines, Assembly Lines | Costs, Time, Labor |
+| **Supply Chain** | Warehouses, Locations | Transport, Storage | Costs, Distance |
+| **Water Networks** | Reservoirs, Treatment | Pumps, Pipes | Costs, Energy |
+
+## Next Steps
+
+- **[Building Models](building-models/index.md)** — Step-by-step guide to constructing models
+- **[Examples](../examples/index.md)** — Working code for common scenarios
+- **[Mathematical Notation](mathematical-notation/index.md)** — Detailed constraint formulations
+
+## Advanced: Extending with linopy
+
+flixOpt is built on [linopy](https://github.com/PyPSA/linopy). You can access and extend the underlying optimization model for custom constraints:
+
+```python
+# Access the linopy model after building
+optimization.do_modeling()
+model = optimization.model
+
+# Add custom constraints using linopy API
+model.add_constraints(...)
+```
+
+This allows advanced users to add domain-specific constraints while keeping flixOpt's convenience for standard modeling.
diff --git a/docs/user-guide/faq.md b/docs/user-guide/faq.md
new file mode 100644
index 000000000..63994180d
--- /dev/null
+++ b/docs/user-guide/faq.md
@@ -0,0 +1,34 @@
+# Frequently Asked Questions
+
+## What is flixOpt?
+
+flixOpt is a Python framework for modeling and optimizing energy and material flow systems. It handles both operational optimization (dispatch) and investment optimization (capacity expansion).
+
+## Which solvers does flixOpt support?
+
+- **HiGHS** (default, included)
+- **Gurobi** (commercial, academic licenses available)
+
+## How do I install flixOpt?
+
+```bash
+pip install flixopt
+```
+
+For full features:
+```bash
+pip install "flixopt[full]"
+```
+
+## Do I need to install a solver separately?
+
+No. HiGHS is included and works out of the box.
+
+## Can I add custom constraints?
+
+Yes. You can add custom constraints directly to the optimization model using linopy.
+
+## Where can I get help?
+
+- Check [Troubleshooting](troubleshooting.md)
+- Open an [issue on GitHub](https://github.com/flixOpt/flixopt/issues)
diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md
new file mode 100644
index 000000000..bfb288ea4
--- /dev/null
+++ b/docs/user-guide/index.md
@@ -0,0 +1,83 @@
+# User Guide
+
+Welcome to the flixOpt User Guide! This guide will help you master energy and material flow optimization with flixOpt.
+
+## What is flixOpt?
+
+flixOpt is a comprehensive framework for modeling and optimizing energy and material flow systems. It supports:
+
+- **Operational Optimization** - Dispatch optimization with fixed capacities
+- **Investment Optimization** - Capacity expansion planning with binary or continuous sizing
+- **Multi-Period Planning** - Sequential investment decisions across multiple periods
+- **Scenario Analysis** - Stochastic modeling with weighted scenarios
+
+## Key Features
+
+
+
+- :material-puzzle: **Flexible Components**
+
+ ---
+
+ Flow, Bus, Storage, LinearConverter - build any system topology
+
+- :material-cog: **Advanced Modeling**
+
+ ---
+
+ Investment decisions, On/Off states, Piecewise linearization
+
+- :material-calculator: **Multiple Solvers**
+
+ ---
+
+ HiGHS (default), Gurobi, CPLEX - choose what fits your needs
+
+- :material-chart-line: **Built-in Analysis**
+
+ ---
+
+ Plotting, export, and result exploration tools
+
+
+
+## Learning Path
+
+This guide follows a sequential learning path:
+
+| Step | Section | What You'll Learn |
+|------|---------|-------------------|
+| 1 | [Core Concepts](core-concepts.md) | Fundamental building blocks: FlowSystem, Bus, Flow, Components, Effects |
+| 2 | [Building Models](building-models/index.md) | How to construct models step by step |
+| 3 | [Running Optimizations](optimization/index.md) | Solver configuration and execution |
+| 4 | [Analyzing Results](results/index.md) | Extracting and visualizing outcomes |
+| 5 | [Mathematical Notation](mathematical-notation/index.md) | Deep dive into the math behind each element |
+| 6 | [Recipes](recipes/index.md) | Common patterns and solutions |
+
+## Quick Links
+
+### Getting Started
+
+- [Quick Start](../home/quick-start.md) - Build your first model in 5 minutes
+- [Minimal Example](../examples/00-Minimal Example.md) - Simplest possible model
+- [Core Concepts](core-concepts.md) - Understand the fundamentals
+
+### Reference
+
+- [Mathematical Notation](mathematical-notation/index.md) - Detailed specifications
+- [API Reference](../api-reference/index.md) - Complete class documentation
+- [Examples](../examples/index.md) - Working code to learn from
+
+### Help
+
+- [FAQ](faq.md) - Frequently asked questions
+- [Troubleshooting](troubleshooting.md) - Common issues and solutions
+- [Community](support.md) - Get help from the community
+
+## Use Cases
+
+flixOpt handles any flow-based optimization problem:
+
+**Energy Systems**: Power dispatch, CHP optimization, renewable integration, battery storage, district heating
+
+**Industrial Applications**: Process optimization, multi-commodity networks, supply chains, resource allocation
diff --git a/docs/user-guide/mathematical-notation/dimensions.md b/docs/user-guide/mathematical-notation/dimensions.md
deleted file mode 100644
index e10ef5ffd..000000000
--- a/docs/user-guide/mathematical-notation/dimensions.md
+++ /dev/null
@@ -1,316 +0,0 @@
-# Dimensions
-
-FlixOpt's `FlowSystem` supports multiple dimensions for modeling optimization problems. Understanding these dimensions is crucial for interpreting the mathematical formulations presented in this documentation.
-
-## The Three Dimensions
-
-FlixOpt models can have up to three dimensions:
-
-1. **Time (`time`)** - **MANDATORY**
- - Represents the temporal evolution of the system
- - Defined via `pd.DatetimeIndex`
- - Must contain at least 2 timesteps
- - All optimization variables and constraints evolve over time
-2. **Period (`period`)** - **OPTIONAL**
- - Represents independent planning periods (e.g., years 2020, 2021, 2022)
- - Defined via `pd.Index` with integer values
- - Used for multi-period optimization such as investment planning across years
- - Each period is independent with its own time series
-3. **Scenario (`scenario`)** - **OPTIONAL**
- - Represents alternative futures or uncertainty realizations (e.g., "Base Case", "High Demand")
- - Defined via `pd.Index` with any labels
- - Scenarios within the same period share the same time dimension
- - Used for stochastic optimization or scenario comparison
-
----
-
-## Dimensional Structure
-
-**Coordinate System:**
-
-```python
-FlowSystemDimensions = Literal['time', 'period', 'scenario']
-
-coords = {
- 'time': pd.DatetimeIndex, # Always present
- 'period': pd.Index | None, # Optional
- 'scenario': pd.Index | None # Optional
-}
-```
-
-**Example:**
-```python
-import pandas as pd
-import numpy as np
-import flixopt as fx
-
-timesteps = pd.date_range('2020-01-01', periods=24, freq='h')
-scenarios = pd.Index(['Base Case', 'High Demand'])
-periods = pd.Index([2020, 2021, 2022])
-
-flow_system = fx.FlowSystem(
- timesteps=timesteps,
- periods=periods,
- scenarios=scenarios,
- scenario_weights=np.array([0.5, 0.5]) # Scenario weights
-)
-```
-
-This creates a system with:
-- 24 time steps per scenario per period
-- 2 scenarios with equal weights (0.5 each)
-- 3 periods (years)
-- **Total decision space:** 24 × 2 × 3 = 144 time-scenario-period combinations
-
----
-
-## Independence of Formulations
-
-**All mathematical formulations in this documentation are independent of whether periods or scenarios are present.**
-
-The equations shown throughout this documentation (for [Flow](elements/Flow.md), [Storage](elements/Storage.md), [Bus](elements/Bus.md), etc.) are written with only the time index $\text{t}_i$. When periods and/or scenarios are added, **the same equations apply** - they are simply expanded to additional dimensions.
-
-### How Dimensions Expand Formulations
-
-**Flow rate bounds** (from [Flow](elements/Flow.md)):
-
-$$
-\text{P} \cdot \text{p}^{\text{L}}_{\text{rel}}(\text{t}_{i}) \leq p(\text{t}_{i}) \leq \text{P} \cdot \text{p}^{\text{U}}_{\text{rel}}(\text{t}_{i})
-$$
-
-This equation remains valid regardless of dimensions:
-
-| Dimensions Present | Variable Indexing | Interpretation |
-|-------------------|-------------------|----------------|
-| Time only | $p(\text{t}_i)$ | Flow rate at time $\text{t}_i$ |
-| Time + Scenario | $p(\text{t}_i, s)$ | Flow rate at time $\text{t}_i$ in scenario $s$ |
-| Time + Period | $p(\text{t}_i, y)$ | Flow rate at time $\text{t}_i$ in period $y$ |
-| Time + Period + Scenario | $p(\text{t}_i, y, s)$ | Flow rate at time $\text{t}_i$ in period $y$, scenario $s$ |
-
-**The mathematical relationship remains identical** - only the indexing expands.
-
----
-
-## Independence Between Scenarios and Periods
-
-**There is no interconnection between scenarios and periods, except for shared investment decisions within a period.**
-
-### Scenario Independence
-
-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)$
-
-Scenarios are connected **only through the objective function** via weights:
-
-$$
-\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \text{Objective}_s
-$$
-
-Where:
-- $\mathcal{S}$ is the set of scenarios
-- $w_s$ is the weight for scenario $s$
-- The optimizer balances performance across scenarios according to their weights
-- **Both the objective effect and Penalty effect are weighted by $w_s$** (see [Penalty weighting](effects-penalty-objective.md#penalty))
-
-### Period Independence
-
-Periods are **completely independent** optimization problems:
-
-- Each period has separate operational variables
-- Each period has separate investment decisions
-- No temporal coupling between periods (e.g., storage state at end of period $y$ does not affect period $y+1$)
-- Periods cannot exchange resources or information
-
-Periods are connected **only through weighted aggregation** in the objective:
-
-$$
-\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \text{Objective}_y
-$$
-
-Where **both the objective effect and Penalty effect are weighted by $w_y$** (see [Penalty weighting](effects-penalty-objective.md#penalty))
-
-### Shared Periodic Decisions: The Exception
-
-**Investment decisions (sizes) can be shared across all scenarios:**
-
-By default, sizes (e.g., Storage capacity, Thermal power, ...) are **scenario-independent** but **flow_rates are scenario-specific**.
-
-**Example - Flow with investment:**
-
-$$
-v_\text{invest}(y) = s_\text{invest}(y) \cdot \text{size}_\text{fixed} \quad \text{(one decision per period)}
-$$
-
-$$
-p(\text{t}_i, y, s) \leq v_\text{invest}(y) \cdot \text{rel}_\text{upper} \quad \forall s \in \mathcal{S} \quad \text{(same capacity for all scenarios)}
-$$
-
-**Interpretation:**
-- "We decide once in period $y$ how much capacity to build" (periodic decision)
-- "This capacity is then operated differently in each scenario $s$ within period $y$" (temporal decisions)
-- "Periodic effects (investment) are incurred once per period, temporal effects (operational) are weighted across scenarios"
-
-This reflects real-world investment under uncertainty: you build capacity once (periodic/investment decision), but it operates under different conditions (temporal/operational decisions per scenario).
-
-**Mathematical Flexibility:**
-
-Variables can be either scenario-independent or scenario-specific:
-
-| Variable Type | Scenario-Independent | Scenario-Specific |
-|---------------|---------------------|-------------------|
-| **Sizes** (e.g., $\text{P}$) | $\text{P}(y)$ - Single value per period | $\text{P}(y, s)$ - Different per scenario |
-| **Flow rates** (e.g., $p(\text{t}_i)$) | $p(\text{t}_i, y)$ - Same across scenarios | $p(\text{t}_i, y, s)$ - Different per scenario |
-
-**Use Cases:**
-
-*Investment problems (with InvestParameters):*
-- **Sizes shared** (default): Investment under uncertainty - build capacity that performs well across all scenarios
-- **Sizes vary**: Scenario-specific capacity planning where different investments can be made for each future
-- **Selected sizes shared**: Mix of shared critical infrastructure and scenario-specific optional/flexible capacity
-
-*Dispatch problems (fixed sizes, no investments):*
-- **Flow rates shared**: Robust dispatch - find a single operational strategy that works across all forecast scenarios (e.g., day-ahead unit commitment under demand/weather uncertainty)
-- **Flow rates vary** (default): Scenario-adaptive dispatch - optimize operations for each scenario's specific conditions (demand, weather, prices)
-
-For implementation details on controlling scenario independence, see the [`FlowSystem`][flixopt.flow_system.FlowSystem] API reference.
-
----
-
-## Dimensional Impact on Objective Function
-
-The objective function aggregates effects across all dimensions with weights:
-
-### Time Only
-$$
-\min \quad \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i)
-$$
-
-### Time + Scenario
-$$
-\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, s) \right)
-$$
-
-### Time + Period
-$$
-\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, y) \right)
-$$
-
-### Time + Period + Scenario (Full Multi-Dimensional)
-$$
-\min \quad \sum_{y \in \mathcal{Y}} \sum_{s \in \mathcal{S}} w_{y,s} \cdot \left( \sum_{\text{t}_i \in \mathcal{T}} \sum_{e \in \mathcal{E}} s_{e}(\text{t}_i, y, s) \right)
-$$
-
-Where:
-- $\mathcal{T}$ is the set of time steps
-- $\mathcal{E}$ is the set of effects (including the Penalty effect $E_\Phi$)
-- $\mathcal{S}$ is the set of scenarios
-- $\mathcal{Y}$ is the set of periods
-- $s_{e}(\cdots)$ are the effect contributions (costs, emissions, etc.)
-- $w_s, w_y, w_{y,s}$ are the dimension weights
-- **Penalty effect is weighted identically to other effects**
-
-**See [Effects, Penalty & Objective](effects-penalty-objective.md) for complete formulations including:**
-- How temporal and periodic effects expand with dimensions
-- Detailed objective function for each dimensional case
-- Periodic (investment) vs temporal (operational) effect handling
-- Explicit Penalty weighting formulations
-
----
-
-## Weights
-
-Weights determine the relative importance of scenarios and periods in the objective function.
-
-### Scenario Weights
-
-You provide scenario weights explicitly via the `scenario_weights` parameter:
-
-```python
-flow_system = fx.FlowSystem(
- timesteps=timesteps,
- scenarios=scenarios,
- scenario_weights=np.array([0.3, 0.7]) # Scenario probabilities
-)
-```
-
-**Default:** If not specified, all scenarios have equal weight (normalized to sum to 1).
-
-### Period Weights
-
-Period weights are **automatically computed** from the period index (similar to how `hours_per_timestep` is computed from the time index):
-
-```python
-# Period weights are computed from the differences between period values
-periods = pd.Index([2020, 2025, 2030, 2035])
-# → period_weights = [5, 5, 5, 5] (representing 5-year intervals)
-
-flow_system = fx.FlowSystem(
- timesteps=timesteps,
- periods=periods,
- # No need to specify period weights - they're computed automatically
-)
-```
-
-**How period weights are computed:**
-- For periods `[2020, 2025, 2030, 2035]`, the weights are `[5, 5, 5, 5]` (the interval sizes)
-- This ensures that when you use `.sel()` to select a subset of periods, the weights are correctly recalculated
-- You can specify `weight_of_last_period` if the last period weight cannot be inferred from the index
-
-### Combined Weights
-
-When both periods and scenarios are present, the combined `weights` array (accessible via `flow_system.model.objective_weights`) is computed as:
-
-$$
-w_{y,s} = w_y \times \frac{w_s}{\sum_{s \in \mathcal{S}} w_s}
-$$
-
-Where:
-- $w_y$ are the period weights (computed from period index)
-- $w_s$ are the scenario weights (user-specified)
-- $\mathcal{S}$ is the set of all scenarios
-- The scenario weights are normalized to sum to 1 before multiplication
-
-**Example:**
-```python
-periods = pd.Index([2020, 2030, 2040]) # → period_weights = [10, 10, 10]
-scenarios = pd.Index(['Base', 'High'])
-scenario_weights = np.array([0.6, 0.4])
-
-flow_system = fx.FlowSystem(
- timesteps=timesteps,
- periods=periods,
- scenarios=scenarios,
- scenario_weights=scenario_weights
-)
-
-# Combined weights shape: (3 periods, 2 scenarios)
-# [[6.0, 4.0], # 2020: 10 × [0.6, 0.4]
-# [6.0, 4.0], # 2030: 10 × [0.6, 0.4]
-# [6.0, 4.0]] # 2040: 10 × [0.6, 0.4]
-```
-
-**Normalization:** Set `normalize_weights=False` in `Optimization` to turn off the normalization.
-
----
-
-## Summary Table
-
-| Dimension | Required? | Independence | Typical Use Case |
-|-----------|-----------|--------------|------------------|
-| **time** | ✅ Yes | Variables evolve over time via constraints (e.g., storage balance) | All optimization problems |
-| **scenario** | ❌ No | Fully independent operations; shared investments within period | Uncertainty modeling, risk assessment |
-| **period** | ❌ No | Fully independent; no coupling between periods | Multi-year planning, long-term investment |
-
-**Key Principle:** All constraints and formulations operate **within** each (period, scenario) combination independently. Only the objective function couples them via weighted aggregation.
-
----
-
-## See Also
-
-- [Effects, Penalty & Objective](effects-penalty-objective.md) - How dimensions affect the objective function
-- [InvestParameters](features/InvestParameters.md) - Investment decisions across scenarios
-- [FlowSystem API][flixopt.flow_system.FlowSystem] - Creating multi-dimensional systems
diff --git a/docs/user-guide/mathematical-notation/effects-and-dimensions.md b/docs/user-guide/mathematical-notation/effects-and-dimensions.md
new file mode 100644
index 000000000..011fc810e
--- /dev/null
+++ b/docs/user-guide/mathematical-notation/effects-and-dimensions.md
@@ -0,0 +1,415 @@
+# Effects & Dimensions
+
+Effects track metrics (costs, CO₂, energy). Dimensions define the structure over which effects aggregate.
+
+## Defining Effects
+
+```python
+costs = fx.Effect(label='costs', unit='€', is_objective=True)
+co2 = fx.Effect(label='co2', unit='kg')
+
+flow_system.add_elements(costs, co2)
+```
+
+One effect is the **objective** (minimized). Others are tracked or constrained.
+
+---
+
+## Effect Types
+
+=== "Temporal"
+
+ Accumulated over timesteps — operational costs, emissions, energy:
+
+ - Per flow hour: $E(t) = p(t) \cdot c \cdot \Delta t$
+ - Per event (startup): $E(t) = s^{start}(t) \cdot c$
+
+ ```python
+ fx.Flow(..., effects_per_flow_hour={'costs': 50}) # €50/MWh
+ ```
+
+=== "Periodic"
+
+ Time-independent — investment costs, fixed fees:
+
+ $E_{per} = P \cdot c_{inv}$
+
+ ```python
+ fx.InvestParameters(effects_of_investment_per_size={'costs': 200}) # €200/kW
+ ```
+
+=== "Total"
+
+ Sum of periodic and temporal components.
+
+---
+
+## Where Effects Are Contributed
+
+=== "Flow"
+
+ ```python
+ fx.Flow(
+ effects_per_flow_hour={'costs': 50, 'co2': 0.2}, # Per MWh
+ )
+ ```
+
+=== "Status"
+
+ ```python
+ fx.StatusParameters(
+ effects_per_startup={'costs': 1000}, # Per startup event
+ effects_per_active_hour={'costs': 10}, # Per hour while running
+ )
+ ```
+
+=== "Investment"
+
+ ```python
+ fx.InvestParameters(
+ effects_of_investment={'costs': 50000}, # Fixed if investing
+ effects_of_investment_per_size={'costs': 800}, # Per kW installed
+ effects_of_retirement={'costs': 10000}, # If NOT investing
+ )
+ ```
+
+=== "Bus"
+
+ ```python
+ fx.Bus(
+ excess_penalty_per_flow_hour=1e6, # Penalty for excess
+ shortage_penalty_per_flow_hour=1e6, # Penalty for shortage
+ )
+ ```
+
+---
+
+## Dimensions
+
+The model operates across three dimensions:
+
+=== "Timesteps"
+
+ The basic time resolution — always required:
+
+ ```python
+ flow_system = fx.FlowSystem(
+ timesteps=pd.date_range('2024-01-01', periods=8760, freq='h'),
+ )
+ ```
+
+ All variables and constraints are indexed by time. Temporal effects sum over timesteps.
+
+=== "Scenarios"
+
+ Represent uncertainty (weather, prices). Operations vary per scenario, investments are shared:
+
+ ```python
+ flow_system = fx.FlowSystem(
+ timesteps=pd.date_range('2024-01-01', periods=8760, freq='h'),
+ scenarios=pd.Index(['sunny_year', 'cloudy_year']),
+ scenario_weights=[0.7, 0.3],
+ )
+ ```
+
+ Scenarios are independent — no energy or information exchange between them.
+
+=== "Periods"
+
+ Sequential time blocks (years) for multi-period planning:
+
+ ```python
+ flow_system = fx.FlowSystem(
+ timesteps=pd.date_range('2024-01-01', periods=8760, freq='h'),
+ periods=pd.Index([2025, 2030]),
+ )
+ ```
+
+ Periods are independent — each has its own investment decisions.
+
+---
+
+## Objective Function
+
+The objective aggregates effects across all dimensions with weights:
+
+=== "Basic"
+
+ Single period, no scenarios:
+
+ $$\min \quad E_{per} + \sum_t E_{temp}(t)$$
+
+=== "With Scenarios"
+
+ Investment decided once, operations weighted by probability:
+
+ $$\min \quad E_{per} + \sum_s w_s \cdot \sum_t E_{temp}(t, s)$$
+
+ - $w_s$ — scenario weight (probability)
+
+=== "With Periods"
+
+ Multi-year planning with discounting:
+
+ $$\min \quad \sum_y w_y \cdot \left( E_{per}(y) + \sum_t E_{temp}(t, y) \right)$$
+
+ - $w_y$ — period weight (duration or discount factor)
+
+=== "Full"
+
+ Periods × Scenarios:
+
+ $$\min \quad \sum_y w_y \cdot \left( E_{per}(y) + \sum_s w_s \cdot \sum_t E_{temp}(t, y, s) \right)$$
+
+The penalty effect is always included: $\min \quad E_{objective} + E_{penalty}$
+
+---
+
+## Weights
+
+=== "Scenario Weights"
+
+ Provided explicitly — typically probabilities:
+
+ ```python
+ scenario_weights=[0.6, 0.4]
+ ```
+
+ Default: equal weights, normalized to sum to 1.
+
+=== "Period Weights"
+
+ Computed automatically from period index (interval sizes):
+
+ ```python
+ periods = pd.Index([2020, 2025, 2030])
+ # → weights: [5, 5, 5] (5-year intervals)
+ ```
+
+=== "Combined"
+
+ When both present:
+
+ $w_{y,s} = w_y \cdot w_s$
+
+---
+
+## Constraints on Effects
+
+=== "Total Limit"
+
+ Bound on aggregated effect (temporal + periodic) per period:
+
+ ```python
+ fx.Effect(label='co2', unit='kg', maximum_total=100_000)
+ ```
+
+=== "Per-Timestep Limit"
+
+ Bound at each timestep:
+
+ ```python
+ fx.Effect(label='peak', unit='kW', maximum_per_hour=500)
+ ```
+
+=== "Periodic Limit"
+
+ Bound on periodic component only:
+
+ ```python
+ fx.Effect(label='capex', unit='€', maximum_periodic=1_000_000)
+ ```
+
+=== "Temporal Limit"
+
+ Bound on temporal component only:
+
+ ```python
+ fx.Effect(label='opex', unit='€', maximum_temporal=500_000)
+ ```
+
+=== "Over All Periods"
+
+ Bound across all periods (weighted sum):
+
+ ```python
+ fx.Effect(label='co2', unit='kg', maximum_over_periods=1_000_000)
+ ```
+
+---
+
+## Cross-Effects
+
+Effects can contribute to each other (e.g., carbon pricing):
+
+```python
+co2 = fx.Effect(label='co2', unit='kg')
+
+costs = fx.Effect(
+ label='costs', unit='€', is_objective=True,
+ share_from_temporal={'co2': 0.08}, # €80/tonne
+)
+```
+
+---
+
+## Penalty Effect
+
+A built-in `Penalty` effect enables soft constraints and prevents infeasibility:
+
+```python
+fx.StatusParameters(effects_per_startup={'Penalty': 1})
+fx.Bus(label='heat', excess_penalty_per_flow_hour=1e5)
+```
+
+Penalty is weighted identically to the objective effect across all dimensions.
+
+---
+
+## Shared vs Independent Decisions
+
+=== "Investments (Sizes)"
+
+ By default, investment decisions are **shared across scenarios** within a period:
+
+ - Build capacity once → operate differently per scenario
+ - Reflects real-world investment under uncertainty
+
+ $$P(y) \quad \text{(one decision per period, used in all scenarios)}$$
+
+=== "Operations (Flows)"
+
+ By default, operational decisions are **independent per scenario**:
+
+ $$p(t, y, s) \quad \text{(different for each scenario)}$$
+
+---
+
+## Use Cases
+
+=== "Carbon Budget"
+
+ Limit total CO₂ emissions across all years:
+
+ ```python
+ co2 = fx.Effect(
+ label='co2', unit='kg',
+ maximum_over_periods=1_000_000, # 1000 tonnes total
+ )
+
+ # Contribute emissions from gas consumption
+ gas_flow = fx.Flow(
+ label='gas', bus=gas_bus,
+ effects_per_flow_hour={'co2': 0.2}, # 0.2 kg/kWh
+ )
+ ```
+
+=== "Investment Budget"
+
+ Cap annual investment spending:
+
+ ```python
+ capex = fx.Effect(
+ label='capex', unit='€',
+ maximum_periodic=5_000_000, # €5M per period
+ )
+
+ battery = fx.Storage(
+ ...,
+ capacity=fx.InvestParameters(
+ effects_of_investment_per_size={'capex': 600}, # €600/kWh
+ ),
+ )
+ ```
+
+=== "Peak Demand Charge"
+
+ Track and limit peak power:
+
+ ```python
+ peak = fx.Effect(
+ label='peak', unit='kW',
+ maximum_per_hour=1000, # Grid connection limit
+ )
+
+ grid_import = fx.Flow(
+ label='import', bus=elec_bus,
+ effects_per_flow_hour={'peak': 1}, # Track instantaneous power
+ )
+ ```
+
+=== "Carbon Pricing"
+
+ Add CO₂ cost to objective automatically:
+
+ ```python
+ co2 = fx.Effect(label='co2', unit='kg')
+
+ costs = fx.Effect(
+ label='costs', unit='€', is_objective=True,
+ share_from_temporal={'co2': 0.08}, # €80/tonne carbon price
+ )
+
+ # Now any CO₂ contribution automatically adds to costs
+ ```
+
+=== "Land Use Constraint"
+
+ Limit total land area for installations:
+
+ ```python
+ land = fx.Effect(
+ label='land', unit='m²',
+ maximum_periodic=50_000, # 5 hectares max
+ )
+
+ pv = fx.Source(
+ ...,
+ output=fx.Flow(
+ ...,
+ invest_parameters=fx.InvestParameters(
+ effects_of_investment_per_size={'land': 5}, # 5 m²/kWp
+ ),
+ ),
+ )
+ ```
+
+=== "Multi-Criteria Optimization"
+
+ Track multiple objectives, optimize one:
+
+ ```python
+ costs = fx.Effect(label='costs', unit='€', is_objective=True)
+ co2 = fx.Effect(label='co2', unit='kg')
+ primary_energy = fx.Effect(label='PE', unit='kWh')
+
+ # All are tracked, costs is minimized
+ # Use maximum_total on co2 for ε-constraint method
+ ```
+
+---
+
+## Reference
+
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $E_{temp}(t)$ | $\mathbb{R}$ | Temporal effect at timestep $t$ |
+| $E_{per}$ | $\mathbb{R}$ | Periodic effect (per period) |
+| $E$ | $\mathbb{R}$ | Total effect ($E_{per} + \sum_t E_{temp}(t)$) |
+| $w_s$ | $\mathbb{R}_{\geq 0}$ | Scenario weight (probability) |
+| $w_y$ | $\mathbb{R}_{> 0}$ | Period weight (duration/discount) |
+| $p(t)$ | $\mathbb{R}_{\geq 0}$ | Flow rate at timestep $t$ |
+| $s^{start}(t)$ | $\{0, 1\}$ | Startup indicator |
+| $P$ | $\mathbb{R}_{\geq 0}$ | Investment size |
+| $c$ | $\mathbb{R}$ | Effect coefficient |
+| $\Delta t$ | $\mathbb{R}_{> 0}$ | Timestep duration (hours) |
+
+| Constraint | Python | Scope |
+|-----------|--------|-------|
+| Total limit | `maximum_total` | Per period |
+| Timestep limit | `maximum_per_hour` | Each timestep |
+| Periodic limit | `maximum_periodic` | Per period (periodic only) |
+| Temporal limit | `maximum_temporal` | Per period (temporal only) |
+| Global limit | `maximum_over_periods` | Across all periods |
+
+**Classes:** [`Effect`][flixopt.effects.Effect], [`EffectCollection`][flixopt.effects.EffectCollection]
diff --git a/docs/user-guide/mathematical-notation/effects-penalty-objective.md b/docs/user-guide/mathematical-notation/effects-penalty-objective.md
deleted file mode 100644
index 1c96f3613..000000000
--- a/docs/user-guide/mathematical-notation/effects-penalty-objective.md
+++ /dev/null
@@ -1,333 +0,0 @@
-# Effects, Penalty & Objective
-
-## Effects
-
-[`Effects`][flixopt.effects.Effect] are used to quantify system-wide impacts like costs, emissions, or resource consumption. These arise from **shares** contributed by **Elements** such as [Flows](elements/Flow.md), [Storage](elements/Storage.md), and other components.
-
-**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)
-
-Effects are categorized into two domains:
-
-1. **Temporal effects** - Time-dependent contributions (e.g., operational costs, hourly emissions)
-2. **Periodic effects** - Time-independent contributions (e.g., investment costs, fixed annual fees)
-
-### Multi-Dimensional Effects
-
-**The formulations below are written with time index $\text{t}_i$ only, but automatically expand when periods and/or scenarios are present.**
-
-When the FlowSystem has additional dimensions (see [Dimensions](dimensions.md)):
-
-- **Temporal effects** are indexed by all present dimensions: $E_{e,\text{temp}}(\text{t}_i, y, s)$
-- **Periodic effects** are indexed by period only (scenario-independent within a period): $E_{e,\text{per}}(y)$
-- Effects are aggregated with dimension weights in the objective function
-
-For complete details on how dimensions affect effects and the objective, see [Dimensions](dimensions.md).
-
----
-
-## Effect Formulation
-
-### Shares from Elements
-
-Each element $l$ contributes shares to effect $e$ in both temporal and periodic domains:
-
-**Periodic shares** (time-independent):
-$$ \label{eq:Share_periodic}
-s_{l \rightarrow e, \text{per}} = \sum_{v \in \mathcal{V}_{l, \text{per}}} v \cdot \text{a}_{v \rightarrow e}
-$$
-
-**Temporal shares** (time-dependent):
-$$ \label{eq:Share_temporal}
-s_{l \rightarrow e, \text{temp}}(\text{t}_i) = \sum_{v \in \mathcal{V}_{l,\text{temp}}} v(\text{t}_i) \cdot \text{a}_{v \rightarrow e}(\text{t}_i)
-$$
-
-Where:
-
-- $\text{t}_i$ is the time step
-- $\mathcal{V}_l$ is the set of all optimization variables of element $l$
-- $\mathcal{V}_{l, \text{per}}$ is the subset of periodic (investment-related) variables
-- $\mathcal{V}_{l, \text{temp}}$ is the subset of temporal (operational) variables
-- $v$ is an optimization variable
-- $v(\text{t}_i)$ is the variable value at timestep $\text{t}_i$
-- $\text{a}_{v \rightarrow e}$ is the effect factor (e.g., €/kW for investment, €/kWh for operation)
-- $s_{l \rightarrow e, \text{per}}$ is the periodic share of element $l$ to effect $e$
-- $s_{l \rightarrow e, \text{temp}}(\text{t}_i)$ is the temporal share of element $l$ to effect $e$
-
-**Examples:**
-- **Periodic share**: Investment cost = $\text{size} \cdot \text{specific\_cost}$ (€/kW)
-- **Temporal share**: Operational cost = $\text{flow\_rate}(\text{t}_i) \cdot \text{price}(\text{t}_i)$ (€/kWh)
-
----
-
-### Cross-Effect Contributions
-
-Effects can contribute shares to other effects, enabling relationships like carbon pricing or resource accounting.
-
-An effect $x$ can contribute to another effect $e \in \mathcal{E}\backslash x$ via conversion factors:
-
-**Example:** CO₂ emissions (kg) → Monetary costs (€)
-- Effect $x$: "CO₂ emissions" (unit: kg)
-- Effect $e$: "costs" (unit: €)
-- Factor $\text{r}_{x \rightarrow e}$: CO₂ price (€/kg)
-
-**Note:** Circular references must be avoided.
-
-### Total Effect Calculation
-
-**Periodic effects** aggregate element shares and cross-effect contributions:
-
-$$ \label{eq:Effect_periodic}
-E_{e, \text{per}} =
-\sum_{l \in \mathcal{L}} s_{l \rightarrow e,\text{per}} +
-\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{per}} \cdot \text{r}_{x \rightarrow e,\text{per}}
-$$
-
-**Temporal effects** at each timestep:
-
-$$ \label{eq:Effect_temporal}
-E_{e, \text{temp}}(\text{t}_{i}) =
-\sum_{l \in \mathcal{L}} s_{l \rightarrow e, \text{temp}}(\text{t}_i) +
-\sum_{x \in \mathcal{E}\backslash e} E_{x, \text{temp}}(\text{t}_i) \cdot \text{r}_{x \rightarrow {e},\text{temp}}(\text{t}_i)
-$$
-
-**Total temporal effects** (sum over all timesteps):
-
-$$\label{eq:Effect_temporal_total}
-E_{e,\text{temp},\text{tot}} = \sum_{i=1}^n E_{e,\text{temp}}(\text{t}_{i})
-$$
-
-**Total effect** (combining both domains):
-
-$$ \label{eq:Effect_Total}
-E_{e} = E_{e,\text{per}} + E_{e,\text{temp},\text{tot}}
-$$
-
-Where:
-
-- $\mathcal{L}$ is the set of all elements in the FlowSystem
-- $\mathcal{E}$ is the set of all effects
-- $\text{r}_{x \rightarrow e, \text{per}}$ is the periodic conversion factor from effect $x$ to effect $e$
-- $\text{r}_{x \rightarrow e, \text{temp}}(\text{t}_i)$ is the temporal conversion factor
-
----
-
-### Constraining Effects
-
-Effects can be bounded to enforce limits on costs, emissions, or other impacts:
-
-**Total bounds** (apply to $E_{e,\text{per}}$, $E_{e,\text{temp},\text{tot}}$, or $E_e$):
-
-$$ \label{eq:Bounds_Total}
-E^\text{L} \leq E \leq E^\text{U}
-$$
-
-**Temporal bounds per timestep:**
-
-$$ \label{eq:Bounds_Timestep}
-E_{e,\text{temp}}^\text{L}(\text{t}_i) \leq E_{e,\text{temp}}(\text{t}_i) \leq E_{e,\text{temp}}^\text{U}(\text{t}_i)
-$$
-
-**Implementation:** See [`Effect`][flixopt.effects.Effect] parameters:
-- `minimum_temporal`, `maximum_temporal` - Total temporal bounds
-- `minimum_per_hour`, `maximum_per_hour` - Hourly temporal bounds
-- `minimum_periodic`, `maximum_periodic` - Periodic bounds
-- `minimum_total`, `maximum_total` - Combined total bounds
-
----
-
-## Penalty
-
-Every FlixOpt model includes a special **Penalty Effect** $E_\Phi$ to:
-
-- Prevent infeasible problems
-- Allow introducing a bias without influencing effects, simplifying results analysis
-
-**Key Feature:** Penalty is implemented as a standard Effect (labeled `Penalty`), so you can **add penalty contributions anywhere effects are used**:
-
-```python
-import flixopt as fx
-
-# Add penalty contributions just like any other effect
-on_off = fx.OnOffParameters(
- effects_per_switch_on={'Penalty': 1} # Add bias against switching on this component, without adding costs
-)
-```
-
-**Optionally Define Custom Penalty:**
-Users can define their own Penalty effect with custom properties (unit, constraints, etc.):
-
-```python
-# Define custom penalty effect (must use fx.PENALTY_EFFECT_LABEL)
-custom_penalty = fx.Effect(
- fx.PENALTY_EFFECT_LABEL, # Always use this constant: 'Penalty'
- unit='€',
- description='Penalty costs for constraint violations',
- maximum_total=1e6, # Limit total penalty for debugging
-)
-flow_system.add_elements(custom_penalty)
-```
-
-If not user-defined, the Penalty effect is automatically created during modeling with default settings.
-
-**Periodic penalty shares** (time-independent):
-$$ \label{eq:Penalty_periodic}
-E_{\Phi, \text{per}} = \sum_{l \in \mathcal{L}} s_{l \rightarrow \Phi,\text{per}}
-$$
-
-**Temporal penalty shares** (time-dependent):
-$$ \label{eq:Penalty_temporal}
-E_{\Phi, \text{temp}}(\text{t}_{i}) = \sum_{l \in \mathcal{L}} s_{l \rightarrow \Phi, \text{temp}}(\text{t}_i)
-$$
-
-**Total penalty** (combining both domains):
-$$ \label{eq:Penalty_total}
-E_{\Phi} = E_{\Phi,\text{per}} + \sum_{\text{t}_i \in \mathcal{T}} E_{\Phi, \text{temp}}(\text{t}_{i})
-$$
-
-Where:
-
-- $\mathcal{L}$ is the set of all elements
-- $\mathcal{T}$ is the set of all timesteps
-- $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.
-
-**Key properties:**
-- Penalty shares are added via `add_share_to_effects(name, expressions={fx.PENALTY_EFFECT_LABEL: ...}, target='temporal'/'periodic')`
-- Like other effects, penalty can be constrained (e.g., `maximum_total` for debugging)
-- Results include breakdown: temporal, periodic, and total penalty contributions
-- Penalty is always added to the objective function (cannot be disabled)
-- Access via `flow_system.effects.penalty_effect` or `flow_system.effects[fx.PENALTY_EFFECT_LABEL]`
-- **Scenario weighting**: Penalty is weighted identically to the objective effect—see [Time + Scenario](#time--scenario) for details
-
----
-
-## Objective Function
-
-The optimization objective minimizes the chosen effect plus the penalty effect:
-
-$$ \label{eq:Objective}
-\min \left( E_{\Omega} + E_{\Phi} \right)
-$$
-
-Where:
-
-- $E_{\Omega}$ is the chosen **objective effect** (see $\eqref{eq:Effect_Total}$)
-- $E_{\Phi}$ is the [penalty effect](#penalty) (see $\eqref{eq:Penalty_total}$)
-
-One effect must be designated as the objective via `is_objective=True`. The penalty effect is automatically created and always added to the objective.
-
-### Multi-Criteria Optimization
-
-This formulation supports multiple optimization approaches:
-
-**1. Weighted Sum Method**
-- The objective effect can incorporate other effects via cross-effect factors
-- Example: Minimize costs while including carbon pricing: $\text{CO}_2 \rightarrow \text{costs}$
-
-**2. ε-Constraint Method**
-- Optimize one effect while constraining others
-- Example: Minimize costs subject to $\text{CO}_2 \leq 1000$ kg
-
----
-
-## Objective with Multiple Dimensions
-
-When the FlowSystem includes **periods** and/or **scenarios** (see [Dimensions](dimensions.md)), the objective aggregates effects across all dimensions using weights.
-
-### Time Only (Base Case)
-
-$$
-\min \quad E_{\Omega} + E_{\Phi} = \sum_{\text{t}_i \in \mathcal{T}} E_{\Omega,\text{temp}}(\text{t}_i) + E_{\Omega,\text{per}} + E_{\Phi,\text{per}} + \sum_{\text{t}_i \in \mathcal{T}} E_{\Phi,\text{temp}}(\text{t}_i)
-$$
-
-Where:
-- Temporal effects sum over time: $\sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i)$ and $\sum_{\text{t}_i} E_{\Phi,\text{temp}}(\text{t}_i)$
-- Periodic effects are constant: $E_{\Omega,\text{per}}$ and $E_{\Phi,\text{per}}$
-
----
-
-### Time + Scenario
-
-$$
-\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( E_{\Omega}(s) + E_{\Phi}(s) \right)
-$$
-
-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$)
-- Temporal effects are **scenario-specific**: $E_{\Omega,\text{temp}}(s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, s)$ and $E_{\Phi,\text{temp}}(s) = \sum_{\text{t}_i} E_{\Phi,\text{temp}}(\text{t}_i, s)$
-
-**Interpretation:**
-- Investment decisions (periodic) made once, used across all scenarios
-- Operations (temporal) differ by scenario
-- Objective balances expected value across scenarios
-- **Both $E_{\Omega}$ (objective effect) and $E_{\Phi}$ (penalty) are weighted identically by $w_s$**
-
----
-
-### Time + Period
-
-$$
-\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( E_{\Omega}(y) + E_{\Phi}(y) \right)
-$$
-
-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)
-- Each period $y$ has **independent** investment and operational decisions
-- **Both $E_{\Omega}$ (objective effect) and $E_{\Phi}$ (penalty) are weighted identically by $w_y$**
-
----
-
-### Time + Period + Scenario (Full Multi-Dimensional)
-
-$$
-\min \quad \sum_{y \in \mathcal{Y}} \left[ w_y \cdot \left( E_{\Omega,\text{per}}(y) + E_{\Phi,\text{per}}(y) \right) + \sum_{s \in \mathcal{S}} w_{y,s} \cdot \left( E_{\Omega,\text{temp}}(y,s) + E_{\Phi,\text{temp}}(y,s) \right) \right]
-$$
-
-Where:
-- $\mathcal{S}$ is the set of scenarios
-- $\mathcal{Y}$ is the set of periods
-- $w_y$ is the period weight (for periodic effects)
-- $w_{y,s}$ is the combined period-scenario weight (for temporal effects)
-- **Periodic effects** $E_{\Omega,\text{per}}(y)$ and $E_{\Phi,\text{per}}(y)$ are period-specific but **scenario-independent**
-- **Temporal effects** $E_{\Omega,\text{temp}}(y,s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, y, s)$ and $E_{\Phi,\text{temp}}(y,s) = \sum_{\text{t}_i} E_{\Phi,\text{temp}}(\text{t}_i, y, s)$ are **fully indexed**
-
-**Key Principle:**
-- Scenarios and periods are **operationally independent** (no energy/resource exchange)
-- Coupled **only through the weighted objective function**
-- **Periodic effects within a period are shared across all scenarios** (investment made once per period)
-- **Temporal effects are independent per scenario** (different operations under different conditions)
-- **Both $E_{\Omega}$ (objective effect) and $E_{\Phi}$ (penalty) use identical weighting** ($w_y$ for periodic, $w_{y,s}$ for temporal)
-
----
-
-## Summary
-
-| Concept | Formulation | Time Dependency | Dimension Indexing |
-|---------|-------------|-----------------|-------------------|
-| **Temporal share** | $s_{l \rightarrow e, \text{temp}}(\text{t}_i)$ | Time-dependent | $(t, y, s)$ when present |
-| **Periodic share** | $s_{l \rightarrow e, \text{per}}$ | Time-independent | $(y)$ when periods present |
-| **Total temporal effect** | $E_{e,\text{temp},\text{tot}} = \sum_{\text{t}_i} E_{e,\text{temp}}(\text{t}_i)$ | Sum over time | Depends on dimensions |
-| **Total periodic effect** | $E_{e,\text{per}}$ | Constant | $(y)$ when periods present |
-| **Total effect** | $E_e = E_{e,\text{per}} + E_{e,\text{temp},\text{tot}}$ | Combined | Depends on dimensions |
-| **Penalty effect** | $E_\Phi = E_{\Phi,\text{per}} + E_{\Phi,\text{temp},\text{tot}}$ | Combined (same as effects) | **Weighted identically to objective effect** |
-| **Objective** | $\min(E_{\Omega} + E_{\Phi})$ | With weights when multi-dimensional | See formulations above |
-
----
-
-## See Also
-
-- [Dimensions](dimensions.md) - Complete explanation of multi-dimensional modeling
-- [Flow](elements/Flow.md) - Temporal effect contributions via `effects_per_flow_hour`
-- [InvestParameters](features/InvestParameters.md) - Periodic effect contributions via investment
-- [Effect API][flixopt.effects.Effect] - Implementation details and parameters
diff --git a/docs/user-guide/mathematical-notation/elements/Bus.md b/docs/user-guide/mathematical-notation/elements/Bus.md
index bfe57d234..464381fe8 100644
--- a/docs/user-guide/mathematical-notation/elements/Bus.md
+++ b/docs/user-guide/mathematical-notation/elements/Bus.md
@@ -1,49 +1,52 @@
-A Bus is a simple nodal balance between its incoming and outgoing flow rates.
+# Bus
-$$ \label{eq:bus_balance}
- \sum_{f_\text{in} \in \mathcal{F}_\text{in}} p_{f_\text{in}}(\text{t}_i) =
- \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.
+A Bus is where flows meet and must balance — inputs equal outputs at every timestep.
-This changes the balance to
+## Basic: Balance Equation
-$$ \label{eq:bus_balance-excess}
- \sum_{f_\text{in} \in \mathcal{F}_\text{in}} p_{f_ \text{in}}(\text{t}_i) + \phi_\text{in}(\text{t}_i) =
- \sum_{f_\text{out} \in \mathcal{F}_\text{out}} p_{f_\text{out}}(\text{t}_i) + \phi_\text{out}(\text{t}_i)
$$
-
-The penalty term is defined as
-
-$$ \label{eq:bus_penalty}
- s_{b \rightarrow \Phi}(\text{t}_i) =
- \text a_{b \rightarrow \Phi}(\text{t}_i) \cdot \Delta \text{t}_i
- \cdot [ \phi_\text{in}(\text{t}_i) + \phi_\text{out}(\text{t}_i) ]
+\sum_{in} p(t) = \sum_{out} p(t)
$$
-With:
+```python
+heat_bus = fx.Bus(label='heat')
+# All flows connected to this bus must balance
+```
-- $\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
-- $\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`)
+If balance can't be achieved → model is **infeasible**.
---
-## Implementation
+## With Imbalance Penalty
+
+Allow imbalance for debugging or soft constraints:
-**Python Class:** [`Bus`][flixopt.elements.Bus]
+$$
+\sum_{in} p(t) + \phi_{in}(t) = \sum_{out} p(t) + \phi_{out}(t)
+$$
+
+The slack variables $\phi$ are penalized: $(\phi_{in} + \phi_{out}) \cdot \Delta t \cdot c_\phi$
-See the API documentation for implementation details and usage examples.
+```python
+heat_bus = fx.Bus(
+ label='heat',
+ imbalance_penalty_per_flow_hour=1e5 # High penalty for imbalance
+)
+```
+
+!!! tip "Debugging"
+ If you see a `virtual_demand` or `virtual_supply` and its non zero in results → your system couldn't meet demand. Check capacities and connections.
---
-## See Also
+## Reference
+
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $p(t)$ | $\mathbb{R}_{\geq 0}$ | Flow rate of connected flows |
+| $\phi_{in}(t)$ | $\mathbb{R}_{\geq 0}$ | Slack: virtual supply (covers shortages) |
+| $\phi_{out}(t)$ | $\mathbb{R}_{\geq 0}$ | Slack: virtual demand (absorbs surplus) |
+| $c_\phi$ | $\mathbb{R}_{\geq 0}$ | Penalty factor (`imbalance_penalty_per_flow_hour`) |
+| $\Delta t$ | $\mathbb{R}_{> 0}$ | Timestep duration (hours) |
-- [Flow](../elements/Flow.md) - Definition of flow rates in the balance
-- [Effects, Penalty & Objective](../effects-penalty-objective.md) - How penalties are included in the objective function
-- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks
+**Classes:** [`Bus`][flixopt.elements.Bus], [`BusModel`][flixopt.elements.BusModel]
diff --git a/docs/user-guide/mathematical-notation/elements/Flow.md b/docs/user-guide/mathematical-notation/elements/Flow.md
index 5914ba911..4f5f9dcf3 100644
--- a/docs/user-guide/mathematical-notation/elements/Flow.md
+++ b/docs/user-guide/mathematical-notation/elements/Flow.md
@@ -1,64 +1,131 @@
# Flow
-The flow_rate is the main optimization variable of the Flow. It's limited by the size of the Flow and relative bounds \eqref{eq:flow_rate}.
+A Flow is the primary optimization variable — the solver decides how much flows at each timestep.
-$$ \label{eq:flow_rate}
- \text P \cdot \text p^{\text{L}}_{\text{rel}}(\text{t}_{i})
- \leq p(\text{t}_{i}) \leq
- \text P \cdot \text p^{\text{U}}_{\text{rel}}(\text{t}_{i})
-$$
-
-With:
+## Basic: Bounded Flow Rate
-- $\text P$ being the size of the Flow
-- $p(\text{t}_{i})$ being the flow-rate at time $\text{t}_{i}$
-- $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i})$ being the relative lower bound (typically 0)
-- $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i})$ being the relative upper bound (typically 1)
-
-With $\text p^{\text{L}}_{\text{rel}}(\text{t}_{i}) = 0$ and $\text p^{\text{U}}_{\text{rel}}(\text{t}_{i}) = 1$,
-equation \eqref{eq:flow_rate} simplifies to
+Every flow has a **size** $P$ (capacity) and a **flow rate** $p(t)$ (what the solver optimizes):
$$
- 0 \leq p(\text{t}_{i}) \leq \text P
+P \cdot p_{rel}^{min} \leq p(t) \leq P \cdot p_{rel}^{max}
$$
+```python
+# 100 kW boiler, minimum 30% when running
+heat = fx.Flow(label='heat', bus=heat_bus, size=100, relative_minimum=0.3)
+# → 30 ≤ p(t) ≤ 100
+```
-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)
-to change the size of the Flow from a constant to an optimization variable.
+!!! warning "Cannot be zero"
+ With `relative_minimum > 0`, the flow cannot be zero. Use `status_parameters` to allow shutdown.
---
-## Mathematical Patterns Used
+## Adding Features
+
+=== "Status"
+
+ Allow the flow to be zero with `status_parameters`:
+
+ $s(t) \cdot P \cdot p_{rel}^{min} \leq p(t) \leq s(t) \cdot P \cdot p_{rel}^{max}$
+
+ Where $s(t) \in \{0, 1\}$: inactive or active.
+
+ ```python
+ generator = fx.Flow(
+ label='power', bus=elec_bus, size=50,
+ relative_minimum=0.4,
+ status_parameters=fx.StatusParameters(
+ effects_per_startup={'costs': 500},
+ min_uptime=2,
+ ),
+ )
+ ```
+
+ See [StatusParameters](../features/StatusParameters.md).
+
+=== "Variable Size"
+
+ Optimize the capacity with `InvestParameters`:
+
+ $P^{min} \leq P \leq P^{max}$
+
+ ```python
+ battery = fx.Flow(
+ label='power', bus=elec_bus,
+ size=fx.InvestParameters(
+ minimum_size=0,
+ maximum_size=1000,
+ specific_effects={'costs': 100_000},
+ ),
+ )
+ ```
+
+ See [InvestParameters](../features/InvestParameters.md).
-Flow formulation uses the following modeling patterns:
+=== "Flow Effects"
-- **[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)
-- **[Bounds with State](../modeling-patterns/bounds-and-states.md#bounds-with-state)** - Investment decisions with [InvestParameters](../features/InvestParameters.md)
+ Add effects per energy (flow hours) moved:
+
+ ```python
+ gas = fx.Flow(
+ label='gas', bus=gas_bus, size=150,
+ effects_per_flow_hour={'costs': 50}, # €50/MWh
+ )
+ ```
+
+ Flow hours: $h(t) = p(t) \cdot \Delta t$
+
+
+=== "Fixed Profile"
+
+ Lock the flow to a time series (demands, renewables):
+
+ $p(t) = P \cdot \pi(t)$
+
+ ```python
+ demand = fx.Flow(
+ label='demand', bus=heat_bus, size=100,
+ fixed_relative_profile=[0.5, 0.8, 1.0, 0.6] # π(t)
+ )
+ ```
---
-## Implementation
+## Optional Constraints
-**Python Class:** [`Flow`][flixopt.elements.Flow]
+=== "Load Factor"
-**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))
+ Constrain average utilization:
-See the [`Flow`][flixopt.elements.Flow] API documentation for complete parameter list and usage examples.
+ $\lambda_{min} \leq \frac{\sum_t p(t)}{P \cdot n_t} \leq \lambda_{max}$
+
+ ```python
+ fx.Flow(..., load_factor_min=0.5, load_factor_max=0.9)
+ ```
+
+=== "Flow Hours"
+
+ Constrain total energy:
+
+ $h_{min} \leq \sum_t p(t) \cdot \Delta t \leq h_{max}$
+
+ ```python
+ fx.Flow(..., flow_hours_min=1000, flow_hours_max=5000)
+ ```
---
-## See Also
+## Reference
+
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $p(t)$ | $\mathbb{R}_{\geq 0}$ | Flow rate at timestep $t$ |
+| $P$ | $\mathbb{R}_{\geq 0}$ | Size (capacity) — fixed or optimized |
+| $s(t)$ | $\{0, 1\}$ | Binary status (with `status_parameters`) |
+| $p_{rel}^{min}$ | $\mathbb{R}_{\geq 0}$ | Minimum relative flow (`relative_minimum`) |
+| $p_{rel}^{max}$ | $\mathbb{R}_{\geq 0}$ | Maximum relative flow (`relative_maximum`) |
+| $\pi(t)$ | $\mathbb{R}_{\geq 0}$ | Fixed profile (`fixed_relative_profile`) |
+| $\Delta t$ | $\mathbb{R}_{> 0}$ | Timestep duration (hours) |
-- [OnOffParameters](../features/OnOffParameters.md) - Binary on/off operation
-- [InvestParameters](../features/InvestParameters.md) - Variable flow sizing
-- [Bus](../elements/Bus.md) - Flow balance constraints
-- [LinearConverter](../elements/LinearConverter.md) - Flow ratio constraints
-- [Storage](../elements/Storage.md) - Flow integration over time
-- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks
+**Classes:** [`Flow`][flixopt.elements.Flow], [`FlowModel`][flixopt.elements.FlowModel]
diff --git a/docs/user-guide/mathematical-notation/elements/LinearConverter.md b/docs/user-guide/mathematical-notation/elements/LinearConverter.md
index b007aa7f5..915537d60 100644
--- a/docs/user-guide/mathematical-notation/elements/LinearConverter.md
+++ b/docs/user-guide/mathematical-notation/elements/LinearConverter.md
@@ -1,50 +1,151 @@
-[`LinearConverters`][flixopt.components.LinearConverter] define a ratio between incoming and outgoing [Flows](../elements/Flow.md).
+# LinearConverter
-$$ \label{eq:Linear-Transformer-Ratio}
- \sum_{f_{\text{in}} \in \mathcal F_{in}} \text a_{f_{\text{in}}}(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = \sum_{f_{\text{out}} \in \mathcal F_{out}} \text b_{f_\text{out}}(\text{t}_i) \cdot p_{f_\text{out}}(\text{t}_i)
+A LinearConverter transforms inputs into outputs with fixed ratios.
+
+## Basic: Conversion Equation
+
+$$
+\sum_{in} a_f \cdot p_f(t) = \sum_{out} b_f \cdot p_f(t)
$$
-With:
+=== "Boiler (η = 90%)"
+
+ $0.9 \cdot p_{gas}(t) = p_{heat}(t)$
+
+ ```python
+ boiler = fx.LinearConverter(
+ label='boiler',
+ inputs=[fx.Flow(label='gas', bus=gas_bus, size=111)],
+ outputs=[fx.Flow(label='heat', bus=heat_bus, size=100)],
+ conversion_factors=[{'gas': 0.9, 'heat': 1}],
+ )
+ ```
+
+=== "Heat Pump (COP = 3.5)"
+
+ $3.5 \cdot p_{el}(t) = p_{heat}(t)$
+
+ ```python
+ hp = fx.LinearConverter(
+ label='hp',
+ inputs=[fx.Flow(label='el', bus=elec_bus, size=100)],
+ outputs=[fx.Flow(label='heat', bus=heat_bus, size=350)],
+ conversion_factors=[{'el': 3.5, 'heat': 1}],
+ )
+ ```
+
+=== "CHP (35% el, 50% th)"
+
+ Two constraints linking fuel to outputs:
+
+ ```python
+ chp = fx.LinearConverter(
+ label='chp',
+ inputs=[fx.Flow(label='fuel', bus=gas_bus, size=100)],
+ outputs=[
+ fx.Flow(label='el', bus=elec_bus, size=35),
+ fx.Flow(label='heat', bus=heat_bus, size=50),
+ ],
+ conversion_factors=[
+ {'fuel': 0.35, 'el': 1},
+ {'fuel': 0.50, 'heat': 1},
+ ],
+ )
+ ```
-- $\mathcal F_{in}$ and $\mathcal F_{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
-- $\text a_{f_\text{in}}(\text{t}_i)$ and $\text b_{f_\text{out}}(\text{t}_i)$ being the ratio of the flow-rate at time $\text{t}_i$ for flow $f_\text{in}$ and $f_\text{out}$, respectively
+---
-With one incoming **Flow** and one outgoing **Flow**, this can be simplified to:
+## Time-Varying Efficiency
-$$ \label{eq:Linear-Transformer-Ratio-simple}
- \text a(\text{t}_i) \cdot p_{f_\text{in}}(\text{t}_i) = p_{f_\text{out}}(\text{t}_i)
-$$
+Pass a list for time-dependent conversion:
+
+```python
+cop = np.array([3.0, 3.2, 3.5, 4.0, 3.8, ...]) # Varies with ambient temperature
-where $\text a$ can be interpreted as the conversion efficiency of the **LinearConverter**.
+hp = fx.LinearConverter(
+ ...,
+ conversion_factors=[{'el': cop, 'heat': 1}],
+)
+```
-#### Piecewise Conversion factors
-The conversion efficiency can be defined as a piecewise linear approximation. See [Piecewise](../features/Piecewise.md) for more details.
+---
+
+## Convenience Classes
+
+```python
+# Boiler
+boiler = fx.linear_converters.Boiler(
+ label='boiler', eta=0.9,
+ Q_th=fx.Flow(label='heat', bus=heat_bus, size=100),
+ Q_fu=fx.Flow(label='fuel', bus=gas_bus),
+)
+
+# Heat Pump
+hp = fx.linear_converters.HeatPump(
+ label='hp', COP=3.5,
+ P_el=fx.Flow(label='el', bus=elec_bus, size=100),
+ Q_th=fx.Flow(label='heat', bus=heat_bus),
+)
+
+# CHP
+chp = fx.linear_converters.CHP(
+ label='chp', eta_el=0.35, eta_th=0.50,
+ P_el=fx.Flow(...), Q_th=fx.Flow(...), Q_fu=fx.Flow(...),
+)
+```
---
-## Implementation
+## Adding Features
+
+=== "Status"
-**Python Class:** [`LinearConverter`][flixopt.components.LinearConverter]
+ A component is active when any of its flows is non-zero. Add startup costs, minimum run times:
-**Specialized Linear Converters:**
+ ```python
+ gen = fx.LinearConverter(
+ ...,
+ status_parameters=fx.StatusParameters(
+ effects_per_startup={'costs': 1000},
+ min_uptime=4,
+ ),
+ )
+ ```
-FlixOpt provides specialized linear converter classes for common applications:
+ See [StatusParameters](../features/StatusParameters.md).
-- **[`HeatPump`][flixopt.linear_converters.HeatPump]** - Coefficient of Performance (COP) based conversion
-- **[`Power2Heat`][flixopt.linear_converters.Power2Heat]** - Electric heating with efficiency ≤ 1
-- **[`CHP`][flixopt.linear_converters.CHP]** - Combined heat and power generation
-- **[`Boiler`][flixopt.linear_converters.Boiler]** - Fuel to heat conversion
+=== "Piecewise Conversion"
-These classes handle the mathematical formulation automatically based on physical relationships.
+ For variable efficiency — all flows change together based on operating point:
-See the API documentation for implementation details and usage examples.
+ ```python
+ chp = fx.LinearConverter(
+ label='CHP',
+ inputs=[fx.Flow('fuel', bus=gas_bus)],
+ outputs=[
+ fx.Flow('el', bus=elec_bus, size=60),
+ fx.Flow('heat', bus=heat_bus),
+ ],
+ piecewise_conversion=fx.PiecewiseConversion({
+ 'el': fx.Piecewise([fx.Piece(5, 30), fx.Piece(40, 60)]),
+ 'heat': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]),
+ 'fuel': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]),
+ }),
+ )
+ ```
+
+ See [Piecewise](../features/Piecewise.md).
---
-## See Also
+## Reference
+
+The converter creates **constraints** linking flows, not new variables.
+
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $p_f(t)$ | $\mathbb{R}_{\geq 0}$ | Flow rate of flow $f$ at timestep $t$ |
+| $a_f$ | $\mathbb{R}$ | Conversion factor for input flow $f$ |
+| $b_f$ | $\mathbb{R}$ | Conversion factor for output flow $f$ |
-- [Flow](../elements/Flow.md) - Definition of flow rates
-- [Piecewise](../features/Piecewise.md) - Non-linear conversion efficiency modeling
-- [InvestParameters](../features/InvestParameters.md) - Variable converter sizing
-- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks
+**Classes:** [`LinearConverter`][flixopt.components.LinearConverter], [`LinearConverterModel`][flixopt.components.LinearConverterModel]
diff --git a/docs/user-guide/mathematical-notation/elements/Storage.md b/docs/user-guide/mathematical-notation/elements/Storage.md
index cd7046592..808fefaed 100644
--- a/docs/user-guide/mathematical-notation/elements/Storage.md
+++ b/docs/user-guide/mathematical-notation/elements/Storage.md
@@ -1,79 +1,120 @@
-# Storages
-**Storages** have one incoming and one outgoing **[Flow](../elements/Flow.md)** with a charging and discharging efficiency.
-A storage has a state of charge $c(\text{t}_i)$ which is limited by its `size` $\text C$ and relative bounds $\eqref{eq:Storage_Bounds}$.
-
-$$ \label{eq:Storage_Bounds}
- \text C \cdot \text c^{\text{L}}_{\text{rel}}(\text t_{i})
- \leq c(\text{t}_i) \leq
- \text C \cdot \text c^{\text{U}}_{\text{rel}}(\text t_{i})
-$$
+# Storage
+
+A Storage accumulates energy over time — charge now, discharge later.
-Where:
+## Basic: Charge Dynamics
-- $\text C$ is the size of the storage
-- $c(\text{t}_i)$ is the state of charge at time $\text{t}_i$
-- $\text c^{\text{L}}_{\text{rel}}(\text t_{i})$ is the relative lower bound (typically 0)
-- $\text c^{\text{U}}_{\text{rel}}(\text t_{i})$ is the relative upper bound (typically 1)
+$$
+c(t+1) = c(t) \cdot (1 - \dot{c}_{loss})^{\Delta t} + p_{in}(t) \cdot \Delta t \cdot \eta_{in} - p_{out}(t) \cdot \Delta t / \eta_{out}
+$$
-With $\text c^{\text{L}}_{\text{rel}}(\text t_{i}) = 0$ and $\text c^{\text{U}}_{\text{rel}}(\text t_{i}) = 1$,
-Equation $\eqref{eq:Storage_Bounds}$ simplifies to
+```python
+battery = fx.Storage(
+ label='battery',
+ charging=fx.Flow(label='charge', bus=elec_bus, size=50),
+ discharging=fx.Flow(label='discharge', bus=elec_bus, size=50),
+ capacity_in_flow_hours=200, # 200 kWh
+ eta_charge=0.95,
+ eta_discharge=0.95,
+)
+# Round-trip efficiency: 95% × 95% = 90.25%
+```
-$$ 0 \leq c(\text t_{i}) \leq \text C $$
+---
-The state of charge $c(\text{t}_i)$ decreases by a fraction of the prior state of charge. The belonging parameter
-$ \dot{ \text c}_\text{rel, loss}(\text{t}_i)$ expresses the "loss fraction per hour". The storage balance from $\text{t}_i$ to $\text t_{i+1}$ is
+## Charge State Bounds
$$
-\begin{align*}
- c(\text{t}_{i+1}) &= c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i))^{\Delta \text{t}_{i}} \\
- &\quad + p_{f_\text{in}}(\text{t}_i) \cdot \Delta \text{t}_i \cdot \eta_\text{in}(\text{t}_i) \\
- &\quad - p_{f_\text{out}}(\text{t}_i) \cdot \Delta \text{t}_i \cdot \eta_\text{out}(\text{t}_i)
- \tag{3}
-\end{align*}
+C \cdot c_{rel}^{min} \leq c(t) \leq C \cdot c_{rel}^{max}
$$
-Where:
-
-- $c(\text{t}_{i+1})$ is the state of charge at time $\text{t}_{i+1}$
-- $c(\text{t}_{i})$ is the state of charge at time $\text{t}_{i}$
-- $\dot{\text{c}}_\text{rel,loss}(\text{t}_i)$ is the relative loss rate (self-discharge) per hour
-- $\Delta \text{t}_{i}$ is the time step duration in hours
-- $p_{f_\text{in}}(\text{t}_i)$ is the input flow rate at time $\text{t}_i$
-- $\eta_\text{in}(\text{t}_i)$ is the charging efficiency at time $\text{t}_i$
-- $p_{f_\text{out}}(\text{t}_i)$ is the output flow rate at time $\text{t}_i$
-- $\eta_\text{out}(\text{t}_i)$ is the discharging efficiency at time $\text{t}_i$
+```python
+fx.Storage(...,
+ relative_minimum_charge_state=0.2, # Min 20% SOC
+ relative_maximum_charge_state=0.8, # Max 80% SOC
+)
+```
---
-## Mathematical Patterns Used
+## Initial & Final Conditions
+
+=== "Fixed Start"
+
+ ```python
+ fx.Storage(..., initial_charge_state=100) # Start at 100 kWh
+ ```
-Storage formulation uses the following modeling patterns:
+=== "Cyclic"
-- **[Basic Bounds](../modeling-patterns/bounds-and-states.md#basic-bounds)** - For charge state bounds (equation $\eqref{eq:Storage_Bounds}$)
-- **[Scaled Bounds](../modeling-patterns/bounds-and-states.md#scaled-bounds)** - For flow rate bounds relative to storage size
+ Must end where it started (prevents "cheating"):
-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))
+ ```python
+ fx.Storage(..., initial_charge_state='equals_final')
+ ```
+
+=== "Final Bounds"
+
+ ```python
+ fx.Storage(...,
+ minimal_final_charge_state=50,
+ maximal_final_charge_state=150,
+ )
+ ```
---
-## Implementation
+## Adding Features
-**Python Class:** [`Storage`][flixopt.components.Storage]
+=== "Self-Discharge"
-**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)$
-- `minimal_final_charge_state`, `maximal_final_charge_state`: Final charge bounds $c(\text{t}_\text{end})$ (optional)
-- `eta_charge`, `eta_discharge`: Charging/discharging efficiencies $\eta_\text{in}, \eta_\text{out}$
+ ```python
+ tank = fx.Storage(...,
+ relative_loss_per_hour=0.02, # 2%/hour loss
+ )
+ ```
-See the [`Storage`][flixopt.components.Storage] API documentation for complete parameter list and usage examples.
+=== "Variable Capacity"
----
+ Optimize storage size:
+
+ ```python
+ battery = fx.Storage(...,
+ capacity_in_flow_hours=fx.InvestParameters(
+ minimum_size=0,
+ maximum_size=1000,
+ specific_effects={'costs': 200}, # €/kWh
+ ),
+ )
+ ```
-## See Also
+=== "Asymmetric Power"
+
+ Different charge/discharge rates:
+
+ ```python
+ fx.Storage(
+ charging=fx.Flow(..., size=100), # 100 MW pump
+ discharging=fx.Flow(..., size=120), # 120 MW turbine
+ ...
+ )
+ ```
+
+---
-- [Flow](../elements/Flow.md) - Input and output flow definitions
-- [InvestParameters](../features/InvestParameters.md) - Variable storage sizing
-- [Modeling Patterns](../modeling-patterns/index.md) - Mathematical building blocks
+## Reference
+
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $c(t)$ | $\mathbb{R}_{\geq 0}$ | Charge state at timestep $t$ |
+| $C$ | $\mathbb{R}_{\geq 0}$ | Capacity (`capacity_in_flow_hours`) |
+| $p_{in}(t)$ | $\mathbb{R}_{\geq 0}$ | Charging power (from `charging` flow) |
+| $p_{out}(t)$ | $\mathbb{R}_{\geq 0}$ | Discharging power (from `discharging` flow) |
+| $\eta_{in}$ | $\mathbb{R}_{\geq 0}$ | Charge efficiency (`eta_charge`) |
+| $\eta_{out}$ | $\mathbb{R}_{\geq 0}$ | Discharge efficiency (`eta_discharge`) |
+| $\dot{c}_{loss}$ | $\mathbb{R}_{\geq 0}$ | Self-discharge rate (`relative_loss_per_hour`) |
+| $c_{rel}^{min}$ | $\mathbb{R}_{\geq 0}$ | Min charge state (`relative_minimum_charge_state`) |
+| $c_{rel}^{max}$ | $\mathbb{R}_{\geq 0}$ | Max charge state (`relative_maximum_charge_state`) |
+| $\Delta t$ | $\mathbb{R}_{> 0}$ | Timestep duration (hours) |
+
+**Classes:** [`Storage`][flixopt.components.Storage], [`StorageModel`][flixopt.components.StorageModel]
diff --git a/docs/user-guide/mathematical-notation/features/InvestParameters.md b/docs/user-guide/mathematical-notation/features/InvestParameters.md
index 14fe02c79..b6e1afe6b 100644
--- a/docs/user-guide/mathematical-notation/features/InvestParameters.md
+++ b/docs/user-guide/mathematical-notation/features/InvestParameters.md
@@ -1,302 +1,143 @@
# InvestParameters
-[`InvestParameters`][flixopt.interface.InvestParameters] model investment decisions in optimization problems, enabling both binary (invest/don't invest) and continuous sizing choices with comprehensive cost modeling.
+InvestParameters make capacity a decision variable — should we build this? How big?
-## Investment Decision Types
+## Basic: Size as Variable
-FlixOpt supports two main types of investment decisions:
-
-### Binary Investment
-
-Fixed-size investment creating a yes/no decision (e.g., install a 100 kW generator):
-
-$$\label{eq:invest_binary}
-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
-
-**Behavior:**
-- $s_\text{invest} = 0$: no investment ($v_\text{invest} = 0$)
-- $s_\text{invest} = 1$: invest at fixed size ($v_\text{invest} = \text{size}_\text{fixed}$)
-
----
-
-### Continuous Sizing
-
-Variable-size investment with bounds (e.g., battery capacity from 10-1000 kWh):
-
-$$\label{eq:invest_continuous}
-s_\text{invest} \cdot \text{size}_\text{min} \leq v_\text{invest} \leq s_\text{invest} \cdot \text{size}_\text{max}
-$$
-
-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)
-- $\text{size}_\text{max}$ being the maximum investment size
-
-**Behavior:**
-- $s_\text{invest} = 0$: no investment ($v_\text{invest} = 0$)
-- $s_\text{invest} = 1$: invest with size in $[\text{size}_\text{min}, \text{size}_\text{max}]$
-
-This uses the **bounds with state** pattern described in [Bounds and States](../modeling-patterns/bounds-and-states.md#bounds-with-state).
-
----
-
-### Optional vs. Mandatory Investment
-
-The `mandatory` parameter controls whether investment is required:
-
-**Optional Investment** (`mandatory=False`, default):
-$$\label{eq:invest_optional}
-s_\text{invest} \in \{0, 1\}
-$$
-
-The optimization can freely choose to invest or not.
-
-**Mandatory Investment** (`mandatory=True`):
-$$\label{eq:invest_mandatory}
-s_\text{invest} = 1
$$
-
-The investment must occur (useful for mandatory upgrades or replacements).
-
----
-
-## Effect Modeling
-
-Investment effects (costs, emissions, etc.) are modeled using three components:
-
-### Fixed Effects
-
-One-time effects incurred if investment is made, independent of size:
-
-$$\label{eq:invest_fixed_effects}
-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)
-
-**Examples:**
-- Fixed installation costs (permits, grid connection)
-- One-time environmental impacts (land preparation)
-- Fixed labor or administrative costs
-
----
-
-### Specific Effects
-
-Effects proportional to investment size (per-unit costs):
-
-$$\label{eq:invest_specific_effects}
-E_{e,\text{spec}} = v_\text{invest} \cdot \text{spec}_e
+P^{min} \leq P \leq P^{max}
$$
-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)
-
-**Examples:**
-- Equipment costs (€/kW)
-- Material requirements (kg steel/kW)
-- Recurring costs (€/kW/year maintenance)
-
----
-
-### Piecewise Effects
-
-Non-linear effect relationships using piecewise linear approximations:
-
-$$\label{eq:invest_piecewise_effects}
-E_{e,\text{pw}} = \sum_{k=1}^{K} \lambda_k \cdot r_{e,k}
-$$
-
-Subject to:
-$$
-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$
-- $v_k$ being the size points defining the pieces
-
-**Use cases:**
-- Economies of scale (bulk discounts)
-- Technology learning curves
-- Threshold effects (capacity tiers with different costs)
-
-See [Piecewise](../features/Piecewise.md) for detailed mathematical formulation.
+```python
+battery = fx.Storage(
+ ...,
+ capacity_in_flow_hours=fx.InvestParameters(
+ minimum_size=10,
+ maximum_size=1000,
+ specific_effects={'costs': 600}, # €600/kWh
+ ),
+)
+```
---
-### Retirement Effects
+## Investment Modes
-Effects incurred if investment is NOT made (when retiring/not replacing existing equipment):
+By default, investment is **optional** — the optimizer can choose $P = 0$ (don't invest).
-$$\label{eq:invest_retirement_effects}
-E_{e,\text{retirement}} = (1 - s_\text{invest}) \cdot \text{retirement}_e
-$$
+=== "Continuous"
-With:
-- $E_{e,\text{retirement}}$ being the retirement contribution to effect $e$
-- $\text{retirement}_e$ being the retirement effect value
+ Choose size within range (or zero):
-**Behavior:**
-- $s_\text{invest} = 0$: retirement effects are incurred
-- $s_\text{invest} = 1$: no retirement effects
+ ```python
+ fx.InvestParameters(
+ minimum_size=10,
+ maximum_size=1000,
+ )
+ # → P = 0 OR 10 ≤ P ≤ 1000
+ ```
-**Examples:**
-- Demolition or disposal costs
-- Decommissioning expenses
-- Contractual penalties for not investing
-- Opportunity costs or lost revenues
+=== "Binary"
----
+ Fixed size or nothing:
-### Total Investment Effects
+ ```python
+ fx.InvestParameters(
+ fixed_size=100, # 100 kW or 0
+ )
+ # → P ∈ {0, 100}
+ ```
-The total contribution to effect $e$ from an investment is:
+=== "Mandatory"
-$$\label{eq:invest_total_effects}
-E_{e,\text{invest}} = E_{e,\text{fix}} + E_{e,\text{spec}} + E_{e,\text{pw}} + E_{e,\text{retirement}}
-$$
+ Force investment with `mandatory=True` — zero not allowed:
-Effects integrate into the overall system effects as described in [Effects, Penalty & Objective](../effects-penalty-objective.md).
+ ```python
+ fx.InvestParameters(
+ minimum_size=50,
+ maximum_size=200,
+ mandatory=True,
+ )
+ # → 50 ≤ P ≤ 200 (no zero option)
+ ```
---
-## Integration with Components
+## Investment Effects
-Investment parameters modify component sizing:
+=== "Per-Size Cost"
-### Without Investment
-Component size is a fixed parameter:
-$$
-\text{size} = \text{size}_\text{nominal}
-$$
+ Cost proportional to capacity (€/kW):
-### With Investment
-Component size becomes a variable:
-$$
-\text{size} = v_\text{invest}
-$$
+ $E = P \cdot c_{spec}$
-This size variable then appears in component constraints. For example, flow rate bounds become:
+ ```python
+ fx.InvestParameters(
+ specific_effects={'costs': 1200}, # €1200/kW
+ )
+ ```
-$$
-v_\text{invest} \cdot \text{rel}_\text{lower} \leq p(t) \leq v_\text{invest} \cdot \text{rel}_\text{upper}
-$$
+=== "Fixed Cost"
-Using the **scaled bounds** pattern from [Bounds and States](../modeling-patterns/bounds-and-states.md#scaled-bounds).
+ One-time cost if investing:
----
+ $E = s_{inv} \cdot c_{fix}$
-## Cost Annualization
+ ```python
+ fx.InvestParameters(
+ effects_of_investment={'costs': 25000}, # €25k
+ )
+ ```
-**Important:** All investment cost values must be properly weighted to match the optimization model's time horizon.
+=== "Retirement Cost"
-For long-term investments, costs should be annualized:
+ Cost if NOT investing:
-$$\label{eq:annualization}
-\text{cost}_\text{annual} = \frac{\text{cost}_\text{capital} \cdot r}{1 - (1 + r)^{-n}}
-$$
-
-With:
-- $\text{cost}_\text{capital}$ being the upfront investment cost
-- $r$ being the discount rate
-- $n$ being the equipment lifetime in years
-
-**Example:** €1,000,000 equipment with 20-year life and 5% discount rate
-$$
-\text{cost}_\text{annual} = \frac{1{,}000{,}000 \cdot 0.05}{1 - (1.05)^{-20}} \approx €80{,}243/\text{year}
-$$
+ $E = (1 - s_{inv}) \cdot c_{ret}$
----
+ ```python
+ fx.InvestParameters(
+ effects_of_retirement={'costs': 8000}, # Demolition
+ )
+ ```
-## Implementation
+=== "Piecewise Cost"
-**Python Class:** [`InvestParameters`][flixopt.interface.InvestParameters]
+ Non-linear cost curves (e.g., economies of scale):
-**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`)
-- `effects_of_investment`: Fixed effects incurred when investing (replaces deprecated `fix_effects`)
-- `effects_of_investment_per_size`: Per-unit effects proportional to size (replaces deprecated `specific_effects`)
-- `piecewise_effects_of_investment`: Non-linear effect modeling (replaces deprecated `piecewise_effects`)
-- `effects_of_retirement`: Effects for not investing (replaces deprecated `divest_effects`)
+ $E = f_{piecewise}(P)$
-See the [`InvestParameters`][flixopt.interface.InvestParameters] API documentation for complete parameter list and usage examples.
+ ```python
+ fx.InvestParameters(
+ piecewise_effects_of_investment=fx.PiecewiseEffects(
+ piecewise_origin=fx.Piecewise([
+ fx.Piece(0, 100),
+ fx.Piece(100, 500),
+ ]),
+ piecewise_shares={
+ 'costs': fx.Piecewise([
+ fx.Piece(0, 80_000), # €800/kW for 0-100
+ fx.Piece(80_000, 280_000), # €500/kW for 100-500
+ ])
+ },
+ ),
+ )
+ ```
-**Used in:**
-- [`Flow`][flixopt.elements.Flow] - Flexible capacity decisions
-- [`Storage`][flixopt.components.Storage] - Storage sizing optimization
-- [`LinearConverter`][flixopt.components.LinearConverter] - Converter capacity planning
-- All components supporting investment decisions
+ See [Piecewise](Piecewise.md) for details on the formulation.
---
-## Examples
+## Reference
-### Binary Investment (Solar Panels)
-```python
-solar_investment = InvestParameters(
- fixed_size=100, # 100 kW system
- mandatory=False, # Optional investment (default)
- effects_of_investment={'cost': 25000}, # Installation costs
- effects_of_investment_per_size={'cost': 1200}, # €1200/kW
-)
-```
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $P$ | $\mathbb{R}_{\geq 0}$ | Investment size (capacity) |
+| $s_{inv}$ | $\{0, 1\}$ | Binary investment decision (0=no, 1=yes) |
+| $P^{min}$ | $\mathbb{R}_{\geq 0}$ | Minimum size (`minimum_size`) |
+| $P^{max}$ | $\mathbb{R}_{\geq 0}$ | Maximum size (`maximum_size`) |
+| $c_{spec}$ | $\mathbb{R}$ | Per-size effect (`effects_of_investment_per_size`) |
+| $c_{fix}$ | $\mathbb{R}$ | Fixed effect (`effects_of_investment`) |
+| $c_{ret}$ | $\mathbb{R}$ | Retirement effect (`effects_of_retirement`) |
-### Continuous Sizing (Battery)
-```python
-battery_investment = InvestParameters(
- minimum_size=10, # kWh
- maximum_size=1000,
- mandatory=False, # Optional investment (default)
- effects_of_investment={'cost': 5000}, # Grid connection
- effects_of_investment_per_size={'cost': 600}, # €600/kWh
-)
-```
-
-### With Retirement Costs (Replacement)
-```python
-boiler_replacement = InvestParameters(
- minimum_size=50, # kW
- maximum_size=200,
- mandatory=False, # Optional investment (default)
- effects_of_investment={'cost': 15000},
- effects_of_investment_per_size={'cost': 400},
- effects_of_retirement={'cost': 8000}, # Demolition if not replaced
-)
-```
-
-### Economies of Scale (Piecewise)
-```python
-battery_investment = InvestParameters(
- minimum_size=10,
- maximum_size=1000,
- piecewise_effects_of_investment=PiecewiseEffects(
- piecewise_origin=Piecewise([
- Piece(0, 100), # Small
- Piece(100, 500), # Medium
- Piece(500, 1000), # Large
- ]),
- piecewise_shares={
- 'cost': Piecewise([
- Piece(800, 750), # €800-750/kWh
- Piece(750, 600), # €750-600/kWh
- Piece(600, 500), # €600-500/kWh (bulk discount)
- ])
- },
- ),
-)
-```
+**Classes:** [`InvestParameters`][flixopt.interface.InvestParameters], [`InvestmentModel`][flixopt.features.InvestmentModel]
diff --git a/docs/user-guide/mathematical-notation/features/OnOffParameters.md b/docs/user-guide/mathematical-notation/features/OnOffParameters.md
deleted file mode 100644
index 6bf40fec9..000000000
--- a/docs/user-guide/mathematical-notation/features/OnOffParameters.md
+++ /dev/null
@@ -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/Piecewise.md b/docs/user-guide/mathematical-notation/features/Piecewise.md
index 688ac8cea..da6405b52 100644
--- a/docs/user-guide/mathematical-notation/features/Piecewise.md
+++ b/docs/user-guide/mathematical-notation/features/Piecewise.md
@@ -1,49 +1,155 @@
# Piecewise
-A Piecewise is a collection of [`Pieces`][flixopt.interface.Piece], which each define a valid range for a variable $v$
+Piecewise linearization approximates non-linear relationships using connected linear segments.
+
+## Mathematical Formulation
+
+A piecewise linear function with $n$ segments uses per-segment interpolation:
-$$ \label{eq:active_piece}
- \beta_\text{k} = \lambda_\text{0, k} + \lambda_\text{1, k}
$$
+x = \sum_{i=1}^{n} \left( \lambda_i^0 \cdot x_i^{start} + \lambda_i^1 \cdot x_i^{end} \right)
+$$
+
+Each segment $i$ has:
-$$ \label{eq:piece}
- v_\text{k} = \lambda_\text{0, k} * \text{v}_{\text{start,k}} + \lambda_\text{1,k} * \text{v}_{\text{end,k}}
+- $s_i \in \{0, 1\}$ — binary indicating if segment is active
+- $\lambda_i^0, \lambda_i^1 \geq 0$ — interpolation weights for segment endpoints
+
+Constraints ensure valid interpolation:
+
+$$
+\lambda_i^0 + \lambda_i^1 = s_i \quad \forall i
$$
-$$ \label{eq:piecewise_in_pieces}
-\sum_{k=1}^k \beta_{k} = 1
+$$
+\sum_{i=1}^{n} s_i \leq 1
$$
-With:
+When segment $i$ is active ($s_i = 1$), the lambdas interpolate between $x_i^{start}$ and $x_i^{end}$. When inactive ($s_i = 0$), both lambdas are zero.
-- $v$: The variable to be defined by the Piecewise
-- $\text{v}_{\text{start,k}}$: the start point of the piece for variable $v$
-- $\text{v}_{\text{end,k}}$: the end point of the piece for variable $v$
-- $\beta_\text{k} \in \{0, 1\}$: defining wether the Piece $k$ is active
-- $\lambda_\text{0,k} \in [0, 1]$: A variable defining the fraction of $\text{v}_{\text{start,k}}$ that is active
-- $\lambda_\text{1,k} \in [0, 1]$: A variable defining the fraction of $\text{v}_{\text{end,k}}$ that is active
+!!! note "Implementation Note"
+ This formulation is an explicit binary reformulation of SOS2 (Special Ordered Set Type 2) constraints. It produces identical results but uses more variables. We will migrate to native SOS2 constraints once [linopy](https://github.com/PyPSA/linopy) supports them.
-Which can also be described as $v \in 0 \cup [\text{v}_\text{start}, \text{v}_\text{end}]$.
+---
-Instead of \eqref{eq:piecewise_in_pieces}, the following constraint is used to also allow all variables to be zero:
+## Building Blocks
-$$ \label{eq:piecewise_in_pieces_zero}
-\sum_{k=1}^k \beta_{k} = \beta_\text{zero}
-$$
+=== "Piece"
+
+ A linear segment from start to end value:
+
+ ```python
+ fx.Piece(start=10, end=50) # Linear from 10 to 50
+ ```
+
+ Values can be time-varying:
+
+ ```python
+ fx.Piece(
+ start=np.linspace(5, 6, n_timesteps),
+ end=np.linspace(30, 35, n_timesteps)
+ )
+ ```
+
+=== "Piecewise"
+
+ Multiple segments forming a piecewise linear function:
+
+ ```python
+ fx.Piecewise([
+ fx.Piece(0, 30), # Segment 1: 0 → 30
+ fx.Piece(30, 60), # Segment 2: 30 → 60
+ ])
+ ```
+
+=== "PiecewiseConversion"
+
+ Synchronizes multiple flows — all interpolate at the same relative position:
+
+ ```python
+ fx.PiecewiseConversion({
+ 'input_flow': fx.Piecewise([...]),
+ 'output_flow': fx.Piecewise([...]),
+ })
+ ```
+
+ All piecewise functions must have the same number of segments.
+
+=== "PiecewiseEffects"
+
+ Maps a size/capacity variable to effects (costs, emissions):
+
+ ```python
+ fx.PiecewiseEffects(
+ piecewise_origin=fx.Piecewise([...]), # Size segments
+ piecewise_shares={'costs': fx.Piecewise([...])}, # Effect segments
+ )
+ ```
+
+---
+
+## Usage
+
+=== "Variable Efficiency"
-With:
+ Converter efficiency that varies with load:
-- $\beta_\text{zero} \in \{0, 1\}$.
+ ```python
+ chp = fx.LinearConverter(
+ ...,
+ piecewise_conversion=fx.PiecewiseConversion({
+ 'el': fx.Piecewise([fx.Piece(5, 30), fx.Piece(40, 60)]),
+ 'heat': fx.Piecewise([fx.Piece(6, 35), fx.Piece(45, 100)]),
+ 'fuel': fx.Piecewise([fx.Piece(12, 70), fx.Piece(90, 200)]),
+ }),
+ )
+ ```
-Which can also be described as $v \in \{0\} \cup [\text{v}_{\text{start_k}}, \text{v}_{\text{end_k}}]$
+=== "Economies of Scale"
+ Investment cost per unit decreases with size:
-## Combining multiple Piecewises
+ ```python
+ fx.InvestParameters(
+ piecewise_effects_of_investment=fx.PiecewiseEffects(
+ piecewise_origin=fx.Piecewise([
+ fx.Piece(0, 100),
+ fx.Piece(100, 500),
+ ]),
+ piecewise_shares={
+ 'costs': fx.Piecewise([
+ fx.Piece(0, 80_000),
+ fx.Piece(80_000, 280_000),
+ ])
+ },
+ ),
+ )
+ ```
-Piecewise allows representing non-linear relationships.
-This is a powerful technique in linear optimization to model non-linear behaviors while maintaining the problem's linearity.
+=== "Forbidden Operating Region"
-Therefore, each Piecewise must have the same number of Pieces $k$.
+ Equipment cannot operate in certain ranges:
-The variables described in [Piecewise](#piecewise) are created for each Piece, but nor for each Piecewise.
-Rather, \eqref{eq:piece} is the only constraint that is created for each Piecewise, using the start and endpoints $\text{v}_{\text{start,k}}$ and $\text{v}_{\text{end,k}}$ of each Piece for the corresponding variable $v$
+ ```python
+ fx.PiecewiseConversion({
+ 'fuel': fx.Piecewise([fx.Piece(0, 0), fx.Piece(40, 100)]),
+ 'power': fx.Piecewise([fx.Piece(0, 0), fx.Piece(35, 95)]),
+ })
+ # Either off (0,0) or operating above 40%
+ ```
+
+---
+
+## Reference
+
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $x$ | $\mathbb{R}$ | Interpolated variable value |
+| $s_i$ | $\{0, 1\}$ | Binary: segment $i$ is active |
+| $\lambda_i^0$ | $[0, 1]$ | Interpolation weight for segment start |
+| $\lambda_i^1$ | $[0, 1]$ | Interpolation weight for segment end |
+| $x_i^{start}$ | $\mathbb{R}$ | Start value of segment $i$ |
+| $x_i^{end}$ | $\mathbb{R}$ | End value of segment $i$ |
+| $n$ | $\mathbb{Z}_{> 0}$ | Number of segments |
+
+**Classes:** [`Piecewise`][flixopt.interface.Piecewise], [`Piece`][flixopt.interface.Piece], [`PiecewiseConversion`][flixopt.interface.PiecewiseConversion], [`PiecewiseEffects`][flixopt.interface.PiecewiseEffects]
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..7b4c08f72
--- /dev/null
+++ b/docs/user-guide/mathematical-notation/features/StatusParameters.md
@@ -0,0 +1,114 @@
+# StatusParameters
+
+StatusParameters add on/off behavior to flows — startup costs, minimum run times, cycling limits.
+
+## Basic: Binary Status
+
+A status variable $s(t) \in \{0, 1\}$ controls whether equipment is active:
+
+```python
+generator = fx.Flow(
+ label='power', bus=elec_bus, size=100,
+ relative_minimum=0.4, # 40% min when ON
+ status_parameters=fx.StatusParameters(
+ effects_per_startup={'costs': 25000}, # €25k per startup
+ ),
+)
+```
+
+When $s(t) = 0$: flow is zero. When $s(t) = 1$: flow bounds apply.
+
+---
+
+## Startup Tracking
+
+Detect transitions: $s^{start}(t) - s^{stop}(t) = s(t) - s(t-1)$
+
+=== "Startup Costs"
+
+ ```python
+ fx.StatusParameters(
+ effects_per_startup={'costs': 25000},
+ )
+ ```
+
+=== "Running Costs"
+
+ ```python
+ fx.StatusParameters(
+ effects_per_active_hour={'costs': 100}, # €/h while on
+ )
+ ```
+
+=== "Startup Limit"
+
+ ```python
+ fx.StatusParameters(
+ startup_limit=20, # Max 20 starts per period
+ )
+ ```
+
+---
+
+## Duration Constraints
+
+=== "Min Uptime"
+
+ Once on, must stay on for minimum duration:
+
+ $s^{start}(t) = 1 \Rightarrow \sum_{j=t}^{t+k} s(j) \geq T_{up}^{min}$
+
+ ```python
+ fx.StatusParameters(min_uptime=8) # 8 hours minimum
+ ```
+
+=== "Min Downtime"
+
+ Once off, must stay off for minimum duration:
+
+ $s^{stop}(t) = 1 \Rightarrow \sum_{j=t}^{t+k} (1 - s(j)) \geq T_{down}^{min}$
+
+ ```python
+ fx.StatusParameters(min_downtime=4) # 4 hours cooling
+ ```
+
+=== "Max Uptime"
+
+ Force shutdown after limit:
+
+ $\sum_{j=t-k}^{t} s(j) \leq T_{up}^{max}$
+
+ ```python
+ fx.StatusParameters(max_uptime=18) # Max 18h continuous
+ ```
+
+=== "Total Hours"
+
+ Limit total operating hours per period:
+
+ $H^{min} \leq \sum_t s(t) \cdot \Delta t \leq H^{max}$
+
+ ```python
+ fx.StatusParameters(
+ active_hours_min=2000,
+ active_hours_max=5000,
+ )
+ ```
+
+---
+
+## Reference
+
+| Symbol | Type | Description |
+|--------|------|-------------|
+| $s(t)$ | $\{0, 1\}$ | Binary status (0=off, 1=on) |
+| $s^{start}(t)$ | $\{0, 1\}$ | Startup indicator |
+| $s^{stop}(t)$ | $\{0, 1\}$ | Shutdown indicator |
+| $T_{up}^{min}$ | $\mathbb{R}_{\geq 0}$ | Min uptime in hours (`min_uptime`) |
+| $T_{up}^{max}$ | $\mathbb{R}_{\geq 0}$ | Max uptime in hours (`max_uptime`) |
+| $T_{down}^{min}$ | $\mathbb{R}_{\geq 0}$ | Min downtime in hours (`min_downtime`) |
+| $H^{min}$ | $\mathbb{R}_{\geq 0}$ | Min total active hours (`active_hours_min`) |
+| $H^{max}$ | $\mathbb{R}_{\geq 0}$ | Max total active hours (`active_hours_max`) |
+| $\Delta t$ | $\mathbb{R}_{> 0}$ | Timestep duration (hours) |
+
+**Classes:** [`StatusParameters`][flixopt.interface.StatusParameters], [`StatusModel`][flixopt.features.StatusModel]
diff --git a/docs/user-guide/mathematical-notation/index.md b/docs/user-guide/mathematical-notation/index.md
index 27e7b7e9a..95e21db5e 100644
--- a/docs/user-guide/mathematical-notation/index.md
+++ b/docs/user-guide/mathematical-notation/index.md
@@ -1,123 +1,107 @@
-
# Mathematical Notation
-This section provides the **mathematical formulations** underlying FlixOpt's optimization models. It is intended as **reference documentation** for users who want to understand the mathematical details behind the high-level FlixOpt API described in the [FlixOpt Concepts](../core-concepts.md) guide.
-
-**For typical usage**, refer to the [FlixOpt Concepts](../core-concepts.md) guide, [Examples](../../examples/index.md), and [API Reference](../../api-reference/index.md) - you don't need to understand these mathematical formulations to use FlixOpt effectively.
-
----
-
-## Naming Conventions
-
-FlixOpt uses the following naming conventions:
-
-- All optimization variables are denoted by italic letters (e.g., $x$, $y$, $z$)
-- All parameters and constants are denoted by non italic small letters (e.g., $\text{a}$, $\text{b}$, $\text{c}$)
-- All Sets are denoted by greek capital letters (e.g., $\mathcal{F}$, $\mathcal{E}$)
-- All units of a set are denoted by greek small letters (e.g., $\mathcal{f}$, $\mathcal{e}$)
-- The letter $i$ is used to denote an index (e.g., $i=1,\dots,\text n$)
-- All time steps are denoted by the letter $\text{t}$ (e.g., $\text{t}_0$, $\text{t}_1$, $\text{t}_i$)
+This section provides the detailed mathematical formulations behind flixOpt. It expands on the concepts introduced in [Core Concepts](../core-concepts.md) with precise equations, variables, and constraints.
-## Dimensions and Time Steps
+!!! tip "When to read this"
+ You don't need this section to use flixOpt effectively. It's here for:
-FlixOpt supports multi-dimensional optimization with up to three dimensions: **time** (mandatory), **period** (optional), and **scenario** (optional).
+ - Understanding exactly what the solver is optimizing
+ - Debugging unexpected model behavior
+ - Extending flixOpt with custom constraints
+ - Academic work requiring formal notation
-**All mathematical formulations in this documentation are independent of whether periods or scenarios are present.** The equations shown are written with time index $\text{t}_i$ only, but automatically expand to additional dimensions when periods/scenarios are added.
+## Structure
-For complete details on dimensions, their relationships, and influence on formulations, see **[Dimensions](dimensions.md)**.
+The documentation follows the same structure as Core Concepts:
-### Time Steps
+| Core Concept | Mathematical Details |
+|--------------|---------------------|
+| **Buses** — where things connect | [Bus](elements/Bus.md) — balance equations, penalty terms |
+| **Flows** — what moves | [Flow](elements/Flow.md) — capacity bounds, load factors, profiles |
+| **Converters** — transform things | [LinearConverter](elements/LinearConverter.md) — conversion ratios |
+| **Storages** — save for later | [Storage](elements/Storage.md) — charge dynamics, efficiency losses |
+| **Effects** — what you track | [Effects & Dimensions](effects-and-dimensions.md) — objectives, costs, scenarios, periods |
-Time steps are defined as a sequence of discrete time steps $\text{t}_i \in \mathcal{T} \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}\}$ (left-aligned in its timespan).
-From this sequence, the corresponding time intervals $\Delta \text{t}_i \in \Delta \mathcal{T}$ are derived as
+## Notation Conventions
-$$\Delta \text{t}_i = \text{t}_{i+1} - \text{t}_i \quad \text{for} \quad i \in \{1, 2, \dots, \text{n}-1\}$$
+### Variables (What the optimizer decides)
-The final time interval $\Delta \text{t}_\text n$ defaults to $\Delta \text{t}_\text n = \Delta \text{t}_{\text n-1}$, but is of course customizable.
-Non-equidistant time steps are also supported.
+Optimization variables are shown in *italic*:
----
+| Symbol | Meaning | Example |
+|--------|---------|---------|
+| $p(t)$ | Flow rate at time $t$ | Heat output of a boiler |
+| $c(t)$ | Charge state at time $t$ | Energy stored in a battery |
+| $P$ | Size/capacity (when optimized) | Installed capacity of a heat pump |
+| $s(t)$ | Binary on/off state | Whether a generator is running |
-## Documentation Structure
+### Parameters (What you provide)
-This reference is organized to match the FlixOpt API structure:
+Parameters and constants are shown in upright text:
-### Elements
-Mathematical formulations for core FlixOpt elements (corresponding to [`flixopt.elements`][flixopt.elements]):
+| Symbol | Meaning | Example |
+|--------|---------|---------|
+| $\eta$ | Efficiency | Boiler thermal efficiency (0.9) |
+| $\Delta t$ | Timestep duration | 1 hour |
+| $p_{min}$, $p_{max}$ | Flow bounds | Min/max operating power |
-- [Flow](elements/Flow.md) - Flow rate constraints and bounds
-- [Bus](elements/Bus.md) - Nodal balance equations
-- [Storage](elements/Storage.md) - Storage balance and charge state evolution
-- [LinearConverter](elements/LinearConverter.md) - Linear conversion relationships
+### Sets and Indices
-**User API:** When you create a `Flow`, `Bus`, `Storage`, or `LinearConverter` in your FlixOpt model, these mathematical formulations are automatically applied.
+| Symbol | Meaning |
+|--------|---------|
+| $t \in \mathcal{T}$ | Time steps |
+| $f \in \mathcal{F}$ | Flows |
+| $e \in \mathcal{E}$ | Effects |
-### Features
-Mathematical formulations for optional features (corresponding to parameters in FlixOpt classes):
+## The Optimization Problem
-- [InvestParameters](features/InvestParameters.md) - Investment decision modeling
-- [OnOffParameters](features/OnOffParameters.md) - Binary on/off operation
-- [Piecewise](features/Piecewise.md) - Piecewise linear approximations
+At its core, flixOpt solves:
-**User API:** When you pass `invest_parameters` or `on_off_parameters` to a `Flow` or component, these formulations are applied.
+$$
+\min \quad objective + penalty
+$$
-### System-Level
-- [Effects, Penalty & Objective](effects-penalty-objective.md) - Cost allocation and objective function
+**Subject to:**
-**User API:** When you create [`Effect`][flixopt.effects.Effect] objects and set `effects_per_flow_hour`, these formulations govern how costs are calculated.
+- Balance constraints at each bus
+- Capacity bounds on each flow
+- Storage dynamics over time
+- Conversion relationships in converters
+- Any additional effect constraints
-### Modeling Patterns (Advanced)
-**Internal implementation details** - These low-level patterns are used internally by Elements and Features. They are documented here for:
+The following pages detail each of these components.
-- Developers extending FlixOpt
-- Advanced users debugging models or understanding solver behavior
-- Researchers comparing mathematical formulations
+## Quick Example
-**Normal users do not need to read this section** - the patterns are automatically applied when you use Elements and Features:
+Consider a simple system: a gas boiler connected to a heat bus serving a demand.
-- [Bounds and States](modeling-patterns/bounds-and-states.md) - Variable bounding patterns
-- [Duration Tracking](modeling-patterns/duration-tracking.md) - Consecutive time period tracking
-- [State Transitions](modeling-patterns/state-transitions.md) - State change modeling
+**Variables:**
----
+- $p_{gas}(t)$ — gas consumption at each timestep
+- $p_{heat}(t)$ — heat production at each timestep
-## Quick Reference
+**Constraints:**
-### Components Cross-Reference
+1. **Conversion** (boiler efficiency 90%):
+ $$p_{heat}(t) = 0.9 \cdot p_{gas}(t)$$
-| Concept | Documentation | Python Class |
-|---------|---------------|--------------|
-| **Flow rate bounds** | [Flow](elements/Flow.md) | [`Flow`][flixopt.elements.Flow] |
-| **Bus balance** | [Bus](elements/Bus.md) | [`Bus`][flixopt.elements.Bus] |
-| **Storage balance** | [Storage](elements/Storage.md) | [`Storage`][flixopt.components.Storage] |
-| **Linear conversion** | [LinearConverter](elements/LinearConverter.md) | [`LinearConverter`][flixopt.components.LinearConverter] |
+2. **Capacity bounds** (boiler max 100 kW):
+ $$0 \leq p_{heat}(t) \leq 100$$
-### Features Cross-Reference
+3. **Balance** (meet demand):
+ $$p_{heat}(t) = demand(t)$$
-| 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] |
-| **Piecewise segments** | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] |
+**Objective** (minimize gas cost at €50/MWh):
+$$\min \sum_t p_{gas}(t) \cdot \Delta t \cdot 50$$
-### Modeling Patterns Cross-Reference
+This simple example shows how the concepts combine. Real models have many more components, but the principles remain the same.
-| Pattern | Documentation | Implementation |
-|---------|---------------|----------------|
-| **Basic bounds** | [bounds-and-states](modeling-patterns/bounds-and-states.md#basic-bounds) | [`BoundingPatterns.basic_bounds()`][flixopt.modeling.BoundingPatterns.basic_bounds] |
-| **Bounds with state** | [bounds-and-states](modeling-patterns/bounds-and-states.md#bounds-with-state) | [`BoundingPatterns.bounds_with_state()`][flixopt.modeling.BoundingPatterns.bounds_with_state] |
-| **Scaled bounds** | [bounds-and-states](modeling-patterns/bounds-and-states.md#scaled-bounds) | [`BoundingPatterns.scaled_bounds()`][flixopt.modeling.BoundingPatterns.scaled_bounds] |
-| **Duration tracking** | [duration-tracking](modeling-patterns/duration-tracking.md) | [`ModelingPrimitives.consecutive_duration_tracking()`][flixopt.modeling.ModelingPrimitives.consecutive_duration_tracking] |
-| **State transitions** | [state-transitions](modeling-patterns/state-transitions.md) | [`BoundingPatterns.state_transition_bounds()`][flixopt.modeling.BoundingPatterns.state_transition_bounds] |
+## Next Steps
-### Python Class Lookup
+Start with the element that's most relevant to your question:
-| Class | Documentation | API Reference |
-|-------|---------------|---------------|
-| `Flow` | [Flow](elements/Flow.md) | [`Flow`][flixopt.elements.Flow] |
-| `Bus` | [Bus](elements/Bus.md) | [`Bus`][flixopt.elements.Bus] |
-| `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] |
-| `Piecewise` | [Piecewise](features/Piecewise.md) | [`Piecewise`][flixopt.interface.Piecewise] |
+- **Why isn't my demand being met?** → [Bus](elements/Bus.md) (balance constraints)
+- **Why is my component not running?** → [Flow](elements/Flow.md) (capacity bounds)
+- **How does storage charge/discharge?** → [Storage](elements/Storage.md) (charge dynamics)
+- **How are efficiencies handled?** → [LinearConverter](elements/LinearConverter.md) (conversion)
+- **How are costs calculated?** → [Effects & Dimensions](effects-and-dimensions.md)
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
deleted file mode 100644
index d5821948f..000000000
--- a/docs/user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md
+++ /dev/null
@@ -1,165 +0,0 @@
-# Bounds and States
-
-This document describes the mathematical formulations for variable bounding patterns used throughout FlixOpt. These patterns define how optimization variables are constrained, both with and without state control.
-
-## Basic Bounds
-
-The simplest bounding pattern constrains a variable between lower and upper bounds.
-
-$$\label{eq:basic_bounds}
-\text{lower} \leq v \leq \text{upper}
-$$
-
-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)
-
-**Implementation:** [`BoundingPatterns.basic_bounds()`][flixopt.modeling.BoundingPatterns.basic_bounds]
-
-**Used in:**
-- Storage charge state bounds (see [Storage](../elements/Storage.md))
-- Flow rate absolute bounds
-
----
-
-## 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:
-
-$$\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
-- $\text{upper}$ being the upper bound when active
-- $\varepsilon$ being a small positive number to ensure numerical stability
-
-**Behavior:**
-- When $s = 0$: variable is forced to zero ($0 \leq v \leq 0$)
-- When $s = 1$: variable can take values in $[\text{lower}, \text{upper}]$
-
-**Implementation:** [`BoundingPatterns.bounds_with_state()`][flixopt.modeling.BoundingPatterns.bounds_with_state]
-
-**Used in:**
-- Flow rates with on/off operation (see [OnOffParameters](../features/OnOffParameters.md))
-- Investment size decisions (see [InvestParameters](../features/InvestParameters.md))
-
----
-
-## Scaled Bounds
-
-When a variable's bounds depend on another variable (e.g., flow rate scaled by component size), scaled bounds are used:
-
-$$\label{eq:scaled_bounds}
-v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper}
-$$
-
-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)
-- $\text{rel}_\text{upper}$ being the relative upper bound factor (typically 1)
-
-**Example:** Flow rate bounds
-- If $v_\text{scale} = P$ (flow size) and $\text{rel}_\text{upper} = 1$
-- Then: $0 \leq p(t_i) \leq P$ (see [Flow](../elements/Flow.md))
-
-**Implementation:** [`BoundingPatterns.scaled_bounds()`][flixopt.modeling.BoundingPatterns.scaled_bounds]
-
-**Used in:**
-- Flow rate constraints (see [Flow](../elements/Flow.md) equation 1)
-- Storage charge state constraints (see [Storage](../elements/Storage.md) equation 1)
-
----
-
-## 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:
-
-$$\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}
-$$
-
-$$\label{eq:scaled_bounds_with_state_2}
-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
-- $\text{rel}_\text{lower}$ being the relative lower bound factor
-- $\text{rel}_\text{upper}$ being the relative upper bound factor
-- $M_\text{misc} = v_\text{scale,max} \cdot \text{rel}_\text{lower}$
-- $M_\text{upper} = v_\text{scale,max} \cdot \text{rel}_\text{upper}$
-- $M_\text{lower} = \max(\varepsilon, v_\text{scale,min} \cdot \text{rel}_\text{lower})$
-
-Where $v_\text{scale,max}$ and $v_\text{scale,min}$ are the maximum and minimum possible values of the scaling variable.
-
-**Behavior:**
-- When $s = 0$: variable is forced to zero
-- When $s = 1$: variable follows scaled bounds $v_\text{scale} \cdot \text{rel}_\text{lower} \leq v \leq v_\text{scale} \cdot \text{rel}_\text{upper}$
-
-**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)
-
----
-
-## Expression Tracking
-
-Sometimes it's necessary to create an auxiliary variable that equals an expression:
-
-$$\label{eq:expression_tracking}
-v_\text{tracker} = \text{expression}
-$$
-
-With optional bounds:
-
-$$\label{eq:expression_tracking_bounds}
-\text{lower} \leq v_\text{tracker} \leq \text{upper}
-$$
-
-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
-
-**Use cases:**
-- Creating named variables for complex expressions
-- Bounding intermediate results
-- Simplifying constraint formulations
-
-**Implementation:** [`ModelingPrimitives.expression_tracking_variable()`][flixopt.modeling.ModelingPrimitives.expression_tracking_variable]
-
----
-
-## Mutual Exclusivity
-
-When multiple binary variables should not be active simultaneously (at most one can be 1):
-
-$$\label{eq:mutual_exclusivity}
-\sum_{i} s_i(t) \leq \text{tolerance} \quad \forall t
-$$
-
-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
-
-**Use cases:**
-- Ensuring only one operating mode is active
-- Mutual exclusion of operation and maintenance states
-- Enforcing single-choice decisions
-
-**Implementation:** [`ModelingPrimitives.mutual_exclusivity_constraint()`][flixopt.modeling.ModelingPrimitives.mutual_exclusivity_constraint]
-
-**Used in:**
-- Operating mode selection
-- Piecewise linear function segments (see [Piecewise](../features/Piecewise.md))
diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md b/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md
deleted file mode 100644
index 5d430d28c..000000000
--- a/docs/user-guide/mathematical-notation/modeling-patterns/duration-tracking.md
+++ /dev/null
@@ -1,159 +0,0 @@
-# Duration Tracking
-
-Duration tracking allows monitoring how long a binary state has been consecutively active. This is essential for modeling minimum run times, ramp-up periods, and similar time-dependent constraints.
-
-## Consecutive Duration Tracking
-
-For a binary state variable $s(t) \in \{0, 1\}$, the consecutive duration $d(t)$ tracks how long the state has been continuously active.
-
-### Duration Upper Bound
-
-The duration cannot exceed zero when the state is inactive:
-
-$$\label{eq:duration_upper}
-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)
-
-**Behavior:**
-- When $s(t) = 0$: forces $d(t) \leq 0$, thus $d(t) = 0$
-- When $s(t) = 1$: allows $d(t)$ to be positive
-
----
-
-### Duration Accumulation
-
-While the state is active, the duration increases by the time step size:
-
-$$\label{eq:duration_accumulation_upper}
-d(t+1) \leq d(t) + \Delta d(t) \quad \forall t
-$$
-
-$$\label{eq:duration_accumulation_lower}
-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
-
-**Behavior:**
-- When $s(t+1) = 1$: both inequalities enforce $d(t+1) = d(t) + \Delta d(t)$
-- When $s(t+1) = 0$: only the upper bound applies, and $d(t+1) = 0$ (from equation $\eqref{eq:duration_upper}$)
-
----
-
-### Initial Duration
-
-The duration at the first time step depends on both the state and any previous duration:
-
-$$\label{eq:duration_initial}
-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
-
-**Behavior:**
-- When $s(0) = 1$: duration continues from previous period
-- When $s(0) = 0$: duration resets to zero
-
----
-
-### Complete Formulation
-
-Combining all constraints:
-
-$$
-\begin{align}
-d(t) &\leq s(t) \cdot M && \forall t \label{eq:duration_complete_1} \\
-d(t+1) &\leq d(t) + \Delta d(t) && \forall t \label{eq:duration_complete_2} \\
-d(t+1) &\geq d(t) + \Delta d(t) + (s(t+1) - 1) \cdot M && \forall t \label{eq:duration_complete_3} \\
-d(0) &= (\Delta d(0) + d_\text{prev}) \cdot s(0) && \label{eq:duration_complete_4}
-\end{align}
-$$
-
----
-
-## Minimum Duration Constraints
-
-To enforce a minimum consecutive duration (e.g., minimum run time), an additional constraint links the duration to state changes:
-
-$$\label{eq:minimum_duration}
-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:**
-- When shutting down ($s(t-1) = 1, s(t) = 0$): enforces $d(t-1) \geq d_\text{min}(t-1)$
-- This ensures the state was active for at least $d_\text{min}$ before turning off
-- When state is constant or turning on: constraint is non-binding
-
----
-
-## Implementation
-
-**Function:** [`ModelingPrimitives.consecutive_duration_tracking()`][flixopt.modeling.ModelingPrimitives.consecutive_duration_tracking]
-
-See the API documentation for complete parameter list and usage details.
-
----
-
-## Use Cases
-
-### Minimum Run Time
-
-Ensuring equipment runs for a minimum duration once started:
-
-```python
-# State: 1 when running, 0 when off
-# Require at least 2 hours of operation
-duration = modeling.consecutive_duration_tracking(
- state_variable=on_state,
- duration_per_step=time_step_hours,
- minimum_duration=2.0
-)
-```
-
-### Ramp-Up Tracking
-
-Tracking time since startup for gradual ramp-up constraints:
-
-```python
-# Track startup duration
-startup_duration = modeling.consecutive_duration_tracking(
- state_variable=on_state,
- duration_per_step=time_step_hours
-)
-# Constrain output based on startup duration
-# (additional constraints would link output to startup_duration)
-```
-
-### Cooldown Requirements
-
-Tracking time in a state before allowing transitions:
-
-```python
-# Track maintenance duration
-maintenance_duration = modeling.consecutive_duration_tracking(
- state_variable=maintenance_state,
- duration_per_step=time_step_hours,
- minimum_duration=scheduled_maintenance_hours
-)
-```
-
----
-
-## Used In
-
-This pattern is used in:
-- [`OnOffParameters`](../features/OnOffParameters.md) - Minimum on/off times
-- Operating mode constraints with minimum durations
-- Startup/shutdown sequence modeling
diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/index.md b/docs/user-guide/mathematical-notation/modeling-patterns/index.md
deleted file mode 100644
index 15ff8dbd2..000000000
--- a/docs/user-guide/mathematical-notation/modeling-patterns/index.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Modeling Patterns
-
-This section documents the fundamental mathematical patterns used throughout FlixOpt for constructing optimization models. These patterns are implemented in `flixopt.modeling` and provide reusable building blocks for creating constraints.
-
-## Overview
-
-The modeling patterns are organized into three categories:
-
-1. **[Bounds and States](bounds-and-states.md)** - Variable bounding with optional state control
-2. **[Duration Tracking](duration-tracking.md)** - Tracking consecutive durations of states
-3. **[State Transitions](state-transitions.md)** - Modeling state changes and transitions
-
-## Pattern Categories
-
-### Bounding Patterns
-
-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)
-- **Scaled Bounds** - Bounds dependent on another variable (e.g., size)
-- **Scaled Bounds with State** - Combination of scaling and binary control
-
-### Tracking Patterns
-
-These patterns track properties over time:
-
-- **Expression Tracking** - Creating auxiliary variables that track expressions
-- **Consecutive Duration Tracking** - Tracking how long a state has been active
-- **Mutual Exclusivity** - Ensuring only one of multiple options is active
-
-### Transition Patterns
-
-These patterns model changes between states:
-
-- **State Transitions** - Tracking switches between binary states (on→off, off→on)
-- **Continuous Transitions** - Linking continuous variable changes to switches
-- **Level Changes with Binaries** - Controlled increases/decreases in levels
-
-## Usage in Components
-
-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
-- [`InvestParameters`](../features/InvestParameters.md) uses **bounds with state** for investment decisions
-
-## Implementation
-
-All patterns are implemented in [`flixopt.modeling`][flixopt.modeling] module:
-
-- [`ModelingPrimitives`][flixopt.modeling.ModelingPrimitives] - Core constraint patterns
-- [`BoundingPatterns`][flixopt.modeling.BoundingPatterns] - Specialized bounding patterns
diff --git a/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md b/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md
deleted file mode 100644
index dc75a8008..000000000
--- a/docs/user-guide/mathematical-notation/modeling-patterns/state-transitions.md
+++ /dev/null
@@ -1,227 +0,0 @@
-# State Transitions
-
-State transition patterns model changes between discrete states and link them to continuous variables. These patterns are essential for modeling startup/shutdown events, switching behavior, and controlled changes in system operation.
-
-## Binary State Transitions
-
-For a binary state variable $s(t) \in \{0, 1\}$, state transitions track when the state switches on or off.
-
-### 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
-
-### Transition Tracking
-
-The state change equals the difference between switch-on and switch-off:
-
-$$\label{eq:state_transition}
-s^\text{on}(t) - s^\text{off}(t) = s(t) - s(t-1) \quad \forall t > 0
-$$
-
-$$\label{eq:state_transition_initial}
-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
-
-**Behavior:**
-- Off → On ($s(t-1)=0, s(t)=1$): $s^\text{on}(t)=1, s^\text{off}(t)=0$
-- On → Off ($s(t-1)=1, s(t)=0$): $s^\text{on}(t)=0, s^\text{off}(t)=1$
-- No change: $s^\text{on}(t)=0, s^\text{off}(t)=0$
-
----
-
-### Mutual Exclusivity of Switches
-
-A state cannot switch on and off simultaneously:
-
-$$\label{eq:switch_exclusivity}
-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
-
----
-
-### Complete State Transition Formulation
-
-$$
-\begin{align}
-s^\text{on}(t) - s^\text{off}(t) &= s(t) - s(t-1) && \forall t > 0 \label{eq:transition_complete_1} \\
-s^\text{on}(0) - s^\text{off}(0) &= s(0) - s_\text{prev} && \label{eq:transition_complete_2} \\
-s^\text{on}(t) + s^\text{off}(t) &\leq 1 && \forall t \label{eq:transition_complete_3} \\
-s^\text{on}(t), s^\text{off}(t) &\in \{0, 1\} && \forall t \label{eq:transition_complete_4}
-\end{align}
-$$
-
-**Implementation:** [`BoundingPatterns.state_transition_bounds()`][flixopt.modeling.BoundingPatterns.state_transition_bounds]
-
----
-
-## Continuous Transitions
-
-When a continuous variable should only change when certain switch events occur, continuous transition bounds link the variable changes to binary switches.
-
-### Change Bounds with Switches
-
-$$\label{eq:continuous_transition}
--\Delta v^\text{max} \cdot (s^\text{on}(t) + s^\text{off}(t)) \leq v(t) - v(t-1) \leq \Delta v^\text{max} \cdot (s^\text{on}(t) + s^\text{off}(t)) \quad \forall t > 0
-$$
-
-$$\label{eq:continuous_transition_initial}
--\Delta v^\text{max} \cdot (s^\text{on}(0) + s^\text{off}(0)) \leq v(0) - v_\text{prev} \leq \Delta v^\text{max} \cdot (s^\text{on}(0) + s^\text{off}(0))
-$$
-
-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
-- $s^\text{on}(t), s^\text{off}(t) \in \{0, 1\}$ being switch binary variables
-
-**Behavior:**
-- When $s^\text{on}(t) = 0$ and $s^\text{off}(t) = 0$: forces $v(t) = v(t-1)$ (no change)
-- When $s^\text{on}(t) = 1$ or $s^\text{off}(t) = 1$: allows change up to $\pm \Delta v^\text{max}$
-
-**Implementation:** [`BoundingPatterns.continuous_transition_bounds()`][flixopt.modeling.BoundingPatterns.continuous_transition_bounds]
-
----
-
-## Level Changes with Binaries
-
-This pattern models a level variable that can increase or decrease, with changes controlled by binary variables. This is useful for inventory management, capacity adjustments, or gradual state changes.
-
-### Level Evolution
-
-The level evolves based on increases and decreases:
-
-$$\label{eq:level_initial}
-\ell(0) = \ell_\text{init} + \ell^\text{inc}(0) - \ell^\text{dec}(0)
-$$
-
-$$\label{eq:level_evolution}
-\ell(t) = \ell(t-1) + \ell^\text{inc}(t) - \ell^\text{dec}(t) \quad \forall t > 0
-$$
-
-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)
-- $\ell^\text{dec}(t)$ being the decrease in level at time $t$ (non-negative)
-
----
-
-### Change Bounds with Binary Control
-
-Changes are bounded and controlled by binary variables:
-
-$$\label{eq:increase_bound}
-\ell^\text{inc}(t) \leq \Delta \ell^\text{max} \cdot b^\text{inc}(t) \quad \forall t
-$$
-
-$$\label{eq:decrease_bound}
-\ell^\text{dec}(t) \leq \Delta \ell^\text{max} \cdot b^\text{dec}(t) \quad \forall t
-$$
-
-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
-
----
-
-### Mutual Exclusivity of Changes
-
-Simultaneous increase and decrease are prevented:
-
-$$\label{eq:change_exclusivity}
-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
-
----
-
-### Complete Level Change Formulation
-
-$$
-\begin{align}
-\ell(0) &= \ell_\text{init} + \ell^\text{inc}(0) - \ell^\text{dec}(0) && \label{eq:level_complete_1} \\
-\ell(t) &= \ell(t-1) + \ell^\text{inc}(t) - \ell^\text{dec}(t) && \forall t > 0 \label{eq:level_complete_2} \\
-\ell^\text{inc}(t) &\leq \Delta \ell^\text{max} \cdot b^\text{inc}(t) && \forall t \label{eq:level_complete_3} \\
-\ell^\text{dec}(t) &\leq \Delta \ell^\text{max} \cdot b^\text{dec}(t) && \forall t \label{eq:level_complete_4} \\
-b^\text{inc}(t) + b^\text{dec}(t) &\leq 1 && \forall t \label{eq:level_complete_5} \\
-b^\text{inc}(t), b^\text{dec}(t) &\in \{0, 1\} && \forall t \label{eq:level_complete_6}
-\end{align}
-$$
-
-**Implementation:** [`BoundingPatterns.link_changes_to_level_with_binaries()`][flixopt.modeling.BoundingPatterns.link_changes_to_level_with_binaries]
-
----
-
-## Use Cases
-
-### Startup/Shutdown Costs
-
-Track startup and shutdown events to apply costs:
-
-```python
-# Create switch variables
-switch_on, switch_off = modeling.state_transition_bounds(
- state_variable=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
-```
-
-### Limited Switching
-
-Restrict the number of state changes:
-
-```python
-# Track all switches
-switch_on, switch_off = modeling.state_transition_bounds(
- state_variable=on_state
-)
-
-# Limit total switches
-model.add_constraint(
- (switch_on + switch_off).sum() <= max_switches
-)
-```
-
-### Gradual Capacity Changes
-
-Model systems where capacity can be incrementally adjusted:
-
-```python
-# Level represents installed capacity
-level_var, increase, decrease, inc_binary, dec_binary = \
- modeling.link_changes_to_level_with_binaries(
- initial_level=current_capacity,
- max_change=max_capacity_change_per_period
- )
-
-# Constrain total increases
-model.add_constraint(increase.sum() <= max_total_expansion)
-```
-
----
-
-## Used In
-
-These patterns are used in:
-- [`OnOffParameters`](../features/OnOffParameters.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/mathematical-notation/others.md b/docs/user-guide/mathematical-notation/others.md
deleted file mode 100644
index bdc602308..000000000
--- a/docs/user-guide/mathematical-notation/others.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Work in Progress
-
-This is a work in progress.
diff --git a/docs/user-guide/optimization/index.md b/docs/user-guide/optimization/index.md
new file mode 100644
index 000000000..7010acfc5
--- /dev/null
+++ b/docs/user-guide/optimization/index.md
@@ -0,0 +1,195 @@
+# Running Optimizations
+
+This section covers how to run optimizations in flixOpt, including different optimization modes and solver configuration.
+
+## Optimization Modes
+
+flixOpt provides three optimization modes to handle different problem sizes and requirements:
+
+### Optimization (Full)
+
+[`Optimization`][flixopt.optimization.Optimization] solves the entire problem at once.
+
+```python
+import flixopt as fx
+
+optimization = fx.Optimization('my_model', flow_system)
+optimization.solve(fx.solvers.HighsSolver())
+```
+
+**Best for:**
+
+- Small to medium problems
+- When you need the globally optimal solution
+- Problems without time-coupling simplifications
+
+### SegmentedOptimization
+
+[`SegmentedOptimization`][flixopt.optimization.SegmentedOptimization] splits the time horizon into segments and solves them sequentially.
+
+```python
+optimization = fx.SegmentedOptimization(
+ 'segmented_model',
+ flow_system,
+ segment_length=24, # Hours per segment
+ overlap_length=4 # Hours of overlap between segments
+)
+optimization.solve(fx.solvers.HighsSolver())
+```
+
+**Best for:**
+
+- Large problems that don't fit in memory
+- Long time horizons (weeks, months)
+- Problems where decisions are mostly local in time
+
+**Trade-offs:**
+
+- Faster solve times
+- May miss globally optimal solutions
+- Overlap helps maintain solution quality at segment boundaries
+
+### ClusteredOptimization
+
+[`ClusteredOptimization`][flixopt.optimization.ClusteredOptimization] uses time series aggregation to reduce problem size by identifying representative periods.
+
+```python
+clustering_params = fx.ClusteringParameters(
+ n_periods=8, # Number of typical periods
+ hours_per_period=24 # Hours per typical period
+)
+
+optimization = fx.ClusteredOptimization(
+ 'clustered_model',
+ flow_system,
+ clustering_params
+)
+optimization.solve(fx.solvers.HighsSolver())
+```
+
+**Best for:**
+
+- Investment planning problems
+- Year-long optimizations
+- When computational speed is critical
+
+**Trade-offs:**
+
+- Much faster solve times
+- Approximates the full problem
+- Best when patterns repeat (e.g., typical days)
+
+## Choosing an Optimization Mode
+
+| Mode | Problem Size | Solve Time | Solution Quality |
+|------|-------------|------------|------------------|
+| `Optimization` | Small-Medium | Slow | Optimal |
+| `SegmentedOptimization` | Large | Medium | Near-optimal |
+| `ClusteredOptimization` | Very Large | Fast | Approximate |
+
+## Solver Configuration
+
+### Available Solvers
+
+| Solver | Type | Speed | License |
+|--------|------|-------|---------|
+| **HiGHS** | Open-source | Fast | Free |
+| **Gurobi** | Commercial | Fastest | Academic/Commercial |
+| **CPLEX** | Commercial | Fastest | Academic/Commercial |
+| **GLPK** | Open-source | Slower | Free |
+
+**Recommendation:** Start with HiGHS (included by default). Use Gurobi/CPLEX for large models or when speed matters.
+
+### Solver Options
+
+```python
+# Basic usage with defaults
+optimization.solve(fx.solvers.HighsSolver())
+
+# With custom options
+optimization.solve(
+ fx.solvers.GurobiSolver(
+ time_limit_seconds=3600,
+ mip_gap=0.01,
+ extra_options={
+ 'Threads': 4,
+ 'Presolve': 2
+ }
+ )
+)
+```
+
+Common solver parameters:
+
+- `time_limit_seconds` - Maximum solve time
+- `mip_gap` - Acceptable optimality gap (0.01 = 1%)
+- `log_to_console` - Show solver output
+
+## Performance Tips
+
+### Model Size Reduction
+
+- Use longer timesteps where acceptable
+- Use `ClusteredOptimization` for long horizons
+- Remove unnecessary components
+- Simplify constraint formulations
+
+### Solver Tuning
+
+- Enable presolve and cuts
+- Adjust optimality tolerances for faster (approximate) solutions
+- Use parallel threads when available
+
+### Problem Formulation
+
+- Avoid unnecessary binary variables
+- Use continuous investment sizes when possible
+- Tighten variable bounds
+- Remove redundant constraints
+
+## Debugging
+
+### Infeasibility
+
+If your model has no feasible solution:
+
+1. **Enable excess penalties on buses** to allow balance violations:
+ ```python
+ # Allow imbalance with high penalty cost (default is 1e5)
+ heat_bus = fx.Bus('Heat', excess_penalty_per_flow_hour=1e5)
+
+ # Or disable penalty to enforce strict balance
+ electricity_bus = fx.Bus('Electricity', excess_penalty_per_flow_hour=None)
+ ```
+ When `excess_penalty_per_flow_hour` is set, the optimization can violate bus balance constraints by paying a penalty, helping identify which constraints cause infeasibility.
+
+2. **Use Gurobi for infeasibility analysis** - When using GurobiSolver and the model is infeasible, flixOpt automatically extracts and logs the Irreducible Inconsistent Subsystem (IIS):
+ ```python
+ # Gurobi provides detailed infeasibility analysis
+ optimization.solve(fx.solvers.GurobiSolver())
+ # If infeasible, check the model documentation file for IIS details
+ ```
+ The infeasible constraints are saved to the model documentation file in the results folder.
+
+3. Check balance constraints - can supply meet demand?
+4. Verify capacity limits are consistent
+5. Review storage state requirements
+6. Simplify model to isolate the issue
+
+See [Troubleshooting](../troubleshooting.md) for more details.
+
+### Unexpected Results
+
+If solutions don't match expectations:
+
+1. Verify input data (units, scales)
+2. Enable logging: `fx.CONFIG.exploring()`
+3. Visualize intermediate results
+4. Start with a simpler model
+5. Check constraint formulations
+
+## Next Steps
+
+- See [Examples](../../examples/03-Optimization Modes.md) for working code
+- Learn about [Mathematical Notation](../mathematical-notation/index.md)
+- Explore [Recipes](../recipes/index.md) for common patterns
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/docs/user-guide/results/index.md b/docs/user-guide/results/index.md
new file mode 100644
index 000000000..92656010d
--- /dev/null
+++ b/docs/user-guide/results/index.md
@@ -0,0 +1,18 @@
+# Analyzing Results
+
+!!! note "Under Development"
+ This section is being expanded with detailed tutorials.
+
+Learn how to work with optimization results:
+
+- Accessing solution data
+- Plotting flows and states
+- Exporting to various formats
+- Comparing scenarios and periods
+
+## Getting Started
+
+For now, see:
+
+- **[Examples](../../examples/index.md)** - Result analysis patterns in working code
+- **[API Reference](../../api-reference/results.md)** - Results class documentation
diff --git a/docs/user-guide/support.md b/docs/user-guide/support.md
new file mode 100644
index 000000000..5f26cdd24
--- /dev/null
+++ b/docs/user-guide/support.md
@@ -0,0 +1,23 @@
+# Support
+
+## Getting Help
+
+**[GitHub Issues](https://github.com/flixOpt/flixopt/issues)** — Report bugs or ask questions
+
+When opening an issue, include:
+
+- Minimal reproducible example
+- flixOpt version: `python -c "import flixopt; print(flixopt.__version__)"`
+- Python version and OS
+- Full error message
+
+## Resources
+
+- [FAQ](faq.md) — Common questions
+- [Troubleshooting](troubleshooting.md) — Common issues
+- [Examples](../examples/index.md) — Working code
+- [API Reference](../api-reference/index.md) — Technical docs
+
+## Contributing
+
+See our [Contributing Guide](../contribute.md) for how to help improve flixOpt.
diff --git a/docs/user-guide/troubleshooting.md b/docs/user-guide/troubleshooting.md
new file mode 100644
index 000000000..2c89be8dc
--- /dev/null
+++ b/docs/user-guide/troubleshooting.md
@@ -0,0 +1,61 @@
+# Troubleshooting
+
+## Infeasible Model
+
+**Problem:** Solver reports the model is infeasible.
+
+**Solutions:**
+
+1. Check that supply can meet demand at all timesteps
+2. Verify capacity limits are sufficient
+3. Review storage initial/final states
+
+## Unbounded Model
+
+**Problem:** Solver reports the model is unbounded.
+
+**Solutions:**
+
+1. Add upper bounds to all flows
+2. Ensure investment parameters have maximum sizes
+3. Verify effect coefficients have correct signs
+
+## Unexpected Results
+
+**Debugging Steps:**
+
+1. Enable logging:
+ ```python
+ from flixopt import CONFIG
+ CONFIG.exploring()
+ ```
+
+2. Start with a minimal model and add complexity incrementally
+
+3. Check units are consistent
+
+4. Visualize results to verify energy balances
+
+## Slow Solve Times
+
+**Solutions:**
+
+1. Use longer timesteps or aggregate time periods
+2. Use Gurobi instead of HiGHS for large models
+3. Set solver options:
+ ```python
+ solver = fx.solvers.GurobiSolver(
+ time_limit_seconds=3600,
+ mip_gap=0.01
+ )
+ ```
+
+## Getting Help
+
+If you're stuck:
+
+1. Search [GitHub Issues](https://github.com/flixOpt/flixopt/issues)
+2. Open a new issue with:
+ - Minimal reproducible example
+ - flixopt and Python version
+ - Full error message
diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py
index 2913f643f..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 ---
@@ -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..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
@@ -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..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, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects
-
-# Import new Optimization classes
+from .interface import InvestParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects, StatusParameters
from .optimization import ClusteredOptimization, Optimization, SegmentedOptimization
__all__ = [
'TimeSeriesData',
'CONFIG',
- 'change_logging_level',
'Flow',
'Bus',
'Effect',
@@ -51,22 +45,16 @@
'LinearConverter',
'Transmission',
'FlowSystem',
- # New Optimization classes (preferred)
'Optimization',
'ClusteredOptimization',
'SegmentedOptimization',
- # Old Calculation classes (deprecated, for backwards compatibility)
- 'FullCalculation',
- 'AggregatedCalculation',
- 'SegmentedCalculation',
'InvestParameters',
- 'OnOffParameters',
+ 'StatusParameters',
'Piece',
'Piecewise',
'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/components.py b/flixopt/components.py
index 07bc5f204..0cfed39eb 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
@@ -41,16 +41,15 @@ class LinearConverter(Component):
behavior approximated through piecewise linear segments.
Mathematical Formulation:
- See the complete mathematical model in the documentation:
- [LinearConverter](../user-guide/mathematical-notation/elements/LinearConverter.md)
+ See
Args:
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 +166,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
@@ -260,18 +259,7 @@ class Storage(Component):
and investment-optimized storage systems with comprehensive techno-economic modeling.
Mathematical Formulation:
- See the complete mathematical model in the documentation:
- [Storage](../user-guide/mathematical-notation/elements/Storage.md)
-
- - Equation (1): Charge state bounds
- - Equation (3): Storage balance (charge state evolution)
-
- Variable Mapping:
- - ``capacity_in_flow_hours`` → C (storage capacity)
- - ``charge_state`` → c(t_i) (state of charge at time t_i)
- - ``relative_loss_per_hour`` → ċ_rel,loss (self-discharge rate)
- - ``eta_charge`` → η_in (charging efficiency)
- - ``eta_discharge`` → η_out (discharging efficiency)
+ See
Args:
label: Element identifier used in the FlowSystem.
@@ -573,8 +561,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 +617,7 @@ class Transmission(Component):
)
```
- Material conveyor with on/off operation:
+ Material conveyor with active/inactive status:
```python
conveyor_belt = Transmission(
@@ -637,10 +625,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 +642,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 +659,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 +668,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 +727,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,13 +760,23 @@ 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
class LinearConverterModel(ComponentModel):
+ """Mathematical model implementation for LinearConverter components.
+
+ Creates optimization constraints for linear conversion relationships between
+ input and output flows, supporting both simple conversion factors and piecewise
+ non-linear approximations.
+
+ Mathematical Formulation:
+ See
+ """
+
element: LinearConverter
def __init__(self, model: FlowSystemModel, element: LinearConverter):
@@ -807,7 +805,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 +817,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',
@@ -827,7 +825,14 @@ def _do_modeling(self):
class StorageModel(ComponentModel):
- """Submodel of Storage"""
+ """Mathematical model implementation for Storage components.
+
+ Creates optimization variables and constraints for charge state tracking,
+ storage balance equations, and optional investment sizing.
+
+ Mathematical Formulation:
+ See
+ """
element: Storage
@@ -978,7 +983,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/config.py b/flixopt/config.py
index dbe2bf3c5..f090430b0 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')
@@ -30,7 +30,7 @@
logging.addLevelName(SUCCESS_LEVEL, 'SUCCESS')
# Deprecation removal version - update this when planning the next major version
-DEPRECATION_REMOVAL_VERSION = '5.0.0'
+DEPRECATION_REMOVAL_VERSION = '6.0.0'
class MultilineFormatter(logging.Formatter):
@@ -808,23 +808,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..5dd53258f 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
@@ -34,16 +32,15 @@
@register_class_for_io
class Effect(Element):
- """
- Represents system-wide impacts like costs, emissions, resource consumption, or other effects.
+ """Represents system-wide impacts like costs, emissions, or resource consumption.
- Effects capture the broader impacts of system operation and investment decisions beyond
- the primary energy/material flows. Each Effect accumulates contributions from Components,
- Flows, and other system elements. One Effect is typically chosen as the optimization
- objective, while others can serve as constraints or tracking metrics.
+ Effects quantify impacts aggregating contributions from Elements across the FlowSystem.
+ One Effect serves as the optimization objective, while others can be constrained or tracked.
+ Supports operational and investment contributions, cross-effect relationships (e.g., carbon
+ pricing), and flexible constraint formulation.
- Effects support comprehensive modeling including operational and investment contributions,
- cross-effect relationships (e.g., carbon pricing), and flexible constraint formulation.
+ Mathematical Formulation:
+ See
Args:
label: The label of the Element. Used to identify it in the FlowSystem.
@@ -304,6 +301,16 @@ def _plausibility_checks(self) -> None:
class EffectModel(ElementModel):
+ """Mathematical model implementation for Effects.
+
+ Creates optimization variables and constraints for effect aggregation,
+ including periodic and temporal tracking, cross-effect contributions,
+ and effect bounds.
+
+ Mathematical Formulation:
+ See
+ """
+
element: Effect # Type hint
def __init__(self, model: FlowSystemModel, element: Effect):
@@ -468,21 +475,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 17730bc98..74ed7bde4 100644
--- a/flixopt/elements.py
+++ b/flixopt/elements.py
@@ -5,17 +5,16 @@
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, 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 +57,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 +69,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 +90,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 +113,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
@@ -191,14 +190,13 @@ class Bus(Element):
or material flows between different Components.
Mathematical Formulation:
- See the complete mathematical model in the documentation:
- [Bus](../user-guide/mathematical-notation/elements/Bus.md)
+ See
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.
@@ -208,7 +206,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
)
```
@@ -217,7 +215,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
)
```
@@ -226,14 +224,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.
@@ -247,11 +245,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] = []
@@ -268,16 +271,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(
@@ -285,8 +288,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."""
@@ -314,7 +317,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,11 +327,10 @@ 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:
- [Flow](../user-guide/mathematical-notation/elements/Flow.md)
+ See
Args:
label: Unique flow identifier within its component.
@@ -340,7 +342,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 +351,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 +388,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 +430,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 +456,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,25 +481,18 @@ 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
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()
@@ -507,8 +502,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 +532,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 +553,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:
@@ -598,6 +593,15 @@ def _format_invest_params(self, params: InvestParameters) -> str:
class FlowModel(ElementModel):
+ """Mathematical model implementation for Flow elements.
+
+ Creates optimization variables and constraints for flow rate bounds,
+ flow-hours tracking, and load factors.
+
+ Mathematical Formulation:
+ See
+ """
+
element: Flow # Type hint
def __init__(self, model: FlowSystemModel, element: Flow):
@@ -666,18 +670,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 +697,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 +722,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 +733,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 +813,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 +829,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 +842,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:
@@ -861,11 +865,20 @@ def previous_states(self) -> xr.DataArray | None:
class BusModel(ElementModel):
+ """Mathematical model implementation for Bus elements.
+
+ Creates optimization variables and constraints for nodal balance equations,
+ and optional excess/deficit variables with penalty costs.
+
+ Mathematical Formulation:
+ See
+ """
+
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):
@@ -878,39 +891,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,
@@ -923,7 +935,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 +944,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 +1002,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..cd9e07151 100644
--- a/flixopt/features.py
+++ b/flixopt/features.py
@@ -16,22 +16,27 @@
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.
+ """Mathematical model implementation for investment decisions.
+
+ Creates optimization variables and constraints for investment sizing decisions,
+ supporting both binary and continuous sizing with comprehensive effect modeling.
+
+ Mathematical Formulation:
+ See
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.
label_of_model: The label of the model. This is needed to construct the full label of the model.
-
"""
parameters: InvestParameters
@@ -75,7 +80,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 +149,40 @@ def invested(self) -> linopy.Variable | None:
return self._variables['invested']
-class OnOffModel(Submodel):
- """OnOff model using factory patterns"""
+class StatusModel(Submodel):
+ """Mathematical model implementation for binary status.
+
+ Creates optimization variables and constraints for binary status modeling,
+ state transitions, duration tracking, and operational effects.
+
+ Mathematical Formulation:
+ See
+ """
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 +190,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 +286,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_uptime(self):
+ """Get previous uptime (consecutive active hours).
- def _get_previous_on_duration(self):
- """Get previous on duration. Previously OFF by default, for one timestep"""
+ 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):
@@ -368,6 +395,15 @@ def _do_modeling(self):
class PiecewiseModel(Submodel):
+ """Mathematical model implementation for piecewise linear approximations.
+
+ Creates optimization variables and constraints for piecewise linear relationships,
+ including lambda variables, piece activation binaries, and coupling constraints.
+
+ Mathematical Formulation:
+ See
+ """
+
def __init__(
self,
model: FlowSystemModel,
diff --git a/flixopt/flow_system.py b/flixopt/flow_system.py
index 52c403396..9015de3e4 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,
@@ -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,8 +76,8 @@ 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=...)
- >>> heat_bus = fx.Bus('Heat', excess_penalty_per_flow_hour=1e4)
+ >>> boiler = fx.Component('Boiler', inputs=[heat_flow], status_parameters=...)
+ >>> 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)
@@ -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/interface.py b/flixopt/interface.py
index cfa210f6d..7995d5e78 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
"""
@@ -82,8 +82,14 @@ def transform_data(self, name_prefix: str = '') -> None:
@register_class_for_io
class Piecewise(Interface):
- """
- Define a Piecewise, consisting of a list of Pieces.
+ """Define piecewise linear approximations for modeling non-linear relationships.
+
+ Enables modeling of non-linear relationships through piecewise linear segments
+ while maintaining problem linearity. Consists of a collection of Pieces that
+ define valid ranges for variables.
+
+ Mathematical Formulation:
+ See
Args:
pieces: list of Piece objects defining the linear segments. The arrangement
@@ -413,7 +419,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.
@@ -706,8 +712,7 @@ class InvestParameters(Interface):
- **Divestment Effects**: Penalties for not investing (demolition, opportunity costs)
Mathematical Formulation:
- See the complete mathematical model in the documentation:
- [InvestParameters](../user-guide/mathematical-notation/features/InvestParameters.md)
+ See
Args:
fixed_size: Creates binary decision at this exact size. None allows continuous sizing.
@@ -1006,19 +1011,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
@@ -1028,46 +1033,45 @@ class OnOffParameters(Interface):
- **Process Equipment**: Compressors, pumps with operational constraints
Mathematical Formulation:
- See the complete mathematical model in the documentation:
- [OnOffParameters](../user-guide/mathematical-notation/features/OnOffParameters.md)
+ See
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 +1079,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 +1193,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/flixopt/optimization.py b/flixopt/optimization.py
index e537029d7..529975df7 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,
)
@@ -329,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
)
],
}
@@ -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/mkdocs.yml b/mkdocs.yml
index 0adba464d..8fb6765ae 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -7,37 +7,60 @@ site_description: Energy and Material Flow Optimization Framework
site_url: https://flixopt.github.io/flixopt/
repo_url: https://github.com/flixOpt/flixopt
repo_name: flixOpt/flixopt
+edit_uri: edit/main/docs/
nav:
- - Home: index.md
+ - Home:
+ - Home: index.md
+ - Getting Started:
+ - Installation: home/installation.md
+ - Quick Start: home/quick-start.md
+ - About:
+ - Users: home/users.md
+ - Citing: home/citing.md
+ - License: home/license.md
+
- User Guide:
- - Getting Started: getting-started.md
+ - Overview: user-guide/index.md
- Core Concepts: user-guide/core-concepts.md
- - Migration to v3.0.0: user-guide/migration-guide-v3.md
+ - Building Models: user-guide/building-models/index.md
+ - Running Optimizations: user-guide/optimization/index.md
+ - Analyzing Results: user-guide/results/index.md
- Mathematical Notation:
- Overview: user-guide/mathematical-notation/index.md
- - Dimensions: user-guide/mathematical-notation/dimensions.md
- - Elements:
- - Flow: user-guide/mathematical-notation/elements/Flow.md
- - Bus: user-guide/mathematical-notation/elements/Bus.md
- - Storage: user-guide/mathematical-notation/elements/Storage.md
- - 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
- - Piecewise: user-guide/mathematical-notation/features/Piecewise.md
- - Effects, Penalty & Objective: user-guide/mathematical-notation/effects-penalty-objective.md
- - Modeling Patterns:
- - Overview: user-guide/mathematical-notation/modeling-patterns/index.md
- - Bounds and States: user-guide/mathematical-notation/modeling-patterns/bounds-and-states.md
- - Duration Tracking: user-guide/mathematical-notation/modeling-patterns/duration-tracking.md
- - State Transitions: user-guide/mathematical-notation/modeling-patterns/state-transitions.md
+ - Bus: user-guide/mathematical-notation/elements/Bus.md
+ - Flow: user-guide/mathematical-notation/elements/Flow.md
+ - LinearConverter: user-guide/mathematical-notation/elements/LinearConverter.md
+ - Storage: user-guide/mathematical-notation/elements/Storage.md
+ - Effects & Dimensions: user-guide/mathematical-notation/effects-and-dimensions.md
+ - Investment: user-guide/mathematical-notation/features/InvestParameters.md
+ - Status: user-guide/mathematical-notation/features/StatusParameters.md
+ - Piecewise: user-guide/mathematical-notation/features/Piecewise.md
- Recipes: user-guide/recipes/index.md
- - Roadmap: roadmap.md
- - Examples: examples/
- - Contribute: contribute.md
+ - Support:
+ - FAQ: user-guide/faq.md
+ - Troubleshooting: user-guide/troubleshooting.md
+ - Community: user-guide/support.md
+ - Migration & Updates:
+ - Migration Guide v3: user-guide/migration-guide-v3.md
+ - Release Notes: changelog.md
+ - Roadmap: roadmap.md
+
+ - Examples:
+ - Overview: examples/index.md
+ - Basic Examples:
+ - examples/00-Minimal Example.md
+ - examples/01-Basic Example.md
+ - Operational Optimization:
+ - examples/02-Complex Example.md
+ - examples/03-Optimization Modes.md
+ - Planning & Investment:
+ - examples/04-Scenarios.md
+ - examples/05-Two-stage-optimization.md
+
- API Reference: api-reference/
- - Release Notes: changelog/
+
+ - Contributing: contribute.md
theme:
name: material
@@ -128,7 +151,7 @@ markdown_extensions:
- toc:
permalink: true
permalink_title: Anchor link to this section
- toc_depth: 2
+ toc_depth: 3
title: On this page
# Code blocks
@@ -147,6 +170,9 @@ markdown_extensions:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
+ - name: plotly
+ class: mkdocs-plotly
+ format: !!python/name:mkdocs_plotly_plugin.fences.fence_plotly
# Enhanced content
- pymdownx.details
@@ -185,6 +211,8 @@ plugins:
- search:
separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
+ - plotly
+
- table-reader
- include-markdown
@@ -310,6 +338,7 @@ extra_css:
extra_javascript:
- javascripts/mathjax.js
+ - javascripts/plotly-instant.js
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
- https://polyfill.io/v3/polyfill.min.js?features=es6
diff --git a/pyproject.toml b/pyproject.toml
index 258b0ab7f..206283767 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -104,6 +104,7 @@ docs = [
"mkdocs-gen-files==0.5.0",
"mkdocs-include-markdown-plugin==7.2.0",
"mkdocs-literate-nav==0.6.2",
+ "mkdocs-plotly-plugin==0.1.3",
"markdown-include==0.8.1",
"pymdown-extensions==10.16.1",
"pygments==2.19.2",
diff --git a/scripts/extract_changelog.py b/scripts/extract_changelog.py
deleted file mode 100644
index 44790fec6..000000000
--- a/scripts/extract_changelog.py
+++ /dev/null
@@ -1,151 +0,0 @@
-#!/usr/bin/env python3
-"""
-Extract individual releases from CHANGELOG.md to docs/changelog/
-Simple script to create one file per release.
-"""
-
-import re
-from pathlib import Path
-
-from packaging.version import InvalidVersion, Version
-from packaging.version import parse as parse_version
-
-
-def extract_releases():
- """Extract releases from CHANGELOG.md and save to individual files."""
-
- changelog_path = Path('CHANGELOG.md')
- output_dir = Path('docs/changelog')
-
- if not changelog_path.exists():
- print('❌ CHANGELOG.md not found')
- return
-
- # Create output directory
- output_dir.mkdir(parents=True, exist_ok=True)
-
- # Read changelog
- with open(changelog_path, encoding='utf-8') as f:
- content = f.read()
-
- # Remove template section (HTML comments)
- content = re.sub(r'', '', content, flags=re.DOTALL)
-
- # Split by release headers
- sections = re.split(r'^## \[', content, flags=re.MULTILINE)
-
- releases = []
- for section in sections[1:]: # Skip first empty section
- # Extract version and date from start of section
- match = re.match(r'([^\]]+)\] - ([^\n]+)\n(.*)', section, re.DOTALL)
- if match:
- version, date, release_content = match.groups()
- releases.append((version, date.strip(), release_content.strip()))
-
- print(f'🔍 Found {len(releases)} releases')
-
- # Sort releases by version (oldest first) to keep existing file prefixes stable.
- def version_key(release):
- try:
- return parse_version(release[0])
- except InvalidVersion:
- return parse_version('0.0.0') # fallback for invalid versions
-
- releases.sort(key=version_key, reverse=False)
-
- # Show what we captured for debugging
- if releases:
- print(f'🔧 First release content length: {len(releases[0][2])}')
-
- for i, (version_str, date, release_content) in enumerate(releases):
- # Clean up version for filename with numeric prefix (newest first)
- index = 99999 - i # Newest first, while keeping the same file names for old releases
- prefix = f'{index:05d}' # Zero-padded 5-digit number
- filename = f'{prefix}-v{version_str.replace(" ", "-")}.md'
- filepath = output_dir / filename
-
- # Clean up content - remove trailing --- separators and emojis from headers
- cleaned_content = re.sub(r'\s*---\s*$', '', release_content.strip())
-
- # Generate navigation links
- nav_links = []
-
- # Previous version (older release)
- if i > 0:
- prev_index = 99999 - (i - 1)
- prev_version = releases[i - 1][0]
- prev_filename = f'{prev_index:05d}-v{prev_version.replace(" ", "-")}.md'
- nav_links.append(f'← [Previous: {prev_version}]({prev_filename})')
-
- # Next version (newer release)
- if i < len(releases) - 1:
- next_index = 99999 - (i + 1)
- next_version = releases[i + 1][0]
- next_filename = f'{next_index:05d}-v{next_version.replace(" ", "-")}.md'
- nav_links.append(f'[Next: {next_version}]({next_filename}) →')
-
- # Always add link back to index
- nav_links.append('[📋 All Releases](index.md)')
- # Add GitHub tag link only for valid PEP 440 versions (skip e.g. "Unreleased")
- ver_obj = parse_version(version_str)
- if isinstance(ver_obj, Version):
- nav_links.append(f'[🏷️ GitHub Release](https://github.com/flixOpt/flixopt/releases/tag/v{version_str})')
- # Create content with navigation
- content_lines = [
- f'# {version_str} - {date.strip()}',
- '',
- ' | '.join(nav_links),
- '',
- '---',
- '',
- cleaned_content,
- '',
- '---',
- '',
- ' | '.join(nav_links),
- ]
-
- # Write file
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write('\n'.join(content_lines))
-
- print(f'✅ Created {filename}')
-
- print(f'🎉 Extracted {len(releases)} releases to docs/changelog/')
-
-
-def extract_index():
- changelog_path = Path('CHANGELOG.md')
- output_dir = Path('docs/changelog')
- index_path = output_dir / 'index.md'
-
- if not changelog_path.exists():
- print('❌ CHANGELOG.md not found')
- return
-
- # Create output directory
- output_dir.mkdir(parents=True, exist_ok=True)
-
- # Read changelog
- with open(changelog_path, encoding='utf-8') as f:
- content = f.read()
-
- intro_match = re.search(r'# Changelog\s+([\s\S]*?)(?=