Changelog#
All notable changes to de-twin are documented here.
Fragment files in upcoming_changes/ are assembled into this file by
towncrier when a release is prepared
(see upcoming_changes/README.rst for contributor instructions).
0.3.0 (2026-10-23)#
API and Behaviour Changes#
Magnification and camera-length ladders are one type,
de_twin.column.ladders.Ladder, which carries both rules for picking the nearest rung (snap_linearfor the column’s detents,snap_logarithmicfor calibration tables).Column.mag_ladder(),Column.cl_ladder(),Calibration.mag_ladder()andCalibration.cl_ladder()return it, and the calibration’s built-in tables moved tode_twin.column.ladders.Column.mag_ladder()andColumn.cl_ladder()now return lists, empty when the mode has no such ladder (they returnedNone), andcl_ladder()is in millimeters likeCalibration.cl_ladder()(it was centimeters). Inde_twin.column.ladders,DIFF_CAMLENGTHS_CMandSTEM_CAMLENGTHS_CMbecameDIFF_CAMERA_LENGTHS_MMandSTEM_CAMERA_LENGTHS_MM. TheCameraLengthproperty is still centimeters, as on the DE-TEM-Channel protocol.Arguments are spelled out:
cfgisconfigeverywhere (beam_current_pa(state, config=...), and so on), and catch-all**kwarguments have names that say what they carry. Several large modules were split by job, so a few names moved: the column’s property tables, vendor tables and errors (column.properties,column.vendor,column.errors), the CEOS corrector (column.ceos), coherent-imaging setup (render.coherent_setup), TEM contrast (render.tem_contrast) and the deapi server’s protocol, request, acquisition, property and result parts (faces.deapi_*).Calibration.defaultno longer takespost_magnification, which scaled the served and the rendered pixel together and so could not express a calibration error. SetOpticsConfig.post_magnificationinstead. (#39)
New Features#
OpticsConfig.post_magnificationputs the camera behind a constant post-column magnification: the rendered pixel is the calibrated one divided by it at every magnification, while the servedSpecimen Pixel Sizekeeps reporting the nominal value.calibration_truth()reports the factor and the true pixel size. (#39)A scan follows DE-Server’s timing for coil changes: it runs a repeat at a time, and a pixel offset (or voltage, rotation or flip) written mid-scan reaches the beam one
Scan - Output Buffer (milliseconds)later, on a repeat boundary, as the new DE-Server property queues it. The offset is clamped to half the scan’s voltage range, inside the scan card’s +-10 V.get_gpu_frames()now maps the twin’s own frames out of GPU memory, through DE-Server’s GPU frame sharing protocol, instead of deapi’s simulated stand-in.The shared-memory face keeps up with a fast camera: with
reuse, a full frame pool is a library that is picked from rather than rendered into, re-rendered when the column moves, and frames go to DE-Server a batch at a time. Its C++ reader yields instead of sleeping for the first few ms of a wait. A 256x256 stream reaches about 27,000 frames a second, from 580.In a scan, the shared-memory face follows DE-Server’s scan coil voltages (
DE_ScanOutput, which DE-Server’s scan simulator writes as a scan card would be written): each frame is rendered where its point’s voltages put the beam, so a pixel offset, an XY array or drift correction moves the beam with DE-Server’s output-buffer latency. A probe held on one point is renderedpool_sizetimes and then picked from (about 25,000 frames a second at 256x256).Calibration standards: Ted Pella’s TEM calibration specimens as presets (“Ted Pella 607 - …”), grouped in
CALIBRATION_STANDARDSwith their published numbers inspecimen.standards.PRODUCTS: the grating replicas (607, 607-A, 606, 603, 603-A, 677, 673), gold-shadowed latex (628-B), evaporated aluminum (619) and MoO3 laths tied to their crystal axis (625). The replicas are conformal carbon films shadowed with metal at an angle (embossed at low magnification; a nanocrystalline island film with dense and sparse regions and soft shadows at high magnification). New: MoO3, and anorientedgrain texture.Faster live-view re-renders on a magnification step or a stage move: the Bragg excitation is computed once per grain and thickness and kept across views, a stage move beyond the pan margin rasterizes only the strips of specimen it brings into view (
RenderConfig.reuse_rasters), large rasters are rasterised in parallel strips (RenderConfig.raster_threads), and the exit wave, crops and upsampling are threaded or numba-compiled. Padded TEM rasters now sit on a world-fixed pixel lattice, with sub-pixel view offsets applied as a phase ramp and crops interpolated to a fraction of a pixel (a move inside the margin was rounded to whole raster pixels).de-twin serve --interactiveturns on the fast previews for a live view.STEM scans now follow DE-Server’s scan coil voltages:
Scan - Voltage H/V Min/Maxis the field the full scan grid spans (a smaller range zooms in, moving it pans), andScan - Pixel Offset X/Ymoves the probe by whole or fractional scan points.Scan - RotationandScan - Flip Horizontally/Verticallyturn and mirror the grid. A live scan (start_acquisition(0)) picks up offset, voltage, rotation and flip changes at its next repetition, so a client can move the probe, correct drift, or pan and zoom while it scans.Specimen field maps can carry an amorphous layer under the primary material (
under_material,under_thickness_nm: a support film under particles, stain around protein); the renderer adds its absorption, phase, amorphous texture and diffuse scattering. Newspecimen.stamps: organic shapes precomputed once (Cahn-Hilliard phase-separation tiles shipped as signed distances, 3D diffusion-limited aggregates) and placed fast in numba with exact coverage;specimen.spheres: binned sphere sets (chords, tops, cast shadows).TEM lattice fringes are kinematic by default (
RenderConfig.lattice_fringe_model="kinematic"): each resolved beam passing the objective aperture is a phase grating of its own kinematic strength, grown with thickness, timeslattice_fringe_efficiency(0.5, calibrated on Ted Pella lattice images). The previous fixed amplitude ("fixed",lattice_phase_rad) gave every crystal the same weak fringes.
Bug Fixes#
The cross grating’s second and fourth orders no longer outweigh its first. The renderer removed the electrons wherever the specimen’s phase changed quickly between raster pixels, and a raster pixel wider than a ridge wall (every frame larger than 1024 x 1024) put that dark rim on every edge of every hole. Rendered images change slightly wherever a specimen has steep edges: the mean frame intensity moves by under 1 % for most presets and by up to 5 % for a few. (#39)
The twin no longer aborts on macOS when two threads render at once (a prefetch alongside a live frame, say). numba’s only threading layer there, the workqueue, aborts the process when two threads launch a parallel kernel together, so on it the kernels are compiled serial; each still releases the GIL, and the twin’s own threads run them side by side.
Removals#
The
legacyTEM imaging model and thelegacydose model are removed. The physical models were already the defaults and are now the only ones, soRenderConfig.tem_model,OpticsConfig.dose_modelandOpticsConfig.legacy_dose_rate_counts_per_sno longer exist.The optics state no longer has
blur_sigma_px,fresnel_gain,fresnel_signorfresnel_sigma_px; only the removed legacy TEM renderer used them.
Maintenance#
Project links, the docs site URL and the trusted-publishing instructions now point at the directelectron organization (https://directelectron.github.io/de-twin/).
Structural cleanup with no change to rendered output: one shared numba kernel decorator (
de_twin.parallel.numba_jit) replaces the per-module copies, the column-realism classes share their derived values, and unused helpers (log_snap,resample_object,upsample2x,point_in_polygon,AmorphousStructure) are removed.
0.2.0 (2026-09-27)#
New Features#
A tilted specimen that is off eucentric height moves in the image (by dz sin(tilt) across the tilt axis), as it does on a real stage: what eucentric-height routines measure.
Apollo super-resolution:
AcquisitionRequest(super_resolution=True)(deapi:Centroiding Mode = Super-resolution) reads a counting camera out at 2x the ROI each way (8192² for a full Apollo frame, ~20 ms). Each event lands in one of its pixel’s 2x2 sub-pixels, drawn uniformly: the rendered flux has no detail finer than a sensor pixel, so that is what a real super-resolution readout of it would show.Beam tilt reaches diffraction: a tilted beam moves the SAED / CBED pattern (tilt / mrad-per-pixel) and tilts the Ewald sphere, so tilting towards -g by the Bragg angle excites +g.
Counting cameras (the Apollo family) run through numba kernels too: a 4096² counted frame in ~9 ms (the numpy path took ~90 ms).
Low dose on the column:
LowDose,LowDoseArea(Search / View / Focus / Record) andLowDoseAreas. Each area stores its magnification, spot, intensity, defocus offset from Record’s focus, image shift and beam shift; switching stores the area left and applies the one entered. On a realistic column the areas do not line up by themselves, which is what SerialEM’s View-to-Record low-dose calibration measures.Pinned acquisitions:
AcquisitionRequest(seed=...)(andsnap(seed=...)) seeds frame i of an acquisition by(seed, i)whatever the twin exposed before;DigitalTwin.reset_serial()restarts the running frame count.Precession: the column’s
Precession,PrecessionAngle(mrad),PrecessionFrequency(Hz) andPrecessionDescandrive the beam tilt round a cone; SAED and kinematic 4D-STEM patterns average over the tilts swept during each frame (the whole cone, or the arc a frame shorter than the period covers). Without descan the pattern sweeps round a ring.STEM and 4D STEM patterns with large disks (a probe’s disks can outgrow the detector) are drawn row by row over the detector with a numba kernel instead of stamping each disk’s whole square: a Steel 4D STEM frame goes from ~3.3 s to ~50 ms.
Shared-memory throughput:
ShmFace(reuse=N, threads=M)(de-twin serve --shm --reuse N --threads M) renders into a pool on its own thread and publishes each frame up to N times, with the detector physics capped at M cores; frames are copied into the mapping once. A 4096² DE16 stream reaches ~170 fps (5.7 GB/s) withreuse=8, threads=8, an Apollo ~250 fps (4.2 GB/s) withreuse=8, threads=4. Recycled frames are always of the current view.Specimen preset
Cross grating 2160 l/mm: a carbon replica cross grating (463 nm period), the pixel-size calibration standard.TEM imaging renders a view 15 % of the field larger on each side and serves a stage move inside that margin by cropping it: a nudge costs ~2 ms instead of a render;
RenderConfig(pan_margin=0)turns it off.The deapi face has a read-only
Simulator Sourceproperty (de-twin <version>) so clients can stamp provenance on what they save.The detector simulation runs as fused numba kernels for integrating cameras: 6x faster at 4096² (170 → 28 ms a frame) and 11x at 1024² (42 → 4 ms), with the same statistics;
DE_TWIN_NUMPY_DETECTOR=1keeps the numpy path. TEM images render at up to 1024² (from 2048²), which makes a re-render after a stage move ~2.7x faster on a 4096² camera. numba is now a dependency.The realistic column (
OpticsConfig.realistic) has a C2 crossover (the beam converges to its smallest at an Intensity that varies by spot size and probe mode, and spreads again past it) and a beam-shift matrix;calibration_truth()reports both. Closed-loop tests run SerialEM’s Beam Crossover and Beam Shift calibrations against them.The realistic column adds image-shift-induced coma and astigmatism (image shift images the specimen off the coma-free axis: an effective beam tilt of ~0.43 mrad per micrometre, and ~15 nm A1 per micrometre) and the high-defocus change of magnification and rotation (~4 % and ~2 deg at -200 um).
calibration_truth()reports them; closed-loop tests run SerialEM’s autofocus, coma-vs-image-shift and high-defocus calibrations against the twin.OpticsConfig(intensity_zoom=True): in TEM imaging the condenser follows the magnification (like TFS Intensity Zoom), so the dose per detector pixel stays put as the operator zooms; Intensity sets the beam atintensity_zoom_reference_mag. Off by default: the dose per pixel goes as 1/mag², so zooming out from a safe dose saturates the camera and zooming in drowns it in shot noise.OpticsConfig(max_raster_pixels=...)sets the largest TEM raster (0: render at the frame’s own sampling, for CTF and MTF measurements);OpticsState.resolution_warningsays when a frame is upsampled from a coarser raster.OpticsConfig.realistic(seed): a column with a real one’s geometry, what SerialEM’s calibrations measure. Per magnification an image rotation (with a large jump between LowMAG and MAG1), a true pixel size off the nominal one, an image-shift matrix (scale, skew, rotation) and an image-centre offset; stage backlash that depends on the approach direction. The ideal column stays the default.DigitalTwin.calibration_truth()reports every true value for closed-loop tests; the pan-crop cache now works for rotated views.RenderConfig(interactive=True): while the view keeps changing (dragging the stage, zooming), a view the pan cache cannot crop is rendered at up topreview_side``² and upsampled, so live view keeps up; once the view has been still for ``settle_sit is rendered in full.de_twin.optics.serialem:to_serialemwrites a column’s calibrations as SerialEM would measure them (SerialEMcalibrations.txtentries: image-shift and stage-to-camera matrices, image-shift offsets, beam-shift calibrations, crossovers, high-focus magnification and focus calibrations; plus the camera’sRotationAndPixellines), andfrom_serialemreads a microscope’s calibrations into aColumnDefinitionthat goes inOpticsConfig(realism=...), so a twin behaves like that microscope. The round trip is exact.
Bug Fixes#
A specimen with an amorphous material in a crystalline slot (the FIB-liftout pattern’s “auto” post) no longer raises in imaging or diffraction; it scatters no Bragg beams.
A twin now starts where an operator would: the stage over the specimen (the nearest intact grid square, well, lamella or chip window, and its largest particle cluster) instead of the stage origin, which was a grid bar or chip frame for some presets and left them black; the beam at a dose the detector takes without saturating a frame; and cryo specimens 1.5 µm underfocus.
DigitalTwin(start_on_specimen=False)keeps the old start, and a supplied column is never moved.In TEM imaging, beam shift moves the illuminated disc across the camera and leaves the specimen where it is (it used to move the image, like image shift). STEM and diffraction are unchanged.
The column’s
mag_ladder()in TEM imaging lists LowMAG’s and MAG1’s magnifications together, the ones aMagnificationwrite can reach, so a client stepping it (Ground Crew’s mag +/-) crosses from low to medium magnification instead of stopping at the top of LowMAG.The deapi face caches its port at start, so a face stopped before its UDP thread binds no longer calls
getsocknameon a closed socket.The deapi face starts with a 0.5 s exposure instead of a single frame, whose sparse electrons read as noise.
The shared-memory face keeps its frame pool across back-to-back requests for the same frames (DE-MC’s live view is a string of short acquisitions), and republishes the last frame while a new request has nothing rendered yet, so a slow render at the start of one no longer stops DE-Server.
The shared-memory face renders on a thread of its own and republishes the last frame while a slow render runs, so DE-Server no longer stops a live acquisition (“no frame from the producer within 1020 ms”) when a view takes over a second to render.
The shared-memory face renders one frame to warm the twin up before it attaches, so DE-Server’s first acquisition no longer times out while a fresh twin starts (about 15 s on first use);
ShmFace.readyis set once it is serving.impulsePy is imported on a thread of its own with a timeout (
holder.impulse.import_impulse): with Impulse not running,connect_holder("impulse")falls back to the simulated holder instead of hanging.
Documentation#
User-guide pages on the realistic column (and intensity zoom), beam tilt and precession, low dose, and SerialEM calibrations; the new classes and functions are in the API reference.
0.1.1 (2026-09-24)#
Bug Fixes#
The deapi face reports “Specimen Pixel Size X/Y (nanometers)” per binned pixel, including hardware and software binning, as DE-Server does.
The deapi face treats
start_acquisition(0)as live view (repeat until stopped), as DE-Server does, instead of a single acquisition; and repeated acquisitions no longer repeat the same detector noise, because noise is now seeded by a twin-wide frame counter rather than the frame index within each request.
0.1.0 (2026-09-24)#
New Features#
First release of the de-twin digital twin: a simulated TEM column (JEOL/TFS-style modes, magnification and camera-length ladders, stage motion, aberrations and an optional probe/image corrector), seeded specimens (grids, thin films, proteins in ice, FIB lamellae, an in-situ heating chip) with real crystal orientations from diffsims and orix, wave-optical TEM imaging, diffraction and coherent 4D-STEM, and a detector model for every Direct Electron camera.
DigitalTwinruns it all in-process.The twin can stand in for real software: a DE-TEM-Channel SOAP server (
de-twin serve --soap) that DE-Server, de_microscope, de_autopilot and de_ground_crew drive unchanged, a deapi-compatible fake DE-Server (--deapi), and a shared-memory frame source that feeds a real DE-Server through its processing pipeline (--shm, test pattern “External Frame Source (Shared Memory)”). It can also follow a real or Dummy DE-TEM-Channel and a DENS Impulse holder instead of simulating them.
Bug Fixes#
Coherent 4D-STEM data no longer depends on the machine: the probe’s partial-coherence modes were truncated through a degenerate pair of eigenmodes, whose basis LAPACK chooses differently with BLAS threading, so the same request gave different patterns on different computers. Truncation now keeps degenerate groups whole.
Maintenance#
Packaging, documentation site and release workflows (towncrier changelog, Prepare Release PR, PyPI publishing when a GitHub Release is published).
Raised the minimum supported versions to numpy 2.0, scipy 1.13 and diffpy.structure 3.2 (and matplotlib 3.9 for the docs), the oldest set the test suite passes with. Timing budgets in the performance tests now scale with
DE_TWIN_PERF_SLACKso shared CI runners can check them without being tuned to one workstation.