Skip to content

Commit b7c2dc7

Browse files
authored
Feature/penalty as effect (#486)
* implemented the migration of Penalty to be a standard Effect. Here's what was done: ✅ Implementation Complete Changes Made: 1. Penalty Effect Creation (flixopt/effects.py): - Created _penalty effect automatically in FlowSystem.__init__ - Added penalty_effect property to EffectCollection - Penalty now has temporal and periodic dimensions like other effects 2. Removed Old Penalty Infrastructure (flixopt/effects.py): - Removed ShareAllocationModel penalty from EffectCollectionModel - Removed add_share_to_penalty() method - Updated objective to use penalty effect: objective_effect + penalty_effect 3. Updated Internal Usage: - Bus (flixopt/elements.py:960-969): Now uses add_share_to_effects with target='temporal' - Aggregation (flixopt/aggregation.py:351-355): Now uses add_share_to_effects with target='temporal' 4. Results Structure (flixopt/calculation.py:109-113): - Changed from 'Penalty': scalar to 'Penalty': {temporal, periodic, total} - Penalty excluded from regular Effects list (shown separately) 5. I/O & Serialization (flixopt/flow_system.py:613-614): - Skip _penalty effect when loading from dataset (auto-created) 6. Documentation (docs/user-guide/mathematical-notation/effects-penalty-objective.md): - Updated to reflect Penalty as an Effect - Updated mathematical formulations - Updated all objective function equations 7. Tests Updated: - tests/test_bus.py: Updated penalty assertions - tests/test_scenarios.py: Updated to use new penalty structure Key Benefits: ✅ Unified Interface: Penalty shares added same way as effect shares ✅ Dimensional Support: Penalties can now vary by time/period/scenario ✅ Constrainable: Can add bounds to penalty (useful for debugging) ✅ Better Results: Penalty breakdown shows temporal vs periodic contributions ✅ Cleaner Architecture: One less special case in the codebase Test Status: The core functionality is working. Tests are passing for: - Bus models with and without penalties - Effect shares and constraints - Scenario weighting - Resample operations - I/O operations * Allow user defined enalty Effect * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md and docs * Fix penöaty test * Remove special treatment for Penalty effect in IO * Update CHANGELOG.md * Fix aggregation.py and its penalties * Fix indices * Revert some changes * Verify that Penalty is not set as the objective * Update docs a bit * Update docs a bit * Update docs a bit * Fix test * Update CHANGELOG.md * Improve main results penalty display * Update CHANGELOG.md * Merge branch 'main' into feature/penalty-as-effect # Conflicts: # flixopt/calculation.py # flixopt/clustering.py * Revert some minimla changes from merge
1 parent b591854 commit b7c2dc7

11 files changed

Lines changed: 261 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,32 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
5151
5252
## [Unreleased] - ????-??-??
5353
54-
**Summary**:
54+
**Summary**: Penalty is now a first-class Effect - add penalty contributions anywhere (e.g., `effects_per_flow_hour={'Penalty': 2.5}`) and optionally define bounds as with any other effect.
5555
5656
If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
5757
5858
### ✨ Added
5959
60+
- **Penalty as first-class Effect**: Users can now add Penalty contributions anywhere effects are used:
61+
```python
62+
fx.Flow('Q', 'Bus', effects_per_flow_hour={'Penalty': 2.5})
63+
fx.InvestParameters(..., effects_of_investment={'Penalty': 100})
64+
```
65+
- **User-definable Penalty**: Optionally define custom Penalty with constraints (auto-created if not defined):
66+
```python
67+
penalty = fx.Effect(fx.PENALTY_EFFECT_LABEL, unit='€', maximum_total=1e6)
68+
flow_system.add_elements(penalty)
69+
```
70+
6071
### 💥 Breaking Changes
6172
6273
### ♻️ Changed
6374
64-
### 🗑️ Deprecated
75+
- Penalty is now a standard Effect with temporal/periodic dimensions
76+
- Unified interface: Penalty uses same `add_share_to_effects()` as other effects (internal only)
77+
- **Results structure**: Penalty now has same structure as other effects in solution Dataset
78+
- Use `results.solution['Penalty']` for total penalty value (same as before, but now it's an effect variable)
79+
- Access components via `results.solution['Penalty(temporal)']` and `results.solution['Penalty(periodic)']` if needed
6580
6681
### 🔥 Removed
6782
@@ -73,9 +88,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
7388
7489
### 📝 Docs
7590
76-
### 👷 Development
77-
78-
### 🚧 Known Issues
91+
- Updated mathematical notation for Penalty as Effect
7992
8093
---
8194

docs/user-guide/mathematical-notation/dimensions.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Where:
114114
- $\mathcal{S}$ is the set of scenarios
115115
- $w_s$ is the weight for scenario $s$
116116
- The optimizer balances performance across scenarios according to their weights
117+
- **Both the objective effect and Penalty effect are weighted by $w_s$** (see [Penalty weighting](effects-penalty-objective.md#penalty))
117118

118119
### Period Independence
119120

@@ -130,6 +131,8 @@ $$
130131
\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \text{Objective}_y
131132
$$
132133

134+
Where **both the objective effect and Penalty effect are weighted by $w_y$** (see [Penalty weighting](effects-penalty-objective.md#penalty))
135+
133136
### Shared Periodic Decisions: The Exception
134137

135138
**Investment decisions (sizes) can be shared across all scenarios:**
@@ -203,16 +206,18 @@ $$
203206

204207
Where:
205208
- $\mathcal{T}$ is the set of time steps
206-
- $\mathcal{E}$ is the set of effects
209+
- $\mathcal{E}$ is the set of effects (including the Penalty effect $E_\Phi$)
207210
- $\mathcal{S}$ is the set of scenarios
208211
- $\mathcal{Y}$ is the set of periods
209212
- $s_{e}(\cdots)$ are the effect contributions (costs, emissions, etc.)
210213
- $w_s, w_y, w_{y,s}$ are the dimension weights
214+
- **Penalty effect is weighted identically to other effects**
211215

212216
**See [Effects, Penalty & Objective](effects-penalty-objective.md) for complete formulations including:**
213217
- How temporal and periodic effects expand with dimensions
214218
- Detailed objective function for each dimensional case
215219
- Periodic (investment) vs temporal (operational) effect handling
220+
- Explicit Penalty weighting formulations
216221

217222
---
218223

docs/user-guide/mathematical-notation/effects-penalty-objective.md

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -142,40 +142,86 @@ $$
142142

143143
## Penalty
144144

145-
In addition to user-defined [Effects](#effects), every FlixOpt model includes a **Penalty** term $\Phi$ to:
145+
Every FlixOpt model includes a special **Penalty Effect** $E_\Phi$ to:
146+
146147
- Prevent infeasible problems
147-
- Simplify troubleshooting by allowing constraint violations with high cost
148+
- Allow introducing a bias without influencing effects, simplifying results analysis
149+
150+
**Key Feature:** Penalty is implemented as a standard Effect (labeled `Penalty`), so you can **add penalty contributions anywhere effects are used**:
151+
152+
```python
153+
import flixopt as fx
154+
155+
# Add penalty contributions just like any other effect
156+
on_off = fx.OnOffParameters(
157+
effects_per_switch_on={'Penalty': 1} # Add bias against switching on this component, without adding costs
158+
)
159+
```
160+
161+
**Optionally Define Custom Penalty:**
162+
Users can define their own Penalty effect with custom properties (unit, constraints, etc.):
163+
164+
```python
165+
# Define custom penalty effect (must use fx.PENALTY_EFFECT_LABEL)
166+
custom_penalty = fx.Effect(
167+
fx.PENALTY_EFFECT_LABEL, # Always use this constant: 'Penalty'
168+
unit='',
169+
description='Penalty costs for constraint violations',
170+
maximum_total=1e6, # Limit total penalty for debugging
171+
)
172+
flow_system.add_elements(custom_penalty)
173+
```
174+
175+
If not user-defined, the Penalty effect is automatically created during modeling with default settings.
176+
177+
**Periodic penalty shares** (time-independent):
178+
$$ \label{eq:Penalty_periodic}
179+
E_{\Phi, \text{per}} = \sum_{l \in \mathcal{L}} s_{l \rightarrow \Phi,\text{per}}
180+
$$
148181

149-
Penalty shares originate from elements, similar to effect shares:
182+
**Temporal penalty shares** (time-dependent):
183+
$$ \label{eq:Penalty_temporal}
184+
E_{\Phi, \text{temp}}(\text{t}_{i}) = \sum_{l \in \mathcal{L}} s_{l \rightarrow \Phi, \text{temp}}(\text{t}_i)
185+
$$
150186

151-
$$ \label{eq:Penalty}
152-
\Phi = \sum_{l \in \mathcal{L}} \left( s_{l \rightarrow \Phi} +\sum_{\text{t}_i \in \mathcal{T}} s_{l \rightarrow \Phi}(\text{t}_{i}) \right)
187+
**Total penalty** (combining both domains):
188+
$$ \label{eq:Penalty_total}
189+
E_{\Phi} = E_{\Phi,\text{per}} + \sum_{\text{t}_i \in \mathcal{T}} E_{\Phi, \text{temp}}(\text{t}_{i})
153190
$$
154191

155192
Where:
156193

157194
- $\mathcal{L}$ is the set of all elements
158195
- $\mathcal{T}$ is the set of all timesteps
159-
- $s_{l \rightarrow \Phi}$ is the penalty share from element $l$
196+
- $s_{l \rightarrow \Phi, \text{per}}$ is the periodic penalty share from element $l$
197+
- $s_{l \rightarrow \Phi, \text{temp}}(\text{t}_i)$ is the temporal penalty share from element $l$ at timestep $\text{t}_i$
198+
199+
**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.
160200

161-
**Current usage:** Penalties primarily occur in [Buses](elements/Bus.md) via the `excess_penalty_per_flow_hour` parameter, which allows nodal imbalances at a high cost.
201+
**Key properties:**
202+
- Penalty shares are added via `add_share_to_effects(name, expressions={fx.PENALTY_EFFECT_LABEL: ...}, target='temporal'/'periodic')`
203+
- Like other effects, penalty can be constrained (e.g., `maximum_total` for debugging)
204+
- Results include breakdown: temporal, periodic, and total penalty contributions
205+
- Penalty is always added to the objective function (cannot be disabled)
206+
- Access via `flow_system.effects.penalty_effect` or `flow_system.effects[fx.PENALTY_EFFECT_LABEL]`
207+
- **Scenario weighting**: Penalty is weighted identically to the objective effect—see [Time + Scenario](#time--scenario) for details
162208

163209
---
164210

165211
## Objective Function
166212

167-
The optimization objective minimizes the chosen effect plus any penalties:
213+
The optimization objective minimizes the chosen effect plus the penalty effect:
168214

169215
$$ \label{eq:Objective}
170-
\min \left( E_{\Omega} + \Phi \right)
216+
\min \left( E_{\Omega} + E_{\Phi} \right)
171217
$$
172218

173219
Where:
174220

175221
- $E_{\Omega}$ is the chosen **objective effect** (see $\eqref{eq:Effect_Total}$)
176-
- $\Phi$ is the [penalty](#penalty) term
222+
- $E_{\Phi}$ is the [penalty effect](#penalty) (see $\eqref{eq:Penalty_total}$)
177223

178-
One effect must be designated as the objective via `is_objective=True`.
224+
One effect must be designated as the objective via `is_objective=True`. The penalty effect is automatically created and always added to the objective.
179225

180226
### Multi-Criteria Optimization
181227

@@ -198,70 +244,70 @@ When the FlowSystem includes **periods** and/or **scenarios** (see [Dimensions](
198244
### Time Only (Base Case)
199245

200246
$$
201-
\min \quad E_{\Omega} + \Phi = \sum_{\text{t}_i \in \mathcal{T}} E_{\Omega,\text{temp}}(\text{t}_i) + E_{\Omega,\text{per}} + \Phi
247+
\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)
202248
$$
203249

204250
Where:
205-
- Temporal effects sum over time: $\sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i)$
206-
- Periodic effects are constant: $E_{\Omega,\text{per}}$
207-
- Penalty sums over time: $\Phi = \sum_{\text{t}_i} \Phi(\text{t}_i)$
251+
- 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)$
252+
- Periodic effects are constant: $E_{\Omega,\text{per}}$ and $E_{\Phi,\text{per}}$
208253

209254
---
210255

211256
### Time + Scenario
212257

213258
$$
214-
\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( E_{\Omega}(s) + \Phi(s) \right)
259+
\min \quad \sum_{s \in \mathcal{S}} w_s \cdot \left( E_{\Omega}(s) + E_{\Phi}(s) \right)
215260
$$
216261

217262
Where:
218263
- $\mathcal{S}$ is the set of scenarios
219264
- $w_s$ is the weight for scenario $s$ (typically scenario probability)
220-
- Periodic effects are **shared across scenarios**: $E_{\Omega,\text{per}}$ (same for all $s$)
221-
- Temporal effects are **scenario-specific**: $E_{\Omega,\text{temp}}(s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, s)$
222-
- Penalties are **scenario-specific**: $\Phi(s) = \sum_{\text{t}_i} \Phi(\text{t}_i, s)$
265+
- Periodic effects are **shared across scenarios**: $E_{\Omega,\text{per}}$ and $E_{\Phi,\text{per}}$ (same for all $s$)
266+
- 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)$
223267

224268
**Interpretation:**
225269
- Investment decisions (periodic) made once, used across all scenarios
226270
- Operations (temporal) differ by scenario
227271
- Objective balances expected value across scenarios
272+
- **Both $E_{\Omega}$ (objective effect) and $E_{\Phi}$ (penalty) are weighted identically by $w_s$**
228273

229274
---
230275

231276
### Time + Period
232277

233278
$$
234-
\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( E_{\Omega}(y) + \Phi(y) \right)
279+
\min \quad \sum_{y \in \mathcal{Y}} w_y \cdot \left( E_{\Omega}(y) + E_{\Phi}(y) \right)
235280
$$
236281

237282
Where:
238283
- $\mathcal{Y}$ is the set of periods (e.g., years)
239284
- $w_y$ is the weight for period $y$ (typically annual discount factor)
240-
- Each period $y$ has **independent** periodic and temporal effects
285+
- Each period $y$ has **independent** periodic and temporal effects (including penalty)
241286
- Each period $y$ has **independent** investment and operational decisions
287+
- **Both $E_{\Omega}$ (objective effect) and $E_{\Phi}$ (penalty) are weighted identically by $w_y$**
242288

243289
---
244290

245291
### Time + Period + Scenario (Full Multi-Dimensional)
246292

247293
$$
248-
\min \quad \sum_{y \in \mathcal{Y}} \left[ w_y \cdot E_{\Omega,\text{per}}(y) + \sum_{s \in \mathcal{S}} w_{y,s} \cdot \left( E_{\Omega,\text{temp}}(y,s) + \Phi(y,s) \right) \right]
294+
\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]
249295
$$
250296

251297
Where:
252298
- $\mathcal{S}$ is the set of scenarios
253299
- $\mathcal{Y}$ is the set of periods
254300
- $w_y$ is the period weight (for periodic effects)
255301
- $w_{y,s}$ is the combined period-scenario weight (for temporal effects)
256-
- **Periodic effects** $E_{\Omega,\text{per}}(y)$ are period-specific but **scenario-independent**
257-
- **Temporal effects** $E_{\Omega,\text{temp}}(y,s) = \sum_{\text{t}_i} E_{\Omega,\text{temp}}(\text{t}_i, y, s)$ are **fully indexed**
258-
- **Penalties** $\Phi(y,s)$ are **fully indexed**
302+
- **Periodic effects** $E_{\Omega,\text{per}}(y)$ and $E_{\Phi,\text{per}}(y)$ are period-specific but **scenario-independent**
303+
- **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**
259304

260305
**Key Principle:**
261306
- Scenarios and periods are **operationally independent** (no energy/resource exchange)
262307
- Coupled **only through the weighted objective function**
263308
- **Periodic effects within a period are shared across all scenarios** (investment made once per period)
264309
- **Temporal effects are independent per scenario** (different operations under different conditions)
310+
- **Both $E_{\Omega}$ (objective effect) and $E_{\Phi}$ (penalty) use identical weighting** ($w_y$ for periodic, $w_{y,s}$ for temporal)
265311

266312
---
267313

@@ -274,7 +320,8 @@ Where:
274320
| **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 |
275321
| **Total periodic effect** | $E_{e,\text{per}}$ | Constant | $(y)$ when periods present |
276322
| **Total effect** | $E_e = E_{e,\text{per}} + E_{e,\text{temp},\text{tot}}$ | Combined | Depends on dimensions |
277-
| **Objective** | $\min(E_{\Omega} + \Phi)$ | With weights when multi-dimensional | See formulations above |
323+
| **Penalty effect** | $E_\Phi = E_{\Phi,\text{per}} + E_{\Phi,\text{temp},\text{tot}}$ | Combined (same as effects) | **Weighted identically to objective effect** |
324+
| **Objective** | $\min(E_{\Omega} + E_{\Phi})$ | With weights when multi-dimensional | See formulations above |
278325

279326
---
280327

flixopt/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
)
2929
from .config import CONFIG, change_logging_level
3030
from .core import TimeSeriesData
31-
from .effects import Effect
31+
from .effects import PENALTY_EFFECT_LABEL, Effect
3232
from .elements import Bus, Flow
3333
from .flow_system import FlowSystem
3434
from .interface import InvestParameters, OnOffParameters, Piece, Piecewise, PiecewiseConversion, PiecewiseEffects
@@ -43,6 +43,7 @@
4343
'Flow',
4444
'Bus',
4545
'Effect',
46+
'PENALTY_EFFECT_LABEL',
4647
'Source',
4748
'Sink',
4849
'SourceAndSink',

flixopt/clustering.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,16 @@ def do_modeling(self):
349349

350350
penalty = self.clustering_parameters.penalty_of_period_freedom
351351
if (self.clustering_parameters.percentage_of_period_freedom > 0) and penalty != 0:
352-
for variable in self.variables_direct.values():
353-
self._model.effects.add_share_to_penalty('Clustering', variable * penalty)
352+
from .effects import PENALTY_EFFECT_LABEL
353+
354+
for variable_name in self.variables_direct:
355+
variable = self.variables_direct[variable_name]
356+
# Sum correction variables over all dimensions to get periodic penalty contribution
357+
self._model.effects.add_share_to_effects(
358+
name='Aggregation',
359+
expressions={PENALTY_EFFECT_LABEL: (variable * penalty).sum('time')},
360+
target='periodic',
361+
)
354362

355363
def _equate_indices(self, variable: linopy.Variable, indices: tuple[np.ndarray, np.ndarray]) -> None:
356364
assert len(indices[0]) == len(indices[1]), 'The length of the indices must match!!'

0 commit comments

Comments
 (0)