Python API#

The public API is the names in powderline.__all__, plus the result envelope, the warning shape and the recipe helpers below.

import powderline

recipe = powderline.load_recipe_asset("examples/example_LaB6/input.json")
checked = powderline.validate(recipe)          # no engine needed
result = powderline.run(recipe, "output/")     # needs the recipe's engine

import powderline imports no engine. validate uses only each gateway’s engine-free schema layer. run imports the engine the recipe’s schema_name names, when it runs.

Run and validate#

powderline.run(recipe, output_dir, *, verbose: bool = False, validate_only: bool = False, **engine_options) → dict[source]#

Run a recipe with the engine its schema_name names; return the standardized result dict.

The gateway is resolved from the recipe (<engine>.<workflow> → powderline.registry.get("<engine>")); the recipe, output_dir, verbose and validate_only are handed to its run together with **engine_options, unchanged and uninterpreted. Each gateway takes its options as explicit keywords, so an option it does not know is a TypeError naming it:

  • gsasii: execution_mode ('auto' | 'server' | 'subprocess') and server_timeout (seconds to wait for the GSAS-II server’s answer; default none, as for an in-process run, 0 = none; past it the run raises ExecutionBackendError timeout and does not fall back in-process, the job being still on the server);

  • topas: topas_dir (the folder holding tc.exe), topas_version (the installed TOPAS release, e.g. "6"; a real run needs it declared, here or in .powderline_config.yaml) and topas_timeout (seconds per tc.exe run, default 3600, 0 = no limit; past it the run raises ExecutionBackendError timeout);

  • easydiffraction: none.

allow_unsupported_engine_version is the gateways’ test-only bypass of the engine version check and is refused here (TypeError); call the gateway’s run directly for compatibility testing.

Before anything on disk changes, the recipe is validated and the gateway’s engine is checked to be importable here: an invalid recipe or a run in an environment without the engine raises and leaves the output directory’s previous results in place. Then PowderLine’s own fixed-name outputs are removed from output_dir (the engine’s files and anything else stay), so a reused directory holds one run’s results.

Parameters:
  • recipe – a recipe dict or a validated model.

  • output_dir – directory for the output files; created by the gateway.

  • verbose – print the gateway’s progress.

  • validate_only – validate and return the gateway’s summary dict (no engine needed, nothing written).

Returns:

The gateway’s ok or generated envelope (powderline.results). Every engine returns these keys: status ("ok", or "generated" when TOPAS’s input was written but no tc.exe is declared), converged (True / False / None when the engine cannot report it or nothing was refined), envelope_version, run_id, rwp, r_exp, gof, chi2_red, simulation_mode, engine_details, warnings, elapsed_time, method, output_files, fit_profile, unit_cell_data, peak_list_data, refined_parameters, output_recipe, output_recipe_valid, parameter_dispositions. GSAS-II and TOPAS results also carry spf_peaks and spf_convergence_diagnostics, and GSAS-II results peak_fit; the Outputs reference page lists every key. A failed run never returns: it raises (below).

Raises:
  • pydantic.ValidationError – the recipe is invalid, or schema_name is missing or malformed.

  • GatewayNotInstalledError – no gateway of that name is installed.

  • EngineNotAvailableError – the gateway’s engine is not installed here.

  • EngineVersionError – the installed engine is outside the supported versions.

  • EngineExecutionError – the engine ran and failed. .reason is one of diverged, unusable_site, peak_fit_failed, engine_crashed, output_missing, output_malformed; .result is the failed envelope, listing the files kept for diagnosis (the engine’s own files and failure.json).

  • ExecutionBackendError – the engine did not run. .reason is one of server_unavailable, server_disabled, communication_failed, timeout, backend_error.

  • TypeError – an option the gateway does not take, or allow_unsupported_engine_version.

  • ValueError – a gateway option with an invalid value.

powderline.validate(recipe, *, verbose: bool = False) → ValidationResult[source]#

Validate a recipe with the gateway its schema_name names; return a ValidationResult.

Engine-free: only the gateway’s schema layer runs, so this works where no engine is installed. A recipe that is already a validated model passes through (idempotent). Nothing is written and nothing is printed unless verbose (forwarded to the gateway).

Parameters:
  • recipe – a recipe dict (as loaded by powderline.load_recipe_asset()) or a validated model.

  • verbose – print the gateway’s validation progress, where it has any.

Returns:

ValidationResult(recipe=<validated model>, warnings=[...]).

Raises:
  • pydantic.ValidationError – the recipe is invalid (every problem listed with its field location); also when schema_name is missing or malformed.

  • GatewayNotInstalledError – no gateway of the name schema_name starts with is installed.

class powderline.ValidationResult(recipe: ~typing.Any, warnings: list[~powderline.exceptions.StructuredWarning] = <factory>)[source]#

What validate() returns: the validated recipe and its warnings.

  • recipe: the gateway’s validated model (e.g. GsasiiRietveldRecipe, TopasRietveldRecipe). Validation never changes a recipe, so the model is the recipe as written plus the schema’s defaults.

  • warnings: the structured warnings (StructuredWarning dicts: code, message, field_path) collected from that model once, in the same call – a default applied, a symmetry remark (Phase.warnings()).

The warnings come with the model because they describe the recipe as written (for example, a block it left out). The validated model states the default instead, so a dump of it cannot show them. The result is frozen: it records one validation. To change the recipe, edit the dict and validate again.

recipe: Any#
warnings: list[StructuredWarning]#
__init__(recipe: ~typing.Any, warnings: list[~powderline.exceptions.StructuredWarning] = <factory>) → None#
powderline.load_recipe_asset(recipe_path: Path) → dict[source]#

Read a recipe file and return it as a dict, uninterpreted.

.json is parsed as JSON; .yaml / .yml / .txt as YAML (an empty YAML document gives {}). Files are read as UTF-8 (a leading BOM is accepted). The content is not validated: pass it to powderline.validate() or powderline.run().

Raises:
  • ValueError – the suffix is none of the above (Unsupported recipe format); the text is not valid JSON / YAML; or the document is not a mapping. The message starts with the file path.

  • OSError – the file cannot be read.

powderline.support_matrix() → List[Dict[str, Any]][source]#

One row per registered gateway: its declarations plus core’s.

Loads each gateway’s engine-free factory through the registry; imports no engine. Undeclared gateways get "declared": False. A gateway whose declarations are malformed (only some keys present) raises ValueError naming it, rather than being listed as undeclared. Consumed by the docs generation (scripts/gen_schema_field_tables.py).

Exceptions#

Every PowderLine error derives from PowderLineError. An invalid recipe raises pydantic’s own ValidationError instead (Validation errors).

exception powderline.PowderLineError[source]#

Base class for all PowderLine errors.

exception powderline.GatewayNotInstalledError(message: str, *, gateway: str)[source]#

No gateway of this name is registered (the gateway package is absent).

__init__(message: str, *, gateway: str) → None[source]#
exception powderline.EngineNotAvailableError(*, gateway: str, dependency: str, env: str, install_command: str)[source]#

The gateway is present, but its engine dependency is not installed here.

__init__(*, gateway: str, dependency: str, env: str, install_command: str) → None[source]#
exception powderline.EngineVersionError(*, gateway: str, engine: str, installed: str, supported: str, message: str | None = None)[source]#

The installed engine version is outside the gateway’s supported range.

__init__(*, gateway: str, engine: str, installed: str, supported: str, message: str | None = None) → None[source]#
exception powderline.EngineExecutionError(message: str, *, reason: str | None = None, result: dict | None = None)[source]#

The engine ran and failed (envelope failure.kind == "engine").

default_reason = 'engine_crashed'#
exception powderline.ExecutionBackendError(message: str, *, reason: str | None = None, result: dict | None = None)[source]#

The engine did not run: the execution backend failed (failure.kind == "backend") – the GSAS-II server was unavailable or disabled, its output is not visible here, the request timed out, or a configuration error stopped the run before the engine started.

default_reason = 'backend_error'#

The result envelope#

The exceptions carry the failed envelope as .result, and every gateway builds its result with these functions. The module also defines ENVELOPE_VERSION ("1"), STATUSES (ok, generated, failed), and ENGINE_REASONS and BACKEND_REASONS, the failure reasons.

The run-result envelope every gateway produces and every caller receives.

Errors are values on the wire and raised in the local API: a gateway (or the GSAS-II server, or a future worker) builds a failed envelope with failure_envelope(); raise_if_failed() turns it into EngineExecutionError (the engine ran and failed) or ExecutionBackendError (the engine did not run) with the envelope on .result. An ok or generated envelope is what powderline.run() returns; a failed one is never returned. Engine-free.

powderline.results.failure_envelope(kind: str, reason: str, message: str, *, run_id: str | None = None, elapsed: float = 0.0, method: str = 'none', warnings: list | None = None, output_files: list | None = None, traceback: str | None = None, engine_details: dict | None = None, simulation_mode: bool | None = None) → dict[source]#

A failed envelope. kind is "engine" (the engine ran and failed) or "backend" (it did not run).

output_files lists the files kept for diagnosis: the engine’s own files written so far and, once powderline.run() has recorded the failure, failure.json. PowderLine’s result tables are never written for a failed run.

powderline.results.generated_envelope(*, run_id: str | None = None, elapsed: float, method: str, warnings: list, output_files: list, simulation_mode: bool, engine_details: dict) → dict[source]#

A generated envelope: the engine’s input was written but the engine did not run (TOPAS without tc.exe).

powderline.results.with_status_fields(result: dict, *, status: str, converged: bool | None) → dict[source]#

Stamp an engine’s ok/generated result with the envelope fields.

status is the outcome, so any success, error or traceback key is removed.

powderline.results.raise_if_failed(envelope: dict) → dict[source]#

Raise the exception a failed envelope stands for; return any other envelope unchanged.

Structured warnings#

The shape of each entry in result["warnings"] and ValidationResult.warnings. The codes are on Messages.

class powderline.exceptions.StructuredWarning[source]#

One entry of the warnings list in results and validate() output.

field_path is the dotted recipe path the warning refers to, or None when it is not tied to one field. Every gateway carries these into its results.

level is "warning" – something the user should act on or decide about (a default applied, a parameter at a limit) – or "info" – a record of something PowderLine did that changes no outcome, such as a coordinate held at its exact symmetry value. Absent means "warning"; consumers read it as w.get("level", "warning"). The CLI prints warnings and only counts info entries unless --verbose; a GUI can show both.

level: Literal['warning', 'info']#
code: str#
message: str#
field_path: str | None#

Recipe helpers#

Move a core block (payload.xrd_data, payload.fit_range or payload.phases.<name>) from one recipe to another. This is how a series of runs is chained (Run a series).

powderline.recipes.block(recipe: dict, path: str) → dict[source]#

A deep copy of the core block at path (payload.xrd_data, payload.fit_range, payload.phases.<name>) of a recipe dict – the chaining currency between tools (KI-04).

Raises:
  • ValueError – path is not a core block (engine blocks are never shared between engines).

  • KeyError – the recipe has no such block.

powderline.recipes.with_block(recipe: dict, path: str, value: dict) → dict[source]#

A deep copy of recipe with the core block at path replaced by a copy of value.

The result is not validated here: the target recipe’s own schema is the gate (powderline.validate()). Raises ValueError for a path that is not a core block.