Skip to content

Make or Buy

Problem Description

A site with a 100 MW CCGT at 50 $/MWh serves a constant 70 MW load over a day, and trades on a market whose price moves between 30 and 90 $/MWh. It can buy or sell up to 100 MW per step. The question is when to produce and when to buy, and whether to run the CCGT beyond the load to sell the rest.

%%{init: {"theme": "base", "themeVariables": {"lineColor": "#95A5A6"}}}%%
flowchart LR
    CCGT(["๐Ÿ”ฅ CCGT<br/>100 MW ยท 50 \$/MWh"]) --> Bus(["โšก Bus"])
    Market(["๐Ÿ“ˆ Market<br/>buy and sell ยท 100 MW<br/>30โ€“90 \$/MWh"]) <--> Bus
    Bus --> Load(["๐Ÿญ Load<br/>70 MW"])
    style CCGT fill:none
    style Market fill:none
    style Bus fill:none
    style Load fill:none

Source: examples/make_or_buy.py. The script wraps the steps below in run_make_or_buy(), which returns the results.

Walkthrough

1. Define the assets and the market

A generator, a fixed load, and a market. A market buys and sells unless you restrict its direction, and it isn't an asset, so it stays out of the portfolio:

from datetime import timedelta

from odys import (
    AssetPortfolio,
    EnergyMarket,
    EnergySystem,
    FixedLoad,
    Generator,
    LoadProfile,
    PriceProfile,
    Scenario,
)

ccgt = Generator(
    name="ccgt",
    nominal_power=100,
    variable_cost=50,
)
load = FixedLoad(
    name="load",
)

market = EnergyMarket(
    name="market",
    max_trading_volume_per_step=100,
)

portfolio = AssetPortfolio([ccgt, load])

2. Describe the day

The load is 70 MW throughout, and the market's price profile is low around midday and high in the morning and evening:

scenario = Scenario(
    profiles=(
        LoadProfile(load=load, values=24 * [70]),
        PriceProfile(
            market=market,
            values=[80, 75, 70, 65, 60, 55, 52, 45, 40, 35, 30, 35, 40, 45, 52, 55, 60, 70, 80, 90, 85, 80, 75, 70],
        ),
    ),
)

3. Solve

The market goes in markets, next to the portfolio:

energy_system = EnergySystem(
    portfolio=portfolio,
    markets=market,
    timestep=timedelta(hours=1),
    number_of_steps=24,
    scenarios=scenario,
)

result = energy_system.optimize()

Results

The top chart shows the CCGT's output and the market trades, with purchases as positive bars and sales as negative ones. The bottom chart shows the market price against the CCGT's 50 $/MWh:

At each step, the optimizer compares the price with the CCGT's cost:

  • Steps 0 to 6 and 14 to 23 (52 to 90 $/MWh): the market pays more than the CCGT costs, so the CCGT runs at its full 100 MW. It serves the 70 MW load and sells the other 30 MW.
  • Steps 7 to 13 (30 to 45 $/MWh): buying is cheaper than producing, so the CCGT is off and the site buys the whole 70 MW.

Over the day, the CCGT produces 1,700 MWh at a cost of 85,000. The site sells 510 MWh for 35,220 and buys 490 MWh for 18,900, so result.objective_value is โˆ’68,680. The site sells only 30 MW because the CCGT is capped at 100 MW; the market's 100 MW volume limit never binds. Selling earns 9,720 of that: without it, the site would only choose between producing and buying, at โˆ’78,400. The trades are in result.markets, where net_volume is negative when the site buys; see Results on the EnergyMarket page.

Discussion

A market sets a price on every MWh at every step, so a unit's cost is weighed against that price rather than against the load: the CCGT runs flat out whenever it earns more than it costs, whatever the site needs. Each step is still decided on its own. The Day-Ahead or Intraday example plans against several possible price futures, and weighs how bad the worst ones could be.