Source code for de_twin.column.lowdose

"""Low dose: the column's Search / View / Focus / Record areas.

Each area stores its own magnification, spot size, intensity, defocus offset (from the
Record focus), image shift and beam shift. Switching area stores the live settings into the
area left, as a real column's low-dose mode does (an operator adjusts an area while in it),
and applies the one entered. Defocus is kept relative to Record's, so refocusing in Record
carries View's large offset along.

On a realistic column (`OpticsConfig.realistic`) the areas do not line up by themselves:
each magnification has its own image rotation, true pixel size, image-shift matrix and image
offset, and View's high defocus changes its scale and rotation again. That is what SerialEM's
low-dose calibration (aligning View to Record, setting the area offsets) measures.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass, field

AREAS = ("Search", "View", "Focus", "Record")

#: Defaults when low dose is switched on: View this much below Record's magnification and
#: this far underfocus; Search further out; Focus image-shifted along x (the tilt axis) by
#: this many image-shift units.
VIEW_MAG_FACTOR = 8.0
VIEW_DEFOCUS_UM = -200.0
SEARCH_MAG_FACTOR = 50.0
SEARCH_DEFOCUS_UM = -200.0
FOCUS_OFFSET_UNITS = 1.5


[docs] @dataclass class LowDoseArea: magnification: float spot_size: int intensity: float defocus_offset_um: float = 0.0 image_shift: tuple[float, float] = (0.0, 0.0) beam_shift: tuple[float, float] = (0.0, 0.0)
[docs] def as_dict(self) -> dict: d = asdict(self) d["image_shift"] = list(self.image_shift) d["beam_shift"] = list(self.beam_shift) return d
[docs] @dataclass class LowDose: """The column's low-dose state; `column.Column` owns one and drives it.""" enabled: bool = False area: str = "Record" areas: dict = field(default_factory=dict) base_focus_um: float = 0.0 # Record's focus # ------------------------------------------------------------- helpers @staticmethod def _rung(ladder, mag: float) -> float: return float(min(ladder, key=lambda m: abs(m - mag)))
[docs] def seed_from(self, column, ladder) -> None: """Areas from the column's current state (Record = now).""" s = column._s rec = area_from_state(s) self.base_focus_um = float(s.defocus_um) focus = area_from_state(s) focus.image_shift = (s.image_shift_um.x + FOCUS_OFFSET_UNITS, s.image_shift_um.y) view = area_from_state(s) view.magnification = self._rung(ladder, s.magnification / VIEW_MAG_FACTOR) view.defocus_offset_um = VIEW_DEFOCUS_UM search = area_from_state(s) search.magnification = self._rung(ladder, s.magnification / SEARCH_MAG_FACTOR) search.defocus_offset_um = SEARCH_DEFOCUS_UM self.areas = {"Search": search, "View": view, "Focus": focus, "Record": rec} self.area = "Record"
[docs] def store(self, column) -> None: """The live settings into the current area (in diffraction the area keeps its imaging magnification).""" from . import ladders as L s = column._s a = self.areas[self.area] if column._fm != L.FM_DIFF: a.magnification = float(s.magnification) a.spot_size = int(s.spot_size) a.intensity = float(s.intensity) a.image_shift = (float(s.image_shift_um.x), float(s.image_shift_um.y)) a.beam_shift = (float(s.beam_shift_um.x), float(s.beam_shift_um.y)) if self.area == "Record": self.base_focus_um = float(s.defocus_um) a.defocus_offset_um = 0.0 else: a.defocus_offset_um = float(s.defocus_um) - self.base_focus_um
[docs] def apply(self, column, name: str, setters) -> None: """Enter area *name* (the live settings are NOT stored: call `store` first). All or nothing: if a setting is refused the column is left as it was.""" from . import ladders as L a = self.areas[name] saved = (column._s.copy(), column._fm, getattr(column, "_last_imaging_fm", None)) try: if column._fm != L.FM_DIFF: # diffraction has no magnification to set setters["Magnification"](column, a.magnification) setters["SpotSize"](column, a.spot_size) setters["Intensity"](column, a.intensity) setters["Defocus"]( column, self.base_focus_um + (0.0 if name == "Record" else a.defocus_offset_um) ) setters["ImageShift"](column, a.image_shift) setters["BeamShift"](column, a.beam_shift) except Exception: column._s, column._fm = saved[0], saved[1] if saved[2] is not None: column._last_imaging_fm = saved[2] raise self.area = name
def area_from_state(s) -> LowDoseArea: """An area holding the column's current settings (no defocus offset).""" return LowDoseArea( float(s.magnification), int(s.spot_size), float(s.intensity), 0.0, (float(s.image_shift_um.x), float(s.image_shift_um.y)), (float(s.beam_shift_um.x), float(s.beam_shift_um.y)), ) def normalise_area(name) -> str | None: n = str(name).strip().lower() for a in AREAS: if a.lower() == n or (a == "Record" and n in ("exposure", "record")): return a return None