Skip to content

Commit ada4376

Browse files
committed
storing seed consistently
1 parent 44f4ed2 commit ada4376

8 files changed

Lines changed: 122 additions & 10 deletions

File tree

ngraph/results/store.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,24 @@ class WorkflowStepMetadata:
1818
step_type: The workflow step class name (e.g., 'CapacityEnvelopeAnalysis').
1919
step_name: The instance name of the step.
2020
execution_order: Order in which this step was executed (0-based).
21+
scenario_seed: Scenario-level seed provided in the YAML (if any).
22+
step_seed: Seed assigned to this step (explicit or scenario-derived).
23+
seed_source: Source for the step seed. One of:
24+
- "scenario-derived": seed was derived from scenario.seed
25+
- "explicit-step": seed was explicitly provided for the step
26+
- "none": no seed provided/active for this step
27+
active_seed: The effective base seed used by the step, if any. For steps
28+
that use Monte Carlo execution, per-iteration seeds are derived from
29+
active_seed (e.g., active_seed + iteration_index).
2130
"""
2231

2332
step_type: str
2433
step_name: str
2534
execution_order: int
35+
scenario_seed: Optional[int] = None
36+
step_seed: Optional[int] = None
37+
seed_source: str = "none"
38+
active_seed: Optional[int] = None
2639

2740

2841
@dataclass
@@ -61,17 +74,35 @@ def put(self, step_name: str, key: str, value: Any) -> None:
6174
self._store[step_name][key] = value
6275

6376
def put_step_metadata(
64-
self, step_name: str, step_type: str, execution_order: int
77+
self,
78+
step_name: str,
79+
step_type: str,
80+
execution_order: int,
81+
*,
82+
scenario_seed: Optional[int] = None,
83+
step_seed: Optional[int] = None,
84+
seed_source: str = "none",
85+
active_seed: Optional[int] = None,
6586
) -> None:
6687
"""Store metadata for a workflow step.
6788
6889
Args:
6990
step_name: The step instance name.
7091
step_type: The workflow step class name.
7192
execution_order: Order in which this step was executed (0-based).
93+
scenario_seed: Scenario-level seed from YAML, if any.
94+
step_seed: Seed attached to this step (explicit or derived), if any.
95+
seed_source: Source of step seed ("scenario-derived", "explicit-step", or "none").
96+
active_seed: Effective base seed used by the step, if any.
7297
"""
7398
self._metadata[step_name] = WorkflowStepMetadata(
74-
step_type=step_type, step_name=step_name, execution_order=execution_order
99+
step_type=step_type,
100+
step_name=step_name,
101+
execution_order=execution_order,
102+
scenario_seed=scenario_seed,
103+
step_seed=step_seed,
104+
seed_source=seed_source,
105+
active_seed=active_seed,
75106
)
76107

77108
def get(self, step_name: str, key: str, default: Any = None) -> Any:
@@ -148,6 +179,10 @@ def to_dict(self) -> Dict[str, Any]:
148179
"step_type": metadata.step_type,
149180
"step_name": metadata.step_name,
150181
"execution_order": metadata.execution_order,
182+
"scenario_seed": metadata.scenario_seed,
183+
"step_seed": metadata.step_seed,
184+
"seed_source": metadata.seed_source,
185+
"active_seed": metadata.active_seed,
151186
}
152187
for step_name, metadata in self._metadata.items()
153188
}

ngraph/scenario.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,13 +486,25 @@ def _build_workflow_steps(
486486
# Ensure the constructed WorkflowStep receives the resolved unique name
487487
normalized_ctor_args["name"] = step_name
488488

489-
# Add seed derivation for workflow steps that don't have explicit seed
490-
if "seed" not in normalized_ctor_args:
489+
# Determine seed provenance and possibly derive a step seed
490+
seed_source: str = "none"
491+
if (
492+
"seed" in normalized_ctor_args
493+
and normalized_ctor_args["seed"] is not None
494+
):
495+
seed_source = "explicit-step"
496+
else:
491497
derived_seed = seed_manager.derive_seed("workflow_step", step_name)
492498
if derived_seed is not None:
493499
normalized_ctor_args["seed"] = derived_seed
500+
seed_source = "scenario-derived"
494501

495502
step_obj = step_cls(**normalized_ctor_args)
503+
# Attach internal provenance for metadata collection
504+
try:
505+
step_obj._seed_source = seed_source
506+
except Exception:
507+
pass
496508
steps.append(step_obj)
497509

498510
return steps

ngraph/workflow/base.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class WorkflowStep(ABC):
7070

7171
name: str = ""
7272
seed: Optional[int] = None
73+
# Internal: provenance of the step seed ("explicit-step" or "scenario-derived" or "none").
74+
_seed_source: str = ""
7375

7476
def execute(self, scenario: "Scenario") -> None:
7577
"""Execute the workflow step with logging and metadata storage.
@@ -94,9 +96,36 @@ def execute(self, scenario: "Scenario") -> None:
9496
step_name = self.name
9597
display_name = step_name or step_type
9698

99+
# Determine seed provenance and effective seed for this step
100+
scenario_seed = getattr(scenario, "seed", None)
101+
step_seed = self.seed
102+
explicit_source = getattr(self, "_seed_source", None)
103+
if step_seed is not None and explicit_source == "explicit-step":
104+
seed_source = "explicit-step"
105+
active_seed = step_seed
106+
elif step_seed is not None and explicit_source == "scenario-derived":
107+
# Step received a derived seed at construction time
108+
seed_source = "scenario-derived"
109+
active_seed = step_seed
110+
elif scenario_seed is not None:
111+
seed_source = "scenario-derived"
112+
# Scenario.from_yaml derives per-step seeds when seed is provided; if a
113+
# concrete seed was not set on the step (self.seed is None), treat the
114+
# scenario seed as the active base (workers may derive offsets internally).
115+
active_seed = scenario_seed
116+
else:
117+
seed_source = "none"
118+
active_seed = None
119+
97120
# Store workflow metadata before execution using the exact step namespace key
98121
scenario.results.put_step_metadata(
99-
step_name=step_name, step_type=step_type, execution_order=_execution_counter
122+
step_name=step_name,
123+
step_type=step_type,
124+
execution_order=_execution_counter,
125+
scenario_seed=scenario_seed,
126+
step_seed=step_seed,
127+
seed_source=seed_source,
128+
active_seed=active_seed,
100129
)
101130
_execution_counter += 1
102131

tests/workflow/test_analysis_integration.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ def test_workflow_step_metadata_storage(self, simple_scenario):
145145
metadata = simple_scenario.results.get_step_metadata("meta_test")
146146
assert metadata is not None
147147
assert metadata.step_type == "NetworkStats"
148+
# Seed metadata presence (scenario has seed set in helper scenarios)
149+
assert hasattr(metadata, "scenario_seed")
150+
assert hasattr(metadata, "step_seed")
151+
assert hasattr(metadata, "seed_source")
148152
assert metadata.step_name == "meta_test"
149153
assert metadata.execution_order >= 0
150154

tests/workflow/test_base.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from ngraph.scenario import Scenario
56
from ngraph.workflow.base import (
67
WORKFLOW_STEP_REGISTRY,
78
WorkflowStep,
@@ -41,14 +42,38 @@ def test_workflow_step_subclass_run_method() -> None:
4142

4243
class ConcreteStep(WorkflowStep):
4344
def run(self, scenario) -> None:
44-
scenario.called = True
45+
# Set a flag on the step instance to confirm invocation
46+
self._ran = True
4547

46-
mock_scenario = MagicMock()
48+
mock_scenario = MagicMock(spec=Scenario)
4749
step_instance = ConcreteStep(name="test_step")
4850
step_instance.run(mock_scenario)
4951

5052
# Check if run() was actually invoked
51-
# e.g., we set scenario.called = True in run()
52-
# but here we can also rely on MagicMock calls or attributes if needed
53-
assert hasattr(mock_scenario, "called") and mock_scenario.called is True
53+
assert getattr(step_instance, "_ran", False) is True
5454
assert step_instance.name == "test_step"
55+
56+
57+
def test_execute_records_metadata_including_seed_fields() -> None:
58+
"""Execute a minimal step and verify metadata includes seed fields."""
59+
from ngraph.results import Results
60+
61+
class Dummy(WorkflowStep):
62+
def run(self, scenario) -> None:
63+
scenario.results.put(self.name, "ok", True)
64+
65+
scen = MagicMock(spec=Scenario)
66+
scen.results = Results()
67+
scen.seed = 1010
68+
step = Dummy(name="d1")
69+
step.execute(scen)
70+
71+
md = scen.results.get_step_metadata("d1")
72+
assert md is not None
73+
assert md.step_type == "Dummy"
74+
assert md.step_name == "d1"
75+
assert isinstance(md.execution_order, int) and md.execution_order >= 0
76+
# New fields
77+
assert hasattr(md, "scenario_seed") and md.scenario_seed == 1010
78+
assert hasattr(md, "step_seed")
79+
assert hasattr(md, "seed_source")

tests/workflow/test_capacity_envelope_analysis.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ def test_run_with_mock_failure_manager(
189189

190190
# Verify results were processed (just check that the step ran without error)
191191
# The analysis and results storage happened as evidenced by the log messages
192+
# Metadata is recorded by the execute() wrapper; here we called run() directly,
193+
# so metadata may be absent. This check is only applicable when using execute().
192194

193195
@patch("ngraph.workflow.capacity_envelope_analysis.FailureManager")
194196
def test_run_with_failure_patterns(self, mock_failure_manager_class, mock_scenario):

tests/workflow/test_msd_perf_safety.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class _MD:
4545
def __init__(self, execution_order: int, step_type: str) -> None:
4646
self.execution_order = execution_order
4747
self.step_type = step_type
48+
# Optional fields added in metadata; harmless defaults for stub
49+
self.scenario_seed = None
50+
self.step_seed = None
51+
self.seed_source = "none"
4852

4953
return {"msd": _MD(0, "MaximumSupportedDemandAnalysis")}
5054

tests/workflow/test_tm_analysis_perf_safety.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def get(self, step: str, key: str) -> Any:
4646
return self._data.get(step, {}).get(key)
4747

4848
def get_all_step_metadata(self):
49+
# Return empty mapping; caller code should handle gracefully
4950
return {}
5051

5152
class _FailurePolicySetStub:

0 commit comments

Comments
 (0)