Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1cf0b7b
Reorganize InvestmentParameters to always create the binary investmen…
FBumann Oct 6, 2025
c2c6d4c
Add new variable that indicates wether investment was taken, independ…
FBumann Oct 6, 2025
d004f46
Improve Handling of linked periods
FBumann Oct 7, 2025
efd961c
Improve Handling of linked periods
FBumann Oct 7, 2025
5b76192
Add examples
FBumann Oct 7, 2025
3f0cbee
Typos
FBumann Oct 8, 2025
f0e7bbe
Merge branch 'feature/v3/main' into feature/v3/feature/invest-v2
FBumann Oct 10, 2025
19ac9e7
Fix: reference invested only after it exists
FBumann Oct 10, 2025
21c26e7
Improve readbility of equation
FBumann Oct 10, 2025
63f7ac0
Update from Merge
FBumann Oct 10, 2025
2f0c6a5
Improve InvestmentModel
FBumann Oct 10, 2025
83ea171
Merge branch 'feature/v3/main' into feature/v3/feature/invest-v2
FBumann Oct 10, 2025
571ac44
Merge branch 'feature/v3/main' into feature/v3/feature/invest-v2
FBumann Oct 10, 2025
06fdbfc
Merge branch 'feature/v3/main' into feature/v3/feature/invest-v2
FBumann Oct 12, 2025
98440e7
Improve readability
FBumann Oct 12, 2025
491e283
Improve readability and reorder methods
FBumann Oct 12, 2025
9337113
Improve logging
FBumann Oct 12, 2025
f37c855
Improve InvestmentModel
FBumann Oct 12, 2025
3de3d15
Rename to "invested"
FBumann Oct 12, 2025
3fbcb55
Update CHANGELOG.md
FBumann Oct 12, 2025
97b9672
Bugfix
FBumann Oct 12, 2025
0a34b98
Improve docstring
FBumann Oct 12, 2025
7773324
Improve InvestmentModel to be more inline with the previous Version
FBumann Oct 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir
- Renamed class `SystemModel` to `FlowSystemModel`
- Renamed class `Model` to `Submodel`
- Renamed `mode` parameter in plotting methods to `style`
- Renamed investment binary variable `is_invested` to `invested` in `InvestmentModel`
- `Calculation.do_modeling()` now returns the `Calculation` object instead of its `linopy.Model`. Callers that previously accessed the linopy model directly should now use `calculation.do_modeling().model` instead of `calculation.do_modeling()`.

### ♻️ Changed
Expand Down
124 changes: 124 additions & 0 deletions examples/07_Investment_Periods/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Investment Periods Examples

This directory contains examples demonstrating the use of the **period dimension** and **linked_periods parameter** in flixopt's `InvestParameters`.

## Overview

The period dimension enables multi-period optimization, allowing you to model investment decisions across multiple time horizons (e.g., years 2020, 2025, 2030). The `linked_periods` parameter controls how investment decisions are connected across these periods.

## Examples

### 1. `investment_periods_example.py`

**Basic period-based investments**

Demonstrates:
- Using InvestParameters with the period dimension
- Period-specific investment costs (e.g., technology learning curves)
- Period-varying constraints (maximum sizes)
- Basic linked_periods usage with `(0, 1)` - linking all periods together

Key concepts:
- Investment costs that decrease over time
- Different maximum capacities per period
- Single investment decision shared across periods (linked_periods)

### 2. `linked_periods_advanced_example.py`

**Advanced linked_periods configurations**

Demonstrates:
- `linked_periods=None`: Independent investment per period
- `linked_periods=(0, 1)`: All periods fully linked (single decision)
- Custom linking patterns with arrays like `[1, 1, 2, 2, 3]`
- Practical use cases: phased rollouts, technology generations, upgrade cycles

Key concepts:
- Group-based linking (same group ID = linked periods)
- Sequential vs. persistent investments
- Technology replacement cycles
- Phased deployment strategies

## Understanding linked_periods

The `linked_periods` parameter accepts:

| Value | Behavior | Use Case |
|-------|----------|----------|
| `None` | Independent decision per period | Equipment that can be installed/removed between periods |
| `(0, 1)` | All periods linked (1D array) | Long-lived infrastructure (buildings, major equipment) |
| `[1,1,2,2,3]` | Custom groups | Phased deployments, technology generations, upgrade cycles |

### Custom Linking Examples

```python
# Example: Two technology generations
linked_periods=np.array([1, 1, 1, 2, 2]) # Periods 0-2 linked, periods 3-4 linked separately

# Example: Completely independent periods
linked_periods=np.array([1, 2, 3, 4, 5]) # Each period is its own group

# Example: Early commitment, later flexibility
linked_periods=np.array([1, 1, 2, 3, 4]) # First two linked, rest independent
```

## Running the Examples

```bash
# Basic period example
PYTHONPATH=/path/to/flixopt python examples/07_Investment_Periods/investment_periods_example.py

# Advanced linked_periods example
PYTHONPATH=/path/to/flixopt python examples/07_Investment_Periods/linked_periods_advanced_example.py
```

## Key Parameters in InvestParameters

```python
InvestParameters(
fixed_size=None, # Scalar or per-period numpy array
minimum_size=0, # Scalar or per-period numpy array
maximum_size=1000, # Scalar or per-period numpy array
optional=True,
fix_effects={'costs': 5000}, # Scalar or per-period numpy array/dict
specific_effects={ # Scalar or per-period numpy array/dict
'costs': np.array([1000, 900, 800]) # Decreasing costs per period
},
linked_periods=(0, 1), # None, (0,1), or numpy array
)
```

## Common Patterns

### Technology Learning Curve
```python
# Costs decrease over time due to technological improvements
specific_effects={'costs': np.array([1200, 1000, 800, 650, 500])}
linked_periods=None # Can invest at different times
```

### Long-lived Infrastructure
```python
# Single investment that persists across all periods
linked_periods=(0, 1) # All periods linked
```

### Phased Rollout
```python
# Two deployment phases
linked_periods=np.array([1, 1, 1, 2, 2]) # Phase 1: periods 0-2, Phase 2: periods 3-4
```

### Replacement Cycles
```python
# Equipment with 2-period lifetime, can be replaced
linked_periods=np.array([1, 1, 2, 2, 3]) # Gen 1, Gen 2, Gen 3
```

## Tips

1. **Start simple**: Use `linked_periods=(0, 1)` for most long-lived assets
2. **Model reality**: Match linking to actual equipment lifecycles
3. **Cost annualization**: Ensure investment costs are properly annualized to the period duration
4. **Check results**: Verify the `invested` binary variable to understand investment timing
5. **Solver settings**: Multi-period MIP problems may need longer solve times or relaxed MIP gaps
203 changes: 203 additions & 0 deletions examples/07_Investment_Periods/investment_periods_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"""
This example demonstrates how to use the period dimension with InvestParameters.

The period dimension allows modeling investment decisions across multiple time periods,
enabling multi-year planning and investment timing optimization.

This example shows:
1. Basic InvestParameters with periods - different investment costs per period
2. Using linked_periods to link investment decisions across periods
3. Period-specific investment constraints
"""

import numpy as np
import pandas as pd

import flixopt as fx

if __name__ == '__main__':
# --- Create Time Series Data ---
# Define timesteps for a single representative day
timesteps = pd.date_range('2020-01-01', periods=24, freq='h')

# Define multiple periods (e.g., years 2020, 2025, 2030)
periods = pd.Index([2020, 2025, 2030], name='period')

# Heat demand profile (kW) - same pattern for each period
heat_demand_per_h = np.array(
[30, 25, 20, 20, 25, 40, 60, 80, 90, 100, 95, 90, 85, 80, 75, 80, 85, 90, 80, 70, 60, 50, 40, 35]
)

# Power prices varying by period (€/kWh) - increasing over time
power_prices_per_period = np.array([0.08, 0.10, 0.12]) # 2020, 2025, 2030

# Create flow system with periods
flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods)

# --- Define Energy Buses ---
flow_system.add_elements(fx.Bus(label='Electricity'), fx.Bus(label='Heat'), fx.Bus(label='Gas'))

# --- Define Effects ---
costs = fx.Effect(
label='costs',
unit='€',
description='Total costs',
is_standard=True,
is_objective=True,
)

CO2 = fx.Effect(
label='CO2',
unit='kg',
description='CO2 emissions',
)

# --- Example 1: Basic Investment with Period-Specific Costs ---
# Solar panels with decreasing costs over time (technology learning curve)
solar_panels = fx.Source(
label='Solar',
outputs=[
fx.Flow(
label='P_solar',
bus='Electricity',
size=fx.InvestParameters(
minimum_size=0,
maximum_size=100, # kW
optional=True,
fix_effects={
'costs': np.array([10000, 8000, 6000]), # Fixed costs decrease over periods
},
specific_effects={
'costs': np.array([1200, 1000, 800]), # €/kW decreases due to technology improvement
'CO2': np.array([-500, -500, -500]), # Avoided emissions per kW (constant)
},
),
)
],
)

# --- Example 2: Investment with Linked Periods ---
# Battery storage - once invested in period 1, it's available in subsequent periods
# linked_periods controls this behavior
battery = fx.Storage(
label='Battery',
charging=fx.Flow('P_charge', bus='Electricity', size=50),
discharging=fx.Flow('P_discharge', bus='Electricity', size=50),
capacity_in_flow_hours=fx.InvestParameters(
minimum_size=10, # kWh
maximum_size=200,
optional=True,
fix_effects={
'costs': 5000, # Grid connection costs (same for all periods)
},
specific_effects={
'costs': np.array([800, 650, 500]), # €/kWh decreases over time
},
# linked_periods: Once invested in an early period, available in later periods
# This creates a binary investment variable that is shared across periods
linked_periods=(2020, 2029), # Links all periods together (1D array with single link group)
),
initial_charge_state=0,
eta_charge=0.95,
eta_discharge=0.95,
relative_loss_per_hour=0.001,
prevent_simultaneous_charge_and_discharge=True,
)

# --- Example 3: CHP with Period-Specific Maximum Size ---
# CHP can be expanded over time (different maximum in each period)
chp = fx.linear_converters.CHP(
label='CHP',
eta_th=0.5,
eta_el=0.4,
P_el=fx.Flow(
'P_el',
bus='Electricity',
size=fx.InvestParameters(
minimum_size=0,
maximum_size=np.array([50, 75, 100]), # Maximum capacity increases per period
optional=True,
fix_effects={
'costs': 15000,
},
specific_effects={
'costs': 1500, # €/kW
'CO2': 200, # kg CO2 per kW (lifecycle)
},
# No linked_periods - can invest independently in each period
),
),
Q_th=fx.Flow('Q_th', bus='Heat'),
Q_fu=fx.Flow('Q_fu', bus='Gas'),
)

# --- Supporting Components ---
# Heat demand
heat_sink = fx.Sink(
label='Heat Demand',
inputs=[fx.Flow(label='Q_th_demand', bus='Heat', size=1, fixed_relative_profile=heat_demand_per_h)],
)

# Gas source
gas_source = fx.Source(
label='Gas Supply',
outputs=[fx.Flow(label='Q_gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': 0.06, 'CO2': 0.2})],
)

# Grid electricity (with period-varying prices)
grid_import = fx.Source(
label='Grid Import',
outputs=[
fx.Flow(
label='P_import', bus='Electricity', size=200, effects_per_flow_hour={'costs': power_prices_per_period}
)
],
)

# Grid export
grid_export = fx.Sink(
label='Grid Export',
inputs=[
fx.Flow(
label='P_export',
bus='Electricity',
size=200,
effects_per_flow_hour={'costs': -0.9 * power_prices_per_period}, # 90% of import price
)
],
)

# --- Build Flow System ---
flow_system.add_elements(costs, CO2, solar_panels, battery, chp, heat_sink, gas_source, grid_import, grid_export)

# --- Visualize and Solve ---
flow_system.plot_network(show=True)

calculation = fx.FullCalculation(name='InvestmentPeriods', flow_system=flow_system)
calculation.do_modeling()
calculation.solve(fx.solvers.HighsSolver(mip_gap=0.01, time_limit_seconds=60))

# --- Analyze Results ---
# The investment decisions are automatically printed in the calculation summary above
print('\n=== Additional Analysis ===')

# Access investment variables directly from the solution
solar_var = 'Solar(P_solar)|size'
battery_var = 'Battery|size'
chp_var = 'CHP(P_el)|size'

if solar_var in calculation.results.solution.data_vars:
print(f'\nSolar capacity per period: {calculation.results.solution[solar_var].values}')

if battery_var in calculation.results.solution.data_vars:
print(f'\nBattery capacity (linked): {calculation.results.solution[battery_var].values}')

if chp_var in calculation.results.solution.data_vars:
print(f'\nCHP capacity per period: {calculation.results.solution[chp_var].values}')

# Plot results
calculation.results['Heat'].plot_node_balance()
calculation.results['Electricity'].plot_node_balance()

# Save results
calculation.results.to_file()
Loading