From c9db35972ea95ac4030a0b835e043d70266eb077 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 3 Jun 2025 15:21:19 -0600 Subject: [PATCH 01/83] DR component same as storage structure --- .../src/Results/DemandResponseAvailability.jl | 80 +++++++++++ .../src/Results/DemandResponseEnergy.jl | 103 ++++++++++++++ .../Results/DemandResponseEnergySamples.jl | 87 ++++++++++++ PRASCore.jl/src/Results/Results.jl | 7 +- .../src/Simulations/DispatchProblem.jl | 132 +++++++++++++++++- PRASCore.jl/src/Simulations/Simulations.jl | 10 ++ PRASCore.jl/src/Simulations/SystemState.jl | 10 ++ PRASCore.jl/src/Simulations/recording.jl | 75 ++++++++++ PRASCore.jl/src/Simulations/utils.jl | 32 +++++ PRASCore.jl/src/Systems/SystemModel.jl | 15 +- PRASCore.jl/src/Systems/Systems.jl | 2 +- PRASCore.jl/src/Systems/TestData.jl | 73 +++++++++- PRASCore.jl/src/Systems/assets.jl | 122 ++++++++++++++++ PRASCore.jl/test/Results/availability.jl | 13 ++ PRASCore.jl/test/Results/energy.jl | 32 +++++ PRASCore.jl/test/Simulations/runtests.jl | 7 +- PRASCore.jl/test/Systems/SystemModel.jl | 10 +- PRASCore.jl/test/Systems/assets.jl | 24 ++++ 18 files changed, 813 insertions(+), 21 deletions(-) create mode 100644 PRASCore.jl/src/Results/DemandResponseAvailability.jl create mode 100644 PRASCore.jl/src/Results/DemandResponseEnergy.jl create mode 100644 PRASCore.jl/src/Results/DemandResponseEnergySamples.jl diff --git a/PRASCore.jl/src/Results/DemandResponseAvailability.jl b/PRASCore.jl/src/Results/DemandResponseAvailability.jl new file mode 100644 index 00000000..12bb5443 --- /dev/null +++ b/PRASCore.jl/src/Results/DemandResponseAvailability.jl @@ -0,0 +1,80 @@ +""" + DemandResponse + +The `DemandResposneAvailability` result specification reports the sample-level +discrete availability of `DemandResponses`, producing a `DemandResponseAvailabilityResult`. + +A `DemandResponseAvailabilityResult` can be indexed by demand response device name and +a timestamp to retrieve a vector of sample-level availability states for +the unit in the given timestep. States are provided as a boolean with +`true` indicating that the unit is available and `false` indicating that +it's unavailable. + +Example: + +```julia +dravail, = + assess(sys, SequentialMonteCarlo(samples=10), DemandResponseAvailability()) + +samples = dravail["MyDR123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] + +@assert samples isa Vector{Bool} +@assert length(samples) == 10 +``` +""" +struct DemandResponseAvailability <: ResultSpec end + +struct DRAvailabilityAccumulator <: ResultAccumulator{DemandResponseAvailability} + + available::Array{Bool,3} + +end + +function accumulator( + sys::SystemModel{N}, nsamples::Int, ::DemandResponseAvailability +) where {N} + + ndrs = length(sys.storages) + available = zeros(Bool, ndrs, N, nsamples) + + return DRAvailabilityAccumulator(available) + +end + +function merge!( + x::DRAvailabilityAccumulator, y::DRAvailabilityAccumulator +) + + x.available .|= y.available + return + +end + +accumulatortype(::DemandResponseAvailability) = DRAvailabilityAccumulator + +struct DemandResponseAvailabilityResult{N,L,T<:Period} <: AbstractAvailabilityResult{N,L,T} + + demandresponses::Vector{String} + timestamps::StepRange{ZonedDateTime,T} + + available::Array{Bool,3} + +end + +names(x::DemandResponseAvailabilityResult) = x.demandresponses + +function getindex(x::DemandResponseAvailabilityResult, s::AbstractString, t::ZonedDateTime) + i_dr = findfirstunique(x.demandresponses, s) + i_t = findfirstunique(x.timestamps, t) + return vec(x.available[i_dr, i_t, :]) +end + +function finalize( + acc::DRAvailabilityAccumulator, + system::SystemModel{N,L,T,P,E}, +) where {N,L,T,P,E} + + return DemandResponseAvailabilityResult{N,L,T}( + system.demandresponses.names, system.timestamps, acc.available) + +end diff --git a/PRASCore.jl/src/Results/DemandResponseEnergy.jl b/PRASCore.jl/src/Results/DemandResponseEnergy.jl new file mode 100644 index 00000000..34be40aa --- /dev/null +++ b/PRASCore.jl/src/Results/DemandResponseEnergy.jl @@ -0,0 +1,103 @@ +""" + DemandResponseEnergy + +The `DemandResponseEnergy` result specification reports the average state of charge +of `DemandResponses`, producing a `DemandResponseEnergyResult`. + +A `DemandResponseEnergyResult` can be indexed by demand response device name and a timestamp to +retrieve a tuple of sample mean and standard deviation, estimating the average +energy level for the given demand response device in that timestep. + +Example: + +```julia +drenergy, = + assess(sys, SequentialMonteCarlo(samples=1000), DemandResponseEnergy()) + +soc_mean, soc_std = + drenergy["MyDemandResponse123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] +``` + +See [`DemandResponseEnergySamples`](@ref) for sample-level demand response states of charge. + +See [`GeneratorStorageEnergy`](@ref) for average generator-storage states +of charge. +""" +struct DemandResponseEnergy <: ResultSpec end + +mutable struct DemandResponseEnergyAccumulator <: ResultAccumulator{DemandResponseEnergy} + + # Cross-simulation energy mean/variances + energy_period::Vector{MeanVariance} + energy_demandresponseperiod::Matrix{MeanVariance} + +end + +function accumulator( + sys::SystemModel{N}, nsamples::Int, ::DemandResponseEnergy +) where {N} + + ndemandresponses = length(sys.demandresponses) + + energy_period = [meanvariance() for _ in 1:N] + energy_demandresponseperiod = [meanvariance() for _ in 1:ndemandresponses, _ in 1:N] + + return DemandResponseEnergyAccumulator( + energy_period, energy_demandresponseperiod) + +end + +function merge!( + x::DemandResponseEnergyAccumulator, y::DemandResponseEnergyAccumulator +) + + foreach(merge!, x.energy_period, y.energy_period) + foreach(merge!, x.energy_demandresponseperiod, y.energy_demandresponseperiod) + + return + +end + +accumulatortype(::DemandResponseEnergy) = DemandResponseEnergyAccumulator + +struct DemandResponseEnergyResult{N,L,T<:Period,E<:EnergyUnit} <: AbstractEnergyResult{N,L,T} + + nsamples::Union{Int,Nothing} + demandresponses::Vector{String} + timestamps::StepRange{ZonedDateTime,T} + + energy_mean::Matrix{Float64} + + energy_period_std::Vector{Float64} + energy_regionperiod_std::Matrix{Float64} + +end + +names(x::DemandResponseEnergyResult) = x.demandresponses + +function getindex(x::DemandResponseEnergyResult, t::ZonedDateTime) + i_t = findfirstunique(x.timestamps, t) + return sum(view(x.energy_mean, :, i_t)), x.energy_period_std[i_t] +end + +function getindex(x::DemandResponseEnergyResult, s::AbstractString, t::ZonedDateTime) + i_dr = findfirstunique(x.demandresponses, s) + i_t = findfirstunique(x.timestamps, t) + return x.energy_mean[i_dr, i_t], x.energy_regionperiod_std[i_dr, i_t] +end + +function finalize( + acc::DemandResponseEnergyAccumulator, + system::SystemModel{N,L,T,P,E}, +) where {N,L,T,P,E} + + _, period_std = mean_std(acc.energy_period) + demandresponseperiod_mean, demandresponseperiod_std = mean_std(acc.energy_demandresponseperiod) + + nsamples = first(first(acc.energy_period).stats).n + + return DemandResponseEnergyResult{N,L,T,E}( + nsamples, system.demandresponses.names, system.timestamps, + demandresponseperiod_mean, period_std, demandresponseperiod_std) + +end diff --git a/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl new file mode 100644 index 00000000..e282a3bb --- /dev/null +++ b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl @@ -0,0 +1,87 @@ +""" + DemandResponseEnergySamples + +The `DemandResponseEnergySamples` result specification reports the sample-level state +of charge of `DemandResponses`, producing a `DemandResponseEnergySamplesResult`. + +A `DemandResponseEnergySamplesResult` can be indexed by demand response device name and +a timestamp to retrieve a vector of sample-level charge states for +the device in the given timestep. + +Example: + +```julia +demandresponseenergy, = + assess(sys, SequentialMonteCarlo(samples=10), DemandResponseEnergySamples()) + +samples = demandresponseenergy["MyDemandResponse123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] + +@assert samples isa Vector{Float64} +@assert length(samples) == 10 +``` + +Note that this result specification requires large amounts of memory for +larger sample sizes. See [`DemandResponseEnergy`](@ref) for estimated average demand response +state of charge when sample-level granularity isn't required. +""" +struct DemandResponseEnergySamples <: ResultSpec end + +struct DemandResponseEnergySamplesAccumulator <: ResultAccumulator{DemandResponseEnergySamples} + + energy::Array{Float64,3} + +end + +function accumulator( + sys::SystemModel{N}, nsamples::Int, ::DemandResponseEnergySamples +) where {N} + + ndrs = length(sys.demandresponses) + energy = zeros(Int, ndrs, N, nsamples) + + return DemandResponseEnergySamplesAccumulator(energy) + +end + +function merge!( + x::DemandResponseEnergySamplesAccumulator, y::DemandResponseEnergySamplesAccumulator +) + + x.energy .+= y.energy + return + +end + +accumulatortype(::DemandResponseEnergySamples) = DemandResponseEnergySamplesAccumulator + +struct DemandResponseEnergySamplesResult{N,L,T<:Period,E<:EnergyUnit} <: AbstractEnergyResult{N,L,T} + + demandresponses::Vector{String} + timestamps::StepRange{ZonedDateTime,T} + + energy::Array{Int,3} + +end + +names(x::DemandResponseEnergySamplesResult) = x.demandresponses + +function getindex(x::DemandResponseEnergySamplesResult, t::ZonedDateTime) + i_t = findfirstunique(x.timestamps, t) + return vec(sum(view(x.energy, :, i_t, :), dims=1)) +end + +function getindex(x::DemandResponseEnergySamplesResult, s::AbstractString, t::ZonedDateTime) + i_dr = findfirstunique(x.demandresponses, s) + i_t = findfirstunique(x.timestamps, t) + return vec(x.energy[i_dr, i_t, :]) +end + +function finalize( + acc::DemandResponseEnergySamplesAccumulator, + system::SystemModel{N,L,T,P,E}, +) where {N,L,T,P,E} + + return DemandResponseEnergySamplesResult{N,L,T,E}( + system.demandresponses.names, system.timestamps, acc.energy) + +end diff --git a/PRASCore.jl/src/Results/Results.jl b/PRASCore.jl/src/Results/Results.jl index ad6f4028..011d31c7 100644 --- a/PRASCore.jl/src/Results/Results.jl +++ b/PRASCore.jl/src/Results/Results.jl @@ -20,8 +20,10 @@ export Flow, FlowSamples, Utilization, UtilizationSamples, StorageEnergy, StorageEnergySamples, GeneratorStorageEnergy, GeneratorStorageEnergySamples, + DemandResponseEnergy, DemandResponseEnergySamples, GeneratorAvailability, StorageAvailability, - GeneratorStorageAvailability, LineAvailability + GeneratorStorageAvailability,DemandResponseAvailability, + LineAvailability include("utils.jl") include("metrics.jl") @@ -144,6 +146,7 @@ getindex(x::AbstractAvailabilityResult, ::Colon, ::Colon) = include("GeneratorAvailability.jl") include("StorageAvailability.jl") include("GeneratorStorageAvailability.jl") +include("DemandResponseAvailability.jl") include("LineAvailability.jl") abstract type AbstractEnergyResult{N,L,T} <: Result{N,L,T} end @@ -162,8 +165,10 @@ getindex(x::AbstractEnergyResult, ::Colon, ::Colon) = include("StorageEnergy.jl") include("GeneratorStorageEnergy.jl") +include("DemandResponseEnergy.jl") include("StorageEnergySamples.jl") include("GeneratorStorageEnergySamples.jl") +include("DemandResponseEnergySamples.jl") function resultchannel( results::T, threads::Int diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 28240c14..abddbbe6 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -47,7 +47,9 @@ Nodes in the problem are ordered as: 5. GenerationStorage discharge capacity (GeneratorStorage order) 6. GenerationStorage grid injection (GeneratorStorage order) 7. GenerationStorage charge capacity (GeneratorStorage order) - 8. Slack node + 8. DemandResponse discharge capacity (DemandResponse order) + 9. DemandResponse charge capacity (DemandResponse order) + 10. Slack node Edges are ordered as: @@ -67,6 +69,10 @@ Edges are ordered as: 14. GenerationStorage charge from inflow (GeneratorStorage order) 15. GenerationStorage charge unused (GeneratorStorage order) 16. GenerationStorage inflow unused (GeneratorStorage order) + 17. DemandResponse discharge to grid (DemandResponse order) + 18. DemandResponse discharge unused (DemandResponse order) + 19. DemandResponse charge from grid (DemandResponse order) + 20. DemandResponse charge unused (DemandResponse order) """ struct DispatchProblem @@ -80,6 +86,8 @@ struct DispatchProblem genstorage_discharge_nodes::UnitRange{Int} genstorage_togrid_nodes::UnitRange{Int} genstorage_charge_nodes::UnitRange{Int} + demandresponse_discharge_nodes::UnitRange{Int} + demandresponse_charge_nodes::UnitRange{Int} slack_node::Int # Edge labels @@ -99,10 +107,19 @@ struct DispatchProblem genstorage_inflowcharge_edges::UnitRange{Int} genstorage_chargeunused_edges::UnitRange{Int} genstorage_inflowunused_edges::UnitRange{Int} + demandresponse_discharge_edges::UnitRange{Int} + demandresponse_dischargeunused_edges::UnitRange{Int} + demandresponse_charge_edges::UnitRange{Int} + demandresponse_chargeunused_edges::UnitRange{Int} + #stor/genstor costs min_chargecost::Int max_dischargecost::Int + #dr costs + min_chargecost_dr::Int + max_dischargecost_dr::Int + function DispatchProblem( sys::SystemModel; unlimited::Int=999_999_999) @@ -110,14 +127,23 @@ struct DispatchProblem nifaces = length(sys.interfaces) nstors = length(sys.storages) ngenstors = length(sys.generatorstorages) + ndrs = length(sys.demandresponses) + #for storage/genstor maxchargetime, maxdischargetime = maxtimetocharge_discharge(sys) min_chargecost = - maxchargetime - 1 max_dischargecost = - min_chargecost + maxdischargetime + 1 shortagepenalty = 10 * (nifaces + max_dischargecost) + #for demandresponse + maxchargetime_dr, maxdischargetime_dr = maxtimetocharge_discharge_dr(sys) + min_chargecost_dr = - maxchargetime_dr - 1 + max_dischargecost_dr = - min_chargecost_dr + maxdischargetime_dr + 1 + + stor_regions = assetgrouplist(sys.region_stor_idxs) genstor_regions = assetgrouplist(sys.region_genstor_idxs) + dr_regions = assetgrouplist(sys.region_dr_idxs) region_nodes = 1:nregions stor_discharge_nodes = indices_after(region_nodes, nstors) @@ -126,7 +152,9 @@ struct DispatchProblem genstor_discharge_nodes = indices_after(genstor_inflow_nodes, ngenstors) genstor_togrid_nodes = indices_after(genstor_discharge_nodes, ngenstors) genstor_charge_nodes = indices_after(genstor_togrid_nodes, ngenstors) - slack_node = nnodes = last(genstor_charge_nodes) + 1 + dr_charge_nodes = indices_after(genstor_charge_nodes, ndrs) + dr_discharge_nodes = indices_after(dr_charge_nodes, ndrs) + slack_node = nnodes = last(dr_discharge_nodes) + 1 region_unservedenergy = 1:nregions region_unusedcapacity = indices_after(region_unservedenergy, nregions) @@ -144,7 +172,11 @@ struct DispatchProblem genstor_inflowcharge = indices_after(genstor_gridcharge, ngenstors) genstor_chargeunused = indices_after(genstor_inflowcharge, ngenstors) genstor_inflowunused = indices_after(genstor_chargeunused, ngenstors) - nedges = last(genstor_inflowunused) + dr_chargeused = indices_after(genstor_inflowunused, ndrs) + dr_chargeunused = indices_after(dr_chargeused, ndrs) + dr_dischargeused = indices_after(dr_chargeunused, ndrs) + dr_dischargeunused = indices_after(dr_dischargeused, ndrs) + nedges = last(dr_dischargeunused) nodesfrom = Vector{Int}(undef, nedges) nodesto = Vector{Int}(undef, nedges) @@ -196,16 +228,23 @@ struct DispatchProblem initedges(genstor_gridcharge, genstor_regions, genstor_charge_nodes) initedges(genstor_inflowcharge, genstor_inflow_nodes, genstor_charge_nodes) initedges(genstor_chargeunused, slack_node, genstor_charge_nodes) - initedges(genstor_inflowunused, genstor_inflow_nodes, slack_node) + # Demand Response charging / discharging + initedges(dr_dischargeused, dr_discharge_nodes, dr_regions) + initedges(dr_dischargeunused, dr_discharge_nodes, slack_node) + initedges(dr_chargeused, dr_regions, dr_charge_nodes) + initedges(dr_chargeunused, slack_node, dr_charge_nodes) + + return new( FlowProblem(nodesfrom, nodesto, limits, costs, injections), region_nodes, stor_discharge_nodes, stor_charge_nodes, genstor_inflow_nodes, genstor_discharge_nodes, - genstor_togrid_nodes, genstor_charge_nodes, slack_node, + genstor_togrid_nodes, genstor_charge_nodes, + dr_charge_nodes,dr_discharge_nodes, slack_node, region_unservedenergy, region_unusedcapacity, iface_forward, iface_reverse, @@ -214,7 +253,11 @@ struct DispatchProblem genstor_dischargegrid, genstor_dischargeunused, genstor_inflowgrid, genstor_totalgrid, genstor_gridcharge, genstor_inflowcharge, genstor_chargeunused, - genstor_inflowunused, min_chargecost, max_dischargecost + genstor_inflowunused, + dr_chargeused, dr_chargeunused, dr_dischargeused, + dr_dischargeunused, + min_chargecost, max_dischargecost, + min_chargecost_dr, max_dischargecost_dr ) end @@ -380,6 +423,58 @@ function update_problem!( end + + + # Update Demand Response charge/discharge limits and priorities + for (i, (charge_node, charge_edge, discharge_node, discharge_edge)) in + enumerate(zip( + problem.demandresponse_charge_nodes, problem.demandresponse_charge_edges, + problem.demandresponse_discharge_nodes, problem.demandresponse_discharge_edges)) + + dr_online = state.drs_available[i] + dr_energy = state.drs_energy[i] + maxenergy = system.demandresponses.energy_capacity[i, t] + + # Update discharging + + maxdischarge = dr_online * system.demandresponses.discharge_capacity[i, t] + dischargeefficiency = system.demandresponses.discharge_efficiency[i, t] + energydischargeable = dr_energy * dischargeefficiency + + if iszero(maxdischarge) + timetodischarge = N + 1 + else + timetodischarge = round(Int, energydischargeable / maxdischarge) + end + + discharge_capacity = + min(maxdischarge, floor(Int, energytopower( + energydischargeable, E, L, T, P))) + updateinjection!( + fp.nodes[discharge_node], slack_node, -discharge_capacity) + + # Largest time-to-discharge = highest priority (discharge first) + dischargecost = problem.max_dischargecost_dr - timetodischarge # Positive cost + updateflowcost!(fp.edges[discharge_edge], dischargecost) + + # Update charging + + maxcharge = dr_online * system.demandresponses.charge_capacity[i, t] + chargeefficiency = system.demandresponses.charge_efficiency[i, t] + energychargeable = (maxenergy - dr_energy) / chargeefficiency + + charge_capacity = + min(maxcharge, floor(Int, energytopower( + energychargeable, E, L, T, P))) + updateinjection!( + fp.nodes[charge_node], slack_node, charge_capacity) + + # Smallest time-to-discharge = highest priority (charge first) + chargecost = problem.min_chargecost_dr + timetodischarge # Negative cost + updateflowcost!(fp.edges[charge_edge], chargecost) + + end + end function update_state!( @@ -390,6 +485,9 @@ function update_state!( edges = problem.fp.edges p2e = conversionfactor(L, T, P, E) + # Storage Update + + #discharging for (i, e) in enumerate(problem.storage_discharge_edges) energy = state.stors_energy[i] energy_drop = ceil(Int, edges[e].flow * p2e / @@ -397,12 +495,16 @@ function update_state!( state.stors_energy[i] = max(0, energy - energy_drop) end - + + #charging for (i, e) in enumerate(problem.storage_charge_edges) state.stors_energy[i] += ceil(Int, edges[e].flow * p2e * system.storages.charge_efficiency[i, t]) end + # GeneratorStorage Update + + #discharging for (i, e) in enumerate(problem.genstorage_dischargegrid_edges) energy = state.genstors_energy[i] energy_drop = ceil(Int, edges[e].flow * p2e / @@ -410,6 +512,7 @@ function update_state!( state.genstors_energy[i] = max(0, energy - energy_drop) end + #charging for (i, (e1, e2)) in enumerate(zip(problem.genstorage_gridcharge_edges, problem.genstorage_inflowcharge_edges)) totalcharge = (edges[e1].flow + edges[e2].flow) * p2e @@ -417,6 +520,21 @@ function update_state!( ceil(Int, totalcharge * system.generatorstorages.charge_efficiency[i, t]) end + #Demand Response Update + #charging (negative of the flows) + for (i, e) in enumerate(problem.demandresponse_charge_edges) + state.drs_energy[i] += + ceil(Int, edges[e].flow * p2e * system.demandresponses.charge_efficiency[i, t]) + end + + + #discharging + for (i, e) in enumerate(problem.demandresponse_discharge_edges) + energy = state.drs_energy[i] + energy_drop = ceil(Int, edges[e].flow * p2e / system.demandresponses.discharge_efficiency[i, t]) + state.drs_energy[i] = max(0, energy - energy_drop) + end + end function assetgrouplist(idxss::Vector{UnitRange{Int}}) diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index b9f220b6..48e922d6 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -175,12 +175,17 @@ function initialize!( rng, state.genstors_available, state.genstors_nexttransition, system.generatorstorages, N) + initialize_availability!( + rng, state.drs_available, state.drs_nexttransition, + system.demandresponses, N) + initialize_availability!( rng, state.lines_available, state.lines_nexttransition, system.lines, N) fill!(state.stors_energy, 0) fill!(state.genstors_energy, 0) + fill!(state.drs_energy, 0) return @@ -204,12 +209,17 @@ function advance!( rng, state.genstors_available, state.genstors_nexttransition, system.generatorstorages, t, N) + update_availability!( + rng, state.drs_available, state.drs_nexttransition, + system.demandresponses, t, N) + update_availability!( rng, state.lines_available, state.lines_nexttransition, system.lines, t, N) update_energy!(state.stors_energy, system.storages, t) update_energy!(state.genstors_energy, system.generatorstorages, t) + update_energy!(state.drs_energy, system.demandresponses, t) update_problem!(dispatchproblem, state, system, t) diff --git a/PRASCore.jl/src/Simulations/SystemState.jl b/PRASCore.jl/src/Simulations/SystemState.jl index a88296cb..7c122752 100644 --- a/PRASCore.jl/src/Simulations/SystemState.jl +++ b/PRASCore.jl/src/Simulations/SystemState.jl @@ -11,6 +11,10 @@ struct SystemState genstors_nexttransition::Vector{Int} genstors_energy::Vector{Int} + drs_available::Vector{Bool} + drs_nexttransition::Vector{Int} + drs_energy::Vector{Int} + lines_available::Vector{Bool} lines_nexttransition::Vector{Int} @@ -30,6 +34,11 @@ struct SystemState genstors_nexttransition = Vector{Int}(undef, ngenstors) genstors_energy = Vector{Int}(undef, ngenstors) + ndrs = length(system.demandresponses) + drs_available = Vector{Bool}(undef, ndrs) + drs_nexttransition = Vector{Int}(undef, ndrs) + drs_energy = Vector{Int}(undef, ndrs) + nlines = length(system.lines) lines_available = Vector{Bool}(undef, nlines) lines_nexttransition = Vector{Int}(undef, nlines) @@ -38,6 +47,7 @@ struct SystemState gens_available, gens_nexttransition, stors_available, stors_nexttransition, stors_energy, genstors_available, genstors_nexttransition, genstors_energy, + drs_available, drs_nexttransition, drs_energy, lines_available, lines_nexttransition) end diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 613dda86..6ec1d1a8 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -117,6 +117,11 @@ function record!( end + for dr in system.region_dr_idxs[r] + dr_idx = problem.demandresponse_chargeunused_edges[dr] + regionsurplus += edges[dr_idx].flow + end + fit!(acc.surplus_regionperiod[r,t], regionsurplus) totalsurplus += regionsurplus @@ -162,6 +167,11 @@ function record!( end + for dr in system.region_dr_idxs[r] + dr_idx = problem.demandresponse_chargeunused_edges[dr] + regionsurplus += edges[dr_idx].flow + end + acc.surplus[r, t, sampleid] = regionsurplus end @@ -326,6 +336,23 @@ end reset!(acc::Results.GenStorAvailabilityAccumulator, sampleid::Int) = nothing +# DemandrResponseAvailability + +function record!( + acc::Results.DRAvailabilityAccumulator, + system::SystemModel{N,L,T,P,E}, + state::SystemState, problem::DispatchProblem, + sampleid::Int, t::Int +) where {N,L,T,P,E} + + acc.available[:, t, sampleid] .= state.drs_available + return + +end + +reset!(acc::Results.DRAvailabilityAccumulator, sampleid::Int) = nothing + + # LineAvailability function record!( @@ -398,6 +425,37 @@ end reset!(acc::Results.GenStorageEnergyAccumulator, sampleid::Int) = nothing + +# DemandResponseEnergy + +function record!( + acc::Results.DemandResponseEnergyAccumulator, + system::SystemModel{N,L,T,P,E}, + state::SystemState, problem::DispatchProblem, + sampleid::Int, t::Int +) where {N,L,T,P,E} + + totalenergy = 0 + ndemandresponses = length(system.demandresponses) + + for s in 1:ndemandresponses + + drenergy = state.drs_energy[s] + fit!(acc.energy_demandresponseperiod[s,t], drenergy) + totalenergy += drenergy + + end + + fit!(acc.energy_period[t], totalenergy) + + return + +end + +reset!(acc::Results.DemandResponseEnergyAccumulator, sampleid::Int) = nothing + + + # StorageEnergySamples function record!( @@ -429,3 +487,20 @@ function record!( end reset!(acc::Results.GenStorageEnergySamplesAccumulator, sampleid::Int) = nothing + + +# DemandResponseEnergySamples + +function record!( + acc::Results.DemandResponseEnergySamplesAccumulator, + system::SystemModel{N,L,T,P,E}, + state::SystemState, problem::DispatchProblem, + sampleid::Int, t::Int +) where {N,L,T,P,E} + + acc.energy[:, t, sampleid] .= state.drs_energy + return + +end + +reset!(acc::Results.DemandResponseEnergySamplesAccumulator, sampleid::Int) = nothing \ No newline at end of file diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 533c9315..81e2ffbc 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -177,6 +177,38 @@ function maxtimetocharge_discharge(system::SystemModel) end +function maxtimetocharge_discharge_dr(system::SystemModel) + + if length(system.demandresponses) > 0 + if any(iszero, system.demandresponses.charge_capacity) + dr_charge_max = length(system.timestamps) + 1 + else + dr_charge_durations = + system.demandresponses.energy_capacity ./ system.demandresponses.charge_capacity + dr_charge_max = ceil(Int, maximum(dr_charge_durations)) + end + + if any(iszero, system.demandresponses.discharge_capacity) + dr_discharge_max = length(system.timestamps) + 1 + else + dr_discharge_durations = + system.demandresponses.energy_capacity ./ system.demandresponses.discharge_capacity + dr_discharge_max = ceil(Int, maximum(dr_discharge_durations)) + end + + else + dr_charge_max = 0 + dr_discharge_max = 0 + + end + + return (dr_charge_max,dr_discharge_max) + +end + + + + function utilization(f::MinCostFlows.Edge, b::MinCostFlows.Edge) flow_forward = f.flow diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index c9091e9e..a100d512 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -56,6 +56,9 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} generatorstorages::GeneratorStorages{N,L,T,P,E} region_genstor_idxs::Vector{UnitRange{Int}} + demandresponses::DemandResponses{N,L,T,P,E} + region_dr_idxs::Vector{UnitRange{Int}} + lines::Lines{N,L,T,P} interface_line_idxs::Vector{UnitRange{Int}} @@ -69,6 +72,7 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, + demandresponses::DemandResponses{N,L,T,P,E}, region_dr_idxs::Vector{UnitRange{Int}}, lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, timestamps::StepRange{ZonedDateTime,T}, attrs::Dict{String, String}=Dict{String, String}() @@ -78,6 +82,7 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} n_gens = length(generators) n_stors = length(storages) n_genstors = length(generatorstorages) + n_drs = length(demandresponses) n_interfaces = length(interfaces) n_lines = length(lines) @@ -85,6 +90,7 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} @assert consistent_idxs(region_gen_idxs, n_gens, n_regions) @assert consistent_idxs(region_stor_idxs, n_stors, n_regions) @assert consistent_idxs(region_genstor_idxs, n_genstors, n_regions) + @assert consistent_idxs(region_dr_idxs, n_drs, n_regions) @assert consistent_idxs(interface_line_idxs, n_lines, n_interfaces) @assert all( @@ -97,7 +103,9 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} new{N,L,T,P,E}( regions, interfaces, generators, region_gen_idxs, storages, region_stor_idxs, - generatorstorages, region_genstor_idxs, lines, interface_line_idxs, + generatorstorages, region_genstor_idxs, + demandresponses, region_dr_idxs, + lines, interface_line_idxs, timestamps, attrs) end @@ -110,6 +118,7 @@ function SystemModel( generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, + demandresponses::Storages{N,L,T,P,E}, region_dr_idxs::Vector{UnitRange{Int}}, lines, interface_line_idxs::Vector{UnitRange{Int}}, timestamps::StepRange{DateTime,T}, attrs::Dict{String, String}=Dict{String, String}() @@ -129,6 +138,7 @@ function SystemModel( generators, region_gen_idxs, storages, region_stor_idxs, generatorstorages, region_genstor_idxs, + demandresponses, region_dr_idxs, lines, interface_line_idxs, timestamps_tz,attrs) @@ -139,6 +149,7 @@ function SystemModel( generators::Generators{N,L,T,P}, storages::Storages{N,L,T,P,E}, generatorstorages::GeneratorStorages{N,L,T,P,E}, + demandresponses::DemandResponses{N,L,T,P,E}, timestamps::StepRange{<:AbstractDateTime,T}, load::Vector{Int}, attrs::Dict{String, String}=Dict{String, String}() @@ -152,6 +163,7 @@ function SystemModel( generators, [1:length(generators)], storages, [1:length(storages)], generatorstorages, [1:length(generatorstorages)], + demandresponses, [1:length(demandresponses)], Lines{N,L,T,P}( String[], String[], Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N), @@ -169,6 +181,7 @@ Base.:(==)(x::T, y::T) where {T <: SystemModel} = x.region_stor_idxs == y.region_stor_idxs && x.generatorstorages == y.generatorstorages && x.region_genstor_idxs == y.region_genstor_idxs && + x.demandresponses == y.demandresponses && x.lines == y.lines && x.interface_line_idxs == y.interface_line_idxs && x.timestamps == y.timestamps && diff --git a/PRASCore.jl/src/Systems/Systems.jl b/PRASCore.jl/src/Systems/Systems.jl index 6b4cd8b1..405e9a80 100644 --- a/PRASCore.jl/src/Systems/Systems.jl +++ b/PRASCore.jl/src/Systems/Systems.jl @@ -12,7 +12,7 @@ export # System assets Regions, Interfaces, - AbstractAssets, Generators, Storages, GeneratorStorages, Lines, + AbstractAssets, Generators, Storages, GeneratorStorages,DemandResponses, Lines, # Units Period, Minute, Hour, Day, Year, diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index bf9d004c..9d332cfa 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -26,8 +26,12 @@ emptygenstors1 = GeneratorStorages{4,1,Hour,MW,MWh}( (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:3)..., (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:2)...) +emptydrs1 = DemandResponses{4,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., + (empty_int(4) for _ in 1:3)..., + (empty_float(4) for _ in 1:5)...) + singlenode_a = SystemModel( - gens1, emptystors1, emptygenstors1, + gens1, emptystors1, emptygenstors1,emptydrs1, ZonedDateTime(2010,1,1,0,tz):Hour(1):ZonedDateTime(2010,1,1,3,tz), [25, 28, 27, 24]) @@ -53,8 +57,12 @@ emptygenstors1_5min = GeneratorStorages{4,5,Minute,MW,MWh}( (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:3)..., (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:2)...) +emptydrs1_5min = DemandResponses{4,5,Minute,MW,MWh}((empty_str for _ in 1:2)..., + (empty_int(4) for _ in 1:3)..., + (empty_float(4) for _ in 1:5)...) + singlenode_a_5min = SystemModel( - gens1_5min, emptystors1_5min, emptygenstors1_5min, + gens1_5min, emptystors1_5min, emptygenstors1_5min,emptydrs1_5min, ZonedDateTime(2010,1,1,0,0,tz):Minute(5):ZonedDateTime(2010,1,1,0,15,tz), [25, 28, 27, 24]) @@ -80,6 +88,10 @@ emptygenstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( (empty_int(6) for _ in 1:3)..., (empty_float(6) for _ in 1:3)..., (empty_int(6) for _ in 1:3)..., (empty_float(6) for _ in 1:2)...) +emptydrs2 = DemandResponses{6,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., + (empty_int(6) for _ in 1:3)..., + (empty_float(6) for _ in 1:5)...) + genstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( ["Genstor1", "Genstor2"], ["Genstorage", "Genstorage"], fill(0, 2, 6), fill(0, 2, 6), fill(4, 2, 6), @@ -88,7 +100,7 @@ genstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( fill(0.0, 2, 6), fill(1.0, 2, 6)) singlenode_b = SystemModel( - gens2, emptystors2, emptygenstors2, + gens2, emptystors2, emptygenstors2,emptydrs2, ZonedDateTime(2015,6,1,0,tz):Hour(1):ZonedDateTime(2015,6,1,5,tz), [28,29,30,31,32,33]) @@ -108,7 +120,7 @@ stors2 = Storages{6,1,Hour,MW,MWh}( fill(0.0, 2, 6), fill(1.0, 2, 6)) singlenode_stor = SystemModel( - gens2, stors2, genstors2, + gens2, stors2, genstors2,emptydrs2, ZonedDateTime(2015,6,1,0,tz):Hour(1):ZonedDateTime(2015,6,1,5,tz), [28,29,30,31,32,33]) @@ -141,7 +153,8 @@ lines = Lines{4,1,Hour,MW}( threenode = SystemModel( regions, interfaces, generators, [1:2, 3:5, 6:8], - emptystors1, fill(1:0, 3), emptygenstors1, fill(1:0, 3), + emptystors1, fill(1:0, 3), emptygenstors1, fill(1:0, 3), + emptydrs1, fill(1:0, 3), lines, [1:1, 2:2, 3:3], ZonedDateTime(2018,10,30,0,tz):Hour(1):ZonedDateTime(2018,10,30,3,tz)) @@ -173,6 +186,10 @@ emptygenstors = GeneratorStorages{1,1,Hour,MW,MWh}( (zeros(Int, 0, 1) for _ in 1:3)..., (zeros(Float64, 0, 1) for _ in 1:3)..., (zeros(Int, 0, 1) for _ in 1:3)..., (zeros(Float64, 0, 1) for _ in 1:2)...) +emptydrs= DemandResponses{1,1,Hour,MW,MWh}((String[] for _ in 1:2)..., + (zeros(Int, 0, 1) for _ in 1:3)..., + (zeros(Float64, 0, 1) for _ in 1:5)...) + interfaces = Interfaces{1,MW}([1], [2], fill(8, 1, 1), fill(8, 1, 1)) lines = Lines{1,1,Hour,MW}( @@ -182,7 +199,7 @@ lines = Lines{1,1,Hour,MW}( zdt = ZonedDateTime(2020,1,1,0, tz) test1 = SystemModel(regions, interfaces, - gens, [1:1, 2:2], emptystors, fill(1:0, 2), emptygenstors, fill(1:0, 2), + gens, [1:1, 2:2], emptystors, fill(1:0, 2), emptygenstors, fill(1:0, 2),emptydrs, fill(1:0, 2), lines, [1:1], zdt:Hour(1):zdt ) @@ -216,7 +233,12 @@ emptygenstors = GeneratorStorages{2,1,Hour,MW,MWh}( (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:3)..., (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:2)...) -test2 = SystemModel(gen, stor, emptygenstors, timestamps, [8, 9]) +emptydrs2 = DemandResponses{2,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., + (empty_int(2) for _ in 1:3)..., + (empty_float(2) for _ in 1:5)...) + + +test2 = SystemModel(gen, stor, emptygenstors,emptydrs2, timestamps, [8, 9]) test2_lole = 0.2 test2_lolps = [0.1, 0.1] @@ -244,6 +266,7 @@ line = Lines{2,1,Hour,MW}( test3 = SystemModel(regions, interfaces, gen, [1:1, 2:1], stor, [1:0, 1:1], emptygenstors, fill(1:0, 2), + emptydrs2, fill(1:0, 2), line, [1:1], timestamps) test3_lole = 0.320951 @@ -267,4 +290,40 @@ test3_util_t = [0.8614, 0.626674] test3_eenergy = [6.561, 7.682202] + +# Test System 4 (Gen + Stor + DR, 1 Region) + +timestamps = ZonedDateTime(2020,1,1,0, tz):Hour(1):ZonedDateTime(2020,1,1,1, tz) + +gen = Generators{2,1,Hour,MW}( + ["Gen 1"], ["Generators"], + fill(10, 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) + +stor = Storages{2,1,Hour,MW,MWh}( + ["Stor 1"], ["Storages"], + fill(10, 1, 2), fill(10, 1, 2), fill(10, 1, 2), + fill(1., 1, 2), fill(1., 1, 2), fill(1., 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) + +emptygenstors = GeneratorStorages{2,1,Hour,MW,MWh}( + (String[] for _ in 1:2)..., + (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:3)..., + (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:2)...) + +dr = DemandResponses{2,1,Hour,MW,MWh}( + ["DR 1"], ["DemandResponses"], + fill(10, 1, 2), fill(10, 1, 2), fill(10, 1, 2), + fill(1., 1, 2), fill(1., 1, 2), fill(1., 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) + +test4= SystemModel(gen, stor, emptygenstors,dr, timestamps, [8, 9]) + +test4_lole = 0.2 +test4_lolps = [0.1, 0.1] + +test4_eue = 1.5542 +test4_eues = [0.8, 0.7542] + +test4_esurplus = [0.18, 1.4022] + +test4_eenergy = [1.62, 2.2842] + end diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 04ee3469..c73af0be 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -487,6 +487,128 @@ function Base.vcat(gen_stors::GeneratorStorages{N,L,T,P,E}...) where {N, L, T, P end +struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAssets{N,L,T,P} + + names::Vector{String} + categories::Vector{String} + + charge_capacity::Matrix{Int} # power + discharge_capacity::Matrix{Int} # power + energy_capacity::Matrix{Int} # energy + + charge_efficiency::Matrix{Float64} + discharge_efficiency::Matrix{Float64} + carryover_efficiency::Matrix{Float64} + + λ::Matrix{Float64} + μ::Matrix{Float64} + + function DemandResponses{N,L,T,P,E}( + names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, + chargecapacity::Matrix{Int}, dischargecapacity::Matrix{Int}, + energycapacity::Matrix{Int}, chargeefficiency::Matrix{Float64}, + dischargeefficiency::Matrix{Float64}, carryoverefficiency::Matrix{Float64}, + λ::Matrix{Float64}, μ::Matrix{Float64} + ) where {N,L,T,P,E} + + n_drs = length(names) + @assert length(categories) == n_drs + @assert allunique(names) + + @assert size(chargecapacity) == (n_drs, N) + @assert size(dischargecapacity) == (n_drs, N) + @assert size(energycapacity) == (n_drs, N) + @assert all(isnonnegative, chargecapacity) + @assert all(isnonnegative, dischargecapacity) + @assert all(isnonnegative, energycapacity) + + @assert size(chargeefficiency) == (n_drs, N) + @assert size(dischargeefficiency) == (n_drs, N) + @assert size(carryoverefficiency) == (n_drs, N) + @assert all(isfractional, chargeefficiency) + @assert all(isfractional, dischargeefficiency) + @assert all(isfractional, carryoverefficiency) + + @assert size(λ) == (n_drs, N) + @assert size(μ) == (n_drs, N) + @assert all(isfractional, λ) + @assert all(isfractional, μ) + + new{N,L,T,P,E}(string.(names), string.(categories), + chargecapacity, dischargecapacity, energycapacity, + chargeefficiency, dischargeefficiency, carryoverefficiency, + λ, μ) + + end + +end + +Base.:(==)(x::T, y::T) where {T <: DemandResponses} = + x.names == y.names && + x.categories == y.categories && + x.charge_capacity == y.charge_capacity && + x.discharge_capacity == y.discharge_capacity && + x.energy_capacity == y.energy_capacity && + x.charge_efficiency == y.charge_efficiency && + x.discharge_efficiency == y.discharge_efficiency && + x.carryover_efficiency == y.carryover_efficiency && + x.λ == y.λ && + x.μ == y.μ + +Base.getindex(dr::DR, idxs::AbstractVector{Int}) where {DR <: DemandResponses} = + DR(dr.names[idxs], dr.categories[idxs],dr.charge_capacity[idxs,:], + dr.discharge_capacity[idxs, :],dr.energy_capacity[idxs, :], + dr.charge_efficiency[idxs, :], dr.discharge_efficiency[idxs, :], + dr.carryover_efficiency[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :]) + +function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} + + n_drs = sum(length(dr) for dr in drs) + + names = Vector{String}(undef, n_drs) + categories = Vector{String}(undef, n_drs) + + charge_capacity = Matrix{Int}(undef, n_drs, N) + discharge_capacity = Matrix{Int}(undef, n_drs, N) + energy_capacity = Matrix{Int}(undef, n_drs, N) + + charge_efficiency = Matrix{Float64}(undef, n_drs, N) + discharge_efficiency = Matrix{Float64}(undef, n_drs, N) + carryover_efficiency = Matrix{Float64}(undef, n_drs, N) + + λ = Matrix{Float64}(undef, n_drs, N) + μ = Matrix{Float64}(undef, n_drs, N) + + last_idx = 0 + + for dr in drs + + n = length(dr) + rows = last_idx .+ (1:n) + + names[rows] = dr.names + categories[rows] = dr.categories + + charge_capacity[rows, :] = dr.charge_capacity + discharge_capacity[rows, :] = dr.discharge_capacity + energy_capacity[rows, :] = dr.energy_capacity + + charge_efficiency[rows, :] = dr.charge_efficiency + discharge_efficiency[rows, :] = dr.discharge_efficiency + carryover_efficiency[rows, :] = dr.carryover_efficiency + + λ[rows, :] = dr.λ + μ[rows, :] = dr.μ + + last_idx += n + + end + + return DemandResponses{N,L,T,P,E}(names, categories, charge_capacity, discharge_capacity, energy_capacity, charge_efficiency, discharge_efficiency, + carryover_efficiency, λ, μ) + +end + """ Lines{N,L,T<:Period,P<:PowerUnit} diff --git a/PRASCore.jl/test/Results/availability.jl b/PRASCore.jl/test/Results/availability.jl index fd06020e..df75424c 100644 --- a/PRASCore.jl/test/Results/availability.jl +++ b/PRASCore.jl/test/Results/availability.jl @@ -41,6 +41,19 @@ @test_throws BoundsError result[r_bad, t] @test_throws BoundsError result[r_bad, t_bad] + # DemandResponses + + result = PRASCore.Results.DemandResponseAvailabilityResult{N,1,Hour}( + DD.resourcenames, DD.periods, available) + + @test length(result[r, t]) == DD.nsamples + @test result[r, t] ≈ vec(available[r_idx, t_idx, :]) + + @test_throws BoundsError result[r, t_bad] + @test_throws BoundsError result[r_bad, t] + @test_throws BoundsError result[r_bad, t_bad] + + # Lines result = PRASCore.Results.LineAvailabilityResult{N,1,Hour}( diff --git a/PRASCore.jl/test/Results/energy.jl b/PRASCore.jl/test/Results/energy.jl index 15f620f1..98d6ab89 100644 --- a/PRASCore.jl/test/Results/energy.jl +++ b/PRASCore.jl/test/Results/energy.jl @@ -34,6 +34,22 @@ @test_throws BoundsError result[r_bad, t] @test_throws BoundsError result[r_bad, t_bad] + # DemandResponses + + result = PRASCore.Results.DemandResponseEnergyResult{N,1,Hour,MWh}( + DD.nsamples, DD.resourcenames, DD.periods, + DD.d1_resourceperiod, DD.d2_period, DD.d2_resourceperiod) + + @test result[t] ≈ (sum(DD.d1_resourceperiod[:, t_idx]), DD.d2_period[t_idx]) + @test result[r, t] ≈ + (DD.d1_resourceperiod[r_idx, t_idx], DD.d2_resourceperiod[r_idx, t_idx]) + + @test_throws BoundsError result[t_bad] + @test_throws BoundsError result[r, t_bad] + @test_throws BoundsError result[r_bad, t] + @test_throws BoundsError result[r_bad, t_bad] + + end @testset "EnergySamplesResult" begin @@ -74,4 +90,20 @@ end @test_throws BoundsError result[r_bad, t] @test_throws BoundsError result[r_bad, t_bad] + # DemandResponses + + result = PRASCore.Results.DemandResponseEnergySamplesResult{N,1,Hour,MWh}( + DD.resourcenames, DD.periods, DD.d) + + @test length(result[t]) == DD.nsamples + @test result[t] ≈ vec(sum(view(DD.d, :, t_idx, :), dims=1)) + + @test length(result[r, t]) == DD.nsamples + @test result[r, t] ≈ vec(DD.d[r_idx, t_idx, :]) + + @test_throws BoundsError result[t_bad] + @test_throws BoundsError result[r, t_bad] + @test_throws BoundsError result[r_bad, t] + @test_throws BoundsError result[r_bad, t_bad] + end diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index 014f11b3..c9117d86 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -48,9 +48,9 @@ assess(TestData.threenode, smallsample, GeneratorAvailability(), LineAvailability(), - StorageAvailability(), GeneratorStorageAvailability(), - StorageEnergy(), GeneratorStorageEnergy(), - StorageEnergySamples(), GeneratorStorageEnergySamples()) + StorageAvailability(), GeneratorStorageAvailability(),DemandResponseAvailability(), + StorageEnergy(), GeneratorStorageEnergy(),DemandResponseEnergy(), + StorageEnergySamples(), GeneratorStorageEnergySamples(),DemandResponseEnergySamples()) @testset "Shortfall Results" begin @@ -409,6 +409,7 @@ TestData.test3_eenergy, simspec.nsamples, nstderr_tol)) + end end diff --git a/PRASCore.jl/test/Systems/SystemModel.jl b/PRASCore.jl/test/Systems/SystemModel.jl index 741b114b..014c75fe 100644 --- a/PRASCore.jl/test/Systems/SystemModel.jl +++ b/PRASCore.jl/test/Systems/SystemModel.jl @@ -17,11 +17,17 @@ rand(1:10, 1, 10), rand(1:10, 1, 10), rand(1:10, 1, 10), fill(0.1, 1, 10), fill(0.5, 1, 10)) + demandresponses = DemandResponses{10,1,Hour,MW,MWh}( + ["S1", "S2"], ["HVAC", "Industrial"], + rand(1:10, 2, 10), rand(1:10, 2, 10), rand(1:10, 2, 10), + fill(0.9, 2, 10), fill(1.0, 2, 10), fill(0.99, 2, 10), + fill(0.1, 2, 10), fill(0.5, 2, 10)) + tz = tz"UTC" timestamps = ZonedDateTime(2020, 1, 1, 0, tz):Hour(1):ZonedDateTime(2020,1,1,9, tz) attrs = Dict("type" => "Single-Region System") single_reg_sys_wo_attrs = SystemModel( - generators, storages, generatorstorages, timestamps, rand(1:20, 10)) + generators, storages, generatorstorages, demandresponses, timestamps, rand(1:20, 10)) single_reg_sys_with_attrs = SystemModel( generators, storages, generatorstorages, timestamps, rand(1:20, 10), attrs) @@ -56,6 +62,7 @@ gen_regions = [1:1, 2:2] stor_regions = [1:0, 1:2] genstor_regions = [1:1, 2:1] + dr_regions = [1:0, 1:2] line_interfaces = [1:2] # Multi-region constructor @@ -63,6 +70,7 @@ regions, interfaces, generators, gen_regions, storages, stor_regions, generatorstorages, genstor_regions, + demandresponses, dr_regions, lines, line_interfaces, timestamps) diff --git a/PRASCore.jl/test/Systems/assets.jl b/PRASCore.jl/test/Systems/assets.jl index 46a256f4..f5fe726c 100644 --- a/PRASCore.jl/test/Systems/assets.jl +++ b/PRASCore.jl/test/Systems/assets.jl @@ -131,6 +131,30 @@ end end + @testset "DemandResponses" begin + + DemandResponses{10,1,Hour,MW,MWh}( + names, categories, vals_int, vals_int, vals_int, + vals_float, vals_float, vals_float, vals_float, vals_float) + + @test_throws AssertionError DemandResponses{5,1,Hour,MW,MWh}( + names, categories, vals_int, vals_int, vals_int, + vals_float, vals_float, vals_float, vals_float, vals_float) + + @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( + names, categories[1:2], vals_int, vals_int, vals_int, + vals_float, vals_float, vals_float, vals_float, vals_float) + + @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( + names[1:2], categories[1:2], vals_int, vals_int, vals_int, + vals_float, vals_float, vals_float, vals_float, vals_float) + + @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( + names, categories, vals_int, vals_int, vals_int, + vals_float, vals_float, -vals_float, vals_float, vals_float) + + end + @testset "Lines" begin lines = Lines{10,1,Hour,MW}( From c0edbf4a0906802ff48095c00ac0701584506188 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 16 Jun 2025 16:17:18 -0600 Subject: [PATCH 02/83] add payback window --- .../src/Simulations/DispatchProblem.jl | 196 ++++++++++++------ PRASCore.jl/src/Simulations/Simulations.jl | 6 +- PRASCore.jl/src/Simulations/SystemState.jl | 4 +- PRASCore.jl/src/Simulations/recording.jl | 6 +- PRASCore.jl/src/Simulations/utils.jl | 58 ++++-- PRASCore.jl/src/Systems/TestData.jl | 56 +++-- PRASCore.jl/src/Systems/assets.jl | 85 ++++---- 7 files changed, 269 insertions(+), 142 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index abddbbe6..1ace4718 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -47,8 +47,8 @@ Nodes in the problem are ordered as: 5. GenerationStorage discharge capacity (GeneratorStorage order) 6. GenerationStorage grid injection (GeneratorStorage order) 7. GenerationStorage charge capacity (GeneratorStorage order) - 8. DemandResponse discharge capacity (DemandResponse order) - 9. DemandResponse charge capacity (DemandResponse order) + 8. DemandResponse payback capacity (DemandResponse order) + 9. DemandResponse banking capacity (DemandResponse order) 10. Slack node Edges are ordered as: @@ -69,10 +69,10 @@ Edges are ordered as: 14. GenerationStorage charge from inflow (GeneratorStorage order) 15. GenerationStorage charge unused (GeneratorStorage order) 16. GenerationStorage inflow unused (GeneratorStorage order) - 17. DemandResponse discharge to grid (DemandResponse order) - 18. DemandResponse discharge unused (DemandResponse order) - 19. DemandResponse charge from grid (DemandResponse order) - 20. DemandResponse charge unused (DemandResponse order) + 17. DemandResponse payback to grid (DemandResponse order) + 18. DemandResponse payback unused (DemandResponse order) + 19. DemandResponse banking from grid (DemandResponse order) + 20. DemandResponse banking unused (DemandResponse order) """ struct DispatchProblem @@ -86,8 +86,8 @@ struct DispatchProblem genstorage_discharge_nodes::UnitRange{Int} genstorage_togrid_nodes::UnitRange{Int} genstorage_charge_nodes::UnitRange{Int} - demandresponse_discharge_nodes::UnitRange{Int} - demandresponse_charge_nodes::UnitRange{Int} + demandresponse_payback_nodes::UnitRange{Int} + demandresponse_bank_nodes::UnitRange{Int} slack_node::Int # Edge labels @@ -107,18 +107,18 @@ struct DispatchProblem genstorage_inflowcharge_edges::UnitRange{Int} genstorage_chargeunused_edges::UnitRange{Int} genstorage_inflowunused_edges::UnitRange{Int} - demandresponse_discharge_edges::UnitRange{Int} - demandresponse_dischargeunused_edges::UnitRange{Int} - demandresponse_charge_edges::UnitRange{Int} - demandresponse_chargeunused_edges::UnitRange{Int} + demandresponse_payback_edges::UnitRange{Int} + demandresponse_paybackunused_edges::UnitRange{Int} + demandresponse_bank_edges::UnitRange{Int} + demandresponse_bankunused_edges::UnitRange{Int} #stor/genstor costs min_chargecost::Int max_dischargecost::Int #dr costs - min_chargecost_dr::Int - max_dischargecost_dr::Int + min_bankcost_dr::Int + max_paybackcost_dr::Int function DispatchProblem( sys::SystemModel; unlimited::Int=999_999_999) @@ -136,9 +136,9 @@ struct DispatchProblem shortagepenalty = 10 * (nifaces + max_dischargecost) #for demandresponse - maxchargetime_dr, maxdischargetime_dr = maxtimetocharge_discharge_dr(sys) - min_chargecost_dr = - maxchargetime_dr - 1 - max_dischargecost_dr = - min_chargecost_dr + maxdischargetime_dr + 1 + maxbanktime_dr, maxpaybacktime_dr = maxtimetobank_payback_dr(sys) + min_bankcost_dr = - maxbanktime_dr - 1 + max_paybackcost_dr = - min_bankcost_dr + maxpaybacktime_dr + 1 stor_regions = assetgrouplist(sys.region_stor_idxs) @@ -152,9 +152,9 @@ struct DispatchProblem genstor_discharge_nodes = indices_after(genstor_inflow_nodes, ngenstors) genstor_togrid_nodes = indices_after(genstor_discharge_nodes, ngenstors) genstor_charge_nodes = indices_after(genstor_togrid_nodes, ngenstors) - dr_charge_nodes = indices_after(genstor_charge_nodes, ndrs) - dr_discharge_nodes = indices_after(dr_charge_nodes, ndrs) - slack_node = nnodes = last(dr_discharge_nodes) + 1 + dr_bank_nodes = indices_after(genstor_charge_nodes, ndrs) + dr_payback_nodes = indices_after(dr_bank_nodes, ndrs) + slack_node = nnodes = last(dr_payback_nodes) + 1 region_unservedenergy = 1:nregions region_unusedcapacity = indices_after(region_unservedenergy, nregions) @@ -172,11 +172,11 @@ struct DispatchProblem genstor_inflowcharge = indices_after(genstor_gridcharge, ngenstors) genstor_chargeunused = indices_after(genstor_inflowcharge, ngenstors) genstor_inflowunused = indices_after(genstor_chargeunused, ngenstors) - dr_chargeused = indices_after(genstor_inflowunused, ndrs) - dr_chargeunused = indices_after(dr_chargeused, ndrs) - dr_dischargeused = indices_after(dr_chargeunused, ndrs) - dr_dischargeunused = indices_after(dr_dischargeused, ndrs) - nedges = last(dr_dischargeunused) + dr_bankused = indices_after(genstor_inflowunused, ndrs) + dr_bankunused = indices_after(dr_bankused, ndrs) + dr_paybackused = indices_after(dr_bankunused, ndrs) + dr_paybackunused = indices_after(dr_paybackused, ndrs) + nedges = last(dr_paybackunused) nodesfrom = Vector{Int}(undef, nedges) nodesto = Vector{Int}(undef, nedges) @@ -230,11 +230,11 @@ struct DispatchProblem initedges(genstor_chargeunused, slack_node, genstor_charge_nodes) initedges(genstor_inflowunused, genstor_inflow_nodes, slack_node) - # Demand Response charging / discharging - initedges(dr_dischargeused, dr_discharge_nodes, dr_regions) - initedges(dr_dischargeunused, dr_discharge_nodes, slack_node) - initedges(dr_chargeused, dr_regions, dr_charge_nodes) - initedges(dr_chargeunused, slack_node, dr_charge_nodes) + # Demand Response banking / payback + initedges(dr_paybackused, dr_payback_nodes, dr_regions) + initedges(dr_paybackunused, dr_payback_nodes, slack_node) + initedges(dr_bankused, dr_regions, dr_bank_nodes) + initedges(dr_bankunused, slack_node, dr_bank_nodes) return new( @@ -244,7 +244,7 @@ struct DispatchProblem region_nodes, stor_discharge_nodes, stor_charge_nodes, genstor_inflow_nodes, genstor_discharge_nodes, genstor_togrid_nodes, genstor_charge_nodes, - dr_charge_nodes,dr_discharge_nodes, slack_node, + dr_bank_nodes,dr_payback_nodes, slack_node, region_unservedenergy, region_unusedcapacity, iface_forward, iface_reverse, @@ -254,10 +254,10 @@ struct DispatchProblem genstor_totalgrid, genstor_gridcharge, genstor_inflowcharge, genstor_chargeunused, genstor_inflowunused, - dr_chargeused, dr_chargeunused, dr_dischargeused, - dr_dischargeunused, + dr_bankused, dr_paybackunused, dr_paybackused, + dr_paybackunused, min_chargecost, max_dischargecost, - min_chargecost_dr, max_dischargecost_dr + min_bankcost_dr, max_paybackcost_dr ) end @@ -318,7 +318,6 @@ function update_problem!( maxenergy = system.storages.energy_capacity[i, t] # Update discharging - maxdischarge = stor_online * system.storages.discharge_capacity[i, t] dischargeefficiency = system.storages.discharge_efficiency[i, t] energydischargeable = stor_energy * dischargeefficiency @@ -426,52 +425,61 @@ function update_problem!( # Update Demand Response charge/discharge limits and priorities - for (i, (charge_node, charge_edge, discharge_node, discharge_edge)) in + for (i, (bank_node, bank_edge, payback_node, payback_edge)) in enumerate(zip( - problem.demandresponse_charge_nodes, problem.demandresponse_charge_edges, - problem.demandresponse_discharge_nodes, problem.demandresponse_discharge_edges)) + problem.demandresponse_bank_nodes, problem.demandresponse_bank_edges, + problem.demandresponse_payback_nodes, problem.demandresponse_payback_edges)) + + #first check if demand response is past allowalable payback window + if state.drs_paybackcounter[i] == -2 + #if payback window is over, set injection + updateinjection!( + fp.nodes[payback_node], slack_node, -state.drs_energy[i]) + continue # No banking possible in same step of load being dropped + end + dr_online = state.drs_available[i] dr_energy = state.drs_energy[i] maxenergy = system.demandresponses.energy_capacity[i, t] - # Update discharging + # Update energy payback - maxdischarge = dr_online * system.demandresponses.discharge_capacity[i, t] - dischargeefficiency = system.demandresponses.discharge_efficiency[i, t] - energydischargeable = dr_energy * dischargeefficiency + maxpayback = dr_online * system.demandresponses.payback_capacity[i, t] + paybackefficiency = system.demandresponses.payback_efficiency[i, t] + energy_payback_allowed = dr_energy * paybackefficiency - if iszero(maxdischarge) - timetodischarge = N + 1 + if iszero(maxpayback) + timetopayback = N + 1 else - timetodischarge = round(Int, energydischargeable / maxdischarge) + timetopayback = round(Int, energy_payback_allowed / maxpayback) end - discharge_capacity = - min(maxdischarge, floor(Int, energytopower( - energydischargeable, E, L, T, P))) + payback_capacity = + min(maxpayback, floor(Int, energytopower( + energy_payback_allowed, E, L, T, P))) updateinjection!( - fp.nodes[discharge_node], slack_node, -discharge_capacity) + fp.nodes[payback_node], slack_node, -payback_capacity) # Largest time-to-discharge = highest priority (discharge first) - dischargecost = problem.max_dischargecost_dr - timetodischarge # Positive cost - updateflowcost!(fp.edges[discharge_edge], dischargecost) + paybackcost = problem.max_paybackcost_dr - timetopayback # Positive cost + updateflowcost!(fp.edges[payback_edge], paybackcost) # Update charging - maxcharge = dr_online * system.demandresponses.charge_capacity[i, t] - chargeefficiency = system.demandresponses.charge_efficiency[i, t] - energychargeable = (maxenergy - dr_energy) / chargeefficiency + maxbank = dr_online * system.demandresponses.bank_capacity[i, t] + bankefficiency = system.demandresponses.bank_efficiency[i, t] + energybankable = (maxenergy - dr_energy) / bankefficiency - charge_capacity = - min(maxcharge, floor(Int, energytopower( - energychargeable, E, L, T, P))) + bank_capacity = + min(maxbank, floor(Int, energytopower( + energybankable, E, L, T, P))) updateinjection!( - fp.nodes[charge_node], slack_node, charge_capacity) + fp.nodes[bank_node], slack_node, bank_capacity) # Smallest time-to-discharge = highest priority (charge first) - chargecost = problem.min_chargecost_dr + timetodischarge # Negative cost - updateflowcost!(fp.edges[charge_edge], chargecost) + bankcost = problem.min_bankcost_dr + timetopayback # Negative cost + updateflowcost!(fp.edges[bank_edge], bankcost) end @@ -521,20 +529,74 @@ function update_state!( end #Demand Response Update - #charging (negative of the flows) - for (i, e) in enumerate(problem.demandresponse_charge_edges) + #banking (negative of the flows) + for (i, e) in enumerate(problem.demandresponse_bank_edges) + #first check if demand response is past allowalable payback window + if state.drs_paybackcounter[i] == -2 + #set back to start of new payback window, count dropped load, and set energy to zero + state.drs_paybackcounter[i] = -1 + state.drs_energy[i] = 0 + continue # No banking possible in same step of load being dropped + end state.drs_energy[i] += - ceil(Int, edges[e].flow * p2e * system.demandresponses.charge_efficiency[i, t]) + ceil(Int, edges[e].flow * p2e * system.demandresponses.bank_efficiency[i, t]) end - - #discharging - for (i, e) in enumerate(problem.demandresponse_discharge_edges) + #paybacking + for (i, e) in enumerate(problem.demandresponse_payback_edges) energy = state.drs_energy[i] - energy_drop = ceil(Int, edges[e].flow * p2e / system.demandresponses.discharge_efficiency[i, t]) + energy_drop = ceil(Int, edges[e].flow * p2e / system.demandresponses.payback_efficiency[i, t]) state.drs_energy[i] = max(0, energy - energy_drop) end + + #=penalty for shortage less than dropping load, higher than everything else- + + simple bool on tracking for when we need to count the window back (don't really care what happens in between) + + ill reach out to him about + + Ignacio-mid day flexible window devices + + + rename charging/discharging + + + load_shift: + + load_banking + + load_curtailment + + + no need to add DR in surplus calculations + + + add it backburner + + Single system + + DR object, generators no transmission + + + penalty for shortage less than dropping load, higher than everything else- + + simple bool on tracking for when we need to count the window back (don't really care what happens in between) + + ill reach out to him about + + Ignacio-mid day flexible window devices + + load_banking + load_reduction + + load_payback + + + =# + + + end function assetgrouplist(idxss::Vector{UnitRange{Int}}) diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index 48e922d6..0c814679 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -186,7 +186,7 @@ function initialize!( fill!(state.stors_energy, 0) fill!(state.genstors_energy, 0) fill!(state.drs_energy, 0) - + fill!(state.drs_paybackcounter, 0) return end @@ -221,8 +221,12 @@ function advance!( update_energy!(state.genstors_energy, system.generatorstorages, t) update_energy!(state.drs_energy, system.demandresponses, t) + update_paybackcounter!(state.drs_paybackcounter,state.drs_energy, system.demandresponses) + + update_problem!(dispatchproblem, state, system, t) + end function solve!( diff --git a/PRASCore.jl/src/Simulations/SystemState.jl b/PRASCore.jl/src/Simulations/SystemState.jl index 7c122752..ac2b9a22 100644 --- a/PRASCore.jl/src/Simulations/SystemState.jl +++ b/PRASCore.jl/src/Simulations/SystemState.jl @@ -14,6 +14,7 @@ struct SystemState drs_available::Vector{Bool} drs_nexttransition::Vector{Int} drs_energy::Vector{Int} + drs_paybackcounter::Vector{Int} lines_available::Vector{Bool} lines_nexttransition::Vector{Int} @@ -38,6 +39,7 @@ struct SystemState drs_available = Vector{Bool}(undef, ndrs) drs_nexttransition = Vector{Int}(undef, ndrs) drs_energy = Vector{Int}(undef, ndrs) + drs_paybackcounter = Vector{Int}(undef, ndrs) nlines = length(system.lines) lines_available = Vector{Bool}(undef, nlines) @@ -47,7 +49,7 @@ struct SystemState gens_available, gens_nexttransition, stors_available, stors_nexttransition, stors_energy, genstors_available, genstors_nexttransition, genstors_energy, - drs_available, drs_nexttransition, drs_energy, + drs_available, drs_nexttransition, drs_energy,drs_paybackcounter, lines_available, lines_nexttransition) end diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 6ec1d1a8..2d6745ec 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -118,7 +118,7 @@ function record!( end for dr in system.region_dr_idxs[r] - dr_idx = problem.demandresponse_chargeunused_edges[dr] + dr_idx = problem.demandresponse_bankunused_edges[dr] regionsurplus += edges[dr_idx].flow end @@ -168,7 +168,7 @@ function record!( end for dr in system.region_dr_idxs[r] - dr_idx = problem.demandresponse_chargeunused_edges[dr] + dr_idx = problem.demandresponse_bankunused_edges[dr] regionsurplus += edges[dr_idx].flow end @@ -336,7 +336,7 @@ end reset!(acc::Results.GenStorAvailabilityAccumulator, sampleid::Int) = nothing -# DemandrResponseAvailability +# DemandResponseAvailability function record!( acc::Results.DRAvailabilityAccumulator, diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 81e2ffbc..6ca72953 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -120,6 +120,36 @@ function update_energy!( end +function update_paybackcounter!( + payback_counter::Vector{Int}, + drs_energy::Vector{Int}, + drs::AbstractAssets +) + + for i in 1:length(payback_counter) + #if energy is zero or negative, set counter to -1 + if drs_energy[i] <= 0 + if payback_counter[i] > 0 + #if no energy banked and counter is positive, reset it to -1 + payback_counter[i] = -1 + end + elseif payback_counter[i] == -1 + #if energy is banked and counter is -1, set it to payback window-start of counting + payback_counter[i] = drs.allowable_payback_period[i] + elseif payback_counter[i] > 0 + #if counter is positive, decrement by one + payback_counter[i] -= 1 + elseif payback_counter[i] == 0 + #if counter is zero, set to -2 (to trigger count load in update_problem!) + payback_counter[i] = -2 + end + + + end + +end + + function maxtimetocharge_discharge(system::SystemModel) if length(system.storages) > 0 @@ -177,32 +207,32 @@ function maxtimetocharge_discharge(system::SystemModel) end -function maxtimetocharge_discharge_dr(system::SystemModel) +function maxtimetobank_payback_dr(system::SystemModel) if length(system.demandresponses) > 0 - if any(iszero, system.demandresponses.charge_capacity) - dr_charge_max = length(system.timestamps) + 1 + if any(iszero, system.demandresponses.bank_capacity) + dr_bank_max = length(system.timestamps) + 1 else - dr_charge_durations = - system.demandresponses.energy_capacity ./ system.demandresponses.charge_capacity - dr_charge_max = ceil(Int, maximum(dr_charge_durations)) + dr_bank_durations = + system.demandresponses.energy_capacity ./ system.demandresponses.bank_capacity + dr_bank_max = ceil(Int, maximum(dr_bank_durations)) end - if any(iszero, system.demandresponses.discharge_capacity) - dr_discharge_max = length(system.timestamps) + 1 + if any(iszero, system.demandresponses.payback_capacity) + dr_payback_max = length(system.timestamps) + 1 else - dr_discharge_durations = - system.demandresponses.energy_capacity ./ system.demandresponses.discharge_capacity - dr_discharge_max = ceil(Int, maximum(dr_discharge_durations)) + dr_payback_durations = + system.demandresponses.energy_capacity ./ system.demandresponses.payback_capacity + dr_payback_max = ceil(Int, maximum(dr_payback_durations)) end else - dr_charge_max = 0 - dr_discharge_max = 0 + dr_bank_max = 0 + dr_payback_max = 0 end - return (dr_charge_max,dr_discharge_max) + return (dr_bank_max,dr_payback_max) end diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index 9d332cfa..fa5054a8 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -28,7 +28,9 @@ emptygenstors1 = GeneratorStorages{4,1,Hour,MW,MWh}( emptydrs1 = DemandResponses{4,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., (empty_int(4) for _ in 1:3)..., - (empty_float(4) for _ in 1:5)...) + (empty_float(4) for _ in 1:3)..., + (empty_int(4) for _ in 1:1)..., + (empty_float(4) for _ in 1:2)...) singlenode_a = SystemModel( gens1, emptystors1, emptygenstors1,emptydrs1, @@ -59,7 +61,9 @@ emptygenstors1_5min = GeneratorStorages{4,5,Minute,MW,MWh}( emptydrs1_5min = DemandResponses{4,5,Minute,MW,MWh}((empty_str for _ in 1:2)..., (empty_int(4) for _ in 1:3)..., - (empty_float(4) for _ in 1:5)...) + (empty_float(4) for _ in 1:3)..., + (empty_int(4) for _ in 1:1)..., + (empty_float(4) for _ in 1:2)...) singlenode_a_5min = SystemModel( gens1_5min, emptystors1_5min, emptygenstors1_5min,emptydrs1_5min, @@ -90,7 +94,9 @@ emptygenstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( emptydrs2 = DemandResponses{6,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., (empty_int(6) for _ in 1:3)..., - (empty_float(6) for _ in 1:5)...) + (empty_float(6) for _ in 1:3)..., + (empty_int(6) for _ in 1:1)..., + (empty_float(6) for _ in 1:2)...) genstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( ["Genstor1", "Genstor2"], ["Genstorage", "Genstorage"], @@ -188,7 +194,9 @@ emptygenstors = GeneratorStorages{1,1,Hour,MW,MWh}( emptydrs= DemandResponses{1,1,Hour,MW,MWh}((String[] for _ in 1:2)..., (zeros(Int, 0, 1) for _ in 1:3)..., - (zeros(Float64, 0, 1) for _ in 1:5)...) + (zeros(Float64, 0, 1) for _ in 1:3)..., + (zeros(Int, 0, 1) for _ in 1:1)..., + (zeros(Float64, 0, 1) for _ in 1:2)...) interfaces = Interfaces{1,MW}([1], [2], fill(8, 1, 1), fill(8, 1, 1)) @@ -235,7 +243,9 @@ emptygenstors = GeneratorStorages{2,1,Hour,MW,MWh}( emptydrs2 = DemandResponses{2,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., (empty_int(2) for _ in 1:3)..., - (empty_float(2) for _ in 1:5)...) + (empty_float(2) for _ in 1:3)..., + (empty_int(2) for _ in 1:1)..., + (empty_float(2) for _ in 1:2)...) test2 = SystemModel(gen, stor, emptygenstors,emptydrs2, timestamps, [8, 9]) @@ -291,30 +301,34 @@ test3_util_t = [0.8614, 0.626674] test3_eenergy = [6.561, 7.682202] -# Test System 4 (Gen + Stor + DR, 1 Region) +# Test System 4 (Gen + DR, 1 Region) -timestamps = ZonedDateTime(2020,1,1,0, tz):Hour(1):ZonedDateTime(2020,1,1,1, tz) +timestamps = ZonedDateTime(2020,1,1,1, tz):Hour(1):ZonedDateTime(2020,1,2,0, tz) -gen = Generators{2,1,Hour,MW}( +gen = Generators{24,1,Hour,MW}( ["Gen 1"], ["Generators"], - fill(10, 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) + fill(10, 1, 24), fill(0.1, 1, 24), fill(0.9, 1, 24)) -stor = Storages{2,1,Hour,MW,MWh}( - ["Stor 1"], ["Storages"], - fill(10, 1, 2), fill(10, 1, 2), fill(10, 1, 2), - fill(1., 1, 2), fill(1., 1, 2), fill(1., 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) - -emptygenstors = GeneratorStorages{2,1,Hour,MW,MWh}( +emptystors = Storages{24,1,Hour,MW,MWh}( (String[] for _ in 1:2)..., - (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:3)..., - (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:2)...) + (zeros(Int, 0, 24) for _ in 1:3)..., + (zeros(Float64, 0, 24) for _ in 1:5)...) + +emptygenstors = GeneratorStorages{24,1,Hour,MW,MWh}( + (String[] for _ in 1:2)..., + (zeros(Int, 0, 24) for _ in 1:3)..., (zeros(Float64, 0, 24) for _ in 1:3)..., + (zeros(Int, 0, 24) for _ in 1:3)..., (zeros(Float64, 0, 24) for _ in 1:2)...) -dr = DemandResponses{2,1,Hour,MW,MWh}( +dr = DemandResponses{24,1,Hour,MW,MWh}( ["DR 1"], ["DemandResponses"], - fill(10, 1, 2), fill(10, 1, 2), fill(10, 1, 2), - fill(1., 1, 2), fill(1., 1, 2), fill(1., 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) + fill(10, 1, 24), fill(10, 1, 24), fill(10, 1, 24), + fill(1., 1, 24), fill(1., 1, 24), fill(1., 1, 24),fill(1, 1, 24), fill(0.1, 1, 24), fill(0.9, 1, 24)) + + +full_day_load_profile = [45,43,42,42,42,44,47,50,52,54,56,58,60,61,63,64,64,63,61,58,55,52,49,46] + -test4= SystemModel(gen, stor, emptygenstors,dr, timestamps, [8, 9]) +test4= SystemModel(gen, emptystors, emptygenstors,dr, timestamps, full_day_load_profile) test4_lole = 0.2 test4_lolps = [0.1, 0.1] diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index c73af0be..75ff91c7 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -492,22 +492,25 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse names::Vector{String} categories::Vector{String} - charge_capacity::Matrix{Int} # power - discharge_capacity::Matrix{Int} # power + bank_capacity::Matrix{Int} # power + payback_capacity::Matrix{Int} # power energy_capacity::Matrix{Int} # energy - charge_efficiency::Matrix{Float64} - discharge_efficiency::Matrix{Float64} + bank_efficiency::Matrix{Float64} + payback_efficiency::Matrix{Float64} carryover_efficiency::Matrix{Float64} + allowable_payback_period::Matrix{Int} + λ::Matrix{Float64} μ::Matrix{Float64} function DemandResponses{N,L,T,P,E}( names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, - chargecapacity::Matrix{Int}, dischargecapacity::Matrix{Int}, - energycapacity::Matrix{Int}, chargeefficiency::Matrix{Float64}, - dischargeefficiency::Matrix{Float64}, carryoverefficiency::Matrix{Float64}, + bankcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, + energycapacity::Matrix{Int}, bankefficiency::Matrix{Float64}, + paybackefficiency::Matrix{Float64}, carryoverefficiency::Matrix{Float64}, + allowablepaybackperiod::Matrix{Int}, λ::Matrix{Float64}, μ::Matrix{Float64} ) where {N,L,T,P,E} @@ -515,28 +518,34 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse @assert length(categories) == n_drs @assert allunique(names) - @assert size(chargecapacity) == (n_drs, N) - @assert size(dischargecapacity) == (n_drs, N) + + @assert size(bankcapacity) == (n_drs, N) + @assert size(paybackcapacity) == (n_drs, N) @assert size(energycapacity) == (n_drs, N) - @assert all(isnonnegative, chargecapacity) - @assert all(isnonnegative, dischargecapacity) + @assert all(isnonnegative, bankcapacity) + @assert all(isnonnegative, paybackcapacity) @assert all(isnonnegative, energycapacity) - @assert size(chargeefficiency) == (n_drs, N) - @assert size(dischargeefficiency) == (n_drs, N) + @assert size(bankefficiency) == (n_drs, N) + @assert size(paybackefficiency) == (n_drs, N) @assert size(carryoverefficiency) == (n_drs, N) - @assert all(isfractional, chargeefficiency) - @assert all(isfractional, dischargeefficiency) + @assert all(isfractional, bankefficiency) + @assert all(isfractional, paybackefficiency) @assert all(isfractional, carryoverefficiency) + @assert size(allowablepaybackperiod) == (n_drs, N) + @assert all(isnonnegative, allowablepaybackperiod) + + @assert size(λ) == (n_drs, N) @assert size(μ) == (n_drs, N) @assert all(isfractional, λ) @assert all(isfractional, μ) new{N,L,T,P,E}(string.(names), string.(categories), - chargecapacity, dischargecapacity, energycapacity, - chargeefficiency, dischargeefficiency, carryoverefficiency, + bankcapacity, paybackcapacity, energycapacity, + bankefficiency, paybackefficiency, carryoverefficiency, + allowablepaybackperiod, λ, μ) end @@ -546,20 +555,21 @@ end Base.:(==)(x::T, y::T) where {T <: DemandResponses} = x.names == y.names && x.categories == y.categories && - x.charge_capacity == y.charge_capacity && - x.discharge_capacity == y.discharge_capacity && + x.bank_capacity == y.bank_capacity && + x.payback_capacity == y.payback_capacity && x.energy_capacity == y.energy_capacity && - x.charge_efficiency == y.charge_efficiency && - x.discharge_efficiency == y.discharge_efficiency && + x.bank_efficiency == y.bank_efficiency && + x.payback_efficiency == y.payback_efficiency && x.carryover_efficiency == y.carryover_efficiency && + x.allowable_payback_period == y.allowable_payback_period && x.λ == y.λ && x.μ == y.μ Base.getindex(dr::DR, idxs::AbstractVector{Int}) where {DR <: DemandResponses} = - DR(dr.names[idxs], dr.categories[idxs],dr.charge_capacity[idxs,:], - dr.discharge_capacity[idxs, :],dr.energy_capacity[idxs, :], - dr.charge_efficiency[idxs, :], dr.discharge_efficiency[idxs, :], - dr.carryover_efficiency[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :]) + DR(dr.names[idxs], dr.categories[idxs],dr.bank_capacity[idxs,:], + dr.payback_capacity[idxs, :],dr.energy_capacity[idxs, :], + dr.bank_efficiency[idxs, :], dr.payback_efficiency[idxs, :], + dr.carryover_efficiency[idxs, :],dr.allowable_payback_period,dr.λ[idxs, :], dr.μ[idxs, :]) function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} @@ -568,14 +578,17 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} names = Vector{String}(undef, n_drs) categories = Vector{String}(undef, n_drs) - charge_capacity = Matrix{Int}(undef, n_drs, N) - discharge_capacity = Matrix{Int}(undef, n_drs, N) + bank_capacity = Matrix{Int}(undef, n_drs, N) + payback_capacity = Matrix{Int}(undef, n_drs, N) energy_capacity = Matrix{Int}(undef, n_drs, N) - charge_efficiency = Matrix{Float64}(undef, n_drs, N) - discharge_efficiency = Matrix{Float64}(undef, n_drs, N) + bank_efficiency = Matrix{Float64}(undef, n_drs, N) + payback_efficiency = Matrix{Float64}(undef, n_drs, N) carryover_efficiency = Matrix{Float64}(undef, n_drs, N) + allowable_payback_period = Matrix{Float64}(undef, n_drs, N) + + λ = Matrix{Float64}(undef, n_drs, N) μ = Matrix{Float64}(undef, n_drs, N) @@ -589,14 +602,16 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} names[rows] = dr.names categories[rows] = dr.categories - charge_capacity[rows, :] = dr.charge_capacity - discharge_capacity[rows, :] = dr.discharge_capacity + bank_capacity[rows, :] = dr.bank_capacity + payback_capacity[rows, :] = dr.payback_capacity energy_capacity[rows, :] = dr.energy_capacity - charge_efficiency[rows, :] = dr.charge_efficiency - discharge_efficiency[rows, :] = dr.discharge_efficiency + bank_efficiency[rows, :] = dr.bank_efficiency + payback_efficiency[rows, :] = dr.payback_efficiency carryover_efficiency[rows, :] = dr.carryover_efficiency + allowable_payback_period[rows, :] = dr.allowable_payback_period + λ[rows, :] = dr.λ μ[rows, :] = dr.μ @@ -604,8 +619,8 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} end - return DemandResponses{N,L,T,P,E}(names, categories, charge_capacity, discharge_capacity, energy_capacity, charge_efficiency, discharge_efficiency, - carryover_efficiency, λ, μ) + return DemandResponses{N,L,T,P,E}(names, categories, bank_capacity, payback_capacity, energy_capacity, bank_efficiency, payback_efficiency, + carryover_efficiency,allowable_payback_period, λ, μ) end From 614d5c7873da480dbe23fc3a22d9b7be67f33e4d Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 16 Jun 2025 18:20:57 -0600 Subject: [PATCH 03/83] working costs/initial state --- .../src/Simulations/DispatchProblem.jl | 24 ++++++++++--------- PRASCore.jl/src/Simulations/Simulations.jl | 2 +- PRASCore.jl/src/Systems/TestData.jl | 6 ++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 1ace4718..ce19e452 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -117,8 +117,8 @@ struct DispatchProblem max_dischargecost::Int #dr costs - min_bankcost_dr::Int - max_paybackcost_dr::Int + max_bankcost_dr::Int + min_paybackcost_dr::Int function DispatchProblem( sys::SystemModel; unlimited::Int=999_999_999) @@ -133,12 +133,14 @@ struct DispatchProblem maxchargetime, maxdischargetime = maxtimetocharge_discharge(sys) min_chargecost = - maxchargetime - 1 max_dischargecost = - min_chargecost + maxdischargetime + 1 - shortagepenalty = 10 * (nifaces + max_dischargecost) - #for demandresponse + #for demandresponse-inverse of storage as we want to payback first and bank last (so payback costs are negative, and banking costs are positive) maxbanktime_dr, maxpaybacktime_dr = maxtimetobank_payback_dr(sys) - min_bankcost_dr = - maxbanktime_dr - 1 - max_paybackcost_dr = - min_bankcost_dr + maxpaybacktime_dr + 1 + min_paybackcost_dr = - maxpaybacktime_dr - 1 + min_chargecost + max_bankcost_dr = - min_paybackcost_dr + maxbanktime_dr + 1 + max_dischargecost + + #for unserved energy + shortagepenalty = 10 * (nifaces + max_bankcost_dr) stor_regions = assetgrouplist(sys.region_stor_idxs) @@ -257,7 +259,7 @@ struct DispatchProblem dr_bankused, dr_paybackunused, dr_paybackused, dr_paybackunused, min_chargecost, max_dischargecost, - min_bankcost_dr, max_paybackcost_dr + max_bankcost_dr, min_paybackcost_dr ) end @@ -461,8 +463,8 @@ function update_problem!( updateinjection!( fp.nodes[payback_node], slack_node, -payback_capacity) - # Largest time-to-discharge = highest priority (discharge first) - paybackcost = problem.max_paybackcost_dr - timetopayback # Positive cost + # Largest time-to-payback = highest priority (payback first) + paybackcost = problem.min_paybackcost_dr - timetopayback # Negative cost updateflowcost!(fp.edges[payback_edge], paybackcost) # Update charging @@ -477,8 +479,8 @@ function update_problem!( updateinjection!( fp.nodes[bank_node], slack_node, bank_capacity) - # Smallest time-to-discharge = highest priority (charge first) - bankcost = problem.min_bankcost_dr + timetopayback # Negative cost + # Smallest time-to-payback = highest priority (bank first) + bankcost = problem.max_bankcost_dr + timetopayback # Positive cost updateflowcost!(fp.edges[bank_edge], bankcost) end diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index 0c814679..84b6ccda 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -186,7 +186,7 @@ function initialize!( fill!(state.stors_energy, 0) fill!(state.genstors_energy, 0) fill!(state.drs_energy, 0) - fill!(state.drs_paybackcounter, 0) + fill!(state.drs_paybackcounter, -1) return end diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index fa5054a8..ae217d51 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -307,7 +307,7 @@ timestamps = ZonedDateTime(2020,1,1,1, tz):Hour(1):ZonedDateTime(2020,1,2,0, tz) gen = Generators{24,1,Hour,MW}( ["Gen 1"], ["Generators"], - fill(10, 1, 24), fill(0.1, 1, 24), fill(0.9, 1, 24)) + fill(57, 1, 24), fill(0.00001, 1, 24), fill(0.9999, 1, 24)) emptystors = Storages{24,1,Hour,MW,MWh}( (String[] for _ in 1:2)..., @@ -320,9 +320,9 @@ emptygenstors = GeneratorStorages{24,1,Hour,MW,MWh}( (zeros(Int, 0, 24) for _ in 1:3)..., (zeros(Float64, 0, 24) for _ in 1:2)...) dr = DemandResponses{24,1,Hour,MW,MWh}( - ["DR 1"], ["DemandResponses"], + ["DR1"], ["DemandResponses"], fill(10, 1, 24), fill(10, 1, 24), fill(10, 1, 24), - fill(1., 1, 24), fill(1., 1, 24), fill(1., 1, 24),fill(1, 1, 24), fill(0.1, 1, 24), fill(0.9, 1, 24)) + fill(1., 1, 24), fill(1., 1, 24), fill(1., 1, 24),fill(6, 1, 24), fill(0.1, 1, 24), fill(0.9, 1, 24)) full_day_load_profile = [45,43,42,42,42,44,47,50,52,54,56,58,60,61,63,64,64,63,61,58,55,52,49,46] From 681b3346c1ab73fc45e060d9e9171380d9da6eed Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 30 Jun 2025 12:22:23 -0600 Subject: [PATCH 04/83] misc fix --- PRASCore.jl/src/Systems/SystemModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index a100d512..52220bcb 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -118,7 +118,7 @@ function SystemModel( generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, - demandresponses::Storages{N,L,T,P,E}, region_dr_idxs::Vector{UnitRange{Int}}, + demandresponses::DemandResponses{N,L,T,P,E}, region_dr_idxs::Vector{UnitRange{Int}}, lines, interface_line_idxs::Vector{UnitRange{Int}}, timestamps::StepRange{DateTime,T}, attrs::Dict{String, String}=Dict{String, String}() From a631622a07cc1ce1edaf64e6fce5ff0ba25cf5b1 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 30 Jun 2025 12:24:54 -0600 Subject: [PATCH 05/83] change DR shortfall calculation to post processing-not through injection --- .../src/Simulations/DispatchProblem.jl | 82 ++++--------------- PRASCore.jl/src/Simulations/recording.jl | 23 ++++-- 2 files changed, 33 insertions(+), 72 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index ce19e452..ca49fbf2 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -287,7 +287,6 @@ function update_problem!( ) - system.regions.load[r, t] updateinjection!(region_node, slack_node, region_netgenavailable) - end # Update bidirectional interface limits (from lines) @@ -432,15 +431,6 @@ function update_problem!( problem.demandresponse_bank_nodes, problem.demandresponse_bank_edges, problem.demandresponse_payback_nodes, problem.demandresponse_payback_edges)) - #first check if demand response is past allowalable payback window - if state.drs_paybackcounter[i] == -2 - #if payback window is over, set injection - updateinjection!( - fp.nodes[payback_node], slack_node, -state.drs_energy[i]) - continue # No banking possible in same step of load being dropped - end - - dr_online = state.drs_available[i] dr_energy = state.drs_energy[i] maxenergy = system.demandresponses.energy_capacity[i, t] @@ -457,9 +447,10 @@ function update_problem!( timetopayback = round(Int, energy_payback_allowed / maxpayback) end - payback_capacity = - min(maxpayback, floor(Int, energytopower( - energy_payback_allowed, E, L, T, P))) + payback_capacity = min( + maxpayback, floor(Int, energytopower( + energy_payback_allowed, E, L, T, P)) + ) updateinjection!( fp.nodes[payback_node], slack_node, -payback_capacity) @@ -472,10 +463,10 @@ function update_problem!( maxbank = dr_online * system.demandresponses.bank_capacity[i, t] bankefficiency = system.demandresponses.bank_efficiency[i, t] energybankable = (maxenergy - dr_energy) / bankefficiency - - bank_capacity = - min(maxbank, floor(Int, energytopower( - energybankable, E, L, T, P))) + bank_capacity = min( + maxbank, floor(Int, energytopower( + energybankable, E, L, T, P)) + ) updateinjection!( fp.nodes[bank_node], slack_node, bank_capacity) @@ -533,12 +524,11 @@ function update_state!( #Demand Response Update #banking (negative of the flows) for (i, e) in enumerate(problem.demandresponse_bank_edges) - #first check if demand response is past allowalable payback window - if state.drs_paybackcounter[i] == -2 - #set back to start of new payback window, count dropped load, and set energy to zero - state.drs_paybackcounter[i] = -1 + if state.drs_paybackcounter[i] == 0 + #if payback window is over, count the unserved energy in drs_unservedenergy state and reset energy + state.drs_unservedenergy[i] = state.drs_energy[i] state.drs_energy[i] = 0 - continue # No banking possible in same step of load being dropped + continue end state.drs_energy[i] += ceil(Int, edges[e].flow * p2e * system.demandresponses.bank_efficiency[i, t]) @@ -546,56 +536,16 @@ function update_state!( #paybacking for (i, e) in enumerate(problem.demandresponse_payback_edges) + if state.drs_paybackcounter[i] == 0 + #if payback window is over, skip the payback + continue + end energy = state.drs_energy[i] energy_drop = ceil(Int, edges[e].flow * p2e / system.demandresponses.payback_efficiency[i, t]) state.drs_energy[i] = max(0, energy - energy_drop) end - #=penalty for shortage less than dropping load, higher than everything else- - - simple bool on tracking for when we need to count the window back (don't really care what happens in between) - - ill reach out to him about - - Ignacio-mid day flexible window devices - - - rename charging/discharging - - - load_shift: - - load_banking - - load_curtailment - - - no need to add DR in surplus calculations - - - add it backburner - - Single system - - DR object, generators no transmission - - - penalty for shortage less than dropping load, higher than everything else- - - simple bool on tracking for when we need to count the window back (don't really care what happens in between) - - ill reach out to him about - - Ignacio-mid day flexible window devices - - load_banking - load_reduction - - load_payback - - - =# diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 2d6745ec..77e5e74a 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -12,13 +12,21 @@ function record!( edges = problem.fp.edges - for r in problem.region_unserved_edges + for (r, dr_idxs) in zip(problem.region_unserved_edges, system.region_dr_idxs) + #count region shortfall and include dr shortfall regionshortfall = edges[r].flow + dr_shortfall = 0 + for i in dr_idxs + dr_shortfall += state.drs_paybackcounter[i] == 0 ? state.drs_unservedenergy[i] : 0 + end + regionshortfall += dr_shortfall isregionshortfall = regionshortfall > 0 + fit!(acc.periodsdropped_regionperiod[r,t], isregionshortfall) fit!(acc.unservedload_regionperiod[r,t], regionshortfall) + fit!(acc.unserveddrload_regionperiod[r,t], dr_shortfall) if isregionshortfall @@ -27,9 +35,9 @@ function record!( acc.periodsdropped_region_currentsim[r] += 1 acc.unservedload_region_currentsim[r] += regionshortfall + acc.unserveddrload_region_currentsim[r] += dr_shortfall end - end if isshortfall @@ -53,6 +61,8 @@ function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) for r in eachindex(acc.periodsdropped_region) fit!(acc.periodsdropped_region[r], acc.periodsdropped_region_currentsim[r]) fit!(acc.unservedload_region[r], acc.unservedload_region_currentsim[r]) + fit!(acc.unserveddrload_region[r], acc.unserveddrload_region_currentsim[r]) + end # Reset for new simulation @@ -60,6 +70,7 @@ function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) fill!(acc.periodsdropped_region_currentsim, 0) acc.unservedload_total_currentsim = 0 fill!(acc.unservedload_region_currentsim, 0) + fill!(acc.unserveddrload_region_currentsim, 0) return @@ -117,11 +128,11 @@ function record!( end - for dr in system.region_dr_idxs[r] + #=for dr in system.region_dr_idxs[r] dr_idx = problem.demandresponse_bankunused_edges[dr] regionsurplus += edges[dr_idx].flow end - + =# fit!(acc.surplus_regionperiod[r,t], regionsurplus) totalsurplus += regionsurplus @@ -167,11 +178,11 @@ function record!( end - for dr in system.region_dr_idxs[r] + #=for dr in system.region_dr_idxs[r] dr_idx = problem.demandresponse_bankunused_edges[dr] regionsurplus += edges[dr_idx].flow end - + =# acc.surplus[r, t, sampleid] = regionsurplus end From 3113c2a47aa17fb53766accb1114befde51a3ae8 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 30 Jun 2025 12:26:15 -0600 Subject: [PATCH 06/83] add unserved energy state var (part 2 of post processing fix) --- PRASCore.jl/src/Simulations/Simulations.jl | 1 + PRASCore.jl/src/Simulations/SystemState.jl | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index 84b6ccda..0fdfc2ce 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -186,6 +186,7 @@ function initialize!( fill!(state.stors_energy, 0) fill!(state.genstors_energy, 0) fill!(state.drs_energy, 0) + fill!(state.drs_unservedenergy, 0) fill!(state.drs_paybackcounter, -1) return diff --git a/PRASCore.jl/src/Simulations/SystemState.jl b/PRASCore.jl/src/Simulations/SystemState.jl index ac2b9a22..2e249771 100644 --- a/PRASCore.jl/src/Simulations/SystemState.jl +++ b/PRASCore.jl/src/Simulations/SystemState.jl @@ -15,7 +15,7 @@ struct SystemState drs_nexttransition::Vector{Int} drs_energy::Vector{Int} drs_paybackcounter::Vector{Int} - + drs_unservedenergy::Vector{Int} lines_available::Vector{Bool} lines_nexttransition::Vector{Int} @@ -39,6 +39,7 @@ struct SystemState drs_available = Vector{Bool}(undef, ndrs) drs_nexttransition = Vector{Int}(undef, ndrs) drs_energy = Vector{Int}(undef, ndrs) + drs_unservedenergy = Vector{Int}(undef, ndrs) drs_paybackcounter = Vector{Int}(undef, ndrs) nlines = length(system.lines) @@ -49,7 +50,8 @@ struct SystemState gens_available, gens_nexttransition, stors_available, stors_nexttransition, stors_energy, genstors_available, genstors_nexttransition, genstors_energy, - drs_available, drs_nexttransition, drs_energy,drs_paybackcounter, + drs_available, drs_nexttransition, drs_energy, + drs_paybackcounter,drs_unservedenergy, lines_available, lines_nexttransition) end From b05e1173840aba4b8cefc30e7ac9fa518af5b785 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 30 Jun 2025 12:27:12 -0600 Subject: [PATCH 07/83] add dr shortfall tracking --- PRASCore.jl/src/Results/Shortfall.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/PRASCore.jl/src/Results/Shortfall.jl b/PRASCore.jl/src/Results/Shortfall.jl index e76c00c1..c30d62dc 100644 --- a/PRASCore.jl/src/Results/Shortfall.jl +++ b/PRASCore.jl/src/Results/Shortfall.jl @@ -58,12 +58,15 @@ mutable struct ShortfallAccumulator <: ResultAccumulator{Shortfall} # Cross-simulation UE mean/variances unservedload_total::MeanVariance unservedload_region::Vector{MeanVariance} + unserveddrload_region::Vector{MeanVariance} unservedload_period::Vector{MeanVariance} unservedload_regionperiod::Matrix{MeanVariance} + unserveddrload_regionperiod::Matrix{MeanVariance} # Running UE totals for current simulation unservedload_total_currentsim::Int unservedload_region_currentsim::Vector{Int} + unserveddrload_region_currentsim::Vector{Int} end @@ -83,19 +86,22 @@ function accumulator( unservedload_total = meanvariance() unservedload_region = [meanvariance() for _ in 1:nregions] + unserveddrload_region = [meanvariance() for _ in 1:nregions] unservedload_period = [meanvariance() for _ in 1:N] unservedload_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] + unserveddrload_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] unservedload_total_currentsim = 0 unservedload_region_currentsim = zeros(Int, nregions) + unserveddrload_region_currentsim = zeros(Int, nregions) return ShortfallAccumulator( periodsdropped_total, periodsdropped_region, periodsdropped_period, periodsdropped_regionperiod, periodsdropped_total_currentsim, periodsdropped_region_currentsim, - unservedload_total, unservedload_region, - unservedload_period, unservedload_regionperiod, - unservedload_total_currentsim, unservedload_region_currentsim) + unservedload_total, unservedload_region,unserveddrload_region, + unservedload_period, unservedload_regionperiod,unserveddrload_regionperiod, + unservedload_total_currentsim, unservedload_region_currentsim,unserveddrload_region_currentsim) end @@ -110,8 +116,10 @@ function merge!( merge!(x.unservedload_total, y.unservedload_total) foreach(merge!, x.unservedload_region, y.unservedload_region) + foreach(merge!, x.unserveddrload_region, y.unserveddrload_region) foreach(merge!, x.unservedload_period, y.unservedload_period) foreach(merge!, x.unservedload_regionperiod, y.unservedload_regionperiod) + foreach(merge!, x.unserveddrload_regionperiod, y.unserveddrload_regionperiod) return From 33c533d360a7d713ec6e69ffcda9067bef9d1313 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 30 Jun 2025 12:27:50 -0600 Subject: [PATCH 08/83] simplify payback counter tracker --- PRASCore.jl/src/Simulations/utils.jl | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 6ca72953..db1386a3 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -127,21 +127,18 @@ function update_paybackcounter!( ) for i in 1:length(payback_counter) - #if energy is zero or negative, set counter to -1 + #if energy is zero or negative, set counter to -1 (to trigger dropped load in update_problem!) if drs_energy[i] <= 0 - if payback_counter[i] > 0 + if payback_counter[i] >= 0 #if no energy banked and counter is positive, reset it to -1 payback_counter[i] = -1 end elseif payback_counter[i] == -1 #if energy is banked and counter is -1, set it to payback window-start of counting - payback_counter[i] = drs.allowable_payback_period[i] - elseif payback_counter[i] > 0 + payback_counter[i] = drs.allowable_payback_period[i]-1 + elseif payback_counter[i] >= 0 #if counter is positive, decrement by one payback_counter[i] -= 1 - elseif payback_counter[i] == 0 - #if counter is zero, set to -2 (to trigger count load in update_problem!) - payback_counter[i] = -2 end From 8f25a1b4803aa50dc141cbcc1b7673eb6db512a6 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 1 Jul 2025 10:04:18 -0600 Subject: [PATCH 09/83] update intra-DR device ordering --- .../src/Simulations/DispatchProblem.jl | 23 ++++++++--------- PRASCore.jl/src/Simulations/utils.jl | 25 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index ca49fbf2..0cf46005 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -134,10 +134,10 @@ struct DispatchProblem min_chargecost = - maxchargetime - 1 max_dischargecost = - min_chargecost + maxdischargetime + 1 - #for demandresponse-inverse of storage as we want to payback first and bank last (so payback costs are negative, and banking costs are positive) - maxbanktime_dr, maxpaybacktime_dr = maxtimetobank_payback_dr(sys) - min_paybackcost_dr = - maxpaybacktime_dr - 1 + min_chargecost - max_bankcost_dr = - min_paybackcost_dr + maxbanktime_dr + 1 + max_dischargecost + #for demand response-we want to bank energy in devices with the longest payback window, and payback energy from devices with the smallest payback window + minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) + min_paybackcost_dr = - maxpaybacktime_dr - 1 + min_chargecost #add min_chargecost to always have DR devices be paybacked first + max_bankcost_dr = - min_paybackcost_dr + minpaybacktime_dr + 1 + max_dischargecost #add max_dischargecost to always have DR devices be banked last #for unserved energy shortagepenalty = 10 * (nifaces + max_bankcost_dr) @@ -442,9 +442,9 @@ function update_problem!( energy_payback_allowed = dr_energy * paybackefficiency if iszero(maxpayback) - timetopayback = N + 1 + allowablepayback = N + 1 else - timetopayback = round(Int, energy_payback_allowed / maxpayback) + allowablepayback = system.demandresponses.allowable_payback_period[i] end payback_capacity = min( @@ -454,12 +454,11 @@ function update_problem!( updateinjection!( fp.nodes[payback_node], slack_node, -payback_capacity) - # Largest time-to-payback = highest priority (payback first) - paybackcost = problem.min_paybackcost_dr - timetopayback # Negative cost + # smallest allowable payback window = highest priority (payback first) + paybackcost = problem.min_paybackcost_dr + allowablepayback # Negative cost updateflowcost!(fp.edges[payback_edge], paybackcost) - # Update charging - + # Update banking maxbank = dr_online * system.demandresponses.bank_capacity[i, t] bankefficiency = system.demandresponses.bank_efficiency[i, t] energybankable = (maxenergy - dr_energy) / bankefficiency @@ -470,8 +469,8 @@ function update_problem!( updateinjection!( fp.nodes[bank_node], slack_node, bank_capacity) - # Smallest time-to-payback = highest priority (bank first) - bankcost = problem.max_bankcost_dr + timetopayback # Positive cost + # Longest allowable payback window = highest priority (bank first) + bankcost = problem.max_bankcost_dr - allowablepayback # Positive cost updateflowcost!(fp.edges[bank_edge], bankcost) end diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index db1386a3..a42c5889 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -127,7 +127,7 @@ function update_paybackcounter!( ) for i in 1:length(payback_counter) - #if energy is zero or negative, set counter to -1 (to trigger dropped load in update_problem!) + #if energy is zero or negative, set counter to -1 (to start counting new) if drs_energy[i] <= 0 if payback_counter[i] >= 0 #if no energy banked and counter is positive, reset it to -1 @@ -204,32 +204,27 @@ function maxtimetocharge_discharge(system::SystemModel) end -function maxtimetobank_payback_dr(system::SystemModel) +function minmax_payback_window_dr(system::SystemModel) if length(system.demandresponses) > 0 - if any(iszero, system.demandresponses.bank_capacity) - dr_bank_max = length(system.timestamps) + 1 + if any(iszero, system.demandresponses.allowable_payback_period) + maxpaybacktime_dr = length(system.timestamps) + 1 else - dr_bank_durations = - system.demandresponses.energy_capacity ./ system.demandresponses.bank_capacity - dr_bank_max = ceil(Int, maximum(dr_bank_durations)) + maxpaybacktime_dr = maximum(system.demandresponses.allowable_payback_period) end if any(iszero, system.demandresponses.payback_capacity) - dr_payback_max = length(system.timestamps) + 1 + minpaybacktime_dr = length(system.timestamps) + 1 else - dr_payback_durations = - system.demandresponses.energy_capacity ./ system.demandresponses.payback_capacity - dr_payback_max = ceil(Int, maximum(dr_payback_durations)) + minpaybacktime_dr = minimum(system.demandresponses.allowable_payback_period) end else - dr_bank_max = 0 - dr_payback_max = 0 - + minpaybacktime_dr = 0 + maxpaybacktime_dr = 0 end - return (dr_bank_max,dr_payback_max) + return (minpaybacktime_dr, maxpaybacktime_dr) end From 1052b93c134c5fbcbca951f98583ad0e140117ae Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 1 Jul 2025 10:08:01 -0600 Subject: [PATCH 10/83] fix DR bank/payback before coutning unserved energy --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 0cf46005..6c33dace 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -523,25 +523,21 @@ function update_state!( #Demand Response Update #banking (negative of the flows) for (i, e) in enumerate(problem.demandresponse_bank_edges) - if state.drs_paybackcounter[i] == 0 - #if payback window is over, count the unserved energy in drs_unservedenergy state and reset energy - state.drs_unservedenergy[i] = state.drs_energy[i] - state.drs_energy[i] = 0 - continue - end state.drs_energy[i] += ceil(Int, edges[e].flow * p2e * system.demandresponses.bank_efficiency[i, t]) end #paybacking for (i, e) in enumerate(problem.demandresponse_payback_edges) - if state.drs_paybackcounter[i] == 0 - #if payback window is over, skip the payback - continue - end energy = state.drs_energy[i] energy_drop = ceil(Int, edges[e].flow * p2e / system.demandresponses.payback_efficiency[i, t]) state.drs_energy[i] = max(0, energy - energy_drop) + + if state.drs_paybackcounter[i] == 0 + #if payback window is over, count the unserved energy in drs_unservedenergy state and reset energy + state.drs_unservedenergy[i] = state.drs_energy[i] + state.drs_energy[i] = 0 + end end From 8b6fe9394ce0757ae7f1293233813582bbfbeefb Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 1 Jul 2025 10:11:10 -0600 Subject: [PATCH 11/83] improve docs --- PRASCore.jl/src/Results/DemandResponseAvailability.jl | 4 ++-- PRASCore.jl/src/Results/DemandResponseEnergy.jl | 9 ++++----- PRASCore.jl/src/Results/DemandResponseEnergySamples.jl | 6 +++--- PRASCore.jl/src/Simulations/DispatchProblem.jl | 10 ++++++++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/PRASCore.jl/src/Results/DemandResponseAvailability.jl b/PRASCore.jl/src/Results/DemandResponseAvailability.jl index 12bb5443..72ae6f6c 100644 --- a/PRASCore.jl/src/Results/DemandResponseAvailability.jl +++ b/PRASCore.jl/src/Results/DemandResponseAvailability.jl @@ -1,7 +1,7 @@ """ DemandResponse -The `DemandResposneAvailability` result specification reports the sample-level +The `DemandResponseAvailability` result specification reports the sample-level discrete availability of `DemandResponses`, producing a `DemandResponseAvailabilityResult`. A `DemandResponseAvailabilityResult` can be indexed by demand response device name and @@ -34,7 +34,7 @@ function accumulator( sys::SystemModel{N}, nsamples::Int, ::DemandResponseAvailability ) where {N} - ndrs = length(sys.storages) + ndrs = length(sys.demandresponses) available = zeros(Bool, ndrs, N, nsamples) return DRAvailabilityAccumulator(available) diff --git a/PRASCore.jl/src/Results/DemandResponseEnergy.jl b/PRASCore.jl/src/Results/DemandResponseEnergy.jl index 34be40aa..eaf3bd30 100644 --- a/PRASCore.jl/src/Results/DemandResponseEnergy.jl +++ b/PRASCore.jl/src/Results/DemandResponseEnergy.jl @@ -1,12 +1,12 @@ """ DemandResponseEnergy -The `DemandResponseEnergy` result specification reports the average state of charge +The `DemandResponseEnergy` result specification reports the average energy banked of `DemandResponses`, producing a `DemandResponseEnergyResult`. A `DemandResponseEnergyResult` can be indexed by demand response device name and a timestamp to retrieve a tuple of sample mean and standard deviation, estimating the average -energy level for the given demand response device in that timestep. +energy banked level for the given demand response device in that timestep. Example: @@ -18,10 +18,9 @@ soc_mean, soc_std = drenergy["MyDemandResponse123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] ``` -See [`DemandResponseEnergySamples`](@ref) for sample-level demand response states of charge. +See [`DemandResponseEnergySamples`](@ref) for sample-level demand response states of banked energy. -See [`GeneratorStorageEnergy`](@ref) for average generator-storage states -of charge. +See [`DemandResponseEnergy`](@ref) for average demand-response banked energy. """ struct DemandResponseEnergy <: ResultSpec end diff --git a/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl index e282a3bb..183ac20a 100644 --- a/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl +++ b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl @@ -2,10 +2,10 @@ DemandResponseEnergySamples The `DemandResponseEnergySamples` result specification reports the sample-level state -of charge of `DemandResponses`, producing a `DemandResponseEnergySamplesResult`. +of banked energy of `DemandResponses`, producing a `DemandResponseEnergySamplesResult`. A `DemandResponseEnergySamplesResult` can be indexed by demand response device name and -a timestamp to retrieve a vector of sample-level charge states for +a timestamp to retrieve a vector of sample-level banked energy states for the device in the given timestep. Example: @@ -22,7 +22,7 @@ samples = demandresponseenergy["MyDemandResponse123", ZonedDateTime(2020, 1, 1, Note that this result specification requires large amounts of memory for larger sample sizes. See [`DemandResponseEnergy`](@ref) for estimated average demand response -state of charge when sample-level granularity isn't required. +banked energy when sample-level granularity isn't required. """ struct DemandResponseEnergySamples <: ResultSpec end diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 6c33dace..57989cb4 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -4,14 +4,17 @@ Create a min-cost flow problem for the multi-region max power delivery problem with generation and storage discharging in decreasing order of priority, and storage charging with excess capacity. Storage and GeneratorStorage devices -within a region are represented individually on the network. +within a region are represented individually on the network. Demand Response +devices will bank energy in devices with the longest payback window first, +and vice versa for payback energy. This involves injections/withdrawals at one node (regional capacity surplus/shortfall) for each modelled region, as well as two/three nodes associated with each Storage/GeneratorStorage device, and a supplementary "slack" node in the network that can absorb undispatched power or pass unserved energy or unused charging capability through to satisfy -power balance constraints. +power balance constraints. Demand Response devices are represented +in a structurally similar manner as storage charging and discharging. Flows from the generation nodes are free, while flows to charging and from discharging nodes are costed or rewarded according to the @@ -23,6 +26,9 @@ dispatch strategy of Evans, Tindemans, and Angeli, as outlined in "Minimizing Unserved Energy Using Heterogenous Storage Units" (IEEE Transactions on Power Systems, 2019). +Demand Response devices are utilized only after discharging all storage/genstor +and paid back before storage/genstor charging. + Flows to the charging node have an attenuated negative cost (reward), incentivizing immediate storage charging if generation and transmission allows it, while avoiding charging by discharging other storage (since that From e593bd86febe8002add5c7f28eda02786166aee0 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 1 Jul 2025 10:24:51 -0600 Subject: [PATCH 12/83] update allowable payback period to be time dependent --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 2 +- PRASCore.jl/src/Simulations/Simulations.jl | 2 +- PRASCore.jl/src/Simulations/utils.jl | 5 +++-- PRASCore.jl/src/Systems/assets.jl | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 57989cb4..cb6546c5 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -450,7 +450,7 @@ function update_problem!( if iszero(maxpayback) allowablepayback = N + 1 else - allowablepayback = system.demandresponses.allowable_payback_period[i] + allowablepayback = system.demandresponses.allowable_payback_period[i,t] end payback_capacity = min( diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index 0fdfc2ce..9a2a5d5e 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -222,7 +222,7 @@ function advance!( update_energy!(state.genstors_energy, system.generatorstorages, t) update_energy!(state.drs_energy, system.demandresponses, t) - update_paybackcounter!(state.drs_paybackcounter,state.drs_energy, system.demandresponses) + update_paybackcounter!(state.drs_paybackcounter,state.drs_energy, system.demandresponses,t) update_problem!(dispatchproblem, state, system, t) diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index a42c5889..3b81d26e 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -123,7 +123,8 @@ end function update_paybackcounter!( payback_counter::Vector{Int}, drs_energy::Vector{Int}, - drs::AbstractAssets + drs::AbstractAssets, + t::Int ) for i in 1:length(payback_counter) @@ -135,7 +136,7 @@ function update_paybackcounter!( end elseif payback_counter[i] == -1 #if energy is banked and counter is -1, set it to payback window-start of counting - payback_counter[i] = drs.allowable_payback_period[i]-1 + payback_counter[i] = drs.allowable_payback_period[i,t]-1 elseif payback_counter[i] >= 0 #if counter is positive, decrement by one payback_counter[i] -= 1 diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 75ff91c7..489a4823 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -569,7 +569,7 @@ Base.getindex(dr::DR, idxs::AbstractVector{Int}) where {DR <: DemandResponses} = DR(dr.names[idxs], dr.categories[idxs],dr.bank_capacity[idxs,:], dr.payback_capacity[idxs, :],dr.energy_capacity[idxs, :], dr.bank_efficiency[idxs, :], dr.payback_efficiency[idxs, :], - dr.carryover_efficiency[idxs, :],dr.allowable_payback_period,dr.λ[idxs, :], dr.μ[idxs, :]) + dr.carryover_efficiency[idxs, :],dr.allowable_payback_period[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :]) function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} From 85bd0ef2d53a048749d3b8b7b972158bf62d2634 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 1 Jul 2025 14:38:18 -0600 Subject: [PATCH 13/83] fix test DR inputs --- PRASCore.jl/test/Systems/SystemModel.jl | 2 +- PRASCore.jl/test/Systems/assets.jl | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PRASCore.jl/test/Systems/SystemModel.jl b/PRASCore.jl/test/Systems/SystemModel.jl index 014c75fe..707b3f72 100644 --- a/PRASCore.jl/test/Systems/SystemModel.jl +++ b/PRASCore.jl/test/Systems/SystemModel.jl @@ -21,7 +21,7 @@ ["S1", "S2"], ["HVAC", "Industrial"], rand(1:10, 2, 10), rand(1:10, 2, 10), rand(1:10, 2, 10), fill(0.9, 2, 10), fill(1.0, 2, 10), fill(0.99, 2, 10), - fill(0.1, 2, 10), fill(0.5, 2, 10)) + fill(4, 2, 10),fill(0.1, 2, 10), fill(0.5, 2, 10)) tz = tz"UTC" timestamps = ZonedDateTime(2020, 1, 1, 0, tz):Hour(1):ZonedDateTime(2020,1,1,9, tz) diff --git a/PRASCore.jl/test/Systems/assets.jl b/PRASCore.jl/test/Systems/assets.jl index f5fe726c..7d90edf0 100644 --- a/PRASCore.jl/test/Systems/assets.jl +++ b/PRASCore.jl/test/Systems/assets.jl @@ -135,23 +135,23 @@ DemandResponses{10,1,Hour,MW,MWh}( names, categories, vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_float, vals_float) + vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) @test_throws AssertionError DemandResponses{5,1,Hour,MW,MWh}( names, categories, vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_float, vals_float) + vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( names, categories[1:2], vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_float, vals_float) + vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( names[1:2], categories[1:2], vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_float, vals_float) + vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( names, categories, vals_int, vals_int, vals_int, - vals_float, vals_float, -vals_float, vals_float, vals_float) + vals_float, vals_float, -vals_float, vals_int, vals_float, vals_float) end From d8a2cc69f895dbc496d063cf2481a33ed55b5635 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 1 Jul 2025 14:38:48 -0600 Subject: [PATCH 14/83] add two tests for DR additions --- PRASCore.jl/src/Systems/TestData.jl | 96 ++++++++++++++++++------ PRASCore.jl/test/Simulations/runtests.jl | 96 ++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 23 deletions(-) diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index ae217d51..3bc93df4 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -301,43 +301,93 @@ test3_util_t = [0.8614, 0.626674] test3_eenergy = [6.561, 7.682202] -# Test System 4 (Gen + DR, 1 Region) +# Test System 4 (Gen + DR, 1 Region for 6 hours) -timestamps = ZonedDateTime(2020,1,1,1, tz):Hour(1):ZonedDateTime(2020,1,2,0, tz) +timestamps = ZonedDateTime(2020,1,1,1, tz):Hour(1):ZonedDateTime(2020,1,1,6, tz) -gen = Generators{24,1,Hour,MW}( +gen = Generators{6,1,Hour,MW}( ["Gen 1"], ["Generators"], - fill(57, 1, 24), fill(0.00001, 1, 24), fill(0.9999, 1, 24)) + fill(57, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) -emptystors = Storages{24,1,Hour,MW,MWh}( +emptystors = Storages{6,1,Hour,MW,MWh}( (String[] for _ in 1:2)..., - (zeros(Int, 0, 24) for _ in 1:3)..., - (zeros(Float64, 0, 24) for _ in 1:5)...) - -emptygenstors = GeneratorStorages{24,1,Hour,MW,MWh}( + (zeros(Int, 0, 6) for _ in 1:3)..., + (zeros(Float64, 0, 6) for _ in 1:5)...) + +emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( + (String[] for _ in 1:2)..., + (zeros(Int, 0, 6) for _ in 1:3)..., (zeros(Float64, 0, 6) for _ in 1:3)..., + (zeros(Int, 0, 6) for _ in 1:3)..., (zeros(Float64, 0, 6) for _ in 1:2)...) + +dr = DemandResponses{6,1,Hour,MW,MWh}( + ["DR1"], ["DemandResponses"], + fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), + fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6),fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) + + +full_day_load_profile = [56,58,60,61,59,53] + + +test4 = SystemModel(gen, emptystors, emptygenstors,dr, timestamps, full_day_load_profile) + +test4_lole = 1.99 +test4_lolps = [0.0998, 0.2629, 0.33, 0.8603, 0.2638, 0.1733] + +test4_eue = 40.75 +test4_eues = [4.69, 5.15, 6.94, 12.99, 6.03, 4.94] + +test4_esurplus = [0.9,0,0,0,0,1.98] + +test4_eenergy = [0.8986299999999925, + 2.4561559999999782, + 4.254399000000163, + 0.997662000000021, + 2.6729959999999235, + 1.388832000000018] + + +# Test System 5 (Gen + DR + Stor, 1 Region for 6 hours) + +timestamps = ZonedDateTime(2020,1,1,1, tz):Hour(1):ZonedDateTime(2020,1,1,6, tz) + +gen = Generators{6,1,Hour,MW}( + ["Gen 1"], ["Generators"], + fill(57, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) + +stor = Storages{6,1,Hour,MW,MWh}( + ["Stor 1"], ["Storages"], + fill(5, 1, 6), fill(5, 1, 6), fill(5, 1, 6), + fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) + +emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( (String[] for _ in 1:2)..., - (zeros(Int, 0, 24) for _ in 1:3)..., (zeros(Float64, 0, 24) for _ in 1:3)..., - (zeros(Int, 0, 24) for _ in 1:3)..., (zeros(Float64, 0, 24) for _ in 1:2)...) + (zeros(Int, 0, 6) for _ in 1:3)..., (zeros(Float64, 0, 6) for _ in 1:3)..., + (zeros(Int, 0, 6) for _ in 1:3)..., (zeros(Float64, 0, 6) for _ in 1:2)...) -dr = DemandResponses{24,1,Hour,MW,MWh}( +dr = DemandResponses{6,1,Hour,MW,MWh}( ["DR1"], ["DemandResponses"], - fill(10, 1, 24), fill(10, 1, 24), fill(10, 1, 24), - fill(1., 1, 24), fill(1., 1, 24), fill(1., 1, 24),fill(6, 1, 24), fill(0.1, 1, 24), fill(0.9, 1, 24)) + fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), + fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6),fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) -full_day_load_profile = [45,43,42,42,42,44,47,50,52,54,56,58,60,61,63,64,64,63,61,58,55,52,49,46] +full_day_load_profile = [56,58,60,61,59,53] -test4= SystemModel(gen, emptystors, emptygenstors,dr, timestamps, full_day_load_profile) +test5 = SystemModel(gen, stor, emptygenstors,dr, timestamps, full_day_load_profile) -test4_lole = 0.2 -test4_lolps = [0.1, 0.1] +test5_lole = 1.969 +test5_lolps = [0.0998, 0.1974, 0.3301, 0.4261, 0.6986, 0.2173] -test4_eue = 1.5542 -test4_eues = [0.8, 0.7542] +test5_eue = 41.18 +test5_eues = [4.69, 5.01, 6.88, 8.33, 11.23, 5.05] -test4_esurplus = [0.18, 1.4022] +test5_esurplus = [0.0901899999999984,0,0,0,0,0.27480199999998633] -test4_eenergy = [1.62, 2.2842] +test5_eenergy = [0.8993600000000469, + 1.8662289999999468, + 3.6625479999998864, + 5.055734000000438, + 1.5473829999999773, + 0.9368359999999988] -end +end \ No newline at end of file diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index c9117d86..02f26a85 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -412,4 +412,100 @@ end + @testset "Test System 4: Gen + DR, 1 Region" begin + + simspec = SequentialMonteCarlo(samples=1_000_000, seed=112) + region = first(TestData.test4.regions.names) + dr = first(TestData.test4.demandresponses.names) + dts = TestData.test4.timestamps + + shortfall, surplus, energy = + assess(TestData.test4, simspec, + Shortfall(), Surplus(), DemandResponseEnergy()) + + # Shortfall - LOLE + @test withinrange(LOLE(shortfall), + TestData.test4_lole, nstderr_tol) + @test withinrange(LOLE(shortfall, region), + TestData.test4_lole, nstderr_tol) + @test all(withinrange.(LOLE.(shortfall, dts), + TestData.test4_lolps, nstderr_tol)) + @test all(withinrange.(LOLE.(shortfall, region, dts), + TestData.test4_lolps, nstderr_tol)) + + # Shortfall - EUE + @test withinrange(EUE(shortfall), + TestData.test4_eue, nstderr_tol) + @test withinrange(EUE(shortfall, region), + TestData.test4_eue, nstderr_tol) + @test all(withinrange.(EUE.(shortfall, dts), + TestData.test4_eues, nstderr_tol)) + @test all(withinrange.(EUE.(shortfall, region, dts), + TestData.test4_eues, nstderr_tol)) + + # Surplus + @test all(withinrange.(getindex.(surplus, dts), + TestData.test4_esurplus, + simspec.nsamples, nstderr_tol)) + @test all(withinrange.(getindex.(surplus, region, dts), + TestData.test4_esurplus, + simspec.nsamples, nstderr_tol)) + # Energy + @test all(withinrange.(getindex.(energy, dts), + TestData.test4_eenergy, + simspec.nsamples, nstderr_tol)) + @test all(withinrange.(getindex.(energy, dr, dts), + TestData.test4_eenergy, + simspec.nsamples, nstderr_tol)) + + end + + @testset "Test System 5: Gen + DR + Stor, 1 Region" begin + + simspec = SequentialMonteCarlo(samples=1_000_000, seed=112) + region = first(TestData.test5.regions.names) + dr = first(TestData.test5.demandresponses.names) + dts = TestData.test5.timestamps + + shortfall, surplus, energy = + assess(TestData.test5, simspec, + Shortfall(), Surplus(), DemandResponseEnergy()) + + # Shortfall - LOLE + @test withinrange(LOLE(shortfall), + TestData.test5_lole, nstderr_tol) + @test withinrange(LOLE(shortfall, region), + TestData.test5_lole, nstderr_tol) + @test all(withinrange.(LOLE.(shortfall, dts), + TestData.test5_lolps, nstderr_tol)) + @test all(withinrange.(LOLE.(shortfall, region, dts), + TestData.test5_lolps, nstderr_tol)) + + # Shortfall - EUE + @test withinrange(EUE(shortfall), + TestData.test5_eue, nstderr_tol) + @test withinrange(EUE(shortfall, region), + TestData.test5_eue, nstderr_tol) + @test all(withinrange.(EUE.(shortfall, dts), + TestData.test5_eues, nstderr_tol)) + @test all(withinrange.(EUE.(shortfall, region, dts), + TestData.test5_eues, nstderr_tol)) + + # Surplus + @test all(withinrange.(getindex.(surplus, dts), + TestData.test5_esurplus, + simspec.nsamples, nstderr_tol)) + @test all(withinrange.(getindex.(surplus, region, dts), + TestData.test5_esurplus, + simspec.nsamples, nstderr_tol)) + # Energy + @test all(withinrange.(getindex.(energy, dts), + TestData.test5_eenergy, + simspec.nsamples, nstderr_tol)) + @test all(withinrange.(getindex.(energy, dr, dts), + TestData.test5_eenergy, + simspec.nsamples, nstderr_tol)) + + end + end From bb00f89b1621d250014fd85926b2e3998795c96f Mon Sep 17 00:00:00 2001 From: jflorez Date: Wed, 2 Jul 2025 10:06:59 -0600 Subject: [PATCH 15/83] DR dispatch order by paybackcounter vs allowable payback window --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index cb6546c5..53418bdd 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -450,7 +450,7 @@ function update_problem!( if iszero(maxpayback) allowablepayback = N + 1 else - allowablepayback = system.demandresponses.allowable_payback_period[i,t] + allowablepayback = state.drs_paybackcounter[i] end payback_capacity = min( From 0b230fc0e851506020577f3a21963352b80082aa Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 8 Jul 2025 16:35:54 -0600 Subject: [PATCH 16/83] update constructor for no DR devices passed --- PRASCore.jl/src/Systems/SystemModel.jl | 93 +++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index 52220bcb..4b58c565 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -66,6 +66,7 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} attrs::Dict{String, String} + #base system constructor-demand response devices function SystemModel{}( regions::Regions{N,P}, interfaces::Interfaces{N,P}, generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, @@ -110,9 +111,34 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} end + #base system constructor- no demand response devices + function SystemModel{}( + regions::Regions{N,P}, interfaces::Interfaces{N,P}, + generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, + storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, + generatorstorages::GeneratorStorages{N,L,T,P,E}, + region_genstor_idxs::Vector{UnitRange{Int}}, + lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, + timestamps::StepRange{ZonedDateTime,T} + ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} + + return SystemModel( + regions, interfaces, + generators, region_gen_idxs, + storages, region_stor_idxs, + generatorstorages, region_genstor_idxs, + DemandResponses{N,L,T,P,E}( + String[], String[], + Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), + Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), + Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), repeat([1:0],length(regions)), + lines, interface_line_idxs, + timestamps) + end + end -# No time zone constructor +# No time zone constructor - demand responses included function SystemModel( regions::Regions{N,P}, interfaces::Interfaces{N,P}, generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, @@ -144,7 +170,41 @@ function SystemModel( end -# Single-node constructor +# No time zone constructor - demand responses not included +function SystemModel( + regions::Regions{N,P}, interfaces::Interfaces{N,P}, + generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, + storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, + generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, + lines, interface_line_idxs::Vector{UnitRange{Int}}, + timestamps::StepRange{DateTime,T} +) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} + + @warn "No time zone data provided - defaulting to UTC. To specify a " * + "time zone for the system timestamps, provide a range of " * + "`ZonedDateTime` instead of `DateTime`." + + utc = tz"UTC" + time_start = ZonedDateTime(first(timestamps), utc) + time_end = ZonedDateTime(last(timestamps), utc) + timestamps_tz = time_start:step(timestamps):time_end + + return SystemModel( + regions, interfaces, + generators, region_gen_idxs, + storages, region_stor_idxs, + generatorstorages, region_genstor_idxs, + DemandResponses{N,L,T,P,E}( + String[], String[], + Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), + Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), + Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), repeat([1:0],length(regions)), + lines, interface_line_idxs, + timestamps_tz) + +end + +# Single-node constructor - demand responses not included function SystemModel( generators::Generators{N,L,T,P}, storages::Storages{N,L,T,P,E}, @@ -172,6 +232,35 @@ function SystemModel( end +# Single-node constructor - demand responses not included +function SystemModel( + generators::Generators{N,L,T,P}, + storages::Storages{N,L,T,P,E}, + generatorstorages::GeneratorStorages{N,L,T,P,E}, + timestamps::StepRange{<:AbstractDateTime,T}, + load::Vector{Int} +) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} + return SystemModel( + Regions{N,P}(["Region"], reshape(load, 1, :)), + Interfaces{N,P}( + Int[], Int[], + Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N)), + generators, [1:length(generators)], + storages, [1:length(storages)], + generatorstorages, [1:length(generatorstorages)], + DemandResponses{N,L,T,P,E}( + String[], String[], + Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), + Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), + Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), [1:0], + Lines{N,L,T,P}( + String[], String[], + Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N), + Matrix{Float64}(undef, 0, N), Matrix{Float64}(undef, 0, N)), + UnitRange{Int}[], timestamps, attrs) +end + + Base.:(==)(x::T, y::T) where {T <: SystemModel} = x.regions == y.regions && x.interfaces == y.interfaces && From 9a7de3d986f232ff6c0fb67358e7e41f4200ff08 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 8 Jul 2025 16:36:13 -0600 Subject: [PATCH 17/83] update tests for no DR --- PRASCore.jl/src/Systems/TestData.jl | 52 ++++++++--------------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index 3bc93df4..6af0a34f 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -26,14 +26,9 @@ emptygenstors1 = GeneratorStorages{4,1,Hour,MW,MWh}( (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:3)..., (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:2)...) -emptydrs1 = DemandResponses{4,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., - (empty_int(4) for _ in 1:3)..., - (empty_float(4) for _ in 1:3)..., - (empty_int(4) for _ in 1:1)..., - (empty_float(4) for _ in 1:2)...) singlenode_a = SystemModel( - gens1, emptystors1, emptygenstors1,emptydrs1, + gens1, emptystors1, emptygenstors1, ZonedDateTime(2010,1,1,0,tz):Hour(1):ZonedDateTime(2010,1,1,3,tz), [25, 28, 27, 24]) @@ -59,14 +54,9 @@ emptygenstors1_5min = GeneratorStorages{4,5,Minute,MW,MWh}( (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:3)..., (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:2)...) -emptydrs1_5min = DemandResponses{4,5,Minute,MW,MWh}((empty_str for _ in 1:2)..., - (empty_int(4) for _ in 1:3)..., - (empty_float(4) for _ in 1:3)..., - (empty_int(4) for _ in 1:1)..., - (empty_float(4) for _ in 1:2)...) singlenode_a_5min = SystemModel( - gens1_5min, emptystors1_5min, emptygenstors1_5min,emptydrs1_5min, + gens1_5min, emptystors1_5min, emptygenstors1_5min, ZonedDateTime(2010,1,1,0,0,tz):Minute(5):ZonedDateTime(2010,1,1,0,15,tz), [25, 28, 27, 24]) @@ -92,11 +82,6 @@ emptygenstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( (empty_int(6) for _ in 1:3)..., (empty_float(6) for _ in 1:3)..., (empty_int(6) for _ in 1:3)..., (empty_float(6) for _ in 1:2)...) -emptydrs2 = DemandResponses{6,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., - (empty_int(6) for _ in 1:3)..., - (empty_float(6) for _ in 1:3)..., - (empty_int(6) for _ in 1:1)..., - (empty_float(6) for _ in 1:2)...) genstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( ["Genstor1", "Genstor2"], ["Genstorage", "Genstorage"], @@ -106,7 +91,7 @@ genstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( fill(0.0, 2, 6), fill(1.0, 2, 6)) singlenode_b = SystemModel( - gens2, emptystors2, emptygenstors2,emptydrs2, + gens2, emptystors2, emptygenstors2, ZonedDateTime(2015,6,1,0,tz):Hour(1):ZonedDateTime(2015,6,1,5,tz), [28,29,30,31,32,33]) @@ -126,7 +111,7 @@ stors2 = Storages{6,1,Hour,MW,MWh}( fill(0.0, 2, 6), fill(1.0, 2, 6)) singlenode_stor = SystemModel( - gens2, stors2, genstors2,emptydrs2, + gens2, stors2, genstors2, ZonedDateTime(2015,6,1,0,tz):Hour(1):ZonedDateTime(2015,6,1,5,tz), [28,29,30,31,32,33]) @@ -160,7 +145,6 @@ threenode = SystemModel( regions, interfaces, generators, [1:2, 3:5, 6:8], emptystors1, fill(1:0, 3), emptygenstors1, fill(1:0, 3), - emptydrs1, fill(1:0, 3), lines, [1:1, 2:2, 3:3], ZonedDateTime(2018,10,30,0,tz):Hour(1):ZonedDateTime(2018,10,30,3,tz)) @@ -192,11 +176,6 @@ emptygenstors = GeneratorStorages{1,1,Hour,MW,MWh}( (zeros(Int, 0, 1) for _ in 1:3)..., (zeros(Float64, 0, 1) for _ in 1:3)..., (zeros(Int, 0, 1) for _ in 1:3)..., (zeros(Float64, 0, 1) for _ in 1:2)...) -emptydrs= DemandResponses{1,1,Hour,MW,MWh}((String[] for _ in 1:2)..., - (zeros(Int, 0, 1) for _ in 1:3)..., - (zeros(Float64, 0, 1) for _ in 1:3)..., - (zeros(Int, 0, 1) for _ in 1:1)..., - (zeros(Float64, 0, 1) for _ in 1:2)...) interfaces = Interfaces{1,MW}([1], [2], fill(8, 1, 1), fill(8, 1, 1)) @@ -207,7 +186,7 @@ lines = Lines{1,1,Hour,MW}( zdt = ZonedDateTime(2020,1,1,0, tz) test1 = SystemModel(regions, interfaces, - gens, [1:1, 2:2], emptystors, fill(1:0, 2), emptygenstors, fill(1:0, 2),emptydrs, fill(1:0, 2), + gens, [1:1, 2:2], emptystors, fill(1:0, 2), emptygenstors, fill(1:0, 2), lines, [1:1], zdt:Hour(1):zdt ) @@ -241,14 +220,8 @@ emptygenstors = GeneratorStorages{2,1,Hour,MW,MWh}( (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:3)..., (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:2)...) -emptydrs2 = DemandResponses{2,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., - (empty_int(2) for _ in 1:3)..., - (empty_float(2) for _ in 1:3)..., - (empty_int(2) for _ in 1:1)..., - (empty_float(2) for _ in 1:2)...) - -test2 = SystemModel(gen, stor, emptygenstors,emptydrs2, timestamps, [8, 9]) +test2 = SystemModel(gen, stor, emptygenstors, timestamps, [8, 9]) test2_lole = 0.2 test2_lolps = [0.1, 0.1] @@ -276,7 +249,6 @@ line = Lines{2,1,Hour,MW}( test3 = SystemModel(regions, interfaces, gen, [1:1, 2:1], stor, [1:0, 1:1], emptygenstors, fill(1:0, 2), - emptydrs2, fill(1:0, 2), line, [1:1], timestamps) test3_lole = 0.320951 @@ -320,15 +292,16 @@ emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( (zeros(Int, 0, 6) for _ in 1:3)..., (zeros(Float64, 0, 6) for _ in 1:2)...) dr = DemandResponses{6,1,Hour,MW,MWh}( - ["DR1"], ["DemandResponses"], + ["DR1"], ["DemandResponse Category"], fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), - fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6),fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) + fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6), + fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) full_day_load_profile = [56,58,60,61,59,53] -test4 = SystemModel(gen, emptystors, emptygenstors,dr, timestamps, full_day_load_profile) +test4 = SystemModel(gen, emptystors, emptygenstors, dr, timestamps, full_day_load_profile) test4_lole = 1.99 test4_lolps = [0.0998, 0.2629, 0.33, 0.8603, 0.2638, 0.1733] @@ -367,13 +340,14 @@ emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( dr = DemandResponses{6,1,Hour,MW,MWh}( ["DR1"], ["DemandResponses"], fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), - fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6),fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) + fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6), + fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) full_day_load_profile = [56,58,60,61,59,53] -test5 = SystemModel(gen, stor, emptygenstors,dr, timestamps, full_day_load_profile) +test5 = SystemModel(gen, stor, emptygenstors, dr, timestamps, full_day_load_profile) test5_lole = 1.969 test5_lolps = [0.0998, 0.1974, 0.3301, 0.4261, 0.6986, 0.2173] From 9a781b70d373676fe6f0d9121f6fc9f6f2a91740 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 8 Jul 2025 18:06:27 -0600 Subject: [PATCH 18/83] add DR reporting --- .../src/Results/DemandResponseShortfall.jl | 304 ++++++++++++++++++ .../Results/DemandResponseShortfallSamples.jl | 172 ++++++++++ PRASCore.jl/src/Results/Results.jl | 7 +- PRASCore.jl/src/Results/Shortfall.jl | 14 +- PRASCore.jl/src/Simulations/recording.jl | 121 +++++-- 5 files changed, 587 insertions(+), 31 deletions(-) create mode 100644 PRASCore.jl/src/Results/DemandResponseShortfall.jl create mode 100644 PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl diff --git a/PRASCore.jl/src/Results/DemandResponseShortfall.jl b/PRASCore.jl/src/Results/DemandResponseShortfall.jl new file mode 100644 index 00000000..1938feaa --- /dev/null +++ b/PRASCore.jl/src/Results/DemandResponseShortfall.jl @@ -0,0 +1,304 @@ +""" + DemandResponseShortfall + +The `DemandResponseShortfall` result specification reports expectation-based resource +adequacy risk metrics such as EUE and LOLE associated with DemandResponse devices only, producing a `DemandResponseShortfallResult`. + +A `DemandResponseShortfallResult` can be directly indexed by a region name and a timestamp to retrieve a tuple of sample mean and standard deviation, estimating + the average unserved energy in that region and timestep. However, in most +cases it's simpler to use [`EUE`](@ref) and [`LOLE`](@ref) constructors to +directly retrieve standard risk metrics. + +Example: + +```julia +DemandResponseShortfall, = + assess(sys, SequentialMonteCarlo(samples=1000), DemandResponseShortfall()) + +period = ZonedDateTime(2020, 1, 1, 0, tz"UTC") + +# Unserved energy mean and standard deviation +sf_mean, sf_std = DemandResponseShortfall["Region A", period] + +# System-wide risk metrics +eue = EUE(DemandResponseShortfall) +lole = LOLE(DemandResponseShortfall) +neue = NEUE(shorfall) + +# Regional risk metrics +regional_eue = EUE(DemandResponseShortfall, "Region A") +regional_lole = LOLE(DemandResponseShortfall, "Region A") +regional_neue = NEUE(DemandResponseShortfall, "Region A") + +# Period-specific risk metrics +period_eue = EUE(DemandResponseShortfall, period) +period_lolp = LOLE(DemandResponseShortfall, period) + +# Region- and period-specific risk metrics +period_eue = EUE(DemandResponseShortfall, "Region A", period) +period_lolp = LOLE(DemandResponseShortfall, "Region A", period) +``` + +See [`DemandResponseShortfallSamples`](@ref) for recording sample-level DemandResponseShortfall results. +""" +struct DemandResponseShortfall <: ResultSpec end + +mutable struct DemandResponseShortfallAccumulator <: ResultAccumulator{DemandResponseShortfall} + + # Cross-simulation LOL period count mean/variances + periodsdropped_total::MeanVariance + periodsdropped_region::Vector{MeanVariance} + periodsdropped_period::Vector{MeanVariance} + periodsdropped_regionperiod::Matrix{MeanVariance} + + # Running LOL period counts for current simulation + periodsdropped_total_currentsim::Int + periodsdropped_region_currentsim::Vector{Int} + + # Cross-simulation UE mean/variances + unservedload_total::MeanVariance + unservedload_region::Vector{MeanVariance} + unservedload_period::Vector{MeanVariance} + unservedload_regionperiod::Matrix{MeanVariance} + + # Running UE totals for current simulation + unservedload_total_currentsim::Int + unservedload_region_currentsim::Vector{Int} + +end + +function accumulator( + sys::SystemModel{N}, nsamples::Int, ::DemandResponseShortfall +) where {N} + + nregions = length(sys.regions) + + periodsdropped_total = meanvariance() + periodsdropped_region = [meanvariance() for _ in 1:nregions] + periodsdropped_period = [meanvariance() for _ in 1:N] + periodsdropped_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] + + periodsdropped_total_currentsim = 0 + periodsdropped_region_currentsim = zeros(Int, nregions) + + unservedload_total = meanvariance() + unservedload_region = [meanvariance() for _ in 1:nregions] + unservedload_period = [meanvariance() for _ in 1:N] + unservedload_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] + + unservedload_total_currentsim = 0 + unservedload_region_currentsim = zeros(Int, nregions) + + return DemandResponseShortfallAccumulator( + periodsdropped_total, periodsdropped_region, + periodsdropped_period, periodsdropped_regionperiod, + periodsdropped_total_currentsim, periodsdropped_region_currentsim, + unservedload_total, unservedload_region, + unservedload_period, unservedload_regionperiod, + unservedload_total_currentsim, unservedload_region_currentsim) + +end + +function merge!( + x::DemandResponseShortfallAccumulator, y::DemandResponseShortfallAccumulator +) + + merge!(x.periodsdropped_total, y.periodsdropped_total) + foreach(merge!, x.periodsdropped_region, y.periodsdropped_region) + foreach(merge!, x.periodsdropped_period, y.periodsdropped_period) + foreach(merge!, x.periodsdropped_regionperiod, y.periodsdropped_regionperiod) + + merge!(x.unservedload_total, y.unservedload_total) + foreach(merge!, x.unservedload_region, y.unservedload_region) + foreach(merge!, x.unservedload_period, y.unservedload_period) + foreach(merge!, x.unservedload_regionperiod, y.unservedload_regionperiod) + + return + +end + +accumulatortype(::DemandResponseShortfall) = DemandResponseShortfallAccumulator + +struct DemandResponseShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: + AbstractShortfallResult{N, L, T} + nsamples::Union{Int, Nothing} + regions::Regions + timestamps::StepRange{ZonedDateTime,T} + + eventperiod_mean::Float64 + eventperiod_std::Float64 + + eventperiod_region_mean::Vector{Float64} + eventperiod_region_std::Vector{Float64} + + eventperiod_period_mean::Vector{Float64} + eventperiod_period_std::Vector{Float64} + + eventperiod_regionperiod_mean::Matrix{Float64} + eventperiod_regionperiod_std::Matrix{Float64} + + + shortfall_mean::Matrix{Float64} # r x t + + shortfall_std::Float64 + shortfall_region_std::Vector{Float64} + shortfall_period_std::Vector{Float64} + shortfall_regionperiod_std::Matrix{Float64} + + function DemandResponseShortfallResult{N,L,T,E}( + nsamples::Union{Int,Nothing}, + regions::Regions, + timestamps::StepRange{ZonedDateTime,T}, + eventperiod_mean::Float64, + eventperiod_std::Float64, + eventperiod_region_mean::Vector{Float64}, + eventperiod_region_std::Vector{Float64}, + eventperiod_period_mean::Vector{Float64}, + eventperiod_period_std::Vector{Float64}, + eventperiod_regionperiod_mean::Matrix{Float64}, + eventperiod_regionperiod_std::Matrix{Float64}, + shortfall_mean::Matrix{Float64}, + shortfall_std::Float64, + shortfall_region_std::Vector{Float64}, + shortfall_period_std::Vector{Float64}, + shortfall_regionperiod_std::Matrix{Float64} + ) where {N,L,T<:Period,E<:EnergyUnit} + + isnothing(nsamples) || nsamples > 0 || + throw(DomainError("Sample count must be positive or `nothing`.")) + + + length(timestamps) == N || + error("The provided timestamp range does not match the simulation length") + + nregions = length(regions.names) + + length(eventperiod_region_mean) == nregions && + length(eventperiod_region_std) == nregions && + length(eventperiod_period_mean) == N && + length(eventperiod_period_std) == N && + size(eventperiod_regionperiod_mean) == (nregions, N) && + size(eventperiod_regionperiod_std) == (nregions, N) && + length(shortfall_region_std) == nregions && + length(shortfall_period_std) == N && + size(shortfall_regionperiod_std) == (nregions, N) || + error("Inconsistent input data sizes") + + new{N,L,T,E}(nsamples, regions, timestamps, + eventperiod_mean, eventperiod_std, + eventperiod_region_mean, eventperiod_region_std, + eventperiod_period_mean, eventperiod_period_std, + eventperiod_regionperiod_mean, eventperiod_regionperiod_std, + shortfall_mean, shortfall_std, + shortfall_region_std, shortfall_period_std, + shortfall_regionperiod_std) + + end + +end + +function getindex(x::DemandResponseShortfallResult) + return sum(x.shortfall_mean), x.shortfall_std +end + +function getindex(x::DemandResponseShortfallResult, r::AbstractString) + i_r = findfirstunique(x.regions.names, r) + return sum(view(x.shortfall_mean, i_r, :)), x.shortfall_region_std[i_r] +end + +function getindex(x::DemandResponseShortfallResult, t::ZonedDateTime) + i_t = findfirstunique(x.timestamps, t) + return sum(view(x.shortfall_mean, :, i_t)), x.shortfall_period_std[i_t] +end + +function getindex(x::DemandResponseShortfallResult, r::AbstractString, t::ZonedDateTime) + i_r = findfirstunique(x.regions.names, r) + i_t = findfirstunique(x.timestamps, t) + return x.shortfall_mean[i_r, i_t], x.shortfall_regionperiod_std[i_r, i_t] +end + + +LOLE(x::DemandResponseShortfallResult{N,L,T}) where {N,L,T} = + LOLE{N,L,T}(MeanEstimate(x.eventperiod_mean, + x.eventperiod_std, + x.nsamples)) + +function LOLE(x::DemandResponseShortfallResult{N,L,T}, r::AbstractString) where {N,L,T} + i_r = findfirstunique(x.regions.names, r) + return LOLE{N,L,T}(MeanEstimate(x.eventperiod_region_mean[i_r], + x.eventperiod_region_std[i_r], + x.nsamples)) +end + +function LOLE(x::DemandResponseShortfallResult{N,L,T}, t::ZonedDateTime) where {N,L,T} + i_t = findfirstunique(x.timestamps, t) + return LOLE{1,L,T}(MeanEstimate(x.eventperiod_period_mean[i_t], + x.eventperiod_period_std[i_t], + x.nsamples)) +end + +function LOLE(x::DemandResponseShortfallResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} + i_r = findfirstunique(x.regions.names, r) + i_t = findfirstunique(x.timestamps, t) + return LOLE{1,L,T}(MeanEstimate(x.eventperiod_regionperiod_mean[i_r, i_t], + x.eventperiod_regionperiod_std[i_r, i_t], + x.nsamples)) +end + + +EUE(x::DemandResponseShortfallResult{N,L,T,E}) where {N,L,T,E} = + EUE{N,L,T,E}(MeanEstimate(x[]..., x.nsamples)) + +EUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} = + EUE{N,L,T,E}(MeanEstimate(x[r]..., x.nsamples)) + +EUE(x::DemandResponseShortfallResult{N,L,T,E}, t::ZonedDateTime) where {N,L,T,E} = + EUE{1,L,T,E}(MeanEstimate(x[t]..., x.nsamples)) + +EUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E} = + EUE{1,L,T,E}(MeanEstimate(x[r, t]..., x.nsamples)) + +function NEUE(x::DemandResponseShortfallResult{N,L,T,E}) where {N,L,T,E} + return NEUE(div(MeanEstimate(x[]..., x.nsamples),(sum(x.regions.load)/1e6))) +end + +function NEUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} + i_r = findfirstunique(x.regions.names, r) + return NEUE(div(MeanEstimate(x[r]..., x.nsamples),(sum(x.regions.load[i_r,:])/1e6))) +end + +function finalize( + acc::DemandResponseShortfallAccumulator, + system::SystemModel{N,L,T,P,E}, +) where {N,L,T,P,E} + + ep_total_mean, ep_total_std = mean_std(acc.periodsdropped_total) + ep_region_mean, ep_region_std = mean_std(acc.periodsdropped_region) + ep_period_mean, ep_period_std = mean_std(acc.periodsdropped_period) + ep_regionperiod_mean, ep_regionperiod_std = + mean_std(acc.periodsdropped_regionperiod) + + _, ue_total_std = mean_std(acc.unservedload_total) + _, ue_region_std = mean_std(acc.unservedload_region) + _, ue_period_std = mean_std(acc.unservedload_period) + ue_regionperiod_mean, ue_regionperiod_std = + mean_std(acc.unservedload_regionperiod) + + nsamples = first(acc.unservedload_total.stats).n + + p2e = conversionfactor(L,T,P,E) + ue_regionperiod_mean .*= p2e + ue_total_std *= p2e + ue_region_std .*= p2e + ue_period_std .*= p2e + ue_regionperiod_std .*= p2e + + return DemandResponseShortfallResult{N,L,T,E}( + nsamples, system.regions, system.timestamps, + ep_total_mean, ep_total_std, ep_region_mean, ep_region_std, + ep_period_mean, ep_period_std, + ep_regionperiod_mean, ep_regionperiod_std, + ue_regionperiod_mean, ue_total_std, + ue_region_std, ue_period_std, ue_regionperiod_std) + +end diff --git a/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl b/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl new file mode 100644 index 00000000..07ff6116 --- /dev/null +++ b/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl @@ -0,0 +1,172 @@ +""" + DemandResponseShortfallSamples + +The `DemandResponseShortfallSamples` result specification reports sample-level unserved energy outcomes, producing a `DemandResponseShortfallSamplesResult`. + +A `DemandResponseShortfallSamplesResult` can be directly indexed by a region name and a +timestamp to retrieve a vector of sample-level unserved energy results in that +region and timestep. [`EUE`](@ref) and [`LOLE`](@ref) constructors can also +be used to retrieve standard risk metrics. + +Example: + +```julia +drshortfall, = + assess(sys, SequentialMonteCarlo(samples=10), DemandResponseShortfallSamples()) + +period = ZonedDateTime(2020, 1, 1, 0, tz"UTC") + +samples = drshortfall["Region A", period] + +@assert samples isa Vector{Float64} +@assert length(samples) == 10 + +# System-wide risk metrics +eue = EUE(drshortfall) +lole = LOLE(drshortfall) +neue = NEUE(drshortfall) + +# Regional risk metrics +regional_eue = EUE(drshortfall, "Region A") +regional_lole = LOLE(drshortfall, "Region A") +regional_neue = NEUE(drshortfall, "Region A") + +# Period-specific risk metrics +period_eue = EUE(drshortfall, period) +period_lolp = LOLE(shortfall, period) + +# Region- and period-specific risk metrics +period_eue = EUE(drshortfall, "Region A", period) +period_lolp = LOLE(drshortfall, "Region A", period) +``` + +Note that this result specification requires large amounts of memory for +larger sample sizes. See [`DemandResponseShortfall`](@ref) for average shortfall outcomes when sample-level granularity isn't required. +""" +struct DemandResponseShortfallSamples <: ResultSpec end + +struct DemandResponseShortfallSamplesAccumulator <: ResultAccumulator{DemandResponseShortfallSamples} + + shortfall::Array{Int,3} + +end + +function accumulator( + sys::SystemModel{N}, nsamples::Int, ::DemandResponseShortfallSamples +) where {N} + + nregions = length(sys.regions) + shortfall = zeros(Int, nregions, N, nsamples) + + return DemandResponseShortfallSamplesAccumulator(shortfall) + +end + +function merge!( + x::DemandResponseShortfallSamplesAccumulator, y::DemandResponseShortfallSamplesAccumulator +) + + x.shortfall .+= y.shortfall + return + +end + +accumulatortype(::DemandResponseShortfallSamples) = DemandResponseShortfallSamplesAccumulator + +struct DemandResponseShortfallSamplesResult{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractShortfallResult{N,L,T} + + regions::Regions{N,P} + timestamps::StepRange{ZonedDateTime,T} + + shortfall::Array{Int,3} # r x t x s + +end + +function getindex( + x::DemandResponseShortfallSamplesResult{N,L,T,P,E} +) where {N,L,T,P,E} + p2e = conversionfactor(L, T, P, E) + return vec(p2e * sum(x.shortfall, dims=1:2)) +end + +function getindex( + x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString +) where {N,L,T,P,E} + i_r = findfirstunique(x.regions.names, r) + p2e = conversionfactor(L, T, P, E) + return vec(p2e * sum(view(x.shortfall, i_r, :, :), dims=1)) +end + +function getindex( + x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime +) where {N,L,T,P,E} + i_t = findfirstunique(x.timestamps, t) + p2e = conversionfactor(L, T, P, E) + return vec(p2e * sum(view(x.shortfall, :, i_t, :), dims=1)) +end + +function getindex( + x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime +) where {N,L,T,P,E} + i_r = findfirstunique(x.regions.names, r) + i_t = findfirstunique(x.timestamps, t) + p2e = conversionfactor(L, T, P, E) + return vec(p2e * x.shortfall[i_r, i_t, :]) +end + + +function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}) where {N,L,T} + eventperiods = sum(sum(x.shortfall, dims=1) .> 0, dims=2) + return LOLE{N,L,T}(MeanEstimate(eventperiods)) +end + +function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, r::AbstractString) where {N,L,T} + i_r = findfirstunique(x.regions.names, r) + eventperiods = sum(view(x.shortfall, i_r, :, :) .> 0, dims=1) + return LOLE{N,L,T}(MeanEstimate(eventperiods)) +end + +function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, t::ZonedDateTime) where {N,L,T} + i_t = findfirstunique(x.timestamps, t) + eventperiods = sum(view(x.shortfall, :, i_t, :), dims=1) .> 0 + return LOLE{1,L,T}(MeanEstimate(eventperiods)) +end + +function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} + i_r = findfirstunique(x.regions.names, r) + i_t = findfirstunique(x.timestamps, t) + eventperiods = view(x.shortfall, i_r, i_t, :) .> 0 + return LOLE{1,L,T}(MeanEstimate(eventperiods)) +end + + +EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} = + EUE{N,L,T,E}(MeanEstimate(x[])) + +EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} = + EUE{N,L,T,E}(MeanEstimate(x[r])) + +EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime) where {N,L,T,P,E} = + EUE{1,L,T,E}(MeanEstimate(x[t])) + +EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,P,E} = + EUE{1,L,T,E}(MeanEstimate(x[r, t])) + +function NEUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} + return NEUE(div(MeanEstimate(x[]),(sum(x.regions.load)/1e6))) +end + +function NEUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} + i_r = findfirstunique(x.regions.names, r) + return NEUE(div(MeanEstimate(x[r]),(sum(x.regions.load[i_r,:])/1e6))) +end + +function finalize( + acc::DemandResponseShortfallSamplesAccumulator, + system::SystemModel{N,L,T,P,E}, +) where {N,L,T,P,E} + + return DemandResponseShortfallSamplesResult{N,L,T,P,E}( + system.regions, system.timestamps, acc.shortfall) + +end diff --git a/PRASCore.jl/src/Results/Results.jl b/PRASCore.jl/src/Results/Results.jl index 011d31c7..e4d029d0 100644 --- a/PRASCore.jl/src/Results/Results.jl +++ b/PRASCore.jl/src/Results/Results.jl @@ -16,7 +16,9 @@ export val, stderror, # Result specifications - Shortfall, ShortfallSamples, Surplus, SurplusSamples, + Shortfall, ShortfallSamples, + DemandResponseShortfall, DemandResponseShortfallSamples, + Surplus, SurplusSamples, Flow, FlowSamples, Utilization, UtilizationSamples, StorageEnergy, StorageEnergySamples, GeneratorStorageEnergy, GeneratorStorageEnergySamples, @@ -80,6 +82,9 @@ NEUE(x::AbstractShortfallResult, ::Colon, ::Colon) = include("Shortfall.jl") include("ShortfallSamples.jl") +include("DemandResponseShortfall.jl") +include("DemandResponseShortfallSamples.jl") + abstract type AbstractSurplusResult{N,L,T} <: Result{N,L,T} end diff --git a/PRASCore.jl/src/Results/Shortfall.jl b/PRASCore.jl/src/Results/Shortfall.jl index c30d62dc..e76c00c1 100644 --- a/PRASCore.jl/src/Results/Shortfall.jl +++ b/PRASCore.jl/src/Results/Shortfall.jl @@ -58,15 +58,12 @@ mutable struct ShortfallAccumulator <: ResultAccumulator{Shortfall} # Cross-simulation UE mean/variances unservedload_total::MeanVariance unservedload_region::Vector{MeanVariance} - unserveddrload_region::Vector{MeanVariance} unservedload_period::Vector{MeanVariance} unservedload_regionperiod::Matrix{MeanVariance} - unserveddrload_regionperiod::Matrix{MeanVariance} # Running UE totals for current simulation unservedload_total_currentsim::Int unservedload_region_currentsim::Vector{Int} - unserveddrload_region_currentsim::Vector{Int} end @@ -86,22 +83,19 @@ function accumulator( unservedload_total = meanvariance() unservedload_region = [meanvariance() for _ in 1:nregions] - unserveddrload_region = [meanvariance() for _ in 1:nregions] unservedload_period = [meanvariance() for _ in 1:N] unservedload_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] - unserveddrload_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] unservedload_total_currentsim = 0 unservedload_region_currentsim = zeros(Int, nregions) - unserveddrload_region_currentsim = zeros(Int, nregions) return ShortfallAccumulator( periodsdropped_total, periodsdropped_region, periodsdropped_period, periodsdropped_regionperiod, periodsdropped_total_currentsim, periodsdropped_region_currentsim, - unservedload_total, unservedload_region,unserveddrload_region, - unservedload_period, unservedload_regionperiod,unserveddrload_regionperiod, - unservedload_total_currentsim, unservedload_region_currentsim,unserveddrload_region_currentsim) + unservedload_total, unservedload_region, + unservedload_period, unservedload_regionperiod, + unservedload_total_currentsim, unservedload_region_currentsim) end @@ -116,10 +110,8 @@ function merge!( merge!(x.unservedload_total, y.unservedload_total) foreach(merge!, x.unservedload_region, y.unservedload_region) - foreach(merge!, x.unserveddrload_region, y.unserveddrload_region) foreach(merge!, x.unservedload_period, y.unservedload_period) foreach(merge!, x.unservedload_regionperiod, y.unservedload_regionperiod) - foreach(merge!, x.unserveddrload_regionperiod, y.unserveddrload_regionperiod) return diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 77e5e74a..5cf363e7 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -26,7 +26,6 @@ function record!( fit!(acc.periodsdropped_regionperiod[r,t], isregionshortfall) fit!(acc.unservedload_regionperiod[r,t], regionshortfall) - fit!(acc.unserveddrload_regionperiod[r,t], dr_shortfall) if isregionshortfall @@ -35,7 +34,6 @@ function record!( acc.periodsdropped_region_currentsim[r] += 1 acc.unservedload_region_currentsim[r] += regionshortfall - acc.unserveddrload_region_currentsim[r] += dr_shortfall end end @@ -61,8 +59,6 @@ function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) for r in eachindex(acc.periodsdropped_region) fit!(acc.periodsdropped_region[r], acc.periodsdropped_region_currentsim[r]) fit!(acc.unservedload_region[r], acc.unservedload_region_currentsim[r]) - fit!(acc.unserveddrload_region[r], acc.unserveddrload_region_currentsim[r]) - end # Reset for new simulation @@ -70,7 +66,6 @@ function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) fill!(acc.periodsdropped_region_currentsim, 0) acc.unservedload_total_currentsim = 0 fill!(acc.unservedload_region_currentsim, 0) - fill!(acc.unserveddrload_region_currentsim, 0) return @@ -85,8 +80,14 @@ function record!( sampleid::Int, t::Int ) where {N,L,T,P,E} - for (r, e) in enumerate(problem.region_unserved_edges) - acc.shortfall[r, t, sampleid] = problem.fp.edges[e].flow + for ((r, e),dr_idxs) in zip(enumerate(problem.region_unserved_edges),system.region_dr_idxs) + #getting dr shortfall + dr_shortfall = 0 + for i in dr_idxs + dr_shortfall += state.drs_paybackcounter[i] == 0 ? states.drs_unservedenergy[i] : 0 + end + + acc.shortfall[r, t, sampleid] = problem.fp.edges[e].flow + dr_shortfall end return @@ -95,6 +96,100 @@ end reset!(acc::Results.ShortfallSamplesAccumulator, sampleid::Int) = nothing +# DemandResponseShortfall + +function record!( + acc::Results.DemandResponseShortfallAccumulator, + system::SystemModel{N,L,T,P,E}, + state::SystemState, problem::DispatchProblem, + sampleid::Int, t::Int +) where {N,L,T,P,E} + + totalshortfall = 0 + isshortfall = false + + for (r, dr_idxs) in zip(problem.region_unserved_edges, system.region_dr_idxs) + + #count region shortfall and include dr shortfall + dr_shortfall = 0 + for i in dr_idxs + dr_shortfall += state.drs_paybackcounter[i] == 0 ? state.drs_unservedenergy[i] : 0 + end + regionshortfall = dr_shortfall + isregionshortfall = regionshortfall > 0 + + + fit!(acc.periodsdropped_regionperiod[r,t], isregionshortfall) + fit!(acc.unservedload_regionperiod[r,t], regionshortfall) + + if isregionshortfall + + isshortfall = true + totalshortfall += regionshortfall + + acc.periodsdropped_region_currentsim[r] += 1 + acc.unservedload_region_currentsim[r] += regionshortfall + + end + end + + if isshortfall + acc.periodsdropped_total_currentsim += 1 + acc.unservedload_total_currentsim += totalshortfall + end + + fit!(acc.periodsdropped_period[t], isshortfall) + fit!(acc.unservedload_period[t], totalshortfall) + + return + +end + +function reset!(acc::Results.DemandResponseShortfallAccumulator, sampleid::Int) + + # Store regional / total sums for current simulation + fit!(acc.periodsdropped_total, acc.periodsdropped_total_currentsim) + fit!(acc.unservedload_total, acc.unservedload_total_currentsim) + + for r in eachindex(acc.periodsdropped_region) + fit!(acc.periodsdropped_region[r], acc.periodsdropped_region_currentsim[r]) + fit!(acc.unservedload_region[r], acc.unservedload_region_currentsim[r]) + end + + # Reset for new simulation + acc.periodsdropped_total_currentsim = 0 + fill!(acc.periodsdropped_region_currentsim, 0) + acc.unservedload_total_currentsim = 0 + fill!(acc.unservedload_region_currentsim, 0) + return + +end + +# DemandResponseShortfallSamples +function record!( + acc::Results.DemandResponseShortfallSamplesAccumulator, + system::SystemModel{N,L,T,P,E}, + state::SystemState, problem::DispatchProblem, + sampleid::Int, t::Int +) where {N,L,T,P,E} + + for (r,dr_idxs) in enumerate(system.region_dr_idxs) + #getting dr shortfall + dr_shortfall = 0 + for i in dr_idxs + dr_shortfall += state.drs_paybackcounter[i] == 0 ? states.drs_unservedenergy[i] : 0 + end + + acc.shortfall[r, t, sampleid] = dr_shortfall + end + + return + +end + +reset!(acc::Results.DemandResponseShortfallSamplesAccumulator, sampleid::Int) = nothing + + # Surplus function record!( @@ -127,12 +222,6 @@ function record!( regionsurplus += min(grid_limit, total_unused) end - - #=for dr in system.region_dr_idxs[r] - dr_idx = problem.demandresponse_bankunused_edges[dr] - regionsurplus += edges[dr_idx].flow - end - =# fit!(acc.surplus_regionperiod[r,t], regionsurplus) totalsurplus += regionsurplus @@ -177,12 +266,6 @@ function record!( regionsurplus += min(grid_limit, total_unused) end - - #=for dr in system.region_dr_idxs[r] - dr_idx = problem.demandresponse_bankunused_edges[dr] - regionsurplus += edges[dr_idx].flow - end - =# acc.surplus[r, t, sampleid] = regionsurplus end From 5cb67175c886a2eccafa37e7866ca8467768a70f Mon Sep 17 00:00:00 2001 From: juflorez Date: Wed, 9 Jul 2025 11:52:58 -0600 Subject: [PATCH 19/83] enable greater than 1 carryoverefficiency --- PRASCore.jl/src/Systems/assets.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 489a4823..ee3c3b53 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -531,7 +531,7 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse @assert size(carryoverefficiency) == (n_drs, N) @assert all(isfractional, bankefficiency) @assert all(isfractional, paybackefficiency) - @assert all(isfractional, carryoverefficiency) + @assert all(isnonnegative, carryoverefficiency) @assert size(allowablepaybackperiod) == (n_drs, N) @assert all(isnonnegative, allowablepaybackperiod) From c86b8fe82ecc81b2543473f22adb4c964f5320f5 Mon Sep 17 00:00:00 2001 From: juflorez Date: Thu, 10 Jul 2025 10:10:35 -0600 Subject: [PATCH 20/83] change bank terminology to borrow --- .../src/Results/DemandResponseEnergy.jl | 8 +- .../Results/DemandResponseEnergySamples.jl | 6 +- .../src/Simulations/DispatchProblem.jl | 76 +++++++++---------- PRASCore.jl/src/Simulations/utils.jl | 4 +- PRASCore.jl/src/Systems/assets.jl | 38 +++++----- 5 files changed, 66 insertions(+), 66 deletions(-) diff --git a/PRASCore.jl/src/Results/DemandResponseEnergy.jl b/PRASCore.jl/src/Results/DemandResponseEnergy.jl index eaf3bd30..0bfb3403 100644 --- a/PRASCore.jl/src/Results/DemandResponseEnergy.jl +++ b/PRASCore.jl/src/Results/DemandResponseEnergy.jl @@ -1,12 +1,12 @@ """ DemandResponseEnergy -The `DemandResponseEnergy` result specification reports the average energy banked +The `DemandResponseEnergy` result specification reports the average energy borrowed of `DemandResponses`, producing a `DemandResponseEnergyResult`. A `DemandResponseEnergyResult` can be indexed by demand response device name and a timestamp to retrieve a tuple of sample mean and standard deviation, estimating the average -energy banked level for the given demand response device in that timestep. +energy borrowed level for the given demand response device in that timestep. Example: @@ -18,9 +18,9 @@ soc_mean, soc_std = drenergy["MyDemandResponse123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] ``` -See [`DemandResponseEnergySamples`](@ref) for sample-level demand response states of banked energy. +See [`DemandResponseEnergySamples`](@ref) for sample-level demand response states of borrowed energy. -See [`DemandResponseEnergy`](@ref) for average demand-response banked energy. +See [`DemandResponseEnergy`](@ref) for average demand-response borrowed energy. """ struct DemandResponseEnergy <: ResultSpec end diff --git a/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl index 183ac20a..a9646ef5 100644 --- a/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl +++ b/PRASCore.jl/src/Results/DemandResponseEnergySamples.jl @@ -2,10 +2,10 @@ DemandResponseEnergySamples The `DemandResponseEnergySamples` result specification reports the sample-level state -of banked energy of `DemandResponses`, producing a `DemandResponseEnergySamplesResult`. +of borrowed energy of `DemandResponses`, producing a `DemandResponseEnergySamplesResult`. A `DemandResponseEnergySamplesResult` can be indexed by demand response device name and -a timestamp to retrieve a vector of sample-level banked energy states for +a timestamp to retrieve a vector of sample-level borrowed energy states for the device in the given timestep. Example: @@ -22,7 +22,7 @@ samples = demandresponseenergy["MyDemandResponse123", ZonedDateTime(2020, 1, 1, Note that this result specification requires large amounts of memory for larger sample sizes. See [`DemandResponseEnergy`](@ref) for estimated average demand response -banked energy when sample-level granularity isn't required. +borrowed energy when sample-level granularity isn't required. """ struct DemandResponseEnergySamples <: ResultSpec end diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 53418bdd..668b5f38 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -5,7 +5,7 @@ Create a min-cost flow problem for the multi-region max power delivery problem with generation and storage discharging in decreasing order of priority, and storage charging with excess capacity. Storage and GeneratorStorage devices within a region are represented individually on the network. Demand Response -devices will bank energy in devices with the longest payback window first, +devices will borrow energy in devices with the longest payback window first, and vice versa for payback energy. This involves injections/withdrawals at one node (regional capacity @@ -54,7 +54,7 @@ Nodes in the problem are ordered as: 6. GenerationStorage grid injection (GeneratorStorage order) 7. GenerationStorage charge capacity (GeneratorStorage order) 8. DemandResponse payback capacity (DemandResponse order) - 9. DemandResponse banking capacity (DemandResponse order) + 9. DemandResponse borrowing capacity (DemandResponse order) 10. Slack node Edges are ordered as: @@ -77,8 +77,8 @@ Edges are ordered as: 16. GenerationStorage inflow unused (GeneratorStorage order) 17. DemandResponse payback to grid (DemandResponse order) 18. DemandResponse payback unused (DemandResponse order) - 19. DemandResponse banking from grid (DemandResponse order) - 20. DemandResponse banking unused (DemandResponse order) + 19. DemandResponse borrowing from grid (DemandResponse order) + 20. DemandResponse borrowing unused (DemandResponse order) """ struct DispatchProblem @@ -93,7 +93,7 @@ struct DispatchProblem genstorage_togrid_nodes::UnitRange{Int} genstorage_charge_nodes::UnitRange{Int} demandresponse_payback_nodes::UnitRange{Int} - demandresponse_bank_nodes::UnitRange{Int} + demandresponse_borrow_nodes::UnitRange{Int} slack_node::Int # Edge labels @@ -115,15 +115,15 @@ struct DispatchProblem genstorage_inflowunused_edges::UnitRange{Int} demandresponse_payback_edges::UnitRange{Int} demandresponse_paybackunused_edges::UnitRange{Int} - demandresponse_bank_edges::UnitRange{Int} - demandresponse_bankunused_edges::UnitRange{Int} + demandresponse_borrow_edges::UnitRange{Int} + demandresponse_borrowunused_edges::UnitRange{Int} #stor/genstor costs min_chargecost::Int max_dischargecost::Int #dr costs - max_bankcost_dr::Int + max_borrowcost_dr::Int min_paybackcost_dr::Int function DispatchProblem( @@ -140,13 +140,13 @@ struct DispatchProblem min_chargecost = - maxchargetime - 1 max_dischargecost = - min_chargecost + maxdischargetime + 1 - #for demand response-we want to bank energy in devices with the longest payback window, and payback energy from devices with the smallest payback window + #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) min_paybackcost_dr = - maxpaybacktime_dr - 1 + min_chargecost #add min_chargecost to always have DR devices be paybacked first - max_bankcost_dr = - min_paybackcost_dr + minpaybacktime_dr + 1 + max_dischargecost #add max_dischargecost to always have DR devices be banked last + max_borrowcost_dr = - min_paybackcost_dr + minpaybacktime_dr + 1 + max_dischargecost #add max_dischargecost to always have DR devices be borrowed last #for unserved energy - shortagepenalty = 10 * (nifaces + max_bankcost_dr) + shortagepenalty = 10 * (nifaces + max_borrowcost_dr) stor_regions = assetgrouplist(sys.region_stor_idxs) @@ -160,8 +160,8 @@ struct DispatchProblem genstor_discharge_nodes = indices_after(genstor_inflow_nodes, ngenstors) genstor_togrid_nodes = indices_after(genstor_discharge_nodes, ngenstors) genstor_charge_nodes = indices_after(genstor_togrid_nodes, ngenstors) - dr_bank_nodes = indices_after(genstor_charge_nodes, ndrs) - dr_payback_nodes = indices_after(dr_bank_nodes, ndrs) + dr_borrow_nodes = indices_after(genstor_charge_nodes, ndrs) + dr_payback_nodes = indices_after(dr_borrow_nodes, ndrs) slack_node = nnodes = last(dr_payback_nodes) + 1 region_unservedenergy = 1:nregions @@ -180,9 +180,9 @@ struct DispatchProblem genstor_inflowcharge = indices_after(genstor_gridcharge, ngenstors) genstor_chargeunused = indices_after(genstor_inflowcharge, ngenstors) genstor_inflowunused = indices_after(genstor_chargeunused, ngenstors) - dr_bankused = indices_after(genstor_inflowunused, ndrs) - dr_bankunused = indices_after(dr_bankused, ndrs) - dr_paybackused = indices_after(dr_bankunused, ndrs) + dr_borrowused = indices_after(genstor_inflowunused, ndrs) + dr_borrowunused = indices_after(dr_borrowused, ndrs) + dr_paybackused = indices_after(dr_borrowunused, ndrs) dr_paybackunused = indices_after(dr_paybackused, ndrs) nedges = last(dr_paybackunused) @@ -238,11 +238,11 @@ struct DispatchProblem initedges(genstor_chargeunused, slack_node, genstor_charge_nodes) initedges(genstor_inflowunused, genstor_inflow_nodes, slack_node) - # Demand Response banking / payback + # Demand Response borrowing / payback initedges(dr_paybackused, dr_payback_nodes, dr_regions) initedges(dr_paybackunused, dr_payback_nodes, slack_node) - initedges(dr_bankused, dr_regions, dr_bank_nodes) - initedges(dr_bankunused, slack_node, dr_bank_nodes) + initedges(dr_borrowused, dr_regions, dr_borrow_nodes) + initedges(dr_borrowunused, slack_node, dr_borrow_nodes) return new( @@ -252,7 +252,7 @@ struct DispatchProblem region_nodes, stor_discharge_nodes, stor_charge_nodes, genstor_inflow_nodes, genstor_discharge_nodes, genstor_togrid_nodes, genstor_charge_nodes, - dr_bank_nodes,dr_payback_nodes, slack_node, + dr_borrow_nodes,dr_payback_nodes, slack_node, region_unservedenergy, region_unusedcapacity, iface_forward, iface_reverse, @@ -262,10 +262,10 @@ struct DispatchProblem genstor_totalgrid, genstor_gridcharge, genstor_inflowcharge, genstor_chargeunused, genstor_inflowunused, - dr_bankused, dr_paybackunused, dr_paybackused, + dr_borrowused, dr_paybackunused, dr_paybackused, dr_paybackunused, min_chargecost, max_dischargecost, - max_bankcost_dr, min_paybackcost_dr + max_borrowcost_dr, min_paybackcost_dr ) end @@ -432,9 +432,9 @@ function update_problem!( # Update Demand Response charge/discharge limits and priorities - for (i, (bank_node, bank_edge, payback_node, payback_edge)) in + for (i, (borrow_node, borrow_edge, payback_node, payback_edge)) in enumerate(zip( - problem.demandresponse_bank_nodes, problem.demandresponse_bank_edges, + problem.demandresponse_borrow_nodes, problem.demandresponse_borrow_edges, problem.demandresponse_payback_nodes, problem.demandresponse_payback_edges)) dr_online = state.drs_available[i] @@ -464,20 +464,20 @@ function update_problem!( paybackcost = problem.min_paybackcost_dr + allowablepayback # Negative cost updateflowcost!(fp.edges[payback_edge], paybackcost) - # Update banking - maxbank = dr_online * system.demandresponses.bank_capacity[i, t] - bankefficiency = system.demandresponses.bank_efficiency[i, t] - energybankable = (maxenergy - dr_energy) / bankefficiency - bank_capacity = min( - maxbank, floor(Int, energytopower( - energybankable, E, L, T, P)) + # Update borrowing + maxborrow = dr_online * system.demandresponses.borrow_capacity[i, t] + borrowefficiency = system.demandresponses.borrow_efficiency[i, t] + energyborrowable = (maxenergy - dr_energy) / borrowefficiency + borrow_capacity = min( + maxborrow, floor(Int, energytopower( + energyborrowable, E, L, T, P)) ) updateinjection!( - fp.nodes[bank_node], slack_node, bank_capacity) + fp.nodes[borrow_node], slack_node, borrow_capacity) - # Longest allowable payback window = highest priority (bank first) - bankcost = problem.max_bankcost_dr - allowablepayback # Positive cost - updateflowcost!(fp.edges[bank_edge], bankcost) + # Longest allowable payback window = highest priority (borrow first) + borrowcost = problem.max_borrowcost_dr - allowablepayback # Positive cost + updateflowcost!(fp.edges[borrow_edge], borrowcost) end @@ -527,10 +527,10 @@ function update_state!( end #Demand Response Update - #banking (negative of the flows) - for (i, e) in enumerate(problem.demandresponse_bank_edges) + #borrowing (negative of the flows) + for (i, e) in enumerate(problem.demandresponse_borrow_edges) state.drs_energy[i] += - ceil(Int, edges[e].flow * p2e * system.demandresponses.bank_efficiency[i, t]) + ceil(Int, edges[e].flow * p2e * system.demandresponses.borrow_efficiency[i, t]) end #paybacking diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 3b81d26e..83f75ae8 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -131,11 +131,11 @@ function update_paybackcounter!( #if energy is zero or negative, set counter to -1 (to start counting new) if drs_energy[i] <= 0 if payback_counter[i] >= 0 - #if no energy banked and counter is positive, reset it to -1 + #if no energy borrowed and counter is positive, reset it to -1 payback_counter[i] = -1 end elseif payback_counter[i] == -1 - #if energy is banked and counter is -1, set it to payback window-start of counting + #if energy is borrowed and counter is -1, set it to payback window-start of counting payback_counter[i] = drs.allowable_payback_period[i,t]-1 elseif payback_counter[i] >= 0 #if counter is positive, decrement by one diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index ee3c3b53..9d27b39d 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -492,11 +492,11 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse names::Vector{String} categories::Vector{String} - bank_capacity::Matrix{Int} # power + borrow_capacity::Matrix{Int} # power payback_capacity::Matrix{Int} # power energy_capacity::Matrix{Int} # energy - bank_efficiency::Matrix{Float64} + borrow_efficiency::Matrix{Float64} payback_efficiency::Matrix{Float64} carryover_efficiency::Matrix{Float64} @@ -507,8 +507,8 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse function DemandResponses{N,L,T,P,E}( names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, - bankcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, - energycapacity::Matrix{Int}, bankefficiency::Matrix{Float64}, + borrowcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, + energycapacity::Matrix{Int}, borrowefficiency::Matrix{Float64}, paybackefficiency::Matrix{Float64}, carryoverefficiency::Matrix{Float64}, allowablepaybackperiod::Matrix{Int}, λ::Matrix{Float64}, μ::Matrix{Float64} @@ -519,17 +519,17 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse @assert allunique(names) - @assert size(bankcapacity) == (n_drs, N) + @assert size(borrowcapacity) == (n_drs, N) @assert size(paybackcapacity) == (n_drs, N) @assert size(energycapacity) == (n_drs, N) - @assert all(isnonnegative, bankcapacity) + @assert all(isnonnegative, borrowcapacity) @assert all(isnonnegative, paybackcapacity) @assert all(isnonnegative, energycapacity) - @assert size(bankefficiency) == (n_drs, N) + @assert size(borrowefficiency) == (n_drs, N) @assert size(paybackefficiency) == (n_drs, N) @assert size(carryoverefficiency) == (n_drs, N) - @assert all(isfractional, bankefficiency) + @assert all(isfractional, borrowefficiency) @assert all(isfractional, paybackefficiency) @assert all(isnonnegative, carryoverefficiency) @@ -543,8 +543,8 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse @assert all(isfractional, μ) new{N,L,T,P,E}(string.(names), string.(categories), - bankcapacity, paybackcapacity, energycapacity, - bankefficiency, paybackefficiency, carryoverefficiency, + borrowcapacity, paybackcapacity, energycapacity, + borrowefficiency, paybackefficiency, carryoverefficiency, allowablepaybackperiod, λ, μ) @@ -555,10 +555,10 @@ end Base.:(==)(x::T, y::T) where {T <: DemandResponses} = x.names == y.names && x.categories == y.categories && - x.bank_capacity == y.bank_capacity && + x.borrow_capacity == y.borrow_capacity && x.payback_capacity == y.payback_capacity && x.energy_capacity == y.energy_capacity && - x.bank_efficiency == y.bank_efficiency && + x.borrow_efficiency == y.borrow_efficiency && x.payback_efficiency == y.payback_efficiency && x.carryover_efficiency == y.carryover_efficiency && x.allowable_payback_period == y.allowable_payback_period && @@ -566,9 +566,9 @@ Base.:(==)(x::T, y::T) where {T <: DemandResponses} = x.μ == y.μ Base.getindex(dr::DR, idxs::AbstractVector{Int}) where {DR <: DemandResponses} = - DR(dr.names[idxs], dr.categories[idxs],dr.bank_capacity[idxs,:], + DR(dr.names[idxs], dr.categories[idxs],dr.borrow_capacity[idxs,:], dr.payback_capacity[idxs, :],dr.energy_capacity[idxs, :], - dr.bank_efficiency[idxs, :], dr.payback_efficiency[idxs, :], + dr.borrow_efficiency[idxs, :], dr.payback_efficiency[idxs, :], dr.carryover_efficiency[idxs, :],dr.allowable_payback_period[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :]) function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} @@ -578,11 +578,11 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} names = Vector{String}(undef, n_drs) categories = Vector{String}(undef, n_drs) - bank_capacity = Matrix{Int}(undef, n_drs, N) + borrow_capacity = Matrix{Int}(undef, n_drs, N) payback_capacity = Matrix{Int}(undef, n_drs, N) energy_capacity = Matrix{Int}(undef, n_drs, N) - bank_efficiency = Matrix{Float64}(undef, n_drs, N) + borrow_efficiency = Matrix{Float64}(undef, n_drs, N) payback_efficiency = Matrix{Float64}(undef, n_drs, N) carryover_efficiency = Matrix{Float64}(undef, n_drs, N) @@ -602,11 +602,11 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} names[rows] = dr.names categories[rows] = dr.categories - bank_capacity[rows, :] = dr.bank_capacity + borrow_capacity[rows, :] = dr.borrow_capacity payback_capacity[rows, :] = dr.payback_capacity energy_capacity[rows, :] = dr.energy_capacity - bank_efficiency[rows, :] = dr.bank_efficiency + borrow_efficiency[rows, :] = dr.borrow_efficiency payback_efficiency[rows, :] = dr.payback_efficiency carryover_efficiency[rows, :] = dr.carryover_efficiency @@ -619,7 +619,7 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} end - return DemandResponses{N,L,T,P,E}(names, categories, bank_capacity, payback_capacity, energy_capacity, bank_efficiency, payback_efficiency, + return DemandResponses{N,L,T,P,E}(names, categories, borrow_capacity, payback_capacity, energy_capacity, borrow_efficiency, payback_efficiency, carryover_efficiency,allowable_payback_period, λ, μ) end From 097db7294f8ed9be7b3b2522383da764f828fa76 Mon Sep 17 00:00:00 2001 From: juflorez Date: Thu, 10 Jul 2025 10:24:06 -0600 Subject: [PATCH 21/83] account for zero allowable payback window --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 668b5f38..7d8b1dea 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -464,8 +464,8 @@ function update_problem!( paybackcost = problem.min_paybackcost_dr + allowablepayback # Negative cost updateflowcost!(fp.edges[payback_edge], paybackcost) - # Update borrowing - maxborrow = dr_online * system.demandresponses.borrow_capacity[i, t] + # Update borrowing-make sure no borrowing is allowed if allowable payback period is equal to zero + maxborrow = system.demandresponses.allowable_payback_period[i,t] != 0 ? dr_online * system.demandresponses.borrow_capacity[i, t] : 0 borrowefficiency = system.demandresponses.borrow_efficiency[i, t] energyborrowable = (maxenergy - dr_energy) / borrowefficiency borrow_capacity = min( From 9e7d199f3325664aeb1797f3c4b9ce15e1bab985 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 15 Jul 2025 15:32:34 -0600 Subject: [PATCH 22/83] add reading and writing for DR components --- PRASFiles.jl/src/PRASFiles.jl | 4 +-- PRASFiles.jl/src/Systems/read.jl | 38 ++++++++++++++++++++++ PRASFiles.jl/src/Systems/write.jl | 53 +++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/PRASFiles.jl/src/PRASFiles.jl b/PRASFiles.jl/src/PRASFiles.jl index 0106e2e1..1baed2da 100644 --- a/PRASFiles.jl/src/PRASFiles.jl +++ b/PRASFiles.jl/src/PRASFiles.jl @@ -1,8 +1,8 @@ module PRASFiles import PRASCore.Systems: SystemModel, Regions, Interfaces, - Generators, Storages, GeneratorStorages, Lines, - timeunits, powerunits, energyunits, unitsymbol + Generators, Storages, GeneratorStorages, DemandResponses, Lines, + timeunits, powerunits, energyunits, unitsymbol import PRASCore.Results: EUE, LOLE, NEUE, ShortfallResult, ShortfallSamplesResult, AbstractShortfallResult, Result import StatsBase: mean diff --git a/PRASFiles.jl/src/Systems/read.jl b/PRASFiles.jl/src/Systems/read.jl index fd37b210..57f6c810 100644 --- a/PRASFiles.jl/src/Systems/read.jl +++ b/PRASFiles.jl/src/Systems/read.jl @@ -58,6 +58,7 @@ function _systemmodel_core(f::File) has_generators = haskey(f, "generators") has_storages = haskey(f, "storages") has_generatorstorages = haskey(f, "generatorstorages") + has_demandresponses = haskey(f, "demandresponses") has_interfaces = haskey(f, "interfaces") has_lines = haskey(f, "lines") @@ -181,6 +182,42 @@ function _systemmodel_core(f::File) end + if has_demandresponses + + dr_core = read(f["demandresponses/_core"]) + dr_names, dr_categories, dr_regionnames = readvector.( + Ref(dr_core), [:name, :category, :region]) + + dr_regions = getindex.(Ref(regionlookup), dr_regionnames) + region_order = sortperm(dr_regions) + + demandresponses = DemandResponses{N,L,T,P,E}( + dr_names[region_order], dr_categories[region_order], + load_matrix(f["demandresponses/borrowcapacity"], region_order, Int), + load_matrix(f["demandresponses/paybackcapacity"], region_order, Int), + load_matrix(f["demandresponses/energycapacity"], region_order, Int), + load_matrix(f["demandresponses/borrowefficiency"], region_order, Float64), + load_matrix(f["demandresponses/paybackefficiency"], region_order, Float64), + load_matrix(f["demandresponses/carryoverefficiency"], region_order, Float64), + load_matrix(f["demandresponses/allowablepaybackperiod"], region_order, Int), + load_matrix(f["demandresponses/failureprobability"], region_order, Float64), + load_matrix(f["demandresponses/repairprobability"], region_order, Float64) + ) + + region_dr_idxs = makeidxlist(dr_regions[region_order], n_regions) + + else + demandresponses = DemandResponses{N,L,T,P,E}( + String[], String[], + zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), + zeros(Int, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N)) + + region_dr_idxs = fill(1:0, n_regions) + + end + + if has_interfaces interfaces_core = read(f["interfaces/_core"]) @@ -304,6 +341,7 @@ function systemmodel_0_8(f::File) generators, region_gen_idxs, storages, region_stor_idxs, generatorstorages, region_genstor_idxs, + demandresponses, region_dr_idxs, lines, interface_line_idxs, timestamps, attrs) diff --git a/PRASFiles.jl/src/Systems/write.jl b/PRASFiles.jl/src/Systems/write.jl index 4bda464f..08ec2e50 100644 --- a/PRASFiles.jl/src/Systems/write.jl +++ b/PRASFiles.jl/src/Systems/write.jl @@ -39,6 +39,14 @@ function savemodel( process_generatorstorages!(f, sys, string_length, compression_level) end + verbose && @info "$(length(sys.demandresponses)) DemandResponses " * + "found in the SystemModel." + + if length(sys.demandresponses) > 0 + verbose && @info "Processing DemandResponses for .pras file ..." + process_demandresponses!(f, sys, string_length, compression_level) + end + if length(sys.regions) > 1 verbose && @info "Processing Lines and Interfaces for .pras file ..." process_lines_interfaces!(f, sys, string_length, compression_level) @@ -213,6 +221,51 @@ function process_generatorstorages!( end +function process_demandresponses!( + f::File, sys::SystemModel, strlen::Int, compression::Int) + + demandresponses = create_group(f, "demandresponses") + + drs_core = Matrix{String}(undef, length(sys.demandresponses), 3) + drs_core_colnames = ["name", "category", "region"] + + drs_core[:, 1] = sys.demandresponses.names + drs_core[:, 2] = sys.demandresponses.categories + drs_core[:, 3] = regionnames( + length(sys.demandresponses), sys.regions.names, sys.region_dr_idxs) + + string_table!(demandresponses, "_core", drs_core_colnames, drs_core, strlen) + + demandresponses["borrowcapacity", deflate = compression] = + sys.demandresponses.borrow_capacity + + demandresponses["paybackcapacity", deflate = compression] = + sys.demandresponses.payback_capacity + + demandresponses["energycapacity", deflate = compression] = + sys.demandresponses.energy_capacity + + demandresponses["borrowefficiency", deflate = compression] = + sys.demandresponses.borrow_efficiency + + demandresponses["paybackefficiency", deflate = compression] = + sys.demandresponses.payback_efficiency + + demandresponses["carryoverefficiency", deflate = compression] = + sys.demandresponses.carryover_efficiency + + demandresponses["allowablepaybackperiod", deflate = compression] = + sys.demandresponses.allowable_payback_period + + demandresponses["failureprobability", deflate = compression] = sys.demandresponses.λ + + demandresponses["repairprobability", deflate = compression] = sys.demandresponses.μ + + return + +end + + function process_lines_interfaces!( f::File, sys::SystemModel, strlen::Int, compression::Int) From f7a9cf47c835b794b9c73aa608ad12ccbf49ee69 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 5 Aug 2025 12:15:30 -0600 Subject: [PATCH 23/83] count any energy in DR at end of sim as unserved --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 4 ++-- PRASCore.jl/src/Simulations/recording.jl | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 7d8b1dea..caf3ab19 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -539,8 +539,8 @@ function update_state!( energy_drop = ceil(Int, edges[e].flow * p2e / system.demandresponses.payback_efficiency[i, t]) state.drs_energy[i] = max(0, energy - energy_drop) - if state.drs_paybackcounter[i] == 0 - #if payback window is over, count the unserved energy in drs_unservedenergy state and reset energy + if (state.drs_paybackcounter[i] == 0) || (t == N) + #if payback window is over or you reach the end of the sim, count the unserved energy in drs_unservedenergy state and reset energy state.drs_unservedenergy[i] = state.drs_energy[i] state.drs_energy[i] = 0 end diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 5cf363e7..327f1061 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -18,7 +18,7 @@ function record!( regionshortfall = edges[r].flow dr_shortfall = 0 for i in dr_idxs - dr_shortfall += state.drs_paybackcounter[i] == 0 ? state.drs_unservedenergy[i] : 0 + dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 end regionshortfall += dr_shortfall isregionshortfall = regionshortfall > 0 @@ -84,7 +84,7 @@ function record!( #getting dr shortfall dr_shortfall = 0 for i in dr_idxs - dr_shortfall += state.drs_paybackcounter[i] == 0 ? states.drs_unservedenergy[i] : 0 + dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? states.drs_unservedenergy[i] : 0 end acc.shortfall[r, t, sampleid] = problem.fp.edges[e].flow + dr_shortfall @@ -113,7 +113,7 @@ function record!( #count region shortfall and include dr shortfall dr_shortfall = 0 for i in dr_idxs - dr_shortfall += state.drs_paybackcounter[i] == 0 ? state.drs_unservedenergy[i] : 0 + dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 end regionshortfall = dr_shortfall isregionshortfall = regionshortfall > 0 @@ -177,7 +177,7 @@ function record!( #getting dr shortfall dr_shortfall = 0 for i in dr_idxs - dr_shortfall += state.drs_paybackcounter[i] == 0 ? states.drs_unservedenergy[i] : 0 + dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? states.drs_unservedenergy[i] : 0 end acc.shortfall[r, t, sampleid] = dr_shortfall From 66e5df5ad8851763707c5b5827d07dd8f86f6590 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Tue, 24 Jun 2025 09:03:47 -0600 Subject: [PATCH 24/83] Allow for additional user defined attributes in PRAS HDF5 --- PRASCore.jl/src/Systems/SystemModel.jl | 5 +++-- PRASFiles.jl/src/PRASFiles.jl | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index 4b58c565..ad22ca59 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -177,7 +177,8 @@ function SystemModel( storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, lines, interface_line_idxs::Vector{UnitRange{Int}}, - timestamps::StepRange{DateTime,T} + timestamps::StepRange{DateTime,T}, + attrs::Dict{String, String}=Dict{String, String}() ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} @warn "No time zone data provided - defaulting to UTC. To specify a " * @@ -200,7 +201,7 @@ function SystemModel( Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), repeat([1:0],length(regions)), lines, interface_line_idxs, - timestamps_tz) + timestamps_tz,attrs) end diff --git a/PRASFiles.jl/src/PRASFiles.jl b/PRASFiles.jl/src/PRASFiles.jl index 1baed2da..8214756b 100644 --- a/PRASFiles.jl/src/PRASFiles.jl +++ b/PRASFiles.jl/src/PRASFiles.jl @@ -2,7 +2,8 @@ module PRASFiles import PRASCore.Systems: SystemModel, Regions, Interfaces, Generators, Storages, GeneratorStorages, DemandResponses, Lines, - timeunits, powerunits, energyunits, unitsymbol + timeunits, powerunits, energyunits, unitsymbol, + get_attrs import PRASCore.Results: EUE, LOLE, NEUE, ShortfallResult, ShortfallSamplesResult, AbstractShortfallResult, Result import StatsBase: mean From 6c102672af25ddbc74f732cbbeee2ac92fec3968 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Tue, 29 Jul 2025 15:49:31 -0600 Subject: [PATCH 25/83] Move demandresponse read logic to 0_8 read version --- PRASFiles.jl/src/Systems/read.jl | 74 ++++++++++++++++---------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/PRASFiles.jl/src/Systems/read.jl b/PRASFiles.jl/src/Systems/read.jl index 57f6c810..c6cd8287 100644 --- a/PRASFiles.jl/src/Systems/read.jl +++ b/PRASFiles.jl/src/Systems/read.jl @@ -58,7 +58,6 @@ function _systemmodel_core(f::File) has_generators = haskey(f, "generators") has_storages = haskey(f, "storages") has_generatorstorages = haskey(f, "generatorstorages") - has_demandresponses = haskey(f, "demandresponses") has_interfaces = haskey(f, "interfaces") has_lines = haskey(f, "lines") @@ -182,42 +181,6 @@ function _systemmodel_core(f::File) end - if has_demandresponses - - dr_core = read(f["demandresponses/_core"]) - dr_names, dr_categories, dr_regionnames = readvector.( - Ref(dr_core), [:name, :category, :region]) - - dr_regions = getindex.(Ref(regionlookup), dr_regionnames) - region_order = sortperm(dr_regions) - - demandresponses = DemandResponses{N,L,T,P,E}( - dr_names[region_order], dr_categories[region_order], - load_matrix(f["demandresponses/borrowcapacity"], region_order, Int), - load_matrix(f["demandresponses/paybackcapacity"], region_order, Int), - load_matrix(f["demandresponses/energycapacity"], region_order, Int), - load_matrix(f["demandresponses/borrowefficiency"], region_order, Float64), - load_matrix(f["demandresponses/paybackefficiency"], region_order, Float64), - load_matrix(f["demandresponses/carryoverefficiency"], region_order, Float64), - load_matrix(f["demandresponses/allowablepaybackperiod"], region_order, Int), - load_matrix(f["demandresponses/failureprobability"], region_order, Float64), - load_matrix(f["demandresponses/repairprobability"], region_order, Float64) - ) - - region_dr_idxs = makeidxlist(dr_regions[region_order], n_regions) - - else - demandresponses = DemandResponses{N,L,T,P,E}( - String[], String[], - zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), - zeros(Int, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N)) - - region_dr_idxs = fill(1:0, n_regions) - - end - - if has_interfaces interfaces_core = read(f["interfaces/_core"]) @@ -326,6 +289,8 @@ end Read a SystemModel from a PRAS file in version 0.8.x format. """ function systemmodel_0_8(f::File) + + has_demandresponses = haskey(f, "demandresponses") regions, interfaces, generators, region_gen_idxs, @@ -334,6 +299,41 @@ function systemmodel_0_8(f::File) lines, interface_line_idxs, timestamps = _systemmodel_core(f) + if has_demandresponses + + dr_core = read(f["demandresponses/_core"]) + dr_names, dr_categories, dr_regionnames = readvector.( + Ref(dr_core), [:name, :category, :region]) + + dr_regions = getindex.(Ref(regionlookup), dr_regionnames) + region_order = sortperm(dr_regions) + + demandresponses = DemandResponses{N,L,T,P,E}( + dr_names[region_order], dr_categories[region_order], + load_matrix(f["demandresponses/borrowcapacity"], region_order, Int), + load_matrix(f["demandresponses/paybackcapacity"], region_order, Int), + load_matrix(f["demandresponses/energycapacity"], region_order, Int), + load_matrix(f["demandresponses/borrowefficiency"], region_order, Float64), + load_matrix(f["demandresponses/paybackefficiency"], region_order, Float64), + load_matrix(f["demandresponses/carryoverefficiency"], region_order, Float64), + load_matrix(f["demandresponses/allowablepaybackperiod"], region_order, Int), + load_matrix(f["demandresponses/failureprobability"], region_order, Float64), + load_matrix(f["demandresponses/repairprobability"], region_order, Float64) + ) + + region_dr_idxs = makeidxlist(dr_regions[region_order], n_regions) + + else + demandresponses = DemandResponses{N,L,T,P,E}( + String[], String[], + zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), + zeros(Int, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N)) + + region_dr_idxs = fill(1:0, n_regions) + + end + attrs = read_attrs(f) return SystemModel( From f08b28d6dc377bd5fbfa608732e3afd328f09930 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Thu, 31 Jul 2025 11:16:35 -0600 Subject: [PATCH 26/83] Correct errors with systemmodel_0_8 and repeated entries in PRASCore SystemModel constructors --- PRASCore.jl/src/Systems/SystemModel.jl | 26 +++++++++++++------------- PRASFiles.jl/src/PRASFiles.jl | 5 ++--- PRASFiles.jl/src/Systems/read.jl | 18 ++++++++++++++---- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index ad22ca59..40299afc 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -111,16 +111,18 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} end +end + #base system constructor- no demand response devices - function SystemModel{}( - regions::Regions{N,P}, interfaces::Interfaces{N,P}, - generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, - storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, - generatorstorages::GeneratorStorages{N,L,T,P,E}, - region_genstor_idxs::Vector{UnitRange{Int}}, - lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, - timestamps::StepRange{ZonedDateTime,T} - ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} +function SystemModel{}( + regions::Regions{N,P}, interfaces::Interfaces{N,P}, + generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, + storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, + generatorstorages::GeneratorStorages{N,L,T,P,E}, + region_genstor_idxs::Vector{UnitRange{Int}}, + lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, + timestamps::StepRange{ZonedDateTime,T} +) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} return SystemModel( regions, interfaces, @@ -134,10 +136,8 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), repeat([1:0],length(regions)), lines, interface_line_idxs, timestamps) - end - end - + # No time zone constructor - demand responses included function SystemModel( regions::Regions{N,P}, interfaces::Interfaces{N,P}, @@ -205,7 +205,7 @@ function SystemModel( end -# Single-node constructor - demand responses not included +# Single-node constructor - demand responses included function SystemModel( generators::Generators{N,L,T,P}, storages::Storages{N,L,T,P,E}, diff --git a/PRASFiles.jl/src/PRASFiles.jl b/PRASFiles.jl/src/PRASFiles.jl index 8214756b..a212b139 100644 --- a/PRASFiles.jl/src/PRASFiles.jl +++ b/PRASFiles.jl/src/PRASFiles.jl @@ -2,9 +2,8 @@ module PRASFiles import PRASCore.Systems: SystemModel, Regions, Interfaces, Generators, Storages, GeneratorStorages, DemandResponses, Lines, - timeunits, powerunits, energyunits, unitsymbol, - get_attrs - + timeunits, powerunits, energyunits, unitsymbol + import PRASCore.Results: EUE, LOLE, NEUE, ShortfallResult, ShortfallSamplesResult, AbstractShortfallResult, Result import StatsBase: mean import Dates: @dateformat_str, format, now diff --git a/PRASFiles.jl/src/Systems/read.jl b/PRASFiles.jl/src/Systems/read.jl index c6cd8287..b46097fa 100644 --- a/PRASFiles.jl/src/Systems/read.jl +++ b/PRASFiles.jl/src/Systems/read.jl @@ -50,6 +50,8 @@ function _systemmodel_core(f::File) P = powerunits[read(metadata["power_unit"])] E = energyunits[read(metadata["energy_unit"])] + type_params = (N,L,T,P,E) + timestep = T(L) end_timestamp = start_timestamp + (N-1)*timestep timestamps = StepRange(start_timestamp, timestep, end_timestamp) @@ -273,7 +275,7 @@ function _systemmodel_core(f::File) storages, region_stor_idxs, generatorstorages, region_genstor_idxs, lines, interface_line_idxs, - timestamps) + timestamps),type_params end """ @@ -281,7 +283,9 @@ Read a SystemModel from a PRAS file in version 0.5.x - 0.7.x format. """ function systemmodel_0_5(f::File) - return SystemModel(_systemmodel_core(f)...) + systemmodel_0_5_objs, _ = _systemmodel_core(f) + + return SystemModel(systemmodel_0_5_objs...) end @@ -292,15 +296,21 @@ function systemmodel_0_8(f::File) has_demandresponses = haskey(f, "demandresponses") - regions, interfaces, + (regions, interfaces, generators, region_gen_idxs, storages, region_stor_idxs, generatorstorages, region_genstor_idxs, lines, interface_line_idxs, - timestamps = _systemmodel_core(f) + timestamps), type_params = _systemmodel_core(f) + + N, L, T, P, E = type_params + + n_regions = length(regions) + regionlookup = Dict(n=>i for (i, n) in enumerate(regions.names)) if has_demandresponses + dr_core = read(f["demandresponses/_core"]) dr_names, dr_categories, dr_regionnames = readvector.( Ref(dr_core), [:name, :category, :region]) From 8e53cbbecfa7dc15310e9498fd33b996ce0a48e3 Mon Sep 17 00:00:00 2001 From: juflorez Date: Thu, 14 Aug 2025 15:34:09 -0600 Subject: [PATCH 27/83] implement copilot fixes --- PRASCore.jl/src/Results/DemandResponseShortfall.jl | 2 +- PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl | 2 +- PRASCore.jl/src/Simulations/DispatchProblem.jl | 4 ++-- PRASCore.jl/src/Simulations/recording.jl | 4 ++-- PRASCore.jl/src/Systems/assets.jl | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/PRASCore.jl/src/Results/DemandResponseShortfall.jl b/PRASCore.jl/src/Results/DemandResponseShortfall.jl index 1938feaa..7aab19ee 100644 --- a/PRASCore.jl/src/Results/DemandResponseShortfall.jl +++ b/PRASCore.jl/src/Results/DemandResponseShortfall.jl @@ -23,7 +23,7 @@ sf_mean, sf_std = DemandResponseShortfall["Region A", period] # System-wide risk metrics eue = EUE(DemandResponseShortfall) lole = LOLE(DemandResponseShortfall) -neue = NEUE(shorfall) +neue = NEUE(DemandResponseShortfall) # Regional risk metrics regional_eue = EUE(DemandResponseShortfall, "Region A") diff --git a/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl b/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl index 07ff6116..434afff4 100644 --- a/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl +++ b/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl @@ -33,7 +33,7 @@ regional_neue = NEUE(drshortfall, "Region A") # Period-specific risk metrics period_eue = EUE(drshortfall, period) -period_lolp = LOLE(shortfall, period) +period_lolp = LOLE(drshortfall, period) # Region- and period-specific risk metrics period_eue = EUE(drshortfall, "Region A", period) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index caf3ab19..a9dbaa39 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -262,8 +262,8 @@ struct DispatchProblem genstor_totalgrid, genstor_gridcharge, genstor_inflowcharge, genstor_chargeunused, genstor_inflowunused, - dr_borrowused, dr_paybackunused, dr_paybackused, - dr_paybackunused, + dr_paybackused,dr_paybackunused, + dr_borrowused, dr_borrowunused, min_chargecost, max_dischargecost, max_borrowcost_dr, min_paybackcost_dr ) diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 327f1061..ebc6fa00 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -84,7 +84,7 @@ function record!( #getting dr shortfall dr_shortfall = 0 for i in dr_idxs - dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? states.drs_unservedenergy[i] : 0 + dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 end acc.shortfall[r, t, sampleid] = problem.fp.edges[e].flow + dr_shortfall @@ -177,7 +177,7 @@ function record!( #getting dr shortfall dr_shortfall = 0 for i in dr_idxs - dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? states.drs_unservedenergy[i] : 0 + dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 end acc.shortfall[r, t, sampleid] = dr_shortfall diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 9d27b39d..6a97e82a 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -586,7 +586,7 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} payback_efficiency = Matrix{Float64}(undef, n_drs, N) carryover_efficiency = Matrix{Float64}(undef, n_drs, N) - allowable_payback_period = Matrix{Float64}(undef, n_drs, N) + allowable_payback_period = Matrix{Int}(undef, n_drs, N) λ = Matrix{Float64}(undef, n_drs, N) From 9accdd68749abbc131d1d37f3cc448ad422acc12 Mon Sep 17 00:00:00 2001 From: juflorez Date: Thu, 14 Aug 2025 18:56:58 -0600 Subject: [PATCH 28/83] fix payback borrow edge definitions --- .../src/Simulations/DispatchProblem.jl | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index a9dbaa39..ce9ce109 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -160,9 +160,9 @@ struct DispatchProblem genstor_discharge_nodes = indices_after(genstor_inflow_nodes, ngenstors) genstor_togrid_nodes = indices_after(genstor_discharge_nodes, ngenstors) genstor_charge_nodes = indices_after(genstor_togrid_nodes, ngenstors) - dr_borrow_nodes = indices_after(genstor_charge_nodes, ndrs) - dr_payback_nodes = indices_after(dr_borrow_nodes, ndrs) - slack_node = nnodes = last(dr_payback_nodes) + 1 + dr_payback_nodes = indices_after(genstor_charge_nodes, ndrs) + dr_borrow_nodes = indices_after(dr_payback_nodes, ndrs) + slack_node = nnodes = last(dr_borrow_nodes) + 1 region_unservedenergy = 1:nregions region_unusedcapacity = indices_after(region_unservedenergy, nregions) @@ -180,11 +180,11 @@ struct DispatchProblem genstor_inflowcharge = indices_after(genstor_gridcharge, ngenstors) genstor_chargeunused = indices_after(genstor_inflowcharge, ngenstors) genstor_inflowunused = indices_after(genstor_chargeunused, ngenstors) - dr_borrowused = indices_after(genstor_inflowunused, ndrs) - dr_borrowunused = indices_after(dr_borrowused, ndrs) - dr_paybackused = indices_after(dr_borrowunused, ndrs) + dr_paybackused = indices_after(genstor_inflowunused, ndrs) dr_paybackunused = indices_after(dr_paybackused, ndrs) - nedges = last(dr_paybackunused) + dr_borrowused = indices_after(dr_paybackunused, ndrs) + dr_borrowunused = indices_after(dr_borrowused, ndrs) + nedges = last(dr_borrowunused) nodesfrom = Vector{Int}(undef, nedges) nodesto = Vector{Int}(undef, nedges) @@ -239,11 +239,10 @@ struct DispatchProblem initedges(genstor_inflowunused, genstor_inflow_nodes, slack_node) # Demand Response borrowing / payback - initedges(dr_paybackused, dr_payback_nodes, dr_regions) - initedges(dr_paybackunused, dr_payback_nodes, slack_node) - initedges(dr_borrowused, dr_regions, dr_borrow_nodes) - initedges(dr_borrowunused, slack_node, dr_borrow_nodes) - + initedges(dr_paybackused, dr_regions, dr_payback_nodes) + initedges(dr_paybackunused, slack_node,dr_payback_nodes) + initedges(dr_borrowused, dr_borrow_nodes, dr_regions) + initedges(dr_borrowunused, dr_borrow_nodes, slack_node) return new( @@ -252,7 +251,7 @@ struct DispatchProblem region_nodes, stor_discharge_nodes, stor_charge_nodes, genstor_inflow_nodes, genstor_discharge_nodes, genstor_togrid_nodes, genstor_charge_nodes, - dr_borrow_nodes,dr_payback_nodes, slack_node, + dr_payback_nodes, dr_borrow_nodes, slack_node, region_unservedenergy, region_unusedcapacity, iface_forward, iface_reverse, @@ -262,7 +261,7 @@ struct DispatchProblem genstor_totalgrid, genstor_gridcharge, genstor_inflowcharge, genstor_chargeunused, genstor_inflowunused, - dr_paybackused,dr_paybackunused, + dr_paybackused, dr_paybackunused, dr_borrowused, dr_borrowunused, min_chargecost, max_dischargecost, max_borrowcost_dr, min_paybackcost_dr From 315356bbdd5973a14394bcdb3f2fd6cb78c6c0ed Mon Sep 17 00:00:00 2001 From: juflorez Date: Fri, 15 Aug 2025 09:49:38 -0600 Subject: [PATCH 29/83] fix wheeling DR ordering overlap --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index ce9ce109..9105bd12 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -142,7 +142,7 @@ struct DispatchProblem #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) - min_paybackcost_dr = - maxpaybacktime_dr - 1 + min_chargecost #add min_chargecost to always have DR devices be paybacked first + min_paybackcost_dr = - maxpaybacktime_dr - 2 + min_chargecost #add min_chargecost to always have DR devices be paybacked first max_borrowcost_dr = - min_paybackcost_dr + minpaybacktime_dr + 1 + max_dischargecost #add max_dischargecost to always have DR devices be borrowed last #for unserved energy From f7fcbf9839f1ebf0f813e792cf38cf06579a17a0 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 25 Aug 2025 13:18:11 -0600 Subject: [PATCH 30/83] refactor dr shortfall to point to shortfall.jl or shortfallsamples.jl --- .../src/Results/DemandResponseShortfall.jl | 237 ++++-------------- .../Results/DemandResponseShortfallSamples.jl | 73 ++---- PRASCore.jl/src/Simulations/recording.jl | 41 ++- 3 files changed, 88 insertions(+), 263 deletions(-) diff --git a/PRASCore.jl/src/Results/DemandResponseShortfall.jl b/PRASCore.jl/src/Results/DemandResponseShortfall.jl index 7aab19ee..c055a2cc 100644 --- a/PRASCore.jl/src/Results/DemandResponseShortfall.jl +++ b/PRASCore.jl/src/Results/DemandResponseShortfall.jl @@ -43,148 +43,51 @@ See [`DemandResponseShortfallSamples`](@ref) for recording sample-level DemandRe """ struct DemandResponseShortfall <: ResultSpec end -mutable struct DemandResponseShortfallAccumulator <: ResultAccumulator{DemandResponseShortfall} - - # Cross-simulation LOL period count mean/variances - periodsdropped_total::MeanVariance - periodsdropped_region::Vector{MeanVariance} - periodsdropped_period::Vector{MeanVariance} - periodsdropped_regionperiod::Matrix{MeanVariance} - - # Running LOL period counts for current simulation - periodsdropped_total_currentsim::Int - periodsdropped_region_currentsim::Vector{Int} - - # Cross-simulation UE mean/variances - unservedload_total::MeanVariance - unservedload_region::Vector{MeanVariance} - unservedload_period::Vector{MeanVariance} - unservedload_regionperiod::Matrix{MeanVariance} - - # Running UE totals for current simulation - unservedload_total_currentsim::Int - unservedload_region_currentsim::Vector{Int} - +mutable struct DemandResponseShortfallAccumulator <: ResultAccumulator{DemandResponseShortfall} + base::ShortfallAccumulator end function accumulator( sys::SystemModel{N}, nsamples::Int, ::DemandResponseShortfall ) where {N} - - nregions = length(sys.regions) - - periodsdropped_total = meanvariance() - periodsdropped_region = [meanvariance() for _ in 1:nregions] - periodsdropped_period = [meanvariance() for _ in 1:N] - periodsdropped_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] - - periodsdropped_total_currentsim = 0 - periodsdropped_region_currentsim = zeros(Int, nregions) - - unservedload_total = meanvariance() - unservedload_region = [meanvariance() for _ in 1:nregions] - unservedload_period = [meanvariance() for _ in 1:N] - unservedload_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] - - unservedload_total_currentsim = 0 - unservedload_region_currentsim = zeros(Int, nregions) - - return DemandResponseShortfallAccumulator( - periodsdropped_total, periodsdropped_region, - periodsdropped_period, periodsdropped_regionperiod, - periodsdropped_total_currentsim, periodsdropped_region_currentsim, - unservedload_total, unservedload_region, - unservedload_period, unservedload_regionperiod, - unservedload_total_currentsim, unservedload_region_currentsim) - + base_acc = accumulator(sys, nsamples, Shortfall()) # call original accumulator + return DemandResponseShortfallAccumulator(base_acc) end -function merge!( - x::DemandResponseShortfallAccumulator, y::DemandResponseShortfallAccumulator -) - - merge!(x.periodsdropped_total, y.periodsdropped_total) - foreach(merge!, x.periodsdropped_region, y.periodsdropped_region) - foreach(merge!, x.periodsdropped_period, y.periodsdropped_period) - foreach(merge!, x.periodsdropped_regionperiod, y.periodsdropped_regionperiod) - merge!(x.unservedload_total, y.unservedload_total) - foreach(merge!, x.unservedload_region, y.unservedload_region) - foreach(merge!, x.unservedload_period, y.unservedload_period) - foreach(merge!, x.unservedload_regionperiod, y.unservedload_regionperiod) - - return +function merge!(x::DemandResponseShortfallAccumulator, y::DemandResponseShortfallAccumulator) + merge!(x.base, y.base) end accumulatortype(::DemandResponseShortfall) = DemandResponseShortfallAccumulator -struct DemandResponseShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: - AbstractShortfallResult{N, L, T} - nsamples::Union{Int, Nothing} - regions::Regions - timestamps::StepRange{ZonedDateTime,T} - - eventperiod_mean::Float64 - eventperiod_std::Float64 - - eventperiod_region_mean::Vector{Float64} - eventperiod_region_std::Vector{Float64} - - eventperiod_period_mean::Vector{Float64} - eventperiod_period_std::Vector{Float64} - eventperiod_regionperiod_mean::Matrix{Float64} - eventperiod_regionperiod_std::Matrix{Float64} - - - shortfall_mean::Matrix{Float64} # r x t +struct DemandResponseShortfallResult{N,L,T,E} <: AbstractShortfallResult{N, L, T} + base::ShortfallResult{N,L,T,E} +end - shortfall_std::Float64 - shortfall_region_std::Vector{Float64} - shortfall_period_std::Vector{Float64} +function DemandResponseShortfallResult{N,L,T,E}( + nsamples::Union{Int,Nothing}, + regions::Regions, + timestamps::StepRange{ZonedDateTime,T}, + eventperiod_mean::Float64, + eventperiod_std::Float64, + eventperiod_region_mean::Vector{Float64}, + eventperiod_region_std::Vector{Float64}, + eventperiod_period_mean::Vector{Float64}, + eventperiod_period_std::Vector{Float64}, + eventperiod_regionperiod_mean::Matrix{Float64}, + eventperiod_regionperiod_std::Matrix{Float64}, + shortfall_mean::Matrix{Float64}, + shortfall_std::Float64, + shortfall_region_std::Vector{Float64}, + shortfall_period_std::Vector{Float64}, shortfall_regionperiod_std::Matrix{Float64} - - function DemandResponseShortfallResult{N,L,T,E}( - nsamples::Union{Int,Nothing}, - regions::Regions, - timestamps::StepRange{ZonedDateTime,T}, - eventperiod_mean::Float64, - eventperiod_std::Float64, - eventperiod_region_mean::Vector{Float64}, - eventperiod_region_std::Vector{Float64}, - eventperiod_period_mean::Vector{Float64}, - eventperiod_period_std::Vector{Float64}, - eventperiod_regionperiod_mean::Matrix{Float64}, - eventperiod_regionperiod_std::Matrix{Float64}, - shortfall_mean::Matrix{Float64}, - shortfall_std::Float64, - shortfall_region_std::Vector{Float64}, - shortfall_period_std::Vector{Float64}, - shortfall_regionperiod_std::Matrix{Float64} - ) where {N,L,T<:Period,E<:EnergyUnit} - - isnothing(nsamples) || nsamples > 0 || - throw(DomainError("Sample count must be positive or `nothing`.")) - - - length(timestamps) == N || - error("The provided timestamp range does not match the simulation length") - - nregions = length(regions.names) - - length(eventperiod_region_mean) == nregions && - length(eventperiod_region_std) == nregions && - length(eventperiod_period_mean) == N && - length(eventperiod_period_std) == N && - size(eventperiod_regionperiod_mean) == (nregions, N) && - size(eventperiod_regionperiod_std) == (nregions, N) && - length(shortfall_region_std) == nregions && - length(shortfall_period_std) == N && - size(shortfall_regionperiod_std) == (nregions, N) || - error("Inconsistent input data sizes") - - new{N,L,T,E}(nsamples, regions, timestamps, +) where {N,L,T<:Period,E<:EnergyUnit} + DemandResponseShortfallResult( + ShortfallResult{N,L,T,E}( + nsamples, regions, timestamps, eventperiod_mean, eventperiod_std, eventperiod_region_mean, eventperiod_region_std, eventperiod_period_mean, eventperiod_period_std, @@ -192,113 +95,69 @@ struct DemandResponseShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: shortfall_mean, shortfall_std, shortfall_region_std, shortfall_period_std, shortfall_regionperiod_std) - - end + ) end + function getindex(x::DemandResponseShortfallResult) - return sum(x.shortfall_mean), x.shortfall_std + return getindex(x.base) end function getindex(x::DemandResponseShortfallResult, r::AbstractString) - i_r = findfirstunique(x.regions.names, r) - return sum(view(x.shortfall_mean, i_r, :)), x.shortfall_region_std[i_r] + return getindex(x.base, r) end function getindex(x::DemandResponseShortfallResult, t::ZonedDateTime) - i_t = findfirstunique(x.timestamps, t) - return sum(view(x.shortfall_mean, :, i_t)), x.shortfall_period_std[i_t] + return getindex(x.base, t) end function getindex(x::DemandResponseShortfallResult, r::AbstractString, t::ZonedDateTime) - i_r = findfirstunique(x.regions.names, r) - i_t = findfirstunique(x.timestamps, t) - return x.shortfall_mean[i_r, i_t], x.shortfall_regionperiod_std[i_r, i_t] + return getindex(x.base, r, t) end LOLE(x::DemandResponseShortfallResult{N,L,T}) where {N,L,T} = - LOLE{N,L,T}(MeanEstimate(x.eventperiod_mean, - x.eventperiod_std, - x.nsamples)) + LOLE(x.base) function LOLE(x::DemandResponseShortfallResult{N,L,T}, r::AbstractString) where {N,L,T} - i_r = findfirstunique(x.regions.names, r) - return LOLE{N,L,T}(MeanEstimate(x.eventperiod_region_mean[i_r], - x.eventperiod_region_std[i_r], - x.nsamples)) + return LOLE(x.base, r) end function LOLE(x::DemandResponseShortfallResult{N,L,T}, t::ZonedDateTime) where {N,L,T} - i_t = findfirstunique(x.timestamps, t) - return LOLE{1,L,T}(MeanEstimate(x.eventperiod_period_mean[i_t], - x.eventperiod_period_std[i_t], - x.nsamples)) + return LOLE(x.base, t) end function LOLE(x::DemandResponseShortfallResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} - i_r = findfirstunique(x.regions.names, r) - i_t = findfirstunique(x.timestamps, t) - return LOLE{1,L,T}(MeanEstimate(x.eventperiod_regionperiod_mean[i_r, i_t], - x.eventperiod_regionperiod_std[i_r, i_t], - x.nsamples)) + return LOLE(x.base, r, t) end - EUE(x::DemandResponseShortfallResult{N,L,T,E}) where {N,L,T,E} = - EUE{N,L,T,E}(MeanEstimate(x[]..., x.nsamples)) + EUE(x.base) EUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} = - EUE{N,L,T,E}(MeanEstimate(x[r]..., x.nsamples)) + EUE(x.base, r) EUE(x::DemandResponseShortfallResult{N,L,T,E}, t::ZonedDateTime) where {N,L,T,E} = - EUE{1,L,T,E}(MeanEstimate(x[t]..., x.nsamples)) + EUE(x.base, t) EUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E} = - EUE{1,L,T,E}(MeanEstimate(x[r, t]..., x.nsamples)) + EUE(x.base, r, t) function NEUE(x::DemandResponseShortfallResult{N,L,T,E}) where {N,L,T,E} - return NEUE(div(MeanEstimate(x[]..., x.nsamples),(sum(x.regions.load)/1e6))) + return NEUE(x.base) end function NEUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} - i_r = findfirstunique(x.regions.names, r) - return NEUE(div(MeanEstimate(x[r]..., x.nsamples),(sum(x.regions.load[i_r,:])/1e6))) + return NEUE(x.base, r) end + function finalize( acc::DemandResponseShortfallAccumulator, system::SystemModel{N,L,T,P,E}, ) where {N,L,T,P,E} + base_result = finalize(acc.base, system) - ep_total_mean, ep_total_std = mean_std(acc.periodsdropped_total) - ep_region_mean, ep_region_std = mean_std(acc.periodsdropped_region) - ep_period_mean, ep_period_std = mean_std(acc.periodsdropped_period) - ep_regionperiod_mean, ep_regionperiod_std = - mean_std(acc.periodsdropped_regionperiod) - - _, ue_total_std = mean_std(acc.unservedload_total) - _, ue_region_std = mean_std(acc.unservedload_region) - _, ue_period_std = mean_std(acc.unservedload_period) - ue_regionperiod_mean, ue_regionperiod_std = - mean_std(acc.unservedload_regionperiod) - - nsamples = first(acc.unservedload_total.stats).n - - p2e = conversionfactor(L,T,P,E) - ue_regionperiod_mean .*= p2e - ue_total_std *= p2e - ue_region_std .*= p2e - ue_period_std .*= p2e - ue_regionperiod_std .*= p2e - - return DemandResponseShortfallResult{N,L,T,E}( - nsamples, system.regions, system.timestamps, - ep_total_mean, ep_total_std, ep_region_mean, ep_region_std, - ep_period_mean, ep_period_std, - ep_regionperiod_mean, ep_regionperiod_std, - ue_regionperiod_mean, ue_total_std, - ue_region_std, ue_period_std, ue_regionperiod_std) - + return DemandResponseShortfallResult(base_result) end diff --git a/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl b/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl index 434afff4..9c84742e 100644 --- a/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl +++ b/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl @@ -46,127 +46,96 @@ larger sample sizes. See [`DemandResponseShortfall`](@ref) for average shortfall struct DemandResponseShortfallSamples <: ResultSpec end struct DemandResponseShortfallSamplesAccumulator <: ResultAccumulator{DemandResponseShortfallSamples} - - shortfall::Array{Int,3} - + base::ShortfallSamplesAccumulator end function accumulator( sys::SystemModel{N}, nsamples::Int, ::DemandResponseShortfallSamples ) where {N} - - nregions = length(sys.regions) - shortfall = zeros(Int, nregions, N, nsamples) - - return DemandResponseShortfallSamplesAccumulator(shortfall) - + base_acc = accumulator(sys, nsamples, ShortfallSamples()) # call original accumulator + return DemandResponseShortfallSamplesAccumulator(base_acc) end function merge!( x::DemandResponseShortfallSamplesAccumulator, y::DemandResponseShortfallSamplesAccumulator ) - - x.shortfall .+= y.shortfall - return - + merge!(x.base, y.base) end accumulatortype(::DemandResponseShortfallSamples) = DemandResponseShortfallSamplesAccumulator struct DemandResponseShortfallSamplesResult{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractShortfallResult{N,L,T} - - regions::Regions{N,P} - timestamps::StepRange{ZonedDateTime,T} - - shortfall::Array{Int,3} # r x t x s - + base::ShortfallSamplesResult{N,L,T,P,E} end function getindex( x::DemandResponseShortfallSamplesResult{N,L,T,P,E} ) where {N,L,T,P,E} - p2e = conversionfactor(L, T, P, E) - return vec(p2e * sum(x.shortfall, dims=1:2)) + return getindex(x.base) end function getindex( x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString ) where {N,L,T,P,E} - i_r = findfirstunique(x.regions.names, r) - p2e = conversionfactor(L, T, P, E) - return vec(p2e * sum(view(x.shortfall, i_r, :, :), dims=1)) + return getindex(x.base, r) end function getindex( x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime ) where {N,L,T,P,E} - i_t = findfirstunique(x.timestamps, t) - p2e = conversionfactor(L, T, P, E) - return vec(p2e * sum(view(x.shortfall, :, i_t, :), dims=1)) + return getindex(x.base, t) end function getindex( x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime ) where {N,L,T,P,E} - i_r = findfirstunique(x.regions.names, r) - i_t = findfirstunique(x.timestamps, t) - p2e = conversionfactor(L, T, P, E) - return vec(p2e * x.shortfall[i_r, i_t, :]) + return getindex(x.base, r, t) end function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}) where {N,L,T} - eventperiods = sum(sum(x.shortfall, dims=1) .> 0, dims=2) - return LOLE{N,L,T}(MeanEstimate(eventperiods)) + return LOLE(x.base) end function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, r::AbstractString) where {N,L,T} - i_r = findfirstunique(x.regions.names, r) - eventperiods = sum(view(x.shortfall, i_r, :, :) .> 0, dims=1) - return LOLE{N,L,T}(MeanEstimate(eventperiods)) + return LOLE(x.base, r) end function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, t::ZonedDateTime) where {N,L,T} - i_t = findfirstunique(x.timestamps, t) - eventperiods = sum(view(x.shortfall, :, i_t, :), dims=1) .> 0 - return LOLE{1,L,T}(MeanEstimate(eventperiods)) + return LOLE(x.base, t) end function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} - i_r = findfirstunique(x.regions.names, r) - i_t = findfirstunique(x.timestamps, t) - eventperiods = view(x.shortfall, i_r, i_t, :) .> 0 - return LOLE{1,L,T}(MeanEstimate(eventperiods)) + return LOLE(x.base, r, t) end EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} = - EUE{N,L,T,E}(MeanEstimate(x[])) + EUE(x.base) EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} = - EUE{N,L,T,E}(MeanEstimate(x[r])) + EUE(x.base, r) EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime) where {N,L,T,P,E} = - EUE{1,L,T,E}(MeanEstimate(x[t])) + EUE(x.base, t) EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,P,E} = - EUE{1,L,T,E}(MeanEstimate(x[r, t])) + EUE(x.base, r, t) function NEUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} - return NEUE(div(MeanEstimate(x[]),(sum(x.regions.load)/1e6))) + return NEUE(x.base) end function NEUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} - i_r = findfirstunique(x.regions.names, r) - return NEUE(div(MeanEstimate(x[r]),(sum(x.regions.load[i_r,:])/1e6))) + return NEUE(x.base, r) end function finalize( acc::DemandResponseShortfallSamplesAccumulator, system::SystemModel{N,L,T,P,E}, ) where {N,L,T,P,E} + base_result = finalize(acc.base, system) - return DemandResponseShortfallSamplesResult{N,L,T,P,E}( - system.regions, system.timestamps, acc.shortfall) + return DemandResponseShortfallSamplesResult(base_result) end diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index ebc6fa00..78dda237 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -118,49 +118,46 @@ function record!( regionshortfall = dr_shortfall isregionshortfall = regionshortfall > 0 - - fit!(acc.periodsdropped_regionperiod[r,t], isregionshortfall) - fit!(acc.unservedload_regionperiod[r,t], regionshortfall) + fit!(acc.base.periodsdropped_regionperiod[r,t], isregionshortfall) + fit!(acc.base.unservedload_regionperiod[r,t], regionshortfall) if isregionshortfall isshortfall = true totalshortfall += regionshortfall - acc.periodsdropped_region_currentsim[r] += 1 - acc.unservedload_region_currentsim[r] += regionshortfall + acc.base.periodsdropped_region_currentsim[r] += 1 + acc.base.unservedload_region_currentsim[r] += regionshortfall end end if isshortfall - acc.periodsdropped_total_currentsim += 1 - acc.unservedload_total_currentsim += totalshortfall + acc.base.periodsdropped_total_currentsim += 1 + acc.base.unservedload_total_currentsim += totalshortfall end - - fit!(acc.periodsdropped_period[t], isshortfall) - fit!(acc.unservedload_period[t], totalshortfall) - + fit!(acc.base.periodsdropped_period[t], isshortfall) + fit!(acc.base.unservedload_period[t], totalshortfall) return end function reset!(acc::Results.DemandResponseShortfallAccumulator, sampleid::Int) - # Store regional / total sums for current simulation - fit!(acc.periodsdropped_total, acc.periodsdropped_total_currentsim) - fit!(acc.unservedload_total, acc.unservedload_total_currentsim) + fit!(acc.base.periodsdropped_total, acc.base.periodsdropped_total_currentsim) + fit!(acc.base.unservedload_total, acc.base.unservedload_total_currentsim) - for r in eachindex(acc.periodsdropped_region) - fit!(acc.periodsdropped_region[r], acc.periodsdropped_region_currentsim[r]) - fit!(acc.unservedload_region[r], acc.unservedload_region_currentsim[r]) + for r in eachindex(acc.base.periodsdropped_region) + + fit!(acc.base.periodsdropped_region[r], acc.base.periodsdropped_region_currentsim[r]) + fit!(acc.base.unservedload_region[r], acc.base.unservedload_region_currentsim[r]) end # Reset for new simulation - acc.periodsdropped_total_currentsim = 0 - fill!(acc.periodsdropped_region_currentsim, 0) - acc.unservedload_total_currentsim = 0 - fill!(acc.unservedload_region_currentsim, 0) + acc.base.periodsdropped_total_currentsim = 0 + fill!(acc.base.periodsdropped_region_currentsim, 0) + acc.base.unservedload_total_currentsim = 0 + fill!(acc.base.unservedload_region_currentsim, 0) return end @@ -180,7 +177,7 @@ function record!( dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 end - acc.shortfall[r, t, sampleid] = dr_shortfall + acc.base.shortfall[r, t, sampleid] = dr_shortfall end return From 0a30c5959b6ccb12f769a4097b012f1708b8e585 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 25 Aug 2025 14:08:07 -0600 Subject: [PATCH 31/83] change carryover_efficiency to borrowed_energy_interest --- PRASCore.jl/src/Simulations/Simulations.jl | 2 +- PRASCore.jl/src/Simulations/utils.jl | 22 ++++++++++++++++++++++ PRASCore.jl/src/Systems/assets.jl | 20 ++++++++++---------- PRASFiles.jl/src/Systems/read.jl | 2 +- PRASFiles.jl/src/Systems/write.jl | 4 ++-- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index 9a2a5d5e..a4e1e4e6 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -220,7 +220,7 @@ function advance!( update_energy!(state.stors_energy, system.storages, t) update_energy!(state.genstors_energy, system.generatorstorages, t) - update_energy!(state.drs_energy, system.demandresponses, t) + update_dr_energy!(state.drs_energy, system.demandresponses, t) update_paybackcounter!(state.drs_paybackcounter,state.drs_energy, system.demandresponses,t) diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 83f75ae8..06b556ea 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -120,6 +120,28 @@ function update_energy!( end +function update_dr_energy!( + drs_energy::Vector{Int}, + drs::AbstractAssets, + t::Int +) + + for i in 1:length(drs_energy) + + soc = drs_energy[i] + efficiency = drs.borrowed_energy_interest[i,t] + 1.0 + maxenergy = drs.energy_capacity[i,t] + + # Decay SoC + soc = round(Int, soc * efficiency) + + # Shed SoC above current energy limit + drs_energy[i] = min(soc, maxenergy) + + end + +end + function update_paybackcounter!( payback_counter::Vector{Int}, drs_energy::Vector{Int}, diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 6a97e82a..f1442c22 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -498,7 +498,7 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse borrow_efficiency::Matrix{Float64} payback_efficiency::Matrix{Float64} - carryover_efficiency::Matrix{Float64} + borrowed_energy_interest::Matrix{Float64} allowable_payback_period::Matrix{Int} @@ -509,7 +509,7 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, borrowcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, energycapacity::Matrix{Int}, borrowefficiency::Matrix{Float64}, - paybackefficiency::Matrix{Float64}, carryoverefficiency::Matrix{Float64}, + paybackefficiency::Matrix{Float64}, borrowedenergyinterest::Matrix{Float64}, allowablepaybackperiod::Matrix{Int}, λ::Matrix{Float64}, μ::Matrix{Float64} ) where {N,L,T,P,E} @@ -528,10 +528,10 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse @assert size(borrowefficiency) == (n_drs, N) @assert size(paybackefficiency) == (n_drs, N) - @assert size(carryoverefficiency) == (n_drs, N) + @assert size(borrowedenergyinterest) == (n_drs, N) @assert all(isfractional, borrowefficiency) @assert all(isfractional, paybackefficiency) - @assert all(isnonnegative, carryoverefficiency) + @assert all(isfractional, borrowedenergyinterest) @assert size(allowablepaybackperiod) == (n_drs, N) @assert all(isnonnegative, allowablepaybackperiod) @@ -544,7 +544,7 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse new{N,L,T,P,E}(string.(names), string.(categories), borrowcapacity, paybackcapacity, energycapacity, - borrowefficiency, paybackefficiency, carryoverefficiency, + borrowefficiency, paybackefficiency, borrowedenergyinterest, allowablepaybackperiod, λ, μ) @@ -560,7 +560,7 @@ Base.:(==)(x::T, y::T) where {T <: DemandResponses} = x.energy_capacity == y.energy_capacity && x.borrow_efficiency == y.borrow_efficiency && x.payback_efficiency == y.payback_efficiency && - x.carryover_efficiency == y.carryover_efficiency && + x.borrowed_energy_interest == y.borrowed_energy_interest && x.allowable_payback_period == y.allowable_payback_period && x.λ == y.λ && x.μ == y.μ @@ -569,7 +569,7 @@ Base.getindex(dr::DR, idxs::AbstractVector{Int}) where {DR <: DemandResponses} = DR(dr.names[idxs], dr.categories[idxs],dr.borrow_capacity[idxs,:], dr.payback_capacity[idxs, :],dr.energy_capacity[idxs, :], dr.borrow_efficiency[idxs, :], dr.payback_efficiency[idxs, :], - dr.carryover_efficiency[idxs, :],dr.allowable_payback_period[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :]) + dr.borrowed_energy_interest[idxs, :],dr.allowable_payback_period[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :]) function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} @@ -584,7 +584,7 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} borrow_efficiency = Matrix{Float64}(undef, n_drs, N) payback_efficiency = Matrix{Float64}(undef, n_drs, N) - carryover_efficiency = Matrix{Float64}(undef, n_drs, N) + borrowed_energy_interest = Matrix{Float64}(undef, n_drs, N) allowable_payback_period = Matrix{Int}(undef, n_drs, N) @@ -608,7 +608,7 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} borrow_efficiency[rows, :] = dr.borrow_efficiency payback_efficiency[rows, :] = dr.payback_efficiency - carryover_efficiency[rows, :] = dr.carryover_efficiency + borrowed_energy_interest[rows, :] = dr.borrowed_energy_interest allowable_payback_period[rows, :] = dr.allowable_payback_period @@ -620,7 +620,7 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} end return DemandResponses{N,L,T,P,E}(names, categories, borrow_capacity, payback_capacity, energy_capacity, borrow_efficiency, payback_efficiency, - carryover_efficiency,allowable_payback_period, λ, μ) + borrowed_energy_interest,allowable_payback_period, λ, μ) end diff --git a/PRASFiles.jl/src/Systems/read.jl b/PRASFiles.jl/src/Systems/read.jl index b46097fa..ff7823f2 100644 --- a/PRASFiles.jl/src/Systems/read.jl +++ b/PRASFiles.jl/src/Systems/read.jl @@ -325,7 +325,7 @@ function systemmodel_0_8(f::File) load_matrix(f["demandresponses/energycapacity"], region_order, Int), load_matrix(f["demandresponses/borrowefficiency"], region_order, Float64), load_matrix(f["demandresponses/paybackefficiency"], region_order, Float64), - load_matrix(f["demandresponses/carryoverefficiency"], region_order, Float64), + load_matrix(f["demandresponses/borrowedenergyinterest"], region_order, Float64), load_matrix(f["demandresponses/allowablepaybackperiod"], region_order, Int), load_matrix(f["demandresponses/failureprobability"], region_order, Float64), load_matrix(f["demandresponses/repairprobability"], region_order, Float64) diff --git a/PRASFiles.jl/src/Systems/write.jl b/PRASFiles.jl/src/Systems/write.jl index 08ec2e50..1b3cb8c0 100644 --- a/PRASFiles.jl/src/Systems/write.jl +++ b/PRASFiles.jl/src/Systems/write.jl @@ -251,8 +251,8 @@ function process_demandresponses!( demandresponses["paybackefficiency", deflate = compression] = sys.demandresponses.payback_efficiency - demandresponses["carryoverefficiency", deflate = compression] = - sys.demandresponses.carryover_efficiency + demandresponses["borrowedenergyinterest", deflate = compression] = + sys.demandresponses.borrowed_energy_interest demandresponses["allowablepaybackperiod", deflate = compression] = sys.demandresponses.allowable_payback_period From ebd658487d02d428c58036bbcce29474304b109c Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 26 Aug 2025 17:18:33 -0600 Subject: [PATCH 32/83] increase seperation-work in progress as multiplying leads to some strange changes --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 9105bd12..e4a7d66d 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -142,7 +142,7 @@ struct DispatchProblem #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) - min_paybackcost_dr = - maxpaybacktime_dr - 2 + min_chargecost #add min_chargecost to always have DR devices be paybacked first + min_paybackcost_dr = (- maxpaybacktime_dr - 50 + min_chargecost) #add min_chargecost to always have DR devices be paybacked first, and -15 for wheeling prevention max_borrowcost_dr = - min_paybackcost_dr + minpaybacktime_dr + 1 + max_dischargecost #add max_dischargecost to always have DR devices be borrowed last #for unserved energy @@ -460,7 +460,7 @@ function update_problem!( fp.nodes[payback_node], slack_node, -payback_capacity) # smallest allowable payback window = highest priority (payback first) - paybackcost = problem.min_paybackcost_dr + allowablepayback # Negative cost + paybackcost = problem.min_paybackcost_dr + allowablepayback # Negative cost-mutliply by 10 for wheeling preventation updateflowcost!(fp.edges[payback_edge], paybackcost) # Update borrowing-make sure no borrowing is allowed if allowable payback period is equal to zero From 42d205a17485498a3ff9d2f1c353475385a31198 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 26 Aug 2025 17:22:54 -0600 Subject: [PATCH 33/83] add multi region with DR --- PRASCore.jl/src/Systems/TestData.jl | 119 ++++++++++++++++++----- PRASCore.jl/test/Simulations/runtests.jl | 69 +++++++++++++ 2 files changed, 163 insertions(+), 25 deletions(-) diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index 6af0a34f..4bd5b0f5 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -294,7 +294,7 @@ emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( dr = DemandResponses{6,1,Hour,MW,MWh}( ["DR1"], ["DemandResponse Category"], fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), - fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6), + fill(1., 1, 6), fill(1., 1, 6), fill(0.0, 1, 6), fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) @@ -303,20 +303,17 @@ full_day_load_profile = [56,58,60,61,59,53] test4 = SystemModel(gen, emptystors, emptygenstors, dr, timestamps, full_day_load_profile) -test4_lole = 1.99 -test4_lolps = [0.0998, 0.2629, 0.33, 0.8603, 0.2638, 0.1733] +test4_lole = 2.118 +test4_lolps = [0.09979000000000159, 0.26288399999999845, 0.3300120000000098, 0.8603499999999678, 0.26384700000001193, 0.30136599999999447] -test4_eue = 40.75 -test4_eues = [4.69, 5.15, 6.94, 12.99, 6.03, 4.94] -test4_esurplus = [0.9,0,0,0,0,1.98] +test4_eue = 42.14 +test4_eues = [4.689609999999829, 5.1521070000000115, 6.940174999999926, 12.988998999999957, 6.031562999999943, 6.333204999999944] -test4_eenergy = [0.8986299999999925, - 2.4561559999999782, - 4.254399000000163, - 0.997662000000021, - 2.6729959999999235, - 1.388832000000018] + +test4_esurplus = [0.9,0,0,0,0,1.9818] + +test4_eenergy = [0.89863, 2.45616, 4.2544, 0.997662, 2.673, 0.0] # Test System 5 (Gen + DR + Stor, 1 Region for 6 hours) @@ -338,9 +335,9 @@ emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( (zeros(Int, 0, 6) for _ in 1:3)..., (zeros(Float64, 0, 6) for _ in 1:2)...) dr = DemandResponses{6,1,Hour,MW,MWh}( - ["DR1"], ["DemandResponses"], + ["DR1"], ["DemandResponse Category"], fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), - fill(1., 1, 6), fill(1., 1, 6), fill(1., 1, 6), + fill(1., 1, 6), fill(1., 1, 6), fill(0.0, 1, 6), fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) @@ -349,19 +346,91 @@ full_day_load_profile = [56,58,60,61,59,53] test5 = SystemModel(gen, stor, emptygenstors, dr, timestamps, full_day_load_profile) -test5_lole = 1.969 -test5_lolps = [0.0998, 0.1974, 0.3301, 0.4261, 0.6986, 0.2173] +test5_lole = 2.007 +test5_lolps = [0.09979000000000159, 0.19739399999999457, 0.33007500000000567, 0.4260809999999895, 0.698582999999988, 0.25501299999998867] + + + +test5_eue = 42.11 +test5_eues = [4.6888800000001805, 5.013817000000116, 6.877069999999737, 8.325742999999783, 11.226304000000445, 5.983083000000084] + + +test5_esurplus = [0.0901899999999984,0,0,0,0,0.27479499999999374] + +test5_eenergy = [0.89936, 1.86623, 3.66255, 5.05573, 1.54738, 0.0] + + +# Multiregion with DR + +regions = Regions{6,MW}(["Region 1", "Region 2", "Region 3"], + [19 20 25 26 24 25; 20 21 30 27 23 24; 22 26 27 25 23 24]) + +generators = Generators{6,1,Hour,MW}( + ["Gen1", "VG A", "Gen 2", "Gen 3", "VG B", "Gen 4", "Gen 5", "VG C"], + ["Gens", "VG", "Gens", "Gens", "VG", "Gens", "Gens", "VG"], + [10 10 10 10 10 10; 4 3 2 3 4 3; # A + 10 10 10 10 10 10; 10 10 10 10 10 10; 6 5 3 4 3 2; # B + 10 10 15 10 10 10; 20 20 25 20 22 24; 2 1 2 1 2 2], # C + fill(0.1, 8, 6), + fill(0.9, 8, 6) +) + +drs = DemandResponses{6,1,Hour,MW,MWh}( + ["DR1", "DR2", "DR3"], + ["DR_TYPE1", "DR_TYPE1", "DR_TYPE1"], + [fill(5, 1, 6); # A borrow capacity + fill(4, 1, 6); # B borrow capacity + fill(3, 1, 6);], # C borrow capacity + [fill(5, 1, 6); # A payback capacity + fill(4, 1, 6); # B payback capacity + fill(3, 1, 6);], # C payback capacity + [fill(10, 1, 6); # A energy capacity + fill(8, 1, 6); # B energy capacity + fill(6, 1, 6);], # C energy capacity + fill(1.0, 3, 6), # All regions 100% borrow efficiency + fill(1.0, 3, 6), # All regions 100% payback efficiency + fill(0.0, 3, 6), # All regions 0% borrowed energy interest + fill(4, 3, 6), # All regions 3 allowable payback time periods + [fill(0.1, 1, 6); # A + fill(0.1, 1, 6); # B + fill(0.1, 1, 6)], # C + [fill(0.9, 1, 6); # A + fill(0.9, 1, 6); # B + fill(0.9, 1, 6)]) # C) + +interfaces = Interfaces{6,MW}( + [1,1,2], [2,3,3], fill(100, 3, 6), fill(100, 3, 6)) + +lines = Lines{6,1,Hour,MW}( + ["L1", "L2", "L3"], ["Lines", "Lines", "Lines"], + fill(100, 3, 6), fill(100, 3, 6), fill(0.1, 3, 6), fill(0.9, 3, 6)) + +threenode_dr = + SystemModel( + regions, interfaces, generators, [1:2, 3:5, 6:8], + emptystors, fill(1:0, 3), emptygenstors, fill(1:0, 3), + drs, [1:1, 2:2, 3:3], + lines, [1:1, 2:2, 3:3], + ZonedDateTime(2018,10,30,0,tz):Hour(1):ZonedDateTime(2018,10,30,5,tz)) + +threenode_dr_lole = 3.204734 +threenode_dr_lole_r = [2.749467; 2.0656159; 1.5787239] +threenode_dr_lole_t = [0.0817; 0.2169519; 0.47371299; 0.843717; 0.588652; 1.0] +threenode_dr_lole_rt = [0.013778; 0.081317; 0.3802359; 0.3650310; 0.274108; 0.951144] + +threenode_dr_eue = 53.0449649 +threenode_dr_eue_r = [26.54526199; 15.15617299; 11.34353] +threenode_dr_eue_t = [0.566655; 1.82072; 5.47627; 9.7399670; 8.7754709; 26.66588199] +threenode_dr_eue_rt = [0.16025; 0.510989; 2.446321; 6.235264; 5.0443439; 12.1480939] -test5_eue = 41.18 -test5_eues = [4.69, 5.01, 6.88, 8.33, 11.23, 5.05] +threenode_dr_esurplus_t = [6.066344; 0.805918; 0.148378; 0.03628699; 0.0952889; 0.10477099] +threenode_dr_esurplus_rt = [2.574232; 0.545672; 0.0; 0.0; 0.0; 0.0] -test5_esurplus = [0.0901899999999984,0,0,0,0,0.27480199999998633] +threenode_dr_flow = -1.0383633 +threenode_dr_flow_t = [-1.576781; -2.386248; -0.256078; -0.6453639; -0.728238; -0.6374709] +threenode_dr_util = 0.23285 +threenode_dr_util_t = [0.116007969;0.12482749;0.1083558;0.106341959;0.108113959; 0.107764689] -test5_eenergy = [0.8993600000000469, - 1.8662289999999468, - 3.6625479999998864, - 5.055734000000438, - 1.5473829999999773, - 0.9368359999999988] +threenode_dr_eenergy = 57.750022 end \ No newline at end of file diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index 02f26a85..905f1b7b 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -508,4 +508,73 @@ end + @testset "Test System 6: Gen + DR, 3 Regions" begin + + simspec = SequentialMonteCarlo(samples=1_000_000, seed=112) + regions = TestData.threenode_dr.regions.names + dr = first(TestData.threenode_dr.demandresponses.names) + dts = TestData.threenode_dr.timestamps + + shortfall, surplus, energy, flow, utilization = + assess(TestData.threenode_dr, simspec, + Shortfall(), Surplus(), DemandResponseEnergy(), Flow(), Utilization()) + + + # Shortfall - LOLE + @info "LOLE(shortfall): $(LOLE(shortfall))" + @info "TestData.threenode_dr_lole: $(TestData.threenode_dr_lole)" + @test withinrange(LOLE(shortfall), + TestData.threenode_dr_lole, nstderr_tol) + @test all(withinrange.(LOLE.(shortfall, regions), + TestData.threenode_dr_lole_r, nstderr_tol)) + @test all(withinrange.(LOLE.(shortfall, dts), + TestData.threenode_dr_lole_t, nstderr_tol)) + @test all(withinrange.(LOLE.(shortfall, "Region 2", dts), + TestData.threenode_dr_lole_rt, nstderr_tol)) + + # Shortfall - EUE + @info "eue(shortfall): $(EUE(shortfall))" + @info "TestData.threenode_dr_eue: $(TestData.threenode_dr_eue)" + @test withinrange(EUE(shortfall), + TestData.threenode_dr_eue, nstderr_tol) + @test all(withinrange.(EUE.(shortfall, regions), + TestData.threenode_dr_eue_r, nstderr_tol)) + @test all(withinrange.(EUE.(shortfall, dts), + TestData.threenode_dr_eue_t, nstderr_tol)) + @test all(withinrange.(EUE.(shortfall, "Region 1", dts), + TestData.threenode_dr_eue_rt, nstderr_tol)) + + # Surplus + @test all(withinrange.(getindex.(surplus, dts), + TestData.threenode_dr_esurplus_t, + simspec.nsamples, nstderr_tol)) + @test all(withinrange.(getindex.(surplus, "Region 2", dts), + TestData.threenode_dr_esurplus_rt, + simspec.nsamples, nstderr_tol)) + + # Flow + @test withinrange(flow["Region 1" => "Region 2"], + TestData.threenode_dr_flow, + simspec.nsamples, nstderr_tol) + @test all(withinrange.(getindex.(flow, "Region 1"=>"Region 2", dts), + TestData.threenode_dr_flow_t, + simspec.nsamples, nstderr_tol)) + + # Utilization + @test withinrange((sum(x[1] for x in getindex.(utilization, "Region 1"=>"Region 2")), sum(x[end] for x in getindex.(utilization, "Region 1"=>"Region 2"))), + TestData.threenode_dr_util, + simspec.nsamples, nstderr_tol) + + @test all(withinrange.(getindex.(utilization, "Region 1"=>"Region 2",dts), + TestData.threenode_dr_util_t, + simspec.nsamples, nstderr_tol)) + # Energy + @test withinrange((sum(x[1] for x in getindex.(energy, dts)), sum(x[end] for x in getindex.(energy, dts))), # fails? + TestData.threenode_dr_eenergy, + simspec.nsamples, nstderr_tol) + + + end + + end From f666a5b99b17daae3dc64ec61b94c8db113f2094 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Fri, 5 Sep 2025 12:56:31 -0600 Subject: [PATCH 34/83] Add type params to Results/Shortfall.jl to double as both Shortfall and DRShortfall --- .../src/Results/DemandResponseShortfall.jl | 163 ------------------ PRASCore.jl/src/Results/Results.jl | 1 - PRASCore.jl/src/Results/Shortfall.jl | 55 +++--- PRASCore.jl/src/Simulations/recording.jl | 117 ++++++------- PRASCore.jl/test/Results/shortfall.jl | 2 +- 5 files changed, 78 insertions(+), 260 deletions(-) delete mode 100644 PRASCore.jl/src/Results/DemandResponseShortfall.jl diff --git a/PRASCore.jl/src/Results/DemandResponseShortfall.jl b/PRASCore.jl/src/Results/DemandResponseShortfall.jl deleted file mode 100644 index c055a2cc..00000000 --- a/PRASCore.jl/src/Results/DemandResponseShortfall.jl +++ /dev/null @@ -1,163 +0,0 @@ -""" - DemandResponseShortfall - -The `DemandResponseShortfall` result specification reports expectation-based resource -adequacy risk metrics such as EUE and LOLE associated with DemandResponse devices only, producing a `DemandResponseShortfallResult`. - -A `DemandResponseShortfallResult` can be directly indexed by a region name and a timestamp to retrieve a tuple of sample mean and standard deviation, estimating - the average unserved energy in that region and timestep. However, in most -cases it's simpler to use [`EUE`](@ref) and [`LOLE`](@ref) constructors to -directly retrieve standard risk metrics. - -Example: - -```julia -DemandResponseShortfall, = - assess(sys, SequentialMonteCarlo(samples=1000), DemandResponseShortfall()) - -period = ZonedDateTime(2020, 1, 1, 0, tz"UTC") - -# Unserved energy mean and standard deviation -sf_mean, sf_std = DemandResponseShortfall["Region A", period] - -# System-wide risk metrics -eue = EUE(DemandResponseShortfall) -lole = LOLE(DemandResponseShortfall) -neue = NEUE(DemandResponseShortfall) - -# Regional risk metrics -regional_eue = EUE(DemandResponseShortfall, "Region A") -regional_lole = LOLE(DemandResponseShortfall, "Region A") -regional_neue = NEUE(DemandResponseShortfall, "Region A") - -# Period-specific risk metrics -period_eue = EUE(DemandResponseShortfall, period) -period_lolp = LOLE(DemandResponseShortfall, period) - -# Region- and period-specific risk metrics -period_eue = EUE(DemandResponseShortfall, "Region A", period) -period_lolp = LOLE(DemandResponseShortfall, "Region A", period) -``` - -See [`DemandResponseShortfallSamples`](@ref) for recording sample-level DemandResponseShortfall results. -""" -struct DemandResponseShortfall <: ResultSpec end - -mutable struct DemandResponseShortfallAccumulator <: ResultAccumulator{DemandResponseShortfall} - base::ShortfallAccumulator -end - -function accumulator( - sys::SystemModel{N}, nsamples::Int, ::DemandResponseShortfall -) where {N} - base_acc = accumulator(sys, nsamples, Shortfall()) # call original accumulator - return DemandResponseShortfallAccumulator(base_acc) -end - - - -function merge!(x::DemandResponseShortfallAccumulator, y::DemandResponseShortfallAccumulator) - merge!(x.base, y.base) -end - -accumulatortype(::DemandResponseShortfall) = DemandResponseShortfallAccumulator - - -struct DemandResponseShortfallResult{N,L,T,E} <: AbstractShortfallResult{N, L, T} - base::ShortfallResult{N,L,T,E} -end - -function DemandResponseShortfallResult{N,L,T,E}( - nsamples::Union{Int,Nothing}, - regions::Regions, - timestamps::StepRange{ZonedDateTime,T}, - eventperiod_mean::Float64, - eventperiod_std::Float64, - eventperiod_region_mean::Vector{Float64}, - eventperiod_region_std::Vector{Float64}, - eventperiod_period_mean::Vector{Float64}, - eventperiod_period_std::Vector{Float64}, - eventperiod_regionperiod_mean::Matrix{Float64}, - eventperiod_regionperiod_std::Matrix{Float64}, - shortfall_mean::Matrix{Float64}, - shortfall_std::Float64, - shortfall_region_std::Vector{Float64}, - shortfall_period_std::Vector{Float64}, - shortfall_regionperiod_std::Matrix{Float64} -) where {N,L,T<:Period,E<:EnergyUnit} - DemandResponseShortfallResult( - ShortfallResult{N,L,T,E}( - nsamples, regions, timestamps, - eventperiod_mean, eventperiod_std, - eventperiod_region_mean, eventperiod_region_std, - eventperiod_period_mean, eventperiod_period_std, - eventperiod_regionperiod_mean, eventperiod_regionperiod_std, - shortfall_mean, shortfall_std, - shortfall_region_std, shortfall_period_std, - shortfall_regionperiod_std) - ) - -end - - -function getindex(x::DemandResponseShortfallResult) - return getindex(x.base) -end - -function getindex(x::DemandResponseShortfallResult, r::AbstractString) - return getindex(x.base, r) -end - -function getindex(x::DemandResponseShortfallResult, t::ZonedDateTime) - return getindex(x.base, t) -end - -function getindex(x::DemandResponseShortfallResult, r::AbstractString, t::ZonedDateTime) - return getindex(x.base, r, t) -end - - -LOLE(x::DemandResponseShortfallResult{N,L,T}) where {N,L,T} = - LOLE(x.base) - -function LOLE(x::DemandResponseShortfallResult{N,L,T}, r::AbstractString) where {N,L,T} - return LOLE(x.base, r) -end - -function LOLE(x::DemandResponseShortfallResult{N,L,T}, t::ZonedDateTime) where {N,L,T} - return LOLE(x.base, t) -end - -function LOLE(x::DemandResponseShortfallResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} - return LOLE(x.base, r, t) -end - -EUE(x::DemandResponseShortfallResult{N,L,T,E}) where {N,L,T,E} = - EUE(x.base) - -EUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} = - EUE(x.base, r) - -EUE(x::DemandResponseShortfallResult{N,L,T,E}, t::ZonedDateTime) where {N,L,T,E} = - EUE(x.base, t) - -EUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E} = - EUE(x.base, r, t) - -function NEUE(x::DemandResponseShortfallResult{N,L,T,E}) where {N,L,T,E} - return NEUE(x.base) -end - -function NEUE(x::DemandResponseShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} - return NEUE(x.base, r) -end - - -function finalize( - acc::DemandResponseShortfallAccumulator, - system::SystemModel{N,L,T,P,E}, -) where {N,L,T,P,E} - base_result = finalize(acc.base, system) - - return DemandResponseShortfallResult(base_result) -end diff --git a/PRASCore.jl/src/Results/Results.jl b/PRASCore.jl/src/Results/Results.jl index e4d029d0..1de375e5 100644 --- a/PRASCore.jl/src/Results/Results.jl +++ b/PRASCore.jl/src/Results/Results.jl @@ -82,7 +82,6 @@ NEUE(x::AbstractShortfallResult, ::Colon, ::Colon) = include("Shortfall.jl") include("ShortfallSamples.jl") -include("DemandResponseShortfall.jl") include("DemandResponseShortfallSamples.jl") diff --git a/PRASCore.jl/src/Results/Shortfall.jl b/PRASCore.jl/src/Results/Shortfall.jl index e76c00c1..89b26e46 100644 --- a/PRASCore.jl/src/Results/Shortfall.jl +++ b/PRASCore.jl/src/Results/Shortfall.jl @@ -43,7 +43,9 @@ See [`ShortfallSamples`](@ref) for recording sample-level shortfall results. """ struct Shortfall <: ResultSpec end -mutable struct ShortfallAccumulator <: ResultAccumulator{Shortfall} +struct DemandResponseShortfall <: ResultSpec end + +mutable struct ShortfallAccumulator{S} <: ResultAccumulator{Shortfall} # Cross-simulation LOL period count mean/variances periodsdropped_total::MeanVariance @@ -68,8 +70,8 @@ mutable struct ShortfallAccumulator <: ResultAccumulator{Shortfall} end function accumulator( - sys::SystemModel{N}, nsamples::Int, ::Shortfall -) where {N} + sys::SystemModel{N}, nsamples::Int, ::S +) where {N,S<:Union{Shortfall,DemandResponseShortfall}} nregions = length(sys.regions) @@ -89,7 +91,7 @@ function accumulator( unservedload_total_currentsim = 0 unservedload_region_currentsim = zeros(Int, nregions) - return ShortfallAccumulator( + return ShortfallAccumulator{S}( periodsdropped_total, periodsdropped_region, periodsdropped_period, periodsdropped_regionperiod, periodsdropped_total_currentsim, periodsdropped_region_currentsim, @@ -117,9 +119,10 @@ function merge!( end -accumulatortype(::Shortfall) = ShortfallAccumulator +accumulatortype(::Shortfall) = ShortfallAccumulator{Shortfall} +accumulatortype(::DemandResponseShortfall) = ShortfallAccumulator{DemandResponseShortfall} -struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: +struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit, S} <: AbstractShortfallResult{N, L, T} nsamples::Union{Int, Nothing} regions::Regions @@ -145,7 +148,7 @@ struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: shortfall_period_std::Vector{Float64} shortfall_regionperiod_std::Matrix{Float64} - function ShortfallResult{N,L,T,E}( + function ShortfallResult{N,L,T,E,S}( nsamples::Union{Int,Nothing}, regions::Regions, timestamps::StepRange{ZonedDateTime,T}, @@ -162,7 +165,7 @@ struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: shortfall_region_std::Vector{Float64}, shortfall_period_std::Vector{Float64}, shortfall_regionperiod_std::Matrix{Float64} - ) where {N,L,T<:Period,E<:EnergyUnit} + ) where {N,L,T<:Period,E<:EnergyUnit,S <: Union{Shortfall,DemandResponseShortfall}} isnothing(nsamples) || nsamples > 0 || throw(DomainError("Sample count must be positive or `nothing`.")) @@ -184,7 +187,7 @@ struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: size(shortfall_regionperiod_std) == (nregions, N) || error("Inconsistent input data sizes") - new{N,L,T,E}(nsamples, regions, timestamps, + new{N,L,T,E,S}(nsamples, regions, timestamps, eventperiod_mean, eventperiod_std, eventperiod_region_mean, eventperiod_region_std, eventperiod_period_mean, eventperiod_period_std, @@ -197,47 +200,47 @@ struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: end -function getindex(x::ShortfallResult) +function getindex(x::ShortfallResult{S}) where {S} return sum(x.shortfall_mean), x.shortfall_std end -function getindex(x::ShortfallResult, r::AbstractString) +function getindex(x::ShortfallResult{S}, r::AbstractString) where {S} i_r = findfirstunique(x.regions.names, r) return sum(view(x.shortfall_mean, i_r, :)), x.shortfall_region_std[i_r] end -function getindex(x::ShortfallResult, t::ZonedDateTime) +function getindex(x::ShortfallResult{S}, t::ZonedDateTime) where {S} i_t = findfirstunique(x.timestamps, t) return sum(view(x.shortfall_mean, :, i_t)), x.shortfall_period_std[i_t] end -function getindex(x::ShortfallResult, r::AbstractString, t::ZonedDateTime) +function getindex(x::ShortfallResult{S}, r::AbstractString, t::ZonedDateTime) where {S} i_r = findfirstunique(x.regions.names, r) i_t = findfirstunique(x.timestamps, t) return x.shortfall_mean[i_r, i_t], x.shortfall_regionperiod_std[i_r, i_t] end -LOLE(x::ShortfallResult{N,L,T}) where {N,L,T} = +LOLE(x::ShortfallResult{N,L,T,S}) where {N,L,T,S} = LOLE{N,L,T}(MeanEstimate(x.eventperiod_mean, x.eventperiod_std, x.nsamples)) -function LOLE(x::ShortfallResult{N,L,T}, r::AbstractString) where {N,L,T} +function LOLE(x::ShortfallResult{N,L,T,S}, r::AbstractString) where {N,L,T,S} i_r = findfirstunique(x.regions.names, r) return LOLE{N,L,T}(MeanEstimate(x.eventperiod_region_mean[i_r], x.eventperiod_region_std[i_r], x.nsamples)) end -function LOLE(x::ShortfallResult{N,L,T}, t::ZonedDateTime) where {N,L,T} +function LOLE(x::ShortfallResult{N,L,T,S}, t::ZonedDateTime) where {N,L,T,S} i_t = findfirstunique(x.timestamps, t) return LOLE{1,L,T}(MeanEstimate(x.eventperiod_period_mean[i_t], x.eventperiod_period_std[i_t], x.nsamples)) end -function LOLE(x::ShortfallResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} +function LOLE(x::ShortfallResult{N,L,T,S}, r::AbstractString, t::ZonedDateTime) where {N,L,T,S} i_r = findfirstunique(x.regions.names, r) i_t = findfirstunique(x.timestamps, t) return LOLE{1,L,T}(MeanEstimate(x.eventperiod_regionperiod_mean[i_r, i_t], @@ -246,31 +249,31 @@ function LOLE(x::ShortfallResult{N,L,T}, r::AbstractString, t::ZonedDateTime) wh end -EUE(x::ShortfallResult{N,L,T,E}) where {N,L,T,E} = +EUE(x::ShortfallResult{N,L,T,E,S}) where {N,L,T,E,S} = EUE{N,L,T,E}(MeanEstimate(x[]..., x.nsamples)) -EUE(x::ShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} = +EUE(x::ShortfallResult{N,L,T,E,S}, r::AbstractString) where {N,L,T,E,S} = EUE{N,L,T,E}(MeanEstimate(x[r]..., x.nsamples)) -EUE(x::ShortfallResult{N,L,T,E}, t::ZonedDateTime) where {N,L,T,E} = +EUE(x::ShortfallResult{N,L,T,E,S}, t::ZonedDateTime) where {N,L,T,E,S} = EUE{1,L,T,E}(MeanEstimate(x[t]..., x.nsamples)) -EUE(x::ShortfallResult{N,L,T,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E} = +EUE(x::ShortfallResult{N,L,T,E,S}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E,S} = EUE{1,L,T,E}(MeanEstimate(x[r, t]..., x.nsamples)) -function NEUE(x::ShortfallResult{N,L,T,E}) where {N,L,T,E} +function NEUE(x::ShortfallResult{N,L,T,E,S}) where {N,L,T,E,S} return NEUE(div(MeanEstimate(x[]..., x.nsamples),(sum(x.regions.load)/1e6))) end -function NEUE(x::ShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} +function NEUE(x::ShortfallResult{N,L,T,E,S}, r::AbstractString) where {N,L,T,E,S} i_r = findfirstunique(x.regions.names, r) return NEUE(div(MeanEstimate(x[r]..., x.nsamples),(sum(x.regions.load[i_r,:])/1e6))) end function finalize( - acc::ShortfallAccumulator, + acc::ShortfallAccumulator{S}, system::SystemModel{N,L,T,P,E}, -) where {N,L,T,P,E} +) where {N,L,T,P,E,S<:Union{Shortfall,DemandResponseShortfall}} ep_total_mean, ep_total_std = mean_std(acc.periodsdropped_total) ep_region_mean, ep_region_std = mean_std(acc.periodsdropped_region) @@ -293,7 +296,7 @@ function finalize( ue_period_std .*= p2e ue_regionperiod_std .*= p2e - return ShortfallResult{N,L,T,E}( + return ShortfallResult{N,L,T,E,S}( nsamples, system.regions, system.timestamps, ep_total_mean, ep_total_std, ep_region_mean, ep_region_std, ep_period_mean, ep_period_std, diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 78dda237..4e0126d6 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -1,11 +1,11 @@ # Shortfall function record!( - acc::Results.ShortfallAccumulator, + acc::Results.ShortfallAccumulator{S}, system::SystemModel{N,L,T,P,E}, state::SystemState, problem::DispatchProblem, sampleid::Int, t::Int -) where {N,L,T,P,E} +) where {N,L,T,P,E,S<:Results.Shortfall} totalshortfall = 0 isshortfall = false @@ -50,7 +50,52 @@ function record!( end -function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) +# DemandResponseShortfall +function record!( + acc::Results.ShortfallAccumulator{S}, + system::SystemModel{N,L,T,P,E}, + state::SystemState, problem::DispatchProblem, + sampleid::Int, t::Int +) where {N,L,T,P,E,S<:Results.DemandResponseShortfall} + + totalshortfall = 0 + isshortfall = false + + for (r, dr_idxs) in zip(problem.region_unserved_edges, system.region_dr_idxs) + + #count region shortfall and include dr shortfall + dr_shortfall = 0 + for i in dr_idxs + dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 + end + regionshortfall = dr_shortfall + isregionshortfall = regionshortfall > 0 + + fit!(acc.periodsdropped_regionperiod[r,t], isregionshortfall) + fit!(acc.unservedload_regionperiod[r,t], regionshortfall) + + if isregionshortfall + + isshortfall = true + totalshortfall += regionshortfall + + acc.periodsdropped_region_currentsim[r] += 1 + acc.unservedload_region_currentsim[r] += regionshortfall + + end + end + + if isshortfall + acc.periodsdropped_total_currentsim += 1 + acc.unservedload_total_currentsim += totalshortfall + end + fit!(acc.periodsdropped_period[t], isshortfall) + fit!(acc.unservedload_period[t], totalshortfall) + return + +end + +function reset!(acc::Results.ShortfallAccumulator{S}, sampleid::Int) where {S} # Store regional / total sums for current simulation fit!(acc.periodsdropped_total, acc.periodsdropped_total_currentsim) @@ -96,72 +141,6 @@ end reset!(acc::Results.ShortfallSamplesAccumulator, sampleid::Int) = nothing -# DemandResponseShortfall - -function record!( - acc::Results.DemandResponseShortfallAccumulator, - system::SystemModel{N,L,T,P,E}, - state::SystemState, problem::DispatchProblem, - sampleid::Int, t::Int -) where {N,L,T,P,E} - - totalshortfall = 0 - isshortfall = false - - for (r, dr_idxs) in zip(problem.region_unserved_edges, system.region_dr_idxs) - - #count region shortfall and include dr shortfall - dr_shortfall = 0 - for i in dr_idxs - dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 - end - regionshortfall = dr_shortfall - isregionshortfall = regionshortfall > 0 - - fit!(acc.base.periodsdropped_regionperiod[r,t], isregionshortfall) - fit!(acc.base.unservedload_regionperiod[r,t], regionshortfall) - - if isregionshortfall - - isshortfall = true - totalshortfall += regionshortfall - - acc.base.periodsdropped_region_currentsim[r] += 1 - acc.base.unservedload_region_currentsim[r] += regionshortfall - - end - end - - if isshortfall - acc.base.periodsdropped_total_currentsim += 1 - acc.base.unservedload_total_currentsim += totalshortfall - end - fit!(acc.base.periodsdropped_period[t], isshortfall) - fit!(acc.base.unservedload_period[t], totalshortfall) - return - -end - -function reset!(acc::Results.DemandResponseShortfallAccumulator, sampleid::Int) - # Store regional / total sums for current simulation - fit!(acc.base.periodsdropped_total, acc.base.periodsdropped_total_currentsim) - fit!(acc.base.unservedload_total, acc.base.unservedload_total_currentsim) - - for r in eachindex(acc.base.periodsdropped_region) - - fit!(acc.base.periodsdropped_region[r], acc.base.periodsdropped_region_currentsim[r]) - fit!(acc.base.unservedload_region[r], acc.base.unservedload_region_currentsim[r]) - end - - # Reset for new simulation - acc.base.periodsdropped_total_currentsim = 0 - fill!(acc.base.periodsdropped_region_currentsim, 0) - acc.base.unservedload_total_currentsim = 0 - fill!(acc.base.unservedload_region_currentsim, 0) - return - -end - # DemandResponseShortfallSamples function record!( acc::Results.DemandResponseShortfallSamplesAccumulator, diff --git a/PRASCore.jl/test/Results/shortfall.jl b/PRASCore.jl/test/Results/shortfall.jl index 9c6d73d1..421bfc7d 100644 --- a/PRASCore.jl/test/Results/shortfall.jl +++ b/PRASCore.jl/test/Results/shortfall.jl @@ -5,7 +5,7 @@ r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod - result = PRASCore.Results.ShortfallResult{N,1,Hour,MWh}( + result = PRASCore.Results.ShortfallResult{N,1,Hour,MWh,Shortfall}( DD.nsamples, Regions{N,MW}(DD.resourcenames, DD.resource_vals), DD.periods, DD.d1, DD.d2, DD.d1_resource, DD.d2_resource, DD.d1_period, DD.d2_period, DD.d1_resourceperiod, DD.d2_resourceperiod, From 13d6cd17f101a129b6311cb5ed23145a03de2d2d Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Fri, 5 Sep 2025 18:53:28 -0600 Subject: [PATCH 35/83] Add type params to Results/ShortfallSamples.jl to double as both ShortfallSamples and DRShortfallSamples; Remove some unnecessary type params --- .../Results/DemandResponseShortfallSamples.jl | 141 ------------------ PRASCore.jl/src/Results/Results.jl | 1 - PRASCore.jl/src/Results/Shortfall.jl | 33 ++-- PRASCore.jl/src/Results/ShortfallSamples.jl | 21 +-- PRASCore.jl/src/Simulations/recording.jl | 17 +-- PRASCore.jl/test/Results/shortfall.jl | 2 +- 6 files changed, 37 insertions(+), 178 deletions(-) delete mode 100644 PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl diff --git a/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl b/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl deleted file mode 100644 index 9c84742e..00000000 --- a/PRASCore.jl/src/Results/DemandResponseShortfallSamples.jl +++ /dev/null @@ -1,141 +0,0 @@ -""" - DemandResponseShortfallSamples - -The `DemandResponseShortfallSamples` result specification reports sample-level unserved energy outcomes, producing a `DemandResponseShortfallSamplesResult`. - -A `DemandResponseShortfallSamplesResult` can be directly indexed by a region name and a -timestamp to retrieve a vector of sample-level unserved energy results in that -region and timestep. [`EUE`](@ref) and [`LOLE`](@ref) constructors can also -be used to retrieve standard risk metrics. - -Example: - -```julia -drshortfall, = - assess(sys, SequentialMonteCarlo(samples=10), DemandResponseShortfallSamples()) - -period = ZonedDateTime(2020, 1, 1, 0, tz"UTC") - -samples = drshortfall["Region A", period] - -@assert samples isa Vector{Float64} -@assert length(samples) == 10 - -# System-wide risk metrics -eue = EUE(drshortfall) -lole = LOLE(drshortfall) -neue = NEUE(drshortfall) - -# Regional risk metrics -regional_eue = EUE(drshortfall, "Region A") -regional_lole = LOLE(drshortfall, "Region A") -regional_neue = NEUE(drshortfall, "Region A") - -# Period-specific risk metrics -period_eue = EUE(drshortfall, period) -period_lolp = LOLE(drshortfall, period) - -# Region- and period-specific risk metrics -period_eue = EUE(drshortfall, "Region A", period) -period_lolp = LOLE(drshortfall, "Region A", period) -``` - -Note that this result specification requires large amounts of memory for -larger sample sizes. See [`DemandResponseShortfall`](@ref) for average shortfall outcomes when sample-level granularity isn't required. -""" -struct DemandResponseShortfallSamples <: ResultSpec end - -struct DemandResponseShortfallSamplesAccumulator <: ResultAccumulator{DemandResponseShortfallSamples} - base::ShortfallSamplesAccumulator -end - -function accumulator( - sys::SystemModel{N}, nsamples::Int, ::DemandResponseShortfallSamples -) where {N} - base_acc = accumulator(sys, nsamples, ShortfallSamples()) # call original accumulator - return DemandResponseShortfallSamplesAccumulator(base_acc) -end - -function merge!( - x::DemandResponseShortfallSamplesAccumulator, y::DemandResponseShortfallSamplesAccumulator -) - merge!(x.base, y.base) -end - -accumulatortype(::DemandResponseShortfallSamples) = DemandResponseShortfallSamplesAccumulator - -struct DemandResponseShortfallSamplesResult{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractShortfallResult{N,L,T} - base::ShortfallSamplesResult{N,L,T,P,E} -end - -function getindex( - x::DemandResponseShortfallSamplesResult{N,L,T,P,E} -) where {N,L,T,P,E} - return getindex(x.base) -end - -function getindex( - x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString -) where {N,L,T,P,E} - return getindex(x.base, r) -end - -function getindex( - x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime -) where {N,L,T,P,E} - return getindex(x.base, t) -end - -function getindex( - x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime -) where {N,L,T,P,E} - return getindex(x.base, r, t) -end - - -function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}) where {N,L,T} - return LOLE(x.base) -end - -function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, r::AbstractString) where {N,L,T} - return LOLE(x.base, r) -end - -function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, t::ZonedDateTime) where {N,L,T} - return LOLE(x.base, t) -end - -function LOLE(x::DemandResponseShortfallSamplesResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} - return LOLE(x.base, r, t) -end - - -EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} = - EUE(x.base) - -EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} = - EUE(x.base, r) - -EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime) where {N,L,T,P,E} = - EUE(x.base, t) - -EUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,P,E} = - EUE(x.base, r, t) - -function NEUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} - return NEUE(x.base) -end - -function NEUE(x::DemandResponseShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} - return NEUE(x.base, r) -end - -function finalize( - acc::DemandResponseShortfallSamplesAccumulator, - system::SystemModel{N,L,T,P,E}, -) where {N,L,T,P,E} - base_result = finalize(acc.base, system) - - return DemandResponseShortfallSamplesResult(base_result) - -end diff --git a/PRASCore.jl/src/Results/Results.jl b/PRASCore.jl/src/Results/Results.jl index 1de375e5..3e4e3300 100644 --- a/PRASCore.jl/src/Results/Results.jl +++ b/PRASCore.jl/src/Results/Results.jl @@ -82,7 +82,6 @@ NEUE(x::AbstractShortfallResult, ::Colon, ::Colon) = include("Shortfall.jl") include("ShortfallSamples.jl") -include("DemandResponseShortfallSamples.jl") abstract type AbstractSurplusResult{N,L,T} <: Result{N,L,T} end diff --git a/PRASCore.jl/src/Results/Shortfall.jl b/PRASCore.jl/src/Results/Shortfall.jl index 89b26e46..afe4adef 100644 --- a/PRASCore.jl/src/Results/Shortfall.jl +++ b/PRASCore.jl/src/Results/Shortfall.jl @@ -119,8 +119,9 @@ function merge!( end -accumulatortype(::Shortfall) = ShortfallAccumulator{Shortfall} -accumulatortype(::DemandResponseShortfall) = ShortfallAccumulator{DemandResponseShortfall} +accumulatortype(::S) where { + S<:Union{Shortfall,DemandResponseShortfall} + } = ShortfallAccumulator{S} struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit, S} <: AbstractShortfallResult{N, L, T} @@ -200,47 +201,47 @@ struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit, S} <: end -function getindex(x::ShortfallResult{S}) where {S} +function getindex(x::ShortfallResult) return sum(x.shortfall_mean), x.shortfall_std end -function getindex(x::ShortfallResult{S}, r::AbstractString) where {S} +function getindex(x::ShortfallResult, r::AbstractString) i_r = findfirstunique(x.regions.names, r) return sum(view(x.shortfall_mean, i_r, :)), x.shortfall_region_std[i_r] end -function getindex(x::ShortfallResult{S}, t::ZonedDateTime) where {S} +function getindex(x::ShortfallResult, t::ZonedDateTime) i_t = findfirstunique(x.timestamps, t) return sum(view(x.shortfall_mean, :, i_t)), x.shortfall_period_std[i_t] end -function getindex(x::ShortfallResult{S}, r::AbstractString, t::ZonedDateTime) where {S} +function getindex(x::ShortfallResult, r::AbstractString, t::ZonedDateTime) i_r = findfirstunique(x.regions.names, r) i_t = findfirstunique(x.timestamps, t) return x.shortfall_mean[i_r, i_t], x.shortfall_regionperiod_std[i_r, i_t] end -LOLE(x::ShortfallResult{N,L,T,S}) where {N,L,T,S} = +LOLE(x::ShortfallResult{N,L,T}) where {N,L,T} = LOLE{N,L,T}(MeanEstimate(x.eventperiod_mean, x.eventperiod_std, x.nsamples)) -function LOLE(x::ShortfallResult{N,L,T,S}, r::AbstractString) where {N,L,T,S} +function LOLE(x::ShortfallResult{N,L,T}, r::AbstractString) where {N,L,T} i_r = findfirstunique(x.regions.names, r) return LOLE{N,L,T}(MeanEstimate(x.eventperiod_region_mean[i_r], x.eventperiod_region_std[i_r], x.nsamples)) end -function LOLE(x::ShortfallResult{N,L,T,S}, t::ZonedDateTime) where {N,L,T,S} +function LOLE(x::ShortfallResult{N,L,T}, t::ZonedDateTime) where {N,L,T} i_t = findfirstunique(x.timestamps, t) return LOLE{1,L,T}(MeanEstimate(x.eventperiod_period_mean[i_t], x.eventperiod_period_std[i_t], x.nsamples)) end -function LOLE(x::ShortfallResult{N,L,T,S}, r::AbstractString, t::ZonedDateTime) where {N,L,T,S} +function LOLE(x::ShortfallResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} i_r = findfirstunique(x.regions.names, r) i_t = findfirstunique(x.timestamps, t) return LOLE{1,L,T}(MeanEstimate(x.eventperiod_regionperiod_mean[i_r, i_t], @@ -249,23 +250,23 @@ function LOLE(x::ShortfallResult{N,L,T,S}, r::AbstractString, t::ZonedDateTime) end -EUE(x::ShortfallResult{N,L,T,E,S}) where {N,L,T,E,S} = +EUE(x::ShortfallResult{N,L,T,E}) where {N,L,T,E} = EUE{N,L,T,E}(MeanEstimate(x[]..., x.nsamples)) -EUE(x::ShortfallResult{N,L,T,E,S}, r::AbstractString) where {N,L,T,E,S} = +EUE(x::ShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} = EUE{N,L,T,E}(MeanEstimate(x[r]..., x.nsamples)) -EUE(x::ShortfallResult{N,L,T,E,S}, t::ZonedDateTime) where {N,L,T,E,S} = +EUE(x::ShortfallResult{N,L,T,E}, t::ZonedDateTime) where {N,L,T,E} = EUE{1,L,T,E}(MeanEstimate(x[t]..., x.nsamples)) -EUE(x::ShortfallResult{N,L,T,E,S}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E,S} = +EUE(x::ShortfallResult{N,L,T,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E} = EUE{1,L,T,E}(MeanEstimate(x[r, t]..., x.nsamples)) -function NEUE(x::ShortfallResult{N,L,T,E,S}) where {N,L,T,E,S} +function NEUE(x::ShortfallResult) return NEUE(div(MeanEstimate(x[]..., x.nsamples),(sum(x.regions.load)/1e6))) end -function NEUE(x::ShortfallResult{N,L,T,E,S}, r::AbstractString) where {N,L,T,E,S} +function NEUE(x::ShortfallResult, r::AbstractString) i_r = findfirstunique(x.regions.names, r) return NEUE(div(MeanEstimate(x[r]..., x.nsamples),(sum(x.regions.load[i_r,:])/1e6))) end diff --git a/PRASCore.jl/src/Results/ShortfallSamples.jl b/PRASCore.jl/src/Results/ShortfallSamples.jl index 9d30b42b..bddca0f6 100644 --- a/PRASCore.jl/src/Results/ShortfallSamples.jl +++ b/PRASCore.jl/src/Results/ShortfallSamples.jl @@ -44,21 +44,22 @@ Note that this result specification requires large amounts of memory for larger sample sizes. See [`Shortfall`](@ref) for average shortfall outcomes when sample-level granularity isn't required. """ struct ShortfallSamples <: ResultSpec end +struct DemandResponseShortfallSamples <: ResultSpec end -struct ShortfallSamplesAccumulator <: ResultAccumulator{ShortfallSamples} +struct ShortfallSamplesAccumulator{S} <: ResultAccumulator{ShortfallSamples} shortfall::Array{Int,3} end function accumulator( - sys::SystemModel{N}, nsamples::Int, ::ShortfallSamples -) where {N} + sys::SystemModel{N}, nsamples::Int, ::S +) where {N,S<:Union{ShortfallSamples,DemandResponseShortfallSamples}} nregions = length(sys.regions) shortfall = zeros(Int, nregions, N, nsamples) - return ShortfallSamplesAccumulator(shortfall) + return ShortfallSamplesAccumulator{S}(shortfall) end @@ -71,9 +72,11 @@ function merge!( end -accumulatortype(::ShortfallSamples) = ShortfallSamplesAccumulator +accumulatortype(::S) where { + S<:Union{ShortfallSamples,DemandResponseShortfallSamples} + } = ShortfallSamplesAccumulator{S} -struct ShortfallSamplesResult{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractShortfallResult{N,L,T} +struct ShortfallSamplesResult{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit, S} <: AbstractShortfallResult{N,L,T} regions::Regions{N,P} timestamps::StepRange{ZonedDateTime,T} @@ -162,11 +165,11 @@ function NEUE(x::ShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N, end function finalize( - acc::ShortfallSamplesAccumulator, + acc::ShortfallSamplesAccumulator{S}, system::SystemModel{N,L,T,P,E}, -) where {N,L,T,P,E} +) where {N,L,T,P,E,S<:Union{ShortfallSamples,DemandResponseShortfallSamples}} - return ShortfallSamplesResult{N,L,T,P,E}( + return ShortfallSamplesResult{N,L,T,P,E,S}( system.regions, system.timestamps, acc.shortfall) end diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 4e0126d6..6a9edcdd 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -95,7 +95,7 @@ function record!( end -function reset!(acc::Results.ShortfallAccumulator{S}, sampleid::Int) where {S} +function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) # Store regional / total sums for current simulation fit!(acc.periodsdropped_total, acc.periodsdropped_total_currentsim) @@ -119,11 +119,11 @@ end # ShortfallSamples function record!( - acc::Results.ShortfallSamplesAccumulator, + acc::Results.ShortfallSamplesAccumulator{S}, system::SystemModel{N,L,T,P,E}, state::SystemState, problem::DispatchProblem, sampleid::Int, t::Int -) where {N,L,T,P,E} +) where {N,L,T,P,E,S<:Results.ShortfallSamples} for ((r, e),dr_idxs) in zip(enumerate(problem.region_unserved_edges),system.region_dr_idxs) #getting dr shortfall @@ -139,15 +139,13 @@ function record!( end -reset!(acc::Results.ShortfallSamplesAccumulator, sampleid::Int) = nothing - # DemandResponseShortfallSamples function record!( - acc::Results.DemandResponseShortfallSamplesAccumulator, + acc::Results.ShortfallSamplesAccumulator{S}, system::SystemModel{N,L,T,P,E}, state::SystemState, problem::DispatchProblem, sampleid::Int, t::Int -) where {N,L,T,P,E} +) where {N,L,T,P,E,S<:Results.DemandResponseShortfallSamples} for (r,dr_idxs) in enumerate(system.region_dr_idxs) #getting dr shortfall @@ -156,15 +154,14 @@ function record!( dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 end - acc.base.shortfall[r, t, sampleid] = dr_shortfall + acc.shortfall[r, t, sampleid] = dr_shortfall end return end -reset!(acc::Results.DemandResponseShortfallSamplesAccumulator, sampleid::Int) = nothing - +reset!(acc::Results.ShortfallSamplesAccumulator, sampleid::Int) = nothing # Surplus diff --git a/PRASCore.jl/test/Results/shortfall.jl b/PRASCore.jl/test/Results/shortfall.jl index 421bfc7d..9f5d4a01 100644 --- a/PRASCore.jl/test/Results/shortfall.jl +++ b/PRASCore.jl/test/Results/shortfall.jl @@ -101,7 +101,7 @@ end r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod - result = PRASCore.Results.ShortfallSamplesResult{N,1,Hour,MW,MWh}( + result = PRASCore.Results.ShortfallSamplesResult{N,1,Hour,MW,MWh,ShortfallSamples}( Regions{N,MW}(DD.resourcenames, DD.resource_vals), DD.periods, DD.d) # Overall From 4b92f48e987203f89821b13f731f0a229c57eaee Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Mon, 8 Sep 2025 19:38:04 -0600 Subject: [PATCH 36/83] Update some constructors to include attrs after rebase --- PRASCore.jl/src/Systems/SystemModel.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index 40299afc..70eec92f 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -113,7 +113,7 @@ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} end - #base system constructor- no demand response devices +#base system constructor- no demand response devices function SystemModel{}( regions::Regions{N,P}, interfaces::Interfaces{N,P}, generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, @@ -121,7 +121,8 @@ function SystemModel{}( generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, - timestamps::StepRange{ZonedDateTime,T} + timestamps::StepRange{ZonedDateTime,T}, + attrs::Dict{String, String}=Dict{String, String}() ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} return SystemModel( @@ -135,7 +136,7 @@ function SystemModel{}( Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), repeat([1:0],length(regions)), lines, interface_line_idxs, - timestamps) + timestamps, attrs) end # No time zone constructor - demand responses included @@ -239,7 +240,8 @@ function SystemModel( storages::Storages{N,L,T,P,E}, generatorstorages::GeneratorStorages{N,L,T,P,E}, timestamps::StepRange{<:AbstractDateTime,T}, - load::Vector{Int} + load::Vector{Int}, + attrs::Dict{String, String}=Dict{String, String}() ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} return SystemModel( Regions{N,P}(["Region"], reshape(load, 1, :)), From 527d925e619f5368e761f1c70838b0048d37f704 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Mon, 8 Sep 2025 20:16:54 -0600 Subject: [PATCH 37/83] Add DRShortfall spec in a test --- PRASCore.jl/test/Simulations/runtests.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index 905f1b7b..7e3cc17f 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -9,7 +9,8 @@ simspec = SequentialMonteCarlo(samples=100_000, seed=1, threaded=false) smallsample = SequentialMonteCarlo(samples=10, seed=123, threaded=false) - resultspecs = (Shortfall(), Surplus(), Flow(), Utilization(), + resultspecs = (Shortfall(), Surplus(), + Flow(), Utilization(), ShortfallSamples(), SurplusSamples(), FlowSamples(), UtilizationSamples(), GeneratorAvailability()) @@ -46,7 +47,7 @@ shortfall2_3, _, flow2_3, util2_3, _ = assess(TestData.threenode, simspec, resultspecs...) - assess(TestData.threenode, smallsample, + assess(TestData.threenode, smallsample, DemandResponseShortfall(), GeneratorAvailability(), LineAvailability(), StorageAvailability(), GeneratorStorageAvailability(),DemandResponseAvailability(), StorageEnergy(), GeneratorStorageEnergy(),DemandResponseEnergy(), From 39cb6886c05894831c9d6b0f8457f5ce3afc2e40 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 16 Sep 2025 14:19:28 -0600 Subject: [PATCH 38/83] add energy samples, dr shortfall, and dr shortfall samples result retrieval to tests --- PRASCore.jl/src/Systems/TestData.jl | 11 ++++++++- PRASCore.jl/test/Simulations/runtests.jl | 31 ++++++++++++++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index 4bd5b0f5..431b42a7 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -431,6 +431,15 @@ threenode_dr_flow_t = [-1.576781; -2.386248; -0.256078; -0.6453639; -0.728238; - threenode_dr_util = 0.23285 threenode_dr_util_t = [0.116007969;0.12482749;0.1083558;0.106341959;0.108113959; 0.107764689] -threenode_dr_eenergy = 57.750022 +threenode_dr_energy = 57.750022 + +threenode_dr_energy_samples = 58.403 + +threenode_dr_shortfall_specific_eue = 23.524975 +threenode_dr_shortfall_specific_eue_r = [10.196; 7.944; 5.385] +threenode_dr_shortfall_specific_eue_t = [0.00; 0.00; 0.00; 0.00; 3.582; 19.943] +threenode_dr_shortfall_specific_eue_rt = [0.0000; 0.0000; 0.0000; 0.0000; 1.895; 8.300] + +threenode_dr_shortfall_samples = 1895487 end \ No newline at end of file diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index 7e3cc17f..8deebaa2 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -516,14 +516,15 @@ dr = first(TestData.threenode_dr.demandresponses.names) dts = TestData.threenode_dr.timestamps - shortfall, surplus, energy, flow, utilization = + shortfall, surplus, dr_energy, dr_energy_samples, dr_shortfall, dr_shortfall_samples, flow, utilization = assess(TestData.threenode_dr, simspec, - Shortfall(), Surplus(), DemandResponseEnergy(), Flow(), Utilization()) + Shortfall(), Surplus(), + DemandResponseEnergy(),DemandResponseEnergySamples(), + DemandResponseShortfall(), DemandResponseShortfallSamples(), + Flow(), Utilization()) # Shortfall - LOLE - @info "LOLE(shortfall): $(LOLE(shortfall))" - @info "TestData.threenode_dr_lole: $(TestData.threenode_dr_lole)" @test withinrange(LOLE(shortfall), TestData.threenode_dr_lole, nstderr_tol) @test all(withinrange.(LOLE.(shortfall, regions), @@ -569,12 +570,26 @@ @test all(withinrange.(getindex.(utilization, "Region 1"=>"Region 2",dts), TestData.threenode_dr_util_t, simspec.nsamples, nstderr_tol)) - # Energy - @test withinrange((sum(x[1] for x in getindex.(energy, dts)), sum(x[end] for x in getindex.(energy, dts))), # fails? - TestData.threenode_dr_eenergy, + # DR Energy + @test withinrange((sum(x[1] for x in getindex.(dr_energy, dts)), sum(x[end] for x in getindex.(dr_energy, dts))), + TestData.threenode_dr_energy, simspec.nsamples, nstderr_tol) - + # DR Energy Samples + @test mean(sum(dr_energy_samples.energy[:, :, i]) for i in 1:1_000) == TestData.threenode_dr_energy_samples + + # DR Shortfall + @test withinrange(EUE(dr_shortfall), + TestData.threenode_dr_shortfall_specific_eue, nstderr_tol) + @test all(withinrange.(EUE.(dr_shortfall, regions), + TestData.threenode_dr_shortfall_specific_eue_r, nstderr_tol)) + @test all(withinrange.(EUE.(dr_shortfall, dts), + TestData.threenode_dr_shortfall_specific_eue_t, nstderr_tol)) + @test all(withinrange.(EUE.(dr_shortfall, "Region 1", dts), + TestData.threenode_dr_shortfall_specific_eue_rt, nstderr_tol)) + + # DR Shortfall Samples + @test sum(dr_shortfall_samples["Region 1",dts[5]]) == TestData.threenode_dr_shortfall_samples end From bcea50107cd2ae95959d3b0a88cad593c77c8fd1 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 16 Sep 2025 17:46:15 -0600 Subject: [PATCH 39/83] update test comparison --- PRASCore.jl/test/Simulations/runtests.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index 8deebaa2..3191571a 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -535,8 +535,6 @@ TestData.threenode_dr_lole_rt, nstderr_tol)) # Shortfall - EUE - @info "eue(shortfall): $(EUE(shortfall))" - @info "TestData.threenode_dr_eue: $(TestData.threenode_dr_eue)" @test withinrange(EUE(shortfall), TestData.threenode_dr_eue, nstderr_tol) @test all(withinrange.(EUE.(shortfall, regions), @@ -576,7 +574,7 @@ simspec.nsamples, nstderr_tol) # DR Energy Samples - @test mean(sum(dr_energy_samples.energy[:, :, i]) for i in 1:1_000) == TestData.threenode_dr_energy_samples + @test round(mean(sum(dr_energy_samples.energy[:, :, i]) for i in 1:1_000)) == round(TestData.threenode_dr_energy_samples) # DR Shortfall @test withinrange(EUE(dr_shortfall), @@ -589,7 +587,7 @@ TestData.threenode_dr_shortfall_specific_eue_rt, nstderr_tol)) # DR Shortfall Samples - @test sum(dr_shortfall_samples["Region 1",dts[5]]) == TestData.threenode_dr_shortfall_samples + @test round(sum(dr_shortfall_samples["Region 1", dts[5]]) / 1e6) == round(TestData.threenode_dr_shortfall_samples / 1e6) end From 5592d87c8b50ff6d25222ff629a6a6cef9f3373b Mon Sep 17 00:00:00 2001 From: juflorez Date: Wed, 17 Sep 2025 09:20:26 -0600 Subject: [PATCH 40/83] restructure specific test design --- PRASCore.jl/test/Simulations/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PRASCore.jl/test/Simulations/runtests.jl b/PRASCore.jl/test/Simulations/runtests.jl index 3191571a..3c518634 100644 --- a/PRASCore.jl/test/Simulations/runtests.jl +++ b/PRASCore.jl/test/Simulations/runtests.jl @@ -574,8 +574,8 @@ simspec.nsamples, nstderr_tol) # DR Energy Samples - @test round(mean(sum(dr_energy_samples.energy[:, :, i]) for i in 1:1_000)) == round(TestData.threenode_dr_energy_samples) - + @test isapprox(mean(sum(dr_energy_samples.energy[:, :, i]) for i in 1:1_000),TestData.threenode_dr_energy_samples, rtol=0.01) + # DR Shortfall @test withinrange(EUE(dr_shortfall), TestData.threenode_dr_shortfall_specific_eue, nstderr_tol) @@ -587,7 +587,7 @@ TestData.threenode_dr_shortfall_specific_eue_rt, nstderr_tol)) # DR Shortfall Samples - @test round(sum(dr_shortfall_samples["Region 1", dts[5]]) / 1e6) == round(TestData.threenode_dr_shortfall_samples / 1e6) + @test isapprox(sum(dr_shortfall_samples["Region 1",dts[5]])/1e4,TestData.threenode_dr_shortfall_samples/1e4, rtol=0.01) end From 8080b2146a2fcd4db95a1c13cbbcca0bb84b6112 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 23 Sep 2025 14:31:14 -0600 Subject: [PATCH 41/83] remove pointer back to itself DemandResponseEnergy docs --- PRASCore.jl/src/Results/DemandResponseEnergy.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/PRASCore.jl/src/Results/DemandResponseEnergy.jl b/PRASCore.jl/src/Results/DemandResponseEnergy.jl index 0bfb3403..0480d688 100644 --- a/PRASCore.jl/src/Results/DemandResponseEnergy.jl +++ b/PRASCore.jl/src/Results/DemandResponseEnergy.jl @@ -19,8 +19,6 @@ soc_mean, soc_std = ``` See [`DemandResponseEnergySamples`](@ref) for sample-level demand response states of borrowed energy. - -See [`DemandResponseEnergy`](@ref) for average demand-response borrowed energy. """ struct DemandResponseEnergy <: ResultSpec end From 1ae8d83c55bd3dc73ea95bceaf0b8c5c777cc7f3 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 23 Sep 2025 14:34:59 -0600 Subject: [PATCH 42/83] misc doc fix DemandResponseAvailability --- PRASCore.jl/src/Results/DemandResponseAvailability.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Results/DemandResponseAvailability.jl b/PRASCore.jl/src/Results/DemandResponseAvailability.jl index 72ae6f6c..fce28d8e 100644 --- a/PRASCore.jl/src/Results/DemandResponseAvailability.jl +++ b/PRASCore.jl/src/Results/DemandResponseAvailability.jl @@ -1,5 +1,5 @@ """ - DemandResponse + DemandResponseAvailability The `DemandResponseAvailability` result specification reports the sample-level discrete availability of `DemandResponses`, producing a `DemandResponseAvailabilityResult`. From bf06e4bf9e541ed1ab96b5b31e5d8a8d19bf219f Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 23 Sep 2025 14:39:17 -0600 Subject: [PATCH 43/83] DispatchProblen docs clarity --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index e4a7d66d..c1bda045 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -2,11 +2,10 @@ DispatchProblem(sys::SystemModel) Create a min-cost flow problem for the multi-region max power delivery problem -with generation and storage discharging in decreasing order of priority, and -storage charging with excess capacity. Storage and GeneratorStorage devices -within a region are represented individually on the network. Demand Response -devices will borrow energy in devices with the longest payback window first, -and vice versa for payback energy. +with generation and storage discharging in decreasing order of priority, +storage charging with excess capacity and demand response devices enabling load shed and shift functionality. +Storage, GeneratorStorage, and Demand Response devices within a region are represented individually on the network. + This involves injections/withdrawals at one node (regional capacity surplus/shortfall) for each modelled region, as well as two/three nodes @@ -24,8 +23,8 @@ capacity is exhausted (implying an operational strategy that prioritizes resource adequacy over economic arbitrage). This is based on the storage dispatch strategy of Evans, Tindemans, and Angeli, as outlined in "Minimizing Unserved Energy Using Heterogenous Storage Units" (IEEE Transactions on Power -Systems, 2019). - +Systems, 2019). Demand Response devices will borrow energy in devices with the +longest payback window first, and vice versa for payback energy. Demand Response devices are utilized only after discharging all storage/genstor and paid back before storage/genstor charging. From 84a81f58f658b43cd50319051a232ae83670517b Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 23 Sep 2025 14:46:31 -0600 Subject: [PATCH 44/83] misc doc fix for shortagepenalty --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index c1bda045..fd3a28f7 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -141,7 +141,7 @@ struct DispatchProblem #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) - min_paybackcost_dr = (- maxpaybacktime_dr - 50 + min_chargecost) #add min_chargecost to always have DR devices be paybacked first, and -15 for wheeling prevention + min_paybackcost_dr = (- maxpaybacktime_dr - 50 + min_chargecost) #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention max_borrowcost_dr = - min_paybackcost_dr + minpaybacktime_dr + 1 + max_dischargecost #add max_dischargecost to always have DR devices be borrowed last #for unserved energy From a0fbc33551f963f47f239bae7084d03e79b07c58 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Mon, 6 Oct 2025 12:14:16 -0600 Subject: [PATCH 45/83] Refactor some recordings code to remove repetition; Create default constructors. --- PRASCore.jl/src/Simulations/recording.jl | 83 ++++-------------------- PRASCore.jl/src/Simulations/utils.jl | 17 ++++- PRASCore.jl/src/Systems/SystemModel.jl | 51 +++------------ PRASCore.jl/src/Systems/assets.jl | 48 ++++++++++++++ PRASCore.jl/src/Systems/collections.jl | 13 +++- PRASFiles.jl/src/Systems/read.jl | 32 ++------- 6 files changed, 104 insertions(+), 140 deletions(-) diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 6a9edcdd..4bdcfc9f 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -5,7 +5,7 @@ function record!( system::SystemModel{N,L,T,P,E}, state::SystemState, problem::DispatchProblem, sampleid::Int, t::Int -) where {N,L,T,P,E,S<:Results.Shortfall} +) where {N,L,T,P,E,S} totalshortfall = 0 isshortfall = false @@ -15,7 +15,11 @@ function record!( for (r, dr_idxs) in zip(problem.region_unserved_edges, system.region_dr_idxs) #count region shortfall and include dr shortfall - regionshortfall = edges[r].flow + #if shortfall include region and dr shortfall + #if drshortfall don't include region shortfall + + regionshortfall = init_regionshortfall(S,edges,r) + dr_shortfall = 0 for i in dr_idxs dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 @@ -50,51 +54,6 @@ function record!( end -# DemandResponseShortfall -function record!( - acc::Results.ShortfallAccumulator{S}, - system::SystemModel{N,L,T,P,E}, - state::SystemState, problem::DispatchProblem, - sampleid::Int, t::Int -) where {N,L,T,P,E,S<:Results.DemandResponseShortfall} - - totalshortfall = 0 - isshortfall = false - - for (r, dr_idxs) in zip(problem.region_unserved_edges, system.region_dr_idxs) - - #count region shortfall and include dr shortfall - dr_shortfall = 0 - for i in dr_idxs - dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 - end - regionshortfall = dr_shortfall - isregionshortfall = regionshortfall > 0 - - fit!(acc.periodsdropped_regionperiod[r,t], isregionshortfall) - fit!(acc.unservedload_regionperiod[r,t], regionshortfall) - - if isregionshortfall - - isshortfall = true - totalshortfall += regionshortfall - - acc.periodsdropped_region_currentsim[r] += 1 - acc.unservedload_region_currentsim[r] += regionshortfall - - end - end - - if isshortfall - acc.periodsdropped_total_currentsim += 1 - acc.unservedload_total_currentsim += totalshortfall - end - fit!(acc.periodsdropped_period[t], isshortfall) - fit!(acc.unservedload_period[t], totalshortfall) - return - -end - function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) # Store regional / total sums for current simulation @@ -123,38 +82,18 @@ function record!( system::SystemModel{N,L,T,P,E}, state::SystemState, problem::DispatchProblem, sampleid::Int, t::Int -) where {N,L,T,P,E,S<:Results.ShortfallSamples} - - for ((r, e),dr_idxs) in zip(enumerate(problem.region_unserved_edges),system.region_dr_idxs) - #getting dr shortfall - dr_shortfall = 0 - for i in dr_idxs - dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 - end - - acc.shortfall[r, t, sampleid] = problem.fp.edges[e].flow + dr_shortfall - end - - return - -end - -# DemandResponseShortfallSamples -function record!( - acc::Results.ShortfallSamplesAccumulator{S}, - system::SystemModel{N,L,T,P,E}, - state::SystemState, problem::DispatchProblem, - sampleid::Int, t::Int -) where {N,L,T,P,E,S<:Results.DemandResponseShortfallSamples} +) where {N,L,T,P,E,S} - for (r,dr_idxs) in enumerate(system.region_dr_idxs) + for (r,dr_idxs) in zip(problem.region_unserved_edges,system.region_dr_idxs) #getting dr shortfall dr_shortfall = 0 for i in dr_idxs dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 end - acc.shortfall[r, t, sampleid] = dr_shortfall + acc.shortfall[r, t, sampleid] = + init_regionshortfall(S,problem.fp.edges,r) + + dr_shortfall end return diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 06b556ea..07f45a8d 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -251,8 +251,23 @@ function minmax_payback_window_dr(system::SystemModel) end +function init_regionshortfall( + ::Type{S}, + edges, + region) where {S <: Union{ + Results.Shortfall, + Results.ShortfallSamples}} + return edges[region].flow +end - +function init_regionshortfall( + ::Type{S}, + edges, + region) where {S <: Union{ + Results.DemandResponseShortfall, + Results.DemandResponseShortfallSamples}} + return 0 +end function utilization(f::MinCostFlows.Edge, b::MinCostFlows.Edge) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index 70eec92f..eb7a7532 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -130,11 +130,7 @@ function SystemModel{}( generators, region_gen_idxs, storages, region_stor_idxs, generatorstorages, region_genstor_idxs, - DemandResponses{N,L,T,P,E}( - String[], String[], - Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), - Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), - Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), repeat([1:0],length(regions)), + DemandResponses{N,L,T,P,E}(), repeat([1:0],length(regions)), lines, interface_line_idxs, timestamps, attrs) end @@ -182,27 +178,14 @@ function SystemModel( attrs::Dict{String, String}=Dict{String, String}() ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} - @warn "No time zone data provided - defaulting to UTC. To specify a " * - "time zone for the system timestamps, provide a range of " * - "`ZonedDateTime` instead of `DateTime`." - - utc = tz"UTC" - time_start = ZonedDateTime(first(timestamps), utc) - time_end = ZonedDateTime(last(timestamps), utc) - timestamps_tz = time_start:step(timestamps):time_end - return SystemModel( regions, interfaces, generators, region_gen_idxs, storages, region_stor_idxs, generatorstorages, region_genstor_idxs, - DemandResponses{N,L,T,P,E}( - String[], String[], - Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), - Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), - Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), repeat([1:0],length(regions)), + DemandResponses{N,L,T,P,E}(), repeat([1:0],length(regions)), lines, interface_line_idxs, - timestamps_tz,attrs) + timestamps,attrs) end @@ -218,18 +201,13 @@ function SystemModel( ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} return SystemModel( - Regions{N,P}(["Region"], reshape(load, 1, :)), - Interfaces{N,P}( - Int[], Int[], - Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N)), + Regions{N,P}(load), + Interfaces{N,P}(), generators, [1:length(generators)], storages, [1:length(storages)], generatorstorages, [1:length(generatorstorages)], demandresponses, [1:length(demandresponses)], - Lines{N,L,T,P}( - String[], String[], - Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N), - Matrix{Float64}(undef, 0, N), Matrix{Float64}(undef, 0, N)), + Lines{N,L,T,P}(), UnitRange{Int}[], timestamps, attrs) end @@ -244,22 +222,13 @@ function SystemModel( attrs::Dict{String, String}=Dict{String, String}() ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} return SystemModel( - Regions{N,P}(["Region"], reshape(load, 1, :)), - Interfaces{N,P}( - Int[], Int[], - Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N)), + Regions{N,P}(load), + Interfaces{N,P}(), generators, [1:length(generators)], storages, [1:length(storages)], generatorstorages, [1:length(generatorstorages)], - DemandResponses{N,L,T,P,E}( - String[], String[], - Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), - Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), - Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)), [1:0], - Lines{N,L,T,P}( - String[], String[], - Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N), - Matrix{Float64}(undef, 0, N), Matrix{Float64}(undef, 0, N)), + DemandResponses{N,L,T,P,E}(), [1:0], + Lines{N,L,T,P}(), UnitRange{Int}[], timestamps, attrs) end diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index f1442c22..816f054c 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -91,6 +91,15 @@ struct Generators{N,L,T<:Period,P<:PowerUnit} <: AbstractAssets{N,L,T,P} end +# Empty Generators constructor +function Generators{N,L,T,P}() where {N,L,T,P} + + return Generators{N,L,T,P}( + String[], String[], zeros(Int, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N)) + +end + Base.:(==)(x::T, y::T) where {T <: Generators} = x.names == y.names && x.categories == y.categories && @@ -226,6 +235,16 @@ struct Storages{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAssets{N,L, end +# Empty Storages constructor +function Storages{N,L,T,P,E}() where {N,L,T,P,E} + + return Storages{N,L,T,P,E}( + String[], String[], + zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N)) +end + Base.:(==)(x::T, y::T) where {T <: Storages} = x.names == y.names && x.categories == y.categories && @@ -408,6 +427,18 @@ struct GeneratorStorages{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAs end +# Empty GeneratorStorages constructor +function GeneratorStorages{N,L,T,P,E}() where {N,L,T,P,E} + + return GeneratorStorages{N,L,T,P,E}( + String[], String[], + zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), + zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N)) + +end + Base.:(==)(x::T, y::T) where {T <: GeneratorStorages} = x.names == y.names && x.categories == y.categories && @@ -552,6 +583,16 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse end +# Empty DemandResponses constructor +function DemandResponses{N,L,T,P,E}() where {N,L,T,P,E} + + return DemandResponses{N,L,T,P,E}( + String[], String[], + Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), + Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), + Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)) +end + Base.:(==)(x::T, y::T) where {T <: DemandResponses} = x.names == y.names && x.categories == y.categories && @@ -686,6 +727,13 @@ struct Lines{N,L,T<:Period,P<:PowerUnit} <: AbstractAssets{N,L,T,P} end +# Empty Lines constructor +function Lines{N,L,T,P}() where {N,L,T,P} + return Lines{N,L,T,P}( + String[], String[], zeros(Int, 0, N), zeros(Int, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N)) +end + Base.:(==)(x::T, y::T) where {T <: Lines} = x.names == y.names && x.categories == y.categories && diff --git a/PRASCore.jl/src/Systems/collections.jl b/PRASCore.jl/src/Systems/collections.jl index a265ae94..1218fb9c 100644 --- a/PRASCore.jl/src/Systems/collections.jl +++ b/PRASCore.jl/src/Systems/collections.jl @@ -32,7 +32,12 @@ struct Regions{N,P<:PowerUnit} end -Base.:(==)(x::T, y::T) where {T <: Regions} = +# Empty Regions constructor +function Regions{N,P}(load::Vector{Int}) where {N,P} + return Regions{N,P}(["Region"], reshape(load, 1, :)) +end + + Base.:(==)(x::T, y::T) where {T <: Regions} = x.names == y.names && x.load == y.load @@ -81,6 +86,12 @@ struct Interfaces{N,P<:PowerUnit} end +# Empty Interfaces constructor +function Interfaces{N,P}() where {N,P} + return Interfaces{N,P}( + Int[], Int[], zeros(Int, 0, N), zeros(Int, 0, N)) +end + Base.:(==)(x::T, y::T) where {T <: Interfaces} = x.regions_from == y.regions_from && x.regions_to == y.regions_to && diff --git a/PRASFiles.jl/src/Systems/read.jl b/PRASFiles.jl/src/Systems/read.jl index ff7823f2..a4cdfa4a 100644 --- a/PRASFiles.jl/src/Systems/read.jl +++ b/PRASFiles.jl/src/Systems/read.jl @@ -100,9 +100,7 @@ function _systemmodel_core(f::File) else - generators = Generators{N,L,T,P}( - String[], String[], zeros(Int, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N)) + generators = Generators{N,L,T,P}() region_gen_idxs = fill(1:0, n_regions) @@ -133,11 +131,7 @@ function _systemmodel_core(f::File) else - storages = Storages{N,L,T,P,E}( - String[], String[], - zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N)) + storages = Storages{N,L,T,P,E}() region_stor_idxs = fill(1:0, n_regions) @@ -172,12 +166,7 @@ function _systemmodel_core(f::File) else - generatorstorages = GeneratorStorages{N,L,T,P,E}( - String[], String[], - zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), - zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N)) + generatorstorages = GeneratorStorages{N,L,T,P,E}() region_genstor_idxs = fill(1:0, n_regions) @@ -259,12 +248,9 @@ function _systemmodel_core(f::File) else - interfaces = Interfaces{N,P}( - Int[], Int[], zeros(Int, 0, N), zeros(Int, 0, N)) + interfaces = Interfaces{N,P}() - lines = Lines{N,L,T,P}( - String[], String[], zeros(Int, 0, N), zeros(Int, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N)) + lines = Lines{N,L,T,P}() interface_line_idxs = UnitRange{Int}[] @@ -293,22 +279,18 @@ end Read a SystemModel from a PRAS file in version 0.8.x format. """ function systemmodel_0_8(f::File) - - has_demandresponses = haskey(f, "demandresponses") (regions, interfaces, generators, region_gen_idxs, storages, region_stor_idxs, generatorstorages, region_genstor_idxs, lines, interface_line_idxs, - timestamps), type_params = _systemmodel_core(f) - - N, L, T, P, E = type_params + timestamps), (N,L,T,P,E) = _systemmodel_core(f) n_regions = length(regions) regionlookup = Dict(n=>i for (i, n) in enumerate(regions.names)) - if has_demandresponses + if haskey(f, "demandresponses") dr_core = read(f["demandresponses/_core"]) From d39a1f449d3fc49b8ec7edd9992e7ccaf47e4c25 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Mon, 6 Oct 2025 12:23:29 -0600 Subject: [PATCH 46/83] Remove white spaces and one unnecessary parameter check --- PRASCore.jl/src/Results/Shortfall.jl | 4 ++-- PRASFiles.jl/src/PRASFiles.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PRASCore.jl/src/Results/Shortfall.jl b/PRASCore.jl/src/Results/Shortfall.jl index afe4adef..796aca4f 100644 --- a/PRASCore.jl/src/Results/Shortfall.jl +++ b/PRASCore.jl/src/Results/Shortfall.jl @@ -201,7 +201,7 @@ struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit, S} <: end -function getindex(x::ShortfallResult) +function getindex(x::ShortfallResult) return sum(x.shortfall_mean), x.shortfall_std end @@ -274,7 +274,7 @@ end function finalize( acc::ShortfallAccumulator{S}, system::SystemModel{N,L,T,P,E}, -) where {N,L,T,P,E,S<:Union{Shortfall,DemandResponseShortfall}} +) where {N,L,T,P,E,S} ep_total_mean, ep_total_std = mean_std(acc.periodsdropped_total) ep_region_mean, ep_region_std = mean_std(acc.periodsdropped_region) diff --git a/PRASFiles.jl/src/PRASFiles.jl b/PRASFiles.jl/src/PRASFiles.jl index a212b139..1baed2da 100644 --- a/PRASFiles.jl/src/PRASFiles.jl +++ b/PRASFiles.jl/src/PRASFiles.jl @@ -3,7 +3,7 @@ module PRASFiles import PRASCore.Systems: SystemModel, Regions, Interfaces, Generators, Storages, GeneratorStorages, DemandResponses, Lines, timeunits, powerunits, energyunits, unitsymbol - + import PRASCore.Results: EUE, LOLE, NEUE, ShortfallResult, ShortfallSamplesResult, AbstractShortfallResult, Result import StatsBase: mean import Dates: @dateformat_str, format, now From 2dfb068c28926d76b2f0110b17c66f160224e906 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 14:25:39 -0600 Subject: [PATCH 47/83] add implied merit order comment --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index fd3a28f7..ae488d68 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -134,6 +134,10 @@ struct DispatchProblem ngenstors = length(sys.generatorstorages) ndrs = length(sys.demandresponses) + + #Implied merit order: + # dr payback_cost < stor/genstor charge cost < stor/genstor discharge cost < borrow cost < unserved energy + #for storage/genstor maxchargetime, maxdischargetime = maxtimetocharge_discharge(sys) min_chargecost = - maxchargetime - 1 @@ -141,8 +145,8 @@ struct DispatchProblem #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) - min_paybackcost_dr = (- maxpaybacktime_dr - 50 + min_chargecost) #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention - max_borrowcost_dr = - min_paybackcost_dr + minpaybacktime_dr + 1 + max_dischargecost #add max_dischargecost to always have DR devices be borrowed last + min_paybackcost_dr = min_chargecost- maxpaybacktime_dr - 50 #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention + max_borrowcost_dr = max_dischargecost - min_paybackcost_dr + minpaybacktime_dr + 1 #add max_dischargecost to always have DR devices be borrowed last #for unserved energy shortagepenalty = 10 * (nifaces + max_borrowcost_dr) From bd15a9354786e54eca0077d55b26cc05a60d8eaa Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 14:26:33 -0600 Subject: [PATCH 48/83] remove old paybackcost comment --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index ae488d68..06927feb 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -463,7 +463,7 @@ function update_problem!( fp.nodes[payback_node], slack_node, -payback_capacity) # smallest allowable payback window = highest priority (payback first) - paybackcost = problem.min_paybackcost_dr + allowablepayback # Negative cost-mutliply by 10 for wheeling preventation + paybackcost = problem.min_paybackcost_dr + allowablepayback updateflowcost!(fp.edges[payback_edge], paybackcost) # Update borrowing-make sure no borrowing is allowed if allowable payback period is equal to zero From 8685a9d512d5bade756a81bc7c6a6aa0b7ee29a8 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 14:39:05 -0600 Subject: [PATCH 49/83] remove old comment --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 06927feb..2422309c 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -529,7 +529,6 @@ function update_state!( end #Demand Response Update - #borrowing (negative of the flows) for (i, e) in enumerate(problem.demandresponse_borrow_edges) state.drs_energy[i] += ceil(Int, edges[e].flow * p2e * system.demandresponses.borrow_efficiency[i, t]) From 5989abebc6b26fe61bf771de8849481328079425 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 14:40:59 -0600 Subject: [PATCH 50/83] misc trailing whitespace --- PRASCore.jl/src/Results/Shortfall.jl | 2 +- PRASCore.jl/src/Simulations/recording.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/src/Results/Shortfall.jl b/PRASCore.jl/src/Results/Shortfall.jl index 796aca4f..0070b39b 100644 --- a/PRASCore.jl/src/Results/Shortfall.jl +++ b/PRASCore.jl/src/Results/Shortfall.jl @@ -205,7 +205,7 @@ function getindex(x::ShortfallResult) return sum(x.shortfall_mean), x.shortfall_std end -function getindex(x::ShortfallResult, r::AbstractString) +function getindex(x::ShortfallResult, r::AbstractString) i_r = findfirstunique(x.regions.names, r) return sum(view(x.shortfall_mean, i_r, :)), x.shortfall_region_std[i_r] end diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 4bdcfc9f..91466ce1 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -54,7 +54,7 @@ function record!( end -function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) +function reset!(acc::Results.ShortfallAccumulator, sampleid::Int) # Store regional / total sums for current simulation fit!(acc.periodsdropped_total, acc.periodsdropped_total_currentsim) From 379339610f4358e6a51929a206fb12710dd7dcd0 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 15:03:48 -0600 Subject: [PATCH 51/83] add DR to system overviews/printing --- PRASCore.jl/src/Systems/SystemModel.jl | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index eb7a7532..b9d26599 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -243,6 +243,7 @@ Base.:(==)(x::T, y::T) where {T <: SystemModel} = x.generatorstorages == y.generatorstorages && x.region_genstor_idxs == y.region_genstor_idxs && x.demandresponses == y.demandresponses && + x.region_dr_idxs == y.region_dr_idxs && x.lines == y.lines && x.interface_line_idxs == y.interface_line_idxs && x.timestamps == y.timestamps && @@ -280,6 +281,7 @@ function Base.show(io::IO, sys::SystemModel{N,L,T,P,E}) where {N,L,T<:Period,P<: print(io, "SystemModel($(length(sys.regions)) regions, $(length(sys.interfaces)) interfaces, ", "$(length(sys.generators)) generators, $(length(sys.storages)) storages, ", "$(length(sys.generatorstorages)) generator-storages,", + "$(length(sys.demandresponses)) demand-responses,", " $(N) $(time_unit)s)") end @@ -291,6 +293,7 @@ function Base.show(io::IO, ::MIME"text/plain", sys::SystemModel{N,L,T,P,E}) wher println(io, " Generators: $(length(sys.generators)) units") println(io, " Storages: $(length(sys.storages)) units") println(io, " GeneratorStorages: $(length(sys.generatorstorages)) units") + println(io, " DemandResponses: $(length(sys.demandresponses)) units") println(io, " Lines: $(length(sys.lines))") println(io, "\nTime series:") println(io, " Start time: $(first(sys.timestamps))") @@ -315,6 +318,7 @@ struct RegionInfo generators::NamedTuple storages::NamedTuple generatorstorages::NamedTuple + demandresponses::NamedTuple peak_load::Int power_unit::String end @@ -335,7 +339,8 @@ function Base.getindex(sys::SystemModel, region_idx::Int) gen_range = sys.region_gen_idxs[region_idx] stor_range = sys.region_stor_idxs[region_idx] genstor_range = sys.region_genstor_idxs[region_idx] - + dr_range = sys.region_dr_idxs[region_idx] + # Get peak load for this region peak_load = maximum(sys.regions.load[region_idx, :]) @@ -355,6 +360,10 @@ function Base.getindex(sys::SystemModel, region_idx::Int) indices = genstor_range, count = length(genstor_range), ), + ( + indices = dr_range, + count = length(dr_range), + ), peak_load, power_unit, ) @@ -378,7 +387,8 @@ function Base.show(io::IO, info::RegionInfo) println(io, " Peak load: $(info.peak_load) $(info.power_unit)") println(io, " Generators: $(info.generators.count) units [indices - $(info.generators.indices)]") println(io, " Storages: $(info.storages.count) units [indices - $(info.storages.indices)]") - print(io, " GeneratorStorages: $(info.generatorstorages.count) units [indices - $(info.generatorstorages.indices)]") + println(io, " GeneratorStorages: $(info.generatorstorages.count) units [indices - $(info.generatorstorages.indices)]") + println(io, " DemandResponses: $(info.demandresponses.count) units [indices - $(info.demandresponses.indices)]") end """ @@ -423,7 +433,12 @@ function _get_asset_by_type(sys::SystemModel, region_idx::Int, ::Type{GeneratorS return sys.generatorstorages[genstor_range] end +function _get_asset_by_type(sys::SystemModel, region_idx::Int, ::Type{DemandResponses}) + dr_range = sys.region_dr_idxs[region_idx] + return sys.demandresponses[dr_range] +end + # Fallback for unsupported types function _get_asset_by_type(sys::SystemModel, region_idx::Int, ::Type{T}) where {T} - error("Asset type $T is not supported. Supported types are: Generators, Storages, GeneratorStorages") + error("Asset type $T is not supported. Supported types are: Generators, Storages, GeneratorStorages, DemandResponses") end From 8c6660f6e9869d539398c14fa5a1b7555bf4ce0a Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 15:12:44 -0600 Subject: [PATCH 52/83] adding misc comments to test data --- PRASCore.jl/src/Systems/TestData.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index 431b42a7..ecc5a0aa 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -274,7 +274,7 @@ test3_eenergy = [6.561, 7.682202] # Test System 4 (Gen + DR, 1 Region for 6 hours) - +# Copper plage, testing DR interactions in one system, that borrowing is occurring and payback is happening timestamps = ZonedDateTime(2020,1,1,1, tz):Hour(1):ZonedDateTime(2020,1,1,6, tz) gen = Generators{6,1,Hour,MW}( @@ -317,7 +317,7 @@ test4_eenergy = [0.89863, 2.45616, 4.2544, 0.997662, 2.673, 0.0] # Test System 5 (Gen + DR + Stor, 1 Region for 6 hours) - +#Copper plate, testing DR and storage interactions in one system timestamps = ZonedDateTime(2020,1,1,1, tz):Hour(1):ZonedDateTime(2020,1,1,6, tz) gen = Generators{6,1,Hour,MW}( @@ -361,7 +361,7 @@ test5_eenergy = [0.89936, 1.86623, 3.66255, 5.05573, 1.54738, 0.0] # Multiregion with DR - +#testing multiple DR interactions and that intra dispatch priority is working properly regions = Regions{6,MW}(["Region 1", "Region 2", "Region 3"], [19 20 25 26 24 25; 20 21 30 27 23 24; 22 26 27 25 23 24]) From 5d7fb0c7ac4413e9c095b4019a3571740687c33f Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 13:07:06 -0600 Subject: [PATCH 53/83] add DR device to sysmodel spec and updated picture --- docs/src/PRAS/sysmodelspec.md | 76 ++++++++++++++++++++----- docs/src/images/resourceparameters.png | Bin 0 -> 93269 bytes 2 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 docs/src/images/resourceparameters.png diff --git a/docs/src/PRAS/sysmodelspec.md b/docs/src/PRAS/sysmodelspec.md index f92d1a8f..a7708794 100644 --- a/docs/src/PRAS/sysmodelspec.md +++ b/docs/src/PRAS/sysmodelspec.md @@ -44,33 +44,37 @@ methods -- while this is also possible with a single multi-year run, it requires some additional post-processing work. PRAS represents a power system as one or more **regions**, each containing -zero or more **generators**, **storages**, and -**generator-storages**. **Interfaces** contain **lines** and +zero or more **generators**, **storages**,**generator-storages**, and +**demand responses**. **Interfaces** contain **lines** and allow power transfer between two regions. The table below summarizes the characteristics of the different resource types (generators, storages, generator-storages, and lines), and the remainder of this section provides more details about each resource type and their associated resource collections (regions or interfaces). -| Parameter | Generator | Storage | Generator-Storage | Line | -|-----------|-----------|---------|-------------------|------| -| *Associated with a(n)...* | *Region* | *Region* | *Region* | *Interface* | -| *Name* | • | • | • | • | -| *Category* | • | • | • | • | +| Parameter | Generator | Storage | Generator-Storage | Demand Response | Line | +|-----------|-----------|---------|-------------------|-----------------|------| +| *Associated with a(n)...* | *Region* | *Region* | *Region* | *Region* | *Interface* | +| *Name* | • | • | • | • | • | +| *Category* | • | • | • | • | • | | Generation Capacity | • | | | | | Inflow Capacity | | | • | | | Charge Capacity | | • | • | | | Discharge Capacity | | • | • | | +| Load Borrow Capacity | | | | • | | +| Load Payback Capacity | | | | • | | | Energy Capacity | | • | • | | +| Borrowed Load Capacity | | | | • | | | Charge Efficiency | | • | • | | | Discharge Efficiency | | • | • | | | Carryover Efficiency | | • | • | | +| Borrowed Energy Interest | | | | • | | | Grid Injection Capacity | | | • | | | Grid Withdrawal Capacity | | | • | | -| Forward Transfer Capacity | | | | • | -| Backward Transfer Capacity | | | | • | -| Available→Unavailable Transition Probability | • | • | • | • | -| Unavailable→Available Transition Probability | • | • | • | • | +| Forward Transfer Capacity | | | | | • | +| Backward Transfer Capacity | | | | | • | +| Available→Unavailable Transition Probability | • | • | • | • | • | +| Unavailable→Available Transition Probability | • | • | • | • | • | *Table: PRAS resource parameters. Parameters in italic are fixed values; all others are provided as a time series.* @@ -113,8 +117,8 @@ time series, and so may be different during different time periods. ```@raw html
-Relations between power and energy parameters for generator, storage, and generator-storage resources. -
Relations between power and energy parameters for generator, storage, and generator-storage resources
+Relations between power and energy parameters for generator, storage, and generator-storage resources. +
Relations between power and energy parameters for generator, storage, generator-storage, and demand response resources
``` @@ -157,7 +161,7 @@ Just as with generators, storages may be in available or unavailable states, and move between these states randomly over time, according to provided state transition probabilities. Unavailable storages cannot inject power into or withdraw power from the grid, but they do maintain their energy state of charge -during an outage (minus any carryover losses occuring over time). +during an outage (minus any carryover losses occurring over time). ## Generator-Storages @@ -190,6 +194,50 @@ its state of charge during outages (subject to carryover losses). ``` +## Demand Responses + +Resources that can shift or shed electrical load forward in time but do +not provide an overall net addition of energy into the system (e.g., a dr event programs) +are referred to as **demand responses** in PRAS. Like storages, demand response components are +associated with descriptive name and category metadata. +Each demand response unit has both a load borrow and load payback capacity time series, representing the +device's maximum ability to borrow load from or payback load into the grid +at a given point in time (as with storage capacity, these values may remain +constant over the simulation or may vary to reflect external constraints). + +Demand response units also have a maximum load capacity time series, reflecting the +maximum amount of borrowed load the device can hold at a given point in +time (increasing or decreasing this value will change the duration of time for +which the device could borrow or payback at maximum power). The demand response's +state of load increases with borrowing and decreases with +payback, and must always remain between zero and the maximum load +capacity in that time period. The energy flow relationships between +these capacities are depicted visually in the energy relation diagram. + +Demand response units may incur losses or gains when moving load into or out of the device +(borrow and payback efficiency), or forward in time (borrowed energy interest). +When borrowing load, the effective increase to the load in the demand response +is determined by multiplying the borrow power by the borrow +efficiency. Similarly, when load is paid back from the unit, the effective decrease to the +load borrowed is calculated by dividing the payback power by the payback +efficiency. The available load borrowed in the next time +period is calculated by multiplying the borrowed energy interest by the current load borrowed +and adding to the current load borrowed. The borrowed energy interest may be positive or negative, +indicating a growing or shrinking respectively borrowed load hour to hour. + +Just as with storage, demand responses may be in available or unavailable states, and +move between these states randomly over time, according to provided state +transition probabilities. Unavailable demand responses cannot pay back load into or +withdraw load from the grid, but they do maintain their current borrowed load +during an outage (plus or minus any borrowed energy interest occurring over time). + +Demand response devices also have a maximum amount of time that they can hold borrowed energy. +This cutoff, where borrowed load is unable to be repaid and transitions over to unserved_energy, +is refereed to as the allowable payback period. This parameter can be time varying, and therefore +enable unique tailoring to the real world device being modeled. The allowable payback window is a integer +and follows the timestep units set for the system. If any surplus exists in the region, the demand response +device will attempt to payback any borrowed load, before charging storage. + ## Interfaces **Interfaces** define a potential capability to directly exchange power diff --git a/docs/src/images/resourceparameters.png b/docs/src/images/resourceparameters.png new file mode 100644 index 0000000000000000000000000000000000000000..8cc3e547a5b85b5f49fe71b0c725880840d8888f GIT binary patch literal 93269 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 literal 0 HcmV?d00001 From 5c1b88e521c0b9f4efdb0197fc77bf0b4e45df64 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 7 Oct 2025 13:09:19 -0600 Subject: [PATCH 54/83] initial dr walkthrough addition --- .../examples/demand_response_walkthrough.jl | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 PRAS.jl/examples/demand_response_walkthrough.jl diff --git a/PRAS.jl/examples/demand_response_walkthrough.jl b/PRAS.jl/examples/demand_response_walkthrough.jl new file mode 100644 index 00000000..364dd411 --- /dev/null +++ b/PRAS.jl/examples/demand_response_walkthrough.jl @@ -0,0 +1,154 @@ +# # [Demand Response Walkthrough](@id demand_response_walkthrough) + +# This is a complete example of adding demand response to a system, +# using the [RTS-GMLC](https://github.com/GridMod/RTS-GMLC) system + +# Load the PRAS package and other tools necessary for analyses +using PRAS +using Plots +using DataFrames +using Printf + +# ## [Add Demand Response to the SystemModel] + +# For the purposes of this example, we'll just use the built-in RTS-GMLC model. For +#further information on loading in systems and exploring please see the [PRAS walkthrough](@ref pras_walkthrough). +rts_gmlc_sys = PRAS.rts_gmlc() + +# We can see the system information and make sure we are structuring out demand response to correctly plug in. +rts_gmlc_sys + +# First, we can now define our new demand response component. In accordance with the broader system +# the component will have a simulation length of 8784 timesteps, hourly interval, and MW/MWh units. +# We will then have a single demand response resource of type "DR_TYPE1" with a 50 MW borrow and payback capacity, +# 200 MWh energy capacity, 100% borrow and payback efficiency, 0% borrowed energy interest, 6 hour allowable payback time periods, +# 10% outage probability, and 90% recovery probability. The setup below uses the `fill` function to create matrices +# with the correct dimensions for each of the parameters, which can be extended to multiple demand response resources +# by changing the `number_of_drs` variable and adjusting names and types accordingly. +sim_length = 8784 +number_of_drs = 1 +new_drs = DemandResponses{sim_length,1,Hour,MW,MWh}( + ["DR1"], + ["DR_TYPE1"], + fill(50, number_of_drs, sim_length), #borrow capacity + fill(50, number_of_drs, sim_length), #payback capacity + fill(200, number_of_drs, sim_length), #energy capacity + fill(1.0, number_of_drs, sim_length), # 100% borrow efficiency + fill(1.0, number_of_drs, sim_length), # 100% payback efficiency + fill(0.0, number_of_drs, sim_length), # 0% borrowed energy interest + fill(6, number_of_drs, sim_length), # 6 hour allowable payback time periods + fill(0.1, number_of_drs, sim_length), #10% outage probability + fill(0.9, number_of_drs, sim_length), #90% recovery probability + ) + +# We will also assign the demand response to region "2" of the system. +dr_region_indices = [1:0,1:1,2:0] + +# We also want to increase the load in the system to see the effect of demand response being utilized. +# We can do this by creating a new load matrix that is 25% higher than the original load. +updated_load = Int.(round.(rts_gmlc_sys.regions.load .* 1.25)) + +# We can then defined our new regions with the updated load. + +new_regions = Regions{sim_length,MW}(["1","2","3"],updated_load) + +# Finally, we can create two new system models, one with dr and one without. +modified_rts_with_dr = SystemModel( + new_regions, rts_gmlc_sys.interfaces, + rts_gmlc_sys.generators, rts_gmlc_sys.region_gen_idxs, + rts_gmlc_sys.storages, rts_gmlc_sys.region_stor_idxs, + rts_gmlc_sys.generatorstorages, rts_gmlc_sys.region_genstor_idxs, + new_drs, dr_region_indices, + rts_gmlc_sys.lines, rts_gmlc_sys.interface_line_idxs, + rts_gmlc_sys.timestamps) + +modified_rts_without_dr = SystemModel( + new_regions, rts_gmlc_sys.interfaces, + rts_gmlc_sys.generators, rts_gmlc_sys.region_gen_idxs, + rts_gmlc_sys.storages, rts_gmlc_sys.region_stor_idxs, + rts_gmlc_sys.generatorstorages, rts_gmlc_sys.region_genstor_idxs, + rts_gmlc_sys.lines, rts_gmlc_sys.interface_line_idxs, + rts_gmlc_sys.timestamps) + +# For validation, we can check that one new demand response device is in the system, and the other system has none. +print(modified_rts_with_dr.demandresponses) +print(modified_rts_without_dr.demandresponses) + +# ## [Run a Production Cost Model with and without Demand Response] +# We can now run a production cost model with and without the demand response to see the effect it has on the system. +# We'll query the shortfall attributable to demand response (load that was borrowed and never able to be paid back), and total system shortfall. +simspec = SequentialMonteCarlo(samples=100, seed=112) +resultspecs = (Shortfall(),DemandResponseShortfall(),DemandResponseEnergy()) + + +shortfall_with_dr, dr_shortfall_with_dr,dr_energy_with_dr = assess(modified_rts_with_dr, simspec, resultspecs...) +shortfall_without_dr, dr_shortfall_without_dr,dr_energy_without_dr = assess(modified_rts_without_dr, simspec, resultspecs...) + +#Querying the results, we can see that total system shortfall is lower with demand response, across EUE and LOLE metrics. +print("LOLE Shortfall with DR: ", LOLE(shortfall_with_dr)) +print("LOLE Shortfall without DR: ", LOLE(shortfall_without_dr)) + +print("EUE Shortfall with DR: ", EUE(shortfall_with_dr)) +print("EUE Shortfall without DR: ", EUE(shortfall_without_dr)) + +#We can also collect the same reliability metrics with the demand response shortfall, which is the amount of load that was borrowed and never able to be paid back. +print("LOLE Demand Response Shortfall: ", LOLE(dr_shortfall_with_dr)) +print("EUE Demand Response Shortfall: ", EUE(dr_shortfall_with_dr)) + +#This means over the simulation, load borrowed and unable to be paid back was 80MWh plus or minus 10 MWh. +# Similarly, we have a loss of load expectation from demand response of 0.8 event hours per year. + +# Lets plot the borrowed energy of the demand response device over the simulation. +borrowed_load = [x[1] for x in dr_energy_with_dr["DR1",:]] + +plot(rts_gmlc_sys.timestamps, test, xlabel="Timestamp", ylabel="DR1 Borrowed Load", title="DR1 Borrowed Load vs Time") + +# We can see that the demand response device was utilized during the summer months, however never borrowing up to its full 200MWh capacity. + +# Lets plot the demand response borrowed load across the month and hour of day for greater granularity on when load is being borrowed. +months = month.(rts_gmlc_sys.timestamps) +hours = hour.(rts_gmlc_sys.timestamps) .+ 1 + +heatmap_matrix = zeros(Float64, 24, 12) +for (val, m, h) in zip(borrowed_load, months, hours) + heatmap_matrix[h, m] += val +end + +heatmap( + 1:12, 0:23, heatmap_matrix; + xlabel="Month", ylabel="Hour of Day", title="Total DR1 Borrowed Load (MWh)", + xticks=(1:12, ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]), + colorbar_title="Borrowed Load", color=cgrad([:white, :red], scale = :linear) +) + +# Maximum borrowed load occurs in the late afternoon July month, with a different peaking pattern as greater surplus exists in August, with reduced load constraints. + +#We can also back calculate the borrow energy and payback energy, by calculating timestep to timestep differences. Note, paback energy here will not capture any dr attributable shortfall + +borrow_energy = zeros(Float64, sim_length) +payback_energy = zeros(Float64, sim_length) +borrow_energy = max.(0.0, borrowed_load[2:end] .- borrowed_load[1:end-1]) +payback_energy = max.(0.0, borrowed_load[1:end-1] .- borrowed_load[2:end]) + + +# And then plotting the two heatmaps to identify when key borrowing and payback periods are occuring. +borrow_heatmap = zeros(Float64, 24, 12) +payback_heatmap = zeros(Float64, 24, 12) + +for (b, p, m, h) in zip(borrow_energy, payback_energy, months[2:end], hours[2:end]) + borrow_heatmap[h, m] += b + payback_heatmap[h, m] += p +end + + +p1 = heatmap(1:12, 0:23, borrow_heatmap; xlabel="Month", ylabel="Hour", title="DR1 Borrowed (MW)", + xticks=(1:12, ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]), + colorbar_title="Borrow", color=cgrad([:white, :red])) +p2 = heatmap(1:12, 0:23, payback_heatmap; xlabel="Month", ylabel="Hour", title="DR1 Payback (MW)", + xticks=(1:12, ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]), + colorbar_title="Payback", color=cgrad([:white, :blue])) +display(plot(p1)) +plot(p2) + +# We can see peak borrowing occurs around 4-6pm, shifting earlier in the following month, with payback, +# occurring almost immediately after borrowing, peaking around 7-9pm in July. \ No newline at end of file From 13a5d6a9a08d7f981c8f8c2393479f9efd034877 Mon Sep 17 00:00:00 2001 From: juflorez Date: Thu, 9 Oct 2025 16:30:47 -0600 Subject: [PATCH 55/83] add wheeling cost prevention to dr --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 2422309c..4a47e900 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -144,8 +144,9 @@ struct DispatchProblem max_dischargecost = - min_chargecost + maxdischargetime + 1 #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window + wheeling_cost_prevention = 50 minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) - min_paybackcost_dr = min_chargecost- maxpaybacktime_dr - 50 #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention + min_paybackcost_dr = min_chargecost- maxpaybacktime_dr - wheeling_cost_prevention #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention max_borrowcost_dr = max_dischargecost - min_paybackcost_dr + minpaybacktime_dr + 1 #add max_dischargecost to always have DR devices be borrowed last #for unserved energy From df96b470192d1fe6a6a57777c901518f64f37b23 Mon Sep 17 00:00:00 2001 From: juflorez Date: Fri, 10 Oct 2025 15:23:19 -0600 Subject: [PATCH 56/83] update DR to enable no passage of borrow and payback efficiency --- .../src/Simulations/DispatchProblem.jl | 2 +- PRASCore.jl/src/Systems/TestData.jl | 6 +-- PRASCore.jl/src/Systems/assets.jl | 50 ++++++++++++++----- PRASCore.jl/test/Systems/SystemModel.jl | 5 +- PRASCore.jl/test/Systems/assets.jl | 10 ++-- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 4a47e900..67751c2c 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -470,7 +470,7 @@ function update_problem!( # Update borrowing-make sure no borrowing is allowed if allowable payback period is equal to zero maxborrow = system.demandresponses.allowable_payback_period[i,t] != 0 ? dr_online * system.demandresponses.borrow_capacity[i, t] : 0 borrowefficiency = system.demandresponses.borrow_efficiency[i, t] - energyborrowable = (maxenergy - dr_energy) / borrowefficiency + energyborrowable = (maxenergy - dr_energy) * borrowefficiency borrow_capacity = min( maxborrow, floor(Int, energytopower( energyborrowable, E, L, T, P)) diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index ecc5a0aa..a0757c14 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -294,7 +294,7 @@ emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( dr = DemandResponses{6,1,Hour,MW,MWh}( ["DR1"], ["DemandResponse Category"], fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), - fill(1., 1, 6), fill(1., 1, 6), fill(0.0, 1, 6), + fill(0.0, 1, 6), fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) @@ -337,7 +337,7 @@ emptygenstors = GeneratorStorages{6,1,Hour,MW,MWh}( dr = DemandResponses{6,1,Hour,MW,MWh}( ["DR1"], ["DemandResponse Category"], fill(10, 1, 6), fill(10, 1, 6), fill(10, 1, 6), - fill(1., 1, 6), fill(1., 1, 6), fill(0.0, 1, 6), + fill(0.0, 1, 6), fill(2, 1, 6), fill(0.1, 1, 6), fill(0.9, 1, 6)) @@ -387,8 +387,6 @@ drs = DemandResponses{6,1,Hour,MW,MWh}( [fill(10, 1, 6); # A energy capacity fill(8, 1, 6); # B energy capacity fill(6, 1, 6);], # C energy capacity - fill(1.0, 3, 6), # All regions 100% borrow efficiency - fill(1.0, 3, 6), # All regions 100% payback efficiency fill(0.0, 3, 6), # All regions 0% borrowed energy interest fill(4, 3, 6), # All regions 3 allowable payback time periods [fill(0.1, 1, 6); # A diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 816f054c..8971b7e2 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -527,8 +527,6 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse payback_capacity::Matrix{Int} # power energy_capacity::Matrix{Int} # energy - borrow_efficiency::Matrix{Float64} - payback_efficiency::Matrix{Float64} borrowed_energy_interest::Matrix{Float64} allowable_payback_period::Matrix{Int} @@ -536,15 +534,22 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse λ::Matrix{Float64} μ::Matrix{Float64} + borrow_efficiency::Matrix{Float64} + payback_efficiency::Matrix{Float64} + function DemandResponses{N,L,T,P,E}( names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, borrowcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, - energycapacity::Matrix{Int}, borrowefficiency::Matrix{Float64}, - paybackefficiency::Matrix{Float64}, borrowedenergyinterest::Matrix{Float64}, + energycapacity::Matrix{Int}, borrowedenergyinterest::Matrix{Float64}, allowablepaybackperiod::Matrix{Int}, - λ::Matrix{Float64}, μ::Matrix{Float64} + λ::Matrix{Float64}, μ::Matrix{Float64}, + borrowefficiency::Union{Nothing,Matrix{Float64}}=nothing,paybackefficiency::Union{Nothing,Matrix{Float64}}=nothing ) where {N,L,T,P,E} + borrowefficiency = isnothing(borrowefficiency) ? ones(Float64, size(borrowcapacity)) : borrowefficiency + paybackefficiency = isnothing(paybackefficiency) ? ones(Float64, size(paybackcapacity)) : paybackefficiency + + n_drs = length(names) @assert length(categories) == n_drs @assert allunique(names) @@ -575,12 +580,30 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse new{N,L,T,P,E}(string.(names), string.(categories), borrowcapacity, paybackcapacity, energycapacity, - borrowefficiency, paybackefficiency, borrowedenergyinterest, + borrowedenergyinterest, allowablepaybackperiod, - λ, μ) + λ, μ,borrowefficiency, paybackefficiency,) + end + #second constructor if you pass nothing in for borrow and payback efficiencies + function DemandResponses{N,L,T,P,E}( + names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, + borrowcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, + energycapacity::Matrix{Int}, borrowedenergyinterest::Matrix{Float64}, + allowablepaybackperiod::Matrix{Int}, + λ::Matrix{Float64}, μ::Matrix{Float64} + ) where {N,L,T,P,E} + return DemandResponses{N,L,T,P,E}( + names, categories, + borrowcapacity, paybackcapacity, energycapacity, + borrowedenergyinterest, allowablepaybackperiod, + λ, μ, + nothing, nothing + ) end + + end # Empty DemandResponses constructor @@ -589,8 +612,9 @@ function DemandResponses{N,L,T,P,E}() where {N,L,T,P,E} return DemandResponses{N,L,T,P,E}( String[], String[], Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N),Matrix{Int}(undef, 0, N), - Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), - Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)) + Matrix{Float64}(undef, 0, N), + Matrix{Int}(undef, 0, N),Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N), + Matrix{Float64}(undef, 0, N),Matrix{Float64}(undef, 0, N)) end Base.:(==)(x::T, y::T) where {T <: DemandResponses} = @@ -609,8 +633,8 @@ Base.:(==)(x::T, y::T) where {T <: DemandResponses} = Base.getindex(dr::DR, idxs::AbstractVector{Int}) where {DR <: DemandResponses} = DR(dr.names[idxs], dr.categories[idxs],dr.borrow_capacity[idxs,:], dr.payback_capacity[idxs, :],dr.energy_capacity[idxs, :], - dr.borrow_efficiency[idxs, :], dr.payback_efficiency[idxs, :], - dr.borrowed_energy_interest[idxs, :],dr.allowable_payback_period[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :]) + dr.borrowed_energy_interest[idxs, :],dr.allowable_payback_period[idxs, :],dr.λ[idxs, :], dr.μ[idxs, :], + dr.borrow_efficiency[idxs, :], dr.payback_efficiency[idxs, :]) function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} @@ -660,8 +684,8 @@ function Base.vcat(drs::DemandResponses{N,L,T,P,E}...) where {N, L, T, P, E} end - return DemandResponses{N,L,T,P,E}(names, categories, borrow_capacity, payback_capacity, energy_capacity, borrow_efficiency, payback_efficiency, - borrowed_energy_interest,allowable_payback_period, λ, μ) + return DemandResponses{N,L,T,P,E}(names, categories, borrow_capacity, payback_capacity, energy_capacity, + borrowed_energy_interest,allowable_payback_period, λ, μ, borrow_efficiency, payback_efficiency) end diff --git a/PRASCore.jl/test/Systems/SystemModel.jl b/PRASCore.jl/test/Systems/SystemModel.jl index 707b3f72..0bf5f577 100644 --- a/PRASCore.jl/test/Systems/SystemModel.jl +++ b/PRASCore.jl/test/Systems/SystemModel.jl @@ -20,8 +20,9 @@ demandresponses = DemandResponses{10,1,Hour,MW,MWh}( ["S1", "S2"], ["HVAC", "Industrial"], rand(1:10, 2, 10), rand(1:10, 2, 10), rand(1:10, 2, 10), - fill(0.9, 2, 10), fill(1.0, 2, 10), fill(0.99, 2, 10), - fill(4, 2, 10),fill(0.1, 2, 10), fill(0.5, 2, 10)) + fill(0.99, 2, 10), + fill(4, 2, 10),fill(0.1, 2, 10), fill(0.5, 2, 10), + fill(0.9, 2, 10), fill(1.0, 2, 10)) tz = tz"UTC" timestamps = ZonedDateTime(2020, 1, 1, 0, tz):Hour(1):ZonedDateTime(2020,1,1,9, tz) diff --git a/PRASCore.jl/test/Systems/assets.jl b/PRASCore.jl/test/Systems/assets.jl index 7d90edf0..b0d13954 100644 --- a/PRASCore.jl/test/Systems/assets.jl +++ b/PRASCore.jl/test/Systems/assets.jl @@ -135,23 +135,23 @@ DemandResponses{10,1,Hour,MW,MWh}( names, categories, vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) + vals_float, vals_int, vals_float, vals_float, vals_float, vals_float) @test_throws AssertionError DemandResponses{5,1,Hour,MW,MWh}( names, categories, vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) + vals_float, vals_int, vals_float, vals_float, vals_float, vals_float,) @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( names, categories[1:2], vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) + vals_float, vals_int, vals_float, vals_float,vals_float, vals_float) @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( names[1:2], categories[1:2], vals_int, vals_int, vals_int, - vals_float, vals_float, vals_float, vals_int, vals_float, vals_float) + vals_float, vals_int, vals_float, vals_float,vals_float, vals_float) @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( names, categories, vals_int, vals_int, vals_int, - vals_float, vals_float, -vals_float, vals_int, vals_float, vals_float) + -vals_float, vals_int, vals_float, vals_float,vals_float, vals_float) end From 3216eda5a85c190becd0f26683f6324518c14ed8 Mon Sep 17 00:00:00 2001 From: juflorez Date: Fri, 10 Oct 2025 17:12:07 -0600 Subject: [PATCH 57/83] update DR such that energy above max energy is counted as unserved energy --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 4 +++- PRASCore.jl/src/Simulations/Simulations.jl | 2 +- PRASCore.jl/src/Simulations/recording.jl | 4 ++-- PRASCore.jl/src/Simulations/utils.jl | 8 ++++++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 67751c2c..396daec6 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -543,7 +543,9 @@ function update_state!( if (state.drs_paybackcounter[i] == 0) || (t == N) #if payback window is over or you reach the end of the sim, count the unserved energy in drs_unservedenergy state and reset energy - state.drs_unservedenergy[i] = state.drs_energy[i] + #adding to exisiting unserved energy as we count any unserved energy above soc during borrowed_energy_interest + #we want to keep a running total (will be either zero or the unserved energy above energy capacity) + state.drs_unservedenergy[i] += state.drs_energy[i] state.drs_energy[i] = 0 end end diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index a4e1e4e6..99b70e96 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -220,7 +220,7 @@ function advance!( update_energy!(state.stors_energy, system.storages, t) update_energy!(state.genstors_energy, system.generatorstorages, t) - update_dr_energy!(state.drs_energy, system.demandresponses, t) + update_dr_energy!(state.drs_energy, state.drs_unservedenergy, system.demandresponses, t) update_paybackcounter!(state.drs_paybackcounter,state.drs_energy, system.demandresponses,t) diff --git a/PRASCore.jl/src/Simulations/recording.jl b/PRASCore.jl/src/Simulations/recording.jl index 91466ce1..a156eb32 100644 --- a/PRASCore.jl/src/Simulations/recording.jl +++ b/PRASCore.jl/src/Simulations/recording.jl @@ -22,7 +22,7 @@ function record!( dr_shortfall = 0 for i in dr_idxs - dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 + dr_shortfall += state.drs_unservedenergy[i] end regionshortfall += dr_shortfall isregionshortfall = regionshortfall > 0 @@ -88,7 +88,7 @@ function record!( #getting dr shortfall dr_shortfall = 0 for i in dr_idxs - dr_shortfall += ((state.drs_paybackcounter[i] == 0) || (t == N)) ? state.drs_unservedenergy[i] : 0 + dr_shortfall += state.drs_unservedenergy[i] end acc.shortfall[r, t, sampleid] = diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 07f45a8d..4bd0d753 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -122,6 +122,7 @@ end function update_dr_energy!( drs_energy::Vector{Int}, + drs_unservedenergy::Vector{Int}, drs::AbstractAssets, t::Int ) @@ -129,15 +130,18 @@ function update_dr_energy!( for i in 1:length(drs_energy) soc = drs_energy[i] - efficiency = drs.borrowed_energy_interest[i,t] + 1.0 + borrowed_energy_interest = drs.borrowed_energy_interest[i,t] + 1.0 maxenergy = drs.energy_capacity[i,t] # Decay SoC - soc = round(Int, soc * efficiency) + soc = round(Int, soc * borrowed_energy_interest) + + unserved_energy_above_max_energy = max(0, soc - maxenergy) # Shed SoC above current energy limit drs_energy[i] = min(soc, maxenergy) + drs_unservedenergy[i] = unserved_energy_above_max_energy end end From a74df6b78235d18027a69a4568e198ec88dec730 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 10:36:45 -0600 Subject: [PATCH 58/83] refactor, fix tests for dr efficiency --- PRASCore.jl/src/Systems/assets.jl | 8 ++------ PRASFiles.jl/src/Systems/read.jl | 11 ++++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 8971b7e2..a44bf052 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -543,13 +543,9 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse energycapacity::Matrix{Int}, borrowedenergyinterest::Matrix{Float64}, allowablepaybackperiod::Matrix{Int}, λ::Matrix{Float64}, μ::Matrix{Float64}, - borrowefficiency::Union{Nothing,Matrix{Float64}}=nothing,paybackefficiency::Union{Nothing,Matrix{Float64}}=nothing + borrowefficiency::Matrix{Float64},paybackefficiency::Matrix{Float64} ) where {N,L,T,P,E} - borrowefficiency = isnothing(borrowefficiency) ? ones(Float64, size(borrowcapacity)) : borrowefficiency - paybackefficiency = isnothing(paybackefficiency) ? ones(Float64, size(paybackcapacity)) : paybackefficiency - - n_drs = length(names) @assert length(categories) == n_drs @assert allunique(names) @@ -598,7 +594,7 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse borrowcapacity, paybackcapacity, energycapacity, borrowedenergyinterest, allowablepaybackperiod, λ, μ, - nothing, nothing + ones(Float64, size(borrowcapacity)), ones(Float64, size(paybackcapacity)) ) end diff --git a/PRASFiles.jl/src/Systems/read.jl b/PRASFiles.jl/src/Systems/read.jl index a4cdfa4a..0565966d 100644 --- a/PRASFiles.jl/src/Systems/read.jl +++ b/PRASFiles.jl/src/Systems/read.jl @@ -305,12 +305,12 @@ function systemmodel_0_8(f::File) load_matrix(f["demandresponses/borrowcapacity"], region_order, Int), load_matrix(f["demandresponses/paybackcapacity"], region_order, Int), load_matrix(f["demandresponses/energycapacity"], region_order, Int), - load_matrix(f["demandresponses/borrowefficiency"], region_order, Float64), - load_matrix(f["demandresponses/paybackefficiency"], region_order, Float64), load_matrix(f["demandresponses/borrowedenergyinterest"], region_order, Float64), load_matrix(f["demandresponses/allowablepaybackperiod"], region_order, Int), load_matrix(f["demandresponses/failureprobability"], region_order, Float64), - load_matrix(f["demandresponses/repairprobability"], region_order, Float64) + load_matrix(f["demandresponses/repairprobability"], region_order, Float64), + load_matrix(f["demandresponses/borrowefficiency"], region_order, Float64), + load_matrix(f["demandresponses/paybackefficiency"], region_order, Float64), ) region_dr_idxs = makeidxlist(dr_regions[region_order], n_regions) @@ -319,8 +319,9 @@ function systemmodel_0_8(f::File) demandresponses = DemandResponses{N,L,T,P,E}( String[], String[], zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), - zeros(Int, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N)) + zeros(Float64, 0, N), + zeros(Int, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), + zeros(Float64, 0, N), zeros(Float64, 0, N)) region_dr_idxs = fill(1:0, n_regions) From ea8e089d338163015c130bfa7da05741e7d2a606 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 16:35:56 -0600 Subject: [PATCH 59/83] remove old iszero check for maxpayback --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 396daec6..8283322f 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -450,11 +450,7 @@ function update_problem!( paybackefficiency = system.demandresponses.payback_efficiency[i, t] energy_payback_allowed = dr_energy * paybackefficiency - if iszero(maxpayback) - allowablepayback = N + 1 - else - allowablepayback = state.drs_paybackcounter[i] - end + allowablepayback = state.drs_paybackcounter[i] payback_capacity = min( maxpayback, floor(Int, energytopower( From 81967b44048638bfe7f7c17e51014c4d9ccade63 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 16:41:57 -0600 Subject: [PATCH 60/83] add ispositive check for allowable paybackperiod instead of nonnegative --- PRASCore.jl/src/Systems/SystemModel.jl | 1 + PRASCore.jl/src/Systems/assets.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index b9d26599..65f7c926 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -256,6 +256,7 @@ unitsymbol(::SystemModel{N,L,T,P,E}) where { unitsymbol(T), unitsymbol(P), unitsymbol(E) isnonnegative(x::Real) = x >= 0 +ispositive(x::Real) = x > 0 isfractional(x::Real) = 0 <= x <= 1 get_params(::SystemModel{N,L,T,P,E}) where {N,L,T,P,E} = diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index a44bf052..bb255c81 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -566,7 +566,7 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse @assert all(isfractional, borrowedenergyinterest) @assert size(allowablepaybackperiod) == (n_drs, N) - @assert all(isnonnegative, allowablepaybackperiod) + @assert all(ispositive, allowablepaybackperiod) @assert size(λ) == (n_drs, N) From 7f8624bfaf70ab79b42191396b64a695c34cf2b9 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 16:43:30 -0600 Subject: [PATCH 61/83] single regions constructor comment fix --- PRASCore.jl/src/Systems/collections.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Systems/collections.jl b/PRASCore.jl/src/Systems/collections.jl index 1218fb9c..a8c04e1d 100644 --- a/PRASCore.jl/src/Systems/collections.jl +++ b/PRASCore.jl/src/Systems/collections.jl @@ -32,7 +32,7 @@ struct Regions{N,P<:PowerUnit} end -# Empty Regions constructor +# Single Regions constructor function Regions{N,P}(load::Vector{Int}) where {N,P} return Regions{N,P}(["Region"], reshape(load, 1, :)) end From a001bb08150debac27d53e06cd697dbcb1852b68 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 16:45:11 -0600 Subject: [PATCH 62/83] fix formatter indent --- PRASCore.jl/src/Systems/collections.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Systems/collections.jl b/PRASCore.jl/src/Systems/collections.jl index a8c04e1d..60fe4939 100644 --- a/PRASCore.jl/src/Systems/collections.jl +++ b/PRASCore.jl/src/Systems/collections.jl @@ -37,7 +37,7 @@ function Regions{N,P}(load::Vector{Int}) where {N,P} return Regions{N,P}(["Region"], reshape(load, 1, :)) end - Base.:(==)(x::T, y::T) where {T <: Regions} = +Base.:(==)(x::T, y::T) where {T <: Regions} = x.names == y.names && x.load == y.load From 88a7e081e00b54e81bd561773f55aecd4223a9f4 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 16:55:21 -0600 Subject: [PATCH 63/83] remove zero check conditions for allowablepayback period and setting max and min times --- PRASCore.jl/src/Simulations/utils.jl | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index 4bd0d753..d7711b0f 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -234,18 +234,9 @@ end function minmax_payback_window_dr(system::SystemModel) if length(system.demandresponses) > 0 - if any(iszero, system.demandresponses.allowable_payback_period) - maxpaybacktime_dr = length(system.timestamps) + 1 - else - maxpaybacktime_dr = maximum(system.demandresponses.allowable_payback_period) - end - - if any(iszero, system.demandresponses.payback_capacity) - minpaybacktime_dr = length(system.timestamps) + 1 - else - minpaybacktime_dr = minimum(system.demandresponses.allowable_payback_period) - end - + #no need to check for zero since allowable_payback_period is force to positive + maxpaybacktime_dr = maximum(system.demandresponses.allowable_payback_period) + minpaybacktime_dr = minimum(system.demandresponses.allowable_payback_period) else minpaybacktime_dr = 0 maxpaybacktime_dr = 0 From ca7ff886a1a1a08c492d0fb1f4bdf4e75f8a1df5 Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 17:25:54 -0600 Subject: [PATCH 64/83] restruc costs for no stor/dr overlap --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 8283322f..17725067 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -146,8 +146,8 @@ struct DispatchProblem #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window wheeling_cost_prevention = 50 minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) - min_paybackcost_dr = min_chargecost- maxpaybacktime_dr - wheeling_cost_prevention #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention - max_borrowcost_dr = max_dischargecost - min_paybackcost_dr + minpaybacktime_dr + 1 #add max_dischargecost to always have DR devices be borrowed last + min_paybackcost_dr = min_chargecost - max_dischargecost - maxpaybacktime_dr - wheeling_cost_prevention #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention + max_borrowcost_dr = minpaybacktime_dr - min_paybackcost_dr + 1 #will always be greater than max_dischargecost and paybacktime #for unserved energy shortagepenalty = 10 * (nifaces + max_borrowcost_dr) From 9ee4d4abf498a50f9141d748468db0e6e8a229ca Mon Sep 17 00:00:00 2001 From: juflorez Date: Mon, 13 Oct 2025 22:44:09 -0600 Subject: [PATCH 65/83] misc flow update for dr test --- PRASCore.jl/src/Systems/TestData.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/PRASCore.jl/src/Systems/TestData.jl b/PRASCore.jl/src/Systems/TestData.jl index a0757c14..6b5ba264 100644 --- a/PRASCore.jl/src/Systems/TestData.jl +++ b/PRASCore.jl/src/Systems/TestData.jl @@ -412,22 +412,22 @@ threenode_dr = ZonedDateTime(2018,10,30,0,tz):Hour(1):ZonedDateTime(2018,10,30,5,tz)) threenode_dr_lole = 3.204734 -threenode_dr_lole_r = [2.749467; 2.0656159; 1.5787239] +threenode_dr_lole_r = [2.748671; 2.078730; 1.581805] threenode_dr_lole_t = [0.0817; 0.2169519; 0.47371299; 0.843717; 0.588652; 1.0] -threenode_dr_lole_rt = [0.013778; 0.081317; 0.3802359; 0.3650310; 0.274108; 0.951144] +threenode_dr_lole_rt = [0.01415000; 0.082619; 0.39146799; 0.360588; 0.279216; 0.9506899] threenode_dr_eue = 53.0449649 -threenode_dr_eue_r = [26.54526199; 15.15617299; 11.34353] +threenode_dr_eue_r = [26.40881199; 15.280649; 11.35548099] threenode_dr_eue_t = [0.566655; 1.82072; 5.47627; 9.7399670; 8.7754709; 26.66588199] -threenode_dr_eue_rt = [0.16025; 0.510989; 2.446321; 6.235264; 5.0443439; 12.1480939] +threenode_dr_eue_rt = [0.147045; 0.5105039; 2.39299799; 6.236285; 5.0194399; 12.102539] threenode_dr_esurplus_t = [6.066344; 0.805918; 0.148378; 0.03628699; 0.0952889; 0.10477099] -threenode_dr_esurplus_rt = [2.574232; 0.545672; 0.0; 0.0; 0.0; 0.0] +threenode_dr_esurplus_rt = [2.67869699; 0.5570409; 0.0; 0.0; 0.0; 0.0] -threenode_dr_flow = -1.0383633 -threenode_dr_flow_t = [-1.576781; -2.386248; -0.256078; -0.6453639; -0.728238; -0.6374709] +threenode_dr_flow = -1.0203119 +threenode_dr_flow_t = [-1.4839049; -2.3727269; -0.2560769; -0.6444219; -0.7271149; -0.6376259] threenode_dr_util = 0.23285 -threenode_dr_util_t = [0.116007969;0.12482749;0.1083558;0.106341959;0.108113959; 0.107764689] +threenode_dr_util_t = [0.115079209;0.12469228;0.1083558;0.106332479;0.10810597; 0.10776672] threenode_dr_energy = 57.750022 From 7a9635bf9ee6f2808e494b78c66f6dfbfe761183 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 09:08:29 -0600 Subject: [PATCH 66/83] update demand response walkthrough --- PRAS.jl/examples/demand_response_walkthrough.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PRAS.jl/examples/demand_response_walkthrough.jl b/PRAS.jl/examples/demand_response_walkthrough.jl index 364dd411..95a514ca 100644 --- a/PRAS.jl/examples/demand_response_walkthrough.jl +++ b/PRAS.jl/examples/demand_response_walkthrough.jl @@ -21,7 +21,7 @@ rts_gmlc_sys # First, we can now define our new demand response component. In accordance with the broader system # the component will have a simulation length of 8784 timesteps, hourly interval, and MW/MWh units. # We will then have a single demand response resource of type "DR_TYPE1" with a 50 MW borrow and payback capacity, -# 200 MWh energy capacity, 100% borrow and payback efficiency, 0% borrowed energy interest, 6 hour allowable payback time periods, +# 200 MWh energy capacity, 0% borrowed energy interest, 6 hour allowable payback time periods, # 10% outage probability, and 90% recovery probability. The setup below uses the `fill` function to create matrices # with the correct dimensions for each of the parameters, which can be extended to multiple demand response resources # by changing the `number_of_drs` variable and adjusting names and types accordingly. @@ -33,8 +33,6 @@ new_drs = DemandResponses{sim_length,1,Hour,MW,MWh}( fill(50, number_of_drs, sim_length), #borrow capacity fill(50, number_of_drs, sim_length), #payback capacity fill(200, number_of_drs, sim_length), #energy capacity - fill(1.0, number_of_drs, sim_length), # 100% borrow efficiency - fill(1.0, number_of_drs, sim_length), # 100% payback efficiency fill(0.0, number_of_drs, sim_length), # 0% borrowed energy interest fill(6, number_of_drs, sim_length), # 6 hour allowable payback time periods fill(0.1, number_of_drs, sim_length), #10% outage probability From 3b64610f19eb2c39206639440d8b4989e8009288 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 09:37:36 -0600 Subject: [PATCH 67/83] misc docs update --- PRAS.jl/examples/demand_response_walkthrough.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/PRAS.jl/examples/demand_response_walkthrough.jl b/PRAS.jl/examples/demand_response_walkthrough.jl index 95a514ca..5cc34097 100644 --- a/PRAS.jl/examples/demand_response_walkthrough.jl +++ b/PRAS.jl/examples/demand_response_walkthrough.jl @@ -3,11 +3,12 @@ # This is a complete example of adding demand response to a system, # using the [RTS-GMLC](https://github.com/GridMod/RTS-GMLC) system -# Load the PRAS package and other tools necessary for analyses +# Load the PRAS package and other tools necessary for analysis using PRAS using Plots using DataFrames using Printf +using Dates # ## [Add Demand Response to the SystemModel] @@ -99,7 +100,7 @@ print("EUE Demand Response Shortfall: ", EUE(dr_shortfall_with_dr)) # Lets plot the borrowed energy of the demand response device over the simulation. borrowed_load = [x[1] for x in dr_energy_with_dr["DR1",:]] -plot(rts_gmlc_sys.timestamps, test, xlabel="Timestamp", ylabel="DR1 Borrowed Load", title="DR1 Borrowed Load vs Time") +plot(rts_gmlc_sys.timestamps, borrowed_load, xlabel="Timestamp", ylabel="DR1 Borrowed Load", title="DR1 Borrowed Load vs Time", label="") # We can see that the demand response device was utilized during the summer months, however never borrowing up to its full 200MWh capacity. @@ -121,7 +122,7 @@ heatmap( # Maximum borrowed load occurs in the late afternoon July month, with a different peaking pattern as greater surplus exists in August, with reduced load constraints. -#We can also back calculate the borrow energy and payback energy, by calculating timestep to timestep differences. Note, paback energy here will not capture any dr attributable shortfall +#We can also back calculate the borrow energy and payback energy, by calculating timestep to timestep differences. Note, payback energy here will not capture any dr attributable shortfall borrow_energy = zeros(Float64, sim_length) payback_energy = zeros(Float64, sim_length) @@ -139,7 +140,7 @@ for (b, p, m, h) in zip(borrow_energy, payback_energy, months[2:end], hours[2:en end -p1 = heatmap(1:12, 0:23, borrow_heatmap; xlabel="Month", ylabel="Hour", title="DR1 Borrowed (MW)", +p1 = heatmap(1:12, 0:23, borrow_heatmap; xlabel="Month", ylabel="Hour", title="DR1 Borrow (MW)", xticks=(1:12, ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]), colorbar_title="Borrow", color=cgrad([:white, :red])) p2 = heatmap(1:12, 0:23, payback_heatmap; xlabel="Month", ylabel="Hour", title="DR1 Payback (MW)", From 56c836ea1358f8bd6edc37270b6c49b300e4521d Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:02:46 -0600 Subject: [PATCH 68/83] update tutorial ordering --- docs/make.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 801de9cd..85beb1c9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,7 +18,6 @@ for file in files documenter = true, credit = true) end -examples_nav = fix_suffix.("./examples/" .* files) # Generate the unified documentation makedocs( @@ -33,7 +32,7 @@ makedocs( "Resource Adequacy" => "resourceadequacy.md", "Getting Started" => [ "Installation" => "installation.md", - "Quick start" => "quickstart.md", + "Quick Start" => "quickstart.md", ], "PRAS Components " => [ "System Model Specification" => "PRAS/sysmodelspec.md", @@ -42,7 +41,10 @@ makedocs( "Capacity Credit Calculation" => "PRAS/capacitycredit.md", ], ".pras File Format" => "SystemModel_HDF5_spec.md", - "Tutorials" => examples_nav, + "Tutorials" => [ + "PRAS 101 Walkthrough" => "examples/pras_walkthrough.md", + "Demand Response Walkthrough" => "examples/demand_response_walkthrough.md", + ], "Extending PRAS" => "extending.md", # "Contributing" => "contributing.md", "Changelog" => "changelog.md", From be0329944306f5a141cc336ccbfe4531ed19ab58 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:03:14 -0600 Subject: [PATCH 69/83] misc update for component graphic --- docs/src/images/resourceparameters.png | Bin 93269 -> 68170 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/src/images/resourceparameters.png b/docs/src/images/resourceparameters.png index 8cc3e547a5b85b5f49fe71b0c725880840d8888f..84fb97758acf35f90484bdc5b2665f4b42713641 100644 GIT binary patch literal 68170 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 z z1~Myx5whk%(RRQRYI>Y(isr3}g4d*M3c2$I%7c60V&Qyzw;73@P{N<4@pLX`J*V1> zLwdxi7e{`1%Nq?P&-1aL2Rl-S4E;_O{U&}<(MW&nryH`=)^iq4o%%aMZ|_$_{|FKn zqPk@SNy$U=EuwDU$r(P?LBm7Ko=vN~X*)3qiBVuHweQq7_B2!p(5OJXVrs&VNu~NJLduH=G}LF}nhH{)CZnqUxuh6UYxrd1XH&3;8uWxh`!!>s0?n2O z1(Cc|Z$y=Or`_ez%T!v9rziwP#2p~@(>YEe?va0mDs#^ft7Dvc?EXMPWM^e%^d@$4 zOX!HZjQtym$iRxK7JRS+z%Hd&&uZ3<^nd#SJcR@K4;W^i${lRIVAUq9O+2_5@FXTk-`Sz05MZ1gE|`rG*JCO>ms_`EU9%-a=y5}RzU5fbCD zz2i6l(l|Yt^X}vuF2q)M$-EzMea_`WBXapPncMM-_;c%l*_AW_IBhV z4Dy}vc3RJp`4Wyv?%s=&th}3NGBqUZ{VN0c<-+64%!tMVqA^SU)!FbG?&9|>(}!C9 zVT74{S{9{J8hUdB4lYr<@`7T+m|$YFv(fE|3dGtzQx*XerU%W(Y|olG;v9Py4Xcc5 zaE4dBQNy&S%f9Js#mwSU<95|$Ekwc@3k>Q;vzgDT)a;?scFxeto7i!XShU($J9Kl1 z{HNKsB{?{6)bN#9>z()?w zil0~49Z#rP?wY;psA4%dgSlU5p^o5ysV}6rlMGIKC$4f znLrpC5F3uwdU~ikH__){wm<0Hb~feR&jzDlwpL+!Lk^4tajXK$uT|2Nt^PsGv1#o z*`yW3OZR`0tU{|^$)TK>2!46N8;eFwd%DaW^>Q=iOpz>1L9q zme}VAL3L{)F!m|16B}WY$^oDw5LJc5>ulGvA%&H!b;0S+vrZt z+|W~yCn%QNqzyP8x!LH(Rf{W@RK*A>fG@eE>aP6uz>GL~jZb~?+~5w(Tn0;$rx6g! zn-Ki6cRt;U88M=9I5o4f8|1dl7bv##6yw(t%j=#>r!smj@V;V&(=?JJnpwU?&g0R$ zy~PHdTWkVNX9U<hS~=+W8N)alC7^IzQ(v-8uYN7pZ7^o~TTG zh2>;bR%a7RfskpoTbgrPqY>8@C>b!tb8GaX*}76tz{7g&9EV%SulJ2+=Uzv%C0L#w z6~z@D^^^Z|rqr+gbz-X?9634E={9naL9Mj;vw^yOu0Z8(_+;~eRq<`Be>aV}WiqSX zg}0ZR7@32dRn1D64MoOB)0Dx{Y+6So*GfqS`Iy5-q92e z*L$ut`3F2qdyui!&rR%ng!8tgnb#*r8A zKYw1w;!u!f+Jezs{Z7anGSxo=>R^-br6e?raCqa4@!B(6A66v#kCoZ}N;xi^#iT%J zgt4L5(kDEPrNC$Ok@j35Kol#I^r94kKW7ND^;jG34+zUuHuiL)n<_2G_?oL>ZT8Im z(6uk!lP-s2A8reo1Go@}{kLaQb4|x{-y;zn!(Bp4nplF3u6^NoRDAk-Rjzn3lK2Wq z>~_dmTvcyqsU!EY7Z|j+qjk!Qvuy9X(@p%z8cX?jADZ}!sQ`+_W`<51O z;dq11N5*L>aLQ(5&ts%hP27Ib?c+hzIgr2haUUR#;_c957I?8Gx6b^ot(r=X1gnw* ztl-Mkr5`pOHd%V9W$?$?S{Thog1!PZ!5vNc7*PG>9-rj$vCT?buoRYsYDGNe7o$;Q zCskRcCq!N2_i1T9I;5ahU+L2-2exweIPZlmy5HlWc8@tRY~T6K9T?X7 zrDQ3JH2-l02hhv(>(jo2U<_DbBEuw)MtOOl$oBp0?Y`dB_PHc3dStpVHu49~19gbw z;b1FETroy5JXw*sd-NxH?%jXcl0aevTT-j&Wn~sQ@OPb6s0Tdch{+RLmTRXTz^;gO zh+c5!6z;jvE@l?JC;Fi7Y*6oZVkBrymq!YGgOnAs+piB>tVT&n5f7Y8hALik93?F- zkcQ*%j#M~KDRE63x8eWX;s)6mZy0k~b^YAFa_Yx$%q=W4r>X;Y>z#t&d7Q}mD z{;NanyNCMw#pejqv8tU0TrUs*M#8|rORfllkY{&QRB}cHC-s3q^4hFhYkYl7_i$pN zI8hDMu+`T&=tvVkbZ%pSC=P#*0bZCtEfT`*2IuQ%`NxIMW5Wl_rHG23t4F&K%>;H> zOT2x^BW!f40TQ)s`}^cblFKh=rf1v!jdP0qml3G2+H_8Rr>C`Yt0^iLSdkW$Dz4;M z^0KK)v9~U(hi{ws}qKkF& z3UnX`?gPcd0;RBw1}eQTiheq_aa*>pR`Pz7&zcmCAK%X@Eb4cM7!&N`dGW3?2GfSx zjw^QOd^^S+(-E>Cg$p5mV1PIp?3+HnBB(Oq%>jLderrsnYoh z?2Oo_`m%TVrt@l$zL=(Ltv^2Z&SFMgyWfA#2*x{{PEVxHWJ3E8&ERXU)ha;NE_{g2 z43xP9=gzC*m1}6|^gqtfe6+AFQHrWn6oP9ut-Hv|TlPtyi5gYMo~NL&9^ECWmAR}K z%}bXvKTG#37Mk?Wr^!}qMT=$+S9qciiw8z!FM!}-w$9lk0TH!vl;arE*h^^mJo zi+Au~%iI_Y(iK2e1ftT#8^4KNI@4yf*bElGbQ!S$^|>=i}abj)B0??D`Y2!iBf zGV4b{O7LsSFm0-Wr<_01o51rxkcC(GUvKn|HJ^_qnz4&I+jw#iqT!BM?0h$$#Ny zyueMY89{wdC&v7L!zef;+bacsG2ixw0(pThP`z9B5>n68oE+tT&bst(toK_(hpv5h z`7c1#iSxI7&`MYi4o$HZMwRT=_vvASO7c9yB-Ww9PQXIQ?I%QM>LJgWx;=f+b7xQn z{)q($`@deKpBv^Xt1zD+*I1RWdbw-UcdQ(XvoT^B$c+HSu<#OdSHFBXCJ_vy{wn&2 z+XYGueX1gKlLg?#o{65+nbv6Am3}OZ7}7WOEUOXn?0cPJ3$NJRZt9}?V+RRmXJ0Hi zR8h=vV6ho6gJ$OMKVlPVf7M3pLU@`~ZhD`k1p1AKOX%dml9dYO zxbl6UxTr?6=~D0W+6?kCl&{)_ zMqh>F4_W@j1ACQo6`!6mI@eED@aDDirz`F)n-2`BibY?7}qgj`W8fs>4Az)PjX2Gb$ zIaINI&S!u+_D!fkt{fL`-*PoaIoGobmlKALdmfxTwZ4y(FHs@xqOqv!?qd0)=7@y( zTT7ZzVUoQkT|K-d_R&|qsivr(&6l$WPX%^`c-BV;tpuZAs9=JhS1q1MsL3Oo*_vlKnGg#5skwcrI;c(G`5NgNr8hDPz<$ zX`cWGu{K)UT)2(VkmNb_prD1Te&u#?*IrEY@=pP93Y}|ud&PMf(aJu;>-E;%5hzHM zTUj~X5i!pSCz+^}Bri*@W>gYs5EtAwq8+eQfLE38NmgM-CfS9Kw)8|Q(l=N>-6;j_ z4r)#v5hg4|KLhM-LDAP87il~NEnpWRf=6K=xuw+>@d5WbIx(BznMhodfrZvZALKJ& zj6gpa{xtUv$o7(UZpmA=P`KB6t7K+kfScrr^OvH3FvfA5>U3{uZ8sEAKm%Na4E32D zWO#tv5Hza4s<}T6Sn^t_5})zs!f1&Rzzf(QI4L=XPw@j)2!?MiSy#!ab>09+o_}149dsv;e5~01>|w{s4yJ;JNWN zb56QRU06?TIPDcb4Uk~Mb=oK!bq~;5v7xjNQNd_CFntPAjnuSrQT`m;H|&bB@w*I( zp_hI9!m(>vTd78bl0Pu`#D#Sy?Rgvi+~G6D1`WVKJ(`Q!o@cOp5^L@k3SH}^f?u6K zhY1Phr}7)I1$n6^9S!d1^+$J26#>BWJKCeG`ueUYyIN_+Wn~mu*rEwyEdUsjR5n#6 zl}+Gink=wtA_^$?M?X~2F{zZCG_>nsn=Aq8jZsFL1=Q4xZYQg4R)Mo~A4Fzen|Uw4 z8sC;de_t(w^wr_q2X0D*q`~h11-ZF7(HFez=54l2FDN9&Nij~GjN~}#%WR1O5C<^G z2d9551cbtNU!ONJFoHN}N$UsgH$p}GB+)L1rbAcFTSE*g)dM5cAFbTRjy>Bge-8!{ zJ|oX5zzH zJ+K2jtH_XxZBS7?4G&h$j%JG-YzKtSjELUwlZ89_!`5XD$-#MM`39Yn7hR+w72khQ zdy;{R4(Up%pZ2NtIsd#L8HZ-z*m@GQcZk><7~q z;zSSx0YQ=yL?b_{@484)Ix^BbLzm!TJJjr=yW<#z1#KB=zO*6{>iW6HQS7RRM75 z?u{Vaf5_pPeeRw29!Cg|ItOo-ZXV7=BH5bVhUx(fL|z~SJ!)i1W{vApbB5EJOn67* zh4yX_-0^|PgoLnFAuG4k;%RgZjfues32(K=Nm%x<5VN&CQIFsXf}Ob@KX-_y5^->% zN9c(EnWvJbqX)oLk_r*Yp=ig2col>S=> z0DdynvX2+RmkIt|bEr-K2B6fW!xI|Bqy@VY*FhKHB}-1(tO&{Dt?EB4H}eF0(+13l z2#CZ*65~m}9}3L1y6&8aZRO|l3bQrouV73DR9#NK5gKWGL0Fq`@q`z0qgJx$(BxyO zCe!M?y^(aeedn(K8?d+(_;TfJlmVxo&2RVbw>WO5$nvcGs`Hyy55jNGolm@Tj|z(8 z)Ut3&-wFUx_{-=VyPRvN`d;*#aUQq_TJu~|QV#q`xlLb#0C~tg((Jag2?tx$@JvwR zk`gLhOeA{ZSVCf2z2M^4j2u z5*1rsD+jJPoj`Zw876u`K`ZW~l!QTBXs5~l?YP1XA;9TvO!xn`XJkk5|3w#6E0Y=N z8bttHFb2k3aRQB5EwW^#1G%48z$?X8AT~4r(RH*W1~^o(}d%oh0gP>ckdr_0Aq+$4pAw9|ow~0^2vkT?l3PY*QUFkkwK(`49h(op|vKnlCGvP20Z%6-eDr=js)lfF#BD-)1j|{*0 z-?0hXa01^q(4q$j7{)H&FlM&>@Gid%mel)^v(=P?fU}~=&0Uzd0qdmeBheM+8s)Pn z?=Ol+-S8h%hrpmN_I@?091Q4b_}hpR*pgiQ-Y)#JeXEyfSgz0Ss5+kiVJz+)Epefx zjO=vBi`=N3;O~ihcVL$6{NL&GS3ch3{r_($Mc@AiN-^51`u_{1u(f)Wi^vzin~tU4 zg^s&W(u88%gxGAF1dn{EX+^z){+!(0T4Tm1N^`SHU?bEvEbX7R zx@Ue>qsahuAw(~x$qz7*z|d^^%Ij<1Q_K~Q8{Dh21t|muA|QYVR&nRIogCzkB+v4m zO5IJBWIYKRGwJxs^mEj)8-l zDyN*JI@Y`(H4t1LpZIIiv-S1+ip4Q$6BDm<9Lxp8w1KHG`4W=oN?S>F@p{C_f1}~E zQ$}yxqOs_(ExtA^AK=0nClruUCRQB4*bHjx4D)Lx1aq}>5c`9JMS}kSqANB<*F64D zw8CgToWOoPb;ZRQ;iL=OnFrEdW*X}HfFL5LS4a(WLo3=AuEm>Q-Psw66uc1wGU8L^ z3fmkYWGMy`!gNswy zpDRd2Mz$!{-N{Z)w;o%K>nZbB2Ma7f0hj<|z`Xaq5KRr< z_F5mMLAZ-xZ7#z#S*guNNJ)F2_qYnccQakCpI1dEFMGL~XgO7hFY*Fr8;P~VYn$za z0^X8FaGcIp+nV1J-nuTT$NSHZ@L_ZjiZRj14Bfd|?f>Em`Sqpo&Z?dWBN6-ra4IV^ zFmUOzIJ>iK)*U#9y}-l-JyQz&(RNqc&57fG!h!hUY9jP2^}SA@Hi~9V`k=_8!KsAM z12lt`qAxM@6lZIK#t1)q0Szf?9DzZbeO6s~AzpU_6Q6ga-}_HOJ-F|Ns59n2m2RC_ zX7YG>Cky?vJoWHWvf%Fh&lg@(b6&SNDhSjq95 zx6Fb+Vj2i$4C?x&?SO_{Iym>}?Zdny8%DU3%kSb>L3QJPlimG|x{Rt0U#&6KqgLWS z?1gPr*ONq_jgM+|3)OnvW++^H0d^4|!UX+VZ3i=8;nD>VqKUut^QTP#QyXj)#M~I1 zz>eiR@Jnvn{x-1$g?qNHp}|{73SHbPN>iXKOubaumRftTtn?aLyU?xu1Q*<)<}o?B z)!yg-ZXpby&-E9l)+OzU)n@B~tJuTvD;dtD?-KZ0KEA_Lpx=L_(I+gU+IzYcs1LC> zuda#0*uEi=kV^4Y8czs9)CL$Mzp%ddJvoB%^K<)~M?R>i#h}C`?`_C2OggELMq`Cl zpUtiS*Bucp^qBf?2A_HG%7AO{Wm$3?q)k|}FWx?4nA{TY^1|EyzlHet$B z_i1JGcljo#D#L!eKNf&&!5&{!{4vU_OssV^4#-39>%Ub_`rOd8?(aAWftQQmwxFB! z2;m1@AiaRN_M;)ZGic*5Z`Udh!Q^+(1-$`V2k-^^zaMhh3+Z|8^TT~N^6jHrRze0d z6+A7ckJS`(IqV8`oljUUb94J%(|j@)>8rP&Xx-G3esTB1MjNHxoOAg^N1E?+8QpWU zF7Y%vyr!r~m|OJ7>Aoi}i&7e-99YD04TcsIgBDJe2xj1U1^Is=l~LPRn!z-H9KoV_ zrC;7sr(#Kc)6};oj`$P+rj;mR!RtW*p08~#d^&BELHXRqe>(q@zzj{n3WX_O_C(Wg z8zwC7Ko$J(w$5MszRJBJ6nb-cp9ElvesQmD8ZnE@^BxHi74%ywI*iQKghQE)%yWCp zaFYWFeCVK`0lYm;HG=n5IU#C#`YlqXhm5bC%XscvNhYwQAMQz+bz$LAe5js4Vk@_Y zCa}k2JcD)}HA@LAB^lo{o9g>&GEZ$Sf~9$q%K~@REG@@kb>JSLD*q?x(O`IkuxlaO zg4Pa^BCej$M)^QO!Tt^7XQ$G}B0)-{%Bl+BwH7Nj@TK}ft6o~95x0(ct&EP>c) z;-@pR8i8=>T1lH;$sGW-AU0fa!~*9SEldH>1`-1Dkh9gtbQ#D1AsdX?uTk5vkWDl~ zaOza-b-)Hk7zNW==IgH&bfuY5xItCMg7eF-{~D)L&Vn%VBsjFU9~);NW+9wrnuk3s zHW7;%0uEA3^-*oh4mcJOX>-6VWd6O^B4oz*U+Z5>K$ujy6DPJ;1C(`edSIXu?)ZFZ zi>XXa?<0>3#UhJz=t6`Gz*7iYDB`@Z~Pk@c`a*zkqa1VKs%4*Ku7GTqkD${f2a}(zsjBh4egk9yne#!; z{Ae@4*MRykh2RB&FX@xOY?G%p)sP3a#3aq;b zr2h5W*v4tk(mQ`>+4@=(Vw{&^m@#a^^kidS^{B_TC}`(d#AkBYAh0aJjiPGw@=e^& ziCSC{{L!qiY&ntV$Ji|}yCc6EWIBW0517wR_rrjv;&>h!CqeHVm2_%!A&3CAkO=Ds zk-Y)-K^U$-M|8sqm|O<9{HwD#{-Zx&B@PpDbbX-wrgIS|v1$>!dk}um7FS=KPVYGN zVALO`ja%N9KSM^U9(=c6x}nTgDKGiQ9&kKSW_$tOM7S+#8u3$o{G8dq}oU1P^WqW3UvtucO7`Db`< zHKOibM)|YQW~1Es(5kU)+O=;R`u?s|@Tuc)1I%L&1Dd-E?z%bGNlcK>#AxZDdqB#C zto7PG74zXE6-z{bEdIT0RsU}HO%vEu>QCne$k)wQ!w)g zATR(JQN*X>7rheHV}0GS_RKQAgu28R&Qsd+x>ZG69s(mayY(76Da^y}nPMcQlN&8; zopMEDK;|xd*a(e|4(P)%WfZevH6Q>q*a8rgwcp$9Uzv0E%u!wEaaL+U5cB)0u727{z04^3>Va87sq%;lTdJHv~Q3_CsE_-a+8=#>e= z6A)l979JMToS3}yy)+E4LU^JA9@lgK$BsY$z!M7)UNgYUtyPwq_TOmP$PPACPA3Q# z5t|oEg>llfTE(XBhv$$vpt@aw5%{3^IU}_yLkYuZB!wf7K$G2m$f)~(KsJ2LcsVtB z6H<-FqGQO!CZMMzdCI>YLkSPKAXFy%kHT|3M073jiDQz-4811OW_;1(7aD#(BHSHQw6dgd#QbeN(N}YUXgg1X~JRm{iSX@ z>$|mBdXQf7Mv8l7)_*c48R?7MXVvY#V>EUObc^%N+w9C@KBW?SpCcbXG*L-Lu|>IM zrJib*lKf}XXNv0Q+mZV^oaw;-^}G~I`W8dwhEZ44ye(X^|7)iAUNvu>2x$T6fBPi} zi5KPoI>8j1RtZInUu%x9o6lqfBJ8%AZ)zmafFfj7#KS;lRIa5#DFuVhif)e>k=)5R zBViID}rNOxSP=d1PfKFf(-EJ6*P z3+YU25mB(zROOcEZz0$69|pVx<&kL%}~t(x!^Y`Z96f5OdE zU2vB1<77KA7KcNChbyzwcxN<5GJ1KJ%nFqlA~jrbIZ1QXmIZ)o5WR#92m zlwrc`kX7e>dIH+6yEgW7;Hkl>L)!@4{s{Yne?SjEwn- zo|C^Pl)3jynt2(JW1Oc9WRMXc{LRkNN&Xt{l+C&g|4+{LdohEh6de?69Z$}VO(!}` z5Qm+*L^hUsu;v;W1b8=yRn7O2MkjBO;AFnAe<(IzN1|6`kj$Nb{VG(hr`2CQc|qrfFlhR!Ts33pp>A+{tr6=(45AWd}MQ$;jtE&g|= z58V+!FL1|3QP0evR}i~d*c{2!p~E^$t@DG59v*4Zp7kNzpr;qJ|Ihc+cjWBhT3>qM zyTDpvUg=W)(o4R*KHKF^6$0lENx;K?ICs(wGl_g#tJ0PF3T7v;dKS%V&3WvpnsIIoE!whPtl$q8cAp5k#0# ziPPsfs39`Ehi_wfU7ny^-|-edLc^ls4;j;TZa|YqJ^XVb$?)&uS*A!*^!~C2Owz$X zV?1L1b2#fu4~&u^bg!-{{MpJpa$&Q(I4++G>Uy0wql&Xc1P)F3>lj5{?&`gZZ}i!_ zeM$W8Hhg8JF$TyHjAQKxtZ)sc0%&F>jvdS??xyc_&ad5Qs8Ui zuNJ_*$>S>tzuq%uH>`#fK-$MbP|maIJEKF+(iyOjMIAJ5g`BD9HX=kC!?9Q>(#E+JUJsU>u^ zYDp%g;0+xf9P=$BA!$NoL{z4-X}~(DB_L`)-2w;Tnh%$WYzdwUUf!x_A(5$)MzvPR z`{8J?F0bX|0akRTCYz;a-zDHnlHUskeC?Aq;0oZxZ;T!Z2w$^alSs{a!6l`7-MVz$ z(LSy?c}f_!X6VzCyPkeyzteVAacVRSvWjCJZ!D$VEdMZ%!Lh(zpN`4tZJ~@d=1fJD zvOl{7WuZ^R`BRJbv%CiSBV70l7oUL;$1BETb;+)m} z)c=x;DB!yMTH)eZS}|xpA+MD|1fLC?pAwN%d9gLG<;;0u6bZM>%dm2It=#ILdktBa zq=4fFhN*7<`lUcdZxJ9g0ZYq^D4O93ydAse{L%4O0jN`#29mm?cT32p;E2#9sSS@t zpI_bU>)93dy>gZEfT7nJem-X+mv*gZXZyz!grlmf(N%8g+LMh!_($Y%*__iT>f>Gg z*1^KtsH2wkaS*&5yiv z!u=aH*tE{!+{@twM+KOueSOKI)tJ=RPksBsN0Ww1q}PEa4gi2D4Z}32G@Z{oO$pU$ zu`utKxMnIyM_O%OyOI7uHVwDc;$+QnTgUN%A54db^i;YCE{cJUfNg*SofD3V+1yJ% zLX32qla-EtJp6ghg9<_}2n>ke6j8k2ZMS*7A~T={wmmz|X&)B)WV^J(VlydqKTh{F z3**TN@wUPfJR18DPSwDk7}3Gke>ez0&O?DIY|5%|i|cMlS7LAK>S{ zqgs@iL{h6*p+m4MTgM8sD|w;%H<9u=o!Y~n)4*fE*+H~p34mb9)aVs658u&-JRG9) zN{d(Nte(9uZr_6*Csxs91hykI_CtlR3oV1;h)U$BG30RwWzPRoSQCWVBc#@vL+bC6X1(7oo7qK+>hxw{jK*V z6nwGapbA>s9M(hUf+V7!GF@P->LwCqgfx#rqa1Q6dYG2}pxqqw;szT*c2)GvSE8Fp zP+vqppHu{zS|-zM11XO)bkVNiRF&A4^j~b=ZcZalw#NL{7&Lznja!i*&h1b}4g08P&EYzXj^YI0E>&NUd1EoHGJaaLDH z$`kaVRgLNqs9AKk)k_)Ny!GhC4QP+5AbbIk2Dha0%`d<^0FZ^0O&Ait0n|8wV<#V4 z6ZB-r=zNA28B)jpV1j_^7TG4&{ElmFS!ziG zDqU?lKgSJkT1)4T5TREYWy+x)#B`8!WRg5iWEZ1^(+S<&YWA@}1qFWC?BsN8+t{$K zoteG)cJ`8waMDe8ED-C1%a#wb-&(I|v-#8Og4{d0V9wRZp**l3F-AA6^eQHmFwoJc z0V~Q?;5nDqjMD_K7u-z8O}N6QmwDKd>L74A_RJ|;i?HnF>@gWW36B(U!HJJZV019E zkqvuy`KW(+c?3!#f{-GR!iw&4A__WOucNGTL9+bT9`TA$0C^iocY=5|FLs6FheHM; zCtf$Jw9Hj$fVT`Gi;&5}zfr9XtJ6u*Y4aIQpE9wsO$}5WRSV(4Vvr!Mt;`wvQ^ZfF zjNh0PhYgI0lcgz(q~AG8n2=U0p4Uu>JKF)h4-@$AiJwtbT8T@ zX~4IH1HZnCL_k7DZB9TxRaDugf{6|`oUgudSpW%O14{Fpu4$Yjc~|dV^}>ytZTnjc zZ`~s27e9;g9*Jpsn8TNH#o$%zcfm;)Ym>ZaE*i%<8ne^lj^lPpV9XGK*Bj9FQ_F&HaCF&lz5Dh;JHMto3kI+6m6Ba z8DnRQrbB0J#t~7d!C{Ym`4wv&NKqAcE)~`yq??^wK(WkauS|3j@naWtR=^Cp`nKK9 z1`mLosa(ib@#r$L*0>1o7qTULn;a###OeN)%%H%xx}*#a`MQ3)-1Wy!(m3abemucy zlAOoq!teZaDzT)wzFAb9X1PR!f%iyAD>cS`_S-;Bf&b~9Hdl?c+275P&d9x=a+hjC z|KS1g%6E(XT$FLb);yW<8s|qzaBnjPK^}46g{*8|PxS`_BOuddvts{$U zN9F65t66NQH*E!OD{ms!k!*~32B?=(`rR4vDUA*VCEW6^Y!fsOGzCZFZ8gna3UBkx z@gh}q-!#c^!=TgB04tyEg#zjwCidP~&Rf^)x_<+TCrQ7rL<4rD#ZOR@bR;cjbFNK@ z&iwcCP}u&QbB?C?Z4}-NVAH#@@(=sl+ZW-INj76}#Ony~?Ra)}-9wcW8JDE+r{Gv( zr$x>k=*5%AyR7_ir?m+=Rq|WNjWP_;xXh3^aQP_69-{14cxv;c*FZG;>gwmpzm}MW zENZ!$-iBz;)XuE{;|P7UVr6x~KX;T65#Yw-;r!#Z49Ob+^-^%D?t87q<^adguR9z# z)J=Hm8j0~Fzru%k9qWAw3X7?(+n6AbA>Oc7zcV+GW$cD91D$}~a{S)4?0(+mkf!?h zil$XE!EHm$XZd9h$B6lFGdayWZ3BYa`KjK1Y*cE=l^lN&ewd))iH`Kt-X4xK=~AMb zdj0aoz4I0D4wcjVk#GsA&n~!Tjf1+4Z!UBH7Zo=rUVv-nKs9j(L&OC=mD92v|85xgy}N9!ZlW#4N2(|Q%=KypC*O!n+PoY^M%~I?tf^ZY-`?qIer(B z`-n3~;Xf(E3d>JdQxl6Lb?wDB4rK^TQfo*oP@#uP0CF>%XWtBTe0bGBbB_K-M24k9 zms22!(hs`>!<6A#%S}wbsmxJ+yHuD&;o7zpqj3 z8ydvBNhj@n(_THd<_MM6{q^tJ-_jnV(H9Fbw~)f5$FgynnnXUh9Km%+oizvR`rY3Af*i(QTmFbuqohLUP)B~P_EqS$ z|D_fukz>%QE^I}*N1Ig{gAInf*5S@b`JQU0gbuFAk1Y>x2;VR>0~wFI90vepE!p;Qp{q(b zUt(707od}0CDTNAUHN_Sgrl2q%bRl9YQvXD&5q~yD^by=y^naEE`D!b)LFI3v?9zi z>($~(4U(24BXX-L7UomEjk!Tuf=^m6N0b&pAC!3GuGfa5nKu1(pY;jIqh1xI*Jf z(sNi)1a)+v^ogVAzJ23dl5NJT8cda7`x%;?wZ8Ln$HAUnCcXnK@D2{bhO$)DWuBa4l6+vC*_KhIG8P*rFRIW#?bvWHkhGW9!`e0d0}(5KpMMt^au zsA*tCwF&$=gTJFK{EvuMcgiUwzmg&RMcE?w9ZwZxOx9L>z1l?2ow>clasQU7dXLs0Cu0`- z_Hf$2`*E1MuJzS?*Grq*E^qEz**VrNbT~wmABWaxmDLK4161uZe!pNE80%s+-sB=*{lq_#=Xb}%@;F9t_yHLmel6f>|%2dj{*0>d& z2~gjx^;1W^y!2ppVu|??k#1sz07ldcwx3(5(P5(W*{rg<>q)iPT@>nSbXcj6J9E}6 z$gv>`M3}Y8V?C5(3(t;j!AYe!m9BhtlGE6s5wp{f?=M8aBX84jI>LH>buO(+e>I7@1+*Hp0Sbnb&m3O9SX=W!L2GWsl zqm7*Ic9WyD!XUa;=92>RUx0q|#+{uMyTw{0*%DSN=o=T|)1tH2{BLY;>wR%>t1c)0 z2fK&oC4uSzUh0>U_MiaR3iVRsq`hw1Iv1%i8&lcez6O%&dIr}RQ3O=l%0hnEyOFsy z``EFEK9a24$vO!tN2lG4YxRqI)HM7)o;s;G{;Ixq%!|UTT@(mX^)0j57SDwBr8wF6 z*kCwi^TrIWE-xu&>Qm5$DbTo zb1Xv{6j`*_Y#P$sk6{NpJh#mEg9IYci%9hzgf5=ZpWNNz=<)v5VIjq6ugd>vf^o&c>x+_W+gn}HgLTE*t%iqr!(cVHw zg~%YJrpgGw_vzoe)Kn{LbUO0I(TswhlgBszQ^x5W7Fv=1PjMyt<#Nyu_!^NdM4MCe zg&kkkcndFK-a5e||4Gi+$D`b{r@sQjvz3Yli70Z~iBM{#u$+NCZY9wJT}L>Z6DKUr zp;@!%azm+_srX&>uNLw*KFV(xQ9lo{#H14OUN%DVToI`6zmc`B#&uEP&B^ZW-><|X z$bn&0(W7|kCjWutOAtcM%+J;#aFhM`dMI`&Xi<r4SH}%H+T^2(dyu zOne`aSxA1bFD+eY)!2TdUYUB2jstE0nu@hSKgAI?vd=G8p<_Z>zHZv&I z1lftn9ZzGX6%N!sobf&Z!@!_E;#y|qX-m{Q1x-_Eaoc zT*ZbI*pcZr;sfvHlT3)HH_B629=WtJLutSg`}8(m&+d;TvkS)yV^S7Cm)NB4HJyM2LoXcN6$1+LV&3oBs!+ z*7w4pz9Hr9_)qM+a*R5X(Ary6h}VMx0&=Q$-f0)F!M{|Os{{LEm{G1Mo-vGiRuF`E zF`ZbNu&2Gv!V-`(P)RTWK!l{AwQuqcpgH42Jk(|LcNB2P0YG8+9(ma7|LO&z#ks56 za{tQ9_CZ}vv`v{(ekC>(Cl*A`UYoS9QFnM=@ku#6a!T^s@?+p-klgflbT3RGG7jtf z+%Jb#Y;_>Dj|qJ`7`+;rRN9!4udIp`n=DmknWo!(OpC~F%qbAnTuT8`pOns+;#(aLpSqH zSQkw@=gIcRcSozJ98lGl&t&>3A)_5^<)w&EIWsp{rSmNeI|^*QG`6*+cb$fg{0Jck z$Eh8*82dit%RhalsFM2LsvtTDLxJ22%>05!(YRz56TrqWoZNy(bBouZi-{iNit~tk z`FJG^L*G38ZS5QzBoqW>os{lAGzn4gy3BoAg?=V-=0bK3c%FcyZ_9ng+a}HynVzW{ z2$$Mq*z@D!c_{m791B;+?YpC< zjA?uiWrHI%X6k6rKIkZ@HhJ$GF2bK6{cLVQS>Vf%`v>1D7j7S!%H7qilg8WpHRB9u zTHAY?oLSA_{z2oy<7zw|@WMa@@e`!a+@X&eES)qiEl#Ll#i8Q()$&yD;QlcxZ;tpV zol-`lXUDwWg8>4ltS{+-`>f&X8rr{}`tFUv!ev@3=>uWq*tjG{zK#22==s2bKga;o z;LKvL8IRHU=lbf45NVM|i-yjw-nR<&VA8vU@u4Y z_cP^n#`^%aVO-1m^$jCU&bys#Exv4&OV`0B`{I^fa{{)FO@&5Ue3au1POqmWeSTmf zUnvJe4QIBFNyeG*E%)*JuPz*u#GOI%{!wy@h`nRHgr<*$P?SXc7>`N$Z`V(D?RT#C zvgg>M3bJk6mlWUwbUfa(Y#@vynER78mMIP*k!Q6#8*?bXWm;gKme7$1YdDTuAHP3x zxjHhc*dBtal85BQv0>_^(k=XgR2Knv>l zm^oa<<3s$3DRlKWn>~eL?yCMZ2SF`?FJzs!4WD9np3GeKk=wVc$h6OPA%6d7cywUv ztMcL|2Y?tS$X$7mO?SPn@o@qWyk;83^Gi~S(a5afc@Q{-l&TJr^Z+HM$G@Z zfsAe@wl0~QZ2xu^)R0ttF~Dj9mIXRhU))c;3jWH0CigxG5T`QpO4*T~n6NVmvti?{ zlvU7)6J9`B16{_*2jRFCd(A$u_bxyabR)HB(iJ|uNfsK#D1PQp@>qxF;UV_;coln{nn_#3pOXp4%f->1oLjo zYQ5V_@9G%Mrmp+E@?(_mY}D@4dweG_RqF%k<|YD z%W=b#PWRCdr^?~Z0J_^GpJb3aJ6}*={`CI@zj(GMQIGF39V3xwcdIKOJ@Md1OLb;p zobuHFwhbk#Eop1Myll~XHoHZSy$rA2IFKM1Wk()`Z9UCGvwjTa!TMIu*b(#M%L@~$ zmWn;D|E3P9{!JXm8Cy^w(}H4Lj*bWuwVz0s7qbM1)6yeOv-tJ;|@?6xX!A)*Tex-#W;#&+H;)&dNNN_F94K-8yW^2fP>*Q7uvdnRM1J?! zrue-(=3N;ub*LhDoz2=*r9i0fy}xVs-KJSzeP5?CC=2{Kp=KiHS;z-uV_%iP$7ibt zhxGhn()uGTNA&D#dCQFH4NNwgKOSWC@A`4AnJzM};LT)9bup9{0z8Nb6n&1jeQvtu zVBKhX?ti+FqT`#n9+YlB`MC=Z3E^rm$l3&IdD(6*@RDXFS6grexSYtvfJz zXn*Jy8F?G((qM|-#>gXE=?+0FLY7zV+^{#_xa=h6ZbRzH7Rp^mtgX*qNEy$i$^40T z^0IF9gAU_WU$;|2yM<=09{p^RHEj0|ao=KP_q~ePd>|!3*Ea?+PY{rie2y* zU$Jd0YPe$Kx*@U*dj$Dopyc>2J#XZp*HPa4F{&$zZ3hLWOJ*fM^%0%C3SHdO?ZwV) zCVFzXmbdNs>Qmh|2;1@&KgE-aEmF!2W>YUZ@06veYCq3!gFS&ZY((b?<*JcM(D{gB zQw{}>q;0nH`y_y0b}->Gc0~^}vCgUwREOxl(H360uBI0?V+RX7K&TIb9l#tn3r$~W zIRjynaTn793rlQ z$*;8!QgX+g-!$rVz$NvLKoO{DylzT+-_>VSc%e=%L7&cf)2}aPVb3t zxB$-&-uJ}*atf!-MATAMX6_dYY3(@Du+FkWlq%qN@Ymn8e?cc~I;aL}1C$l85D*<_ zRP$u;jC8r23NKyQ#jf64{Bv%w#dGXZfFcrQh71h z8T+=C&r^r<>x(!H#&aK_MY+q}X;kTEp|@NHb@Lts4k!~8v_%b zrFsPaQ#aj7G8yDmy*&U}yry*r`U0Ap`js$$vVZoWZJaE$qD*`$KV!esl8iD|_>cr4 zcS>&^q?cc-E6~i5SWg%uO9xKpP-kj8lCwWHeBsIRR?Cq z{XW%E8%Y7nAv0^s?hCoTPr=Z@5?JPU2wRp3xE1k$V!4}fe~7$pu!i;Ao$5h+=9V-+ z{21&|L{id}TlFa71Vu)@=#*;j)R1yIYH^>2Uw`?)?T4+jFzXTfbYXgN$0!|^U(Tdi zKg{*wG$=A3NP`(zxXUs`#b867Lp$NJT5#^k?r3R6p#^WPDxhU5o|omWJpJ(Dl*D|W z{}T0AmrThR=Fd|UFAFz%2z&69ojSfrhBhH>-H770*Dq}^$#ygime3!~=(x}R#Q{G@ z@R&fAl+rl@sMSp(MQhSsU5goM91gMe0JAjoN{zRWm5))`kKS}c1oa0M6`?a{>7bdY z{(D6thC)~T2>Qhl+lTU^}m$o=)`8ACq@-YIX8Nt*NlvZ~^oe>-R{IlVV zjxcV&4Z>1tJDzNx^gfxWr&4USi-{rqmzKrFEq8@k4wlB=@^ifMS~S0F)JS4K(gKA3 z7crK@>}RkxEn+;9zWtU&%e}WdRT|HM95tvUbTB^W#I5!}zI(`%9Zo#0O6-$Jg)SEM zK}4ZL`Dh$X$*T{lD>A=S0Wm$#u8Zh-@?un_xcFr(s}G<{c*O{ybw{piRd5z) z5?HlJM1iGu>b?ITs*gH&DPq=&dd^V4P?T)NGZYRbFkNf-Iqhz5p6U=BZv6O8i#wBh z?JYug7m?CFr{-wq0T?b zOG{`t>#dHvxM|D{i=C;Mws$;@{4OKsb^YyUlI9D(s7K@qL!VflyNKgHYNm2O!f|`p zt#v;?yk!o%>`@V;=#1ZUi7&6wIfBWa^wAf$$9-lJmyG`Y^j4I-hi6AZQ-W*f^(g#e zw~4~RnvTki+oOWa-zqECS1#qi1C0nCr9$^`Z#OeA!s8)>-9U&5DgYh@c^{+dN&Q)H z@KEp^wa<5EknevVr5u0%_Rr5Y|9|)yx$Jx*>_FtYKmL2&$e>cmnU6lu*aXBqHm@KE z2v|Ha#v=T85NgzIsQ-_jsl=tFrR~RyuvDAA_>FKG;inXzyQ&r`{h>Hp;U^ui9}KP$SB56BKOz*QU?y4zo6e~|R%#x02n&6os&mGl+T zE@sBG%$9#IgqO8<(c-aZHI^S5NlK`^ddCanNCQf4w1LTj2n>l@C6*$g9uv3H zQ}i^C^H2drX=w+8|K7!yQ}Ajl!-F}?w)Otc?%Wg)D_!S!7QB^MT#p&v)!f2d|0?1l z^4QSnM1p`5Ho_()3fU=)X*Uczpm5A^D z!@wpsAmDYwf5W?I>2}#BTiIlzOCq$Lf#8@Y!=zE*M019lhzK(fjsEZ3ni1D2lt^|w zie>K9?8wXEtV0c^a?E^w&A@?c=)^hmi;FQ$=6^2zjm;tsrLbgV9v&hAw7Xiuu&vTuuQ^Ui*|EO6J9> zqpo{q9=Xvnn%#{>`)98yr7fQj+2Cw)V&5kC5R{Fn?W0BR_X^9i4<;fw;0fFBv>tV~ zS8ZWVscS zy%ial&B;ONRQTY)z3`gRz@>zT}hPvsNyyS(5{!$MZh3@~_JjVtv^SyB< z#Z`3`nk>wdqA`fg;OQ*1b;I*l(dW-~YzOnh-c(Gov0)`z4H7lpSo zKPO4!|0?pbz^IP1HU3#c(T=;u-1-~$Cfp!S)4EArhyb&1b2Q6ItW+<=_eBT25Z>Et zuj*mSx^~An(&$swZraQtoJF#Y{J_QRxX=q$UWze8`V)>8$#H_JkOG7rca z9rcAi4qps~N#KR;0$e8Pqg+IY=7sSQ^|Nl<2Iz)xvR6-6W z7-(LiCftmsg{1x~N-+tv8ir3xJE+|?&l!Jt2b6~0EMsqaFNQC*r7%lYsB6ilv5JlA zpVMqk70SW1ea(OsifRyW^P8;rBEm^%+2&U(MpVlF->6GI7kbial80ZqNtmIqI=T2! z20|sga#B-0nciRcoY_Y1{Z7M;7AGe-U2fxabB9N!S9vjYv+8D4RjFKGB5C2jVECRC zCwHi$%u((lw&+J-U!6?1&_!hSB`4K+y^*EF3&C-Bu z^LREvYsU+Rf$f~3;??C}+{s_RM;<+$zJndgBtv7SJ|DPa=)Q=tcpT;jCn9^OSaIy$ z``KzH0mh zI>u38_GA@HO5SBn`l02$8c%VpwjF0H=b-6e|8L?qOWk#sX}Ys5;Y-<(v^V!q+1J-k zBIXWiezbsm=i}7kjBB7&?&?ai?67e70VZz&e`(xZrN+0>Z@#tFZGN%g*Pb5QD$t*v z8-X2u&!jUeh)2mj%}5)zxRSK2%cyQF_9%9v13yWyGKtz~Gm0=GTw9%aI_DGF!5Rmb zq3W;a_Qj4;u?&($;S$tA1ks&~SDdby;&~8zQl8V^xa>@+HZPz^yUER{R1|cI#B!5M z7lUP88;@a9i5wRg1qEg~1<38^!?})+7SxI?D{DTnNn1%bufFe=z99}5-ag*-{kTeb zJt8I^Q);j>SwLFak`U57Dy_rjTb&EfI?dZqxYF&4Ufn*XR|4RiiG|ThX=(9hYC13R4oQ$b`~{f2qp1u*vS3a z;oYn4UySS}isO;*acFmWJp8&&6i$-V_m$o`q5D(rm?de7*Zr@qiCq$^(967&gGQUIfEmU?3 z+OKy-Q}H}gI9X_ZW?jWbw$o>OqS-1!`Q^u-P1xh8sj=fLxytU$1Q@8?wWLSmykS#8 zyT2YXn6iC+#`yrhIG{PL{b5#}CNhAA8}*F6ROU5C)M(Knyf?T>0tC&&^xOX&k-jDW zAoHy0s2e-GDCVGlu@W-Sj|q?L&gk_{x_Ux?z(l#F?J*`tzfwAxiApW-P-@jTFg=@1 zv|}t5HfQQfY+5|jcnVC{>IOIWuhvW1w3yfg6_jkbKRdwO18BHROiYds8&X4Gkb+xW zy0o|TI>U^2d5VHEMb=X!Z@1~@t3A-wLXE~{ksV)X60xacbXKc$Ej7|(5BZc?^`(J} zI)^peVeAr*><_U)a(ki=%$@!(ep^5)uwFk`PaufXdW(k&;qOw#-u?xDR(bBx++bwPje61Nlg(O> zemt+)%$kc0mn+?&1V}N7&hgv z;uM7|pGIT`mRjArtV&x-bPy$tyQDRT%m`-oeYX|o&sc? zdgFWV+8sM8_Ifwk5dzliA7Vjq+l)5+mtGt1o2iU$j^_dL^5!fOQ@r-<8bD@gu_&K! zE-Qvqg2!fRzrTvSs}Zdo_^4pklBNVtSna%*+ZYcU`t{B?N0(9CeLGviZ@1>20QkU| zXn1hQ9E?ux$*1BhNrPS^LBo@s(ONlqzTp=a5cc`Z5#Rp%5};kp(udUmBBe$5-q3j6 z+NI5TeBX5OuB2#^*=2y|ggmY$4S4CfW3?>jPb16-<%BdNb_S%V`!AZX6=H1!WII6O z7|EYzEf17Jc_rM|Zb?naAK#~nd9C{l?+mcI)MJlIN` zoY7O6*Qz(O)cxlN>7l@|Xe{pF_q*F$IYlu51B8S0skrE3a&cwd^h#fj#9l{$o5-|{ zbcY_RGRDM51ExYnZ@WpXmdMXkF-^TW7NfTBYJ^HV?WCmhWxplx8k;vuphqh|D#%N$ z-ZP71DfO5=?i7Etu{6VNjouv?#f!#s9T&U^|D9JLSWaip-NT7BwtmeZIJmm zdOD9JnehjW|91>pnex~=oZHerk+qO)nmG7yvs^mP4ZDQiF5 zkZ5sKK;n$lc~N&3o{ZdcN>qdUQbrcrV_f3@%Ey$}+bRi{0sYNbtI||!@+*X118n@| z!-HyUr)NW*AuD!;My|8d`=q0NRiR6~_&kPZR~e9Ti!qCm_{VEN64{YCCBul4xu#*& zdW2y$9ctwf{(EskE_5wibc;V#;i=Q=-Mm%lqWx7q9d=6~Lqze7L zs8^BNYfZy>Ku(}pIpacox!zy&D)wQ&dgT1C>W~I^sU#W2$2BLF#$RWMOamIGUt5tK zD8)T(prc`$Ty%Qt0CT zpe^jG4A_$|+lV`h2;oPfM@83b6s|y zILkU?O8DI)y~gv9$FDQ8lWbMlItaZBdfb;q_@&pp56*QjDYp#N2TO5;z38(d9qpt$ z%Ncs9rbA|(d>1wN-6~aKHT#L(^6*rq}3*)?j-X=+LRL$~Tr#wenaB%G;4EoM`EetUtyiXwXqZi6%3uQIS<)@*fz*bjR49m3$hi z#F*I&DTit6kOfP`D4uKR2w!mg zwqUX`yf$RBZP!4p+P-}EY81A}C+E8Bs?nsV4w@6viBm@&0-d3cl>F3R$wq-0IqiQ( z(YWUSLqxygcZ~U8eySWUr1ui(>egcWB|Tm6(v{7ryDZIccJTT<8IwPgDu%WNpo_b1v?2;W2swUO6}) z4_>-S^Ek42mKz-hOKDo6qYUH*i{t{89)$FpYIIc>ogOd?xE!Je)?Jl)I0`C#!lb49 zxFOC4y}B(u7-e##UTN*n|AEe2w{uS9^ToYCT}?#`>3Kd6S2Yx*q43bXU7n$=Zhw`{ zDf-BP+ODdQT=vd$DW22~_n<@ZOFudTq&)hHlVDvcQg^IdhAuDEsapg$#xUrfnlD^1 z%43hfxT~Ldpwa2^okJ+?TQm`dZbasgP<{1CWQiXV2P-Eco^A~1=}KL%YpKN_Q5{R# z^MX3^m$_b%esh_z`INn8TE)z*N9!)iWA6h3v|?|*=Twp&j4W&Fua_DS@X-!?Y+QeI zIli;$$1im0K2aW&w0-E)>tX5^*inb6k5n>QLziu0^vl_KSeUl)jr!-N-XsRCkqJ{6 zu8-I_yNuGbR5Y1rT40Dr+|{QsBOf<93X;mz5q%le zBv6N^l`io6+0t*Wl|OGGNwDfMQ+h||^1g=y45is?U92*F92DmEnG@GkRPJK+w;t!DW;#bHL9Zzf? zn2)SlUQD?dOvzt*_VdfwO-eu*4X=6H=M=L6KI;^+r}INWX9A4wfRFY30%8g?z7<(P*kPK_qFR-RS|xRN_7{;!7>U2hFh3W$f1F0=hx{2rp~pglBjaWR-SY~uy&e@1(p+S?Qw33vYzu@Na>{Kk76^7s9vLxE2&HHQyx^Pl9}!bShc^pwwq z@Oe{e`RPPk3VusRqY7*5bvv255TBn3hQ*LH`a3ez?ruE7-$sh;ZoI}TMN(8XEh$WQ zXWnrrQyXu4`IDg)lpgEt2y>ZDj>iCy>msOOB_R1n0eFnmVdd~PC+=+E`D&{ zD+{;oN?|@R`!fbf9ftS%A(QrXRm)Viz2G57i%@|OKr7BC118(98O@o~TiP+HpNcD< zR9ni6c05_8Km|hS>?y&q?bn5H5!7C*IeOT}wFdmf*TPQa3ZDBO;SArOe5II4slAd@ zbjnlERZH1(s;u#_db5>ZrD^)6rid6EMjLmJxqS~*H*eMDFujf~*Dw~Q8vqiwf{GB_ z7X3=N?&o+2zW*a5gBg~3#Zgb(?;d6k?tzodo7)O@WOi`* zJ~yO&nzNH&d~%;1-<0H{S89gMMEA*4_V2uW&MPD^{{CRT59`zTe%#DqBv4CXFc~x4 zMqi1oGY_(EbR{sFDKwvNs9WDGJJY2xvjM6+eaP`_Q>BHkUmuv7|B+3&o_59+@y^hr zDKN8Hu`Co^%^J!qCpk6BM#LEOeSxht3S+WGNan2rmq}zgo^KA!K z2|-+oPuH-VyBd?ml+eTmVLNgQ2r7uyEhs2Y>&uF1+$|$6HAY#XZM-hN4HZeLmqbEV z@gtU`^c&qp%xP0+>kR ziO!rmu?pl*wdWDe?~i&y=&r}Xdg5?zw1*!J*R^Qi=4QGzG5(+_Be3f!>N-~Bl= z_Q9!vie4^XNvs#neStM^UP?iTiB?Ji2)He*W($uofc=0+9B5By(X3rxL9&oOJ)=hI zYfwT6A?bYz|Fk!jEpwycju3V9lEU1=o5_8luUQ=4P*D2DK2hL$jDL5+GBNf1G0LmV z`0`?8d_kq$gIgIU3~uwcG!n-im~Xbl3gUxC(EsWTVG2vr@%|MIlphGU{zsn+w(ovK zNGYUrt+T&0m&`8N^A!(78S&v<7wg1MiRb4w$p2?Xl#q3C{8gYtb$Mh;%{#VUt|T1u zqoc+!+f?6IYbMmKPruw?O~w4 zs3p4TVS;FoGIYa1$)RSn7$z80w)BBHeV8YN`X}HQ25so;y3U2cqN_o6^X)JX{cl{d zI}Q~Sa^HhEmd#m>19Tz}NdOAV>+eEjv)%*wS8#`<}2r-5$HY|znq&=A7l=yk2Yv$ zir_YRT|{~1-ab~Va?7kxsFfcK2g+v%xD4a@JH7-TE`fxFNOFmXL?%{Z*G2I+2b3;e zb;iEa6KLxw>+k|Cx06oIlGRGC{k6^F-R{Dw_9)KM3l3~7HnGG)7E zfylE3qvC=XS!IKJ9~G4S1>~3z3ibqEKs2u5t2-v9|-7sK++Y|(4o^t9VJ~099giZpplx}U(pC=iH9Rv zXO0DE^IJU^yn~7*&Bh^#;AKni)Q#shl&}{Nz0PG6-U<|$3kjE{&6M7h}r+gLu zwp^+sY4$28c_%vZ?wt`_JI#jyv8Cw!bN5(W+=W10bPh9Yqy3Ai19#@!LqGHyxIg6$ zk;;@D-CEvy&5xUV@No-u%GdsE;vpm46XzayU7>j%_)A44$fWN*0M0Q;me()ws<9dOg16 zu1r1q#YqeNtqS=oS2pXCcQ*MDDRF7QKb7Z+TA3LwL@h>G9ApXO9Ijo(V5P>KK(BlI z`o?WNM2_)jFLe{s{M=TM6JBmF(t<}0#|A`dg6x{`jfCncO0xulA^zG`!I$YJhBRkv zf+cN5uB9`s{PQ${U1&(8d|Tquyl}|5L%d*i>_J!%9Aw%4u;fdIJ1okjs-HExr~L)# zBX_Q2;D~nRA+oB%Y|9HX<`H)+LaMiz&8u{O5i}DkWe?3*jGfxvo>UN%TxZiLO1#wB z3id!3aZ!{j>Fi8dA(d;bc#IedMAmw1`S+@tsl8B7_~9s_P;raWm3C3h z3B1^wW%J~R9-KbqoQk?}N6ytd;Vx&cP$}!+1srTA+AYvYEp30;wgDOx4^mC!!pB$q zk$wr+W~cPgQLEH?gjuQ%%dQR7H6%HSc?J(Hp3E=BVKsDneiFb(*xIz^j@No>W<_$qoYJ^YwgcYwxoLT(Ry4auFQ7J)(jrGh;sS;~N_C}JLLd1A^=zSP{j(>KCJAeaAl2B0eh|N0)T&7T)4c}H zKjLap!z~MX@Q2ZaJT+`bV^HwdM?5U5Zme2XNX3!i zSIbzbDTFEj9`s+%1vD&NF(}I9d-=ZJhZ%}H zTjPA8u^(eeFwHzU&6a1cf(!(v2=5;WD%3jIa!LV$#G9OncCqr*B zV;85Nn3TylfPQIBjNsuj4^pSRC@b+ztu5VRmPBkbPf8T=h5v0{Q%>y6f1sNc-aI>v z)49fc$5_34m{WaIGQYyg{R*~H1TJr|cWfqzq2iDdrVoy;f(r4lVEmd%$Au&K01;kq zNTc+S25hJ{IahL7tJw*#`!w1{-$pYz`RDHCaP*M^n}R|O299q(mg-b%i8k4G;@zc9 zJ<{ckxX3*rg3);`iNHqbC;y@;)ge9jk2Eas6T}JS9NmFxXcd_5OWcCEiG0#v!*vhKck7ys5k30U|aa zd2QOL#o9k6ZcCB9si7O*kjF|LgqK~y+$clqz-1gvo_l)nT&lDlve)XrGeW<6U|4GQOZ5E`+m;JtQZukbu7CsVuu3dfJcrJz{}<&!zG&-vMh$-oBB8^%nNR zJI~^5iFUiV8E07#BTw9xT%R+}s1_ZPRT|iYwtL3~5f9bt-1JnQTp0&)5|Wp=!xpBB z!TkIc9G)kqb(&ILM7}eFY5#&`BSnba0oW6f_3>J0Pbck`;W7e5`!-(fFk!D~=&?Pn z8+7{YDRV#`UQiU1V$m|w)p9J*%^KM=lOCN+Hng5jwTszA-+g26bdDvK*dA)K} z0lvzkhGd>kqZc#=D{L~RT-_WE1Vv9gE+T1AbCtrd&)?AC7Yp7B9~y8B(*-uh8^iPR z9G30sM!yJ2$iwNn)*8-sgqxSh*?`BGoY*Gzm@vN?^6K_p@?=hh%>Aox0xl{vG-UL| z4sD1w`?k!=H9CE({Uut}|7CaQCZ>blX%`8|%=Z&yptj$~dqo30KP9mKC?CErojc_# zyD=4lb9^D|F8pV9Gq-Kxs0?(HU7_@ml!~VPLJq;vfa(vm4JDNVJFHh~CME?BnV6Hd zsET(5SCZFk{Ju9KzT_7yU&^MGm9fi5v`wiSJTw3|`ZPvV&$2QE zi3G^PR3&6o=CX^fc-WG6vW5O6ifn)=^{`T~+fhy;U^s{{p8%4|*uv#>w>8O%Fy7Vk z^w7H#C{>4bLi@yHN#nq+FQ2%;jCg(*XK*XA`^K&xUm06&a96s~4qFiFwSBFyW1$~s zDszXi2X1fnr1&7zu$rCetLL=R4<|&Eog{0!BCO~$Z#}z8nC8t_6}(V6fr>O0tTWy=RhDY@*>EmkErhYAUWxj>QpX40sP27A0w}#!&rkGi;zo!} zQT;Rfvm#4j9cM495fRH1KLOi^k92a*vhjE_CoA)-Yt(_*wP^ZZq``kxW#v}oev$X3 z=v|h%wH1cuGp?b($*(rHkIn~Fe{(C0s4B?Xn}4NC@vmy_{H!Y(BvVJoBI^(Ip1h{D zS&~!O>|;H6I$5lB+iU9XC%qArB;(jH|{tLdtxJ;$CT@9)Q|;|S9FO!oe`-okz#DO9+j ziUnaFG2Pl!eU}@yU0b4w5E?+`w!q=*91*jO-3UEZq43P!F+B}BWjw2Lrv;UKhSX&| zQb@k&PHrh^f>DJLJ4{*2HQ!?jh_b4QWon-!yeuymbO1uR=^O2Xx7zmC4K}e+bppEt zbwXRLi1w9`+YiL2h0uv8h;HQvN3zoSp^uMco3+I4Yas&>f>WyUrz>PZfOYC3!0Y$} z1qDT}-+{duM#zU=lsgNJ@B~rT@=qX5DsXpF6lzlCg{E?P5EGR%29PzBCk$9#W+mq* z{HrJZe&Z9Ke0U?Oz~1>3%z|BqNbp%%s0jL%r-?de~!GwgnKX+D3L zLPw$%p^V+NKHQv0((PI?nxD!7(Rya_-due%wk;TbXmSz7^Il~MDZf2!7>iwkfRZ%+ z@6kgxLdOf~D~%w7Nba_#EEg=QV>P*d_gvyBeVHni3D`col#?hgy!d_6>yi13KX=0A z=P#7w;R;Ob{3WM{9X?>XE+R7j~eFQciZn_@6WcY4H!B(j1qDM@1z;UO;XB z&LRs@$RYjfC}|I|YhJ#nzA00|Y}|2Q_=Od4-#3|wki zpjg284n~Mqt>Xc3pukyns@;#p*q?ea6YH&X?(N_9nnq_?7MCYgKcV$;TNafmT9$fB zbCd!KN0sU#{3^G>>lzM+oJikAsnozl9OPi@@3--J_O6Wz<}Y`642kM>XV#TywG}}n ztyX06!RQk!V&rpdqnjG5FOt`rgh~%~Db}i~>>5e6ms)ic;Hj7Qj|5Q-O9jnsuID$` ze;I(_F_k0{eyx0wWny@Tri||Vf*}^$D|zM->!%@yc1mKpw8V}6_wIo!{sY~e#e2{tNr*D&B>ux!k?^<3RHWO?|rCc-wlEE-|Knu`>~Qc8F+1J&+={h}-7 zV$amUNB1NxzAHP{cFSwvj>~9POMf z99=LR5BHR{c+7n0Dp!b$LWBk2&ZXf~18_Icoqa?RFG;BXWR%##+9{p*eG4(lKc?WN z$W4qw@N@hFPwz1{*`ky9H;QL(a_0P~6A;8Grcppp$9#s~W*pTE)!n6S*& zaL7WyvC}#vy8l&owpF`2sS%Vgi8m#9r^L4+2zwsdSR$&F6Tvqb)AB8FX-ekx0RMOc zu|Nid%SDRCs9DLFS$O(hVYIH8(r!8KU!V!iS@_6>pE=8Zf$ohTs9Jcpqc5$WYV>wEz2?JXF z0&BKT+)j;u*{mPqc`f5{(hl;XH=R6jX1iTMT$Sk`_B+QYePPdMAIP7w`cvOksDQ}D z<|1XOu=}k4H?bHEoktzKej3~Y3mtql=i|7D2?OFXVo^b~78Ig;+9>2QOHIvL+HIa& z9$Fn9>M~ZR8#@yx9ok|gjo!gU?4t?$WuMlsEF|j*1AISW|KnIwx^hJ@eijfsVmg3O zfq?=iBqjudgc>(gd!3$_W!u2FF1mz-H^|%zyPk!BGls<}ddA}WMI`3@ey+kF42wP& zk^&x836Jqz&Lq#JivuI>h122sW7_6uYlsgz$%g;O7203N1>nv@;3|q9{5P<<*u}Dn z+PL3Bu)a5c|MLg>(ks6l*ilm!TQ-8h^I-m($qG*Lj93C+Y|RGZs{!QjZ4e)Y2txuM zOR|V{2l;Y~DZiI={A76z941la)UOCiVBdnm`Pg%cLvVHp5;izB2oSfGk=x{z?X$*& z!4YH{50+*wZ#$+?i4e6uhn%>=-p47waZ6Iv(_62E_0aU5L+E;5?6tI`i5lP79QG#`B1YM}m-|-JB+6wYB!jQS7`P%Q;UJy;UyXyF}DB3%X zh6iy;jj=hMgWghqN%g=qMZyV$#gpjYC~=A5D-FjrYIoNo+uGR*Gg*>3M8iNRZUUFn zoWb|+?by~;i`)QIV8x8;_fMIAt?+52jL5_KBH;mLuu|k1&c?#KDzuMoflSO6D-Tm7`eaVHeX1FclsHQmFeOZULN%yZr=@6q_)An;GZ)X$U%^vgxj%G=ITovHSU zT+xYSS*VSL^QL$v;9a4NcwVQV%hFyaNhtIOK+X2Ub)!qKm*oA-mC|kW2J6 zYpiw*xTXy{zr{QZ-s8i}Aqe>*A|Glsl>t!As30d$f~s^*J@h@~BqTrwzSs|b@W&S9 z*v$d!TqWHFRiR8Xi7)eIj+5qK7B^H~%IL426$cPOV^6>G3Rhz{E@1i5p6BHw4kk6W zB=$}Uj*jOMZ&JY7uSJD8inllz@iszjjO9Nt{kh1(E5mGlsk@_H34A}Xy!J#$8^bSR zKgNw8DnW5~l;jI}Q5@{9U&{+ZNv+K3AsLPYzX=jY#8sAVoTi+syDps?*T< zd4@pWD!K{UM~EC?-XC&qexR7}SCOL%17nPn+sX%&c%i_#r|zf15+-J#=B{Z=UpJAu zquKBj4(?-aQ$Zm==9vv)4}XgD#C)yM>>SMayFIaSw;P2XI?J6^vKB<_a$ei?-`Zff zW^;ZsvSZYiS&02t)*yKPP;tUvw* z<-hF|{Gjyy4(GFK@+W-6lj!x{#g-YUkY3#K`83(B$_X>-8OfFX9VIgP|v#xXFE zuv3=M02d-z=dlptcI7sfSYV+$dRAk0m&0^CCMn{@J=tj1eT_JM1jWtz;#-z(EKsux z>)Jak{j;(PX+IN2YF)Mu1GT)HFelT#KJmL?&M*DSD%#b)g2>dJ(zcKWvxeU({gq;) z<*5h9IotVRDr27(jSZ>XvV4CN&KeDJnCD!T$Nh(pOsA|V^3(*$W)eG0KhJu*TVoa1k@;$S?1tEu_oXz(gVg2lfo606H+f4_alglAjB1U8xiK#QsfZ&?SGRiZGE=$TxNTMgt(>^R=_6 z8P|T2Acug<3LF}CY;bfFu_JX4vY}HBdRChxtrWUB7{^`7$ypGS_t$Tpin?Eecgy< zY;X;St^VSo)ZE6ktM!0OPm6WpmMO8CNN{R!;RG1+wtTZ3OkBVJPD4lLpN@kveQ_nk zCKj^n!Ve8@NOx76ijZjKJ=xE;U3R$(Yhc{|z0l;Ul*A#r?sRe;|E~WgmSk1S?)&;# zg2kb|53RZckIO3lnldH_bWYUQ&M|>^EA~9cQ|3bC&&AmJOvpB#ZkW%(4b<)aD0z+>k9Ar>e6vKj{cH`A|pdH*q$cXa-uI z>I1b)ASqg(9bhJ4^95EP6p2<5K+_0H9NJQ{fBbnG!dJd|tmhA`IVdP7uYWumZ0j=U zAZ>M&i=tb`+yPRXFm1n&{S(m6RjZ6TS$0!HTLhA~*S+}ao>dEG4z36lTn5fVv0O9+ zDi!FczR#2OuMuPG?r$Kn1+W&TEs9y*$4@E3KeUgV*Uh@T4 znU%}s)u-mcuA9f(O%(#!M8?+M%0g&c!pH0)=E~*SNuOcDu$K5|*mx-QaY_*A_Xr<9 z4{dx08#_J-Y9Z)CY99@Qi-3mSx}JWfEat+DUnq@vFOqvj%@Twb3Y+^OG(Q?M(Os4iZ+yQZ+RbuOMw}x^epMYjxjzx9qc`%oQ4f1nEFu9`-{oW{X z>i+Kw4@b%j3XH*K#BHh!9Vv-$4*_V;f4wq9P(Kw8CNua2UFW^^`X~z8;teM^)7%Kq z#AAl&fE>A*#9RdGe!mHYVdb&lb4HTp7gc_sr=MY-{o4=<0kRxOJn2DkAo;-%dm0_b z+lTmN#Wu-7Ov)oCx4MK76$m#2Rz2vklU{oi+TKf=&RNE*(p2Qiivnd`dMGj2*={K4 z|8!aju>FtI>fQY$@nBJh0K}#Q?YRvPFa9;m#ag*vSU)j_2KaXi`;LP*f%}w1d;}ZZ zDCos~B+{ihd~8Q(RLpon`{(6%drNNA1$y?rpas64k-}4iM5PysZvJr?i9{%f2T#}# zw>LsJ4D+8~VIqr(x33&PDoIaeH$MQ*d1Yl*AyA>B)$);At)KV+H7G4nT+$6D)P97AcAgQ&ux%C#bn8&=#5O2F%uczLe?7bkH5vc6D zX8eaLY-Yon{(m=giBO`@f5(S{RdI%ts0X2iqpIT@sV$zH-y)Vx^Z9riA-sgKpEj8V zwfGAvw2V_(0@?ZbtU>!*-C>tf8O>^vgi4CfM-qg6j`08yKhnLoKLiD?10b)UC$#v` zG#>;o1O4-w3I%a!URTqNg2PqiS=GC<|bN%eO&2!wUeRNp_KQ4L=&_E$@ zP#EGeZ(kd(6pxzb4`EOUQ!@fU> zPDKp=|Jbgw>oVbxOWuANqYrj^ymhvP)X?P&EvtJZA5c5K%qc5<+8)Yq0zfIs!Q#%a zyGh2*2QtOt+Z<%FTj#`~KLqiz%=DrfN;VpVQv#LW-M)^*Nr$rlvn^vd-62vN-h6e~ zH~H$(7a}Zik}L7x+yPB~QpNfNY;C&b-a#8gi>2=0uO=Erl44dS9`j?ZE+RLbcmlWV zKrORe(4jL+&Iel&h#(epT${KuFw=&hVL?lFs?jL5H8=thvmf(#0AUFkD2w0{#c@_# zb`863mfQ=YdOumXvy$=#HCV(cIR1dT&Gk&WQ%c)F^@-LTNP^$>*WZ@6D7($L(W@IS zQ^jCtp50MQAPC~Iqe4tz|ClR?vrOtR`z`4>HuHF;e}<#c|8u_qOo!3rfS~AM?H-B-Cmf_^Td43)FerO4({`yhBW84 zsT|hEKs&Rb^>u6>Vym}4gk+=9Z0Oo3gUL-sFyA3JsTdrOcx?)J)YAm%4H+>r=@NlePI&r@(b7~WRbvJ8ALA+YYu&pmoEsI?%{KN z?eo`R5>Q7Io6$NG0qX!MgT|evijdQCG(4hrJ8uq{@Rxjwy(%_l=J@_sRxigBP_Rbx| z?*`9-dw2Nl1Y{BUh?-b87}Njq)z2@|bg&IKDqv8&X379@*kS3OAddR`YKpMJyC2G- z?`Hd1RM>}&ecY>y7mbjfbTAoa50%k?emQlw93bJGyIPmsbuc+c`-qiYp8w%Z4qsPisB~r#F6Nx>0{n^WeQXvrw{S>jgzKV zeccfq8F~s`;AsCjZ|~)CauNGKWq5VXeyg_GPy^!9=n?+3t6<_7$ z^nWHF!~c!ZR=@Ke*l<{179{$YqoG}zdPI@9Rg*+WNmTCMKNLKdoYzT*+yKX#cIY~G zLI(XC&MhqZh9aA}Qk2Yxk7qL;1;)yN4f5%#?C|joRu`NBxdTMJ%__{{4T^_7`APwZZ!+ylIh^kOonZ4iO0@1r!Ns z5Tr#Eq+7bAL`nf^k?wHQDIg-LQX(ZGB`wl$W})x@|2yZ4b6w}VzJ0waFKpIc>simt z+;h*&y^;?CjMR&e9=5?Gq?qdb(5K(|?U+t)mt^=bn~>yx_i}Fi;Ti0LD;;0yp^G;V z+Lw&IoKhxx1nPxX&!OpdvQ>wP&@k79bhvy?n1psUHXf_90tsy#LHQLr@eELTtM4i} zd)m!?t908qvycMS@4T7=ow3lqhv?~mxJ!H9)95SVsvYk6%k+!&itjD&Z`Y|L`GZ2t zo?CdzyXUPI>9y~e7o@Z`kghmTL)<8ZPWKsF6H_t>O?t z?}jD)6F;(bf1%cw2G=xO#S!L#BM~JTM&a{9=J8s~1^UkBYnT@u^Z_$3;?u=b{8I%o>kMrwo6bI(H#({+ z)ifBeCRuM;w&d<3ngRPCEfYQ*Qmt0t*_m&!zic>CZ8mo&QnTiI=>v)Ex%e0ZF6gLI zH?cz3s`MbbBK_d&_kSKme9GPuCqWR_sjrkBj?jEbt)8?kKc*@MpbQCL*Cm4%A~u(= zN8ayw(eKK%w8hN|*#^XA>7Sn+gEJ zgJu!C{|n{L|Ire6R9CH^0)hI|q#Ov)B>y2-TJN~x^|+(aXGm{m@swE5~fa|+e3$dXjvxU z6LucSWFa+lxXED{!I>WBMy`7p7Nf~^ng1yBbqOEAVM-zMM7r0zlW`Hvw9{sI^@g}< zQIaPzAp9^}SO5)W8_nHx|2ZVH@q*sa8_NI^Y|_w{yj`2>RbhWsK*4v%3k!!c zi#r`q7vz7^c%`xqJv;_0O;u2b4;=q<35EsU_j_a>dSwLz$VA2=iZ#~sB*TBt2OKyQ zi^vU2{gqE>`Gbu0-B$eA#`<0KMpfRQm$#8QAThV69hYYgjNf?iu7Xm= znY4|3`pk_UH#^1&HlJs-9{BlOcV-LEA_<^d!0D_)$-&ztB4+?N82V_%l6#=z;dm17 zjlqL(K|KkXv{L!@qN>N88Mr)s$m!m&Ssmm~{er2PBAM~XqGU}R6Q(EZ=8n>74@1G{ z?CpKo>6dML1656jq%Axvr?WBjFIHh%0VA|v&u<1UuCfOeJAn+mH~iv$wB&`LU*?cwq) z!N&GY3{$UGnQtzeN%0k~%AIqDPj*P!1aB@bjTx)e=nfrr&98QDHaEvWECNZb^p%mD zWlhvkqoA`DU2fT&CjrfhZ|?R4exS`^Uta-~kCb{V zkHx1vPFoDVWA2UJ?&uH}`3XO2Z!*%rCe3u58)=~3FQy4|gnq?Xqnny(j+Z1R!6@I* zRB>S%z7kE_V^{{9duDzBataXe+wi!Hj6c1(TT2FY#&bDKa%XSk;h1}3$+oZ?2yji+ z2t8B@02wUr$)sWiTwjD9!vrR;-2~MD;G+cCw!}(t&P!23Zf;{ z#p7a{#xUUGobVecx1baCQQ-!X-7JO0T=Qw=Ihqh+qsvBz{_ox?4z6BkRtA}%+sQUp z)GP0Mh~45LvO;0F*?i^hHn^^`Eqxj~24emnwlhh8jxOQ=qyU{w7+~py$MROT1O}$rq^KQGz!C=N9}|{wD$<*VtH8 zdQnzq5w*qRP8MkS8Ti&x)Ci%vKZjU)Z{EkI+?P^_@IO7uB3bAX71^;y!DRLyDMU90 zRu1O!IwN7KLu$5u`;0(>Dg@eT#do3~ehAK{5gm01eZP{u+%EE)5vJ(U6@QU@{rC-{ z`!lnSI%P&Bd&ZH#a#u!a$b$JJ{ye}IiUU(b$U6Au1{c5bFysIF=n!avhs{7x9P6P9 zW&)lPl3zF`X$DCUOu9g?sT3e$Y9i?8INmXck@FT+Bd-3~eOj9C) zrg+~yDB`Zf;3~_~!>ABS3Q{&;*M5klV1W!IX`($IgtqTJ_ErBUwsxFr)nT0GOSy6F zwu$DdArX@LiXuEMc`7n(3X)bR6l9W`PmD02VLi-?6~gVCH3C=jZl9~X z-cy;o?0GsOOFLvc75Pc_i2yD8i7y^&^7@&_&7pH9li$maz$? zx0D3_uW~rVx*UR{? z?5rAAg1mUhNB6V?aoK6l+4?>c#*>q_@ljT|Pi+_SFLUf_nDOB+u{8k|3@j1KAXjA2 z2Jt-rK`lur!}| z-K0OOHz(A4D*6*f!~?4?QYlsC&=qv%+9?sy~cYcOKMvOrYmL#O$_=%Hui6SB9SEk!&ofmLXKRl2Lx* z(c4g^^#PGZ!E-Rwig=2<9>40&-=v$wf}9_j9{zKj>?_;|sZYYG!m1P6O8s=WGXog& zCa;KA3#l}xi_rGp{HNu9AWT66ae|ELf^XG!(u0ZT7I2Mw6$91uxx8X;N25D;ba%^P zUQo?SzP`8JUFdKBA0{dtrpJ6=vuwX{8G|bZlBET-HJfINV5fitQF{1V5F}O2Wmc4M z4?d0O!+SObw9LAbpY{8>N|C3qTTCId$;ebjpR{k^BCp7^Piw<@jKT=n>)L}1Vecgl zKurSCJ;QzV(ftttsBVO}B5V#w$bS58iU!*CUC=%3ti|mG1$bfDI!9IUP9uLy0`Vw#ifp_&Y7D_1u zJXm2(F>JjkTZv}oj(W)FTQ|Gf`*mkj|G*fI&BG)kKQ=KkWJ2Xs$OOpHmqp!iR_JEB zF*xvNzT43HgaxrU*Iv~B>@f-|N5O}V&hF7bIYAr*tjdXC`LY!vWUd@3P31WtwFfei zde*f9MU`8KNG(_}1T}3?Uo@kE`IdeUY{2L*)3^j+wq`4^gUmE6Dl*HfHT;Z#d+Llw zD&j3}LHNw&<&yz*ML2<0NAK%@VCa4Zrg8YztRN8q7jFVGxuY+1BG1=<6GFzo-~gKm z@R!5<)$hTPd(LRb|4(D27co$SS?AmH#1OXx9Fpb}Z`NF66^PygBjQch-vqFQe;s)7 zFAp9#vz>?2HY%AqB@KPS`EnuFR;ltxOBm3jsQ!dP{s<6ygdmN4>+p-)-}rA9bEszw z144-UCCr7NgHp-c+Fk#g-u}cKByT{a-=*OmmO(~bSxfFV?H{T%V4z?!7doZAVY7p_ zz@;V^kOu7AlJLG`m^uSm$AAkzl_27omqWJ3X@RhmLdqmNemf8EE5iUW)KH{xd{9#4 zjF(UD|6eJ=JwarIb;j!M8yi5qd(~Q?913s%LHwIrH}RfgUd-Zp=#FXjJ+7;T94ZWQ z#<;$o+`zV}(JMk4{ zYjrEWv(H1J4zlC1QjMZvj{#wbgN*A#f#PZ7+k$hnuYQ$F-m|Tj3TpYF2`7CG?KFFTBE~Iep znsR%JPxsbj1s=r=8Yk0p9-zG{489*WaDlcblHI!JO+;kYE)W9G*);X4dZkNbHy$qQ z+4(k(Q>O6Yy-?cN3tG@uoMT0c0wp?V&5=q=429vKBPbF>LZ=g7kF)m;JkNVjOW1S0 z-1a00tb6bmrLbtoYe%kZ|4!^+t4L z=*G&u{md^fZcuB@6NlXd@)X$mRImRxD4fC3wYlrKK(~ubFS*l0=&ruXG-Fyw89v8+*2=R=g2eu&5G3l*1d>1u$-(ei}!y%W4ec?3=r|^{+o~ex80P}E4kDEBphkPBVw(=wC(-hH8iVg z;;KhZj`BTQt?4Ch3gm<;MNJOR6$xEBOCifYxqoIV|M#+r_W8+CfeSy%!fHN1Fb;rG z!82)<*zgT@eWAN!S%w2w0!|$nWa%*M77pxCSmJfdt8Cl9ihfeD`{vwbz=Ybmgohy= zqJ{BP;fuqSjN1Qv4mR}72^`Fi=GNND9@mJ*RU`HeyAeneW>BoYKcEZq3$7m>5G@e4 z5Is`qNiz{Bxt#GB>T&Ii=534z_lzjw!)QAcd4djpVIU$K(7Za3+4UvgI%u;Gk{uA@ z#F2qv-}XvjRq$M}Q6gX@d|H&{bo7;~Ikp%Bov1PFZx)7E^Py_3C_53q-YEyrRi7Zzv2#KsFRz%z(#=+bm(%((@$c{xCnG>fzTti=-M)A zR;;f0|0Z5>*O8NlytqVbWozdgb+DucJ5B@+qZhmPLRocmxq~KcT=&Zn9Ba|E<6=WL%=K>Qf0oT~_ zq-liV9s{69<#Sr`U>ckkaO?nCbo=;p;O%1Kvw+F{I}gi;a0|-==OM8O(?FpD?$)^& zIMlL4Wy4e+m?JXzYIe?Rjcl3}j1bZDi0FDVTFW5O!tRDe+qfMKFkll)hO`r41)^0U zI&&FdkIB&rEznKsCBBKa*Iux(;jcHA0CO$jVZe&B4&@1u)4^eAKG)e^=KN;*Wj9jA z`<<0PuX)x5l=}Vk{HZc_>LX9dyOrCwm6esDQwgCKc#NRS+CD@N(Wt-B2AVthCzn;w zj}b17_~cdcI1(Q%A3jGI)gOYSi30d`TGOAV)i#r%XW|FwGlWclzyubEZDib^5E7#U zVmJGupGs2e&KrR|2kG%Gkx>-i?MI+pIWuJOhXtp=QhFPx~L>pS($Y zsb(v(rE$?*j-R7{8Ru;GDaxO4nMH#L$EkRQNf5{)O^q~&<^GB=22JXlX^Hp|LMoq$ zdf>!itW)#Jkw^1$N@V`mMjr)+>nogKdf)~r58uc6VR#RczMf&e-O&%tD4@bOJ3oA? z7X#GVC7cjM_h-hT+1a$plQ?U+c}H;f3XFC=`mhq_t7G14;ob5AB|nX@-=&VB;Cjb!faYe?3bugLoz61en$yUGl9nM2{H>iZemVHnIA zR$OpXs#Si;0BM-t<>15-@CHBp8E=_S(YFgo_uZ(^{kK?Wc>E|pxBH+{2vi+198TgR zh$j^8nKw%J|76DD{r**y#kug`<0mxn@~B?CG@l+pe_8_uf)`4}FXIDnGJqL?{RH$E z4s1}A*P{Ua(gSE?VFVeQg>$x#G!61bp_1~yNHj!A{q%)Kw5*`ym){lM#vzG>TBRZ1 zlw3GS{kGXaIEzLXmDmGLsEF$>pDWvE*I(qwwN70R6dAk(Lv^6!o|I~Pu z%-**^{j;1B$Spu^lHveIgSi&H&QngGYwT#1J!H?VYEk#$3(yI_UPuDGFpI5k1Tqbw z523#Ym-|q8h2-xt6;Z&y!*O8h3v~~X+^~wj1lV)~C*0z%qJVBY_)sjAQ$=O**iTv} zMsFKr4TLa!(-SPauG{{HGJm`_F$T5|h;)6;eJT!q2^{7V!Fj(#;{S1USy>A1p}$Xd zLFx}P{{q$j%h!LU#ByI*h70+Eaf6r+=KjEi>dhRJ>EF)(Wv}1M@K7IdyrA^3&b6-y ziZQjTKmwb9 zS%CqEYYe~o`=A;mSAo<38rBd^Ab*grfehm_8Av&W-n|H?ZvXexGnRWHw?>*2fVZ(< zs=X4iPXzrMt&r41;(>#LzvMulWsW)O8XG?Nh99pHU#|F3yo)JV>xsU4KgEKO4`H36wfYm=0sewc2 zfqW&xs)Q^l{F8ZcsSig~Gp~ZCQ~1B9lcm~G(&-mSQa1YVX)B_g(?+F-# z+6l0~r~2jv{~leW#)ITy;Ey0t*Cskfi~y9CAU6L!3o?hk%>dB+LjY{>IjJA2(0?ZppG#FRgnCXL=Z#%kErbe|pt>RW%gdkSut z!Rh(L_>D%`nJ8E~=l-JGp(&0_DtpULjd}GGM-0>z4VHeciRZ^DJRw!I6bD$%b=S9O zP?pj^BNVWGwaMx4Ie7?+o>?r+|L`X*tn9c$W^cdF$`~)xxpEhW)6t>JfB%Kqft;Dr zS@WE=D-p()d;tV$)W;zp-2kRHUZk?uVNfT}qi+nXX%T?Uu1yxkj0v_xpX_-`_YMVxsxs%7*g&8D4Xcl?Gb2|Ijn2tXWrhQBLa4 zEEwGW)+lEvmIzuv;WZ4-f8gQoFyWc(;$t}%>sK~Sy@oAMotH)JLyF1K-NkXemR~q3 zV>50>zdrYdS_3)d%`p1u()Zma8`NKg^^nI&{I^GFzT&!S8qogMXf&WdJNnu4I&muB z`QSxbtB4jRA>0&Pl8G<)W(R`|)uMV`&RtW8;1Ll^DSp7 zwWe+LdvO~wtzkpUxeDrM(e1IS6pm`7(>`!pTwTpi>7R1sHsg8PO-&n_y75p~2K|xG z-gM)9O@123KYM}=x4_?Es-S7zfj42c4|hbG=iuysfE64PR_UU|duF^d;hzkuRj1oV zlNIwjP|~SPZaZ|Jxy~tkmal1z|LtX2$GfGNJmQ&~YdHL{P%iR>aU4jtgf)xbtOgp5 z!+B21ziz0hFVE){nSE`rWyJLKCdst=GqvY%=zg=f`ufzktJ4~LrVIZCF{R6COt4`# z6;npM-&LPJ!b?z0>HGlNU$6N5ZnDN=V&2;Po2fw;Wi`7Ioaf*<{j(Z={JBLH8X#NL z6L>7JkdkglFxMX84ubKBKUk%u4qo6FZxlCpmNS$p>z+l|TBjd9+7c*j&|0?k9EOX~ zhsoseoObJ;qEDT#wwh`Vh1%B^*qf<~cuxq^el~gfNaz9zRm6r3UlOiT`Ptr$=P3ky=WX`JHjK+1HPqffLlWx#j1sY7P(Pqp zfG=%{M-Dsq)|(!jx(eD>^&;>2^Al#*m_PfQwJhDWo(Ah2PyTTJ1M6PavHtc9+6VWN9PzSe!&n!N~(QXaR9?w%>zw`(H#=j_}d$b}~ zUoAkA$Dp*3Li!9NG|qRJK&X56IWZC>PoG(Uc!T^zajcM&+F|}LP1zcD;Amy#%by;l z1y6``5({%o&MBA<1+CpTjK+cw7%b+TE=Scl%MS z*}i#m!>x}gKC|}5=lHd~V#V~n%(#t<8x7BrwFawCRxXC^HK{CLz;g-vWZ?EQEk{eR z7Smk*XmRUnLh@&ft|qZaoS9h4dBqz#jZNp5{}`$dEYw0Q8H+9^Jo`(;(BJ&~;j(?G zB38Q@Gw-W{l^Y#;zmc80DL^W@wcNDCYIN66f@CmlG)+)bvimgt8xMNlk{P(;6h=rH zj6z+zwZ8I>PUe?b9P zcBNXil+r;EqUBsG>To}FN)DS(6AA2YN?myI^ZQ0`uct^s@x##FO5Qa}TkSEooe)8e zFs1?=s+f$35cIA^+|{=FLfLlp3PHJ7VS8QI&cm8*y6lLMgA4Fv^KZ|U)3YFmv%Ve- zKO#=n7z!0La6|iUfzQ77i%*lDO9%}7(DKm@!`iy*03F&F^{6!G-;HZ(6=p`19-r;+zOsIfYhm>&X?;4 zhSgMTHcgb{CU`wVC)4zLRK_OfAaf`m+FS(1Z}zC(D1{-qV##vasD6-C(}FYfHv`}I z`wqr$bH|W)%(eDaNS-G7%EHy^qYt|635kB3uT{5&DvjP5RSqb;n$v2O#z*bCi-&RQ z%b>%iCSrOlW@qa6RHKay-XLM!*3;RL>=MDH3Crd~dj$+E%Iz$d6hq#vrpmTrvm>#f zK`v6QpLSitzdi)WeeC4Hs=p`PD3od%xO@%VKpoy7W1wc;P&*;Jn_!P;=sd>6ROapa zhk&4(=Cx~!an}jOlvC1PBbS?-b$YpM6Qna$g}3S-Q-u13{_NMNZs94)Rew@I6K6>o zthx6vw3%)M+0(meS{)P^1?OJvPz0Ftm_Le|Y~OjtIFYX6g=x~*6^ELFC^<7BOoGGx zsA2g5D4)$<0zkObjSDLQp}SVWU&?Ygj~2ZpSnK*ygd&P!J(~NQC+<$8`$yYOtsuKG zj4AckK2hbo8)QpCpO#Jg_n8Pb67ks_Rg2`QkkGBB=+a|_I$C?WZNtAZzQ)GJRe299 z*L%QnwpqVx)<>IxJeYD|scS$!e~o2`$?uc9MnOD_3oAi(YWV1~)Z9_qzJzL?Ip(W1 z?nWj}Z`U{KC0W;yz4Rzw3$2)-n6PB!I%(X79-{fEHq5G>ZaMHD?#{3p@! z0LRf_P<|;RZb-CytzzFnTWcoNCwaXKSK~m*a-I~!Tlg9i=O%$>o`IQVNnRxrs(VkB z2D`8)mOSS3#a<2Bu__78!uE>ag+`Z8{8y*Z#V)CX#Y`Jw+6O9)+g_L}Z(X|E`((E- zr!wVBo@T8X#yE{z%$9NkaqpO$zmR=KuwgPuq)Ep{a|+^UbgOpGt$J-mc?K+cw6eQH z3KAAP8sXr0F5a-EPE-Eyz9~PeuH`)zKY-m#Bn=)Eu++C3U-tqC_DLDSnv>=ZvSb%v z_hp9XG?e{th9?Zg^$WF`ClM%KOKTxN5%KE@4#VUnvI@(Rx1`~-?7|$B4aOPvBf?k` z!`YlCszEkI^-j;t-K}T$kr2^|->J(lLDTMX@=HL-()qdn8<)cN$vL-zmNi#-B(;JU z`r5H8u53}i@MTOq+W|}O5Clb5uH0{?n^}>h#Cx@OWxmx6IVf79jWFn^%erUHqy;IH zY2{6gBvEe0zKPfVNwv>r0>*Kq8M>CGd33)}Z+*Blq<4S6ye9~tahGE2!2Lcr;g)yV z6jq41pt9M)&Me(Zc(EHW^BNC2eixjqw zS5g;fzd4s{ZX5miVI9iIT^1g7N!9rF0EOK1q(oh(9lcLVq0L4)+sz4$p8@QanfSah zdh;u;ZShaZ-LJZjQ+TGMm^dyz8lQ-xq%J7XRx=wDD#bFa=F;jdbayVud8=1?ym7=z z-;_&8#VXHLhoMq`LB1>{R-Z}}buFP=8UeQ%L(gDtUl8-C%bTADILmISNBBz>&vQ6> z=&2&{Y!@$0D$P@h7AyR`Yvxbo7kBQO{Emo5|8P1y6R?bye1Maa#70!x3jjQgYo=?I z;B&xVRKKr?8$rI1#h#x+VJ-XTW3O+O_UDk))M5{SvQH#5XLTm?u5IJfpFaKo4Sj)X zNoAW!o=w_A>k|~X9$1DfSmp|iT(MQku;kC+$iopB5VKK$NHSL#ntjFE?pD*jz%~c- zIQ92Ps_>inid$`2_LX;BE6mwh<>)qMvCVUuq}sU4i*6OOS1vEltnu!f%Y_=WU`7=6 zJl>z#aC~J|CZX6Q3tMd(fQHF@=(#3Z@so-r>Nx8HkH_EeU)A1xoYHD}WI*)C^W5n^ zr$Cm_0Ya4E$&Hos*kM()fLLDY*)A+u4R@*!cKd}2x8*iQzQj=O#5=!AnBR6M_Sx)-z6cMMQ!To-Zth*NbY zezU=Hy*J2ngAXu5u?Vtae!F=6tyQ59m8^Ftc@Du*g(b>(&!_ibTtq&2v26Xk!QARy z^_;hmRd44@e^I3)q1y9ub*u0Nu=g^)HDcv_L{QU07&w|ke|0~9iWY2j%$h8VOAvwq z)TJBc@DFB;`;C4rFnk#YtDzJa?QV((8i(C=UIqx zvOJO%8gad>y1^xN3B_i1=K)i3Tz_GB{+mvod751C;_NW~;A4t;r_P7w5AQoXKXIP; zYVrC30?zaJ7reyd6&x5Ke>ajZDmJI%Y(L$M(ZY(HF+-^hyTbH zs5H9z;dcZvweFd=ds-0y-96pYSBtd(orlr63c1S>bANOCz}No;(!*Qw&VQE0W`TCt zGWyv`gLu17<1wCdvvD|toIJG+Nz=IZke@y~3d;A3lv>l#Tzy}l-#7T0#sO@s`r`P> z!wL8daIR)^!h7h(4%m-HBHCqCcU6^dsI24KA~2U;{^1?$Y?4KGNsEq+bKKuude;(| zZWuB0pPoXE**UEW?abkVo_c}M+spLN5E^3UJQqNr8(+kN2cH(Dzv#P!@8muBf+mGfVFJF3lD;Xc$BcylUpQ`XY$HAUY#A^v+GHwQp z8eMVJt-m~T8FG3-Tfv)^qu<0^*!;Oa_Ya;kxAJc%Mk>-M4xezB^x&=<51&c%OD1|2fz%L^Hcxe*-cl zax7(d7bKiz(>v@5#>&h|B$PP!uMEM4>ZDiw1G9Am}q zwXLy&4!_38{T}eJ-LL6E8+%G#+<;S@>UG+Fr^hgfIciQ^Wy%|M$F{!%_6X0d;*hV% zv1G0F>WESIl=YNq1*j@)6`ILsV%FK;f z3*sUf+?KsoYr4~D5kzbU0g1DK%Pg1rZ~MZ~W68Ds(q0=I_Kc3rVg z1E0hSFX|GG&m9Jv`mG+Emzvl**E4EPZ`n%3|9+1p{sQJ!g ztq93_SU^y%`x6^QxJ3IR^U>YxQv?aWp{D{hf~% z;c2T@mw61Jh@#Hwqo_h_Ojrr($3g=eVrf^oYge%N5*gXurTyor895WctM1F)s;M! z@;d^~%ar|kP8scjphmg>{7|(Td=TP^;%p=WAVYr;`Rh5*EYmjDn>Ts^5rXHF4PG?C zAH+mKQh<1sP({iShuN_DNe!#JXZviDkgOWq7^*oL7j``bcl@|~j0l+-so*~=#*{E`b{uAK-;~uI)Nbu45n09E4*8NUFNk&!kTwhS9#l6(5J; zVD@WC!KJ|Zx4Y?=F8r~nZcUQgsp7FHTd2?olX?$H)5-4fImUzCYWg$b#9uqNtj8rA zcd%P6yWC>p@C>bBb=}tER7PAsQ1Lg9gB!McO9fc8ReLcnnCdwsE(m!5v0k2gIp^(PMeCoL+BJ^l1v?_`pVWWTxYkzZX z1#&P4`Hyr)DJ0agQApk=T4TfFxMu}2PuCm*zAm$WkO*`P_0y@$#r8>}sm@JuyfNNB-y>dr_qwH+M6!2$}60 zHoP%H=ST_Y#CushDI}UPkT|cTobr?Z>Yzq~XF(OetD~r0FYcM)zjlx2UdbTPwQA-y zR&K4Liyal{h6Jwq9rY!-VAJiYRR3yMqF=uHPvu#}lt>`BUcdX&7=TjP{9a>mV)EFP znxFmuDgn&R7QU_u{mORnFShvBbrd(p_82Zjy2>#3(o1{Q#lr>3Rq0t?hx&FB%N~nNhw--^r&pf zK3424Br|g}6Ms=-Twlq;_pezH0s}>U4Wm_lIb80=`-=%1u>R4M(@s)f4sZ@6)(!jk zPIO3`^)GUcC<(A^^qp2&EG0(iYi;TQvvv51xap%}k>04gt4V%S94sLFslUMXV zSK-_{$}kalYBS;$X@aofM%pY84P=Auu-+w-9=Z)ZGa`!UNH6|Ts-@kUPT<#>E(bLC zC$4c3`#c%%|5;yUY(}-GEOuQ608R_)Y6sS|=qGa$&#SSGQ(DP0gM1 zntS+lVNC$Vj1b)l*%*+Veb0kLiTKuht}0TA;3IaMxXF3n^T1d4lI~5D{>QUhgC&j$ z^xXmrS8s<9wi6j}kGKqIEEoXhyRq86fO2&viaTEB1_a4C_sZtMrJ%Qp@5sO_?CQ<+ zy#=$_SpIPL<$@*;q7VILq;r zCuK_}iNJ`SUB*32zmd_i)?9EaS@1cz6JJpl6yoUht})78lrP{wI{cBFINDPoZ>!e9OJs?Wh0aC#!CyB+v7HW34h1$phKlrkpa zC@E5paA20-_k@Io+KKjto8sm6%{<`lOUJ)=syIPzoYAM!a&Hd*tY&Tg(Ur%j=5zV& zf6@j7bqAcDS0`>bHCAtoZ`d>mPywQro6`zg<8f97=h`Lo03oG;fkNsGFjXnK_jNw! z)Ir;D~5%miPN3Zl$;FmE-VolLlBnX}t3uOvM~@EMS}PI4;)WVM}V=lE6nl z_;l){KW{^m0-DQwB1sb|;L3icSuF2iJ>T^z?&vC-=H?8{qU!R>?Brm?F9^zrdfXeVEh?sT|94)Qk2|uwi zQ)f^vDco{~p9j-ZY=;Jrbv>?%9H2Gc1P1&n#1+U0G$Uhw@10l-3fSj=zUC3H*RD_G zo$?M6>CfW5fhm{$jkE`aK2$iZ>DAtM*-kLe-W@*WB3Lq(p$ja->XIO$9)fn) znpf0>QDXM@fJm=_R7-rOXLI?_nx~KyaDR}BDWDnEtUV=fJ#qVr*9OOr7B3exJkjC< z@Rr%lGC;Qb5xg_-vjW>}C%-s$x$zyfg{%xbwIJ4Y`bn0>4z(Q%uyeZNFnqMc&wP5? z87Vv90C|g3#O?$|j?*JueY;7*3zMvO1)qJG$?>eCpAD^|o#lQGXZ`6)1sl}tRgRc0|d=yMlSb&eFgFq9Er;WP2=b;%jh2*q#= z6rmDvM{#TmA5+Zk5PcZ&SFSi6Az9!~0Ph+vDcghV#YE^}pNS~_wPsE0JDzbv=1@W_ z`TErOQ9fnb?EUvAg<{XlzxU?x9&NFLu%|W%duC>Ns$1s{z2CWU3d7a9NXHD=$%GQu z&ZQQY`NeZ8Kr|bi`Up^ovOPWP7gL^1gstzR!YZL1$;Oh&O+Gg(VuheAj9tpC;x34& z)mBr3RDA?KpDCe`{p2LCy2aUdAkah51ws?j46*+BA`?S)$Cn-pe?1A-!E^qu-?W&a z!Qz0Y$BNUGsvq$13!(JZ1ih)!nxK7bp{%XH&38QTSSZ=yr~%&k%IAq^*3`?=H2o#d zaqtU59razWNm#K~BSuD^aQooCn6tZuMdR4^oua{~%s{|Oxid-wHeARcGScm5UJ#foVAnU2T)Fn)?(EFSi(zj)UI{b+> zZm;;Cxs#b}A3VGVYcv1(3FwSMbKn|;hzw+U3QA^n$Bu?b2O_ni$%MY+dzahk>oJ~X zE~tWSlxD__iYzxcKfy%5j4zib6pOG_+%iTQD!L`_b_s8r?aN<;J`8)|AOtrXV9k_1 zh~1yxDKZ#QqtlV`Bcq#-W)n>~UAn zduE3F<8}RQ7!pWg%3?{2(ygK=lJpm&Qe zY+h!L(TX-;YP$KNVxb2BsBr86m4JE{JZZ( zoqC$B4%W_lQjPaGCg_K35EHDb?hXk@1yOS$bA&ch?RNHT|9lDOLkmK}eORMp5a{82JFW z*eslyp%o=Tn2ikiW@Be?RlzXe={+Inr&#Y$7^fI-JjZf#ZjXGzM$4i^(-auROY(Hl z#zgIq;UR|vU?<|)@&fz|hh7riIrAauoI&G=J6>skXQ1t22gu!d3S2y3*b7dfyf%x9dJZNlv?6O}$|n z($}3@UU1tI7FEiAPD`@zIK2Wb9Ru{;<&t8r{gwTJadh2k z`FF3;mCAx6m+#I^OU1QIqxpkFXl=!Uq5S@UTshL00lreRq@salzQ@P6A11t3l6fzU zIZ?7aUHfq?_QAtdd^?}ru{3=a*}zCH&UXx#c5jkmOC$feJjeDuWPEgt6{T~!DzPG#z$I%0!&A^*LXh(eLV?pZl{vFNK zjnIQAz3jPuo$tWPg^)LB4xuQ+)Dt@o?I{Nb<*fKEwiZmB$tAdzhqx< zKYI5_RQ>Jj$hI@OlhX;?glBb4?ftb(TsSSbqRP_mgz5N=HmdHRFvfCb`bQKMPlw-p z#sW0j1s+sSr_7O!yrlg&>U#CM*ISI*UPhEuP$d@fahF0GdZ!R}1C;Bckw){cdh9h*^? zo4J3VRPj!$@4o1pR? z+MAWOMLMF79M%t46g#`mNL#q_O^9gqTC<#?I^Dj1B#wfWFzCGhc#|O4_Q$*pOKND~ z_(yrXp46D+{a{X1rgm5~HD`Nc7p)Dsp%Hx;a}9w*mwqWvLCkk0>WZ9eJp{5*V>qy( zAH-k1kn`>};|SZcJJIw%{X&mz9tPY{JXc@F-kVevVN4gcEFNbK>p9xjV-rwm74z!0 zYRwGENln1hl!;6pp--OI?rQtR?aWg=mu3#FK$XyQ{Jchn9dY3CvNWQBf6uR@m$H%en(rzpj&n@s=iP^jlC?Azc>IX!Y3`u_- zkXwtTWfi_|{;Q$F^@2Sav*5+R@3Q~4Oxqdi&!{LgoF-$op49!8vp3_~qt~ozlu5+V z>d&_YkC+`CWWgi6zWk}Wu5@7e_in4tYI;H4`QrCvN9@n<%swTFfK6)kX)GWk>P$4_ zOb}eqMBe?1fh%wh8=s<0f7LztI3o5mix?J8-ro9pgVBcv$IRXhDv>ia&S!S)(Fgmt z{@LUPKlNv|5h*Y!GjW2DzQhNtmwGXOHE5b`PCmtSJ65~NkLml3kQ0~%Mx$}dSsS)O z*mqMLmt?pTX4}UPrRqg4qri&Ba=+LoJ^HyXv?WX9l=6&QG`jh)BwBD!Z$Zq1pLHaK zqWJP(hZ{on@R)B0Z^}nfNGW2<+ur`W&I)g7l1x9z`xtE7`bH6oJT(W+N7wJ2cwHt; zxJS{BUSSJ*!t>4b9+pK+v?;km1K!;WWQfJyT>UeJbrIb14~gQ1rPX#Yo3V$e=7J9H zljr6tI2D=oZ?NFZ4zDV7!XE$Ynx-*mH(2^TtK}~2`OEy7lBS>kuGp%a9qi=1V>%yX zIq{f|geza<+CGPpRS_()HXUA}X3Mlt=bcMI?o!j->ER4X9%=BuZM%xPUOA7KWKgb4*7{@;u;j>fl|2a~ z@k+g4GshF6Q{yO{MBy`k>^W7Lbdxf>xZmYB-GYFHh9xtcIeW2nmB>*fXYRz{c(}z5 zl|xc!F#TU2%!g76lOhrK;K5{wzlz1Nc-KLAM{#&8(58Wr$azQgcdHh~oWcC6%*cgR zi=@0vex}RBt65AX9((ao?nF<9ZkgQ_N2R}6W#m*0w--8KaXs(P{Nz%?%QAu3I{QqX z7s7>&vZ8TT-u%~M7~AZ<4b$7p#>7O#P*=)G&z;A>enA|v>Db!IL|a`a`A%Df4)6b< z>@A?GT;F!zNk}6gAe|y0ASo%0NJvT}4T6G{fOMyVq9W3zQc@z)C9Q-g-3^ix5D+By z{kqoo?eqW68RP6dhAx*lVb1x!&vVCh|E^~Szr})amMgjHDo(;>O7#4b5m|iHOk}M@ zp~h{ikP$z$qe4p$of4JUI=ba&44C~_E%`VscD&YNWz04pKvJe=k5x@BOtUyM= zzkZmFdt0|o)W;{V^*0~dYB=2f)1%+a(q9-b9#TPiF{zux<}V&ySJwP88Ad85F#~t` z`1fOr17C^$jzz8u%zeYeXgIC9#75KYV|Hv+Zwezin0xUm5*(1n=EXa~&Q`pgN3jn} zYCnL(LNb?9r$erYqD9XJk2b)}!thVwyJCh6lLDm=350%kzXb`vNd=YXJ07k*Hyw*{ zy-mhogB3KaF(vS)JJ2jMd%sq9BCIjJzROAOoTxK+zn0_1P`0(3ZSJBbbUp54^zJUNtr1^1T8ysJ7a)y>Go-94;X37;ctFR7Q;O29u5Tj_?C`~%6c zJ0AD)>N_Umnen){&yJElT-V(mJYI#<-dl}=F5@qd*iq+}@e5sv@bTUr;1ov-d(MWR zD0(d_3k0}+3M&p2ymi%jBZZnvu`1RwykPw$dyRhovmL$NS4pi}BtB;l*lf}{-yTt- zUb7uoK;-cAD?bm3Z%8*KLyZpiMck!^nnC3ZyB9gcn;C0&EV(M{&K;Eri@Ek}nUnMD z`;MjSsn}IQmVWO2CMMuymzu9&^%GIYUb^s<3&ZAs=XaxPcE8H2wC`5&{s{1Io6C22 zeP>GEdHfn5si?!))-T-3ZTRh)KSIhAQhGL^DsVCa;Q*;nCORmY0t{~1Ne-^ZdtrM zYuIhBkevL2=~FvfMa7u4HC%|ER_phj>SAMmuP->|I8N8lj8{6f&rSxm{L#tC%=yUI zLeq|Mws60{6uoFJ&$VMyPWF4z{W;r*=AtXH}=G zSD)o=P4a7T&|-{!`Br|M%Ea=`vs()b_w?lEW~-)>mv|xmf5c=&sWA!(jBA(_bil37 z8(z^a>9>Y!rB8aVZNPPRA8X|jp~H9s*O830uBqouQ*3MA zihF(f?{&P>dwOzZ=eEV@4xI|=GK&WUOzE*Q^4#-GGa^kYoUN(h(bXyV|6C;bN-v!4 z)~)-qG@o@a=pGYZ-zYcB?SM*ftNHW|M#(@sF~3&c6{{t!Cfw{-rb1i-x&%;3$vvc^ z_u}nbh&FPteTTDL!pPlL=1bUFyIVX0!W&$?UwI|v} zxFpw|zB5Ce?IW3*$J+gx2Rb0A@}0HK?_4XV!})=l`Gzkk;F?Q+&WqN*8!~KiVNS=% z$DE+8(5flu@L&&E!pG5G(ZkBRFM|Exa4q-r@Jp{@Ze&vaUgk0PolwZRQ}LgyjxSf4 zjyriUJ8(WorivOb-@xKJb}U{NhB9*7r8e3Bg=Z$;3m<0dMPsz>97}T{M=M1p!J-o@ zywHV`ZEfY%RU6koOOa$k%W~%=Z@!dgZOs{wJ$7?sL(*x1v)y1JO;l>1txxB-8-WVJ z_Uz`?ue;KG9$7q3<`Ns}CdrwtnS!$S;K*J$K+ut%cD%-wtM{<`qV(R?C9F%Z((cO` z8MZW;T-374qfm`)z?>y%h9_|MI1^h)(m-lA$fgcxc2p481IZ+Un~5K?6R*jU`YWTp zoX>||MU{A=r!bRXP^mMI^%Bp=TS3!dBy8xP$jK&u?5#pA)wWO`=lOt^ z+F8Y*^doIvb~}beU`4ylSU+4xpBmD#=>#?}Ywc7~_cSRl%RsVu8vouO*F1iNr&T+C zn3GnGe%;@eod4!K^kq&%pSFv!+awciwXKH?sX@u<#wyiQAw;~mr_bGXuz1WIl}fK} z7U{h{v1XP3^doeR5GA09-C8ZZZ4u;uqyBvN1j#G){9tyBs>*~2g%wLf0SEpQjg4`r zx6-KIZ_lNoAcK5cQv3Y3i1->D@Gbnl`o{|_1j87(fm5br7}Y7y_i0H8lrbH0z#JBj|xxbU)nNXK2aGGA=ooJeNW2vO(i_GkBQf+1(N_CN-%LScn7kJG(D3<)CNNmt}cAG zd?sNiJx&T$bCj&o9lG~ioi1}TW@sI+%f0FfhBntWcZTbaZ+)qwC_KsEU$jPhU6txE zF(1@@Js+C3=_;BMbt^ge@wN`j&h5`CoHr`XV&Uyfd^x#jP{#MrGk0&eq(Icksy?E6 z{@nveN;KTVAI(bclqjX;(3)rbq0n0#I(e$N$KfH(&zMlra8cZ@th?b^wL>gRmBFzd zi`O+r49WVsm7_{Colv^!={F0u()vzbG}osN820b;U~Oxn8OIg2uCiTrpyYxG zy3OT@lx1=9*9!MPKuO`#IRyGZp^Av9x+)8q)%=4% zlcEo338KZlbn+Dy+?NmVS&bC#nv|{=ylJpzP-aTwUO#Ht&i8Vja#IKCqC6x5ONj8^ zhPyd5T@TKm2Dv>|s3#7#y;X>RYGWJbB3h~ytlFU0_qP}78+t$Qgc(y1k|g9i%UbAt zU#OY&?Tt&Uu>o^!dhe`nom-czjZ$T@F#0M)*e zF6k&S1b2Pvz6*2+J1k6G!GJu*N<3&MrWsBEOXV8Pd*3 zgNIP(Eho9GT}9QyINP;bDtFTje;?lrQ@r{1Z4vR=LDw4ckSESlvNme;0jFbk3LOaM zrS@VQ>ooQmhWposQYJVww+D|X&oC8-RfEsbEsr#%h;z+Di4BY*ra6P>{7K1$Sv-&-}UbOG58Ex!v$UR7vB5w#-An@93b2w zYFTc0zG=6`=!WmYPbx&yF}s`phv!GtJDLgg#CjJpWOdjPrAqjb_B`F!)-#J}t}6%< zpcb;5wTeTjLOn&hiY~2asyb^u$lpb!I4AkQ9Y{B;WQ6t8vJoOFvD`Yz2RG`J>oo8s ze{8+Jj-s8oZhXRQT&S<%QD+^6*TY4LArV#WDUapkl&TasqDda2PD|}s1hO;YRy-Pl zxuzUcRRV1@Z*rxXK0I7z(S05NA%vzyj)6Qn^E)2ec0l*_oo<3GOflV=4|NL{ z>(3sC`mP|M@#&vi`XAB>QTduzvwJsgqAT5TMyunP=4YOHdXqd_DV)j0r}FI_d@8 zPRZSwpLbH&L$OYdyFa%x&y?)Po`^FJ>&@_VEu&-3%NbFsK(sr56B-aCMd=Sc$qr_g zBTDd35I#7tXQ-+1W$`4pT5GldJ?0f-p%CtRpdS|n=m>Q*dfkJWFPldiC!k8;?!R=k zhEG}UZUy8&B!?kFe4u&J&8TL|MdFCw$S+^XHjATVqne-lXFbQua@nL~gKWHQn+W|b zd<%=~MGJV6xW$QSQ?g)4jMlemIu~|4cU2jF6%aRJp`f8{dn4vts3E9^y9yFdMemV0 zMvMbz@&0J@k-K9ebZ(FvgFOvySTwQmE$Ho?6CcvpZ+nT&L@`_;l>*yv;Vs=GB-Vmz03|@PzsU=|OkwBsTC&(Xa z{ncxO6O*JAw80M3Ce15vBbPIEJQf+yPRfY_(Y5%Hl7~u<&f%1Od{w10gp$qL6~?zR z>KoCDJ=t_t(DP23J>XIk^6t4i%+{+el=P${E#eU@%sAO5t>>MRWT4(?mfVTmwbOS^ z(Jskvf~Z@aVg%~YGs;w}DMFN1!Ae(hY}6A=Q-l2OI7;{19cnp^k)_VOsi!0kDZ*T; z&hxEJ_4l0WoG*OJV#|5=P!ywm$I|x4z@)Vujj-sH-MLo~B9+rq+Q&Lx22XK+b?Y}A zPD*LhedWneN%+ffJn?VL8>L5uxPI@5td<%P+Y4c4{Pfmfbu0Sq{-^h<6We#H@ zU&>r4wAm95=w|0<3CTpt{cjeL8qjp}T)V|rn4};B6{zCs$|OVdh`c;rvJhgn?>Fd9 z+^{oLHo-o4BOBdKepl@E9b9M;VKKYrHXvf^^02i=Zp?4Dmg`-Y41+IIV-*>lGcF5v zUv!d(6dnct05?1dn+{a*XD6ey5?X}T8^sg9E*{Uyc;cAUI`9wEweK8m)f!7wvASNI z(eTGLlW?a46?wUv4gw-lM{l|Y9GG5(sOHYkBe(dBPOr#o<-23tiQ~p4RZw4{WWEWq zz60ea0!Q4b>%PMaKj%PzM79p3OUkP8SG3BTEgSuyCP(WOt$O&}yHg9g{WWpEJ6hb4 zN!&yYmrdIULX7*&m#FuQ-h50zQn?i;Tkl<}hz0}yguO(%8`|Md?W9Cp>yOj*kpKMwvlIM2c;Cj9I$!{Eis(|0OMvBlN+)2U~eN7K``eU)mi zDnf?+i4tRDYO8bnN00B=r20Pf$I>&bcO~aO(1ImEnO$jgw8yb_KsGcF)h2s?n@Oux z+-Kr_Kh%s_{ECcYO3KJE+ar6ywbIL2^#)ZP!ro)ApH$!~xsOj&BNz7HG`~TCc6HgG<&NnxaPi68*Xd|YwyD&8HPx=< zd-zels^fh6e6FER%;=c)d~Jo_QB&vs-MQ;H4ik0hx9n{8BJU`tZjMxUG);6&NR1Q? z&v#~*sD*@@ET#8)Zo0){N$z9p`&leW&-fPUM3Q83Zc4x|^l#rc9x_pS0vDf3aUH~#vA=t?@=s#zE8Ee z6QfONRhM~xmDYU=7X|B(3L={_5kteDqx(N1uGmaZ-e+lI0)67Pg);S$R5``141Wyd zw`Q?P^vXhG_akS?+`O`KOa)?^@k>SU(u)1v9;eF>j%f9ElFn*mdq2nVam0NKYA|#v zB44AjMg&hf`nx{y+5{$IyeJr@@?IMf!w*k<{VP#Lv+EkXFvsW| zbr)HPQovj8hPS+=xhrqr$#)vxV}2=$ISQI0&rry(sw8A0**L#0K8_I3+C7}py7pdCFo<*B#C{>%pWJ=!JxMt_7NE{`y#Hj|zwrM}SaPb<*feU9nCKFY=4?UWF% z!suK=b-v0a$KTD0hSjroZ`^2`X{s(-lCG zQt)Ljg5r@tTPgFE?oi&J;)v{*E;SUoAFj3!7kp%o)TMcqL>oMG;>)g=|m! zzDf*a2^RuJ8I0tE-WSI{`9s;ZZij!f71HeDywQ9+#_FQD1UCqh2G z4Tg%p*ZGtflKpwAQfdQU|GTA5oh``DRVCPgg3kKp<03gKr<+g9rAPjxkIn}PR0%@a zie8eNPYDqD``(f2FS2&(XWF-$0&4hozD{-veh*-MAZmk=`IBi+A;4qp`Kf0zbCfh9 z+?_rke&R3=^WQ9klQ)8v#gIssR-OpROhz~_ep;Dj+*Nh7R(zyFUccZAGeFR_ZItcL zw#4H~fUn^0fI&mt;QRyN9Y{T{Mhtc)FU?$M9l5= zsb<^@YWc$}+wJ+kOV?fIsRhaHXVQQCF1D#z;enWB--$SKuQ>&%e6$S zFQ7flLAl<|dxYkCH19-9fqsbg``O}|L8zFMO~!L~ktniXiDY)9!L+V}=$q@#hEQSv zYoX{e>N~VNA(Xsco9m`d+gtdMyoTE>ks9 ze%!)PBie>4qn=BOI({h=###OG=^Xo>(E$NwEOvv#8tcUtbIRs1k%{-6T`r4#1`|C& z0x7K%s`rIGxvNq#3Fvz1u4PHq7ngi?hugqoe#Tl*U%f4j=*q(l)vaUCy_Sj>JL-1>+`6YUKyHs1&4Mb__VLj$C?TPuhWw zd|Flg{fF(ke&qpyQoQIG+aflRmj@qjCXU~f&}#E^B)9FhlG5*J{iQKF-*tl~ugg4L zAjMlqAb`tRYxNgZt~#cnhJ4m&VbXFdRReeG~E>m7q|i@kRBdk%x!I5>9ia(2>{?l_pcBs%-N5uc#!hPKg*`^0ek;WxWd zMxCnu*^p*d!wsaTsMpppa=M^Q%Zpr1y@o`OfvQm*g+a}*4eU5U5!;8NzwKbYy`<6KeWsFQ+<-I-^-Y$yaLqXci_?JQYD&8Su_<>5r zZK;%z%H$+{Z^Vqk>_dJA%l&VXH##(fEBg}S|Gaf@Y93sVKGgx^PhATPI6CywQHRoQP>vgjY9Kh6`|Nod$9;TqpbhNP*nANy7|XeLUT zttTlG{y5(9Ki}$fp(<*0FBr;%8#gqY!$!PJx>o3RIeOJw!bY&;=CM(whL|tPgq$a@ zt{Sqw;c!i1e)y>A;BnmdvA))thWNXeT|C@E1(~DD8x7~4MJ=oBo*1}QHrqVOa<(81 zxx_llNs5e8{f>{b9Gv8D>0CmVQs1S=E);lLI!4c8f1_=r68`uVKY2N>oG4A+vtRDw z;v#z}#63GZYEX4NCYtcv|F_$a8|PK^r}z4XT12(ZL3K1CWdqYXEfW_kxR6kiy=>S- z+QKg~k6g?#sUv(9rS1zZ5HKB1dANB-R{Um^xGupDdjvZo4CCTAM}8<|Qd>Gr|HwQP zHU7wM^mriev|!yKxn`@Ate@3t=)Y9{-$`g;<7aUQk+x2TKnN)tcSPM_nw-)2`{LU6 zi@?*gy%9#JRT9O|@FgxS4$NXb5@vX?QpWdzalA{4^e?8cw6>Yp|6>RTqRlFLka@F~ z9yA_T&(hBI@MzLZg7EHJa=1LSbgSBKm6%A2fp!|)eoe9T_Twin<$de(R&hUbSfCk! z9>x7rd6vFTc;UQ~ADE)?y^)=7 zDHAhHQ8_LuNN zIe9SC&8FNl3qZxLUCb8q{AG>S!64ip`dyTQ^!?=XxqbUrJa|A6A6WIx*fh(<)O~Gc zCx)ivDp)zr&0w}{=NWSKSli3%*L};mY227}9czWdYUTj}3IN3RH)3n2cM|k65%3Fb zTi|lW{Au`e=RI)^*y|x5l>T;0>vy-DzuhB3H6>=t6NeNpnlyvfhUUH0ZiWRUBDEm_ ziY!d(Lq5)Tpmf-~cvFsO?;-UD@)MYEs;1>cR4(ED&4!Oe&ISGRWxP2{ZPO;1w0|CR z>B2vJBm4~qHbyd9bVTn2dK5_Sh1z7OWU?kR+Lz3X?*IUW0Ve^-k=5!gyaKvUx?@O@ zv0UGqBfbU=(xsRM{|AIH#)ZcLs8g*jGWxQZHG?LEx%)}flP37cgqfn=EQOX?^lb1%m-!B_id3I6 z*}aet&hPN9S#JIt^q+ld+s@&B`n6Rf@dB&jgG_9VXXo?m*X1=gzqA)ub$IhsKHE$5 z;+N+8J_qp0K~?k&w;N7O=!A|p@xhV7pxARa5&%oICP(m-M)1kzO~~H3v=wd(LibHE z^eL;TAmQlawes6UOrB&7wf!NRo1a`#A{9e5Vs{juk`mC9uB_pp4rYtbU-sCTFvY(V zej{p{hv??bLQJa{D1**BTbs^kOCV_IyNX&)DxBcy`K2bXfPV^(tzFKl$+_gpG;Rfg zGK?w@rZu~?^EYhDLEGC7T*CQ0Zib5k9MGy$Yrz<#2l z+tk_5t^{aRb^Q5o7X@q`FlnMA;^%0Iz60Fc>vV_S6ZdM`%JHhP-?>c#gBUlw{H zi*BMn!2cxnuu`i4?g?@xCL7k5Hz0uw_3k3=A>eagBrO~6<-&m4IRh^O1_rm<)JI$! zp44_JtZih^+RBKw+=u*rvCVrP*3k={v2J^K!h?C8tg7t$ExF#roPD2wdwmiEf&44+ zS$*Ze&w(9bbyM4YmYe7QVZ_d2KT6r5p6atv&3x6^X-~H^3-I25X~;N0 zt&xie;Ld}t_wPbaVC>+vXLto>y(o+Asx=QE`raais@Q_bVVS>dpp|<~vOgK973lqF z?7*lOB3w6R8V~cZ&8MA!cM0b-`M}mhhrD$2bo`Fh8w&aa0+J#1CR)KXAt;wXR4A~JJp|z6Z=jsZ;^)GY_1yHFJM-6jni1$lhitI%zP8D5 z5g^*eZv1WYYlm;lQSF3&;QU6X9pFStKvaGystI~NMUWqXe{$;2i|gU!N~gG~pF-!D zXv7T*L_{5T@-JSA(VBc=KND^VumEp>H&rUx8rCYy(F7pLMKlTEn;=+GGQt}9Z2v^) zsK}nYJd9R@I~+i<39(A@3%6Eehd2*c%n#LzObkAnL!X?VsGr97@BPdhEZiafT41ez zq^nxT-zp!6l)TJ|6ctCy_8;8hf`1Q8$*D^y6acm~U|(HFE9N#(K_gdP+h0I)Md}71 zlqeMC*m*y_fqS>-BTH~d^>_^Cc!GfC9=)t?E5ruO0ogpCrTPn)r0mdf;d|vsrx># z00#P*hGX+>Em~nkf*;X~hnp@=ScjHs45S^NpE^~GetqdmH|!uCUy)MwAKZYGTjP|e z<-xkKPD}KAp@G3k2P)w%Jgk3EZL_oR0TivzKaK+d>bF*u{0)QU_E1Zy8wq{8IHlxa z&EXS>?e~sVOv3QnukDBAR>xujjQEc_Qk6A%p6uKNs)XEca%OXdNVAmGYTlwE)J~6v z-Qk%V4Tx;UXhYIt25oCqzvb z(GD=-{%pYB42s0m$uC|4VMnd6rlfS7av+O;6mN=w(PCi=I{Nw^!$`W`<9`v%m`3c< z;0sl?crNuHowA_rAe)wVoBi{Ap8k6ZNw8*k^4qJv%RS9^UhcqCrn3D1FZ zmJfOv4(s0>Pas)B2{$vdos=^ZQ~m2K{kXa`_X&Xi!3AH) zHZuwYjf7(~dt*OCQpTfa=r%K$Ynnf0y!^m=lUsIlhs)wErO~+%?)h2? zneaUT=R@cHc_b(S-K|CU|o8y$OFtY#s zFm9lk z;{PYZS>BfC&|Vl`e&384aH82o5~q3O_ifi{iWYd`|0f7%VBo%MAUH{^CaH*R1s!dy zIAB&^Xr^~nKv#9&JaqeXGZwNuLebY>n-KF@SHku+GC0miqNEfkE@ArbqFP`TOIB^N zUS>|!oA6Pf=~JpRf5JvF2;g8nl@@ z`s#J;7*BiVTuzImnhb|arTe4!(7U~w4196coSeHh!z8AYBuIWjOe3M@N247}6MFru zQzR|Ny+84(?75JGg;yUFcuIh-oU+LCXT9)2rW0}yTzy$6ow-x3CI_D`m0gzD^EYya zaJ%qIWPUkbLSh+v{&U)Usum)GCb2I$6;~$0#p0a3h&!zjv%paZx7vW~<5c=0F69Rs zpQkVWLv$u%Z&k%6B|35x4kG> zuG_RDb_xoXKiANm6+)tV#rHlMYCoVYAls>7J_lcv)nGDBKE`1|hbri!@?}4#m>Lch z;RpW+#5bj8-b#6L`87hqp;5(yb==b zei^*G&8_qVA&CClg!We&Iod_(714b6_B=7#&PWze%DOHu@4^#xLD?vsYIg2$$?y(#XB6 z<}}0!wyq5%el2loLkv5Og;$uniTl_h#`H<2X#bN96~IY3`u{~k`C$T!K>r5MC@w{7 z$3q2l{nfsJ+1(*d_5p5i)NPNt_IKshoO zM&53gGkP}8VejD0jP#~%WP63vHP7A5@-ke8Lo>iSqtH{cG+QL);^dOmDU1YQV~dY> zEf0~R%#}e#*RcIV` z#au>a9j8Z?C6{>bE2E>y7{s;95mG`;?$-wY zCp)1DhXi)WTznmD9#{B|kk8@HUY*W|yvxN$zUoWA)@SObV6rb!Fc0vim&44~>>Zr+ z$A4-jq>&f|oIFg#OTplFD3$+nD=jvA*c$9L2OH!~&?vkBM1EM<$5jIbJIcG{vPV(m zLmH>0ZNSAZfi20CDg`?J70lrdx3%Rp#}%{6fty*+;(XB+zsWrKRwaA^oM!c@73P`j zsMtLMs~9+Rl&OSzA!B~|+O%|cB5Cuhr}LfYnv#QW(a|+H69LTs)c~1ere3#T@A^!V zUeAk_QTp&Iuyg=de9Jh*MmR;$@gh)cl+{$iqpSGJ#>dur>i2tAhSm^mPGM5Y1dx#T zow)m+L_ZqgaatW}5w(#-7w%76(Sa^>MsZaOeDxsyP${*e@-`K(o2>w%X4?wt%i zDlea>1Mu;Av=tR7QQ;%%Auug|Q(fvDZpjY9dQsl=JJA?GdJ_F=CPw(z}^nab<{ z{0v}(5L`B`9x9u@w)^>Wbru7zvvC(4O|QnC0v-l(k+b{WaTIKtI}9!#^&wbk2Hp#* z(2KPQC!8{342GIO7QGY(6$*P36F=|=scYP)zY>{?DobNVnz-8`Zsw%mwD4@ z0Ky1V=MP0$D(v*mi!nEKj8@gq3sVwdtzUZI@Uqp&Vq1C?ytI?o*s6?V2UKlO?(%HU za_dTw4*0jFJ^Z4yU}G5n;{7?qdPnpW##B~ELp@?J#&=&k3TPO1uy^s=pZ?&#wxmsY zj1lxQ<%e=^VI1Dht;Km`d2)g5K(DDBqQk5=c_rOpjBJTUIm{0&F4(@GgglQ%oeD_By zqwZ0>4Ut~6J1oVN9t))kcT zb+|8d7Y2%T!y5l!sRg-nU@tjX)xUJ&1(fPkzNa4$40w*- z=WQC|_9^`&I0~4RqTKB9EruvuqyOE-VhGb1x-g);>FceIcKMcs&+k3DTRs}{zQkjB z+;@VVd|=f>fzl7@%}4Tr*%rqM&7X}GUbKHc3f!uQQTF?~7~w=Ghrv>uV!g$?$ZB5q zS&MaleuYQyFpDlm8V^P-@`pkN%rAUqK+XQ}GfmX>PnS^PX%)2jOzVCm;Hk(AO9Q$lR&BCdw$F+Zgy?|l$^6{P@m<^p^A3I1igu7*Z&h)5%!ix#u z3tZr05FoMAyPFjwQ#*p-@K{p#`T4_cdcHEQnf{@{&=lb!F@J?h{}o=zCkxqEkfv{nvuQ@a%|-_oL5_oXnE23IOl@*p@^ew9HN5 z8>>?g#RJ;pYwD&@Wq^TYjZY=@9O5532;mLt~L(*SgZr(N#=kM#2xiC}bwN1lBMO{9Q zekd6Odi$ueevbEKss0(N9} zo;L(u$`yMWldy{@Ox<~q|CZ(h%C zQ}e|zakQ7KGBn2+ae>H%w@)_Ty5|Fz>7z7m)>QHH$7#!%czPK*`So;XNB70o>cG#0 zdVc3e2)tX+6u`;I46Qv}IIw_%&4slYrNw?%$sK?bHzO;ijVR6CyvgaKYuWx_I-8mT+>#(IG{*0?B|p-pW~5n zdN#$yQNm>>6n$@8WGsySfky+k3Gpe@W$3x2JSJ;Cky0ID2YaJuoT*B(Q0!jS%<6t& zN7%-jc}7dAiVk09URNAb(GJGgUj0>-I2HqHeXR05KR>KcQhG9cRF%28-6J1$m@>)_ zl7qS&3Nyx~n^&)Se%*ZE^s-ZO`=Br5vos{F8VhK*vbpM)f{?CqJ&x$wS(({7_9??GWv7Ks3Yy7&N+4u_{RGO_^r098jfn_A%F`+Bw7D$K>0?qa2p~IG$7|g26vi_1# zZU8G#qABvcZB;FiB)P?hDKpw zbvn_S9W_dL2o|F(r%TWT^}%D}S4f#`c7cowG+xl+x21Zo)#9WvLW6+anYJ?1S>(HMu)%S+ z$}C?2UNRdmCRmIRkXu^A7PUM*J<-BbPQ*KXLv z4b3|R-yb0+sCTdR0`~>P{0UHtV;aZX3k*YRND(myi*(ifP_F2?{_vX)5QadP+4?Gc z`pVRk3PZ9@!-W!mIcQcaAJ0@u#%h09SnU4YjbaWCZW#Tv!?yOHYW?z$gtWm_c_|z_ zD1s3mCFMoJio%{h^75ixy)jj7dIOV0fq7$PQ7cBsMtd5JJf2QQji|7O0`jSbQxPdU zp4%O*RscP~YJBsyej^x18F8BgJ?9^Au-}>-{#n&imzW{$AX5U9=_qd-+IQ&>-5-65 z{nN{6bnXFZ#F|(eD+a6y>m{$)fZRv%qV6-c>U0ealTi?!#%c;nI0Uu22Yst>%92S0 zdCjZTg4@C91_q6zrZJ-owxUhXuo&2hs(nHsqvNeq3|Y=qI-7dg&B50CBk}t z*{10K!HbH0Q6`f5tH}-(D_^@^Xo~-sTFkc&7qPe01feZrodJx)J8#fmGW=sG;)vMe z;IKQ_)`76?a?fWVP@p1QzQX;l5ev#wurz@^MH##WU`v{1z4-PYE0d2uRfznhAdFNJ zH;-(Xtwe7&T6S%&d>#deyIUh;KHdDkjZv>F1EOK%6h>1NcywgCQ_x z`{yrUHBbPLLe)E^*`nPR@W*ggC&lIt7e@kb2i7Wp?>HO?o?Y!|s^j4NcPedb@Z8#r z4=p|5cC<6y=%3brdw%mYWz@I*;)L&o_W3KFwGqJPSZ)wQ7^hD{EaWjtTd+P+fus1ETi4OhOpCqO|_ice+uNEi*Zm&s|M=o zz4|fifBKR#k(91?EJ1#7Z1y*|m;UkmDKDH}ume@LEKnUiYt>@@yQGy#R69a;T|f6$ zJl4p7o%c~E{@vL4bX+p$IZ>t@_3FH?clFF;!*i#NMgh>!*outj78T%{Pd#W&za@+$ zbSCsFj#OILMM$df*N&sMZVHxmcKhlhbAMe-RF!WdISe5SOJGDfGq=I+Gd5uOt?EN= z{&SMfWVXxn^z?;XbrTstdMA>v;PXBHvVQGD6PO2)IW6?!Ri8O&HDVh$xV)s95*`VN7AABV|!^LQ_tcpLwI)BpG_y&@|7Om)cIZLvrXBn6VqM~u@F{6Z3 zEI_LI`1-oeP5W;D*=|{0zo(QZLGZ*?^#C_=W~rRJ@aN~m#jV}3JyJaEgWt_Y^k>5| z-`^E=96T=F(m(uhH0mQgWQ*!C|A^TTEEnWzanklUwyGA@E@!_!U^6gL^qB+q^l^#w zbqrBYPwa3-=4*lkO=R{HRlly*p8+!QT#|#hiR=Zbcx~p}dg62fXSH+@2HwoG>*z}5OL2N`6{(pCnB4aa-V zgO(6on75bjgd6g>Dt$S?2fpQPR>kHf7j(y6>C<@Gfy}A|gf25Dj+|A8QXpqg|6~e! zzD=&>kxOw^eZA2EP^#nIM)Tl_aop-r^ZA3u)8NiNt1$Jbh&~z8dGLw>jO2fUc#gJJ zgk~4t0{HQzMl+hwi5apzz#T1$JU8K33EmV5bjbk;KQLW2R zuL%5@hJ$eY3+jk&LcdPZSPoKs0qwzV3@aj6fOZn$mRyg>W2(AW&|eP5{gtC zj{jPWfZ;@ahPwKNr0CN-QkwLE8_u57-@FVPC)|76cn^_cI{e5^glHMVtQi( z@ladDy0fs;SAHVZJtBtG3#LRDP&dumBMmx&oWnu_$KBSC^fbg-bLjnJTSv=p2S*aOO6oN-(xkETUC)A&hDZBI;$(i7cNx*y3L*G04 z5rRq#cOvmjE|?r;H9&EejlK-|hnzJq2t(3uE`WKz9LiJWTyMzNbEr(kck`qtdG z>xRJ$E@$^4lK2yOQ>9gLd?DM?bSxCI%%}_m*362z_!I)ro+#Ag0jw*8L^OgZ-PDJnoLqRa4%TpvX_kkTm_y;0ICwp(~c6aoFv7&Tp)0*W$v}v z8iNa8o}5MFcZl)aUqU|6p3gizkr? zz9@{qn_F1z_Z9eYi1YGI5bLP<*%p`7WMs9~X#Hv}q5?qV=m1Q$^_gfu(Viz4~X(px55FT^)-;>K<@wTSS z$GC=vC8gnDe>_mW?8@GV0b{@yK_r$2|DLf0)aLu#gnC@m<7gCXFc$DKf=fcY&}G7b zTklFbvgdLHJih_>9?@F8`BN7>0#lNv0?T7Ee&D8u{R{IG;{~FoPfh>8?X&e|SbV(D zasACn6$7HwMVRuYO)Kb(hVvF*Dq8%k?ngHFh9g z?G9KV*L#OD;mkkiQ=$Gx&SO(cT#J{e^RBV#J||xV4h!VezCw!;7}f$_WwV<**;xUtvW}|6H%xC)32MH2F+(C3W2I82 z^TIYYT|tsQPc9?7kihn)ro}z6f~P7_l;sW=b;Jy~JCUO-{M2|(q|CSM#K2kC0jHSRO;FLUxE?}oMI+D_P30- z*rfD&6?ts$zI?~ybwN0OQ6D$(bG1@lzOKSmZzo!~XfG8CM<%jYDd&b8AD z`QD4rUMFv(`#~}8;+SZ}U=zfA{b?vypq5A~3cMSe*F0-Y3B^4#Km=QB=eP=0b(4vY zAN0O&Jqs|_z?2D_ybj0-j7O1k)(Rd`eK&`b$dJ<_S}rnw-~gV(=;ya%+FSioph(S( z)`tV>G>e6w1A`~8dEGymn^}%V@d^^^Pnn}|`Iy;=Fjy?Ne(!}=I-9eYSv`1*W6t<7 z_)O?7l-4637Wa|x`>Ep7ht+X@;=OBG!Qh|y_C8oaz3h$FuuT;v%@FOA`Akd$2@}nMn5pn+4 z)pa0dg^{yUk+>C~P`SI9+AvFKL-sjz9>434E+U2%yG3;k(QEfkdlxtNhxUrq*J=zFo{B}Fz*@Hyv7*k`8%_7L$q76v_VANNILfTV*J6rZVq-w=QN4^U}m_! zne!$4TUQ)@^NH_gZ`cVDzDd4C#L6%YOL)f_+Cxtk=({%ySr>k?=3r$&LxuCWDKu#N zbnGM}J;E;^T6^<`je2fd6PXzpvL>lH6ch+=gNL69z65sIK?;ZI(#0Ga=?a5S4chl@Ltxqxae3Wrbf+V1VN;gM`!F<=ZM15s zX{>ocpMXUSHf!b7+!TN49(1xNwM7fZSqMG4TuQjJf06Mv*9$2<+8gVvW=4e2)ZsB1 zx~T3XjRR8_go8Iw8oV~Z`&V`r=rwI}u6&Q8chTr3K3YrtM(5$fq?9?qnzixvuqAeH zU~@R>F$q6~7b_jIGq*$!7Vu;Np}dBGJoq|x)WMR^dY^DZ2@AJkqc!(#ljTZST1u@V zJZcv5$B5x%G^gYH;w?VJR=;HMq;M5{kl6gwWi86Ia`v-s17-wl$MT=)+C<`-%`?!i zg7$Lb{d^ffw_IKlw*lZ}tW^=Z_{eCC)59rt-|hP2i)xaFa78yS@?zFxj^67$4m0FT z4T;582lw6nY0Dv2Z%=CR`^^31b*1Ig%(~0LDvOAT4=Ix2xEIlXkUZkat@|jD`Qi%Y za4puYPOScDO-U$a@=ZLtCYo8DrhfNM1zlgyE&Ek>0>gravy%`3Z+U7OzDgRoQ5d5Q z%^FqiX|_eU#9Qi!IllJi0;xvC+Ii)qr&ts}2zCocktbuup!uH`;1!o%ezngWy4YD6 zOAR1jCOCMPbwdHufDB(t088z+Lj}Po7yf?Rj3! zKLt5DvSS`v`9MLTf6^om_cD+B%(asPBQ9wAO~WhO+c;->>32wwW`@m5%f%i0QElzy z&=>+aFI0yoLz5^k;e~$F1}+w=v~nvcYGOmUE!eaP@?hb*I1VfXQ3a_ zI3ewy*?vL!Zm^upO9GQ_Jy5iNqVs%gDO8}7JT-g;%rNN^g7l20(uB%#IC4l!&%WGy z61LAlH~ALyyBjhX?08nyfos4=?wOOjV zMM5HW0iO0Y@neykypmKOcjVW#xm+o$u>GxlaBwJ{zv%@d)`;|Lj8DYSCTra{cOKUX zapPWMZk_ELc=IBoW|B*~nZ2Hw)~Yq3*xq#6P+P>lA{@pji!d2PqLXYO@S%j;cENgZ8^yqR_^6Fs8~`oTRRuk^ajj$lkyU>@ z=B%!mAu19vUC@Srg9nI{VFsG}E-cmzUVqj1`o;5L(U=6ML1mn54fN^Q-S)87O+I3H zjD>!^EWPJha|q_~BD%0<@tKGsyEV_KhWvj=)>hcT{i^8F4U546umc}3&wghw7~~ME z4x?>RUM9a!k5zQ-@L`vag>pVzg$XP`KYFi?c^?7{UJOtPNGA=*2>54ru4ZPlG)4yL zZPjv7?u!?G)-DJDgLIeDe%B{T6?n%1>P0%4pS}G7t;M5BfrzhiA3qXgtal+Zd3lfb zV+2$_j|O&Ui+>AuI=OFK+bnP1nY2&mEk~-$aAn2`z&?DUO7tU>gBW20m<`+tXqPYhQ6_o?OQzFH$w{U zt1plIDaK0H-;>v_`a(wqT+|5p1zwX%50y=tKaADpM5YJ=@xmu zM)E2i#Im5?mc$~4=}GtUcW6QbRTyTx1kHBzWvKe+i^Cv^0%ETOm?ETM_F6t7qxEHc zbzvt&cC1;{)VB2-{N0{cVM67s>wHxZv=iw-SIWb;|+2%Ls>wo^7zr?Xo=7L zrHG67mXg3H-KtMx{LjHc5&-`PJJBY~%O$z=wxp+wDz&RVAUcGfUB*cu3%X(GmHu=} zz6^VQgoO(Q6uOh@Etu8JHjAJkE{2n5-Q!V3?cghbSs2WvaT3pZ>n!I7llo^PnVNwD zVi$K*%8^3~KVbT??IjBr5J-dhw+@77nqxT7vMSt}0PP2Vd*=O{mcxJ!>|V0X@SA zS))0_Jm5TiN+Ou7Jp&n9ShWF%{Fid>G7rUjM$|8QN75`Dq?&i29)$G(7cayU`sEjj zwg6#3^|fmEl;SB}Akrp?ts)2JMgO~J=9G#3tA=r;8*}Q zpUgYnM8uq@>b3X!{Sj3Ss4W0nQ}Rhd8LUGfCnx4w<&vuD{z|}YE&)R7@^QEoj|eKT=U}! zKN@&!718x0QS(jCh@Qlg7>N&O=XloxA9>@QLBS24r*Nl~+^IX7P%^?$t0GP=GT*97 zyUkhy2vmQ~Rbg6y-2=%Bf=vc^3hzS=&X4K^{ePk1fOM?DYY(&PxHmxwH^UN-E&?<* zAEiWrR3V^Avkq@GRt5g8qZKU-nb&I1NbV2}A;{~0uJIQThZMQ3m`oNF&83RnM*fK4 z#TwrqHmV|>%C(X9i5Desb@zJ*PJS__;u>}$K<7CtfAr3cM7~6hy3&ne6B+DxzPn_k z$%CblvOx><0+=59)i`PBGST${wX6|dwB%{AMxa_fl~<=^WOX{E2&-}?ysVlOQ};8_N!mI|}n zjV-$xhbK!QW@0j|QQxU`6F&Qr`@fD3{*-+`(71x49O2*qSgCx(7oOMArCLz$qz;Zq zefZ-~1Y^Ff{%hA^b@-=ZdofdJr(XN&{H?1ektk)eb2a!TR*Gt*41B@4}FF4AjgZr~%FyE*N5p z6=EF=s3Q%+lkDWw_rpwr6bbvpKy#`c_VP!}9AV%K;QOQxZxN6>^ut)c zBna=UiQQuOm%L|Uk*>Uu^A5LYhlC!MCT-CNwfh6Wht5fnom>H_LlHv@J^&>FKxv?@ zb{!leZPB@sfO7`B9Y8}LOyLY6jZ=8O>WC)`#_cgsmN|3|%eNg@gm^Ya0G}-dEH+r~ zaNpmSo}0skzl4TfETN6f@s$>5HIXzWlSGV9vvVsBZ!6u`Hfq~AtnXBJBverYUn|$5 z{$zEt^nY7HO5T9%6Ry&tTh{&{|Eri9^_%~|^l9Lb{<_4R2L4W78>6oCeYCMK4)))s z4Qc(0Ullz#cV-TJ#C&hE{4J#eu?R7o9T3#&BN}GqhXj#`R{|8rp0-ZrV2gy`!w&Q% zP~DecJO+U&g4P0@Nx*& zACUM*q&35fcNVZ3z?dtKHV%&=@uu>)fCzy>nTzlbpmW9utYQycQfz^qKU)I=sc%ic zozz>x(@z2d7`^U`QlOaOe&FQ#yJ#R2i7Q8#)gNr2_>*~aD@d%4sURRoUMI{G*$m@(6$Du?(R35GI+tV3#No zqX6)K=o(hTq381E-^%{dR1qt(Mk~=LCE|Bhqmv5CqpuK>$R= zYkS7v`uQV@vuWmj@SS|1V|;Nu{&jf1BZoKFo7v8e-$mzNp>f0IlT=l2s=II_=GFz} zsjwe_u7MF0tu>eeD0u$ryYUARY+J&we>@4r?kmM7%eLeICUXTERl?dGF^7t`%9Q`W z-r_q}v_FDlA{1<@$gP7wHIVQFc!E&eK)n_nD{S})Z(9zE+==)3-Y_lzC1?Mz800x{ zsg0{}fqf+pQDBx38f|)3z7&0B&F6Ci-cU0z*4I6o7DWDIb9{gKx+&HcoS>IwIzT*e z<8Xk$4Ij-n$?12Lw2voZ-AkV#(CdZrdPU~g|3I&!C9E&Ay9(;Bu4v90|8&V}dBOY? z2t{jww(0dx{(&u=FFf+OURE$z`-3e_?9*ZQi&wqyV|LZcnjc?q;ma2- zErd{#OIK4ca0l!WD}n{QHTuya&#;GFUj}bK1BG)m>5FKhotsQlvh9F_w2*wHsY5oO zRFj^C4_;qW%haRLj+7$V!l894*P?Dr&}9|Izenr${r|GYB-&mfhDXMRVBSR$ezEu&2tgN+KO40dtR1hAHu z?cccB`Ht{W)^GYNd>H(VAsaQB43rMBN*?nv&giK00GDXYKIC1KohOAQM$ts2_9B~y z&&C}roA59eR;GtuSD;_YAvc2f4L+14EfXal)jb2<9?Va%d>1?ZF*h`zO{Kb`y@gM> z*n?BT1s+eN`3!rg;#)b!*N$jgjM9peAPXqw8R?Dvj!XMl#lLV3QR)g}S@WtC*m8FB{ zv4q2&PhV3RgZUPgP)B?&Bj2NhKU_vh>P;o@)xaGGX-IiQ&otRMw1^_3zI(2mMH}U} zK&|rEVxi%4A|M~^?7t-Ih9PY4(;LoxrQdB8EAqk}^7b;nffh3tpR1(GcSGpMUG@Sr zhnef_7ttU5Px-&*ZX$NW<8Y(@?IW1nc-ew8vO#9ADG6IiK;sERKtpq}*Zq+9V9Trn zdY&ThZ1jjNprb2kNtpbuMeyzQ!S)SwHD-^Lz27kps!&OkK= z19kxPGD3+_-p>5DX=f$&nYS@FpvW83Gmz((iPF>nU1S2bj#LeypM5JH=t*qKt?m?mOTE6;KF_g>a5ncC0daG3Qd-XL4!WFa z@veMCo&&Y59B{uj30JkRHC!IIUpRJ#?m(gQEHiWp1CMubSmA@fh|JgQX4r`9r;7;j z6+Y-?Wj%rcD8rF|_~Pdum#;mDNdf^tG@V>WnsHv=Sew>;u1d?(@=>Fbl#d$pnsvp7 zbrJnI=r=OiIa*-eQ#hJgM_lqS5$Q|n6?>P`r-9_v%JrP^qu4hRICWOrnZ^@=V7DHJ zp~_C34qO+gLIJoi6PnqbV~#?-5RsR;^Q5Sb7Zq#-qa+Ica+>rWtacst2S#9C{5+P$ z_>I)mt2r14$PVScn$bUFjlXP z18bzVL^Sp%Sr2>jHjYI!0iA>72|ywZv_gdYKoEZZA~U00 zKyU#0KAW?F8x?z9*I%yeDzmupK;J|3+o2HCM`Ih1dLz>efbq=>jv$Sdi>p+C^!WA7 z{}PQ5jebzY#)cJ4@S#WA<&GWnCw~?YXwd9z_UUFRro4ud z38r@88tnC1hV4Ct59!7G;prhniiFRStb3x_%<~eLT3u=`NvA=Po)koo$X5NZ9K`aL zk3UehED#ve(#_k(elJTtO3SGlPWP_EQ2nQ**qIF}Idv+)N`x(&+O*+=&!*q*=HtBW zNdK$4&${`sEVo;L^t8LV^M6b2-IX=no_M{s@O}vQKGPEDG$5tHoYmdsmqOJQ7Om9E zzRjwfyup{HDi#1u>BE55Pi9pBQ2UVU0(d1Vmr17xB2H+^BOK+H5_(a$s6(wcdnu*O zdmiII4Z*#@!||I3RR`)c*qrQZc=>{X0cngOS~M7$k?OBj1vJB5t`8R#xlUG>IE;Re zlCVtyR1)u9y!4&)3Bpy z#CRzXSwNT|{o!`Vck!V3E3h?9Y<}T`^E=49_I%Yke$AQ`(~yX9LWiAl?6B6FOkNsbRn7Qw*}s~hPy}IuZhruD0a$*O)(M$mg&n30NXf08cv03C zJ6Frt3LA;fvbzZuhM6DEW@f$zLj_I%&3qlu%%!P10+x ztgJSk8mnBdd6meeZgit?jd_&D%Q74xT{X+k!9IsG+9+>A61Gorire88mpI}~v+ zQD5y`0iQtG(x{fvlrbULI|HB_0WFQcs0%KlG#{o;R6AHO?piqo$>$}M`0X!smP4=Q z|G_Ojy2YK=>R9A0hwwYYw!FbMCE?ccz%66l7#auAKk$U!;Os`uAnv1zl<@;2@FD-x_ z))8&D|E_5C4Gv=9gLX0s1tXJFY!4Q=&UfW#4i^ZngJ_3|lo;XtfdF8@{HGDoTj$4D z5F&8GU@HRl6|(!5Mjv#2{`}K7m8Y0MJp{kmoL~s+|1k~QV|Xd7W=4$%a4>p7!dqgH zl|+9ym}lcaQK!Jz^ci)5Lh-RasIoz{%hcIQ4wxC1EQ?mT%2$xCNO+klh8}=`MOOY? zqEtB)hroI9NTCElwX)&UA7b#>PDxNV;58g{#3N4rR}#;n1RML%h;QQ~=@skDzP0;u z@f_7*Rr|Mj>eYjbkBE?hh-hxyh!#H)`Vc>bZ`3`*;?#@bL+F}z(KuJGc*0Lar{Awd zMTAx12F=zWzJ8E67s3%-3(ieNeEUX;lpFy~cTrGEiZ48S4!(n#ZFG)t9n4L#+x4o? z966ARKPV2opcUUo6P>$Fg}&hK*?CpvToQB3k{a^@w(#Ul4LB9>Ex7#o(W?1tP4o{Z z(f;DI?U@~=rk}U@!i6;iI{_%S-*w?)#&enbrec7G+ZPxE?4kQDfN|K_L5JR{=fzqh z98B0c``w$W)&JCV*YogF4T$B@vvMMuEfgl`hgdSr8lm6>lRpEQu;eNnROm6G&38FB%&lwNkG|Cl7EhiYD*>_zZCEf}~e(uo0iT+m2?tUP&B z;YS;2sPnUVI+HA8jhM_Pn0Y$BO;{X$zG6s8#|gg=G~#wb$;lg62sEucZtMAh_?ot` z;#C2yHMh51*|i6uC%w~ z!Ns+OM(@|eR=mHGrN2O)mJffkrQ{k9LtBjK%whVqc$voN*U4Z6GYt38ux-Ra=ZYH| zh>036C@@mGkjh;URq00OTOYsR(@_0M+9^1UOY_cty{X(;$j9{Btc_F9NdSe>yx+bh z1<@h$Wv-8Nde2*7IYb$3AM~>!?uf@jrbwuir7TR5CfCT^OmC#jW}r^5Q~~R3CW78? zpz)I5uzlZ1{R28Gjw@>G9p~W?az^XO`F7V4n&FUs#e;y&uxDXK1C5dL)3MObOR4sB z5(cbrD-X7i7twu8)PX*yHYGOVf5q0}x(O|Q$hU*?exlH19!XyTOBSF!=e(32y1uLU zCU;>BLD5#3DISWLW-avD)TVAl2aG~D|3XX@I+gc!B0Rj_je|QYufCN2a+jv972nPp z8I^)TH#d+J;jz9&a|hzwTm~I%kz9^-#3KrRMc<4lAq0N*mkB0au#r+C{X#79@5D^# z8U6>d`R&?s$X<{h4zGj_7^20TV2*TRG_H@JTA-BN=m+H=Otq$T58g*+HB_%FEf~{&yV7(a|xC>CQUumz2JHi~k3>evSW~m2$*Wez9_G z{y&P6*#Zy8$gvj!%l5Wl7*$-q&AhzryzR`n@v@}P^BI_85E>d40ju_}2X3v=osy0k zJba$W#k6?;0myoF$<|%xlxp`7?F_hXPB8Bc!VX}w^`(i(7L*m z&v3@+LRwzL_BnP+Te9$aB4Xn?zjGhYY3}ikbF_f7DZNKFyDp9)GyzG4xCjw3CyoZD z=Fh|sr@_7WgtwEEx`?92)%}DUQCDY&dK<=*p4DHia9YD#5z%L-RJa_Lp#Ngz^}FrBBD;AN@?eK_-CxyfGVwO^i$=+j}7 z7Vm+QYI!I*nDF@4_UA8!D0td-_<6zAy7#S|@ON`L)y8w-0FZFP5ZmTgr@V7gPbccD zo?ejOcMrUaJ+)fihHq}efZD0x7|KRQ5IgVFEF)gr%|X)}4BL+xxIy$)pA6}4GN5Hy z5gFJ9-E?ZjCN_c&b#n);NTv~Us=Rhj$2fNs8oY@J;R9}N?nr^II^V;ETJEf<@dYzZ z|A!ca=Y|zLys8zNT(K=VGHMMKsNs=OSR;)4L0}E5gHIwWRaoaeueOnQOB*c2bs%xr zx^x2pK1Mq6_PV&kX^oEC;y1U*-c0n^NIh007oYr`h=k?rwqM5$2~Nql_h|TAS{6xV z6c?DM64v7Y1X#c2af$&e704}Kfj@BU#z zm0QZ%$PC`$!0YP(_LP{e1)Mk|ARIwPYYQjPitjtn9}%Bdjz;kKcgl*cRP-z~8Dd{m z)-R{$#LT!ZY5%R_E~UUm0%-mLU(ax`ifUKP}Y?F#m}_3pmUj)c?L9+Lc>=nlO?-kDFo+;fMGC&qif49>QViVr1A-c5dt*SK*3^`v z;}vPRq1Xg93Q`7ww!m{;1!@ae`#$eCqc#pyN>}Rtg;%!}( z#!;eKr`YY&=50?~&yMmd<)D9rQ7F7cZkE5<>zhn?N$iJL&4be#md=s%tspBGF&u&v zLfNi_OBoX%jT{s~u(yOdfp-bv{_ltg3GLEjB02{Ve9BqFp)53|c3biiN^9y|uRWR) z%VBMg&l@;a3_n2Hg;z&7_bEeMXhGr>(sNDCc`)9Pu^#XcL$HAn4+Aa=OfrB!C>O>G zYS$x|dgw&6SOino&5L+QvS5Tu_(OHm8>enSGd2rIMT0Ib<{kXMBN-&M1NQJna5|yK zzgqL`NFkW21~&E{N+%30Lu8u-wQZ~ z@ZBx)caapQ;P$UbIQK_oYC_tzc|PlGRsswe%A#oUIi$Ef*}YhxCGgJ27W_U1AIP_c z8~Yuo)o#<~X20b{3S#8_eKxu~M85^ne`c>6R3*%a7rhA4Z!iZTs#2WMPIjV>R5SeE zpUT#@@46gB!49E_gC$yXl)fPd0%1h$PHTMDSYVx24Ovz8f2Mh|rC=U!E6-aA*x`!(~LgZ}3^!PrissuQE_@fZkKd=LIvXzVx6Y?~VTwKXdb;9G${2 zu&JM>>cSGGxEdwBR9r}CkZ#iPbanq7SgoOr_Wmo$>J3ujzx&+ zg>aUUVKN?#by>x051DR%)#q|Fe;K7^8^zerb#6cgT`*q(x70_t@>AB7zjW2+b z*cKdd=Da4RFN!nZdFxf7?ncS|;EgkY4y;NBqraE$Q~oS&mOScue}?{HXmXp3aya?6 zx^^`FgY1Tu)Ps4#mQRfeAP`3o*08?gkV{?(`Fq9x8I@E?VR;`lUa8CZmxRH~OoTMq zz|ny$O={$Us(i&>t9M&EN)h$Ioq~;qT8F6yfR~iqlJ>Svq9rA1kG;>YJl^mVvSSK? zf3f+8F2LycwZZxEn8x>!xW@SVCOJso0-o+VjYUUzF^}uy+WnqUe<>I&vs|BDz^Cl9 zBxJ$8(o3-O$$=y&VD719GiCheqwZK26Ba;iBkiPkF{j+Ay4dTT?o>+uIru=5#?I*^ z>;Q;rP|aB#=Ovm3nMd^g05531)P{Wxu)|Jd^|@cP|3^w}vVy7EK>NH0RHLB&LL}K$ zIfK58jp(HNG)KIu&}}r(>M#Nhc@fA9@;cI!-rlyi5Vkc2T*ySzA9&PX7X5DTPrt|# zI6Ur{sLO*Nk3KvE0E2q15Uavd6Qb=BNhKVbHnAS|2VGA?g4sCD{F`g339M{*;k*t; zI#g8c=9G=@p|3sJT=)}}fkSp;KtX6agmL+Qe5zz<;wb+t=lz{9!+7LkkMSh?BVI|g zZRZD>TNZ7x4472)OsK$M9(F8CE)b)6-O07W?3TO@zX5syK6v*ISP1Vr#IOZEdgabe zzBd1L>r`lG|IN*;YAU7xJ-ajN-PYzP)aWNn9^p->xYInjZ$;iPG8E8@~1}ApJ zK86H~mVhI6L=){5&2G;Iz`|g3|3WavEfd-+aNwt^s=Bfdne6_NRWon2UeT}{JK`f#2oBrE zPnwF#E`d}#Xq@v2=^GP$(P=V7xXV@eT(#Trz-NZK2*hCDXzgxHIb|+TN+& zrR91R4_!;m)>6245s-6&QVFQG!Ocr(H?!yKaT)80GMQjU`v*o_dgsiyy zu=7It{8cmxpjSjTLU3Mx38MKdmIFr`$(h)^a0yLTIqdtW3muAW7VK222%qHE>;jnK z5LOI4%fspVSNUiH@Orb247+sYmPO$}K?vx$ank}(QCIH?89#xiw|GG)1?pN*o*ucz z=snq5WuxpGewb1`-IkYOWf9uV;_>xnJ z^i=u3MBLP0u5z#Q51w02*5k5xZ75_r-y z3SW?e9=;C;Rwa)i-z1)tD&UfZkz2#L%NoeYLK zsV#6<^v#pEp3}<0c$k5KpL}7{buU)zBKnORU-*R@8vF6VPm4%&lkfS`sMlgqxv|^5 z_DYpRS*LM9r34Ascn%k-5u(tDsYJ$<9P74>>yOlzdKF(Q`xeGW`s%;xcDl4^CD78c zaJES+KNp4z?8xRu5+#fQX9EPzuu=2_Iz01@2d%4EK@7W_OpB-;RFC0~LfLRgAqpY{v;eXU`m4n|0=Jsj0y?Tn}9_D|>_kn@K-JjW--Vl&BY4 z`1Y)ZD-i+LL#L?M$C!$b4F13XSq2gHe2Lk&)09~vZ6fX=B4F@ULzBeYy4wQ{f-vu& zL)PzW#*JfZ4VB^wBr+Y$0C?N^v4o`c6;pXM!@pQ;e8E+?nfc}(jKJZxg9M|Hp3svx zA)n0RTuk_A<7sp+DOz@|xn@gJ1A1Nc6dsb~WV0Ayk!OnUZEXi=uqP)XaTO4bFzq5s zAq%f%q|Bu~K|>i!62G7ClK4Fyy3iXps9l`bC;f9exa&)gt zEr&Ra48mk!4p$D|_DfA=`cvCeA7e!S2U`$!nNLueDLaBYN1TcYB9#5~aTlRvI}(T< z#`LLPuzq?-5gHo$uAQ)+m8wJwHo8rw@eK5@PGk1%;g>~L8{46=T|zgX7~c5(iJNuyF|>J-`>KKy0)8_&A@k4NEw zNKB1B39PvBqB~rZXY_>syDw)Yoy_LK_kRf-2)X%BzV1IgwKqjb#L<6CpBY!%{_U|{ z_d67ZoZE6I#ZmC0v2<45S{(d_Fe~8Yo(dmce6+Fq>E-)R>bLtQK@|-pAhMQ1ybu-H zm3&#%=DOt^h#GpM!24`XBfH#)BikxkPrtBhH}M5HG=f2FjwFJB@K*|55|alL3E!v` zBt_hwQihed)QE@}#J9-T%=%8%3+d%S7-kCsXe7CDY@h%n2Ouu+|1COvjcsf1hYg65 zwuk9SPDVX448Y#H%zCiK~N%8H3p@We`f^X$VQ8aG90+5@utWW%J9tm zY9rgX(Hxmeet*0Cgcp`eUhd5mUA;XYE|uks#DKjH-$h-(5O}gT>uFh1ReSONVQ(-hdwz1 zaM_z}tl5vw9Cmau6J9Be0Ui}XRkVn!gd0V`J`u#ecf!-puS*Qx=TTFn4PdC`o4^_R zWa@!bc=ro$pMG^+5cmjLKFJ3nfX#y6KmceW=%Usgj>mKGpUh}O-3Sx%9(WeA zyL30z_SWEYQ4_;emiD`Wa5ZlPknW#lV|r?D z?{HMv%mA5kPIYOsHA5;{O-o%g93d4`gQF1BPZl^3b@19zRB$4gm^k90RHO=;c~5|q zWbhPw5~7kOjGqK4IFrE&33-)>ejEA|NN|$$WiZ%-R6w}QniASQq0j&tpGO1d(ezU3 zO1Qnag(9YhB)VjiM(R6UWUvbgH~;_WxTQb_R`W1Coe(MOek6~90@+l|Bk3)_5%9rUhVv{WX*hWZ(#~)(V7W zJeZj5#%_q6Az^0l2hYx(%gdLS{eP9pMR-k0D*_Z7qNk$;ci@^J0D8P@pvMyi%pdue z4^<+4Yep0L>!-icLDC9RO$Z3nkXWezSNOXsk*np^&2uZDu;1uG#OI*EF0w8A*43{K zqQgL(jO=OxNNhwy(OOy})U^C^kZFe$@*ykY=g<2}+VAg)ZFbsAG{Pd-K5ETh6?-ah zYm~sl2=<%cpFw+i_J_cs{3?>qi@;!j0s{kwiY`I8uNiG0u!@gX+27-PD|;E5GDg_s zC*f5Wk}U@T(y95aJ{qDsGkZh3ub-f%!K8BPmJ}}$v;;V*QMJZbi`I4d5G1=p{A(w~ z%XkGKhKf55vsTQj zXfYLx4XT4%6_>6#@Z46K1^zNha&GQWfPp+RvkfN=O43wH-h%SqNuai7Ai zpgpk<2TVT-lttO)NuZ`842M71-@RV29xv#PoDScZ%?6xu@|w)Mabp?Y_Xf2iz~6*tGP_Oimj&{G3pzZfbn{xQNTwkyu|O4;XGf3XFPJt0zmI?&i4A zu|QsHdm*{#CVqgT_%kF9c8o&0?NgjG=tl3|$>qGO5g>vpc}>n~@jq1Kl9XDJ!uSZ3 z9a&uM8t{^Z~%b31+a1Z+(RI?;l1hBI(ur zsn;}t1+vqneUv9?N-cTC9zZlCpvQyQt~|<-el(2HvS1m%_Av1h<*Nssg8`YA$bcOW zp!S-`92b=3?0Y&OIHryuSH6FD=Wz1Jm->hE1Ze?n&&avqrG&+!3#kONAJYkuHqJkw zexPp`?ms3>*g|CG?4`l(mMD0PAZ*sBR4Tqw^+Trp%-Kl_w@0Y$zQNw@(&DHe(lP~g zX)CK(Z)LM)wU~qA*eg=_R6TgKpSQNE+U{ z&jSi%x7OBC_t}}--5{Q?Wi_8WB_NucJYRpnmyW4W8K<&H!ED_zuvN2K+(&)|`P*j#k(p}=B6m0XGO9(04aqS}Um=T9G5@YC_ZHQd@HR_yp0 z-991wgQB(MoUc9Co|kk7h+k(iTy0Gca=AFIsuWj!BP94YvHb&+cfmP;8A^om?D=!q zX1~_#&NFRgjPyC1i;fkSkG=v08%!H86{li9st7`^@%g7qp1=~_d`*e)2LZmM{J6E7 z1vIV){rv{Gn=fTwoAJ5t%Txc!(I)x5;@|E?>MT*aum{j4O4~c@@2V~UenjS#QAYdW zIRt#9@6p|RO3=E#KI6Nwv13exR`(B-$+1Kd*13ZMIiP*cJk)r(10$5{FMKDXaH5lkWRIw z23GdxeA>JOF&NFKZ&V`E=;~}WRGpu82<|=)domFUbQ6xJ{dS-@$ZL7cUQQH-S=Fez z7zj?MFgj0Ato+TlX&k%&$(XB+327|PCb;Hv?>}yU<9x;89OOnxdF_t2Iv3Q`D4a$CzpYWbp5geieBI6< z!Dl3uBKVF4E0Q)IQpa!8;{~r)~1*W*zs@wXxkGA7x-Pi=N*a*Br zqa?U<&xT;XrFX13OSs{bQEPoP4Y(aEDxX^eJ5n7nu6ZNk1;f;M?;KvyU~eWI=|<#I zqTr3SImq$4OupqA7FZd2%U<`71@T)R?BGa0vZ{=;xo1%r9I-c8n{9RFX)l=|od;}6 z5S4h|W4YZ^iyIrr#?IBXG_*DpgasNAo!y2-rBnKyvGM?oEw9Fc^=+wk1OkHcGn(V) z`G2~c!|BWTbM!X5JT@ziY}$-v#vKs7)%*B>Njg8uX?_W=B1q#zWS|Hb8sdbbcI|Hn z+_xJ1z-?YCHuXr0FXXSWXvRPeT~h7VJ0o|kw<4_tL&E z?t@XzHUB*&C3%8KDM*jOS)S)Fcd!>Cy!XKDlEq-MbHXQa!SK4B)*mV@>|UwrN8VT; zK&$3F-Q9fUo&)$=3J9WL^V+k{(bUK0uSf*pFJPI{M3bN@ap*kU3BbN3jQEAD_-K8p zdQ_O?EaNAeY9=GKq>26G(3%~l2(ZkT`<1ENw)n=t)dZU{6Tv#U!K!3}&r@`K*bWD8 zuTJIiRC7(8@<+RzL!`lxsf;1Z@fDU!r`KVxD0KVuJ%kzeds%CTalj@J@?GyzFj=sO zwFEh2^QowZ{c;x)`u)#&gru%?CJyhYJWa`K$EI^TLV-x8D~ub!MU{-R$(`@{#oWPb zq&~ZH?KBxXZ(uq)r};o{w?5^q5GW%xV@M(F##y;Np025rELNfqA8Il+Q#t~QDG`-= zLG#$l#!%rveFUGr%f#W;t9`wW&d2`W|4k5xJBrq#zF@+e%|g0x{;kF(%sQp}80*Z; z-YoXBd~Evlnkjj+n`sv6lbVdal6rfg*OYf9HMKCyDnl518fSzL?2>-#>N!eLX?`M|3QM-I8UetBx)$GAzenCAI( z<>S``=ETwYzr?)Q6B8agKd4ea$ng)pp#9~3krH{-1Gc+Q-xa7{pOQ>*6kEhL8tc<~ zUDYIIJ@+%KcJ_z;8MYn9Gb!AwzQnw@lu>i&>e-%|N2YT&ruFEaJMJcxGov20XoQw- zQ}I=HTff=Ue_#-CT1_nBcXMBwXA}{VCoBTT4*$)vSi#zY$1GB7jop9jbGC9GrwGi? zPmUW2jPn|jX(UpX{bc`&vA zev#k}GHbEVvjukeRxIgTD-y!A1P+!GLZWM22ULp{JkkyE7YHUS&kwQ7@B$?whP>xc z@X6zeipS0VezItHUI89a9 zOH{cWmGP3>EsU@9zKJ-VSL_pqpUR?s%OpB66C^mL>T1nJ=S`U@sD5BR&VOVEp;a{zYh9CHnpo34>&y{6<)UAtgzm_9Co;x0cQh4 z6*(JuMOlThmfr09B>I*QSk5`iWh&E(cpUjS38O6Kj*l*P1*)XvESSa_T%Vs5fC5`t zfjIf&^q4}3+WywHPFr&Pnt$tyOV_ykNUYKz9hWfP4$Z%xiGMVQ);kwwD8ky zJF({s3pwHQl%_%4HF1s~XQsZejCjkVH40t}(#Cpet;GQAV8+p%Epj-fVcrjC zN^p)ll;a{|76c#0o$mpqqi!)6^DN~;FBWR&$saYZ=J=^=6xg<@ZX27@e zsc#x$Wn<48mS*J&w_;v7E|f;)u1j3La_UTfc68Fe$bD=7ocy#sHsZU#JhKXAC^?<} z6wa8g2X(XQLndK%bOrh|!jIx7G`M$t&b7ry$r8l~QN6=Mkm<|nvCEMGS8CNy!Dc7U zP1ty0`^ZZm%;3)s^*gqsQHeJ4`-(l4-wg_NT=xiw){c=JW?L$<5HdVZ?D|LK#H#1x zTCE*gg@?Vbz>q4%zS9zWTN-o|uIjE`G3JMTn#qD!i5;F(sVs4m9e3}#1#K)gymz_k zn%*JSl~_rBK`X6t^d^%m&zyO#rCc5c-r2U<9AjBVkjA2MX}9Y5CXmpPb(eyjxk@I8U|LI%uDmh0dGEMf(6p!&UJo8-?(hN`C+Sts&`K)pj27U6sk< zL7M~BHUokVzf^A=@$+mHTo%G{^$nH<#4==M+&G&-i_{ejuuxYx3~CLwmN>`s|5$6# z-E7*Uktd=0l8U~zgxbYnO|rZlvr_7f(>~{RbTUbK+(%}6tcoH_n{%$8KL*-#J250x zYUrli&)y8W$zC$?c=_ex`UdYTa;}2sNF%ribLH?=r^kONNHm28hwk_ z7ln?C;I{dBHlK5%ntMW?^DZ`~OHXta9$cP{N#hFV-|xE?DA`pF-S3l!Qo48fCuM}u z-zjfDIS+wb(?(>iY z-Lpc2s9H1g0XSh?? zfm3UU#%n}&FJx0%%a?v#;i9+3VwboeEQ?tr?wMxm#iJDJPc2rPs;>zUB#8aA?G#mr z6obD~{x0+L^_QWnskHE*LKamFy&{npW-F^j=5*TD+yYYm0YGg3-~fxtg$#7~xwd z+R=dF|F&NhyGaZEYZEc`jQ8iC_OjOYL<2O6(WJj|(h7*J-L)-CBv!PPqD6H5-p8#G z(U9p`tjdHwz|t#1naAL6zaMDpJ2bLNk=RSH(ANu4yDDpvODcCi);6#23hJblwtm%# zb!V^m)_J_a>T+|~@4R~i;dhqUWPG)}o#f501+*ut29o4|WPmq1{Hz1xbP6;(65UBX zGxGNY#>X)|CkzriDd^WK>`G``@^xV=Qp9L<1vYvw#9z`zS?|zR)zg)y6jLbTMU-q* z!oPjo@cZ6{D5kcD!8u1n)dy+2Ldnu%`-hNL6;7Vx`;@WnQ@?YZ{SQr8Lod|xwp|a+ z34~o*IBkJ9{N0!5Sm`o{SFbCEj=w=;xpz?ps)@55Cy{f5*LiK+IF?oJ)$Mdu$us9Q zH}XaH*3iyUc&^*;iRL7Vt?dpCK9$oNqCIIoJ(@2G`x(?(#A>X?eC_delul@<=4lsA zufLyk-UaV+NmQDaCHb{?;c0RF)aD^h*h+G(o#E4)xH8Q*P$=_!)zCMy(EtpDsdMOS z00@@ko71$p?m!mc;&)7W3$yhxUtLz2E4_dWZDjkVXKn?Z!^F4}Td)r5l9UZ05`zrb5 z&-KaFuKS$v(vO$Pnrdj8Hlw#r7X8jtsTr^u^3=2hX>80nwF z6Lq6Ca)hEcyP9R=zfEFG(4(QxF~WrazCYo|&sc`?S`3Chu2*;Vq|9_~i;HRoa7+zr z=zNh75W136a?F`+P^la*!Be+9!zCmivOsaSgc78dxqs@RM#$MN%gn`^)b$)_b+yg? z_(p&Elp2RiU`EU61~aCDt?HE5zDM;N+E%Ndn7H|P>Iz9hJ9^_TqN2mJu>w#v`eb?qJn;$fa$eqW&BkR-Wdwy0-3Qv)v)gykIv$`KPj&P`>&=!1N zjGa^BlV@TwBjHd}?`vetfvc{-K#rfZL*CG%9vH`=-$_`4nc52~G$It+{h zzdgZCuA`wJUD{Bw=Dy0N_GW`qXxF7kFMUJui!3@kc81A9=GtF37r0n$%D#pWlE4G# zc-|T(tTxo}$zy!`gUI>8)n>_HotlGrL#BM8S5os#p1ouEJYKc;a6ewX{xsC4RD@m3 zF^eSbqE0!@i;AzO1oM37b$-|~nD+`_EDP_sX$_kodZS@)r zvnK(0Yj@LK<&$k9(<$R_>92^S4lL9j?h-;E~%PXq!E<`n( zjijtj>knIm%+5?32Qjk1i@tF>jEze;Tk@&!&z#ul(OKnbs~MroP6S$1a&|7|?WYgW zP}7Ff+(uTUhfcbvq5P7XVvH;Rj_=gIqoanjw13s`$jr~Qew_Q97n>3rnMp{#3#fea zY_VD6Z{dBPTz3VGEC7DY?9yl>gOH$wI zhGMz8kP98&w1-N_+aF7eedp!;cfzY*S;JBYCO`Z--Q4(aZI3DTDwE3z-@k$}2KWmK z#^UYi2!)Iz4>ES&nx4#!<6ra+k-uk+5I&J%mo+iNH@{i)-s3EPol-z&0Z{a4Ad9h| znZ-S83t_;_Xed>l^!AK&{bM)E=SeQ3!+UX_n_C_y4&3{d$ws9#mcEipKg`X|ES`(zo6A*7 z(Psi=y|~))wjvkrp08G$krc~S6hUMmxphvD6}BJBcskBpMK!(2$Tk-)`1?XodKEzRn;nl)7k;u8pYlH#M(@U%hMZY^T3TQdX2hYg;}w?v z9A&r49;S`f}m2o4G`lKki7I z<%jnm#oC;UPJ)TD#&Z;@sBYp}>wb_Txpkn$1z6WJWD3&ick9o^EB?1?9G zk2jttsXY!2d+6dE`&yXHIVetQ8nD?HYVqy9n|kc~Nz2YH(@ujUKYvYj(ywEQ ze!j-7r1u2>Ga$5Ffw3r!DOuCwluPtY)*djfBb1yY)n0czM?*3A<8$J5}+-m5LT_P90bIu)k&Z>btp=@U<6#O6l?mA;oH)@QfOl3SC(6 zBjGTGouoH5MRT&DY^D4beXxLh1GJ%TLq{IZ*yHBx0M`(Y?nhM=0$aTyA3l?wNP7_( zd4I!$BocObl%j&IWL%z_a%a$ITY3T)irDDD*McgGORq4g_TgSV)8@o?SWu{3UzC?Q z|C63xMJ+G%7U?U`zEIDW;IsgJ{KZAwpwHmfqT5Ay;q+W0tS=*&T{b}0j@P(4)MxKr zA9UFg?YBS?W1Z$15BMAgf#GcL$XpOAZIr7Ti0+eKVavg+>*q+|I2~K@RGJN!w#LW>HCWQ+gb#{pZ-PBAQ;W(;P_dUBmvBr)2_n8x=lr zhSk@RBt%|YTRXZ}QoMd1J#bx4Ks~%a->RnhEi%)^jt}AP4noo=Bf_p1%59O#Fw!vQ z74+&*jh49CW!2=EY9QfUTwaaC7ELE-sVw0sO}2Sxob=GNrZe0U` z!%H%`xOJL0)#;>KjHHTa?$OGnIlp~dJ|oQmG84^wUzo_xCoMO&L82Osp^tp|N4BUh zJn#`~%NzNTV>ensXyiR1AI(6V+7O_(Xt}}5rxjeRY#p5YH8iHoIE%8e?Qgfy*=%1h zDHSCm7nrqJ_%^o?v3ABk1)Stbo1@pv!j!XGd!r^%20Ox ziti~<1&k4*Mk`MqhYS|#>Ke3WdwZXmq^xcnnh6jok3ALQya6H->%B~Wsf+P0ok}fu ziD&AJaVDF0n_6M=FtVa*+$d4M&bS;tozxtyQyvISss>E8e_gohTBWfwHfY=N)#0#a zy{`ZIy$TO}&RfXx=+`Q#{Ob+!NOXy`Po}S1Uj7i;dVR8s<&969DSbN4ykty3A>D!3 zp-_-&{Ij0Ces;J%X|4VIoz@2u5fi3^&2j(8zM|&o#}cD^k3VWKIsC>ws6&l*RZ@?X zJlwbl`|%yspcCrEx6Jh5xOM8Hd&{)o)c8f+hC|)zwl`l4jG@z9hueBz`o=-MAU-z# zy+=D|=R9eI)5)u4u4Xf~Wf%k3^B8}?vQD0Q~=3aW0i3$azMRnHEt8dv+JlslFUdpyu3KiL{=kIsTCKyzjxBV2o= zTsX_nJ=7x?i%oAo74hc15>noMz0aSzc6`^U2>1{j%kf%xP?|hbYS2L?HbaUO`blaA z@6EwaStZ3~4++(`8;CALY4_7$X-dGewBrudPoZ-9{L=$HFjjcf+u(urE9f?~O3bP} z8OxFdu$PS4k{k64NcT~p1`uaz!>T4-_IVzvIO<{T72)KplJa!-#{0(D(_guGE?&{H zwh^7YY-TP-ABXk_Jpu|_>37uVX?LsOH}2E92j}b+t6kG|Pnx0|MgR7k`^<=-h)JlU zQwnfvwn#jI!;e`jSXOdDFMV-`UcpxGN+1&n{4^zvD_xIOPRD)rRs4i?Gc?^;!r+`rBd=Z?u#O&DYn#L{;Eh*1J zoQ}1lp5n4!6%MIg@PprR4*)G?4`AtI+H^-woV48gfX+!?sg}?^Y5F`ximSGO> z!T9Lqz_^JSuYZ#vZh?wS~-JQo$`fxvmqP;@O$0w#ZfXgPD0b<%0{)nn(1-F;v7Nxh$*4Mgd3X+$zZpeiNr7>62x&YWu2L!Y!*p?=38H zCZ3pbuvC#rz~?2#RTL(ONY3VhifG#;bRPM^c+PM^0$0-hy4$+hM?@_*?lE&sb;M|> zis5azW!|OX$Px9rAln9Ih;_@tSk72wHbG@L0*mG?D2vpI-JyhlK-qs<{2I=0Y`vy zuMI!ZU1@VX)ZoIZye%>~(?tu51~CyzcXyrx1Pq3HAzJj>xlNYw7fhWp*E5$YmvbpA z8@uw=YP0B;5$}>=vZw+-CbRa>TOVSFV739z`6MH7_oEaErtCk5C9n#u5;wHK_iy`_ z6e-O$lk$w&?w}zRK|Z>5*Dr!XZ$JK=Jdy!)+@Lv&@%{rB8v~RpK4==3LX^ih3Y!A) zquzJXbr@s3iRDl7P7`7y=b$$V@oR88vYNPN>$|5N;+>Go=N=o^sWLnMsEq0bio1zwkHTGkAKmitA=~*{!WaVtEE5Tjj~~*dBt= zInsDqX(BgI0ET1T;#l7?eWrhqt+$?;_r_!fE@<^iIi!!_ZMBZkN2AG4wyR~*FTI_h z6!~amcClftA`sKWBW-@tfLwejX=GXP^4XpF=ywa$~`(#-_qQ)?Ux{rH`iZ*H_{l`Hw z|CCYpMtX{W1Z6Ld@#sM`GaWUmxci4`d-E&dO5Y+?3YWo4q=rE4e#T_je3pi`$v|5{tt8FBbZuTzX=H89I zvN<$QyV2&8x!esn$@gA{GzL-83w(q=!FBu}i0cRZMSeI;ri;+U(r>}Qi7f=ys1DyI z*7uSzl_k}8w4J_Oj8VzeA@7i%YF9HSGXq479m%VE$h%D8=1wf~ZdO+l&f9i$VZFNB z3JUm%{`8YfU%&;zeSL|rUJ$f5n7*~Gi}ohwwHT{f3GhE9Uv7X6&GGk6{uzZUB7Y*?ysx^JO0U ziCXaPckc&J1h8soy+Fq_S*88lKRot|=zQmzuh462p)V#RRgQlHr#6hW9&;(e`@B9! z^ULjY(bK|zJ4c#D5H71+1Bs3)+kFq73~7Iyn@`~yZa*l95f!72-I*uJd>tXN%%f1* z*|N!FG1WTAHrg)nR^xcScQR<*%-k6>P>xY>+!&UD-W024cLVSZ21)U2xry9a3vx!- z0IoQ7ZxCN2!*K9N?c9#%WxP(1{CVjX!_~Ltxj3p9D0e*A2hC5*R7SC^eu~lyo@`-d zfu}n#YEVZk+#DpiMjP) zk6zxW($k^zvel@!fS4JU1(zOv&<}cghl`rK*u-?CctCRDP1I6S{ND}TOmbbe z9U3+aiH?TN+*jcp5Rx2l&g$9w&LXLqzbyLF&a2M+`iJ2^mskUpl+1Pl>R$r!g+`;N ztzA)Behd27G&&lLFLDCE1vV8_6s~r08wLLSk;x$`PS`jbdozV5_0J2XNTGvLE z=HY}vmu3JZ4ZfPJ@*#kM?>?u~>rIBLdTX5-4F&f}$a(YNY-D^ga45nSTan4CnF$%R zfv-3Svq?SVG6HV zsj|6`=9SA&b9&00+Q;w7@4N}~ru+p@#HA84{oadiBlkk6DokQpQ5fX5d1X@f2XJdm z7V=W5JGwZzmRM6BjDk8RpYbvg>G*4|l9G<~m*+-71==AealQkh+M?sQohNX*6<#W* z;FR`);8+L&S(S1+V}9@FpEp0!3Bml>pgY6iV!teKnoY9 zWB{<)Re8*SIO{||$Uo&foIW9WM_^9>eZ3xlAR=dVwz`Nq9Z7_oxG=yesipDL;^x*5 zA6r;GRkCp*1tp|Hf8bK@K~SJFOxbME*m7ppRZA$rZXW34!q|6$`PsiWuH{rfIo`|RcurBMMi3m?U4jh{^}g9f4`}K z;BZuE@Cp8_`gVXrmhwL!6Dpu7(9_%s2r{a^%N#sYoRrmlY@BE+O`anEl{mJXW-EnR z^2}Y`NbKlB6r!G5C74o(dumKf2EF}Z5lt8%ypY;1<}m7#aK;+X?x%Fkr57KImg5k@ zF?w;=LQ%;btL~5s>hJ#<=`xJ9E`2{_|3$3m!vT+pb+;v##{RJYBL@)?zdwSQzx_Kx z@0)%<9Jy~Gv8_hM@%am+Yruz$Il)GbHsjMT61f(9YW2+ZaIUqMOedN-Fq+*jIG7y) z%8pa^WU@%q;!z6qlulU=xJ!%kYwLv6&q6?i2Xf^s+HK3?u0^^6AuWrR z#CQWUBP^J3$Ww1C6LLo2G|nxaCGfiLf9m19@a~blo2A^ZOHQ>t&?z4AAe1CqSmbJ< z-DXpX+tvA;_~*iv@`XzE&D=8W1MjK62E9j!c} zrHY)w-X%3tO1pA+4@WL0^QdBcOT75%Eg#cUz3H$_$x=;6^EcO$%T!N~!*R8Qp6%-D z-&7%LnRg2^lPksFcw8%be;|yf@8>+dvUSm>uHIx|4QqaxV;q*z*zZLp?0^(mqR1Sj z1ejL#_mJ`rtL45?2pA5>g~~zzc6WWtaB<=_vteu=_0n9r^mJ#Sg3Fa!fk+9uB4~gr zeJMB(beFSIRVQHln77W!5J~!UAvi3zaW@~Oa${T0-1{;-m|O`$v=H#X{#MJ7h#fbeJYv7>&+W|3F|&?G zsshsD4@b7vykRbeEU!7T!w|i^O?7(aNN^&y)BzKHQfylCdvkr%OanO6X&?ASPt~M# zr-F|=(UPv@%9@nDI3Slhx`Cpi`bV*N0qT$>CgG!q!@V7IzJ{gyJ8&k|9QjQ)$q(W|J7ao?y5C=6?13k^oujC<{)HNSswH3493b8QjXeVMz5l2 z8Q8R(?-97~%aRGNSBK4PaJP2i?kUFWtwajB1P#8YFzDTYEv^Wc6^L~VFpKd=roWra6IpKQg(rqpeOFg3YtG8ghL(=RrzTYZ6c-@R1g`U?C4%FoMl z&Bdv-y?=${BtKq`k%ASgmu7wbX8Ux%LVsG<)Z387D{xwv{M!)5%1^T>w0briCmH*# zTykk1w*ohOPWdSGCrn@`hSjfD?!{0H}mva+-3{!vP&*8C$XpTk$;6<2N?UGsX- zs~tlZYMe{qK=ZB5NU_5%K)+nm`SaB8?aQbN+TtK0ZPddc8Rolr2}~g8Ksf9ySO#JT zEI}P8)XZahDlNYU_h-<``l;)?hjw*I5a>@Vr_`UHk9xUi`n&w{9GY=U{Y}f2FPf5K zA~gEZL!-3(x5#8GGd`2a-s>T#)c1c2;YkKD)O@+7!AGIFVGM1kYYe`huprfjJQ`uq z!n0o63J%#l^dU<-l<}HJ4m{Ygoa^zZ%OeMuX;=}6EXXt@{m(vyC4cc%;>x33it?Vv zM_W7`MciNUeqh92``s>FV`1^#bN-i5YPgH?Op(hBavqW`HN0+1XZwlQ`PPMLs`~xx zlx>dPoG5+k8~FqDAgHrh29_AJfX*xIaw4Dd{VS09q)Vj@hQC2X0V=iQigSFkpBYr5 z&TQwxSzS0Rc)07^e5Ie$VYKpXS6XUhCQEUe;2SqHuLR;WYQnwxyEEe9;GC&cEp_U3 z@npp+;eY0N+B(B@3cVXM(ZJgwd0&wCDI4RT)z&lD+a)?m*`0*j3%9U5wO+e^Zta}v z@<&WDP2eZHAF%xePQAd6(Ve5Uy?cAQGyEXXBqa$GfS-oSf`;^bQTwXT&P`y$$8KzJ zbtK;?3;AzGmlcwObKZb=bUL$tI?QYe!Hih6D^Nd-Z$HZFiHyC%jZg^%OcjkMY&V#i z+ml)QA?y}dXcyfBZKeCJ7BjyrI$U_6p2rj=38u$ioUFE1F~6Z8HUc#|q}>(9#3g=6 zRP#p!d3{mK@tR#Iq#TQPS|Nk+t=11uNU6fN_nU$c`dJI4b!H3dXzKy;_4j$*;z`8T z7_l}@)pF>FR?;YiN&AWIC_qO^*kM@OZ z#h%dugHj3KmQQ+w4ymtZ-4rx+D5P~?ET7)=Pb$gR8tM(&Q0 z_xI1>@Ne!>UryKQ60Wo8?fmi8)yv~ibUq5_rM9%+b$2t10uFYYvXo)MHzuy9eRU<= zf~AKNFpBl!50j4yDI1Y&(;thPsXdBs4=H^)9TYV~@xQ;vGTDzX=Ddvu$(|S@ zud@!l5qXJ`onCThn(Xu-;L53oCm%a?_eH!2-;g9{a3MtHAT&4Xo)uQiaPy;(X>&&5 z`is;=8dRh~;M^l-!%2PrTFVpel`Wc*rdMI`kdkdGIGFLu`mGnW$A@I^E+Yj9(wVjZ zo{A5~wDcn5QkS+{0TKZh7&X-TPWasevKvtR!I9;?tOE^M1Co&cAJo(V4ln8 zC47>xY`H;9zJQ0i!ERSEp4jcw70P#+-T?f667q#`h^lnzNJ1NN70rjP1?I!S`9##u zxgEEXhW@c;ZkJZ1wbtAFoS|s2T-ZMlBd5V$v{dqm0}_aI^?!v=`fwTw&~HMngE)bx zj>Eo*EpHW+aEYB66Hkx1e*bk^RCX8etGZ(FBIk$v&a;bN?wB8CUXPSq+caed>{}vfrjkp zZf46<(29IzeFKY<4$3Fs;V;!v!V*O@861-)TCm97ol5qJIX8=*ol^eZRMpD)l+Dxd z>+AHBkGLdwwPnVdsG4u(IXpBsM%;`c(N4o*V}il9RGk#=gt2@c@mAAil<|-xtGbr@ zq)()D!T^b(zdm9xO0~oC!;D9V%k1Pqp{pfpq#af>RPihu?y1eb?U(zzhI#V2r}`Si zsi`~yaNuVuZeg)p*5cq_xq3*e!6Jtd4j@e<#z~uRfmHn1!_p)F#A##GfLosaVYOyU zMO^R<8nlcYI&r9O2Q-dYBKT`O8kT>HVFSe+5q;t;fa6ivD^PQ9X!dt>*XS=km}VTG z*Bz(>UJ#(2eLKWvtSkXPYXIcg0{(XSN{66`F&C%5MV$z(nU+U-p^V{|dG0$1? z81;C&R(z4t+sj%A?_fA2VUXi0!%26QOtaFyJG;VERbML4>3FKM7)wgK&^JPe_#r0vFz>)i}E#= zRQVdJ%l@5%^a-=b_KXQ?YhK8-QvbS(pD=dG9ascFhV3A{W>ea9UoSO{@&2E7ZZb~P zvzBe+I%-ZrU!G&2hWHtM$ruCOzd+(NupS%bxV;xW=6gA4^t_$}y8qlO8YAtfQV923 zwA|!lR(~~;*pBo^Xm~|EIIW)#6=oF_3kS(?14T6rY-rw4PBxwPcpLN@Ief-RKdNlh z3Vyzy6o6mWKRMGD`&DPc?`=IXBW&2Hjz(Q@I-l3J5sZsu6@M>fmJYdnmN`6&4fnUCJdCAxXVQvT$ZbkHrtnPC?tJ&ko|-h4ZX62fYhhD z8OlBqB{EW@4`DT{)Q*1F*k38`^BCp^;GlLHo$Cx-KZtbqH*~y%*6$xhd0~d;- z(26x9qSeJS%51ClD}8Yo7hL=8_-C~SOwLV_S;qWA4Rz^GbUN&Vu;92(=X;22D5bHu z+j^6cIr8@zzk$2~QtQyZ<7>hURJ4zyD%ILvco@vK0oLhuttpQJutw<1rCVuhjCgJx zqb(ZM!(ST#g8kZG-06tWo|}(aqTL|WDRXY*YdECeHTc7%0s`(`YZkfdvv=eBX22qkGvXFroqgDpE}>Plg=};V8diHPdm)c zgJ?$xe><(aU-3OT==%|_ACld&K<@cR^AWsD7mm*0tnazdOKech=ZNBsIa{V4HMuV^ z_uAD`(KIDFv*6j;TUyll+NR2i{$1PVh08N_ey~&n2|PcFlg(|*2f`+q9!|YpU@u3T z&NGgml`Vq^DX#DeFDUC{l7A*W9h>Iu4dnY6`lVa14L`dXB~T!?yOFDOFmw1GbKJWa zP+5g;zvC|~_d<(H&HZBn#w(UYZuIS!{yr+6pCja&?=@&|#eYS;B56vcDnTtxA{WYr z7a5`C=+2@}b4e;`H1z4$4AfZL%8j|n*|3I96^lX;Xo}p>Ird%xB)reW70WKrpv4;Ba2T=*-r6##0xpG|@ z04kzhC*>vfMQD!)8ZI#gO2cH?_oQ>8UydqG0NP&;c8{X@7#U*dBNdH!Zs1bs>_i?YBu7NC9j@Xx*7JU7YYZUI;r(2rqVmTqe4}cM?;D{X3 zhvEDQQe}T)bn;JqEf|=bj+F7eZ&KcOmMh0{={v*rK@)&_$hYgo(P76nQ19EZUOIlo zxP2f8G4fX`4H@)o9Qu4*kI;Y{0H9XLzmALoj2uiAi0E%KmN2?NF(2De1(5{B^Q)`J zwQzpIWVN+>2X1+p%!k{Z{ENuHlH8T1rt!raX{h36t}kUkT4}pD`I|Dl=B((PL<6M5 z{l$yfLLa%p%r1Stn`(qmIiC*TzW&{_Yzc*rgTE~smcHc0!x!}vPLOqm9X0?K=URC< zjte{O>~E*ox!*~1Tc5xJKUU)?jYH`?GS9)$AtVi`gDwpqyW!_Smo(*k#d!N{IWo z!YrC;QHZ$pUn&{)u@Ry~y>cmmhHNL|WPibr<^%F#@SNK4Vu@zxGhYE!k9U~QCjqT|2nB|GAfL6S7dEK&%AxJ^@gTD- z!0!mtmg6SL`jkCba8ptQ^><{48oHW^V(_&)KhS4+$UgNo60yPV2|jhk;76?dx^gd) z3oFlc^di6NkpAG3Z{eP?Y7aNkDUB9z-JkpJLJ*1@)6l2ep>tnb_?fZieXhEr%I5+b zx|`9>ws*2H?)cs~-zgSJ0)3ESl0W{DqC6X9^Q(vV94Tu&kSq2aYcR$_rO-ymWXYYZ zMUDq}Q=gGtZfO8S^C2mH7`eP=bOPRB`xk@-E9F zR7XLLMtbbr4Q!BYUBs=V>$7QnvnR7henn0)96FNk6zwan86)M|Y@T<f)(-|q<<3gnb5J>4hb%zemtkK?i_ULBDLy90!aOj=u-w_-SgyQYC z0+OE;#{ZE4Fi0Md_m*oSA;q=Wi9U#wABTtHA(7X5E3G>{-Ad}yObr-D`jEVP|700S zd}+?m!>7x6Gmk|aNF1FNn|iXARxF%V^%EN+Z_s9n>=D!olF8A=<)9?-=04VSo}Wv;a!Zw5U4)s1eZCcXg}p!eW5lDD$RL^GF_>viyGuxF8usxsd{u zZ^toO@4bp;3h^PE$nB}EGoR!SK_DpFKX@`X1~LX~aF=6Ix72LKlhb^JYuS+A3uV8FDFS!fX3X?Tw5ne z2cLz&juq+mA+tSDVff7wfFjf)%~qh8pu42Rp=UR+sFupdEW9zi@6 z?tdX*2G}8oas(Xjb#lyhS`(R%n95-Mp0)~usE=fgf#^b0h?WOmpN<>WAhPA}eKU-s zmDO6s*Jz!aN_H;f6<)sSv~(>5Diq5ll|UxEBO{eLSSL-XeCcmKrE}3{gOnrKI%6C4<;(V0?%F#*vbskP#}DyRz&rEh zLuEN`<%w!POR9GtHSg(c{gVb9d}BAT6v0zYSP&J7KnEC!$mFoi=u1~!atG{A$_LfG z+B)Dav(bB}FYsCAQ*odyjZIn#dRW_w{&2rrN(s7iyTWr)(F8G+#+Y%2qMc~-Rbb=F zoA`~&Y-_<+`K4edM*;z@*_;7$b2NHZHYLY@83!fgpm6Z&>;G36CZ5B!M!)+{{zHD_ qZ~GSsLtga%|MtHa%>Ua+ocS6)_$o#sV4jYGAGa0N6+X%tzx+RE1>j-; From fc9f74ed53df54bd1278882e86506ab3c7772c52 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:03:51 -0600 Subject: [PATCH 70/83] update system model dr overview --- docs/src/PRAS/sysmodelspec.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/src/PRAS/sysmodelspec.md b/docs/src/PRAS/sysmodelspec.md index a7708794..3bcafb77 100644 --- a/docs/src/PRAS/sysmodelspec.md +++ b/docs/src/PRAS/sysmodelspec.md @@ -197,7 +197,7 @@ its state of charge during outages (subject to carryover losses). ## Demand Responses Resources that can shift or shed electrical load forward in time but do -not provide an overall net addition of energy into the system (e.g., a dr event programs) +not provide an overall net addition of energy into the system (e.g., dr event programs) are referred to as **demand responses** in PRAS. Like storages, demand response components are associated with descriptive name and category metadata. Each demand response unit has both a load borrow and load payback capacity time series, representing the @@ -213,17 +213,27 @@ state of load increases with borrowing and decreases with payback, and must always remain between zero and the maximum load capacity in that time period. The energy flow relationships between these capacities are depicted visually in the energy relation diagram. +If the maximum load capacity is less than the previous hour's state, the load borrowed will +be counted as unserved energy. + + +| **Demand Response Type** | **Description** | **Relevance to PRAS?** | +|:----------|:---------------|:----------------------| +| **Shift** | Demand response that encourages the movement of energy consumption from times of high demand to times of day when there is a surplus of generation.​ | Yes: can be dispatched as part of the PRAS optimization, to move load out of (net) peak periods.​ | +| **Shed** | Loads that can be curtailed to provide peak capacity reduction and support the system in emergency or contingency events with a range in advance notice times.​ | Yes: but caution must be used to avoid too frequent load shed and to capture only load that can truly be shed (i.e., not able to be paid back at a later time). Setting the `borrowed_energy_interest` to -0.99 will drop all effective borrowed load.​ | +| **Shape** | Demand response that reshapes customer load profiles through price response or behavioral campaigns — ‘load-modifying DR’ — with advance notice of months to days.​ | Only through pre-processing to load; no original implementation option within the PRAS optimization.| +| **Shimmy** | Loads that dynamically adjust to alleviate short-run ramps and disturbances on the system at timescales ranging from seconds up to an hour.​ | No: the system conditions and ramping are more granular than what is possible in PRAS. | +*Table: Common demand response categories and what is possible to model in PRAS. Demand Response type categories are taken from Piette, Mary Ann, et al. "2025 California Demand Response Potential Study.".* + + +Demand response units may incur losses or gains forward in time (borrowed energy interest). +The available load borrowed in the next time period is calculated by multiplying the borrowed energy interest +by the current load borrowed and adding to the current load borrowed. +The borrowed energy interest may be positive or negative, +indicating a growing or shrinking respectively borrowed load hour to hour. Borrowed energy interest +may lead to cases where the maximum load capacity of the demand response device is passed. If this occurs, +any load above the maximum capacity will be tracked as unserved energy. -Demand response units may incur losses or gains when moving load into or out of the device -(borrow and payback efficiency), or forward in time (borrowed energy interest). -When borrowing load, the effective increase to the load in the demand response -is determined by multiplying the borrow power by the borrow -efficiency. Similarly, when load is paid back from the unit, the effective decrease to the -load borrowed is calculated by dividing the payback power by the payback -efficiency. The available load borrowed in the next time -period is calculated by multiplying the borrowed energy interest by the current load borrowed -and adding to the current load borrowed. The borrowed energy interest may be positive or negative, -indicating a growing or shrinking respectively borrowed load hour to hour. Just as with storage, demand responses may be in available or unavailable states, and move between these states randomly over time, according to provided state @@ -236,7 +246,8 @@ This cutoff, where borrowed load is unable to be repaid and transitions over to is refereed to as the allowable payback period. This parameter can be time varying, and therefore enable unique tailoring to the real world device being modeled. The allowable payback window is a integer and follows the timestep units set for the system. If any surplus exists in the region, the demand response -device will attempt to payback any borrowed load, before charging storage. +device will attempt to payback any borrowed load, before charging storage. If the demand response device +pays back all borrowed load before the end of the period, the counter is reset upon further use. ## Interfaces From c17c582b0c8415133a6f3a3a5e86b51790cdf65f Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:04:15 -0600 Subject: [PATCH 71/83] add docstring exports to api --- docs/src/PRASCore/api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/PRASCore/api.md b/docs/src/PRASCore/api.md index 4cb5426c..a555af07 100644 --- a/docs/src/PRASCore/api.md +++ b/docs/src/PRASCore/api.md @@ -7,6 +7,7 @@ PRASCore.Systems.Regions PRASCore.Systems.Generators PRASCore.Systems.Storages PRASCore.Systems.GeneratorStorages +PRASCore.Systems.DemandResponses PRASCore.Systems.Lines PRASCore.Systems.Interfaces ``` @@ -37,5 +38,8 @@ PRASCore.Results.GeneratorStorageEnergySamples PRASCore.Results.StorageAvailability PRASCore.Results.StorageEnergy PRASCore.Results.StorageEnergySamples +PRASCore.Results.DemandResponseAvailability +PRASCore.Results.DemandResponseEnergy +PRASCore.Results.DemandResponseEnergySamples PRASCore.Results.LineAvailability ``` From b5604b4383f5b6218ee3feb7fa0316a93991f1ab Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:04:43 -0600 Subject: [PATCH 72/83] add docstring to DemandResponses --- PRASCore.jl/src/Systems/assets.jl | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index bb255c81..42c22f96 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -518,6 +518,40 @@ function Base.vcat(gen_stors::GeneratorStorages{N,L,T,P,E}...) where {N, L, T, P end +""" + DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} + +A struct representing demand response devices in the system. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `L`: Length of each timestep in T units +- `T`: The time period type used for temporal representation, subtype of `Period` +- `P`: The power unit used for capacity measurements, subtype of `PowerUnit` +- `E`: The energy unit used for energy storage, subtype of `EnergyUnit` + +# Fields + - `names`: Name of demand response device + - `categories`: Category of demand response device + - `borrow_capacity`: Maximum available borrowing capacity for each demand response unit in each + timeperiod, expressed in units given by the `power_units` (`P`) type parameter + - `payback_capacity`: Maximum available payback capacity for each demand response unit in + each timeperiod, expressed in units given by the `power_units` (`P`) type parameter + - `energy_capacity`: Maximum available energy capable of being held for each demand response unit in + each timeperiod, expressed in units given by the `energy_units` (`E`) type parameter + - `borrowed_energy_interest`: Growth or decay rate of borrowed energy in the demand response device + from the beginning of one period to energy retained at the end of the previous period, for + each demand response unit in each timeperiod. A `borrowed_energy_interest` of 0.0 has no growth or decay. Unitless. + - `allowable_payback_period`: Maximum number of time steps a demand response device can hold borrowed load. + Any energy still contained at the end of the period will be counted as unserved load for that hour. + If borrowed load is paid back before the end of the `allowable_payback_period`, counter is reset upn further use. (`T`) type parameter + - `λ` (failure probability): Probability the unit transitions from operational to forced + outage during a given simulation timestep, for each storage unit in each timeperiod. + Unitless. + - `μ` (repair probability): Probability the unit transitions from forced outage to + operational during a given simulation timestep, for each storage unit in each + timeperiod. Unitless. +""" struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAssets{N,L,T,P} names::Vector{String} From 4453f1b161bfdee8d334798f3c0371f45ac8df9b Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:05:18 -0600 Subject: [PATCH 73/83] misc pras walkthrough update --- PRAS.jl/examples/pras_walkthrough.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PRAS.jl/examples/pras_walkthrough.jl b/PRAS.jl/examples/pras_walkthrough.jl index 359de2ce..350cffe2 100644 --- a/PRAS.jl/examples/pras_walkthrough.jl +++ b/PRAS.jl/examples/pras_walkthrough.jl @@ -1,4 +1,4 @@ -# # [PRAS walkthrough](@id pras_walkthrough) +# # [PRAS Walkthrough](@id pras_walkthrough) # This is a complete example of running a PRAS assessment, # using the [RTS-GMLC](https://github.com/GridMod/RTS-GMLC) system @@ -9,7 +9,7 @@ using Plots using DataFrames using Printf -# ## [Read and explore a SystemModel](@id explore_systemmodel) +# ## [Read and Explore a SystemModel](@id explore_systemmodel) # You can load in a system model from a [.pras file](@ref prasfile) if you have one like so: # ```julia @@ -77,7 +77,7 @@ region_3_storage = sys["3", Storages] # and the generation-storage device in region "2" like so: region_2_genstorage = sys["2", GeneratorStorages] -# ## Run a Sequential Monte Carlo simulation +# ## Run a Sequential Monte Carlo Simulation # We can run a Sequential Monte Carlo simulation on this system using the # [assess](@ref PRASCore.Simulations.assess) function. From 3231fab6fa18e091d9856c0344f4dcf1c5e9ee04 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:05:35 -0600 Subject: [PATCH 74/83] broader dr walkthrough revamp --- .../examples/demand_response_walkthrough.jl | 136 +++++++++--------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/PRAS.jl/examples/demand_response_walkthrough.jl b/PRAS.jl/examples/demand_response_walkthrough.jl index 5cc34097..44a63212 100644 --- a/PRAS.jl/examples/demand_response_walkthrough.jl +++ b/PRAS.jl/examples/demand_response_walkthrough.jl @@ -7,51 +7,51 @@ using PRAS using Plots using DataFrames -using Printf using Dates +using Measures -# ## [Add Demand Response to the SystemModel] +# ## Add Demand Response to the SystemModel # For the purposes of this example, we'll just use the built-in RTS-GMLC model. For -#further information on loading in systems and exploring please see the [PRAS walkthrough](@ref pras_walkthrough). -rts_gmlc_sys = PRAS.rts_gmlc() +# further information on loading in systems and exploring please see the [PRAS walkthrough](@ref pras_walkthrough). +rts_gmlc_sys = PRAS.rts_gmlc(); -# We can see the system information and make sure we are structuring out demand response to correctly plug in. +# Lets overview the system information and make sure the demand response we are creating is correct unit wise. rts_gmlc_sys -# First, we can now define our new demand response component. In accordance with the broader system -# the component will have a simulation length of 8784 timesteps, hourly interval, and MW/MWh units. -# We will then have a single demand response resource of type "DR_TYPE1" with a 50 MW borrow and payback capacity, +# First, we define our new demand response component. In accordance with the broader system +# the component will have a simulation length of 8784 timesteps, hourly interval, and MW/MWh power/energy units. +# We will then have a single demand response resource of type `"DR_TYPE1"` with a 50 MW borrow and payback capacity, # 200 MWh energy capacity, 0% borrowed energy interest, 6 hour allowable payback time periods, # 10% outage probability, and 90% recovery probability. The setup below uses the `fill` function to create matrices # with the correct dimensions for each of the parameters, which can be extended to multiple demand response resources # by changing the `number_of_drs` variable and adjusting names and types accordingly. -sim_length = 8784 -number_of_drs = 1 +sim_length = 8784; +number_of_drs = 1; new_drs = DemandResponses{sim_length,1,Hour,MW,MWh}( ["DR1"], ["DR_TYPE1"], - fill(50, number_of_drs, sim_length), #borrow capacity - fill(50, number_of_drs, sim_length), #payback capacity - fill(200, number_of_drs, sim_length), #energy capacity - fill(0.0, number_of_drs, sim_length), # 0% borrowed energy interest - fill(6, number_of_drs, sim_length), # 6 hour allowable payback time periods - fill(0.1, number_of_drs, sim_length), #10% outage probability - fill(0.9, number_of_drs, sim_length), #90% recovery probability - ) + fill(50, number_of_drs, sim_length), # borrow power capacity + fill(50, number_of_drs, sim_length), # payback power capacity + fill(200, number_of_drs, sim_length), # load energy capacity + fill(0.0, number_of_drs, sim_length), # 0% borrowed energy interest + fill(6, number_of_drs, sim_length), # 6 hour allowable payback time periods + fill(0.1, number_of_drs, sim_length), # 10% outage probability + fill(0.9, number_of_drs, sim_length), # 90% recovery probability + ); # We will also assign the demand response to region "2" of the system. -dr_region_indices = [1:0,1:1,2:0] +dr_region_indices = [1:0,1:1,2:0]; # We also want to increase the load in the system to see the effect of demand response being utilized. -# We can do this by creating a new load matrix that is 25% higher than the original load. -updated_load = Int.(round.(rts_gmlc_sys.regions.load .* 1.25)) +# We do this by creating a new load matrix that is 25% higher than the original load. +updated_load = Int.(round.(rts_gmlc_sys.regions.load .* 1.25)); -# We can then defined our new regions with the updated load. +# Lets define our new regions with the updated load. -new_regions = Regions{sim_length,MW}(["1","2","3"],updated_load) +new_regions = Regions{sim_length,MW}(["1","2","3"],updated_load); -# Finally, we can create two new system models, one with dr and one without. +# Finally, we create two new system models, one with dr and one without. modified_rts_with_dr = SystemModel( new_regions, rts_gmlc_sys.interfaces, rts_gmlc_sys.generators, rts_gmlc_sys.region_gen_idxs, @@ -59,7 +59,7 @@ modified_rts_with_dr = SystemModel( rts_gmlc_sys.generatorstorages, rts_gmlc_sys.region_genstor_idxs, new_drs, dr_region_indices, rts_gmlc_sys.lines, rts_gmlc_sys.interface_line_idxs, - rts_gmlc_sys.timestamps) + rts_gmlc_sys.timestamps); modified_rts_without_dr = SystemModel( new_regions, rts_gmlc_sys.interfaces, @@ -67,87 +67,91 @@ modified_rts_without_dr = SystemModel( rts_gmlc_sys.storages, rts_gmlc_sys.region_stor_idxs, rts_gmlc_sys.generatorstorages, rts_gmlc_sys.region_genstor_idxs, rts_gmlc_sys.lines, rts_gmlc_sys.interface_line_idxs, - rts_gmlc_sys.timestamps) + rts_gmlc_sys.timestamps); # For validation, we can check that one new demand response device is in the system, and the other system has none. -print(modified_rts_with_dr.demandresponses) -print(modified_rts_without_dr.demandresponses) +println("System with DR\n ",modified_rts_with_dr.demandresponses) +println("\nSystem without DR\n ",modified_rts_without_dr.demandresponses) -# ## [Run a Production Cost Model with and without Demand Response] -# We can now run a production cost model with and without the demand response to see the effect it has on the system. -# We'll query the shortfall attributable to demand response (load that was borrowed and never able to be paid back), and total system shortfall. -simspec = SequentialMonteCarlo(samples=100, seed=112) -resultspecs = (Shortfall(),DemandResponseShortfall(),DemandResponseEnergy()) +# ## Run a Sequential Monte Carlo Simulation with and without Demand Response +# We can now run a sequential monte carlo simulation with and without the demand response to see the effect it has on the system. +# We will query the shortfall attributable to demand response (load that was borrowed and never able to be paid back) and total system shortfall. +simspec = SequentialMonteCarlo(samples=100, seed=112); +resultspecs = (Shortfall(),DemandResponseShortfall(),DemandResponseEnergy()); -shortfall_with_dr, dr_shortfall_with_dr,dr_energy_with_dr = assess(modified_rts_with_dr, simspec, resultspecs...) -shortfall_without_dr, dr_shortfall_without_dr,dr_energy_without_dr = assess(modified_rts_without_dr, simspec, resultspecs...) +shortfall_with_dr, dr_shortfall_with_dr,dr_energy_with_dr = assess(modified_rts_with_dr, simspec, resultspecs...); +shortfall_without_dr, dr_shortfall_without_dr,dr_energy_without_dr = assess(modified_rts_without_dr, simspec, resultspecs...); -#Querying the results, we can see that total system shortfall is lower with demand response, across EUE and LOLE metrics. -print("LOLE Shortfall with DR: ", LOLE(shortfall_with_dr)) -print("LOLE Shortfall without DR: ", LOLE(shortfall_without_dr)) +# Querying the results, we can see that total system shortfall is lower with demand response, across EUE and LOLE metrics. +println("LOLE Shortfall with DR: ", LOLE(shortfall_with_dr)) +println("LOLE Shortfall without DR: ", LOLE(shortfall_without_dr)) -print("EUE Shortfall with DR: ", EUE(shortfall_with_dr)) -print("EUE Shortfall without DR: ", EUE(shortfall_without_dr)) +println("\nEUE Shortfall with DR: ", EUE(shortfall_with_dr)) +println("EUE Shortfall without DR: ", EUE(shortfall_without_dr)) -#We can also collect the same reliability metrics with the demand response shortfall, which is the amount of load that was borrowed and never able to be paid back. -print("LOLE Demand Response Shortfall: ", LOLE(dr_shortfall_with_dr)) -print("EUE Demand Response Shortfall: ", EUE(dr_shortfall_with_dr)) +# We can also collect the same reliability metrics with the demand response shortfall, which is the amount of load that was borrowed and never able to be paid back. +println("EUE Demand Response Shortfall: ", EUE(dr_shortfall_with_dr)) +println("LOLE Demand Response Shortfall: ", LOLE(dr_shortfall_with_dr)) -#This means over the simulation, load borrowed and unable to be paid back was 80MWh plus or minus 10 MWh. -# Similarly, we have a loss of load expectation from demand response of 0.8 event hours per year. - -# Lets plot the borrowed energy of the demand response device over the simulation. -borrowed_load = [x[1] for x in dr_energy_with_dr["DR1",:]] +# This means over the simulation, load borrowed and unable to be paid back was 250MWh plus or minus 30 MWh. +# Similarly, we have a loss of load expectation from demand response of 2.1 event hours per year. +# Lets plot the borrowed load of the demand response device over the simulation. +borrowed_load = [x[1] for x in dr_energy_with_dr["DR1",:]]; plot(rts_gmlc_sys.timestamps, borrowed_load, xlabel="Timestamp", ylabel="DR1 Borrowed Load", title="DR1 Borrowed Load vs Time", label="") # We can see that the demand response device was utilized during the summer months, however never borrowing up to its full 200MWh capacity. # Lets plot the demand response borrowed load across the month and hour of day for greater granularity on when load is being borrowed. -months = month.(rts_gmlc_sys.timestamps) -hours = hour.(rts_gmlc_sys.timestamps) .+ 1 +months = month.(rts_gmlc_sys.timestamps); +hours = hour.(rts_gmlc_sys.timestamps) .+ 1; -heatmap_matrix = zeros(Float64, 24, 12) +heatmap_matrix = zeros(Float64, 24, 12); for (val, m, h) in zip(borrowed_load, months, hours) - heatmap_matrix[h, m] += val + heatmap_matrix[h, m] += val; end heatmap( 1:12, 0:23, heatmap_matrix; xlabel="Month", ylabel="Hour of Day", title="Total DR1 Borrowed Load (MWh)", xticks=(1:12, ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]), - colorbar_title="Borrowed Load", color=cgrad([:white, :red], scale = :linear) + colorbar_title="Borrowed Load", color=cgrad([:white, :red], scale = :linear), + left_margin = 5mm, right_margin = 5mm ) # Maximum borrowed load occurs in the late afternoon July month, with a different peaking pattern as greater surplus exists in August, with reduced load constraints. -#We can also back calculate the borrow energy and payback energy, by calculating timestep to timestep differences. Note, payback energy here will not capture any dr attributable shortfall +# We can also back calculate the borrow power and payback power, by calculating timestep to timestep differences. +# Note, payback power here will not capture any dr attributable shortfall or the impact of `borrowed_energy_interest`. -borrow_energy = zeros(Float64, sim_length) -payback_energy = zeros(Float64, sim_length) -borrow_energy = max.(0.0, borrowed_load[2:end] .- borrowed_load[1:end-1]) -payback_energy = max.(0.0, borrowed_load[1:end-1] .- borrowed_load[2:end]) +borrow_power = zeros(Float64, sim_length); +payback_power= zeros(Float64, sim_length); +borrow_power = max.(0.0, borrowed_load[2:end] .- borrowed_load[1:end-1]); +payback_power = max.(0.0, borrowed_load[1:end-1] .- borrowed_load[2:end]); # And then plotting the two heatmaps to identify when key borrowing and payback periods are occuring. borrow_heatmap = zeros(Float64, 24, 12) payback_heatmap = zeros(Float64, 24, 12) -for (b, p, m, h) in zip(borrow_energy, payback_energy, months[2:end], hours[2:end]) +for (b, p, m, h) in zip(borrow_power, payback_power, months[2:end], hours[2:end]) borrow_heatmap[h, m] += b payback_heatmap[h, m] += p end - -p1 = heatmap(1:12, 0:23, borrow_heatmap; xlabel="Month", ylabel="Hour", title="DR1 Borrow (MW)", +p1 = heatmap(1:12, 0:23, borrow_heatmap; ylabel = "Hour of Day", title="DR1 Borrow", xticks=(1:12, ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]), - colorbar_title="Borrow", color=cgrad([:white, :red])) -p2 = heatmap(1:12, 0:23, payback_heatmap; xlabel="Month", ylabel="Hour", title="DR1 Payback (MW)", + xtickfont=font(7), + colorbar_title="Borrow (MW)", color=cgrad([:white, :red]), + left_margin = 5mm, right_margin = 3mm); +p2 = heatmap(1:12, 0:23, payback_heatmap; ylabel = "Hour of Day", title="DR1 Payback", xticks=(1:12, ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]), - colorbar_title="Payback", color=cgrad([:white, :blue])) -display(plot(p1)) -plot(p2) + xtickfont=font(7), + colorbar_title="Payback (MW)", color=cgrad([:white, :blue]), + left_margin = 3mm, right_margin = 5mm); + +plot(p1, p2; layout=(1,2), size=(1000, 500), link = :all) # We can see peak borrowing occurs around 4-6pm, shifting earlier in the following month, with payback, # occurring almost immediately after borrowing, peaking around 7-9pm in July. \ No newline at end of file From 9f455a2ec06f2996edc29e6ec599bcde0658d798 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 14:55:21 -0600 Subject: [PATCH 75/83] add Measures to Deps for docs --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 4f3c2baa..c39c6e12 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -3,6 +3,7 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" PRASCapacityCredits = "2e1a2ed5-e89d-4cd3-bc86-c0e88a73d3a3" PRASCore = "c5c32b99-e7c3-4530-a685-6f76e19f7fe2" PRASFiles = "a2806276-6d43-4ef5-91c0-491704cd7cf1" From f5147c73f59310d79f8e20c718223c76405e51e5 Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 15:18:59 -0600 Subject: [PATCH 76/83] misc cost update to catch where costs are equal --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 17725067..21f24034 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -147,7 +147,7 @@ struct DispatchProblem wheeling_cost_prevention = 50 minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) min_paybackcost_dr = min_chargecost - max_dischargecost - maxpaybacktime_dr - wheeling_cost_prevention #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention - max_borrowcost_dr = minpaybacktime_dr - min_paybackcost_dr + 1 #will always be greater than max_dischargecost and paybacktime + max_borrowcost_dr = minpaybacktime_dr - min_paybackcost_dr + 2 #will always be greater than max_dischargecost and paybacktime, need to have 2 to not overlap on the minimum and minimum values #for unserved energy shortagepenalty = 10 * (nifaces + max_borrowcost_dr) From a50653457d327875bfadbd4d2c44fcfbcd20675b Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 15:48:30 -0600 Subject: [PATCH 77/83] simplify dr costs --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 7 +++---- PRASCore.jl/src/Simulations/utils.jl | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index 21f24034..e09b5352 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -144,10 +144,9 @@ struct DispatchProblem max_dischargecost = - min_chargecost + maxdischargetime + 1 #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window - wheeling_cost_prevention = 50 - minpaybacktime_dr, maxpaybacktime_dr = minmax_payback_window_dr(sys) - min_paybackcost_dr = min_chargecost - max_dischargecost - maxpaybacktime_dr - wheeling_cost_prevention #add min_chargecost to always have DR devices be paybacked first, and -50 for wheeling prevention - max_borrowcost_dr = minpaybacktime_dr - min_paybackcost_dr + 2 #will always be greater than max_dischargecost and paybacktime, need to have 2 to not overlap on the minimum and minimum values + maxpaybacktime_dr = max_payback_window_dr(sys) + min_paybackcost_dr = - max_dischargecost - maxpaybacktime_dr - 1 #add max_dischargecost (will mean greater than storage charge as well) to always have DR devices be paybacked first + max_borrowcost_dr = - min_paybackcost_dr + maxpaybacktime_dr + 1 #will always be greater than max_dischargecost and paybacktime, need to add 1 to not overlap with payback reward #for unserved energy shortagepenalty = 10 * (nifaces + max_borrowcost_dr) diff --git a/PRASCore.jl/src/Simulations/utils.jl b/PRASCore.jl/src/Simulations/utils.jl index d7711b0f..4c9e555a 100644 --- a/PRASCore.jl/src/Simulations/utils.jl +++ b/PRASCore.jl/src/Simulations/utils.jl @@ -231,18 +231,16 @@ function maxtimetocharge_discharge(system::SystemModel) end -function minmax_payback_window_dr(system::SystemModel) +function max_payback_window_dr(system::SystemModel) if length(system.demandresponses) > 0 #no need to check for zero since allowable_payback_period is force to positive maxpaybacktime_dr = maximum(system.demandresponses.allowable_payback_period) - minpaybacktime_dr = minimum(system.demandresponses.allowable_payback_period) else - minpaybacktime_dr = 0 maxpaybacktime_dr = 0 end - return (minpaybacktime_dr, maxpaybacktime_dr) + return maxpaybacktime_dr end From 253bce0ceb1bb2942dae10ae2ff80a386bf0a55d Mon Sep 17 00:00:00 2001 From: juflorez Date: Tue, 14 Oct 2025 16:04:05 -0600 Subject: [PATCH 78/83] add back in storage wheeling prevention --- PRASCore.jl/src/Simulations/DispatchProblem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PRASCore.jl/src/Simulations/DispatchProblem.jl b/PRASCore.jl/src/Simulations/DispatchProblem.jl index e09b5352..63bac7bb 100644 --- a/PRASCore.jl/src/Simulations/DispatchProblem.jl +++ b/PRASCore.jl/src/Simulations/DispatchProblem.jl @@ -145,7 +145,8 @@ struct DispatchProblem #for demand response-we want to borrow energy in devices with the longest payback window, and payback energy from devices with the smallest payback window maxpaybacktime_dr = max_payback_window_dr(sys) - min_paybackcost_dr = - max_dischargecost - maxpaybacktime_dr - 1 #add max_dischargecost (will mean greater than storage charge as well) to always have DR devices be paybacked first + cost_for_storage_wheeling_prevention = 75 #prevent storage discharging and wheeling across large number of regions which then results in it being cheaper to borrow than to discharge from storage + min_paybackcost_dr = - max_dischargecost - maxpaybacktime_dr - 1 - cost_for_storage_wheeling_prevention #add max_dischargecost (will mean greater than storage charge as well) to always have DR devices be paybacked first max_borrowcost_dr = - min_paybackcost_dr + maxpaybacktime_dr + 1 #will always be greater than max_dischargecost and paybacktime, need to add 1 to not overlap with payback reward #for unserved energy From 9548addbcd4a9aee32b69305e3198b67f04d6821 Mon Sep 17 00:00:00 2001 From: juflorez Date: Wed, 15 Oct 2025 14:46:32 -0600 Subject: [PATCH 79/83] update PRAS docs --- .../examples/demand_response_walkthrough.jl | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/PRAS.jl/examples/demand_response_walkthrough.jl b/PRAS.jl/examples/demand_response_walkthrough.jl index 44a63212..9d3fe643 100644 --- a/PRAS.jl/examples/demand_response_walkthrough.jl +++ b/PRAS.jl/examples/demand_response_walkthrough.jl @@ -26,18 +26,18 @@ rts_gmlc_sys # 10% outage probability, and 90% recovery probability. The setup below uses the `fill` function to create matrices # with the correct dimensions for each of the parameters, which can be extended to multiple demand response resources # by changing the `number_of_drs` variable and adjusting names and types accordingly. -sim_length = 8784; +(timesteps,periodlen,periodunit,powerunit,energyunit) = get_params(rts_gmlc_sys); number_of_drs = 1; -new_drs = DemandResponses{sim_length,1,Hour,MW,MWh}( +new_drs = DemandResponses{timesteps,periodlen,periodunit,powerunit,energyunit}( ["DR1"], ["DR_TYPE1"], - fill(50, number_of_drs, sim_length), # borrow power capacity - fill(50, number_of_drs, sim_length), # payback power capacity - fill(200, number_of_drs, sim_length), # load energy capacity - fill(0.0, number_of_drs, sim_length), # 0% borrowed energy interest - fill(6, number_of_drs, sim_length), # 6 hour allowable payback time periods - fill(0.1, number_of_drs, sim_length), # 10% outage probability - fill(0.9, number_of_drs, sim_length), # 90% recovery probability + fill(50, number_of_drs, timesteps), # borrow power capacity + fill(50, number_of_drs, timesteps), # payback power capacity + fill(200, number_of_drs, timesteps), # load energy capacity + fill(0.0, number_of_drs, timesteps), # 0% borrowed energy interest + fill(6, number_of_drs, timesteps), # 6 hour allowable payback time periods + fill(0.1, number_of_drs, timesteps), # 10% outage probability + fill(0.9, number_of_drs, timesteps), # 90% recovery probability ); # We will also assign the demand response to region "2" of the system. @@ -49,7 +49,7 @@ updated_load = Int.(round.(rts_gmlc_sys.regions.load .* 1.25)); # Lets define our new regions with the updated load. -new_regions = Regions{sim_length,MW}(["1","2","3"],updated_load); +new_regions = Regions{timesteps,powerunit}(["1","2","3"],updated_load); # Finally, we create two new system models, one with dr and one without. modified_rts_with_dr = SystemModel( @@ -125,8 +125,8 @@ heatmap( # We can also back calculate the borrow power and payback power, by calculating timestep to timestep differences. # Note, payback power here will not capture any dr attributable shortfall or the impact of `borrowed_energy_interest`. -borrow_power = zeros(Float64, sim_length); -payback_power= zeros(Float64, sim_length); +borrow_power = zeros(Float64, timesteps); +payback_power= zeros(Float64, timesteps); borrow_power = max.(0.0, borrowed_load[2:end] .- borrowed_load[1:end-1]); payback_power = max.(0.0, borrowed_load[1:end-1] .- borrowed_load[2:end]); From 5785555caec254b03e152452a8effec690f4b9a9 Mon Sep 17 00:00:00 2001 From: juflorez Date: Thu, 16 Oct 2025 15:01:24 -0600 Subject: [PATCH 80/83] allow dr borrowed energy interest to be inclusive -1.0 to 1.0 --- PRASCore.jl/src/Systems/assets.jl | 3 ++- PRASCore.jl/test/Systems/assets.jl | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 42c22f96..3a6c0ce3 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -597,7 +597,8 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse @assert size(borrowedenergyinterest) == (n_drs, N) @assert all(isfractional, borrowefficiency) @assert all(isfractional, paybackefficiency) - @assert all(isfractional, borrowedenergyinterest) + @assert all(borrowedenergyinterest .<= 1.0) + @assert all(borrowedenergyinterest .>= -1.0) @assert size(allowablepaybackperiod) == (n_drs, N) @assert all(ispositive, allowablepaybackperiod) diff --git a/PRASCore.jl/test/Systems/assets.jl b/PRASCore.jl/test/Systems/assets.jl index b0d13954..33b5df29 100644 --- a/PRASCore.jl/test/Systems/assets.jl +++ b/PRASCore.jl/test/Systems/assets.jl @@ -148,11 +148,6 @@ @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( names[1:2], categories[1:2], vals_int, vals_int, vals_int, vals_float, vals_int, vals_float, vals_float,vals_float, vals_float) - - @test_throws AssertionError DemandResponses{10,1,Hour,MW,MWh}( - names, categories, vals_int, vals_int, vals_int, - -vals_float, vals_int, vals_float, vals_float,vals_float, vals_float) - end @testset "Lines" begin From 47ff1dfd87e9d969dc22574472868558e6ec4746 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Fri, 17 Oct 2025 10:19:54 -0600 Subject: [PATCH 81/83] Update 0.8 change log --- docs/src/changelog.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index acc22738..9282e19f 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -1,9 +1,11 @@ # Changelog -## [Unreleased] -- Initial changelog created. +## [0.8.0], 2025 - October -## [0.8.0], 2025 - September +- Add a demand response component which can model shift and shed type DR devices +- Add SystemModel attributes +- Enable pretty printing of different PRAS component information +- Enable auto-generated documentation website with detailed tutorials and walk-throughs ## [0.7], 2024 - December From 696fa9c54215198a50685f133d30985edd2d3e78 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Fri, 17 Oct 2025 10:25:05 -0600 Subject: [PATCH 82/83] Use empty constructor in system model read and move second constructor out of DR class definition in assets.jl --- PRASCore.jl/src/Systems/assets.jl | 36 +++++++++++++++---------------- PRASFiles.jl/src/Systems/read.jl | 7 +----- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 3a6c0ce3..a794a473 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -615,28 +615,26 @@ struct DemandResponses{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAsse allowablepaybackperiod, λ, μ,borrowefficiency, paybackefficiency,) end +end - #second constructor if you pass nothing in for borrow and payback efficiencies - function DemandResponses{N,L,T,P,E}( - names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, - borrowcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, - energycapacity::Matrix{Int}, borrowedenergyinterest::Matrix{Float64}, - allowablepaybackperiod::Matrix{Int}, - λ::Matrix{Float64}, μ::Matrix{Float64} - ) where {N,L,T,P,E} - return DemandResponses{N,L,T,P,E}( - names, categories, - borrowcapacity, paybackcapacity, energycapacity, - borrowedenergyinterest, allowablepaybackperiod, - λ, μ, - ones(Float64, size(borrowcapacity)), ones(Float64, size(paybackcapacity)) - ) - end - - - +# second constructor if borrow and payback efficiencies are not provided +function DemandResponses{N,L,T,P,E}( + names::Vector{<:AbstractString}, categories::Vector{<:AbstractString}, + borrowcapacity::Matrix{Int}, paybackcapacity::Matrix{Int}, + energycapacity::Matrix{Int}, borrowedenergyinterest::Matrix{Float64}, + allowablepaybackperiod::Matrix{Int}, + λ::Matrix{Float64}, μ::Matrix{Float64} +) where {N,L,T,P,E} + return DemandResponses{N,L,T,P,E}( + names, categories, + borrowcapacity, paybackcapacity, energycapacity, + borrowedenergyinterest, allowablepaybackperiod, + λ, μ, + ones(Float64, size(borrowcapacity)), ones(Float64, size(paybackcapacity)) + ) end + # Empty DemandResponses constructor function DemandResponses{N,L,T,P,E}() where {N,L,T,P,E} diff --git a/PRASFiles.jl/src/Systems/read.jl b/PRASFiles.jl/src/Systems/read.jl index 0565966d..d49f26cc 100644 --- a/PRASFiles.jl/src/Systems/read.jl +++ b/PRASFiles.jl/src/Systems/read.jl @@ -316,12 +316,7 @@ function systemmodel_0_8(f::File) region_dr_idxs = makeidxlist(dr_regions[region_order], n_regions) else - demandresponses = DemandResponses{N,L,T,P,E}( - String[], String[], - zeros(Int, 0, N), zeros(Int, 0, N), zeros(Int, 0, N), - zeros(Float64, 0, N), - zeros(Int, 0, N), zeros(Float64, 0, N), zeros(Float64, 0, N), - zeros(Float64, 0, N), zeros(Float64, 0, N)) + demandresponses = DemandResponses{N,L,T,P,E}() region_dr_idxs = fill(1:0, n_regions) From 47c38bf2fa039e3ae8508aa5ee9f48ffa12ff6d5 Mon Sep 17 00:00:00 2001 From: Srihari Sundar Date: Fri, 17 Oct 2025 10:44:26 -0600 Subject: [PATCH 83/83] Address some of Surya's comments on PRAS 101 walkthrough --- PRAS.jl/examples/pras_walkthrough.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/PRAS.jl/examples/pras_walkthrough.jl b/PRAS.jl/examples/pras_walkthrough.jl index 350cffe2..081f982a 100644 --- a/PRAS.jl/examples/pras_walkthrough.jl +++ b/PRAS.jl/examples/pras_walkthrough.jl @@ -30,7 +30,7 @@ sys # This system has 3 regions, with multiple Generators, one GenerationStorage in # region "2" and one Storage in region "3". We can see regional information by -# indexing the system with the region name: +# indexing the system by region name: sys["2"] # We can visualize a time series of the regional load in region "2": @@ -45,7 +45,7 @@ system_generators = sys.generators # This returns an object of the asset type [Generators](@ref PRASCore.Systems.Generators) # and we can retrieve capacities of all generators in the system, which returns -# a Matrix with the shape (number of generators) x (number of timepoints): +# a Matrix with the shape (number of generators) x (number of timesteps): system_generators.capacity # We can visualize a time series of the total system capacity @@ -151,20 +151,20 @@ utilization_str = join([@sprintf("Interface between regions 1 and 2 utilization: utilization["1" => "3", max_lole_ts][1])], "\n"); println(utilization_str) -# We see that the interfaces are not fully utilized, meaning there is -# no excess generation in the system that could be wheeled into region "2" +# We see that the interfaces are not fully utilized, which means there is +# no excess generation in the system that could be transferred into region "2" # and we can confirm this by looking at the surplus generation in each region println("Surplus in") -println(@sprintf(" region 1: %0.2f",surplus["1",max_lole_ts][1])) -println(@sprintf(" region 2: %0.2f",surplus["2",max_lole_ts][1])) -println(@sprintf(" region 3: %0.2f",surplus["3",max_lole_ts][1])) +@printf(" region 1: %0.2f\n",surplus["1",max_lole_ts][1]) +@printf(" region 2: %0.2f\n",surplus["2",max_lole_ts][1]) +@printf(" region 3: %0.2f\n",surplus["3",max_lole_ts][1]) # Is local storage another alternative for region 3? One can check on the average # state-of-charge of the existing battery in region "3", both in the # hour before and during the problematic period: -println(@sprintf("Storage energy T-1: %0.2f",storage["313_STORAGE_1", max_lole_ts-Hour(1)][1])) -println(@sprintf("Storage energy T: %0.2f",storage["313_STORAGE_1", max_lole_ts][1])) +@printf("Storage energy T-1: %0.2f\n",storage["313_STORAGE_1", max_lole_ts-Hour(1)][1]) +@printf("Storage energy T: %0.2f\n",storage["313_STORAGE_1", max_lole_ts][1]) # It may be that the battery is on average charged going in to the event, # and perhaps retains some energy during the event, even as load is being