Transform cases with a recipe¶
You have admitted cases — for example a field and its mesh that must travel together — and you want Nexus to convert or extract them into a target collection.
A recipe is a reusable source → target transformation. This page continues the VTK field + mesh layout from Describing your data: build a converter recipe, validate it, run it, then inspect outputs.
Spec authoring and validation without a live run: transform.py.
Start here: convert field files to XDMF¶
Desired outcome:
Converter¶
A converter (RemoteSigmaConverter) declares required extensions, a reader, and a writer:
from miura.nexus.recipe.sigma_converter import RemoteSigmaConverter, SigmaClassRef
converter = RemoteSigmaConverter(
required_extensions=[".vtk"],
reader=SigmaClassRef(class_name="vtkDataSetReader"),
writer=SigmaClassRef(class_name="vtkXdmfWriter"),
)
Bind grouping and selection¶
Grouping decides which files form a case. Selection decides which admitted cases enter the run (item metadata; see Selection).
For the simplest layout—one case per immediate child folder—use root-path grouping:
from miura.nexus.case_grouping import CaseGrouping
from miura.sdk import LocalDataSource, RecipeSpec
case_grouping = CaseGrouping.root_path("/", depth=1)
upload = source.upload(
LocalDataSource(data_path),
case_grouping=case_grouping,
)
spec = RecipeSpec.from_sigma_converter(
source=source.name,
target=target.name,
converter=converter,
)
recipe = project.deploy_recipe(
"vtk-to-xdmf",
spec,
cases=upload.cases,
)
run = project.run_recipe(recipe)
For a tree containing /simulation_000/... and /simulation_001/..., this
admits two cases. Use the more explicit role-based form below when each case
must contain several named files.
from miura.nexus.case_grouping import CaseGrouping
from miura.sdk import RecipeSpec
from miura.sdk.recipe import CaseSelection
case_grouping = CaseGrouping.join_by_path_id(
"case_id",
{
"boundary_solution": "{case_id}/{case_id}.vtk",
"mesh": "{case_id}/{case_id}_mesh.vtk",
},
require=["boundary_solution", "mesh"],
)
selection = CaseSelection.where(
split="train",
labels={"solver": "simpleFoam"},
)
spec = RecipeSpec.from_sigma_converter(
source="source-vtk",
target="target-xdmf",
converter=converter,
case_selection=selection,
)
| Question | Mechanism |
|---|---|
| Which files belong to one case? | case_grouping= on upload/plan (admit); cases= on deploy_recipe (pin) |
| Which cases enter this run? | case_selection= on the spec (optional) |
Admit the grouping when the data goes in, then pass the cases it admitted when deploying — not a raw CaseGrouping, and not on the RecipeSpec:
from miura.sdk import LocalDataSource
upload = source.upload(LocalDataSource(data_path), case_grouping=case_grouping)
recipe = project.deploy_recipe(
"vtk-to-xdmf",
spec,
cases=upload.cases, # or plan.cases
)
deploy_recipe(..., cases=) accepts AdmittedCases from upload.cases / plan.cases (preferred), or an admitted CaseGroupingResult for advanced callers. Bare result_id strings, raw CaseGrouping descriptions, and the upload/plan objects themselves are not accepted — pass .cases.
Companions¶
Most converter recipes do not need a transform entry. You author the Sigma
converter (extensions, reader, writer), pin the admitted cases, and run. For
each case, the platform stages the case files and the converter picks its input
from those files (for example by required_extensions). That is enough when
one obvious file per case feeds the transformation.
When a case must carry several parts together — for example a field that needs
its mesh — keep that guarantee in case grouping
with require=[...]. Every admitted case then already has both roles before
the recipe runs. You still omit any transform entry on the spec.
Naming the primary modality on the recipe¶
When you need the run to bind by published logical assets
— which asset is primary, and which companion modalities must be present —
pass a TransformEntry on the spec:
from miura.nexus.recipe.sigma_handoff import TransformEntry
spec = RecipeSpec.from_sigma_converter(
source="source-vtk",
target="target-xdmf",
converter=converter,
transform_entry=TransformEntry(
primary_modality_id="field",
required_companion_modalities=["mesh"],
),
sigma_handoff_capability=capability,
)
transform_entry names the modalities; sigma_handoff_capability tells the
run how those assets become the converter’s local input. Omit both when the
converter should simply pick its input from the staged case files, as in the
path above.
Validate, then run¶
project.validate_recipe(spec) checks the spec without creating a recipe or starting a run. The same check runs when you create a recipe.
from miura.display import print_validation_result
validation = project.validate_recipe(spec)
print_validation_result(validation)
Register, queue, and wait:
from miura.display import print_run_triggered
from miura.nexus.recipe.recipe_execution import RunState
recipe = project.deploy_recipe(
"vtk-to-xdmf",
spec,
description="Convert VTK meshes to XDMF",
cases=upload.cases,
)
run = recipe.run()
print_run_triggered(run)
run = run.wait()
if run.state == RunState.FAILED:
raise RuntimeError(run.execution_error or "recipe run failed")
Inspect the target collection after success:
for item, depth in target.walk_items(path="/", include_dirs=False):
print(item.item_uri, item.file_size)
Changing a recipe¶
Recipes are stable identities with append-only immutable revisions. Prefer these ops over delete-and-recreate:
| Intent | Call |
|---|---|
| Deploy current definition | project.deploy_recipe(name, spec, cases=…) |
| Typed rebind (e.g. new case grouping) | recipe.update(cases=upload.cases) |
| Same definition, same data universe | run = recipe.run() then run.wait() (default inputs="pinned") |
| Encompass newer collection membership | recipe.run(inputs="latest") once re-admit is wired; until then use plan → update → run |
| Explicit first create only | project.create_recipe(name, spec) (conflicts if name exists) |
| Intentionally replace the name | new recipe name, or delete-and-recreate |
Primary path: deploy_recipe(..., cases=…) → recipe.run() → run.wait(). deploy_recipe requires admitted cases, creates when absent, no-ops when equivalent, and activates revision N+1 when the executable definition differs. Use create_recipe only to store an intentionally unbound, non-runnable definition. get_or_create_recipe remains for strict create-or-verify (raises RecipeSpecConflictError on mismatch).
Update scenarios (what to expect)¶
| Situation | Outcome |
|---|---|
deploy_recipe on a new name |
Creates recipe (revision 1) |
deploy_recipe with identical effective spec |
No-op; same revision handle |
deploy_recipe with different executable spec / CGR |
New revision activated |
get_or_create_recipe when stored spec differs |
RecipeSpecConflictError (recipe unchanged) — then deploy_recipe or update |
recipe.update(cases=…) |
New revision with rebinding; prior revision preserved |
| Stale handle after another revise | RecipeConflictError — refresh and retry |
| Invalid deploy payload | ValidationError — no revision |
Runnable walkthrough: recipe_lifecycle.py. Unit matrix: tests/unit/test_api/test_recipe_update_scenarios.py.
A stale handle after a concurrent revision raises RecipeConflictError — refresh the recipe and retry. Ordinary run tags are metadata only; reserved keys such as case_grouping_result_id are rejected on run_recipe (use update / inputs="latest" instead).
A stale handle after a concurrent revision raises RecipeConflictError — refresh the recipe and retry. Ordinary run tags are metadata only; reserved keys such as case_grouping_result_id are rejected on run (use deploy / update / inputs="latest" instead).
recipe.run() is the sync shorthand for project.run_recipe(recipe) when the recipe came from the sync API. You can still pass a recipe name or id to project.run_recipe.
To create and queue in one step, pass submit=True to create_recipe (still returns the recipe handle; use recipe.run() when you need the run handle).
Track progress¶
recipe.run() returns a run handle (SyncRecipeRunHandle in sync code). It exposes execute-response fields immediately (id, state, created_at) and grows richer after refresh().
While a run is in flight:
run.state— currentRunState(PENDING,RUNNING,SUCCESS, …)run.refresh()— poll monitoring once; useinclude_diagnostics=Truefor task-level detailrun.wait()— poll until a terminal state (default timeout 60 minutes)
wait() drives live progress through RecipeRunProgressEngine. By default it follows session progress settings (Nexus(progress="auto") or MIURA_PROGRESS). Override per wait with progress= ("off", "log", "stdout", "auto", "rich"):
Extract structured tables instead¶
You need this when the outcome is rows and columns (for pandas / training), not converted mesh files. Name the writer that produces that output:
from miura.sdk import RecipeSpec
from miura.sdk.sigma import sigma
spec = RecipeSpec.from_sigma_dataset_generator(
source=source.name,
target=target.name,
dataset_generator=sigma.DataSetGenerator(
sigma.GlobalExtractionSchema(
sealed_algorithm={"global_param_name": "param_1"},
)
),
writer="parquet_tabular",
)
recipe = project.deploy_recipe(
"cases-to-tables",
spec,
cases=upload.cases,
)
The writer id decides what the run produces — parquet_tabular writes the extracted rows as Parquet tables. deploy_recipe checks the id against your deployment before creating anything, so a typo fails immediately instead of after a run starts:
validation_error: Writer 'parquet_tabluar' not found. Did you mean: ['parquet_tabular']? Available writers: ['parquet_tabular']
To see which ids your deployment offers, list them:
available_readers returns the Sigma reader class_name values registered on
your deployment — the same strings you pass to SigmaClassRef(class_name=...).
Leave the Sigma reader unset when you want auto-detect; that path is not
checked against the catalog.
Needs the [sigma] extra. After a successful run, read outputs in Read recipe results for analysis — published tables for generator recipes, or Parquet items when files land in the target collection.
Target upload mode¶
Recipe runs write into the target collection. That is separate from local ingest, but uses the same UploadMode vocabulary (INSERT, UPDATE, UPSERT, REPLACE). Source collections never take an upload mode.
Set a default on the spec with TargetCollectionRef.upload_mode (omit to accept the platform default INSERT):
from miura.sdk import RecipeSpec, UploadMode
from miura.sdk.recipe import TargetCollectionRef
spec = RecipeSpec.from_sigma_converter(
source="source-vtk",
target=TargetCollectionRef(
collection_name="target-xdmf",
upload_mode=UploadMode.UPSERT,
),
converter=converter,
case_selection=selection,
)
recipe = project.deploy_recipe(
"vtk-to-xdmf",
spec,
cases=upload.cases,
)
Override one run with target_collection_mode= on recipe.run / run_recipe (wins over the recipe default):
When you need deeper run control¶
Skip this section for the common validate → run → wait path.
run.refresh()/run.wait(on_poll=...)— poll or custom status lines (display helpers)project.list_runs(...)— list summaries; callrefresh()on an item to hydrate monitoring fields (there is noproject.get_run(run_id))create_recipe(..., submit=True)— create and queue in one step (returns the recipe handle)
See Recipe for the full run-handle surface.
Next step¶
You can declare how cases are transformed, validate the spec, run the recipe, and inspect target outputs.