Skip to content

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) -> int

reflectofit.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,
) -> Curve
Read 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') -> None

stitch ​

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',
) -> float
q 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,
) -> float
Film 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) -> str

trust_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,
) -> FitResult
Trust-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,
) -> FitResult
Differential 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,
) -> FitResult
Staged 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,
) -> MCMCResult
Posterior 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,
) -> dict
Every 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 = None
Instrument 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) -> complex
Complex 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: float

information_criteria ​

python
def information_criteria(deviance: float, k: int, n: int) -> Criteria
AIC = 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) -> float

Model.start ​

python
def start(self) -> NDArray
Starting 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) -> NDArray
Observed 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) -> Model
Read 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) -> str
TOML 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: float
The 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,
) -> Edges
The 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) -> Tensor
MSE of log10 R: sensitive to the fringes and the dynamic range.

linear_mse ​

python
def linear_mse(r: Tensor, log_y: Tensor, mask: Tensor) -> Tensor
MSE of R: dominated by the plateau and the critical edge (scale, alignment).

correlation ​

python
def correlation(r: Tensor, log_y: Tensor, mask: Tensor) -> Tensor
1 - 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) -> Tensor
log10 MSE plus 10 x the MSE of its point-to-point slope.

hybrid ​

python
def hybrid(r: Tensor, log_y: Tensor, mask: Tensor) -> Tensor
20 x linear MSE + log10 MSE.

poisson_deviance ​

python
def poisson_deviance(counts_scale: float) -> LossFn
Poisson 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) -> LossFn
A 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 | Tensor
Start 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 = None
Which 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) -> Tensor
The model at the current state (B, Q).

StagedFit.loss ​

python
def loss(self, kind: str='log_mse', per_curve: bool=False) -> Tensor
A 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,
) -> None
Adam 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) -> None
Free the stage's keys (they become active, and stay so) and optimise them.

model_simulate ​

python
def model_simulate(model, q) -> Simulate
A `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 = 0j

Stack.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) -> Packed

as_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) -> None
Shapes (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) -> Tensor
Observed 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] | float
Scattering 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] | float
2θ [deg] from q [Å⁻¹].

fwhm_to_sigma ​

python
def fwhm_to_sigma(fwhm: ArrayLike) -> NDArray[np.float64] | float
Gaussian standard deviation from a full width at half maximum.

Each project section is built from that project’s docs/ directory.