├── .github └── workflows │ ├── PRAS.jl.yml │ ├── PRASCapacityCredits.jl.yml │ ├── PRASCore.jl.yml │ └── PRASFiles.jl.yml ├── .gitignore ├── PRAS.jl ├── LICENSE.md ├── Project.toml ├── src │ └── PRAS.jl └── test │ └── runtests.jl ├── PRASCapacityCredits.jl ├── LICENSE.md ├── Project.toml ├── src │ ├── CapacityCreditResult.jl │ ├── EFC.jl │ ├── ELCC.jl │ ├── PRASCapacityCredits.jl │ └── utils.jl └── test │ └── runtests.jl ├── PRASCore.jl ├── LICENSE.md ├── Project.toml ├── src │ ├── PRASCore.jl │ ├── Results │ │ ├── Flow.jl │ │ ├── FlowSamples.jl │ │ ├── GeneratorAvailability.jl │ │ ├── GeneratorStorageAvailability.jl │ │ ├── GeneratorStorageEnergy.jl │ │ ├── GeneratorStorageEnergySamples.jl │ │ ├── LineAvailability.jl │ │ ├── Results.jl │ │ ├── Shortfall.jl │ │ ├── ShortfallSamples.jl │ │ ├── StorageAvailability.jl │ │ ├── StorageEnergy.jl │ │ ├── StorageEnergySamples.jl │ │ ├── Surplus.jl │ │ ├── SurplusSamples.jl │ │ ├── Utilization.jl │ │ ├── UtilizationSamples.jl │ │ ├── metrics.jl │ │ └── utils.jl │ ├── Simulations │ │ ├── DispatchProblem.jl │ │ ├── Simulations.jl │ │ ├── SystemState.jl │ │ ├── recording.jl │ │ └── utils.jl │ └── Systems │ │ ├── SystemModel.jl │ │ ├── Systems.jl │ │ ├── TestData.jl │ │ ├── assets.jl │ │ ├── collections.jl │ │ └── units.jl └── test │ ├── Results │ ├── availability.jl │ ├── energy.jl │ ├── flow.jl │ ├── metrics.jl │ ├── runtests.jl │ ├── shortfall.jl │ ├── surplus.jl │ └── utilization.jl │ ├── Simulations │ └── runtests.jl │ ├── Systems │ ├── SystemModel.jl │ ├── assets.jl │ ├── collections.jl │ ├── runtests.jl │ └── units.jl │ ├── dummydata.jl │ └── runtests.jl ├── PRASFiles.jl ├── LICENSE.md ├── Project.toml ├── src │ ├── PRASFiles.jl │ ├── Results │ │ ├── utils.jl │ │ └── write.jl │ └── Systems │ │ ├── read.jl │ │ ├── rts.pras │ │ ├── toymodel.pras │ │ ├── utils.jl │ │ └── write.jl └── test │ └── runtests.jl ├── README.md ├── SystemModel_HDF5_spec.md └── docs ├── _config.yaml ├── getting-started.md └── index.md /.github/workflows/PRAS.jl.yml: -------------------------------------------------------------------------------- 1 | name: PRAS.jl tests 2 | # Run on master, tags, or any pull request 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST) 6 | push: 7 | branches: [main] 8 | tags: ["*"] 9 | pull_request: 10 | jobs: 11 | test: 12 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | version: 18 | - "lts" # Latest LTS release, min supported 19 | - "1" # Latest release 20 | os: 21 | - ubuntu-latest 22 | - macOS-latest 23 | - windows-latest 24 | arch: 25 | - x64 26 | - aarch64 27 | exclude: 28 | - os: windows-latest 29 | arch: aarch64 30 | - os: ubuntu-latest 31 | arch: aarch64 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: julia-actions/setup-julia@v2 35 | with: 36 | version: ${{ matrix.version }} 37 | arch: ${{ matrix.arch }} 38 | - uses: actions/cache@v4 39 | env: 40 | cache-name: cache-artifacts 41 | with: 42 | path: ~/.julia/artifacts 43 | key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}- 46 | ${{ runner.os }}-${{ matrix.arch }}-test- 47 | ${{ runner.os }}-${{ matrix.arch }}- 48 | ${{ runner.os }}- 49 | - run: julia --project=PRAS.jl -e 'import Pkg; 50 | Pkg.develop([ 51 | (path="PRASCore.jl",), 52 | (path="PRASFiles.jl",), 53 | (path="PRASCapacityCredits.jl",) 54 | ])' 55 | shell: bash 56 | - uses: julia-actions/julia-buildpkg@latest 57 | with: 58 | project: PRAS.jl 59 | - run: | 60 | git config --global user.name Tester 61 | git config --global user.email te@st.er 62 | - uses: julia-actions/julia-runtest@latest 63 | with: 64 | project: PRAS.jl 65 | env: 66 | JULIA_NUM_THREADS: 2 67 | - uses: julia-actions/julia-processcoverage@v1 68 | with: 69 | directories: PRAS.jl/src 70 | - uses: codecov/codecov-action@v4 71 | with: 72 | token: ${{ secrets.CODECOV_TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/PRASCapacityCredits.jl.yml: -------------------------------------------------------------------------------- 1 | name: PRASCapacityCredits.jl tests 2 | # Run on master, tags, or any pull request 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST) 6 | push: 7 | branches: [main] 8 | tags: ["*"] 9 | pull_request: 10 | jobs: 11 | test: 12 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | version: 18 | - "lts" # Latest LTS release, min supported 19 | - "1" # Latest release 20 | os: 21 | - ubuntu-latest 22 | - macOS-latest 23 | - windows-latest 24 | arch: 25 | - x64 26 | - aarch64 27 | exclude: 28 | - os: windows-latest 29 | arch: aarch64 30 | - os: ubuntu-latest 31 | arch: aarch64 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: julia-actions/setup-julia@v2 35 | with: 36 | version: ${{ matrix.version }} 37 | arch: ${{ matrix.arch }} 38 | - uses: actions/cache@v4 39 | env: 40 | cache-name: cache-artifacts 41 | with: 42 | path: ~/.julia/artifacts 43 | key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}- 46 | ${{ runner.os }}-${{ matrix.arch }}-test- 47 | ${{ runner.os }}-${{ matrix.arch }}- 48 | ${{ runner.os }}- 49 | - run: julia --project=PRASCapacityCredits.jl -e 'import Pkg; Pkg.develop(path="PRASCore.jl")' 50 | shell: bash 51 | - uses: julia-actions/julia-buildpkg@latest 52 | with: 53 | project: PRASCapacityCredits.jl 54 | - run: | 55 | git config --global user.name Tester 56 | git config --global user.email te@st.er 57 | - uses: julia-actions/julia-runtest@latest 58 | with: 59 | project: PRASCapacityCredits.jl 60 | env: 61 | JULIA_NUM_THREADS: 2 62 | - uses: julia-actions/julia-processcoverage@v1 63 | with: 64 | directories: PRASCapacityCredits.jl/src 65 | - uses: codecov/codecov-action@v4 66 | with: 67 | token: ${{ secrets.CODECOV_TOKEN }} 68 | -------------------------------------------------------------------------------- /.github/workflows/PRASCore.jl.yml: -------------------------------------------------------------------------------- 1 | name: PRASCore.jl tests 2 | # Run on master, tags, or any pull request 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST) 6 | push: 7 | branches: [main] 8 | tags: ["*"] 9 | pull_request: 10 | jobs: 11 | test: 12 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | version: 18 | - "lts" # Latest LTS release, min supported 19 | - "1" # Latest release 20 | os: 21 | - ubuntu-latest 22 | - macOS-latest 23 | - windows-latest 24 | arch: 25 | - x64 26 | - aarch64 27 | exclude: 28 | - os: windows-latest 29 | arch: aarch64 30 | - os: ubuntu-latest 31 | arch: aarch64 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: julia-actions/setup-julia@v2 35 | with: 36 | version: ${{ matrix.version }} 37 | arch: ${{ matrix.arch }} 38 | - uses: actions/cache@v4 39 | env: 40 | cache-name: cache-artifacts 41 | with: 42 | path: ~/.julia/artifacts 43 | key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}- 46 | ${{ runner.os }}-${{ matrix.arch }}-test- 47 | ${{ runner.os }}-${{ matrix.arch }}- 48 | ${{ runner.os }}- 49 | - uses: julia-actions/julia-buildpkg@latest 50 | with: 51 | project: PRASCore.jl 52 | - run: | 53 | git config --global user.name Tester 54 | git config --global user.email te@st.er 55 | - uses: julia-actions/julia-runtest@latest 56 | with: 57 | project: PRASCore.jl 58 | env: 59 | JULIA_NUM_THREADS: 2 60 | - uses: julia-actions/julia-processcoverage@v1 61 | with: 62 | directories: PRASCore.jl/src 63 | - uses: codecov/codecov-action@v4 64 | with: 65 | token: ${{ secrets.CODECOV_TOKEN }} 66 | -------------------------------------------------------------------------------- /.github/workflows/PRASFiles.jl.yml: -------------------------------------------------------------------------------- 1 | name: PRASFiles.jl tests 2 | # Run on master, tags, or any pull request 3 | on: 4 | schedule: 5 | - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST) 6 | push: 7 | branches: [main] 8 | tags: ["*"] 9 | pull_request: 10 | jobs: 11 | test: 12 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | version: 18 | - "lts" # Latest LTS release, min supported 19 | - "1" # Latest release 20 | os: 21 | - ubuntu-latest 22 | - macOS-latest 23 | - windows-latest 24 | arch: 25 | - x64 26 | - aarch64 27 | exclude: 28 | - os: windows-latest 29 | arch: aarch64 30 | - os: ubuntu-latest 31 | arch: aarch64 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: julia-actions/setup-julia@v2 35 | with: 36 | version: ${{ matrix.version }} 37 | arch: ${{ matrix.arch }} 38 | - uses: actions/cache@v4 39 | env: 40 | cache-name: cache-artifacts 41 | with: 42 | path: ~/.julia/artifacts 43 | key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}- 46 | ${{ runner.os }}-${{ matrix.arch }}-test- 47 | ${{ runner.os }}-${{ matrix.arch }}- 48 | ${{ runner.os }}- 49 | - run: julia --project=PRASFiles.jl -e 'import Pkg; Pkg.develop(path="PRASCore.jl")' 50 | shell: bash 51 | - uses: julia-actions/julia-buildpkg@latest 52 | with: 53 | project: PRASFiles.jl 54 | - run: | 55 | git config --global user.name Tester 56 | git config --global user.email te@st.er 57 | - uses: julia-actions/julia-runtest@latest 58 | with: 59 | project: PRASFiles.jl 60 | env: 61 | JULIA_NUM_THREADS: 2 62 | - uses: julia-actions/julia-processcoverage@v1 63 | with: 64 | directories: PRASFiles.jl/src 65 | - uses: codecov/codecov-action@v4 66 | with: 67 | token: ${{ secrets.CODECOV_TOKEN }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.jl.cov 2 | *.jl.*.cov 3 | *.jl.mem 4 | Manifest.toml 5 | *.DS_Store 6 | *.pras 7 | *.json 8 | -------------------------------------------------------------------------------- /PRAS.jl/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alliance for Sustainable Energy, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PRAS.jl/Project.toml: -------------------------------------------------------------------------------- 1 | name = "PRAS" 2 | uuid = "05348d26-1c52-11e9-35e3-9d51842d34b9" 3 | authors = [ 4 | "Gord Stephen ", 5 | "Surya Chandan Dhulipala ", 6 | "Hari Sundar " 7 | ] 8 | version = "0.7.1" 9 | 10 | [deps] 11 | PRASCapacityCredits = "2e1a2ed5-e89d-4cd3-bc86-c0e88a73d3a3" 12 | PRASCore = "c5c32b99-e7c3-4530-a685-6f76e19f7fe2" 13 | PRASFiles = "a2806276-6d43-4ef5-91c0-491704cd7cf1" 14 | Reexport = "189a3867-3050-52da-a836-e630ba90ab69" 15 | 16 | [compat] 17 | PRASCapacityCredits = "0.7" 18 | PRASCore = "0.7.1" 19 | PRASFiles = "0.7.1" 20 | Reexport = "1" 21 | julia = "1.10" 22 | 23 | [extras] 24 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 25 | 26 | [targets] 27 | test = ["Test"] 28 | -------------------------------------------------------------------------------- /PRAS.jl/src/PRAS.jl: -------------------------------------------------------------------------------- 1 | module PRAS 2 | 3 | using Reexport 4 | 5 | @reexport using PRASCore 6 | @reexport using PRASFiles 7 | @reexport using PRASCapacityCredits 8 | 9 | import PRASFiles: toymodel, rts_gmlc, read_addl_attrs 10 | 11 | end 12 | -------------------------------------------------------------------------------- /PRAS.jl/test/runtests.jl: -------------------------------------------------------------------------------- 1 | using PRAS 2 | using Test 3 | 4 | sys = PRAS.rts_gmlc() 5 | 6 | sf, = assess(sys, SequentialMonteCarlo(samples=100), Shortfall()) 7 | 8 | eue = EUE(sf) 9 | lole = LOLE(sf) 10 | neue = NEUE(sf) 11 | 12 | @test val(eue) isa Float64 13 | @test stderror(eue) isa Float64 14 | @test val(neue) isa Float64 15 | @test stderror(neue) isa Float64 16 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alliance for Sustainable Energy, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/Project.toml: -------------------------------------------------------------------------------- 1 | name = "PRASCapacityCredits" 2 | uuid = "2e1a2ed5-e89d-4cd3-bc86-c0e88a73d3a3" 3 | authors = ["Gord Stephen "] 4 | version = "0.7.0" 5 | 6 | [deps] 7 | Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" 8 | PRASCore = "c5c32b99-e7c3-4530-a685-6f76e19f7fe2" 9 | 10 | [compat] 11 | Distributions = "0.25" 12 | PRASCore = "0.7" 13 | julia = "1.10" 14 | 15 | [extras] 16 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 17 | 18 | [targets] 19 | test = ["Test"] 20 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/src/CapacityCreditResult.jl: -------------------------------------------------------------------------------- 1 | struct CapacityCreditResult{ 2 | S <: CapacityValuationMethod, M <: ReliabilityMetric, P <: PowerUnit} 3 | 4 | target_metric::M 5 | lowerbound::Int 6 | upperbound::Int 7 | bound_capacities::Vector{Int} 8 | bound_metrics::Vector{M} 9 | 10 | function CapacityCreditResult{S,M,P}( 11 | target_metric::M, lowerbound::Int, upperbound::Int, 12 | bound_capacities::Vector{Int}, bound_metrics::Vector{M}) where {S,M,P} 13 | 14 | length(bound_capacities) == length(bound_metrics) || 15 | throw(ArgumentError("Lengths of bound_capacities and bound_metrics must match")) 16 | 17 | new{S,M,P}(target_metric, lowerbound, upperbound, 18 | bound_capacities, bound_metrics) 19 | 20 | end 21 | 22 | end 23 | 24 | minimum(x::CapacityCreditResult) = x.lowerbound 25 | maximum(x::CapacityCreditResult) = x.upperbound 26 | extrema(x::CapacityCreditResult) = (x.lowerbound, x.upperbound) 27 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/src/EFC.jl: -------------------------------------------------------------------------------- 1 | struct EFC{M} <: CapacityValuationMethod{M} 2 | 3 | capacity_max::Int 4 | capacity_gap::Int 5 | p_value::Float64 6 | regions::Vector{Tuple{String,Float64}} 7 | verbose::Bool 8 | 9 | function EFC{M}( 10 | capacity_max::Int, regions::Vector{Pair{String,Float64}}; 11 | capacity_gap::Int=1, p_value::Float64=0.05, verbose::Bool=false) where M 12 | 13 | @assert capacity_max > 0 14 | @assert capacity_gap > 0 15 | @assert 0 < p_value < 1 16 | @assert sum(x.second for x in regions) ≈ 1.0 17 | 18 | return new{M}(capacity_max, capacity_gap, p_value, Tuple.(regions), verbose) 19 | 20 | end 21 | 22 | end 23 | 24 | function EFC{M}( 25 | capacity_max::Int, region::String; kwargs... 26 | ) where M 27 | return EFC{M}(capacity_max, [region=>1.0]; kwargs...) 28 | end 29 | 30 | function assess(sys_baseline::S, sys_augmented::S, 31 | params::EFC{M}, simulationspec::SequentialMonteCarlo 32 | ) where {N, L, T, P, S <: SystemModel{N,L,T,P}, M <: ReliabilityMetric} 33 | 34 | _, powerunit, _ = unitsymbol(sys_baseline) 35 | 36 | regionnames = sys_baseline.regions.names 37 | regionnames != sys_augmented.regions.names && 38 | error("Systems provided do not have matching regions") 39 | 40 | # Add firm capacity generators to the relevant regions 41 | efc_gens, sys_variable, sys_target = 42 | add_firmcapacity(sys_baseline, sys_augmented, params.regions) 43 | 44 | target_metric = M(first(assess(sys_target, simulationspec, Shortfall()))) 45 | 46 | capacities = Int[] 47 | metrics = typeof(target_metric)[] 48 | 49 | lower_bound = 0 50 | lower_bound_metric = M(first(assess(sys_variable, simulationspec, Shortfall()))) 51 | push!(capacities, lower_bound) 52 | push!(metrics, lower_bound_metric) 53 | 54 | upper_bound = params.capacity_max 55 | update_firmcapacity!(sys_variable, efc_gens, upper_bound) 56 | upper_bound_metric = M(first(assess(sys_variable, simulationspec, Shortfall()))) 57 | push!(capacities, upper_bound) 58 | push!(metrics, upper_bound_metric) 59 | 60 | while true 61 | 62 | params.verbose && println( 63 | "\n$(lower_bound) $powerunit\t< EFC <\t$(upper_bound) $powerunit\n", 64 | "$(lower_bound_metric)\t> $(target_metric) >\t$(upper_bound_metric)") 65 | 66 | midpoint = div(lower_bound + upper_bound, 2) 67 | capacity_gap = upper_bound - lower_bound 68 | 69 | # Stopping conditions 70 | 71 | ## Return the bounds if they are within solution tolerance of each other 72 | if capacity_gap <= params.capacity_gap 73 | params.verbose && @info "Capacity bound gap within tolerance, stopping bisection." 74 | break 75 | end 76 | 77 | # If the null hypothesis lower_bound_metric !>= upper_bound_metric 78 | # cannot be rejected, terminate and return the loose bounds 79 | pval = pvalue(upper_bound_metric, lower_bound_metric) 80 | if pval >= params.p_value 81 | @warn "Gap between upper and lower bound risk metrics is not " * 82 | "statistically significant (p_value=$pval), stopping bisection. " * 83 | "The gap between capacity bounds is $(capacity_gap) $powerunit, " * 84 | "while the target stopping gap was $(params.capacity_gap) $powerunit." 85 | break 86 | end 87 | 88 | # Evaluate metric at midpoint 89 | update_firmcapacity!(sys_variable, efc_gens, midpoint) 90 | midpoint_metric = M(first(assess(sys_variable, simulationspec, Shortfall()))) 91 | push!(capacities, midpoint) 92 | push!(metrics, midpoint_metric) 93 | 94 | # Tighten capacity bounds 95 | if val(midpoint_metric) > val(target_metric) 96 | lower_bound = midpoint 97 | lower_bound_metric = midpoint_metric 98 | else # midpoint_metric <= target_metric 99 | upper_bound = midpoint 100 | upper_bound_metric = midpoint_metric 101 | end 102 | 103 | end 104 | 105 | return CapacityCreditResult{typeof(params), typeof(target_metric), P}( 106 | target_metric, lower_bound, upper_bound, capacities, metrics) 107 | 108 | end 109 | 110 | function add_firmcapacity( 111 | s1::SystemModel{N,L,T,P,E}, s2::SystemModel{N,L,T,P,E}, 112 | region_shares::Vector{Tuple{String,Float64}} 113 | ) where {N,L,T,P,E} 114 | 115 | n_regions = length(s1.regions.names) 116 | n_region_allocs = length(region_shares) 117 | 118 | region_allocations = allocate_regions(s1.regions.names, region_shares) 119 | efc_gens = similar(region_allocations) 120 | 121 | new_gen(i::Int) = Generators{N,L,T,P}( 122 | ["_EFC_$i"], ["_EFC Calculation Dummy Generator"], 123 | zeros(Int, 1, N), zeros(1, N), ones(1, N)) 124 | 125 | variable_gens = Generators{N,L,T,P}[] 126 | variable_region_gen_idxs = similar(s1.region_gen_idxs) 127 | 128 | target_gens = similar(variable_gens) 129 | target_region_gen_idxs = similar(s2.region_gen_idxs) 130 | 131 | ra_idx = 0 132 | 133 | for r in 1:n_regions 134 | 135 | s1_range = s1.region_gen_idxs[r] 136 | s2_range = s2.region_gen_idxs[r] 137 | 138 | if (ra_idx < n_region_allocs) && (r == first(region_allocations[ra_idx+1])) 139 | 140 | ra_idx += 1 141 | 142 | variable_region_gen_idxs[r] = incr_range(s1_range, ra_idx-1, ra_idx) 143 | target_region_gen_idxs[r] = incr_range(s2_range, ra_idx-1, ra_idx) 144 | 145 | gen = new_gen(ra_idx) 146 | push!(variable_gens, gen) 147 | push!(target_gens, gen) 148 | efc_gens[ra_idx] = ( 149 | first(s1_range) + ra_idx - 1, 150 | last(region_allocations[ra_idx])) 151 | 152 | else 153 | 154 | variable_region_gen_idxs[r] = incr_range(s1_range, ra_idx) 155 | target_region_gen_idxs[r] = incr_range(s2_range, ra_idx) 156 | 157 | end 158 | 159 | push!(variable_gens, s1.generators[s1_range]) 160 | push!(target_gens, s2.generators[s2_range]) 161 | 162 | end 163 | 164 | sys_variable = SystemModel( 165 | s1.regions, s1.interfaces, 166 | vcat(variable_gens...), variable_region_gen_idxs, 167 | s1.storages, s1.region_stor_idxs, 168 | s1.generatorstorages, s1.region_genstor_idxs, 169 | s1.lines, s1.interface_line_idxs, s1.timestamps) 170 | 171 | sys_target = SystemModel( 172 | s2.regions, s2.interfaces, 173 | vcat(target_gens...), target_region_gen_idxs, 174 | s2.storages, s2.region_stor_idxs, 175 | s2.generatorstorages, s2.region_genstor_idxs, 176 | s2.lines, s2.interface_line_idxs, s2.timestamps) 177 | 178 | return efc_gens, sys_variable, sys_target 179 | 180 | end 181 | 182 | function update_firmcapacity!( 183 | sys::SystemModel, gens::Vector{Tuple{Int,Float64}}, capacity::Int) 184 | 185 | for (g, share) in gens 186 | sys.generators.capacity[g, :] .= round(Int, share * capacity) 187 | end 188 | 189 | end 190 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/src/ELCC.jl: -------------------------------------------------------------------------------- 1 | struct ELCC{M} <: CapacityValuationMethod{M} 2 | 3 | capacity_max::Int 4 | capacity_gap::Int 5 | p_value::Float64 6 | regions::Vector{Tuple{String,Float64}} 7 | verbose::Bool 8 | 9 | function ELCC{M}( 10 | capacity_max::Int, regions::Vector{Pair{String,Float64}}; 11 | capacity_gap::Int=1, p_value::Float64=0.05, verbose::Bool=false) where M 12 | 13 | @assert capacity_max > 0 14 | @assert capacity_gap > 0 15 | @assert 0 < p_value < 1 16 | @assert sum(x.second for x in regions) ≈ 1.0 17 | 18 | return new{M}(capacity_max, capacity_gap, p_value, Tuple.(regions), verbose) 19 | 20 | end 21 | 22 | end 23 | 24 | function ELCC{M}( 25 | capacity_max::Int, region::String; kwargs... 26 | ) where M 27 | return ELCC{M}(capacity_max, [region=>1.0]; kwargs...) 28 | end 29 | 30 | function assess(sys_baseline::S, sys_augmented::S, 31 | params::ELCC{M}, simulationspec::SequentialMonteCarlo 32 | ) where {N, L, T, P, S <: SystemModel{N,L,T,P}, M <: ReliabilityMetric} 33 | 34 | _, powerunit, _ = unitsymbol(sys_baseline) 35 | 36 | regionnames = sys_baseline.regions.names 37 | regionnames != sys_augmented.regions.names && 38 | error("Systems provided do not have matching regions") 39 | 40 | target_metric = M(first(assess(sys_baseline, simulationspec, Shortfall()))) 41 | 42 | capacities = Int[] 43 | metrics = typeof(target_metric)[] 44 | 45 | elcc_regions, base_load, sys_variable = 46 | copy_load(sys_augmented, params.regions) 47 | 48 | lower_bound = 0 49 | lower_bound_metric = M(first(assess(sys_variable, simulationspec, Shortfall()))) 50 | push!(capacities, lower_bound) 51 | push!(metrics, lower_bound_metric) 52 | 53 | upper_bound = params.capacity_max 54 | update_load!(sys_variable, elcc_regions, base_load, upper_bound) 55 | upper_bound_metric = M(first(assess(sys_variable, simulationspec, Shortfall()))) 56 | push!(capacities, upper_bound) 57 | push!(metrics, upper_bound_metric) 58 | 59 | while true 60 | 61 | params.verbose && println( 62 | "\n$(lower_bound) $powerunit\t< ELCC <\t$(upper_bound) $powerunit\n", 63 | "$(lower_bound_metric)\t< $(target_metric) <\t$(upper_bound_metric)") 64 | 65 | midpoint = div(lower_bound + upper_bound, 2) 66 | capacity_gap = upper_bound - lower_bound 67 | 68 | # Stopping conditions 69 | 70 | ## Return the bounds if they are within solution tolerance of each other 71 | if capacity_gap <= params.capacity_gap 72 | params.verbose && @info "Capacity bound gap within tolerance, stopping bisection." 73 | break 74 | end 75 | 76 | # If the null hypothesis upper_bound_metric !>= lower_bound_metric 77 | # cannot be rejected, terminate and return the loose bounds 78 | pval = pvalue(lower_bound_metric, upper_bound_metric) 79 | if pval >= params.p_value 80 | @warn "Gap between upper and lower bound risk metrics is not " * 81 | "statistically significant (p_value=$pval), stopping bisection. " * 82 | "The gap between capacity bounds is $(capacity_gap) $powerunit, " * 83 | "while the target stopping gap was $(params.capacity_gap) $powerunit." 84 | break 85 | end 86 | 87 | # Evaluate metric at midpoint 88 | update_load!(sys_variable, elcc_regions, base_load, midpoint) 89 | midpoint_metric = M(first(assess(sys_variable, simulationspec, Shortfall()))) 90 | push!(capacities, midpoint) 91 | push!(metrics, midpoint_metric) 92 | 93 | # Tighten capacity bounds 94 | if val(midpoint_metric) < val(target_metric) 95 | lower_bound = midpoint 96 | lower_bound_metric = midpoint_metric 97 | else # midpoint_metric <= target_metric 98 | upper_bound = midpoint 99 | upper_bound_metric = midpoint_metric 100 | end 101 | 102 | end 103 | 104 | return CapacityCreditResult{typeof(params), typeof(target_metric), P}( 105 | target_metric, lower_bound, upper_bound, capacities, metrics) 106 | 107 | end 108 | 109 | function copy_load( 110 | sys::SystemModel{N,L,T,P,E}, 111 | region_shares::Vector{Tuple{String,Float64}} 112 | ) where {N,L,T,P,E} 113 | 114 | region_allocations = allocate_regions(sys.regions.names, region_shares) 115 | 116 | new_regions = Regions{N,P}(sys.regions.names, copy(sys.regions.load)) 117 | 118 | return region_allocations, sys.regions.load, SystemModel( 119 | new_regions, sys.interfaces, 120 | sys.generators, sys.region_gen_idxs, 121 | sys.storages, sys.region_stor_idxs, 122 | sys.generatorstorages, sys.region_genstor_idxs, 123 | sys.lines, sys.interface_line_idxs, sys.timestamps) 124 | 125 | end 126 | 127 | function update_load!( 128 | sys::SystemModel, 129 | region_shares::Vector{Tuple{Int,Float64}}, 130 | load_base::Matrix{Int}, 131 | load_increase::Int 132 | ) 133 | for (r, share) in region_shares 134 | sys.regions.load[r, :] .= load_base[r, :] .+ 135 | round(Int, share * load_increase) 136 | end 137 | 138 | end 139 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/src/PRASCapacityCredits.jl: -------------------------------------------------------------------------------- 1 | module PRASCapacityCredits 2 | 3 | import PRASCore.Systems: Generators, PowerUnit, Regions, SystemModel, unitsymbol 4 | import PRASCore.Simulations: assess, SequentialMonteCarlo 5 | import PRASCore.Results: ReliabilityMetric, Result, Shortfall, stderror, val 6 | 7 | import Base: minimum, maximum, extrema 8 | import Distributions: ccdf, Normal 9 | 10 | export EFC, ELCC 11 | 12 | abstract type CapacityValuationMethod{M<:ReliabilityMetric} end 13 | 14 | include("utils.jl") 15 | include("CapacityCreditResult.jl") 16 | include("EFC.jl") 17 | include("ELCC.jl") 18 | 19 | end 20 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/src/utils.jl: -------------------------------------------------------------------------------- 1 | function pvalue(lower::T, upper::T) where {T<:ReliabilityMetric} 2 | 3 | vl = val(lower) 4 | sl = stderror(lower) 5 | 6 | vu = val(upper) 7 | su = stderror(upper) 8 | 9 | if iszero(sl) && iszero(su) 10 | result = Float64(vl ≈ vu) 11 | else 12 | # single-sided z-test with null hypothesis that (vu - vl) not > 0 13 | z = (vu - vl) / sqrt(su^2 + sl^2) 14 | result = ccdf(Normal(), z) 15 | end 16 | 17 | return result 18 | 19 | end 20 | 21 | function allocate_regions( 22 | region_names::Vector{String}, 23 | regionname_shares::Vector{Tuple{String,Float64}} 24 | ) 25 | 26 | region_allocations = similar(regionname_shares, Tuple{Int,Float64}) 27 | 28 | for (i, (name, share)) in enumerate(regionname_shares) 29 | 30 | r = findfirst(isequal(name), region_names) 31 | 32 | isnothing(r) && 33 | error("$name is not a region name in the provided systems") 34 | 35 | region_allocations[i] = (r, share) 36 | 37 | end 38 | 39 | return sort!(region_allocations) 40 | 41 | end 42 | 43 | incr_range(rnge::UnitRange{Int}, inc::Int) = rnge .+ inc 44 | incr_range(rnge::UnitRange{Int}, inc1::Int, inc2::Int) = 45 | (first(rnge) + inc1):(last(rnge) + inc2) 46 | -------------------------------------------------------------------------------- /PRASCapacityCredits.jl/test/runtests.jl: -------------------------------------------------------------------------------- 1 | using PRASCapacityCredits 2 | using PRASCore 3 | using Test 4 | 5 | import PRASCore.Systems: TestData 6 | 7 | @testset "PRASCapacityCredits" begin 8 | 9 | empty_str = String[] 10 | empty_int(x) = Matrix{Int}(undef, 0, x) 11 | empty_float(x) = Matrix{Float64}(undef, 0, x) 12 | 13 | gens = Generators{4,1,Hour,MW}( 14 | ["Gen1", "Gen2", "Gen3", "VG"], 15 | ["Gens", "Gens", "Gens", "VG"], 16 | [fill(10, 3, 4); [5 6 7 8]], 17 | [fill(0.1, 3, 4); fill(0.0, 1, 4)], 18 | [fill(0.9, 3, 4); fill(1.0, 1, 4)] 19 | ) 20 | 21 | gens_after = Generators{4,1,Hour,MW}( 22 | ["Gen1", "Gen2", "Gen3", "Gen4", "VG"], 23 | ["Gens", "Gens", "Gens", "Gens", "VG"], 24 | [fill(10, 4, 4); [5 6 7 8]], 25 | [fill(0.1, 4, 4); fill(0.0, 1, 4)], 26 | [fill(0.9, 4, 4); fill(1.0, 1, 4)] 27 | ) 28 | 29 | emptystors = Storages{4,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., 30 | (empty_int(4) for _ in 1:3)..., 31 | (empty_float(4) for _ in 1:5)...) 32 | 33 | emptygenstors = GeneratorStorages{4,1,Hour,MW,MWh}( 34 | (empty_str for _ in 1:2)..., 35 | (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:3)..., 36 | (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:2)...) 37 | 38 | load = [25, 28, 27, 24] 39 | 40 | tz = tz"UTC" 41 | timestamps = ZonedDateTime(2010,1,1,0,tz):Hour(1):ZonedDateTime(2010,1,1,3,tz) 42 | 43 | sys_before = SystemModel( 44 | gens, emptystors, emptygenstors, timestamps, load) 45 | 46 | sys_after = SystemModel( 47 | gens_after, emptystors, emptygenstors, timestamps, load) 48 | 49 | threenode2 = deepcopy(TestData.threenode) 50 | threenode2.generators.capacity[1, :] .+= 5 51 | 52 | smc = SequentialMonteCarlo(samples=100_000, threaded=false) 53 | 54 | @testset "EFC" begin 55 | 56 | cc = assess(sys_before, sys_after, EFC{EUE}(10, "Region"), smc) 57 | @test extrema(cc) == (8, 9) 58 | 59 | cc = assess(TestData.threenode, threenode2, EFC{EUE}(10, "Region A"), smc) 60 | @test extrema(cc) == (3, 4) 61 | 62 | end 63 | 64 | @testset "ELCC" begin 65 | 66 | cc = assess(sys_before, sys_after, ELCC{EUE}(10, "Region"), smc) 67 | @test extrema(cc) == (7, 8) 68 | 69 | cc = assess(TestData.threenode, threenode2, ELCC{EUE}(10, "Region A"), smc) 70 | @test extrema(cc) == (3, 4) 71 | 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /PRASCore.jl/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alliance for Sustainable Energy, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PRASCore.jl/Project.toml: -------------------------------------------------------------------------------- 1 | name = "PRASCore" 2 | uuid = "c5c32b99-e7c3-4530-a685-6f76e19f7fe2" 3 | authors = ["Gord Stephen "] 4 | version = "0.7.1" 5 | 6 | [deps] 7 | Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" 8 | MinCostFlows = "62286e6e-1779-56f1-888a-1c0056788ce0" 9 | OnlineStats = "a15396b6-48d5-5d58-9928-6d29437db91e" 10 | OnlineStatsBase = "925886fa-5bf2-5e8e-b522-a9147a512338" 11 | Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" 12 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 13 | Random123 = "74087812-796a-5b5d-8853-05524746bad3" 14 | Reexport = "189a3867-3050-52da-a836-e630ba90ab69" 15 | StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" 16 | TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" 17 | 18 | [compat] 19 | Dates = "1" 20 | MinCostFlows = "0.1.2" 21 | OnlineStats = "1" 22 | OnlineStatsBase = "1" 23 | Printf = "1.10" 24 | Random = "1" 25 | Random123 = "1" 26 | Reexport = "1" 27 | StatsBase = "0.34" 28 | TimeZones = "1" 29 | julia = "1.10" 30 | 31 | [extras] 32 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 33 | 34 | [targets] 35 | test = ["Test"] 36 | -------------------------------------------------------------------------------- /PRASCore.jl/src/PRASCore.jl: -------------------------------------------------------------------------------- 1 | module PRASCore 2 | 3 | import Reexport: @reexport 4 | 5 | include("Systems/Systems.jl") 6 | include("Results/Results.jl") 7 | include("Simulations/Simulations.jl") 8 | 9 | end 10 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/Flow.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Flow 3 | 4 | The `Flow` result specification reports the estimated average flow across 5 | transmission `Interfaces`, producing a `FlowResult`. 6 | 7 | A `FlowResult` can be indexed by a directional `Pair` of region names and a 8 | timestamp to retrieve a tuple of sample mean and standard deviation, estimating 9 | the average net flow magnitude and direction relative to the given directed 10 | interface in that timestep. For a query of `"Region A" => "Region B"`, if 11 | estimated average flow was from A to B, the reported value would be positive, 12 | while if average flow was in the reverse direction, from B to A, the value 13 | would be negative. 14 | 15 | Example: 16 | 17 | ```julia 18 | flows, = 19 | assess(sys, SequentialMonteCarlo(samples=1000), Flow()) 20 | 21 | flow_mean, flow_std = 22 | flows["Region A" => "Region B", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 23 | flow2_mean, flow2_std = 24 | flows["Region B" => "Region A", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 25 | @assert flow_mean == -flow2_mean 26 | ``` 27 | 28 | See [`FlowSamples`](@ref) for sample-level flow results. 29 | """ 30 | struct Flow <: ResultSpec end 31 | 32 | struct FlowAccumulator <: ResultAccumulator{Flow} 33 | 34 | flow_interface::Vector{MeanVariance} 35 | flow_interfaceperiod::Matrix{MeanVariance} 36 | 37 | flow_interface_currentsim::Vector{Int} 38 | 39 | end 40 | 41 | function accumulator( 42 | sys::SystemModel{N}, nsamples::Int, ::Flow 43 | ) where {N} 44 | 45 | n_interfaces = length(sys.interfaces) 46 | flow_interface = [meanvariance() for _ in 1:n_interfaces] 47 | flow_interfaceperiod = [meanvariance() for _ in 1:n_interfaces, _ in 1:N] 48 | 49 | flow_interface_currentsim = zeros(Int, n_interfaces) 50 | 51 | return FlowAccumulator( 52 | flow_interface, flow_interfaceperiod, flow_interface_currentsim) 53 | 54 | end 55 | 56 | function merge!( 57 | x::FlowAccumulator, y::FlowAccumulator 58 | ) 59 | 60 | foreach(merge!, x.flow_interface, y.flow_interface) 61 | foreach(merge!, x.flow_interfaceperiod, y.flow_interfaceperiod) 62 | 63 | end 64 | 65 | accumulatortype(::Flow) = FlowAccumulator 66 | 67 | struct FlowResult{N,L,T<:Period,P<:PowerUnit} <: AbstractFlowResult{N,L,T} 68 | 69 | nsamples::Union{Int,Nothing} 70 | interfaces::Vector{Pair{String,String}} 71 | timestamps::StepRange{ZonedDateTime,T} 72 | 73 | flow_mean::Matrix{Float64} 74 | 75 | flow_interface_std::Vector{Float64} 76 | flow_interfaceperiod_std::Matrix{Float64} 77 | 78 | end 79 | 80 | function getindex(x::FlowResult, i::Pair{<:AbstractString,<:AbstractString}) 81 | i_i, reverse = findfirstunique_directional(x.interfaces, i) 82 | flow = mean(view(x.flow_mean, i_i, :)) 83 | return reverse ? -flow : flow, x.flow_interface_std[i_i] 84 | end 85 | 86 | function getindex(x::FlowResult, i::Pair{<:AbstractString,<:AbstractString}, t::ZonedDateTime) 87 | i_i, reverse = findfirstunique_directional(x.interfaces, i) 88 | i_t = findfirstunique(x.timestamps, t) 89 | flow = x.flow_mean[i_i, i_t] 90 | return reverse ? -flow : flow, x.flow_interfaceperiod_std[i_i, i_t] 91 | end 92 | 93 | function finalize( 94 | acc::FlowAccumulator, 95 | system::SystemModel{N,L,T,P,E}, 96 | ) where {N,L,T,P,E} 97 | 98 | nsamples = length(system.interfaces) > 0 ? 99 | first(acc.flow_interface[1].stats).n : nothing 100 | 101 | flow_mean, flow_interfaceperiod_std = mean_std(acc.flow_interfaceperiod) 102 | flow_interface_std = last(mean_std(acc.flow_interface)) / N 103 | 104 | fromregions = getindex.(Ref(system.regions.names), system.interfaces.regions_from) 105 | toregions = getindex.(Ref(system.regions.names), system.interfaces.regions_to) 106 | 107 | return FlowResult{N,L,T,P}( 108 | nsamples, Pair.(fromregions, toregions), system.timestamps, 109 | flow_mean, flow_interface_std, flow_interfaceperiod_std) 110 | 111 | end 112 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/FlowSamples.jl: -------------------------------------------------------------------------------- 1 | """ 2 | FlowSamples 3 | 4 | The `FlowSamples` result specification reports the sample-level magnitude and 5 | direction of power flows across `Interfaces`, producing a `FlowSamplesResult`. 6 | 7 | A `FlowSamplesResult` can be indexed by a directional `Pair` of region names and a 8 | timestamp to retrieve a vector of sample-level net flow magnitudes and 9 | directions relative to the given directed interface in that timestep. For a 10 | query of `"Region A" => "Region B"`, if flow in one sample was from A to B, the 11 | reported value would be positive, while if flow was in the reverse direction, 12 | from B to A, the value would be negative. 13 | 14 | Example: 15 | 16 | ```julia 17 | flows, = 18 | assess(sys, SequentialMonteCarlo(samples=10), FlowSamples()) 19 | 20 | samples = flows["Region A" => "Region B", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 21 | 22 | @assert samples isa Vector{Float64} 23 | @assert length(samples) == 10 24 | 25 | samples2 = flows["Region B" => "Region A", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 26 | 27 | @assert samples == -samples2 28 | ``` 29 | 30 | Note that this result specification requires large amounts of memory for 31 | larger sample sizes. See [`Flow`](@ref) for estimated average flow results 32 | when sample-level granularity isn't required. 33 | """ 34 | struct FlowSamples <: ResultSpec end 35 | 36 | struct FlowSamplesAccumulator <: ResultAccumulator{FlowSamples} 37 | 38 | flow::Array{Int,3} 39 | 40 | end 41 | 42 | function accumulator( 43 | sys::SystemModel{N}, nsamples::Int, ::FlowSamples 44 | ) where {N} 45 | 46 | ninterfaces = length(sys.interfaces) 47 | flow = zeros(Int, ninterfaces, N, nsamples) 48 | 49 | return FlowSamplesAccumulator(flow) 50 | 51 | end 52 | 53 | function merge!( 54 | x::FlowSamplesAccumulator, y::FlowSamplesAccumulator 55 | ) 56 | 57 | x.flow .+= y.flow 58 | return 59 | 60 | end 61 | 62 | accumulatortype(::FlowSamples) = FlowSamplesAccumulator 63 | 64 | struct FlowSamplesResult{N,L,T<:Period,P<:PowerUnit} <: AbstractFlowResult{N,L,T} 65 | 66 | interfaces::Vector{Pair{String,String}} 67 | timestamps::StepRange{ZonedDateTime,T} 68 | 69 | flow::Array{Int,3} 70 | 71 | end 72 | 73 | function getindex(x::FlowSamplesResult, i::Pair{<:AbstractString,<:AbstractString}) 74 | i_i, reverse = findfirstunique_directional(x.interfaces, i) 75 | flow = vec(mean(view(x.flow, i_i, :, :), dims=1)) 76 | return reverse ? -flow : flow 77 | end 78 | 79 | 80 | function getindex(x::FlowSamplesResult, i::Pair{<:AbstractString,<:AbstractString}, t::ZonedDateTime) 81 | i_i, reverse = findfirstunique_directional(x.interfaces, i) 82 | i_t = findfirstunique(x.timestamps, t) 83 | flow = vec(x.flow[i_i, i_t, :]) 84 | return reverse ? -flow : flow 85 | end 86 | 87 | function finalize( 88 | acc::FlowSamplesAccumulator, 89 | system::SystemModel{N,L,T,P,E}, 90 | ) where {N,L,T,P,E} 91 | 92 | fromregions = getindex.(Ref(system.regions.names), system.interfaces.regions_from) 93 | toregions = getindex.(Ref(system.regions.names), system.interfaces.regions_to) 94 | 95 | return FlowSamplesResult{N,L,T,P}( 96 | Pair.(fromregions, toregions), system.timestamps, acc.flow) 97 | 98 | end 99 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/GeneratorAvailability.jl: -------------------------------------------------------------------------------- 1 | """ 2 | GeneratorAvailability 3 | 4 | The `GeneratorAvailability` result specification reports the sample-level 5 | discrete availability of `Generators`, producing a `GeneratorAvailabilityResult`. 6 | 7 | A `GeneratorAvailabilityResult` can be indexed by generator name and 8 | timestamp to retrieve a vector of sample-level availability states for 9 | the unit in the given timestep. States are provided as a boolean with 10 | `true` indicating that the unit is available and `false` indicating that 11 | it's unavailable. 12 | 13 | Example: 14 | 15 | ```julia 16 | genavail, = 17 | assess(sys, SequentialMonteCarlo(samples=10), GeneratorAvailability()) 18 | 19 | samples = genavail["MyGenerator123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 20 | 21 | @assert samples isa Vector{Bool} 22 | @assert length(samples) == 10 23 | ``` 24 | """ 25 | struct GeneratorAvailability <: ResultSpec end 26 | 27 | function accumulator( 28 | sys::SystemModel{N}, nsamples::Int, ::GeneratorAvailability 29 | ) where {N} 30 | 31 | ngens = length(sys.generators) 32 | available = zeros(Bool, ngens, N, nsamples) 33 | 34 | return GenAvailabilityAccumulator(available) 35 | 36 | end 37 | 38 | struct GenAvailabilityAccumulator <: 39 | ResultAccumulator{GeneratorAvailability} 40 | 41 | available::Array{Bool,3} 42 | 43 | end 44 | 45 | function merge!( 46 | x::GenAvailabilityAccumulator, y::GenAvailabilityAccumulator 47 | ) 48 | 49 | x.available .|= y.available 50 | return 51 | 52 | end 53 | 54 | accumulatortype(::GeneratorAvailability) = GenAvailabilityAccumulator 55 | 56 | struct GeneratorAvailabilityResult{N,L,T<:Period} <: AbstractAvailabilityResult{N,L,T} 57 | 58 | generators::Vector{String} 59 | timestamps::StepRange{ZonedDateTime,T} 60 | 61 | available::Array{Bool,3} 62 | 63 | end 64 | 65 | names(x::GeneratorAvailabilityResult) = x.generators 66 | 67 | function getindex(x::GeneratorAvailabilityResult, g::AbstractString, t::ZonedDateTime) 68 | i_g = findfirstunique(x.generators, g) 69 | i_t = findfirstunique(x.timestamps, t) 70 | return vec(x.available[i_g, i_t, :]) 71 | end 72 | 73 | function finalize( 74 | acc::GenAvailabilityAccumulator, 75 | system::SystemModel{N,L,T,P,E}, 76 | ) where {N,L,T,P,E} 77 | 78 | return GeneratorAvailabilityResult{N,L,T}( 79 | system.generators.names, system.timestamps, acc.available) 80 | 81 | end 82 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/GeneratorStorageAvailability.jl: -------------------------------------------------------------------------------- 1 | """ 2 | GeneratorStorageAvailability 3 | 4 | The `GeneratorStorageAvailability` result specification reports the sample-level 5 | discrete availability of `GeneratorStorages`, producing a 6 | `GeneratorStorageAvailabilityResult`. 7 | 8 | A `GeneratorStorageAvailabilityResult` can be indexed by generator-storage 9 | name and timestamp to retrieve a vector of sample-level availability states for 10 | the unit in the given timestep. States are provided as a boolean with 11 | `true` indicating that the unit is available and `false` indicating that 12 | it's unavailable. 13 | 14 | Example: 15 | 16 | ```julia 17 | genstoravail, = 18 | assess(sys, SequentialMonteCarlo(samples=10), GeneratorStorageAvailability()) 19 | 20 | samples = genstoravail["MyGenerator123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 21 | 22 | @assert samples isa Vector{Bool} 23 | @assert length(samples) == 10 24 | ``` 25 | """ 26 | struct GeneratorStorageAvailability <: ResultSpec end 27 | 28 | struct GenStorAvailabilityAccumulator <: ResultAccumulator{GeneratorStorageAvailability} 29 | 30 | available::Array{Bool,3} 31 | 32 | end 33 | 34 | function accumulator( 35 | sys::SystemModel{N}, nsamples::Int, ::GeneratorStorageAvailability 36 | ) where {N} 37 | 38 | ngenstors = length(sys.generatorstorages) 39 | available = zeros(Bool, ngenstors, N, nsamples) 40 | 41 | return GenStorAvailabilityAccumulator(available) 42 | 43 | end 44 | 45 | function merge!( 46 | x::GenStorAvailabilityAccumulator, y::GenStorAvailabilityAccumulator 47 | ) 48 | 49 | x.available .|= y.available 50 | return 51 | 52 | end 53 | 54 | accumulatortype(::GeneratorStorageAvailability) = GenStorAvailabilityAccumulator 55 | 56 | struct GeneratorStorageAvailabilityResult{N,L,T<:Period} <: AbstractAvailabilityResult{N,L,T} 57 | 58 | generatorstorages::Vector{String} 59 | timestamps::StepRange{ZonedDateTime,T} 60 | 61 | available::Array{Bool,3} 62 | 63 | end 64 | 65 | names(x::GeneratorStorageAvailabilityResult) = x.generatorstorages 66 | 67 | function getindex(x::GeneratorStorageAvailabilityResult, gs::AbstractString, t::ZonedDateTime) 68 | i_gs = findfirstunique(x.generatorstorages, gs) 69 | i_t = findfirstunique(x.timestamps, t) 70 | return vec(x.available[i_gs, i_t, :]) 71 | end 72 | 73 | function finalize( 74 | acc::GenStorAvailabilityAccumulator, 75 | system::SystemModel{N,L,T,P,E}, 76 | ) where {N,L,T,P,E} 77 | 78 | return GeneratorStorageAvailabilityResult{N,L,T}( 79 | system.generatorstorages.names, system.timestamps, acc.available) 80 | 81 | end 82 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/GeneratorStorageEnergy.jl: -------------------------------------------------------------------------------- 1 | """ 2 | GeneratorStorageEnergy 3 | 4 | The `GeneratorStorageEnergy` result specification reports the average state of 5 | charge of `GeneratorStorages`, producing a `GeneratorStorageEnergyResult`. 6 | 7 | A `GeneratorStorageEnergyResult` can be indexed by generator-storage device 8 | name and a timestamp to retrieve a tuple of sample mean and standard deviation, 9 | estimating the average energy level for the given generator-storage device in 10 | that timestep. 11 | 12 | Example: 13 | 14 | ```julia 15 | genstorenergy, = 16 | assess(sys, SequentialMonteCarlo(samples=1000), GeneratorStorageEnergy()) 17 | 18 | soc_mean, soc_std = 19 | genstorenergy["MyGeneratorStorage123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 20 | ``` 21 | See [`GeneratorStorageEnergySamples`](@ref) for sample-level generator-storage 22 | states of charge. 23 | 24 | See [`StorageEnergy`](@ref) for average storage states of charge. 25 | """ 26 | struct GeneratorStorageEnergy <: ResultSpec end 27 | 28 | mutable struct GenStorageEnergyAccumulator <: ResultAccumulator{GeneratorStorageEnergy} 29 | 30 | # Cross-simulation energy mean/variances 31 | energy_period::Vector{MeanVariance} 32 | energy_genstorperiod::Matrix{MeanVariance} 33 | 34 | end 35 | 36 | function accumulator( 37 | sys::SystemModel{N}, nsamples::Int, ::GeneratorStorageEnergy 38 | ) where {N} 39 | 40 | ngenstors = length(sys.generatorstorages) 41 | 42 | energy_period = [meanvariance() for _ in 1:N] 43 | energy_genstorperiod = [meanvariance() for _ in 1:ngenstors, _ in 1:N] 44 | 45 | return GenStorageEnergyAccumulator( 46 | energy_period, energy_genstorperiod) 47 | 48 | end 49 | 50 | function merge!( 51 | x::GenStorageEnergyAccumulator, y::GenStorageEnergyAccumulator 52 | ) 53 | 54 | foreach(merge!, x.energy_period, y.energy_period) 55 | foreach(merge!, x.energy_genstorperiod, y.energy_genstorperiod) 56 | 57 | return 58 | 59 | end 60 | 61 | accumulatortype(::GeneratorStorageEnergy) = GenStorageEnergyAccumulator 62 | 63 | struct GeneratorStorageEnergyResult{N,L,T<:Period,E<:EnergyUnit} <: AbstractEnergyResult{N,L,T} 64 | 65 | nsamples::Union{Int,Nothing} 66 | generatorstorages::Vector{String} 67 | timestamps::StepRange{ZonedDateTime,T} 68 | 69 | energy_mean::Matrix{Float64} 70 | 71 | energy_period_std::Vector{Float64} 72 | energy_regionperiod_std::Matrix{Float64} 73 | 74 | end 75 | 76 | names(x::GeneratorStorageEnergyResult) = x.generatorstorages 77 | 78 | function getindex(x::GeneratorStorageEnergyResult, t::ZonedDateTime) 79 | i_t = findfirstunique(x.timestamps, t) 80 | return sum(view(x.energy_mean, :, i_t)), x.energy_period_std[i_t] 81 | end 82 | 83 | function getindex(x::GeneratorStorageEnergyResult, gs::AbstractString, t::ZonedDateTime) 84 | i_gs = findfirstunique(x.generatorstorages, gs) 85 | i_t = findfirstunique(x.timestamps, t) 86 | return x.energy_mean[i_gs, i_t], x.energy_regionperiod_std[i_gs, i_t] 87 | end 88 | 89 | function finalize( 90 | acc::GenStorageEnergyAccumulator, 91 | system::SystemModel{N,L,T,P,E}, 92 | ) where {N,L,T,P,E} 93 | 94 | _, period_std = mean_std(acc.energy_period) 95 | genstorperiod_mean, genstorperiod_std = mean_std(acc.energy_genstorperiod) 96 | 97 | nsamples = first(first(acc.energy_period).stats).n 98 | 99 | return GeneratorStorageEnergyResult{N,L,T,E}( 100 | nsamples, system.generatorstorages.names, system.timestamps, 101 | genstorperiod_mean, period_std, genstorperiod_std) 102 | 103 | end 104 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/GeneratorStorageEnergySamples.jl: -------------------------------------------------------------------------------- 1 | """ 2 | GeneratorStorageEnergySamples 3 | 4 | The `GeneratorStorageEnergySamples` result specification reports the 5 | sample-level state of charge of `GeneratorStorages`, producing a 6 | `GeneratorStorageEnergySamplesResult`. 7 | 8 | A `GeneratorStorageEnergySamplesResult` can be indexed by generator-storage 9 | device name and a timestamp to retrieve a vector of sample-level charge states 10 | for the device in the given timestep. 11 | 12 | Example: 13 | 14 | ```julia 15 | genstorenergy, = 16 | assess(sys, SequentialMonteCarlo(samples=10), GeneratorStorageEnergySamples()) 17 | 18 | samples = genstorenergy["MyGeneratorStorage123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 19 | 20 | @assert samples isa Vector{Float64} 21 | @assert length(samples) == 10 22 | ``` 23 | 24 | Note that this result specification requires large amounts of memory for 25 | larger sample sizes. See [`GeneratorStorageEnergy`](@ref) for estimated average 26 | generator-storage state of charge when sample-level granularity isn't required. 27 | """ 28 | struct GeneratorStorageEnergySamples <: ResultSpec end 29 | 30 | struct GenStorageEnergySamplesAccumulator <: ResultAccumulator{GeneratorStorageEnergySamples} 31 | 32 | energy::Array{Float64,3} 33 | 34 | end 35 | 36 | function accumulator( 37 | sys::SystemModel{N}, nsamples::Int, ::GeneratorStorageEnergySamples 38 | ) where {N} 39 | 40 | ngenstors = length(sys.generatorstorages) 41 | energy = zeros(Int, ngenstors, N, nsamples) 42 | 43 | return GenStorageEnergySamplesAccumulator(energy) 44 | 45 | end 46 | 47 | function merge!( 48 | x::GenStorageEnergySamplesAccumulator, 49 | y::GenStorageEnergySamplesAccumulator 50 | ) 51 | 52 | x.energy .+= y.energy 53 | return 54 | 55 | end 56 | 57 | accumulatortype(::GeneratorStorageEnergySamples) = GenStorageEnergySamplesAccumulator 58 | 59 | struct GeneratorStorageEnergySamplesResult{N,L,T<:Period,E<:EnergyUnit} <: AbstractEnergyResult{N,L,T} 60 | 61 | generatorstorages::Vector{String} 62 | timestamps::StepRange{ZonedDateTime,T} 63 | 64 | energy::Array{Int,3} 65 | 66 | end 67 | 68 | names(x::GeneratorStorageEnergySamplesResult) = x.generatorstorages 69 | 70 | function getindex(x::GeneratorStorageEnergySamplesResult, t::ZonedDateTime) 71 | i_t = findfirstunique(x.timestamps, t) 72 | return vec(sum(view(x.energy, :, i_t, :), dims=1)) 73 | end 74 | 75 | function getindex(x::GeneratorStorageEnergySamplesResult, gs::AbstractString, t::ZonedDateTime) 76 | i_gs = findfirstunique(x.generatorstorages, gs) 77 | i_t = findfirstunique(x.timestamps, t) 78 | return vec(x.energy[i_gs, i_t, :]) 79 | end 80 | 81 | function finalize( 82 | acc::GenStorageEnergySamplesAccumulator, 83 | system::SystemModel{N,L,T,P,E}, 84 | ) where {N,L,T,P,E} 85 | 86 | return GeneratorStorageEnergySamplesResult{N,L,T,E}( 87 | system.generatorstorages.names, system.timestamps, acc.energy) 88 | 89 | end 90 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/LineAvailability.jl: -------------------------------------------------------------------------------- 1 | """ 2 | LineAvailability 3 | 4 | The `LineAvailability` result specification reports the sample-level 5 | discrete availability of `Lines`, producing a `LineAvailabilityResult`. 6 | 7 | A `LineAvailabilityResult` can be indexed by line name and 8 | timestamp to retrieve a vector of sample-level availability states for 9 | the unit in the given timestep. States are provided as a boolean with 10 | `true` indicating that the unit is available and `false` indicating that 11 | it's unavailable. 12 | 13 | Example: 14 | 15 | ```julia 16 | lineavail, = 17 | assess(sys, SequentialMonteCarlo(samples=10), LineAvailability()) 18 | 19 | samples = lineavail["MyLine123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 20 | 21 | @assert samples isa Vector{Bool} 22 | @assert length(samples) == 10 23 | ``` 24 | """ 25 | struct LineAvailability <: ResultSpec end 26 | 27 | struct LineAvailabilityResult{N,L,T<:Period} <: AbstractAvailabilityResult{N,L,T} 28 | 29 | lines::Vector{String} 30 | timestamps::StepRange{ZonedDateTime,T} 31 | 32 | available::Array{Bool,3} 33 | 34 | end 35 | 36 | names(x::LineAvailabilityResult) = x.lines 37 | 38 | function getindex(x::LineAvailabilityResult, l::AbstractString, t::ZonedDateTime) 39 | i_l = findfirstunique(x.lines, l) 40 | i_t = findfirstunique(x.timestamps, t) 41 | return vec(x.available[i_l, i_t, :]) 42 | end 43 | 44 | struct LineAvailabilityAccumulator <: ResultAccumulator{LineAvailability} 45 | 46 | available::Array{Bool,3} 47 | 48 | end 49 | 50 | accumulatortype(::LineAvailability) = LineAvailabilityAccumulator 51 | 52 | function accumulator( 53 | sys::SystemModel{N}, nsamples::Int, ::LineAvailability 54 | ) where {N} 55 | 56 | nlines = length(sys.lines) 57 | available = zeros(Bool, nlines, N, nsamples) 58 | 59 | return LineAvailabilityAccumulator(available) 60 | 61 | end 62 | 63 | function merge!( 64 | x::LineAvailabilityAccumulator, y::LineAvailabilityAccumulator 65 | ) 66 | 67 | x.available .|= y.available 68 | return 69 | 70 | end 71 | 72 | function finalize( 73 | acc::LineAvailabilityAccumulator, 74 | system::SystemModel{N,L,T,P,E}, 75 | ) where {N,L,T,P,E} 76 | 77 | return LineAvailabilityResult{N,L,T}( 78 | system.lines.names, system.timestamps, acc.available) 79 | 80 | end 81 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/Results.jl: -------------------------------------------------------------------------------- 1 | @reexport module Results 2 | 3 | import Base: broadcastable, getindex, merge! 4 | import OnlineStats: Series 5 | import OnlineStatsBase: EqualWeight, Mean, Variance, value 6 | import Printf: @sprintf 7 | import StatsBase: mean, std, stderror 8 | 9 | import ..Systems: SystemModel, ZonedDateTime, Period, 10 | PowerUnit, EnergyUnit, conversionfactor, 11 | unitsymbol, Regions 12 | export 13 | 14 | # Metrics 15 | ReliabilityMetric, LOLE, EUE, NEUE, 16 | val, stderror, 17 | 18 | # Result specifications 19 | Shortfall, ShortfallSamples, Surplus, SurplusSamples, 20 | Flow, FlowSamples, Utilization, UtilizationSamples, 21 | StorageEnergy, StorageEnergySamples, 22 | GeneratorStorageEnergy, GeneratorStorageEnergySamples, 23 | GeneratorAvailability, StorageAvailability, 24 | GeneratorStorageAvailability, LineAvailability 25 | 26 | include("utils.jl") 27 | include("metrics.jl") 28 | 29 | abstract type ResultSpec end 30 | 31 | abstract type ResultAccumulator{R<:ResultSpec} end 32 | 33 | abstract type Result{ 34 | N, # Number of timesteps simulated 35 | L, # Length of each simulation timestep 36 | T <: Period, # Units of each simulation timestep 37 | } end 38 | 39 | broadcastable(x::ResultSpec) = Ref(x) 40 | broadcastable(x::Result) = Ref(x) 41 | 42 | abstract type AbstractShortfallResult{N,L,T} <: Result{N,L,T} end 43 | 44 | getindex(x::AbstractShortfallResult, ::Colon, t::ZonedDateTime) = 45 | getindex.(x, x.regions, t) 46 | 47 | getindex(x::AbstractShortfallResult, r::AbstractString, ::Colon) = 48 | getindex.(x, r, x.timestamps) 49 | 50 | getindex(x::AbstractShortfallResult, ::Colon, ::Colon) = 51 | getindex.(x, x.regions, permutedims(x.timestamps)) 52 | 53 | 54 | LOLE(x::AbstractShortfallResult, ::Colon, t::ZonedDateTime) = 55 | LOLE.(x, x.regions.names, t) 56 | 57 | LOLE(x::AbstractShortfallResult, r::AbstractString, ::Colon) = 58 | LOLE.(x, r, x.timestamps) 59 | 60 | LOLE(x::AbstractShortfallResult, ::Colon, ::Colon) = 61 | LOLE.(x, x.regions.names, permutedims(x.timestamps)) 62 | 63 | 64 | EUE(x::AbstractShortfallResult, ::Colon, t::ZonedDateTime) = 65 | EUE.(x, x.regions.names, t) 66 | 67 | EUE(x::AbstractShortfallResult, r::AbstractString, ::Colon) = 68 | EUE.(x, r, x.timestamps) 69 | 70 | EUE(x::AbstractShortfallResult, ::Colon, ::Colon) = 71 | EUE.(x, x.regions.names, permutedims(x.timestamps)) 72 | 73 | NEUE(x::AbstractShortfallResult, r::AbstractString, ::Colon) = 74 | NEUE.(x, r, x.timestamps) 75 | 76 | NEUE(x::AbstractShortfallResult, ::Colon, ::Colon) = 77 | NEUE.(x, x.regions.names, permutedims(x.timestamps)) 78 | 79 | include("Shortfall.jl") 80 | include("ShortfallSamples.jl") 81 | 82 | abstract type AbstractSurplusResult{N,L,T} <: Result{N,L,T} end 83 | 84 | getindex(x::AbstractSurplusResult, ::Colon) = 85 | getindex.(x, x.timestamps) 86 | 87 | getindex(x::AbstractSurplusResult, ::Colon, t::ZonedDateTime) = 88 | getindex.(x, x.regions, t) 89 | 90 | getindex(x::AbstractSurplusResult, r::AbstractString, ::Colon) = 91 | getindex.(x, r, x.timestamps) 92 | 93 | getindex(x::AbstractSurplusResult, ::Colon, ::Colon) = 94 | getindex.(x, x.regions, permutedims(x.timestamps)) 95 | 96 | include("Surplus.jl") 97 | include("SurplusSamples.jl") 98 | 99 | abstract type AbstractFlowResult{N,L,T} <: Result{N,L,T} end 100 | 101 | getindex(x::AbstractFlowResult, ::Colon) = 102 | getindex.(x, x.interfaces) 103 | 104 | getindex(x::AbstractFlowResult, ::Colon, t::ZonedDateTime) = 105 | getindex.(x, x.interfaces, t) 106 | 107 | getindex(x::AbstractFlowResult, i::Pair{<:AbstractString,<:AbstractString}, ::Colon) = 108 | getindex.(x, i, x.timestamps) 109 | 110 | getindex(x::AbstractFlowResult, ::Colon, ::Colon) = 111 | getindex.(x, x.interfaces, permutedims(x.timestamps)) 112 | 113 | include("Flow.jl") 114 | include("FlowSamples.jl") 115 | 116 | abstract type AbstractUtilizationResult{N,L,T} <: Result{N,L,T} end 117 | 118 | getindex(x::AbstractUtilizationResult, ::Colon) = 119 | getindex.(x, x.interfaces) 120 | 121 | getindex(x::AbstractUtilizationResult, ::Colon, t::ZonedDateTime) = 122 | getindex.(x, x.interfaces, t) 123 | 124 | getindex(x::AbstractUtilizationResult, i::Pair{<:AbstractString,<:AbstractString}, ::Colon) = 125 | getindex.(x, i, x.timestamps) 126 | 127 | getindex(x::AbstractUtilizationResult, ::Colon, ::Colon) = 128 | getindex.(x, x.interfaces, permutedims(x.timestamps)) 129 | 130 | include("Utilization.jl") 131 | include("UtilizationSamples.jl") 132 | 133 | abstract type AbstractAvailabilityResult{N,L,T} <: Result{N,L,T} end 134 | 135 | getindex(x::AbstractAvailabilityResult, ::Colon, t::ZonedDateTime) = 136 | getindex.(x, names(x), t) 137 | 138 | getindex(x::AbstractAvailabilityResult, name::String, ::Colon) = 139 | getindex.(x, name, x.timestamps) 140 | 141 | getindex(x::AbstractAvailabilityResult, ::Colon, ::Colon) = 142 | getindex.(x, names(x), permutedims(x.timestamps)) 143 | 144 | include("GeneratorAvailability.jl") 145 | include("StorageAvailability.jl") 146 | include("GeneratorStorageAvailability.jl") 147 | include("LineAvailability.jl") 148 | 149 | abstract type AbstractEnergyResult{N,L,T} <: Result{N,L,T} end 150 | 151 | getindex(x::AbstractEnergyResult, ::Colon) = 152 | getindex.(x, x.timestamps) 153 | 154 | getindex(x::AbstractEnergyResult, ::Colon, t::ZonedDateTime) = 155 | getindex.(x, names(x), t) 156 | 157 | getindex(x::AbstractEnergyResult, name::String, ::Colon) = 158 | getindex.(x, name, x.timestamps) 159 | 160 | getindex(x::AbstractEnergyResult, ::Colon, ::Colon) = 161 | getindex.(x, names(x), permutedims(x.timestamps)) 162 | 163 | include("StorageEnergy.jl") 164 | include("GeneratorStorageEnergy.jl") 165 | include("StorageEnergySamples.jl") 166 | include("GeneratorStorageEnergySamples.jl") 167 | 168 | function resultchannel( 169 | results::T, threads::Int 170 | ) where T <: Tuple{Vararg{ResultSpec}} 171 | 172 | types = accumulatortype.(results) 173 | return Channel{Tuple{types...}}(threads) 174 | 175 | end 176 | 177 | merge!(xs::T, ys::T) where T <: Tuple{Vararg{ResultAccumulator}} = 178 | foreach(merge!, xs, ys) 179 | 180 | function finalize( 181 | results::Channel{<:Tuple{Vararg{ResultAccumulator}}}, 182 | system::SystemModel{N,L,T,P,E}, 183 | threads::Int 184 | ) where {N,L,T,P,E} 185 | 186 | total_result = take!(results) 187 | 188 | for _ in 2:threads 189 | thread_result = take!(results) 190 | merge!(total_result, thread_result) 191 | end 192 | close(results) 193 | 194 | return finalize.(total_result, system) 195 | 196 | end 197 | 198 | end 199 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/Shortfall.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Shortfall 3 | 4 | The `Shortfall` result specification reports expectation-based resource 5 | adequacy risk metrics such as EUE and LOLE, producing a `ShortfallResult`. 6 | 7 | A `ShortfallResult` can be directly indexed by a region name and a timestamp to retrieve a tuple of sample mean and standard deviation, estimating 8 | the average unserved energy in that region and timestep. However, in most 9 | cases it's simpler to use [`EUE`](@ref) and [`LOLE`](@ref) constructors to 10 | directly retrieve standard risk metrics. 11 | 12 | Example: 13 | 14 | ```julia 15 | shortfall, = 16 | assess(sys, SequentialMonteCarlo(samples=1000), Shortfall()) 17 | 18 | period = ZonedDateTime(2020, 1, 1, 0, tz"UTC") 19 | 20 | # Unserved energy mean and standard deviation 21 | sf_mean, sf_std = shortfall["Region A", period] 22 | 23 | # System-wide risk metrics 24 | eue = EUE(shortfall) 25 | lole = LOLE(shortfall) 26 | neue = NEUE(shorfall) 27 | 28 | # Regional risk metrics 29 | regional_eue = EUE(shortfall, "Region A") 30 | regional_lole = LOLE(shortfall, "Region A") 31 | regional_neue = NEUE(shortfall, "Region A") 32 | 33 | # Period-specific risk metrics 34 | period_eue = EUE(shortfall, period) 35 | period_lolp = LOLE(shortfall, period) 36 | 37 | # Region- and period-specific risk metrics 38 | period_eue = EUE(shortfall, "Region A", period) 39 | period_lolp = LOLE(shortfall, "Region A", period) 40 | ``` 41 | 42 | See [`ShortfallSamples`](@ref) for recording sample-level shortfall results. 43 | """ 44 | struct Shortfall <: ResultSpec end 45 | 46 | mutable struct ShortfallAccumulator <: ResultAccumulator{Shortfall} 47 | 48 | # Cross-simulation LOL period count mean/variances 49 | periodsdropped_total::MeanVariance 50 | periodsdropped_region::Vector{MeanVariance} 51 | periodsdropped_period::Vector{MeanVariance} 52 | periodsdropped_regionperiod::Matrix{MeanVariance} 53 | 54 | # Running LOL period counts for current simulation 55 | periodsdropped_total_currentsim::Int 56 | periodsdropped_region_currentsim::Vector{Int} 57 | 58 | # Cross-simulation UE mean/variances 59 | unservedload_total::MeanVariance 60 | unservedload_region::Vector{MeanVariance} 61 | unservedload_period::Vector{MeanVariance} 62 | unservedload_regionperiod::Matrix{MeanVariance} 63 | 64 | # Running UE totals for current simulation 65 | unservedload_total_currentsim::Int 66 | unservedload_region_currentsim::Vector{Int} 67 | 68 | end 69 | 70 | function accumulator( 71 | sys::SystemModel{N}, nsamples::Int, ::Shortfall 72 | ) where {N} 73 | 74 | nregions = length(sys.regions) 75 | 76 | periodsdropped_total = meanvariance() 77 | periodsdropped_region = [meanvariance() for _ in 1:nregions] 78 | periodsdropped_period = [meanvariance() for _ in 1:N] 79 | periodsdropped_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] 80 | 81 | periodsdropped_total_currentsim = 0 82 | periodsdropped_region_currentsim = zeros(Int, nregions) 83 | 84 | unservedload_total = meanvariance() 85 | unservedload_region = [meanvariance() for _ in 1:nregions] 86 | unservedload_period = [meanvariance() for _ in 1:N] 87 | unservedload_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] 88 | 89 | unservedload_total_currentsim = 0 90 | unservedload_region_currentsim = zeros(Int, nregions) 91 | 92 | return ShortfallAccumulator( 93 | periodsdropped_total, periodsdropped_region, 94 | periodsdropped_period, periodsdropped_regionperiod, 95 | periodsdropped_total_currentsim, periodsdropped_region_currentsim, 96 | unservedload_total, unservedload_region, 97 | unservedload_period, unservedload_regionperiod, 98 | unservedload_total_currentsim, unservedload_region_currentsim) 99 | 100 | end 101 | 102 | function merge!( 103 | x::ShortfallAccumulator, y::ShortfallAccumulator 104 | ) 105 | 106 | merge!(x.periodsdropped_total, y.periodsdropped_total) 107 | foreach(merge!, x.periodsdropped_region, y.periodsdropped_region) 108 | foreach(merge!, x.periodsdropped_period, y.periodsdropped_period) 109 | foreach(merge!, x.periodsdropped_regionperiod, y.periodsdropped_regionperiod) 110 | 111 | merge!(x.unservedload_total, y.unservedload_total) 112 | foreach(merge!, x.unservedload_region, y.unservedload_region) 113 | foreach(merge!, x.unservedload_period, y.unservedload_period) 114 | foreach(merge!, x.unservedload_regionperiod, y.unservedload_regionperiod) 115 | 116 | return 117 | 118 | end 119 | 120 | accumulatortype(::Shortfall) = ShortfallAccumulator 121 | 122 | struct ShortfallResult{N, L, T <: Period, E <: EnergyUnit} <: 123 | AbstractShortfallResult{N, L, T} 124 | nsamples::Union{Int, Nothing} 125 | regions::Regions 126 | timestamps::StepRange{ZonedDateTime,T} 127 | 128 | eventperiod_mean::Float64 129 | eventperiod_std::Float64 130 | 131 | eventperiod_region_mean::Vector{Float64} 132 | eventperiod_region_std::Vector{Float64} 133 | 134 | eventperiod_period_mean::Vector{Float64} 135 | eventperiod_period_std::Vector{Float64} 136 | 137 | eventperiod_regionperiod_mean::Matrix{Float64} 138 | eventperiod_regionperiod_std::Matrix{Float64} 139 | 140 | 141 | shortfall_mean::Matrix{Float64} # r x t 142 | 143 | shortfall_std::Float64 144 | shortfall_region_std::Vector{Float64} 145 | shortfall_period_std::Vector{Float64} 146 | shortfall_regionperiod_std::Matrix{Float64} 147 | 148 | function ShortfallResult{N,L,T,E}( 149 | nsamples::Union{Int,Nothing}, 150 | regions::Regions, 151 | timestamps::StepRange{ZonedDateTime,T}, 152 | eventperiod_mean::Float64, 153 | eventperiod_std::Float64, 154 | eventperiod_region_mean::Vector{Float64}, 155 | eventperiod_region_std::Vector{Float64}, 156 | eventperiod_period_mean::Vector{Float64}, 157 | eventperiod_period_std::Vector{Float64}, 158 | eventperiod_regionperiod_mean::Matrix{Float64}, 159 | eventperiod_regionperiod_std::Matrix{Float64}, 160 | shortfall_mean::Matrix{Float64}, 161 | shortfall_std::Float64, 162 | shortfall_region_std::Vector{Float64}, 163 | shortfall_period_std::Vector{Float64}, 164 | shortfall_regionperiod_std::Matrix{Float64} 165 | ) where {N,L,T<:Period,E<:EnergyUnit} 166 | 167 | isnothing(nsamples) || nsamples > 0 || 168 | throw(DomainError("Sample count must be positive or `nothing`.")) 169 | 170 | 171 | length(timestamps) == N || 172 | error("The provided timestamp range does not match the simulation length") 173 | 174 | nregions = length(regions.names) 175 | 176 | length(eventperiod_region_mean) == nregions && 177 | length(eventperiod_region_std) == nregions && 178 | length(eventperiod_period_mean) == N && 179 | length(eventperiod_period_std) == N && 180 | size(eventperiod_regionperiod_mean) == (nregions, N) && 181 | size(eventperiod_regionperiod_std) == (nregions, N) && 182 | length(shortfall_region_std) == nregions && 183 | length(shortfall_period_std) == N && 184 | size(shortfall_regionperiod_std) == (nregions, N) || 185 | error("Inconsistent input data sizes") 186 | 187 | new{N,L,T,E}(nsamples, regions, timestamps, 188 | eventperiod_mean, eventperiod_std, 189 | eventperiod_region_mean, eventperiod_region_std, 190 | eventperiod_period_mean, eventperiod_period_std, 191 | eventperiod_regionperiod_mean, eventperiod_regionperiod_std, 192 | shortfall_mean, shortfall_std, 193 | shortfall_region_std, shortfall_period_std, 194 | shortfall_regionperiod_std) 195 | 196 | end 197 | 198 | end 199 | 200 | function getindex(x::ShortfallResult) 201 | return sum(x.shortfall_mean), x.shortfall_std 202 | end 203 | 204 | function getindex(x::ShortfallResult, r::AbstractString) 205 | i_r = findfirstunique(x.regions.names, r) 206 | return sum(view(x.shortfall_mean, i_r, :)), x.shortfall_region_std[i_r] 207 | end 208 | 209 | function getindex(x::ShortfallResult, t::ZonedDateTime) 210 | i_t = findfirstunique(x.timestamps, t) 211 | return sum(view(x.shortfall_mean, :, i_t)), x.shortfall_period_std[i_t] 212 | end 213 | 214 | function getindex(x::ShortfallResult, r::AbstractString, t::ZonedDateTime) 215 | i_r = findfirstunique(x.regions.names, r) 216 | i_t = findfirstunique(x.timestamps, t) 217 | return x.shortfall_mean[i_r, i_t], x.shortfall_regionperiod_std[i_r, i_t] 218 | end 219 | 220 | 221 | LOLE(x::ShortfallResult{N,L,T}) where {N,L,T} = 222 | LOLE{N,L,T}(MeanEstimate(x.eventperiod_mean, 223 | x.eventperiod_std, 224 | x.nsamples)) 225 | 226 | function LOLE(x::ShortfallResult{N,L,T}, r::AbstractString) where {N,L,T} 227 | i_r = findfirstunique(x.regions.names, r) 228 | return LOLE{N,L,T}(MeanEstimate(x.eventperiod_region_mean[i_r], 229 | x.eventperiod_region_std[i_r], 230 | x.nsamples)) 231 | end 232 | 233 | function LOLE(x::ShortfallResult{N,L,T}, t::ZonedDateTime) where {N,L,T} 234 | i_t = findfirstunique(x.timestamps, t) 235 | return LOLE{1,L,T}(MeanEstimate(x.eventperiod_period_mean[i_t], 236 | x.eventperiod_period_std[i_t], 237 | x.nsamples)) 238 | end 239 | 240 | function LOLE(x::ShortfallResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} 241 | i_r = findfirstunique(x.regions.names, r) 242 | i_t = findfirstunique(x.timestamps, t) 243 | return LOLE{1,L,T}(MeanEstimate(x.eventperiod_regionperiod_mean[i_r, i_t], 244 | x.eventperiod_regionperiod_std[i_r, i_t], 245 | x.nsamples)) 246 | end 247 | 248 | 249 | EUE(x::ShortfallResult{N,L,T,E}) where {N,L,T,E} = 250 | EUE{N,L,T,E}(MeanEstimate(x[]..., x.nsamples)) 251 | 252 | EUE(x::ShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} = 253 | EUE{N,L,T,E}(MeanEstimate(x[r]..., x.nsamples)) 254 | 255 | EUE(x::ShortfallResult{N,L,T,E}, t::ZonedDateTime) where {N,L,T,E} = 256 | EUE{1,L,T,E}(MeanEstimate(x[t]..., x.nsamples)) 257 | 258 | EUE(x::ShortfallResult{N,L,T,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E} = 259 | EUE{1,L,T,E}(MeanEstimate(x[r, t]..., x.nsamples)) 260 | 261 | function NEUE(x::ShortfallResult{N,L,T,E}) where {N,L,T,E} 262 | return NEUE(div(MeanEstimate(x[]..., x.nsamples),(sum(x.regions.load)/1e6))) 263 | end 264 | 265 | function NEUE(x::ShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E} 266 | i_r = findfirstunique(x.regions.names, r) 267 | return NEUE(div(MeanEstimate(x[r]..., x.nsamples),(sum(x.regions.load[i_r,:])/1e6))) 268 | end 269 | 270 | function finalize( 271 | acc::ShortfallAccumulator, 272 | system::SystemModel{N,L,T,P,E}, 273 | ) where {N,L,T,P,E} 274 | 275 | ep_total_mean, ep_total_std = mean_std(acc.periodsdropped_total) 276 | ep_region_mean, ep_region_std = mean_std(acc.periodsdropped_region) 277 | ep_period_mean, ep_period_std = mean_std(acc.periodsdropped_period) 278 | ep_regionperiod_mean, ep_regionperiod_std = 279 | mean_std(acc.periodsdropped_regionperiod) 280 | 281 | _, ue_total_std = mean_std(acc.unservedload_total) 282 | _, ue_region_std = mean_std(acc.unservedload_region) 283 | _, ue_period_std = mean_std(acc.unservedload_period) 284 | ue_regionperiod_mean, ue_regionperiod_std = 285 | mean_std(acc.unservedload_regionperiod) 286 | 287 | nsamples = first(acc.unservedload_total.stats).n 288 | 289 | p2e = conversionfactor(L,T,P,E) 290 | ue_regionperiod_mean .*= p2e 291 | ue_total_std *= p2e 292 | ue_region_std .*= p2e 293 | ue_period_std .*= p2e 294 | ue_regionperiod_std .*= p2e 295 | 296 | return ShortfallResult{N,L,T,E}( 297 | nsamples, system.regions, system.timestamps, 298 | ep_total_mean, ep_total_std, ep_region_mean, ep_region_std, 299 | ep_period_mean, ep_period_std, 300 | ep_regionperiod_mean, ep_regionperiod_std, 301 | ue_regionperiod_mean, ue_total_std, 302 | ue_region_std, ue_period_std, ue_regionperiod_std) 303 | 304 | end 305 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/ShortfallSamples.jl: -------------------------------------------------------------------------------- 1 | """ 2 | ShortfallSamples 3 | 4 | The `ShortfallSamples` result specification reports sample-level unserved energy outcomes, producing a `ShortfallSamplesResult`. 5 | 6 | A `ShortfallSamplesResult` can be directly indexed by a region name and a 7 | timestamp to retrieve a vector of sample-level unserved energy results in that 8 | region and timestep. [`EUE`](@ref) and [`LOLE`](@ref) constructors can also 9 | be used to retrieve standard risk metrics. 10 | 11 | Example: 12 | 13 | ```julia 14 | shortfall, = 15 | assess(sys, SequentialMonteCarlo(samples=10), ShortfallSamples()) 16 | 17 | period = ZonedDateTime(2020, 1, 1, 0, tz"UTC") 18 | 19 | samples = shortfall["Region A", period] 20 | 21 | @assert samples isa Vector{Float64} 22 | @assert length(samples) == 10 23 | 24 | # System-wide risk metrics 25 | eue = EUE(shortfall) 26 | lole = LOLE(shortfall) 27 | neue = NEUE(shortfall) 28 | 29 | # Regional risk metrics 30 | regional_eue = EUE(shortfall, "Region A") 31 | regional_lole = LOLE(shortfall, "Region A") 32 | regional_neue = NEUE(shortfall, "Region A") 33 | 34 | # Period-specific risk metrics 35 | period_eue = EUE(shortfall, period) 36 | period_lolp = LOLE(shortfall, period) 37 | 38 | # Region- and period-specific risk metrics 39 | period_eue = EUE(shortfall, "Region A", period) 40 | period_lolp = LOLE(shortfall, "Region A", period) 41 | ``` 42 | 43 | Note that this result specification requires large amounts of memory for 44 | larger sample sizes. See [`Shortfall`](@ref) for average shortfall outcomes when sample-level granularity isn't required. 45 | """ 46 | struct ShortfallSamples <: ResultSpec end 47 | 48 | struct ShortfallSamplesAccumulator <: ResultAccumulator{ShortfallSamples} 49 | 50 | shortfall::Array{Int,3} 51 | 52 | end 53 | 54 | function accumulator( 55 | sys::SystemModel{N}, nsamples::Int, ::ShortfallSamples 56 | ) where {N} 57 | 58 | nregions = length(sys.regions) 59 | shortfall = zeros(Int, nregions, N, nsamples) 60 | 61 | return ShortfallSamplesAccumulator(shortfall) 62 | 63 | end 64 | 65 | function merge!( 66 | x::ShortfallSamplesAccumulator, y::ShortfallSamplesAccumulator 67 | ) 68 | 69 | x.shortfall .+= y.shortfall 70 | return 71 | 72 | end 73 | 74 | accumulatortype(::ShortfallSamples) = ShortfallSamplesAccumulator 75 | 76 | struct ShortfallSamplesResult{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractShortfallResult{N,L,T} 77 | 78 | regions::Regions{N,P} 79 | timestamps::StepRange{ZonedDateTime,T} 80 | 81 | shortfall::Array{Int,3} # r x t x s 82 | 83 | end 84 | 85 | function getindex( 86 | x::ShortfallSamplesResult{N,L,T,P,E} 87 | ) where {N,L,T,P,E} 88 | p2e = conversionfactor(L, T, P, E) 89 | return vec(p2e * sum(x.shortfall, dims=1:2)) 90 | end 91 | 92 | function getindex( 93 | x::ShortfallSamplesResult{N,L,T,P,E}, r::AbstractString 94 | ) where {N,L,T,P,E} 95 | i_r = findfirstunique(x.regions.names, r) 96 | p2e = conversionfactor(L, T, P, E) 97 | return vec(p2e * sum(view(x.shortfall, i_r, :, :), dims=1)) 98 | end 99 | 100 | function getindex( 101 | x::ShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime 102 | ) where {N,L,T,P,E} 103 | i_t = findfirstunique(x.timestamps, t) 104 | p2e = conversionfactor(L, T, P, E) 105 | return vec(p2e * sum(view(x.shortfall, :, i_t, :), dims=1)) 106 | end 107 | 108 | function getindex( 109 | x::ShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime 110 | ) where {N,L,T,P,E} 111 | i_r = findfirstunique(x.regions.names, r) 112 | i_t = findfirstunique(x.timestamps, t) 113 | p2e = conversionfactor(L, T, P, E) 114 | return vec(p2e * x.shortfall[i_r, i_t, :]) 115 | end 116 | 117 | 118 | function LOLE(x::ShortfallSamplesResult{N,L,T}) where {N,L,T} 119 | eventperiods = sum(sum(x.shortfall, dims=1) .> 0, dims=2) 120 | return LOLE{N,L,T}(MeanEstimate(eventperiods)) 121 | end 122 | 123 | function LOLE(x::ShortfallSamplesResult{N,L,T}, r::AbstractString) where {N,L,T} 124 | i_r = findfirstunique(x.regions.names, r) 125 | eventperiods = sum(view(x.shortfall, i_r, :, :) .> 0, dims=1) 126 | return LOLE{N,L,T}(MeanEstimate(eventperiods)) 127 | end 128 | 129 | function LOLE(x::ShortfallSamplesResult{N,L,T}, t::ZonedDateTime) where {N,L,T} 130 | i_t = findfirstunique(x.timestamps, t) 131 | eventperiods = sum(view(x.shortfall, :, i_t, :), dims=1) .> 0 132 | return LOLE{1,L,T}(MeanEstimate(eventperiods)) 133 | end 134 | 135 | function LOLE(x::ShortfallSamplesResult{N,L,T}, r::AbstractString, t::ZonedDateTime) where {N,L,T} 136 | i_r = findfirstunique(x.regions.names, r) 137 | i_t = findfirstunique(x.timestamps, t) 138 | eventperiods = view(x.shortfall, i_r, i_t, :) .> 0 139 | return LOLE{1,L,T}(MeanEstimate(eventperiods)) 140 | end 141 | 142 | 143 | EUE(x::ShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} = 144 | EUE{N,L,T,E}(MeanEstimate(x[])) 145 | 146 | EUE(x::ShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} = 147 | EUE{N,L,T,E}(MeanEstimate(x[r])) 148 | 149 | EUE(x::ShortfallSamplesResult{N,L,T,P,E}, t::ZonedDateTime) where {N,L,T,P,E} = 150 | EUE{1,L,T,E}(MeanEstimate(x[t])) 151 | 152 | EUE(x::ShortfallSamplesResult{N,L,T,P,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,P,E} = 153 | EUE{1,L,T,E}(MeanEstimate(x[r, t])) 154 | 155 | function NEUE(x::ShortfallSamplesResult{N,L,T,P,E}) where {N,L,T,P,E} 156 | return NEUE(div(MeanEstimate(x[]),(sum(x.regions.load)/1e6))) 157 | end 158 | 159 | function NEUE(x::ShortfallSamplesResult{N,L,T,P,E}, r::AbstractString) where {N,L,T,P,E} 160 | i_r = findfirstunique(x.regions.names, r) 161 | return NEUE(div(MeanEstimate(x[r]),(sum(x.regions.load[i_r,:])/1e6))) 162 | end 163 | 164 | function finalize( 165 | acc::ShortfallSamplesAccumulator, 166 | system::SystemModel{N,L,T,P,E}, 167 | ) where {N,L,T,P,E} 168 | 169 | return ShortfallSamplesResult{N,L,T,P,E}( 170 | system.regions, system.timestamps, acc.shortfall) 171 | 172 | end 173 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/StorageAvailability.jl: -------------------------------------------------------------------------------- 1 | """ 2 | StorageAvailability 3 | 4 | The `StorageAvailability` result specification reports the sample-level 5 | discrete availability of `Storages`, producing a `StorageAvailabilityResult`. 6 | 7 | A `StorageAvailabilityResult` can be indexed by storage device name and 8 | a timestamp to retrieve a vector of sample-level availability states for 9 | the unit in the given timestep. States are provided as a boolean with 10 | `true` indicating that the unit is available and `false` indicating that 11 | it's unavailable. 12 | 13 | Example: 14 | 15 | ```julia 16 | storavail, = 17 | assess(sys, SequentialMonteCarlo(samples=10), StorageAvailability()) 18 | 19 | samples = storavail["MyStorage123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 20 | 21 | @assert samples isa Vector{Bool} 22 | @assert length(samples) == 10 23 | ``` 24 | """ 25 | struct StorageAvailability <: ResultSpec end 26 | 27 | struct StorAvailabilityAccumulator <: ResultAccumulator{StorageAvailability} 28 | 29 | available::Array{Bool,3} 30 | 31 | end 32 | 33 | function accumulator( 34 | sys::SystemModel{N}, nsamples::Int, ::StorageAvailability 35 | ) where {N} 36 | 37 | nstors = length(sys.storages) 38 | available = zeros(Bool, nstors, N, nsamples) 39 | 40 | return StorAvailabilityAccumulator(available) 41 | 42 | end 43 | 44 | function merge!( 45 | x::StorAvailabilityAccumulator, y::StorAvailabilityAccumulator 46 | ) 47 | 48 | x.available .|= y.available 49 | return 50 | 51 | end 52 | 53 | accumulatortype(::StorageAvailability) = StorAvailabilityAccumulator 54 | 55 | struct StorageAvailabilityResult{N,L,T<:Period} <: AbstractAvailabilityResult{N,L,T} 56 | 57 | storages::Vector{String} 58 | timestamps::StepRange{ZonedDateTime,T} 59 | 60 | available::Array{Bool,3} 61 | 62 | end 63 | 64 | names(x::StorageAvailabilityResult) = x.storages 65 | 66 | function getindex(x::StorageAvailabilityResult, s::AbstractString, t::ZonedDateTime) 67 | i_s = findfirstunique(x.storages, s) 68 | i_t = findfirstunique(x.timestamps, t) 69 | return vec(x.available[i_s, i_t, :]) 70 | end 71 | 72 | function finalize( 73 | acc::StorAvailabilityAccumulator, 74 | system::SystemModel{N,L,T,P,E}, 75 | ) where {N,L,T,P,E} 76 | 77 | return StorageAvailabilityResult{N,L,T}( 78 | system.storages.names, system.timestamps, acc.available) 79 | 80 | end 81 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/StorageEnergy.jl: -------------------------------------------------------------------------------- 1 | """ 2 | StorageEnergy 3 | 4 | The `StorageEnergy` result specification reports the average state of charge 5 | of `Storages`, producing a `StorageEnergyResult`. 6 | 7 | A `StorageEnergyResult` can be indexed by storage device name and a timestamp to 8 | retrieve a tuple of sample mean and standard deviation, estimating the average 9 | energy level for the given storage device in that timestep. 10 | 11 | Example: 12 | 13 | ```julia 14 | storenergy, = 15 | assess(sys, SequentialMonteCarlo(samples=1000), StorageEnergy()) 16 | 17 | soc_mean, soc_std = 18 | storenergy["MyStorage123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 19 | ``` 20 | 21 | See [`StorageEnergySamples`](@ref) for sample-level storage states of charge. 22 | 23 | See [`GeneratorStorageEnergy`](@ref) for average generator-storage states 24 | of charge. 25 | """ 26 | struct StorageEnergy <: ResultSpec end 27 | 28 | mutable struct StorageEnergyAccumulator <: ResultAccumulator{StorageEnergy} 29 | 30 | # Cross-simulation energy mean/variances 31 | energy_period::Vector{MeanVariance} 32 | energy_storageperiod::Matrix{MeanVariance} 33 | 34 | end 35 | 36 | function accumulator( 37 | sys::SystemModel{N}, nsamples::Int, ::StorageEnergy 38 | ) where {N} 39 | 40 | nstorages = length(sys.storages) 41 | 42 | energy_period = [meanvariance() for _ in 1:N] 43 | energy_storageperiod = [meanvariance() for _ in 1:nstorages, _ in 1:N] 44 | 45 | return StorageEnergyAccumulator( 46 | energy_period, energy_storageperiod) 47 | 48 | end 49 | 50 | function merge!( 51 | x::StorageEnergyAccumulator, y::StorageEnergyAccumulator 52 | ) 53 | 54 | foreach(merge!, x.energy_period, y.energy_period) 55 | foreach(merge!, x.energy_storageperiod, y.energy_storageperiod) 56 | 57 | return 58 | 59 | end 60 | 61 | accumulatortype(::StorageEnergy) = StorageEnergyAccumulator 62 | 63 | struct StorageEnergyResult{N,L,T<:Period,E<:EnergyUnit} <: AbstractEnergyResult{N,L,T} 64 | 65 | nsamples::Union{Int,Nothing} 66 | storages::Vector{String} 67 | timestamps::StepRange{ZonedDateTime,T} 68 | 69 | energy_mean::Matrix{Float64} 70 | 71 | energy_period_std::Vector{Float64} 72 | energy_regionperiod_std::Matrix{Float64} 73 | 74 | end 75 | 76 | names(x::StorageEnergyResult) = x.storages 77 | 78 | function getindex(x::StorageEnergyResult, t::ZonedDateTime) 79 | i_t = findfirstunique(x.timestamps, t) 80 | return sum(view(x.energy_mean, :, i_t)), x.energy_period_std[i_t] 81 | end 82 | 83 | function getindex(x::StorageEnergyResult, s::AbstractString, t::ZonedDateTime) 84 | i_s = findfirstunique(x.storages, s) 85 | i_t = findfirstunique(x.timestamps, t) 86 | return x.energy_mean[i_s, i_t], x.energy_regionperiod_std[i_s, i_t] 87 | end 88 | 89 | function finalize( 90 | acc::StorageEnergyAccumulator, 91 | system::SystemModel{N,L,T,P,E}, 92 | ) where {N,L,T,P,E} 93 | 94 | _, period_std = mean_std(acc.energy_period) 95 | storageperiod_mean, storageperiod_std = mean_std(acc.energy_storageperiod) 96 | 97 | nsamples = first(first(acc.energy_period).stats).n 98 | 99 | return StorageEnergyResult{N,L,T,E}( 100 | nsamples, system.storages.names, system.timestamps, 101 | storageperiod_mean, period_std, storageperiod_std) 102 | 103 | end 104 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/StorageEnergySamples.jl: -------------------------------------------------------------------------------- 1 | """ 2 | StorageEnergySamples 3 | 4 | The `StorageEnergySamples` result specification reports the sample-level state 5 | of charge of `Storages`, producing a `StorageEnergySamplesResult`. 6 | 7 | A `StorageEnergySamplesResult` can be indexed by storage device name and 8 | a timestamp to retrieve a vector of sample-level charge states for 9 | the device in the given timestep. 10 | 11 | Example: 12 | 13 | ```julia 14 | storenergy, = 15 | assess(sys, SequentialMonteCarlo(samples=10), StorageEnergySamples()) 16 | 17 | samples = storenergy["MyStorage123", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 18 | 19 | @assert samples isa Vector{Float64} 20 | @assert length(samples) == 10 21 | ``` 22 | 23 | Note that this result specification requires large amounts of memory for 24 | larger sample sizes. See [`StorageEnergy`](@ref) for estimated average storage 25 | state of charge when sample-level granularity isn't required. 26 | """ 27 | struct StorageEnergySamples <: ResultSpec end 28 | 29 | struct StorageEnergySamplesAccumulator <: ResultAccumulator{StorageEnergySamples} 30 | 31 | energy::Array{Float64,3} 32 | 33 | end 34 | 35 | function accumulator( 36 | sys::SystemModel{N}, nsamples::Int, ::StorageEnergySamples 37 | ) where {N} 38 | 39 | nstors = length(sys.storages) 40 | energy = zeros(Int, nstors, N, nsamples) 41 | 42 | return StorageEnergySamplesAccumulator(energy) 43 | 44 | end 45 | 46 | function merge!( 47 | x::StorageEnergySamplesAccumulator, y::StorageEnergySamplesAccumulator 48 | ) 49 | 50 | x.energy .+= y.energy 51 | return 52 | 53 | end 54 | 55 | accumulatortype(::StorageEnergySamples) = StorageEnergySamplesAccumulator 56 | 57 | struct StorageEnergySamplesResult{N,L,T<:Period,E<:EnergyUnit} <: AbstractEnergyResult{N,L,T} 58 | 59 | storages::Vector{String} 60 | timestamps::StepRange{ZonedDateTime,T} 61 | 62 | energy::Array{Int,3} 63 | 64 | end 65 | 66 | names(x::StorageEnergySamplesResult) = x.storages 67 | 68 | function getindex(x::StorageEnergySamplesResult, t::ZonedDateTime) 69 | i_t = findfirstunique(x.timestamps, t) 70 | return vec(sum(view(x.energy, :, i_t, :), dims=1)) 71 | end 72 | 73 | function getindex(x::StorageEnergySamplesResult, s::AbstractString, t::ZonedDateTime) 74 | i_s = findfirstunique(x.storages, s) 75 | i_t = findfirstunique(x.timestamps, t) 76 | return vec(x.energy[i_s, i_t, :]) 77 | end 78 | 79 | function finalize( 80 | acc::StorageEnergySamplesAccumulator, 81 | system::SystemModel{N,L,T,P,E}, 82 | ) where {N,L,T,P,E} 83 | 84 | return StorageEnergySamplesResult{N,L,T,E}( 85 | system.storages.names, system.timestamps, acc.energy) 86 | 87 | end 88 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/Surplus.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Surplus 3 | 4 | The `Surplus` result specification reports unused generation and storage 5 | discharge capability of `Regions`, producing a `SurplusResult`. 6 | 7 | A `SurplusResult` can be indexed by region name and timestamp to retrieve 8 | a tuple of sample mean and standard deviation, estimating the average 9 | unused capacity in that region and timestep. 10 | 11 | Example: 12 | 13 | ```julia 14 | surplus, = 15 | assess(sys, SequentialMonteCarlo(samples=1000), Surplus()) 16 | 17 | surplus_mean, surplus_std = 18 | surplus["Region A", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 19 | ``` 20 | 21 | See [`SurplusSamples`](@ref) for sample-level surplus results. 22 | """ 23 | struct Surplus <: ResultSpec end 24 | 25 | mutable struct SurplusAccumulator <: ResultAccumulator{Surplus} 26 | 27 | # Cross-simulation surplus mean/variances 28 | surplus_period::Vector{MeanVariance} 29 | surplus_regionperiod::Matrix{MeanVariance} 30 | 31 | end 32 | 33 | function accumulator( 34 | sys::SystemModel{N}, nsamples::Int, ::Surplus 35 | ) where {N} 36 | 37 | nregions = length(sys.regions) 38 | 39 | surplus_period = [meanvariance() for _ in 1:N] 40 | surplus_regionperiod = [meanvariance() for _ in 1:nregions, _ in 1:N] 41 | 42 | return SurplusAccumulator( 43 | surplus_period, surplus_regionperiod) 44 | 45 | end 46 | 47 | function merge!( 48 | x::SurplusAccumulator, y::SurplusAccumulator 49 | ) 50 | 51 | foreach(merge!, x.surplus_period, y.surplus_period) 52 | foreach(merge!, x.surplus_regionperiod, y.surplus_regionperiod) 53 | 54 | return 55 | 56 | end 57 | 58 | accumulatortype(::Surplus) = SurplusAccumulator 59 | 60 | struct SurplusResult{N,L,T<:Period,P<:PowerUnit} <: AbstractSurplusResult{N,L,T} 61 | 62 | nsamples::Union{Int,Nothing} 63 | regions::Vector{String} 64 | timestamps::StepRange{ZonedDateTime,T} 65 | 66 | surplus_mean::Matrix{Float64} 67 | 68 | surplus_period_std::Vector{Float64} 69 | surplus_regionperiod_std::Matrix{Float64} 70 | 71 | end 72 | 73 | function getindex(x::SurplusResult, t::ZonedDateTime) 74 | i_t = findfirstunique(x.timestamps, t) 75 | return sum(view(x.surplus_mean, :, i_t)), x.surplus_period_std[i_t] 76 | end 77 | 78 | function getindex(x::SurplusResult, r::AbstractString, t::ZonedDateTime) 79 | i_r = findfirstunique(x.regions, r) 80 | i_t = findfirstunique(x.timestamps, t) 81 | return x.surplus_mean[i_r, i_t], x.surplus_regionperiod_std[i_r, i_t] 82 | end 83 | 84 | function finalize( 85 | acc::SurplusAccumulator, 86 | system::SystemModel{N,L,T,P,E}, 87 | ) where {N,L,T,P,E} 88 | 89 | _, period_std = mean_std(acc.surplus_period) 90 | regionperiod_mean, regionperiod_std = mean_std(acc.surplus_regionperiod) 91 | 92 | nsamples = first(first(acc.surplus_period).stats).n 93 | 94 | return SurplusResult{N,L,T,P}( 95 | nsamples, system.regions.names, system.timestamps, 96 | regionperiod_mean, period_std, regionperiod_std) 97 | 98 | end 99 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/SurplusSamples.jl: -------------------------------------------------------------------------------- 1 | """ 2 | SurplusSamples 3 | 4 | The `SurplusSamples` result specification reports sample-level unused 5 | generation and storage discharge capability of `Regions`, producing a 6 | `SurplusSamplesResult`. 7 | 8 | A `SurplusSamplesResult` can be indexed by region name and timestamp to retrieve 9 | a vector of sample-level surplus values in that region and timestep. 10 | 11 | Example: 12 | 13 | ```julia 14 | surplus, = 15 | assess(sys, SequentialMonteCarlo(samples=10), SurplusSamples()) 16 | 17 | samples = surplus["Region A", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 18 | 19 | @assert samples isa Vector{Float64} 20 | @assert length(samples) == 10 21 | ``` 22 | 23 | Note that this result specification requires large amounts of memory for 24 | larger sample sizes. See [`Surplus`](@ref) for estimated average surplus values 25 | when sample-level granularity isn't required. 26 | """ 27 | struct SurplusSamples <: ResultSpec end 28 | 29 | struct SurplusSamplesAccumulator <: ResultAccumulator{SurplusSamples} 30 | 31 | surplus::Array{Int,3} 32 | 33 | end 34 | 35 | function accumulator( 36 | sys::SystemModel{N}, nsamples::Int, ::SurplusSamples 37 | ) where {N} 38 | 39 | nregions = length(sys.regions) 40 | surplus = zeros(Int, nregions, N, nsamples) 41 | 42 | return SurplusSamplesAccumulator(surplus) 43 | 44 | end 45 | 46 | function merge!( 47 | x::SurplusSamplesAccumulator, y::SurplusSamplesAccumulator 48 | ) 49 | 50 | x.surplus .+= y.surplus 51 | return 52 | 53 | end 54 | 55 | accumulatortype(::SurplusSamples) = SurplusSamplesAccumulator 56 | 57 | struct SurplusSamplesResult{N,L,T<:Period,P<:PowerUnit} <: AbstractSurplusResult{N,L,T} 58 | 59 | regions::Vector{String} 60 | timestamps::StepRange{ZonedDateTime,T} 61 | 62 | surplus::Array{Int,3} 63 | 64 | end 65 | 66 | function getindex(x::SurplusSamplesResult, t::ZonedDateTime) 67 | i_t = findfirstunique(x.timestamps, t) 68 | return vec(sum(view(x.surplus, :, i_t, :), dims=1)) 69 | end 70 | 71 | function getindex(x::SurplusSamplesResult, r::AbstractString, t::ZonedDateTime) 72 | i_r = findfirstunique(x.regions, r) 73 | i_t = findfirstunique(x.timestamps, t) 74 | return vec(x.surplus[i_r, i_t, :]) 75 | end 76 | 77 | function finalize( 78 | acc::SurplusSamplesAccumulator, 79 | system::SystemModel{N,L,T,P,E}, 80 | ) where {N,L,T,P,E} 81 | 82 | return SurplusSamplesResult{N,L,T,P}( 83 | system.regions.names, system.timestamps, acc.surplus) 84 | 85 | end 86 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/Utilization.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Utilization 3 | 4 | The `Utilization` result specification reports the estimated average 5 | absolute utilization of `Interfaces`, producing a `UtilizationResult`. 6 | 7 | Whereas `Flow` reports the average directional power transfer across an 8 | interface, `Utilization` reports the absolute value of flow relative to the 9 | interface's transfer capability (counting the effects of line outages). 10 | For example, a symmetrically-constrained interface which is fully congested 11 | with max power flowing in one direction in half of the samples, and the other 12 | direction in the remaining samples, would have an average flow of 0 MW, but 13 | an average utilization of 100%. 14 | 15 | A `UtilizationResult` can be indexed by a `Pair` of region names and a 16 | timestamp to retrieve a tuple of sample mean and standard deviation, estimating 17 | the average utilization of the interface. Given the absolute value nature of 18 | the outcome, results are independent of direction. Querying 19 | `"Region A" => "Region B"` will yield the same result as 20 | `"Region B" => "Region A"`. 21 | 22 | Example: 23 | 24 | ```julia 25 | utils, = 26 | assess(sys, SequentialMonteCarlo(samples=1000), Utilization()) 27 | 28 | util_mean, util_std = 29 | utils["Region A" => "Region B", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 30 | 31 | util2_mean, util2_std = 32 | utils["Region B" => "Region A", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 33 | 34 | @assert util_mean == util2_mean 35 | ``` 36 | 37 | See [`UtilizationSamples`](@ref) for sample-level utilization results. 38 | """ 39 | struct Utilization <: ResultSpec end 40 | 41 | struct UtilizationAccumulator <: ResultAccumulator{Utilization} 42 | 43 | util_interface::Vector{MeanVariance} 44 | util_interfaceperiod::Matrix{MeanVariance} 45 | 46 | util_interface_currentsim::Vector{Float64} 47 | 48 | end 49 | 50 | function accumulator( 51 | sys::SystemModel{N}, nsamples::Int, ::Utilization 52 | ) where {N} 53 | 54 | n_interfaces = length(sys.interfaces) 55 | util_interface = [meanvariance() for _ in 1:n_interfaces] 56 | util_interfaceperiod = [meanvariance() for _ in 1:n_interfaces, _ in 1:N] 57 | 58 | util_interface_currentsim = zeros(Int, n_interfaces) 59 | 60 | return UtilizationAccumulator( 61 | util_interface, util_interfaceperiod, util_interface_currentsim) 62 | 63 | end 64 | 65 | function merge!( 66 | x::UtilizationAccumulator, y::UtilizationAccumulator 67 | ) 68 | 69 | foreach(merge!, x.util_interface, y.util_interface) 70 | foreach(merge!, x.util_interfaceperiod, y.util_interfaceperiod) 71 | 72 | end 73 | 74 | accumulatortype(::Utilization) = UtilizationAccumulator 75 | 76 | struct UtilizationResult{N,L,T<:Period} <: AbstractUtilizationResult{N,L,T} 77 | 78 | nsamples::Union{Int,Nothing} 79 | interfaces::Vector{Pair{String,String}} 80 | timestamps::StepRange{ZonedDateTime,T} 81 | 82 | utilization_mean::Matrix{Float64} 83 | 84 | utilization_interface_std::Vector{Float64} 85 | utilization_interfaceperiod_std::Matrix{Float64} 86 | 87 | end 88 | 89 | function getindex(x::UtilizationResult, i::Pair{<:AbstractString,<:AbstractString}) 90 | i_i, _ = findfirstunique_directional(x.interfaces, i) 91 | return mean(view(x.utilization_mean, i_i, :)), x.utilization_interface_std[i_i] 92 | end 93 | 94 | function getindex(x::UtilizationResult, i::Pair{<:AbstractString,<:AbstractString}, t::ZonedDateTime) 95 | i_i, _ = findfirstunique_directional(x.interfaces, i) 96 | i_t = findfirstunique(x.timestamps, t) 97 | return x.utilization_mean[i_i, i_t], x.utilization_interfaceperiod_std[i_i, i_t] 98 | end 99 | 100 | function finalize( 101 | acc::UtilizationAccumulator, 102 | system::SystemModel{N,L,T,P,E}, 103 | ) where {N,L,T,P,E} 104 | 105 | nsamples = length(system.interfaces) > 0 ? 106 | first(acc.util_interface[1].stats).n : nothing 107 | 108 | util_mean, util_interfaceperiod_std = mean_std(acc.util_interfaceperiod) 109 | util_interface_std = last(mean_std(acc.util_interface)) / N 110 | 111 | fromregions = getindex.(Ref(system.regions.names), system.interfaces.regions_from) 112 | toregions = getindex.(Ref(system.regions.names), system.interfaces.regions_to) 113 | 114 | return UtilizationResult{N,L,T}( 115 | nsamples, Pair.(fromregions, toregions), system.timestamps, 116 | util_mean, util_interface_std, util_interfaceperiod_std) 117 | 118 | end 119 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/UtilizationSamples.jl: -------------------------------------------------------------------------------- 1 | """ 2 | UtilizationSamples 3 | 4 | The `UtilizationSamples` result specification reports the sample-level 5 | absolute utilization of `Interfaces`, producing a `UtilizationSamplesResult`. 6 | 7 | Whereas `FlowSamples` reports the directional power transfer across an 8 | interface, `UtilizationSamples` reports the absolute value of flow relative to the 9 | interface's transfer capability (counting the effects of line outages). 10 | For example, a 100 MW symmetrically-constrained interface which is fully 11 | congested may have a flow of +100 or -100 MW, but in both cases the utilization 12 | will be 100%. If a 50 MW line in the interface went on outage, flow may drop 13 | to +50 or -50 MW, but utilization would remain at 100%. 14 | 15 | A `UtilizationSamplesResult` can be indexed by a `Pair` of region 16 | names and a timestamp to retrieve a vector of sample-level utilizations of the 17 | interface in that timestep. Given the absolute value nature of the outcome, 18 | results are independent of direction. Querying 19 | `"Region A" => "Region B"` will yield the same result as 20 | `"Region B" => "Region A"`. 21 | 22 | Example: 23 | 24 | ```julia 25 | utils, = 26 | assess(sys, SequentialMonteCarlo(samples=10), UtilizationSamples()) 27 | 28 | samples = 29 | utils["Region A" => "Region B", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 30 | 31 | @assert samples isa Vector{Float64} 32 | @assert length(samples) == 10 33 | 34 | samples2 = 35 | utils["Region B" => "Region A", ZonedDateTime(2020, 1, 1, 0, tz"UTC")] 36 | 37 | @assert samples == samples2 38 | ``` 39 | 40 | Note that this result specification requires large amounts of memory for 41 | larger sample sizes. See [`Utilization`](@ref) for sample-averaged utilization 42 | results when sample-level granularity isn't required. 43 | """ 44 | struct UtilizationSamples <: ResultSpec end 45 | 46 | struct UtilizationSamplesAccumulator <: ResultAccumulator{UtilizationSamples} 47 | 48 | utilization::Array{Float64,3} 49 | 50 | end 51 | 52 | function accumulator( 53 | sys::SystemModel{N}, nsamples::Int, ::UtilizationSamples 54 | ) where {N} 55 | 56 | ninterfaces = length(sys.interfaces) 57 | utilization = zeros(Float64, ninterfaces, N, nsamples) 58 | 59 | return UtilizationSamplesAccumulator(utilization) 60 | 61 | end 62 | 63 | function merge!( 64 | x::UtilizationSamplesAccumulator, y::UtilizationSamplesAccumulator 65 | ) 66 | 67 | x.utilization .+= y.utilization 68 | return 69 | 70 | end 71 | 72 | accumulatortype(::UtilizationSamples) = UtilizationSamplesAccumulator 73 | 74 | struct UtilizationSamplesResult{N,L,T<:Period} <: AbstractUtilizationResult{N,L,T} 75 | 76 | interfaces::Vector{Pair{String,String}} 77 | timestamps::StepRange{ZonedDateTime,T} 78 | 79 | utilization::Array{Float64,3} 80 | 81 | end 82 | 83 | function getindex(x::UtilizationSamplesResult, 84 | i::Pair{<:AbstractString,<:AbstractString}) 85 | i_i, _ = findfirstunique_directional(x.interfaces, i) 86 | return vec(mean(view(x.utilization, i_i, :, :), dims=1)) 87 | end 88 | 89 | 90 | function getindex(x::UtilizationSamplesResult, 91 | i::Pair{<:AbstractString,<:AbstractString}, t::ZonedDateTime) 92 | i_i, _ = findfirstunique_directional(x.interfaces, i) 93 | i_t = findfirstunique(x.timestamps, t) 94 | return vec(x.utilization[i_i, i_t, :]) 95 | end 96 | 97 | function finalize( 98 | acc::UtilizationSamplesAccumulator, 99 | system::SystemModel{N,L,T,P,E}, 100 | ) where {N,L,T,P,E} 101 | 102 | fromregions = getindex.(Ref(system.regions.names), system.interfaces.regions_from) 103 | toregions = getindex.(Ref(system.regions.names), system.interfaces.regions_to) 104 | 105 | return UtilizationSamplesResult{N,L,T}( 106 | Pair.(fromregions, toregions), system.timestamps, acc.utilization) 107 | 108 | end 109 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/metrics.jl: -------------------------------------------------------------------------------- 1 | abstract type ReliabilityMetric end 2 | 3 | struct MeanEstimate 4 | 5 | estimate::Float64 6 | standarderror::Float64 7 | 8 | function MeanEstimate(est::Real, stderr::Real) 9 | 10 | stderr >= 0 || throw(DomainError(stderr, 11 | "Standard error of the estimate should be non-negative")) 12 | 13 | new(convert(Float64, est), convert(Float64, stderr)) 14 | 15 | end 16 | 17 | end 18 | 19 | MeanEstimate(x::Real) = MeanEstimate(x, 0) 20 | MeanEstimate(x::Real, ::Real, ::Nothing) = MeanEstimate(x, 0) 21 | MeanEstimate(mu::Real, sigma::Real, n::Int) = MeanEstimate(mu, sigma / sqrt(n)) 22 | 23 | function MeanEstimate(xs::AbstractArray{<:Real}) 24 | est = mean(xs) 25 | return MeanEstimate(est, std(xs, mean=est), length(xs)) 26 | end 27 | 28 | val(est::MeanEstimate) = est.estimate 29 | stderror(est::MeanEstimate) = est.standarderror 30 | 31 | Base.isapprox(x::MeanEstimate, y::MeanEstimate) = 32 | isapprox(x.estimate, y.estimate) && 33 | isapprox(x.standarderror, y.standarderror) 34 | 35 | Base.div(x::MeanEstimate, y::Float64) = 36 | MeanEstimate(x.estimate/y, x.standarderror/y) 37 | 38 | function Base.show(io::IO, x::MeanEstimate) 39 | v, s = stringprecision(x) 40 | print(io, v, x.standarderror > 0 ? "±"*s : "") 41 | end 42 | 43 | function stringprecision(x::MeanEstimate) 44 | 45 | if iszero(x.standarderror) 46 | 47 | v_rounded = @sprintf "%0.5f" x.estimate 48 | s_rounded = "0" 49 | 50 | else 51 | 52 | stderr_round = round(x.standarderror, sigdigits=1) 53 | digits = -floor(Int, log(10, stderr_round)) 54 | 55 | if digits > 0 56 | v_rounded = @sprintf "%0.*f" digits x.estimate 57 | s_rounded = @sprintf "%0.*f" digits x.standarderror 58 | else 59 | v_rounded = @sprintf "%0.0f" round(x.estimate, digits=digits) 60 | s_rounded = @sprintf "%0.0f" round(x.standarderror, digits=digits) 61 | end 62 | 63 | end 64 | 65 | return v_rounded, s_rounded 66 | 67 | end 68 | 69 | Base.isapprox(x::ReliabilityMetric, y::ReliabilityMetric) = 70 | isapprox(val(x), val(y)) && isapprox(stderror(x), stderror(y)) 71 | 72 | """ 73 | LOLE 74 | 75 | `LOLE` reports loss of load expectation over a particular time period 76 | and regional extent. When the reporting period is a single simulation 77 | timestep, the metric is equivalent to loss of load probability (LOLP). 78 | 79 | Contains both the estimated value itself as well as the standard error 80 | of that estimate, which can be extracted with `val` and `stderror`, 81 | respectively. 82 | """ 83 | struct LOLE{N, L, T <: Period} <: ReliabilityMetric 84 | lole::MeanEstimate 85 | 86 | function LOLE{N,L,T}(lole::MeanEstimate) where {N,L,T<:Period} 87 | val(lole) >= 0 || throw(DomainError(val, 88 | "$val is not a valid expected count of event-periods")) 89 | new{N,L,T}(lole) 90 | end 91 | 92 | end 93 | 94 | val(x::LOLE) = val(x.lole) 95 | stderror(x::LOLE) = stderror(x.lole) 96 | 97 | function Base.show(io::IO, x::LOLE{N,L,T}) where {N,L,T} 98 | 99 | t_symbol = unitsymbol(T) 100 | print(io, "LOLE = ", x.lole, " event-", 101 | L == 1 ? t_symbol : "(" * string(L) * t_symbol * ")", "/", 102 | N*L == 1 ? "" : N*L, t_symbol) 103 | 104 | end 105 | 106 | """ 107 | EUE 108 | 109 | `EUE` reports expected unserved energy over a particular time period and 110 | regional extent. 111 | 112 | Contains both the estimated value itself as well as the standard error 113 | of that estimate, which can be extracted with `val` and `stderror`, 114 | respectively. 115 | """ 116 | struct EUE{N,L,T<:Period,E<:EnergyUnit} <: ReliabilityMetric 117 | 118 | eue::MeanEstimate 119 | 120 | function EUE{N,L,T,E}(eue::MeanEstimate) where {N,L,T<:Period,E<:EnergyUnit} 121 | val(eue) >= 0 || throw(DomainError( 122 | "$val is not a valid unserved energy expectation")) 123 | new{N,L,T,E}(eue) 124 | end 125 | 126 | end 127 | 128 | val(x::EUE) = val(x.eue) 129 | stderror(x::EUE) = stderror(x.eue) 130 | 131 | function Base.show(io::IO, x::EUE{N,L,T,E}) where {N,L,T,E} 132 | 133 | print(io, "EUE = ", x.eue, " ", 134 | unitsymbol(E), "/", N*L == 1 ? "" : N*L, unitsymbol(T)) 135 | 136 | end 137 | 138 | """ 139 | NEUE 140 | 141 | `NEUE` reports normalized expected unserved energy over a regional extent. 142 | 143 | Contains both the estimated value itself as well as the standard error 144 | of that estimate, which can be extracted with `val` and `stderror`, 145 | respectively. 146 | """ 147 | struct NEUE <: ReliabilityMetric 148 | 149 | neue::MeanEstimate 150 | 151 | function NEUE(neue::MeanEstimate) 152 | val(neue) >= 0 || throw(DomainError( 153 | "$val is not a valid unserved energy expectation")) 154 | new(neue) 155 | end 156 | 157 | end 158 | 159 | val(x::NEUE) = val(x.neue) 160 | stderror(x::NEUE) = stderror(x.neue) 161 | 162 | function Base.show(io::IO, x::NEUE) 163 | 164 | print(io, "NEUE = ", x.neue, " ppm") 165 | 166 | end 167 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Results/utils.jl: -------------------------------------------------------------------------------- 1 | const MeanVariance = Series{ 2 | Number, Tuple{Mean{Float64, EqualWeight}, 3 | Variance{Float64, Float64, EqualWeight}}} 4 | 5 | meanvariance() = Series(Mean(), Variance()) 6 | 7 | function mean_std(x::MeanVariance) 8 | m, v = value(x) 9 | return m, sqrt(v) 10 | end 11 | 12 | function mean_std(x::AbstractArray{<:MeanVariance}) 13 | 14 | means = similar(x, Float64) 15 | vars = similar(means) 16 | 17 | for i in eachindex(x) 18 | m, v = mean_std(x[i]) 19 | means[i] = m 20 | vars[i] = v 21 | end 22 | 23 | return means, vars 24 | 25 | end 26 | 27 | function findfirstunique_directional(a::AbstractVector{<:Pair}, i::Pair) 28 | i_idx = findfirst(isequal(i), a) 29 | if isnothing(i_idx) 30 | i_idx = findfirstunique(a, last(i) => first(i)) 31 | reverse = true 32 | else 33 | reverse = false 34 | end 35 | return i_idx, reverse 36 | end 37 | 38 | function findfirstunique(a::AbstractVector{T}, i::T) where T 39 | i_idx = findfirst(isequal(i), a) 40 | i_idx === nothing && throw(BoundsError(a)) 41 | return i_idx 42 | end 43 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Simulations/Simulations.jl: -------------------------------------------------------------------------------- 1 | @reexport module Simulations 2 | 3 | import ..Systems: SystemModel, AbstractAssets, Generators, Lines, 4 | conversionfactor, energytopower 5 | 6 | import ..Results 7 | import ..Results: ResultSpec, ResultAccumulator, 8 | accumulator, resultchannel, finalize 9 | 10 | import Base: broadcastable 11 | import Base.Threads: nthreads, @spawn 12 | import MinCostFlows 13 | import MinCostFlows: FlowProblem, solveflows!, 14 | updateinjection!, updateflowlimit!, updateflowcost! 15 | import OnlineStatsBase: fit! 16 | import Random: AbstractRNG, rand, seed! 17 | import Random123: Philox4x 18 | 19 | export assess, SequentialMonteCarlo 20 | 21 | include("SystemState.jl") 22 | include("DispatchProblem.jl") 23 | include("recording.jl") 24 | include("utils.jl") 25 | 26 | """ 27 | SequentialMonteCarlo(; 28 | samples::Int=10_000, 29 | seed::Integer=rand(UInt64), 30 | verbose::Bool=false, 31 | threaded::Bool=true 32 | ) 33 | 34 | Sequential Monte Carlo simulation parameters for PRAS analysis 35 | 36 | It it recommended that you fix the random seed for reproducibility. 37 | 38 | # Arguments 39 | 40 | - `samples::Int=10_000`: Number of samples 41 | - `seed::Integer=rand(UInt64)`: Random seed 42 | - `verbose::Bool=false`: Print progress 43 | - `threaded::Bool=true`: Use multi-threading 44 | 45 | # Returns 46 | 47 | - `SequentialMonteCarlo`: PRAS simulation specification 48 | """ 49 | struct SequentialMonteCarlo 50 | 51 | nsamples::Int 52 | seed::UInt64 53 | verbose::Bool 54 | threaded::Bool 55 | 56 | function SequentialMonteCarlo(; 57 | samples::Int=10_000, seed::Integer=rand(UInt64), 58 | verbose::Bool=false, threaded::Bool=true 59 | ) 60 | samples <= 0 && throw(DomainError("Sample count must be positive")) 61 | seed < 0 && throw(DomainError("Random seed must be non-negative")) 62 | new(samples, UInt64(seed), verbose, threaded) 63 | end 64 | end 65 | 66 | broadcastable(x::SequentialMonteCarlo) = Ref(x) 67 | 68 | """ 69 | assess(system::SystemModel, method::SequentialMonteCarlo, resultspecs::ResultSpec...) 70 | 71 | Run a Sequential Monte Carlo simulation on a `system` using the `method` data 72 | and return `resultspecs`. 73 | 74 | # Arguments 75 | 76 | - `system::SystemModel`: PRAS data structure 77 | - `method::SequentialMonteCarlo`: method for PRAS analysis 78 | - `resultspecs::ResultSpec...`: PRAS metric for metrics like [`Shortfall`](@ref) missing generation 79 | 80 | # Returns 81 | 82 | - `results::Tuple{Vararg{ResultAccumulator{SequentialMonteCarlo}}}`: PRAS metric results 83 | """ 84 | function assess( 85 | system::SystemModel, 86 | method::SequentialMonteCarlo, 87 | resultspecs::ResultSpec... 88 | ) 89 | 90 | threads = nthreads() 91 | sampleseeds = Channel{Int}(2*threads) 92 | results = resultchannel(resultspecs, threads) 93 | 94 | @spawn makeseeds(sampleseeds, method.nsamples) 95 | 96 | if method.threaded 97 | 98 | if (threads == 1) 99 | @warn "It looks like you haven't configured JULIA_NUM_THREADS before you started the julia repl. \n If you want to use multi-threading, stop the execution and start your julia repl using : \n julia --project --threads auto" 100 | end 101 | 102 | for _ in 1:threads 103 | @spawn assess(system, method, sampleseeds, results, resultspecs...) 104 | end 105 | else 106 | assess(system, method, sampleseeds, results, resultspecs...) 107 | end 108 | 109 | return finalize(results, system, method.threaded ? threads : 1) 110 | 111 | end 112 | 113 | function makeseeds(sampleseeds::Channel{Int}, nsamples::Int) 114 | 115 | for s in 1:nsamples 116 | put!(sampleseeds, s) 117 | end 118 | 119 | close(sampleseeds) 120 | 121 | end 122 | 123 | function assess( 124 | system::SystemModel{N}, method::SequentialMonteCarlo, 125 | sampleseeds::Channel{Int}, 126 | results::Channel{<:Tuple{Vararg{ResultAccumulator}}}, 127 | resultspecs::ResultSpec... 128 | ) where N 129 | 130 | dispatchproblem = DispatchProblem(system) 131 | systemstate = SystemState(system) 132 | recorders = accumulator.(system, method.nsamples, resultspecs) 133 | 134 | # TODO: Test performance of Philox vs Threefry, choice of rounds 135 | # Also consider implementing an efficient Bernoulli trial with direct 136 | # mantissa comparison 137 | rng = Philox4x((0, 0), 10) 138 | 139 | for s in sampleseeds 140 | 141 | seed!(rng, (method.seed, s)) 142 | initialize!(rng, systemstate, system) 143 | 144 | for t in 1:N 145 | 146 | advance!(rng, systemstate, dispatchproblem, system, t) 147 | solve!(dispatchproblem, systemstate, system, t) 148 | foreach(recorder -> record!( 149 | recorder, system, systemstate, dispatchproblem, s, t 150 | ), recorders) 151 | 152 | end 153 | 154 | foreach(recorder -> reset!(recorder, s), recorders) 155 | 156 | end 157 | 158 | put!(results, recorders) 159 | 160 | end 161 | 162 | function initialize!( 163 | rng::AbstractRNG, state::SystemState, system::SystemModel{N} 164 | ) where N 165 | 166 | initialize_availability!( 167 | rng, state.gens_available, state.gens_nexttransition, 168 | system.generators, N) 169 | 170 | initialize_availability!( 171 | rng, state.stors_available, state.stors_nexttransition, 172 | system.storages, N) 173 | 174 | initialize_availability!( 175 | rng, state.genstors_available, state.genstors_nexttransition, 176 | system.generatorstorages, N) 177 | 178 | initialize_availability!( 179 | rng, state.lines_available, state.lines_nexttransition, 180 | system.lines, N) 181 | 182 | fill!(state.stors_energy, 0) 183 | fill!(state.genstors_energy, 0) 184 | 185 | return 186 | 187 | end 188 | 189 | function advance!( 190 | rng::AbstractRNG, 191 | state::SystemState, 192 | dispatchproblem::DispatchProblem, 193 | system::SystemModel{N}, t::Int) where N 194 | 195 | update_availability!( 196 | rng, state.gens_available, state.gens_nexttransition, 197 | system.generators, t, N) 198 | 199 | update_availability!( 200 | rng, state.stors_available, state.stors_nexttransition, 201 | system.storages, t, N) 202 | 203 | update_availability!( 204 | rng, state.genstors_available, state.genstors_nexttransition, 205 | system.generatorstorages, t, N) 206 | 207 | update_availability!( 208 | rng, state.lines_available, state.lines_nexttransition, 209 | system.lines, t, N) 210 | 211 | update_energy!(state.stors_energy, system.storages, t) 212 | update_energy!(state.genstors_energy, system.generatorstorages, t) 213 | 214 | update_problem!(dispatchproblem, state, system, t) 215 | 216 | end 217 | 218 | function solve!( 219 | dispatchproblem::DispatchProblem, state::SystemState, 220 | system::SystemModel, t::Int 221 | ) 222 | solveflows!(dispatchproblem.fp) 223 | update_state!(state, dispatchproblem, system, t) 224 | end 225 | 226 | end 227 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Simulations/SystemState.jl: -------------------------------------------------------------------------------- 1 | struct SystemState 2 | 3 | gens_available::Vector{Bool} 4 | gens_nexttransition::Vector{Int} 5 | 6 | stors_available::Vector{Bool} 7 | stors_nexttransition::Vector{Int} 8 | stors_energy::Vector{Int} 9 | 10 | genstors_available::Vector{Bool} 11 | genstors_nexttransition::Vector{Int} 12 | genstors_energy::Vector{Int} 13 | 14 | lines_available::Vector{Bool} 15 | lines_nexttransition::Vector{Int} 16 | 17 | function SystemState(system::SystemModel) 18 | 19 | ngens = length(system.generators) 20 | gens_available = Vector{Bool}(undef, ngens) 21 | gens_nexttransition= Vector{Int}(undef, ngens) 22 | 23 | nstors = length(system.storages) 24 | stors_available = Vector{Bool}(undef, nstors) 25 | stors_nexttransition = Vector{Int}(undef, nstors) 26 | stors_energy = Vector{Int}(undef, nstors) 27 | 28 | ngenstors = length(system.generatorstorages) 29 | genstors_available = Vector{Bool}(undef, ngenstors) 30 | genstors_nexttransition = Vector{Int}(undef, ngenstors) 31 | genstors_energy = Vector{Int}(undef, ngenstors) 32 | 33 | nlines = length(system.lines) 34 | lines_available = Vector{Bool}(undef, nlines) 35 | lines_nexttransition = Vector{Int}(undef, nlines) 36 | 37 | return new( 38 | gens_available, gens_nexttransition, 39 | stors_available, stors_nexttransition, stors_energy, 40 | genstors_available, genstors_nexttransition, genstors_energy, 41 | lines_available, lines_nexttransition) 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Simulations/utils.jl: -------------------------------------------------------------------------------- 1 | function initialize_availability!( 2 | rng::AbstractRNG, 3 | availability::Vector{Bool}, nexttransition::Vector{Int}, 4 | devices::AbstractAssets, t_last::Int) 5 | 6 | for i in 1:length(devices) 7 | 8 | λ = devices.λ[i, 1] 9 | μ = devices.μ[i, 1] 10 | online = rand(rng) < μ / (λ + μ) 11 | 12 | availability[i] = online 13 | 14 | transitionprobs = online ? devices.λ : devices.μ 15 | nexttransition[i] = randtransitiontime( 16 | rng, transitionprobs, i, 1, t_last) 17 | 18 | end 19 | 20 | return availability 21 | 22 | end 23 | 24 | function update_availability!( 25 | rng::AbstractRNG, 26 | availability::Vector{Bool}, nexttransition::Vector{Int}, 27 | devices::AbstractAssets, t_now::Int, t_last::Int) 28 | 29 | for i in 1:length(devices) 30 | 31 | if nexttransition[i] == t_now # Unit switches states 32 | transitionprobs = (availability[i] ⊻= true) ? devices.λ : devices.μ 33 | nexttransition[i] = randtransitiontime( 34 | rng, transitionprobs, i, t_now, t_last) 35 | end 36 | 37 | end 38 | 39 | end 40 | 41 | function randtransitiontime( 42 | rng::AbstractRNG, p::Matrix{Float64}, 43 | i::Int, t_now::Int, t_last::Int 44 | ) 45 | 46 | cdf = 0. 47 | p_noprevtransition = 1. 48 | 49 | x = rand(rng) 50 | t = t_now + 1 51 | 52 | while t <= t_last 53 | p_it = p[i,t] 54 | cdf += p_noprevtransition * p_it 55 | x < cdf && return t 56 | p_noprevtransition *= (1. - p_it) 57 | t += 1 58 | end 59 | 60 | return t_last + 1 61 | 62 | end 63 | 64 | function available_capacity( 65 | availability::Vector{Bool}, 66 | lines::Lines, 67 | idxs::UnitRange{Int}, t::Int 68 | ) 69 | 70 | avcap_forward = 0 71 | avcap_backward = 0 72 | 73 | for i in idxs 74 | if availability[i] 75 | avcap_forward += lines.forward_capacity[i, t] 76 | avcap_backward += lines.backward_capacity[i, t] 77 | end 78 | end 79 | 80 | return avcap_forward, avcap_backward 81 | 82 | end 83 | 84 | function available_capacity( 85 | availability::Vector{Bool}, 86 | gens::Generators, 87 | idxs::UnitRange{Int}, t::Int 88 | ) 89 | 90 | caps = gens.capacity 91 | avcap = 0 92 | 93 | for i in idxs 94 | availability[i] && (avcap += caps[i, t]) 95 | end 96 | 97 | return avcap 98 | 99 | end 100 | 101 | function update_energy!( 102 | stors_energy::Vector{Int}, 103 | stors::AbstractAssets, 104 | t::Int 105 | ) 106 | 107 | for i in 1:length(stors_energy) 108 | 109 | soc = stors_energy[i] 110 | efficiency = stors.carryover_efficiency[i,t] 111 | maxenergy = stors.energy_capacity[i,t] 112 | 113 | # Decay SoC 114 | soc = round(Int, soc * efficiency) 115 | 116 | # Shed SoC above current energy limit 117 | stors_energy[i] = min(soc, maxenergy) 118 | 119 | end 120 | 121 | end 122 | 123 | function maxtimetocharge_discharge(system::SystemModel) 124 | 125 | if length(system.storages) > 0 126 | 127 | if any(iszero, system.storages.charge_capacity) 128 | stor_charge_max = length(system.timestamps) + 1 129 | else 130 | stor_charge_durations = 131 | system.storages.energy_capacity ./ system.storages.charge_capacity 132 | stor_charge_max = ceil(Int, maximum(stor_charge_durations)) 133 | end 134 | 135 | if any(iszero, system.storages.discharge_capacity) 136 | stor_discharge_max = length(system.timestamps) + 1 137 | else 138 | stor_discharge_durations = 139 | system.storages.energy_capacity ./ system.storages.discharge_capacity 140 | stor_discharge_max = ceil(Int, maximum(stor_discharge_durations)) 141 | end 142 | 143 | else 144 | 145 | stor_charge_max = 0 146 | stor_discharge_max = 0 147 | 148 | end 149 | 150 | if length(system.generatorstorages) > 0 151 | 152 | if any(iszero, system.generatorstorages.charge_capacity) 153 | genstor_charge_max = length(system.timestamps) + 1 154 | else 155 | genstor_charge_durations = 156 | system.generatorstorages.energy_capacity ./ system.generatorstorages.charge_capacity 157 | genstor_charge_max = ceil(Int, maximum(genstor_charge_durations)) 158 | end 159 | 160 | if any(iszero, system.generatorstorages.discharge_capacity) 161 | genstor_discharge_max = length(system.timestamps) + 1 162 | else 163 | genstor_discharge_durations = 164 | system.generatorstorages.energy_capacity ./ system.generatorstorages.discharge_capacity 165 | genstor_discharge_max = ceil(Int, maximum(genstor_discharge_durations)) 166 | end 167 | 168 | else 169 | 170 | genstor_charge_max = 0 171 | genstor_discharge_max = 0 172 | 173 | end 174 | 175 | return (max(stor_charge_max, genstor_charge_max), 176 | max(stor_discharge_max, genstor_discharge_max)) 177 | 178 | end 179 | 180 | function utilization(f::MinCostFlows.Edge, b::MinCostFlows.Edge) 181 | 182 | flow_forward = f.flow 183 | max_forward = f.limit 184 | 185 | flow_back = b.flow 186 | max_back = b.limit 187 | 188 | util = if flow_forward > 0 189 | flow_forward/max_forward 190 | elseif flow_back > 0 191 | flow_back/max_back 192 | elseif iszero(max_forward) && iszero(max_back) 193 | 1.0 194 | else 195 | 0.0 196 | end 197 | 198 | return util 199 | 200 | end 201 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Systems/SystemModel.jl: -------------------------------------------------------------------------------- 1 | """ 2 | SystemModel 3 | 4 | A `SystemModel` contains a representation of a power system to be studied 5 | with PRAS. 6 | """ 7 | struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} 8 | regions::Regions{N, P} 9 | interfaces::Interfaces{N, P} 10 | 11 | generators::Generators{N,L,T,P} 12 | region_gen_idxs::Vector{UnitRange{Int}} 13 | 14 | storages::Storages{N,L,T,P,E} 15 | region_stor_idxs::Vector{UnitRange{Int}} 16 | 17 | generatorstorages::GeneratorStorages{N,L,T,P,E} 18 | region_genstor_idxs::Vector{UnitRange{Int}} 19 | 20 | lines::Lines{N,L,T,P} 21 | interface_line_idxs::Vector{UnitRange{Int}} 22 | 23 | timestamps::StepRange{ZonedDateTime,T} 24 | 25 | function SystemModel{}( 26 | regions::Regions{N,P}, interfaces::Interfaces{N,P}, 27 | generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, 28 | storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, 29 | generatorstorages::GeneratorStorages{N,L,T,P,E}, 30 | region_genstor_idxs::Vector{UnitRange{Int}}, 31 | lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, 32 | timestamps::StepRange{ZonedDateTime,T} 33 | ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} 34 | 35 | n_regions = length(regions) 36 | n_gens = length(generators) 37 | n_stors = length(storages) 38 | n_genstors = length(generatorstorages) 39 | 40 | n_interfaces = length(interfaces) 41 | n_lines = length(lines) 42 | 43 | @assert consistent_idxs(region_gen_idxs, n_gens, n_regions) 44 | @assert consistent_idxs(region_stor_idxs, n_stors, n_regions) 45 | @assert consistent_idxs(region_genstor_idxs, n_genstors, n_regions) 46 | @assert consistent_idxs(interface_line_idxs, n_lines, n_interfaces) 47 | 48 | @assert all( 49 | 1 <= interfaces.regions_from[i] < interfaces.regions_to[i] <= n_regions 50 | for i in 1:n_interfaces) 51 | 52 | @assert step(timestamps) == T(L) 53 | @assert length(timestamps) == N 54 | 55 | new{N,L,T,P,E}( 56 | regions, interfaces, 57 | generators, region_gen_idxs, storages, region_stor_idxs, 58 | generatorstorages, region_genstor_idxs, lines, interface_line_idxs, 59 | timestamps) 60 | 61 | end 62 | 63 | end 64 | 65 | # No time zone constructor 66 | function SystemModel( 67 | regions::Regions{N,P}, interfaces::Interfaces{N,P}, 68 | generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, 69 | storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, 70 | generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, 71 | lines, interface_line_idxs::Vector{UnitRange{Int}}, 72 | timestamps::StepRange{DateTime,T} 73 | ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} 74 | 75 | @warn "No time zone data provided - defaulting to UTC. To specify a " * 76 | "time zone for the system timestamps, provide a range of " * 77 | "`ZonedDateTime` instead of `DateTime`." 78 | 79 | utc = tz"UTC" 80 | time_start = ZonedDateTime(first(timestamps), utc) 81 | time_end = ZonedDateTime(last(timestamps), utc) 82 | timestamps_tz = time_start:step(timestamps):time_end 83 | 84 | return SystemModel( 85 | regions, interfaces, 86 | generators, region_gen_idxs, 87 | storages, region_stor_idxs, 88 | generatorstorages, region_genstor_idxs, 89 | lines, interface_line_idxs, 90 | timestamps_tz) 91 | 92 | end 93 | 94 | # Single-node constructor 95 | function SystemModel( 96 | generators::Generators{N,L,T,P}, 97 | storages::Storages{N,L,T,P,E}, 98 | generatorstorages::GeneratorStorages{N,L,T,P,E}, 99 | timestamps::StepRange{<:AbstractDateTime,T}, 100 | load::Vector{Int} 101 | ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} 102 | 103 | return SystemModel( 104 | Regions{N,P}(["Region"], reshape(load, 1, :)), 105 | Interfaces{N,P}( 106 | Int[], Int[], 107 | Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N)), 108 | generators, [1:length(generators)], 109 | storages, [1:length(storages)], 110 | generatorstorages, [1:length(generatorstorages)], 111 | Lines{N,L,T,P}( 112 | String[], String[], 113 | Matrix{Int}(undef, 0, N), Matrix{Int}(undef, 0, N), 114 | Matrix{Float64}(undef, 0, N), Matrix{Float64}(undef, 0, N)), 115 | UnitRange{Int}[], timestamps) 116 | 117 | end 118 | 119 | Base.:(==)(x::T, y::T) where {T <: SystemModel} = 120 | x.regions == y.regions && 121 | x.interfaces == y.interfaces && 122 | x.generators == y.generators && 123 | x.region_gen_idxs == y.region_gen_idxs && 124 | x.storages == y.storages && 125 | x.region_stor_idxs == y.region_stor_idxs && 126 | x.generatorstorages == y.generatorstorages && 127 | x.region_genstor_idxs == y.region_genstor_idxs && 128 | x.lines == y.lines && 129 | x.interface_line_idxs == y.interface_line_idxs && 130 | x.timestamps == y.timestamps 131 | 132 | broadcastable(x::SystemModel) = Ref(x) 133 | 134 | unitsymbol(::SystemModel{N,L,T,P,E}) where { 135 | N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} = 136 | unitsymbol(T), unitsymbol(P), unitsymbol(E) 137 | 138 | isnonnegative(x::Real) = x >= 0 139 | isfractional(x::Real) = 0 <= x <= 1 140 | 141 | function consistent_idxs(idxss::Vector{UnitRange{Int}}, nitems::Int, ngroups::Int) 142 | 143 | length(idxss) == ngroups || return false 144 | 145 | expected_next = 1 146 | for idxs in idxss 147 | first(idxs) == expected_next || return false 148 | expected_next = last(idxs) + 1 149 | end 150 | 151 | expected_next == nitems + 1 || return false 152 | return true 153 | 154 | end 155 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Systems/Systems.jl: -------------------------------------------------------------------------------- 1 | @reexport module Systems 2 | 3 | import Base: broadcastable 4 | 5 | import Dates: @dateformat_str, AbstractDateTime, DateTime, 6 | Period, Minute, Hour, Day, Year 7 | 8 | import TimeZones: ZonedDateTime, @tz_str 9 | 10 | export 11 | 12 | # System assets 13 | Regions, Interfaces, 14 | AbstractAssets, Generators, Storages, GeneratorStorages, Lines, 15 | 16 | # Units 17 | Period, Minute, Hour, Day, Year, 18 | PowerUnit, kW, MW, GW, TW, 19 | EnergyUnit, kWh, MWh, GWh, TWh, 20 | unitsymbol, conversionfactor, powertoenergy, energytopower, 21 | 22 | # Main data structure 23 | SystemModel, 24 | 25 | # Convenience re-exports 26 | ZonedDateTime, @tz_str 27 | 28 | include("units.jl") 29 | include("collections.jl") 30 | include("assets.jl") 31 | include("SystemModel.jl") 32 | include("TestData.jl") 33 | 34 | end 35 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Systems/TestData.jl: -------------------------------------------------------------------------------- 1 | module TestData 2 | 3 | using ..Systems 4 | using TimeZones 5 | 6 | const tz = tz"UTC" 7 | 8 | empty_str = String[] 9 | empty_int(x) = Matrix{Int}(undef, 0, x) 10 | empty_float(x) = Matrix{Float64}(undef, 0, x) 11 | 12 | ## Single-Region System A 13 | 14 | gens1 = Generators{4,1,Hour,MW}( 15 | ["Gen1", "Gen2", "Gen3", "VG"], ["Gens", "Gens", "Gens", "VG"], 16 | [fill(10, 3, 4); [5 6 7 8]], 17 | [fill(0.1, 3, 4); fill(0.0, 1, 4)], 18 | [fill(0.9, 3, 4); fill(1.0, 1, 4)]) 19 | 20 | emptystors1 = Storages{4,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., 21 | (empty_int(4) for _ in 1:3)..., 22 | (empty_float(4) for _ in 1:5)...) 23 | 24 | emptygenstors1 = GeneratorStorages{4,1,Hour,MW,MWh}( 25 | (empty_str for _ in 1:2)..., 26 | (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:3)..., 27 | (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:2)...) 28 | 29 | singlenode_a = SystemModel( 30 | gens1, emptystors1, emptygenstors1, 31 | ZonedDateTime(2010,1,1,0,tz):Hour(1):ZonedDateTime(2010,1,1,3,tz), 32 | [25, 28, 27, 24]) 33 | 34 | singlenode_a_lole = 0.355 35 | singlenode_a_lolps = [0.028, 0.271, 0.028, 0.028] 36 | singlenode_a_eue = 1.59 37 | singlenode_a_eues = [0.29, 0.832, 0.29, 0.178] 38 | 39 | ## Single-Region System A - 5 minute version 40 | 41 | gens1_5min = Generators{4,5,Minute,MW}( 42 | ["Gen1", "Gen2", "Gen3", "VG"], ["Gens", "Gens", "Gens", "VG"], 43 | [fill(10, 3, 4); [5 6 7 8]], 44 | [fill(0.1, 3, 4); fill(0.0, 1, 4)], 45 | [fill(0.9, 3, 4); fill(1.0, 1, 4)]) 46 | 47 | emptystors1_5min = Storages{4,5,Minute,MW,MWh}((empty_str for _ in 1:2)..., 48 | (empty_int(4) for _ in 1:3)..., 49 | (empty_float(4) for _ in 1:5)...) 50 | 51 | emptygenstors1_5min = GeneratorStorages{4,5,Minute,MW,MWh}( 52 | (empty_str for _ in 1:2)..., 53 | (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:3)..., 54 | (empty_int(4) for _ in 1:3)..., (empty_float(4) for _ in 1:2)...) 55 | 56 | singlenode_a_5min = SystemModel( 57 | gens1_5min, emptystors1_5min, emptygenstors1_5min, 58 | ZonedDateTime(2010,1,1,0,0,tz):Minute(5):ZonedDateTime(2010,1,1,0,15,tz), 59 | [25, 28, 27, 24]) 60 | 61 | singlenode_a_lole = 0.355 62 | singlenode_a_lolps = [0.028, 0.271, 0.028, 0.028] 63 | singlenode_a_eue = 1.59 64 | singlenode_a_eues = [0.29, 0.832, 0.29, 0.178] 65 | 66 | ## Single-Region System B 67 | 68 | gens2 = Generators{6,1,Hour,MW}( 69 | ["Gen1", "Gen2", "VG"], ["Gens", "Gens", "VG"], 70 | [10 10 10 15 15 15; 20 20 20 25 25 25; 7 8 9 9 8 7], 71 | [fill(0.1, 2, 6); fill(0.0, 1, 6)], 72 | [fill(0.9, 2, 6); fill(1.0, 1, 6)]) 73 | 74 | emptystors2 = Storages{6,1,Hour,MW,MWh}((empty_str for _ in 1:2)..., 75 | (empty_int(6) for _ in 1:3)..., 76 | (empty_float(6) for _ in 1:5)...) 77 | 78 | emptygenstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( 79 | (empty_str for _ in 1:2)..., 80 | (empty_int(6) for _ in 1:3)..., (empty_float(6) for _ in 1:3)..., 81 | (empty_int(6) for _ in 1:3)..., (empty_float(6) for _ in 1:2)...) 82 | 83 | genstors2 = GeneratorStorages{6,1,Hour,MW,MWh}( 84 | ["Genstor1", "Genstor2"], ["Genstorage", "Genstorage"], 85 | fill(0, 2, 6), fill(0, 2, 6), fill(4, 2, 6), 86 | fill(1.0, 2, 6), fill(1.0, 2, 6), fill(.99, 2, 6), 87 | fill(0, 2, 6), fill(0, 2, 6), fill(0, 2, 6), 88 | fill(0.0, 2, 6), fill(1.0, 2, 6)) 89 | 90 | singlenode_b = SystemModel( 91 | gens2, emptystors2, emptygenstors2, 92 | ZonedDateTime(2015,6,1,0,tz):Hour(1):ZonedDateTime(2015,6,1,5,tz), 93 | [28,29,30,31,32,33]) 94 | 95 | singlenode_b_lole = 0.96 96 | singlenode_b_lolps = [0.19, 0.19, 0.19, 0.1, 0.1, 0.19] 97 | singlenode_b_eue = 7.11 98 | singlenode_b_eues = [1.29, 1.29, 1.29, 0.85, 1.05, 1.34] 99 | 100 | 101 | # Single-Region System B, with storage 102 | #TODO: Storage tests 103 | 104 | stors2 = Storages{6,1,Hour,MW,MWh}( 105 | ["Stor1", "Stor2"], ["Storage", "Storage"], 106 | repeat([1,0], 1, 6), repeat([1,0], 1, 6), fill(4, 2, 6), 107 | fill(1.0, 2, 6), fill(1.0, 2, 6), fill(.99, 2, 6), 108 | fill(0.0, 2, 6), fill(1.0, 2, 6)) 109 | 110 | singlenode_stor = SystemModel( 111 | gens2, stors2, genstors2, 112 | ZonedDateTime(2015,6,1,0,tz):Hour(1):ZonedDateTime(2015,6,1,5,tz), 113 | [28,29,30,31,32,33]) 114 | 115 | 116 | ## Multi-Region System 117 | 118 | regions = Regions{4,MW}(["Region A", "Region B", "Region C"], 119 | [19 20 21 20; 20 21 21 22; 22 21 23 22]) 120 | 121 | generators = Generators{4,1,Hour,MW}( 122 | ["Gen1", "VG A", "Gen 2", "Gen 3", "VG B", "Gen 4", "Gen 5", "VG C"], 123 | ["Gens", "VG", "Gens", "Gens", "VG", "Gens", "Gens", "VG"], 124 | [10 10 10 10; 4 3 2 3; # A 125 | 10 10 10 10; 10 10 10 10; 6 5 3 4; # B 126 | 10 10 15 10; 20 20 25 20; 2 1 2 1], # C 127 | [fill(0.1, 1, 4); fill(0.0, 1, 4); # A 128 | fill(0.1, 2, 4); fill(0.0, 1, 4); # B 129 | fill(0.1, 2, 4); fill(0.0, 1, 4)], # C 130 | [fill(0.9, 1, 4); fill(1.0, 1, 4); # A 131 | fill(0.9, 2, 4); fill(1.0, 1, 4); # B 132 | fill(0.9, 2, 4); fill(1.0, 1, 4)]) # C) 133 | 134 | interfaces = Interfaces{4,MW}( 135 | [1,1,2], [2,3,3], fill(100, 3, 4), fill(100, 3, 4)) 136 | 137 | lines = Lines{4,1,Hour,MW}( 138 | ["L1", "L2", "L3"], ["Lines", "Lines", "Lines"], 139 | fill(8, 3, 4), fill(8, 3, 4), fill(0., 3, 4), fill(1., 3, 4)) 140 | 141 | threenode = 142 | SystemModel( 143 | regions, interfaces, generators, [1:2, 3:5, 6:8], 144 | emptystors1, fill(1:0, 3), emptygenstors1, fill(1:0, 3), 145 | lines, [1:1, 2:2, 3:3], 146 | ZonedDateTime(2018,10,30,0,tz):Hour(1):ZonedDateTime(2018,10,30,3,tz)) 147 | 148 | threenode_lole = 1.3756 149 | threenode_lolps = [0.14707, 0.40951, 0.40951, 0.40951] 150 | threenode_eue = 12.12885 151 | threenode_eues = [1.75783, 3.13343, 2.87563, 4.36196] 152 | 153 | threenode_lole_copperplate = 1.17877 154 | threenode_lolps_copperplate = [.14707, .40951, .21268, .40951] 155 | threenode_eue_copperplate = 11.73276 156 | threenode_eues_copperplate = [1.75783, 3.13343, 2.47954, 4.36196] 157 | 158 | # Test System 1 (2 Gens, 2 Regions) 159 | 160 | regions = Regions{1, MW}( 161 | ["Region A", "Region B"], reshape([8, 9], 2, 1)) 162 | 163 | gens = Generators{1,1,Hour,MW}( 164 | ["Gen 1", "Gen 2"], ["Generators", "Generators"], 165 | fill(15, 2, 1), fill(0.1, 2, 1), fill(0.9, 2, 1)) 166 | 167 | emptystors = Storages{1,1,Hour,MW,MWh}((String[] for _ in 1:2)..., 168 | (zeros(Int, 0, 1) for _ in 1:3)..., 169 | (zeros(Float64, 0, 1) for _ in 1:5)...) 170 | 171 | emptygenstors = GeneratorStorages{1,1,Hour,MW,MWh}( 172 | (String[] for _ in 1:2)..., 173 | (zeros(Int, 0, 1) for _ in 1:3)..., (zeros(Float64, 0, 1) for _ in 1:3)..., 174 | (zeros(Int, 0, 1) for _ in 1:3)..., (zeros(Float64, 0, 1) for _ in 1:2)...) 175 | 176 | interfaces = Interfaces{1,MW}([1], [2], fill(8, 1, 1), fill(8, 1, 1)) 177 | 178 | lines = Lines{1,1,Hour,MW}( 179 | ["Line 1"], ["Lines"], 180 | fill(8, 1, 1), fill(8, 1, 1), fill(0.1, 1, 1), fill(0.9, 1, 1) 181 | ) 182 | 183 | zdt = ZonedDateTime(2020,1,1,0, tz) 184 | test1 = SystemModel(regions, interfaces, 185 | gens, [1:1, 2:2], emptystors, fill(1:0, 2), emptygenstors, fill(1:0, 2), 186 | lines, [1:1], zdt:Hour(1):zdt 187 | ) 188 | 189 | test1_lole = .19 190 | test1_loles = [.1, .1] 191 | 192 | test1_eue = .647 193 | test1_eues = [.314, .333] 194 | 195 | test1_esurplus = 10.647 196 | test1_esurpluses = [5.733, 4.914] 197 | 198 | test1_i1_flow = 0.081 199 | test1_i1_util = 0.231625 200 | 201 | # Test System 2 (Gen + Stor, 1 Region) 202 | 203 | timestamps = ZonedDateTime(2020,1,1,0, tz):Hour(1):ZonedDateTime(2020,1,1,1, tz) 204 | 205 | gen = Generators{2,1,Hour,MW}( 206 | ["Gen 1"], ["Generators"], 207 | fill(10, 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) 208 | 209 | stor = Storages{2,1,Hour,MW,MWh}( 210 | ["Stor 1"], ["Storages"], 211 | fill(10, 1, 2), fill(10, 1, 2), fill(10, 1, 2), 212 | fill(1., 1, 2), fill(1., 1, 2), fill(1., 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) 213 | 214 | emptygenstors = GeneratorStorages{2,1,Hour,MW,MWh}( 215 | (String[] for _ in 1:2)..., 216 | (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:3)..., 217 | (zeros(Int, 0, 2) for _ in 1:3)..., (zeros(Float64, 0, 2) for _ in 1:2)...) 218 | 219 | test2 = SystemModel(gen, stor, emptygenstors, timestamps, [8, 9]) 220 | 221 | test2_lole = 0.2 222 | test2_lolps = [0.1, 0.1] 223 | 224 | test2_eue = 1.5542 225 | test2_eues = [0.8, 0.7542] 226 | 227 | test2_esurplus = [0.18, 1.4022] 228 | 229 | test2_eenergy = [1.62, 2.2842] 230 | 231 | # Test System 3 (Gen + Stor, 2 Regions) 232 | 233 | regions = Regions{2, MW}(["Region A", "Region B"], [8 9; 6 7]) 234 | gen = Generators{2,1,Hour,MW}( 235 | ["Gen 1"], ["Generators"], 236 | fill(25, 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2)) 237 | 238 | interfaces = Interfaces{2,MW}([1], [2], fill(15, 1, 2), fill(15, 1, 2)) 239 | line = Lines{2,1,Hour,MW}( 240 | ["Line 1"], ["Lines"], 241 | fill(15, 1, 2), fill(15, 1, 2), fill(0.1, 1, 2), fill(0.9, 1, 2) 242 | ) 243 | 244 | test3 = SystemModel(regions, interfaces, 245 | gen, [1:1, 2:1], stor, [1:0, 1:1], 246 | emptygenstors, fill(1:0, 2), 247 | line, [1:1], timestamps) 248 | 249 | test3_lole = 0.320951 250 | test3_lole_r = [0.2, 0.255341] 251 | test3_lole_t = [0.19, 0.130951] 252 | test3_lole_rt = [0.1 0.1; 0.19 0.065341] 253 | 254 | test3_eue = 3.179289 255 | test3_eue_t = [1.94, 1.239289] 256 | test3_eue_r = [1.581902, 1.597387] 257 | test3_eue_rt = [0.8 0.781902; 1.14 0.457387] 258 | 259 | test3_esurplus_t = [3.879, 11.53228] 260 | test3_esurplus_rt = [3.879 6.618087; 0. 4.914189] 261 | 262 | test3_flow = 9.5424075 263 | test3_flow_t = [11.421, 7.663815] 264 | 265 | test3_util = 0.7440337 266 | test3_util_t = [0.8614, 0.626674] 267 | 268 | test3_eenergy = [6.561, 7.682202] 269 | 270 | end 271 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Systems/collections.jl: -------------------------------------------------------------------------------- 1 | struct Regions{N,P<:PowerUnit} 2 | 3 | names::Vector{String} 4 | load::Matrix{Int} 5 | 6 | function Regions{N,P}( 7 | names::Vector{<:AbstractString}, load::Matrix{Int} 8 | ) where {N,P<:PowerUnit} 9 | 10 | n_regions = length(names) 11 | 12 | @assert size(load) == (n_regions, N) 13 | @assert all(isnonnegative, load) 14 | 15 | new{N,P}(string.(names), load) 16 | 17 | end 18 | 19 | end 20 | 21 | Base.:(==)(x::T, y::T) where {T <: Regions} = 22 | x.names == y.names && 23 | x.load == y.load 24 | 25 | Base.length(r::Regions) = length(r.names) 26 | 27 | struct Interfaces{N,P<:PowerUnit} 28 | 29 | regions_from::Vector{Int} 30 | regions_to::Vector{Int} 31 | limit_forward::Matrix{Int} 32 | limit_backward::Matrix{Int} 33 | 34 | function Interfaces{N,P}( 35 | regions_from::Vector{Int}, regions_to::Vector{Int}, 36 | forwardcapacity::Matrix{Int}, backwardcapacity::Matrix{Int} 37 | ) where {N,P<:PowerUnit} 38 | 39 | n_interfaces = length(regions_from) 40 | @assert length(regions_to) == n_interfaces 41 | 42 | @assert size(forwardcapacity) == (n_interfaces, N) 43 | @assert size(backwardcapacity) == (n_interfaces, N) 44 | @assert all(isnonnegative, forwardcapacity) 45 | @assert all(isnonnegative, backwardcapacity) 46 | 47 | new{N,P}(regions_from, regions_to, forwardcapacity, backwardcapacity) 48 | 49 | end 50 | 51 | end 52 | 53 | Base.:(==)(x::T, y::T) where {T <: Interfaces} = 54 | x.regions_from == y.regions_from && 55 | x.regions_to == y.regions_to && 56 | x.limit_forward == y.limit_forward && 57 | x.limit_backward == y.limit_backward 58 | 59 | Base.length(i::Interfaces) = length(i.regions_from) 60 | -------------------------------------------------------------------------------- /PRASCore.jl/src/Systems/units.jl: -------------------------------------------------------------------------------- 1 | # Augment time units 2 | 3 | unitsymbol(T::Type{<:Period}) = string(T) 4 | unitsymbol(::Type{Minute}) = "min" 5 | unitsymbol(::Type{Hour}) = "h" 6 | unitsymbol(::Type{Day}) = "d" 7 | unitsymbol(::Type{Year}) = "y" 8 | 9 | conversionfactor(F::Type{<:Period}, T::Type{<:Period}) = 10 | conversionfactor(F, Hour) * conversionfactor(Hour, T) 11 | 12 | conversionfactor(::Type{Minute}, ::Type{Hour}) = 1 / 60 13 | conversionfactor(::Type{Hour}, ::Type{Minute}) = 60 14 | 15 | conversionfactor(::Type{Hour}, ::Type{Hour}) = 1 16 | 17 | conversionfactor(::Type{Hour}, ::Type{Day}) = 1 / 24 18 | conversionfactor(::Type{Day}, ::Type{Hour}) = 24 19 | 20 | timeunits = Dict( 21 | unitsymbol(T) => T 22 | for T in [Minute, Hour, Day, Year]) 23 | 24 | # Define power units 25 | 26 | abstract type PowerUnit end 27 | struct kW <: PowerUnit end 28 | struct MW <: PowerUnit end 29 | struct GW <: PowerUnit end 30 | struct TW <: PowerUnit end 31 | 32 | unitsymbol(T::Type{<:PowerUnit}) = string(T) 33 | unitsymbol(::Type{kW}) = "kW" 34 | unitsymbol(::Type{MW}) = "MW" 35 | unitsymbol(::Type{GW}) = "GW" 36 | unitsymbol(::Type{TW}) = "TW" 37 | 38 | conversionfactor(F::Type{<:PowerUnit}, T::Type{<:PowerUnit}) = 39 | conversionfactor(F, MW) * conversionfactor(MW, T) 40 | 41 | conversionfactor(::Type{kW}, ::Type{MW}) = 1 / 1000 42 | conversionfactor(::Type{MW}, ::Type{kW}) = 1000 43 | 44 | conversionfactor(::Type{MW}, ::Type{MW}) = 1 45 | 46 | conversionfactor(::Type{MW}, ::Type{GW}) = 1 / 1000 47 | conversionfactor(::Type{GW}, ::Type{MW}) = 1000 48 | 49 | conversionfactor(::Type{MW}, ::Type{TW}) = 1 / 1_000_000 50 | conversionfactor(::Type{TW}, ::Type{MW}) = 1_000_000 51 | 52 | powerunits = Dict( 53 | unitsymbol(T) => T 54 | for T in [kW, MW, GW, TW]) 55 | 56 | # Define energy units 57 | 58 | abstract type EnergyUnit end 59 | struct kWh <: EnergyUnit end 60 | struct MWh <: EnergyUnit end 61 | struct GWh <: EnergyUnit end 62 | struct TWh <: EnergyUnit end 63 | 64 | unitsymbol(T::Type{<:EnergyUnit}) = string(T) 65 | unitsymbol(::Type{kWh}) = "kWh" 66 | unitsymbol(::Type{MWh}) = "MWh" 67 | unitsymbol(::Type{GWh}) = "GWh" 68 | unitsymbol(::Type{TWh}) = "TWh" 69 | 70 | subunits(::Type{kWh}) = (kW, Hour) 71 | subunits(::Type{MWh}) = (MW, Hour) 72 | subunits(::Type{GWh}) = (GW, Hour) 73 | subunits(::Type{TWh}) = (TW, Hour) 74 | 75 | energyunits = Dict( 76 | unitsymbol(T) => T 77 | for T in [kWh, MWh, GWh, TWh]) 78 | 79 | function conversionfactor(F::Type{<:EnergyUnit}, T::Type{<:EnergyUnit}) 80 | 81 | from_power, from_time = subunits(F) 82 | to_power, to_time = subunits(T) 83 | 84 | powerconversion = conversionfactor(from_power, to_power) 85 | timeconversion = conversionfactor(from_time, to_time) 86 | 87 | return powerconversion * timeconversion 88 | 89 | end 90 | 91 | function conversionfactor( 92 | L::Int, T::Type{<:Period}, P::Type{<:PowerUnit}, E::Type{<:EnergyUnit}) 93 | to_power, to_time = subunits(E) 94 | powerconversion = conversionfactor(P, to_power) 95 | timeconversion = conversionfactor(T, to_time) 96 | return powerconversion * timeconversion * L 97 | end 98 | 99 | function conversionfactor( 100 | L::Int, T::Type{<:Period}, E::Type{<:EnergyUnit}, P::Type{<:PowerUnit}) 101 | from_power, from_time = subunits(E) 102 | powerconversion = conversionfactor(from_power, P) 103 | timeconversion = conversionfactor(from_time, T) 104 | return powerconversion * timeconversion / L 105 | end 106 | 107 | powertoenergy( 108 | p::Real, P::Type{<:PowerUnit}, 109 | L::Real, T::Type{<:Period}, 110 | E::Type{<:EnergyUnit}) = p*conversionfactor(L, T, P, E) 111 | 112 | energytopower( 113 | e::Real, E::Type{<:EnergyUnit}, 114 | L::Real, T::Type{<:Period}, 115 | P::Type{<:PowerUnit}) = e*conversionfactor(L, T, E, P) 116 | 117 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/availability.jl: -------------------------------------------------------------------------------- 1 | @testset "AvailabilityResult" begin 2 | 3 | N = DD.nperiods 4 | r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource 5 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 6 | available = rand(Bool, DD.nresources, N, DD.nsamples) 7 | 8 | # Generators 9 | 10 | result = PRASCore.Results.GeneratorAvailabilityResult{N,1,Hour}( 11 | DD.resourcenames, DD.periods, available) 12 | 13 | @test length(result[r, t]) == DD.nsamples 14 | @test result[r, t] ≈ vec(available[r_idx, t_idx, :]) 15 | 16 | @test_throws BoundsError result[r, t_bad] 17 | @test_throws BoundsError result[r_bad, t] 18 | @test_throws BoundsError result[r_bad, t_bad] 19 | 20 | # Storages 21 | 22 | result = PRASCore.Results.StorageAvailabilityResult{N,1,Hour}( 23 | DD.resourcenames, DD.periods, available) 24 | 25 | @test length(result[r, t]) == DD.nsamples 26 | @test result[r, t] ≈ vec(available[r_idx, t_idx, :]) 27 | 28 | @test_throws BoundsError result[r, t_bad] 29 | @test_throws BoundsError result[r_bad, t] 30 | @test_throws BoundsError result[r_bad, t_bad] 31 | 32 | # GeneratorStorages 33 | 34 | result = PRASCore.Results.GeneratorStorageAvailabilityResult{N,1,Hour}( 35 | DD.resourcenames, DD.periods, available) 36 | 37 | @test length(result[r, t]) == DD.nsamples 38 | @test result[r, t] ≈ vec(available[r_idx, t_idx, :]) 39 | 40 | @test_throws BoundsError result[r, t_bad] 41 | @test_throws BoundsError result[r_bad, t] 42 | @test_throws BoundsError result[r_bad, t_bad] 43 | 44 | # Lines 45 | 46 | result = PRASCore.Results.LineAvailabilityResult{N,1,Hour}( 47 | DD.resourcenames, DD.periods, available) 48 | 49 | @test length(result[r, t]) == DD.nsamples 50 | @test result[r, t] ≈ vec(available[r_idx, t_idx, :]) 51 | 52 | @test_throws BoundsError result[r, t_bad] 53 | @test_throws BoundsError result[r_bad, t] 54 | @test_throws BoundsError result[r_bad, t_bad] 55 | 56 | end 57 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/energy.jl: -------------------------------------------------------------------------------- 1 | @testset "EnergyResult" begin 2 | 3 | N = DD.nperiods 4 | r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource 5 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 6 | 7 | # Storages 8 | 9 | result = PRASCore.Results.StorageEnergyResult{N,1,Hour,MWh}( 10 | DD.nsamples, DD.resourcenames, DD.periods, 11 | DD.d1_resourceperiod, DD.d2_period, DD.d2_resourceperiod) 12 | 13 | @test result[t] ≈ (sum(DD.d1_resourceperiod[:, t_idx]), DD.d2_period[t_idx]) 14 | @test result[r, t] ≈ 15 | (DD.d1_resourceperiod[r_idx, t_idx], DD.d2_resourceperiod[r_idx, t_idx]) 16 | 17 | @test_throws BoundsError result[t_bad] 18 | @test_throws BoundsError result[r, t_bad] 19 | @test_throws BoundsError result[r_bad, t] 20 | @test_throws BoundsError result[r_bad, t_bad] 21 | 22 | # GeneratorStorages 23 | 24 | result = PRASCore.Results.GeneratorStorageEnergyResult{N,1,Hour,MWh}( 25 | DD.nsamples, DD.resourcenames, DD.periods, 26 | DD.d1_resourceperiod, DD.d2_period, DD.d2_resourceperiod) 27 | 28 | @test result[t] ≈ (sum(DD.d1_resourceperiod[:, t_idx]), DD.d2_period[t_idx]) 29 | @test result[r, t] ≈ 30 | (DD.d1_resourceperiod[r_idx, t_idx], DD.d2_resourceperiod[r_idx, t_idx]) 31 | 32 | @test_throws BoundsError result[t_bad] 33 | @test_throws BoundsError result[r, t_bad] 34 | @test_throws BoundsError result[r_bad, t] 35 | @test_throws BoundsError result[r_bad, t_bad] 36 | 37 | end 38 | 39 | @testset "EnergySamplesResult" begin 40 | 41 | N = DD.nperiods 42 | r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource 43 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 44 | 45 | # Storages 46 | 47 | result = PRASCore.Results.StorageEnergySamplesResult{N,1,Hour,MWh}( 48 | DD.resourcenames, DD.periods, DD.d) 49 | 50 | @test length(result[t]) == DD.nsamples 51 | @test result[t] ≈ vec(sum(view(DD.d, :, t_idx, :), dims=1)) 52 | 53 | @test length(result[r, t]) == DD.nsamples 54 | @test result[r, t] ≈ vec(DD.d[r_idx, t_idx, :]) 55 | 56 | @test_throws BoundsError result[t_bad] 57 | @test_throws BoundsError result[r, t_bad] 58 | @test_throws BoundsError result[r_bad, t] 59 | @test_throws BoundsError result[r_bad, t_bad] 60 | 61 | # GeneratorStorages 62 | 63 | result = PRASCore.Results.GeneratorStorageEnergySamplesResult{N,1,Hour,MWh}( 64 | DD.resourcenames, DD.periods, DD.d) 65 | 66 | @test length(result[t]) == DD.nsamples 67 | @test result[t] ≈ vec(sum(view(DD.d, :, t_idx, :), dims=1)) 68 | 69 | @test length(result[r, t]) == DD.nsamples 70 | @test result[r, t] ≈ vec(DD.d[r_idx, t_idx, :]) 71 | 72 | @test_throws BoundsError result[t_bad] 73 | @test_throws BoundsError result[r, t_bad] 74 | @test_throws BoundsError result[r_bad, t] 75 | @test_throws BoundsError result[r_bad, t_bad] 76 | 77 | end 78 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/flow.jl: -------------------------------------------------------------------------------- 1 | @testset "FlowResult" begin 2 | 3 | N = DD.nperiods 4 | i, i_idx, i_bad = DD.testinterface, DD.testinterface_idx, DD.notaninterface 5 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 6 | 7 | result = PRASCore.Results.FlowResult{N,1,Hour,MW}( 8 | DD.nsamples, DD.interfacenames, DD.periods, 9 | DD.d1_resourceperiod, DD.d2_resource, DD.d2_resourceperiod) 10 | 11 | # Interface-specific 12 | 13 | @test result[i] ≈ (mean(DD.d1_resourceperiod[i_idx, :]), DD.d2_resource[i_idx]) 14 | @test_throws BoundsError result[i_bad] 15 | 16 | # Interface + period-specific 17 | 18 | @test result[i, t] ≈ 19 | (DD.d1_resourceperiod[i_idx, t_idx], DD.d2_resourceperiod[i_idx, t_idx]) 20 | 21 | @test_throws BoundsError result[i, t_bad] 22 | @test_throws BoundsError result[i_bad, t] 23 | @test_throws BoundsError result[i_bad, t_bad] 24 | 25 | end 26 | 27 | @testset "FlowSamplesResult" begin 28 | 29 | N = DD.nperiods 30 | i, i_idx, i_bad = DD.testinterface, DD.testinterface_idx, DD.notaninterface 31 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 32 | 33 | result = PRASCore.Results.FlowSamplesResult{N,1,Hour,MW}( 34 | DD.interfacenames, DD.periods, DD.d) 35 | 36 | # Interface-specific 37 | 38 | @test length(result[i]) == DD.nsamples 39 | @test result[i] ≈ vec(mean(view(DD.d, i_idx, :, :), dims=1)) 40 | @test_throws BoundsError result[i_bad] 41 | 42 | # Region + period-specific 43 | 44 | @test length(result[i, t]) == DD.nsamples 45 | @test result[i, t] ≈ vec(DD.d[i_idx, t_idx, :]) 46 | @test_throws BoundsError result[i, t_bad] 47 | @test_throws BoundsError result[i_bad, t] 48 | @test_throws BoundsError result[i_bad, t_bad] 49 | 50 | end 51 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/metrics.jl: -------------------------------------------------------------------------------- 1 | @testset "Metrics" begin 2 | 3 | @testset "MeanEstimate" begin 4 | 5 | me1 = MeanEstimate(513.1, 26) 6 | @test val(me1) == 513.1 7 | @test stderror(me1) == 26. 8 | @test string(me1) == "510±30" 9 | 10 | me2 = MeanEstimate(0.03, 0.0001) 11 | me3 = MeanEstimate(0.03, 0.001, 100) 12 | @test me2 ≈ me2 13 | @test val(me2) == 0.03 14 | @test stderror(me2) == 0.0001 15 | @test string(me2) == "0.0300±0.0001" 16 | 17 | me4 = MeanEstimate(2.4) 18 | @test val(me4) == 2.4 19 | @test stderror(me4) == 0. 20 | @test string(me4) == "2.40000" 21 | 22 | me5 = MeanEstimate([1,2,3,4,5]) 23 | @test val(me5) == 3. 24 | @test stderror(me5) ≈ sqrt(0.5) 25 | 26 | me6 = MeanEstimate(-503.1, 260) 27 | @test val(me6) == -503.1 28 | @test stderror(me6) == 260. 29 | @test string(me6) == "-500±300" 30 | 31 | @test_throws DomainError MeanEstimate(1.23, -0.002) 32 | 33 | end 34 | 35 | @testset "LOLE" begin 36 | 37 | lole1 = LOLE{4380,2,Hour}(MeanEstimate(1.2)) 38 | @test string(lole1) == "LOLE = 1.20000 event-(2h)/8760h" 39 | 40 | lole2 = LOLE{8760,1,Hour}(MeanEstimate(2.4, 0.1)) 41 | @test string(lole2) == "LOLE = 2.4±0.1 event-h/8760h" 42 | 43 | lole3 = LOLE{3650,1,Day}(MeanEstimate(1.0, 0.01)) 44 | @test string(lole3) == "LOLE = 1.00±0.01 event-d/3650d" 45 | 46 | @test_throws DomainError LOLE{3650,1,Day}(MeanEstimate(-1.2, 0.)) 47 | 48 | 49 | end 50 | 51 | @testset "EUE" begin 52 | 53 | eue1 = EUE{2,1,Hour,MWh}(MeanEstimate(1.2)) 54 | @test string(eue1) == "EUE = 1.20000 MWh/2h" 55 | 56 | eue2 = EUE{1,2,Year,GWh}(MeanEstimate(17.2, 1.3)) 57 | @test string(eue2) == "EUE = 17±1 GWh/2y" 58 | 59 | @test_throws DomainError EUE{1,1,Hour,MWh}(MeanEstimate(-1.2)) 60 | 61 | end 62 | 63 | @testset "NEUE" begin 64 | 65 | neue = NEUE(MeanEstimate(1.2)) 66 | @test string(neue) == "NEUE = 1.20000 ppm" 67 | 68 | neue2 = NEUE(MeanEstimate(17.2, 1.3)) 69 | @test string(neue2) == "NEUE = 17±1 ppm" 70 | 71 | @test_throws DomainError NEUE(MeanEstimate(-1.2)) 72 | 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/runtests.jl: -------------------------------------------------------------------------------- 1 | @testset "Results" begin 2 | 3 | include("metrics.jl") 4 | include("shortfall.jl") 5 | include("surplus.jl") 6 | include("flow.jl") 7 | include("utilization.jl") 8 | include("energy.jl") 9 | include("availability.jl") 10 | 11 | end 12 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/shortfall.jl: -------------------------------------------------------------------------------- 1 | @testset "ShortfallResult" begin 2 | 3 | 4 | N = DD.nperiods 5 | r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource 6 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 7 | 8 | result = PRASCore.Results.ShortfallResult{N,1,Hour,MWh}( 9 | DD.nsamples, Regions{N,MW}(DD.resourcenames, DD.resource_vals), DD.periods, 10 | DD.d1, DD.d2, DD.d1_resource, DD.d2_resource, 11 | DD.d1_period, DD.d2_period, DD.d1_resourceperiod, DD.d2_resourceperiod, 12 | DD.d3_resourceperiod, 13 | DD.d4, DD.d4_resource, DD.d4_period, DD.d4_resourceperiod) 14 | 15 | # Overall 16 | 17 | @test result[] ≈ (sum(DD.d3_resourceperiod), DD.d4) 18 | 19 | lole = LOLE(result) 20 | @test val(lole) ≈ DD.d1 21 | @test stderror(lole) ≈ DD.d2 / sqrt(DD.nsamples) 22 | 23 | eue = EUE(result) 24 | @test val(eue) ≈ first(result[]) 25 | @test stderror(eue) ≈ last(result[]) / sqrt(DD.nsamples) 26 | 27 | neue = NEUE(result) 28 | load = sum(DD.resource_vals) 29 | @test val(neue) ≈ first(result[]) / load*1e6 30 | @test stderror(neue) ≈ last(result[]) / sqrt(DD.nsamples) / load*1e6 31 | # Region-specific 32 | 33 | @test result[r] ≈ (sum(DD.d3_resourceperiod[r_idx,:]), DD.d4_resource[r_idx]) 34 | 35 | region_lole = LOLE(result, r) 36 | @test val(region_lole) ≈ DD.d1_resource[r_idx] 37 | @test stderror(region_lole) ≈ DD.d2_resource[r_idx] / sqrt(DD.nsamples) 38 | 39 | region_eue = EUE(result, r) 40 | @test val(region_eue) ≈ first(result[r]) 41 | @test stderror(region_eue) ≈ last(result[r]) / sqrt(DD.nsamples) 42 | 43 | region_neue = NEUE(result, r) 44 | load = sum(DD.resource_vals[r_idx,:]) 45 | @test val(region_neue) ≈ first(result[r]) / load*1e6 46 | @test stderror(region_neue) ≈ last(result[r]) / sqrt(DD.nsamples) / load*1e6 47 | 48 | @test_throws BoundsError result[r_bad] 49 | @test_throws BoundsError LOLE(result, r_bad) 50 | @test_throws BoundsError EUE(result, r_bad) 51 | @test_throws BoundsError NEUE(result, r_bad) 52 | 53 | # Period-specific 54 | 55 | @test result[t] ≈ (sum(DD.d3_resourceperiod[:, t_idx]), DD.d4_period[t_idx]) 56 | 57 | period_lole = LOLE(result, t) 58 | @test val(period_lole) ≈ DD.d1_period[t_idx] 59 | @test stderror(period_lole) ≈ DD.d2_period[t_idx] / sqrt(DD.nsamples) 60 | 61 | period_eue = EUE(result, t) 62 | @test val(period_eue) ≈ first(result[t]) 63 | @test stderror(period_eue) ≈ last(result[t]) / sqrt(DD.nsamples) 64 | 65 | @test_throws BoundsError result[t_bad] 66 | @test_throws BoundsError LOLE(result, t_bad) 67 | @test_throws BoundsError EUE(result, t_bad) 68 | 69 | # Region + period-specific 70 | 71 | @test result[r, t] ≈ 72 | (DD.d3_resourceperiod[r_idx, t_idx], DD.d4_resourceperiod[r_idx, t_idx]) 73 | 74 | regionperiod_lole = LOLE(result, r, t) 75 | @test val(regionperiod_lole) ≈ DD.d1_resourceperiod[r_idx, t_idx] 76 | @test stderror(regionperiod_lole) ≈ 77 | DD.d2_resourceperiod[r_idx, t_idx] / sqrt(DD.nsamples) 78 | 79 | regionperiod_eue = EUE(result, r, t) 80 | @test val(regionperiod_eue) ≈ first(result[r, t]) 81 | @test stderror(regionperiod_eue) ≈ last(result[r, t]) / sqrt(DD.nsamples) 82 | 83 | @test_throws BoundsError result[r, t_bad] 84 | @test_throws BoundsError result[r_bad, t] 85 | @test_throws BoundsError result[r_bad, t_bad] 86 | 87 | @test_throws BoundsError LOLE(result, r, t_bad) 88 | @test_throws BoundsError LOLE(result, r_bad, t) 89 | @test_throws BoundsError LOLE(result, r_bad, t_bad) 90 | 91 | @test_throws BoundsError EUE(result, r, t_bad) 92 | @test_throws BoundsError EUE(result, r_bad, t) 93 | @test_throws BoundsError EUE(result, r_bad, t_bad) 94 | 95 | end 96 | 97 | 98 | @testset "ShortfallSamplesResult" begin 99 | 100 | N = DD.nperiods 101 | r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource 102 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 103 | 104 | result = PRASCore.Results.ShortfallSamplesResult{N,1,Hour,MW,MWh}( 105 | Regions{N,MW}(DD.resourcenames, DD.resource_vals), DD.periods, DD.d) 106 | 107 | # Overall 108 | 109 | @test length(result[]) == DD.nsamples 110 | @test result[] ≈ vec(sum(DD.d, dims=1:2)) 111 | 112 | lole = LOLE(result) 113 | eventperiods = sum(sum(DD.d, dims=1) .> 0, dims=2) 114 | @test val(lole) ≈ mean(eventperiods) 115 | @test stderror(lole) ≈ std(eventperiods) / sqrt(DD.nsamples) 116 | 117 | eue = EUE(result) 118 | @test val(eue) ≈ mean(result[]) 119 | @test stderror(eue) ≈ std(result[]) / sqrt(DD.nsamples) 120 | 121 | neue = NEUE(result) 122 | load = sum(DD.resource_vals) 123 | @test val(neue) ≈ mean(result[]) / load*1e6 124 | @test stderror(neue) ≈ std(result[]) / sqrt(DD.nsamples) / load*1e6 125 | 126 | # Region-specific 127 | 128 | @test length(result[r]) == DD.nsamples 129 | @test result[r] ≈ vec(sum(view(DD.d, r_idx, :, :), dims=1)) 130 | 131 | region_lole = LOLE(result, r) 132 | region_eventperiods = sum(view(DD.d, r_idx, :, :) .> 0, dims=1) 133 | @test val(region_lole) ≈ mean(region_eventperiods) 134 | @test stderror(region_lole) ≈ std(region_eventperiods) / sqrt(DD.nsamples) 135 | 136 | region_eue = EUE(result, r) 137 | @test val(region_eue) ≈ mean(result[r]) 138 | @test stderror(region_eue) ≈ std(result[r]) / sqrt(DD.nsamples) 139 | 140 | region_neue = NEUE(result, r) 141 | load = sum(DD.resource_vals[r_idx,:]) 142 | @test val(region_neue) ≈ mean(result[r]) / load*1e6 143 | @test stderror(region_neue) ≈ std(result[r]) / sqrt(DD.nsamples) / load*1e6 144 | 145 | @test_throws BoundsError result[r_bad] 146 | @test_throws BoundsError LOLE(result, r_bad) 147 | @test_throws BoundsError EUE(result, r_bad) 148 | @test_throws BoundsError NEUE(result, r_bad) 149 | 150 | # Period-specific 151 | 152 | @test length(result[t]) == DD.nsamples 153 | @test result[t] ≈ vec(sum(view(DD.d, :, t_idx, :), dims=1)) 154 | 155 | period_lole = LOLE(result, t) 156 | period_eventperiods = result[t] .> 0 157 | @test val(period_lole) ≈ mean(period_eventperiods) 158 | @test stderror(period_lole) ≈ std(period_eventperiods) / sqrt(DD.nsamples) 159 | 160 | period_eue = EUE(result, t) 161 | @test val(period_eue) ≈ mean(result[t]) 162 | @test stderror(period_eue) ≈ std(result[t]) / sqrt(DD.nsamples) 163 | 164 | @test_throws BoundsError result[t_bad] 165 | @test_throws BoundsError LOLE(result, t_bad) 166 | @test_throws BoundsError EUE(result, t_bad) 167 | 168 | # Region + period-specific 169 | 170 | @test length(result[r, t]) == DD.nsamples 171 | @test result[r, t] ≈ vec(DD.d[r_idx, t_idx, :]) 172 | 173 | regionperiod_lole = LOLE(result, r, t) 174 | regionperiod_eventperiods = result[r, t] .> 0 175 | @test val(regionperiod_lole) ≈ mean(regionperiod_eventperiods) 176 | @test stderror(regionperiod_lole) ≈ 177 | std(regionperiod_eventperiods) / sqrt(DD.nsamples) 178 | 179 | regionperiod_eue = EUE(result, r, t) 180 | @test val(regionperiod_eue) ≈ mean(result[r, t]) 181 | @test stderror(regionperiod_eue) ≈ std(result[r, t]) / sqrt(DD.nsamples) 182 | 183 | @test_throws BoundsError result[r, t_bad] 184 | @test_throws BoundsError result[r_bad, t] 185 | @test_throws BoundsError result[r_bad, t_bad] 186 | 187 | @test_throws BoundsError LOLE(result, r, t_bad) 188 | @test_throws BoundsError LOLE(result, r_bad, t) 189 | @test_throws BoundsError LOLE(result, r_bad, t_bad) 190 | 191 | @test_throws BoundsError EUE(result, r, t_bad) 192 | @test_throws BoundsError EUE(result, r_bad, t) 193 | @test_throws BoundsError EUE(result, r_bad, t_bad) 194 | 195 | end 196 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/surplus.jl: -------------------------------------------------------------------------------- 1 | @testset "SurplusResult" begin 2 | 3 | N = DD.nperiods 4 | r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource 5 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 6 | 7 | result = PRASCore.Results.SurplusResult{N,1,Hour,MW}( 8 | DD.nsamples, DD.resourcenames, DD.periods, 9 | DD.d1_resourceperiod, DD.d2_period, DD.d2_resourceperiod) 10 | 11 | # Period-specific 12 | 13 | @test result[t] ≈ (sum(DD.d1_resourceperiod[:, t_idx]), DD.d2_period[t_idx]) 14 | @test_throws BoundsError result[t_bad] 15 | 16 | # Region + period-specific 17 | 18 | @test result[r, t] ≈ 19 | (DD.d1_resourceperiod[r_idx, t_idx], DD.d2_resourceperiod[r_idx, t_idx]) 20 | 21 | @test_throws BoundsError result[r, t_bad] 22 | @test_throws BoundsError result[r_bad, t] 23 | @test_throws BoundsError result[r_bad, t_bad] 24 | 25 | end 26 | 27 | @testset "SurplusSamplesResult" begin 28 | 29 | N = DD.nperiods 30 | r, r_idx, r_bad = DD.testresource, DD.testresource_idx, DD.notaresource 31 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 32 | 33 | result = PRASCore.Results.SurplusSamplesResult{N,1,Hour,MW}( 34 | DD.resourcenames, DD.periods, DD.d) 35 | 36 | # Period-specific 37 | 38 | @test length(result[t]) == DD.nsamples 39 | @test result[t] ≈ vec(sum(view(DD.d, :, t_idx, :), dims=1)) 40 | @test_throws BoundsError result[t_bad] 41 | 42 | # Region + period-specific 43 | 44 | @test length(result[r, t]) == DD.nsamples 45 | @test result[r, t] ≈ vec(DD.d[r_idx, t_idx, :]) 46 | @test_throws BoundsError result[r, t_bad] 47 | @test_throws BoundsError result[r_bad, t] 48 | @test_throws BoundsError result[r_bad, t_bad] 49 | 50 | end 51 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Results/utilization.jl: -------------------------------------------------------------------------------- 1 | @testset "UtilizationResult" begin 2 | 3 | N = DD.nperiods 4 | i, i_idx, i_bad = DD.testinterface, DD.testinterface_idx, DD.notaninterface 5 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 6 | 7 | result = PRASCore.Results.UtilizationResult{N,1,Hour}( 8 | DD.nsamples, DD.interfacenames, DD.periods, 9 | DD.d1_resourceperiod, DD.d2_resource, DD.d2_resourceperiod) 10 | 11 | # Interface-specific 12 | 13 | @test result[i] ≈ (mean(DD.d1_resourceperiod[i_idx, :]), DD.d2_resource[i_idx]) 14 | @test_throws BoundsError result[i_bad] 15 | 16 | # Interface + period-specific 17 | 18 | @test result[i, t] ≈ 19 | (DD.d1_resourceperiod[i_idx, t_idx], DD.d2_resourceperiod[i_idx, t_idx]) 20 | 21 | @test_throws BoundsError result[i, t_bad] 22 | @test_throws BoundsError result[i_bad, t] 23 | @test_throws BoundsError result[i_bad, t_bad] 24 | 25 | end 26 | 27 | @testset "UtilizationSamplesResult" begin 28 | 29 | N = DD.nperiods 30 | i, i_idx, i_bad = DD.testinterface, DD.testinterface_idx, DD.notaninterface 31 | t, t_idx, t_bad = DD.testperiod, DD.testperiod_idx, DD.notaperiod 32 | 33 | result = PRASCore.Results.UtilizationSamplesResult{N,1,Hour}( 34 | DD.interfacenames, DD.periods, DD.d) 35 | 36 | # Interface-specific 37 | 38 | @test length(result[i]) == DD.nsamples 39 | @test result[i] ≈ vec(mean(view(DD.d, i_idx, :, :), dims=1)) 40 | @test_throws BoundsError result[i_bad] 41 | 42 | # Region + period-specific 43 | 44 | @test length(result[i, t]) == DD.nsamples 45 | @test result[i, t] ≈ vec(DD.d[i_idx, t_idx, :]) 46 | @test_throws BoundsError result[i, t_bad] 47 | @test_throws BoundsError result[i_bad, t] 48 | @test_throws BoundsError result[i_bad, t_bad] 49 | 50 | end 51 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Systems/SystemModel.jl: -------------------------------------------------------------------------------- 1 | @testset "SystemModel" begin 2 | 3 | generators = Generators{10,1,Hour,MW}( 4 | ["Gen A", "Gen B"], ["CC", "CT"], 5 | rand(1:10, 2, 10), fill(0.1, 2, 10), fill(0.1, 2, 10)) 6 | 7 | storages = Storages{10,1,Hour,MW,MWh}( 8 | ["S1", "S2"], ["Battery", "Pumped Hydro"], 9 | rand(1:10, 2, 10), rand(1:10, 2, 10), rand(1:10, 2, 10), 10 | fill(0.9, 2, 10), fill(1.0, 2, 10), fill(0.99, 2, 10), 11 | fill(0.1, 2, 10), fill(0.5, 2, 10)) 12 | 13 | generatorstorages = GeneratorStorages{10,1,Hour,MW,MWh}( 14 | ["GS1"], ["CSP"], 15 | rand(1:10, 1, 10), rand(1:10, 1, 10), rand(1:10, 1, 10), 16 | fill(0.9, 1, 10), fill(1.0, 1, 10), fill(0.99, 1, 10), 17 | rand(1:10, 1, 10), rand(1:10, 1, 10), rand(1:10, 1, 10), 18 | fill(0.1, 1, 10), fill(0.5, 1, 10)) 19 | 20 | tz = tz"UTC" 21 | timestamps = ZonedDateTime(2020, 1, 1, 0, tz):Hour(1):ZonedDateTime(2020,1,1,9, tz) 22 | 23 | # Single-region constructor 24 | SystemModel( 25 | generators, storages, generatorstorages, timestamps, rand(1:20, 10)) 26 | 27 | regions = Regions{10,MW}( 28 | ["Region A", "Region B"], rand(1:20, 2, 10)) 29 | 30 | interfaces = Interfaces{10,MW}( 31 | [1], [2], fill(100, 1, 10), fill(100, 1, 10)) 32 | 33 | lines = Lines{10,1,Hour,MW}( 34 | ["Line 1", "Line 2"], ["Line", "Line"], 35 | fill(10, 2, 10), fill(10, 2, 10), fill(0., 2, 10), fill(1.0, 2, 10)) 36 | 37 | gen_regions = [1:1, 2:2] 38 | stor_regions = [1:0, 1:2] 39 | genstor_regions = [1:1, 2:1] 40 | line_interfaces = [1:2] 41 | 42 | # Multi-region constructor 43 | SystemModel( 44 | regions, interfaces, 45 | generators, gen_regions, storages, stor_regions, 46 | generatorstorages, genstor_regions, 47 | lines, line_interfaces, 48 | timestamps) 49 | 50 | end 51 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Systems/assets.jl: -------------------------------------------------------------------------------- 1 | @testset "Assets" begin 2 | 3 | names = ["A1", "A2", "B1"] 4 | categories = ["A", "A", "B"] 5 | 6 | vals_int = rand(1:100, 3, 10) 7 | vals_float = rand(3, 10) 8 | 9 | @testset "Generators" begin 10 | 11 | Generators{10,1,Hour,MW}( 12 | names, categories, vals_int, vals_float, vals_float) 13 | 14 | @test_throws AssertionError Generators{5,1,Hour,MW}( 15 | names, categories, vals_int, vals_float, vals_float) 16 | 17 | @test_throws AssertionError Generators{10,1,Hour,MW}( 18 | names[1:2], categories, vals_int, vals_float, vals_float) 19 | 20 | @test_throws AssertionError Generators{10,1,Hour,MW}( 21 | names[1:2], categories[1:2], vals_int, vals_float, vals_float) 22 | 23 | @test_throws AssertionError Generators{10,1,Hour,MW}( 24 | names, categories, -vals_int, vals_float, vals_float) 25 | 26 | end 27 | 28 | @testset "Storages" begin 29 | 30 | Storages{10,1,Hour,MW,MWh}( 31 | names, categories, vals_int, vals_int, vals_int, 32 | vals_float, vals_float, vals_float, vals_float, vals_float) 33 | 34 | @test_throws AssertionError Storages{5,1,Hour,MW,MWh}( 35 | names, categories, vals_int, vals_int, vals_int, 36 | vals_float, vals_float, vals_float, vals_float, vals_float) 37 | 38 | @test_throws AssertionError Storages{10,1,Hour,MW,MWh}( 39 | names, categories[1:2], vals_int, vals_int, vals_int, 40 | vals_float, vals_float, vals_float, vals_float, vals_float) 41 | 42 | @test_throws AssertionError Storages{10,1,Hour,MW,MWh}( 43 | names[1:2], categories[1:2], vals_int, vals_int, vals_int, 44 | vals_float, vals_float, vals_float, vals_float, vals_float) 45 | 46 | @test_throws AssertionError Storages{10,1,Hour,MW,MWh}( 47 | names, categories, vals_int, vals_int, vals_int, 48 | vals_float, vals_float, -vals_float, vals_float, vals_float) 49 | 50 | end 51 | 52 | @testset "GeneratorStorages" begin 53 | 54 | GeneratorStorages{10,1,Hour,MW,MWh}( 55 | names, categories, 56 | vals_int, vals_int, vals_int, vals_float, vals_float, vals_float, 57 | vals_int, vals_int, vals_int, vals_float, vals_float) 58 | 59 | @test_throws AssertionError GeneratorStorages{5,1,Hour,MW,MWh}( 60 | names, categories, 61 | vals_int, vals_int, vals_int, vals_float, vals_float, vals_float, 62 | vals_int, vals_int, vals_int, vals_float, vals_float) 63 | 64 | 65 | @test_throws AssertionError GeneratorStorages{10,1,Hour,MW,MWh}( 66 | names, categories[1:2], 67 | vals_int, vals_int, vals_int, vals_float, vals_float, vals_float, 68 | vals_int, vals_int, vals_int, vals_float, vals_float) 69 | 70 | @test_throws AssertionError GeneratorStorages{10,1,Hour,MW,MWh}( 71 | names[1:2], categories[1:2], 72 | vals_int, vals_int, vals_int, vals_float, vals_float, vals_float, 73 | vals_int, vals_int, vals_int, vals_float, vals_float) 74 | 75 | @test_throws AssertionError GeneratorStorages{10,1,Hour,MW,MWh}( 76 | names, categories, 77 | vals_int, vals_int, vals_int, vals_float, vals_float, -vals_float, 78 | vals_int, vals_int, vals_int, vals_float, vals_float) 79 | 80 | end 81 | 82 | @testset "Lines" begin 83 | 84 | Lines{10,1,Hour,MW}( 85 | names, categories, vals_int, vals_int, vals_float, vals_float) 86 | 87 | @test_throws AssertionError Lines{5,1,Hour,MW}( 88 | names, categories, vals_int, vals_int, vals_float, vals_float) 89 | 90 | @test_throws AssertionError Lines{10,1,Hour,MW}( 91 | names[1:2], categories, vals_int, vals_int, vals_float, vals_float) 92 | 93 | @test_throws AssertionError Lines{10,1,Hour,MW}( 94 | names[1:2], categories[1:2], vals_int, vals_int, vals_float, vals_float) 95 | 96 | @test_throws AssertionError Lines{10,1,Hour,MW}( 97 | names, categories, -vals_int, vals_int, vals_float, vals_float) 98 | 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Systems/collections.jl: -------------------------------------------------------------------------------- 1 | @testset "Collections" begin 2 | 3 | @testset "Regions" begin 4 | 5 | Regions{10,MW}(["Region A", "Region B"], rand(1:10, 2, 10)) 6 | 7 | @test_throws AssertionError Regions{10,MW}( 8 | ["Region A", "Region B"], rand(1:10, 2, 5)) 9 | 10 | @test_throws AssertionError Regions{10,MW}( 11 | ["Region A", "Region B"], rand(1:10, 3, 10)) 12 | 13 | @test_throws AssertionError Regions{10,MW}( 14 | ["Region A", "Region B"], -rand(1:10, 2, 10)) 15 | 16 | end 17 | 18 | @testset "Interfaces" begin 19 | 20 | Interfaces{10,MW}( 21 | [1,1,2], [2,3,3], rand(1:15, 3, 10), rand(1:15, 3, 10)) 22 | 23 | @test_throws AssertionError Interfaces{10,MW}( 24 | [1,1,2], [2,3], rand(1:15, 3, 10), rand(1:15, 3, 10)) 25 | 26 | @test_throws AssertionError Interfaces{10,MW}( 27 | [1,1,2], [2,3,3], rand(1:15, 2, 10), rand(1:15, 2, 10)) 28 | 29 | @test_throws AssertionError Interfaces{10,MW}( 30 | [1,1,2], [2,3,3], rand(1:15, 3, 11), rand(1:15, 3, 11)) 31 | 32 | @test_throws AssertionError Interfaces{10,MW}( 33 | [1,1,2], [2,3,3], rand(1:15, 3, 10), -rand(1:15, 3, 10)) 34 | 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Systems/runtests.jl: -------------------------------------------------------------------------------- 1 | @testset "Systems" begin 2 | 3 | include("units.jl") 4 | include("assets.jl") 5 | include("collections.jl") 6 | include("SystemModel.jl") 7 | 8 | end 9 | -------------------------------------------------------------------------------- /PRASCore.jl/test/Systems/units.jl: -------------------------------------------------------------------------------- 1 | @testset "Units and Conversions" begin 2 | 3 | @test powertoenergy(10, MW, 2, Hour, MWh) == 20 4 | @test powertoenergy(10, MW, 30, Minute, MWh) == 5 5 | 6 | @test energytopower(100, MWh, 10, Hour, MW) == 10 7 | @test energytopower(100, MWh, 30, Minute, MW) == 200 8 | 9 | @test unitsymbol(MW) == "MW" 10 | @test unitsymbol(GW) == "GW" 11 | 12 | @test unitsymbol(MWh) == "MWh" 13 | @test unitsymbol(GWh) == "GWh" 14 | @test unitsymbol(TWh) == "TWh" 15 | 16 | @test unitsymbol(Minute) == "min" 17 | @test unitsymbol(Hour) == "h" 18 | @test unitsymbol(Day) == "d" 19 | @test unitsymbol(Year) == "y" 20 | 21 | end 22 | -------------------------------------------------------------------------------- /PRASCore.jl/test/dummydata.jl: -------------------------------------------------------------------------------- 1 | module DummyData 2 | 3 | using Dates 4 | using TimeZones 5 | 6 | const tz = tz"UTC" 7 | 8 | nsamples = 100 9 | 10 | resourcenames = ["A", "B", "C"] 11 | nresources = length(resourcenames) 12 | testresource_idx = 2 13 | testresource = resourcenames[testresource_idx] 14 | notaresource = "NotAResource" 15 | 16 | interfacenames = ["A"=>"B", "B"=>"C", "A"=>"C"] 17 | ninterfaces = length(interfacenames) 18 | testinterface_idx = 3 19 | testinterface = interfacenames[testinterface_idx] 20 | notaninterface = "X"=>"Y" 21 | 22 | periods = ZonedDateTime(2012,4,1,0,tz):Hour(1):ZonedDateTime(2012,4,7,23,tz) 23 | nperiods = length(periods) 24 | resource_vals = rand(0:999, nresources, nperiods) 25 | testperiod_idx = 29 26 | testperiod = periods[testperiod_idx] 27 | notaperiod = ZonedDateTime(2010,1,1,0,tz) 28 | 29 | d = rand(0:999, nresources, nperiods, nsamples) 30 | 31 | d1 = rand() 32 | d1_resource = rand(nresources) 33 | d1_period = rand(nperiods) 34 | d1_resourceperiod = rand(nresources, nperiods) 35 | 36 | d2 = rand() 37 | d2_resource = rand(nresources) 38 | d2_period = rand(nperiods) 39 | d2_resourceperiod = rand(nresources, nperiods) 40 | 41 | d3 = rand() 42 | d3_resource = rand(nresources) 43 | d3_period = rand(nperiods) 44 | d3_resourceperiod = rand(nresources, nperiods) 45 | 46 | d4 = rand() 47 | d4_resource = rand(nresources) 48 | d4_period = rand(nperiods) 49 | d4_resourceperiod = rand(nresources, nperiods) 50 | 51 | end 52 | 53 | import .DummyData 54 | const DD = DummyData 55 | -------------------------------------------------------------------------------- /PRASCore.jl/test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Dates 2 | using PRASCore 3 | using StatsBase 4 | using Test 5 | using TimeZones 6 | 7 | import PRASCore.Results: MeanEstimate, ReliabilityMetric 8 | import PRASCore.Systems: TestData, Regions 9 | 10 | withinrange(x::ReliabilityMetric, y::Real, n::Real) = 11 | isapprox(val(x), y, atol=n*stderror(x)) 12 | 13 | withinrange(x::Tuple{<:Real, <:Real}, y::Real, nsamples::Int, n::Real) = 14 | isapprox(first(x), y, atol=n*last(x)/sqrt(nsamples)) 15 | 16 | Base.isapprox(x::T, y::T) where {T <: Tuple} = all(isapprox.(x, y)) 17 | 18 | Base.isapprox(x::T, y::T) where {T <: ReliabilityMetric} = 19 | isapprox(val(x), val(y)) && isapprox(stderror(x), stderror(y)) 20 | 21 | Base.isapprox(x::Tuple{Float64,Float64}, y::Vector{<:Real}) = 22 | isapprox(x[1], mean(y)) && isapprox(x[2], std(y)) 23 | 24 | @testset "PRAS" begin 25 | include("dummydata.jl") 26 | include("Systems/runtests.jl") 27 | include("Results/runtests.jl") 28 | include("Simulations/runtests.jl") 29 | end 30 | -------------------------------------------------------------------------------- /PRASFiles.jl/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alliance for Sustainable Energy, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PRASFiles.jl/Project.toml: -------------------------------------------------------------------------------- 1 | name = "PRASFiles" 2 | uuid = "a2806276-6d43-4ef5-91c0-491704cd7cf1" 3 | authors = ["Gord Stephen ", "Surya Chandan Dhulipala "] 4 | version = "0.7.1" 5 | 6 | [deps] 7 | Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" 8 | HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" 9 | JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" 10 | PRASCore = "c5c32b99-e7c3-4530-a685-6f76e19f7fe2" 11 | StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" 12 | StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" 13 | TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" 14 | 15 | [compat] 16 | Dates = "1" 17 | HDF5 = "0.16,0.17" 18 | JSON3 = "1.14" 19 | PRASCore = "0.7.1" 20 | StatsBase = "0.34" 21 | StructTypes = "1.11" 22 | TimeZones = "1" 23 | julia = "1.10" 24 | 25 | [extras] 26 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 27 | 28 | [targets] 29 | test = ["Test"] 30 | -------------------------------------------------------------------------------- /PRASFiles.jl/src/PRASFiles.jl: -------------------------------------------------------------------------------- 1 | module PRASFiles 2 | 3 | const PRASFILE_VERSION = "v0.7.0" 4 | 5 | import PRASCore.Systems: SystemModel, Regions, Interfaces, 6 | Generators, Storages, GeneratorStorages, Lines, 7 | timeunits, powerunits, energyunits, unitsymbol 8 | 9 | import PRASCore.Results: EUE, LOLE, NEUE, ShortfallResult, ShortfallSamplesResult, AbstractShortfallResult, Result 10 | import StatsBase: mean 11 | import Dates: @dateformat_str, format, now 12 | import TimeZones: ZonedDateTime 13 | import HDF5: HDF5, attributes, File, Group, Dataset, Datatype, dataspace, 14 | h5open, create_group, create_dataset, hdf5_type_id 15 | import HDF5.API: h5t_create, h5t_copy, h5t_insert, h5t_set_size, 16 | H5T_COMPOUND, h5d_write, H5S_ALL, H5P_DEFAULT 17 | 18 | import StructTypes: StructType, Struct, OrderedStruct 19 | import JSON3: pretty 20 | 21 | export savemodel 22 | export saveshortfall 23 | 24 | include("Systems/read.jl") 25 | include("Systems/write.jl") 26 | include("Systems/utils.jl") 27 | include("Results/utils.jl") 28 | include("Results/write.jl") 29 | 30 | function toymodel() 31 | path = dirname(@__FILE__) 32 | return SystemModel(joinpath(path, "Systems","toymodel.pras")) 33 | end 34 | 35 | function rts_gmlc() 36 | path = dirname(@__FILE__) 37 | return SystemModel(joinpath(path, "Systems","rts.pras")) 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /PRASFiles.jl/src/Results/utils.jl: -------------------------------------------------------------------------------- 1 | struct TypeParams 2 | N::Int64 3 | L::Int64 4 | T::String 5 | P::String 6 | E::String 7 | end 8 | 9 | function TypeParams(pras_sys::SystemModel{N,L,T,P,E}) where {N,L,T,P,E} 10 | return TypeParams( 11 | N, 12 | L, 13 | unitsymbol(T), 14 | unitsymbol(P), 15 | unitsymbol(E), 16 | ) 17 | end 18 | 19 | struct EUEResult 20 | mean::Float64 21 | stderror::Float64 22 | end 23 | 24 | function EUEResult(shortfall::AbstractShortfallResult; region::Union{Nothing, String} = nothing) 25 | 26 | eue = (region === nothing) ? EUE(shortfall) : EUE(shortfall, region) 27 | return EUEResult( 28 | eue.eue.estimate, 29 | eue.eue.standarderror, 30 | ) 31 | end 32 | 33 | struct LOLEResult 34 | mean::Float64 35 | stderror::Float64 36 | end 37 | 38 | function LOLEResult(shortfall::AbstractShortfallResult; region::Union{Nothing, String} = nothing) 39 | 40 | lole = (region === nothing) ? LOLE(shortfall) : LOLE(shortfall, region) 41 | return LOLEResult( 42 | lole.lole.estimate, 43 | lole.lole.standarderror, 44 | ) 45 | end 46 | 47 | struct NEUEResult 48 | mean::Float64 49 | stderror::Float64 50 | end 51 | 52 | function NEUEResult(shortfall::AbstractShortfallResult; region::Union{Nothing, String} = nothing) 53 | 54 | neue = (region === nothing) ? NEUE(shortfall) : NEUE(shortfall, region) 55 | return NEUEResult( 56 | neue.neue.estimate, 57 | neue.neue.standarderror, 58 | ) 59 | end 60 | 61 | struct RegionResult 62 | name::String 63 | eue::EUEResult 64 | lole::LOLEResult 65 | neue::NEUEResult 66 | load::Vector{Int64} 67 | peak_load::Float64 68 | capacity::Dict{String,Vector{Int64}} 69 | shortfall_mean::Vector{Float64} 70 | shortfall_timestamps::Vector{ZonedDateTime} 71 | end 72 | 73 | struct SystemResult 74 | num_samples::Int64 75 | type_params::TypeParams 76 | timestamps::Vector{ZonedDateTime} 77 | eue::EUEResult 78 | lole::LOLEResult 79 | neue::NEUEResult 80 | region_results::Vector{RegionResult} 81 | end 82 | 83 | function get_shortfall_mean(shortfall::ShortfallResult) 84 | return shortfall.shortfall_mean 85 | end 86 | 87 | function get_shortfall_mean(shortfall::ShortfallSamplesResult) 88 | return mean(shortfall.shortfall, dims = 3) 89 | end 90 | 91 | function get_nsamples(shortfall::ShortfallResult) 92 | return shortfall.nsamples 93 | end 94 | 95 | function get_nsamples(shortfall::ShortfallSamplesResult) 96 | return size(shortfall.shortfall,3) 97 | end 98 | 99 | # Define structtypes for different structs defined above 100 | StructType(::Type{TypeParams}) = Struct() 101 | StructType(::Type{EUEResult}) = Struct() 102 | StructType(::Type{NEUEResult}) = Struct() 103 | StructType(::Type{LOLEResult}) = Struct() 104 | StructType(::Type{RegionResult}) = OrderedStruct() 105 | StructType(::Type{SystemResult}) = OrderedStruct() -------------------------------------------------------------------------------- /PRASFiles.jl/src/Results/write.jl: -------------------------------------------------------------------------------- 1 | function generate_systemresult(shortfall::AbstractShortfallResult, pras_sys::SystemModel) 2 | 3 | region_results = RegionResult[] 4 | shortfall_mean = get_shortfall_mean(shortfall) 5 | for (idx,reg_name) in enumerate(pras_sys.regions.names) 6 | region_gen_cats = unique(pras_sys.generators.categories[pras_sys.region_gen_idxs[idx]]) 7 | region_stor_cats = unique(pras_sys.storages.categories[pras_sys.region_stor_idxs[idx]]) 8 | append!(region_gen_cats,region_stor_cats) 9 | region_gen_cap = pras_sys.generators.capacity[pras_sys.region_gen_idxs[idx],:] 10 | region_stor_cap = pras_sys.storages.energy_capacity[pras_sys.region_stor_idxs[idx],:] 11 | 12 | installed_cap = Dict(region_gen_cats .=> [Vector{Int64}[] for i in range(1,length=length(region_gen_cats))]) 13 | for (gen_idx,gen_cat) in enumerate(pras_sys.generators.categories[pras_sys.region_gen_idxs[idx]]) 14 | push!(installed_cap[gen_cat],region_gen_cap[gen_idx,:]) 15 | end 16 | for (gen_idx,gen_cat) in enumerate(pras_sys.storages.categories[pras_sys.region_stor_idxs[idx]]) 17 | push!(installed_cap[gen_cat],region_stor_cap[gen_idx,:]) 18 | end 19 | 20 | capacity = Dict(map(=>, keys(installed_cap), sum.(values(installed_cap)))) 21 | peak_load = maximum(pras_sys.regions.load[idx,:]) 22 | reg_shortfall_mean = shortfall_mean[idx,:] 23 | shortfall_timestamps = collect(shortfall.timestamps)[findall(reg_shortfall_mean .!= 0.0)] 24 | 25 | push!(region_results, 26 | RegionResult( 27 | reg_name, 28 | EUEResult(shortfall, region = reg_name), 29 | LOLEResult(shortfall, region = reg_name), 30 | NEUEResult(shortfall, region = reg_name), 31 | pras_sys.regions.load[idx,:], 32 | peak_load, 33 | capacity, 34 | reg_shortfall_mean, 35 | shortfall_timestamps, 36 | ) 37 | ) 38 | 39 | end 40 | 41 | sys_result = SystemResult( 42 | get_nsamples(shortfall), 43 | TypeParams(pras_sys), 44 | collect(shortfall.timestamps), 45 | EUEResult(shortfall), 46 | LOLEResult(shortfall), 47 | NEUEResult(shortfall), 48 | region_results, 49 | ) 50 | 51 | return sys_result 52 | 53 | end 54 | 55 | """ 56 | saveshortfall( 57 | shortfall::AbstractShortfallResult, 58 | pras_sys::SystemModel, 59 | outfile::String, 60 | ) 61 | 62 | Save ShortfallResult or ShortfallSample Result in JSON format. Only aggregate system and region level results are exported. Sample level results are not exported.. 63 | 64 | # Arguments 65 | 66 | - `shortfall::AbstractShortfallResult`: ShortfallResult (or) ShortfallSamplesResult 67 | - `pras_sys::SystemModel`: PRAS SystemModel 68 | - `outfile::String`: Location to save the ShortfallResult 69 | 70 | # Returns 71 | 72 | - Location where ShortfallResult (or) ShortfallSamplesResult is exported in JSON format. 73 | """ 74 | function saveshortfall( 75 | shortfall::AbstractShortfallResult, 76 | pras_sys::SystemModel, 77 | outfile::String, 78 | ) 79 | 80 | dt_now = format(now(), "dd-u-yy-H-M-S") 81 | export_location = joinpath(outfile, dt_now) 82 | if ~(isdir(export_location)) 83 | mkpath(export_location) 84 | end 85 | 86 | sys_result = generate_systemresult(shortfall, pras_sys); 87 | open(joinpath(export_location, "pras_results.json"), "w") do io 88 | pretty(io, sys_result) 89 | end 90 | 91 | @info "Successfully exported PRAS ShortfallResult here: $(export_location)" 92 | return export_location 93 | end 94 | 95 | function saveshortfall( 96 | shortfall::R, 97 | pras_sys::SystemModel, 98 | outfile::String, 99 | ) where {R <: Result} 100 | 101 | error("saveshortfall is not implemented for $(typeof(shortfall))") 102 | end 103 | -------------------------------------------------------------------------------- /PRASFiles.jl/src/Systems/rts.pras: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NREL/PRAS/4e6194c88bc7baf56cbeb5305cc33d58e7f31e10/PRASFiles.jl/src/Systems/rts.pras -------------------------------------------------------------------------------- /PRASFiles.jl/src/Systems/toymodel.pras: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NREL/PRAS/4e6194c88bc7baf56cbeb5305cc33d58e7f31e10/PRASFiles.jl/src/Systems/toymodel.pras -------------------------------------------------------------------------------- /PRASFiles.jl/src/Systems/utils.jl: -------------------------------------------------------------------------------- 1 | function makeidxlist(collectionidxs::Vector{Int}, n_collections::Int) 2 | 3 | n_assets = length(collectionidxs) 4 | 5 | idxlist = Vector{UnitRange{Int}}(undef, n_collections) 6 | active_collection = 1 7 | start_idx = 1 8 | a = 1 9 | 10 | while a <= n_assets 11 | if collectionidxs[a] > active_collection 12 | idxlist[active_collection] = start_idx:(a-1) 13 | active_collection += 1 14 | start_idx = a 15 | else 16 | a += 1 17 | end 18 | end 19 | 20 | idxlist[active_collection] = start_idx:n_assets 21 | active_collection += 1 22 | 23 | while active_collection <= n_collections 24 | idxlist[active_collection] = (n_assets+1):n_assets 25 | active_collection += 1 26 | end 27 | 28 | return idxlist 29 | 30 | end 31 | 32 | function load_matrix(data::HDF5.Dataset, roworder::Vector{Int}, T::DataType) 33 | 34 | result = read(data) 35 | 36 | if roworder != 1:size(result, 1) 37 | @warn("HDF5 data is ordered differently from in-memory requirements. " * 38 | "Data will be reordered, but this may temporarily " * 39 | "consume large amounts of memory.") 40 | # TODO: More memory-efficient approaches are possible 41 | result = result[roworder, :] 42 | end 43 | 44 | if eltype(result) != T 45 | @warn("HDF5 data is typed differently from in-memory requirements. " * 46 | "Data conversion will be attempted, but this may temporarily " * 47 | "consume large amounts of memory.") 48 | result = T.(result) 49 | end 50 | 51 | return result 52 | 53 | end 54 | -------------------------------------------------------------------------------- /PRASFiles.jl/src/Systems/write.jl: -------------------------------------------------------------------------------- 1 | """ 2 | savemodel(sys::SystemModel, outfile::String) -> nothing 3 | 4 | Export a PRAS SystemModel `sys` as a .pras file, saved to `outfile` 5 | """ 6 | function savemodel( 7 | sys::SystemModel, outfile::String; 8 | string_length::Int=64, compression_level::Int=1, verbose::Bool=false, 9 | user_attributes::Union{Dict{String, String},Nothing}=nothing) 10 | 11 | verbose && 12 | @info "The PRAS system being exported is of type $(typeof(sys))" 13 | 14 | h5open(outfile, "w") do f::File 15 | 16 | verbose && @info "Processing metadata for .pras file ..." 17 | process_metadata!(f, sys; 18 | user_attributes=user_attributes) 19 | 20 | verbose && @info "Processing Regions for .pras file ..." 21 | process_regions!(f, sys, string_length, compression_level) 22 | 23 | if verbose 24 | @info "The PRAS System being exported is a " * 25 | (length(sys.regions) == 1 ? "single-node" : "zonal") * 26 | " model." 27 | end 28 | 29 | if length(sys.generators) > 0 30 | verbose && @info "Processing Generators for .pras file ..." 31 | process_generators!(f, sys, string_length, compression_level) 32 | end 33 | 34 | if length(sys.storages) > 0 35 | verbose && @info "Processing Storages for .pras file ..." 36 | process_storages!(f, sys, string_length, compression_level) 37 | end 38 | 39 | if length(sys.generatorstorages) > 0 40 | verbose && @info "Processing GeneratorStorages for .pras file ..." 41 | process_generatorstorages!(f, sys, string_length, compression_level) 42 | end 43 | 44 | if length(sys.regions) > 1 45 | verbose && @info "Processing Lines and Interfaces for .pras file ..." 46 | process_lines_interfaces!(f, sys, string_length, compression_level) 47 | end 48 | 49 | end 50 | 51 | verbose && @info "Successfully exported the PRAS SystemModel to " * 52 | ".pras file " * outfile 53 | 54 | return 55 | 56 | end 57 | 58 | function process_metadata!( 59 | f::File, sys::SystemModel{N,L,T,P,E}; 60 | user_attributes::Union{Dict{String, String},Nothing}=nothing) where {N,L,T,P,E} 61 | 62 | attrs = attributes(f) 63 | 64 | attrs["timestep_count"] = N 65 | attrs["timestep_length"] = L 66 | attrs["timestep_unit"] = unitsymbol(T) 67 | attrs["power_unit"] = unitsymbol(P) 68 | attrs["energy_unit"] = unitsymbol(E) 69 | 70 | attrs["start_timestamp"] = string(sys.timestamps.start); 71 | attrs["pras_dataversion"] = PRASFILE_VERSION 72 | 73 | if !isnothing(user_attributes) 74 | for (key, value) in user_attributes 75 | attrs[key] = value 76 | end 77 | end 78 | 79 | return 80 | 81 | end 82 | 83 | function process_regions!( 84 | f::File, sys::SystemModel, strlen::Int, compression::Int) 85 | 86 | n_regions = length(sys.regions.names) 87 | 88 | regions = create_group(f, "regions") 89 | regions_core = reshape(sys.regions.names, :, 1) 90 | regions_core_colnames = ["name"] 91 | 92 | string_table!(regions, "_core", regions_core_colnames, regions_core, strlen) 93 | 94 | regions["load", deflate = compression] = sys.regions.load 95 | 96 | return 97 | 98 | end 99 | 100 | function process_generators!( 101 | f::File, sys::SystemModel, strlen::Int, compression::Int) 102 | 103 | generators = create_group(f, "generators") 104 | 105 | gens_core = Matrix{String}(undef, length(sys.generators), 3) 106 | gens_core_colnames = ["name", "category", "region"] 107 | 108 | gens_core[:, 1] = sys.generators.names 109 | gens_core[:, 2] = sys.generators.categories 110 | gens_core[:, 3] = regionnames( 111 | length(sys.generators), sys.regions.names, sys.region_gen_idxs) 112 | 113 | string_table!(generators, "_core", gens_core_colnames, gens_core, strlen) 114 | 115 | generators["capacity", deflate = compression] = sys.generators.capacity 116 | 117 | generators["failureprobability", deflate = compression] = sys.generators.λ 118 | 119 | generators["repairprobability", deflate = compression] = sys.generators.μ 120 | 121 | return 122 | 123 | end 124 | 125 | function process_storages!( 126 | f::File, sys::SystemModel, strlen::Int, compression::Int) 127 | 128 | storages = create_group(f, "storages") 129 | 130 | stors_core = Matrix{String}(undef, length(sys.storages), 3) 131 | stors_core_colnames = ["name", "category", "region"] 132 | 133 | stors_core[:, 1] = sys.storages.names 134 | stors_core[:, 2] = sys.storages.categories 135 | stors_core[:, 3] = regionnames( 136 | length(sys.storages), sys.regions.names, sys.region_stor_idxs) 137 | 138 | string_table!(storages, "_core", stors_core_colnames, stors_core, strlen) 139 | 140 | storages["chargecapacity", deflate = compression] = 141 | sys.storages.charge_capacity 142 | 143 | storages["dischargecapacity", deflate = compression] = 144 | sys.storages.discharge_capacity 145 | 146 | storages["energycapacity", deflate = compression] = 147 | sys.storages.energy_capacity 148 | 149 | storages["chargeefficiency", deflate = compression] = 150 | sys.storages.charge_efficiency 151 | 152 | storages["dischargeefficiency", deflate = compression] = 153 | sys.storages.discharge_efficiency 154 | 155 | storages["carryoverefficiency", deflate = compression] = 156 | sys.storages.carryover_efficiency 157 | 158 | storages["failureprobability", deflate = compression] = sys.storages.λ 159 | 160 | storages["repairprobability", deflate = compression] = sys.storages.μ 161 | 162 | return 163 | 164 | end 165 | 166 | function process_generatorstorages!( 167 | f::File, sys::SystemModel, strlen::Int, compression::Int) 168 | 169 | generatorstorages = create_group(f, "generatorstorages") 170 | 171 | genstors_core = Matrix{String}(undef, length(sys.generatorstorages), 3) 172 | genstors_core_colnames = ["name", "category", "region"] 173 | 174 | genstors_core[:, 1] = sys.generatorstorages.names 175 | genstors_core[:, 2] = sys.generatorstorages.categories 176 | genstors_core[:, 3] = regionnames( 177 | length(sys.generatorstorages), sys.regions.names, sys.region_genstor_idxs) 178 | 179 | string_table!(generatorstorages, "_core", 180 | genstors_core_colnames, genstors_core, strlen) 181 | 182 | generatorstorages["inflow", deflate = compression] = 183 | sys.generatorstorages.inflow 184 | 185 | generatorstorages["gridwithdrawalcapacity", deflate = compression] = 186 | sys.generatorstorages.gridwithdrawal_capacity 187 | 188 | generatorstorages["gridinjectioncapacity", deflate = compression] = 189 | sys.generatorstorages.gridinjection_capacity 190 | 191 | generatorstorages["chargecapacity", deflate = compression] = 192 | sys.generatorstorages.charge_capacity 193 | 194 | generatorstorages["dischargecapacity", deflate = compression] = 195 | sys.generatorstorages.discharge_capacity 196 | 197 | generatorstorages["energycapacity", deflate = compression] = 198 | sys.generatorstorages.energy_capacity 199 | 200 | generatorstorages["chargeefficiency", deflate = compression] = 201 | sys.generatorstorages.charge_efficiency 202 | 203 | generatorstorages["dischargeefficiency", deflate = compression] = 204 | sys.generatorstorages.discharge_efficiency 205 | 206 | generatorstorages["carryoverefficiency", deflate = compression] = 207 | sys.generatorstorages.carryover_efficiency 208 | 209 | generatorstorages["failureprobability", deflate = compression] = 210 | sys.generatorstorages.λ 211 | 212 | generatorstorages["repairprobability", deflate = compression] = 213 | sys.generatorstorages.μ 214 | 215 | return 216 | 217 | end 218 | 219 | function process_lines_interfaces!( 220 | f::File, sys::SystemModel, strlen::Int, compression::Int) 221 | 222 | lines = create_group(f, "lines") 223 | 224 | lines_core = Matrix{String}(undef, length(sys.lines), 4) 225 | lines_core_colnames = ["name", "category", "region_from", "region_to"] 226 | 227 | lines_core[:, 1] = sys.lines.names 228 | lines_core[:, 2] = sys.lines.categories 229 | for (lines, r_from, r_to) in zip(sys.interface_line_idxs, 230 | sys.interfaces.regions_from, 231 | sys.interfaces.regions_to) 232 | lines_core[lines, 3] .= sys.regions.names[r_from] 233 | lines_core[lines, 4] .= sys.regions.names[r_to] 234 | end 235 | 236 | string_table!(lines, "_core", lines_core_colnames, lines_core, strlen) 237 | 238 | lines["forwardcapacity", deflate = compression] = 239 | sys.lines.forward_capacity 240 | 241 | lines["backwardcapacity", deflate = compression] = 242 | sys.lines.backward_capacity 243 | 244 | lines["failureprobability", deflate = compression] = sys.lines.λ 245 | 246 | lines["repairprobability", deflate = compression] = sys.lines.μ 247 | 248 | 249 | interfaces = create_group(f, "interfaces") 250 | 251 | ints_core = Matrix{String}(undef, length(sys.interfaces), 2) 252 | ints_core_colnames = ["region_from", "region_to"] 253 | 254 | ints_core[:, 1] = 255 | getindex.(Ref(sys.regions.names), sys.interfaces.regions_from) 256 | ints_core[:, 2] = 257 | getindex.(Ref(sys.regions.names), sys.interfaces.regions_to) 258 | string_table!(interfaces, "_core", ints_core_colnames, ints_core, strlen) 259 | 260 | interfaces["forwardcapacity", deflate = compression] = 261 | sys.interfaces.limit_forward 262 | 263 | interfaces["backwardcapacity", deflate = compression] = 264 | sys.interfaces.limit_backward 265 | 266 | return 267 | 268 | end 269 | 270 | function regionnames( 271 | n_units::Int, regions::Vector{String}, unit_idxs::Vector{UnitRange{Int}}) 272 | 273 | result = Vector{String}(undef, n_units) 274 | for (r, units) in enumerate(unit_idxs) 275 | result[units] .= regions[r] 276 | end 277 | 278 | return result 279 | 280 | end 281 | 282 | function string_table!( 283 | f::Group, tablename::String, colnames::Vector{String}, 284 | data::Matrix{String}, strlen::Int 285 | ) 286 | 287 | nrows, ncols = size(data) 288 | 289 | length(colnames) == ncols || 290 | error("Number of column names does not match provided data") 291 | 292 | allunique(colnames) || 293 | error("All column names must be unique") 294 | 295 | all(x -> x <= strlen, length.(data)) || 296 | error("Input data exceeds the specified HDF5 string length") 297 | 298 | stringtype_id = h5t_copy(hdf5_type_id(String)) 299 | h5t_set_size(stringtype_id, strlen) 300 | stringtype = Datatype(stringtype_id) 301 | 302 | dt_id = h5t_create(H5T_COMPOUND, ncols * strlen) 303 | for (i, colname) in enumerate(colnames) 304 | h5t_insert(dt_id, colname, (i-1)*strlen, stringtype) 305 | end 306 | 307 | rawdata = UInt8.(vcat(vec(convertstring.(permutedims(data), strlen))...)) 308 | 309 | dset = create_dataset(f, tablename, Datatype(dt_id), 310 | dataspace((nrows,))) 311 | h5d_write( 312 | dset, dt_id, H5S_ALL, H5S_ALL, H5P_DEFAULT, rawdata) 313 | 314 | end 315 | 316 | function convertstring(s::AbstractString, strlen::Int) 317 | 318 | oldstring = ascii(s) 319 | newstring = fill('\0', strlen) 320 | 321 | for i in 1:min(strlen, length(s)) 322 | newstring[i] = oldstring[i] 323 | end 324 | 325 | return newstring 326 | 327 | end 328 | -------------------------------------------------------------------------------- /PRASFiles.jl/test/runtests.jl: -------------------------------------------------------------------------------- 1 | using PRASCore 2 | using PRASFiles 3 | using Test 4 | using JSON3 5 | 6 | @testset "PRASFiles" begin 7 | 8 | @testset "Roundtrip .pras files to/from disk" begin 9 | 10 | # TODO: Verify systems accurately depicted? 11 | path = dirname(@__FILE__) 12 | 13 | toy = PRASFiles.toymodel() 14 | savemodel(toy, path * "/toymodel2.pras") 15 | toy2 = SystemModel(path * "/toymodel2.pras") 16 | @test toy == toy2 17 | 18 | rts = PRASFiles.rts_gmlc() 19 | savemodel(rts, path * "/rts2.pras") 20 | rts2 = SystemModel(path * "/rts2.pras") 21 | @test rts == rts2 22 | 23 | savemodel(rts,path * "rts_userattrs.pras", 24 | user_attributes=Dict("about"=>"this is a representation of the RTS GMLC system")) 25 | user_attrs = PRASFiles.read_addl_attrs(path * "rts_userattrs.pras") 26 | @test user_attrs == Dict("about"=>"this is a representation of the RTS GMLC system") 27 | 28 | end 29 | 30 | @testset "Run RTS-GMLC" begin 31 | 32 | assess(PRASFiles.rts_gmlc(), SequentialMonteCarlo(samples=100), Shortfall()) 33 | 34 | end 35 | 36 | @testset "Save Aggregate Results" begin 37 | rts_sys = PRASFiles.rts_gmlc() 38 | # Make load in all regions in rts_sys 10 times the original load for meaningful results 39 | for i in 1:length(rts_sys.regions.names) 40 | rts_sys.regions.load[i, :] = 10 * rts_sys.regions.load[i, :] 41 | end 42 | results = assess(rts_sys, SequentialMonteCarlo(samples=10, threaded = false, seed = 1), Shortfall(), ShortfallSamples(), Surplus()); 43 | shortfall = results[1]; 44 | path = joinpath(dirname(@__FILE__),"PRAS_Results_Export"); 45 | exp_location_1 = PRASFiles.saveshortfall(shortfall, rts_sys, path); 46 | @test isfile(joinpath(exp_location_1, "pras_results.json")) 47 | exp_results_1 = JSON3.read(joinpath(exp_location_1, "pras_results.json"), PRASFiles.SystemResult) 48 | @test exp_results_1.lole.mean == PRASCore.LOLE(shortfall).lole.estimate 49 | @test exp_results_1.eue.mean == PRASCore.EUE(shortfall).eue.estimate 50 | @test exp_results_1.neue.mean == PRASCore.NEUE(shortfall).neue.estimate 51 | @test exp_results_1.region_results[1].lole.mean == PRASCore.LOLE(shortfall, exp_results_1.region_results[1].name).lole.estimate 52 | @test exp_results_1.region_results[1].eue.mean == PRASCore.EUE(shortfall, exp_results_1.region_results[1].name).eue.estimate 53 | @test exp_results_1.region_results[1].neue.mean == PRASCore.NEUE(shortfall, exp_results_1.region_results[1].name).neue.estimate 54 | 55 | shortfall_samples = results[2]; 56 | exp_location_2 = PRASFiles.saveshortfall(shortfall_samples, rts_sys, path); 57 | @test isfile(joinpath(exp_location_2, "pras_results.json")) 58 | exp_results_2 = JSON3.read(joinpath(exp_location_2, "pras_results.json"), PRASFiles.SystemResult) 59 | @test exp_results_2.lole.mean == PRASCore.LOLE(shortfall_samples).lole.estimate 60 | @test exp_results_2.eue.mean == PRASCore.EUE(shortfall_samples).eue.estimate 61 | @test exp_results_2.neue.mean == PRASCore.NEUE(shortfall_samples).neue.estimate 62 | @test exp_results_2.region_results[1].lole.mean == PRASCore.LOLE(shortfall_samples, exp_results_2.region_results[1].name).lole.estimate 63 | @test exp_results_2.region_results[1].eue.mean == PRASCore.EUE(shortfall_samples, exp_results_2.region_results[1].name).eue.estimate 64 | @test exp_results_2.region_results[1].neue.mean == PRASCore.NEUE(shortfall_samples, exp_results_2.region_results[1].name).neue.estimate 65 | 66 | @test exp_results_1.lole.mean ≈ exp_results_2.lole.mean 67 | @test exp_results_1.eue.mean ≈ exp_results_2.eue.mean 68 | @test exp_results_1.neue.mean ≈ exp_results_2.neue.mean 69 | @test exp_results_1.region_results[1].lole.mean ≈ exp_results_2.region_results[1].lole.mean 70 | @test exp_results_1.region_results[1].eue.mean ≈ exp_results_2.region_results[1].eue.mean 71 | @test exp_results_1.region_results[1].neue.mean ≈ exp_results_2.region_results[1].neue.mean 72 | 73 | surplus = results[3] 74 | @test_throws "saveshortfall is not implemented for" PRASFiles.saveshortfall(surplus, rts_sys, path) 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Probabilistic Resource Adequacy Suite 2 | 3 | [![PRAS.jl Tests](https://github.com/NREL/PRAS/actions/workflows/PRAS.jl.yml/badge.svg?branch=main)](https://github.com/NREL/PRAS/actions/workflows/PRAS.jl.yml) 4 | [![PRASCore.jl Tests](https://github.com/NREL/PRAS/actions/workflows/PRASCore.jl.yml/badge.svg?branch=main)](https://github.com/NREL/PRAS/actions/workflows/PRASCore.jl.yml) 5 | [![PRASFiles.jl Tests](https://github.com/NREL/PRAS/actions/workflows/PRASFiles.jl.yml/badge.svg?branch=main)](https://github.com/NREL/PRAS/actions/workflows/PRASFiles.jl.yml) 6 | [![PRASCapacityCredits.jl Tests](https://github.com/NREL/PRAS/actions/workflows/PRASCapacityCredits.jl.yml/badge.svg?branch=main)](https://github.com/NREL/PRAS/actions/workflows/PRASCapacityCredits.jl.yml) 7 | 8 | [![codecov](https://codecov.io/gh/NREL/PRAS/branch/master/graph/badge.svg?token=WiP3quRaIA)](https://codecov.io/gh/NREL/PRAS) 9 | [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://nrel.github.io/PRAS) 10 | [![DOI](https://img.shields.io/badge/DOI-10.11578/dc.20190814.1-blue.svg)](https://www.osti.gov/biblio/1557438) 11 | 12 | The Probabilistic Resource Adequacy Suite (PRAS) is a collection of tools for 13 | bulk power system resource adequacy analysis and capacity credit calculation. 14 | The most recent documentation report (for version 0.6) is available 15 | [here](https://www.nrel.gov/docs/fy21osti/79698.pdf). 16 | 17 | # Installation 18 | 19 | PRAS is written in the [Julia](https://julialang.org/) numerical programming 20 | language. If you haven't already, your first step should be to install Julia. 21 | Instructions are available at 22 | [julialang.org/downloads](https://julialang.org/downloads/). 23 | 24 | Once you have Julia installed, PRAS can be installed from the Julia [General registry](https://pkgdocs.julialang.org/v1/registries/) which is installed by default if you have no other registries installed. 25 | 26 | From the main Julia prompt, type `]` to enter the package management REPL. 27 | The prompt should change from `julia>` to something like `(v1.10) pkg>` 28 | (your version number may be slightly different). 29 | Type (or paste) the following (minus the `pkg>` prompt) 30 | ``` 31 | pkg> add PRAS 32 | ``` 33 | 34 | This will automatically install the PRAS Julia module and all of its 35 | related dependencies. At this point you can hit Backspace to switch back to the 36 | main `julia>` prompt. 37 | 38 | # Basic usage 39 | 40 | With PRAS installed, you can load it into Julia as follows: 41 | 42 | ```Julia 43 | using PRAS 44 | ``` 45 | 46 | This will make the core PRAS functions (most importantly, the `SystemModel` 47 | and `assess` functions) available for use in your Julia script or 48 | interactive REPL session. 49 | 50 | The following snippet shows expected unserved energy (EUE) assessment for the [RTS-GMLC](https://github.com/GridMod/RTS-GMLC) system, which is packaged with PRAS. 51 | 52 | ```Julia 53 | rts_gmlc_sys = PRAS.rts_gmlc(); 54 | shortfall, = assess(rts_gmlc_sys, 55 | SequentialMonteCarlo(samples=10,seed=1), 56 | Shortfall() 57 | ); 58 | println("Total system $(EUE(shortfall))") 59 | # Total system EUE = 0.00000 MWh/8784h 60 | ``` 61 | 62 | The [Getting Started](docs/getting-started.md) document provides more information 63 | on using PRAS. -------------------------------------------------------------------------------- /docs/_config.yaml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | title: PRAS 3 | description: The Probabilistic Resource Adequacy Suite 4 | show_downloads: false 5 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with PRAS 2 | 3 | For a complete overview of PRAS, see the 4 | [v0.6 documentation report](https://www.nrel.gov/docs/fy21osti/79698.pdf). 5 | This page provides a briefer overview to help you start running PRAS quickly. 6 | 7 | ## Parallel Processing 8 | 9 | PRAS uses multi-threading, so be sure to set the environment variable 10 | controlling the number of threads available to Julia (36 in this Bash example, 11 | which is a good choice for NREL Eagle nodes - on a laptop you would probably 12 | only want 4 or so) before running scripts or launching the REPL: 13 | 14 | ```sh 15 | export JULIA_NUM_THREADS=36 16 | ``` 17 | 18 | ## Power System Data 19 | 20 | The recommended way to store and retreive PRAS system data is in an HDF5 file 21 | that conforms to the 22 | [PRAS system data format](https://github.com/NREL/PRAS/blob/master/SystemModel_HDF5_spec.md). 23 | Once your system is represented in that format you can load it into PRAS with: 24 | 25 | ```julia 26 | using PRAS 27 | sys = SystemModel("filepath/to/systemdata.pras") 28 | ``` 29 | 30 | ## Resource Adequacy Assessment 31 | 32 | PRAS functionality is distributed across groups of 33 | modular specifications that can be mixed, extended, or replaced to support the 34 | needs of a particular analysis. When assessing reliability or capacity value, 35 | one can define the specs to be used while passing along any associated 36 | parameters or options. 37 | 38 | The categories of specifications are: 39 | 40 | **Simulation Specifications**: How should power system operations be simulated? 41 | Options are `Convolution` and `SequentialMonteCarlo`. 42 | 43 | **Result Specifications**: What kind and level of detail of results should be 44 | saved out during simulations? 45 | Options include `Shortfall`, `Surplus`, interface `Flow`, `StorageEnergy`, 46 | and `GeneratorStorageEnergy`. Not all simulation specifications support all 47 | result specifications. 48 | (See the [PRAS documentation](https://www.nrel.gov/docs/fy21osti/79698.pdf) 49 | for more details.) 50 | 51 | **Capacity Credit Specifications**: If performing a capacity credit 52 | calculation, which method should be used? 53 | Options are `EFC` and `ELCC`. 54 | 55 | ### Running an analysis 56 | 57 | Analysis centers around the `assess` method with different arguments passed 58 | depending on the desired analysis to run. 59 | For example, to run a copper-plate convolution-based reliability assessment 60 | (`Convolution`) with LOLE and EUE reporting (`Shortfall`), 61 | one would run: 62 | 63 | ```julia 64 | assess(mysystemmodel, Convolution(), Shortfall()) 65 | ``` 66 | 67 | If you instead want to run a simulation that considers energy-limited resources 68 | and transmission constraints, using 100,000 Monte Carlo samples, 69 | the method call becomes: 70 | 71 | ```julia 72 | assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall()) 73 | ``` 74 | 75 | You can request multiple kinds of result from a single assessment: 76 | 77 | ```julia 78 | assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall(), Flow()) 79 | ``` 80 | 81 | ### Querying Results 82 | 83 | Each result type requested returns a seperate result object. These objects can 84 | then be queried using indexing syntax to extract values of interest. 85 | 86 | ```julia 87 | shortfalls, flows = 88 | assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall(), Flow()) 89 | 90 | flows["Region A" => "Region B"] 91 | ``` 92 | 93 | The full PRAS documentation provides more details on how different result 94 | object types can be indexed. 95 | 96 | Because the main results of interest from resource adequacy assessments are 97 | probabilistic risk metrics (i.e. EUE and LOLE), `Shortfall` result objects 98 | define some additional methods: a particular risk metric can be 99 | obtained by calling that metric's constructor with the `Shortfall` 100 | result object. For example, to obtain the system-wide LOLE and EUE over the 101 | simulation period: 102 | 103 | ```julia 104 | lole = LOLE(shortfalls) 105 | eue = EUE(shortfalls) 106 | ``` 107 | 108 | Single-period metrics can also be extracted. For example, to get system-wide 109 | EUE for April 27th, 2024 at 1pm EST: 110 | 111 | ```julia 112 | period_eue = EUE(shortfalls, ZonedDateTime(2024, 4, 27, 13, tz"EST")) 113 | ``` 114 | 115 | Region-specific metrics can also be extracted. For example, to obtain the LOLE 116 | of Region A across the entire simulation period: 117 | 118 | ```julia 119 | eue_a = LOLE(shortfalls, "Region A") 120 | ``` 121 | 122 | Finally, metrics can be obtained for both a specific region and time: 123 | 124 | ```julia 125 | period_eue_a = EUE(shortfalls, "Region A", ZonedDateTime(2024, 4, 27, 13, tz"EST")) 126 | ``` 127 | 128 | ## Capacity Credit Calculations 129 | 130 | Capacity credit calcuations build on probabilistic resource adequacy assessment 131 | to provided capacity-based quantifications of the marginal benefit to 132 | system resource adequacy associated with a specific resource or collection of 133 | resources. Two capacity credit metrics (EFC and ELCC) are currently supported. 134 | 135 | ### Equivalent Firm Capacity (EFC) 136 | 137 | `EFC` estimates the amount of idealized, 100%-available capacity that, when 138 | added to a baseline system, reproduces the level of system adequacy associated 139 | with the baseline system plus the study resource. The following parameters must 140 | be specified: 141 | 142 | - The risk metric to be used for comparison (i.e. EUE or LOLE) 143 | - A known upper bound on the EFC value (usually the resource's nameplate 144 | capacity) 145 | - The regional distribution of the firm capacity to be added. This is 146 | typically chosen to match the regional distribution of the study resource's 147 | nameplate capacity. 148 | 149 | For example, to assess the EUE-based EFC of a new resource with 1000 MW 150 | nameplate capacity, added to the system in a single region named "A": 151 | 152 | ```julia 153 | # The base system, with power units in MW 154 | base_system 155 | 156 | # The base system augmented with some incremental resource of interest 157 | augmented_system 158 | 159 | # Get the lower and upper bounds on the EFC estimate for the resource 160 | efc = assess( 161 | base_system, augmented_system, EFC{EUE}(1000, "A"), 162 | SequentialMonteCarlo(samples=100_000)) 163 | min_efc, max_efc = extrema(efc) 164 | ``` 165 | 166 | If the study resource were instead split between regions "A" (600MW) and "B" 167 | (400 MW), one could specify the firm capacity distribution as: 168 | 169 | ```julia 170 | efc = assess( 171 | base_system, augmented_system, EFC{EUE}(1000, ["A"=>0.6, "B"=>0.4]), 172 | SequentialMonteCarlo(samples=100_000)) 173 | ``` 174 | 175 | ### Equivalent Load Carrying Capability (ELCC) 176 | 177 | `ELCC` estimates the amount of additional load that can be added to the system 178 | (in every time period) in the presence of a new study resource, while 179 | maintaining the baseline system's original adequacy level. The following 180 | parameters must be specified: 181 | 182 | - The risk metric to be used for comparison (i.e. EUE or LOLE) 183 | - A known upper bound on the ELCC value (usually the resource's nameplate 184 | capacity) 185 | - The regional distribution of the load to be added. Note that this choice is 186 | somewhat ambiguous in multi-region systems, so assumptions should be clearly 187 | specified when reporting analysis results. 188 | 189 | For example, to assess the EUE-based ELCC of a new resource with 1000 MW 190 | nameplate capacity, serving new load in region "A": 191 | 192 | ```julia 193 | # The base system, with power units in MW 194 | base_system 195 | 196 | # The base system augmented with some incremental resource of interest 197 | augmented_system 198 | 199 | # Get the lower and upper bounds on the ELCC estimate for the resource 200 | elcc = assess( 201 | base_system, augmented_system, ELCC{EUE}(1000, "A"), 202 | SequentialMonteCarlo(samples=100_000)) 203 | min_elcc, max_elcc = extrema(elcc) 204 | ``` 205 | 206 | If instead the goal was to study the ability of the new resource to provide 207 | load evenly to regions "A" and "B", one could use: 208 | 209 | ```julia 210 | elcc = assess( 211 | base_system, augmented_system, ELCC{EUE}(1000, ["A"=>0.5, "B"=>0.5]), 212 | SequentialMonteCarlo(samples=100_000)) 213 | ``` 214 | 215 | ### Capacity credit calculations in the presence of sampling error 216 | 217 | For non-deterministic assessment methods (i.e. Monte Carlo simulations), 218 | running a resource adequacy assessment with different random number generation 219 | seeds will result in different risk metric estimates for the same underlying 220 | system. Capacity credit assessments can be sensitive to this uncertainty, 221 | particularly when attempting to study the impact of a small resource on a 222 | large system with a limited number of simulation samples. 223 | 224 | PRAS takes steps to a) limit this uncertainty and b) warn against 225 | potential deficiencies in statistical power resulting from this uncertainty. 226 | 227 | First, the same random seed is used across all simulations in the capacity 228 | credit assessment process. If the number of resources and their reliability 229 | parameters (MTTF and MTTR) remain constant across the baseline and augmented 230 | test systems, seed re-use ensures that unit-level outage profiles remain 231 | identical across RA assessments, providing a fixed background against which to 232 | measure changes in RA resulting from the addition of the study resource. Note 233 | that satisfying this condition requires that the study resource be present in 234 | the baseline case, but with its contributions eliminated (e.g. by setting its 235 | capacity to zero). When implementing an assessment method that modifies the 236 | user-provided system to add new resources (such as EFC), the programmer should 237 | assume this invariance exists in the provided data, and not violate it in any 238 | automated modifications. 239 | 240 | Second, capacity credit assessments have two different stopping criteria. The 241 | ideal case is that the upper and lower bounds on the capacity 242 | credit metric converge to be sufficiently tight relative to a desired level 243 | of precision. This target precision is 1 system power unit (e.g. MW) by 244 | default, but can be relaxed to loosen the convergence bounds if desired via 245 | the `capacity_gap` keyword argument. Once the lower and upper bounds are 246 | tighter than this gap, their values are returned. 247 | 248 | Additionally, at each bisection step, a hypothesis test is performed to ensure 249 | that the theoretically-larger bounding risk metric is in fact larger than the 250 | smaller-valued risk metric with a specified level of statistical significance. 251 | By default, this criteria is a maximum p-value of 0.05, although this value 252 | can be changed as desired via the `p_value` keyword argument. If at some point 253 | the null hypothesis (the higher risk is not in fact larger than the lower 254 | risk) cannot be rejected at the desired significance level, the assessment 255 | will provide a warning indicating the size of the remaining capacity gap and 256 | return the lower and upper bounds on the capacity credit estimate. 257 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | The Probabilistic Resource Adequacy Suite (PRAS) provides an open-source, 2 | research-oriented collection of tools for analysing the resource adequacy of a 3 | bulk power system. The simulation methods offered support everything from 4 | classical convolution-based analytical techniques through to high-performance 5 | sequential Monte Carlo methods supporting multi-region composite reliability 6 | assessment, including simulation of energy-limited resources such as storage. 7 | 8 | PRAS is developed and maintained at the US 9 | [National Renewable Energy Laboratory](https://www.nrel.gov/) (NREL). 10 | 11 | For help installing PRAS, see the [instructions in the PRAS GitHub page](https://github.com/NREL/PRAS). To get started using PRAS, see the [Getting Started](./getting-started) page. 12 | --------------------------------------------------------------------------------