The PowderLine recipe

PowderLine is a file-less JSON recipe for a powder refinement. One document carries the pattern, the instrument, the phases, the background and a refine flag per parameter, and is dispatched to one of several engines behind a single result shape. read_recipe turns such a document into the objects this package refines with, and write_recipe_tables writes a finished refinement back out as the four tables its consumers read.

Use it when a pipeline already speaks that format and you want this package to be one of the engines it can send work to. A recipe is an interchange format. For everything else, build the objects directly (Patterns, structures and instruments) or open a project (Files and projects).

Running one

import rietx as rx

recipe = rx.read_recipe("input.json")
ref = rx.Refinement(recipe.structure, recipe.instrument)
result = ref.fit(recipe.pattern, plan=recipe.plan,
                 two_theta_limits=recipe.limits)
rx.write_recipe_tables(ref, "output/",
                       phase_names=dict(zip(
                           [p.name for p in ref.fitted_structure.phases],
                           recipe.phase_names)))

Pass the path wherever one exists. The format is file-less by design, so a recipe carries its whole pattern inline and a 4096-channel one is 0.4 MB of JSON. That is upstream’s contract, and it is a payload that should cross a filesystem rather than a prompt or a message body.

Recipe carries seven fields. Recipe.structure, Recipe.instrument and Recipe.pattern are the ordinary objects of Patterns, structures and instruments. Recipe.plan and Recipe.limits are the recipe’s own refinement intent, so pass both rather than picking a preset. The recipe states which parameters refine, and Recipe.limits is its fit_range. Recipe.schema_name and Recipe.schema_version are what the document declared, and Recipe.phase_names is the recipe’s own key per phase, in order, which the output file names use.

Recipe.diagnostics is the eighth field. Read it before the answer: every field dropped, every convention assumed and every value re-seeded goes down it, and a caller that ignores it is not being told what was changed.

for d in recipe.diagnostics:
    print(d.level, d.code, d.message)

read_recipe also takes diagnostics=, the same opt-in list read_pattern takes, for a caller accumulating across several reads.

Refusals

A recipe this package cannot represent raises RecipeError, a ValueError, naming the field. That is upstream’s own rule for an engine: reject loudly, and never silently ignore a refine flag.

Refused

Because

schema_name other than GSASII_Rietveld

GSASII_SPF is single-peak fitting, which is fit_peaks here and not a recipe: it refines nothing

Type other than PXC

every other type puts something that is not 2θ on the x axis

a size_broadening or strain_broadening model other than isotropic

PowderLine itself raises NotImplementedError for these

a non-zero Zero

see below

a non-zero Z

this package’s Lorentzian width is X/cosθ + Y·tanθ, with no constant term

a negative W, X or Y

those are softplus-bounded at zero here; reading one would silently give ≈0 rather than the declared value

a hump whose Lorentzian γ exceeds one 2θ step

HumpComponent is a Gaussian, deliberately

an Uaniso atom

anisotropic displacement through a recipe is not read yet

a top-level single_peaks block with any live entry

free-standing peaks are not part of a refinement here; fit_peaks fits them on their own

Zero is refused because PowderLine states its unit twice and the two statements disagree. Its easydiffraction engine converts Zero as centidegrees; its config loader annotates it “degrees 2theta”. The readings differ by 100×, which is a wrong cell rather than a slightly wrong one, and no committed recipe carries a non-zero Zero to settle it. A non-zero one is therefore refused, under the same rule that governs a CIF whose cell angle contradicts its space group (Patterns, structures and instruments): where two statements contradict each other, choosing is the caller’s.

A fixed value the model reaches its identity at is dropped instead, with a diagnostic naming it. Z = 0 is no constant Lorentzian, and a hump’s γ below one channel is a width the data cannot hold. A fixed identity is a report and a live value a contradiction, which is the reader-repair rule of Patterns, structures and instruments applied one format over.

Unit conventions

Every unit conversion was measured against the reference output PowderLine commits beside its own recipes, then corroborated against upstream’s second engine. The full table with the measurements is tests/data/README.md. The rows that change an answer:

Recipe field

Unit

Becomes

U, V, W

centideg² Gaussian variance

instrument.profile.u/v/w × 8 ln2 × 10⁻⁴

X, Y

centideg Lorentzian FWHM

instrument.profile.x/y × 10⁻²

isotropic_size

µm

phases.*.lor_size and phases.*.gauss_size, split by LG_eta

isotropic_strain

10⁻⁶ Δd/d

phases.*.lor_strain and phases.*.gauss_strain, likewise

SH/L

(S+H)/L

axial_sl = axial_hl = SH/L ÷ 2

Uiso

Ų

biso = 8π²·Uiso

Itth_weights

1/σ²

PatternData.sigma = 1/√w

fit_range

°2θ, inclusive both ends

Recipe.limits

GSAS-II carries one magnitude per broadening effect plus a Lorentzian share LG_eta; this package carries a Lorentzian and a Gaussian parameter per effect and no share. So the translation is a split rather than a rename: η of the magnitude becomes the Lorentzian coefficient and (1 − η) the Gaussian one. At η = 1 the Gaussian half is exactly zero, where a softplus parameter’s gradient is its own value and the optimiser cannot move it. That half is held rather than declared free, with a warning saying the fit runs one parameter short of the reference engine’s.

Two numbers are deliberately not carried across. The Chebyshev coefficients are re-seeded from zero, because the two codes scale the polynomial’s domain differently and the numbers are not the same numbers; only the term count and the refine flag survive. The phase scale is re-seeded to match the summed calculated intensity to the data, because a scale factor’s normalisation is each code’s own. On the committed fixtures GSAS-II and TOPAS converge four orders of magnitude apart for the same specimen.

The refinement plan

PowderLine runs one pass over everything flagged. Translating that as a single stage is faithful and it does not work from a cold start: on the two-phase fixture it walked a monoclinic cell to a = 4231 Å.

So Recipe.plan frees the recipe’s parameters over several stages in the McCusker turn-on order of Running a refinement. Staging here is cumulative, every stage keeping what the earlier ones freed, so the last stage is exactly the recipe’s single pass over everything flagged, reached by a route that survives a cold start. The set of free parameters at the end is the recipe’s, and only the order is this engine’s.

The four tables

write_recipe_tables takes the Refinement. A result carries the fitted curve but not the compiled model, and the peak list needs the per-reflection widths and |F|². It writes

  • refined_parameters.csv: one row per refined parameter, with its esd;

  • <phase>_unit_cell_report.csv: one per phase;

  • <phase>_peak_list_report.csv: one per phase;

  • fit_profile.txt: the fitted channels, tab separated.

Three of those four headers are a real contract and one is not. GSAS-II and TOPAS write byte-identical headers for the first two and the profile, and different ones for the peak list: 15 columns against 11, sharing nine. So the first three are reproduced byte for byte and the peak list carries the columns this package can honestly fill. F_obs_squared is not among them. An observed |F|² from a Rietveld fit is I(calc) times the reflection’s own obs/calc ratio, so it flatters whatever model partitioned it (Reading the numbers), and an absent column beats a flattering one.

An esd column is empty where nothing computed the number and 0 where the parameter is held. The two are different statements and this package does not collapse them. cell_volume is the one empty esd today: propagating it needs the cell block’s covariance through dV/d(a,b,c,α,β,γ), which nothing here does yet.

For agents

This is a pipeline surface. There is one integration surface for an agent and it is the python API (Calling rietx from a program). A recipe earns its place only because running one specimen through several engines and comparing is a job no python API can do for a code it cannot import. If you are choosing, choose Refinement.fit.

Read Recipe.diagnostics before the answer. Roughly a dozen codes can fire, all prefixed RECIPE_, and several of them mean the fit you are about to run differs from the reference engine’s in a stated way: a dropped refine flag, a declined engine default, a degenerate hump. A RecipeError names the field and says what to do instead.

The cross-engine fixtures

Two complete cross-engine fixtures are committed under tests/data/powderline/. Each is one pattern refined by two independent engines with both outputs in the repository. That is a cross-code consistency check with a second opinion attached, and the second opinion disagrees.

On the two-phase cathode the two references differ by 2665 ppm on the cubic cell, because the recipe co-refines size and strain on a pattern where GSAS-II itself reports two SVD singularities and a 100 % correlation. It returns a negative crystallite size for one phase, and TOPAS returns none at all. This package lands 11–93 ppm from TOPAS on all five free cell parameters, at Rwp 7.333 % against TOPAS’s 7.326 %, while sitting the same 1004–2575 ppm from GSAS-II that TOPAS does.

Read that as the calibration for any cross-code claim made through this format. The cells are the comparable quantity across engines and the broadening coefficients are not. tests/data/README.md has the numbers and tests/test_acceptance_powderline.py asserts them.