Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new resource type: CCS with solvent storage #795

Open
wants to merge 19 commits into
base: solvent-storage
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
65e25d7
Add new charging capacity constraint to VRE_STOR (#770)
qluo0320github Nov 18, 2024
6fabc96
Additional constraints to prevent violation of state of charge limits…
federicoparolin Nov 18, 2024
7147493
Fix getproperty for `Vector{Resources}` to align with updated array i…
lbonaldo Nov 18, 2024
f1bbba4
Fix description of TDR setting parameters in Tutorial 1 (#793)
lbonaldo Nov 19, 2024
5b0c5ff
Remove unused `CapRes_*` columns from `Network.csv` (784)
lbonaldo Nov 26, 2024
d201184
Fix writing cost for zones with no resources (#796)
lbonaldo Dec 3, 2024
4eb44e8
Fix demand_data path in tutorial 8 (#794)
lbonaldo Dec 3, 2024
165836d
Add `add_similar_to_expression!` for arrays of `Number`s (#798)
lbonaldo Dec 4, 2024
faaf58a
Update README.md and doc with development lead from Binghamton Univ (…
sambuddhac Dec 4, 2024
012a28d
Fix typos in documentation (#768)
sambuddhac Dec 4, 2024
cccab41
Settings flag for new LDS constraints (#801)
lbonaldo Dec 23, 2024
74650dd
Add instructions for skipping precompilation
lbonaldo Dec 23, 2024
e008d1f
Add GH action to automatically update version number
lbonaldo Nov 7, 2024
c2ff871
Update GH workflows
lbonaldo Nov 7, 2024
5e3d0be
Add `eTotalCMaxCapSlack` to `cUnmetPolicyPenalty` in `write_costs.jl`…
lbonaldo Jan 6, 2025
9f82165
Prep for v0.4.2
lbonaldo Dec 23, 2024
26d82e8
Merge pull request #808 from GenXProject/release/0.4.2
lbonaldo Jan 6, 2025
e0e3dca
Fix path to MGA reference in documentation (#813)
lbonaldo Jan 17, 2025
4daa001
Update `write_rsv.jl` with corrected filenames for RSV (#814)
sambuddhac Jan 23, 2025
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fusion plant optional features for thermal plants (#743).
- Support for reusing the same Gurobi environment for multiple solves when
number of concurrent Gurobi uses is limited (#783).
- Additional long-duration storage constraints to bound state of charge in
non-representative periods (#781).

### Changed
- The `charge.csv` and `storage.csv` files now include only resources with
charge and storage variables (#760 and #763).
- Deduplicated docs on optimized scheduled maintenance for thermal resources (#745).

### Fixed
- Add constraint to ensure that electricity charged from the grid cannot exceed
the charging capacity of the storage component in VRE_STOR (#770).
- Update `getproperty` function for vectors of resources to ensure compatibility
with Julia v1.11 (#785).

## [0.4.1] - 2024-08-20

### Added
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 = "GenX"
uuid = "5d317b1e-30ec-4ed6-a8ce-8d2d88d7cfac"
authors = ["Bonaldo, Luca", "Chakrabarti, Sambuddha", "Cheng, Fangwei", "Ding, Yifu", "Jenkins, Jesse D.", "Luo, Qian", "Macdonald, Ruaridh", "Mallapragada, Dharik", "Manocha, Aneesha", "Mantegna, Gabe ", "Morris, Jack", "Patankar, Neha", "Pecci, Filippo", "Schwartz, Aaron", "Schwartz, Jacob", "Schivley, Greg", "Sepulveda, Nestor", "Xu, Qingyu", "Zhou, Justin"]
version = "0.4.1-dev.9"
version = "0.4.1-dev.13"

[deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/Tutorials/Tutorial_1_configuring_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ GenX is easy to customize to fit a variety of problems. In this tutorial, we sho
There are 21 settings available to edit in GenX, found in the file `genx_settings.yml`. These settings are described at the [Model settings parameters
](@ref) page of the documentation. The file is located in the `settings` folder in the working directory. To change the location of the file, edit the `settings_path` variable in `Run.jl` within your directory.

Most settings are set as either 0 or 1, which correspond to whether or not to include a specific feature. For example, to use `TimeDomainReduction`, you would set its parameter to 0 within `genx_settings.yml`. If you would like to run GenX without it, you would set its parameter to 1.
Most settings are set as either 0 or 1, which correspond to whether or not to include a specific feature. For example, to use `TimeDomainReduction`, you would set its parameter to 1 within `genx_settings.yml`. If you would like to run GenX without it, you would set its parameter to 0.

Other settings, such as `CO2Cap`, have more options corresponding to integers, while some settings such as `ModelingtoGenerateAlternativeSlack` take a numerical input directly (in this case, the slack value). Two settings, `Solver` and `TimeDomainReductionFolder` take in text as input. To learn more about different solvers, read [here](https://github.com/GenXProject/GenX.jl/blob/main/docs/src/User_Guide/solver_configuration.md). For `TimeDomainReductionFolder`, specify the name of the directory you wish to see the results in. For a more comprehensive description of the input options, see the documentation linked above.

Expand Down
1 change: 1 addition & 0 deletions src/case_runners/case_runner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ run_genx_case!("path/to/case", Gurobi.Optimizer)
```
"""
function run_genx_case!(case::AbstractString, optimizer::Any = HiGHS.Optimizer)
print_genx_version() # Log the GenX version
genx_settings = get_settings_path(case, "genx_settings.yml") # Settings YAML file path
writeoutput_settings = get_settings_path(case, "output_settings.yml") # Write-output settings YAML file path
mysetup = configure_settings(genx_settings, writeoutput_settings) # mysetup dictionary stores settings and GenX-specific parameters
Expand Down
4 changes: 3 additions & 1 deletion src/load_inputs/load_resources_data.jl
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,9 @@ function check_retrofit_id(rs::Vector{T}) where {T <: AbstractResource}
retrofit_options = ids_retrofit_options(rs)

# check that all retrofit_ids for resources that can retrofit and retrofit options match
if Set(rs[units_can_retrofit].retrofit_id) != Set(rs[retrofit_options].retrofit_id)
can_retrofit_retro_ids = [r.retrofit_id for r in rs[units_can_retrofit]] # retrofit cluster ids for units that can retrofit
retrofit_option_retro_ids = [r.retrofit_id for r in rs[retrofit_options]] # retrofit cluster ids for retrofit options
if Set(can_retrofit_retro_ids) != Set(retrofit_option_retro_ids)
msg = string("Retrofit IDs for resources that \"can retrofit\" and \"retrofit options\" do not match.\n" *
"All retrofitting units must be associated with a retrofit option.")
push!(warning_strings, msg)
Expand Down
66 changes: 66 additions & 0 deletions src/model/resources/hydro/hydro_inter_period_linkage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,43 @@ Finally, the next constraint enforces that the initial storage level for each in
\quad \forall n \in \mathcal{N}, o \in \mathcal{O}^{LDES}
\end{aligned}
```

**Bound storage inventory in non-representative periods**
We need additional variables and constraints to ensure that the storage content is within zero and the installed energy capacity in non-representative periods. We introduce
the variables $\Delta Q^{max,pos}_{o,z,m}$ and $\Delta Q^{max,neg}_{o,z,m}$ that represent the maximum positive and negative storage content variations within the representative
period $m$, respectively, extracted as:

```math
\begin{aligned}
& \Delta Q^{max,pos}_{o,z,m} \geq \Gamma_{o,z,(m-1)\times \tau^{period}+t } - \Gamma_{o,z,(m-1)\times \tau^{period}+1 } \quad \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, m \in \mathcal{M}, t \in \mathcal{T}
& \end{aligned}
```

```math
\begin{aligned}
& \Delta Q^{max,neg}_{o,z,m} \leq \Gamma_{o,z,(m-1)\times \tau^{period}+t } - \Gamma_{o,z,(m-1)\times \tau^{period}+1 } \quad \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, m \in \mathcal{M}, t \in \mathcal{T}
& \end{aligned}
```

For every input period $n \in \mathcal{N}$, the maximum storage is computed and constrained to be less than or equal to the installed energy capacity as:

```math
\begin{aligned}
& Q_{o,z,n} \times \left(1-\eta_{o,z}^{loss}\right) - \frac{1}{\eta_{o,z}^{discharge}}\Theta_{o,z,(m-1)\times \tau^{period}+1} + \eta_{o,z}^{charge}\Pi_{o,z,(m-1)\times \tau^{period}+1} + \Delta Q^{max,pos}_{o,z,f(n)} \leq \Delta^{total, energy}_{o,z} \\
& \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, n \in \mathcal{N}
& \end{aligned}
```

Similarly, the minimum storage content is imposed to be positive in every period of the time horizon:

```math
\begin{aligned}
& Q_{o,z,n} \times \left(1-\eta_{o,z}^{loss}\right) - \frac{1}{\eta_{o,z}^{discharge}}\Theta_{o,z,(m-1)\times \tau^{period}+1} + \eta_{o,z}^{charge}\Pi_{o,z,(m-1)\times \tau^{period}+1} + \Delta Q^{max,neg}_{o,z,f(n)} \geq 0 \\
& \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, n \in \mathcal{N}
& \end{aligned}
```

Additional details on this approach are available in [Parolin et al., 2024](https://doi.org/10.48550/arXiv.2409.19079).
"""
function hydro_inter_period_linkage!(EP::Model, inputs::Dict)
println("Long Duration Storage Module for Hydro Reservoir")
Expand Down Expand Up @@ -71,6 +108,12 @@ function hydro_inter_period_linkage!(EP::Model, inputs::Dict)
# Build up inventory can be positive or negative
@variable(EP, vdSOC_HYDRO[y in STOR_HYDRO_LONG_DURATION, w = 1:REP_PERIOD])

# Maximum positive storage inventory change within subperiod
@variable(EP, vdSOC_maxPos_HYDRO[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD] >= 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@variable(EP, vdSOC_maxPos_HYDRO[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD] >= 0)
@variable(EP, vdSOC_maxPos_HYDRO[y in STOR_HYDRO_LONG_DURATION, w = 1:REP_PERIOD]>=0)


# Maximum negative storage inventory change within subperiod
@variable(EP, vdSOC_maxNeg_HYDRO[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD] <= 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
# Maximum negative storage inventory change within subperiod
@variable(EP, vdSOC_maxNeg_HYDRO[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD] <= 0)
# Maximum negative storage inventory change within subperiod
@variable(EP, vdSOC_maxNeg_HYDRO[y in STOR_HYDRO_LONG_DURATION, w = 1:REP_PERIOD]<=0)


### Constraints ###

# Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position
Expand Down Expand Up @@ -111,4 +154,27 @@ function hydro_inter_period_linkage!(EP::Model, inputs::Dict)
r in REP_PERIODS_INDEX],
vSOC_HYDROw[y,r]==EP[:vS_HYDRO][y, hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] -
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
vSOC_HYDROw[y,r]==EP[:vS_HYDRO][y, hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] -
vSOC_HYDROw[y,
r]==EP[:vS_HYDRO][y, hours_per_subperiod * dfPeriodMap[r, :Rep_Period_Index]] -

vdSOC_HYDRO[y, dfPeriodMap[r, :Rep_Period_Index]])

# Extract maximum storage level variation (positive) within subperiod
@constraint(EP, cMaxSoCVarPos_H[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxPos_HYDRO[y,w] >= EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+t] - EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+1])
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cMaxSoCVarPos_H[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxPos_HYDRO[y,w] >= EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+t] - EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+1])
@constraint(EP,
cMaxSoCVarPos_H[
y in STOR_HYDRO_LONG_DURATION, w = 1:REP_PERIOD, t = 2:hours_per_subperiod],
vdSOC_maxPos_HYDRO[y,
w]>=EP[:vS_HYDRO][y, hours_per_subperiod * (w - 1) + t] -
EP[:vS_HYDRO][y, hours_per_subperiod * (w - 1) + 1])


# Extract maximum storage level variation (negative) within subperiod
@constraint(EP, cMaxSoCVarNeg_H[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxNeg_HYDRO[y,w] <= EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+t] - EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+1])
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cMaxSoCVarNeg_H[y in STOR_HYDRO_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxNeg_HYDRO[y,w] <= EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+t] - EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+1])
@constraint(EP,
cMaxSoCVarNeg_H[
y in STOR_HYDRO_LONG_DURATION, w = 1:REP_PERIOD, t = 2:hours_per_subperiod],
vdSOC_maxNeg_HYDRO[y,
w]<=EP[:vS_HYDRO][y, hours_per_subperiod * (w - 1) + t] -
EP[:vS_HYDRO][y, hours_per_subperiod * (w - 1) + 1])


# Max storage content within each modeled period cannot exceed installed energy capacity
@constraint(EP, cSoCLongDurationStorageMaxInt_H[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX],
vSOC_HYDROw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
-EP[:vSPILL][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]
+inputs["pP_Max"][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]*EP[:eTotalCap][y]
+vdSOC_maxPos_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]] <= hydro_energy_to_power_ratio(gen[y])*EP[:eTotalCap][y])
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cSoCLongDurationStorageMaxInt_H[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX],
vSOC_HYDROw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
-EP[:vSPILL][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]
+inputs["pP_Max"][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]*EP[:eTotalCap][y]
+vdSOC_maxPos_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]] <= hydro_energy_to_power_ratio(gen[y])*EP[:eTotalCap][y])
@constraint(EP,
cSoCLongDurationStorageMaxInt_H[
y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX],
vSOC_HYDROw[y, r] - (1 / efficiency_down(gen[y]) *
EP[:vP][y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1])
-
EP[:vSPILL][y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1]
+
inputs["pP_Max"][
y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1] *
EP[:eTotalCap][y]
+
vdSOC_maxPos_HYDRO[y, dfPeriodMap[r, :Rep_Period_Index]]<=hydro_energy_to_power_ratio(gen[y]) *
EP[:eTotalCap][y])


# Min storage content within each modeled period cannot be negative
@constraint(EP, cSoCLongDurationStorageMinInt_H[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX],
vSOC_HYDROw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
-EP[:vSPILL][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]
+inputs["pP_Max"][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]*EP[:eTotalCap][y]
+vdSOC_maxPos_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]] >= 0)

Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cSoCLongDurationStorageMinInt_H[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX],
vSOC_HYDROw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
-EP[:vSPILL][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]
+inputs["pP_Max"][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1]*EP[:eTotalCap][y]
+vdSOC_maxPos_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]] >= 0)
@constraint(EP,
cSoCLongDurationStorageMinInt_H[
y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX],
vSOC_HYDROw[y, r] - (1 / efficiency_down(gen[y]) *
EP[:vP][y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1])
-
EP[:vSPILL][y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1]
+
inputs["pP_Max"][
y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1] *
EP[:eTotalCap][y]
+ vdSOC_maxPos_HYDRO[y, dfPeriodMap[r, :Rep_Period_Index]]>=0)

end
24 changes: 10 additions & 14 deletions src/model/resources/resources.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ Allows to set the attribute `sym` of an `AbstractResource` object using dot synt
- `value`: The value to set for the attribute.

"""
Base.setproperty!(r::AbstractResource, sym::Symbol, value) = setindex!(parent(r),
value,
sym)
Base.setproperty!(r::AbstractResource, sym::Symbol, value) = setindex!(parent(r), value, sym)

"""
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
"""
Base.setproperty!(r::AbstractResource, sym::Symbol, value) = setindex!(
parent(r), value, sym)

haskey(r::AbstractResource, sym::Symbol)
Expand Down Expand Up @@ -106,32 +104,31 @@ end
"""
Base.getproperty(rs::Vector{<:AbstractResource}, sym::Symbol)

Allows to access attributes of a vector of `AbstractResource` objects using dot syntax. If the `sym` is an element of the `resource_types` constant, it returns all resources of that type. Otherwise, it returns the value of the attribute for each resource in the vector.
Allows to return all resources of a given type of a vector of `AbstractResource` objects using dot syntax.

# Arguments:
- `rs::Vector{<:AbstractResource}`: The vector of `AbstractResource` objects.
- `sym::Symbol`: The symbol representing the attribute name or a type from `resource_types`.
- `sym::Symbol`: The symbol representing the type from `resource_types`.

# Returns:
- If `sym` is an element of the `resource_types` constant, it returns a vector containing all resources of that type.
- If `sym` is an attribute name, it returns a vector containing the value of the attribute for each resource.

## Examples
```julia
julia> vre_gen = gen.Vre; # gen vector of resources
julia> typeof(vre_gen)
Vector{Vre} (alias for Array{Vre, 1})
julia> vre_gen.zone
julia> GenX.zone_id.(vre_gen)
```
"""
function Base.getproperty(rs::Vector{<:AbstractResource}, sym::Symbol)
# if sym is Type then return a vector resources of that type
# if sym is one of the resource types then return a vector resources of that type
if sym ∈ resource_types
res_type = eval(sym)
return Vector{res_type}(rs[isa.(rs, res_type)])
else
return getfield(rs, sym)
end
# if sym is a field of the resource then return that field for all resources
return [getproperty(r, sym) for r in rs]
end

"""
Expand Down Expand Up @@ -255,8 +252,7 @@ julia> findall(r -> max_cap_mwh(r) != 0, gen.Storage)
50
```
"""
Base.findall(f::Function, rs::Vector{<:AbstractResource}) = resource_id.(filter(r -> f(r),
rs))
Base.findall(f::Function, rs::Vector{<:AbstractResource}) = resource_id.(filter(r -> f(r), rs))

"""
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
"""
Base.findall(f::Function, rs::Vector{<:AbstractResource}) = resource_id.(filter(
r -> f(r), rs))

interface(name, default=default_zero, type=AbstractResource)
Expand Down Expand Up @@ -531,14 +527,14 @@ const default_zero = 0

# INTERFACE FOR ALL RESOURCES
resource_name(r::AbstractResource) = r.resource
resource_name(rs::Vector{T}) where {T <: AbstractResource} = rs.resource
resource_name(rs::Vector{T}) where {T <: AbstractResource} = resource_name.(rs)

resource_id(r::AbstractResource)::Int64 = r.id
resource_id(rs::Vector{T}) where {T <: AbstractResource} = resource_id.(rs)
resource_type_mga(r::AbstractResource) = r.resource_type

zone_id(r::AbstractResource) = r.zone
zone_id(rs::Vector{T}) where {T <: AbstractResource} = rs.zone
zone_id(rs::Vector{T}) where {T <: AbstractResource} = zone_id.(rs)

# getter for boolean attributes (true or false) with validation
function new_build(r::AbstractResource)
Expand Down
65 changes: 64 additions & 1 deletion src/model/resources/storage/long_duration_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,44 @@ If the capacity reserve margin constraint is enabled, a similar set of constrain
& \frac{1}{\eta_{o,z}^{discharge}}\Theta^{CRM}_{o,z,(m-1)\times \tau^{period}+1} - \eta_{o,z}^{charge}\Pi^{CRM}_{o,z,(m-1)\times \tau^{period}+1} \quad \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, m \in \mathcal{M}
\end{aligned}
```
All other constraints are identical to those used to track the actual state of charge, except with the new variables $Q^{CRM}_{o,z,n}$ and $\Delta Q^{CRM}_{o,z,n}$ used in place of $Q_{o,z,n}$ and $\Delta Q_{o,z,n}$, respectively.
All other constraints are identical to those used to track the actual state of charge, except with the new variables $Q^{CRM}_{o,z,n}$ and $\Delta Q^{CRM}_{o,z,n}$ used in place of $Q_{o,z,n}$ and $\Delta Q_{o,z,n}$, respectively. \

**Bound storage inventory in non-representative periods**
We need additional variables and constraints to ensure that the storage content is within zero and the installed energy capacity in non-representative periods. We introduce
the variables $\Delta Q^{max,pos}_{o,z,m}$ and $\Delta Q^{max,neg}_{o,z,m}$ that represent the maximum positive and negative storage content variations within the representative
period $m$, respectively, extracted as:

```math
\begin{aligned}
& \Delta Q^{max,pos}_{o,z,m} \geq \Gamma_{o,z,(m-1)\times \tau^{period}+t } - \Gamma_{o,z,(m-1)\times \tau^{period}+1 } \quad \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, m \in \mathcal{M}, t \in \mathcal{T}
& \end{aligned}
```

```math
\begin{aligned}
& \Delta Q^{max,neg}_{o,z,m} \leq \Gamma_{o,z,(m-1)\times \tau^{period}+t } - \Gamma_{o,z,(m-1)\times \tau^{period}+1 } \quad \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, m \in \mathcal{M}, t \in \mathcal{T}
& \end{aligned}
```

For every input period $n \in \mathcal{N}$, the maximum storage is computed and constrained to be less than or equal to the installed energy capacity as:

```math
\begin{aligned}
& Q_{o,z,n} \times \left(1-\eta_{o,z}^{loss}\right) - \frac{1}{\eta_{o,z}^{discharge}}\Theta_{o,z,(m-1)\times \tau^{period}+1} + \eta_{o,z}^{charge}\Pi_{o,z,(m-1)\times \tau^{period}+1} + \Delta Q^{max,pos}_{o,z,f(n)} \leq \Delta^{total, energy}_{o,z} \\
& \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, n \in \mathcal{N}
& \end{aligned}
```

Similarly, the minimum storage content is imposed to be positive in every period of the time horizon:

```math
\begin{aligned}
& Q_{o,z,n} \times \left(1-\eta_{o,z}^{loss}\right) - \frac{1}{\eta_{o,z}^{discharge}}\Theta_{o,z,(m-1)\times \tau^{period}+1} + \eta_{o,z}^{charge}\Pi_{o,z,(m-1)\times \tau^{period}+1} + \Delta Q^{max,neg}_{o,z,f(n)} \geq 0 \\
& \forall o \in \mathcal{O}^{LDES}, z \in \mathcal{Z}, n \in \mathcal{N}
& \end{aligned}
```

Additional details on this approach are available in [Parolin et al., 2024](https://doi.org/10.48550/arXiv.2409.19079).
"""
function long_duration_storage!(EP::Model, inputs::Dict, setup::Dict)
println("Long Duration Storage Module")
Expand Down Expand Up @@ -96,6 +133,12 @@ function long_duration_storage!(EP::Model, inputs::Dict, setup::Dict)
@variable(EP, vCAPRES_dsoc[y in STOR_LONG_DURATION, w = 1:REP_PERIOD])
end

# Maximum positive storage inventory change within subperiod
@variable(EP, vdSOC_maxPos[y in STOR_LONG_DURATION, w=1:REP_PERIOD] >= 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@variable(EP, vdSOC_maxPos[y in STOR_LONG_DURATION, w=1:REP_PERIOD] >= 0)
@variable(EP, vdSOC_maxPos[y in STOR_LONG_DURATION, w = 1:REP_PERIOD]>=0)


# Maximum negative storage inventory change within subperiod
@variable(EP, vdSOC_maxNeg[y in STOR_LONG_DURATION, w=1:REP_PERIOD] <= 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
# Maximum negative storage inventory change within subperiod
@variable(EP, vdSOC_maxNeg[y in STOR_LONG_DURATION, w=1:REP_PERIOD] <= 0)
# Maximum negative storage inventory change within subperiod
@variable(EP, vdSOC_maxNeg[y in STOR_LONG_DURATION, w = 1:REP_PERIOD]<=0)


### Constraints ###

# Links last time step with first time step, ensuring position in hour 1 is within eligible change from final hour position
Expand Down Expand Up @@ -181,4 +224,24 @@ function long_duration_storage!(EP::Model, inputs::Dict, setup::Dict)
r in MODELED_PERIODS_INDEX],
vSOCw[y, r]>=vCAPRES_socw[y, r])
end

# Extract maximum storage level variation (positive) within subperiod
@constraint(EP, cMaxSoCVarPos[y in STOR_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxPos[y,w] >= EP[:vS][y,hours_per_subperiod*(w-1)+t] - EP[:vS][y,hours_per_subperiod*(w-1)+1])
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cMaxSoCVarPos[y in STOR_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxPos[y,w] >= EP[:vS][y,hours_per_subperiod*(w-1)+t] - EP[:vS][y,hours_per_subperiod*(w-1)+1])
@constraint(EP,
cMaxSoCVarPos[y in STOR_LONG_DURATION, w = 1:REP_PERIOD, t = 2:hours_per_subperiod],
vdSOC_maxPos[y,
w]>=EP[:vS][y, hours_per_subperiod * (w - 1) + t] -
EP[:vS][y, hours_per_subperiod * (w - 1) + 1])


# Extract maximum storage level variation (negative) within subperiod
@constraint(EP, cMaxSoCVarNeg[y in STOR_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxNeg[y,w] <= EP[:vS][y,hours_per_subperiod*(w-1)+t] - EP[:vS][y,hours_per_subperiod*(w-1)+1])
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cMaxSoCVarNeg[y in STOR_LONG_DURATION, w=1:REP_PERIOD, t=2:hours_per_subperiod],
vdSOC_maxNeg[y,w] <= EP[:vS][y,hours_per_subperiod*(w-1)+t] - EP[:vS][y,hours_per_subperiod*(w-1)+1])
@constraint(EP,
cMaxSoCVarNeg[y in STOR_LONG_DURATION, w = 1:REP_PERIOD, t = 2:hours_per_subperiod],
vdSOC_maxNeg[y,
w]<=EP[:vS][y, hours_per_subperiod * (w - 1) + t] -
EP[:vS][y, hours_per_subperiod * (w - 1) + 1])


# Max storage content within each modeled period cannot exceed installed energy capacity
@constraint(EP, cSoCLongDurationStorageMaxInt[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX],
(1-self_discharge(gen[y]))*vSOCw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+(efficiency_up(gen[y])*EP[:vCHARGE][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+vdSOC_maxPos[y,dfPeriodMap[r,:Rep_Period_Index]] <= EP[:eTotalCapEnergy][y])
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cSoCLongDurationStorageMaxInt[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX],
(1-self_discharge(gen[y]))*vSOCw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+(efficiency_up(gen[y])*EP[:vCHARGE][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+vdSOC_maxPos[y,dfPeriodMap[r,:Rep_Period_Index]] <= EP[:eTotalCapEnergy][y])
@constraint(EP,
cSoCLongDurationStorageMaxInt[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX],
(1 - self_discharge(gen[y])) * vSOCw[y, r] - (1 / efficiency_down(gen[y]) *
EP[:vP][y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1])
+
(efficiency_up(gen[y]) * EP[:vCHARGE][
y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1])
+ vdSOC_maxPos[y, dfPeriodMap[r, :Rep_Period_Index]]<=EP[:eTotalCapEnergy][y])


# Min storage content within each modeled period cannot be negative
@constraint(EP, cSoCLongDurationStorageMinInt[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX],
(1-self_discharge(gen[y]))*vSOCw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+(efficiency_up(gen[y])*EP[:vCHARGE][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+vdSOC_maxNeg[y,dfPeriodMap[r,:Rep_Period_Index]] >= 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
@constraint(EP, cSoCLongDurationStorageMinInt[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX],
(1-self_discharge(gen[y]))*vSOCw[y,r]-(1/efficiency_down(gen[y])*EP[:vP][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+(efficiency_up(gen[y])*EP[:vCHARGE][y,hours_per_subperiod*(dfPeriodMap[r,:Rep_Period_Index]-1)+1])
+vdSOC_maxNeg[y,dfPeriodMap[r,:Rep_Period_Index]] >= 0)
@constraint(EP,
cSoCLongDurationStorageMinInt[y in STOR_LONG_DURATION, r in MODELED_PERIODS_INDEX],
(1 - self_discharge(gen[y])) * vSOCw[y, r] - (1 / efficiency_down(gen[y]) *
EP[:vP][y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1])
+
(efficiency_up(gen[y]) * EP[:vCHARGE][
y, hours_per_subperiod * (dfPeriodMap[r, :Rep_Period_Index] - 1) + 1])
+ vdSOC_maxNeg[y, dfPeriodMap[r, :Rep_Period_Index]]>=0)

end
Loading
Loading