Use PowderLine from Python and in pipelines#
Goal: check and run recipes from your own code: a notebook, a beamline pipeline, a web service, a recipe builder or an agent. Results come back as Python objects, failures as exceptions you can catch, and your metadata travels with the analysis.
You need:
A local clone of PowderLine.
pixi for your own project.
For runs, the engine your recipes name.
A recipe is structured data: in Python, a dict. It can come from a file, a
database record, a message on a queue or a live data stream. PowderLine
never needs it on disk.
1. Add PowderLine to your project#
PowderLine is not on PyPI or conda-forge. You add it to your project as an editable install of a local clone. Changes to the clone’s source are picked up without reinstalling.
Two pixi.toml templates in docs/integration/ match PowderLine’s own
environments:
Template |
What it installs |
Use it for |
|---|---|---|
|
PowderLine and its own dependencies, no engine |
building, editing or checking recipes; CI pre-flight checks |
|
the same, plus GSAS-II |
running |
Copy the template into your project as
pixi.toml:cp /path/to/PowderLine/docs/integration/pixi_schema_only.toml /path/to/your-project/pixi.toml
Set your project’s name in
[workspace]and the path to your clone in[pypi-dependencies]:[workspace] name = "your-project-name" [pypi-dependencies] powderline = { path = "/path/to/PowderLine", editable = true }
Install and check:
cd /path/to/your-project pixi install pixi run python -c "import powderline; print(powderline.support_matrix())"
Check: the matrix lists three gateways, easydiffraction, gsasii and
topas, each with the schema versions it accepts.
Engines in your project#
GSAS-II is not on PyPI or conda-forge either.
pixi_full.tomldeclares it as a package built by pixi from thetacaswell/GSAS-IIfork, pinned to the same commit as PowderLine’s ownpixi.toml. Keep that pin: the gateway checks for GSAS-II 5.7.9 at run time and refuses another version.easydiffraction: add what PowderLine’s
easydiffractionfeature declares: Python 3.12 or later,easydiffraction ==0.21.1from PyPI anddiffpy.pdffit2from conda-forge.TOPAS has no Python package. See Run TOPAS recipes.
import powderline imports no engine. An engine is imported only when a
recipe of that engine runs.
2. Validate a recipe#
Validation needs no engine. It works for every engine’s recipes in the validation-only environment.
import json
from pydantic import ValidationError
import powderline
recipe = json.loads(text) # from a file, a database record, a message ...
try:
checked = powderline.validate(recipe)
except ValidationError as exc:
for error in exc.errors():
print(".".join(str(p) for p in error["loc"]), "|", error["msg"])
else:
for w in checked.warnings: # {code, message, field_path[, level]}
print(w["code"], w["field_path"])
Every problem is listed at once, each at its field, most with the value to write:
payload.instrument.radiaton | Extra inputs are not permitted
payload.phases.LaB6.unit_cell | Value error, unit cell is inconsistent with 'P m -3 m' (cubic): stated b = 4.2, but by symmetry b = a; write b = 4.15682
A valid recipe can still carry warnings: a default applied, or a value held
in its exact symmetry form. checked.recipe is the validated model.
Messages lists every warning code.
To read a recipe saved on disk, powderline.load_recipe_asset(path) returns
the dict without interpreting it.
3. Run a recipe#
from pathlib import Path
result = powderline.run(recipe, Path("output/run_0001"))
print(result["status"], result["rwp"], result["converged"])
cell = result["unit_cell_data"]["LaB6"] # a pandas DataFrame
run() validates the recipe, hands it to the engine its schema_name
names, writes the report files into the output directory and returns a
dict. Tables are pandas DataFrames. Per-phase tables are dicts keyed by
phase name. Every engine returns the same keys; see
Outputs.
powderline.run(recipe, output_dir, validate_only=True) validates and
returns a short summary, without an engine and without writing anything.
Engine options#
Engine options are keyword arguments. An option the recipe’s engine does
not take is a TypeError.
Engine |
Option |
Meaning |
|---|---|---|
GSAS-II |
|
|
GSAS-II |
|
seconds to wait for the GSAS-II server’s answer; default no limit |
TOPAS |
|
the folder holding |
TOPAS |
|
the installed TOPAS version, |
TOPAS |
|
seconds per |
easydiffraction |
none |
result = powderline.run(topas_recipe, Path("out"), topas_version="6", topas_dir=Path(r"C:\TOPAS6"))
4. Handle failures#
A failed run never returns a result. It raises EngineExecutionError when
the engine ran and failed, and ExecutionBackendError when it never ran:
try:
result = powderline.run(recipe, out)
except powderline.EngineExecutionError as e:
print("refinement failed:", e.reason, e.result["output_files"])
except powderline.ExecutionBackendError as e:
print("could not run the engine:", e.reason)
.reasonis a short code, such asdivergedortimeout. Messages lists every reason and what it means..resultis the full failure record..result["failure"]holds thekind,reason,message,tracebackandengine_details. For GSAS-II and TOPAS it also holdsconsole_tail, the last of what the engine printed.The same record is written to
failure.jsonin the output directory, next to whatever the engine wrote. No result table is written for a failed run.
Other errors come before anything runs:
An invalid recipe raises pydantic’s
ValidationError.A recipe whose engine is not installed here raises
EngineNotAvailableError, naming the environment to use.An installed engine of the wrong version raises
EngineVersionError.
An invalid recipe or a missing engine leaves the output directory as it
was, with any earlier results in place. All PowderLine errors derive from
powderline.PowderLineError. pydantic’s ValidationError is a
ValueError.
Carry metadata and provenance through#
A recipe may carry a free-form metadata block: a sample ID, a scan ID, the
data’s place in your catalog, how the recipe was made.
recipe["metadata"] = {"sample_id": "S-0421", "scan_id": "70c707"}
PowderLine never reads it. It must be JSON-serializable and at most 1 MiB as
JSON. The output recipe keeps it unchanged and adds one key,
powderline_provenance:
result["output_recipe"]["metadata"]
# {'sample_id': 'S-0421', 'scan_id': '70c707',
# 'powderline_provenance': {'parent_run_id': '…', 'schema_name': 'gsasii.rietveld',
# 'engine': 'gsasii', 'engine_version': '5.7.9',
# 'powderline_version': '0.2.0', 'created': '2026-…'}}
Results explains every provenance key
and the output recipe. Every result has its own run_id, and its output
recipe names that run as parent_run_id. The next run replaces that block,
so an output recipe names only the run just before it. Store each step’s
input recipe and run_id with its results: the input’s parent_run_id then
links each step to the one before, back to the data and the first recipe. A failure.json carries the same provenance
(Outputs).
Use the GSAS-II server client directly#
powderline.run() is the way to run recipes from Python. For gsasii.*
recipes it already uses the GSAS-II server in its default auto mode
(Run at a beamline).
A tool that talks to the server itself can use its client, GSASClient:
from pathlib import Path
from powderline.gateways.gsasii.client import GSASClient
client = GSASClient(fallback_to_subprocess=True) # run in-process if the server cannot be used
result = client.submit_simulation(
recipe=Path("my_sample/input.json"), # also a dict, or a validated gsasii recipe model
output_dir=Path("my_sample/output"),
auto_start_server=True, # start the server if it is not running
)
if result["status"] == "ok":
print(result["method"], result["rwp"]) # "server" or "subprocess"
else:
failure = result["failure"]
print(failure["kind"], failure["reason"], failure["message"])
The client differs from powderline.run():
It returns a failed result instead of raising. Check
result["status"],"ok"or"failed".fallback_to_subprocess=Falsebehaves likeexecution_mode="server": no in-process run.request_timeout=600behaves likeserver_timeout=600.use_server=Falseinsubmit_simulationskips the server and runs in-process.The tables come back as plain lists and dicts, not DataFrames.
It builds no output recipe and no parameter dispositions, and it writes no
failure.json. It does not clear earlier results from the output directory.
Example scripts#
Both scripts work as they are from a PowderLine clone:
docs/integration/example_schema_only.pyvalidates one recipe, then every committed example recipe of every engine, with no engine installed:pixi run python docs/integration/example_schema_only.py
docs/integration/example_full.pyvalidates the LaB6 example, runs it withpowderline.run(), then runs it again through the GSAS-II server client,GSASClient:pixi run -e gsasii python docs/integration/example_full.py
If you copy a script elsewhere, edit its POWDERLINE_REPO constant.
Note
The gateways are found through install-time entry points. If
support_matrix() comes back empty, or pixi run powderline --doctor
reports no gateway, see KI-13.
Next: Run a series, Run at a beamline, Python API.