Quickstart¶
This walkthrough builds a tiny two-step pipeline: image a measurement set with
WSClean, then make a mask from the
resulting image with breizorro. It mirrors
examples/simple_selfcal.py in the repository.
Define some cabs¶
A Cab is a typed, backend-agnostic description of an atomic
task. Give it a name, the command to run, an optional container
image, and pydantic models for its inputs and outputs.
from pydantic import BaseModel
from shinobi import Cab, Recipe, step
from shinobi.loaders import build_model
class ImageInputs(BaseModel):
ms: str = "obs.ms"
prefix: str = "img"
class ImageOutputs(BaseModel):
restored: str | None = None
wsclean = Cab(
name="wsclean",
command="wsclean",
image="quay.io/stimela/wsclean:latest",
inputs_model=ImageInputs,
outputs_model=ImageOutputs,
)
breizorro = Cab(
name="breizorro",
command="breizorro",
image="breizorro:latest",
inputs_model=build_model("MaskInputs", {"restored_image": ("File", True, None)}),
outputs_model=build_model("MaskOutputs", {"mask": ("File", False, None)}),
)
You can hand-write the pydantic models, or build them from a compact
{name: (dtype, required, default)} spec with
shinobi.loaders.build_model() (the same helper the YAML loaders use).
Run a single cab¶
Wrap a cab in a @shinobi.step function to make it
runnable. The body receives an ExecContext (ctx); calling
ctx.run() executes the cab on the chosen backend and returns its result.
@step(wsclean, backend="native")
def image(ctx):
"""Image the visibilities. A near-empty body auto-runs the cab."""
return ctx.run()
Save the file as myrecipe.py and run it straight from the command line –
the cab’s input schema becomes the CLI options:
$ ninja run myrecipe.py:image --ms data.ms --prefix out
Compose a recipe¶
A Recipe wires steps together. Use add_step with the
recipe.inputs / recipe.outputs proxies to declare the data flow: each
proxy attribute is a reference that the engine resolves at run time.
selfcal = Recipe(
name="selfcal",
inputs_model=ImageInputs,
outputs_model=build_model("Out", {"mask": ("File", False, None)}),
)
selfcal.add_step("image", wsclean, ms=selfcal.inputs.ms, prefix=selfcal.inputs.prefix)
selfcal.add_step("mask", breizorro, restored_image=selfcal.outputs.image.restored)
selfcal.set_output("mask", selfcal.outputs.mask.mask)
Here selfcal.outputs.image.restored is the restored output of the step
named image – wiring it into the mask step’s restored_image input
creates the dependency edge between the two.
Preview the graph¶
Before running anything, use --dryrun to see the execution graph the recipe
produces for a given set of inputs:
$ ninja run myrecipe.py:selfcal --ms data.ms --dryrun
[ image ]
|
v
[ mask ]
Run the recipe¶
Drop --dryrun to execute it for real:
$ ninja run myrecipe.py:selfcal --ms data.ms
Where to next¶
Cabs – defining cabs in Python or loading them from YAML.
Steps –
@shinobi.stepvs@shinobi.pystep.Recipes – declarative vs orchestration-function wiring.
Backends – running natively, in containers, or on a cluster.
Offloading to a cluster – compiling a recipe to Slurm and detaching.