Repository navigation
Public hierarchy identity, multi-horizon paths, and scenario grouping APIs #103
Description
The AI wanted to change the API of TimeStruct, instead I made it write up as an issue for discussion. I think we could consider exposing some more structure querying to the public API and also improve the API for scenarios with more compex time structures.
The AI generated suggestions below for a starting point for discussion and future reference:
Public hierarchy identity, multi-horizon paths, and scenario grouping APIs
Summary
Downstream packages that partition optimization models by time need stable, public ways to
identify the hierarchy path associated with a TimeStruct period or iterator. This includes
the current strategic period, representative period, operational scenario, and strategic
tree branch levels, but the API should also support multi-horizon structures with additional
planning or operational layers.
TimeStruct already carries this information internally, but consumers currently need to
depend on internal functions such as _strat_per, _rper, and _opscen, inspect concrete
wrapper types, or use wrapper objects themselves as dictionary keys. Those approaches are
fragile when wrappers are recreated and make it difficult to distinguish:
- a representative-period/scenario slice; from
- one coherent scenario realization spanning several representative periods.
This issue proposes public hierarchy identity, extensible path, and optional scenario-label
APIs. It does not propose adding optimization, decomposition, JuMP, or solver concepts to
TimeStruct.
Motivation
Consider a strategic period whose operational structure is:
RepresentativePeriods(
...,
[
OperationalScenarios(...), # representative period 1
OperationalScenarios(...), # representative period 2
# ...
],
)opscenarios(sp) currently flattens the operational scenarios from every representative
period. With 12 representative periods and 30 operational scenarios, this produces 360
StratReprOpScenario wrappers.
Flattening is useful for traversal, but the public API does not answer whether:
- all 360 wrappers are independent realizations;
- wrappers with the same scenario position across the 12 representative periods belong to
one coherent realization; or - scenario identities differ between representative periods despite sharing the same local
integer index.
For example, an application may use 30 weather years. Weather year 1991 should contain all
12 representative periods and may need to be treated as one coherent realization. A
downstream package should not infer that relationship from wrapper object identity,
show output, or private fields.
Stable identity is also useful for reporting, result tables, caching, serialization, and
mapping profiles—not only decomposition.
Multi-horizon use cases
A multi-horizon model may combine several nested resolutions or decision horizons, for
example:
investment stage
└── annual planning horizon
└── weekly scheduling horizon
└── hourly dispatch periods
It may also use rolling or overlapping horizons in which a fine-resolution period belongs
to a particular horizon window and position within that window. These structures require
more than a fixed tuple of (strategic, representative, scenario, branch).
A public identity API should therefore distinguish:
- the semantic kind of each hierarchy level;
- the stable identity of a node at that level;
- the complete ancestry of a leaf period;
- local position within a parent from globally meaningful identity;
- scenario/branch dimensions from resolution or horizon dimensions; and
- hierarchy identity from chronological time, duration, or weighting.
This would let downstream packages partition at an investment, planning, scheduling, or
dispatch boundary without inspecting concrete wrapper types. It would also avoid forcing a
future multi-horizon implementation into today's four known hierarchy fields.
Current limitations
Internal-only hierarchy indices
TimeStruct has internal accessors such as:
_strat_per;_rper;_opscen;_branch.
Downstream packages can call them, but they are not a supported public contract. Their
fallback value is commonly 1, which also makes it impossible to distinguish:
- “this hierarchy level is absent”; from
- “this is the first item at this hierarchy level.”
Wrapper identity is not semantic identity
Traversal methods recreate wrappers. Two wrappers can describe the same semantic strategic,
representative, or scenario position without being suitable identity keys.
Conversely, two scenarios can both have local index 1 under different parents while
representing different realizations. A consumer needs the scope of each index.
Flattened operational scenarios lose grouping intent
For RepresentativePeriods{OperationalScenarios}, opscenarios(sp) returns a flat
collection of representative/scenario slices. The local scenario integer is retained, but
there is no public declaration that equal scenario integers across representative periods
form one coherent realization.
A fixed set of accessors is not sufficient for multi-horizon structures
Accessors such as strategic_index and scenario_index are useful conveniences for the
current hierarchy, but they do not provide an extension point for additional nested
horizons. Adding one new accessor for every future level would make downstream code depend
on a closed hierarchy model and would not provide a generic way to inspect ancestry.
Likewise, a fixed TimePath struct with four fields would solve current identity problems
but would need a breaking redesign when TimeStruct adds another planning or operational
horizon.
Convenience accessors for current hierarchy levels
Add public accessors that expose existing hierarchy positions without changing iteration:
strategic_index(x)::Union{Nothing,Int}
representative_index(x)::Union{Nothing,Int}
scenario_index(x)::Union{Nothing,Int}
branch_index(x)::Union{Nothing,Int}Semantics
- Return the enclosing hierarchy index represented by
x. - Return
nothingwhen the hierarchy level is absent. - Work for:
- individual
TimePeriodvalues; AbstractStrategicPeriod;AbstractRepresentativePeriod;AbstractOperationalScenario;- outer iterators returned by
strategic_periods,repr_periods, andopscenarios; PeriodPartitionvalues;- strategic-tree wrappers.
- individual
- Be stable across repeated traversal and recreated wrapper values.
- Preserve the current integer ordering.
- Avoid changing existing internal accessors initially; public methods can delegate to them
through the existing indexability traits. - Be defined as convenience views over the extensible hierarchy-path API proposed below,
rather than being the only representation of hierarchy.
Example:
for sp in strategic_periods(ts)
for rp in repr_periods(sp)
for sc in opscenarios(rp)
@assert strategic_index(sc) == strategic_index(sp)
@assert representative_index(sc) == representative_index(rp)
@assert scenario_index(sc) !== nothing
end
end
endScenario identity beyond a local integer
These accessors expose position, but a local scenario index is not always sufficient
to express scenario identity across representative periods.
Two compatible follow-up designs are worth considering.
Option A: optional scenario labels
Allow OperationalScenarios to receive stable labels:
OperationalScenarios(
scenarios,
probabilities;
labels = ["weather-1991", "weather-1992", ...],
)Expose:
scenario_label(x)Requirements:
- Existing constructors remain unchanged and default labels to the numeric index.
- Labels are unique within their declared scenario collection.
- Labels propagate through
ScenarioPeriod,OperationalScenario,
StratOpScenario,StratReprOpScenario, and strategic-tree variants. - Labels may be symbols, strings, integers, or another documented immutable key type.
- Equality/hash behavior of existing time wrappers should not change merely because labels
are added.
This is convenient for applications that already have semantic scenario IDs such as weather
year, demand realization, or market path.
Option B: explicit realization/group key
Expose a grouping key separately from display labels:
scenario_key(x)The key answers whether scenario slices under different representative periods belong to
the same realization. A default may be based on scenario position, while constructors or a
mapping function can override it.
This is more explicit than assuming that equal local indices always mean equal scenarios.
It also permits labels to remain presentation metadata.
Recommendation
Define the extensible hierarchy-path identity contract first, then provide these accessors
as conveniences for current standard levels. Design scenario_key together with optional
labels, because coherent cross-representative grouping is semantic and should not be
inferred universally from equal integer positions.
Extensible hierarchy path API
To support multi-horizon structures, TimeStruct should expose a generic ordered hierarchy
path in addition to the convenience accessors. One possible shape is:
struct HierarchyLevel
kind::Symbol
key
end
struct TimePath
levels::Tuple
end
time_path(x)::TimePath
hierarchy_key(x, kind::Symbol)The exact types and naming are open for discussion. The important semantics are:
levelsis ordered from outermost to innermost;- each level has a documented semantic
kindand stable immutablekey; - a key is stable across repeated traversal, reconstructed wrappers, and serialization;
- identity is scoped by its ancestry, so equal local keys below different parents do not
collide; - implementations and extensions can introduce additional level kinds without changing
the shape ofTimePath; and - current accessors are equivalent to querying standard kinds such as
:strategic,
:branch,:representative, and:scenario.
For a multi-horizon structure, a path might conceptually contain:
TimePath((
HierarchyLevel(:investment, 2),
HierarchyLevel(:planning_horizon, 5),
HierarchyLevel(:scheduling_horizon, 3),
HierarchyLevel(:dispatch, 17),
))This is an identity example, not a requirement that level kinds use Symbol or that users
construct paths manually.
Advantages:
- one serializable value for reporting and dictionary keys;
- absent levels are unambiguous because they do not appear in the path;
- no closed enumeration of supported nesting depth;
- generic parent/ancestor queries for arbitrary nested horizons; and
- a localized compatibility layer between current private indices and future hierarchy
implementations.
Individual accessors remain useful and can be implemented in terms of time_path.
The key requirement is that TimePath describes hierarchy position, not object identity and
not a prescribed optimization partition.
Parent and ancestry queries
Multi-horizon consumers commonly need to group fine periods by an enclosing horizon. The
path representation may be sufficient, but small public helpers would avoid each consumer
reimplementing path manipulation:
parent_path(x)
ancestor_key(x, kind)It should be possible to answer “which planning horizon contains this dispatch period?”
without relying on wrapper fields. Child traversal does not need to be part of the minimal
API if existing iterators already provide it.
Local position versus stable key
Multi-horizon windows may repeat the same local positions, such as hour 1 in every scheduling
horizon. The API should not imply that a local index is globally unique.
If TimeStruct exposes both concepts, their names should make the distinction explicit:
- an index or ordinal is a local traversal position; and
- a key, qualified by its ancestry, is stable identity.
This distinction is also relevant to operational scenarios and strategic-tree branches.
Overlapping and rolling horizons
If a physical period can participate in more than one rolling horizon, a single ancestry
path may not fully describe membership. The initial API can document that time_path(x)
identifies the wrapper/traversal context, while a future membership API could expose all
horizon windows associated with a physical timestamp or canonical period.
The identity proposal should not assume that all future multi-horizon structures are strict
trees. It should, however, provide stable identity for each traversal context and avoid
equating path identity with physical-time identity.
Coherent scenario traversal
A higher-level convenience could expose coherent realizations without changing
opscenarios:
scenario_groups(sp)Each returned group would contain the scenario slices belonging to one realization across
representative periods.
Open questions:
- Should grouping require explicit
scenario_keyvalues? - Should TimeStruct reject representative periods whose scenario keys/probabilities differ?
- Should a group itself be iterable over all operational periods?
- How should differing operational scenario structures across representative periods be
represented? - Should scenario groups be scoped to a particular enclosing horizon path?
This convenience should be deferred until scenario identity semantics are agreed. Public
indices are independently useful and lower risk.
Probability and multiplier invariants
The identity API should make no changes to:
duration;multiple;multiple_strat;mult_repr;mult_scen;probability;probability_scen;probability_branch.
Documentation should clarify that:
- representative-period share/repetition and scenario probability are different concepts;
- scenario identity does not itself determine probability;
- equal scenario keys across representative periods require compatible probability
semantics; - strategic-tree branch identity is separate from operational-scenario identity;
- horizon identity is separate from duration, chronological timestamp, and resolution;
- the same local ordinal may occur under several horizon parents; and
- repeated or overlapping horizon membership must not be inferred from equal indices.
A future scenario_groups API should validate probability consistency rather than silently
combining incompatible slices.
Non-goals
- No JuMP dependency.
- No optimization-variable or constraint APIs.
- No master/subproblem terminology.
- No decomposition policy in TimeStruct.
- No assumption about which hierarchy level is a decomposition boundary.
- No change to the meaning or order of
opscenarios. - No automatic assumption that every equal local scenario index across representative
periods is one coherent realization. - No requirement to add weighted-sum helpers as part of this issue.
Backward compatibility
- Existing constructors continue to work.
- Existing iterator order and returned wrapper types remain valid.
- Existing internal accessors can remain available during migration.
- Public accessors return
nothingfor absent levels, avoiding ambiguity without changing
internal fallback behavior. - Optional labels/keys should default to current numeric scenario ordering.
- Existing structures can expose the generic path using the four currently known hierarchy
kinds; adding new multi-horizon kinds does not require changingTimePath.
Suggested implementation sequence
- Define stable identity semantics and an extensible
TimePathrepresentation. - Implement
time_pathfor current period, iterator, partition,
representative/scenario, and strategic-tree types. - Add convenience accessors for the current standard hierarchy kinds.
- Add API documentation, including nested multi-horizon examples and the distinction
between local ordinals, stable keys, and physical time. - Add stability tests across repeated traversal, reconstructed wrappers, serialization,
and arbitrary nesting depth. - Design optional scenario labels and
scenario_keyseparately. - Consider coherent
scenario_groupsand rolling-horizon membership APIs after practical
feedback.
Suggested tests
- Simple time structures return
nothingfor absent hierarchy levels. - Strategic periods and their operational periods report the same strategic index.
- Representative wrappers and periods report the same representative index.
- Operational scenario wrappers and periods report the same scenario index.
- Nested representative/scenario wrappers report all applicable indices.
- A synthetic structure with more than four nested levels produces a complete ordered path.
- Two repeated local horizon positions under different parents have distinct full paths.
- Parent/ancestor queries return the correct enclosing horizon at every nesting depth.
- Partitions preserve the hierarchy identity of their source iterator.
- Repeated calls to
opscenarios,repr_periods, and iteration produce equal public
identities. - Paths remain equal after supported serialization and reconstruction.
- Chronologically equal periods in different rolling-horizon traversal contexts retain
distinct path identities. - Strategic branch and operational scenario identities remain distinct.
- If labels/keys are added, they propagate across all wrappers and reject duplicates or
inconsistent grouping as specified.
Why this helps downstream packages
With public hierarchy identity, downstream packages can:
- build stable cache and serialization keys;
- produce unambiguous result tables;
- group time slices according to application policy without private-field access;
- select arbitrary investment, planning, scheduling, or dispatch boundaries in
multi-horizon models; - distinguish representative-period slices from coherent scenario realizations;
- validate scenario probability consistency;
- remain compatible as TimeStruct wrapper implementations evolve.
The API remains broadly useful and solver-independent while removing the need for consumers
to reverse-engineer TimeStruct's internal hierarchy.
Activity
JulStraus commentedon Sep 28, 2026
Interesting thoughts. While I agree with some of the mentioned topics (e.g. exposing more of the internal functions to the user), I am not convinced regarding other topics. Specifically I disagree with the concept of
To me, this reads a bit like nodal time structures as it is the case in
Tulipaor resource specific ones as inSpineOpt. In my opinion, this was never the aim ofTimeStructto include it.In addition, it seems to me that it got a bit hang up regarding the flatten approach. While we do not explicitly mention it anywhere in the documentation (although it is included in the respective docstrings), it is introduced to simplify the introduction of variables. It also does not really see the aim of the package that the user does not need to have full understanding of the respective time period /structure one is in as the types are invariant.
I still think that we can implement some of thoughts while others are, at least in my opinion, contradictory to the philosophy we have within
TimeStruct.trulsf commentedon Sep 28, 2026
I agree with @JulStraus here. I think it is worth considering exposing the various indices (e.g.
scenario_index). I am not convinced on thetime_pathsuggestions, as I do not see the good use cases and it will potentially confuse the user more than helping by exposing a slightly alternative API.There is a related issue regarding the
TimeStructuse in Empire: ntnuiotenergy/OpenEMPIRE.jl#41It has a different suggested approach for handling different scenarios across multiple representative periods.