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
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.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #110 +/- ##
==========================================
+ Coverage 83.13% 83.15% +0.01%
==========================================
Files 45 46 +1
Lines 2325 2380 +55
==========================================
+ Hits 1933 1979 +46
- Misses 392 401 +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} | ||
|
|
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.
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.
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.
| 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 |
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