Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/TimeStruct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ include("partitions/tree_periods.jl")
include("partitions/rep_periods.jl")
include("partitions/opscenarios.jl")

include("hashing.jl")

export TimeStructure
export SimpleTimes
export CalendarTimes
Expand Down
35 changes: 35 additions & 0 deletions src/hashing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Hashing of periods
#
# Periods are often used as keys of dictionaries, e.g. in JuMP containers indexed by
# nodes and time periods. Periods of bits types (e.g. `OperationalPeriod`) keep
# Julia's default hash, which hashes their bytes and is fast. For the others (e.g.
# `StrategicPeriod`, which carries along its time structure in the field
# `operational`), the default hash is the generic hash of the object, which is about
# ten times slower. For those, the methods below hash the type's name and the fields
# that identify a period instead: its numbers, dates and nested periods. Fields
# holding a time structure, a time profile, or of type `Any` (e.g. the parent of a
# strategic node) are left out.
#
# Equality is unchanged (the default `===`). Values that are `===` have `===`
# fields and hence the same hash, so `isequal(a, b)` still implies
# `hash(a) == hash(b)`; leaving fields out can only make two periods hash alike.

function _hashed_field(::Type{F}) where {F}
F === Any && return false
F <: TimeStructurePeriod && return true # (hashed by the methods below)
return !(F <: Union{TimeStructure,TimeStructInnerIter,TimeStructOuterIter,TimeProfile})
end

@generated function _hash_period(x, h::UInt)
isbitstype(x) && return :(invoke(hash, Tuple{Any,UInt}, x, h))
ex = :(hash($(QuoteNode(nameof(x))), h))
for (f, F) in zip(fieldnames(x), fieldtypes(x))
_hashed_field(F) || continue
ex = :(hash(getfield(x, $(QuoteNode(f))), $ex))
end
return ex
end

Base.hash(t::TimePeriod, h::UInt) = _hash_period(t, h)
Base.hash(t::TimeStructurePeriod, h::UInt) = _hash_period(t, h)
Base.hash(t::PeriodPartition, h::UInt) = _hash_period(t, h)
58 changes: 56 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,6 @@ end
end

@testitem "Duration invariants" begin

#=
day = SimpleTimes(24,1)
@test sum(duration(sp) for sp in strat_periods(day)) == TimeStruct._total_duration(day)
Expand All @@ -805,7 +804,6 @@ end
@test sum(duration(sp) for sp in strat_periods(periods)) == TimeStruct._total_duration(periods)
@test sum(duration(rp) for sp in strat_periods(periods) for rp in repr_periods(sp)) == TimeStruct._total_duration(periods)
=#

end

@testitem "Profiles" begin
Expand Down Expand Up @@ -2722,5 +2720,61 @@ end
@test hasproperty(df4, :start_time)
end

@testitem "Hashing of periods" begin
using Dates

day = SimpleTimes(24, 1)
cal = CalendarTimes(DateTime(2024, 1, 1), 12, Hour(1))
osc = OperationalScenarios(3, day)
rp_osc = RepresentativePeriods(2, 8760, [0.5, 0.5], [osc, osc])
two_level = TwoLevel(3, 8760, rp_osc)
tree = regular_tree(2, [3, 2], RepresentativePeriods(2, 8760, [0.5, 0.5], [day, day]))
sps = strat_periods(two_level)

# The periods of each kind, collected twice (as new objects) to compare with
function periods()
return [
collect(day),
collect(cal),
collect(osc),
collect(two_level),
collect(tree),
collect(opscenarios(osc)),
collect(sps),
[rp for sp in sps for rp in repr_periods(sp)],
[sc for sp in sps for rp in repr_periods(sp) for sc in opscenarios(rp)],
collect(strat_nodes(tree)),
collect(strategic_scenarios(tree)),
[pd for sp in sps for pd in partition_duration(sp, 4)],
[pd for n in strat_nodes(tree) for pd in partition_duration(n, 4)],
]
end
for (xs, ys) in zip(periods(), periods())
# Equal periods have equal hashes
@test all(isequal(x, y) && hash(x) == hash(y) for (x, y) in zip(xs, ys))
# Distinct periods have distinct hashes (no collisions within a structure)
@test length(unique(hash.(xs))) == length(xs)
# Dictionaries keyed by periods (and tuples with periods)
d = Dict(("n", x) => k for (k, x) in enumerate(xs))
@test all(d[("n", y)] == k for (k, y) in enumerate(ys))
end

# The time structure a period holds is not hashed: strategic periods that only
# differ in their operational periods hash alike (and are not equal)
sp = first(sps)
sp_other = TimeStruct.StrategicPeriod(sp.sp, sp.duration, sp.mult_sp, SimpleTimes(5, 1))
@test hash(sp_other) == hash(sp)
@test !isequal(sp_other, sp)

# Periods defined outside TimeStruct are hashed by their fields as well
struct _NamedPeriod <: TimeStruct.TimePeriod
name::String
op::Int
end
named = [_NamedPeriod(n, k) for n in ("a", "b") for k in 1:3]
@test length(unique(hash.(named))) == length(named)
@test hash(_NamedPeriod("a", 1)) == hash(_NamedPeriod("a", 1))
end

@run_package_tests
Aqua.test_all(TimeStruct; ambiguities = false, unbound_args = false)
Loading