Battery Storage

Flat demand with variable PV leaves evenings short of solar. With a battery makebatterystorage shifts daytime surplus forward on the same electricity node so less OCGT generation is needed in the evening. An OCGT plant covers hours the battery cannot reach. Capacities are fixed.

using Posy2
using Nosy
using HiGHS
import JuMP: set_silent

# Simulation and Posy2 input configuration
sim = Sim(Model(HiGHS.Optimizer); mesh=TimeMesh())
set_silent(model(sim))
example_data_dir = joinpath(pkgdir(Posy2), "data")
snapshot = Snapshot(sim, Dict(:posy => Posy2Options(
    data_dir=example_data_dir,
    techdata_file="tech_data.xlsx",
    timeseries_file="time_series.xlsx",
    tech_mode=:arguments,
    timeseries_mode=:excel,
)))

# Electricity node and CO2 sink
electricity = Node("country1", EnergyCarrier("electricity country1", sim), rule=:curtailed, tags=[:electricity])
co2 = Node("CO2", CO2Carrier("CO2", sim), rule=:curtailed, tags=[:co2])

# Flat 50 MW demand so evenings short of solar come from the PV shape alone
makedemand("Demand", "country1", electricity, snapshot; profile_multiplier=0.0, annual_flat_demand=50.0 * 8760)

# Fixed 100 MW PV
makeintermittentsource(
    "Solar", electricity, snapshot; co2_node=co2, tech_column="PV",
    cap=100.0,
    weather_year=2019,
    om_var_cost=15.0,
)

# Fixed battery: 50 MW power rating, 85% round-trip, 4 h energy duration
makebatterystorage(
    "Battery", electricity, snapshot; tech_column="Battery",
    power_cap=50.0,
    roundtrip_eff=0.85,
    duration=4.0,
)

# Fixed 50 MW OCGT backup for hours storage cannot cover
makedispatchable(
    "OCGT", electricity, snapshot; co2_node=co2, tech_column="OCGT",
    cap=50.0,
    fuel_cost=68.24,
)

# Minimise total system cost and extract solved values
optimize!(snapshot, cost(snapshot))
result = extract(snapshot)

Annual battery charge (:input) and discharge (:output) recover the stated 85% round-trip efficiency. The gap is storage loss. OCGT still covers most of the year because the battery only shifts a slice of the daytime surplus.

julia> charge = balance(result, "Battery country1", :input, energy; collapse=true, aggregate=true)
12021.6263

julia> discharge = balance(result, "Battery country1", :output, energy; collapse=true, aggregate=true)
10218.382355000003

julia> discharge / charge
0.8500000000000003

Write the solved result with write_results to open the workbook report:

julia> write_results(result, "results/battery-storage.xlsx", overwrite=true)
"results/battery-storage.xlsx"

On the Time series sheet look at the rows where Hour is 4065–4077 (GW / GWh). Daytime PV surplus charges the battery and the level rises. After the level peaks the battery discharges as solar falls. OCGT takes over again when discharge stops:

HourSolar country1charging Battery country1level Battery country1discharging Battery country1OCGT country1
40650.05796480.0079648000
40660.07265360.02265360.0130128200
40670.08290820.03290820.03662658500
40680.08830650.03830650.066892832500
40690.08830650.03830650.099453357500
40700.08290820.03290820.12971960500
40710.07121130.02121130.152720392500
40720.05134380.00134380.1623063100
40730.037684700.16287742500.0123153
40740.023251400.16027775750.0051993350.021549265
40750.00924700.137301590.0407530
40760.000306600.092078390.04969340
4077000.0672316900.05