[ESSREDUCE] Minimal implementation-independent workflow specifications - #690
SimonHeybrock wants to merge 6 commits into
Conversation
WorkflowSpec describes a workflow's user-facing interface (identity, title/description, one pydantic params model, structural output descriptions) without factories, sciline keys, or registries, so generic UIs can be generated from it regardless of where compute happens. serialize() projects one-way onto SerializedWorkflowSpec (params as JSON Schema) for cross-process consumers; authoritative validation stays with the process owning the model class. Includes a scipp-free shared parameter vocabulary (unit enums, range/edges models with cross-field validation) with scipp conversions quarantined in spec.conversions, and ADR 0001 recording the design and its rationale (see #653, scipp/esslivedata#889). Adds pydantic as an essreduce dependency. The existing ess.reduce.parameter/workflow machinery is superseded but untouched; removal is a later hard break. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reminder of the extensions the essapps architecture sketch (scipp/essapps, branch
None of this changes the ADR's core: pure interface, one params model, one-way serialization, identity by name and version. |
The spike on D3 and D6 the architecture sketch calls for, built as a real package under packages/essapps (import ess.apps) so it can move into the scipp/ess monorepo unchanged. What is in: the spec vocabulary with data-reference fields and collections (a local copy of the shape in scipp/ess#690 plus the D13 extensions, to be replaced once that merges); run requests and records; a SQLite record store with a single-writer lock and the registry of disk copies; a data store with a private memory cache and a scipp HDF5 disk tier; the binding registry; one runner for both execution shapes; a session launcher (outputs stay in memory) and a subprocess launcher (outputs to disk with a completion marker, reconciled by the backend); the backend with three-layer validation, atomic group submit with pending outputs, failure and cancellation propagation, explicit recompute, stand-in resolution for local files, and slots as a label plus a latest query; views as plain arrays; the Python client interface; and example workflows for load, rebin, and map-combine. Repo tooling copied from the monorepo: ruff config, pre-commit, the test file name check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ArrayValue, and Array(spec) with a structure constraint, accept a reference or an in-process data object and reject plain data, without the spec module naming scipp. Data-reference fields are now found through optionals, unions, and collections, so an optional array input is materialized like any other. as_ref is the one place that decides whether a plain value is a reference; walk_refs, group rewriting, literal inlining, and materialization all use it, and ref_fields lists the fields of a model that may hold one. This is the vocabulary feedback for scipp/ess#690 in code form. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Outputs become a pydantic model class in the same vocabulary as params, with title and description as field metadata, replacing the dict of OutputSpec. Without this, non-array outputs had no type, so a beam centre or direct beam could not be checked against the parameter it feeds in the next workflow. New module ess.reduce.spec.data defines data fields: parameters and outputs holding a file or array rather than a literal. A field is a union of a reference (OutputRef to a run's output, or DatasetRef to data the framework did not compute) and the materialized value (path or scipp object), annotated with DataField(kind, array). Arrays are constrained by ArraySpec on both sides, which gains a binned flag. Collections of data fields are allowed and walked. The spec layer still imports no scipp: the in-process form of an array is validated as "not plain data", and the structural check against ArraySpec lives in spec.conversions. In JSON Schema a data field shows only its reference form, under a dataField key carrying kind and structure. Adds Quantity (scalar or short vector with unit) to the vocabulary and an optional code_revision to the spec; SerializedWorkflowSpec carries outputs_schema next to params_schema. Driven by the essapps architecture sketch (D13), see the discussion on #690. ADR 0001 amended accordingly, including the requirement that a spec module be importable without workflow code and the list of foreseen extensions (cheap parameters, failure reasons, contribution output, discovery) deferred until a consumer exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
I do not like how this got complex in the latest commit -- need to think more about how to reference input (and output) data. Should inputs always be filenames and outputs |
A data field's type was a union of the reference a request names and the value the workflow receives, a path or a scipp object. That made the params model wrong in both phases, needed a validator that accepted anything not plain data, hid that member from the JSON Schema, and put a materialization instruction, Kind, into a spec meant to be pure interface. A data field is now a Ref, annotated with the Format of the bytes and, for scipp data, an ArraySpec. How a workflow gets at the bytes is decided where it is called, by the executor binding, so a framework can add an in-memory path for chained runs without changing a spec or a workflow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…object A data-reference field was typed as a union of the reference a request names and the value the callable receives, with a Kind on the spec telling the runner how to materialize it. That made the params model wrong in both phases, needed a validator accepting anything not plain data, and put an execution decision into the spec. A data field now holds a reference only, annotated with the format of the bytes, which the backend checks between producer and consumer and the picker filters by. The callable takes the validated params and an Inputs and asks for a path or a scipp object per parameter; the sciline wrappers name that form next to the key each parameter maps to. Where the bytes come from stays with the runner, so an in-memory chain in a session changes no spec and no binding. Callables return outputs by field name; the runner validates literals through the outputs model and stores data outputs. The built-in file spec paragraph is gone with the "any kind" rule it existed for. Mirrors scipp/ess#690 at 19f1b15. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The spec module was a copy of scipp/ess#690 with the sketch's additions mixed in, so every change to the proposal cost two edits and the names had drifted. essreduce is now a dependency on the PR branch, and ess.apps.spec keeps what the sketch adds: the spec identity, the contribution output and the parameters finalize reads, the derived models the runner and backend validate against, and the dataset identity. Names follow the branch: OutputRef and Ref for the two forms of reference and their union, DataField and data_fields, dataField in the schema. A dataset reference is the branch's single identity string; dataset_ref encodes the framework's three identities into it, a PID, an instrument and run number, or a path, and dataset_path reads the last back. A dictionary is a dataset reference only when its one key is dataset, which removes the case where a params model with a single pid or path field could be mistaken for one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ADR 0001 listed a contribution output, with the parameters its finalize stage reads, as a foreseen extension for the additive combine of the essapps sketch. The sketch no longer needs it: an aggregation over runs is now two plain specs, a contribute spec and a combine spec, so the contribution is an ordinary output and the finalize parameters are the ordinary parameters of the combine spec. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ADR 0001 listed the parameters a warm workflow can change cheaply as a foreseen spec field. Which parameters are cheap depends on where the implementation cuts its graph, which is an implementation choice and not part of the interface. A workflow author who wants a cheap step to be visible publishes it as a workflow with its own spec, which the ADR already covers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fresh counter-proposal for #653, as announced in the review on #689 — I propose closing that PR in favor of this one.
The core difference to #689: the spec is pure interface. No workflow factory, no sciline keys, no
use_workflow_default— nothing that presumes a particular implementation or execution location. The same spec must make sense whether the workflow runs as a local sciline pipeline, behind a web service, or as a cluster job; compute binding is a parallel mechanism, deliberately out of scope here.The second commit extends the first with what the essapps architecture sketch needs (D13, see the comment below): outputs as a typed model and data-reference fields, so that any output of one workflow run can be checked against the parameter it feeds in the next.
Main decisions (full rationale in ADR 0001, included in this PR):
Parameterentries. Enables cross-parameter validation and JSON Schema for free. The graph-derived "select outputs → see relevant params" feature is dropped, not ported: it treats output selection as workflow slicing, which only the sciline implementation can express.Ref, a reference to an output of an earlier run or to a dataset the framework did not compute, annotated with the format of the bytes (raw NeXus file, scipp object, opaque file) and, for scipp data, anArraySpec(dims, unit, coord units,binned). There is no separate input section: an input is a parameter of data-field type. Collections of data fields are allowed on both sides, and a reference may name one element of a collection output. The spec says nothing about how a workflow gets at the bytes, a path or an in-memory object; that is decided where the workflow is called, by the executor binding, so a framework can add an in-memory path for chained runs without touching a spec. The spec layer still imports no scipp; the structural check of a scipp object against itsArraySpeclives inspec.conversions.titleanddescriptionmandatory; identity isname+version— noinstrument, noWorkflowIdclass. Scoping and global uniqueness are the enumerating context's responsibility. An optionalcode_revisionis provenance, not identity.WorkflowSpec(in-process, holds the model classes, full validation) andSerializedWorkflowSpec(plain data, params and outputs as JSON Schema, JSON-round-trippable). Data fields appear in the schema under adataFieldkey with their format and structure. No inverse — validators don't survive JSON Schema, so authoritative validation stays with the process owning the model classes; remote consumers validate optimistically. This matches the announcement-as-contract design from Rethink workflow registration: move schema ownership to domain packages esslivedata#889.Quantityfor small values with a unit); scipp conversions quarantined inspec.conversions. Value defaults are set by workflow authors at the use site, not by the vocabulary.ess.livedataWorkflowSpeceventually inherits from this. esslivedata already declares outputs as a model with field metadata, so the remaining difference is field types: itssc.DataArraytemplate fields become data fields constrained byArraySpec.Deferred until a consumer exists, listed in the ADR so they are not lost: declared failure reasons, an intermediate flag for retention, declared collection keys, and spec discovery via entry points.
Adds
pydantic>=2.5as an essreduce dependency (already present in the locked CI environments). The existingess.reduce.parameter/workflow/widgets machinery is superseded but untouched; removal is a later hard break — no one depends on it currently.🤖 Generated with Claude Code