Projects
What problem this layer solves
Everything below this layer is a capability the SDK offers; projects is
where a specific study actually consumes them, and it does so as data, not
code. A project.yaml (kind: StudyProject) plus a workflow.yaml
(kind: Workflow) fully describe a study — the DAG of stages, their inputs,
and what gets validated. gridalyn/projects/runner.py executes that DAG as
subprocesses; nothing shares process memory, so a stage's only input is a
file another stage wrote, and its only output is a file on disk.
The vocabulary
StudyProject/WorkflowStage— frozen dataclasses that are the in-memory form of the two YAML files, produced byload_project.ProjectScript— the boilerplate-free stage-script context (project_script()): the study directory (root, aProjectDir) and the workspace root (workspace_root, aWorkspaceRoot) as distinct types, output paths, headless matplotlib, typed input loading, andwrite_report.- The typed input loaders (
gridalyn/projects/model_inputs.py) —load_radial_feeder_spec,load_der_dispatch_assets,load_generated_load_profiles, and siblings. They own the camelCase→ snake_case key mapping, the defaults, and the required-field checks, so a stage script never reaches intoscript.project.rawby hand. bind_project_components(script)— resolves aProjectScriptinto a frozenProjectComponents(script,feeder_spec,load_profiles,backend,surrogate,registered). A project is bound, not hand-wired: a stage consumes a resolved role rather than importing a solver or a surrogate directly. Two roles are wired today —backend(spec.simulation.powerflowBackend) andsurrogate(spec.simulation.surrogate) — each resolved through its registry and recorded in the run manifest.observation_producerandpolicyare declared follow-up surface: their registries do not yet expose theregistration_sourcediscriminator that tells a project-registered component from a core one.- The channel model (
spec.simulation.channelModel), for a study whose simulated agents exchange messages. It declares a registeredidand itsparameters. A model that draws randomness also names aseedStreamfromspec.simulation.seeds, and never a seed of its own.ProjectScript.channel_model()resolves it, and the manifest records it asprovenance.channel_model. A study that declares none records nothing, so its manifest bytes stay unchanged. - Sense checks and regression —
project_sense_checkruns objective plausibility checks;run_project_regressioncompares a run's outputs againstbaselines/results_baseline.json.
The contract
Three distinct questions, three distinct mechanisms, never conflated: is the
contract well-formed (validate_project_file, before anything runs); do the
numbers make sense (project_sense_check, which writes
project_sense_check_report.json and fails validation.valid on any
error-severity check); did the numbers move (run_project_regression,
which reads the pinned baseline and compares json_path by json_path). A
project with neither a registered checker nor declarative sense-check
rules in its YAML fails the project_has_registered_sense_checks gate — a
study cannot pass vacuously by declaring nothing to check.
flowchart TB
subgraph WF["1 · Is the contract well-formed?"]
direction TB
V["validate_project_file"] --> VO["schema check on<br/>project.yaml + workflow.yaml"]
VO --> VX["fails before any stage runs"]
end
subgraph SN["2 · Do the numbers make sense?"]
direction TB
S["project_sense_check"] --> SO["project_sense_check_report.json"]
SO --> SX["validation.valid = false<br/>on any error-severity check"]
end
subgraph RG["3 · Did the numbers move?"]
direction TB
R["run_project_regression"] --> RO["regression_report.json"]
RO --> RX["json_path by json_path<br/>vs results_baseline.json"]
end
WF --> SN --> RG
classDef q fill:#e8eaf6,stroke:#3f51b5,color:#1a237e
classDef out fill:#e0f2f1,stroke:#00897b,color:#004d40
classDef verdict fill:#fff3e0,stroke:#ef6c00,color:#e65100
class V,S,R q
class VO,SO,RO out
class VX,SX,RX verdict
Nothing in that picture folds into anything else: a well-formed contract says nothing about whether the numbers are plausible, and a plausible number says nothing about whether it moved since the baseline was pinned.
The run manifest (outputs/manifests/project_run_manifest.json) is the
governed record of what happened: git_commit, one entry per stage with
status/started_at/ended_at/exit_code, and an overall status that is
"completed" only if every stage exited zero — the first non-zero exit marks
both the stage and the run "failed" and re-raises with a re-run hint.
A declared role is recorded, not merely resolved. provenance.powerflow_backend
names the solver a run used; provenance.surrogate names the surrogate that
stood in for a solve and its stated error bound, because naming a surrogate
without its accuracy invites the reader to assume there is none. Both carry a
declared_source saying whether the study declared the component or inherited
the registry default — so a study that names the default explicitly stays
distinguishable from one that named nothing.
Using it
from gridalyn.projects.developer import ProjectComponents
import dataclasses
print([f.name for f in dataclasses.fields(ProjectComponents)])
Verifying it
uv run gridalyn project validate projects/minimal_grid_project
uv run gridalyn project run projects/minimal_grid_project
python3 -m json.tool projects/minimal_grid_project/outputs/manifests/project_run_manifest.json
The manifest this produces carries exactly the fields described above — this page's claims are read off that file, not recalled.
Where this sits
projects sits on Operations (and, through it, every layer
below): a study's stages call down through simulation, assets and twin, using
operations when the study needs a market. What builds on projects is
Interfaces: the CLI, reports and dashboard that let a person
actually run and read what this layer produces.