Architecture#
This page is for contributors and engine developers. It shows how the package is laid out, what happens during a run, and which parts of the code carry which guarantees. For the rules that bind a change, see Policies.
Design principles#
Example-driven. Features are added when a real refinement case needs them. The examples are also the regression references.
Schema-first. Every rule a recipe must obey lives in the schema models. Validation reports every problem at its field before any engine runs.
Honest. An engine’s silent default, floor or conversion is refused at validation, with the value to write, or reported as a structured warning. It is never papered over.
Engine-native. Nothing is translated between engines. A recipe states the numbers its engine computes with, in that engine’s units.
Reproducible. The same recipe produces the same refinement, and the reference outputs are tracked in the repository.
Package architecture#
flowchart TB
U["powderline CLI · powderline.run / validate"]
subgraph core["powderline core: every environment, imports no engine"]
API["api · kicker"]
REG["registry<br/>entry-point group powderline.gateways"]
SCH["schema_core · symmetry · compat"]
OUT["fitstats · reports · results · roundtrip"]
end
U --> API --> REG
REG --> G1["gateways/gsasii"] --> E1["GSAS-II<br/>env gsasii"]
REG --> G2["gateways/topas"] --> E2["tc.exe<br/>a Windows machine with TOPAS 6"]
REG --> G3["gateways/easydiffraction"] --> E3["easydiffraction<br/>env easydiffraction"]
The core is installed in every pixi environment and never imports an engine.
Each gateway adapts one engine. Its engine lives in its own environment, or,
for TOPAS, on the machine that has TOPAS installed. The dev environment
holds every engine plus the docs and test tooling. See
Install for the environments.
Package map#
Core (src/powderline/)#
Module |
Role |
|---|---|
|
The public API ( |
|
|
|
The command line ( |
|
|
|
Gateway discovery from entry points, the |
|
Core schema: the recipe frame, parameter shapes, data, fit range, Chebyshev background, the phase block, and the building blocks engine schemas are made from |
|
gemmi-backed symmetry: canonical space-group names, cell ties, special positions, coupling groups |
|
Declared schema compatibility, |
|
The uniform Rwp, Rexp, GoF and reduced χ² |
|
The standardized report writers and the list of fixed file names |
|
The result envelope: status values, failure reasons, |
|
The output recipe and the parameter dispositions |
|
|
|
The exception classes and |
|
Reads |
|
Console status glyphs |
Gateways (src/powderline/gateways/<engine>/)#
Every gateway has the same two layers.
Schema layer:
schema.py. It holds the engine schema models built onschema_core, the version declarations (DECLARATIONS),SCHEMA_NAMESandvalidate_recipe. It imports no engine.powderline.validate()runs only this layer.Runtime layer:
gateway.pyand the modules that drive the engine.gateway.pyexports the entry-point factoryget_gateway()andENGINE_VERSION_SPEC. The engine is imported lazily, fromrun().
Gateway |
Runtime modules |
|---|---|
|
Frozen: |
|
|
|
|
gsasii/simulation_builder.py (SimulationRecipeBuilder) turns a
crystal-structure dict into a gsasii.rietveld simulation recipe, with
refinement_cycles: 1 and every refine flag false:
Input:
space_group,unit_cell,formula, an optionalmaterial_id, andsites, which lists every atom of the full conventional cell. The builder reducessitesto one atom per orbit, and refuses a list whose atom count does not match each site’s multiplicity.Space group: written as its canonical name. A group with two origins, given in origin choice 1, is moved to origin choice 2, because GSAS-II cannot use origin 1.
Defaults: every value is stated in the recipe, in GSAS-II’s units. SH/L is 0.002, GSAS-II’s floor. The built-in defaults are overridden by a
simulation_defaultsdict, whichconfig_loader.ConfigLoaderreads from.powderline_config.yaml. Unknown keys there are errors.
No engine at import time#
import powderline, powderline.api, powderline.kicker and the schema
layer of every gateway import no engine. Subprocess tests block the engine
imports and fail if one slips in. Keep engine imports inside code that runs
from run().
Note
The pixi run test suite installs a guard that makes any engine import fail.
POWDERLINE_CORE_GUARD=1 forces the guard on for a hand-picked selection;
=0 turns it off.
Entry points#
Gateways are discovered from the powderline.gateways entry-point group in
pyproject.toml:
[project.entry-points."powderline.gateways"]
gsasii = "powderline.gateways.gsasii.gateway:get_gateway"
topas = "powderline.gateways.topas.gateway:get_gateway"
easydiffraction = "powderline.gateways.easydiffraction.gateway:get_gateway"
There is no in-code fallback. The in-repo gateways use exactly the mechanism a third-party gateway would.
Important
Entry-point metadata is written when the package is installed. After you
edit [project.entry-points] or [project.scripts], run
pixi reinstall powderline --all, because every environment has its own install.
Until then the registry finds nothing (KI-13).
register_gateway(name, factory) registers a gateway in code. It is meant
for tests and development, and takes precedence over an entry point of the
same name.
A run, step by step#
flowchart TD
A["powderline recipe.json<br/>or powderline.run(recipe, output_dir)"] --> B["api: schema_name → registry.get(engine)"]
B --> C["gateway.validate<br/>schema layer"]
C --> D["engine importable?<br/>then clear the fixed-name outputs"]
D --> E["gateway.run<br/>engine version check"]
E --> F["runtime layer<br/>runs the engine"]
F --> G["fitstats · roundtrip<br/>reports"]
G --> H["result dict<br/>status ok or generated"]
F -- "failure" --> X["EngineExecutionError or<br/>ExecutionBackendError<br/>+ failure.json"]
The CLI or
powderline.run()reads the engine fromschema_name(<engine>.<workflow>) and asks the registry for that gateway.The recipe is validated through the gateway’s schema layer. An invalid recipe raises one
pydantic.ValidationErrorand nothing on disk changes.run()checks that the engine is importable. Only then does it remove PowderLine’s fixed-name files from the output directory.The gateway checks the installed engine version against
ENGINE_VERSION_SPECand runs the engine:gsasii:
routing.run_refinementin this process or on the GSAS-II server, thenexecutors.run_refinementand GSAS-II’s scripting API.topas:
native_writerwrites the INP and.xye,tc.exeruns, andnative_runreads TOPAS’s files. Withouttc.exethe run is generate-only.easydiffraction:
native_builderbuilds the project, then a fit or a calculation runs, andnative_runreads the result.
The gateway computes the uniform statistics, builds the output recipe and writes the report files.
A failed run raises.
run()writes the failed envelope tofailure.jsonbeside the engine’s own files and re-raises. No result table is written.
With validate_only=True the gateway returns a summary and runs nothing.
See Outputs for the result dict and
Results for what the failure path means to a user.
GSAS-II execution modes#
flowchart TD
M{"execution_mode"}
M -- "subprocess<br/>(--no-server)" --> IP["in this process"]
M -- "auto (default)" --> C["GSASClient"]
M -- "server<br/>(--use-server)" --> C
C --> Q{"server usable?<br/>started if needed,<br/>current code"}
Q -- "yes" --> SV["GSAS-II server"]
Q -- "no, auto" --> IP
Q -- "no, server" --> F["ExecutionBackendError"]
IP --> R["routing.run_refinement<br/>→ executors"]
SV --> R
Mode |
CLI |
Behavior |
|---|---|---|
|
default |
Uses the server, starting it if needed; runs in-process if it is unavailable |
|
|
Server only; fails if the server cannot be used |
|
|
Skips the server; GSAS-II runs in the calling process, despite the name |
The server (server.py, with client.py as its client) keeps GSAS-II
imported in one process, so a batch of runs pays GSAS-II’s import once. Its
security model:
Aspect |
What the code does |
|---|---|
Owner |
One server per user. A kernel-held lock in the state directory allows a single server. |
Network |
Binds the loopback address |
Authentication |
A random token, made at each start, signs every request and every response with an HMAC over method, path, nonce, timestamp and body. The token never travels. A client never trusts a listener that cannot sign. |
Unsigned requests |
A request without well-formed signing headers is refused before its body is read. |
Token storage, Linux and macOS |
A private state directory, mode 0700. The server refuses one that another user owns or can reach, or that is a symlink. |
Token storage, Windows |
|
Files |
Each job runs in a private scratch directory. Output files travel back inside the signed response, so client and server need no shared filesystem. |
POWDERLINE_NO_SERVER=1 turns the server off. Operating the server is
covered in Run at a beamline.
Warning
The server caches the code it loaded. After editing anything it runs, stop
it with pixi run -e gsasii gsas-server stop; the next auto or server run
starts a fresh one in the background. gsas-server restart and start run
the server in the foreground of that terminal until you stop it. The client
compares the server’s PowderLine version and source-tree hash with its own. A
stale server is skipped in auto mode and refused in server mode.
A server timeout never falls back to an in-process run. The job is still running on the server, so a second run would double the work.
The validation model#
flowchart TD
R["recipe"] --> V{"each validator"}
V -- "breaks a rule, or the engine<br/>would silently change it" --> E["error naming<br/>the value to write"]
V -- "within tolerance of an<br/>exact symmetry value" --> H["held exactly<br/>info: symmetry_value_derived"]
V -- "left out, documented default" --> W["default applied<br/>+ structured warning"]
V -- "as written" --> OK["validated model"]
H --> OK
W --> OK
E --> X["one ValidationError<br/>listing every problem"]
Every recipe check is a pydantic validator. An invalid recipe raises one
pydantic.ValidationErrorlisting every problem at its field path. There is no PowderLine wrapper exception for invalid recipes.Validation never changes what a recipe means. The validated model holds the recipe’s values, the documented defaults and the exact symmetry-derived values. A value an engine would silently replace is an error, never an adjustment.
Structured warnings (
{code, message, field_path[, level]}) are collected once from the validated model. Re-validating the same recipe reproduces them. Every code is registered inexceptions.WARNING_CODESand listed in Messages.An engine schema builds on core but cannot redefine, skip or rewrite core’s rules. This is guarded when the class is defined, and
tests/test_phase_canary.pyruns core’s must-fail recipes through every engine’s phase model.A schema rule that exists because of an engine quirk cites its public
EB-NNID in the error message. The entries are on the Engines pages.
Why validation is this strict is explained in Validation.
The frozen GSAS-II modules#
Six modules in src/powderline/gateways/gsasii/ hold the hand-written
functions that set up and run a GSAS-II refinement: helpers.py,
project.py, setters.py, executors.py, extractors.py and
constraints.py. They manipulate GSAS-II’s project dictionary directly.
These conventions can’t be fully pinned down by tests, so the modules are
frozen.
A behavior-changing edit needs the maintainers’ explicit consent.
Comment and docstring edits need no consent.
src/powderline/_code_hash.jsonholds a hash of each module’s raw bytes, andtests/test_code_hash.pyfails when a module changes. After any edit, runpixi run update-code-hashand commit the hash file with the change.
The full rule is on Policies. New behavior goes into the modules around the frozen code:
Module |
What it adds |
|---|---|
|
Compares GSAS-II’s site symmetry and multiplicity for every atom with core’s. Stops with an error when GSAS-II misreads a site, or cannot name one while a coordinate or |
|
Computes the uniform statistics over the fit window. Reads GSAS-II’s stored messages into warnings, its convergence flag and its count of varied parameters. Raises on a non-finite calculated pattern |
|
One entry point for in-process, server and client runs |
|
Maps GSAS-II parameter names to recipe paths for the output recipe |
|
Keeps the tail of GSAS-II’s console text for a failed run’s envelope |
|
GSAS-II’s ion table and spelling |
|
The version check, the execution modes and the result tables |
The documented gsasii defaults (sample broadening, background, cycles) are
schema defaults. The frozen setters always receive explicit values, so
GSAS-II’s own defaults are never used. GSAS-II sets refine flags in lumps (one
for the cell, one for all three coordinates of an atom). constraints.py
sets the lumped flag and adds GSAS-II “Hold” constraints for the parts the
recipe does not refine. The schema has already refused any flag pattern
GSAS-II could not honor.
Output generation#
Every engine writes the same fixed file names through powderline.reports:
refined_parameters.csv, <phase>_unit_cell_report.csv,
<phase>_peak_list_report.csv and fit_profile.txt. A single-peak fit adds
a peak report. A refinement adds output_recipe.json and
parameter_dispositions.csv. The writers use one column order, UTF-8 and LF
line endings, so the files are byte-stable across platforms.
The engines’ own files stay beside them. gsasii keeps dummy.gpx and
dummy.lst. topas keeps the INP, the .xye and every file TOPAS wrote.
powderline.run() removes only the names in reports.FIXED_OUTPUT_NAMES and
reports.FIXED_OUTPUT_PATTERNS, never an engine’s files. The full list is on
Outputs.
The output recipe is built in core by roundtrip. A gateway hands it the
refined values by recipe path, in recipe units, with their ESDs. It also
names the flagged paths the engine did not vary and those that ended at a
limit. Core does the rest: tie propagation, the multiplicity check,
output_recipe, output_recipe_valid and parameter_dispositions.
Domain essentials#
Rietveld refinement fits a calculated powder pattern to the observed one
by adjusting structural, profile and background parameters. PowderLine
computes the headline statistics once, in fitstats, the same way for every
engine:
The sums run over the points inside the fit window with \(w_i > 0\). \(N\) is
the number of those points. \(P\) is the engine’s own count of varied
parameters, read after the run. Reduced χ² is \(\mathrm{GoF}^2\). The engine’s
own values go under engine_details.
Tip
A lower Rwp means a better fit of that model to those points. Across engines, compare lattice parameters rather than Rwp: the models differ.
Parameter types. Every refinable is a [value, flag] or
[value, flag, min, max] list.
Structural: cell, coordinates, occupancies, ADPs. This is core’s phase block, identical in every engine.
Intensity: the phase
scale.Peak shape: sample size and strain (gsasii, topas) and the instrument profile (every engine).
Background: Chebyshev terms, and background peaks in gsasii and topas.
Instrument: wavelength, zero shift, polarization, asymmetry, in the engine’s own names and units.
Phase and histogram parameters. Phase parameters belong to a crystal structure: cell, atoms, scale and sample broadening. Histogram parameters belong to the measurement: instrument profile and background. In a multi-phase refinement the histogram parameters are shared.
Terms used here (recipe, gateway, tie group, special position) are defined in the Glossary. Contributor terms: a golden is a committed engine output a test compares against; the schema layer and runtime layer are the engine-free and engine-importing halves of a gateway.