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:
objectEnable the element/B-factor-dependent absorptive Bloch-wave model.
- 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:
objectLegacy apparent-thickness MLP settings used by the default refinement path.
- class diffBloch.specs.BeamSelection(rsg: float = 0.66, dsg: float = 0.0015, integration: IntegrationGeometry = <factory>)[source][source]¶
Bases:
objectValidated cutoffs for the
select_beamsKlar 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 thesg_max - |Sg| > dsgtest) carries no positivity invariant – a negative margin legitimately loosens the cone – so it is left unconstrained rather than fabricate a bound.integration(anIntegrationGeometry) supplies thesemianglethat scalessg_maxand thegeometrythat fixes its lever arm; it is shared with theRockingCurveintegrator (one physical angle, so the two cannot disagree).The default cutoffs are the values used by the default preprocess path.
- 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:
objectStep 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.
- class diffBloch.specs.ConvergenceTolerance(r_factor_threshold: float = 0.005, max_iterations: int = 100)[source][source]¶
Bases:
objectStopping 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_thresholdis the largest consecutive-simulation R-factor still counted as “converged”.max_iterationsis the hard cap on sweep steps before non-convergence is raised; it also givesiterate_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.
- class diffBloch.specs.FrameSelection(min_observed: int = 0)[source][source]¶
Bases:
objectValidated criterion for the
select_framesper-rotation (whole-frame) drop.The sibling of
BeamSelection: where that prunes reflections within a frame,select_framesdrops whole frames whose observed pattern is too sparse to inform the fit, for the beam-damaged tail of a rotation scan.min_observedis 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 == 0keeps 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.
- class diffBloch.specs.IntegrationGeometry(semiangle: float = 1.0, geometry: Literal['continuous_rotation', 'precession'] = 'continuous_rotation')[source][source]¶
Bases:
objectThe 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 byBeamSelectionandRockingCurverather 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).geometryis the data-collection sweep – distance from the goniometer rock axis forcontinuous_rotation, distance from the beam forprecession– which fixes thesg_maxlever arm and the tilt axis, so both consumers must agree on it too.
- 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:
objectBounds for the
optimize_orientationlocal Nelder-Mead search (degrees).Optimizes the three goniometer-correction angles
(alpha, beta, omega)(seegoniometer_rotation()) directly withscipy.optimize.minimize(method="Nelder-Mead"), seeded from a fixed initial simplex of edge lengthstep_sizearound the seed orientation ((0, 0, 0)) – pickstep_sizecomfortably larger than the expected misorientation of the seed (PETS-derived) orientation.
- class diffBloch.specs.OrientationSelection(ignore_orientations: tuple[int, ...] = ())[source][source]¶
Bases:
objectOriginal PETS rotation indices excluded from every downstream experiment stage.
Indices are zero-based in the source
.cif_petsrotation 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.
- class diffBloch.specs.PerTiltCoupling(g_max: float = 2.25, sg_max: float = 0.01)[source][source]¶
Bases:
objectIndependent beam selection and Bloch basis for every rocking-curve tilt.
For each individual sub-tilt, recompute
Sgover the radialg_maxpool, 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.
- class diffBloch.specs.RockingCurve(sampling: int = 42, integration: IntegrationGeometry = <factory>)[source][source]¶
Bases:
objectValidated 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
samplingslightly-tilted sub-orientations spanning +/- the integrationsemiangleand sums their intensities.samplingis the number of tilts;sampling = 1is the identity (a single static solve), which is how the integration composes off by default.integration(anIntegrationGeometry) supplies the tilt half-widthsemiangle– the same physical angular range as the Klar beam-selection window, shared withBeamSelectionso the two cannot disagree – and thegeometrythat selects the sweep (continuous_rotation, goniometer x-axis tilts; orprecession, uniformly sampled around a fixed-angle cone).- integration: IntegrationGeometry¶
- class diffBloch.specs.ScoredHklSelection(klar: BeamSelection = <factory>, g_max: float = 1.6)[source][source]¶
Bases:
objectThe 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.klarsupplies the former (BeamSelection–rsg/dsg+ the sharedIntegrationGeometry), andg_maxthe latter. No lower-shell bound is modelled; add one when a dataset needs it.g_maxis 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¶
- class diffBloch.specs.ThicknessGrid(min_thickness: float = 5.0, max_thickness: float = 2000.0, n_steps: int = 100)[source][source]¶
Bases:
objectValidated grid of candidate thicknesses for
optimize_thickness(Angstroms).optimize_thicknessevaluatesn_stepscandidates spaced evenly frommin_thicknesstomax_thickness(inclusive) and keeps the lowest-wR2 one. The defaults span 5 A to 2000 A in 100 steps.
- class diffBloch.specs.TiltIndependent[source][source]¶
Bases:
objectThe default coupling: one beam set shared across every rocking-curve tilt.
The baseline: the active beam set
select_beamspicks 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:
objectTilt-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
Btilts intofixed_n_segmentscontiguous, 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_maxand|g| < g_max(a hard excitation-error + coupling-radius cutoff, distinct from the Klar relative filter ofBeamSelection). 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_maxis the coupling radius: a beam couples when|g| < g_max. The cutoff is the physical solve radius, with no additional margin.sg_maxis the excitation-error cutoff. The mean-inner-potentialu0and beam energy are experiment quantities threaded in at build time, not policy knobs.union_adaptivechooses how the chunk boundaries are placed.Falseusesfixed_n_segmentsfixed even-sized chunks.Trueplaces boundaries by recursive bisection: a tilt range is split further only while its midpoint adds more thanunion_max_new_beams_pctof 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 modefixed_n_segmentsis ignored.The defaults suit the standard rocking-curve recipe (12 fixed even-sized chunks, fixed mode).
- class diffBloch.specs.TrialCoupling(policy: UnionCoupling | PerTiltCoupling, scored: ScoredHklSelection)[source][source]¶
Bases:
objectPer-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 tooptimize_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_maxgrid 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). When2 * g_max <= grid_g_maxthe dense integerstructure_factor_hklsphere therefore contains every difference, so the per-segment gathers cannot address a reflection outside it – exactly the condition that makesbuild_structure_factor_gather()validate=Falsesound 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 as2 * g_max, so it only bites a programmatic caller that hand-builds a grid smaller than its coupling radius needs.