Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions templates/CHP.iesopt.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
parameters:
mode: ~ # operation mode: extraction, condensing, backpressure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mode: ~ # operation mode: extraction, condensing, backpressure
mode: ~ # operation mode: extraction (full characteristic field), condensing (power only), backpressure (acc. to power ratio line)

I would like that but maybe too crowded then.

fuel: ~ # fuel carrier
fuel_from: ~ # fuel input node
power_to: ~ # electricity output node
heat_to: ~ # condensing only if not given
co2_to: ~ # no CO2 tracking if not given
fuel_co2_emission_factor: ~ # CO2 emission factor of the fuel
power_max: ~ # maximum electricity output
heat_max: ~ # maximum heat output
power_at_heat_max: ~ # electricity output at maximum heat output
power_ratio: 0.55 # electricity per heat in backpressure operation
power_loss_ratio: 0.20 # electricity lost per heat extracted along an isofuel line
efficiency_condensing: ~ # electricity per fuel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
efficiency_condensing: ~ # electricity per fuel
efficiency_condensing: ~ # electricity per fuel (in condensing mode)

efficiency_backpressure: ~ # electricity+heat per fuel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this now the combined efficiency? maybe use that term?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
efficiency_backpressure: ~ # electricity+heat per fuel
efficiency_backpressure: ~ # electricity+heat per fuel (combined efficiency)

# TODO:
# power_min: 0
# heat_min: 0
# efficiency_condensing_min: ~
# efficiency_backpressure_min: ~
# availability: ~
# availability_factor: ~
# other missing parameters: ramping, specific unit commitment settings, etc.
_turbine_outputs: ~
_turbine_conversion: ~

components:
turbine:
type: Unit
inputs: {<fuel>: <fuel_from>}
outputs: <_turbine_outputs>
conversion: <_turbine_conversion>
capacity: <power_max> out:electricity

heat:
type: Unit
enabled: <mode> == extraction
inputs: {electricity: <self>.aux_node}
outputs: {heat: <heat_to>}
conversion: 1 electricity -> <power_loss_ratio> heat
capacity: <heat_max> out:heat

aux_node:
type: Node
enabled: <mode> == extraction
carrier: electricity

power:
type: Connection
enabled: <mode> == extraction
node_from: <self>.aux_node
node_to: <power_to>
lb: 0

# TODO: discuss that these functions get really hard to read/maintain when they grow larger (since in a YAML)
Copy link
Contributor

@Villyes Villyes Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, the confusing part is the lack of conversions at the units. Could we reference the lines as a comment or is this stupid?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much overhead is it to include individual units (e.g., one additional one for backpressure because the modeling via combined efficiency is different anyhow?) Then we could also have the backpressure calculation with the extraction-calculation-method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much overhead is it to include individual units (e.g., one additional one for backpressure because the modeling via combined efficiency is different anyhow?) Then we could also have the backpressure calculation with the extraction-calculation-method.

We'll do that sounds helpful!

# should we just default to addons?

functions:
validate: |
# TODO: A lot of additional validation checks.

# Check mandatory parameters.
@check !isnothing(this.get("mode"))
@check !isnothing(this.get("fuel"))
@check !isnothing(this.get("fuel_from"))
@check !isnothing(this.get("power_to"))

# Check mode.
@check this.get("mode") in ["extraction", "condensing", "backpressure"]

# Check for non-negativity.
@check isnothing(this.get("power_max")) || (this.get("power_max") >= 0)
@check isnothing(this.get("heat_max")) || (this.get("heat_max") >= 0)
@check isnothing(this.get("power_at_heat_max")) || (this.get("power_at_heat_max") >= 0)
@check isnothing(this.get("power_ratio")) || (this.get("power_ratio") >= 0)
@check isnothing(this.get("power_loss_ratio")) || (this.get("power_loss_ratio") >= 0)
@check isnothing(this.get("efficiency_condensing")) || (this.get("efficiency_condensing") >= 0)
@check isnothing(this.get("efficiency_backpressure")) || (this.get("efficiency_backpressure") >= 0)

# Mode dependent checks.
if this.get("mode") == "backpressure"
@check !isnothing(this.get("heat_to"))
@check !isnothing(this.get("efficiency_backpressure"))
elseif this.get("mode") == "condensing"
@check isnothing(this.get("heat_to"))
@check !isnothing(this.get("efficiency_condensing"))
elseif this.get("mode") == "extraction"
@check !isnothing(this.get("heat_to"))
@check !isnothing(this.get("efficiency_condensing")) || !isnothing(this.get("efficiency_backpressure"))
end

prepare: |
cm = this.get("power_ratio")
cv = this.get("power_loss_ratio")

fuel = this.get("fuel")
to_p = this.get("power_to")
to_co2 = this.get("co2_to")

if this.get("mode") == "backpressure"
to_h = this.get("heat_to")

# -------------------------------------------------
# Prepare: power_max, heat_max, power_ratio
# -------------------------------------------------
p_max = this.get("power_max")
h_max = this.get("heat_max")
cm = this.get("power_ratio")

if isnothing(p_max)
power_max = cm * h_max
this.set("power_max", power_max)
end

if isnothing(h_max)
# Currently, nothing to do here (`h_max` is not actively used in backpressure mode).
end

if isnothing(cm)
cm = p_max / h_max
this.set("power_ratio", cm)
end
Comment on lines +119 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being super picky (or will you prevent the input-combination 'cm+h_max+p_at_h_max' in the validate section):

Suggested change
if isnothing(cm)
cm = p_max / h_max
this.set("power_ratio", cm)
end
p_at_h_max= this.get("power_at_heat_max")
if isnothing(p_at_h_max)
p_at_h_max=p_max
end
if isnothing(cm)
cm = p_at_h_max / h_max
this.set("power_ratio", cm)
end

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion => we do not want want to allow over-specification, therefore we check for that in the validate, and then do not need to care about any over-specified combinations.

# -------------------------------------------------

# -------------------------------------------------
# Prepare: eta_p, eta_h
# -------------------------------------------------
eta = this.get("efficiency_backpressure")
eta_h = eta / (1.0 + cm)
eta_p = cm * eta_h
# -------------------------------------------------

this.set("_turbine_outputs", "{electricity: $(to_p), heat: $(to_h)}")
this.set("_turbine_conversion", "1 $(fuel) -> $(eta_p) electricity + $(eta_h) heat")
elseif this.get("mode") == "condensing"
eta_p = this.get("efficiency_condensing")
this.set("_turbine_outputs", "{electricity: $(to_p)}")
this.set("_turbine_conversion", "1 $(fuel) -> $(eta_p) electricity")
elseif this.get("mode") == "extraction"
# -------------------------------------------------
# Prepare: power_max, heat_max, power_ratio, power_loss_ratio
# -------------------------------------------------
p_max = this.get("power_max")
h_max = this.get("heat_max")
p_at_h_max = this.get("power_at_heat_max")
cm = this.get("power_ratio")
cv = this.get("power_loss_ratio")

if isnothing(p_max)
Copy link
Contributor

@Villyes Villyes Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusing to me. Isn´t p_max one of the required inputs. Why wouldn´t you have this one.

p_max = isnothing(p_at_h_max) ? (cm * h_max + cv * h_max) : (p_at_h_max + cv * h_max)
this.set("power_max", p_max)
end

if isnothing(h_max)
h_max = isnothing(p_at_h_max) ? (p_max / (cm + cv)) : (p_at_h_max / cm)
this.set("heat_max", h_max)
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if isnothing(p_at_h_max)
p_at_h_max=p_max
end

if isnothing(cm)
cm = p_at_h_max / h_max
this.set("power_ratio", cm)
end

if isnothing(cv)
cv = (p_max - p_at_h_max) / h_max
this.set("power_loss_ratio", cv)
end
# -------------------------------------------------

# -------------------------------------------------
# Prepare: eta_p
# -------------------------------------------------
eta_p = nothing
eta_cd = this.get("efficiency_condensing")
eta_bp = this.get("efficiency_backpressure")

if !isnothing(eta_cd) && isnothing(eta_bp)
eta_p = eta_cd
elseif isnothing(eta_cd) && !isnothing(eta_bp)
eta_p = eta_bp / (1.0 + cm)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check. I arrive at:

Suggested change
eta_p = eta_bp / (1.0 + cm)
eta_p = eta_bp * cm / (1.0 + cm)

elseif !isnothing(eta_cd) && !isnothing(eta_bp)
eta_p = eta_cd

@check isapprox(eta_cd, eta_bp / (1.0 + cm))
if !isapprox(eta_cd, eta_bp / (1.0 + cm))
@error "Falling back to condensing efficiency, to keep model running, but this error should be fixed!"
end
end
# -------------------------------------------------

aux_node = "$(this.get("self")).aux_node"
this.set("_turbine_outputs", "{electricity: $(aux_node)}")
this.set("_turbine_conversion", "1 $(fuel) -> $(eta_p) electricity")
end

if !isnothing(to_co2)
t_out = this.get("_turbine_outputs")
t_conv = this.get("_turbine_conversion")
ef = this.get("fuel_co2_emission_factor")

this.set("_turbine_outputs", "$(t_out[1:end-1]), co2: $(to_co2)}")
this.set("_turbine_conversion", "$(t_conv) + $(ef) co2")
end

finalize: |
if this.get("mode") == "backpressure"
this.exp.out_electricity = this.turbine.exp.out_electricity
this.exp.out_heat = this.turbine.exp.out_heat
elseif this.get("mode") == "condensing"
this.exp.out_electricity = this.turbine.exp.out_electricity
elseif this.get("mode") == "extraction"
this.exp.out_electricity = this.power.exp.out
this.exp.out_heat = this.heat.exp.out_heat

# Construct the lower-bound / backpressure constraint.
JuMP.@constraint(this.model, this.exp.out_electricity .>= this.get("power_ratio") .* this.exp.out_heat)
end

if !isnothing(this.get("co2_to"))
this.exp.out_co2 = this.turbine.exp.out_co2
end