Source code for de_twin.render.config
"""Render-side options (the renderer knobs of VirtualSpecimenProps plus the twin's
wave-optical TEM model)."""
from __future__ import annotations
from dataclasses import dataclass
[docs]
@dataclass
class RenderConfig:
# ---- TEM imaging ------------------------------------------------------
diffraction_contrast_scale: float = 1.0 # scales the Bragg loss from the bright-field beam
mip_phase: bool = True # mean-inner-potential phase (gives Fresnel fringes)
edge_taper_nm: float = 1.0 # Gaussian roughness of specimen edges applied to the MIP phase
refraction_loss: bool = True # electrons refracted beyond the imaging band by steep edges are lost
phase_texture_scale: float = 1.0 # amorphous atomic-granularity phase (gives Thon rings)
texture_bandlimit_nm: float = 0.07 # Gaussian sigma: atomic form-factor band limit
amplitude_contrast: float = 0.07 # absorptive part of the amorphous texture (w)
lattice_fringes: bool = True
#: "kinematic": each resolved beam that reaches the image is a phase grating of its own
#: kinematic strength, a = sqrt(2 f) for a Friedel pair carrying f of the electrons, grown
#: with the thickness (heavy crystals give strong fringes, light ones weak), up to its
#: MAX_FRINGE_BEAMS strongest pairs. "fixed": every crystal's fringes share a total phase
#: `lattice_phase_rad` (the older model).
lattice_fringe_model: str = "kinematic"
#: The kinematic amplitude times this: what dynamical scattering (saturation in thick,
#: heavy crystals), vibration, drift and the recording take from the fringes. Calibrated
#: on Ted Pella's lattice images (646 Au [001], 675 Si <011>: fringe contrast 27 % and
#: 34 %, twice the kinematic value).
lattice_fringe_efficiency: float = 0.5
lattice_phase_rad: float = 0.15 # total phase amplitude of resolved lattice fringes ("fixed")
#: TEM imaging renders a view this fraction of the field larger on each side and
#: serves a stage move that stays inside the margin by cropping it: the image moves
#: rigidly with the specimen, so a joystick nudge costs a crop, not a render. 0 off.
pan_margin: float = 0.15
#: Pad by the objective's point spread instead (`renderer.transfer_spread_px`, x1.3,
#: rounded up to one of a few guard sizes, ``min_margin_px`` to ``max_margin_px``): a
#: smaller raster and FFT near focus and at low magnification, a wider guard at large
#: defocus and high magnification. ``pan_margin`` is then unused.
adaptive_margin: bool = True
min_margin_px: int = 64
max_margin_px: int = 256
#: Padded rasters kept for cropping (a jump back to a recent place is a crop too).
pan_cache_size: int = 6
#: A new padded raster takes the specimen it shares with a cached one (same sampling, on
#: the same world-fixed pixel lattice) from that one's field map when they overlap by at
#: least ``reuse_min_overlap`` of its area, and rasterises only the rest.
reuse_rasters: bool = True
reuse_min_overlap: float = 0.25
#: Threads a large raster is rasterised on, in row strips (1 = serial). Each strip's
#: numba kernels get the matching share of numba's threads.
raster_threads: int = 4
#: Interactive use (dragging the stage, zooming): while the view keeps changing, a
#: view the cache cannot crop is rendered at up to ``preview_side`` squared and upsampled
#: (~35-70 ms rather than ~0.3-0.9 s); the first time the same view is asked for
#: again — the next frame after you stop — it is rendered in full. Off by default,
#: so a library caller always gets the full render.
interactive: bool = False
#: Live view: after each new view, render the view that repeating the last change would
#: give (the next magnification rung, tilt, focus or stage step) in the background, so
#: stepping through a series is a crop. One view at a time; the latest wins.
prefetch: bool = False
preview_side: int = 256
#: How long (wall-clock seconds) the view must stay put before it counts as stopped
#: and is rendered in full. Live view asks for several frames per drag step, so
#: "asked for the same view twice" is not "stopped".
settle_s: float = 0.3
illumination_profile: bool = True # draw the edge of the illuminated disk when it is in view
texture_seed_salt: int = 0x7E57
# ---- diffraction (de_twin.crystal library + DiffractionCache) -------
diffraction_cache_mb: int = 256
max_g_inv_nm: float = 25.0 # reciprocal-lattice extent (raise it for HOLZ reflections)
film_halo: bool = True # support-film halo under every crystalline pattern
scaled_diffraction_saturation: bool = True # per-material amorphous saturation thickness
diffuse_scattering: bool = True # (1-T) redistributed as a screened-Rutherford background
beam_stop_radius_px: float = 0.0 # SAED beam stop, detector pixels; 0 = none
# ---- STEM -----------------------------------------------------------
probe_footprint_blend: bool = True
add_descan: bool = False
descan_ramp_scale: float = 1.0
descan_ramp_px: tuple[float, float, float] = (6.0, 4.0, 4.0) # C++ legacy x span, y span, diag
intensity_jitter: bool = True # C++ per-scan-point 0.9..1.1 fluctuation
park_on_feature: bool = False # C++ default was on; off keeps the probe on the optic axis
# ---- coherent 4D-STEM (render/coherent.py) ----------------------------
# "coherent": probe wavefunction x transmission function, |FFT|^2 (ptychography / tcBF work);
# "kinematic": cached disk patterns (fast, no interference);
# "auto": coherent when the simulation grid fits coherent_max_grid without cutting the probe
# window and the binned pattern is <= coherent_auto_max_pattern_px, or the scan step is
# below 2x the probe size (ptychographic overlap); else kinematic.
stem_model: str = "auto"
coherent_max_grid: int = 1024 # largest simulation grid side per pattern
coherent_auto_max_pattern_px: int = 256 * 256
coherent_probe_tail: float = 6.0 # window margin beyond the geometric probe radius, in lambda/alpha
coherent_mode_power: float = 0.98 # partial coherence: keep probe modes up to this power fraction
coherent_max_modes: int = 4
coherent_focal_samples: int = 0 # 0 = auto (1/3/5 Gauss-Hermite samples of the focal spread)
coherent_focal_spread: bool = True # temporal partial coherence (OpticsState.focal_spread_nm)
coherent_source_size: bool = True # spatial partial coherence (OpticsState.source_size_nm)
coherent_beam_tilt: bool = True # beam tilt enters chi (coma etc.) and shifts the probe
coherent_incoherent_scattering: bool = True # Bragg disks + diffuse background added incoherently
# t(r) band limit K_t, 1/nm: scattering to |g| <= K_t is coherent, beyond it incoherent. 0 = auto:
# k_det + alpha/lambda (everything that can reach the detector is coherent)
coherent_object_bandwidth_inv_nm: float = 0.0
coherent_field_max_px: int = (
8_000_000 # larger scan fields build transmission tiles per block (~1 GB peak at 8M)
)
coherent_cache_mb: int = 64 # computed pattern blocks kept for frame-by-frame rendering
coherent_read_ahead: int = 2 # live 4D-STEM: blocks of the scan computed ahead in the background
# ---- caching --------------------------------------------------------
fieldmap_cache_size: int = 4
time_quantum_s: float = 0.1 # re-rasterise a time-dependent specimen at most this often