Skip to content
Open
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
10 changes: 9 additions & 1 deletion .claude/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ Key files:
- `production_variable_cost_curve.jl` — `CostCurve{T,U}` / `FuelCurve{T,U}` (units in the type parameter)
- `relative_units.jl` — `RelativeUnits` submodule: unit-system markers and `RelativeQuantity`
- `outputs.jl` — abstract `Outputs` interface (not-implemented stubs for downstream packages)
- `serialization.jl` — JSON-based serialization (stdlib-adjacent `JSON`; JSON3/StructTypes were removed)
- `geographic_supplemental_attribute.jl` — `GeographicInfo` supplemental attribute (hand-written template)
- `data_source_supplemental_attribute.jl` — `DataSource` supplemental attribute: data-provenance record
(organization, dataset, url, version, retrieved/published timestamps, confidence, recorded_by, covered
fields, `extra`). Mutable with `get_*`/`set_*!`; `published_at`/`recorded_by` are `Union{Nothing,_}` with
dispatch-based `has_*`/`get_*` predicate accessors. Shared across components via the association store
(create-once / link-many). Hand-written, not exported.
- `serialization.jl` — JSON-based serialization (stdlib-adjacent `JSON`; JSON3/StructTypes were removed).
Per-type deserialize methods narrow JSON's loose types to declared field types: optional `DateTime`
(`Union{Nothing, Dates.DateTime}`) and `Vector{String}` (JSON arrays parse to `Vector{Any}`).

Subdirectories:
- `function_data/` — `FunctionData` hierarchy, time-series-backed function data, convexity/validity checks
Expand Down
2 changes: 1 addition & 1 deletion docs/src/docs_best_practices/how-to/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Pkg.add(["PowerSystems"]);
2. Run the formatter once to format the rest of the file
3. Add the text back in
4. Add the `ignore` keyword argument with the file name to
[`JuliaFormatter.format`](@extref `JuliaFormatter.format-Tuple{Any}`) in `scripts/formatter/formatter_code.jl`
[`JuliaFormatter.format`](@extref) in `scripts/formatter/formatter_code.jl`
to skip the file in the future:

```julia
Expand Down
2 changes: 1 addition & 1 deletion docs/src/docs_best_practices/how-to/write_a_tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ code blocks.
!!! tip "Do"

Display all code, starting from `using SomeSiennaPackage`. Example: See
[Working with Time Series](@extref tutorial_time_series).
[Working with Time Series](@extref PowerSystems Working-with-Time-Series-Data).

!!! warning "Don't"

Expand Down
1 change: 1 addition & 0 deletions src/InfrastructureSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ include("containers.jl")
include("component_container.jl")
include("component_uuids.jl")
include("geographic_supplemental_attribute.jl")
include("data_source_supplemental_attribute.jl")
include("generated/includes.jl")
include("time_series_parser.jl")
include("single_time_series.jl")
Expand Down
256 changes: 256 additions & 0 deletions src/data_source_supplemental_attribute.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
"""
mutable struct DataSource <: SupplementalAttribute

A supplemental attribute recording the provenance of component data: the publishing
organization, dataset, URL, version (vintage), publication and retrieval timestamps,
confidence, the user/agent that recorded the value, and the component field names the
record applies to. Attach one instance to many components to avoid repeated entries.

# Arguments
$(DocStringExtensions.TYPEDFIELDS)
"""
mutable struct DataSource <: SupplementalAttribute
"Publishing organization (required), e.g. \"U.S. Energy Information Administration\"."
organization::String
"When the data was retrieved (required)."
retrieved_at::Dates.DateTime
"Dataset identifier, e.g. \"EIA-860 2023, Schedule 3\"."
dataset::String
"Source URL."
url::String
"Data version / vintage, e.g. \"2023 final\"."
version::String
"When the source published the data; `nothing` if unknown."
published_at::Union{Nothing, Dates.DateTime}
"Confidence qualifier, e.g. \"high\", \"medium\"."
confidence::String
"User or agent that recorded the value; `nothing` if unrecorded."
recorded_by::Union{Nothing, String}
"Component field names this record applies to (no meaning is attached by IS)."
fields::Vector{String}
"Escape hatch for additional, evolving provenance keys."
extra::Dict{String, Any}
internal::InfrastructureSystemsInternal
end

function DataSource(;
organization::String,
retrieved_at::Dates.DateTime,
fields::Vector{String},
dataset::String = "",
url::String = "",
version::String = "",
published_at::Union{Nothing, Dates.DateTime} = nothing,
confidence::String = "",
recorded_by::Union{Nothing, String} = nothing,
extra::Dict{String, Any} = Dict{String, Any}(),
internal::InfrastructureSystemsInternal = InfrastructureSystemsInternal(),
)
return DataSource(
organization,
retrieved_at,
dataset,
url,
version,
published_at,
confidence,
recorded_by,
fields,
extra,
internal,
)
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the publishing organization.
"""
get_organization(ds::DataSource) = ds.organization

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the publishing organization.
"""
function set_organization!(ds::DataSource, val::String)
ds.organization = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the retrieval timestamp.
"""
get_retrieved_at(ds::DataSource) = ds.retrieved_at

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the retrieval timestamp.
"""
function set_retrieved_at!(ds::DataSource, val::Dates.DateTime)
ds.retrieved_at = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the dataset identifier.
"""
get_dataset(ds::DataSource) = ds.dataset

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the dataset identifier.
"""
function set_dataset!(ds::DataSource, val::String)
ds.dataset = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the source URL.
"""
get_url(ds::DataSource) = ds.url

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the source URL.
"""
function set_url!(ds::DataSource, val::String)
ds.url = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the data version (vintage).
"""
get_version(ds::DataSource) = ds.version

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the data version (vintage).
"""
function set_version!(ds::DataSource, val::String)
ds.version = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the confidence qualifier.
"""
get_confidence(ds::DataSource) = ds.confidence

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the confidence qualifier.
"""
function set_confidence!(ds::DataSource, val::String)
ds.confidence = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the component field names this record applies to.
"""
get_fields(ds::DataSource) = ds.fields

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the component field names this record applies to.
"""
function set_fields!(ds::DataSource, val::Vector{String})
ds.fields = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Append a component field name to `fields`.
"""
function add_field!(ds::DataSource, name::String)
push!(ds.fields, name)
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the extra provenance dictionary.
"""
get_extra(ds::DataSource) = ds.extra

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the extra provenance dictionary.
"""
function set_extra!(ds::DataSource, val::Dict{String, Any})
ds.extra = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return `true` if `published_at` is set.
"""
has_published_at(ds::DataSource) = _has_published_at(ds.published_at)
_has_published_at(::Nothing) = false
_has_published_at(::Dates.DateTime) = true

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the publication timestamp. Guard with [`has_published_at`](@ref) — throws if unset.
"""
get_published_at(ds::DataSource) = _get_published_at(ds.published_at)
_get_published_at(val::Dates.DateTime) = val
function _get_published_at(::Nothing)
throw(
ArgumentError(
"published_at is not set on this DataSource; guard with has_published_at",
),
)
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the publication timestamp.
"""
function set_published_at!(ds::DataSource, val::Dates.DateTime)
ds.published_at = val
return nothing
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return `true` if `recorded_by` is set.
"""
has_recorded_by(ds::DataSource) = _has_recorded_by(ds.recorded_by)
_has_recorded_by(::Nothing) = false
_has_recorded_by(::String) = true

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Return the recorder. Guard with [`has_recorded_by`](@ref) — throws if unset.
"""
get_recorded_by(ds::DataSource) = _get_recorded_by(ds.recorded_by)
_get_recorded_by(val::String) = val
function _get_recorded_by(::Nothing)
throw(
ArgumentError(
"recorded_by is not set on this DataSource; guard with has_recorded_by",
),
)
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)
Set the recorder.
"""
function set_recorded_by!(ds::DataSource, val::String)
ds.recorded_by = val
return nothing
end

get_internal(ds::DataSource) = ds.internal
get_uuid(ds::DataSource) = get_uuid(get_internal(ds))
13 changes: 13 additions & 0 deletions src/serialization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,16 @@ end

deserialize(::Type{Dates.DateTime}, val::AbstractString) = Dates.DateTime(val)

# Mirror of the `deserialize(T::Union, data::Dict)` dispatcher above for string-valued data:
# an optional field like `Union{Nothing, Dates.DateTime}` serializes to a JSON string, which
# the concrete-type methods do not match. Drop `Nothing` and recurse to the remaining type's
# string deserializer. The `nothing` case is handled by the generic path.
function deserialize(T::Union, data::AbstractString)
non_nothing = filter(x -> x !== Nothing, Base.uniontypes(T))
length(non_nothing) == 1 && return deserialize(only(non_nothing), data)
throw(ArgumentError("Cannot pick which type of union $T to deserialize from a string"))
end

# The next methods fix serialization of UUIDs. The underlying type of a UUID is a UInt128.
# JSON tries to encode this as a number in JSON. Encoding integers greater than can
# be stored in a signed 64-bit integer sometimes does not work - at least when using
Expand Down Expand Up @@ -311,6 +321,9 @@ deserialize(::Type{Complex{T}}, data::Dict) where {T} =
Complex(T(data["real"]), T(data["imag"]))

deserialize(::Type{Vector{Symbol}}, data::Vector) = Symbol.(data)
# JSON arrays parse to Vector{Any}; narrow to the declared element type for typed fields.
# Explicit comprehension keeps the empty case concrete (`String.(Any[])` stays Vector{Any}).
deserialize(::Type{Vector{String}}, data::Vector) = String[String(x) for x in data]
serialize(value::Vector{Complex{T}}) where {T} =
[Dict("real" => real(x), "imag" => imag(x)) for x in value]
deserialize(::Type{Vector{Complex{T}}}, data::Array) where {T} =
Expand Down
5 changes: 5 additions & 0 deletions test/test_serialization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,8 @@ end
_, result = validate_serialization(sys2)
@test result
end

@testset "Deserialize optional DateTime field" begin
@test IS.deserialize(Union{Nothing, Dates.DateTime}, "2024-09-01T00:00:00") ==
Dates.DateTime("2024-09-01T00:00:00")
end
Loading
Loading