Exports and derived tables¶
A finished fit holds more than its refined parameters. The calculated pattern, the reflection list behind it and the tick positions drawn under it are all functions of the converged state, and the package will compute any of them on request. This chapter is those derived quantities and the files they become. Where each file lands, and the rest of the on-disk map, is Files and projects.
The calculated pattern¶
RefinementResult carries the curves of the fit itself: RefinementResult.y_obs,
RefinementResult.y_calc and RefinementResult.y_background on
RefinementResult.two_theta. Two more belong with them.
RefinementResult.sigma is the per-point σ the fit divided by, copied verbatim
from the pattern at stage compile. RefinementResult.sig is the method to call
rather than the field to read: it is the one authority for σ, and every weighted
residual in the package goes through it, the static plot, the interactive page,
the report’s Layer 0 and the GUI’s own window alike. Two conditioning steps
are the reason it is a method. A result recorded before v0.2 carries no σ at all
and gets the Poisson √max(y,1) fallback the fit itself would have used, and
non-positive entries are floored, so a zero esd becomes a small σ instead of an
infinity that would lose the whole trace.
RefinementResult.sig says nothing about where σ came from. By the time a
result exists, a file’s esd column and a Poisson estimate are the same array of
floats. DataRef.has_sigma is the fact that survives, and it is what a plot
axis and the text document are labelled from (Files and projects).
RefinementResult.ticks maps a phase name to its reflection positions in
degrees 2θ. It covers every emission line, not only the primary one. The
calculated pattern really does have a peak at each Kα₂ position, and a tick list
that omitted them would make the report flag every Kα₂ peak as an unindexed
impurity, which it once did.
Refinement.predict evaluates y_calc at the parameters as they stand. Given a
grid (a PatternData, or an array of 2θ values) it compiles a fresh model on it,
so it will extrapolate beyond the fitted range or resample inside it. With
no argument it uses the grid the last fit ran on.
import numpy as np
fine = np.arange(10.0, 90.0, 0.001)
y = refinement.predict(fine)
y_here = refinement.predict(data) # the pattern's own grid
It does not need a fit. Evaluating a model is not refining it. The values come
off the structure and instrument, whether a fit put them there, set_value did,
or you typed them. So drawing the curve a set of parameters implies costs no
solver time, and a zero-stage plan, which refines nothing and is refused, is not
the way to ask for it. Le Bail and Pawley are the exception, and say so. Their
per-hkl intensities are extracted by a fit, so there is nothing to carry over
before one has run.
The two forms are not bit-identical on the same grid, and the difference is the
frozen-per-stage invariant rather than a defect. predict() reuses the model
compiled for the last stage, whose per-reflection evaluation windows were sized
at the values that stage started from, while a grid argument sizes them at the
values it ended on. On the synthetic five-stage fit that is 36 of 4200 channels,
by at most 8e-6 of the peak height, all of them in peak tails at a window edge.
RefinementResult.y_calc is the first of the two: the curve the fit minimised.
Plotting the fit¶
RefinementResult.plot draws the standard panel A first refinement opens with:
observed points, the calculated line, the obs − calc difference on the same
axis at the same scale, and one row of reflection ticks per phase. It needs
matplotlib (the viz extra), and it returns the figure, so passing path=
is optional.
two_theta_range= is a window and not a crop. The intensity scale and the rows
below it are built from what the window contains, so a zoom into a weak region
is a figure of its own data.
weighted=True draws Δ/σ instead, in its own panel with a ±3σ band. A raw
difference shares the intensity axis, so the eye reads a small deviation on a
strong peak as a large error, while Δ/σ has expectation 1 under a correct model
and the band is an absolute scale rather than a relative one. It is not the
default because it costs the one thing the classic layout gives away free, which
is the residual and the peak that caused it in a single glance.
wavelength= puts λ on the 2θ axis, which is meaningless without it. The result
does not carry the emission line, so it has to be passed. It is also what
x_axis="q" and x_axis="d" are derived through, and those two carry no λ of
their own, which is the point of them.
y_scale= takes "sqrt" (equal display distance for equal counting σ), "log"
or "asinh". Any of them moves the difference into its own panel, since an
offset raw difference is negative by construction and a nonlinear intensity axis
cannot draw it.
style="dark" is for a figure going onto a dark page, and figsize=/
font_size= are the exposure surface: build the figure at the width it will be
read at, and do not scale it in the document afterwards.
The reflection list¶
Refinement.reflection_table returns one row per (emission line, reflection) of
every phase, and not one row per reflection. reflection_table is the same
table as a function, taking the compiled model, the decoded parameter values and
the structure, for a caller that holds those instead of a Refinement.
ReflectionRow is one row.
Field |
Holds |
|---|---|
|
the phase name |
|
which emission line, indexed from 0 |
|
that line’s λ in Å |
|
the Miller indices |
|
the d-spacing in Å |
|
the apparent position, in degrees |
|
the Laue-group multiplicity of the orbit |
|
|F|², or |
|
the modelled integrated intensity of this row |
Three of those need care.
ReflectionRow.two_theta is where the model places the peak: the Bragg angle
plus the zero shift plus the geometry’s own position correction, so it matches
RefinementResult.ticks rather than the ideal Bragg angle. A reflection
whose 2θ is non-physical at a given line’s wavelength (sin θ > 1) is dropped for
that line only, so a row missing from one line may be present in another.
ReflectionRow.intensity is the whole modelled contribution and not |F|². In
Rietveld mode it is scale × multiplicity × |F|² × preferred orientation ×
line weight × Lp × extinction × absorption × roughness. It is what the peak
under the tick is made of.
ReflectionRow.f_squared is None in Le Bail and Pawley mode, where the
per-reflection intensity is extracted or refined rather than computed from the
structure. With anomalous scattering on, which is the default, it is the
Friedel-averaged ⟨|F|²⟩ and not the representative reflection’s own |F(h)|².
The two differ in a non-centrosymmetric group, both land in the same
powder peak, and only the average is observable in a powder.
Writing them out¶
Files and projects has the three writers that turn a result into a file, what each
file contains, and why the CIF carries a symmetry-operation loop of its own.
Refinement carries the same three as methods on the refinement that produced
the result, which saves passing the pieces back in: Refinement.write_cif,
Refinement.write_reflection_table and Refinement.write_qpa_table. Each takes
a path and passes any other keyword through, so
refinement.write_reflection_table("refl.tsv") picks a tab delimiter from the
suffix.
Numbers with uncertainties¶
format_su writes a value and its esd in the crystallographic value(su)
notation [Schwarzenbach et al., 1989]: the su carries two significant figures and
the value is quoted to exactly that precision.
from rietx.io.exporters import format_su
assert format_su(4.593700, 2.5e-4) == "4.59370(25)"
assert format_su(123.4, 2.5) == "123.4(25)"
assert format_su(12345.0, 250.0) == "12340(250)"
4.59370(25), not 4.593700(250). An esd of 2.5 × 10⁻⁴ says the sixth decimal
is not knowledge, so the esd sets the number of decimals and the decimals
argument governs only the case where there is none.
Three cases the function handles that a format string does not. An esd like
0.0999 rounds up to two figures as 100, which is renormalised to 0.10 and one
fewer decimal rather than written as a spurious three-figure (100). An esd of
1 or more takes decimals off the value and keeps its own trailing magnitude, so
12345 ± 250 is 12340(250). And no esd at all (None, non-positive, or not
finite, which is a fixed parameter or one the fit could not estimate) writes the
plain number, never an implied uncertainty.
from rietx.io.exporters import format_su
assert format_su(1.234567, 0.0999) == "1.23(10)"
assert format_su(4.5937, None) == "4.593700"