Skip to content

Negative Prices in Linearized Unit Commitment

Adapted from: PyPSA Examples – Negative Prices in Linearized Unit Commitment

import datetime as dt

import pandas as pd

import typsa
from typsa.components import Bus, Carrier, CommittableGenerator, Load
from typsa.time_variation import IntegerSnapshots, RangedSeries
network = typsa.Network(IntegerSnapshots(range(5), spacing=dt.timedelta(hours=1)))

network.add_components(Carrier(name="AC"))
network.add_components(bus := Bus(name="bus", carrier="AC"))
network.add_components(
    load := Load(
        name="load",
        bus=bus.name,
        p_set=RangedSeries(pd.Series([50, 120, 50, 20, 50])),
    )
)
base_generator_marginal_cost = 20
network.add_components(
    base_generator := CommittableGenerator(
        name="base",
        bus=bus.name,
        p_nom=100,
        marginal_cost=base_generator_marginal_cost,
        p_min_pu=0.4,
        start_up_cost=4000,
        shut_down_cost=2000,
    ),
    peak_generator := CommittableGenerator(
        name="peak",
        bus=bus.name,
        p_nom=50,
        marginal_cost=70,
        p_min_pu=0.2,
        start_up_cost=250,
    ),
)
optimized_network, _ = network.optimize(linearized_unit_commitment=True)
dispatch = optimized_network.dynamic_results.of_all_generators.p
status = optimized_network.dynamic_results.of_committable_generators.status
pd.DataFrame(
    {
        "Load (MW)": optimized_network.dynamic_results.of_all_loads.p[load.name],
        "Base Gen (MW)": dispatch[base_generator.name],
        "Peak Gen (MW)": dispatch[peak_generator.name],
        "Total Gen (MW)": dispatch.sum(axis="columns"),
        "Base Status": status[base_generator.name],
        "Peak Status": status[peak_generator.name],
        "Price (€/MWh)": optimized_network.dynamic_results.of_all_buses.marginal_price[
            bus.name
        ],
    }
).rename_axis("Time Period")
Load (MW) Base Gen (MW) Peak Gen (MW) Total Gen (MW) Base Status Peak Status Price (€/MWh)
Time Period
0 50.0 50.0 -0.0 50.0 1.0 0.0 20.0
1 120.0 100.0 20.0 120.0 1.0 0.4 75.0
2 50.0 50.0 -0.0 50.0 1.0 0.0 20.0
3 20.0 20.0 -0.0 20.0 0.5 0.0 -30.0
4 50.0 50.0 -0.0 50.0 0.5 0.0 20.0
periods_low = [2, 3, 4]

cycle_cost = base_generator.start_up_cost + base_generator.shut_down_cost

gen_low = float(dispatch.loc[periods_low, base_generator.name].sum())
op_cost = gen_low * base_generator_marginal_cost

print("Why stay online?")
print("=" * 25)
print(f"Start-up cost:        {base_generator.start_up_cost:,.0f} €")
print(f"Shut-down cost:       {base_generator.shut_down_cost:,.0f} €")
print(f"Total cycling cost:   {cycle_cost:,.0f}\n")

print(f"Output (snapshots 2-4): {gen_low:.1f} MWh")
print(f"Operational cost:       {op_cost:,.0f}\n")

decision = "Stay online" if op_cost < cycle_cost else "Cycle off/on"
savings = abs(cycle_cost - op_cost)

print(f"Decision: {decision} is cheaper.")
print(f"Savings vs alternative: {savings:,.0f} €")
Why stay online?
=========================
Start-up cost:        4,000 €
Shut-down cost:       2,000 €
Total cycling cost:   6,000 €

Output (snapshots 2-4): 120.0 MWh
Operational cost:       2,400 €

Decision: Stay online is cheaper.
Savings vs alternative: 3,600 €