The agent skill¶
The operating protocol ships with the package as an Agent Skill, a directory
holding a SKILL.md an agent reads whole when the task calls for it and
reference files it loads only when it needs one. The protocol is what to free in
what order, what to check before believing a number, and how to read an
abstention.
The format is an open standard rather than one vendor’s feature, so the same directory is read natively by Claude Code, Codex, Cursor, Copilot, Gemini CLI, opencode, Goose, Cline, Amp, Kiro, Devin, Zed, Junie, Qwen Code and OpenHands, among others. Nothing in it is specific to one of them.
This chapter is the whole skill, rendered from the files the package ships, so it cannot drift from what an agent actually reads.
For agents
Do not read this chapter. Read the skill itself: rietx skill --path prints the
directory, capabilities().skill_path is the same answer from Python, and your
harness has probably loaded it already. This page exists so a person can see
what the machine was told.
Getting it¶
rietx skill --path # where this build keeps it
rietx skill --print # the body, as text
rietx skill --print diagnostics # one reference file
rietx skill --install # into this repository, for every harness
rietx skill --install [DIR] writes one real copy to
DIR/.agents/skills/rietx/, the directory the specification recommends and most
harnesses scan. Each harness that reads somewhere else is pointed at it with a
relative symlink, so the link survives the project being moved or cloned
elsewhere. --agent NAME (repeatable) chooses which, and --list-agents prints
the harness table with the source and date each row’s directories were read
from. --user installs for the user rather than for a project. --copy copies
instead of linking, and Windows gets that in any case.
The command prints, and never writes, the two lines that name the skill in a
project’s AGENTS.md or CLAUDE.md. Those files are the project’s own
instructions to its agents, and appending to one uninvited would be editing
something the package does not own.
For a harness that reads no skills at all, rietx skill --print all is the
whole tree as plain text, for a --read flag or a system prompt.
The protocol¶
Audience: an agent driving rietx on real data. Not a tutorial and not an
API reference. A protocol: what to do, in what order, what to check before
believing a number, and where the package will tell you your answer is wrong
even though it looks right.
Rwp is not the objective function of your job. A refinement can converge, report an excellent Rwp, and return displacement parameters biased by 100 %, phase fractions wrong by 5 wt % and a cell that is right for the wrong reason. Every rule below exists because one of those happened and was measured. Sections 1-4 are ordinary Rietveld discipline that would apply in any code; sections 5-10 are specific to running it with no human at the plot.
Load these when the task calls for them¶
This file is the judgement core: what holds for every fit. The lookup tables, and
the rules one task shape needs, live beside it, loaded on demand, one file
each. The user manual holds the object model this protocol drives and is not
restated here; a page named x below is https://rietx.org/using/x.html.
When |
Load |
Manual page |
|---|---|---|
you are about to call rietx: entry points, constructors, the four answer types and their fields, the report |
|
|
you were handed another program’s input file, not a pattern |
|
|
§7/§7g/§7h — a |
|
|
§6 — something declined to answer: abstentions, caveats, gate failures, |
|
|
§5 — you are about to quote a number: which field carries which fact, and read numbers rather than pixels |
|
|
§4/§4b — a judging or deliverable rule needs its measurement, before you override one |
|
|
§8 — the fit did something that makes no sense: twenty-one measured results that contradict an intuition |
|
|
§7b-7f — the phase is unknown, or you want the peaks themselves and no cell: peak picking, fitting peaks you name, indexing, the closed loop, the extinction screen |
|
|
§9 — one fit is not the answer: the trajectory, and the history DAG as a search structure |
|
|
§9b — an in-situ ramp, a sweep or a tray: chaining N patterns, and checking the chain both ways |
|
|
§9c, deciding: ranking, differencing, auditing, identifiability |
|
|
§9c, operating: budget, cost, timing, the log, inventory, fault tolerance |
|
|
§9d — a human may be watching this fit, or you want to hand one a window onto a long run; also reading a finished run off disk |
|
|
writing the answer out: CIF, QPA table, reflection table, plots |
|
|
1. Before you refine: what the method can and cannot do¶
Rietveld refinement fits a structural model you already believe to a whole powder pattern. It is a local, gradient-based optimisation of a strongly non-convex, strongly correlated problem. It is not structure solution, not phase identification, and not a search.
Preconditions, all of which must hold before fit() is meaningful:
Requirement |
How to satisfy it |
If you cannot |
|---|---|---|
Every crystalline phase present is in the model |
|
An unmodelled phase’s peaks land in the residual; Layer 0’s |
The starting cell is within ~1 % |
from the CIF, or from |
The peaks are outside their frozen evaluation windows and the refinement cannot walk there; Layer 2 says so with |
The wavelength is right |
from the beamline |
Every cell you report is wrong by the same scale factor and nothing in the fit will tell you. Do not hand-enter a wavelength from a textbook to “match” one of these: the table is one scale end to end (§8.11) and mixing scales is a ~100 ppm cell error |
The geometry is right |
|
The aberration model is wrong; displacement/transparency/roughness/absorption are geometry-gated and silently absent |
The intensities are un-manipulated counts, with esds if available |
|
Weights are wrong ⇒ every esd and every χ² is wrong |
The starting peak width is within a factor of ~2 |
measure it: median FWHM of the dozen most prominent peaks, then |
|
Never subtract a background before refining. Subtraction invalidates the
counting-statistics weights and can make intensities negative. Hold an estimated
background additively (BackgroundFixedPlusChebyshev) or co-refine it under a
smoothness penalty (BackgroundPSpline). rx.auto_background(data) does the
right thing.
2. The turn-on order, and why it is not negotiable¶
Free parameters in groups, cumulatively, in a stable order (McCusker, Von Dreele, Cox, Louër & Scardi, 1999, J. Appl. Cryst. 32, 36), each group running to convergence before the next is freed. The reason is not tradition: the correlations between groups are severe, and a simultaneous release from a poor starting point walks into a local minimum that a staged release avoids. Toby (2024, J. Appl. Cryst. 57, 175, the “recipe problem”): once parameters have refined to unphysical values, adding more parameters no longer lets the fit recover.
The plans encode this. Use them; do not hand-roll a free set unless you have a
reason you can state. The staged discipline is what is not negotiable; the
preset sequence is a default, because the right next group depends on the data
and the current values — and ref.suggest(data) answers that at the current
state, one analytic-Jacobian evaluation ranking every held parameter by predicted
Δχ², with no fit and no mutation.
plan="mccusker_default" # scale+bkg → zero → cell → W → U,V,X,Y (profile only)
plan="mccusker_structural" # …then coordinates → displacement → PO → extinction → roughness
plan="lab_bragg_brentano" # …with sample displacement, Kα2 ratio, FCJ axial
plan="lab_calibrate" # instrument calibration on a standard, certified cell HELD
plan="lab_sample_refine" # sample against a frozen calibrated instrument
plan="profile_only" # Le Bail
plan="pawley_default" # Pawley
rx.PLAN_INFO carries a title, description, modes and when-to-use for each.
Three ordering rules. None is in the guidelines; each is this package’s own measured finding.
Widths last among the profile terms,
WbeforeU,V,X,Y.Wis the constant term; freeing the tanθ and 1/cosθ terms first lets them absorb a constant offset and then fight it.Intensity-scaling corrections go last, after the structure has settled. Preferred orientation, extinction and surface roughness rescale intensities in a Q-dependent way, and so do the scale, the occupancies and the displacement parameters. Freeing a correction early lets it eat structure that belongs to the structure.
Anisotropic strain is freed inside the sample-broadening stage, not after. A Stephens block locks
lor_strain— its isotropic direction is that column — so deferring it leaves the isotropic width unrefined right up to the moment fifteen correlated coefficients turn on at once.
Structure-free first when you can. Le Bail (mode="lebail") extracts
intensities from the data instead of computing them, so it converges the cell,
zero and profile with no structural assumption. Do that first, then switch to
Rietveld with the converged cell and profile: it is the single most reliable way
to avoid a structural minimum that is really a profile error. Its cell is
the weaker half of that: where reflections are dense, arbitrary intensities can
index one pattern more than one way, so check a Le Bail cell against even a
rough structural model before quoting it (Peterson 2005). The variable is
reflections per FWHM, so on a resolved pattern skip the check. Two rules about it
the API does not tell you, both measured on third-party lab data.
Iterate the whole plan to a fixed point; one
fit()is not enough, and keep the best pass, not the last. The extracted per-hkl intensities are frozen inside each least-squares run, so intensities and profile converge only by alternating, and the alternation is not a descent on one objective, so a later pass can come back worse.Seed the background before the first pass, always.
auto_backgroundstarts every coefficient at 0.0, so the firstlebail_updateruns before the background has ever been fitted, is handed the whole pedestal, and gives it to the Bragg reflections. Seed the constant term from a low percentile ofy_obs.
Both measurements, and multi-phase Le Bail’s one surviving caveat:
references/judging.md.
3. The degeneracies. Memorise these¶
Almost every wrong-but-good-looking Rietveld result is one of these. They are not bugs; they are the geometry of the problem.
Degenerate group |
Their angular signatures |
Consequence of getting it wrong |
|---|---|---|
zero shift · sample displacement · cell |
const · cosθ · tanθ |
Over a narrow 2θ range these are collinear. A cell “refined” against a free zero on 20° of data is not measured. Bragg-Brentano only — the two flat-plate aberrations are held fixed on any other geometry. |
zero shift · the two capillary offsets · cell |
const · sin2θ · cos2θ · tanθ |
The same trap in Debye-Scherrer’s own shapes (McCusker eq 4, §8.18). Separable over 5–160°, not over 5–25°: the unit-column Gram’s smallest eigenvalue is 5.2e-2 against 1.1e-5, a factor of ~4600. |
crystallite size · microstrain |
1/cosθ · tanθ |
Williamson-Hall separability. Over a short range they are one parameter, not two. |
phase scale · Biso/ADPs · background · absorption · surface roughness · extinction |
all smooth in Q |
This is the big one. Every member depresses or lifts intensity as a smooth function of angle. Any of them can absorb any other. |
capillary µR · phase scale · Biso |
exp(c·sin²θ) — exactly |
Not “correlated”: singular. µR is computed from the specimen and never refined, and the fit is identical with and without it (§8.1). |
flat-plate µt · phase scale · Biso |
mostly, but not exactly |
60–99 % absorbable, so it is also computed rather than refined — but the remainder does move Rwp, and a wrong thickness lands partly in the fit and partly in the ADPs (§8.12). |
preferred orientation · site occupancy |
both rescale specific hkl |
An occupancy refined against uncorrected texture is a texture measurement. |
overlapped reflection intensities (Pawley/Le Bail) |
identical |
The sum is determined; the split is not. |
a declared extra peak · the reflection under it |
identical where they overlap |
An extra peak on a reflection is a scale/intensity degeneracy by construction: two terms, one peak, and Rwp cannot say which owns the counts. |
Do not free the second member of a group without checking the first is pinned by something outside the fit.
lab_calibrateexists for this: refining a certified standard with its cell held fixed is what decorrelates zero from displacement from cell, because the cell is supplied rather than fitted.A correlation of 0.98+ means you refined one parameter and reported two.
HIGH_CORRELATIONfires for you. The right response is almost never “widen the bounds”; it is to fix one, or to extend the data range until the signatures separate.Where chemistry says two quantities are one quantity, constrain them rather than refining both.
ref.tie_equal([paths])makes an equality group,ref.tie(path, source, scale=, offset=)the general affine form (occ₁ = 1 − occ₀on a mixed site isscale=-1, offset=1),ref.untiereleases them.A constraint removes a parameter, unlike a restraint, which adds a weighted observation and leaves the count alone, so it is the one move that raises the observation-to-parameter ratio. The two cases worth reaching for are McCusker’s: equal displacement parameters across atoms in the same environment, and occupancies summing to a known total. Measured on fluorapatite’s three phosphate oxygens, tying them gives a B(O) tighter than the best free value.
Check the premise before you tie, and not with Rwp — it moved by 0.05 % of itself there. The check is in the free refinement: if each free value lies within its own esd of the others, the data does not contradict the claim that they are one parameter. Where they disagree by more than their esds, the atoms are saying they are not in the same environment, and tying them replaces a measurement with an assumption. Symmetry always outranks a user tie, and a refused tie says so by name.
4. Judging a fit — and what Rwp is actually for¶
Rwp compares your model to the data you have, weighted by counting statistics. It is dominated by the strongest peaks and by the background level: a useful relative number between two fits of the same data over the same channels, and a nearly useless absolute one. Measured, 18 refinements of one identical PbSO₄ dataset returned Rwp 8.2–20.0 % (Hill, 1992, J. Appl. Cryst. 25, 589), and Toby (2006, Powder Diffr. 21, 67) finds “no simple way to distinguish a good fit from one that is just plain wrong based on R factors”.
Judge a fit in this order. print(result) renders steps 9 and 17 (per-stage
status, every diagnostic, provenance, agreement indices last), and
ref.summary(deliverable=…) adds the rows that need the compiled model. The
measured evidence behind each rule is
references/judging.md.
Status and guards outrank every statistic.
result.status, thenresult.diagnostics.statistics.max_shift_over_esdis the measured quantity behind “converged” (McCusker §7 converges at ≤ 0.1). A converged solve satisfies it a fortiori, so read it where a stage stopped onSTAGE_MAX_ITER: its magnitude says how far the solve was still moving, in esd units.Read the shape of the difference curve region by region, not its size.
report.regionscarries per-region local Rwp and χ² share, andcumulative_chi2_breakpointslocates where the model starts failing.Read the unmatched peaks.
report.unmatchedwithkind="unmatched_obs"is an impurity or a missing phase;"unmatched_calc"is a phase you modelled that is not there, or an absence error. Readunmatched_calcin Rietveld mode only. Le Bail and Pawley extraction takes away most of the residual it looks for, so there it fires on noise near a tick.Ask whether the refined values are physically possible — negative Biso, occupancies above 1, a cell that moved 0.5 %, an ADP tensor that is not an ellipsoid — and ask it of the structure too.
result.geometryis aGeometryTableofbonds,contactsandangles, which McCusker §11 ranks with the profile fit and above every R value, so read it before step 16. Nothing scores it: a Si–O at 1.75 Å or a 60° O–M–O is yours to recognise. The number of rows naming an atom is its coordination number, and aNoneesd means no covariance behind the row or fixed by symmetry, never zero.Quote no esd without its inflation.
statistics.esd_inflationis the Bérar-Lelann factor for serial correlation, and it has an expected value of ≈1.51 even for perfectly white residuals, so it is an upper bound on the damage rather than a measurement of it.report.identifiabilitycarries the trio to pass on with any esd — raw χ²_red, the inflation, Durbin-Watson — plus the δR line. Scaling variances by GoF² alone is “highly questionable” (Schwarzenbach, 1989).Ask whether the converged answer is the only one, and settle it by a swap.
report.identifiability.exchangesand.soft_modesoutrank the statistics, and the verdict that licenses isambiguous, notconverged.They are about what “converged” means:
convergedis a statement about the free set, while anexchangeable=Truerow says a held parameter’s signature is reproducible inside the fitted span and that a fitted partner stands many σ from its null.The swap resolves it and is a measurement: fit each member of the pair alone, the other held at its null, and compare χ² — two warm fits, seconds, and
rx.report.compare_rivals(ref, data, finding)runs exactly that. R² cannot stand in for it. Read the outcome onRIVAL_DECISIVE_MIN_CHI2_RATIO(= 1.10,rietx.report), the losing rival’s χ² over the winner’s: at or above it the data has chosen, and you quote the winner without caveat, since hedging a won swap is a measured failure rather than caution; below it the pair is genuinely unresolved and the resolution is protocol or a declared ambiguity. No sentence converts a tie into an answer. The licence also travels asresult.statistics.identifiability_clause.What you must not do is free the held parameter alongside its partner and refit: both free lands on §3’s degenerate ridge and reports the unconstrained combination at a better Rwp — the most common misreading of the clause.
Read what the background is doing before you read Rwp, because it decides how to read Rwp. In
report.background,worst_absorption(withworst_absorption_path) is how much of a structural parameter the background column span can reproduce, andoff_region_chi2_reducedwithoff_region_durbin_watsonis whether the residual between the peak regions is systematic. Layer 0’s regions are peak clusters, so that second failure lands in noreport.regionsentry and step 10 cannot see it.Only then Rwp and GoF, and never alone — as a pair with
background.rwp_background_subtracted. The raw number is flattered by whatever the background carries. The subtracted one is what separates two fits of the same data.Read the structure R factors last, and never in isolation.
result.phase_agreementcarriesr_bragg(R_B) andr_f(R_F) per phase. A powder pattern does not measure individual reflection intensities, so I(obs) is the pattern partitioned in proportion to I(calc): a wrong model receives the intensity it predicted and both flatter it (Toby 2006: R_Bragg “has no statistical validity”). Watch R_B fall as you improve a model; never read it as evidence a correction helped. Absent in Le Bail and Pawley, where the intensities are the fit. Do not compare a trace phase’s R_B with the major phase’s: neither is weighted, and a minor phase’s windows sit under the major phase’s peaks.
Adding parameters: use ΔBIC, not Hamilton’s R-ratio. Measured at 7251 channels, Hamilton’s test blesses a 0.13 % χ² improvement that is physically inert; ΔBIC carries the sample-size penalty powder channel counts need.
Comparing against another code means adopting its protocol, not just reading
its numbers. Mirror its refined-parameter set, its held parameters and its
excluded regions, then check the channel count matches before believing any Rwp
comparison. Measured: guessing a plausible protocol on the GSAS-II fluorapatite
tutorial gave Rwp 16 % and a +390 ppm cell, while mirroring the converged .EXP
gave 9.73 % against GSAS’s 10.05 % on an identical 5750 channels.
4b. Declare the deliverable — “good enough” is a question about purpose¶
Much real work is non-ideal by construction — nanoparticle broadening, intensity
error from unknown pore contents. No bar moves for such data: the gates
auto-scale to information content, and “good enough” is a different question
answered exactly, not a relaxed standard. What changes is which report rows
decide your deliverable. Declare it, then read its rows: the report is
purpose-neutral and will not infer yours.
ref.summary(deliverable=…) prints them for one fit, and a chain’s are on
SeriesResult.summary(deliverable="series").
Deliverable |
The rows that decide it |
Stop when |
|---|---|---|
Phase ID — which phases are present? |
|
no strong unmatched observed peaks and the gap readable, whatever Rwp says. An |
QPA — how much of each? |
|
fractions stable under a background-flexibility change, |
Trajectory — a parameter against T, t, p or composition |
|
every number you quote names the one thing that would have to be wrong for it to be wrong, and that thing has been checked |
Microstructure — how big are the domains, how strained? |
|
|
Structure — where are the atoms? |
Above, plus |
§10’s full ladder, with no |
The QPA row outranks every statistic beside it: an over-flexible background
wins on every agreement index while biasing displacement parameters to 0.958
and 0.000 Ų against a truth of 0.5, and worst_absorption (0.46 against 0.08)
is the only row separating the two fits — the plot does not either.
resolution_limited is a stopping point, not a failure: the edit directions
are indistinguishable on merged peaks, not the model wrong — a legitimate end
state for phase-ID work; for structure-grade work, collect better data.
The capability floor. Verify before acting (rx.report.predict_then_verify,
or a history branch), treat a capped confidence as an unresolved question
rather than a low-priority instruction, and never execute a vetoed action. There
is no ceiling: the report supplies evidence, judgement stays with the reader.
Every deliverable’s worked measurement, and the round robins’ two QPA rules:
references/judging.md.
6. Abstention is a result. Do not convert it into a number¶
The package’s hardest rule is never return a confident wrong singleton. Several places will decline to answer. When they do, that is the answer.
Propagate an abstention; do not paper over it.
report.abstained_reasonset means the global maturity gate refused Layer 1 — branch onabstained_kindfirst, and do not readattribution.INDEX_ABSTAINEDmeans the candidates are there so you can see what was considered, not so you can pick one.Never take
candidates[0]because it is ranked first. The ranking orders the hypotheses, the gate judges them, and the two are different questions.IndexingResult.best_or_none()returningNoneis the most likely outcome of a first indexing run and is not a failure: read each candidate’sconfidence_caveatsand act on the refuting ones first.A failed gate is not a cause.
region.gates_passed is Falsemeans the coefficients are present for transparency only; readregion.gate_failures, whose codes are closed and typed.Two collinear templates are not one answer. A trend reported non-separable, a
PAWLEY_OVERLAP_UNRESOLVEDgroup, anINDEX_GEOMETRIC_AMBIGUITY, anEXTINCTION_GROUPS_NOT_SEPARABLE: the information is absent from the measurement, not buried in noise. Extend the range, report both, or carry the whole list forward — a group’s sum is the datum, and no counting time separates space groups differing only by elements that produce no absences.A held or unquotable value is not a measurement.
PHASE_UNCONSTRAINED,STEPHENS_STRAIN_NOT_POSITIVE,BOUND_HITandHARMONIC_HELDeach say a number in the result did not come from the data. Their esds do not make them measurements, and a good Rwp does not cover them: a parameter that does not move y_calc does not move Rwp either.
Every signal, its meaning and its correct response:
references/abstention.md.
10. A worked default¶
A lab pattern, a CIF and no other information. Adapt, do not skip the checks — the right order depends on the data and the starting values (Toby, 2024).
import rietx as rx
data = rx.read_pattern("sample.xy")
structure = rx.Structure.from_cif("phase.cif")
instrument = rx.Instrument.bragg_brentano(radiation="CuKa",
monochromator_two_theta=26.6)
instrument.background = rx.auto_background(data)
ref = rx.Refinement(structure, instrument, history="session.jsonl")
# 1. structure-free first: cell + profile without any structural assumption
ref.fit(data, mode="lebail", plan="profile_only")
# 2. Rietveld from the converged cell/profile
result = ref.fit(data, plan="lab_bragg_brentano")
# 3. the termination view: per-stage status, diagnostics, agreement indices last
print(result)
# 4. guards outrank statistics, and each one carries what to do about it
for d in result.diagnostics:
print(d.level, d.code, d.where, d.message, "->", d.suggestion)
# 5. numbers, not pixels
report = ref.report(plan="lab_bragg_brentano")
if report.abstained_reason:
print("Layer 1 abstained:", report.abstained_reason, report.abstained_kind)
else:
for r in report.attribution:
if r.gates_passed:
print(r.two_theta_lo, r.two_theta_hi,
[(c.kind, c.value, c.stderr, c.share) for c in r.coefficients])
# 6. impurities / missing phases
print([u for u in report.unmatched if u.kind == "unmatched_obs"])
# 7. the whole judgement in one call, for a declared purpose
print(ref.summary(deliverable="structure"))
The three stop conditions. Stop refining when
every diagnostic is understood and either resolved or reported as a caveat;
Layer 1 attributes no remaining region above the significance gate; and
adding the next parameter group fails a ΔBIC test or trips a guard.
Do not stop merely because Rwp stopped falling, and do not continue merely because it is still falling. These are the structure-grade conditions; §4b maps the earlier stopping points a declared phase-ID or QPA deliverable is entitled to.
What to report. The refined values with their (inflated) esds, the
diagnostics you could not resolve named as systematics, the protocol you actually
ran (plan, held parameters, excluded ranges, channel count), and the package
version, backend and solver from result.provenance. A number without its
protocol is not a measurement.
The API¶
There is one integration surface and it is the Python API. A caller runs a
verb, reads the typed answer, and dumps it with model_dump(mode="json") when a
file is wanted. A failure raises: there is no envelope and no error code.
A RefinementCancelled you did not request is not a bug in your call. Every
fit records itself, and a human watching one can stop it from rietx watch. The
stages that finished are kept. .completed_stages and .node_id say where the
work stands, and that node id is a checkout target. Report where you got to. Do
not re-run the fit as though it had failed.
Do not quote a signature from memory. rx.capabilities() says what this
build supports, rx.help_for(path) says what a parameter is, and
inspect.signature(obj) or help(obj) gives any call’s arguments. The names
are references/api.md, every one of them checked against
the installed package by test.
See also¶
The manual, Part 2 (theory): https://rietx.org/manual.html — every equation with its source, and the bibliography each author-year citation below resolves in
The repository, https://github.com/yue-here/rietx:
README.md,docs/DESIGN.md(why the FitReport is shaped this way),tests/data/README.md(provenance of every bundled dataset); none ships in the wheel
Reference: 4/4b. The evidence behind the judging rules¶
Load it when a rule in §4 (judging a fit) or §4b (declaring the deliverable) needs its measurement — before overriding one, and before quoting a number whose trustworthiness the rule decides.
A reference file of the rietx skill. The body it belongs to is
SKILL.md; section numbers are the ones the body cites.
Step 9 — what max_shift_over_esd separates¶
McCusker et al. (1999) §7 calls a refinement converged when max|Δθ|/esd ≤ 0.1.
The band is quoted from the paper and gates nothing here. A converged solve
satisfies it a fortiori, so the number earns its keep on the other branch: where
a stage stopped on STAGE_MAX_ITER, its magnitude says how far the solve was
still moving in esd units, which separates “nearly there” (just over the band)
from a fit that stopped mid-flight — measured ≈14 on one starved iteration.
Step 12 — the geometry table’s esds¶
stderr is propagated through the whole covariance, which McCusker §10
requires of any derived quantity, and stderr_diagonal beside it is what
ignoring the correlations would have given. Quote the first; use the pair when
you need to say how much the correlations mattered (measured on 11-BM NAC,
dropping them moves an esd by ×0.71 to ×1.15, in both directions, so a
diagonal esd is not the conservative choice).
None covers all four ways a number is unavailable: no covariance behind the
row (an evaluate-only pass), no free source, a quadratic form that reaches zero
by cancelling, and a straight angle where linear propagation does not hold. An
esd of 0 on a symmetry-fixed 90° angle would be a claim about precision rather
than a statement about constraint.
write_refinement_cif writes the whole table as _geom_bond_*,
_geom_contact_* and _geom_angle_* loops, with the symmetry codes resolvable
against the _space_group_symop_operation_xyz loop it writes beside them.
§2 rules 4-5 — the two Le Bail measurements¶
Iterate to a fixed point, and keep the best pass. On PbSO4 pass 1 stops at Rwp 20.756 % with an unphysical Caglioti V = +0.0615; passes 2-4 reach 10.247 % with the curve sane. Re-run the plan until Rwp stops moving. Because the alternation is not a descent on one objective, a later pass can come back worse — seen on Tb2BaCoO5, 17.3 % → 18.7 %.
Seed the background first. auto_background chooses the knot spacing or the
Chebyshev order but starts every coefficient at 0.0, so the modelled
background is identically zero and the first lebail_update runs before the
background has ever been fitted. The partition is then handed
max(y_obs − 0, 0) — the whole pedestal — and gives it to the Bragg
reflections. Measured on a synthetic pattern whose background is 5× its
strongest peak: cycle one claims 571× the true Bragg intensity.
The cell is the weaker half. Peterson (2005, Powder Diffr. 20, 14) fitted triclinic tricalcium silicate at NSLS X7A. The Le Bail fit won on both indices reported (χ² 16.46 against 22.81, R_p 2.34 against 3.52 at ambient) and its cell parameters wandered over a heating and cooling series, while the Rietveld cell from an admittedly imperfect model stayed consistent. Any single Le Bail fit there looked acceptable, and only the trend exposed it. The cause is reflection density rather than low symmetry alone, and the same disagreement has been reported on an I4/mmm structure. Peterson’s own scope is “particularly for high reflection density data”, and the abstract says so.
Measured at the other end of that variable, one range and one instrument treatment, three modes: 11-BM LaB₆, 2-40°, 55 reflections, mean FWHM 0.0097°, 0.014 reflections per FWHM. Rietveld a = 4.1568414(52) Å, Le Bail 4.1568425(51), Pawley 4.1568431(51) — a spread of 0.4 ppm, a third of one esd, with the esds equal to 2 %. The Le Bail fit also won on Rwp there (0.0861 against 0.0879) and its cell was still right, so the better Rwp is not itself the signal. Count reflections per FWHM: crowded, check the cell against a structural model; resolved, do not spend the fit.
Multi-phase Le Bail was broken until v1.0 and is now supported: the shares sum to 1 across all phases at every channel (measured Σ calculated / Σ observed excess 1.79 → 1.0000 on LaB₆ + CaF₂, single-phase path bit-identical). One caveat survives the fix and is about the method: the intensities of two phases whose reflections coincide are not separately determined by the data, since the partition splits them by the current model, which is a starting value and not a measurement. Treat a high multi-phase Le Bail Rwp as a reason to check the seeding above.
§3 rule 8 — what tying three oxygens actually bought¶
Fluorapatite’s three phosphate oxygens, tied as one displacement parameter: 20 → 18 free parameters, 287.5 → 319.4 observations per parameter, and B(O) 0.2763(1810) / 0.5279(1911) / 0.4149(1282) Ų free against 0.4138(899) Ų tied — tighter than the best of the three. Rwp moved by 0.05 % of itself, which is why it can tell you neither that the constraint helped nor that it hurt.
Every tie is recorded as a set_tie history node and restored by a checkout, so
a constrained protocol replays as one. Symmetry always outranks a user tie: a
cell axis the space group already ties, a coordinate behind its site-symmetry
direction, and a lebail/pawley mode-fixed path are refused by name rather
than silently ignored.
Step 13 — why the inflation is an upper bound, and why the trio travels¶
The Bérar-Lelann factor has an expected value of ≈1.51 even for perfectly white
residuals. That is a house derivation rather than the paper’s: chance same-sign
runs give E[χ²′]/χ² = 1 + 4/π (optimize.statistics.berar_lelann_factor,
simulation-verified). So it is an upper bound on the damage, not a measurement
of it.
report.identifiability quotes the qualifying trio side by side — raw χ²_red,
the inflation (already in every quoted esd, dividable back out), Durbin-Watson —
plus the δR line (delta_r_slope / delta_r_intercept: sorted Δ/σ against
normal quantiles; slope ≈ 1 and intercept ≈ 0 on honest σ, slope > 1 when σ is
underestimated).
The round robins measured why the ingredients matter: the same data refined under different protocols spread by up to ×17–25 of the quoted esds on cell dimensions (Hill, 1992; Hill & Cranswick, 1994, J. Appl. Cryst. 27, 802), whose explanation is §3’s first degeneracy row — the cell compensating 2θ-scale errors. Durbin-Watson is in the trio because serial correlation is precisely what makes the raw esds untrustworthy, and d stays discriminating where Rwp and GoF do not (Hill & Flack, 1987, J. Appl. Cryst. 20, 356).
Step 14 — the exchange row, the swap, and the ridge¶
The row and its verdict. An exchangeable=True row says a held
parameter’s signature is reproducible inside the fitted span (r2 → 1) and
that a fitted partner stands many σ from its null. An E2-shaped answer reads
“converged, but the fitted zero_shift is exchangeable with the held
sample_displacement — this fit cannot tell you which is physical”. Measured,
the fit carrying a planted displacement inside a compensating zero and its clean
reference differ in nothing but this row: χ²_red 1.012 against 1.010, R²
identical to six decimals, and only the partner’s 128σ against 1.6σ separates
them.
Why R² cannot decide it. R² is a geometric statement about column overlap and cannot say whether the counts in hand separate the pair. On real SRM 660c an R² of 0.9977 pair comes apart decisively: χ² 4.0752 (zero only) against 3.4890 (displacement only) on 5332 points, with the zero-only model biasing a by +100 ppm.
The decision band, and hedging a won swap. RIVAL_DECISIVE_MIN_CHI2_RATIO
is 1.10, read on the losing rival’s χ² over the winning rival’s. On the round-3
eval’s solvable control (rivals decisive at 1.1679, the SRM 660c pair above) the
agents that ran the swap recovered the true displacement and still declined or
hedged the answer: the control went 0/7 valid. Below the band the pair is
genuinely unresolved — the two real tie states measure 1.0075 and 1.0001.
Why the clause is on the statistics block as well as in the summary.
Measured consumers pipe the answer to a file and grep the statistics back, and
the summary string is what those greps drop: the licence reached agent context
in 2 of 12 cells from the summary, and 4 of 4 from the statistics block once
placed there. None there means no report was built or nothing crossed a
comment threshold, never a verdict.
The ridge. Freeing the held parameter alongside its partner and refitting lands on §3’s degenerate ridge and reports the unconstrained combination at a better Rwp. Measured over 30 agent runs, seven of twenty position cells took it. The swap runs each rival alone; the ridge runs them together.
Step 16 — what background-subtracted Rwp separates¶
A sharp LaB₆ fit and one under 0.6° of broadening both report Rwp 0.0137,
and background-subtracted they read 0.0490 and 0.0766. Raw Rwp is flattered by
whatever the background carries — 89 % of the observed intensity in both — so
the number that separates two fits is the subtracted one. The literature says
the same twice: Toby (2006, Fig. 1) shows identical model discrepancies reading
Rwp 23 % with no background and 3.5 % with one, and Hill’s 1992 round robin
recommends quoting the background-subtracted forms for exactly this comparison.
It is published on every report and deliberately never mentioned in summary,
because every background-dominated pattern would trigger it, converged ones
included.
Step 17 — a trace phase’s R_B¶
Neither index is weighted, so a reflection the fit barely constrains weighs as
much as one that dominates it — the weighted R_WI of Cox & Papoular (1996,
Mater. Sci. Forum 228–231, 233) exists to answer exactly this and is not
computed here — and a minor phase’s windows sit under the major phase’s peaks,
where the counts the major phase failed to describe are handed out too. Measured
on 11-BM NAC with 1.35 wt % CaF₂: 0.052 for the major phase against 0.385 for
the impurity, all of the latter in four reflections at I(obs)/I(calc) ≈ 2.2,
each under a strong NAC peak. Read it beside qpa.phases[].weight_fraction, and
treat a trace phase’s value as a question rather than a measurement.
write_refinement_cif writes them as _refine_ls_R_I_factor and
_refine_ls_R_factor_all on each phase’s own block, beside a
_pd_proc_ls_special_details that states the esd method in full — the base
estimator √diag(χ²_red·(JᵀJ)⁻¹), then the Bérar-Lelann factor it was multiplied
by, which §10 of the guidelines requires any publication to state.
§4b — Phase ID: the unmatched list and the Le Bail-gap read¶
report.unmatched’s kind="unmatched_obs" entries are the strong lines your
phase set does not produce — an unmodelled phase’s signature, distinct from
resolution-limited noise. report.lebail_gap is the structural-against-profile
triage: it re-partitions the per-hkl intensities at the frozen converged state
and reports both Rwp. A ratio ≫ 1 means positions and profile alone account
for the pattern, so every line is indexed and phase identification is safe
at any absolute Rwp — the structural model does not have to fit well for
the phase list to be right.
unmatched_calc asks the same question from the other side and only answers it
in Rietveld mode. Le Bail and Pawley extraction takes each intensity from
max(y_obs − y_bkg, 0), so a reflection with nothing under it is assigned
nothing and most of the residual the detector looks for goes with it. What
survives is a noise excursion near a tick. Measured on a synthetic LaB₆ pattern
(WP-1024): 17 of the certified cell’s own 28 reflections
and 94 of a doubled cell’s 153, 61 % either way, so it does not separate them at
all. The count that does is LeBailValidation.predicted_but_absent, which
integrates net intensity above the fitted background over each predicted
position. The blind direction is the one de Wolff’s M₂₀ has, and it is why
Oishi-Tomiyasu (2013, J. Appl. Cryst. 46, 1277) reversed the figure.
The clip at zero is rietx’s, and it changes what a bad background looks like.
Unrectified, the method returns negative intensities wherever the background
is overestimated, which David & Sivia (2002, Structure Determination from
Powder Diffraction Data, ch. 8) found and Le Bail’s retrospection passes on.
rietx partitions max(y_obs − y_bkg, 0), so a background
set too high hides in intensities pinned at nothing. A background driven too
low is the documented pathology on the other side: absent_reflections found
0 of 163 absences on a wrong candidate whose co-refined background had gone
negative.
§4b — the QPA background measurement in full¶
Fractions ride on scales, so the rows that decide a QPA are the ones that bias
scales silently. report.background.absorption, keyed by parameter path, is
the detector: the block projection R² of each structural parameter’s Jacobian
column onto the background column span sees §3’s scale↔Biso↔background
degeneracy that a pairwise ρ cannot. A negative Biso is that same error
laundered through a scale. The Le Bail gap reads the other way here than for
phase ID: a large ratio means the intensity model is wrong, and wrong
intensities are wrong fractions.
LaB₆, broad peaks, same data both times. Fitted with a 1°-knot unpenalized
spline the refinement reports Rwp 0.08852 and GoF 1.022, against 0.08969
and 1.025 with a correct Chebyshev-6 — the wrong background wins on every
agreement index — and its displacement parameters come back 0.958 and 0.000 Ų
against a truth of 0.5, one of them on its bound, where the correct background
gives 0.691 and 0.327. worst_absorption reads 0.46 against 0.08. Nothing
else in the report distinguishes these two fits, and the plot does not either:
the over-flexible residual is white noise inside ±3σ.
The whole report.background.absorption table is published rather than only the
entries over BACKGROUND_ABSORPTION_NOTABLE, because a fired/not-fired bit is a
verdict and the diagnostic already carries the verdict. A pairwise ρ misses the
effect entirely: ~0.2 per coefficient while the block absorbed 46 %.
§4b — the two QPA rules the round robins own¶
Madsen et al., 2001, J. Appl. Cryst. 34, 409; Scarlett et al., 2002, J. Appl. Cryst. 35, 383.
A Rietveld σ(W) reflects only the fit’s mathematical precision and is “not necessarily related to the accuracy”. Judge a fraction against the published participant spread, never against its own esd. (This is the policy the repository’s
tests/data/README.mdapplies to the bundledqarr/patterns, which are the round robin’s own samples.)Microabsorption is the largest physical obstacle to X-ray QPA — “may prove to be insurmountable in some circumstances” — and a Brindley correction applied where none is needed reduces accuracy (their sample 1 and synthetic bauxite;
BRINDLEY_OUTSIDE_REGIME).
§4b — the worked example that stops at GoF 2.97¶
LaB₆ pore proxy: a guest scatterer at the 1b site in the data only, host model
refined to convergence. Rietveld Rwp 0.0405, GoF 2.97 — a “bad fit” by GoF. The
report gives zero suggested actions; intensity carries 83 % of the misfit in
per-region errors of 9–18 % with alternating sign ((100) low, (110) high,
(111) low — structure-factor interference, which scale, ADP and texture cannot
produce, and the summary names it as un-modelled scattering contents); and
lebail_gap.rwp_lebail reads 0.0170 against 0.0405, a ratio of ×2.4.
Read by deliverable: phase ID is done — stop, at GoF 2.97. A structure determination is not, and its next move is chemistry (what occupies the pores), never finer profile corrections, which this evidence says cannot help.
§4b — why no bar moves for non-ideal data¶
The gates auto-scale to information content. Measured: a zero error read at confidence 0.997 on sharp data produces silence, GoF 1.02, on the same error under 0.6° of broadening. Pushing finer corrections into a fit whose attribution is resolution-limited changes numbers it cannot justify.
§4b — the trajectory deliverable, and where its rows come from¶
Four codes on SeriesResult.summary(deliverable="series") carry the chain’s
own rows. SEQUENTIAL_PATH_DEPENDENT is an ordering artefact — only a
direction="both" chain can produce one, so a one-way chain has not looked.
SEQUENTIAL_PERSISTENT_FINDING states a persistent count no per-pattern code
can, measured at 42 of 68 patterns on this ramp. SEQUENTIAL_DISCONTINUITY
flags either the science or a chain failure, and verify_discontinuities=True
tells them apart (below). PHASE_UNCONSTRAINED says the value is held, not
measured, so its trajectory is the one you handed in — then the QPA
deliverable’s own background check, applied at every point along the
chain: an absent phase took 40–96 wt % at equal Rwp.
The row exists because an agent needed it and wrote it itself. Given 68 patterns
of a variable-temperature ramp, rietx, and “tell me what the cell does, and
flag anything you would not quote”, a run recorded in full built its own
stopping rules and they were the right ones: model selection by ΔBIC and the
rival χ² ratio rather than by Rwp; the step at 430 → 440 °C verified by an
independent cold refit of both patterns; tan θ against cos θ to tell a cell
change from a specimen-height jump; a CaF₂ impurity used as an internal standard
to bound 2θ drift to ±27 ppm over 280 K; and the explicit split — precision on
the shape of a(T) at ±0.00015 Å, no accuracy claim on the absolute beyond
~100 ppm, because nothing pinned the 2θ scale. About 34 of its 90 calls went
into those checks. None of them is a fit statistic, and §4b named none of them.
Two are now the package’s:
The cold-refit check is
verify_discontinuities=True. Each flagged step’s two patterns are refitted cold and independently, and the diagnostic’svaluebecomes the cold step over the chain’s — 1.0 in the data, 0 the chain’s own. Measured on that ramp, reproducing the run’s own protocol: the chain takes 11.6–12.0 s and the check adds 5 % (12.1–12.2 s) for four flagged steps over four patterns, and the real transition reproduces at 1.00. The cost scales with the patterns flagged, not with the series length.The rows are
SeriesResult.summary(deliverable="series"). In the same re-run,PHASE_UNCONSTRAINEDfires on the impurity’s cell in 40 of 68 patterns and the trajectory of a held value is not a measurement — which is exactly the “would not quote” list the agent arrived at by hand.
The two the package cannot supply stay the caller’s, and the row says so rather than leaving them blank: nothing in a pattern file records what pinned the 2θ scale, and no esd can tell you it is a precision on the shape rather than an accuracy on the absolute.
Microstructure: what a domain size is, and what it is not¶
result.microstructure reads each of a phase’s four sample-broadening
coefficients as the quantity behind it — a coherent domain size in Å from
the two 1/cosθ terms, a dimensionless Δd/d from the two tanθ terms — with an
esd, or a named reason there is none (MicrostructureTerm.unavailable:
at_zero, no_wavelength, not_measured).
A domain size is not a particle size. Phase.particle_radius_um is a
different quantity for a different correction, the absorption path through a
grain; profile broadening measures the coherent domain inside it, which is
smaller and unrelated. Reporting one as the other is the commonest way to
misread this block.
It is not a two-figure number either. The Scherrer constant moves 10-20 %
with crystallite shape, and the codes disagree about which constant and which
measure of breadth: rietx reads the FWHM with K = 0.9, GSAS-II the FWHM with
K = 1, FullProf the integral breadth — which for a pure Lorentzian is 1.571×
rietx’s answer. Quote the size as an order of magnitude with the scherrer_k
the block carries, and quote a Δd/d with its convention (rietx’s is the FWHM of
the d-spacing distribution; GSAS-II’s mustrain is twice it, FullProf’s
apparent strain half of it before the breadth factor).
Read separable before either number. Over a short 2θ range 1/cosθ and
tanθ are one curve, so the fit trades a size against a strain at no cost in
Rwp. PhaseMicrostructure.separable is the width trend’s own verdict and
size_strain_collinearity the correlation it was decided on. False is not a
smaller number to quote — it is a wider range to collect. Refining one of the
pair and holding the other is not a workaround: the answer then depends on
which was held.
Then read size_agreement. rietx registers the Gaussian and Lorentzian
halves of each mechanism as independent columns, where GSAS-II refines one
magnitude and a mixing coefficient. Nothing makes them agree, so the ratio is a
measurement: far from 1 means the two describe different specimens and neither
is quotable alone.
In a joint fit, read the size and never the degrees. A size coefficient is
proportional to λ, so MultiHistogramRefinement normalises the shared column
and reports it at histogram 0’s wavelength, saying so through
SIZE_NORMALISED_ACROSS_WAVELENGTHS. Each histogram’s own structure copy
carries the coefficient it needs; the crystallite behind them is one number.
SIZE_UNUSUALLY_SMALL and STRAIN_UNUSUALLY_LARGE flag a coefficient
outside its usual range, and BOUND_HIT on the same path says why it stopped
there: a width term pinned to a bound is a held value, not a measurement,
and reads no differently in the table until you check for it.
§4b — Structure: why the intensity-model rows are not optional¶
Structure determination needs everything Phase ID, QPA, Trajectory and
Microstructure check, plus the rows that speak to the intensity model
directly: per-region intensity coefficients and their angular trends,
report.texture and report.strain with their caveats, restraint tension,
ADP positive-definiteness, and report.identifiability.exchanges with
.soft_modes (step 14, above). Here a notable Le Bail gap is a blocker,
not a comfort the way it can be for phase ID: the intensity model is the
structural claim, and a large gap says that model does not carry the pattern,
whatever the profile-only fit’s Rwp says.
Reference: 5. Read numbers, not pixels¶
Load it when a fit has converged and you are about to quote, compare or believe one of its numbers.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
This is the design premise of the package and the first thing that changes when the operator is an agent.
A human judges a fit by looking at it, especially at peak-shape misfit. A
vision model cannot do that reliably: frontier VLMs fail precise value
extraction from dense plots (the CharXiv, ChartMuseum and ExChart benchmarks),
and one PNG costs ~1000–1600 tokens — about the same as 50 regions of exact
numbers. All three prior agentic Rietveld systems (AgentBuild, Rongzai,
guillemot — the repository’s docs/DESIGN.md § “Outputs & fit assessment”
holds the survey) fed plot images to a VLM and all three report the same
failure:
locally bad, globally fine fits that the image hides.
So:
report = ref.report(plan="lab_bragg_brentano") # the plan supplies the Layer-2 veto
Layer 0 — model-free, always trustworthy: regions, per-region χ² share, cumulative-χ² breakpoints, unmatched peaks. Use this when the fit is bad.
Layer 1 — gated linear attribution: per region, how much of the misfit is a position error, a width error, an intensity error, a mixing error, an asymmetry error, with esds and each term’s share of the explained misfit.
Layer 2 — typed suggested actions from a closed enum, each with a confidence, a rationale,
alternatives, andvetoed_by.
The action vocabulary is closed (ActionKind, versioned by
report_thresholds_version), and each kind is carried out one of three ways —
how, quoted from the package’s own recipe table (report/apply.py) and
stamped on every emitted action as SuggestedAction.execution (WP-1106), so a
JSON consumer reads it beside the numbers rather than from this table alone:
stage (one run_stage over the action’s globs), index (a search, not a
stage), or advice (no verb — the note is the deliverable, and
parameter_paths is empty by design, not by omission). The table is every
member; emission conditions are the measured ones as of this writing and their
moves are logged in the schema version history:
Kind |
How |
Emitted when |
|
|---|---|---|---|
|
stage |
the |
|
|
stage |
the |
|
|
stage |
the |
|
|
stage |
the |
|
|
stage |
the |
|
|
stage |
the |
|
|
stage |
a width template is significant — always as the instrument-side peer of the sample action, at half its confidence, because a width trend alone cannot separate the two sides (the instrument’s Gaussian polynomial spans the same shapes; Toby 2024 §4’s U/V/W example). Try the sample terms first; reach for this when they leave the trend standing (measured: the sample proxy stalls at χ²_red 4.3 on a planted Gaussian deficit, this action takes the same state to the 1.01 noise floor — WP-1106) |
|
|
stage |
the |
|
|
stage |
the |
|
|
stage |
a significant asymmetry coefficient in gated regions below 2θ = 40° |
|
|
stage |
the relative intensity error trends with sin²θ/λ² — the ADP signature |
|
|
stage |
|
|
|
stage |
the |
|
|
advice |
strong unmatched observed peaks (> 8σ) not explained by the position-error evidence; when every one matches that evidence it is still emitted, capped at 0.3 with |
empty by design — no phase is named yet, so there is nothing to free; the note says what to do instead |
|
advice |
between-peak misfit is systematic (high off-region χ²_red at low Durbin-Watson) — the too-stiff detector; capped at 0.6 however strong the evidence (§7’s code block has why) |
empty by design — the edit is to the background’s shape, not to the free set; |
|
advice |
the background column span reproduces a notable share of a structural parameter ( |
empty by design, same reason |
|
index |
validity-radius failures are widespread among the misfitting regions — and it survives abstention, where it matters most (§6) |
|
|
advice |
the abstention classifier read the fit as |
empty by design — no parameter can be freed when the pattern itself is the limit |
And read it at more than one state. A report describes the state it was
built at, and the state a staged plan finishes in is routinely the least
informative one in the run: a compensated fit arrives somewhere that looks
converged and suggests nothing, because a real error has been absorbed into
whatever parameter the plan did free. Measured on the WP-1053 fixtures — a
−0.02 mm sample displacement, which no mccusker_default stage frees — the
final report reads Rwp 0.0137 with an empty action list, while the same
plan’s first stage names refine_sample_displacement at confidence 0.997.
Nothing was hidden; only the last state was ever delivered. So take the
trajectory (§9), and treat a rung’s high-confidence action as evidence about
the specimen even when the final report is silent.
Images are secondary evidence. plot_for_vlm() exists and renders what VLMs
can read (annotated multi-panel montage, worst regions auto-zoomed, Δ/σ panel,
high contrast, never JPEG) — use it to sanity-check a conclusion you already
reached from numbers, not to reach one. The Δ/σ panel is the literature’s own
recommendation for human plots too (Toby, 2024: the weighted difference shows
the weighting, stops intense regions dominating with statistically
insignificant deviations, and sits on an absolute scale with expectation 1).
Reference: 6. Abstention is a result. Do not convert it into a number¶
Load it when something declined to answer — an abstention, a caveat, a failed gate, a best_or_none() that came back None.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
The package’s hardest rule is never return a confident wrong singleton. Several places will decline to answer. When they do, that is the answer — propagate it, do not paper over it.
Signal |
Meaning |
Correct agent response |
|---|---|---|
|
The global maturity gate refused Layer 1: the model is too far from converged for linearisation to mean anything |
Branch on |
|
Strong unmatched observed peaks coexist with the texture detection, and the per-reflection extraction partitions un-modelled intensity onto its calculated neighbours — an impurity can manufacture exactly this signature |
The detection measures the residual, not necessarily the specimen. Resolve the unmatched peaks first; the |
|
This region’s coefficients failed resolvability / validity-radius / significance |
Read |
A trend is reported non-separable |
Two angular templates (e.g. size vs strain) are collinear over this range |
Do not pick one. Extend the range or report both |
|
A group’s summed intensity is determined; the split is not |
Treat the group sum as the datum |
|
σ²(M) went negative on some reflection — outside the physical cone |
The S_HKL are not quotable, and this is never evidence of anisotropy. Re-run with |
|
The tensor is not an ellipsoid; its Debye-Waller factor diverges at high Q |
Revert the site to isotropic |
|
The refined correction depresses no modelled reflection by >1 % |
Drop the block. The value it refined to is arbitrary |
|
The model carries no phase at all — the fit was refused before it started, and no result exists to read. A pattern-only project is a legal state (WP-1207): peak picking and indexing work over it, the parameter table holds the instrument and the background, and only refining is refused |
Do not retry the fit — nothing about it will change, and this is not the engine failing. Get a cell: |
|
A phase’s sample strain refined past 1.5 deg (the FWHM coefficient of tanθ, |
A number to check, not a failure — this is the one code here that is not a refusal. The threshold is the top of our archive’s 606 solved TOPAS refinements (p99 1.21, hand-written ceilings topping out at 1.5); above it that corpus holds 8 records in 5 contexts and all 8 are divergence signatures rather than materials. So the fit is finished and may be right: confirm the broad lines are really this phase’s and not an unmodelled peak shape, an amorphous component or a second phase it is absorbing. If |
|
A phase’s sample size broadening refined to an apparent crystallite below 5 nm (from |
A number to check, not a failure — the size twin of |
|
This phase’s strongest modelled point sits below 1σ of the observation noise, so the data cannot distinguish it from absent. |
Those values are not measurements either way, whatever their esds say: a held one is your own input, a refined one moved in a flat direction, since every structural parameter of a phase reaches the pattern only through scale × |F|² × profile. Do not read them, and do not read a converged status or a good Rwp as covering them — a parameter that does not move y_calc does not move Rwp either. Decide which case it is: the phase is not in this specimen and belongs out of the model; its scale is stuck at its floor and must be seeded before anything else of it is freed; or, for the third wording, the fitted range is wrong for the question. Nothing is owed in response to the hold itself — it costs nothing, it releases itself in the stage where the phase appears, and re-running to “free” the parameters would only reproduce it. On a series, expect this per pattern — see §7’s note on reading it across a run |
|
A parameter’s trajectory differs between the forward and backward chains by more than their esds allow. Judged per pattern, and only where both chains measured an esd for that same pattern — so a parameter one chain held throughout, and one the two chains share no measured pattern for, is not judged at all |
That trajectory is an artefact of the refinement order, not a measurement. Hold the parameter, restrain it, or quote the forward/backward spread as its uncertainty. Silence is not clearance: this code’s absence means the two chains agreed where they were comparable, which for a path one direction held throughout is nowhere. Two lengths cannot tell you which it was — equal lengths are consistent with zero shared patterns, and identical labels with not one pattern measured by both. Count the patterns actually judged: intersect |
|
A per-pattern code fired in more than half the patterns; |
Read it as a property of the model, not of any pattern. One |
|
The chain was cancelled: the pattern in flight was abandoned and the rest were never started |
The reported entries are complete fits and stand on their own, but the trajectory is truncated, not finished — do not read its last point as the end of the ramp, and do not compare a slope over it with one over the whole series |
|
A pattern diverged and stayed diverged after every rung of the escalation ladder ( |
Read that point as a failed fit, not a datum — do not interpolate across it and do not quote its parameters. Its neighbours are unaffected: the chain stepped over it, so the successor warm-started from the last pattern that converged and the reseed median never saw it. Fit the pattern on its own to find out why (a specimen change the model lacks, a bad scan, a starting model that no longer suits this end of the series) |
|
No candidate cell reached the confidence gate |
This is the most likely outcome of a first indexing run, and it is not a failure. Read each candidate’s |
|
The result declined to name a cell, and says why |
Propagate the abstention. The candidates are there so you can see what was considered, not so you can pick one |
|
Two distinct lattices explain the positions equally well (Mighell & Santoro 1975) |
Do not pick one. The information is absent from the measurement, not buried in noise — collect to the 2θ in |
|
More than one candidate satisfies the whole gate |
Compare the panels and each |
|
A budget expired before the domain did |
Do not read “no cell found” as “no cell exists”. Only a completed exhaustive search says that, and |
|
The declared whole-run ceiling ( |
The answer covers what was reached. Read the three states of |
|
Systems were not searched |
Read a failure as “no cell in the systems searched”, never as a statement about the specimen — and in particular never as “this is multiphase” (§8.15) |
|
No extinction class is separated from its rivals by these data |
Read the ranked |
|
The leading class holds more than one space group |
Nothing. This is not a data problem and no counting time fixes it: those groups differ only by elements that produce no absences. Carry the whole list forward and choose with chemistry (§7e) |
Layer 1’s gates, for reference, are: resolvability on the scale-normalised
Gram matrix, a 0.4·FWHM validity radius (a peak 5 FWHM off must trigger
“re-detect”, never a confident small-offset reading), local χ²_red
significance, and share-based global maturity. Confidence weights importance
(share of χ²), not only statistical significance — at high counting statistics
the second-order leakage of a peak shift into the width column is significant
but carries a per-cent-level share.
The per-region refusals arrive typed — region.gate_failures is a list of
GateFailure(code, message) since WP-1003, promoted from formatted strings
precisely so a consumer can branch on the name; the message carries the
measured numbers and is display-only. The vocabulary (GateCode) is closed,
the global maturity gate has no entry here (it abstains the whole layer,
abstained_reason/abstained_kind above), and one member is not a failure at
all:
|
Meaning |
Correct response |
|---|---|---|
|
this region’s local χ²_red is at the noise floor — there is nothing to attribute |
Nothing. This is a region the model already fits, not a failure of the basis and not evidence about the model; a report full of these is what a good fit looks like |
|
the region’s misfit is real but the basis explained too little of it (low local R²) |
Do not read the coefficients as causes. The misfit is not position/width/intensity/mixing/asymmetry-shaped — suspect what the basis cannot express: an unmatched peak (Layer 0), background shape, peak-shape misfit |
|
the templates’ scale-normalised Gram matrix is ill-conditioned here: the edit directions are indistinguishable on these merged peaks |
The resolution-limited signature, per region ( |
|
the fitted offset exceeds the 0.4·FWHM linearisation radius, and a linear fit pushed past it saturates |
Re-detect, never shift: read the fitted offset as a lower bound, not a measurement. Widespread across misfitting regions, this is exactly what emits |
Reference: 7. The diagnostics are the channel for “your answer is wrong although Rwp is fine”¶
Load it when a Diagnostic fires and you need the row: what it means, what to do, and what you must not do.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
Every code below is a structured Diagnostic on result.diagnostics with a
level, a where list of parameter paths, a message and a suggestion.
Branch on the code, not on the message text.
One namespace note, because two vocabularies share the same UPPER_SNAKE shape:
every code in this document is the engine’s, a Diagnostic.code. The GUI
server’s session codes (NOT_FOUND, RUN_IN_FLIGHT, STALE_REVISION, …) are
a separate vocabulary with no rows here, so a code met outside
result.diagnostics is the server’s, not the engine’s.
Code |
What it means you must not do |
|---|---|
|
Quote both members of the pair as independently measured. Deduplicated across a plan’s stages (worst |ρ| kept, every stage it fired in named in the message) and capped at 10 per fit, worst first — a |
|
(info) Read it as “nothing was dropped from the fit” — only from this message. |
|
Quote a parameter sitting on its bound as a measurement. Iterating |
|
Quote ADPs, scales or QPA fractions from this fit — the background can imitate them, and Rwp improved while they biased. Read the number, not the bit: |
|
Quote anything from a fit whose declared hump has narrowed towards the instrumental resolution — the peak parameters, the displacement parameters it was supposed to protect, or the Rwp. A |
|
Quote the intensity split between the phase and the declared peak as if it were measured — it is the §3 degeneracy, two terms over one peak. Not wrong by itself: a holder line overlapping a sample peak is what the member is for. What it rules out is the shortcut reading, a component declared to make one misfitting reflection go away — check the phase’s scale, profile and preferred orientation first. If the overlap is real, quote the phase quantities and say they overlap |
|
Quote the declared peak’s centre, or its esd. The area refined onto its zero bound, so the data does not see the peak — and a peak reaches the pattern only through |
|
Quote roughness and the displacement parameters as two separate results |
|
Interpret the roughness parameters physically |
|
Quote anything about this phase — its cell, its coordinates, its ADPs, its weight fraction — held or refined alike, and in particular do not read a held value as “it did not move, so it is right”. |
|
Report the tensor as measured |
|
Report any S_HKL |
|
Quote the strain, the crystallite size derived from the same profile, or that phase’s weight fraction without saying the width is this large — a strain this far out reaches across the pattern and competes with the background and with any phase it overlaps. Unlike every other row here this is not an instruction to withhold the number, only to adjudicate it |
|
Quote the crystallite size, the strain, or that phase’s weight fraction without saying the crystallite is this small — a size this far out broadens every line and, like a large strain, competes with the background and with any phase it overlaps. Like |
|
(info) Read a shared |
|
Quote QPA weight fractions — unequal f′ across phases biases them directly (measured: RMS 2.26 → 0.69 wt % once applied) |
|
Quote an occupancy or a displacement parameter on that site without first checking this wavelength is away from the resonance. The neutron twin of |
|
Assume the model scatters like the ion its species label names. |
|
Transfer the fitted per cent to another histogram, or read it as a property of the specimen. It is a refined scale ratio between two wavelength components of one measurement on one monochromator, so it is not comparable across instruments and not a material constant. At |
|
Read it as a failure of the correction. It is a result: the declared λ/n line refined to nothing, which is what a monochromator whose nth order is extinct must give. Dropping the declaration reproduces the fit with one parameter fewer |
|
Quote the harmonic fraction at all — the weight never entered θ, so the number is the value the caller declared, not a measured one. Free |
|
Assume the absorption correction ran. It did not; the fit has no specimen absorption correction |
|
Trust the magnitude of the correction — µR > 1 extrapolates the Rouse fit |
|
Quote displacement parameters without checking the flat specimen’s thickness — part of the correction is not absorbable by the scale and ADPs, so a wrong µt lands in both |
|
(info) Read it as a fit problem — a transmission plate far from µt = 1 costs counts, not accuracy |
|
(info) Quote the cell without saying what pinned the scale the wavelength was measured against — the held cell in a single-histogram fit, or which histogram’s wavelength was held in a joint one. This fires whenever a wavelength was refined, and reports how far it moved from its declared value in ppm together with the shift over its own esd. The reference is the wavelength declared when the refinement was constructed, so the ppm is the cumulative move across successive λ-freeing stages (a |
|
(info) Read a cell from this fit as free of position systematics. The eq (4) offsets were held at zero for want of a goniometer radius, and an unmodelled offset moves every axis by the same fraction — so an axial ratio can agree to a few ppm while no axis does (measured: 143 ppm on both Cr2WO6 axes, c/a agreeing to 3.4 ppm). It fires on any capillary geometry with no radius, calibrated synchrotrons included (11-BM among them): there the 2θ scale is pinned and a certified cell reproduces to spec, so note it and move on; on a laboratory capillary the same held offset is a real accuracy gap |
|
Prefer the corrected fractions; past µR ≈ 0.05 the “correction” can be further from truth than none — the QPA round robin’s community-wide finding (Madsen et al. 2001; Scarlett et al. 2002: over-correction where none was needed cost more accuracy than the effect it corrects) |
|
Assume microabsorption was handled |
|
Use an individual reflection intensity from the group |
|
Read the esds as the whole story. There are fewer effective observations per structural parameter than the guidelines ask for (at least three, preferably five), and an over-parameterised Rietveld refinement does not fail — it reports large esds while the algorithm’s own N, the number of profile steps, hides the shortage. |
|
Quote integrated intensities, ADPs or QPA fractions and expect the step size not to be the limit. The median resolved peak has fewer than five steps across its FWHM, so those intensities were never measured finely enough — and unlike everything else in this table, no choice made after the fact repairs it: the counts do not exist. The fit still runs and the number is on |
|
(warning) Trust the background below the first reflection because |
|
Silently accept the averaged compromise — the data and your prior disagree by >3σ |
|
Read this point of a series as evidence that the trajectory is continuous — its starting values did not come from its neighbour |
|
Report the jump as physics without opening that pattern’s own fit; it is equally the signature of a chain failure. |
|
Quote that parameter’s per-pattern esd as its uncertainty — the between-chain spread is larger and is the honest one |
|
Read the shortened |
|
Read that point’s values as a measurement, or its failure as evidence about its neighbours — nothing was chained through it |
|
(info, |
|
(info) Read this as anything about the reported values — it is not. |
|
Read |
|
(error) Read |
|
Read the result’s |
|
(warning) Read the composition, ZMV or any weight fraction as the one you meant. The phase names a bare Hermann-Mauguin symbol the tables hold in more than one setting (40 of them — the |
|
(warning — on |
|
(warning — from the reader, same channel as |
|
(info — from the pattern reader: pass |
|
(info — from the reader) Quote the file’s point count as the fitted channel count. One or more 2θ values appeared twice with the same intensity and the repeats were dropped. A repeat with a different intensity is not reported here: it raises, because averaging invents a datum and dropping picks one |
|
(warning — from the reader) Read the result as a fit to the file. The file holds several scans, none was named, and scan 0 was read — a third of a measurement is a choice, and this one was made by default rather than by you. Pass |
|
(warning — from the reader) Trust the 2θ scale. The file states which axis it scanned and named one this reader does not recognise, so the x column was read as 2θ in degrees. Each format states it somewhere different — |
|
(warning — from the reader) Quote esds, χ² or a σ-normalised residual from this fit as if the weights were measured. The stored intensities are not whole numbers and no counting time in the header makes them whole either, so whether they are counts or a rate could not be established — and no σ was supplied. The Poisson fallback √max(y,1) is therefore being applied to a quantity that may already be divided by a counting time, in which case every weight is wrong by √t. The fit still runs and the parameters are largely unaffected; it is the uncertainties that are not quotable. Re-export in counts, or supply esds |
|
(warning — from the reader) Compare intensities across the 2θ range this names. A Rigaku |
|
(info — from the reader) Nothing; it is here because the correction is large and invisible in the file’s own numbers. A PANalytical beam attenuator dropped a foil in front of the detector for the points named, and the reported intensity is |
|
(info — from the reader) Nothing to the intensities; they are already right. A Bruker automatic absorber engaged over the points named, and the stored series is already corrected for it, so nothing was multiplied — but those points carry a factor fewer counts than their height suggests, and σ was derived as √(y/a)·a rather than √y. The third answer to one question: |
|
(info — from the reader) Assume a reader option you passed took effect. This format does not take it, so the file was read as if it had not been given — normal when a form carries a value across a change of file, and a mistake when you named a |
|
(info — from the reader: pass |
The TOPAS_* and FULLPROF_* families are not here: they belong to
rietx.io.projects, they fire at import rather than on a fit, and none of
them ever reaches result.diagnostics. They have their own file —
§7g, references/diagnostics-projects.md — because
the question they answer comes before a fit exists: is the model I just
imported the model that file describes? Load that one if a TOPAS .inp or a
FullProf .pcr import handed you a diagnostic.
The RECIPE_* family is not here either: it belongs to the PowderLine
recipe reader (read_recipe, Recipe.diagnostics) rather than
result.diagnostics, and it answers a question the others do not: how does
the fit I am about to run differ from the one the recipe describes? It has
its own file too — §7g, references/diagnostics-projects.md
— since a PowderLine recipe is another program’s file just as a TOPAS .inp
or a FullProf .pcr is. Load that one if a PowderLine recipe handed you a
diagnostic.
codes = {d.code for d in result.diagnostics}
if "BACKGROUND_ABSORPTION" in codes:
# stiffen the background BEFORE reporting anything intensity-derived
...
# the same measurement as evidence rather than as a bit — and the two
# background hypotheses, which are advice: they change what the background can
# *absorb*, not which parameters move, so there is no stage to run
bg = report.background # None only if no background curve
bg.worst_absorption, bg.worst_absorption_path # 0.46, "…atoms.0.biso"
bg.absorption # every screened path → R²
bg.off_region_chi2_reduced, bg.off_region_durbin_watson # 12.6, 0.19
report.action("decrease_background_flexibility") # fewer terms, larger λ
report.action("increase_background_flexibility") # capped at 0.6 — see below
increase_background_flexibility is capped however strong its evidence,
because an amorphous hump and an un-modelled broad crystalline phase produce
the same between-peak signature as a too-stiff background, and bending the
background over either of the last two hides it while improving every
statistic. add_impurity_phase rides in its alternatives for that reason,
and each names the other. If you add the flexibility anyway, read any QPA
afterwards as fractions of the crystalline content you did model.
Reference: 7b-7f. Indexing: peak picking, the answer, the closed loop, the extinction screen¶
Load it when the phase is unknown — peak picking through indexing, space-group screening, and on to a refinement.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
7b. Peak picking and indexing (PeakList.diagnostics, DataQualityReport.diagnostics)¶
These arrive from rietx.pick_peaks and
rietx.indexing.assess_peak_list, before any refinement exists — so they
are read on the peak list, not on a RefinementResult.
Code |
What it means you must not do |
|---|---|
|
Read the answer as scored. Below 20 usable lines the classical figures (M₂₀, F₂₀, Smith’s envelope) are undefined, so the search still runs — over the systems the line count supports — but ranks on the reduced panel, and nothing in the answer is comparable to a published threshold. (Before WP-1043 this code refused the search outright; that conflated scoring with searching, and it refused fluorite’s 18 clean lines that all three engines index at −5 ppm) |
|
Spend a search budget. The gate has already decided the data cannot support a search in any system, and it names which of the two reasons applies (lines per metric degree of freedom, or σ(Q)/Q) |
|
Treat an absent figure as zero, or compute your own M on fewer lines and quote it as M₂₀. Each absent member is named with its reason on |
|
Quote a precision, or weight lines by 1/σ² as if that meant something — every σ in the list is the same assumed constant. Re-pick from the pattern if you have it. You may still index it: an assumed σ is not grounds for refusing, so the σ(Q)/Q abstention below does not run on such a list (it would be quoting a precision this package invented) |
|
Treat the flagged component as a line, or as noise. It is neither: the fit believes in it as shape and disbelieves it as a line, so it stays in |
|
(info/warning) Ignore it when choosing a tolerance: it is the resolving power of the list, and it bounds every tolerance downstream |
|
Absorb the shift into the cell. The named template is the physical cause; correct the instrument ( |
|
Pick one cause from this data. The magnitude is measured and the cell is safe to |
|
Assume the dropped candidates were noise — each is at a stronger line’s Bragg-predicted Kα2 position, and a genuine coincident line is indistinguishable from an alias in one pattern |
|
(info) Read the flagged components as lattice lines — or exclude them without looking. Each sits on the axial-divergence tail side of a much stronger group-mate (low-2θ below 90°, high-2θ above — the one sign flip nothing else has), and they stay usable because the side test is evidence, not proof. Measured on SRM 660c (WP-1043): five such components carried 125 ppm of certified-cell bias, and excluding exactly the flagged ones plus measuring the shift took the gate to |
|
(info) Treat it as a line of anything. It sits at a strong group-mate’s predicted Kα2 maximum at a few per cent of its area — the residual of a modelled doublet, re-created by a re-seed pass after detection dropped the alias. Kept usable only because a genuine reflection can coincide with an alias position; exclude it before indexing unless you have a reason to believe one does |
|
Quote one of a pair as an independent line. Their σ already carries the correlation |
|
Subtract it. Ghosts are flagged and excluded from |
|
Trust the positions of the flagged lines. An unmodelled one-sided aberration biases a centroid in one direction, which σ cannot see — and the low-angle lines are the ones indexing depends on most |
|
Leave |
|
(info) Read a shoulder-seeded line as a detection. Survival was decided by ΔBIC, not by detection |
|
Read “no cell found” as “no cell exists”. Only a completed exhaustive search says that; this one ran out of budget, and |
|
Conclude the pattern cannot be indexed. The exact-solve engine found nothing at its base-line index table but found a cell with a wider one, which means one axis is long enough (or short enough) that the lowest observed lines carry large indices. Use the dichotomy engine, which bounds the metric instead of assuming indices |
|
(info) Quote the winning cell without fitting a shift template. The search assumed a systematic allowance (no shift had been measured), and a cell found inside a widened window absorbs the shift — measured, +1400 ppm on a certified pattern. Re-fit with |
|
(info) Read the reported amplitude as naming a cause. It does not: the pair method measures the shift’s size from harmonic reflection pairs with no reference, and |
Peaks you name (rx.fit_peaks)¶
pick_peaks decides what the lines are; fit_peaks(data, instrument, positions)
fits exactly the positions you pass and nothing else. Reach for it when the
question is about the peaks rather than about a cell: a width analysis
(Williamson-Hall) over lines you chose, d-spacings for a lookup, one reflection
checked. It needs no structure and no space group, and it runs in tens of
milliseconds on a lab pattern.
The answer is an ordinary PeakList, so §7b’s flags and the usable()/peaks
split read exactly as they do above. Four rules are specific to naming your own
positions.
A position with no peak comes back flagged, not dropped. It carries
no_intensity(and usuallyposition_at_bound), and it is out ofusable(). Do not read itstwo_theta: a component at zero intensity has no gradient on its own position, so the fitter leaves it wherever the solve ended, and the esd says so in the only way it can — 3e+15° on the bundled fluorapatite pattern. Count the flag, quote nothing (WP-1101).unnamed_neighbourmeans your list is incomplete, and the bias is real. It fires when the window holds a component you did not name, decided by the same ΔBIC testpick_peaksuses to accept one. Measured on the bundled fluorapatite: naming 52.253° alone gives 52.2502(10)° with χ²_red 4.94; naming its weak neighbour at 52.170° too gives 52.2529(8)° with χ²_red 1.50 — the line you wanted moved 2.7 m°, 2.7 of its own esds. On a deliberately doubled synthetic the same omission moved a position 51 m°, 26 esds, with χ²_red 0.93 → 82.5. Re-fit with the neighbour named, or say the position is biased; do not quote it silently.Widths are the pattern’s, not the specimen’s.
ObservedPeak.fwhmis what was measured: instrument convolved with sample. A Williamson-Hall intercept from raw widths is a floor set by the diffractometer (1355 Å on that fluorapatite, where the instrument dominates), not a crystallite size. Calibrate the instrument on a standard first (lab_calibrate,save_instrument_profile), or refine the sample-broadening terms and readrietx.model.microstructure, which reports each mechanism with an esd and says whether the two are separable. State K and whether β is FWHM or integral breadth whenever you quote a size: they differ by ~10 % on the same data.Refusals are by name, not by silence. A position off the end of the pattern, or in a gap or an excluded region, raises
ValueErrornaming the position and the channel count. That is a fact about the range you asked for; re-read the pattern’s limits rather than retrying. The same position named twice is refused too: two components at one 2θ are exactly singular in their intensities, so there is no answer to give.
Positions sharing a window are fitted together in one solve, so pass the whole cluster at once rather than calling once per line — separate calls over overlapping lines each bias the other.
7c. The answer’s own diagnostics (IndexingResult.diagnostics, and each candidate’s)¶
These arrive from rietx.index_pattern. Statements about one candidate live
on that candidate (result.candidates[i].diagnostics); statements about the
result live on the result. INDEX_ABSTAINED names the top candidate’s caveats,
which is the pointer from one level to the other — so read both, and start at the
result.
Code |
What it means you must not do |
|---|---|
|
Keep this candidate. The lattice needs reflections the pattern does not have — the oversized-cell signature, and the one M₂₀ cannot see. Prefer the smaller cell that indexes the same lines: a cell whose extra reflections are systematically absent has a translation it is not using, so the lattice is the sublattice. Do not check this with Rwp — measured, an oversized cell scores 0.379 against a correct 0.216, a gap smaller than the spread between specimens |
|
Read it as one thing. A handful of unexplained lines is an impurity; most of the pattern is a wrong metric (measured, 95 of them when the metric was 1 % off). And note the fence: this package does not index multi-phase patterns, so a second phase means subtracting the solved one first |
|
Refine in the higher symmetry because it was reported. The stated system is the conservative one; refine there and test the higher one, never the reverse. A disagreement between gemmi and spglib is information, not a bug — their tolerances are different kinds of number (a Le Page obliquity in degrees against a |
|
Quote the cell. It is outside what these data can support — below a single atom’s exclusion volume, or clear of Smith’s (1977) envelope for the number of lines observed |
|
Read a |
|
(warning — on the candidate’s |
|
Read the answer as covering the requested search. The ceiling ( |
|
(info) Read |
|
Quote a Bragg-Brentano cell to its esd. The esd is a precision from the line positions; the goniometer radius alone carries ≈ ±85 ppm that no esd reports, because the data cannot identify it (Rwp moves 0.029 points across 180–320 mm) |
|
(info) Read the reported list as everything the search produced. It is the top |
|
(info) Read the answer as unsteered. It was steered — this diagnostic names each declared prior and its fate (confirmed by engines / entered unconfirmed / refuted / refused at the box) — but steering changed only when things were searched and what seeded the stochastic engine, never a range, a system set, or a rank: prior-only candidates are appended after the ranked list and never enter the Borda ranking. A candidate whose |
7d. The closed loop: from a pattern of an unknown phase to a refinement¶
Indexing is the step that used to be missing. Before it, this package could
refine a structure against a pattern but could not find the cell, so an unknown
phase was out of reach entirely. index_pattern is a peer of refine and the
loop between them closes:
These names are provisional, and the answers they return are versioned.
Indexing is under active development, so everything under rietx.indexing and
every answer type in rietx.schemas.indexing may change in any release — the
compatibility promise
declares the subsystem rather than listing names, and every change is in the
release notes. Two things do not move with them:
capabilities().indexing_thresholds_version, which is what the gates below are
versioned by, and the serialized shape of an IndexingResult. So a tool loop
that reads an answer sees any observable change as a version bump; one that
imports these types should pin an exact version.
How long will this take? Since WP-1042 the default answers this itself:
index_pattern resolves the quick preset — every engine, every requested
system, and a whole-run ceiling (SEARCH_PRESETS["quick"]) covering search,
probe and validation, with each validation fit drawing an equal slice of the
remaining clock. Nothing is narrowed; a run that hits the ceiling says so
(INDEX_BUDGET_EXHAUSTED, §7c) rather than having silently searched less, and
what it cuts is the trailing low-symmetry systems — cheapest-first ordering’s
documented cost. Progress and a graded shortlist for every completed system
stream on the event ladder as the run goes (events=), so the useful answer
usually arrives seconds in, long before the run ends. preset="full" is the
unbounded pre-1.0 behaviour — reach for it when a quick run reports truncated
or not-reached systems and the answer may live there. For the arithmetic, ask
rietx.indexing.engines.estimate_ceiling(spec) (CLI: rietx index --ceiling): budget_seconds (default 30) is per (engine × system), the
worst case is that arithmetic plus the probe plus per-fit validation (measured
0.6–44 s each), against measured typicals an order of magnitude lower, because
searches usually finish their systems early. Budgets are runaway guards, not
timers — this package’s record has six point measurements where a longer run
never bought a better answer, and one where too little budget reported a wrong
centring, so bound generously and read the three states rather than shrinking
the search.
State what you know. You often hold something the search does not: an
isostructural analogue from a database hit, a homologue’s cell, a family’s
space group. Declare it (WP-1045) — SearchSpec.prior_cells /
prior_spacegroups, the same fields on the agent request’s search and in
the GUI’s Search controls — and the search runs the prior’s crystal system
first, seeds the stochastic engine’s starting basin with the stated metric,
and checks the cell itself against the lines. Three facts make this safe to
do liberally: a prior steers, never gates (no system dropped, no range
changed, prior-only candidates appended after the ranked list — a wrong
prior costs time, not truth, and that sentence is pinned by test); a real
prior is then found by the engines themselves, so found_by and the grade
keep their meaning; and INDEX_PRIOR_USED records what you assumed and what
it changed, so assumed knowledge can never read as measured knowledge. A
worked example — you suspect the specimen is isostructural with calcite
(R -3 c, a = 4.99 Å, c = 17.06 Å):
idx = rietx.index_pattern(
peaks, data=data, instrument=instrument,
spec=rietx.indexing.SearchSpec(
prior_cells=((4.99, 4.99, 17.06, 90.0, 90.0, 120.0),),
prior_spacegroups=("R -3 c",))) # trigonal jumps the queue; the
# centring steers the prior's check
# the same controls survive a JSON round trip (a project document stores them):
# {"search": {"prior_cells": [[4.99, 4.99, 17.06, 90, 90, 120]],
# "prior_spacegroups": ["R -3 c"]}}
If the analogue is right, the truth arrives in the first streamed per-system shortlist instead of after the whole sweep; if it is wrong, the final list is the one you would have had anyway, plus the record that the prior was tried.
peaks = rietx.pick_peaks(data, instrument) # fitted positions + σ
report = rietx.indexing.assess_peak_list(peaks) # fit to index at all?
if not report.supports_indexing:
... # abstention. Do not spend a budget (§6)
idx = rietx.index_pattern(peaks, data=data, instrument=instrument)
cell = idx.best_or_none()
if cell is None:
... # read confidence_caveats; do NOT take candidates[0]
phase = rietx.indexing.structure_from_candidate(cell) # dummy atom, lattice group
result = rietx.refine(data, phase, instrument, mode="lebail",
plan="profile_only")
screen = rietx.determine_extinction_symbol(data, cell, instrument)
klass = screen.best_or_none() # an extinction *class*, never one group
if klass is not None:
# any member fits the data equally well — that is what the class means
phase = rietx.indexing.structure_from_candidate(
cell, space_group=klass.space_groups[0])
Five things about that sequence are load-bearing:
Pass the pattern, not only the peaks. It is what turns whole-profile validation on, and validation is what catches the oversized cell no figure of merit sees. Without it every candidate caps at
mediumand the result abstains.best_or_none()returningNoneis the normal first outcome. With no measured systematic shift it is currently unreachable to gethighon real lab data at all — both engines widen their matching window by an assumed allowance and say so — and the fix is evidence (an internal standard, a calibratedshift_allowance_deg), not a bigger constant.Go through
structure_from_candidate. It supplies the mandatory dummy atom and, more importantly, defaults the space group to the absence-free lattice group. A plausible-looking space group would hide exactly the reflections whose absence is not yet established — which is also why the indexing gate’spredicted_but_absenttest must keep running against the lattice group even after the screen has named a class.The extinction screen answers the next question, and answers it as a class.
determine_extinction_symbolranks the classes the lattice admits, each listing its space groups; the powder observable is the extinction symbol, so a returned class with three groups in it is a complete answer, not a hedge (§7e).Choosing inside the class is chemistry, not diffraction. Any member can be handed to
structure_from_candidatefor the Le Bail or Rietveld step that follows — they predict the same reflections at the same positions. What separates them is what you know about the compound, or which one a structure solution works in.
The reverse direction closes too. When a refinement’s Layer 2 emits
reindex_or_recheck_cell — peak offsets beyond the linearisation radius in most
of the misfitting regions, i.e. the cell is wrong (or the calibration grossly
off) rather than slightly off — that action has something to call: pick peaks
and run index_pattern on the same data. Since WP-1054 it survives abstention,
which is where it matters most: the wrong-cell state abstains, and before the
fix it surfaced only a confident add_impurity_phase built from its own
displaced peaks. The action carries refine_zero_shift /
refine_sample_displacement in alternatives because the validity-radius
signature cannot choose between a wrong cell and a gross calibration error —
re-indexing is still the safe first move, because index_pattern searches
under its own shift allowance.
7e. The extinction screen (ExtinctionScreen.diagnostics, and each class’s)¶
These arrive from rietx.determine_extinction_symbol, which runs after a cell
is in hand and answers the next question — which systematic absences the pattern
shows. Same split as §7c: a refutation lives on the class it refutes.
Code |
What it means you must not do |
|---|---|
|
(info) Pick one of the listed space groups and call it the answer. They produce identical powder patterns by construction — a centre of symmetry, an enantiomorph or a mirror leaves no absence — so this is not weak data and not a tie to be broken by counting longer. Carry the list; |
|
Read the ranked first class as the answer. It fires for three different reasons and says which: a runner-up inside the decisive ΔBIC margin, a leading class none of whose absences is testable here (each is outside the range, coincides with a line the class still allows, or sits where the class’s own fit already puts a neighbour’s tail), or classes a |
|
Keep this class. A position it forbids carries intensity its own Le Bail fit cannot account for, and the hkl and 2θ are named so you can look. Two things it is not: a position under a neighbour’s tail, which is no longer testable at all (WP-1077), and necessarily a violated absence — one flagged position can be an impurity line, so check it against the indexing result’s |
|
(info) Read |
|
(error) Read the empty ranking, or any class’s absence from it, as evidence about the symmetry. The reference Le Bail fit of the absence-free lattice group raised (the message names the exception), so no class was screened at all — |
Three things about the screen that change how you use its answer:
The score is a nested comparison, not Rwp. A class with fewer absences has more reflections and can only fit at least as well, so Rwp ranks the least-constrained class first every time.
delta_bicis BIC(class) − BIC(absence-free lattice); negative favours the class, and the difference between two classes’ values is itself a ΔBIC. Measured on a synthetic P 2₁/c specimen: the true class and its screw-free partner differ by 1e-5 in Rwp and by 24 in ΔBIC.n_addedcounts only testable absences, so a class whose extra absences all hide under allowed neighbours earns nothing for them. Readn_testablebesiden_absent: if it is zero the class is a hypothesis these data cannot address, whatever its Rwp; if it isNonethe class was never fitted, so the question was not asked. The third clause of testable — the class’s own fit must leave the window below the detection threshold — is what stops a badly-modelled peak tail refuting a true class, and it is measured: on certified corundum, sham positions 1–3 FWHM below an allowed line, carrying no reflection at all, clear the same 3σ test on 40–50 % of probes.The absence-free class winning is a result, not a failure. On NAC (I 2₁3) it is the correct answer: I-centring already extinguishes the very reflections the 2₁ screws would, so those screws are invisible in principle. It is a wrong answer when the shared profile fit is bad, and
ExtinctionScreen.profile_rwpis the field that tells the two apart: on certified corundum the screen returns the certifiedR - c -at Rwp 0.149 and the absence-freeR - - -at 0.270, from the same cell and the same pattern. Give it a range and a width law its profile fit can match before reading a refutation.
where now names the paths on every guard code, HIGH_CORRELATION included
(v1.0, WP-1007). It used to be empty on that one — the paths were recovered from
the message by taking its first word, which for a pair is not a path at all —
so a consumer had to parse "a ~ b (ρ=+0.994)" to learn which two parameters
were degenerate. Read d.where; never split the message.
for d in result.diagnostics:
if d.code == "HIGH_CORRELATION":
a, b = d.where # the degenerate pair, as dot-paths
And ask the package what it can do rather than assuming: rietx.capabilities()
returns the live registries — backends (with whether each optional dependency is
importable on this machine), solvers, plan presets with their when_to_use
text, modes, anodes, the pattern formats read_pattern opens, and the six
versioned contracts (schema_version, report_thresholds_version,
event_schema_version, project_format_version, textdoc_format_version,
indexing_thresholds_version). Its features map is derived
from the tree, so features["indexing"] tells you whether this build has an
indexer instead of leaving you to try one.
Two of those flags are about speed rather than capability, and they are the
ones to read before reporting that a refinement is slow.
features["compiled_kernels"] says whether the compiled peak kernels can be
built here (numba is a required dependency, but an install may legitimately
omit it), and features["compiled_kernels_active"] says whether the next
refinement will use them — RIETX_COMPILED=0 in the environment switches them
off without a reinstall. Both false on a slow fit is an explanation; both true
is not, and the answer is somewhere in the stage plan. Nothing else changes:
the numbers agree to one or two units in the last place either way, and the
accumulation is bit-for-bit identical.
7f. Two consumers, one answer: the gate and the evidence (WP-1043)¶
The gate exists for unattended use: a machine that cannot weigh evidence
must never be handed one cell confidently, so best_or_none() stays as strict
as it is and nothing in this section loosens it. But the gate’s three levels
compress the judgement’s inputs, and a consumer that can reason — an LLM in
a tool loop, or a human at a screen — wants the inputs, not only the verdict.
The design call this section serves: give them all the information, and let
the judge, human or machine, be the judge.
What a reasoning consumer reads: result.evidence(), the companion to dump
beside the answer. Per candidate: every caveat with its
refuting/capping kind (the split confidence_caveats alone withholds);
the panel members that ranked, with values, beside fom_undefined — the
figures that could not be computed, each absent with its reason, never
silently zero; and the whole-profile numbers together — lebail_rwp and both
detector counts, surfaced so a reader can see when a detector has failed (as
it measurably does on magnetite’s rival, whose fit buys a negative background
— §7c’s row), and never a thing to score on. Result-wide: what the search
covered (systems_searched + search_complete) against what the list
supports (systems_supported). The visual check is part of the answer, not
documentation of it: rietx.viz.plot_indexing(result, peaks, data=..., instrument=...) draws the ranked tick rows and the Le Bail panel from the
result alone.
Worked example — fluorite, why the two reads differ. Seventeen usable
lines on certified CaF₂ (Fm-3m, a = 5.4631 Å). The unattended read:
best_or_none() is None — correct, and exactly as strict as ever, because
the fom_panel_reduced caveat (capping) holds every below-twenty-line
candidate at medium. The reasoning read: the certified cell at rank 1 at
−18 ppm, found by every engine that ran, Le Bail-validated converged, four
systems searched to completion, and the only caveat on it is the reduced
panel — M₂₀/F₂₀ absent for cause, the coverage and reversed members all
ranked. A consumer that can weigh that is entitled to adopt the cell with its
eyes open; before WP-1043 the same list was refused outright — the old gate
conflated scoring (twenty lines, where M₂₀/F₂₀ are defined) with searching
(MIN_LINES_PER_DOF per system: seventeen lines are seventeen-fold
over-determined for a cubic metric), and neither consumer got anything.
Worked example — bethanechol set F, the truth already at rank 1. On the
one externally graded benchmark, the published P2₁/n cell comes back ranked
first on set F (measured 2026-08-06: trial_error under the paper’s own
manual-mode conditions, −340/+56/+67 ppm with β out by 0.012°) — and the sets
are bare positions, so there is no profile to validate against and a
single-engine find grades low: the gate can never promote it. The evidence
view is where that answer exists for a consumer — rank 1, its figures, and
caveats that say precisely what was not checked. An unattended pipeline
correctly gets nothing; a reasoner gets the answer with its qualifications
attached.
One qualifier on everything above, and on any scoreboard number you quote: nine of ten real-data corpus datasets sit at ≤ 2 free metric parameters (0 orthorhombic, 1 monoclinic, 0 triclinic), so every measured claim here is about high-symmetry lattices until the corpus moves — post-v1 by scope call.
Reference: 7g. The project and recipe readers: what an imported file does not carry¶
Load it when you read another program’s project file — a TOPAS .inp or a FullProf .pcr — or its recipe file — a PowderLine recipe — and a Diagnostic came with it.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
These are the codes of rietx.io.projects — one module per foreign format,
governed by WP-1118 — and of the PowderLine recipe reader. They are a different
channel from every other family in §7, but not because none of them ever
appears on result.diagnostics — that is equally true of
CIF_SPECIES_NORMALISED, CIF_CELL_ANGLE_CORRECTED and every PATTERN_* row,
all of which stay in §7. The criterion that does separate them is whose file
you are reading: another program’s project or recipe file — a TOPAS .inp,
a FullProf .pcr, a PowderLine recipe — against your own data and structure
files, which every fit reads whether or not another program was ever involved.
A project reader emits its codes at import, into a list you pass in
(read_topas_inp(..., diagnostics=[]),
rietx.io.projects.fullprof.to_structure(..., diagnostics=[])); the recipe
reader emits its own at the read (read_recipe, Recipe.diagnostics). If you
do not pass a list the finding for a project import is still on the model —
model.coverage.reported, model.skipped_blocks, species_raw — but nothing
is raised. So the question a project reader answers is not “is my fit wrong”
but “is the model I just imported the model that file describes”, asked
before a fit exists; the recipe reader answers a related but distinct question
of its own, below. GSAS and GSAS-II have a file of their own, §7h,
references/diagnostics-gsas.md, holding four file
kinds between them and the writers that put three of them back, under the same
criterion.
Each row states, as the others do, what you must not assume once it fires.
Two conventions run across the family. A reader repairs and reports where
the repair is a stated, recoverable loss and refuses where carrying on
would mean choosing between two readings of the file — so an _NORMALISED or
_DROPPED code is a repair you can undo, and a _REFUSED one is a build that
did not happen. And every construct a format can write is classified in
rietx.io.projects.coverage, one row per feature, so support for a construct
arrives by moving its row rather than by growing a reader.
code |
what you must not assume, and what to do |
|---|---|
|
(info — from the TOPAS |
|
(info — from the TOPAS |
|
(warning — from the TOPAS |
|
(warning — from the TOPAS |
|
(warning — from the TOPAS |
|
(warning — from the TOPAS |
|
(info — from the FullProf project reader: pass |
|
(info — from the FullProf project reader, same channel) Assume the built phase’s space group is the |
|
(warning — from the FullProf project reader, same channel) Assume the origin/axes choice above was verified. The check that verifies it compares each site’s |
|
(warning — from the FullProf project reader, same channel) Assume the built |
The RECIPE_* family is the reader of the PowderLine interchange format
(read_recipe, Recipe.diagnostics) rather than result.diagnostics, and it
answers a question the others do not: how does the fit I am about to run
differ from the one the recipe describes? A recipe is written for whichever
engine gets it, so some of what it declares has no counterpart here, and every
such difference is a row below rather than a silence. Anything the format
states that this package cannot represent at all raises RecipeError naming
the field — see the manual’s recipe chapter for the list and the reason behind
each.
Code |
What it means you must not do |
|---|---|
|
(warning) Compare this fit’s free-parameter count, esds or χ² against the reference engine’s as if the two refined the same problem. A parameter the recipe flagged for refinement has no counterpart here and its flag was not honoured — GSAS-II’s constant Lorentzian |
|
(info) Assume every field of the recipe reached the model. A fixed value sitting at its identity was dropped rather than translated: |
|
(info) Look for a mixing parameter in the result. GSAS-II carries one magnitude per broadening effect plus a Lorentzian share |
|
(info) Read a difference from the reference engine’s broadening as a disagreement about the data. The recipe left a size or strain magnitude null, which GSAS-II fills with its own project default (1 µm, 1000 × 10⁻⁶ Δd/d — measured off its committed output, not read from a manual) and this package reads as silence. On a synchrotron pattern the strain default alone is 0.057°·tanθ, a quarter of the peak width at the top of the range, so the instrument terms will differ by however much that was. Another engine’s project default is not physics; if you want it, state it in the recipe |
|
(info) Compare a background coefficient or a phase scale against the recipe’s own number, or against another engine’s. Neither is transferable: the two codes scale the Chebyshev domain differently, and a scale factor’s normalisation is each code’s own (on one committed specimen GSAS-II converges to 3.77e-2 and TOPAS to 2.61e-6). The term count and the refine flag are carried; the values are re-seeded, the scale by matching the summed calculated intensity to the data. Phase scale ratios are comparable and the absolute values are not — which is what a recipe quantifying phase fractions actually needs |
|
(warning) Quote the background-peak parameters, or expect the fit to converge. The recipe declares a peak whose FWHM reaches the fitted range, so it never falls to half height inside the window and carries no curvature the Chebyshev terms have not got — it will correlate with the low-order background at |ρ| = 1 and the stage will spend its budget walking that valley. Both reference engines confirm it from opposite ends on the one committed instance: one let the peak run to 8.77e10 °2θ at esd 0, the other kept it at 1.63° with an esd 188× its own value. A hump substitutes for polynomial terms; it never adds to them (§ |
|
(info) Treat the split as measured. GSAS-II’s |
|
(info) Read the stage list as a change to what the recipe asked to refine. PowderLine runs one pass over everything flagged; |
|
(info) Read it as an approximation you are choosing. The recipe’s wavelength is outside the bundled Cromer-Liberman table’s 3-70 keV band — a PDF-beamline λ = 0.1665 Å is 74.5 keV — so f = f₀ is used. Every edge of every element in such a recipe is more than an order of magnitude below that energy, so it is the correct limit rather than a concession; what would be wrong is extrapolating the table. The result still carries |
|
(info) Compare an Rwp against another engine’s before checking this count against theirs. A recipe states a |
|
(info) Assume the pattern’s channel count is the fitted count. A recipe excludes a point by giving it weight 0, which has no finite σ, so those channels became |
|
(info) Read a |
|
(warning) Trust a field whose meaning may have moved. The recipe declares a |
|
(warning) Read the answer as a refinement. No parameter in the recipe is flagged, so the plan has one stage that frees nothing — the model evaluated at its declared values, which is a simulation |
Reference: 7h. The GSAS and GSAS-II readers and writers: what their files do not carry¶
Load it when you read a GSAS .EXP or a GSAS-II .gpx project file, a GSAS .prm or a GSAS-II .instprm instrument-parameter file, or when you write one of those back or export a phase CIF for GSAS-II, and a Diagnostic came with it.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
These are the codes of rietx.io.projects.gsas and rietx.io.projects.gsas2,
together with the instrument-file and phase-CIF pairs rx.read_gsas_prm /
rx.write_gsas_prm, rx.read_gsas2_instprm / rx.write_gsas2_instprm and
rx.write_gsas2_phase_cif. They are §7g’s channel and not the engine’s: a
reader or a writer emits them into a list you pass in
(rx.read_gsas_exp(..., diagnostics=[]), rx.read_gsas2_gpx(..., diagnostics=[]), rx.read_gsas_prm(..., diagnostics=[])), and none of them
ever reaches result.diagnostics. §7g,
references/diagnostics-projects.md, states that
rule in full and holds the TOPAS and FullProf rows.
The two programs share a file because their row sets are parallel and a
migration reads them against each other. Five of the .EXP suffixes are also
.gpx suffixes, and two of the .prm ones are also .instprm ones, so the
row you want sits beside the row you are comparing it with. The writer rows
are here too. A .EXP, a .prm, an .instprm and a GSAS-II phase CIF are all
written back, this build writing no .gpx, and what a writer could not carry is
as much a difference from the other program’s answer as what a reader could not
read.
Each row states, as the others do, what you must not assume once it fires.
Two conventions run across the family. A reader repairs and reports where
the repair is a stated, recoverable loss and refuses where carrying on
would mean choosing between two readings of the file — so an _NORMALISED or
_DROPPED code is a repair you can undo, and a _REFUSED one is a build that
did not happen. And every construct a format can write is classified in
rietx.io.projects.coverage, one row per feature, so support for a construct
arrives by moving its row rather than by growing a reader.
code |
what you must not assume, and what to do |
|---|---|
|
(warning — from the GSAS |
|
(info — from the GSAS |
|
(warning — from the GSAS |
|
(info — from the GSAS |
|
(info — from the GSAS |
|
(info — from the GSAS |
|
(info — from the GSAS |
|
(warning — from the GSAS |
|
(warning — from the GSAS-II |
|
(info — from the GSAS-II |
|
(warning — from the GSAS-II |
|
(warning — from the GSAS-II |
|
(info — from the GSAS-II |
|
(info — from the GSAS-II |
|
(info — from the GSAS-II |
|
(info — from the GSAS-II |
|
(info — from |
|
(warning — from |
|
(warning — from |
|
(info — from |
|
(info — from |
|
(warning — same reader) Do not assume the geometry was read: this file stated no |
|
(warning — same reader) Assume every coefficient came from the file. The named items are absent from it and came back at this package’s defaults. GSAS-II writes every item of a bank and its own header says not to delete any, so a missing one means the file was hand-edited — check the values it does state before trusting the instrument |
|
(info — same reader) Assume the whole file crossed. The named items have no place in an |
|
(warning — from |
|
(warning — same writer) Assume GSAS-II will model the |
|
(warning — same writer) Assume an exported |
|
(info — from |
|
(warning — same writer) Assume a GSAS-II project built from this CIF starts where your refinement did. A phase CIF states no refine flags, no phase scale and no sample broadening, so that program starts from its own defaults for all three, and any Stephens strain, preferred orientation, extinction or restraints the phase carries are named here because a CIF has no tags for them |
Reference: 8. Twenty-four things that will surprise you, all measured¶
Load it when something the fit did makes no sense. Every entry is a measured result that contradicts an intuition.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
These are the findings from building the package that change how an agent should behave. Each one cost a debugging pass.
8.1 A correction that provably cannot improve Rwp can still be essential.
The capillary absorption factor factors exactly into a constant × exp(c·sin²θ)
— a Debye-Waller shape — so applying it to a model with a free scale and free
Biso leaves the residual unchanged to machine precision. Its entire content is
that a Biso refined without it comes back low by 0.49 Ų at µR = 1 (Cu Kα),
which is comparable to Biso itself and 19σ against its own esd. result.absorption
reports the bias because no fit statistic can. Corollary for the agent: never
judge a correction by Δ Rwp. Ask which physical quantity it unbiases.
Measured end to end on a real capillary pattern (11-BM, NIST SRM 660a LaB₆ in the documented 0.81 mm bore, µR = 0.674): Rwp moved 3 × 10⁻⁸, the lattice parameter 8 × 10⁻¹² Å, and both displacement parameters moved by +0.0166542 Ų against a predicted 0.0166542. That is what “exact” means here. It is not a general property of absorption corrections — see 8.12.
rietx compare is this rule made mechanical: a browser UI comparing refinement
settings side by side on the bundled standards, whose cumulative-Δχ² panel shows
where a correction acted rather than whether Rwp moved.
8.2 The opposite also happens: an improvement that passes every statistical test and is still rejected. On round-robin brucite, adding anisotropic strain improves Rwp 18.55 → 17.90 % with ΔBIC +488 — and drives σ²(M) negative on 12 of 43 reflections, so the coefficients are unphysical and unquotable. A statistical test cannot see a violated positivity cone.
8.3 Judge a correction at the reflections, not on the fitted grid. The IUCr round-robin patterns start at 5° 2θ but their first reflections are at 25–32°. A grid-based fence cheerfully reported a 27 % low-angle intensity depression that no modelled peak ever experienced. If you are reasoning about where a correction has leverage, the relevant coordinate is where the reflections are.
8.4 A multiplicative correction is trivially ~0.96 “scale-like”. Any
correction that rescales intensity projects almost entirely onto the scale
column, so a raw R² against a block containing the scale is a constant that
says nothing. Use the partial R² with the scale and background projected
out first — optimize.statistics.block_projection_r2(..., nuisance=...). With
that fix the statistic tracks real identifiability (R² 0.06 → 0.95 as the
low-angle reflections leave the fitted range); without it, it saturates and the
guard is blind.
8.5 Pairwise correlation is the wrong statistic for a block. With ~100
spline coefficients, each individual |ρ| against a structural parameter stays
around 0.2 while the block collectively absorbs ~46 % of it. Ask “can this
group, acting together, imitate that one?” — that is what
background_absorption measures.
8.6 Some parameters are dead at zero and some explode there. A
softplus-transformed coefficient (extinction, roughness strength) has
dp/du → 0 at the floor, so it will never move: it needs Stage.seed. A
Stephens block has Λ ∝ √Σ with unbounded slope at Σ = 0, so it needs
Stage.strain_seed to start on the isotropic ray instead. These are opposite
pathologies with opposite fixes; the plans already carry both.
8.7 A default is a decision, in either direction. Anisotropic ADPs,
Stephens strain, surface roughness and preferred orientation are opt-in per
atom/phase, because each needs a number about this specimen that the file
does not carry. Anomalous dispersion is the exception and is on by default
since v1.0: it needs only the species and the wavelength, both already in the
model, so declining it is the choice that has to be justified. Turning any of
them on or off changes every number downstream, including published acceptance
values — if you change one, re-measure; do not carry a comparison across the
change. Note the two knock-on effects WP-1001 measured when dispersion went to
the default: on a specimen sitting inside an absorption-edge interval the
lookup raises rather than degrading (that is deliberate — a selective
fallback would leave some species corrected and others not, manufacturing
exactly the unequal cross-phase bias the correction exists to remove; decline
the block or supply overrides), and light-atom ADPs come back less precise
even as they come back less biased (rutile U11/U33 separate at 1.9σ with the
block on against 2.2σ without, because f″ raises the heavy atom’s share).
8.8 Never transfer a literature constant without a numerical check across all its arguments. A published cylinder-absorption coefficient printed as “−0·0375” is really −0·3750; the error is invisible against a constant-θ slice of the paper’s own table (which constrains only the other two coefficients) and is 0.082 wrong at µR = 1. What caught it was a quadrature of the exact integral the fit approximates — which shares no constant with any published fit. The general rule: the strongest anchor is the integral a fit approximates, not another code’s transcription of the same fit. The same applies to Stephens S_HKL, where codes fold symmetry multiplicities into their templates and print values differing by small integer factors.
8.9 An absurd statistic is more often the linear algebra than the physics.
JᵀJ here is routinely conditioned at ~10²⁰ — that is what the degeneracy table
in §3 means numerically. Until 2026-07-28 the covariance was taken with
np.linalg.pinv’s general SVD path, which does not know the matrix is
symmetric and returned correlations as large as |ρ| ≈ 1.6 × 10³ (and +2.75 for
scale ~ axial_sl on a real fluorite fit). If a reported correlation, esd or
weight-fraction uncertainty is impossible rather than merely surprising, check
the conditioning before you invent a physical story for it — and remember that
the fix is to compute the covariance properly, not to clip the output, because
clipping 2.75 to 1.0 reports a degeneracy the arithmetic invented rather than
the one the data has.
8.10 Conventions are documented by physics, never by letter. GSAS and FullProf swap the X/Y size/strain labels. Most tabulations print the absorption correction A* = 1/A where this package multiplies by the transmission A ≤ 1 — and both equal 1 at µR = 0, so an identity test cannot tell them apart; only the direction of the θ-dependence can. The March coefficient r means opposite habits in reflection and transmission geometry. Read the docstring, not the symbol.
8.11 The anode is a physics choice, not a number to look up. All six
radiation= presets come from one column of one evaluation (NIST XRTE
SRD 128), and the shipped CuKa pair is bit-identical to it — that is what
makes the others trustworthy, so never substitute a value from elsewhere;
Bearden’s widely-quoted numbers are a different scale, 24–26 ppm away at Mo/Ag.
Three things then follow from which anode, all measured:
The Kα1/Kα2 gap grows from 20 eV at Cu to 173 eV at Ag, so the one-|F|²-per- source assumption gets weaker. A census over Z = 3–98 × six anodes refuses 7 of 576 combinations, and one is a real specimen: Ru at Ag Kα, K edge 22.14 keV, between the lines. The refusal is correct — split the lines into separate histograms or supply a measured override.
What
DISPERSION_NEGLECTEDis warning about is anode-dependent: hematite is awarningat Co Kα (180 eV under the Fe K edge, f′ = −3.3 e) and aninfoat Mo Ka (f′ = +0.3 e). Same specimen, same code, different severity.Contamination checks are per anode. An unrecognised wavelength (synchrotron, an untabulated anode) yields
contamination == []— not checked, not clean.background.identify_anode(λ)returnsNonethere and is how you tell the two apart.
8.12 “Infinitely thick” is a modelling claim you make by saying nothing.
Every flat-plate fit in this package — and by default in every Rietveld code —
assumes the specimen is thicker than the beam penetrates. That is exactly right
for a filled well and badly wrong for a thin layer on a zero-background holder,
which is how small, precious or air-sensitive samples are usually mounted. The
error is much larger than the capillary case and has the opposite sign:
ΔBiso = −1.5 Ų at µt = 0.2 over a Cu Kα range, because a thin specimen runs
out of material exactly where the beam penetrates deepest, and the missing
high-angle intensity reads as thermal motion.
Four consequences for an agent:
Declare
Geometry.mu_t(orthickness_mm, and let it be estimated) whenever the specimen is a thin mount. Silence means thick.The “off” value is µt = ∞, not 0 — the reverse of every other correction here.
mu_t = 0is a specimen of no thickness and is refused for reflection geometry rather than being taken as “no correction”. Underflat_plate_transmissionit is the other way round: silence means a transparent plate, and the sec θ footprint factor applies regardless, because it belongs to the tilt rather than to the absorption.The reported ΔBiso is a lower bound here, not the answer. For the capillary it is exact (seven decimals on real data); for a flat plate the bias a fit actually absorbs runs 1.06–1.5× larger, tracking
absorption.unabsorbed_fraction— which is on the record for exactly this reason. Quote it as “at least this much”, with the residue beside it.Unlike the capillary case this one does move Rwp, because it is not an exact reparameterisation (1–40 % of ln A survives a free scale and Biso). So 8.1’s rule inverts: here a worse Rwp after declaring a thickness is evidence the specimen was not that thin. Measured on round-robin fluorite — a thick back-packed mount — declaring µt = 0.5 takes Rwp 0.1793 → 0.1830 and drives one Biso onto its bound. That is the correction correctly refusing to fit a specimen that is not there.
8.13 A stage that takes minutes is telling you it is degenerate. Measured
on three weighed NaCl/Li₂CO₃ mixtures — identical models, identical parameter
counts, same-sized patterns — wall clock ran 39 s, 858 s and 2 838 s, a 73×
spread with no corresponding difference in the answer, and the pass reported
status="converged" either way. Until v1.1 the budget made that spread far
worse than it needed to be: max_nfev was max_iter × n_par, pricing a
finite-difference Jacobian the package does not build, so at 46 free parameters
a single max_iter=100 stage could spend 4 600 evaluations before giving
up. It is now max_iter × a small measured constant (the worst
evaluations-per-iteration ratio over 28 real stages is 3.2), so a stalling
stage says so roughly 30× sooner and a converging one is untouched — every
protocol measured stops an order of magnitude inside the cap. That changes when
you hear about it, not what you should do about it. The
stages that stall are the degenerate groups of §3, so the signal is available
before you run them: per-phase size/strain freed against a still-free
instrument U,V,W,X,Y (they model one width curve; the package’s own
lab_sample_refine only frees them against a frozen calibrated instrument),
or preferred orientation whose coefficient has reached a bound. Corollary:
treat elapsed time as a diagnostic. If a stage runs long, do not wait for it —
look at what you freed.
8.14 A bound that exists is not a bound that holds.
PreferredOrientation.r is declared min=0.0 with a softplus transform, the
idiom that is supposed to keep a parameter strictly positive. The softplus
pre-image runs to −∞, so r reaches exactly 0, and the March-Dollase factor
then evaluates (1 − c)/r and returns inf/NaN. Nothing raises: the residual
becomes garbage and the trust region grinds through its whole budget on it (a
3-second stage that had not returned after ten minutes). Bounding r to
0.15–6 fixed the stall and the fit, Rwp 30.8 % → 13.2 %. Corollary: for any
parameter whose model divides by it, set a real floor rather than trusting the
transform — and read RuntimeWarning: divide by zero as a fit-stopping error,
not noise.
8.15 A coverage score cannot tell a mixture from a low-symmetry single phase,
and this project already published that mistake and withdrew it. Measured on
third-party data with an engine restricted to two metric parameters: it indexed
47–60 % of the lines of single-phase orthorhombic and monoclinic patterns,
82–100 % of genuinely tetragonal or hexagonal ones, and 69 % of a real
two-phase mixture. The bands overlap. A “this pattern contains at least two
phases” claim built on the 69 % had to be retracted, because the same number is
what a single-phase pattern of symmetry the engine could not reach produces.
IndexingResult therefore carries systems_searched beside search_complete
and reports failure as “no cell found in the systems searched”; no diagnostic
code in the indexing vocabulary asserts a phase count at all, and a test pins
that. Corollary: partial coverage is a statement about your search, not about
the specimen. Widen the systems before you conclude anything about the sample.
8.16 An indexer’s tolerance is not its precision, and the gap is 11σ on real
data. The peak list carries a fitted σ(2θ) per line — median 0.0056° on the
bundled corundum pattern, whose cell is certified — and that σ is exactly right
for weighting and exactly wrong as a matching window: the same pattern’s
lines sit a median 0.060° from the certified positions, a cos θ specimen
displacement of −0.065°. At 3σ the true cell indexes zero lines and both
engines return nothing, on a pattern whose answer is known. That is why every
indexing program in the literature ships a global ~0.03° tolerance, and why the
engines here add an assumed 0.05° in quadrature and say so with
INDEX_SHIFT_ALLOWANCE. Corollary for the agent: a cell found under a widened
window has absorbed the shift (+1400 ppm measured), so re-fit it with
shift_template before quoting it — and the way to earn high confidence is to
supply a measured shift_allowance_deg — the shift’s amplitude, not the
residual scatter a template leaves — from an internal standard, not to widen
further.
8.17 “Is there intensity here?” is not one question — it depends on what else
your hypothesis predicts nearby. Two detectors in this package ask it with the
same window (±½ FWHM) and the same threshold (3σ), and they must use different
null models. WP-1024’s predicted_but_absent asks it against the fitted
background, which is right for an oversized cell’s phantom reflection because
a phantom sits in a gap. WP-1025’s extinction screen asks it at a forbidden
position, which sits inside a dense predicted pattern — and measured on the FAP
lab pattern, the 003 that P 6₃/m forbids is 0.89 FWHM from an allowed neighbour
ten times stronger whose tail fills the window to +27.6 σ. Against the class’s
own y_calc — background plus every reflection the class still allows — the same
window reads −3.9 σ. Corollary for the agent: never compute your own
“nothing is here” test from the raw pattern minus a background. Where nothing
else is predicted nearby the two agree; where something is, the raw test refutes
the true answer.
8.18 A position correction belongs to a geometry, and the suggestion you get
now says which. cos θ is the flat-plate specimen-displacement shape, and
instrument.geometry.sample_displacement is force-fixed on anything that is not
bragg_brentano. A capillary off the centre of the 2θ circle has its own pair
(McCusker eq 4): instrument.geometry.capillary_offset_along_beam carries sin 2θ
and …_across_beam carries cos 2θ, they exist only on debye_scherrer, and both
need goniometer_radius_mm, which eq (4) divides by — a value or a vary without
one is refused by name rather than defaulted. Free them for a laboratory capillary
or Guinier camera; at a synchrotron with a crystal analyser the paper says the
displacement error is eliminated, and measured on 11-BM NAC the fit agrees, so
freeing them there measures nothing.
The report’s position templates and actions are now chosen by geometry, so the
two refine_sample_* actions no longer reach a capillary fit at all (before
WP-1073 they did, naming parameters that could not be freed). On
flat_plate_transmission they do not reach either (WP-1003): that geometry
models neither aberration, so a cos_theta or sin_2theta trend there is
reported as a shape with no action — read it as “a flat specimen off the
axis”, evidence with no legal one-click repair. And this is a
correction whose cause the endpoint hides: measured on a synthetic capillary
with a 0.30/−0.20 mm offset, refusing the pair puts −290 ppm into a, and the
converged report names no position cause, because the zero shift and the cell
between them imitate most of eq (4). The zero stage’s own rung names
refine_capillary_offset_along_beam at 0.66. Corollary for the agent: this is
§9’s rule with a concrete case — read the trajectory, not the last state.
8.19 A restraint’s weight is a per-stage decision, and a fit can converge to
an impossible bond without saying so. Stage.restraint_weight_scale is c_w of
McCusker eq (7), S = S_y + c_w·S_G: high while the structure is incomplete or
approximate, reduced as it improves. It defaults to 1.0 (every restraint exactly
as declared), and 0.0 silences the restraints for a stage while keeping their
rows, so the row count the statistics exclude never changes mid-plan.
Measured on a synthetic case whose data under-determines two oxygen sites, starting from a Zr–O of 3.73 Å for a 1.87 Å bond: the same three stages run at c_w = 1 throughout converge with that distance at 4.834 Å, the restraint 148σ in tension and the coordinates 0.425 rms from truth; run at c_w = 300 then 1, the stiff stage lands the bond at 1.866 Å (0.03σ) and the relaxed stage converges at 1.872 Å, 0.00107 rms. The plans differ in nothing but c_w.
Corollary for the agent: this is a case where the fit statistics are the
weaker channel and you must read result.restraints. The failed fit’s Rwp is
0.0393 against 0.0327 and its GoF 1.23 against 1.02 — a slightly worse fit, not
an announcement of a 4.8 Å bond; RESTRAINT_TENSION is what fires. And a stiff
c_w makes a restraint more authoritative, not more correct: where the assumed
coordination is wrong, §8 of the paper says the refinement “will not progress
satisfactorily”, and raising c_w makes that worse rather than better.
RestraintReport.weight_scale records which value produced a report, so the
penalty actually minimised is weight_scale · restraint_chi2.
8.20 Intermediate stages are not converged, on purpose, and the last one is.
A staged plan stops every stage but the last at ftol = 1e-6 rather than the
solver’s 1e-9 (RefinementPlan.intermediate_ftol, default since 1.1). The
reason is 8.13’s mechanism seen from the other side: those long stages are
walking a near-degenerate direction at ≈0.93 per iteration, and 99.99 % of the
cost decrease is banked by evaluation 55 of 93 — the rest is digits the next
stage refines again anyway, because stages are cumulative and the last one
polishes everything at 1e-9. Measured over the three lab-shaped benchmark
cases: 1.51×, 1.62× and 1.55× fewer evaluations, every non-degenerate parameter
within 0.03 esd of the fully converged plan, QPA within 0.0014 wt %.
Corollary for the agent, in three parts. Do not read a small parameter
difference between a 1.0.x number and a 1.1 number as a physics change; check
StageResult.ftol first. Set intermediate_ftol = None when a number is going
into a paper, or when you are reproducing an earlier release, and say that you
did. And measure a series rather than assuming it: the same chained
ten-pattern comparison came out 1.04× worse on one tree and 1.12× better on
the next, one commit apart, because each pattern warm-starts from its
predecessor and a different seed changes how many recovery rungs the next one
needs (§9b). The per-fit bound above does not survive a chain in either
direction.
8.21 A model-selection test can bless a change that makes half your phases
stop being measurements. On a four-phase in-situ scan, the majority phase’s
lines were visibly mis-shaped and the cause was real: its control file modelled
hkl-dependent strain and the transcription had dropped it. Freeing the
equivalent Stephens block (six Laue-allowed coefficients under Pmn21) took Rwp
0.1092 → 0.0878 and χ² 8460 → 5465 for +4 net free parameters at 3887
channels — ΔBIC −2962, so the test §4 prescribes endorses it overwhelmingly —
and removed 79-82 % of the two worst regions’ χ². It also made
PHASE_UNCONSTRAINED fire on two of the four phases, which were supported
without it: six hkl-dependent coefficients on the majority phase are enough to
imitate the minority phases’ lines, and StageResult.held grew from five paths
to twelve. The better fit is the one where half the answers stopped being
measurements. Corollary for the agent: ΔBIC counts parameters, not whether
each phase still has support. After any Rwp gain from a new width or shape
model, re-read PHASE_UNCONSTRAINED and StageResult.held before quoting a
single scale, fraction or cell from the improved fit.
The same run refutes a tempting converse. That width error owned 35 % of the
fit’s χ², and correcting it moved the background level by ≤ 2 % in every
region. A phase width and the background can be degenerate — a phase broadened
past the instrument becomes a second background — but they are not
automatically coupled: a width wrong enough to dominate the residual can leave
the background where it was. So do not read a width finding as a background
finding, or the reverse, without measuring. BACKGROUND_ABSORPTION on the
majority phase’s scale was likewise unmoved, 0.442 before and 0.437 after, which
is the useful half: it measures the geometry of the problem, not the current
misfit, so it fires from a good start at the same value it fires at from a bad
one.
8.22 A tie carries its dependent’s bounds back onto its source, so a source
can stop somewhere it never declared. The least-squares box covers the free
column, and a tied parameter is not one — it is reconstructed from its source
after the solve — so a dependent’s own limits have to reach the solver through
the tie or not at all. They do: dependent = coefficient · source + offset
inverts to a range on the source, intersected over every dependent that source
drives and with whatever the source declares itself. Atom.biso is [0, 25] Ų,
so under ref.tie("phases.0.atoms.2.biso", "phases.0.atoms.0.biso", scale=2.0)
the master is given a ceiling of 12.5 whatever its own max says, and
BOUND_HIT names the master when a stage stops there. Measured: master 0.66 and
dependent 0.33 under a declared dependent ceiling of 0.33 at coefficient 0.5,
Rwp 0.0458 — a converged fit that stopped at a derived limit, not a failed one.
(Measured: WP-1119, four-site LaB6.)
What this means when you read a run. A BOUND_HIT on a source whose own
min/max are nowhere near the value is not a bug and not a bad bound — it is
one of its dependents’ ceilings arriving through the tie, and the fix is to widen
that parameter, not the one the diagnostic names. Check the coefficients before
touching anything.
One case it cannot close. A tie with several sources is a slanted boundary, and the optimiser can only be handed a range, so what it gets is the smallest range containing every allowed point — it never rules out an answer you asked for, and it can leave a corner where two sources conspire to put a dependent out of range. Landing there raises on write-back, after the solve, naming the parameter, its bounds and the tie: “writing phases.0.atoms.1.biso=40 back to the model breaks its own bounds [0, 25]; it follows 2·phases.0.atoms.0.biso”. Read that as a constraint to widen, never as a corrupt model or a bad CIF. (Measured: WP-1119.)
8.23 Declaring an intruding peak is not more accurate than excluding the
region — it is cheaper, and on some patterns that is the whole difference.
A sample holder line sitting on a sample peak can be handled two ways: declare a
PeakComponent so the channels stay in the fit, or put the region in
excluded_regions. On the NIST SRM 660c LaB6 protocol with two holder lines
injected onto reflections, both recover the clean-pattern cell — declaring to
−1.0 ppm, excluding to +0.6 ppm — while ignoring the intruder costs +7.6 ppm
and inflates the cell esd 7.5x. Excluding cost 256 of 5332 channels (4.8 %).
So do not tell a user that declaring is the more accurate choice; it is not,
on a pattern with plenty of reflections. What declaring buys is the retained
channels and a measured intruder (area and position with esds) instead of a
masked one. The balance tips toward declaring as the reflection count falls —
a low-symmetry phase, a narrow range, or a series whose trajectory needs every
reflection at every point — and that is the case to recommend it for.
(Measured: WP-1103, SRM 660c.)
And read EXTRA_PEAK_ON_REFLECTION before quoting anything that overlaps.
It fires whenever the declared component lands within 0.08° of a position the
model already predicts, which on the design case is expected — the intruder
really is on a sample peak. It is telling you the intensity split between phase
and component is a degeneracy, not a measurement: in the run above the recovered
component areas came back 1-2σ high because they had taken a little of the
reflection underneath. Quote the phase quantities and say the two overlap; never
quote the component’s share as if the fit had resolved it.
(Measured: WP-1103.)
8.24 An instrument profile does not carry a declared peak, even when the peak
really is the instrument’s. save_instrument_profile writes the widths, the
zero and the geometry and drops extra_components entirely — measured: an
Instrument with one PeakComponent saves and loads back with zero, while
profile.w survives unchanged. The stripping is deliberate for a
HumpComponent, which belongs to the specimen, but a holder or window line is
exactly the thing that does recur on the next sample, and it is dropped on the
same code path. So in the calibrate to save to load to sample workflow, re-declare
the component on the sample’s own instrument after load_instrument_profile;
nothing warns you. Check len(instrument.extra_components) after the load if
you are driving that workflow unattended.
(Measured: WP-1103.)
Reference: 9. The trajectory, and the history DAG as a search structure¶
Load it when one fit is not the answer: reading the trajectory rather than the last state, and using the history DAG as a search structure.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
Read the run, not just its last state¶
The first thing to do with a fit is read the report at every stage it passed
through, and that costs you one flag. task="refine" returns trajectory[]
when the request sets "report_trajectory": true — off by default since
WP-1003, because WP-1064 measured that rungs handed over unasked bought no
better decisions at more calls — and in python:
ref.fit(data, plan="mccusker_default", stage_reports=True)
for rung in ref.stage_reports_:
print(rung.stage, rung.rwp, [(a.kind, a.confidence) for a in rung.actions])
Each rung is the same three-layer report, projected (FitReport.for_stage) to
the numbers §4 judges a fit on, the summary sentence, and the active
suggestions — those the plan you ran will not fix, since the strategy veto is
applied against the whole plan. Two properties make it safe to rely on:
It changes no number. The rungs are read off states the plan already passes through; a fit run with the trajectory lands bit-for-bit where the same fit runs without it (measured: identical Rwp to full float precision on the synthetic fixtures, 0.140249 on 11-BM NAC). Nothing is refined to produce a rung.
It costs ≈2.5–2.8× the fit’s wall clock (1.06 s → 2.70 s on 59.5k channels of real 11-BM data; 0.30 s → 0.82 s on the 4200-channel synthetic LaB₆ fixture, re-measured 2026-08-19) and single-digit kB of payload: on that LaB₆ fixture, 0.6–0.8 kB a rung, 3.5 kB for the whole five-rung trajectory, ~3 % of the 111 kB report it ships beside. Quote that share with its fixture — the report’s size is dominated by its geometry table (89 kB of the 111), which no rung carries, so beside a geometry-light report the same trajectory reads as ~26 % (the
StageReportdocstring’s WP-1058-era episode-fixture measurement). The cost is flat per stage, not per iteration, so on a hard or diverging fit it disappears into the noise. Turn it off for a fit you are not going to read.
What to look for, in order:
A rung that names a cause the final report does not. That is the compensation signature of §5: the plan absorbed a real error. The named parameter is the hypothesis;
predict_then_verifybelow is how you test it rather than believing it.A confidence that climbs across rungs. On real 11-BM data with an unmodelled CaF₂ impurity,
add_impurity_phasereads 0.3 → 0.6 → 0.9 as the host phase fits: a hypothesis getting stronger as the model improves is about the specimen, not about the starting values.abstained_kindchanging.immatureearly is ordinary. Ending atresolution_limitedis a legitimate stopping point for a phase-ID deliverable (§4b) — not a licence to escalate corrections.n_actions_vetoed. These are the suggestions your own plan already answers; a rung whose actions are all vetoed is telling you the plan is already the right one.
The rungs deliberately carry no regions, curves or per-region attribution: a
rung is a pointer to a state worth asking about, and ref.report() (or the
report arm) is where you ask. There is no task="diagnose" and no
declared bootstrap ladder to invoke, because the states are already there —
every preset opens on a background+scale stage, which is that ladder’s first
rung (WP-1058). A hand-rolled one-stage plan is the one case with nothing to
report but its end: the turn-on order is what makes a trajectory informative.
The DAG: branch, verify, roll back¶
This is the part of the API that exists because the operator might be a search process rather than a person. Every stage auto-commits an immutable, restorable node (~10 kB — state, not curves), so branching is cheap and a rejected experiment leaves no trace in the working state.
The canonical agent loop:
ref = rx.Refinement(structure, instrument, history="session.jsonl")
ref.fit(data, plan="lab_bragg_brentano")
ref.history.tag(ref.history.head, "baseline")
# try a hypothesis on a branch — rollback is structural, not manual
rival = ref.branch("baseline")
rival.run_stage(data, rx.Stage("aniso_strain", ["phases.*.microstrain.dof.*"],
strain_seed=1000.0))
ref.history.compare([n.id for n in ref.history.leaves()])
ref.checkout(ref.history.best("rwp").id)
and the machine-checked version of “should I take this suggestion?”:
outcome = rx.report.predict_then_verify(ref, data, report.suggested_actions[0])
# runs the action on a branch, keeps it only if χ² actually improves by ≥1 %
print(outcome.accepted, outcome.reason, outcome.predicted_delta_chi2,
outcome.observed_delta_chi2)
Note expected_delta_chi2 on a suggested action is the linear model’s
prediction — an optimistic upper bound, not a promise. The gap between
predicted and observed is itself information: a large predicted improvement
that does not materialise means the linearisation was invalid there, which is
usually a peak far enough off that it should have been re-detected rather than
shifted.
This loop is executable, not aspirational: tests/test_report_loop.py runs it
closed — report → top surviving suggestion → verify → checkout/rollback →
re-report — from eight planted-cause starts and measures planted-parameter
recovery, stopping behaviour and rollback hygiene against the
mccusker_default preset (WP-1052).
The same shape answers the other question the report asks and does not settle — which of an exchangeable pair is physical (§4 step 6):
finding = next(e for e in report.identifiability.exchanges if e.exchangeable)
swap = rx.report.compare_rivals(ref, data, finding) # two branch fits
for r in swap.rivals: # [0] frees the held one, [1] the partner
print(r.freed_path, r.chi2, r.rwp, r.freed_value, r.freed_esd, r.n_free)
print(swap.chi2_ratio) # < 1 ⇒ the parameter the fit HELD wins
Three things about it, and each is deliberate. It runs each rival alone
with the other held at its null — never both together, which is the ridge
(§3), and never with the rival at its last fitted value, which is neither
rival. The free set is otherwise unchanged, so n_free matches across the two
and raw χ² is comparable without an information criterion. And there is no
decisive field: the package states the reading rule and never applies it,
the same fence predict_then_verify respects by reporting
observed_delta_chi2 beside its own threshold. The reading rule is §4 step
6’s band, orientation-neutral because chi2_ratio is directional: take the
winning rival’s side whichever index it is — the losing χ² over the winning
χ², i.e. max(ratio, 1/ratio) — and compare that against
RIVAL_DECISIVE_MIN_CHI2_RATIO. At or above it, the winner’s fit is the
answer, quoted without caveat (the 0.86 above is 1/0.86 = 1.17, decisive);
below it the pair has tied and the resolution is protocol. A pair with no
null (a cell edge, a scale) is refused by name — that one is resolved by
protocol, not by measurement.
Two properties worth relying on:
Node metrics are as-optimised, measured on a model frozen at the values each stage started from.
rx.replay(tree, node_id, data)recompiles at the values the stage ended on, so the two can differ marginally. That gap is a staleness signal, not a bug.Each node carries the API call that produced it, so a session doubles as a reproducible script, and
cherry_pickreplays another node’s stage action (not its values) on top of the current state.
Reference: 9b. Series: refine a ramp as a chain, and check it both ways¶
Load it for an in-situ ramp, a parametric sweep or a tray of related specimens.
A reference file of the rietx skill. The body it belongs to is SKILL.md; section numbers are the ones the body cites.
An in-situ ramp, a parametric sweep or a tray of related specimens is
rx.SequentialRefinement / rx.refine_sequential: N separate refinements,
each warm-started from its predecessor. (One joint residual over patterns
that share structural parameters is the different verb rx.refine_multi.)
What comes back is a SeriesResult — per-pattern summaries plus
trajectory(path), qpa_trajectory(phase), to_table(), write_csv().
series = rx.refine_sequential(patterns, structure, instrument,
x=temperatures, x_label="T (K)",
plan="lab_sample_refine")
a_of_T = series.trajectory("phases.0.cell.a") # x, value, stderr
to_table/write_csv take the derived paths too, resolved the same way
the plots are: paths=["qpa.Rutile"] exports the weight-fraction curve and
r_bragg.<phase>/r_f.<phase> the agreement indices, beside any ordinary
dot-path. Two things to expect. A path no pattern carries raises, naming
it and listing what does exist: the phases carrying that kind of curve for a
derived path, the series’ own parameter paths for an ordinary one. A typo is
therefore an error rather than a column of blanks. And an agreement index gets
no _esd column at all, because it is a residual and has no esd in any
series — where a column is present and a cell is empty, that pattern did not
estimate one, which is a different fact. (Measured: WP-1310.)
x is the series coordinate, and where it comes from is the file: a reader
puts a scan’s own temperature in data.metadata["temperature_k"], and
rx.io.readers.list_scans(path) reports the same number per scan before any of
them is read. Today the Bruker .raw v3 range header is the one format here
with such a field; the others record no specimen temperature and none is
guessed from an axis named for something else. A missing key is a file that
recorded nothing — refuse rather than substitute an ambient value, because
an invented coordinate makes the trajectory a fiction while every fit in it
stays perfectly good.
What an operator must know, all measured:
Chaining is worth ~3x in iterations, not in accuracy. On the eight round-robin sample-1 mixtures: 2863 iterations unchained, 904 chained, at identical Rwp and identical weight fractions. Use it to make a long series affordable, never to make an individual fit better.
What licenses a chain is physical continuity, and a tray has none — so of the three cases above, the tray is the one to think twice about. Chaining eight ex-situ YBaCo₄O₇ specimens that shared only a method created the disagreement it was then used to measure: refitting them as independent cold points shrank the referenced pattern’s Rwp gap by 13× and its cell-a gap by 12×, and what had been recorded as that campaign’s largest cell-a disagreement with TOPAS was the chain’s own artefact. Warm chaining is ~10x cheaper per pattern (0.16 s against 1.82 s measured on one series), and on specimens with no physical ordering that is the wrong saving. Inside a genuine ramp the same test is worth running step by step rather than once: on that series a different, larger jump survived having no chain, so that one was real — chaining artefacts are established or refuted transition by transition.
The default
refit="single"collapses the plan into one stage for every pattern after the first. The staged turn-on order exists to keep early stages conditioned from a poor starting model; a converged neighbour is not one. A pattern where that turns out to be wrong is caught by the reseed fence, which escalates one rung at a time — the full staged plan from the warm state, then the full staged plan cold — and keeps the best attempt.entry.rungsays which one produced the values andentry.rungs_triedsays what else was tried;entry.reseededstill means only the cold rung won, so a"warm_staged"point is one whose chain is unbroken.A pattern that no rung recovers is quarantined, not merely flagged (
SEQUENTIAL_UNRECOVERED): it seeds no successor and its Rwp is left out of the median that decides every later trigger. So a single failure cannot propagate down the chain or quietly raise the bar for the patterns after it — but it is still reported, and reading its parameters is on you.A sequential trajectory is path-dependent by construction, so a smooth curve is exactly what a poisoned chain produces.
direction="both"runs the series each way and reportsSEQUENTIAL_PATH_DEPENDENTper parameter. For any trajectory you intend to publish, run it — it is the only check that separates a measurement from an ordering artefact.But forward/backward agreement is not evidence of correctness, and the two failures it cannot see were both measured. A flat degenerate band reproduces the same wrong answer in both directions — |Δweight| median 0.000, max 0.030, 0 of 35 patterns disagreeing by more than 5 pp on a construction that was physically wrong — because the check compares two paths, not two basins. And where findings are present the set is not the signal:
SEQUENTIAL_PATH_DEPENDENTwas non-empty on all four series of one tranche, and the single chain actually sitting in a wrong basin was localised by σ magnitude (98.9σ and 84.4σ on the scales the QPA is built from, against ≤8.8σ everywhere else). Read what was flagged before reading physics into it: two of the largest flags measured anywhere — 81.5σ for a and b exchanging inFdddand 15.8σ for a and c inPnma— are degenerate axis relabellings, and the largest on another run (16.6σ) was a held cell reaching the pattern with two different held histories, which is bookkeeping rather than two measurements.SEQUENTIAL_RESEEDis not the net for a wrong basin. Its trigger is Rwp >reseed_factor× the running median of accepted patterns (default 1.25), so it needs roughly a 25 % relative Rwp jump — and a polymorph swap does not cost that. Tested directly on 20 such patterns it fired 0 times; where it can fire it works, 6 firings elsewhere in the same run all keeping the chain out of the disagreement set. A wrong basin at negligible Rwp cost is the case fordirection="both"and for cold refits, not for the reseed ladder.The
SEQUENTIAL_*codes live onSeriesResult.diagnostics, one level up from the entries. Readingentry.diagnosticsfor them returns zero on every series, which presents as a clean run rather than as a lookup in the wrong place — it cost one operator a full re-run of the chain stage for nine series, caught only by distrusting a suspiciously flat all-zero result. Per-entry diagnostics are real and carry every per-pattern occurrence; the rollups are not among them.An unrun check is indistinguishable from a passed one. A series whose forward pass completed all 125 patterns and whose backward pass crashed reports zero
SEQUENTIAL_PATH_DEPENDENTfindings — byte-for-byte what a clean series reports. Confirm both passes completed before reading an empty set as a pass, and report the empty set as a measured result rather than omitting it. Both safety checks are also worth pricing up front: on one series the backward pass was 43.9 % of wall clock and theverify_discontinuitiesrefits 15.1 %, so 59 % of the run bought assurance rather than answers — the right trade for a trajectory you will publish, the wrong one for a screen.A flagged step can check itself.
verify_discontinuities=Truerefits eachSEQUENTIAL_DISCONTINUITY’s two patterns cold and independently and writes the cold step over the chain’s step to the diagnostic’svalue, signed: near 1.0 the step is in the data, near 0 the chain made it, negative a cold pair that moved the other way. Off by default because a cold fit is the full staged plan from the initial models; measured on a 68-pattern ramp flagging four steps over four patterns it costs 5 % of the chain, and the cost scales with the patterns flagged rather than with the series length. Nothing else moves: the refits are separateRefinementruns writing to their own<label>.verifyhistories. An abstaining verification is a finding, not a pass: on a polymorph-swap exhibit the refit fired and then reported “an independent cold refit of both patterns does not determine this parameter, so the step could not be re-measured” — the check needs the parameter determined and the pathology is that it is not. Read that message asPHASE_UNCONSTRAINEDabout the step, and go to a cold refit sample rather than concluding the step was real.The check that covers both blind spots is a cold-refit sample, and it is affordable. Neither
SEQUENTIAL_RESEED(needs a ~25 % Rwp jump) nordirection="both"(agrees in a degenerate band) sees a wrong basin at negligible Rwp cost, and the audited job that did see it refitted 380 patterns cold against the chain and found 316 material disagreements, 29 of them at |ΔRwp| ≤ 0.5 pp — invisible to both. Whole job, warm chain plus 380 cold refits plus 244 minority-geometry fits plus 2 reference fits, 57 minutes single-threaded. Sample the chain cold; it is the only check here that catches a cheap wrong basin.One batch row applies to a chain and lives in §9c, which a series operator never loads: the shared
EventStreamopens its file in append mode, so a re-run of a ramp writes into the previous run’s log unless the path is rotated (references/batch-operating.md§ 9c.13) — measured on chains. The companion rule — a phase that switches fully in and fully out along the ramp is a degenerate split rather than a coexistence, and whether its fraction is an observable at all depends on the transition’s order — is this file’s own Is the transition first order? section, not a batch row.A trajectory of phase fractions is a QPA question at every point of it, and the background is what decides it. Fractions ride on scales, and an over-flexible background biases scales silently while improving every agreement index — which is why §4b’s QPA row reads
report.background.worst_absorptionbefore any statistic beside it. Nothing inSeriesResultrepeats that check for you, and noSEQUENTIAL_*code can: they compare each pattern against its neighbours, and a background too flexible for the specimen is wrong in the same direction at every point, so the trajectory it produces is smooth, self-consistent and false. Measured on a real 68-pattern reel: a 12-term cold fit put LT-ZrMo₂O₈ at 77.9 wt % when that phase is not present at all, Rwp 0.0821 against 0.0822 for the correct answer, with a difference curve that looks fine; over the round the absent phase took 40-96 wt % at an Rwp within 0.01 of the right one. Neither Rwp nor the plot separates them. So readbackground.worst_absorptionper pattern — at least on the first, the last and every flagged step — before quoting a fraction trajectory at all. The route is notSeriesResult, which carries summaries only, and notrx.refine_sequential, which discards the per-pattern results: use the class form, whoseresults_holds them.sr = rx.SequentialRefinement(structure, instrument),series = sr.fit(patterns, ...), thenrx.build_report(sr.results_[i]).background.worst_absorption.
Driving one: fit() is all-or-nothing, so wire on_result= before starting
anything long. The per-pattern loop catches RefinementCancelled and nothing
else and returns its SeriesResult only at the very end, so one exception
discards every pattern already fitted — measured as a LinAlgError: SVD did not converge at pattern 217 of 531 taking all 216 with it, and a deterministic
LinAlgError: Eigenvalues did not converge at index 78 destroying 103
already-fitted patterns whose values were fine, because the fit had converged
and only the error bars failed. Cell lengths carry no physical upper bound,
so a pattern reaching a = −347.644 Å raises out of the reflection enumerator
and takes the chain with it (3 of 9 chains on one tranche, one losing 20 of
24 patterns). The failure mode is reassuringly narrow — across 1846
fit_start/fit_end pairs the only exceptions raised anywhere were those
reflection-enumeration guards, no hangs, no NaNs, no silent wrong-shape
results — so a driver only has to survive that one. on_result=(index, result) fires as each pattern lands; persisting there is what saved the 103
above, and it is the single highest-value line in a series driver.
on_result fires on the forward pass only, and so does results_. With
direction="both" the backward chain is passed None for the callback, so
backward-pass patterns have event-log timings and no RefinementResult — 53
of them on one run. The backward SeriesResult is on .backward_ and on
series.backward; any per-pattern instrumentation you plan is half-populated by
design, which matters most for exactly the check above, since
background.worst_absorption is unreachable for the backward pass.
Turn history on explicitly — the two defaults disagree. Refinement’s
history defaults to True and SequentialRefinement’s to False, and
one harness silently lost the restorable per-pattern tree for 550 fits by
never passing it. Here the path becomes a directory of one JSONL per pattern
holding more files than patterns, because reseed retries write their own
(291 files for 275 patterns) — so file count is not a completion check.
When the deliverable is the trajectory, print its deciding rows:
series.summary(deliverable="series") — §4b’s fourth row, and the two
statements no diagnostic can make for you.
print(series.summary(deliverable="series"))
carry (dot-path globs) restricts what crosses a pattern boundary. Reach for
it when a parameter must provably not be chained; do not reach for it
because a parameter jumps. That hypothesis was tested on a series whose
composition swings 1 → 94 wt % and it is false: carrying everything is cheaper
there than excluding the scales.
prepare=(index, data, structure, instrument) is the other half of that, and
the case above is what forces it: excluding a parameter from carry only falls
back to the first pattern’s guess, which is not the same as re-estimating
it. prepare runs on the warmed models just before each fit, so a scale
that must be estimated from this pattern rather than carried or left at an
initial value has somewhere to be set. Two smaller hooks worth passing on any
long run: progress= (a stream or path) emits one line per stage boundary per
pattern and is the cheap way to know a run is alive, and labels= names the
entries — without it every downstream table is keyed by integer. One letter
away, label= names the whole chain in rietx watch’s list, and a sequence
passed to it raises TypeError rather than being recorded. A cancelled
series returns what completed, with SEQUENTIAL_CANCELLED. Note that
stage_reports=True does not exist here; it is a Refinement.fit() argument
and raises TypeError, and per-stage Rwp lives on the stage_end events of the
shared EventStream instead (present on 637 of 637 payloads in one run,
where StageResult carries no rwp field at all).
Where stage_reports=True does apply — the single Refinement.fit() calls of a batch — it
is expensive, and the name suggests only that it adds output. On the bundled NAC fixture it
cost 6.5×: 1.923 s against 0.296 s, median of three, with Rwp identical to six decimal
places, so it changes nothing about the refinement. cProfile puts 75 % of the fit inside
_stage_report, whose March-Dollase texture and Stephens strain analyses re-run at every
stage and each cost more than the optimiser step they describe; the solve itself is 23 %.
Sample it — the first fit, the last, anything you will plot — rather than setting it on every
fit of a long run.
That cost also sits outside the stage brackets, which makes one natural way of totalling a
run wrong. Σ(stage_start→stage_end) is 11 % of wall clock with stage reports on and 50 %
with them off, and the bracketed total is unchanged between the two (0.225 s against 0.223 s)
because it only ever covered the solve. The log is not missing the time —
fit_start→fit_end accounts for ~100 % either way — so take a fit’s cost from that span,
and never from a sum of per-stage durations.
Microstrain evolves along a chain — leave it free, or it lands in the fractions¶
Hold the instrument profile after calibrating it once, and let the per-phase width
terms refine per pattern. That is not a stylistic preference: in an in-situ reaction, an
electrochemical cycle or a temperature ramp, microstrain and domain size genuinely change
along the series, and a chain that holds sample broadening fixed has nowhere to put that
change. It goes into whatever is free — usually the scales, and therefore the weight
fractions, which is the one number such a run is normally for. Measured on an 11-BM
cryostat ramp, a phase’s lor_strain rose 0.019° → ~0.11° toward low temperature,
reproducibly and in both directions; a chain with that term pinned would have had to absorb
the same intensity redistribution somewhere else.
The mirror-image mistake is a numeric floor carried across instruments. A microstrain or
size floor is a hedge against the term collapsing to zero and capturing no broadening at all,
and it has to be re-derived from the peak widths of the data in front of you. A 0.02° floor
that behaved well on lab Cu Kα data was reused on 11-BM, where the instrumental width is one
to two orders of magnitude smaller; it pinned lor_size and lor_strain to the bound on
every pattern of a series and inflated Rwp 3–4× (0.546 with the floor, 0.148 without, same
pattern and seed), while BOUND_HIT fired on the width paths on essentially every fit. The
package’s own suggested remedy for the resulting misfit — free instrument.profile.u — did
not help (0.546 → 0.544). So: measure the FWHM of two or three strong isolated peaks off the
observed data, keep any floor well below the narrowest of them, and say which number you
used and why. A floor at or above the instrumental width is not a floor, it is the profile.
A symmetry test that has not been run against a null is not yet a test¶
A trajectory that ends in “the symmetry changes at T*” needs the claim checked against a compound, or a sub-series, where the symmetry is reported not to change — and the check has to be able to fail. The reason is specific and it defeated two candidate methods on one tranche: a lower-symmetry model carries freedom that a naive parameter count does not see — more resolved reflections, and more atoms once sites split — so a bare ΔBIC comparison tilts toward the lower symmetry regardless of the physics. Applied to a cubic control it “found” a distortion below 235 K in a compound reported cubic throughout.
What survived that control was a two-part criterion: a jump in Rwp(T) against its own baseline scatter, confirmed by the shape of the ΔBIC curve — a step followed by a new plateau, rather than a smooth decay. Write the threshold down before running it, report the null arm’s result next to the positive one, and if the null fires too, say that the detector measured flexibility rather than physics. A shuffled-coordinate run on the same patterns is the cheapest null available when no second compound is to hand: on one series it gave ~12× the β scatter and ~3× the Rwp scatter of the true ordering.
Is the transition first order? The order decides whether a phase fraction is an observable at all¶
Establish the order before reading any phase fraction across the transition. A first-order transition genuinely coexists, so a
parent-plus-distorted two-phase model is correct and its fraction is the
measurement: ZnCr₂O₄ and MgCr₂O₄ hold both low-temperature phases in
near-equal amounts down to base temperature, and Mn₃O₄ does the same below its
T_N — all explicit in the papers cited below, whose figures are the ones to
quote. A continuous transition never coexists, and a parent whose distortion
is undetectable never has a second phase at all — in either case the child’s
metric approaches the parent’s, the two become one phase, and the weight split
is unidentifiable by construction, which is exactly what
HIGH_CORRELATION on the two scales reports (ρ ≈ −0.98 on 87 of 182
patterns, all of them above T_N). Read the symptom correctly: a candidate that
is fully in (ΔBIC ≈ −10⁴) at the coldest scan, out (+9.5, weight exactly
0) at the next, back at a third and gone at a fourth is a profile-shaping
device switched on wherever the peak shape is imperfect, so ΔBIC across a
batch is not by itself a discriminator even at −8 700 to −41 300 — only a
distortion magnitude against a published value separated them, and it
agreed with the paper to ~1 %. The observable across a continuous transition
is the order parameter, never the fraction. (Measured: archive screening
campaign, 11-BM variable-temperature series — a compound published as showing
no detected structural transition, modelled as two phases; the first-order
coexistence figures are Kemei et al., J. Phys.: Condens. Matter 25, 326001
(2013) and Phys. Rev. B 90, 064418 (2014).)
Reference: 9c. Many fits as one job: deciding an answer across fits¶
Load it when a batch’s fits are in hand and the task is to decide what they
show together — ranking candidate models, differencing two populations,
auditing an aggregate, or judging whether an answer is trustworthy at all.
One refinement is a unit of a larger job here: candidate models screened
against one pattern, or many patterns fitted independently rather than
chained (§9b is the chain, whose fits share an order and a path).
references/batch-operating.md is the companion file
for running the batch itself — budgeting, cost, logging, inventory — rather
than reading its answer.
A reference file of the rietx skill. The body it belongs to is
SKILL.md; section numbers are the ones the body cites. Every
row carries its evidence: (Measured: …) names the run and its number,
(Hypothesis: …) names what would decide it. Where that run belongs to the
archive screening campaign, the tag names that corpus instead of a run a
reader can open — a screening campaign over a private diffraction archive of
laboratory, synchrotron and reactor-neutron patterns, refined against the
existing TOPAS, GSAS and XND answers for the same data wherever those survive.
The archive is unpublished and ships in no wheel, so those rows cannot be
reproduced from this repository; every number in them still comes from the log,
and no scientific magnitude of any specimen in it appears in any row.
A batch is N separate rx.Refinement runs, or N branches of one history tree
(references/history.md), each a whole §1-§10 job with its own
deliverable, stop rule and report. It is not rx.refine_sequential (§9b: each
fit is warm-started from its neighbour, so the set has an order and its
trajectory a path) and not rx.refine_multi (one joint residual over patterns
that share structural parameters). What the body settles for one fit is
settled here too and is not restated: the deliverable and its deciding rows
(§4b), the three stop conditions (§10), abstention (§6). This file holds what
a comparison, an aggregate or a trust judgement needs once the fits already
exist; running the batch that produced them is
references/batch-operating.md.
Writing a row¶
A row is a rule an operator of a batch needs and a single fit never does, with
the measurement that produced it. Ask first whether it is about deciding what
a batch’s fits show or about running the batch itself — the latter belongs in
batch-operating.md instead. From a run’s logs the form is: the rule as
one imperative sentence in bold, numbered 9c.N; what was measured — the
dataset or episode, N, the number that decides — in two to four sentences;
then the tag. (Measured: …) names the run. Where the run is in this
repository — a WP, an eval round, a dataset in tests/data/README.md — name
it so a reader can go and look. Where it is not, name the corpus the file
declares in its provenance line above, and name it the same way every time:
the package has to be tested on data it cannot ship, so a row measured on data
a reader cannot open is admitted, but it must be recognisable as one before
the reader acts on it. Two things the private case does not relax. Every
number still comes from the log, and anything the log does not decide is a
Hypothesis row: unverifiable is not a licence to be vaguer. And a scientific
magnitude derived from unpublished data stays out. The line is what the row is
about: what the run did may be quoted — counts, rates, wall clock, a ratio of
fit qualities, which is what the rows below are made of anyway — and what the
specimen is may not: a cell, a phase fraction, a domain size, a transition
temperature, and a ratio of two of those is still one of those. Quote the
shape, or a published figure, instead, which is where the lesson was anyway.
(Hypothesis: …) is for a
rule the logs suggest but do not decide, and names what would decide it. A
hypothesis row is not a weaker rule; it is an open question stated as one, and
§6 applies to it — do not act on it as if it were measured. Before adding a
row, ask whether it holds for one fit alone: if it does, it belongs in the
body or in §8, not here. All rows sit under The rows, one after another,
each closed by its tag; tests/test_skill.py refuses a row without one, and
CONTRIBUTING.md § The agent skill has the sync step.
The rows¶
9c.1 Declare each job’s deliverable and stop rule before the batch starts, and read the stop on the report, never on Rwp. §4b’s Stop when column is the stop rule per job and it differs by deliverable; a batch launched without one stops on whatever its author reached for — in the campaign’s six refining runs an external comparison, a script exit, an instruction, and for three of them nothing: they ended waiting. (Measured: WP-1307 round 1.1, R11 — three of four ramp cells stopped on a §4b deliverable row, against 0 of 6 in the 86-run campaign and 0 in round 1.0, when §4b had no such row to reach for.)
9c.2 A candidate the data cannot see is unseen, not refuted.
PHASE_UNCONSTRAINED says the phase was held for the stage because nothing of
it moved the residual (§6, rule 22): its scale at the floor is not a
measurement of 0 wt %, and its cell is the one you handed in. Screening
candidates, sort such a fit into “not testable on this pattern”, never into
the rejects — and never let the batch walk it: before the hold existed, one
absent phase cost 27 % of a 35-minute session, and its chain, reproduced on 13
sub-onset patterns, took 6.7 s with cell bounds and was killed unfinished at
13 minutes without them. (Measured: WP-1301, and WP-1307’s baseline ramp run
— the flat direction’s share of wall.)
9c.3 Rank candidates only on fits of the same channels, the same excluded
regions and the same background flexibility. §4 makes Rwp a relative number
between fits of the same data over the same channels, and §4 step 17 makes
R_B flatter whichever model partitioned the intensities, so neither ranks
across protocols. A batch that varies the background order or the excluded
regions per candidate has ranked protocols, not models: on one fit, the
over-flexible background won on every agreement index while being wrong, and
worst_absorption (0.46 against 0.08) was the only row that separated the
two. Compare rwp_background_subtracted pairs, the Le Bail gap ratio and ΔBIC
for nested models, each on an identical protocol. (Hypothesis: follows from
§4’s same-channels rule and WP-1055’s single-fit measurement; no batch has
measured the ranking itself.)
9c.4 A ranking is only as strong as the best null you actually reached, so fit the null as hard as the candidate. A protocol held constant across candidates still ranks them wrongly if the baseline was left underfitted: one symmetry adjudication’s cubic baseline was 1.1 pp of Rwp worse than achievable on the same data, and the richer candidate’s extra freedom absorbed that deficit and won on every global statistic. The guard is to localise the improvement where the physics predicts it rather than to total it — windowing the six largest-multiplicity reflection families that the claimed symmetry-breaking actually splits, 6.2 % of points captured −28 % of the total Δχ², i.e. net negative exactly where a real distortion must concentrate its advantage while the global numbers pointed the other way. The same asymmetry appears in plan hygiene: restoring two Gaussian terms silently dropped from a copied plan moved one weight fraction by 0.09 in absolute terms and Rwp by 0.04, so a candidate compared against a copied predecessor is being compared against nothing in particular. (Measured: archive screening campaign — the cubic-versus-rhombohedral NiO adjudication, whose ΔBIC −5553 for the richer model was real and reproducible while the interpretation was wrong, and its windowed Δχ²; the dropped Gaussian terms are from a low-temperature scan of a spinel series.)
9c.10 Compare populations conditioned on a diagnostic; do not regress a
number against whether it fired. Whether an unsupported phase costs or saves
time is a threshold, not a slope: PHASE_UNCONSTRAINED holds that phase’s
7–9 structural parameters (9c.2), which made fits slightly faster on one
batch and 13.8× slower on another, reconciled by whether holding it leaves
a flat direction behind for the solver to grind on. The methodological point
is the transferable one — on the batch where the effect is 13.8×, the
linear correlation against the flagged fraction is r = +0.073, which a
regression would have reported as “no relationship”. (Measured: archive
screening campaign rounds 2–3 — the (La,Sr)FeO₃ and 11-BM GeNi₂O₄ batches,
whose signs are opposite; the same trap applies to any guard finding whose
effect is conditional rather than graded.)
9c.18 Read a code that fires on every fit as a statement about the model,
and reduce the volume before reading it at all. Per-pattern diagnostics do
not scale and are not meant to: on 275 patterns BOUND_HIT fired 1289
times, 4.7 per pattern, and BACKGROUND_ABSORPTION 841. A code with no
per-pattern variation carries no per-pattern information —
PHASE_UNCONSTRAINED on a fifth phase fired 275/275 and a
species-fallback code 36/36 — and the finding is one level up: a single
BOUND_HIT is a pattern that hit a bound, while a BOUND_HIT in most of them
is a bound that is wrong, including one you imposed yourself (a
lor_strain floor the operator had set, flagged in 178 of 215 patterns).
Change the model or the plan; refitting reproduces it. Read which parameter
where names rather than merely that the code fired — two draft conclusions
in this campaign were wrong because a BOUND_HIT was attributed to the wrong
parameter. (Measured: archive screening campaign — the 275-pattern CuO/Cu₂O
reduction series for the volume and the rollup, and the 11-BM GeNi₂O₄ series
for the operator-set lor_strain floor at 178 of 215 patterns, 83 % of that
series.)
9c.19 Gate every summary statistic on determinacy, state the gate beside the
number, and report the screened n. Reference and fitted rows alike carry
undetermined parameters, and differencing them silently is how a batch
publishes noise: real rows read a cell edge whose esd was an order of
magnitude larger than the edge itself, a microstrain esd of 78011, and a
phase present at 6e-5 wt %. Screen by a minimum weight fraction and by the
untrustworthy-value codes (PHASE_UNCONSTRAINED, BOUND_HIT) before
differencing anything, then say so with the result — a median screened to
patterns above 1 wt % that did not declare its screen could not be reproduced
from the saved rows by a later audit, which is a defect in the record even
where the value is right. The screen is not cosmetic: one unscreened draft
median read 0.329 pp where the screened one was 0.931 pp over 142 rows.
(Measured: archive screening campaign, the CuO/Cu₂O tranche’s cross-code
agreement medians and their re-audit.)
9c.20 Sum each side’s weight percents over the phases the comparison shares
before differencing them. weight_percent normalises over the phases in
the model, so a model missing a real phase pushes every remaining fraction
toward 100 % — one five-against-six-phase comparison inflated four phases’
apparent disagreement 12–14× through exactly that, and it was first
written up as a separate “normalisation basis” defect before review reduced it
to the omitted phase itself: one mechanism, not two. The tell sits on the
reference side, since a rietx model’s own fractions always sum to 100:
exported columns summing well short of 100 % say the generating model
carried a phase the export does not. Difference like-for-like sums and print
the assertion rather than claiming it in prose. (Measured: archive screening
campaign — the CuO/Cu₂O five-against-six-phase comparison, and the audit that
reclassified my first account of it.)
9c.21 Count how many units actually carry a reference before quoting an agreement statistic. A median over data with no reference is a fabrication, and partial coverage is invisible in the aggregate: one folder had 1 paired point out of 8 fitted patterns, and that lone point sat at 0.572 pp — 3.4× above the ceiling the series had been credited with — while another had zero paired points and only an aggregate range. Where coverage is partial, a qualitative event both codes agree on, such as the unit at which an onset occurs, is more defensible than a Δ-statistic that silently assumes uniform coverage. Match a reference’s blocks by their reported statistics, too, rather than taking the tail of the file: its last converged block is not necessarily the published one. (Measured: archive screening campaign, the cross-code pairing audit.)
9c.22 Run a negative control through the identical machinery, and expect the
threshold you pre-registered to be wrong. A control is what fixes the
false-positive floor, and without one a threshold is a guess about the noise:
the control’s apparent distortion fixed a floor that the real claim sat 7.8×
above, and so survived — but the threshold written in advance sat below
that floor, so it was guaranteed to fire whatever the data said. Committing
to a threshold early is right; committing before the quantity’s noise floor is
known is a coin flip, and here a threshold a few times the measured floor
would have separated true from false. Relatedly, gate secondary criteria on
the candidate being competitive at all: one criterion rejected a model
everywhere and the next then read that rejected model’s c/a and found it ≠
1, which is a parameter being non-zero inside a model nothing supports. Pair
the control with a positive arm: a detector that has never fired on a true
case licenses nothing, and this floor was only interpretable because the same
machinery reproduced a published distortion magnitude, to ~1 %, on the one
case in the batch known to be real. (Measured: archive screening campaign,
11-BM variable-temperature series — the distortion claim, its negative
control, and the published positive it reproduced.)
9c.23 Hold the configuration constant before reading a trend along the batch
axis — then do not read the surviving trend as physics either. Where
configuration is confounded with the axis, a pooled trend mixes two
measurements, so restrict first: one series restricted to its 194
identically-configured scans still showed the effect cleanly — a monotone
fall of more than two orders of magnitude from the coldest scans to the
warmest. Note what that bought — it exonerated the configuration and left a
trend that was still an artefact, the negative control’s
temperature-driven peak broadening bought as apparent c/a by a tetragonal
model fitted to a broadened-but-unsplit reflection (9c.22’s floor). Surviving
a configuration hold is necessary and nowhere near sufficient. The same
reasoning retires a null result: freezing a covariate removes its dynamic
range, so “broadening does not predict cost” measured under a frozen
instrument is a claim about that protocol only. (Measured: archive screening
campaign — the 11-BM negative control’s configuration restriction, and the
frozen-width CuO/Cu₂O timing null.)
9c.25 Once a scale is at its bound, every local identifiability statistic
goes quiet — scan it instead. On one such phase background.absorption gave
0.183 and 0.208, below a well-behaved phase’s 0.318, the top five
correlations were unrelated pairs, and the phase did not appear in
soft_modes at all: three misses. A pin-and-scan profile likelihood saw
it at once — 17 pp of weight for +0.20/0.39/0.51 pp of Rwp, inside the
noise floor. Batch-wide counts corroborate rather than detect (BOUND_HIT on
that scale in 180 of 275 patterns, PHASE_UNCONSTRAINED in 274 of
275), so a scan is what turns a suspicion into a bound on the answer.
(Measured: archive screening campaign, CuO/Cu₂O reduction series — the
pin-and-scan control on the fifth phase.)
9c.26 Trustworthiness is a property of the unit, not of the protocol, so do
not credit a batch with its best fit’s identifiability. Identifiability
varies along a batch: a profile-likelihood scan showed three basins spanning
nearly the whole 0–100 wt % range inside a total Rwp span of ~0.01 pp at one
temperature and was clean and single-minimum at another with 58× more
signal, and at the bad point the reported weight_fraction_stderr
understated the admissible range by ~2 orders of magnitude — a covariance
esd describes local curvature and cannot see a second basin. Run any such scan
warm-started from one common state: the first attempt, run cold, gave
scattered minima at Rwp 3.5–6.1 and measured the optimiser rather than the
landscape. (Measured: archive screening campaign, the YBaCo₄O₇ gas-cell
series — the 300 °C pin-and-scan against its 200 °C control.)
9c.27 Multi-start agreement is evidence about the landscape only if the
starts were where you think. Three seeds converging on one basin proved
nothing on a fit whose seed conversion was wrong: both “biased” starts were
19–42× below the intended values, so all three began from effectively the
same tiny strain and the agreement measured the seeding bug. Verify each
seed’s physical value, not merely that the three differ. Where a term wants to
sit on a floor, seeding and flooring it away from zero is what makes the basin
reachable at all — a lor_strain lower bound of 0.02 kept the optimiser off
the bound in 14 of 15 patterns and moved that phase from a fraction the
reference contradicts to one agreeing with it within 0.5 pp, at an Rwp
0.10 pp better than baseline. (Measured: archive screening campaign — the
ZnCr₂O₄ size-strain arm, and the YBaCo₄O₇ strain-floor arm.)
9c.28 Freeze the model between rounds when the purpose is regression. Changing the model destroys the comparison that proves the machinery moved nothing: one round reproduced a predecessor’s 316-of-380 disagreement count to the pattern across all nine series, which is a strong pass on the sequential machinery and is only available because nothing else moved. Where the protocol has to deviate from the reference, report the deviation rather than fixing it mid-round. (Measured: archive screening campaign R3, the nine-series regression round.)
9c.29 Audit the aggregation before publishing it, and recompute every cell
from the machine-readable per-unit rows. A batch’s write-up is a second
dataset with its own error rate: one pass over a campaign’s own numbers
returned 61 confirmed, 11 disagreeing, 8 unverifiable and 2 unverifiable
without a refit, with the disagreements concentrated in how results were
written up rather than in the results themselves — recomputed p90 cells off
by 0.03–0.04 pp, a pilot’s prose total 1.9 % from its own results.json,
and a wt % quoted as 16.09 where the row says 16.10. Two of the eleven were
descriptions of upstream defects that had already been fixed at the commit
checked, so a reader would have concluded two open issues were still open —
which is the argument for auditing before publishing rather than after. The
audit’s own totals were not exempt from its own rule: its header says eleven
disagreements, its closing prose says twelve, and its per-row labels support
nine, so quote the cell you actually recounted rather than the summary above
it. Keep narrative estimates out of the same register as measurements, too: a
prose “~40 min” beside a log-derived “43.8 s” makes an unverifiable number
look measured, and one campaign’s own 68.5 min session figure was
unverifiable from its event logs, which spanned 25–62 min depending how
generously they were stretched. (Measured: archive screening campaign, the
full numeric re-audit of its own findings record.)
9c.32 Build the FitReport on the pilot fit and read report.background —
result.diagnostics does not carry the between-peak verdict, and
report.suggested_actions names the remedy even when Layer 1 abstains. Almost every
capillary or cryostat pattern holds a container amorphous halo, and a low-order
polynomial cuts straight through one while Rwp stays respectable because the
Bragg channels dominate the weighted residual. A 6-term Chebyshev with no
declared peak left the fitted background 35 % high below 3.5° and up to 2×
low across 4–7° at Rwp 0.157, and the report said so on three channels at
once — off_region_chi2_reduced 2.885 against its 1.5 threshold,
off_region_durbin_watson 0.393 against 1.0 over 7 408 off-region
channels, the summary naming the between-peak residual “systematic, not noise”,
and the halo’s own maximum listed first in report.unmatched at 7.6σ —
while result.diagnostics carried only BACKGROUND_ABSORPTION. The remedy was
on the report too, under a kind a driver can branch on:
report.suggested_actions held increase_background_flexibility active and
unvetoed even though Layer 1 had abstained as unreadable, because
background_actions runs on the abstain path by design — an over-stiff
background is a cause of an immature fit, so branch on
suggested_actions[].kind and do not read an abstention as “no advice”. Model
the feature with a HumpComponent or a BackgroundPSpline rather than more
polynomial terms, which hide it while improving every statistic. (Measured: archive screening campaign — the 11-BM VT Mn₃O₄ 8.281 K fit, whose report was
rebuilt and read only after the tranche was written up; the package’s own
manual measures the same Kapton halo at d = 4.74 Å and needs fourteen Chebyshev
terms to match one Gaussian’s three.)
9c.33 Never multiply a quoted esd by esd_inflation — the esds already carry
it. Statistics.esd_inflation is the Bérar-Lelann serial-correlation factor,
and the fit-side and report-side docstrings both say the reported esds have
already been multiplied by it; you divide it out to recover raw χ²·(JᵀJ)⁻¹
esds. It is conservative by construction — perfectly white residuals land at
≈1.51 — so it is an upper bound on serial-correlation damage rather than a
measurement, and a batch acceptance bar set near 2 fires on sound fits. Read a
large value as evidence about the model, i.e. unmodelled profile detail whose
residual is serially correlated, which report.background and report.regions
then localise — never as an uncertainty correction to apply. (Measured: archive
screening campaign — 8.45 on the 11-BM VT Mn₃O₄ 8.281 K fit, against the 2-4
band the docstring gives for lab data and the ≈1.51 white-residual expectation
the package verifies in its own tests; three independent readings of that number
in one session inverted its direction, one while quoting the docstring that
states it.)
Reference: 9c. Many fits as one job: operating the run¶
Load it when a batch is running or about to run and the question is how to
run it well — budgeting a job’s cost, timing stages, keeping the event log
honest, scoping the inventory, recovering from a per-unit failure. One
refinement is a unit of a larger job here: candidate models screened against
one pattern, or many patterns fitted independently rather than chained (§9b
is the chain, whose fits share an order and a path).
references/batch.md is the companion file for reading what the
fits, once run, show — ranking, differencing, trusting an answer.
A reference file of the rietx skill. The body it belongs to is
SKILL.md; section numbers are the ones the body cites. Every
row carries its evidence: (Measured: …) names the run and its number,
(Hypothesis: …) names what would decide it. Where that run belongs to the
archive screening campaign, the tag names that corpus instead of a run a
reader can open — a screening campaign over a private diffraction archive of
laboratory, synchrotron and reactor-neutron patterns, refined against the
existing TOPAS, GSAS and XND answers for the same data wherever those survive.
The archive is unpublished and ships in no wheel, so those rows cannot be
reproduced from this repository; every number in them still comes from the log,
and no scientific magnitude of any specimen in it appears in any row.
A batch is N separate rx.Refinement runs, or N branches of one history tree
(references/history.md), each a whole §1-§10 job with its own
deliverable, stop rule and report. It is not rx.refine_sequential (§9b: each
fit is warm-started from its neighbour, so the set has an order and its
trajectory a path) and not rx.refine_multi (one joint residual over patterns
that share structural parameters). What the body settles for one fit is
settled here too and is not restated: the deliverable and its deciding rows
(§4b), the three stop conditions (§10), abstention (§6). This file holds only
what running a batch as a set costs and requires — budget, timing, logging,
inventory, fault tolerance; reading what the fits show once they exist is
references/batch.md.
Writing a row¶
A row is a rule an operator of a batch needs and a single fit never does, with
the measurement that produced it. Ask first whether it is about running the
batch itself or about deciding what a batch’s fits show — the latter belongs
in batch.md instead. From a run’s logs the form is: the rule as
one imperative sentence in bold, numbered 9c.N; what was measured — the
dataset or episode, N, the number that decides — in two to four sentences;
then the tag. (Measured: …) names the run. Where the run is in this
repository — a WP, an eval round, a dataset in tests/data/README.md — name
it so a reader can go and look. Where it is not, name the corpus the file
declares in its provenance line above, and name it the same way every time:
the package has to be tested on data it cannot ship, so a row measured on data
a reader cannot open is admitted, but it must be recognisable as one before
the reader acts on it. Two things the private case does not relax. Every
number still comes from the log, and anything the log does not decide is a
Hypothesis row: unverifiable is not a licence to be vaguer. And a scientific
magnitude derived from unpublished data stays out. The line is what the row is
about: what the run did may be quoted — counts, rates, wall clock, a ratio of
fit qualities, which is what the rows below are made of anyway — and what the
specimen is may not: a cell, a phase fraction, a domain size, a transition
temperature, and a ratio of two of those is still one of those. Quote the
shape, or a published figure, instead, which is where the lesson was anyway.
(Hypothesis: …) is for a
rule the logs suggest but do not decide, and names what would decide it. A
hypothesis row is not a weaker rule; it is an open question stated as one, and
§6 applies to it — do not act on it as if it were measured. Before adding a
row, ask whether it holds for one fit alone: if it does, it belongs in the
body or in §8, not here. All rows sit under The rows, one after another,
each closed by its tag; tests/test_skill.py refuses a row without one, and
CONTRIBUTING.md § The agent skill has the sync step.
The rows¶
9c.5 Budget a job from what a converged job costs on this batch, and read a job past its budget as a diagnosis. §8.13: a stage that takes minutes is telling you it is degenerate. On a chain the rung budget is a factor times the dearest converged first rung, never a fixed wall (WP-1127); a job several times the median converged job is a flat direction or a degeneracy to read in its trajectory, not a fit to wait for. (Hypothesis: the mechanism is measured on a chain, WP-1127, and on the ramp run’s 27 %; the factor for independent jobs is not.)
9c.6 Cost tracks solver iteration count and almost nothing else, so predict a batch from iterations rather than from the model. Stratified by pattern size, cost against iteration count gave r = +0.859 and +0.988 within stratum on the two stratified batches with no sign flip, and +0.983 pooled on a third whose single frozen protocol needed no strata; peak broadening, minimum phase fraction and reflection count all flip sign between pooled and stratified, so each of them correlates with “which protocol is this” rather than with work done. Stratify by (protocol × geometry) before correlating anything against cost — pooling warm chain fits with cold ones, and 180-point windows with 4787-point ones, manufactured a reflection-count correlation of +0.398 pooled (n = 976) against a stratified median of +0.052 (12 strata, n = 961). (Measured: archive screening campaign R3 — three batches, the (La,Sr)FeO₃ chemical-looping series, YBaCo₄O₇ and the six-phase CuO/Cu₂O tranche; the pooled-vs-stratified reversal replicates, though its magnitude does not on a batch with only two pattern sizes.)
9c.7 Do not carry a per-iteration cost model from one batch to another.
The same fit machinery costs a different amount per iteration on different
data, so a regression fitted on one batch is not a package constant. One
batch’s s/iter = 4.4 µs × n_points + 2.9 ms (r = +0.972 there) predicted
8.61 ms/iter for a six-phase batch that measured 28.82 — 3.35× low —
and sat at 0.34–0.49× on a third. The missing term is model complexity:
six phases on the smallest patterns was the dearest per iteration of
anything measured, which is the opposite of what point count alone predicts.
(Measured: archive screening campaign R3 — the regression is the (La,Sr)FeO₃
series’, the 3.35× miss is the six-phase CuO/Cu₂O tranche and the 0.34–0.49×
is YBaCo₄O₇, i.e. two batches on opposite sides of the same line.)
9c.8 A stage that exhausts its iteration budget is not by itself waste —
pair max_nfev with the cost reduction it bought. Across three batches,
max_nfev-terminated stages took 46.6 % / 42.9 % / 23.5 % of stage time
at 72× / 53.6× / 18.0× the median converging stage; the shape replicates
and weakens each time, so the multiplier is not quotable but the sink is real.
Splitting them by what they achieved is what separates the two populations: on
one batch twelve exhausted warm_refit stages returned a 17 % median cost
reduction — real work needing more budget — while five exhausted refit_all
stages burned 30.8 s for 0.018 %, and the pooled median of 12.7 % hides
the split entirely. Report it per stage name, and do not key a guard on the
stage name: three batches put the waste in three different stages
(warm_refit/biso, then refit_all, then strain), so a name-keyed guard
misses two of three. (Measured: archive screening campaign R3 — three
batches; the waste signal is max_nfev and near-zero fractional cost
reduction.)
9c.9 Read terminations before blaming the iteration cap for a slow batch.
The quieter sink is the larger one: of 63 stages exceeding 5× the median on
one batch — 9.9 % of stages and 52.5 % of stage time — 43 terminated on
ftol and only 19 on max_nfev. Long converging stages cost more in
aggregate than exhausted ones, so raising the cap addresses the smaller half
of the problem. (Measured: archive screening campaign R3, the six-phase
CuO/Cu₂O tranche’s full stage census — the run whose 40 minutes had been
attributed to the iteration cap.)
9c.11 Sum fit_start→fit_end for a fit’s cost; never sum its stage
durations. Inter-stage bookkeeping belongs to no stage, so stage-level
totals are systematically low: fit-level agreed with an independent wall
clock to 0.62 % while stage-level undercounted by 19.9 % on one batch
and ~27 % on another. Anyone budgeting a batch from summed stage durations
is 20–27 % low before any other error. (Measured: archive screening campaign
R3 — YBaCo₄O₇ at −19.9 % and the CuO/Cu₂O tranche at ~27 %, both audited
against an external clock, fit-level agreeing to 0.62 %.)
9c.12 Pair timing events in order; never bucket them by pattern index. A
pattern index recurs — reseed retries, verification refits, restarted segments
— so max(fit_end) − min(fit_start) within a bucket spans everything that
happened in between, and nothing about the result looks wrong: one segmented
recovery chain reported 700.686 s of fit time against a 147.988 s wall
clock, 4.73×. Ordered fit_start→fit_end pairing is the fix, but it can
also overstate where fits are silently re-executed with no retry marker
(2.7× on one audit), so cross-check against a per-part table before quoting.
Assert event-log-summed fit time against an independently measured wall clock
on every unit of the batch — it is the only check that caught any of this.
(Measured: archive screening campaign R3, the (La,Sr)FeO₃ chemical-looping
series and its segmented-recovery audit.)
9c.13 Rotate or re-path the event log before every run. EventStream
opens its file in append mode unconditionally and writes no run marker, so
re-running a batch silently interleaves two runs into one file and every
derived duration is a mixture. It corrupted timings on two independent
batches, once reporting a 10.276 s calibration as 458.995 s — 44.67×.
Truncate or re-path before every write, and note concurrent load on the
machine as a confound to state rather than to absorb. (Measured: archive
screening campaign R3, the (La,Sr)FeO₃ series among two affected batches;
history/events.py opens with mode "a" on current main.)
9c.14 Pilot the model on a dozen patterns before multiplying it by N. The
dominant failure at scale is not a bad fit but a good fit to a wrong model,
N times, and the errors that do it are invisible in the residual: a
space-group setting resolved from a bare Hermann-Mauguin symbol (40 symbols
in gemmi’s own table carry more than one setting, and origin-2 coordinates
under an origin-1 setting converge at an ordinary Rwp with a whole reflection
class silently zeroed and no diagnostic firing), a unit conversion made
once in a shared calibration and therefore wrong in every pattern that uses
it, and a mis-parsed axis (a GSAS FXYE with its BANK line commented out
returned [50.0, 4399.6]° instead of [0.5, 43.996]° — same point count,
no exception, provenance None). Total cost is near-linear in length, so a
dozen patterns is a few per cent of a long run and tests all of it;
per-pattern cost is sub-linear in model size (1.489 s → 4.170 s, 2.80× for
a 5.35× increase in reflections), so the pilot’s per-pattern time is an
upper bound — time it as well as reading it (9c.31). Assert a sanity bound on
every parsed 2θ axis, check a transcribed phase’s cell mass against the source
file’s own (286.182 against 286.184, 7 ppm, settled an ambiguous Cu₂O
setting whose origin-1 alternative was a 1:2 Cu:O compound — though mass
balance cannot catch the origin error above, so the two checks complement
rather than substitute), and validate any reference-file reader against check
values you did not derive. (Measured: archive screening campaign — the Cu₂O
transcription against its own TOPAS .inp, the FXYE reader fallback on a
ZnCr₂O₄ pattern, and the CuO/Cu₂O tranche’s five-to-six-phase cost scaling and
12-pattern timing pilot.)
9c.15 Re-derive the batch’s inventory from the archive and de-duplicate by file identity. A batch scoped from a list someone wrote is scoped to that list: one brief’s own per-series counts summed to 638 rather than the 658 it stated, and of those 638 only 624 were diffraction patterns — the other 14 gas-monitor markers with no diffraction angles and furnace temperature logs — inside a directory set holding 1055 raw files in all; a whole-archive manifest elsewhere carried 1372 rows but 1366 distinct members, six byte-identical duplicates that would have double-weighted four temperatures. Assert the expected file count immediately after the selection glob, so a wrong pattern set fails loudly instead of fitting. (Measured: archive screening campaign — the (La,Sr)FeO₃ tranche’s 1055/638/624 count, and the whole-archive 11-BM manifest whose 1372 entries were 1366 distinct scans, of which the GeNi₂O₄ series contributed 215 fitted after the sentinel row.)
9c.16 Scope from one level above the data, and write the unrun remainder down. A batch scoped from the files it had already chosen looks complete against itself, and the gap is invisible from inside the scope: one campaign covered 3 of 10 series and left 771 patterns unrun while reading as finished. Two smaller instances from the same inventory — a series-looking name that was 2 patterns with no temperature recorded, and a folder whose reference row count disagreed with its pattern count because it held two scan geometries, to be filtered by filename rather than reconciled by arithmetic. Where an arm is time-boxed out, record it as undetermined rather than passed and name the exact excluded set, because an omitted arm in a summary table reads as a clean one. (Measured: archive screening campaign — the inventory rebuilt one directory above the fitted set.)
9c.17 Give every per-unit iteration its own try, not just the innermost
loop. A batch driver is judged by what one failure costs, and the guard
belongs at each level that can lose work independently: the same
reflection-enumeration guard that kills a fit fired inside a calibration
fit, and because that loop had no per-geometry try, all of that series’
minority geometries were lost — 8 of 9 series produced them and that one
produced none. Persist each unit’s result as it lands rather than at the
end. (Measured: archive screening campaign R3, the minority-geometry pass.)
9c.30 Expect model debugging, not the solve, to be the batch’s cost. In the
one batch where both halves were timed, “the batch is expensive” was a claim
about getting the model right: that pilot’s summed solve time was 13.2 min
(409.7 s + 382.0 s, confirmed against results.json) after 20–25 min of
setup before the first production fit, while a fully audited job of 380
chained patterns plus 380 cold refits plus 244 minority-geometry cold fits plus
2 reference fits ran in 57 minutes single-threaded. Speed was not the
constraint in either job measured, so budget the pilot and the inventory rather
than the cores — and pass an explicit long
timeout, since a production run will outlive a shell’s default and being
auto-backgrounded mid-run is indistinguishable from finishing. (Measured: archive screening campaign — the CuO/Cu₂O pilot’s audited solve sums, whose
68.5 min session wall figure is itself unverifiable from the event logs and is
9c.29’s own example, and R3’s audited nine-series (La,Sr)FeO₃ job at 57
minutes.)
9c.31 Pin the compute environment for the whole batch and assert it per
unit, not once at the start. Thread count and package version are covariates
of every timing and every regression claim you will make: a 12-pattern timing
pilot ran 5.92 s per pattern against the same tranche’s 4.17 s in the
previous round, a 42 % miss traced to single-threaded BLAS and caught
only because the pilot was timed before a chunk size was committed to. Version
drift is worse because it is silent — one machine’s three separately-frozen
baseline environments had all become the same release, which unreproduces
every version-difference claim measured against them without any of them
erroring. Set the *_NUM_THREADS variables inside the driver rather than in
the shell that launched it, and record result.provenance per unit so a later
reader can tell which build produced which number. (Measured: archive
screening campaign — the CuO/Cu₂O timing pilot’s BLAS miss, and the campaign’s
collapsed version baselines.)
9c.32 Name every run with label=, after the thing that fit is about.
Unnamed, a run takes the name of the working directory it ran in, so a batch
driven from one directory writes one name down every row of rietx watch’s
list, and the reader tells the runs apart by start time alone. label= is on
every verb that records a run (fit, run_stage, refine,
refine_sequential, Project.fit) and costs one keyword at the call. Pass the
candidate, the phase, the specimen: the fact that makes this fit the one you
will want to reopen. It is telemetry, so it changes no number the fit produces
and appears in no result. On a chain label= names the job while labels=
names the patterns, and the list shows the pattern. (Hypothesis: the limit it
removes is measured, WP-1424 having made the rows tell apart by start second
and reported that the record still names no run after its science. No campaign
has yet run with labels on, so whether a named list is read faster than a
timestamped one is undecided. What would decide it: a batch operator asked to
reopen a named candidate from the watcher list, against the same task on a
timestamped one.)
Reference: 9d. A human is watching: run directories, and the window onto a long fit¶
Load it when a person may be looking over your shoulder at a fit that takes a
while, when you want to hand one a live view rather than a promise to report
back, or when you need to read a finished run off disk. §9b is a chain of fits
and §9c a batch of them; this file is about the record every single fit leaves
behind. A batch inherits one directory per fit. A series does not: one
refine_sequential call is one job and records one directory for the whole
chain, however many patterns it walks.
A reference file of the rietx skill. The body it belongs to is
SKILL.md; section numbers are the ones the body cites. Every
row carries its evidence: (Measured: …) names the run and its number,
(Hypothesis: …) names what would decide it.
9d.1 You did not have to arrange any of this, and you cannot forget to.
Every fit, run_stage, refine and refine_sequential writes a run
directory to disk whether or not you passed events=. There is no flag to set
first and nothing to remember, so the correct action when a human asks to watch
is to point them at a command, never to restart the fit with telemetry on.
(Measured: WP-1403’s acceptance — a plain fit() in an empty directory leaves
one run the watcher lists, 204 kB over six files on the synthetic five-stage
LaB6 case: snapshot.json 171 kB, events.jsonl 30.8 kB over 87 events,
summary.txt 1425 B, meta.json 203 B, status.json 235 B, run.lock empty.
[dev] venv, macOS arm64.)
9d.2 Hand a human rietx watch, run from the directory you are working in.
It serves a run list on 127.0.0.1:8899, with --port to move it and --open
to open a browser. They pick a run and get the obs/calc/difference plot plus the
event console, both updating as you fit. A region they have zoomed into stays
zoomed while the stages go past, which is what makes it worth their attention
rather than a picture they have to re-find their place in after every stage.
Give them the command and the port, and say which run is yours if the directory
holds several. Name the run as you start it, with label= on the fit verb
(9c.32). Unnamed, its label is the working directory’s name, so in a shared tree
say the run id instead. The window has a stop button in it, and
9d.9 is what that does to you.
(Measured: WP-1402 checked the zoom in chromium — the axis range is identical before and after a forced redraw. The watcher’s own effect on your fit is nothing: it reads, and it constructs no project and no refinement.)
9d.3 What the run holds is the parameter trajectory, and it is not yours to
leak. events.jsonl carries every free parameter’s value at every recorded
evaluation, with the stage boundaries and their statistics. snapshot.json
carries the stage’s curves decimated for drawing. No pattern bytes are ever
copied into a run.
On a shared filesystem that trajectory is a disclosure nobody opted into. If the
work is confidential, set RIETX_TELEMETRY=0 before the first fit rather than
deleting directories afterwards.
(Measured: WP-1406, [dev] venv, macOS arm64 — an eval event carries
stage, n_eval, cost, accepted and values, the last being the free
vector itself; a stage_end carries rwp, status, termination,
n_iterations, held and released. The snapshot’s decimation budget is 4000
points, so a 4200-point pattern kept 4143 and a 22 003-point one is a picture of
itself.)
9d.4 Read a finished run back with the reader, and never by opening the
project. runs.discover(root) lists what is under a directory,
runs.read_run(path) reads one, runs.tail_events(path / "events.jsonl", offset) returns events from a byte offset with the next offset, and
runs.liveness_of(run) says whether it is still being written. summary.txt
in the run is the termination view as print(result) gives it, which is the
cheapest way to see how a fit ended without recomputing anything.
The reader constructs nothing on purpose. Project.open appends an annotation
to a project’s history before you have done anything, so opening a project to
look at a run would change the thing you came to look at.
(Measured: WP-1406, against rietx 1.4.0 — discover returned the
run, read_run its id, tail_events 87 events at offset 30782 with 0 bad
lines, liveness_of done with the evidence “the run recorded itself done”.)
9d.5 abandoned is a real answer and the one you act on. A run’s liveness
is running when a process holds its lock, done / failed / cancelled when
the writer recorded its own last word, abandoned when the status still says
running and the lock is free, and unknown when the question cannot be answered
here. abandoned is what a killed process leaves behind. Read it as “that fit
is over and nobody wrote an ending”, and do not wait for it to finish.
unknown is not evidence that nothing is happening. It covers a run written on
another host, a run with no state recorded, and every run written before the
recorder existed.
(Measured: WP-1401 built the ladder and tests/test_runs.py drives every rung
of it — a released lock under a running status reads abandoned, a foreign
host reads unknown, a status with no state reads unknown rather than
running, and a legacy run reads unknown and says so.)
9d.6 Switching recording off is one line, and the environment outranks you.
RIETX_TELEMETRY=0 covers a process and everything it starts, telemetry=False
covers one call, telemetry=<path> moves the root, and runs.set_enabled(False)
switches it inside a process. No value of telemetry= argues back against the
environment variable, so on a machine where an operator has set it you record
nothing and should not try to.
Do not switch it off to save time. The cost is small and measured, and a run you did not record is a run nobody can look at afterwards.
(Measured: WP-1403 — RIETX_TELEMETRY=0 and telemetry=False each left
nothing at all, asserted as an empty rglob. WP-1322 measured three of three
subagents switching telemetry off when it was opt-in, which is why it is on by
default now.)
9d.7 Recording costs a few per cent and changes no answer. The event log alone runs 1.01 to 1.03 times a bare fit’s wall clock. Adding the per-stage picture takes it to 1.03 to 1.23 times over three patterns of 22 003, 7251 and 4165 points. The charge is per stage and nearly constant, so a short fit pays the largest multiple and a long one barely notices. Disk is bounded by retention, which deletes by age and size rather than by count: nothing younger than a week goes, and above a 1 GiB ceiling the oldest finished runs go first. A root over the ceiling with nothing old enough warns and keeps everything, so a long batch never eats its own early runs.
(Measured: WP-1401, WP-1402, WP-1404 and WP-1413 on nac (22 003 points),
cpd-2 (7251) and trigger (4165), seven interleaved repeats, [dev] venv,
macOS arm64. Every configuration returned a bit-identical Rwp. The retention
scan runs once per process and cost 0.2 ms at 10 runs, 2.2 ms at 100 and 27.6 ms
at 1000.)
9d.8 A failure to record is not a failure to fit. If the recorder cannot
write, it stops, warns once for the process, and puts the reason in the run’s
status.json where it can still write one. Your fit carries on and its result is
unaffected. A RuntimeWarning about telemetry is therefore never a reason to
re-run anything.
A callback you passed through events= is the opposite case. It is yours, it
runs outside the recorder’s protection, and if it raises it takes the fit down.
That is deliberate: a monitoring hook that crashes the refinement is a bug you
want to see.
(Measured: WP-1403 — a read-only directory latches the recorder off with a
warning and the fit completes; a caller’s callback exception still propagates,
asserted in both directions in tests/test_telemetry.py.)
9d.9 The human watching can stop you, and it arrives as an exception. The
window you handed them has a stop button. Pressing it raises
RefinementCancelled in your process at the next residual evaluation, whether
or not you passed a cancel= of your own. Catch it. A RefinementCancelled you
did not ask for is not a bug in your call. Re-running the fit is the wrong
response to one.
What survives is stated on the exception. .completed_stages are the stages
that finished, .node_id is the history node the working state stands at, and
that id is a checkout target. The stage in flight is abandoned: no node, no
committed parameters, and the structure and instrument go back to where that
stage found them. A cancelled run has no summary.txt, because there is no
result to write one from.
You cannot tell a human’s stop from your own token.cancel() by looking at the
exception, and you are not meant to. The record can: status.json carries
cancelled_by when a request caused it, and nothing when your own code did.
In a series a stop ends the whole chain. The call returns what completed, with
SEQUENTIAL_CANCELLED among the diagnostics.
Report where you got to and hand back the node id. If a fit genuinely must be
uninterruptible, the honest answer is telemetry=False, which removes the run
directory and the window with it. rietx watch --read-only is a flag the human
sets on their own watcher.
(Measured: WP-1405 — a second process writes the request and the fit exits
0.117-0.126 s later, at stage 1 of 150, with empty stderr, while the control
that is never asked runs all 150. A 4-pattern series stopped at 0 completed
entries with SEQUENTIAL_CANCELLED, against 4 of 4 unasked. [dev] venv,
macOS arm64.)
Reference: The API index¶
Load it when you are about to call rietx and want the name, the signature or the constructor rather than a guess.
A reference file of the rietx skill. The body it belongs to is
SKILL.md. Generated by docs/skill/make_api_index.py from the
installed package — every signature, field and default below is rendered, not
typed, and a test fails when this file is older than the code. Do not edit it;
edit the generator’s selection.
There is one integration surface and it is the Python API. A caller runs a
verb, reads the typed answer, and dumps it with model_dump(mode="json") when a
file is wanted. A failure raises: there is no envelope and no error code.
Everything in rx. is import rietx as rx; rx.report and rx.viz are
submodules the package imports for you. inspect.signature(obj) or help(obj)
gives any call’s full docstring; A -> B is a return annotation; a
rx.Parameter(…) default names the parameter’s starting value and whether it
is free.
In¶
Readers and constructors. rx.read_pattern opens every format rx.capabilities() lists and takes the file’s esd column when it has one; a wavelength comes from the instrument preset or the file, never from memory (§1). A file extension does not name a format here: dispatch is on content, and a declined .raw names the six vendors who write one and picks none, so read the refusal rather than the suffix (Measured: WP-1407). Handed another program’s input file — a PowderLine GSASII_Rietveld recipe — read it with rx.read_recipe rather than parsing it yourself: it returns the model, the instrument and a plan together, and a unit it could not carry says so as a RECIPE_* diagnostic. rx.read_gsas_prm reads a GSAS-I .prm instrument-parameter file (the dominant one-bank, constant-wavelength case; a neutron time-of-flight file and every other GSAS profile function are refused by name). A Kα1/Kα2 doublet there comes back as two emission lines, the second weighted by the file’s own KRATIO, so do not add a Kα2 line yourself after reading one (Measured: WP-1118). rx.write_gsas_prm is its inverse, and the one writer here whose payload is not the refine flags: an instrument-parameter file is a calibration, so it goes out frozen. It refuses a non-zero zero_shift — ICONS’ ZERO has no established unit here and a guess is wrong by 100× — so zero it and let the receiving program refine it. rx.read_gsas2_instprm/rx.write_gsas2_instprm are that pair one program over, for GSAS-II’s text calibration: PXC and PNC banks, and a negative width coefficient refused rather than clamped, which 2 of the 4 constant-wavelength files in GSAS-II’s own tutorial corpus trip (Measured: WP-1118). Handed another program’s whole refinement — a TOPAS .inp, a FullProf .pcr, a GSAS .EXP — call rx.read_project_model, which dispatches on content and returns what the file stated plus a .to_structure() carrying the file’s own refine flags, the half nobody can rebuild from a CIF plus a pattern (Measured: WP-1118 — six agents handed a hand-transcribed series all named the transcription as the hardest part of the work). Read .format.reports_at to know which call takes your diagnostics= list: a .inp reports at read, a .pcr at to_structure, a .EXP at both. A GSAS .EXP carries the whole experiment rather than one phase, so read the wavelengths, excluded regions and refined profile terms off model.stated — a Structure cannot hold them. A blank field there is not a zero: ka2_ratio is None where the file states no Ka2/Ka1 ratio, and the 0.5 one field earlier is the polarization — both are conventionally 0.5 (Measured: WP-1118). A GSAS-II .gpx reads through the same door and carries two things the older formats cannot: the constraints, and the vary_list naming every variable the run refined. It is a pickle, so the reader resolves an allow-list and refuses any other global by name without reading the file (Measured: WP-1118). to_structure refuses a negative Uiso and a phase with no sites, both of which real projects contain. Two codes, two goodness-of-fit conventions: a .gpx’s gof is the square root of reduced χ² and a .EXP’s reduced_chi2 is not a root at all, so comparing your fit with either file’s figure means knowing which one it quoted (Measured: WP-1118). All four formats write back: rx.write_topas_inp, rx.write_fullprof_pcr and rx.write_gsas_exp are each format’s to_structure inverse, a file whose refine flags reproduce the Structure’s own vary exactly and whose space group is get_spacegroup(...).xhm(), never the phase’s stored spelling. FullProf’s grammar has no origin/axis suffix at all, so write_fullprof_pcr refuses a phase whose resolved setting a bare symbol cannot reach — most often origin choice 1 (Measured: WP-1118). A .EXP is the one written by column, so it reports what narrowed to fit a field and which merged refine flags it freed; pass diagnostics=[] for both (a Biso always narrows, the file storing Uiso). GSAS-II has no project file to write, so it takes the pair it imports: rx.write_gsas2_phase_cif for the phases and rx.write_gsas2_instprm for the machine. That CIF states the setting three ways, because GSAS-II reads a bare F d -3 m as origin choice 2 where gemmi reads choice 1 (Measured: WP-1118).
rx.read_pattern(path: str | Path, *, diagnostics: list[Diagnostic] | None = None, **options: Any) -> PatternData— Read any supported pattern file, dispatching on content first.rx.read_pdcif(path: str | Path, *, block: str | None = None, diagnostics: list[Diagnostic] | None = None) -> PatternData— Read a powder pattern from a pdCIF file.rx.read_recipe(source: str | Path | dict, *, diagnostics: list[Diagnostic] | None = None) -> Recipe— Read a PowderLineGSASII_Rietveldrecipe.rx.read_gsas_prm(path: str | Path, *, diagnostics: list[Diagnostic] | None = None) -> Instrument— Read a GSAS-I.prminstrument-parameter file as a frozenInstrument.rx.write_gsas_prm(instrument: Instrument, path: str | Path, *, header: str = '', diagnostics: list[Diagnostic] | None = None)— Writeinstrumenttopathas a GSAS-I.prm.rx.read_project_model(path: str | Path, *, diagnostics: list[Diagnostic] | None = None) -> ProjectModel— Read a refinement another program wrote, dispatching on content.rx.identify_project_format(path: str | Path) -> ProjectFormat— Which registered project format claimspath— the dispatch, once.rx.read_topas_inp(path: str | Path, *, diagnostics: list[Diagnostic] | None = None) -> TopasModel— Parse a.inp.rx.write_topas_inp(structure: Structure, path: str | Path)— Writestructuretopathas a TOPAS.inp.rx.read_fullprof_pcr(path: str | Path) -> FullProfModel— Parse a.pcr.rx.write_fullprof_pcr(structure: Structure, path: str | Path)— Writestructuretopathas a FullProf.pcr.rx.read_gsas_exp(path: str | Path, *, diagnostics: list[Diagnostic] | None = None) -> GsasModel— Read a GSAS-I.EXPexperiment file.rx.write_gsas_exp(structure: Structure, path: str | Path, *, title: str = '', diagnostics: list[Diagnostic] | None = None)— Writestructuretopathas a GSAS-I.EXP.rx.read_gsas2_gpx(path: str | Path, *, diagnostics: list[Diagnostic] | None = None) -> Gsas2Model— Read a GSAS-II.gpxproject file.rx.write_gsas2_phase_cif(structure, path: str | Path, *, diagnostics: list[Diagnostic] | None = None)— Writestructuretopathas the phase CIF GSAS-II imports.rx.read_gsas2_instprm(path: str | Path, *, bank: int | None = None, diagnostics: list[Diagnostic] | None = None) -> Instrument— Read a GSAS-II.instprmfile as a frozenInstrument.rx.write_gsas2_instprm(instrument: Instrument, path: str | Path, *, diagnostics: list[Diagnostic] | None = None)— Writeinstrumenttopathas a GSAS-II.instprm.rx.Structure.from_cif(path: str, *, phase_name: str | None = None, aniso: bool = False, diagnostics: list | None = None) -> Structurerx.Instrument.bragg_brentano(*, radiation: str = 'CuKa', goniometer_radius_mm: float = 217.5, monochromator_two_theta: float | None = None, ka2_ratio: float = 0.5, mu_t: float | None = None, thickness_mm: float | None = None) -> Instrument— Lab flat-plate diffractometer preset with a Kα1/Kα2 doublet.rx.Instrument.debye_scherrer(wavelength: float, *, polarization: float = 0.99, goniometer_radius_mm: float | None = None, capillary_radius_mm: float | None = None, packing_fraction: float = 0.6, mu_r: float | None = None) -> Instrument— Synchrotron/capillary preset with a single wavelength.rx.estimate_mu_r(structure: Structure, instrument: Instrument) -> float | None— Starting µR for a packed capillary, from composition and geometry.rx.auto_background(data: PatternData, *, kind: str = 'pspline', diagnostics: PatternDiagnostics | None = None, wavelength: float | None = None) -> Background— Build a background model sized to the pattern.rx.diagnose(data: PatternData, *, wavelength: float | None = None, baseline_lambda: float | None = None) -> PatternDiagnostics— ComputePatternDiagnosticsfor a raw pattern.rx.load_instrument_profile(path: str | Path) -> Instrument— Read a profile file back as a frozen instrument.rx.save_instrument_profile(instrument: Instrument, path: str | Path)— Write the instrument’s calibrated state to a JSON profile file.rx.capabilities() -> Capabilities— Everything this build can do — see the module docstring.rx.help_for(path: str) -> HelpEntry | None— The entry for a parameter dot-path, orNoneif no family claims it.
The model objects¶
Every refinable quantity is a rx.Parameter (value, vary, bounds), addressed by a dot-path such as phases.0.cell.a or instrument.profile.w; rx.help_for(path) says what any of them is. Each class below is its own constructor — the fields are the keyword arguments, a field with no default is required.
rx.Parameter— A single refinable scalar. Fields:value: float,vary: bool = False,min: float = -inf,max: float = inf,expr: str | None = None,transform: Literal['identity', 'softplus', 'exp', 'logit'] = 'identity',unit: str | None = None,stderr: float | None = Nonerx.Structure— The phases refined against the same pattern(s) — possibly none. Fields:phases: list[Phase]rx.Phase— A crystalline phase: symmetry, cell, atoms, scale, sample broadening. Fields:name: str,space_group: str,cell: Cell,atoms: list[Atom],scale: Parameter = Parameter(1.0, min=0.0),extinction: Parameter = Parameter(0.0, min=0.0),preferred_orientation: PreferredOrientation | None = None,lor_size: Parameter = Parameter(0.0, min=0.0),lor_strain: Parameter = Parameter(0.0, min=0.0),gauss_size: Parameter = Parameter(0.0, min=0.0),gauss_strain: Parameter = Parameter(0.0, min=0.0),microstrain: StephensStrain | None = None,particle_radius_um: float | None = None,restraints: list[BondRestraint | AngleRestraint | ValueRestraint] = []rx.Atom— One site in the asymmetric unit. Fields:label: str,species: str,x: Parameter,y: Parameter,z: Parameter,occ: Parameter = Parameter(1.0, min=0.0, max=1.5),biso: Parameter = Parameter(0.5, min=0.0, max=25.0),aniso: AnisoU | None = Nonerx.Cell— Unit-cell lengths (Å) and angles (degrees). Fields:a: Parameter,b: Parameter,c: Parameter,alpha: Parameter,beta: Parameter,gamma: Parameterrx.AnisoU— Anisotropic displacement tensor in the CIF U^ij convention (Ų). Fields:u11: Parameter,u22: Parameter,u33: Parameter,u12: Parameter = Parameter(0.0),u13: Parameter = Parameter(0.0),u23: Parameter = Parameter(0.0)rx.PreferredOrientation— Single-axis March-Dollase preferred-orientation correction (Dollase 1986). Fields:axis: tuple[int, int, int],r: Parameter = Parameter(1.0, min=0.15, max=6.0)rx.StephensStrain— Anisotropic strain broadening coefficients S_HKL (Stephens, 1999). Fields:s400: Parameter = Parameter(0.0),s310: Parameter = Parameter(0.0),s301: Parameter = Parameter(0.0),s220: Parameter = Parameter(0.0),s211: Parameter = Parameter(0.0),s202: Parameter = Parameter(0.0),s130: Parameter = Parameter(0.0),s121: Parameter = Parameter(0.0),s112: Parameter = Parameter(0.0),s103: Parameter = Parameter(0.0),s040: Parameter = Parameter(0.0),s031: Parameter = Parameter(0.0),s022: Parameter = Parameter(0.0),s013: Parameter = Parameter(0.0),s004: Parameter = Parameter(0.0)rx.Instrument— Everything about the measurement except the sample. Fields:source: Source | NeutronSource,geometry: Geometry = Geometry(…),zero_shift: Parameter = Parameter(0.0, min=-0.5, max=0.5),profile: ProfileTCHZ = ProfileTCHZ(…),background: BackgroundChebyshev | BackgroundFixedPlusChebyshev | BackgroundPSpline = BackgroundChebyshev(…),extra_components: list[HumpComponent | PeakComponent] = []rx.Source— Constant-wavelength X-ray source. Fields:kind: Literal['xray_cw'] = 'xray_cw',lines: list[EmissionLine],polarization: Parameter = Parameter(0.5, min=0.0, max=1.0),dispersion: Dispersion | None = Dispersion(…),harmonics: list[Harmonic] = []rx.NeutronSource— Constant-wavelength neutron source: one wavelength, nuclear scattering. Fields:kind: Literal['neutron_cw'] = 'neutron_cw',wavelength: Parameter,harmonics: list[Harmonic] = []rx.Geometry— Diffraction geometry. Fields:kind: Literal['debye_scherrer', 'bragg_brentano', 'flat_plate_transmission'] = 'debye_scherrer',goniometer_radius_mm: float | None = None,surface_roughness: RoughnessSuortti | RoughnessPitschke | None = None,sample_displacement: Parameter = Parameter(0.0, min=-1.0, max=1.0),sample_transparency: Parameter = Parameter(0.0, min=0.0, max=0.05),capillary_offset_along_beam: Parameter = Parameter(0.0, min=-1.0, max=1.0),capillary_offset_across_beam: Parameter = Parameter(0.0, min=-1.0, max=1.0),axial_sl: Parameter = Parameter(0.0, min=0.0, max=0.2),axial_hl: Parameter = Parameter(0.0, min=0.0, max=0.2),mu_r: float | None = None,capillary_radius_mm: float | None = None,mu_t: float | None = None,thickness_mm: float | None = None,packing_fraction: float = 0.6rx.ProfileTCHZ— Thompson-Cox-Hastings pseudo-Voigt width parameters. Fields:shape: Literal['tchz_pv', 'voigt'] = 'tchz_pv',u: Parameter = Parameter(0.0, min=-0.05, max=1.0),v: Parameter = Parameter(0.0, min=-0.5, max=0.5),w: Parameter = Parameter(0.001, min=0.0, max=1.0),x: Parameter = Parameter(0.001, min=0.0, max=1.0),y: Parameter = Parameter(0.0, min=0.0, max=1.0)rx.BackgroundChebyshev— Shifted-Chebyshev background, linear in its coefficients. Fields:kind: Literal['chebyshev'] = 'chebyshev',coefficients: list[Parameter] = [Parameter(0.0) × 4]rx.BackgroundPSpline— Penalized cubic P-spline background, co-refined with the structure. Fields:kind: Literal['pspline'] = 'pspline',breakpoints: list[float],coefficients: list[Parameter],lambda_smooth: float = 1.0,air_scatter: Parameter = Parameter(0.0, min=0.0)rx.BackgroundFixedPlusChebyshev— A fixed estimated curve (never subtracted; held additively) plus a small refinable Chebyshev correction on top. Fields:kind: Literal['fixed_plus_chebyshev'] = 'fixed_plus_chebyshev',fixed_two_theta: list[float],fixed_intensity: list[float],chebyshev: BackgroundChebyshev = BackgroundChebyshev(…)rx.Dispersion— Anomalous scattering corrections f′, f″ at the source wavelengths. Fields:table: Literal['cromer_liberman'] = 'cromer_liberman',overrides: dict[str, tuple[float, float]] = {}rx.PatternData— A 1-D constant-wavelength powder pattern. Fields:two_theta: list[float],intensity: list[float],sigma: list[float] | None = None,excluded_regions: list[tuple[float, float]] = [],metadata: dict[str, str] = {}
Refining¶
rx.Refinement is the stateful entry point and rx.refine the one-shot function form. Plans are named in rx.PLAN_INFO or built as a rx.RefinementPlan of rx.Stages; ref.parameters() lists every entry, fixed, locked and tied included, and the editing verbs auto-commit a history node each.
rx.Refinement(structure: Structure, instrument: Instrument, *, backend: str = 'numpy', solver: str = 'trf', history: bool | str | Path | RefinementTree = True)— Refinestructure+instrumentagainst a powder pattern.rx.Refinement.fit(data: PatternData, *, mode: Mode = 'rietveld', plan: RefinementPlan | str = 'mccusker_default', two_theta_limits: tuple[float, float] | None = None, events=None, cancel=None, telemetry=None, label: str | None = None, stage_reports: bool = False, progress=None) -> RefinementResult— Run a staged refinement.rx.Refinement.report(*, plan: RefinementPlan | str | None = None, **kw)— The fullrietx.report.FitReportfor the last fit.rx.Refinement.summary(*, deliverable: str | None = None, plot: str | None = None, plan: RefinementPlan | str | None = None, report: FitReport | None = None) -> str— The termination view (WP-1302): “done, or not, and why” from one call.rx.Refinement.suggest(data: PatternData, *, top_n: int = 5, include: str | list[str] = '*', exclude: list[str] | tuple[str, ...] = (), mode: Mode | None = None, two_theta_limits: tuple[float, float] | None = None, report=None) -> SuggestionResult— Which held parameter should be freed next?rx.Refinement.predict(two_theta=None) -> np.ndarray— y_calc at the current parameters — the evaluate-only path.rx.Refinement.run_stage(data: PatternData, stage: Stage, *, mode: Mode | None = None, two_theta_limits: tuple[float, float] | None = None, correlation_guard: float = 0.98, events=None, cancel=None, telemetry=None, label: str | None = None) -> RefinementResult— Run a single stage from the current state, recording a child node.rx.Refinement.parameters(*, mode: Mode | None = None) -> list[ParameterRow]— Every parameter as data — fixed, locked and tied rows included.rx.Refinement.set_vary(path_globs: list[str] | str, vary: bool = True) -> list[str]— Free (or hold) every parameter matchingpath_globs.rx.Refinement.set_values(values: dict[str, float])— Set parameter values by dot-path, recording aset_valuenode.rx.Refinement.tie(path: str, source: str | dict[str, float] | Sequence[tuple[str, float]], *, scale: float = 1.0, offset: float = 0.0) -> str— Constrainpathtoscale·source + offset, recording a node.rx.Refinement.tie_equal(paths: list[str] | str, *, source: str | None = None) -> list[str]— Make every parameter matchingpathsone parameter.rx.Refinement.untie(paths: list[str] | str) -> list[str]— Release user ties, recording aset_tienode.rx.Refinement.add_variable(name: str, value: float, *, vary: bool = False, min: float = -inf, max: float = inf, transform: str = 'identity', unit: str | None = None) -> str— Declare a named variable other parameters can follow.rx.Refinement.remove_variable(name: str) -> str— Delete a named variable, refusing while anything follows it.rx.Refinement.edit(*, structure: Structure | None = None, instrument: Instrument | None = None, label: str = '') -> str | None— Record a change to the model itself — adding an impurity phase, raising the background order, swapping the geometry.rx.refine(data: PatternData, structure: Structure, instrument: Instrument, *, mode: Mode = 'rietveld', plan: RefinementPlan | str = 'mccusker_default', two_theta_limits: tuple[float, float] | None = None, backend: str = 'numpy', solver: str = 'trf', history: bool | str | Path | RefinementTree = False, events=None, cancel=None, telemetry=None, label: str | None = None) -> RefinementResult— One-shot functional API:refine(data, structure, instrument).rx.RefinementPlanFields:stages: list[Stage],correlation_guard: float = 0.98,intermediate_ftol: float | None = 1e-06rx.Stage— One turn-on group of a staged plan — a declaration, not a result. Fields:name: str,turn_on: list[str],max_iter: int = 100,ftol: float | None = None,lebail_cycles: int = 3,seed: float = 0.0,strain_seed: float = 0.0,restraint_weight_scale: float = 1.0,window_slack_deg: float | None = Nonerx.PLAN_INFO— keys:mccusker_default,mccusker_structural,lab_bragg_brentano,lab_calibrate,lab_sample_refine,profile_only,pawley_defaultrx.ParameterRow— One row of the parameter table — the mirror ofparams.vector.Entry. Fields:path: str,value: float,vary: bool = False,lo: float = -inf,hi: float = inf,transform: Literal['identity', 'softplus', 'exp', 'logit'] = 'identity',tie: TieSpec | None = None,locked: bool = False,esd: float | None = None,mode_fixed: bool = False,needs_held_cell: bool = False,help_key: str | None = None
The four answers are four different types¶
A refinement, a series, an indexing run and a suggestion each return their own type, and none nests inside another. An rx.IndexingResult carries no cell key by design: best_or_none() is the only way to one cell, and it returns None more often than not (§6). RefinedParameter.at_bound is three-valued — test is True.
rx.RefinementResultFields:status: Literal['converged', 'max_iter', 'diverged'],mode: Literal['rietveld', 'lebail', 'pawley'],parameters: list[RefinedParameter],statistics: Statistics,stages: list[StageResult] = [],diagnostics: list[Diagnostic] = [],provenance: Provenance,node_id: str | None = None,tree_id: str | None = None,two_theta: list[float] = [],y_obs: list[float] = [],y_calc: list[float] = [],y_background: list[float] = [],sigma: list[float] = [],ticks: dict[str, list[float]] = {},qpa: QuantitativePhaseAnalysis | None = None,phase_agreement: list[PhaseAgreement] = [],data_support: DataSupport | None = None,absorption: AbsorptionCorrection | None = None,restraints: RestraintReport | None = None,geometry: GeometryTable | None = None,microstructure: list[PhaseMicrostructure] = [],identifiability: Identifiability | None = None,n_extra_components: int | None = None,n_background_components: int | None = None,histograms: list[HistogramResult] = []rx.StageResult— One stage’s outcome. Fields:name: str,status: Literal['converged', 'max_iter', 'diverged'],n_iterations: int,cost_initial: float,cost_final: float,freed: list[str] = [],ftol: float | None = None,n_constraint_truncations: int = 0,n_degenerate_cell_probes: int = 0,held: list[str] = [],released: list[str] = []rx.Statistics— Agreement indices, defined per Toby (2006), Powder Diffraction 21, 67. Fields:rwp: float,rp: float,rexp: float,chi2: float,gof: float,rwp_background_subtracted: float | None = None,durbin_watson: float | None = None,esd_inflation: float | None = None,n_points: int,n_free_parameters: int,max_shift_over_esd: float | None = None,identifiability_clause: str | None = Nonerx.RefinedParameter— One row ofRefinementResult.parameters. Fields:path: str,value: float,stderr: float | None = None,vary: bool = True,at_bound: bool | None = Nonerx.Diagnostic— A structured, actionable message produced by the engine. Fields:level: Literal['info', 'warning', 'error'],code: str,message: str,where: list[str] = [],suggestion: str | None = None,value: float | None = Nonerx.SeriesResult— The result of a sequential refinement over an ordered set of patterns. Fields:mode: Literal['rietveld', 'lebail', 'pawley'] = 'rietveld',entries: list[SeriesEntry] = [],x_label: str = 'index',direction: Literal['forward', 'backward', 'both'] = 'forward',backward: SeriesResult | None = None,diagnostics: list[Diagnostic] = [],provenance: Provenance | None = Nonerx.SeriesResult.trajectory(path: str) -> Trajectory— One parameter’s trajectory across the series.rx.SeriesResult.to_table(*, paths: list[str] | None = None) -> tuple[list[str], list[list]]—(header, rows): one row per pattern, a column per path (+ esd where that kind of path has one).rx.SeriesResult.write_csv(path, *, delimiter: str | None = None, paths: list[str] | None = None)— Writeto_tableto CSV/TSV (delimiter inferred from suffix).rx.SeriesResult.summary(*, max_entries: int = 5, deliverable: str | None = None) -> str— The series termination view (WP-1302): the trajectory table, theSEQUENTIAL_*rows, first and lastmax_entrieswith the count.rx.SeriesEntry— One pattern’s place in the series: what was fitted and how it went. Fields:index: int,label: str = '',x: float | None = None,status: Literal['converged', 'max_iter', 'diverged'] = 'converged',statistics: Statistics | None = None,parameters: list[RefinedParameter] = [],qpa: QuantitativePhaseAnalysis | None = None,phase_agreement: list[PhaseAgreement] = [],diagnostics: list[Diagnostic] = [],n_iterations: int = 0,reseeded: bool = False,rwp_warm: float | None = None,rung: Literal['warm', 'warm_staged', 'cold'] = 'warm',rungs_tried: list[str] = [],node_id: str | None = None,tree_id: str | None = Nonerx.IndexingResult— Whatrietx.index_patternreturns — and what it cannot return. Fields:candidates: list[CellCandidate] = [],engines_run: list[str] = [],systems_searched: list[str] = [],search_complete: dict[str, bool] = {},engine_stats: dict[str, float] = {},fom_panel_disagrees: bool = False,quality: DataQualityReport | None = None,validated: bool = False,wavelength: float = 0.0,n_usable_lines: int = 0,provenance: Provenance,preset: str | None = None,thresholds_version: str = '1.3',diagnostics: list[Diagnostic] = []rx.IndexingResult.best_or_none() -> CellCandidate | None— The single candidate, or None.rx.IndexingResult.evidence() -> IndexingEvidence— The machine-readable evidence view (WP-1043).rx.SuggestionResult— Ranked, gated answer to “which parameter should be freed next?”. Fields:groups: list[CandidateGroup] = [],non_separable: list[ParameterCandidate] = [],skipped: list[str] = [],n_evaluated: int = 0,chi2_red: float,noise_floor: float,summary: strrx.CandidateGroup— One rankable unit: a single candidate, or a tie the data cannot split. Fields:members: list[ParameterCandidate],gain: float,resolved: bool,delta_bic: float
The report¶
ref.report() or rx.build_report gives the three-layer rx.FitReport §5 reads. compare_rivals and predict_then_verify are the two experiments §4 step 14 and §4b call for.
rx.build_report(result: RefinementResult, *, model=None, values=None, plan=None, free_paths: list[str] | None = None, top_n: int = 15, match_tol_deg: float = 0.08, min_peak_sigma: float = 5.0) -> FitReport— Build the report, going as deep as the inputs allow.rx.FitReport— All three layers. Fields:thresholds_version: str = '1.5',rwp: float,gof: float,cumulative_chi2_breakpoints: list[float] = [],regions: list[Region] = [],n_regions_total: int = 0,unmatched: list[UnmatchedPeak] = [],lebail_gap: LeBailGap | None = None,background: BackgroundEvidence | None = None,identifiability: IdentifiabilityEvidence | None = None,summary: str = '',attribution: list[RegionAttribution] = [],trends: list[TrendAnalysis] = [],texture: list[TextureAnalysis] = [],strain: list[StrainAnalysis] = [],restraints: RestraintReport | None = None,geometry: GeometryTable | None = None,microstructure: list[PhaseMicrostructure] = [],layer1_available: bool = False,abstained_reason: str | None = None,abstained_kind: Literal['immature', 'resolution_limited', 'unreadable'] | None = None,suggested_actions: list[SuggestedAction] = []rx.report.compare_rivals(refinement, data, finding: ExchangeFinding | tuple[str, str]) -> RivalComparison— Fit each member of an exchangeable pair alone, the other at its null.rx.report.predict_then_verify(refinement, data, action: SuggestedAction, *, min_improvement: float = 0.01) -> VerificationOutcome— Try an action on a branch, keep it only if χ² actually improves.
Series, history, projects¶
refine_sequential chains N patterns by warm start (§9b); refine_multi stacks them into one joint residual, which is a different thing. The history verbs work the DAG every fit commits to (§9); a Project owns a .rex directory; a CancelToken cancels cooperatively, between residual evaluations.
rx.refine_sequential(patterns: Sequence[PatternData], structure: Structure, instrument: Instrument, *, carry: Sequence[str] = ('*'), backend: str = 'numpy', solver: str = 'trf', history: bool | str | Path = False, **kw) -> SeriesResult— One-shot functional API for a warm-started series.rx.SequentialRefinement(structure: Structure, instrument: Instrument, *, backend: str = 'numpy', solver: str = 'trf', carry: Sequence[str] = ('*'), history: bool | str | Path = False)— Refine an ordered series of patterns, each warm-started from the last.rx.refine_multi(data: list[PatternData], structure: Structure, instruments: list[Instrument], *, plan: RefinementPlan | str = 'mccusker_default', sharing: SharingMap | None = None, two_theta_limits=None, weights: list[float] | None = None, backend: str = 'numpy', solver: str = 'trf') -> RefinementResult— One-shot joint refinement ofstructureagainst severaldata/instrumentspairs.rx.MultiHistogramRefinement(structure: Structure, instruments: list[Instrument], *, sharing: SharingMap | None = None, backend: str = 'numpy', solver: str = 'trf')— Joint Rietveld refinement of a shared structure against several patterns.rx.Refinement.checkout(node_id: str) -> Refinement— Restore the state recorded atnode_id(a node id or a tag).rx.Refinement.branch(node_id: str | None = None) -> Refinement— A second working tree over the same history, for a rival strategy.rx.Refinement.merge(other: str, *, prefer: str = 'theirs', label: str = '') -> str— Three-way merge of another branch into the current state.rx.Refinement.cherry_pick(node_id: str, data: PatternData) -> RefinementResult— Re-run another node’s stage action on top of the current state.rx.replay(tree: RefinementTree, node_id: str, data: PatternData) -> RefinementResult— Recompute the curves and statistics of a recorded node.rx.Project(path: str | Path, doc: ProjectDoc, data: PatternData, refinement: Refinement, data_diagnostics: list[Diagnostic] | None = None)— An openable, savable refinement session on disk.rx.Project.create(path: str | Path, *, pattern: str | Path, structure: Structure | None = None, instrument: Instrument, mode: Mode = 'rietveld', plan: Any = None, two_theta_limits: tuple[float, float] | None = None, excluded_regions: list[tuple[float, float]] | None = None, reader_options: dict[str, Any] | None = None, ui: dict[str, Any] | None = None, backend: str = 'numpy', solver: str = 'trf') -> Project— Create a project directory around a pattern file and a model.rx.Project.open(path: str | Path, *, backend: str = 'numpy', solver: str = 'trf') -> Project— Open an existing project, resuming at the history head.rx.Project.save()— Rewriteproject.json.rx.CancelToken()— A thread-safe “stop asking for evaluations” flag.
An unknown phase¶
Peaks, then a cell, then the extinction symbol — the closed loop of §7b-7f, each step returning a ranked list and never a singleton. rx.fit_peaks leaves that loop: it fits the positions you name, for a width analysis or a d-spacing, with no cell in the question.
rx.pick_peaks(data: PatternData, instrument: Instrument, *, two_theta_range: tuple[float, float] | None = None, shoulders: bool = True, flag_contamination: bool = True) -> PeakList— Every resolvable line indata, with a fitted position and its esd.rx.fit_peaks(data: PatternData, instrument: Instrument, positions: np.ndarray | list[float], *, two_theta_range: tuple[float, float] | None = None) -> PeakList— Profile-fit exactly the peaks atpositions— no structure, no space group, no refinement.rx.index_pattern(peaks: PeakList | None = None, *, data: PatternData | None = None, instrument: Instrument | None = None, spec=None, preset: str | None = None, engines: Sequence[str] | None = None, quality=None, shift_from_pairs: bool = True, validate: bool = True, check_top: int | None = None, two_theta_limits: tuple[float, float] | None = None, events=None, cancel=None)— Find the unit cell — or say, in the shape of the answer, that it cannot.rx.determine_extinction_symbol(data: PatternData, candidate: CellCandidate, instrument: Instrument, *, peaks: PeakList | None = None, two_theta_limits: tuple[float, float] | None = None, k_sigma: float = 3.0, max_classes: int | None = None, cancel=None) -> ExtinctionScreen— Rank the extinction classes compatible with an indexed lattice.
Out¶
Files and figures. rx.format_su renders a value with its esd as 1.2345(12); plot_for_vlm is the montage §5 allows as a check on a conclusion already reached from numbers. rx.write_recipe_tables is the return leg of rx.read_recipe — a finished refinement as PowderLine’s four tables, for a pipeline that dispatched the job here.
rx.write_refinement_cif(result: RefinementResult, structure: Structure, instrument: Instrument, path: str | Path)— Write a refinement CIF: structure (values + esds), R-factors, wavelength, profile/background description, and the observed/calculated pattern loop.rx.write_qpa_table(qpa: QuantitativePhaseAnalysis, path: str | Path, *, delimiter: str | None = None)— Write the QPA table to CSV/TSV (delimiter inferred from suffix).rx.write_reflection_table(rows: list[ReflectionRow], path: str | Path, *, delimiter: str | None = None)— Write reflection rows to CSV/TSV (delimiter inferred from suffix).rx.reflection_table(model: CompiledModel, values: dict[str, float], structure: Structure) -> list[ReflectionRow]— Reflection rows for every (emission line, reflection) of every phase.rx.write_recipe_tables(refinement, out_dir: str | Path, *, phase_names: dict[str, str] | None = None) -> dict[str, Path]— Write a finished refinement as PowderLine’s four output tables.rx.format_su(value: float, esd: float | None, *, decimals: int = 6) -> str— A number with its standard uncertainty invalue(su)notation.rx.viz.plot_result(result: RefinementResult, *, path: str | None = None, two_theta_range: tuple[float, float] | None = None, show_background: bool = True, weighted: bool = False, style: str = 'light', wavelength: float | None = None, x_axis: str = 'two_theta', y_scale: str = 'linear', label_align: str = 'bottom', figsize: tuple[float, float] | None = None, font_size: float = 11.0, dpi: int = 300)— Standard Rietveld panel: observed, calculated, difference, tick rows.rx.viz.plot_for_vlm(result: RefinementResult, report=None, *, path: str, n_regions: int = 4, dpi: int = 140)— Annotated multi-panel montage rendered for what VLMs can read.rietx.viz.html.write_html(result: RefinementResult, path: str, *, weighted: bool = False, include_plotlyjs: bool | str = True, max_points: int = 200000)— Render aRefinementResultto a self-contained HTML file.