Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 11 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Release Notes

## Version 0.8.4 (2026-08-24)

### Bug fixes

* Fixed a bug in the tests of the checks of an `AbstractElectrolyzer`.
* Fixed a bug in the lifetime bounds of an `AbstractElectrolyzer`.
* The bug occured when
1. the sum of the durations of the operational periods were not equal to the parameter `op_per_strat` and
2. the stack lifetime was shorter than the parameter `op_per_strat`.
* Solving the bug resulted in changing the constraint for the upper bound of the lifetime with redundant strategic constraints when the stack lifetime is longer than the paramerer `op_per_strat` times the duration of a strategic period.

## Version 0.8.3 (2025-08-17)

### Bugfix
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "EnergyModelsHydrogen"
uuid = "44855f8b-b147-4985-ac18-48817d03c548"
authors = ["Julian Straus <Julian.Straus@sintef.no>, Avinash Subramanian"]
version = "0.8.3"
version = "0.8.4"

[deps]
EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50"
Expand Down
43 changes: 26 additions & 17 deletions docs/src/nodes/electrolyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ Hence, incorporating the potential for stack replacement and the associated cost
Stack replacement is cheaper than rebuilding a complete plant.
Furthermore, it results in an improved efficiency as it resets the degradation.

!!! note "Degradation and stack utilization calculations"
The degradation and stack utilization calculations do not include the multiplier (to scale between operational and strategic periods) within a given `SimpleTimes` structure or the duration of a strategic period.
We only consider the duration of the operational period.

As an example, consider the following time structure:

```julia
𝒯 = TwoLevel(1, 2, SimpleTimes(24, 1); 8760.0)
```

which corresponds to modelling a strategic period with a duration of 2 years and 24 operational periods with a duration of 1 h each.
In this situation, the maximum achievable degradation and stack utilization is given by the degradation after 24 hours of usage while theoretically it would be possible to have in the second year of the strategic period a total previous usage of 17 496 hours.
This is an optimistic treatment of the behavior while maintaining the degradation change within the given `SimpeTimes` structure.

However, when using representative periods, we do account for the multiplier for `SimpleTimes` structures as the core assumption is that the individual time structure within a representative period is repeated.

## [Introduced types and their fields](@id nodes-elec-fields)

Electrolysis is incorporated through two composite types with the same parameters.
Expand Down Expand Up @@ -71,7 +87,7 @@ The standard fields are given as:
The lower limit has to be non-negative while the upper limit has to be higher than the lower limit.
- **`degradation_rate::Real`**:\
The degradation rate is the reduction in efficiency of the electrolyser due to utilization.
It has to be provided as a percentage drop in efficiency in 1000 time the length of an operational duration (see *[Utilize `TimeStruct`](@extref EnergyModelsBase how_to-utilize_TS-struct-sp)* for an explanation).
It has to be provided as a percentage drop in efficiency in 1000 times the length of an operational duration (see *[Utilize `TimeStruct`](@extref EnergyModelsBase how_to-utilize_TS-struct-sp)* for an explanation).
If a duration of 1 in an operational period corresponds to an hour, then the unit is %/1000h.\
The degradation rate has to be given as ``[0, 1)``.
- **`stack_replacement_cost::TimeProfile`**:\
Expand Down Expand Up @@ -299,15 +315,22 @@ The calculation of the previous usage of the electrolyzer node requires the defi
The overall approach is similar to the calculation of the level constraints in `EnergyModelsBase`.
This is achieved through the function `constraints_usage()` and the individual functions calculated from the function.

Within this function, we first calculate ``\forall t_{inv, 1} \in T^{Inv},~ t_{inv, 2} \in T^{Inv}`` the linear reformulation of the product

First, the usage in each investment period ``t_{inv}`` is calculated:

```math
\texttt{elect\_use\_sp}[n_{el}, t_{inv}] \times 1000 = \sum_{t \in t_{inv}}\texttt{elect\_on\_b}[n_{el}, t]
\times scale\_op\_sp(t_{inv}, t)
```

In addition, we add the constraint on the stack lifetime.

```math
\begin{aligned}
stack\_&lifetime(n) / 1000 \geq \\ &
\texttt{elect\_prev\_use\_sp}[n_{el}, t]_{inv} + \texttt{elect\_use\_sp}[n_{el}, t_{inv}] \times duration\_strat(t_{inv})
\end{aligned}
```

The previous usage up the current investment period ``t_{inv}`` is calculated through the function `constraints_usage_sp`.
In the first investment period, the previous usage is fixed to a value of 0:

Expand Down Expand Up @@ -362,20 +385,6 @@ If the `TimeStructure` includes representative periods, then the usage in each r
\times scale\_op\_sp(t_{inv}, t)
```

In addition, if we are in the last operational period (of the last representative period) of an investment period, we calculate (for each operational scenario) the constraint

```math
\begin{aligned}
stack\_&lifetime(n) \geq \\ &
\texttt{elect\_prev\_use}[n_{el}, t] \times 1000 + \\ &
\texttt{elect\_use\_sp}[n_{el}, t_{inv}] \times (duration\_strat(t_{inv}) - 1) \times 1000 + \\
& \texttt{elect\_on\_b}[n_{el}, t] \times scale\_op\_sp(t_{inv}, t)
\end{aligned}
```

to avoid a violation of the lifetime constraint.
This constraint is only necessary for the last operational period as stack replacement is only allowed at the beginning of an investment period.

The declaration of the actual constraint for the previous usage can be differentiated in four individual cases:

1. In the first operational period (in the first representative period) in the first investment period:\
Expand Down
29 changes: 9 additions & 20 deletions src/constraints/electrolyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ degradation calculations.
function constraints_usage(m, n::AbstractElectrolyzer, 𝒯ᴵⁿᵛ, modeltype::EnergyModel)
# Mass/energy balance constraints for stored energy carrier.
for (t_inv_prev, t_inv) ∈ withprev(𝒯ᴵⁿᵛ)
# Calculation of hte usage within a strategic period
# Calculation of the usage within a strategic period
@constraint(m,
m[:elect_use_sp][n, t_inv] * 1000 ==
sum(m[:elect_on_b][n, t] * scale_op_sp(t_inv, t) for t ∈ t_inv)
)

# Upper constraint for the total usage of the electrolyzer
@constraint(m,
stack_lifetime(n)/1000 ≥
m[:elect_prev_use_sp][n, t_inv] +
m[:elect_use_sp][n, t_inv] * duration_strat(t_inv)
)

# Initialization of the periods
prev_pers = PreviousPeriods(t_inv_prev, nothing, nothing);
elec_pers = ElecPeriods(𝒯ᴵⁿᵛ, t_inv, nothing, true)

Expand Down Expand Up @@ -165,25 +173,6 @@ function constraints_usage_iterate(
_::SimpleTimes,
modeltype::EnergyModel,
)
# Constraint for the total usage of the electrolyzer including the current time step.
# This ensures that the last repetition of the strategic period is appropriately
# constrained.
# The conditional statement activates this constraint only for the last representative
# period, if representative periods are present as stack replacement is only feasible
# once per strategic period
if is_last(elec_pers)
t_inv = strat_per(elec_pers)
t = last(per)
@constraint(m,
stack_lifetime(n) ≥
(
m[:elect_prev_use][n, t] +
m[:elect_use_sp][n, t_inv]*(duration_strat(t_inv) - 1)
)
* 1000 + m[:elect_on_b][n, t] * scale_op_sp(t_inv, t)
)
end

# Iterate through the operational structure
for (t_prev, t) ∈ withprev(per)
prev_pers = PreviousPeriods(EMB.strat_per(prev_pers), EMB.rep_per(prev_pers), t_prev);
Expand Down
1 change: 0 additions & 1 deletion src/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ function EMB.create_node(m, n::AbstractElectrolyzer, 𝒯, 𝒫, modeltype::Ener
+ stack_replace[t_inv] * stack_replacement_cost(n, t_inv) / duration_strat(t_inv)
)


# Call of the functions for the variable OPEX constraint introduction
constraints_opex_var(m, n, 𝒯ᴵⁿᵛ, modeltype)
end
Expand Down
4 changes: 2 additions & 2 deletions test/test_checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ CO2 = ResourceEmit("CO2", 1.0)

# Function for setting up the system for testing an `AbstractElectrolyzer` node
function simple_graph_elec(;
cap = FixedProfile(-25), # Installed capacity [MW]
cap = FixedProfile(25), # Installed capacity [MW]
opex_var = FixedProfile(5), # Variable Opex
opex_fixed = FixedProfile(100), # Fixed Opex
input = Dict(Power => 1), # Input: Ratio of Input flows to characteristic throughput
Expand Down Expand Up @@ -76,7 +76,7 @@ end
@test_throws AssertionError simple_graph_elec(cap=FixedProfile(-25))

# Test that a wrong fixed OPEX is caught by the checks
@test_throws AssertionError simple_graph_elec(;opex_var=FixedProfile(5))
@test_throws AssertionError simple_graph_elec(;opex_fixed=FixedProfile(-5))

# Test that a wrong input dictionary is caught by the checks
@test_throws AssertionError simple_graph_elec(;input=Dict(Power => -1))
Expand Down
33 changes: 23 additions & 10 deletions test/test_electrolyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,20 @@ function penalty_test(m, case)
𝒯ᴵⁿᵛ = strategic_periods(𝒯)

# Reassign variables
penalty = m[:elect_efficiency_penalty][elec, :]
stack_replace = m[:elect_stack_replace_b][elec, :]
penalty = value.(m[:elect_efficiency_penalty][elec, :])
stack_replace = value.(m[:elect_stack_replace_b][elec, :])
prev_use = value.(m[:elect_prev_use][elec, :])

# Calculation of the penalty
@test all(
value.(penalty[t])value.(penalty[t_prev])
penalty[t] ⪅ penalty[t_prev]
for (t_prev, t) ∈ withprev(𝒯) if !isnothing(t_prev)
)
@test all(prev_use[t] ⪅ EMH.stack_lifetime(elec) for t ∈ 𝒯)
@test all(
value.(m[:elect_prev_use][elec, t]) ⪅ EMH.stack_lifetime(elec) for t ∈ 𝒯
)
@test all(
value.(penalty[t]) ≈
1 - EMH.degradation_rate(elec)/100 * value.(m[:elect_prev_use][elec, t])
isapprox(
penalty[t], 1 - EMH.degradation_rate(elec)/100 * prev_use[t], atol=1e-6
)
for t ∈ 𝒯)

# Test that the previous usage is correctly calculated
Expand All @@ -119,9 +119,16 @@ function penalty_test(m, case)
(
value.(m[:elect_prev_use_sp][elec, t_inv_prev]) +
value.(m[:elect_use_sp][elec, t_inv_prev]) * 2
) * (1 - value.(stack_replace[t_inv])),
) * (1 - stack_replace[t_inv]),
atol = TEST_ATOL)
for (t_inv_prev, t_inv) ∈ withprev(𝒯ᴵⁿᵛ) if !isnothing(t_inv_prev))

# Test that the stack lifetime constraint is not violated
@test all(
EMH.stack_lifetime(elec)/1000 ⪆
value.(m[:elect_prev_use_sp][elec, t_inv]) * (1-stack_replace[t_inv]) +
value.(m[:elect_use_sp][elec, t_inv]) * duration_strat(t_inv)
for t_inv ∈ 𝒯ᴵⁿᵛ)
end

# Testset for the individual extraction methods incorporated in the model
Expand Down Expand Up @@ -201,6 +208,12 @@ end
# Test that there are no quadratic constraints for SimpleElectrolyzer types
@test isempty(all_constraints(m, QuadExpr, MOI.EqualTo{MOI.Float64}))
finalize(backend(m).optimizer.model)

# Test for stack lifetimes shorter than op_per_strat
𝒯 = TwoLevel(8, 2, SimpleTimes(20, 1); op_per_strat=8760)
stack_lifetime = 8000
# Run and test the model
m, case, modeltype = elec_test_case(𝒯; stack_cost, deficit_cost, stack_lifetime)
end

# Test set for the used load limits allowing for both production above and below capacity
Expand Down Expand Up @@ -241,7 +254,7 @@ end
@testset "Investment extension test" begin
@testset "Without investment data" begin
# Specifying the input parameters
𝒯 = TwoLevel(8, 2, SimpleTimes(20, 8760/20); op_per_strat=8760)
𝒯 = TwoLevel(8, 2, SimpleTimes(20, 1); op_per_strat=8760)
deficit_cost = StrategicProfile([25, 25, 25, 25, 30])
stack_cost = FixedProfile(3e8)

Expand Down
Loading