diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..a2cfc98c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/devcontainers/base:ubuntu + +# Install NREL root certs. +# Uncomment the following lines (5-7) if you're using devcontainers on a NREL machine +# RUN curl -fsSLk -o /usr/local/share/ca-certificates/nrel_root.crt https://raw.github.nrel.gov/TADA/nrel-certs/v20180329/certs/nrel_root.pem && \ +# curl -fsSLk -o /usr/local/share/ca-certificates/nrel_xca1.crt https://raw.github.nrel.gov/TADA/nrel-certs/v20180329/certs/nrel_xca1.pem && \ +# update-ca-certificates \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..cd0f193c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/JuliaLang/devcontainer-templates/tree/main/src/julia +{ + "build": { "dockerfile": "Dockerfile" }, + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + // A Feature to install Julia via juliaup. More info: https://github.com/JuliaLang/devcontainer-features/tree/main/src/julia. + "ghcr.io/julialang/devcontainer-features/julia:1": { + + }, + "ghcr.io/devcontainers/features/git:1": { + "ppa": true, + "version": "latest" + }, + "ghcr.io/devcontainers/features/git-lfs:1": { + "autoPull": true, + "version": "latest" + } + }, + + "postCreateCommand": "julia --project=PRAS.jl -e 'import Pkg; Pkg.develop([(path=\"PRASCore.jl\",),(path=\"PRASFiles.jl\",),(path=\"PRASCapacityCredits.jl\",)])'", + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..e94c4723 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: Documentation + +on: + push: + branches: + - main + tags: '*' + pull_request: + +jobs: + docs: + name: Documentation + runs-on: macOS-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + - uses: julia-actions/setup-julia@latest + with: + version: '1' + - name: Configure doc environment + run: | + julia --project=docs/ -e ' + println(pwd()); + using Pkg + # Develop all packages in workspace + Pkg.develop([ + (path="PRASCore.jl",), + (path="PRASFiles.jl",), + (path="PRASCapacityCredits.jl",), + (path="PRAS.jl",) + ]) + Pkg.instantiate()' + - name: Build and deploy docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: julia --project=docs/ --threads auto docs/make.jl deploy=true diff --git a/.github/workflows/docsCleanup.yml b/.github/workflows/docsCleanup.yml new file mode 100644 index 00000000..7a91c13b --- /dev/null +++ b/.github/workflows/docsCleanup.yml @@ -0,0 +1,33 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +# Ensure that only one "Doc Preview Cleanup" workflow is force pushing at a time +concurrency: + group: doc-preview-cleanup + cancel-in-progress: false + +jobs: + doc-preview-cleanup: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + - name: Delete preview and history + push changes + run: | + if [ -d "${preview_dir}" ]; then + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git rm -rf "${preview_dir}" + git commit -m "delete preview" + git branch gh-pages-new "$(echo "delete history" | git commit-tree "HEAD^{tree}")" + git push --force origin gh-pages-new:gh-pages + fi + env: + preview_dir: previews/PR${{ github.event.number }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1fe04f98..c725c05e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ Manifest.toml *.DS_Store *.pras -*.json +PRASFiles.jl/test/PRAS_Results_Export/ + +docs/build/ +docs/site/ +docs/src/examples/ \ No newline at end of file diff --git a/PRAS.jl/examples/pras_walkthrough.jl b/PRAS.jl/examples/pras_walkthrough.jl new file mode 100644 index 00000000..359de2ce --- /dev/null +++ b/PRAS.jl/examples/pras_walkthrough.jl @@ -0,0 +1,178 @@ +# # [PRAS walkthrough](@id pras_walkthrough) + +# This is a complete example of running a PRAS assessment, +# using the [RTS-GMLC](https://github.com/GridMod/RTS-GMLC) system + +# Load the PRAS package and other tools necessary for analyses +using PRAS +using Plots +using DataFrames +using Printf + +# ## [Read and explore a SystemModel](@id explore_systemmodel) + +# You can load in a system model from a [.pras file](@ref prasfile) if you have one like so: +# ```julia +# sys = SystemModel("mysystem.pras") +# ``` + +# For the purposes of this example, we'll just use the built-in RTS-GMLC model. +sys = PRAS.rts_gmlc(); + +# We see some information about the system by just typing its name +# (or rather the variable that holds it): +sys + +# We retrieve the parameters of the system using the `get_params` +# function and use this for the plots below to ensure we have +# correct units: +(timesteps,periodlen,periodunit,powerunit,energyunit) = get_params(rts_gmlc()) + +# This system has 3 regions, with multiple Generators, one GenerationStorage in +# region "2" and one Storage in region "3". We can see regional information by +# indexing the system with the region name: +sys["2"] + +# We can visualize a time series of the regional load in region "2": +region_2_load = sys.regions.load[sys["2"].index,:] +plot(sys.timestamps, region_2_load, + xlabel="Time", ylabel="Region 2 load ($(powerunit))", + legend=false) + +# We can find more information about all the Generators in the system by +# retriving the `generators` in the SystemModel: +system_generators = sys.generators + +# This returns an object of the asset type [Generators](@ref PRASCore.Systems.Generators) +# and we can retrieve capacities of all generators in the system, which returns +# a Matrix with the shape (number of generators) x (number of timepoints): +system_generators.capacity + +# We can visualize a time series of the total system capacity +# (sum over individual generators' capacity at each time step) +plot(sys.timestamps, sum(system_generators.capacity, dims=1)', + xlabel="Time", ylabel="Total system capacity (MW)", legend=false) + +# Or, by category of generators: +category_indices = Dict([cat => findall(==(cat), system_generators.categories) + for cat in unique(system_generators.categories)]); +capacity_matrix = Vector{Vector{Int}}(); +for (category,indices) in category_indices + push!(capacity_matrix, sum(system_generators.capacity[indices, :], dims=1)[1,:]) +end +areaplot(sys.timestamps, hcat(capacity_matrix...), + label=permutedims(collect(keys(category_indices))), + xlabel="Time", ylabel="Total system capacity (MW)") + +# Similarly we can also retrieve all the Storages in the system and +# GenerationStorages in the system using `sys.storages` and `sys.generatorstorages`, +# respectively. + +# To retrieve the assets in a particular region, we can index by the region name +# and asset type (`Generators` here): +region_2_generators = sys["2", Generators] + +# We get the storage device in region "3" like so: +region_3_storage = sys["3", Storages] +# and the generation-storage device in region "2" like so: +region_2_genstorage = sys["2", GeneratorStorages] + +# ## Run a Sequential Monte Carlo simulation + +# We can run a Sequential Monte Carlo simulation on this system using the +# [assess](@ref PRASCore.Simulations.assess) function. +# Here we will also use four different [result specifications](@ref results): +shortfall, surplus, utilization, storage = assess( + sys, SequentialMonteCarlo(samples=100, seed=1), + Shortfall(), Surplus(), Utilization(), StorageEnergy()); + +# Start by checking the overall system adequacy: +lole = LOLE(shortfall); # event-hours per year +eue = EUE(shortfall); # unserved energy per year +println("System $(lole), $(eue)") + +# Given we use only 100 samples and the RTS-GMLC system is quite reliable, +# we see a system which is reliable, with LOLE and EUE both near zero. +# For the purposes of this example, let's increase the system load homogenously +# by 700MW in every hour and region, and re-run the assessment. + +sys.regions.load .+= 700.0 +shortfall, surplus, utilization, storage = assess( + sys, SequentialMonteCarlo(samples=100, seed=1), + Shortfall(), Surplus(), Utilization(), StorageEnergy()); +lole = LOLE(shortfall); # event-hours per year +eue = EUE(shortfall); # unserved energy per year +neue = NEUE(shortfall); # unserved energy per year +println("System $(lole), $(eue), $(neue)") + +# Now we see a system which is slightly unreliable with a normalized +# expected unserved energy (NEUE) of close to 470 parts per million of total load. + +# We can now look at the hourly loss-of-load expectation (LOLE) to see when +# when shortfalls are occurring. +# `LOLE.(shortfall, many_hours)` is Julia shorthand for calling LOLE +# on every timestep in the collection many_hours +lolps = LOLE.(shortfall, sys.timestamps) + +# Here results are in terms of event-hours per hour, which is equivalent +# to the loss-of-load probability (LOLP) for each hour. The LOLE object is +# shown as mean ± standard error. We are mostly interested in the mean here, +# we can retrieve this using `val.(lolps)` and visualize this: +plot(sys.timestamps, val.(lolps), + xlabel="Time", ylabel="Hourly LOLE (event-hours/hour)", legend=false) + +# We see the shortfall is concentrated in a few hours and there are many +# hours with LOLE = 1, which means that hour had a shortfall in every +# Monte Carlo sample. + +# We can find the regional NEUE for the entire simulation period, +# and obtain it in as a DataFrame for easier viewing: +regional_eue = DataFrame([(Region=reg_name, NEUE=val(NEUE(shortfall, reg_name))) + for reg_name in sys.regions.names], + ) +# So, region "1" has the highest overall NEUE, and has a higher +# load normalized shortfall + +# We may be interested in the EUE in the hour with highest LOLE +max_lole_ts = sys.timestamps[findfirst(val.(lolps).==1)]; +println("Hour with first LOLE of 1.0: ", max_lole_ts) + +# And we can find the unserved energy by region in that hour: +unserved_by_region = EUE.(shortfall, sys.regions.names, max_lole_ts) +# which returns a Vector of EUE values for each region. + +# Region 2 has highest EUE in that hour, and we can look at the +# utilization of interfaces into that region in that period: +utilization_str = join([@sprintf("Interface between regions 1 and 2 utilization: %0.2f", + utilization["1" => "2", max_lole_ts][1]), + @sprintf("Interface between regions 3 and 2 utilization: %0.2f", + utilization["3" => "2", max_lole_ts][1]), + @sprintf("Interface between regions 1 and 3 utilization: %0.2f", + utilization["1" => "3", max_lole_ts][1])], "\n"); +println(utilization_str) + +# We see that the interfaces are not fully utilized, meaning there is +# no excess generation in the system that could be wheeled into region "2" +# and we can confirm this by looking at the surplus generation in each region +println("Surplus in") +println(@sprintf(" region 1: %0.2f",surplus["1",max_lole_ts][1])) +println(@sprintf(" region 2: %0.2f",surplus["2",max_lole_ts][1])) +println(@sprintf(" region 3: %0.2f",surplus["3",max_lole_ts][1])) + +# Is local storage another alternative for region 3? One can check on the average +# state-of-charge of the existing battery in region "3", both in the +# hour before and during the problematic period: + +println(@sprintf("Storage energy T-1: %0.2f",storage["313_STORAGE_1", max_lole_ts-Hour(1)][1])) +println(@sprintf("Storage energy T: %0.2f",storage["313_STORAGE_1", max_lole_ts][1])) + +# It may be that the battery is on average charged going in to the event, +# and perhaps retains some energy during the event, even as load is being +# dropped. The device's ability to mitigate the shortfall must then be limited +# only by its discharge capacity, so increasing the regions storage +# capacity by adding more storage devices may help mitigate some shortfall. + +# Note that if the event was less consistent, this analysis could also have been +# performed on the subset of samples in which the event was observed, using the +# `ShortfallSamples`, `UtilizationSamples`, and +# `StorageEnergySamples` result specifications instead. diff --git a/PRASCore.jl/src/Simulations/Simulations.jl b/PRASCore.jl/src/Simulations/Simulations.jl index 0178c75e..b9f220b6 100644 --- a/PRASCore.jl/src/Simulations/Simulations.jl +++ b/PRASCore.jl/src/Simulations/Simulations.jl @@ -75,7 +75,7 @@ and return `resultspecs`. - `system::SystemModel`: PRAS data structure - `method::SequentialMonteCarlo`: method for PRAS analysis - - `resultspecs::ResultSpec...`: PRAS metric for metrics like [`Shortfall`](@ref) missing generation + - `resultspecs::ResultSpec...`: PRAS metric for metrics like [`Shortfall`](@ref PRASCore.Results.Shortfall) missing generation # Returns diff --git a/PRASCore.jl/src/Systems/SystemModel.jl b/PRASCore.jl/src/Systems/SystemModel.jl index eb74c9b0..c9091e9e 100644 --- a/PRASCore.jl/src/Systems/SystemModel.jl +++ b/PRASCore.jl/src/Systems/SystemModel.jl @@ -1,8 +1,47 @@ """ - SystemModel - -A `SystemModel` contains a representation of a power system to be studied -with PRAS. + SystemModel{N, L, T<:Period, P<:PowerUnit, E<:EnergyUnit} + +A SystemModel struct contains a representation of a power system to be studied +with PRAS. See [system specifications](@ref system_specification) for more +details on components of a system model. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `L`: Length of each timestep in T units +- `T`: Time period type (e.g., `Hour`, `Minute`) +- `P`: Power unit type (e.g., `MW`, `GW`) +- `E`: Energy unit type (e.g., `MWh`, `GWh`) + +# Fields +- `regions`: Representation of system regions (Type - [Regions](@ref)) +- `interfaces`: Information about connections between regions (Type - [Interfaces](@ref)) +- `generators`: Collection of system generators (Type - [Generators](@ref)) +- `region_gen_idxs`: Mapping of generators to their respective regions +- `storages`: Collection of system storages (Type - [Storages](@ref)) +- `region_stor_idxs`: Mapping of storage resources to their respective regions +- `generatorstorages`: Collection of system generation-storages (Type - [GeneratorStorages](@ref)) +- `region_genstor_idxs`: Mapping of hybrid resources to their respective regions +- `lines`: Collection of transmission lines connecting regions (Type - [Lines](@ref)) +- `interface_line_idxs`: Mapping of transmission lines to interfaces +- `timestamps`: Time range for the simulation period +- `attrs`: Dictionary of system metadata and attributes + +# Constructors + SystemModel(regions, interfaces, generators, region_gen_idxs, storages, region_stor_idxs, + generatorstorages, region_genstor_idxs, lines, interface_line_idxs, + timestamps, [attrs]) + +Create a system model with all components specified. + + SystemModel(generators, storages, generatorstorages, timestamps, load, [attrs]) + +Create a single-node system model with specified generators, storage, and load profile. + + SystemModel(regions, interfaces, generators, region_gen_idxs, storages, region_stor_idxs, + generatorstorages, region_genstor_idxs, lines, interface_line_idxs, + timestamps::StepRange{DateTime}, [attrs]) + +Create a system model with `DateTime` timestamps (will be converted to UTC time zone). """ struct SystemModel{N, L, T <: Period, P <: PowerUnit, E <: EnergyUnit} regions::Regions{N, P} @@ -144,6 +183,9 @@ unitsymbol(::SystemModel{N,L,T,P,E}) where { isnonnegative(x::Real) = x >= 0 isfractional(x::Real) = 0 <= x <= 1 +get_params(::SystemModel{N,L,T,P,E}) where {N,L,T,P,E} = + (N,L,T,P,E) + function consistent_idxs(idxss::Vector{UnitRange{Int}}, nitems::Int, ngroups::Int) length(idxss) == ngroups || return false @@ -164,12 +206,12 @@ function Base.show(io::IO, sys::SystemModel{N,L,T,P,E}) where {N,L,T<:Period,P<: print(io, "SystemModel($(length(sys.regions)) regions, $(length(sys.interfaces)) interfaces, ", "$(length(sys.generators)) generators, $(length(sys.storages)) storages, ", "$(length(sys.generatorstorages)) generator-storages,", - "$(N) $(time_unit)s)") + " $(N) $(time_unit)s)") end function Base.show(io::IO, ::MIME"text/plain", sys::SystemModel{N,L,T,P,E}) where {N,L,T,P,E} time_unit = unitsymbol_long(T) - println(io, "\nPRAS system with $(length(sys.regions)) regions, and $(length(sys.interfaces)) interfaces between these regions.") + println(io, "PRAS system with $(length(sys.regions)) regions, and $(length(sys.interfaces)) interfaces between these regions.") println(io, "Region names: $(join(sys.regions.names, ", "))") println(io, "\nAssets: ") println(io, " Generators: $(length(sys.generators)) units") diff --git a/PRASCore.jl/src/Systems/Systems.jl b/PRASCore.jl/src/Systems/Systems.jl index dc4989b4..6b4cd8b1 100644 --- a/PRASCore.jl/src/Systems/Systems.jl +++ b/PRASCore.jl/src/Systems/Systems.jl @@ -21,7 +21,7 @@ export unitsymbol, unitsymbol_long, conversionfactor, powertoenergy, energytopower, # Main data structure - SystemModel, + SystemModel, get_params, # Convenience re-exports ZonedDateTime, @tz_str diff --git a/PRASCore.jl/src/Systems/assets.jl b/PRASCore.jl/src/Systems/assets.jl index 0269521d..04ee3469 100644 --- a/PRASCore.jl/src/Systems/assets.jl +++ b/PRASCore.jl/src/Systems/assets.jl @@ -37,6 +37,27 @@ function Base.show(io::IO, a::AbstractAssets) end +""" + Generators{N,L,T<:Period,P<:PowerUnit} + +A struct representing generating assets within a power system. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `L`: Length of each timestep in T units +- `T`: The time period type used for temporal representation, subtype of `Period` +- `P`: The power unit used for capacity measurements, subtype of `PowerUnit` + +# Fields + - `names`: Name of generator + - `categories`: Category of generator + - `capacity`: Maximum available generation capacity in each timeperiod, expressed + in units given by the `power_units` (`P`) type parameter + - `λ` (failure probability): probability the generator transitions from + operational to forced outage during a given simulation timestep (unitless) + - `μ` (repair probability): probability the generator transitions from forced + outage to operational during a given simulation timestep (unitless) +""" struct Generators{N,L,T<:Period,P<:PowerUnit} <: AbstractAssets{N,L,T,P} names::Vector{String} @@ -114,6 +135,41 @@ function Base.vcat(gs::Generators{N,L,T,P}...) where {N, L, T, P} end +""" + Storages{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} + +A struct representing storage devices in the system. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `L`: Length of each timestep in T units +- `T`: The time period type used for temporal representation, subtype of `Period` +- `P`: The power unit used for capacity measurements, subtype of `PowerUnit` +- `E`: The energy unit used for energy storage, subtype of `EnergyUnit` + +# Fields + - `names`: Name of storage device + - `categories`: Category of storage device + - `charge_capacity`: Maximum available charging capacity for each storage unit in each + timeperiod, expressed in units given by the `power_units` (`P`) type parameter + - `discharge_capacity`: Maximum available discharging capacity for each storage unit in + each timeperiod, expressed in units given by the `power_units` (`P`) type parameter + - `energy_capacity`: Maximum available energy storage capacity for each storage unit in + each timeperiod, expressed in units given by the `energy_units` (`E`) type parameter + - `charge_efficiency`: Ratio of power injected into the storage device's reservoir to + power withdrawn from the grid, for each storage unit in each timeperiod. Unitless. + - `discharge_efficiency`: Ratio of power injected into the grid to power withdrawn from + the storage device's reservoir, for each storage unit in each timeperiod. Unitless. + - `carryover_efficiency`: Ratio of energy available in the storage device's reservoir at + the beginning of one period to energy retained at the end of the previous period, for + each storage unit in each timeperiod. Unitless. + - `λ` (failure probability): Probability the unit transitions from operational to forced + outage during a given simulation timestep, for each storage unit in each timeperiod. + Unitless. + - `μ` (repair probability): Probability the unit transitions from forced outage to + operational during a given simulation timestep, for each storage unit in each + timeperiod. Unitless. +""" struct Storages{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAssets{N,L,T,P} names::Vector{String} @@ -236,6 +292,47 @@ function Base.vcat(stors::Storages{N,L,T,P,E}...) where {N, L, T, P, E} end +""" + GeneratorStorages{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} + +A struct representing generator-storage hybrid devices within a power system. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `L`: Length of each timestep in T units +- `T`: The time period type used for temporal representation, subtype of `Period` +- `P`: The power unit used for capacity measurements, subtype of `PowerUnit` +- `E`: The energy unit used for energy storage, subtype of `EnergyUnit` + +# Fields + - `names`: Name of generator-storage unit + - `categories`: Category of generator-storage unit + - `charge_capacity`: Maximum available charging capacity for each generator-storage + unit in each timeperiod, in `power_units` (`P`) + - `discharge_capacity`: Maximum available discharging capacity for each generator-storage + unit in each timeperiod, in `power_units` (`P`) + - `energy_capacity`: Maximum available energy storage capacity for each generator-storage + unit in each timeperiod, in `energy_units` (`E`) + - `charge_efficiency`: Ratio of power injected into the device's reservoir to power + withdrawn from the grid, for each generator-storage unit in each timeperiod. Unitless. + - `discharge_efficiency`: Ratio of power injected into the grid to power withdrawn from + the device's reservoir, for each generator-storage unit in each timeperiod. Unitless. + - `carryover_efficiency`: Ratio of energy available in the device's reservoir at the + beginning of one period to energy retained at the end of the previous period, for each + generator-storage unit in each timeperiod. Unitless. + - `inflow`: Exogenous power inflow available to each generator-storage unit in each + timeperiod, in `power_units` (`P`) + - `gridwithdrawal_capacity`: Maximum available capacity to withdraw power from the grid + for each generator-storage unit in each timeperiod, in `power_units` (`P`) + - `gridinjection_capacity`: Maximum available capacity to inject power to the grid for + each generator-storage unit in each timeperiod, in `power_units` (`P`) + - `λ` (failure probability): Probability the unit transitions from operational to forced + outage during a given simulation timestep, for each generator-storage unit in each + timeperiod. Unitless. + - `μ` (repair probability): Probability the unit transitions from forced outage to + operational during a given simulation timestep, for each generator-storage unit in each + timeperiod. Unitless. +""" struct GeneratorStorages{N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} <: AbstractAssets{N,L,T,P} names::Vector{String} @@ -390,6 +487,31 @@ function Base.vcat(gen_stors::GeneratorStorages{N,L,T,P,E}...) where {N, L, T, P end +""" + Lines{N,L,T<:Period,P<:PowerUnit} + +A struct representing individual transmission lines between regions in a power +system. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `L`: Length of each timestep in T units +- `T`: The time period type used for temporal representation, subtype of `Period` +- `P`: The power unit used for capacity measurements, subtype of `PowerUnit` + +# Fields + - `names`: Name of line + - `categories`: Category of line + - `forward_capacity`: Maximum available power transfer capacity from `region_from` to + `region_to` along the line, for each line in each timeperiod, in `power_units` (`P`) + - `backward_capacity`: Maximum available power transfer capacity from `region_to` to + `region_from` along the line, for each line in each timeperiod, in `power_units` (`P`) + - `λ` (failure probability): Probability the line transitions from operational to forced + outage during a given simulation timestep, for each line in each timeperiod. Unitless. + - `μ` (repair probability): Probability the line transitions from forced outage to + operational during a given simulation timestep, for each line in each timeperiod. + Unitless. +""" struct Lines{N,L,T<:Period,P<:PowerUnit} <: AbstractAssets{N,L,T,P} names::Vector{String} diff --git a/PRASCore.jl/src/Systems/collections.jl b/PRASCore.jl/src/Systems/collections.jl index 1d144e9e..a265ae94 100644 --- a/PRASCore.jl/src/Systems/collections.jl +++ b/PRASCore.jl/src/Systems/collections.jl @@ -1,3 +1,17 @@ +""" + Regions{N,P<:PowerUnit} + +A struct representing regions within a power system. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `P`: The power unit used for demand measurements, subtype of `PowerUnit` + +# Fields + - `names`: Name of region (unique) + - `load`: Aggregated electricity demand in each region for each timeperiod, in + `power_units` (`P`) +""" struct Regions{N,P<:PowerUnit} names::Vector{String} @@ -24,6 +38,23 @@ Base.:(==)(x::T, y::T) where {T <: Regions} = Base.length(r::Regions) = length(r.names) +""" + Interfaces{N,P<:PowerUnit} + +A struct representing transmission interfaces between regions in a power system. + +# Type Parameters +- `N`: Number of timesteps in the system model +- `P`: The power unit used for interface limits, subtype of `PowerUnit` + +# Fields + - `regions_from`: Index of the first region connected by the interface + - `regions_to`: Index of the second region connected by the interface + - `limit_forward`: Maximum possible total power transfer from `regions_from` to + `regions_to`, for each interface in each timeperiod, in `power_units` (`P`) + - `limit_backward`: Maximum possible total power transfer from `regions_to` to + `regions_from`, for each interface in each timeperiod, in `power_units` (`P`) +""" struct Interfaces{N,P<:PowerUnit} regions_from::Vector{Int} diff --git a/README.md b/README.md index d50441bd..5183ca5d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [![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) [![codecov](https://codecov.io/gh/NREL/PRAS/branch/main/graph/badge.svg?token=WiP3quRaIA)](https://codecov.io/gh/NREL/PRAS) -[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://nrel.github.io/PRAS) +[![docsstable](https://img.shields.io/badge/docs-stable-blue.svg)](https://nrel.github.io/PRAS/) +[![docsdev](https://img.shields.io/badge/docs-dev-blue.svg)](https://nrel.github.io/PRAS/dev) [![DOI](https://img.shields.io/badge/DOI-10.11578/dc.20190814.1-blue.svg)](https://www.osti.gov/biblio/1557438) The Probabilistic Resource Adequacy Suite (PRAS) is a collection of tools for diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 00000000..4f3c2baa --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,12 @@ +[deps] +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" +PRASCapacityCredits = "2e1a2ed5-e89d-4cd3-bc86-c0e88a73d3a3" +PRASCore = "c5c32b99-e7c3-4530-a685-6f76e19f7fe2" +PRASFiles = "a2806276-6d43-4ef5-91c0-491704cd7cf1" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" + +[compat] +Documenter = "1" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..84be7859 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# Updating and pushing documentation + +These are some steps to follow when making changes to documentation. +1. Checkout a new branch from `main` (e.g. `git checkout -b docupdate`). +2. Instantiate the environment in the `docs` folder by running `] instantiate` in the Julia REPL. +3. Make changes to the documentation files in the `docs` folder and/or the example scripts in the `examples` folder. +4. You can iteratively make changes and build documentation to serve locally by running the `serve.jl` script in the `docs` folder by running + ```bash + julia --project=. serve.jl --threads auto + ``` +5. Once you push to origin (github.com/NREL/PRAS) and create a pull request, the documentation will be built and served at https://nrel.github.io/PRAS/previews/PR{#}. Once the changes are merged to `main`, the documentation will be available at https://nrel.github.io/PRAS/dev. On tagging a PRAS version for release, the documentation will be available at https://nrel.github.io/PRAS/stable and https://nrel.github.io/PRAS/vX.Y.Z (where X.Y.Z is the version number). +6. More instructions on version tagging and release can be found in the [Documenter.jl](https://documenter.juliadocs.org/stable/man/hosting/#Documentation-Versions) instructions. \ No newline at end of file diff --git a/docs/_config.yaml b/docs/_config.yaml deleted file mode 100644 index 3c0d055c..00000000 --- a/docs/_config.yaml +++ /dev/null @@ -1,4 +0,0 @@ -theme: jekyll-theme-cayman -title: PRAS -description: The Probabilistic Resource Adequacy Suite -show_downloads: false diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index c0dd2673..00000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,256 +0,0 @@ -# Getting Started with PRAS - -For a complete overview of PRAS, see the -[v0.6 documentation report](https://www.nrel.gov/docs/fy21osti/79698.pdf). -This page provides a briefer overview to help you start running PRAS quickly. - -## Parallel Processing - -PRAS uses multi-threading, so be sure to set the environment variable -controlling the number of threads available to Julia (36 in this Bash example, -which is a good choice for NREL Eagle nodes - on a laptop you would probably -only want 4 or so) before running scripts or launching the REPL: - -```sh -export JULIA_NUM_THREADS=36 -``` - -## Power System Data - -The recommended way to store and retreive PRAS system data is in an HDF5 file -that conforms to the -[PRAS system data format](https://github.com/NREL/PRAS/blob/master/SystemModel_HDF5_spec.md). -Once your system is represented in that format you can load it into PRAS with: - -```julia -using PRAS -sys = SystemModel("filepath/to/systemdata.pras") -``` - -## Resource Adequacy Assessment - -PRAS functionality is distributed across groups of -modular specifications that can be mixed, extended, or replaced to support the -needs of a particular analysis. When assessing reliability or capacity value, -one can define the specs to be used while passing along any associated -parameters or options. - -The categories of specifications are: - -**Simulation Specifications**: How should power system operations be simulated? -Options are `Convolution` and `SequentialMonteCarlo`. - -**Result Specifications**: What kind and level of detail of results should be -saved out during simulations? -Options include `Shortfall`, `Surplus`, interface `Flow`, `StorageEnergy`, -and `GeneratorStorageEnergy`. Not all simulation specifications support all -result specifications. -(See the [PRAS documentation](https://www.nrel.gov/docs/fy21osti/79698.pdf) -for more details.) - -**Capacity Credit Specifications**: If performing a capacity credit -calculation, which method should be used? -Options are `EFC` and `ELCC`. - -### Running an analysis - -Analysis centers around the `assess` method with different arguments passed -depending on the desired analysis to run. -For example, to run a copper-plate convolution-based reliability assessment -(`Convolution`) with LOLE and EUE reporting (`Shortfall`), -one would run: - -```julia -assess(mysystemmodel, Convolution(), Shortfall()) -``` - -If you instead want to run a simulation that considers energy-limited resources -and transmission constraints, using 100,000 Monte Carlo samples, -the method call becomes: - -```julia -assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall()) -``` - -You can request multiple kinds of result from a single assessment: - -```julia -assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall(), Flow()) -``` - -### Querying Results - -Each result type requested returns a seperate result object. These objects can -then be queried using indexing syntax to extract values of interest. - -```julia -shortfalls, flows = - assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall(), Flow()) - -flows["Region A" => "Region B"] -``` - -The full PRAS documentation provides more details on how different result -object types can be indexed. - -Because the main results of interest from resource adequacy assessments are -probabilistic risk metrics (i.e. EUE and LOLE), `Shortfall` result objects -define some additional methods: a particular risk metric can be -obtained by calling that metric's constructor with the `Shortfall` -result object. For example, to obtain the system-wide LOLE and EUE over the -simulation period: - -```julia -lole = LOLE(shortfalls) -eue = EUE(shortfalls) -``` - -Single-period metrics can also be extracted. For example, to get system-wide -EUE for April 27th, 2024 at 1pm EST: - -```julia -period_eue = EUE(shortfalls, ZonedDateTime(2024, 4, 27, 13, tz"EST")) -``` - -Region-specific metrics can also be extracted. For example, to obtain the LOLE -of Region A across the entire simulation period: - -```julia -eue_a = LOLE(shortfalls, "Region A") -``` - -Finally, metrics can be obtained for both a specific region and time: - -```julia -period_eue_a = EUE(shortfalls, "Region A", ZonedDateTime(2024, 4, 27, 13, tz"EST")) -``` - -## Capacity Credit Calculations - -Capacity credit calcuations build on probabilistic resource adequacy assessment -to provided capacity-based quantifications of the marginal benefit to -system resource adequacy associated with a specific resource or collection of -resources. Two capacity credit metrics (EFC and ELCC) are currently supported. - -### Equivalent Firm Capacity (EFC) - -`EFC` estimates the amount of idealized, 100%-available capacity that, when -added to a baseline system, reproduces the level of system adequacy associated -with the baseline system plus the study resource. The following parameters must -be specified: - - - The risk metric to be used for comparison (i.e. EUE or LOLE) - - A known upper bound on the EFC value (usually the resource's nameplate - capacity) - - The regional distribution of the firm capacity to be added. This is - typically chosen to match the regional distribution of the study resource's - nameplate capacity. - -For example, to assess the EUE-based EFC of a new resource with 1000 MW -nameplate capacity, added to the system in a single region named "A": - -```julia -# The base system, with power units in MW -base_system - -# The base system augmented with some incremental resource of interest -augmented_system - -# Get the lower and upper bounds on the EFC estimate for the resource -efc = assess( - base_system, augmented_system, EFC{EUE}(1000, "A"), - SequentialMonteCarlo(samples=100_000)) -min_efc, max_efc = extrema(efc) -``` - -If the study resource were instead split between regions "A" (600MW) and "B" -(400 MW), one could specify the firm capacity distribution as: - -```julia -efc = assess( - base_system, augmented_system, EFC{EUE}(1000, ["A"=>0.6, "B"=>0.4]), - SequentialMonteCarlo(samples=100_000)) -``` - -### Equivalent Load Carrying Capability (ELCC) - -`ELCC` estimates the amount of additional load that can be added to the system -(in every time period) in the presence of a new study resource, while -maintaining the baseline system's original adequacy level. The following -parameters must be specified: - - - The risk metric to be used for comparison (i.e. EUE or LOLE) - - A known upper bound on the ELCC value (usually the resource's nameplate - capacity) - - The regional distribution of the load to be added. Note that this choice is - somewhat ambiguous in multi-region systems, so assumptions should be clearly - specified when reporting analysis results. - -For example, to assess the EUE-based ELCC of a new resource with 1000 MW -nameplate capacity, serving new load in region "A": - -```julia -# The base system, with power units in MW -base_system - -# The base system augmented with some incremental resource of interest -augmented_system - -# Get the lower and upper bounds on the ELCC estimate for the resource -elcc = assess( - base_system, augmented_system, ELCC{EUE}(1000, "A"), - SequentialMonteCarlo(samples=100_000)) -min_elcc, max_elcc = extrema(elcc) -``` - -If instead the goal was to study the ability of the new resource to provide -load evenly to regions "A" and "B", one could use: - -```julia -elcc = assess( - base_system, augmented_system, ELCC{EUE}(1000, ["A"=>0.5, "B"=>0.5]), - SequentialMonteCarlo(samples=100_000)) -``` - -### Capacity credit calculations in the presence of sampling error - -For non-deterministic assessment methods (i.e. Monte Carlo simulations), -running a resource adequacy assessment with different random number generation -seeds will result in different risk metric estimates for the same underlying -system. Capacity credit assessments can be sensitive to this uncertainty, -particularly when attempting to study the impact of a small resource on a -large system with a limited number of simulation samples. - -PRAS takes steps to a) limit this uncertainty and b) warn against -potential deficiencies in statistical power resulting from this uncertainty. - -First, the same random seed is used across all simulations in the capacity -credit assessment process. If the number of resources and their reliability -parameters (MTTF and MTTR) remain constant across the baseline and augmented -test systems, seed re-use ensures that unit-level outage profiles remain -identical across RA assessments, providing a fixed background against which to -measure changes in RA resulting from the addition of the study resource. Note -that satisfying this condition requires that the study resource be present in -the baseline case, but with its contributions eliminated (e.g. by setting its -capacity to zero). When implementing an assessment method that modifies the -user-provided system to add new resources (such as EFC), the programmer should -assume this invariance exists in the provided data, and not violate it in any -automated modifications. - -Second, capacity credit assessments have two different stopping criteria. The -ideal case is that the upper and lower bounds on the capacity -credit metric converge to be sufficiently tight relative to a desired level -of precision. This target precision is 1 system power unit (e.g. MW) by -default, but can be relaxed to loosen the convergence bounds if desired via -the `capacity_gap` keyword argument. Once the lower and upper bounds are -tighter than this gap, their values are returned. - -Additionally, at each bisection step, a hypothesis test is performed to ensure -that the theoretically-larger bounding risk metric is in fact larger than the -smaller-valued risk metric with a specified level of statistical significance. -By default, this criteria is a maximum p-value of 0.05, although this value -can be changed as desired via the `p_value` keyword argument. If at some point -the null hypothesis (the higher risk is not in fact larger than the lower -risk) cannot be rejected at the desired significance level, the assessment -will provide a warning indicating the size of the remaining capacity gap and -return the lower and upper bounds on the capacity credit estimate. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index d320b2c1..00000000 --- a/docs/index.md +++ /dev/null @@ -1,11 +0,0 @@ -The Probabilistic Resource Adequacy Suite (PRAS) provides an open-source, -research-oriented collection of tools for analysing the resource adequacy of a -bulk power system. The simulation methods offered support everything from -classical convolution-based analytical techniques through to high-performance -sequential Monte Carlo methods supporting multi-region composite reliability -assessment, including simulation of energy-limited resources such as storage. - -PRAS is developed and maintained at the US -[National Renewable Energy Laboratory](https://www.nrel.gov/) (NREL). - -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. diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 00000000..801de9cd --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,61 @@ +using Documenter +using PRASCore +using PRASFiles +using PRASCapacityCredits +using Literate + +# Building examples was inspired by COSMO.jl repo +@info "Building example problems..." + +fix_suffix(filename) = replace(filename, ".jl" => ".md") + +example_path = joinpath(@__DIR__, "..","PRAS.jl","examples/") +build_path = joinpath(@__DIR__, "src", "examples/") +files = readdir(example_path) +filter!(x -> endswith(x, ".jl"), files) +for file in files + Literate.markdown(example_path * file, build_path; + documenter = true, credit = true) +end + +examples_nav = fix_suffix.("./examples/" .* files) + +# Generate the unified documentation +makedocs( + sitename = "PRAS", + format = Documenter.HTML( + prettyurls = true, + canonical = "https://nrel.github.io/PRAS/stable" + ), + modules = [PRASCore, PRASFiles, PRASCapacityCredits], + pages = [ + "Home" => "index.md", + "Resource Adequacy" => "resourceadequacy.md", + "Getting Started" => [ + "Installation" => "installation.md", + "Quick start" => "quickstart.md", + ], + "PRAS Components " => [ + "System Model Specification" => "PRAS/sysmodelspec.md", + "Simulation Specifications" => "PRAS/simulations.md", + "Result Specifications" => "PRAS/results.md", + "Capacity Credit Calculation" => "PRAS/capacitycredit.md", + ], + ".pras File Format" => "SystemModel_HDF5_spec.md", + "Tutorials" => examples_nav, + "Extending PRAS" => "extending.md", +# "Contributing" => "contributing.md", + "Changelog" => "changelog.md", + "API Reference" => [ + "PRASCore" => "PRASCore/api.md", + "PRASFiles" => "PRASFiles/api.md", + "PRASCapacityCredits" => "PRASCapacityCredits/api.md" + ] + ], + checkdocs = :exports, +) + +deploydocs( + repo = "github.com/NREL/PRAS.git", + push_preview = true, +) \ No newline at end of file diff --git a/docs/serve.jl b/docs/serve.jl new file mode 100644 index 00000000..ff4cc908 --- /dev/null +++ b/docs/serve.jl @@ -0,0 +1,15 @@ +using LiveServer +using Pkg + +# Develop all packages +for pkg in ["PRASCore.jl", "PRASFiles.jl", "PRASCapacityCredits.jl"] + pkg_path = joinpath("..", pkg) + isdir(pkg_path) || continue + Pkg.develop(PackageSpec(path=pkg_path)) +end + +# Build docs +include("make.jl") + +# Serve documentation +serve(dir="build") \ No newline at end of file diff --git a/docs/src/PRAS/acknowledgements.md b/docs/src/PRAS/acknowledgements.md new file mode 100644 index 00000000..684e25f8 --- /dev/null +++ b/docs/src/PRAS/acknowledgements.md @@ -0,0 +1,8 @@ +# Acknowledgments + +Special thanks to NREL's Trieu Mai, Paul Denholm, and Greg Brinkman for their +support and feedback in preparing this work, and to Sarah Awara and Sinnott +Murphy (NREL), as well as Armando Figueroa Acevedo (Black & Veatch), for +reviewing drafts of this report. Additional thanks go +to Jaquelin Cochran, Bethany Frew, Wesley Cole, and Aaron Bloom for +their broad historical support in the development of the PRAS model. diff --git a/docs/src/PRAS/capacitycredit.md b/docs/src/PRAS/capacitycredit.md new file mode 100644 index 00000000..8edbfbd2 --- /dev/null +++ b/docs/src/PRAS/capacitycredit.md @@ -0,0 +1,221 @@ +# [Capacity Credit Calculation](@id capacitycredit) + +Resource adequacy paradigms premised on adding resource capacities together to +meet a planning reserve margin require the quantification of a "capacity credit" (sometimes called "capacity value") +for individual resources. While the process of assigning capacity credits is +relatively straightforward for thermal generating units with consistent +potential contributions to system adequacy throughout the day and year +(assuming no fuel constraints), the contributions of variable and +energy-limited resources can be much more difficult to represent as a +single capacity rating. + +In these cases, an accurate characterization depends on +the broader system context in which the resource operates. +Probabilistically derived capacity credit calculations +provide a technology-agnostic means of expressing the contributions of +different resources (with diverse and potentially complicated operating +characteristics and constraints) in terms of a common, simple measure of +capacity. + +PRAS provides two different methods for mapping incremental resource adequacy +contributions to generic capacity: Equivalent Firm Capacity (**EFC**) +and Effective Load Carrying Capability (**ELCC**). In each case, the user +must provide PRAS with two system representations: one that contains the study +resource (the augmented system), and one that does not (the base system). The +difference between the two systems is then quantified in terms of a capacity +credit. + +By choosing what is included in the base case relative to the augmented case, +the user can study either the average, portfolio-level capacity credit of a +resource class (by excluding all resources of that class from the base case, +and including them all in the augmented case) or the marginal capacity credit +(by including almost all of the resource type in the base case, and adding a +single incremental unit in the augmented case). + +Note that probabilistically derived capacity credit calculations always involve +some kind of measurement of the reduction in system risk associated with moving +from the base system to the augmented system. If the base system's risk cannot +be reduced (perhaps because the base system's shortfall risk is too small to +obtain a non-zero estimate, or because shortfall only occurs in load pockets +elsewhere in the system), adequacy-based capacity credit metrics may not be +meaningful. In these cases, the starting system may need to be modified, or a +different capacity credit calculation method may be required. + +The remainder of this chapter provides details on the theoretical and practical +aspects of using EFC and ELCC for capacity credit analysis in PRAS. Further +mathematical details regarding capacity credit are available in +[Zachary and Dent (2011)](#references). + +## Equivalent Firm Capacity (EFC) + +### Theory + +EFC calculates the amount of idealized "firm" capacity (uniformly available +across all periods, without ever going on outage) that is required to +reproduce the observed resource adequacy benefit (reduction of a specific risk +metric) associated with some study resource of interest. It requires both a base case system (without the study +resource added) and an augmented system (with the study resource added). The +analysis then proceeds as follows: + +1. Assess the shortfall risk of the base system according to the + chosen metric (EUE or LOLE). +2. Assess the (lower) shortfall risk of the augmented system according + to the chosen metric. +3. Reassess the shortfall risk of the base system after adding some + amount of "firm" capacity. If the risk matches that of the augmented + system, stop. The amount of firm capacity added is the + Equivalent Firm Capacity of the study resource. +4. If the base+firm and augmented system risks do not match, change the + amount of firm capacity added to the base system, repeating until + the chosen shortfall risk metrics for each system match. + +Typically, the counterfactual firm capacity is added +to the system as a direct replacement for the study resource, and so is located +in the same region (or distributed across multiple regions in corresponding +proportions) as the study resource. PRAS uses a bisection method to +find the appropriate total firm capacity to add to the base system. + +### Usage + +Performing an EFC assessment in PRAS requires specifying two different +`SystemModel`s: one representing the base system, and a second +representing the augmented system. It also requires specifying the probabilistic risk metric to +use when comparing system risks, an upper bound on the EFC (usually, +the nameplate capacity of the study resource) and to which region(s) the +counterfactual firm capacity should be added. Finally, the simulation +specification should be provided (any simulation method can be used). + +For example, to calculate EFC based on EUE for a resource in region A, with an +upper EFC bound of 1000 MW (assuming the `SystemModel`s are represented +in MW), using the sequential Monte Carlo simulation specification: + +```julia +assess(base_system, augmented_system, + EFC{EUE}(1000, "A"), SequentialMonteCarlo()) +``` + +If the study resources are spread over multiple regions (for example, 600 MW +of wind in region A and 400 MW of wind in region B), the fraction of total firm +capacity added to each region can be specified as: + +```julia +assess(base_system, augmented_system, + EFC{EUE}(1000, ["A"=>0.6, "B"=>0.4]), SequentialMonteCarlo()) +``` + +The `EFC()` specification accepts multiple optional keyword +arguments, which can be provided in any order: + +**p_value**: A floating point value giving the maximum allowable p-value +from a one-sided hypothesis test. The test considers whether the lower risk +metric used during bisection is in fact less than the upper risk metric. If the p-value exceeds this level, the +assessment will terminate early due to a lack of statistical power. Note that this only matters for simulation +specifications returning estimates with non-zero standard errors, i.e. +Monte Carlo-based methods. Defaults to `0.05`. + +**capacity_gap**: An integer giving the maximum desired difference between +reported upper and lower bounds on capacity credit. Once the gap between upper +and lower bounds is less than or equal to this value, the assessment will +terminate. Defaults to `1`. + +**verbose**: A boolean value defaulting to `false`. If `true`, +PRAS will output informative text describing the progress of the assessment. + +## Effective Load Carrying Capability (ELCC) + +### Theory + +ELCC quantifies the capacity credit of a study resource according to the +amount of additional constant load the system can serve while +maintaining the same shortfall risk. Like EFC, it requires both a base case +system (without the study resource added) and an augmented system (with the +study resource added). The analysis then proceeds as follows: + +1. Assess the shortfall risk of the base system according to the + chosen metric (EUE or LOLE). +2. Assess the (lower) shortfall risk of the augmented system according + to the chosen metric. +3. Reassess the shortfall risk of the augmented system after adding some + amount of constant load. If the risk matches that of the base + system, stop. The amount of constant load added is the + Effective Load Carrying Capability of the study resource. +4. If the base and augmented+load system risks do not match, change the + amount of load added to the augmented system, repeating until + the chosen shortfall risk metrics for each system match. + +ELCC calculations in a multi-region system require choosing where load should +be increased. There are many possible options, including uniformly distributing +new load across each region, distributing load proportional to +total energy demand in each region, and adding load only in the region with +the study resource. The "correct" choice will depend on the goals of the +specific analysis. Once the regional load distribution is specified, PRAS uses +a bisection method to find the appropriate amount of total load to add to the +system. + +### Usage +Performing an ELCC assessment in PRAS requires specifying two different +`SystemModel`s: one representing the base system, and a second +representing the augmented system. It also requires specifying the probabilistic risk metric to +use when comparing system risks, an upper bound on the ELCC (usually, +the nameplate capacity of the study resource) and to which region(s) the +additional load should be added. Finally, the simulation +specification should be provided. + +For example, to calculate ELCC based on EUE for a resource intending to serve +load in region A, with an upper ELCC bound of 1000 MW (assuming the `SystemModel`s are represented +in MW): + +```julia +assess(base_system, augmented_system, + ELCC{EUE}(1000, "A"), SequentialMonteCarlo()) +``` + +If the load serving assessment is to be spread over multiple regions +(for example, 50% of load in region A and 50% in region B), the fraction of additional load added +to each region can be specified as: + +```julia +assess(base_system, augmented_system, + ELCC{EUE}(1000, ["A"=>0.5, "B"=>0.5]), SequentialMonteCarlo()) +``` + +The `ELCC()` specification accepts multiple optional keyword +arguments, which can be provided in any order: + +**p_value**: A floating point value giving the maximum allowable p-value +from a one-sided hypothesis test. The test considers whether the lower risk +metric used during bisection is in fact less than the upper risk metric. If the p-value exceeds this level, the +assessment will terminate early due to a lack of statistical power. Note that this only matters for simulation +specifications returning estimates with non-zero standard errors, i.e. +Monte Carlo-based methods. Defaults to `0.05`. + +**capacity_gap**: An integer giving the maximum desired difference between +reported upper and lower bounds on capacity credit. Once the gap between upper +and lower bounds is less than or equal to this value, the assessment will +terminate. Defaults to `1`. + +**verbose**: A boolean value defaulting to `false`. If `true`, +PRAS will output informative text describing the progress of the assessment. + +## Capacity Credit Results + +Both EFC and ELCC assessments return `CapacityCreditResult` objects. +These objects contain information on estimated lower and upper bounds of the +capacity credit, as well as additional details about the process through +which the capacity credit was calculated. Results can be retrieved as follows: + +```julia +cc_result = assess(base_system, augmented_system, EFC{EUE}(1000, "A"), + SequentialMonteCarlo()) + +# Get lower and upper bounds on CC estimate +cc_lower = minimum(cc_result) +cc_upper = maximum(cc_result) + +# Get both bounds at once +cc_lower, cc_upper = extrema(cc_result) +``` + +## References + +Zachary, S., & Dent, C. J. (2011). Probability theory of capacity value of additional generation. Proc. IMechE Part O: J. Risk and Reliability, 226, 33-43. diff --git a/docs/src/PRAS/results.md b/docs/src/PRAS/results.md new file mode 100644 index 00000000..893af5d7 --- /dev/null +++ b/docs/src/PRAS/results.md @@ -0,0 +1,246 @@ +# [Result Specifications](@id results) + +Different analyses require different kinds of results, and different levels of +detail within those results. PRAS considers many operational decisions and +system states internally, not all of which are relevant outputs for every analysis. When a user invokes +PRAS' `assess` function, one or more "result specifications" must be provided in order to indicate +the simulation outcomes that are of interest, and the desired level of +sample aggregation or unit type (if applicable) for which those +results should be reported. In general, sample-level disaggregation +should be used with care, as this can require large amounts of memory if +simulating with many samples. + +The current version of PRAS includes six built-in result specification +families, with additional user-defined specifications possible (see +[Custom Result Specifications](@ref customsimspec)). These families can be classified into regional +results (**Shortfall** and **Surplus**), interface results +(**Flow** and **Utilization**), and unit results +(**Availability** and **Energy**). + +When invoking `assess` in Julia, result specifications are provided as +the final arguments to the function call, and a tuple of results are returned +in that same order. (Note that a tuple is *always* returned, even if a +single result specification is requested.) An example of requesting three +result specifications is: + +```julia +surplus, flow, genavail = assess( + sys, SequentialMonteCarlo(), Surplus(), Flow(), GeneratorAvailability()) +``` + +Depending on the result specification, a result object may support indexing +into it to obtain results for a specific time period, region, interface, or +unit. For example, using the results returned above: + +```julia +timestamp = ZonedDateTime(2020, 1, 1, 13, tz"UTC") +genname = "Generator 1" +regionname = "Region A" +interface = "Region A" => "Region B" + +# get the sample mean and standard deviation of observed total system +# surplus capacity at 1pm UTC on January 1, 2020: +m, sd = surplus[timestamp] + +# get the sample mean and standard deviation of observed surplus capacity +# in Region A at 1pm UTC on January 1, 2020: +m, sd = surplus[regionname, timestamp] + +# get the sample mean and standard deviation of average interface flow +# between Region A and Region B: +m, sd = flow[interface] + +# get the sample mean and standard deviation of interface flow +# between Region A and Region B at 1pm UTC on January 1, 2020: +m, sd = flow[interface, timestamp] + +# get the vector of random generator availability states in every sample +# for Generator 1, at 1pm UTC on January 1, 2020: +states = genavail[genname, timestamp] +``` + +Results can be reported in different ways depending on the result +specification being used, and not all types of indexing are appropriate for +every result specification. For example, it would not make sense to aggregate +interface flows across all interfaces in the system, or surplus power +(potentially from energy-limited devices) across all time periods. + +The remainder of this chapter provides additional +details about the six built-in result specification families in PRAS. + +## Regional Results + +The Shortfall and Surplus result families are defined over regions, and their +result objects can all be indexed into by region name. The table below outlines the simulation specifications that members of +these families are compatible with, as well as the levels of disaggregation +they support. + +| Result Specification | Units | SMC | Sample | Region | Timestep | Region + Timestep | +|----------------------|-------|-----|--------|--------|---------|------------------| +| `Shortfall` | Energy | • | | • | • | • | +| `ShortfallSamples` | Energy | • | • | • | • | • | +| `Surplus` | Power | • | | | • | • | +| `SurplusSamples` | Power | • | • | | • | • | + +*Table: Regional result specification characteristics.* + +### Shortfall +The Shortfall family of result specifications (`Shortfall` and +`ShortfallSamples`) reports on unserved energy +occuring during simulations. As quantifying unserved energy is the core aspect +of resource adequacy analysis, in practice almost every assessment requests a +Shortfall-related result. The basic `Shortfall` specification is most +commonly used and reports average shortfall results, while +`ShortfallSamples` provides more detailed results at the level of +individual simulations (samples). + +Shortfall result objects can be indexed into by region, timestep, both region +and timestep, or neither. Indexing on neither (via `result[]`) reports +the total shortfall across all regions and time periods. + +Shortfall results are unique among the built-in result types in that the raw +results can also be converted to specific probabilistic risk metrics +(**EUE** and **LOLE**). For sampling-based methods, both metric +estimates and the standard error of those estimates are provided. For example, +after assessing the system, metrics across all regions and the full simulation +horizon can be extracted as: + +```julia +shortfall, = assess(sys, SequentialMonteCarlo(), Shortfall()) +eue_overall = EUE(shortfall) +lole_overall = LOLE(shortfall) +``` + +More specific metrics can be obtained as well: + +```julia +region = "Region A" +period = ZonedDateTime(2020, 1, 1, 0, tz"America/Denver") + +eue_period = EUE(shortfall, period) +lole_region = LOLE(shortfall, region) +eue_region_period = EUE(shortfall, region, period) +``` + +### Surplus +The Surplus family of result specifications (`Surplus` and +`SurplusSamples`) reports on excess grid injection capacity (via +generation or discharging) in the system. This can be used to study +"near misses" where shortfall came close to occuring but did not actually +happen. The `Surplus` specification reports average surplus across +samples, while `SurplusSamples` reports simulation-level observations. + +Surplus capacity is reported in terms of power, and so results are always +disaggregated by timestep (indexed either by timestep or both region and +timestep). + +## Interface Results + +The Flow and Utilization families of result specifications are defined over +interfaces, and their result objects can all be indexed into by a pair of +region names (indicating the source and destination regions for power +transfer). The table below outlines the simulation specifications +that members of these families are compatible with, as well as the levels of +disaggregation they support. + +| Result Specification | Units | SMC | Sample | Interface | Timestep | Interface + Timestep | +|----------------------|-------|-----|--------|-----------|---------|---------------------| +| `Flow` | Power | • | | • | | • | +| `FlowSamples` | Power | • | • | • | | • | +| `Utilization` | -- | • | | • | | • | +| `UtilizationSamples` | -- | • | • | • | | • | + +*Table: Interface result specification characteristics.* + +### Flow + +The Flow family of result specifications (`Flow` and +`FlowSamples`) reports the direction and magnitude of power transfer +on an interface. This can be used to study which regions are importers vs +exporters of energy, either on average or at specific periods in time. The +`Flow` specification reports average flow across all samples, while +`FlowSamples` reports simulation-level observations. Flow results are +directional, so the order in which the regions are provided when looking up +a result will determine the result's sign. For example: + +```julia +m1, sd1 = flow["Region A" => "Region B"] +m2, sd2 = flow["Region B" => "Region A"] + +m1 == -m2 # true +sd1 == sd2 # true +``` + +Flow values are reported in terms of power, and results are always +disaggregated by interface. Results that aggregate over time report the average +flow over the time span. + +### Utilization + +The Utilization family of result specifications (`Utilization` and +`UtilizationSamples`) is similar to the Flow +family, but reports the fraction of an interface's +available transfer capacity that is used in the direction of flow, instead of +the flow power itself. Results can therefore range between 0 and 1. +This metric can be useful for studying the impact of line outages and +transmission congestion on unserved energy. +The `Utilization` specification reports average flow across all samples, +while `UtilizationSamples` reports simulation-level observations. Unlike +Flow, Utilization results are not directional and so will report the same +utilization regardless of the flow direction implied by the order of the +provided regions: + +```julia +util, = assess(sys, SequentialMonteCarlo(), Utilization()) +util["Region A" => "Region B"] == util["Region B" => "Region A"] +``` + +Utilization values are unitless, and results are always +disaggregated by interface. Results that aggregate over time report the average +utilization over the time span. + +## Unit Results + +The Availability and Energy families of result specifications are defined over +individual units, and their result objects can all be indexed into by a unit +name and timestep. The table below outlines the simulation +specifications that members of these families are compatible with, as well as +the levels of disaggregation they support. + +| Result Specification | Units | SMC | Sample | Unit | Timestep | Unit + Timestep | +|----------------------|-------|-----|--------|------|---------|----------------| +| `GeneratorAvailability` | -- | • | • | | | • | +| `StorageAvailability` | -- | • | • | | | • | +| `GeneratorStorageAvailability` | -- | • | • | | | • | +| `LineAvailability` | -- | • | • | | | • | +| `StorageEnergy` | Energy | • | | | • | • | +| `StorageEnergySamples` | Energy | • | • | | • | • | +| `GeneratorStorageEnergy` | Energy | • | | | • | • | +| `GeneratorStorageEnergySamples` | Energy | • | • | | • | • | + +*Table: Unit result specification characteristics.* + +### Availability + +The Availability family of result specifications +(`GeneratorAvailability`, `StorageAvailability`, +`GeneratorStorageAvailability`, and `LineAvailability`) reports the availability state +(available, or unavailable due to an unplanned outage) of individual units in +the simulation. The four result specification variants correspond to the four +categories of resources: generators, storages, generator-storages, and +lines. Availability is reported as a boolean value (with `true` +indicating the unit is available, and `false` indicating it isn't), and +is always disaggregated by unit, timestep, and sample. + +### Energy + +The Energy family of result specifications (`StorageEnergy`, +`StorageEnergySamples`, `GeneratorStorageEnergy`, and +`GeneratorStorageEnergySamples`) reports the energy state-of-charge +associated with individual energy-limited resources. Result specification +variants are available for selecting the category of energy-limited resource +(storage or generator-storage) to report, as well as for requesting +sample-level disaggregation. Energy is always disaggregated by timestep and +may also be disaggregated by unit (get the state of charge of a single +unit) or aggregated across the system (get the sum of states of charge +of all storage devices in the system). \ No newline at end of file diff --git a/docs/src/PRAS/simulations.md b/docs/src/PRAS/simulations.md new file mode 100644 index 00000000..e4acc070 --- /dev/null +++ b/docs/src/PRAS/simulations.md @@ -0,0 +1,74 @@ +# [Simulation Specifications](@id simulations) + +There are many different simplifying assumptions that can be made when +simulating power system operations for the purpose of studying resource +adequacy. The level of simplification a modeller is willing to accept +will depend on the goals of the study and the computational resources +available to carry out the modelling exercise. + +PRAS is referred to as a "suite" because of its inclusion of multiple power +system operations models of varying fidelity and computational complexity. +Each PRAS analysis (a single invocation of PRAS' `assess` function) is associated with exactly one of these +operational models, or "simulation specifications". A simulation specification +encodes a particular set of assumptions and simplifications that will be used +when simulating operations in order to assess the resource adequacy of the +study system. + +The current version of PRAS defines the **Sequential Monte Carlo** specification, +with additional user-defined specifications possible (see [Custom Simulation Specifications](@ref customsimspec)). The remainder of this +section describes the methods and underlying assumptions of each of these +built-in simulation specifications. + +## Sequential Monte Carlo + +The Sequential Monte Carlo method simulates the chronological evolution of the system, tracking individual unit-level outage states and the state of charge of energy-limited resources. While it is the most computationally intensive simulation method provided in PRAS, it remains much simpler (and therefore runs much faster) than a production cost model. + +### Theory and Assumptions + +The Sequential Monte Carlo method simulates unit-level outages using a two-state Markov model. In each time period, the availability state of each generator, storage, generator-storage, and line either changes or remains the same, at random, based on the unit's provided state transition probabilities. The capacities from each available generator (or line) in a given time period are then added together to determine the total available generating (transfer) capacity for a region (interface). Storage and generator-storage units are similarly enabled or disabled based on their availability states. + +Each set of sampled parameters is used to formulate the "pipe-and-bubble" network flow problem shown below, in which local demand in each region is satisfied (or not) using a combination of local generation, imported power, and unserved energy. Regions with surplus capacity can export that power to their neighbours if transmission limits allow, with a small transfer penalty applied to prevent loop flows. Any demand that cannot be supplied under the randomly drawn generation and transmission limits is considered unserved. + +```@raw html +
+Example three-region network flow problem to be solved
+        by the Non-Sequential Monte Carlo simulation specification. Any
+        load that cannot be served within the sampled generation
+        and transmission constraints goes unserved. +
Example three-region network flow problem to be solved + by the Non-Sequential Monte Carlo simulation specification. Any + load that cannot be served within the sampled generation + and transmission constraints goes unserved
+
+``` + +The Sequential Monte Carlo method is unique in its ability to represent energy-limited resources (storages and generator-storages). These resources are dispatched conservatively so as to approximately minimize unserved energy over the full simulation horizon, charging from the grid whenever surplus generating capacity is available, and discharging only when needed to avoid or mitigate unserved energy. Charging and discharging is coordinated between resources using the time-to-go priority described in [Evans et al. (2019)](#references): resources that would be able to discharge the longest at their maximum rate are discharged first, and resources that would take the longest time to charge at their maximum charge rate are charged first. Cross-charging (discharging one resource in order to charge another) is not permitted. + +In Sequential Monte Carlo simulations, one "sample" involves chronological simulation of the system over the full operating horizon. Unserved energy results for each hour and the overall horizon are recorded before restarting the simulation and repeating the process with new random outage draws. Once all samples have been completed, hourly and overall system risk metrics can be calculated. + +### Usage + +A sequential Monte Carlo resource adequacy assessment is invoked by calling PRAS' `assess` method in Julia, with `SequentialMonteCarlo()` as the simulation specification argument: + +```julia +assess(sys, SequentialMonteCarlo(), Shortfall()) +``` + +The `SequentialMonteCarlo()` specification accepts several optional keyword arguments, which can be provided in any order: + +**samples**: A positive integer value defaulting to `10000`. It defines the number of samples (replications) to be used in the Monte Carlo simulation process. + +**seed**: An integer defaulting to a random value. It defines the seed to beused for random number generation when sampling generator and line outage state transitions. + +**threaded**: A boolean value defaulting to `true`. If `true`, PRAS will parallelize simulations across the number of threads available to Julia. Setting this to `false` can help with debugging if an assessment is hanging. + +**verbose**: A boolean value defaulting to `false`. If `true`, +PRAS will output informative text describing the progress of the assessment. + +## References + +Billinton, R. (1970). Power System Reliability Evaluation. Gordon and Breach. + +Evans, M. P., Tindemans, S. H., & Angeli, D. (2019). Minimizing Unserved Energy Using Heterogeneous Storage Units. IEEE Transactions on Power Systems, 34(5), 3647-3656. + +Haringa, G. E., Jordan, G. A., & Garver, L. L. (1991). Application of Monte Carlo simulation to multi-area reliability evaluations. IEEE Computer Applications in Power, 4(1), 21-25. diff --git a/docs/src/PRAS/sysmodelspec.md b/docs/src/PRAS/sysmodelspec.md new file mode 100644 index 00000000..f92d1a8f --- /dev/null +++ b/docs/src/PRAS/sysmodelspec.md @@ -0,0 +1,219 @@ +# [System Model Specification](@id system_specification) + + +Assessing the resource adequacy of a power system requires a description +of the various resources available to that system, as well as its requirements +for serving load. In PRAS, this involves representing the +system's supply, storage, transmission, and demand characteristics in a specific +data format. This information is stored in memory as a `SystemModel` +Julia data structure, and on disk as an HDF5-formatted file with a +`.pras` file extension. Loading the system data from disk to memory is +accomplished via the following Julia code: + +```julia +using PRAS +sys = SystemModel("filepath/to/mysystem.pras") +``` + +A full technical specification of the `.pras` storage format is +available in the PRAS source code repository. +Storing system data in this format ensures that it will remain +readable in the future, even if PRAS' in-memory data representation changes. +Newer versions of the PRAS package are always able to read +`.pras` files created for older versions. + +An in-memory `SystemModel` data structure can also be written back to +disk: + +```julia +savemodel(sys, "filepath/to/mynewsystem.pras") +``` + +PRAS simulates simplified power system operations over one or +more consecutive time periods. The number of time periods to model and the +temporal duration of a single time period are specified on a per-system basis, +and must be consistent with provided starting and ending timestamps (defined +with respect to a specific time zone). + +When working with multiple years of +weather data, a user may wish to create separate system models and perform runs +for each year independently, or create a single system model containing the +full multi-year dataset. The first approach can be useful for studying +inter-annual variability of annual risk metrics using only the built-in +methods -- while this is also possible with a single multi-year run, it +requires some additional post-processing work. + +PRAS represents a power system as one or more **regions**, each containing +zero or more **generators**, **storages**, and +**generator-storages**. **Interfaces** contain **lines** and +allow power transfer between two regions. The table below summarizes +the characteristics of the different resource types (generators, storages, +generator-storages, and lines), and the +remainder of this section provides more details about each resource type +and their associated resource collections (regions or interfaces). + +| Parameter | Generator | Storage | Generator-Storage | Line | +|-----------|-----------|---------|-------------------|------| +| *Associated with a(n)...* | *Region* | *Region* | *Region* | *Interface* | +| *Name* | • | • | • | • | +| *Category* | • | • | • | • | +| Generation Capacity | • | | | | +| Inflow Capacity | | | • | | +| Charge Capacity | | • | • | | +| Discharge Capacity | | • | • | | +| Energy Capacity | | • | • | | +| Charge Efficiency | | • | • | | +| Discharge Efficiency | | • | • | | +| Carryover Efficiency | | • | • | | +| Grid Injection Capacity | | | • | | +| Grid Withdrawal Capacity | | | • | | +| Forward Transfer Capacity | | | | • | +| Backward Transfer Capacity | | | | • | +| Available→Unavailable Transition Probability | • | • | • | • | +| Unavailable→Available Transition Probability | • | • | • | • | + +*Table: PRAS resource parameters. Parameters in italic are fixed values; all others are provided as a time series.* + +The [system model exploration](@ref explore_systemmodel) section in the PRAS walkthrough +example shows different ways to retrieve and visualize information from these assets. + +## Regions +PRAS does not represent the power system's individual electrical buses. Intead, +PRAS **regions** are used to represent a collection of electrical buses +that are grouped together for resource adequacy assessment purposes. Power +transfer between buses within a single PRAS region is assumed to take place on +a "copper sheet" with no intraregional transfer limits or line reliability +limitations considered. + +In a PRAS system representation, each region is associated with a descriptive +name and an aggregate load time series, representing the total real power +demand across all buses in the region, for every simulation period defined +by the model. + +## Generators + +Electrical supply resources with no modeled energy constraints (e.g., a thermal +generator that can never exhaust its fuel supply) are represented in PRAS as +**generators**. Generators are the simplest supply resource modeled in +PRAS. In addition to a descriptive name and category, each generator unit is +associated with a time series of maximum generating capacity. This +time series can be a simple constant value (e.g., for a thermal plant) or can +change in any arbitrary manner (e.g., for a solar PV array). Each generator is +associated with a single PRAS region. + +For each period of an operations simulation, each generator takes on one of +two possible availability states. If the unit is available, it is capable of +injecting power up to its maximum generation capacity (for that time period) in +its associated region. If the unit is unavailable (representing some kind of +unplanned or forced outage), it is incapable of injecting any power into the +system. Between time periods, the unit may randomly transition to the +opposite state according to unit-specific state transition probabilities. Like +maximum available capacity, these transition probabilities are represented as +time series, and so may be different during different time periods. + +```@raw html +
+Relations between power and energy parameters for generator, storage, and generator-storage resources. +
Relations between power and energy parameters for generator, storage, and generator-storage resources
+
+``` + +## Storages + +Resources that can shift electrical power availability forward in time but do +not provide an overall net addition of energy into the system (e.g., a battery) +are referred to as **storages** in PRAS. Like generators, storages are +associated with descriptive name and category metadata. +Each storage unit has both a charge and discharge capacity time series, representing the +device's maximum ability to withdraw power from or inject power into the grid +at a given point in time (as with generator capacity, these values may remain +constant over the simulation or may vary to reflect external constraints). + +Storage units also have a maximum energy capacity time series, reflecting the +maximum amount of dischargeable energy the device can hold at a given point in +time (increasing or decreasing this value will change the duration of time for +which the device could charge or discharge at maximum power). The storage's +state of charge increases with charging and decreases with +discharging, and must always remain between zero and the maximum energy +capacity in that time period. The energy flow relationships between +these capacities are depicted visually in the figure above. + +If a storage device is charged and the maximum +energy capacity decreases such that the state of charge exceeds the energy +limit, the additional energy is automatically "spilled" (the surplus energy +is not injected into the grid, but simply vanishes from the system). + +Storage units may incur losses when moving energy into or out of the device +(charge and discharge efficiency), or forward in time (carryover efficiency). +When charging the unit, the effective increase to the state +of charge is determined by multiplying the charging power by the charge +efficiency. Similarly, when discharging the unit, the effective decrease to the +state of charge is calculated by dividing the discharge power by the discharge +efficiency. The available state of charge in the next time +period is determined by multiplying the state of charge in the current time +period by the carryover efficiency. + +Just as with generators, storages may be in available or unavailable states, and +move between these states randomly over time, according to provided state +transition probabilities. Unavailable storages cannot inject power into or +withdraw power from the grid, but they do maintain their energy state of charge +during an outage (minus any carryover losses occuring over time). + +## Generator-Storages + +Resources that add net new energy into the system but can also move that +energy forward in time instead of injecting it immediately (see figure below for examples) are referred to as +**generator-storages** in PRAS. As the name suggests, they combine the +characteristics of both generator and storage devices into a single unit. + +As with generator and storage units, generator-storages have associated +name and category metadata, and two availability states with random +transition probabilities. They have a potentially time-varying maximum inflow +capacity (representing potential new energy being added to the system and +analogous to the generator's maximum generating capacity) as well as +all the power and energy capacity and efficiency parameters associated with +storages. They also have separate maximum grid injection and withdrawal capacity time series, +reflecting the fact that (for example) they may not be able to discharge their +internal storage at full capacity while simultaneously injecting their full +exogenous energy inflow to the grid. The energy flow relationships between +these capacities are depicted visually in the figure above. + +A generator-storage in the unavailable state can neither charge nor discharge +its storage, nor send energy inflow to the grid. Like storage, it does retain +its state of charge during outages (subject to carryover losses). + + +```@raw html +
+Example applications of the generator-storage resource type +
Example applications of the generator-storage resource type
+
+``` + +## Interfaces + +**Interfaces** define a potential capability to directly exchange power +between two regions. Any set of two regions can have at most one interface +connecting them. Each interface has both a "forward" and "backward" +time-varying maximum transfer capability: the maximum "forward" transfer +capability refers to the largest amount of total net power that can be moved +from the first region to the second at a given point of time. Similarly, the +maximum "backward" transfer capability refers to the largest amount of total +net power that can be moved from the second region to the first. + +## Lines + +Individual **lines** are assigned to a single specific interface and +enable moving power between the two regions defined by that interface. Like +other resources, a line is associated with name and category metadata, and +transitions randomly between two availability states according to potentially +time-varying transition probabilities. Like interfaces, lines have a +potentially time-varying "forward" and "backward" transfer capability, where the forward and backward directions +match those associated with the line's interface. + +The total interregional transfer +capability of an interface in a given direction is the lower of either the +sum of transfer limits of available lines in that interface, or the +interface-level transfer limit. A line in the unavailable state cannot move +power between regions, and so does not contribute to the corresponding +interface's sum of line-level transfer limits. diff --git a/docs/src/PRASCapacityCredits/api.md b/docs/src/PRASCapacityCredits/api.md new file mode 100644 index 00000000..3e0cb82f --- /dev/null +++ b/docs/src/PRASCapacityCredits/api.md @@ -0,0 +1,4 @@ +```@autodocs +Modules = [PRASCapacityCredits] +Order = [:function,:type] +``` diff --git a/docs/src/PRASCapacityCredits/index.md b/docs/src/PRASCapacityCredits/index.md new file mode 100644 index 00000000..16da55fe --- /dev/null +++ b/docs/src/PRASCapacityCredits/index.md @@ -0,0 +1,10 @@ +# PRAS Capacity Credits + +PRASCapacityCredits.jl provides functionality for calculating capacity credits of different resource types within a power system. This package integrates with PRASCore for resource adequacy analysis and capacity credit calculations. + +This package includes: +- Methods for calculating capacity credits using various methodologies +- Integration with PRASCore system models and results +- Analysis tools for understanding resource contributions to reliability + +For detailed information on available methods and types, see the [API Reference](./api.md). diff --git a/docs/src/PRASCore/api.md b/docs/src/PRASCore/api.md new file mode 100644 index 00000000..4cb5426c --- /dev/null +++ b/docs/src/PRASCore/api.md @@ -0,0 +1,41 @@ +# PRASCore API reference + +## Systems +```@docs +PRASCore.Systems.SystemModel +PRASCore.Systems.Regions +PRASCore.Systems.Generators +PRASCore.Systems.Storages +PRASCore.Systems.GeneratorStorages +PRASCore.Systems.Lines +PRASCore.Systems.Interfaces +``` + +## Simulations +```@autodocs +Modules = [PRASCore.Simulations] +Order = [:function,:type] +``` + +## Results +```@docs +PRASCore.Results.LOLE +PRASCore.Results.EUE +PRASCore.Results.NEUE +PRASCore.Results.Shortfall +PRASCore.Results.ShortfallSamples +PRASCore.Results.Surplus +PRASCore.Results.SurplusSamples +PRASCore.Results.Flow +PRASCore.Results.FlowSamples +PRASCore.Results.Utilization +PRASCore.Results.UtilizationSamples +PRASCore.Results.GeneratorAvailability +PRASCore.Results.GeneratorStorageAvailability +PRASCore.Results.GeneratorStorageEnergy +PRASCore.Results.GeneratorStorageEnergySamples +PRASCore.Results.StorageAvailability +PRASCore.Results.StorageEnergy +PRASCore.Results.StorageEnergySamples +PRASCore.Results.LineAvailability +``` diff --git a/docs/src/PRASCore/index.md b/docs/src/PRASCore/index.md new file mode 100644 index 00000000..a27694a0 --- /dev/null +++ b/docs/src/PRASCore/index.md @@ -0,0 +1 @@ +# PRAS Core \ No newline at end of file diff --git a/docs/src/PRASFiles/api.md b/docs/src/PRASFiles/api.md new file mode 100644 index 00000000..607c4be2 --- /dev/null +++ b/docs/src/PRASFiles/api.md @@ -0,0 +1,5 @@ +```@autodocs +Modules = [PRASFiles] +Order = [:function,:type] +Private = false +``` diff --git a/docs/src/PRASFiles/index.md b/docs/src/PRASFiles/index.md new file mode 100644 index 00000000..9ed9f195 --- /dev/null +++ b/docs/src/PRASFiles/index.md @@ -0,0 +1,10 @@ +# PRAS Files + +PRASFiles.jl provides functionality for reading and writing PRAS-specific file formats, allowing you to save and load resource adequacy data structures. + +This package enables: +- Loading and saving PRAS system models +- Importing data from various formats +- Exporting results and system information to standard formats + +For detailed information on available methods and types, see the [API Reference](./api.md). diff --git a/SystemModel_HDF5_spec.md b/docs/src/SystemModel_HDF5_spec.md similarity index 95% rename from SystemModel_HDF5_spec.md rename to docs/src/SystemModel_HDF5_spec.md index a636d15f..ee0d3eb5 100644 --- a/SystemModel_HDF5_spec.md +++ b/docs/src/SystemModel_HDF5_spec.md @@ -1,19 +1,21 @@ -_Note: A useful reference for HDF5 file structure concepts is the -[HDF5 Glossary](https://portal.hdfgroup.org/display/HDF5/HDF5+Glossary). -This document contains links to glossary entries to explain HDF5 terms when used -for the first time._ - -# `SystemModel` HDF5 representation specification +# [`SystemModel` HDF5 representation specification](@id prasfile) This document specifies a representation of the PRAS `SystemModel` data -structure in terms of -[objects](https://portal.hdfgroup.org/display/HDF5/HDF5+Glossary#HDF5Glossary-Object) -in an HDF5 file. This specification is version-controlled in the same +structure in terms of objects like +[groups](https://support.hdfgroup.org/documentation/hdf5/latest/_h5_g__u_g.html#subsec_group_descr), +[datatypes](https://support.hdfgroup.org/documentation/hdf5/latest/_h5_t__u_g.html#sec_datatype), and +[datasets](https://support.hdfgroup.org/documentation/hdf5/latest/_h5_d__u_g.html#sec_dataset) +in an [HDF5 file](https://support.hdfgroup.org/documentation/hdf5/latest/_l_b_file_org.html). This specification is version-controlled in the same repository as the `SystemModel` source code: any time the `SystemModel` definition is changed, those updates are expected to be reconciled with this document as appropriate. The version of this specification should therefore be taken as the version of the package containing this file. +_Note: A useful reference for HDF5 file structure concepts is the +[HDF5 Glossary](https://support.hdfgroup.org/documentation/hdf5/latest/_g_l_s.html). +This document contains links to documentation entries to explain HDF5 terms when used +for the first time._ + ## Filename extension By convention, HDF5 file representations of a PRAS `SystemModel` struct are @@ -34,7 +36,7 @@ to generically as "resource collections". ### Root group attributes The HDF5 file must define seven -[attributes](https://portal.hdfgroup.org/display/HDF5/HDF5+Glossary#HDF5Glossary-Attribute) +[attributes](https://support.hdfgroup.org/documentation/hdf5/latest/_h5_d_m__u_g.html#subsubsec_data_model_abstract_attr) on the [root group](https://portal.hdfgroup.org/display/HDF5/HDF5+Glossary#HDF5Glossary-Rootgroup). There can also be additional attributes describing the system, including data descriptors used @@ -139,7 +141,7 @@ take: ### Resource / resource collection data The file may define the following six -[groups](https://portal.hdfgroup.org/display/HDF5/HDF5+Glossary#HDF5Glossary-Group) +[groups](https://support.hdfgroup.org/documentation/hdf5/latest/_h5_g__u_g.html) as children of the root group. At least two groups are mandatory. The file must include: @@ -169,7 +171,7 @@ paragraphs. Each group contains one `_core` dataset providing static parameters and/or relations for the group entities, in the form of a vector / one-dimensional array of -[compound datatype](https://portal.hdfgroup.org/display/HDF5/HDF5+Glossary#HDF5Glossary-Compounddatatype) +[compound datatype](https://support.hdfgroup.org/documentation/hdf5/latest/_g_l_s.html) instances. These datasets may use HDF5's automatic compression features to reduce filesize. diff --git a/docs/src/changelog.md b/docs/src/changelog.md new file mode 100644 index 00000000..acc22738 --- /dev/null +++ b/docs/src/changelog.md @@ -0,0 +1,58 @@ +# Changelog + +## [Unreleased] +- Initial changelog created. + +## [0.8.0], 2025 - September + + +## [0.7], 2024 - December +- PRAS codebase refactored as a monorepo, and subpackages `PRAS.jl`, `PRASCore.jl`, `PRASFiles.jl`, and `PRASCapacityCredits.jl` available through the Julia General registries. +- Removed `Convolution` and `NonSequentialMonteCarlo` simulation capabilities to simplify codebase. +- Bump Julia version required to `v1.10` +- Add results serialization capability for `ShortfallResults` + +## [0.6], 2021 - May +- Major updates to the results interface, including for capacity credit and SequentialMonteCarlo. +- Refactored and simplified metric types. +- Added and tested new modular result specifications. +- Updated capacity credit calculations and de-duplicated specification keyword defaults. +- Added tests for savemodel and support for exporting .pras files. +- Replaced Travis CI with GitHub Actions for continuous integration. +- Improved compatibility with HDF5.jl and ensured non-negative storage state of charge. +- Enforced interface-level limits and forced outages for storage and generator-storage. +- Removed unused test files and updated the README with a code coverage badge. + +This version is availabe at the NREL Julia registries or through source code available +in the [`master`](https://github.com/NREL/PRAS/tree/master) branch of the PRAS github +repository. Documentation for this version is available [here] +(https://docs.nrel.gov/docs/fy21osti/79698.pdf). + + +## [0.5], 2020 - November +- Major refactor of simulation specs and result types. +- Added support for sequential and nonsequential simulation methods. +- Improved modularity of result specifications and metrics. +- Added support for capacity credit and spatiotemporal result specs. +- Improved documentation and test coverage. + +## [0.4], 2019 - July +- Added support for storage and generator-storage modeling. +- Improved network flow and copperplate simulation methods. +- Added new result types and improved performance. +- Refactored codebase for better modularity and maintainability. + +## [0.3], 2019 - April +- Added support for network result types and transmission modeling. +- Improved simulation performance and result extraction. +- Added new tests and documentation. + +## [0.2], 2019 - August +- Added tests and improved SystemModel constructor. +- Improved compatibility with Julia 1.0+. +- Added Travis CI integration and coverage reporting. + +## [0.1], 2018 - March +- Initial public release of PRAS. +- Basic resource adequacy simulation and result extraction. +- Initial documentation and test coverage. diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 100644 index 00000000..73684a89 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,16 @@ +# Contributing to PRAS + +Thank you for your interest in contributing to the Probabilistic Resource Adequacy Suite (PRAS) project! + +## How to Contribute + +- **Report Issues:** If you find a bug or have a feature request, please open an issue on the GitHub repository. +- **Submit Pull Requests:** Contributions are welcome via pull requests. Please ensure your code is well-documented and tested. +- **Documentation:** Improvements to documentation are always appreciated. Feel free to suggest edits or add new content. +- **Code Style:** Follow the existing code style and conventions used in the project. + +## Getting Help + +If you have questions or need help, please open an issue or start a discussion on GitHub. + +We appreciate your contributions to PRAS! diff --git a/docs/src/extending.md b/docs/src/extending.md new file mode 100644 index 00000000..31520f44 --- /dev/null +++ b/docs/src/extending.md @@ -0,0 +1,271 @@ +# [Extending PRAS](@id extending) + +PRAS provides opportunties for users to non-invasively build on its general +simulation framework by redefining how simulations are executed, augmenting +how results are reported, or both. This allows for customized analyses +without requiring the user to modify code in the main PRAS package or +implement their own model from scratch. + +To implement custom functionality, a user needs to define specific Julia data +structures as well as implement function methods that operate on those +structures. Julia's multiple dispatch functionality can then identify and use +these newly defined capabilities when the `assess` function is invoked +appropriately. + +## [Custom Simulation Specifications](@id customsimspec) + +Custom simulation specifications allow for redefining how PRAS models system +operations. In addition to the data structures and methods listed here, +defining a new simulation specification also requires defining the appropriate +simulation-result interactions (see [Simulation-Result Interfaces](#simulation-result-interfaces)). + +### New Data Structure Requirements + +The following new data structure (struct / type) should be defined in Julia: + +#### Simulation Specification + +The main type representing the new simulation specification. It should be a +subtype of the `SimulationSpec` abstract type and can contain +fields that store simulation parameters (such as the number of Monte Carlo +samples to run or the random number generation seed to use). For example: + +```julia +struct MyCustomSimSpec <: SimulationSpec + nsamples::UInt64 + seed::UInt64 +end +``` + +### New Method Requirements + +The following new function method should be defined in Julia: + +#### assess + +The method to be invoked when the `assess` function is called with the +previously defined simulation specification. By convention, the method should +take a `SystemModel` as the first argument, followed by a specific subtype of +`SimulationSpec`, followed by one or more unspecified subtypes of +`ResultSpec`. For example (using the `MyCustomSimSpec` type +defined above): + +```julia +function PRAS.assess( + sys::SystemModel, simspec::MyCustomSimSpec, resultspecs::ResultSpec...) + + # Implement the simulation logic for MyCustomSimSpec here + + # This will include simulation-result interaction calls to result + # recording methods, which will need to be implemented by any result + # specification wanting to be compatible with MyCustomSimSpec + +end +``` + +## Custom Result Specifications + +Custom result specifications allow for saving out additional information that +may be generated during simulations of system operations. In addition to the +data structures and methods listed here, defining a new result specification +also requires defining the appropriate simulation-result interactions (see +[Simulation-Result Interfaces](#simulation-result-interfaces)). + +### New Data Structure Requirements +The following new data structures (structs / types) should be defined in Julia: + +#### Result Specification + +The main type representing the result specification. It should be a subtype of +the `ResultSpec` abstract type and can contain fields that store result +parameters (although this is usually not necessary). For example: + +```julia +struct MyCustomResultSpec <: ResultSpec +end +``` + +#### Result + +The type of the data that is returned at the end of an assessment and stores +any information to be reported to the end-user. It should be a subtype of the +`Result` abstract type and should contain fields that store the desired +results. For example: + +```julia +struct MyCustomResult <: Result + myoutput1::Float64 + myoutput2::Vector{Bool} +end +``` + +### New Method Requirements + +#### Indexing + +Result data should support index lookups to report overall results or values +for specific time periods, regions, interfaces, units, etc. +The specifics of how the result data is indexed will depend on the nature of +the result type, but will likely involve implementing one of more of the +following methods (here we assume the new result type is +`MyCustomResult`): + +```julia +Base.getindex(result::MyCustomResult) +Base.getindex(result::MyCustomResult, region_or_unit::String) +Base.getindex(result::MyCustomResult, interface::Pair{String,String}) +Base.getindex(result::MyCustomResult, period::ZonedDateTime) +Base.getindex(result::MyCustomResult, + region_or_unit::String, period::ZonedDateTime) +Base.getindex(result::MyCustomResult, + interface::Pair{String,String}, period::ZonedDateTime) +``` + +#### Risk Metrics + +If the result includes information that can be used to calculate resource +adequacy metrics, some or all of following new function methods should be +defined (here we assume the new result type is `MyCustomResult`): + +```julia +PRAS.LOLE(result::MyCustomResult) +PRAS.LOLE(result::MyCustomResult, region::String) +PRAS.LOLE(result::MyCustomResult, period::ZonedDateTime) +PRAS.LOLE(result::MyCustomResult, region::String, period::ZonedDateTime) + +PRAS.EUE(result::MyCustomResult) +PRAS.EUE(result::MyCustomResult, region::String) +PRAS.EUE(result::MyCustomResult, period::ZonedDateTime) +PRAS.EUE(result::MyCustomResult, region::String, period::ZonedDateTime) +``` + +If desired, new result specifications may define additional result-specific +accessor methods as well. + +## Simulation-Result Interfaces + +Result specifications need a way to map information produced by a simulation +to outcomes of interest. The specifics of how this is implemented will vary +between simulation specifications, but in general, a specific `assess` +method will invoke another method that records abstract results. This +recording method will then be implemented by all of the concrete result +specifications wishing to support that simulation specification. A very +simplified example of this pattern is: + +```julia +function assess( + sys::SystemModel, simspec::MyCustomSimSpec, resultspecs::ResultSpec...) + + # Implement the simulation logic for MyCustomSimSpec here, + # and collect full results + simulationdata = ... + + # Store requested results + results = () + for resultspec in resultspecs + results = (results..., record(simspec, resultspec, simulationdata)) + end + + return results + +end + +function record( + simspec::MyCustomSimSpec, resultspec::Shortfall, simulationdata) + # Map simulationdata to shortfall results here + return ShortfallResult(...) +end + +function record( + simspec::MyCustomSimSpec, resultspec::Surplus, simulationdata) + # Map simulationdata to surplus results here + return SurplusResult(...) +end + +function record( + simspec::MyCustomSimSpec, resultspec::MyCustomResultSpec, simulationdata) + # Map simulationdata to my custom results here + return MyCustomResult(...) +end +``` + +By implementing the types and methods described here, a new result +specification can be made compatible with these existing simulation types. In +each case, we assume the `MyResultSpec <: ResultSpec` and +`MyResult <: Result` types have been previously defined. + +### Result Accumulator + +A sequential Monte Carlo result accumulator incrementally collects relevant +intermediate outcomes as chronological simulations under different random +samples are performed. + +```julia +# Define the accumulator structure +struct SMCMyResultAccumulator <: ResultAccumulator{SequentialMonteCarlo,MyResultSpec} + # fields for holding intermediate data go here +end + +# Help PRAS know which accumulator type to expect before one's created +PRAS.ResourceAdequacy.accumulatortype(::SequentialMonteCarlo, ::MyResultSpec) = + SMCMyResultAccumulator + +# Initialize a new accumulator +function PRAS.ResourceAdequacy.accumulator( + sys::SystemModel, simspec::SequentialMonteCarlo, resultspec::MyResultSpec) + return SMCMyResultAccumulator(...) +end +``` + +#### record! + +Once system operations in a given time period `t` have been simulated +within a given chronological sample sequence `s`, the `record!` +method extracts outcomes of interest from one or both of the system's +current `state` and the solution to the period's dispatch problem +`prob`. These results are used to update the accumulator `acc` +in-place. + +```julia +PRAS.ResourceAdequacy.record!( + acc::SMCMyResultAccumulator, sys::SystemModel, state::SystemState, + prob::DispatchProblem, s::Int, t::Int) +``` + +#### reset! + +At the end of each chronological sequence of time periods `s`, the +`reset!` method updates the accumulator `acc` in-place to +finalize recording of any results requiring information from multiple periods, +and prepare the accumulator to start receiving values from a new chronological +simulation sequence. + +```julia +PRAS.ResourceAdequacy.reset!(acc::SMCMyResultAccumulator, s::Int) + +# Often no action is required here, +# so a simple one-line implementation is possible +PRAS.ResourceAdequacy.reset!(acc::SMCMyResultAccumulator, s::Int) = nothing +``` + +#### merge! + +For multithreaded assessments PRAS creates one accumulator per worker thread (parallel task) and merges each thread's accumulator information togther once work is completed. `merge!` defines how an accumulator `a` should be updated in-place to incorporate the results obtained by another accumulator `b`. + +```julia +PRAS.ResourceAdequacy.merge!( + a::SMCMyResultAccumulator, b::SMCMyResultAccumulator) +``` + +#### finalize! + +Once all of the thread accumulators have been merged down to a single accumulator reflecting results from all of the threads, this final accumulator `acc` is mapped to the final result output through a `finalize` method. + +```julia +function PRAS.ResourceAdequacy.finalize( + acc::SMCMyResultAccumulator, sys::SystemModel) + + return MyResult(...) + +end +``` diff --git a/docs/src/images/genstorexamples.svg b/docs/src/images/genstorexamples.svg new file mode 100644 index 00000000..bf213fa6 --- /dev/null +++ b/docs/src/images/genstorexamples.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Hydro Reservoir + + + + + + + + + +UpstreamInflow +Turbine +Pump + + + + +Battery + + + + + + + + + +PVPanel +[Bidirectional]Inverter + + + + + +Heat Reservoir + + + + + + + +CSPHeat +Turbine + + + + + + + +Fuel Stockpile + + + + + + + +FuelDelivery +Combustion + + + + + + + diff --git a/docs/src/images/inputoutput.svg b/docs/src/images/inputoutput.svg new file mode 100644 index 00000000..196c6dfd --- /dev/null +++ b/docs/src/images/inputoutput.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/images/networkflow.svg b/docs/src/images/networkflow.svg new file mode 100644 index 00000000..2cc6ab5c --- /dev/null +++ b/docs/src/images/networkflow.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +A + + + +B + + + +C + + +0 < g +A +< G +A + + +0 < g +B +< G +B + + +0 < g +C +< G +C +F +BA +< f +AB +< F +AB + + + + + + + + + + + + + + + + + + + + +Load + + +Load + + +Load + + diff --git a/docs/src/images/resourceparameters.svg b/docs/src/images/resourceparameters.svg new file mode 100644 index 00000000..222b3a0f --- /dev/null +++ b/docs/src/images/resourceparameters.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + +1 + + + + + + + + + + + + + +4 +5 +3 +2 +6 +GeneratorStorage +Inflow Capacity (1) +ChargeCapacity(2) +Discharge Capacity (3) +GridInjection Capacity (4) +GridWithdrawalCapacity (5) +Energy Capacity (6) + +Storage +Charge Capacity (1) +Discharge Capacity (2)Energy Capacity (3) + + + + + + +3 +1 +2 + +Generator +Generation Capacity (1) + + +1 + + + + diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 00000000..0c0f515d --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,33 @@ +# PRAS + +The Probabilistic Resource Adequacy Suite (PRAS) provides an open-source, research-oriented collection of tools for analysing the [resource adequacy](@ref resourceadequacy) of a +bulk power system. It allows the user to simulate power system operations under a wide range of operating conditions in order to study the risk of failing to meet demand (due to a lack of supply or deliverability), and identify the time periods and regions in which that risk occurs. It offers high-performance sequential Monte Carlo methods supporting multi-region composite reliability assessment, including simulation of energy-limited resources such as storage. + +PRAS is developed and maintained at the US +[National Renewable Energy Laboratory](https://www.nrel.gov/) (NREL). + +To get started on using PRAS, see the [installation](@ref Installation) and [quick start](@ref quickstart) pages. + +## Basic usage + +PRAS maps a provided representation of a power system to a probabilistic description of operational outcomes of interest, using a particular choice of operations simulation. The input system representation is called a "[system model](@ref system_specification)", the choice of operational representation is referred to as a "[simulation specification](@ref simulations)", and different types of operating outcomes of interest are described by "[result specifications](@ref results)". + +```@raw html +
+PRAS model structure and corresponding assessment function arguments +
PRAS model structure and corresponding assessment function arguments
+
+``` + +PRAS is written in the Julia programming language, and is controlled through the use of Julia scripts. The three components of a PRAS resource adequacy assessment (a system model, a simulation specification, and result specifications) map directly to the Julia function arguments required to launch a PRAS run. A typical resource adequacy assessment with PRAS involves creating or loading a system model, then invoking PRAS' `assess` function to perform the analysis: + +```julia +using PRAS + +sys = SystemModel("filepath/to/mysystem.pras") + +shortfallresult, flowresult = + assess(sys, SequentialMonteCarlo(), Shortfall(), Flow()) + +eue, lole = EUE(shortfallresult), LOLE(shortfallresult) +``` diff --git a/docs/src/installation.md b/docs/src/installation.md new file mode 100644 index 00000000..aa460bcd --- /dev/null +++ b/docs/src/installation.md @@ -0,0 +1,20 @@ +# Installation + +PRAS is written in the [Julia](https://julialang.org/) numerical programming +language. If you haven't already, your first step should be to install Julia. +Instructions are available at +[julialang.org/downloads](https://julialang.org/downloads/). + +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. + +From the main Julia prompt, type `]` to enter the package management REPL. +The prompt should change from `julia>` to something like `(v1.10) pkg>` +(your version number may be slightly different). +Type (or paste) the following (minus the `pkg>` prompt) +``` +pkg> add PRAS +``` + +This will automatically install the PRAS Julia module and all of its +related dependencies. At this point you can hit Backspace to switch back to the +main `julia>` prompt. \ No newline at end of file diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md new file mode 100644 index 00000000..5ef5dea2 --- /dev/null +++ b/docs/src/quickstart.md @@ -0,0 +1,172 @@ +# [Getting Started with PRAS](@id quickstart) + +Once you have PRAS [installed](@ref Installation), this page provides a brief overview to help you start running PRAS quickly. A more detailed example with analyses is available at [PRAS walkthrough](@ref pras_walkthrough). + +## Parallel Processing + +PRAS uses multi-threading, so be sure to set the environment variable controlling the number of threads available to Julia (36 in this Bash example, which is a good choice for NREL Eagle nodes - on a laptop you would probably only want 4 or so) before running scripts or launching the REPL: + +```sh +export JULIA_NUM_THREADS=36 +``` + +## Power System Data + +The recommended way to store and retrieve PRAS system data is in an HDF5 file that conforms to the [PRAS system data format](./SystemModel_HDF5_spec.md). Once your system is represented in that format you can load it into PRAS with: + +```julia +using PRAS +sys = SystemModel("filepath/to/systemdata.pras") +``` + +## Resource Adequacy Assessment + +PRAS functionality is distributed across groups of modular specifications that can be mixed, extended, or replaced to support the needs of a particular analysis. When assessing reliability or capacity value, one can define the specs to be used while passing along any associated parameters or options. + +The categories of specifications are: + +**Simulation Specifications**: How should power system operations be simulated? You can use the `SequentialMonteCarlo` specification. + +**Result Specifications**: What kind and level of detail of results should be saved out during simulations? Options include `Shortfall`, `Surplus`, interface `Flow`, `StorageEnergy`, and `GeneratorStorageEnergy`. See [Result specification](./PRAS/results.md) for more details. + +**Capacity Credit Specifications**: If performing a capacity credit calculation, which method should be used? Options are `EFC` and `ELCC`. + +### Running an analysis + +Analysis centers around the `assess` method with different arguments passed depending on the desired analysis to run. For example, if you instead want to run a simulation that considers energy-limited resources and transmission constraints, using 100,000 Monte Carlo samples, the method call becomes: + +```julia +assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall()) +``` + +You can request multiple kinds of result from a single assessment: + +```julia +assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall(), Flow()) +``` + +### Querying Results + +Each result type requested returns a seperate result object. These objects can then be queried using indexing syntax to extract values of interest. + +```julia +shortfalls, flows = + assess(mysystemmodel, SequentialMonteCarlo(samples=100_000), Shortfall(), Flow()) + +flows["Region A" => "Region B"] +``` + +The full PRAS documentation provides more details on how different result object types can be indexed. + +Because the main results of interest from resource adequacy assessments are probabilistic risk metrics (i.e. EUE and LOLE), `Shortfall` result objects +define some additional methods: a particular risk metric can be obtained by calling that metric's constructor with the `Shortfall` result object. For example, to obtain the system-wide LOLE and EUE over the simulation period: + +```julia +lole = LOLE(shortfalls) +eue = EUE(shortfalls) +``` + +Single-period metrics can also be extracted. For example, to get system-wide EUE for April 27th, 2024 at 1pm EST: + +```julia +period_eue = EUE(shortfalls, ZonedDateTime(2024, 4, 27, 13, tz"EST")) +``` + +Region-specific metrics can also be extracted. For example, to obtain the LOLE of Region A across the entire simulation period: + +```julia +eue_a = LOLE(shortfalls, "Region A") +``` + +Finally, metrics can be obtained for both a specific region and time: + +```julia +period_eue_a = EUE(shortfalls, "Region A", ZonedDateTime(2024, 4, 27, 13, tz"EST")) +``` + +## Capacity Credit Calculations + +Capacity credit calculations build on probabilistic resource adequacy assessment to provide capacity-based quantifications of the marginal benefit to system resource adequacy associated with a specific resource or collection of resources. Two capacity credit metrics (EFC and ELCC) are currently supported. + +### Equivalent Firm Capacity (EFC) + +`EFC` estimates the amount of idealized, 100%-available capacity that, when added to a baseline system, reproduces the level of system adequacy associated +with the baseline system plus the study resource. The following parameters must be specified: + + - The risk metric to be used for comparison (i.e. EUE or LOLE) + - A known upper bound on the EFC value (usually the resource's nameplate + capacity) + - The regional distribution of the firm capacity to be added. This is + typically chosen to match the regional distribution of the study resource's + nameplate capacity. + +For example, to assess the EUE-based EFC of a new resource with 1000 MW nameplate capacity, added to the system in a single region named "A": + +```julia +# The base system, with power units in MW +base_system + +# The base system augmented with some incremental resource of interest +augmented_system + +# Get the lower and upper bounds on the EFC estimate for the resource +efc = assess( + base_system, augmented_system, EFC{EUE}(1000, "A"), + SequentialMonteCarlo(samples=100_000)) +min_efc, max_efc = extrema(efc) +``` + +If the study resource were instead split between regions "A" (600MW) and "B" (400 MW), one could specify the firm capacity distribution as: + +```julia +efc = assess( + base_system, augmented_system, EFC{EUE}(1000, ["A"=>0.6, "B"=>0.4]), + SequentialMonteCarlo(samples=100_000)) +``` + +### Equivalent Load Carrying Capability (ELCC) + +`ELCC` estimates the amount of additional load that can be added to the system (in every time period) in the presence of a new study resource, while maintaining the baseline system's original adequacy level. The following parameters must be specified: + + - The risk metric to be used for comparison (i.e. EUE or LOLE) + - A known upper bound on the ELCC value (usually the resource's nameplate + capacity) + - The regional distribution of the load to be added. Note that this choice is + somewhat ambiguous in multi-region systems, so assumptions should be clearly + specified when reporting analysis results. + +For example, to assess the EUE-based ELCC of a new resource with 1000 MW nameplate capacity, serving new load in region "A": + +```julia +# The base system, with power units in MW +base_system + +# The base system augmented with some incremental resource of interest +augmented_system + +# Get the lower and upper bounds on the ELCC estimate for the resource +elcc = assess( + base_system, augmented_system, ELCC{EUE}(1000, "A"), + SequentialMonteCarlo(samples=100_000)) +min_elcc, max_elcc = extrema(elcc) +``` + +If instead the goal was to study the ability of the new resource to provide load evenly to regions "A" and "B", one could use: + +```julia +elcc = assess( + base_system, augmented_system, ELCC{EUE}(1000, ["A"=>0.5, "B"=>0.5]), + SequentialMonteCarlo(samples=100_000)) +``` + +### Capacity credit calculations in the presence of sampling error + +For non-deterministic assessment methods (i.e. Monte Carlo simulations), running a resource adequacy assessment with different random number generation seeds will result in different risk metric estimates for the same underlying system. Capacity credit assessments can be sensitive to this uncertainty, particularly when attempting to study the impact of a small resource on a large system with a limited number of simulation samples. + +PRAS takes steps to a) limit this uncertainty and b) warn against potential deficiencies in statistical power resulting from this uncertainty. + +First, the same random seed is used across all simulations in the capacity credit assessment process. If the number of resources and their reliability parameters (MTTF and MTTR) remain constant across the baseline and augmented test systems, seed re-use ensures that unit-level outage profiles remain identical across RA assessments, providing a fixed background against which to measure changes in RA resulting from the addition of the study resource. Note that satisfying this condition requires that the study resource be present in the baseline case, but with its contributions eliminated (e.g. by setting its capacity to zero). When implementing an assessment method that modifies the user-provided system to add new resources (such as EFC), the programmer should assume this invariance exists in the provided data, and not violate it in any automated modifications. + +Second, capacity credit assessments have two different stopping criteria. The ideal case is that the upper and lower bounds on the capacity credit metric converge to be sufficiently tight relative to a desired level of precision. This target precision is 1 system power unit (e.g. MW) by default, but can be relaxed to loosen the convergence bounds if desired via the `capacity_gap` keyword argument. Once the lower and upper bounds are tighter than this gap, their values are returned. + +Additionally, at each bisection step, a hypothesis test is performed to ensure that the theoretically-larger bounding risk metric is in fact larger than the smaller-valued risk metric with a specified level of statistical significance. By default, this criteria is a maximum p-value of 0.05, although this value can be changed as desired via the `p_value` keyword argument. If at some point the null hypothesis (the higher risk is not in fact larger than the lower risk) cannot be rejected at the desired significance level, the assessment will provide a warning indicating the size of the remaining capacity gap and return the lower and upper bounds on the capacity credit estimate. diff --git a/docs/src/resourceadequacy.md b/docs/src/resourceadequacy.md new file mode 100644 index 00000000..e2546579 --- /dev/null +++ b/docs/src/resourceadequacy.md @@ -0,0 +1,59 @@ +# [Resource Adequacy Background](@id resourceadequacy) + +An electrical power system is considered resource adequate if it has procured +sufficient resources (including supply, transmission, and responsive demand) +such that it runs a sufficiently low risk of invoking emergency measures (such +as involuntary load shedding) due to resource unavailability or deliverability +constraints. Resource adequacy +is a necessary (but not sufficient) condition for overall power system +reliability, which considers a broader set of system constraints including +operational flexibility and the stability of system voltages and frequency. + +Probabilistic resource adequacy assessment is the process by which resource +shortfall risk is quantified. It involves mapping quantified uncertainties in +system operating conditions (primarily forced outages of generators and lines) into +probability distributions for operating outcomes of interest by simulating +system operations under different probabilistically weighted +scenarios. The nature of those simulations varies between models, and can range +from simple snapshot comparisons of peak demand versus available supply, +through to chronological simulations of system dispatch and power flow over +the full operating horizon. + +The resulting outcomes can then be used to calculate industry-standard +[probabilistic risk metrics [1]](#references). + +**Expected Unserved Energy (EUE)** is the expected (average) total energy +shortfall over the study period. It may be expressed in energy units +(e.g. GWh per year) or normalized against the system's total energy demand and +expressed as a fraction (normalized EUE, or NEUE, expressed as a percentage or +in parts-per-million, ppm). + +**Loss-of-Load Expectation (LOLE)** is the expected (average) count of +periods experiencing shortfall over the study period. It is expressed in terms +of event-periods (e.g. event-hours per year, event-days per year). When +reported in terms of event-hours, LOLE is sometimes referred to as LOLH +(loss-of-load hours). + +While a system's shortfall risk can never be eliminated entirely, if these +risk metrics are assessed to be lower than some predetermined threshold, the +system is considered resource adequate. + +It can sometimes also be useful to express the average and/or incremental +contribution of a particular resource to overall system adequacy in terms of +capacity. This quantity (either in units of power, or as a fraction of the +unit's nameplate capacity) is known as the capacity credit (sometimes called +capacity value) of the resource. While many different methods are used to +estimate the capacity credit of a resource, the most rigorous approaches +generally involve assessing the change in probabilistic system adequacy +associated with adding or removing the resource from the system. As a result, +capacity credit calculation is often closely associated with probabilistic +resource adequacy assessment. + +## References + +1. de Mijolla, G., Bello, M., Danti Lopez, I., Entriken, R., Hytowitz, R., + Lannoye, E., Ranola, J.A., Roark, J., Tuohy, A., & Wang, Q. (2022). + [Resource adequacy for a decarbonized future: A summary of existing and + proposed resource adequacy metrics] + (https://www.epri.com/research/products/000000003002023230). + Tech. Rep. 3002023230, Electric Power Research Institute (EPRI). \ No newline at end of file