Specs

Validated value-types for the preprocess algorithms — beam selection, rocking-curve geometry, mosaicity, orientation search, convergence sweeps. Pydantic parses YAML at the boundary and hands these frozen dataclasses to the steps, so the algorithm contract stays pydantic-free; invalid bounds are unrepresentable by construction.

Validated parameter value-types for the preprocess calibration steps.

These are the parsed forms of the sweep parameters (parse, don’t validate): each frozen dataclass validates its own invariants in __post_init__, so an invalid spec is unrepresentable and the pure Plan -> Plan steps that consume them never re-validate. The pydantic config blocks at the YAML edge (diffBloch.config.schema) parse into these via to_search / to_grid and delegate their validation here – one home for each rule, no drift between config and function.

They are plain frozen dataclasses (the codebase’s one value-object vocabulary, like RefinableParams), so the algorithm contract stays pydantic-free: pydantic parses YAML at the edge but never rides into a step. A direct/test caller constructs them the same way the config does and gets the same construction-time error.

Failures raise ValueError (fail-fast): the callers are config-load and direct construction. A boundary adapter that needs to surface validation errors as values rather than as exceptions (for a TUI or batch runner) can wrap these raising constructors without changing the step contract.

class diffBloch.specs.Absorption(enabled: bool = False)[source][source]

Bases: object

Enable the element/B-factor-dependent absorptive Bloch-wave model.

enabled: bool = False
class diffBloch.specs.ApparentThicknessNetwork(enabled: bool = True, num_samples: int = 40, sample_thickness: bool = False, form: Literal['min_thickness'] = 'min_thickness', min_thickness: float = 100.0, max_thickness: float = 2000.0, init_seed: int = 0)[source][source]

Bases: object

Legacy apparent-thickness MLP settings used by the default refinement path.

enabled: bool = True
num_samples: int = 40
sample_thickness: bool = False
form: Literal['min_thickness'] = 'min_thickness'
min_thickness: float = 100.0
max_thickness: float = 2000.0
init_seed: int = 0
class diffBloch.specs.BeamSelection(rsg: float = 0.66, dsg: float = 0.0015, integration: IntegrationGeometry = <factory>)[source][source]

Bases: object

Validated cutoffs for the select_beams Klar et al. (2023) active-set filter.

The knobs jointly define each orientation’s active beam set. rsg (relative excitation error cutoff – a reflection is kept when |Sg| / sg_max < rsg) must be positive, since at zero it rejects every reflection. dsg (the absolute excitation-error margin in the sg_max - |Sg| > dsg test) carries no positivity invariant – a negative margin legitimately loosens the cone – so it is left unconstrained rather than fabricate a bound. integration (an IntegrationGeometry) supplies the semiangle that scales sg_max and the geometry that fixes its lever arm; it is shared with the RockingCurve integrator (one physical angle, so the two cannot disagree).

The default cutoffs are the values used by the default preprocess path.

rsg: float = 0.66
dsg: float = 0.0015
integration: IntegrationGeometry
class diffBloch.specs.ConvergenceTest(g_max_step: float = 0.1, sg_max_step: float = 0.005, tilt_steps_step: int = 2, num_passes: int = 2)[source][source]

Bases: object

Step sizes for simulation-convergence testing.

The test increases g_max, sg_max, and the number of rocking-curve tilt steps. Each control is advanced until consecutive simulations differ by less than the requested tolerance. Multiple passes revisit the controls after the others have changed.

g_max_step: float = 0.1
sg_max_step: float = 0.005
tilt_steps_step: int = 2
num_passes: int = 2
class diffBloch.specs.ConvergenceTolerance(r_factor_threshold: float = 0.005, max_iterations: int = 100)[source][source]

Bases: object

Stopping rule for a convergence sweep: stability threshold + a runaway cap.

A convergence sweep grows a simulation-accuracy knob and stops the first time consecutive simulations stop changing: r_factor_threshold is the largest consecutive-simulation R-factor still counted as “converged”. max_iterations is the hard cap on sweep steps before non-convergence is raised; it also gives iterate_until’s otherwise-bare cap a home.

The stopping rule is deliberately simple: the first below-threshold step stops the sweep – no patience window and no null-step handling.

r_factor_threshold: float = 0.005
max_iterations: int = 100
class diffBloch.specs.FrameSelection(min_observed: int = 0)[source][source]

Bases: object

Validated criterion for the select_frames per-rotation (whole-frame) drop.

The sibling of BeamSelection: where that prunes reflections within a frame, select_frames drops whole frames whose observed pattern is too sparse to inform the fit, for the beam-damaged tail of a rotation scan. min_observed is the fewest strong observed reflections (intensity > 3 * sigma, strict) a frame must carry to be kept; frames below it are dropped. The count is model-independent – it reads the observed pattern only, never the calculated fit – so it cannot circularly keep the frames the current model already explains.

min_observed == 0 keeps every frame (the disabled / no-op default – opting in requires a positive threshold); a negative count is meaningless and rejected. The drop is derived from a data-quality floor rather than a hard-coded list of frame indices.

min_observed: int = 0
class diffBloch.specs.IntegrationGeometry(semiangle: float = 1.0, geometry: Literal['continuous_rotation', 'precession'] = 'continuous_rotation')[source][source]

Bases: object

The angular integration range a rotation frame sweeps – one shared physical value.

As the crystal rocks, each reflection is integrated over an angular range. That range sets BOTH the Klar beam-selection window (the excitation-error span sg_max) and the rocking-curve tilt span, because it is the same physical angle. Modelling it once here – shared by BeamSelection and RockingCurve rather than each declaring its own – makes it impossible to give the two consumers different values (the drift a duplicated field invites).

semiangle (degrees) is the tilt half-width / integration cone half-angle; it must be positive (zero rejects every reflection). geometry is the data-collection sweep – distance from the goniometer rock axis for continuous_rotation, distance from the beam for precession – which fixes the sg_max lever arm and the tilt axis, so both consumers must agree on it too.

semiangle: float = 1.0
geometry: Literal['continuous_rotation', 'precession'] = 'continuous_rotation'
class diffBloch.specs.NelderMeadSearch(step_size: float = 0.05, max_iterations: int = 60, x_tolerance: float = 0.001, f_tolerance: float = 0.001, penalize_fewer_reflections: bool = True)[source][source]

Bases: object

Bounds for the optimize_orientation local Nelder-Mead search (degrees).

Optimizes the three goniometer-correction angles (alpha, beta, omega) (see goniometer_rotation()) directly with scipy.optimize.minimize(method="Nelder-Mead"), seeded from a fixed initial simplex of edge length step_size around the seed orientation ((0, 0, 0)) – pick step_size comfortably larger than the expected misorientation of the seed (PETS-derived) orientation.

step_size: float = 0.05
max_iterations: int = 60
x_tolerance: float = 0.001
f_tolerance: float = 0.001
penalize_fewer_reflections: bool = True
class diffBloch.specs.OrientationSelection(ignore_orientations: tuple[int, ...] = ())[source][source]

Bases: object

Original PETS rotation indices excluded from every downstream experiment stage.

Indices are zero-based in the source .cif_pets rotation order. Filtering happens before the train/validation split, beam construction, orientation/thickness fitting, inference, and structure refinement. Duplicate or negative indices are rejected so the recorded experiment selection has one unambiguous identity; the experiment boundary checks the data-dependent upper bound once the number of PETS rotations is known.

ignore_orientations: tuple[int, ...] = ()
class diffBloch.specs.PerTiltCoupling(g_max: float = 2.25, sg_max: float = 0.01)[source][source]

Bases: object

Independent beam selection and Bloch basis for every rocking-curve tilt.

For each individual sub-tilt, recompute Sg over the radial g_max pool, retain only |Sg| < sg_max, and build a structure-factor gather and structure matrix for that tilt’s exact beam set. No beam set is shared or unioned across tilts.

g_max: float = 2.25
sg_max: float = 0.01
class diffBloch.specs.RockingCurve(sampling: int = 42, integration: IntegrationGeometry = <factory>)[source][source]

Bases: object

Validated geometry for rocking-curve integration (tilts as sub-orientations).

A rotation-electron-diffraction frame integrates each reflection’s intensity as the crystal sweeps through the Ewald sphere, so the forward model samples sampling slightly-tilted sub-orientations spanning +/- the integration semiangle and sums their intensities. sampling is the number of tilts; sampling = 1 is the identity (a single static solve), which is how the integration composes off by default. integration (an IntegrationGeometry) supplies the tilt half-width semiangle – the same physical angular range as the Klar beam-selection window, shared with BeamSelection so the two cannot disagree – and the geometry that selects the sweep (continuous_rotation, goniometer x-axis tilts; or precession, uniformly sampled around a fixed-angle cone).

sampling: int = 42
integration: IntegrationGeometry
class diffBloch.specs.ScoredHklSelection(klar: BeamSelection = <factory>, g_max: float = 1.6)[source][source]

Bases: object

The SCORED selector: the Klar window intersected with a scoring-resolution cap.

When a fit re-derives its reflection sets per trial under a coupling policy, the scored set is not the solve union – it is the union filtered back down to the reflections actually compared against the observed pattern. That is two filters: the Klar relative-excitation window followed by a radial |g| cap. klar supplies the former (BeamSelectionrsg / dsg + the shared IntegrationGeometry), and g_max the latter. No lower-shell bound is modelled; add one when a dataset needs it.

g_max is the scoring-resolution cap, given its own named home here so it is distinct from the seed beam-pool radius – the two may be numerically equal but are separate quantities. It must be positive.

klar: BeamSelection
g_max: float = 1.6
class diffBloch.specs.ThicknessGrid(min_thickness: float = 5.0, max_thickness: float = 2000.0, n_steps: int = 100)[source][source]

Bases: object

Validated grid of candidate thicknesses for optimize_thickness (Angstroms).

optimize_thickness evaluates n_steps candidates spaced evenly from min_thickness to max_thickness (inclusive) and keeps the lowest-wR2 one. The defaults span 5 A to 2000 A in 100 steps.

min_thickness: float = 5.0
max_thickness: float = 2000.0
n_steps: int = 100
class diffBloch.specs.TiltIndependent[source][source]

Bases: object

The default coupling: one beam set shared across every rocking-curve tilt.

The baseline: the active beam set select_beams picks for the nominal orientation is reused, unchanged, at every tilt of the rocking curve. Fieldless because it carries no policy of its own – the shared set is already fixed on the plan; it is the identity member of the coupling discriminated union, chosen by construction when a run does not want the tilt-dependent per-chunk re-selection.

class diffBloch.specs.UnionCoupling(fixed_n_segments: int = 12, g_max: float = 2.25, sg_max: float = 0.01, union_adaptive: bool = True, union_max_new_beams_pct: float = 0.01)[source][source]

Bases: object

Tilt-segment-union beam coupling: per-tilt-chunk beam sets, not one set for the whole curve.

The coupling policy for rocking-curve integration: it partitions the B tilts into fixed_n_segments contiguous, disjoint chunks and gives each chunk its own coupled beam set: the union of the excited-beam masks at the chunk’s two boundary tilts. A beam is excited at a tilt when |Sg| < sg_max and |g| < g_max (a hard excitation-error + coupling-radius cutoff, distinct from the Klar relative filter of BeamSelection). Because a sharp reflection drifts through the Ewald sphere as the crystal rocks, the excited set genuinely differs across the curve; one tilt-independent set either over-couples (slow) or drops beams a later tilt needs. The per-chunk union is the compromise this policy strikes. Each reflection’s full rocking curve is later reassembled across chunks before the mosaicity reduction (the smoothing span can cross chunk boundaries).

g_max is the coupling radius: a beam couples when |g| < g_max. The cutoff is the physical solve radius, with no additional margin. sg_max is the excitation-error cutoff. The mean-inner-potential u0 and beam energy are experiment quantities threaded in at build time, not policy knobs.

union_adaptive chooses how the chunk boundaries are placed. False uses fixed_n_segments fixed even-sized chunks. True places boundaries by recursive bisection: a tilt range is split further only while its midpoint adds more than union_max_new_beams_pct of the boundary union’s beams (else the range is frozen as one chunk), so segments are dense where the excited set drifts and sparse where it is stable. In the adaptive mode fixed_n_segments is ignored.

The defaults suit the standard rocking-curve recipe (12 fixed even-sized chunks, fixed mode).

fixed_n_segments: int = 12
g_max: float = 2.25
sg_max: float = 0.01
union_adaptive: bool = True
union_max_new_beams_pct: float = 0.01
class diffBloch.specs.TrialCoupling(policy: UnionCoupling | PerTiltCoupling, scored: ScoredHklSelection)[source][source]

Bases: object

Per-trial re-derivation of both reflection sets during an orientation fit.

The orientation objective under this coupling re-runs the whole forward at every trial orientation: it re-couples the SOLVE union (the excitation coupling of policy) and re-selects the SCORED set (scored) from that fresh union, so both sets track the trial orientation rather than staying pinned to the seed. Passed to optimize_orientation() (coupling=...) to opt a fit into that behaviour; its absence (None) keeps the tilt-independent fit (one fixed beam set across the search).

Bundling both selectors makes the invalid state – coupling active but a selector missing – unrepresentable, so the fit takes one optional parameter with no cross-parameter guard.

policy: UnionCoupling | PerTiltCoupling
scored: ScoredHklSelection
diffBloch.specs.assert_grid_covers_coupling(policy: UnionCoupling | PerTiltCoupling, grid_g_max: float) None[source][source]

Guarantee the |g| <= grid_g_max grid sphere spans every coupled beam difference (O(1)).

A coupled solve union admits only beams with |g| < g_max, so any pairwise difference is |g_j - g_i| < 2 * g_max (triangle inequality). When 2 * g_max <= grid_g_max the dense integer structure_factor_hkl sphere therefore contains every difference, so the per-segment gathers cannot address a reflection outside it – exactly the condition that makes build_structure_factor_gather() validate=False sound on the coupled fit path (it closes the silent-zero coverage gap the O(N^2) integrity checks otherwise catch). The radius is orientation-independent, so this one scalar comparison covers every trial of every rotation – checked at fit setup, failing loudly before any solve rather than silently gathering zeros deep in the search. The default recipe derives the grid as 2 * g_max, so it only bites a programmatic caller that hand-builds a grid smaller than its coupling radius needs.