save_spec
save_spec(spec, path, *, defaults=False)Write a specification back out as YAML - what was set, not what it holds.
from_yaml reads a configuration and PlatypusSpec builds one in code; this is the way back, so the two interfaces are one pipeline rather than one and a half. A worked example can write the YAML of the run it has just done beside the results, and a specification assembled in R or Python can become the file somebody else runs.
It writes the fields that were set. Not the fields the object holds, which on a two-field model is 29 of them - and the difference is not verbosity. A model given weights and no architecture adopts the architecture from the weights sidecar, and that adoption fires on architecture not being in model_fields_set. Dump the defaults into the file and the reloaded specification has all 29 set, so the adoption can never fire again: the file would look like the original and quietly stop doing what it did. R paid for this once already in the other direction (JOURNAL.md §4aa) and dropped its own defaults for the same reason.
defaults=True is the opposite want, which is legitimate and is a second mode rather than the default: freeze this run exactly as it resolved, so it reproduces in five years. The cost is the one above - the file stops adopting anything, by design.
Every discriminator is always written, whatever was set. task is the obvious one - it is what from_yaml dispatches on, and a configuration that does not say what it is asking for means whatever the version reading it decides. The same is true one level down and is easier to miss: DiceLoss(smooth=0.5) sets smooth and not name, because name is a Literal with a default, so a dump of what was set alone writes loss: {smooth: 0.5} - a block with no tag, which comes back as Unable to extract tag using discriminator ‘name’. A field annotated Literal is a tag here, and tags survive.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| spec | PlatypusSpec | The specification to write. Either task. | required |
| path | str | Path | Where to write it. The parent directory must exist. | required |
| defaults | bool | Write every field at its resolved value, including the ones nobody set. | False |
Returns
| Name | Type | Description |
|---|---|---|
| Path | The path written, so a caller can log or return it. |
import tempfile, pathlib from pyplatypus.spec import SegmentationSpec, SegmentationData, SegmentationModel spec = SegmentationSpec( … data=SegmentationData( … train_path=“train/”, validation_path=“valid/”, … colormap=[[0, 0, 0], [255, 255, 255]], … ), … models=[SegmentationModel(name=“m”, input_shape=(32, 32))], … ) path = pathlib.Path(tempfile.mkdtemp()) / “spec.yaml” _ = save_spec(spec, path) sorted(yaml.safe_load(path.read_text())[“models”][0]) [‘input_shape’, ‘name’] frozen = save_spec(spec, path.with_name(“frozen.yaml”), defaults=True) len(yaml.safe_load(frozen.read_text())[“models”][0]) 29