Python API
Signatures and docstrings of the public functions and classes in reflectofit at main (d244649), read from the source without importing it. The guides on this site say how the pieces fit together; this page is for looking up an argument.
reflectofit.cli
Command line: reflectofit calc | fit | score | stitch.
main
python
def main(argv: list[str] | None=None) -> intreflectofit.data
Measured curves: reading, stitching attenuated segments, held-out splits. Measured files are never part of this repository (DESIGN.md section 6); tests use synthetic files.
Curve
python
@dataclass
class Curve:
q: NDArray[np.float64]
y: NDArray[np.float64]
sigma: NDArray[np.float64] | None = None
meta: dict = field(default_factory=dict)Curve.tth
python
def tth(self, wavelength: float | None=None) -> NDArray[np.float64]read
python
def read(
path: str | Path,
x: Literal['tth', 'q'] = 'tth',
wavelength: float = CU_KALPHA1,
) -> CurveRead 2- or 3-column text (x, intensity[, error]); whitespace or commas, comment and header lines skipped. Sorted by q; equal q values are kept.
write
python
def write(path: str | Path, curve: Curve, x: Literal['tth', 'q']='tth') -> Nonestitch
python
def stitch(
scans: list[NDArray],
method: Literal['boundary', 'median'] = 'boundary',
) -> NDArray[np.float64]Join scans (N, 2+) of [x, intensity, (error, ...)] measured through different attenuators. boundary (xrr_combine, ReflectoLearn): scans in increasing x share exactly one boundary point; the last scan is the reference, each earlier scan is scaled so its last point equals the next scan's first point, and the duplicate is dropped. median (xray_ooguy): the first scan is the reference; each later scan is scaled by the median of the point-by-point ratio to the curve assembled so far (linear interpolation) over their overlap, which needs at least two points; all points are kept. A scan without such an overlap raises. Every column after x is scaled together.
block_split
python
def block_split(
q: NDArray,
n_blocks: int,
held: tuple[int, ...],
q_min: float = 0.0,
) -> tuple[NDArray[np.bool_], NDArray[np.bool_]]Training and held-out masks: points with q < q_min always train; the rest are cut into `n_blocks` contiguous blocks of equal size (np.array_split) and the blocks in `held` (0-based) are held out (ReflectoLearn#35 section 3.2).
reflectofit.estimate
Quick estimators read directly off a curve, without a fit.
critical_q
python
def critical_q(
q: NDArray,
y: NDArray,
rule: Literal['steepest', 'half_plateau'] = 'steepest',
) -> floatq of the critical edge. steepest: the steepest descent of log R above the plateau (the minimum of d log10 R / dq, refined by a parabola through it and its neighbours). half_plateau: where the 3-point mean of y first falls to half the plateau, the plateau being the largest 3-point mean in the first 10 % of points; linear interpolation between the two points around the crossing (the rule of ReflectoLearn#35 protocol A1, step 2). NaN if it never falls that far. Neither is exactly the film's q_c: the half-plateau point is about 9 % high for 250 Å films and can be several times low for 20-40 Å dense films whose edge tunnels (physics review on ReflectoLearn#35).
kiessig_thickness
python
def kiessig_thickness(
q: NDArray,
y: NDArray,
d_range: tuple[float, float] = (20.0, 2000.0),
sld: float | None = None,
d_min: float = 60.0,
mask: NDArray | None = None,
) -> floatFilm thickness [Å] from the Kiessig period: the strongest peak of the real-space transform of the detrended log R (port of ReflectoLearn `refine.fft_thickness`, b6fb0c4). Only points with y > 0 (and inside `mask`, if given) are used: a zero-count point has no logarithm. Fringes are periodic in the film's own wavevector sqrt(q² − q_c²); with `sld` [1e-6 Å⁻²] the transform runs over it, otherwise over q (which overestimates d by a few percent near the edge). NaN with fewer than 40 usable points or no peak in [max(d_min, lo), hi].
reflectofit.fit
Fitting: least squares in a trust window, differential evolution, and scoring. `counts_scale` S says the data are counts with S counts at R = 1; then lam = S·R and the Poisson kind is the likelihood. Without it the data are reflectivity and lam = R.
FitResult
python
@dataclass
class FitResult:
model: Model
values: dict[str, float]
errors: dict[str, float]
loss: str
objective: float
n_points: int
aic: float
bic: float
success: bool
message: str
nfev: int
method: str
njev: int = 0
status: int | None = None
settings: dict = field(default_factory=dict)
covariance: NDArray | None = None
at_bound: list[str] = field(default_factory=list)FitResult.to_json
python
def to_json(self) -> strtrust_bounds
python
def trust_bounds(model: Model, margin: float) -> tuple[NDArray, NDArray]The model's bounds narrowed to start · (1 ± margin) for every parameter with a non-zero start (the trust window of ReflectoLearn refine around a network guess).
lsq
python
def lsq(
model: Model,
curve: Curve,
loss: str = 'log',
counts_scale: float | None = None,
trust: float | None = None,
max_nfev: int = 200,
mask=None,
) -> FitResultTrust-region reflective least squares (scipy) on the residuals of `loss`, with a central finite-difference Jacobian computed as one batched forward call.
de
python
def de(
model: Model,
curve: Curve,
loss: str = 'log',
counts_scale: float | None = None,
popsize: int = 20,
maxiter: int = 300,
seed: int | None = 0,
polish: bool = True,
mask=None,
) -> FitResultDifferential evolution (scipy) over the model's bounds; each generation is one batched forward call. With `polish`, the best member is refined by `lsq`.
staged
python
def staged(
model: Model,
curve: Curve,
stages,
counts_scale: float | None = None,
mask=None,
penalty: dict[str, float] | None = None,
penalty_weight: float = 0.0,
) -> FitResultStaged gradient fit (`reflectofit.staged`, needs torch): each stage frees some of the model's parameters and runs Adam on one loss within the model's bounds. The result's objective is the Poisson deviance with counts_scale, else the log residual sum; it has no covariance.
MCMCResult
python
@dataclass
class MCMCResult:
names: list[str]
samples: NDArray
intervals: dict[str, tuple[float, float, float]]
tau: dict[str, float]
tau_margin: float
converged: bool
steps: int
walkers: int
acceptance: float
log_prob_max: float
best: dict[str, float]
settings: dict = field(default_factory=dict)mcmc
python
def mcmc(
model: Model,
curve: Curve,
start: FitResult | None = None,
loss: str = 'poisson',
counts_scale: float | None = None,
steps: int = 5000,
max_steps: int = 20000,
walkers: int = 64,
seed: int = 0,
mask=None,
n_independent: int | None = None,
) -> MCMCResultPosterior sampling with emcee (extra `mcmc`), flat priors within the model's bounds. The walkers move by differential evolution (emcee's DEMove), which mixed in correlated posteriors where the stretch move did not (ReflectoLearn#29, R2c). Coordinates are whitened by the start fit's covariance (θ = θ_start + L z, L its Cholesky factor); DE is affine-invariant, so this sets the shape of the initial ensemble, a 0.1 σ ball around the start. emcee's DESnookerMove is not used: it does not leave the target invariant (ReflectoFit#10). The first half of the chain is discarded. While the kept half is shorter than TAU_FACTOR times the largest integrated autocorrelation time, the chain is extended, up to `max_steps`; if it is still short, `converged` is False and a RuntimeWarning says so. `converged` is that τ criterion only: a basin the walkers never reach from the start does not show in τ. `loss` "poisson" is the likelihood of counts (needs counts_scale); "log" is a Gaussian in log10 R with one fitted spread ln σ, scaled to `n_independent` points if given (as ReflectoLearn's refine.mcmc).
score
python
def score(
model: Model,
curve: Curve,
counts_scale: float | None = None,
mask=None,
) -> dictEvery loss kind that the data allow, and AIC/BIC with the model's free-parameter count.
reflectofit.forward
Observed reflectivity: the layer kernel plus the instrument. The nominal 2θ plus the alignment offset is the true angle; the resolution is a Gaussian in q around the q of that angle. At each quadrature node: R for Kα₁ (and Kα₂ at the same angle, mixed by intensity ratio) times the footprint factor at that angle. The weighted sum over nodes is scaled by I0 and the background is added (DESIGN.md section 2). Every instrument field may be a scalar or a (B,) array, one value per curve.
Instrument
python
@dataclass(frozen=True)
class Instrument:
wavelength: float = CU_KALPHA1
i0: Any = 1.0
background: Any = 0.0
tth_offset: Any = 0.0
sigma_q: Any = 0.0
dq_over_q: Any = 0.0
kalpha2_ratio: Any = 0.0
kalpha2_wavelength: float = CU_KALPHA2
sample_length: Any = None
beam_sigma: Any = None
nodes: int | None = NoneInstrument model. Angles in degrees, lengths in mm, resolution as Gaussian σ in q.
Instrument.replace
python
def replace(self, **changes) -> 'Instrument'forward
python
def forward(
q: ArrayLike,
model,
instrument: Instrument | None = None,
backend: str = 'numba',
) -> NDArray[np.float64]Observed reflectivity for one model (returns (Q,)) or a batch (returns (B, Q)). `model` is a Stack, a packed (sld, thickness, roughness) tuple of (N,) or (B, N) arrays, or a sequence of either. Without an instrument this is the bare |r|² of the stack.
resolution_report
python
def resolution_report(q: ArrayLike, model, instrument: Instrument) -> dict[str, NDArray]For the default resolution rule: nodes per point, the estimated relative error of the refined range between the critical q, and whether refinement stopped at `resolution.MAX_DEPTH`, each (B, Q) (or (Q,) for one model). What `forward` integrates with, without evaluating the curve.
reflectofit.kernel
Float64 Parratt recursion compiled with numba. k_z² = (q/2)² − 4π(ρ − ρ_ambient) with ρ in Å⁻², Névot-Croce roughness. The SLD is conjugated so that the principal square root gives Im k_z ≥ 0 (decay into absorbers) with the exp(+2i k_z d) recursion; callers pass Im SLD > 0 for absorption.
parratt
python
def parratt(
q: NDArray[np.float64],
sld: NDArray[np.complex128],
thickness: NDArray[np.float64],
roughness: NDArray[np.float64],
) -> NDArray[np.float64]|r|² for q (B, Q) and media arrays (B, N), ambient first. Returns (B, Q).
smear
python
def smear(
q_true: NDArray[np.float64],
sigma: NDArray[np.float64],
counts: NDArray[np.int64],
u: NDArray[np.float64],
w: NDArray[np.float64],
sld: NDArray[np.complex128],
thickness: NDArray[np.float64],
roughness: NDArray[np.float64],
wl1: float,
wl2: float,
ratio: NDArray[np.float64],
length: NDArray[np.float64],
beam: NDArray[np.float64],
) -> NDArray[np.float64]Σ w · R over each point's nodes q_true + sigma · u, for (B, Q) points whose nodes are listed point after point (counts (B·Q,) nodes each): Kα₁ (and Kα₂ at the same angle when wl2 > 0, mixed by ratio), times the footprint factor where length is not NaN. Returns (B, Q).
reflectofit.materials
X-ray SLD from composition and mass density (DECISIONS #8: computed, never copied). Uses the X-ray scattering factor tables of `periodictable`; at Cu Kα₁ Si at 2.33 g/cm³ gives 20.071 + 0.457i and SiO₂ at 2.20 g/cm³ gives 18.865 + 0.244i (1e-6 Å⁻²).
sld
python
def sld(formula: str, density: float, wavelength: float=CU_KALPHA1) -> complexComplex SLD [1e-6 Å⁻²] of a compound at a mass density [g/cm³]; Im > 0 is absorption.
reflectofit.metrics
Residuals, losses and information criteria with one signature (DESIGN.md section 3.2). `lam` is the model's expected counts and `y` the measured counts, both per point; `scale` is the counts at R = 1. Data given as reflectivity are counts with scale 1 for the log, raw and chi2 kinds; the Poisson kind needs real counts. Works on numpy arrays and torch tensors alike.
residuals
python
def residuals(kind: str, lam, y, scale: float=1.0, sigma=None, floor: float=0.5)Per-point residuals whose sum of squares is the loss of that kind. log: log10 lam - log10 max(y, floor); raw: (lam - y) / scale; poisson: sign(lam - y) sqrt(2 [lam - y + y ln(y / lam)]), zero counts included; chi2: (lam - y) / sigma.
loss
python
def loss(kind: str, lam, y, scale: float=1.0, sigma=None, mask=None, floor: float=0.5)Sum of squared residuals over the mask (last axis); the Poisson kind sums to the deviance.
deviance
python
def deviance(lam, y, mask=None)Poisson deviance 2 sum [lam - y + y ln(y / lam)] over the last axis, zero counts included.
Criteria
python
class Criteria(NamedTuple):
aic: float
bic: floatinformation_criteria
python
def information_criteria(deviance: float, k: int, n: int) -> CriteriaAIC = D + 2k and BIC = D + k ln n, with the deviance standing in for -2 ln L (the saturated model's constant cancels in any comparison on the same data).
reflectofit.model
A fit model: a stack, an instrument and the free parameters with their bounds. Parameter names: `<layer>.thickness`, `<layer>.roughness`, `<layer>.sld` (real part), `<layer>.isld` (imaginary part) for each named layer and `substrate`; instrument fields by name (`i0`, `background`, `tth_offset`, `sigma_q`, `dq_over_q`, `kalpha2_ratio`, `sample_length`, `beam_sigma`); and `log10_background`, which sets `background` to 10**value.
Model
python
@dataclass
class Model:
stack: Stack
instrument: Instrument = field(default_factory=Instrument)
free: dict[str, tuple[float, float]] = field(default_factory=dict)Model.names
python
@property
def names(self) -> list[str]Model.bounds
python
@property
def bounds(self) -> tuple[NDArray, NDArray]Model.value
python
def value(self, key: str) -> floatModel.start
python
def start(self) -> NDArrayStarting values, clipped into the bounds.
Model.batch
python
def batch(self, theta: NDArray) -> tuple[tuple[NDArray, NDArray, NDArray], Instrument]Packed (B, N) arrays and a per-curve instrument for parameter rows theta (B, P).
Model.curves
python
def curves(self, q: NDArray, theta: NDArray) -> NDArrayObserved reflectivity (B, Q) for parameter rows theta (B, P).
Model.with_values
python
def with_values(self, theta: NDArray) -> 'Model'A copy with the free parameters set to theta (P,).
Model.select
python
def select(self, names) -> 'Model'The same model with only `names` free (others held at their current values); `names` is a sequence of parameter names or a boolean mask over `self.names`.
load
python
def load(path: str | Path) -> ModelRead a model TOML: [instrument], [[layer]] top first, [substrate], [fit] bounds. A layer gives `sld = [re, im]` (1e-6 Å⁻²) or `material` and `density` (g/cm³).
dumps
python
def dumps(model: Model) -> strTOML text for a model (values as given or fitted).
reflectofit.resolution
Nodes and weights for the resolution integral, chosen per measured point. Each point integrates R against its Gaussian over ±SPAN σ. R has a square-root kink at the critical edge q_c of the substrate (kz = √(q² − q_c²)/2 of a semi-infinite medium changes from imaginary to real there; absorption rounds it over a width 8π Im ρ / q_c). A finite layer enters R only through even functions of its own kz, Névot–Croce factors included, so its edge is smooth, though R can change fast there. Uniform nodes converge slowly on a kink, so the window is split at every edge inside it (Kα₂ edges included; splitting at a smooth edge costs nodes, not accuracy), and each piece [l, r] is mapped from s in [0, 1] so that the distance to an edge end goes as s²: - edge at both ends: u = l + (r − l) sin²(π s / 2); - edge at the left end: u = l + (r − l) s²; at the right end: u = r − (r − l) (1 − s)²; - no edge: u = l + (r − l) s. R is then smooth in s, and each piece gets Gauss–Legendre panels of PANEL nodes, as many as the larger of: the change of every layer's phase or evanescent decay d_j |√(q_r² − q_cj²) − √(q_l² − q_cj²)| across it, summed over the layers (Kα₁ and Kα₂ images apart, the larger), over PHASE_PER_PANEL; and its length over GAUSS_PER_PANEL σ; both times the largest stretch of the map (2 with an edge at one end, π/2 at both). Where a medium absorbs, waveguide resonances between the lowest and highest critical q are about as narrow as the narrowest absorbing edge width w, and the pieces there get nodes RESONANCE · w apart (in q). Between the lowest and highest critical q, multilayers without absorption also have narrow features whose width no edge sets: in a superlattice, the transmission resonances of its first Kronig–Penney miniband, which narrow with the number of periods (physics review of ReflectoFit#8). There each panel is compared with its two halves and split while they differ by more than REFINE_TOL of the point's integral over that range, up to MAX_DEPTH halvings. The tolerance holds per panel; the sum of those differences is the point's estimated relative error, which `layout` returns, and `forward` warns when it exceeds REFINE_TOL or a point stops at MAX_DEPTH. So only points near edges pay for them, and the nodes depend only on the curve's own stack and instrument, never on the rest of a batch. The counts and node positions are read from parameter values and are not traced; gradients come through R, the node positions q + σ u and the weights.
Edges
python
class Edges(NamedTuple):
qc: np.ndarray
width: np.ndarray
depth: np.ndarray
image: np.ndarray
light: floatThe critical edges of one curve: q_c [Å⁻¹] of every medium denser than the ambient, its edge width 8π Im ρ / q_c (0 for a real SLD), its thickness (0 for the substrate) and its image (0 for Kα₁, 1 for the Kα₂ copy at q_c λ₂/λ₁ in nominal q); `light` is the total thickness of the layers not denser than the ambient, which have no edge.
edges
python
def edges(
sld: np.ndarray,
thickness: np.ndarray,
kalpha2_scale: float | None = None,
) -> EdgesThe edges of one curve (sld, thickness (N,), ambient first; 1e-6 Å⁻², Å). With `kalpha2_scale` = λ₂/λ₁ the Kα₂ edges are added.
layout
python
def layout(
q: np.ndarray,
sigma: np.ndarray,
packed: tuple[np.ndarray, np.ndarray, np.ndarray],
wavelength: float,
kalpha2_wavelength: float | None = None,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]Nodes of a batch of curves (q, sigma (B, Q) true q and σ; packed (B, N) media), by the rules of the module docstring: node counts per point (B·Q,); for every node, point after point, its position u in units of σ and its weight (Gaussian, normalised per point); and per point the estimated relative error of the refined range and whether refinement stopped at MAX_DEPTH.
reflectofit.staged
Staged gradient fitting: the refine strategy of ReflectoLearn, for any differentiable model. A fit runs stages in order. Each stage frees some parameters and minimises one loss with Adam for a fixed number of steps; the other parameters are held at their current values. Parameters live on bounds through a sigmoid of a latent variable, so they never leave [lo, hi]. Keys freed by any stage so far are `active`; the model receives them, so it can switch an effect on only once a stage has fitted it (ReflectoLearn turns resolution and the oxide on that way). An optional Occam penalty pulls chosen parameters towards a neutral value, scaled by their bound range. The model is a callable `simulate(values, active) -> R` with `values` a dict of (B,) tensors and R (B, Q) linear reflectivity; `reflectofit.fit.staged` builds one from a `Model`. Observed data are given as log10 R (`log_y`), as in ReflectoLearn; with counts, log10(counts / counts_scale). The losses are those of ReflectoLearn `refine/losses.py`: per curve, a mean over the masked points, then the mean over the batch.
log_mse
python
def log_mse(r: Tensor, log_y: Tensor, mask: Tensor) -> TensorMSE of log10 R: sensitive to the fringes and the dynamic range.
linear_mse
python
def linear_mse(r: Tensor, log_y: Tensor, mask: Tensor) -> TensorMSE of R: dominated by the plateau and the critical edge (scale, alignment).
correlation
python
def correlation(r: Tensor, log_y: Tensor, mask: Tensor) -> Tensor1 - Pearson correlation of log10 R: blind to scale and attenuation, so it matches the fringe period.
log_gradient
python
def log_gradient(r: Tensor, log_y: Tensor, mask: Tensor) -> Tensorlog10 MSE plus 10 x the MSE of its point-to-point slope.
hybrid
python
def hybrid(r: Tensor, log_y: Tensor, mask: Tensor) -> Tensor20 x linear MSE + log10 MSE.
poisson_deviance
python
def poisson_deviance(counts_scale: float) -> LossFnPoisson deviance per point for counts = counts_scale · R, zero counts included. It is the likelihood only on the measured points with their raw counts.
loss_fn
python
def loss_fn(kind: str, counts_scale: float | None=None) -> LossFnA loss by name: one of LOSSES, or "poisson" (needs counts_scale).
Param
python
@dataclass(frozen=True)
class Param:
start: float | Tensor
lo: float | Tensor
hi: float | TensorStart value and bounds; each a number or a (B,) tensor, one value per curve.
Stage
python
@dataclass(frozen=True)
class Stage:
name: str
free: tuple[str, ...]
loss: str = 'log_mse'
steps: int = 200
lr: float = 0.03
q_min: float | None = None
q_max: float | None = NoneWhich parameters to free, with which loss, for how many Adam steps.
StagedFit
python
class StagedFit:
def __init__(
self,
simulate: Simulate,
params: dict[str, Param],
log_y: Tensor,
mask: Tensor | None = None,
*,
q: Tensor | None = None,
counts_scale: float | None = None,
penalty: dict[str, float] | None = None,
penalty_weight: float = 0.0,
)The state of a staged fit of B curves: values, bounds and active keys. `log_y` (B, Q) is log10 of the observed reflectivity; points where it is not finite (zero counts) are left out of every loss except the Poisson deviance, which counts them.
StagedFit.curve
python
def curve(self, active: set[str] | None=None) -> TensorThe model at the current state (B, Q).
StagedFit.loss
python
def loss(self, kind: str='log_mse', per_curve: bool=False) -> TensorA loss at the current state; per_curve gives (B,) values instead of the batch mean.
StagedFit.optimize
python
def optimize(
self,
keys,
loss: str,
steps: int,
lr: float,
q_min: float | None = None,
q_max: float | None = None,
) -> NoneAdam on `keys` through their sigmoid latents; the result is written back to the state. Does not change `active` (a stage does, see `run`).
StagedFit.run
python
def run(self, stage: Stage) -> NoneFree the stage's keys (they become active, and stay so) and optimise them.
model_simulate
python
def model_simulate(model, q) -> SimulateA `simulate` for a `reflectofit.model.Model`: values keyed by the model's parameter names (see `Model`), forward model on the torch backend in the dtype of the values.
reflectofit.structure
Layer stacks: ambient on top, substrate at the bottom. A medium is (SLD [1e-6 Å⁻², complex, Im > 0 absorbs], thickness [Å], roughness [Å] of the interface above it). `Stack.packed()` gives the arrays every backend takes, ambient first.
Layer
python
@dataclass(frozen=True)
class Layer:
sld: complex
thickness: float = 0.0
roughness: float = 0.0
name: str = ''Stack
python
@dataclass(frozen=True)
class Stack:
layers: list[Layer]
substrate: Layer
ambient: complex = 0jStack.names
python
@property
def names(self) -> list[str]Names of the layers and the substrate, top first; unnamed ones are layer0, layer1, ...
Stack.packed
python
def packed(self) -> Packedas_batch
python
def as_batch(model) -> tuple[Packed, bool](B, N) arrays from a Stack, a packed tuple, or a sequence of either; and whether it was one.
check_packed
python
def check_packed(sld: NDArray, thickness: NDArray, roughness: NDArray) -> NoneShapes (B, N), at least ambient and substrate, no gain, no negative lengths.
reflectofit.torch_backend
Differentiable backend: the same Parratt recursion and instrument model in torch. complex128 when q is float64 (the default), complex64 when q is float32. Large batches run in chunks so the (B, Q, N) wavevector array stays bounded.
parratt
python
def parratt(q: Tensor, sld: Tensor, thickness: Tensor, roughness: Tensor) -> Tensor|r|² for q (B, Q) and media (B, N), ambient first; Im SLD > 0 absorbs. Returns (B, Q).
forward
python
def forward(q, model, instrument: Instrument | None=None, chunk: int=CHUNK) -> TensorObserved reflectivity as a differentiable tensor; arguments as `reflectofit.forward`. Parameters given as tensors (packed tuple, instrument fields) keep their graph and work under torch.func transforms; for tensors no value is checked (Im SLD >= 0 is the caller's to keep), and a tensor instrument field always counts as switched on.
reflectofit.units
Angles, wavelengths and resolution widths. Lengths in Å, angles in degrees.
tth_to_q
python
def tth_to_q(
tth: ArrayLike,
wavelength: float = CU_KALPHA1,
) -> NDArray[np.float64] | floatScattering vector q = 4π/λ sin(2θ/2) [Å⁻¹] from 2θ [deg].
q_to_tth
python
def q_to_tth(q: ArrayLike, wavelength: float=CU_KALPHA1) -> NDArray[np.float64] | float2θ [deg] from q [Å⁻¹].
fwhm_to_sigma
python
def fwhm_to_sigma(fwhm: ArrayLike) -> NDArray[np.float64] | floatGaussian standard deviation from a full width at half maximum.