Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 71 additions & 0 deletions .github/workflows/PRASReport.jl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: PRASReport.jl tests
# Run on master, tags, or any pull request
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST)
push:
branches: [main]
tags: ["*"]
pull_request:
jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
version:
- "lts" # Latest LTS release, min supported
- "1" # Latest release
os:
- ubuntu-latest
- macOS-latest
- windows-latest
arch:
- x64
- aarch64
exclude:
- os: windows-latest
arch: aarch64
- os: ubuntu-latest
arch: aarch64
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: actions/cache@v4
env:
cache-name: cache-artifacts
with:
path: ~/.julia/artifacts
key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-
${{ runner.os }}-${{ matrix.arch }}-test-
${{ runner.os }}-${{ matrix.arch }}-
${{ runner.os }}-
- run: julia --project=PRASReport.jl -e 'import Pkg;
Pkg.develop([
(path="PRASCore.jl",),
(path="PRASFiles.jl",),
])'
shell: bash
- uses: julia-actions/julia-buildpkg@latest
with:
project: PRASReport.jl
- run: |
git config --global user.name Tester
git config --global user.email te@st.er
- uses: julia-actions/julia-runtest@latest
with:
project: PRASReport.jl
env:
JULIA_NUM_THREADS: 2
- uses: julia-actions/julia-processcoverage@v1
with:
directories: PRASReport.jl/src
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ Manifest.toml
*.DS_Store
*.pras
*.json

# Any html and duckdb files generated during testing
*.html
*.duckdb
3 changes: 3 additions & 0 deletions PRASCore.jl/src/Results/Results.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ EUE(x::AbstractShortfallResult, r::AbstractString, ::Colon) =
EUE(x::AbstractShortfallResult, ::Colon, ::Colon) =
EUE.(x, x.regions.names, permutedims(x.timestamps))

NEUE(x::AbstractShortfallResult, ::Colon, t::ZonedDateTime) =
NEUE.(x, x.regions.names, t)

NEUE(x::AbstractShortfallResult, r::AbstractString, ::Colon) =
NEUE.(x, r, x.timestamps)

Expand Down
6 changes: 6 additions & 0 deletions PRASCore.jl/src/Results/Shortfall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ function NEUE(x::ShortfallResult{N,L,T,E}, r::AbstractString) where {N,L,T,E}
return NEUE(div(MeanEstimate(x[r]..., x.nsamples),(sum(x.regions.load[i_r,:])/1e6)))
end

function NEUE(x::ShortfallResult{N,L,T,E}, r::AbstractString, t::ZonedDateTime) where {N,L,T,E}
i_r = findfirstunique(x.regions.names, r)
i_t = findfirstunique(x.timestamps, t)
return NEUE(div(MeanEstimate(x[r, t]..., x.nsamples),x.regions.load[i_r,i_t]/1e6))
end

function finalize(
acc::ShortfallAccumulator,
system::SystemModel{N,L,T,P,E},
Expand Down
21 changes: 21 additions & 0 deletions PRASReport.jl/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Alliance for Sustainable Energy, LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
29 changes: 29 additions & 0 deletions PRASReport.jl/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name = "PRASReport"
uuid = "c003f3f0-f5d5-4077-8f93-207e59ecb3ff"
authors = ["Hari Sundar <Hari.Sundar@nrel.gov>"]
version = "0.1.0"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
DuckDB = "d2f5444f-75bc-4fdf-ac35-56f514c445e1"
PRASCore = "c5c32b99-e7c3-4530-a685-6f76e19f7fe2"
PRASFiles = "a2806276-6d43-4ef5-91c0-491704cd7cf1"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"

[compat]
Dates = "1"
PRASCore = "0.8.0"
PRASFiles = "0.8.0"
StatsBase = "0.34"
TimeZones = "^1.14"
julia = "1.10"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"

[targets]
test = ["Test","Suppressor"]
13 changes: 13 additions & 0 deletions PRASReport.jl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# PRASReport.jl

## Usage
```julia
using PRASReport

create_pras_report(system_path="path_to_sys.pras", # Path to your .pras file
samples=100,seed=1, # Number of samples and random seed for analysis
report_name="report", # Name of the output HTML file
report_path=pwd(), # Path for HTML file output
threshold=0, # hourly EUE threshold to be considered part of event
title="Resource Adequacy Report") # Report title
```
16 changes: 16 additions & 0 deletions PRASReport.jl/TODOs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# TODO
0. Verify timezone handling is correct. SO CONFUSING!
1. Landing page shows most important characteristics like
- Number of events with total EUE exceeds the ## number
- Same with LOLE
- Top 1 ptile events
- Event summary graph (like the ESIG report)
2. Users from the webpage can specify which regions need to be aggregated to get combined metric
3. PRASReport exports an assess function which automatically runs assess with Shortfall(), Surplus(), Flows(), and Utilization()
4. Event selector creates a tab for an event to show regional shortfall and flows etc
5. Does NEUE have to be reported as MWh/MWh
6. Explore how a directed graph can be stored in DuckDB, and how it can be drawn on a webpage with WASM-DuckDB
7. Julia indentation
8. Better names for Shortfall_timeseries and Flow_timeseries?
9. Update glossary with event period definitions, for example it says EUE per year, instead it should say per event period etc.
10. Add a analysis narrative section to the report.
12 changes: 12 additions & 0 deletions PRASReport.jl/examples/run_report.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Revise
using PRAS
using PRASReport

rts_sys = rts_gmlc();
rts_sys.regions.load .+= 375;

sf,flow = assess(rts_sys,SequentialMonteCarlo(samples=100),Shortfall(),Flow());

create_pras_report(sf,flow, report_name="rts_report",
title="RTS-GMLC (load modified) RA Report",
threshold=1)
31 changes: 31 additions & 0 deletions PRASReport.jl/src/PRASReport.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module PRASReport

import PRASCore.Systems: SystemModel, Regions, Interfaces,
Generators, Storages, GeneratorStorages, Lines,
timeunits, powerunits, energyunits, unitsymbol,
unitsymbol_long
import PRASCore.Simulations: assess, SequentialMonteCarlo
import PRASCore.Results: EUE, LOLE, NEUE,
Shortfall, Flow,
ShortfallResult, FlowResult,
ShortfallSamplesResult, AbstractShortfallResult,
Result, MeanEstimate, findfirstunique,
val, stderror
import PRASFiles: SystemModel
import StatsBase: mean
import Dates: @dateformat_str, format, now, DateTime
import TimeZones: ZonedDateTime, @tz_str, TimeZone
import Base64: base64encode
import Tables: columntable
import DuckDB

export
Event, get_events, event_length,
Shortfall_timeseries, Flow_timeseries,
get_db, create_pras_report

include("events.jl")
include("writedb.jl")
include("report.jl")

end # module PRASReport
95 changes: 95 additions & 0 deletions PRASReport.jl/src/event_db_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
-- System and Simulation parameters
CREATE TABLE systemsiminfo (
timesteps INTEGER,
step_size INTEGER NOT NULL,
time_unit TEXT NOT NULL,
power_unit TEXT NOT NULL,
energy_unit TEXT NOT NULL,
start_timestamp TIMESTAMP WITHOUT TIME ZONE,
end_timestamp TIMESTAMP WITHOUT TIME ZONE,
timezone TEXT,
n_samples INTEGER,
eue_mean REAL NOT NULL,
eue_stderr REAL NOT NULL,
lole_mean REAL NOT NULL,
lole_stderr REAL NOT NULL,
neue_mean REAL NOT NULL,
neue_stderr REAL NOT NULL,
eventthreshold INTEGER NOT NULL,

-- Constraint to ensure valid ISO 8601 duration units
CONSTRAINT valid_time_unit CHECK (
time_unit IN ('Year', 'Day', 'Hour', 'Minute', 'Second')
)
);

-- Regions lookup table
CREATE TABLE regions (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);

-- Interfaces lookup table (region to region connections)
CREATE TABLE interfaces (
id INTEGER PRIMARY KEY,
region_from_id INTEGER REFERENCES regions(id),
region_to_id INTEGER REFERENCES regions(id),
name TEXT, -- name like "Region1->Region2"
UNIQUE(region_from_id, region_to_id)
);

-- Main events table (clean, no parameters)
CREATE SEQUENCE eventid_sequence START 1;
CREATE TABLE events (
id INTEGER PRIMARY KEY DEFAULT nextval('eventid_sequence'),
name TEXT NOT NULL,
start_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
end_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
time_period_count INTEGER NOT NULL -- N parameter
);

-- System-level metrics for each event (aggregated)
CREATE TABLE event_system_shortfall (
event_id INTEGER REFERENCES events(id),
lole REAL NOT NULL,
eue REAL NOT NULL,
neue REAL NOT NULL,
PRIMARY KEY (event_id)
);

-- Regional metrics for each event (aggregated)
CREATE TABLE event_regional_shortfall (
id INTEGER PRIMARY KEY,
event_id INTEGER REFERENCES events(id),
region_id INTEGER REFERENCES regions(id),
lole REAL NOT NULL,
eue REAL NOT NULL,
neue REAL NOT NULL,
UNIQUE(event_id, region_id)
);

-- Time-series metrics for each timestamp within an event (from sf_ts struct)
-- Optimized with better data types and ordering for columnar storage
CREATE TABLE event_timeseries_shortfall (
event_id INTEGER NOT NULL,
region_id INTEGER NOT NULL,
timestamp_value TIMESTAMP WITHOUT TIME ZONE NOT NULL,
lole REAL NOT NULL,
eue REAL NOT NULL,
neue REAL NOT NULL,
FOREIGN KEY (event_id) REFERENCES events(id),
FOREIGN KEY (region_id) REFERENCES regions(id),
PRIMARY KEY (event_id, region_id, timestamp_value)
);

-- Flow data for each timestamp within an event (from flow_ts struct)
-- Optimized with better data types and ordering for columnar storage
CREATE TABLE event_timeseries_flows (
event_id INTEGER NOT NULL,
interface_id INTEGER NOT NULL,
timestamp_value TIMESTAMP WITHOUT TIME ZONE NOT NULL,
flow REAL NOT NULL, -- Flow value (NEUE units)
FOREIGN KEY (event_id) REFERENCES events(id),
FOREIGN KEY (interface_id) REFERENCES interfaces(id),
PRIMARY KEY (event_id, interface_id, timestamp_value)
);
Loading
Loading