Add non-contiguous timestamp support (multiple time slices)#110
Add non-contiguous timestamp support (multiple time slices)#110sriharisundar wants to merge 7 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #110 +/- ##
==========================================
+ Coverage 83.16% 83.17% +0.01%
==========================================
Files 45 46 +1
Lines 2429 2484 +55
==========================================
+ Hits 2020 2066 +46
- Misses 409 418 +9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds support for non-contiguous timestamp axes by representing a system’s time domain as multiple contiguous StepRange slices with gaps, while preserving existing “flat” indexing semantics across the concatenated timesteps. This spans core modeling (SystemModel + new SlicedTimestamps), result timestamp typing, and PRAS file read/write support for persisting slice metadata.
Changes:
- Introduces
SlicedTimestamps <: AbstractVector{ZonedDateTime}and addsSystemModelconstructors that accept aVector{StepRange}of slices. - Updates PRASFiles HDF5 metadata handling to optionally persist and reconstruct multi-slice timestamp axes.
- Widens result timestamp fields to support non-contiguous axes and adds tests + version bumps to 0.9.0 across packages.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| PRASFiles.jl/test/runtests.jl | Adds a non-contiguous slice round-trip + assess-across-gap testset. |
| PRASFiles.jl/src/Systems/write.jl | Writes slice metadata (n_slices, slice_*) for multi-slice systems. |
| PRASFiles.jl/src/Systems/read.jl | Reads optional slice metadata and rebuilds SlicedTimestamps; broadens supported file-version range. |
| PRASFiles.jl/src/PRASFiles.jl | Imports SlicedTimestamps from PRASCore for I/O logic. |
| PRASFiles.jl/Project.toml | Bumps PRASFiles to 0.9.0 and widens PRASCore compat to 0.9. |
| PRASCore.jl/test/Systems/SystemModel.jl | Adds SystemModel multi-slice construction/validation/printing tests. |
| PRASCore.jl/src/Systems/utils.jl | Introduces SlicedTimestamps and timestep(...) helper(s). |
| PRASCore.jl/src/Systems/Systems.jl | Exports/includes the new sliced-timestamp utilities. |
| PRASCore.jl/src/Systems/SystemModel.jl | Widens timestamps storage type and adds slice-based constructors; updates display and timestep assertion. |
| PRASCore.jl/src/Results/UtilizationSamples.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/Utilization.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/SurplusSamples.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/Surplus.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/StorageEnergySamples.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/StorageEnergy.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/StorageAvailability.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/ShortfallSamples.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/Shortfall.jl | Widens result timestamps field type and constructor signature. |
| PRASCore.jl/src/Results/LineAvailability.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/GeneratorStorageEnergySamples.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/GeneratorStorageEnergy.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/GeneratorStorageAvailability.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/GeneratorAvailability.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/FlowSamples.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/Flow.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/DemandResponseEnergySamples.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/DemandResponseEnergy.jl | Widens result timestamps field type. |
| PRASCore.jl/src/Results/DemandResponseAvailability.jl | Widens result timestamps field type. |
| PRASCore.jl/Project.toml | Bumps PRASCore to 0.9.0. |
| PRASCapacityCredits.jl/Project.toml | Bumps PRASCapacityCredits to 0.9.0 and widens PRASCore compat through 0.9. |
| PRAS.jl/Project.toml | Bumps umbrella PRAS to 0.9.0 and pins component packages to 0.9. |
Comments suppressed due to low confidence (1)
PRASFiles.jl/src/Systems/write.jl:92
- User-defined
sys.attrsare written after the reserved PRAS metadata attributes. With multi-slice support this now includesn_slices/slice_*keys; if a user supplies any attribute with the same key, it will overwrite the reserved metadata and can make the file unreadable (e.g.,n_slicesbecomes a string, causing parse failures on load). Guard against collisions so reserved metadata cannot be overridden by user attributes.
# Non-contiguous time axis: persist per-slice (start, length). Written only
# for multi-slice systems so single-slice .pras files stay byte-identical and
# remain readable by older PRAS versions.
if sys.timestamps isa SlicedTimestamps
slices = sys.timestamps.slices
attrs["n_slices"] = length(slices)
attrs["slice_start_timestamps"] = [string(first(s)) for s in slices]
attrs["slice_lengths"] = [length(s) for s in slices]
end
# Existing system attributes
sys_attributes = sys.attrs
for (key, value) in sys_attributes
attrs[key] = value
end
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| timestep(ts::SlicedTimestamps) = step(first(ts.slices)) | ||
| timestep(ts::AbstractRange{ZonedDateTime}) = step(ts) |
| # Either a contiguous `StepRange` (single time slice) or a `SlicedTimestamps` | ||
| # (multiple non-contiguous slices). Both behave as a flat length-N vector. | ||
| timestamps::AbstractVector{ZonedDateTime} | ||
|
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
PRASFiles.jl/test/runtests.jl:1
- This test writes
toy_noncontig.prasinto the repository test directory and does not clean it up, which can leave artifacts locally and can create collisions in parallel test runs/CI caching. Usemktempdir()/tempname()(andjoinpath) and ensure the file is removed after the test (e.g., with atry/finally) so the test is hermetic.
| if sys.timestamps isa SlicedTimestamps | ||
| slices = sys.timestamps.slices | ||
| attrs["n_slices"] = length(slices) | ||
| attrs["slice_start_timestamps"] = [string(first(s)) for s in slices] | ||
| attrs["slice_lengths"] = [length(s) for s in slices] | ||
| end |
| if haskey(metadata, "n_slices") && Int(read(metadata["n_slices"])) > 1 | ||
| # Non-contiguous time axis: rebuild each slice from its (start, length). | ||
| slice_starts = ZonedDateTime.(read(metadata["slice_start_timestamps"]), | ||
| dateformat"yyyy-mm-ddTHH:MM:SSz") | ||
| slice_lengths = Int.(read(metadata["slice_lengths"])) | ||
| slices = [StepRange(s, timestep, s + (len - 1) * timestep) | ||
| for (s, len) in zip(slice_starts, slice_lengths)] | ||
| timestamps = SlicedTimestamps(slices) | ||
| else |
| # 0.8 reader handles (it falls back to a contiguous range when absent). | ||
| systemmodel_0_8(f) | ||
| else | ||
| error("PRAS file format $versionstring not supported by this version of PRASBase.") |
| function SystemModel( | ||
| regions::Regions{N,P}, interfaces::Interfaces{N,P}, | ||
| generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, | ||
| storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, | ||
| generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, | ||
| demandresponses::DemandResponses{N,L,T,P,E}, region_dr_idxs::Vector{UnitRange{Int}}, | ||
| lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, | ||
| slices::Vector{<:StepRange{ZonedDateTime}}, | ||
| attrs::Dict{String, String}=Dict{String, String}() | ||
| ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} | ||
|
|
||
| return SystemModel( | ||
| regions, interfaces, | ||
| generators, region_gen_idxs, | ||
| storages, region_stor_idxs, | ||
| generatorstorages, region_genstor_idxs, | ||
| demandresponses, region_dr_idxs, | ||
| lines, interface_line_idxs, | ||
| SlicedTimestamps(collect(slices)), attrs) | ||
| end |
| function SystemModel( | ||
| regions::Regions{N,P}, interfaces::Interfaces{N,P}, | ||
| generators::Generators{N,L,T,P}, region_gen_idxs::Vector{UnitRange{Int}}, | ||
| storages::Storages{N,L,T,P,E}, region_stor_idxs::Vector{UnitRange{Int}}, | ||
| generatorstorages::GeneratorStorages{N,L,T,P,E}, region_genstor_idxs::Vector{UnitRange{Int}}, | ||
| lines::Lines{N,L,T,P}, interface_line_idxs::Vector{UnitRange{Int}}, | ||
| slices::Vector{<:StepRange{ZonedDateTime}}, | ||
| attrs::Dict{String, String}=Dict{String, String}() | ||
| ) where {N,L,T<:Period,P<:PowerUnit,E<:EnergyUnit} | ||
|
|
||
| return SystemModel( | ||
| regions, interfaces, | ||
| generators, region_gen_idxs, | ||
| storages, region_stor_idxs, | ||
| generatorstorages, region_genstor_idxs, | ||
| DemandResponses{N,L,T,P,E}(), repeat([1:0],length(regions)), | ||
| lines, interface_line_idxs, | ||
| SlicedTimestamps(collect(slices)), attrs) | ||
| end |
Allow a SystemModel time axis to consist of multiple contiguous StepRange
"slices" with gaps between them (e.g. a summer week + a winter week),
instead of a single contiguous range.
- New SlicedTimestamps <: AbstractVector{ZonedDateTime} stores a vector of
StepRange slices but presents a flat length-N view, so the simulation
engine and result indexing are unchanged. Per-sample state carries across
slice boundaries as if adjacent.
- SystemModel.timestamps field widened to AbstractVector{ZonedDateTime};
new constructors accept a Vector{StepRange} of slices. Passing a single
StepRange is fully backward compatible (still stored as a StepRange).
- Results timestamp fields widened accordingly.
- PRASFiles: persist/reconstruct slices via n_slices, slice_start_timestamps,
and slice_lengths HDF5 metadata, written only for multi-slice systems so
existing single-slice .pras files stay byte-identical. Reader remains
backward compatible with pre-0.9 files.
- Bump PRASCore, PRASFiles, PRASCapacityCredits, and PRAS to 0.9.0 and
widen inter-package compat pins.
- Add tests for multi-slice construction, file round-trip, and assess.
Make SlicedTimestamps internal (qualified import in PRASFiles/tests still works) so it no longer trips Documenter's missing-docs check. Add a Base.show that displays the underlying StepRange slices, one per line in the REPL, instead of dumping every timestamp.
Narrow the base SystemModel constructors' timestamps argument to
Union{StepRange{ZonedDateTime}, SlicedTimestamps} so a flat
Vector{ZonedDateTime} fails cleanly at dispatch with a MethodError
listing valid signatures, instead of an opaque timestep() error.
Document the non-contiguous slice-vector constructor and the
flat-vector restriction in the SystemModel docstring.
Restore the concrete field type lost when the axis was broadened for
non-contiguous support: use a 2-member Union of concrete types,
Union{StepRange{ZonedDateTime,T}, SlicedTimestamps{T}}, instead of
AbstractVector{ZonedDateTime}. The compiler union-splits it, so
sys.timestamps access stays type-stable without adding a struct type
parameter.
SlicedTimestamps is no longer exported, so `using PRASCore` does not bring it into scope. The non-contiguous roundtrip tests referenced it unqualified and errored on CI. Add the qualified import, matching the fix already applied to the PRASCore test suite.
The base constructors accepted Union{StepRange{ZonedDateTime},
SlicedTimestamps} untied to T, so a range whose step equals T(L) but
has a different period type (e.g. Minute(60) vs Hour(1)) passed the
timestep assertion and then failed opaquely when assigned into the
T-tied timestamps field. Tighten both signatures to
Union{StepRange{ZonedDateTime,T}, SlicedTimestamps{T}} so the mismatch
is a clean dispatch-time MethodError, consistent with the field type.
Add a test covering a Minute(60)-stepped range against an Hour-unit
system.
Describe the optional n_slices/slice_start_timestamps/slice_lengths HDF5 attributes and their backward-compat rules in the .pras format spec, add a user-facing slice-vector constructor example to the system model specification, and add a 0.9.0 changelog entry. Add a note in the system model spec and the PRAS walkthrough advising that the time axis comes from sys.timestamps (used directly or via collect), and must not be reconstructed from first + length + step, which fills the gaps of a non-contiguous axis with nonexistent times.
fd83de3 to
9be8097
Compare
akrivi
left a comment
There was a problem hiding this comment.
Looks great, Hari!
I left a couple comments but I think the main remaining question is how we want the non-contiguous slices to behave in the simulation. Since slices represent calendar gaps, I would expect each slice to be simulated independently, with state evolving within a slice but not carrying over between slices (for example, a slice in July should not inherit SOC from the previous slice in January).
In that case, I think we would additionally need the following modifications in the Simulations scripts:
-
We should make slice boundaries explicit in the simulation loop. To achieve that, at each slice start we should reinitialize availability, storage and generator-storage SOC, demand-response debt/payback state, line availability and transition schedules. Also at each slice end, we need to settle or reset any remaining DR obligations.
-
We should use deterministic slice-local RNG state so that slices remain reproducible and isolated from unrelated changes to earlier slices.
Happy to discuss this more!
|
|
||
| end | ||
|
|
||
| # Single-node constructor - demand responses included |
There was a problem hiding this comment.
Can we also support non-contiguous timestamps in the single-node constructors? Currently, both the demand response and non-demand-response constructors only accept a StepRange.
| "step $(step(slice)) but slice 1 has step $Δ")) | ||
| end | ||
|
|
||
| # Slices must be strictly ordered and non-overlapping |
There was a problem hiding this comment.
Can we also reject adjacent slices without timestamp gaps and prompt the user to use a single StepRange instead?
| lines, line_interfaces, | ||
| minute_timestamps) | ||
| end | ||
| end |
There was a problem hiding this comment.
Could we also add a small deterministic simulation test with non-contiguous time slices verifying that EUE and LOLE aggregate only the modeled N timesteps and not the total elapsed calendar time between slice endpoints?
FWIW, in ReEDS, the two things we use noncontiguous timestamps for (both because of data limitations) are:
For the first, at least for our application, we wouldn't want to reinitialize all of the SOCs just because we're missing a day of data. For the second, I'm not sure which is better (either way, forcing storage to always start empty is a problem for long-duration storage). But it seems like users can already implement something like the second by just setting up multiple independent PRAS runs over different time intervals. I guess it depends which you expect to be the more common use case. If it's the first (handling missing days or hours here and there), it'd probably be safest to just act like the data gap didn't happen and keep SOC etc continuous. (That's what we would do with leap years in ReEDS.) If the latter, then it's not really clear which is better; if you're skipping a full year (from Dec 31 2013 to Jan 1 2015), and you have LDES, then it seems safer to keep things continuous; if you're skipping something on the order of months, then I'm not sure there's a great solution either way. |
Summary
Allows a
SystemModeltime axis to consist of multiple contiguousStepRange"slices" with gaps between them (e.g. a representative summer week + winter week), instead of a single contiguous range.Design
SlicedTimestamps <: AbstractVector{ZonedDateTime}stores aVector{StepRange}of slices but presents a flat length-N view (lazy indexing), so the SequentialMonteCarlo engine and all result indexing are unchanged. Per-sample state (storage SoC, Markov availability) carries across slice boundaries as if adjacent.SystemModel.timestampsfield widened fromStepRange{ZonedDateTime,T}toAbstractVector{ZonedDateTime}. New constructors accept aVector{StepRange}of slices.StepRangeworks exactly as before (still stored as a compactStepRange, no materialization).timestampsfields widened accordingly.File format
n_slices,slice_start_timestamps,slice_lengths— written only for multi-slice systems, so existing single-slice.prasfiles are byte-identical and pre-0.9 files still read.Versioning
Tests
assess-across-the-gap testset)🤖 Generated with Claude Code