Skip to content

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:

source cases (VTK field + mesh)
→ recipe run
→ target collection with converted case outputs

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)
  is_valid: True
  issues: 0

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 — current RunState (PENDING, RUNNING, SUCCESS, …)
  • run.refresh() — poll monitoring once; use include_diagnostics=True for task-level detail
  • run.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"):

run = recipe.run()
run = run.wait(progress="stdout")

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:

print(list(project.available_writers))
print(list(project.available_readers))

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):

run = recipe.run(target_collection_mode="replace")

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; call refresh() on an item to hydrate monitoring fields (there is no project.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.

Next: read those results for analysis or training.