# The fit report
A converged fit gives you Rwp. The report gives you *where* the model and the
data disagree, *what kind* of error would explain it, and — separately — how
much of that the package is willing to stand behind.
:::{admonition} For agents
:class: agent
The `FitReport` is built for a program to read: numbers rather than pixels, so
a caller can close a refinement loop without looking at a plot. That is why the
sections below read as field lists.
A person gets more out of it than out of Rwp too, and `FitReport.summary` is a
paragraph of prose written for exactly that reader.
This chapter is the **object model**: what a `FitReport` carries, field by
field, and how to get one. The **judgement** — what to believe, in what order,
when to disbelieve Rwp, and how to act on an abstention — is
[`docs/AGENT_PROTOCOL.md`](https://github.com/yue-here/rietx/blob/main/docs/AGENT_PROTOCOL.md)
§4 to §6, and this chapter does not restate a line of it.
:::
The three layers answer three different questions, and each one can decline to
answer:
```{mermaid}
graph TD
R["RefinementResult"] --> L0["Layer 0
Rwp, GoF, misfit regions,
unmatched peaks
independent of the model"]
L0 --> M{"is the fit mature
enough to linearise?"}
M -- no --> AB["abstain
FitReport.abstained_kind
the next move is a better
starting model"]
M -- yes --> L1["Layer 1
project the residual onto the
shape-derivative basis"]
L1 --> G{"do all four
gates pass?"}
G -- no --> NP["reported as not passing
RegionAttribution.gate_failures
with the numbers"]
G -- yes --> L2["Layer 2
FitReport.suggested_actions
advisory, never applied"]
```
## Building a report
`build_report` takes a `RefinementResult`. `Refinement.report` is the same thing
from the session, and it is the form to use, because it passes the compiled
model along. Without the model there is no Layer 1 at all.
Every report stamps `FitReport.thresholds_version`, the version of the gate
thresholds it was built under. Store that with any report you store: the
thresholds are a versioned contract, and a number compared across two versions
compares two different questions.
## Layer 0: statistics independent of the model
Nothing here depends on the model being right, so nothing here can be wrong
about the *data*.
- `FitReport.rwp` and `FitReport.gof`, the two headline statistics.
- `FitReport.regions` — the misfit clustered into 2θ regions, worst first. Each
`Region` carries `Region.local_rwp`, `Region.chi2_share` (its share of the
total χ², which is what makes it worth attention),
`Region.max_abs_delta_over_sigma` and `Region.n_reflections`.
`FitReport.n_regions_total` says how many there were before the list was
truncated.
- `FitReport.unmatched` — observed peaks the model does not account for, and
calculated peaks with no observed intensity. `UnmatchedPeak.kind` says which,
`UnmatchedPeak.height_over_sigma` says how strong. This is how an impurity
phase announces itself.
- `FitReport.cumulative_chi2_breakpoints` — where along 2θ the running χ² jumps,
which localises a problem that a per-region view spreads thin.
- `FitReport.summary` — one paragraph of prose assembled from the above.
## Layer 1: attributing the misfit
Layer 1 answers a different question: *what kind* of error is this?
The residual in each region is projected onto a shape-derivative basis built
from the profile itself — intensity, position, width, mixing, axial asymmetry —
so the answer reads "the peaks here are 0.01° low and 5 % too weak", whichever
parameters happen to be free. The five columns are not orthogonal, so they are
fitted in one joint weighted solve rather than by independent dot products,
which would cross-contaminate.
`FitReport.attribution` holds one `RegionAttribution` per region, and
`RegionAttribution.coefficients` (each a `BasisCoefficient`) is the reading.
`FitReport.layer1_available` says whether the layer ran at all.
Above the per-region view, `FitReport.trends` regresses the region coefficients
against the angular templates a per-region view structurally cannot see: width
against 1/cos θ and tan θ, intensity against sin²θ/λ² — the
displacement-parameter signature — and position against the shapes **its own
geometry has**. A Bragg-Brentano fit is tested against constant, cos θ, sin 2θ
and tan θ; a capillary against constant, sin 2θ, cos 2θ and tan θ, because a
specimen displacement in the flat-plate sense is not an error a capillary can
make. Fitting the union would name aberrations the instrument does not have,
and the parameters behind them are force-fixed in that geometry anyway.
`TrendAnalysis.max_template_collinearity` and `TrendAnalysis.separable` are the
load-bearing pair. Over a limited angular range two templates can be
indistinguishable, and an inseparable pair is **declared inseparable** rather
than resolved into a confident wrong singleton. `FitReport.texture` and
`FitReport.strain` give preferred orientation and anisotropic strain the same
treatment.
### The four gates
None of Layer 1 is trustworthy unconditionally, which is what the gates are for.
There are four, and every statement passes all four or the region is reported as
not passing:
| Gate | Field | What it rejects |
|---|---|---|
| local significance | `RegionAttribution.chi2_reduced`, `RegionAttribution.has_significant_misfit` | a region whose "misfit" is noise |
| explanatory power | `RegionAttribution.r2` | a residual this basis does not explain at all |
| resolvability | `RegionAttribution.gram_condition` | columns too collinear here to be told apart |
| validity radius | {{ VALIDITY_RADIUS_FWHM }}·FWHM on the position coefficient | a peak far enough away that linearising it is meaningless. The answer must be "re-detect this peak", never a confident small offset |
`RegionAttribution.gates_passed` is the verdict, and
`RegionAttribution.gate_failures` names each failure — a `GateFailure` whose
`GateFailure.code` is the gate (branch on it) and whose `GateFailure.message`
carries the measured numbers — so a rejected reading tells you *why* it was
rejected.
### When Layer 1 abstains
If the fit is too immature to linearise, Layer 1 does not guess. It abstains,
says so in `FitReport.abstained_reason`, and classifies the abstention in
`FitReport.abstained_kind`: *immature* (the starting model is bad enough that
attributing structure to the residual would be attributing structure to the
starting model), *resolution_limited*, or *unreadable*.
An abstention is information. It says the next move is a better starting model,
not a better interpretation. Do not convert it into a number.
## Supporting evidence
Four sections carry measurements that are not attributions. Each is there
because a fit statistic is blind to it.
- **`FitReport.background`** (`BackgroundEvidence`). A background flexible
enough to imitate the peaks biases displacement parameters up and scales —
hence phase fractions — down, *while Rwp improves*. So the flexibility is
measured directly. `BackgroundEvidence.absorption` is the block projection of
each structural Jacobian column onto the background column span, which a
pairwise correlation misses, and
`BackgroundEvidence.worst_absorption_path` names the parameter worst affected.
The opposite failure — a background too stiff — shows in
`BackgroundEvidence.off_region_chi2_reduced` and
`BackgroundEvidence.off_region_durbin_watson`, which Layer 0's peak-cluster
regions cannot see by construction.
- **`FitReport.identifiability`** (`Identifiability`). Whether the esds mean what
they appear to: `Identifiability.top_correlations` (`CorrelationPair`),
`Identifiability.soft_modes` (`SoftMode`, the softest directions of the normal
matrix), and `Identifiability.exchangeability`, which lists held parameters
whose effect a refined one could absorb. An `ExchangeFinding` carries both
halves of the discriminator, `ExchangeFinding.r2` and
`ExchangeFinding.partner_significance`, because R² alone is a property of the
design matrix and fires on clean fits too.
- **`FitReport.lebail_gap`** (`LeBailGap`). `LeBailGap.ratio` is the
structural-versus-profile triage statistic: how much of the remaining misfit a
free-intensity fit could remove. A large gap says the problem is the structure,
not the profile. It is `None` outside Rietveld mode, which is absence for
cause. This is the IUCr guidelines' rule that a structure-free fit's Rwp is the
best profile fit the data allow, and a Rietveld Rwp should approach it
{cite}`mccusker1999` — measured rather than left to the reader.
- **`FitReport.restraints`** — what each soft restraint is contributing, with
`RestraintReport.weight_scale` recording the c_w the stage ran at, so a
deviation can be read against the weight that was insisting on it.
- **`FitReport.geometry`** (`GeometryTable`) — the distances and angles, carried
through from the result. It is the one section here that measures the
*structure* rather than the fit, which is why it survives an abstention
unchanged: "are these distances chemically sensible" is the question a reader
asks first when the profile evidence refuses to speak. Nothing scores it —
see [](results.md) for the fields and for what the esds mean.
## Layer 2: suggested actions
`FitReport.suggested_actions` is a typed, advisory list. Each `SuggestedAction`
carries `SuggestedAction.kind`, `SuggestedAction.parameter_paths` (the dot-paths
it would free), `SuggestedAction.rationale`,
`SuggestedAction.expected_delta_chi2`, `SuggestedAction.alternatives` and
`SuggestedAction.confidence` — which weights *importance*, the share of χ² at
stake, rather than statistical significance alone. `SuggestedAction.vetoed_by`
records where the staged-strategy engine overruled an action, and
`FitReport.action` looks one up by kind.
The action a position trend maps to is chosen by geometry, for the same reason
the templates are: `refine_sample_displacement` on a flat plate,
`refine_capillary_offset_along_beam` and `refine_capillary_offset_across_beam`
on a capillary. A suggestion naming a parameter the geometry force-fixes is one
a caller cannot act on, and this is where that is prevented rather than
apologised for afterwards. On `flat_plate_transmission`, which models neither
displacement nor transparency, a `cos_theta` or `sin_2theta` trend is
reported as a shape with **no** action: the diagnosis (a flat specimen off
the axis) is right, and there is no parameter a suggestion could legally
free.
Advisory is the design, not a limitation. The package never applies these for
you.
## Reports at every stage
**A converged report is routinely the least informative one in the run.** A plan
absorbs an error it cannot free into whatever it can, and arrives converged with
nothing to suggest, while its own first stage named the cause out loud. So the
report exists at every stage boundary, not only at the end:
```python
result = ref.fit(data, stage_reports=True)
for rung in ref.stage_reports_:
print(rung.stage, rung.rwp, rung.summary)
```
Each rung is a `StageReport`, with `StageReport.stage`, `StageReport.rwp`,
`StageReport.summary`, `StageReport.actions` and the abstention fields. They are
read off states the plan already visits, so the answer is bit-identical to the
run without them. `FitReport.for_stage` projects a report onto one rung
directly. The flag is off by default in the library, because `fit` is called in
loops.
A `StageReport` is the report at a stage boundary. The stage's own arithmetic —
what it freed, how many iterations it took, whether it converged — is a
`StageResult`, in [](refining.md).
## Comparing settings with `rietx compare`
"Did that correction help?" is not a question ΔRwp answers. Some corrections
provably cannot move Rwp — capillary absorption is an exact reparameterisation
of the scale and the displacement parameters — and others improve it by
absorbing physics that belongs elsewhere.
`rietx.viz.compare` runs a bundled standard under several settings and renders
the comparison that does settle it:
```python
from rietx.viz import compare
record = compare.run("srm660c", "roughness_suortti")
```
Read the **cumulative Δχ² against the reference** panel rather than the Rwp. It
localises *where* along 2θ the change acted, which is what separates a
correction doing its job from one absorbing someone else's error.
`compare.STANDARDS` and `compare.VARIANTS` are the registries, and
`rietx compare --open` is the same thing with a UI.
The house rule behind all of this: a new correction ships with a record field or
a diagnostic that states what it changed, never with an Rwp comparison as its
evidence.