Combined Heat And Power
This example adds a combined heat and power (CHP) unit to a system with power and heat demand. The CHP is a dispatchable heat source with free electricity co-production. A VariableComposedCapacity constrains heat and electricity under one optimised capacity, with weights 1 for heat and 3 for electricity. A linked joint flow represents gas input and carries the fuel cost. The power node is curtailed, so excess CHP or PV generation can be spilled.
using Nosy
using HiGHS
import JuMP: set_silent
# Generate a simulation and carriers.
s = Sim(Model(HiGHS.Optimizer); mesh=TimeMesh())
set_silent(model(s))
elec_carrier = EnergyCarrier("power", s)
heat_carrier = EnergyCarrier("heat", s)
gas_carrier = EnergyCarrier("gas", s)
# Synthetic data for power demand, heat demand, and PV.
hours = 1:8760
day_angle = 2pi .* ((hours .- 1) .% 24) ./ 24
season_angle = 2pi .* (hours .- 1) ./ 8760
power_load = 2400 .+ 700 .* sin.(day_angle .- pi / 2) .+
120 .* sin.(season_angle .- pi / 2)
heat_load = 900 .+ 550 .* sin.(season_angle .- pi / 2) .+
120 .* sin.(day_angle .- pi / 2)
cf_pv = [
x < 1e-6 ? 0.0 : x for x in [
max(0, cos((h % 24 - 12) / 12 * pi) * (0.6 + 0.4 * sin(2pi * (h / 24) / 365)))
for h in 1:8760
]
]
# Snapshot initialisation.
snapshot = Snapshot(s)
# One curtailed electricity node and one balanced heat node.
grid = Node("grid", elec_carrier, rule=:curtailed, evalprice=true)
heat_node = Node("heat", heat_carrier)
# Components: power and heat demand.
power_demand = Component("power demand", Demand(elec_carrier, power_load))
connect!(snapshot, power_demand, grid)
heat_demand = Component("heat demand", Demand(heat_carrier, heat_load))
connect!(snapshot, heat_demand, heat_node)
# Component: PV.
pv = Component(
"PV",
ProfileSource(elec_carrier, cf_pv),
[
VariableCapacity("output", energy),
FixedCost(:capex, "output", energy, 45_000.0),
],
)
connect!(snapshot, pv, grid)
# Component: battery storage.
battery = Component(
"battery",
BasicStorage(elec_carrier, elec_carrier, elec_carrier, energy; eff_i=0.85),
[
VariableCapacity("input", energy),
FixedCost(:capex, "input", energy, 45_000.0),
Duration(4),
],
)
connect!(snapshot, battery, grid)
# Component: combined heat and power (CHP) with linked gas input.
chp = Component(
"CHP",
DispatchableSource(heat_carrier),
[
FreeJointFlow("power", elec_carrier, :output),
# input gas as a joint flow of heat and power: gas = 0.9 * (heat + 1/0.3 * power)
# this flow will not be connected to a node
LinkedJointFlow(
"gas",
gas_carrier,
:input,
("output", "power"),
x -> 0.9 * (x[1] .+ 1/0.3 * x[2]);
modifier=energy,
mustconnect=false,
),
VariableComposedCapacity(["output", "power"], energy; weights=[1.0, 3.0]),
FixedCost(:capex, "output", energy, 35_000.0),
VariableCost(:fuel, "gas", energy, 55.0),
],
)
connect!(snapshot, chp, heat_node, "output")
connect!(snapshot, chp, grid, "power")
# Optimisation
optimize!(snapshot, cost(snapshot))
result = extract(snapshot)Expected results:
julia> table(result, capacity)
1×5 DataFrame
Row │ CHP PV battery heat demand power demand
│ Float64 Float64 Float64 Float64 Float64
─────┼──────────────────────────────────────────────────────
1 │ 4485.28 19903.5 6464.37 0.0 0.0
julia> cost(result)
2.135348563904119e9
julia> chp_output = balance(result, "CHP", :output, energy; collapse=true, aggregate=false);
julia> chp_output["output"] # heat
7.883999999999987e6
julia> chp_output["power"]
2.4336509431618745e6
julia> balance(result, "CHP", :input, energy; collapse=true, aggregate=true)
1.4396552829485629e7The heat output is exactly the annual heat demand. The CHP capacity is the maximum value of heat + 3 * power, while the gas input is the flow that receives the :fuel cost.