Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
84a6554
Begin documentation build with documenter.jl
sriharisundar Jan 9, 2025
a892cdb
Docs updates
sriharisundar Aug 6, 2025
42c3c3b
Add more docs
sriharisundar Aug 7, 2025
b20a60d
Try using devcontainer
sriharisundar Aug 8, 2025
5486351
Add Docker image to allow installing NREL certs
sriharisundar Aug 12, 2025
7d616c0
Add CI to generate docs
sriharisundar Aug 13, 2025
acdd359
Change branches from which Documentation action is run
sriharisundar Aug 13, 2025
7960187
Change package dev command in docs action
sriharisundar Aug 13, 2025
bb315b2
Change Julia version in docs action
sriharisundar Aug 13, 2025
46e0a8d
Change pkg dev command in docs action
sriharisundar Aug 13, 2025
0dde431
Change the devbranch
sriharisundar Aug 13, 2025
c2662cd
Add dosctrings for PRAS Core structs
sriharisundar Aug 14, 2025
924b1e1
Update documentation website structure, adding new elements
sriharisundar Aug 14, 2025
75fa976
Add example, minor changes with doc structure, refs etc.
sriharisundar Sep 2, 2025
4e2fcab
Add Generator docstring that got dropped during rebase
sriharisundar Sep 9, 2025
7252e9d
Update PRAS run through example with plots and outputs
sriharisundar Sep 24, 2025
d658283
Update walk through and reorg docs
sriharisundar Sep 25, 2025
099f8d1
Add docs dataframes dependency
sriharisundar Sep 25, 2025
6efc882
Wrap up walk through example
sriharisundar Sep 25, 2025
6be39b9
Make changes to runners, some docs outputs, add docs instructions
sriharisundar Sep 25, 2025
6ebb0a9
Add new docs badges, correct a docs bug
sriharisundar Sep 25, 2025
e9a5f93
Small docs/README change and remove old docs yaml
sriharisundar Sep 25, 2025
6e5e51e
Typo corrections
sriharisundar Sep 25, 2025
845b149
Use SVG images
sriharisundar Sep 25, 2025
71591c6
Change stable docs pointer to original PRAS website for now
sriharisundar Oct 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
37 changes: 37 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions .github/workflows/docsCleanup.yml
Original file line number Diff line number Diff line change
@@ -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 }}
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
Manifest.toml
*.DS_Store
*.pras
*.json
PRASFiles.jl/test/PRAS_Results_Export/

docs/build/
docs/site/
docs/src/examples/
178 changes: 178 additions & 0 deletions PRAS.jl/examples/pras_walkthrough.jl
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion PRASCore.jl/src/Simulations/Simulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 48 additions & 6 deletions PRASCore.jl/src/Systems/SystemModel.jl
Original file line number Diff line number Diff line change
@@ -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}
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion PRASCore.jl/src/Systems/Systems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export
unitsymbol, unitsymbol_long, conversionfactor, powertoenergy, energytopower,

# Main data structure
SystemModel,
SystemModel, get_params,

# Convenience re-exports
ZonedDateTime, @tz_str
Expand Down
Loading
Loading