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 |
|---|---|
|
|
|
every other type puts something that is not 2θ on the x axis |
a |
PowderLine itself raises |
a non-zero |
see below |
a non-zero |
this package’s Lorentzian width is X/cosθ + Y·tanθ, with no constant term |
a negative |
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 |
|
an |
anisotropic displacement through a recipe is not read yet |
a top-level |
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 |
|---|---|---|
|
centideg² Gaussian variance |
|
|
centideg Lorentzian FWHM |
|
|
µm |
|
|
10⁻⁶ Δd/d |
|
|
(S+H)/L |
|
|
Ų |
|
|
1/σ² |
|
|
°2θ, inclusive both ends |
|
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.