PV, Storage, And Consumption With Heterogeneous Time Meshes
This example compares the same PV, storage, and flat-consumption system with three simulation time meshes:
- a normal hourly mesh;
- a coarse 2-hour mesh everywhere;
- a heterogeneous mesh with 1-hour day steps and 2-hour night steps.
The point is to show where a coarse mesh can be useful, but also how it can be misleading.
The normal hourly time mesh is used as a reference. A 2-hour mesh everywhere smooths the daylight operation and can change storage sizing. The heterogeneous mesh keeps hourly resolution during daylight and only coarsens the night, when there is no PV and the system is only working on storage.
We define a function that generates and solves a capacity expansion and dispatch problem, while taking the time mesh as argument. Then, we compare the results associated with the three meshes.
using Nosy
using DataFrames
using HiGHS
import JuMP: set_silent
normal_mesh = TimeMesh(fill(1//1, 8760))
coarse_mesh = TimeMesh(fill(2//1, 4380))
heterogeneous_day = vcat(fill(2//1, 2), fill(1//1, 18), [2//1])
heterogeneous_mesh = TimeMesh(repeat(heterogeneous_day, 365))
function solve_pv_storage_consumption_case(mesh)
s = Sim(Model(HiGHS.Optimizer); mesh=mesh)
set_silent(model(s))
power = EnergyCarrier("power", s)
snapshot = Snapshot(s)
grid = Node("grid", power, rule=:curtailed)
consumption = Component("consumption", Demand(power, 0.42))
connect!(snapshot, consumption, grid)
pv_day = [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.05, 0.20, 0.45, 0.70, 0.90, 1.00,
0.95, 0.80, 0.55, 0.30, 0.10, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
]
pv_profile = repeat(pv_day, 365)
pv = Component(
"PV",
ProfileSource(power, pv_profile),
[
VariableCapacity("output", energy),
FixedCost(:capex, "output", energy, 1.0),
],
)
connect!(snapshot, pv, grid)
storage = Component(
"storage",
BasicStorage(power, power, power, energy; eff_i=0.92),
[
VariableCapacity("input", energy),
FixedCost(:capex, "input", energy, 0.20),
Duration(4),
],
)
connect!(snapshot, storage, grid)
optimize!(snapshot, cost(snapshot))
variables = Nosy.nvariables(sim(snapshot))
constraints = Nosy.nconstraints(sim(snapshot))
result = extract(snapshot)
return (;
result,
variables,
constraints,
objective=cost(result),
pv=capacity(result, "PV"),
storage=capacity(result, "storage"),
)
end
normal = solve_pv_storage_consumption_case(normal_mesh);
coarse = solve_pv_storage_consumption_case(coarse_mesh);
heterogeneous = solve_pv_storage_consumption_case(heterogeneous_mesh);
nothingExpected results:
julia> DataFrame(
mesh=["hourly", "2-hour", "heterogeneous"],
pv_capacity=[normal.pv, coarse.pv, heterogeneous.pv],
storage_capacity=[normal.storage, coarse.storage, heterogeneous.storage],
cost=[normal.objective, coarse.objective, heterogeneous.objective],
variables=[normal.variables, coarse.variables, heterogeneous.variables],
)
3×5 DataFrame
Row │ mesh pv_capacity storage_capacity cost variables
│ String Float64 Float64 Float64 Int64
─────┼──────────────────────────────────────────────────────────────────
1 │ hourly 1.76842 1.50426 2.06927 26282
2 │ 2-hour 1.7697 1.40364 2.05042 13142
3 │ heterogeneous 1.76842 1.50426 2.06927 22997The 2-hour mesh is smaller, but it is not automatically a free win: here it also changes the storage expansion and the objective value. The heterogeneous mesh is more deliberate. It simplifies the problem by removing night-time steps where hourly PV detail is irrelevant, while preserving the same capacity expansion and cost as the hourly reference.