Direct ptychography API#
Use DirectPtycho from Python on MPS or CUDA. The default .reconstruct() method is conventional SSB. See regularized phase and complex reconstruction for the experimental regularized methods (CUDA and MPS). Find aberrations once, inspect the search,
and reuse those parameters for reconstruction. Start with the
Python workflow for a short example.
For actual images and saved outputs, run the Gold notebook.
This page covers parameters, calibration, ownership, and backend limits.
Choose a search#
ptycho.fit_aberrations() defaults to search="standard": 200 TPE trials
for initial defocus, astigmatism, tilt and depth, including its Newton polish
on the exact gradient and Hessian, then the joint refinement through fifth order
on exact gradients. No additional fifth-order
TPE search is performed. search="broad" adds 400 fifth-order TPE trials
before the same joint refinement. Both fit the same model and retain all valid BF pixels.
search="primary" retains the earlier initial-only fit and its custom bounds
and fixed coefficients. Existing constrained calls using search="standard"
should select "primary" explicitly.
Every fit uses refinement="auto": the exact-derivative refinements on CUDA
and MPS, with a ceiling of 400 full-data objectives per start at fifth order.
Choosing the method is retired: "newton", "bfgs" or "nelder-mead" print
one line and run "auto"; refinement=None keeps the best trial of a
search="primary" fit unrefined. A search="primary", tilt=False fit that
locks a coefficient (refine_lock), bounds phi12 or holds a nonzero C12 refines
on the exact gradient in (C10, C12, phi12), where those limits are bounds
(result.refinement == "bfgs"); Nelder-Mead is retired. An evaluation cap is
not proof of convergence.
Scientific objective/reconstruction work stays on the GPU; the optimizer’s
candidate generation and float64 control run on the CPU.
See the matched initialization study and optimization lessons.
In notebooks, the initial Auto-sampled phase appears while refinement continues.
After standard/broad fitting, resident CUDA sessions replace that view with
three shared-contrast phase images: conventional SSB, regularized phase-only,
and amplitude + phase. The regularized alternatives use the run defaults
(strength 0.03 for each applicable channel). Preparation happens after fitting;
show=False skips both the previews and this extra work. MPS and streamed
sessions retain conventional SSB. See direct comparisons.
Use columns=1 for a vertical stack, columns=2 to wrap the methods, or the
default columns=3 for a single comparison row. size sets each panel’s size:
use a preset ("small", "medium", "large", "xlarge") or a positive integer
width in pixels, such as size=750. size=900 matches "xlarge" at the static
100-DPI reference. The same sizing applies to result and focus comparisons;
source sampling and fitted values are unchanged.
CLI calls create no plots. fit.timings separates the initial fit, refinement,
initial handoff and final display; display work is excluded from the fit clock.
Applications can pass on_initial_fit=callback to receive the initial native fit.
The method returns only after final refinement.
Review and save a fit#
Keep the returned result to inspect and reuse the fitted model:
fit = ptycho.fit_aberrations()
fit.report() # optics, geometry, objective, counts and timing
fit.report(columns=2) # two parameter/value groups, less scrolling
parameters = fit.report(to_dict=True) # coefficients and geometry as a dictionary
fit.report(show_convergence=True) # table plus recorded convergence
fit.show("probe") # model probe intensities
fit.save_calibration("calibration.json")
Read just the coefficients from fit.aberrations, the existing dictionary.
In a notebook, fit.report() displays one row per parameter, with units in
the row labels. Both the pandas table and dictionary retain full precision.
A cell ending on fit shows the same layout with display-only rounding.
For a compact table, use fit.report(columns=2) or columns=3. Read down
each parameter/value group, then across. All recorded fields remain present;
the layout does not affect to_dict=True exports or series trial tables.
parameters["aberrations"] contains every fitted coefficient with unit-bearing
names such as C30_nm and phi23_deg. Geometry is separate:
scan_detector_rotation_deg, and, when present, sample_tilt_mrad in
(row, col) order and depth_spread_nm. Effective depth is a model parameter,
not an independently measured thickness. Editing the table or dictionary does
not change the result. The report describes the saved fit, not later viewer
edits; use the viewer’s calibration export for those.
The report includes recorded elapsed time, stage timings, the objective name,
model agreement, variance diagnostic loss, search/refinement counts and the
optimizer’s stopping reason. The model agreement (sample_agreement) is the
fraction of G’s power in the fit band (0.2-0.9 1/A) that the fitted model
explains, from 0 to 1, the same quantity for the low-order and fifth-order fits;
noise and unmodelled structure keep it below 1. These quantities have distinct meanings; reaching
an evaluation limit is not proof of convergence. Stage times can overlap and
must not be summed. Unrecorded diagnostics are omitted, not invented.
fit.report(show_convergence=True) displays the existing convergence figure
and returns the same vertical pandas table. Plotting uses recorded history
only; absent history raises an error
explaining how to obtain it. Export and plotting are separate calls:
to_dict=True cannot be combined with show_convergence=True. Reports add no
fit callbacks, image transfers or history collection. fit_aberrations(show=False)
disables the live phase preview; normal scalar fit history still belongs to the
fitter and supports result inspection later.
search="broad" uses the same result API for the broader fifth-order
search. Plotting convergence uses saved scalar evaluations, with different
objectives and rotation searches kept separate. It does not rerun the fit;
if a refinement trajectory was not recorded, the plot says so. A budget stop
is reported as a budget stop, rather than proof of convergence.
Compare search budgets#
For an optional broad-search study, use a sequence of trial counts:
fits = ptycho.fit_aberrations(search="broad", trials=(200, 400, 600), seed=17)
fits[200].show("convergence", compare=fits)
This draws one elapsed-time chart with dashed TPE curves and solid refinement curves. Each budget starts independently from the same session state and uses all selected data. The parent session is unchanged; choose a fit explicitly:
image = ptycho.reconstruct(fits[400], upsampling="auto")
A scalar trials=400 still returns one fit. The default is one 200-trial initial fit followed by joint refinement.
The optional broad search adds 400 trials. A comparison runs every requested budget sequentially, so use it
for analysis rather than every acquisition. Loaded data are borrowed; each fit
owns its preparation. save_dir="budget-review" saves each complete result in
trials-200/, trials-400/, etc., without overwriting another budget.
fit.show("convergence") also uses elapsed time for newly recorded fifth-order fits.
The clock starts before the primary fit; it includes search and refinement but
excludes acquisition loading and plotting. Only fifth-order objective values
are drawn, since the primary fit has a different objective. Recording adds
scalar timestamps, not objective calls or GPU synchronization. Inspect measured
stage durations in fit.timings. Convergence plotting runs only when fit.show("convergence") is called.
Use x="evaluations" to change the horizontal axis. Older histories and primary
fits without timestamps use evaluation counts automatically; x="time" requires
recorded times. Overlays use the unnormalized model agreement so differing
normalization factors do not change the comparison. They require the same data,
calibration, region and objective; keep differently fitted rotations in
separate plots.
save_calibration exports this fit’s coefficients, tilt/depth, rotation and
acquisition identity in ShowPtycho’s calibration format. It computes no phase
or loss; file fingerprinting and writing happen only when called. It requires
a calibrated file-backed session and selects Auto sampling for the viewer.
Use the widget’s own save_calibration() to save subsequent interactive edits.
This JSON contains the correction and calibration, not the reconstructed
image or the convergence history; fit_aberrations(save_dir=...) saves the
complete fit for reuse.
Fit methods: model-based by default#
quantem fits SSB aberrations with a physical model of the measurement, not an image-quality score. For each trial set of aberrations, sample tilt and depth spread, it predicts the bright-field data G(q, k) the detector should have recorded, then keeps the parameters whose prediction agrees best with the measurement (least squares). This model-based, tilt-corrected fit is the default everywhere.
Call |
Method |
Use it for |
|---|---|---|
|
model-based, tilt-corrected, refined through fifth order |
the default: every fit |
|
model-based, tilt and depth spread held at 0 |
thin, untilted samples, or to see what tilt correction changes |
Each method searches 200 seeded trials and then refines on exact derivatives
of the agreement with refinement="auto" (above). result.objective records
the objective and report() shows it; result.loss is an optional
native-grid phase-variance diagnostic, not the fitted objective. Fifth-order
fitting skips it by default; pass compute_loss=True to calculate it. The same
names pass through scan_rotations(...) and are part of a saved
fit’s settings.
The phase-variance search (objective="phase_variance") is retired: that value
raises, since a silent agreement fit would land elsewhere, and
objective="agreement" prints one line and fits as the default. It chose
the aberrations under which the corrected phase images of all bright-field
pixels agree best, so it scored images instead of predicting the data, could
not see sample tilt or depth spread, and was about 10x slower; on simulated
crystals with known aberrations the model-based fit lands about 3x closer to
the true C10 (the standard-fit objective).
Method |
MPS |
CUDA |
Browser |
|---|---|---|---|
|
yes |
yes |
no fit |
|
yes |
yes |
no fit |
The browser engine has no aberration search.
Higher-order box#
The fifth-order refinement bounds each of the 22 higher-order coordinates
(C21 through C56) to ±20 rad of probe phase at the aperture edge
(higher_order_limit=None, the default; higher_order_limit=2 keeps the
earlier ±2 box; search="primary" stops at low order and refuses either). A
±20 fit is checked on two halves of the band: the fifth order is refitted on
alternate 0.05 Å⁻¹ rings of the band frequencies from the same low-order fit,
and when the two probe phases through fifth order (C10 and C12 included, tilt
and depth not) differ by more than 0.5 rad rms over the aperture, the ±2 fit is
returned, its higher orders are marked not trusted and one line says so.
optimizer["higher_order_consistency"] records half_band_rms_rad,
trusted, shown_limit, both halves and the checked ±20 fit;
optimizer["higher_order_limit"] is the limit of the fit returned. CUDA, MPS
and native Metal use the same definition.
Inputs and outputs#
DirectPtycho.open(path, ...) loads a supported detector source. DirectPtycho(data, ...)
accepts an existing Dataset4dstemGPU or supported array. Prefer loading once
and inspecting the acquisition before constructing the algorithm:
from quantem.gpu import DirectPtycho, io
data = io.load("acquisition.qem")
data.calibration.semiangle_mrad = 30 # set what the file does not record
data # The notebook displays geometry, calibration, units, and overridden values.
In the next cell, pass that same dataset to SSB and reconstruct:
ptycho = DirectPtycho(data)
result = ptycho.reconstruct() # fits the aberrations once when none are known
Both file-backed entry points use the loaded voltage_kV, semiangle_mrad,
scan_sampling_A, and detector_sampling_mrad metadata unless explicitly
overridden. The first three remain required when supplying an array without
metadata. Missing physical calibration raises an error. Explicit keyword
overrides affect the session without rewriting data.metadata.
scan_detector_rotation_deg is the physical scan-to-detector rotation, kept in
[0, 360). Omitted, SSB uses the acquisition’s recorded rotation, else estimates
it from the curl of the centre-of-mass field and prints one line. The curl knows
the angle only modulo 180 degrees; for an estimated angle reconstruct() and
fit_aberrations(check_rotation=True) fit both 180-degree branches and keep
the one whose atomic columns have positive phase (see
Settle the 180° branch). A given or recorded angle is
fitted once and never moved. scan_detector_rotation_flipped=True means the
detector’s row and column axes are swapped relative to the scan, which no
rotation can produce; it is estimated with the angle, or False when an angle is
given. Reported centers stay in the original detector axes. Give a calibrated
angle to skip the estimate: DirectPtycho(data, scan_detector_rotation_deg=12.3).
reconstruct() fits the aberrations with fit_aberrations() defaults (the
tilt-corrected joint fit) when the session has none, then reuses them. For custom
primary bounds or fixed coefficients, call fit_aberrations(search="primary", ...)
first. This fits defocus, astigmatism, tilt and depth without the default
fifth-order stage. The primary joint fit takes a C10_nm range or value and a C12_nm range from 0, or
holds C12_nm=0:
ptycho.fit_aberrations(search="primary", tilt=True, search_ranges={"C10_nm": (-50, 50)}, fixed_aberrations={"C12_nm": 0.0}).
It searches phi12_deg over its whole period, so a phi12_deg range or a
nonzero held C12_nm alone raises and names what to pass; holding all three
coefficients needs check_rotation=False. The joint fit searches depth
spreads in quantem.gpu.direct_ptycho.DEPTH_SPREAD_RANGE_NM (2 to 60 nm). Assign ptycho.aberrations = {...} to
state a known calibration, which reconstruct() then applies without fitting.
detector_sampling_mrad is detector angular sampling in
mrad per pixel, (row, col). Calibrate it as its own step before SSB:
fit = data.calibration.fit_detector_sampling() # semiangle / bright-field edge radius, recorded on data
fit.show() # the mean pattern with the fitted disk edge, axes in mrad
The bright-field disk is the image of the aperture, so its edge sits at the convergence semiangle. The fit
records the detector step with the source Fitted (bright-field disk), together with the disk centre and its
edge radius, and DirectPtycho(data) reads all of them. When nothing is recorded, CUDA and MPS estimate the step with the
same rule (quantem.gpu.detector.fit_bright_field_disk: semiangle_mrad divided by the radius in detector
pixels at which the bright-field disk of the full-detector mean pattern falls to half its plateau) and print one
line saying so. A detector step set by hand, on data.calibration or as a keyword, always wins. In
DirectPtycho(data, bright_field_center_px=..., bright_field_radius_px=...), disk geometry uses
the input detector grid. The session adjusts the center if it takes a
bright-field crop.
DirectPtycho.open uses the canonical io.load path, including its ANS-only GPU
acquisition policy.
DirectPtycho.open owns the acquisition it loads. On CUDA, for fit-sized scans, it
decodes the bright-field crop and releases the acquisition; large CUDA integer
sources stay encoded until close() or context-manager exit. On MPS, integer
acquisitions stay encoded until close(): the session decodes their
bright-field columns a few scan rows at a time while it builds G(q, k) and
keeps only G(q, k), so a 512 x 512 scan holds no decoded copy of its crop.
DirectPtycho(data, ...) borrows the caller-owned dataset or array and never releases
it; on MPS keep the dataset open while the session is in use. close() frees
only the session’s own crop and Fourier data. Encoded inputs decode only the
bright-field evidence needed by the session.
Treat detector values as fixed for the lifetime of a session: SSB retains their
prepared Fourier data. After editing the array, construct a new DirectPtycho session.
SSB Fourier-stack preparation and reconstruction are additional work; they
are not included in a detector-viewer loading-time claim.
fit_aberrations() returns one SSBResult. Its primary field is the complex64
object_wave with shape (scan_row, scan_column). phase and amplitude are
derived as angle(object_wave) and abs(object_wave). The result also records
the backend, fitted aberrations, rotation, loss, trial counts, the local
refinement applied (None when a search="primary" fit kept its best trial), bright-field
geometry, timings, reuse state, and provenance metadata. A fit that compared
the two 180-degree branches records them as rotation_branches (a
RotationBranches); reconstruct() keeps it on its result whenever it applies
that fit’s coefficients, so report() shows the suggested rotation and
result.show("rotation") draws the evidence. Saved results (save_dir) are
quantem.ssb-result/10 records; a /9 record is computed again. A record
holds no absolute path: it names its source by file name (and its path
relative to the results folder when the data lives under it), and it is reused
when the acquisition’s portable identity matches
(quantem.gpu.direct_ptycho.persistence.acquisition_identity: file name, size and the
SHA-256 of the first MiB of the master and of each data file, the scan and
detector shapes, and how flagged pixels were filled). A results folder moved or
copied with its data is therefore reused; another acquisition is fitted again.
Shapes, coordinates, dtypes, and units#
detector input:
I[scan_row, scan_column, detector_row, detector_column];complex result:
object_wave[scan_row, scan_column], complex64, on the session’s output grid;bright_field_center_px:(detector_row, detector_column);scan_sampling_Aandobject_sampling_A:(row, col)pairs, in Å; every entry point (DirectPtycho,DirectPtycho.open,reconstruct_series,parallax.run) also takes one number for both axes and stores and reports the pair;C10_nmandC12_nm: nm; andphi12_deg: degrees.
For a dense CUDA input, native supported scan shapes are 128×128, 256×256,
512×512 and 1024×1024. Other smaller scan shapes are padded with the mean
diffraction pattern to a supported square. For example, a 32×32 input currently
produces a 128×128 working grid before any requested upsampling. Check
ptycho.scan_shape and result.phase.shape; padded pixels are not additional
measured scan positions; the session prints one line naming the padded size.
Dense CUDA arrays larger than 1024 along either axis are unsupported. Use the
encoded acquisition for the bounded-memory large-scan workflow below.
Computational padding is separate from reconstruct(..., upsampling=2).
The default fit evaluates 200 seeded TPE candidates of the least-squares
agreement of the SSB model with the full active bright field, keeps the best,
refines it with a trust-region Newton on the exact gradient and Hessian of the
agreement, then refines every term through fifth order on exact gradients. The
standard fit (search="primary", tilt=False) runs the same low-order search
and Newton with the sample tilt and depth spread held at 0, on CUDA and MPS
(Fit methods). No fit averages optimizer candidates.
This is the implemented calibration workflow; Levenberg–Marquardt is not an
available refinement mode. The browser SSB engine has no aberration search:
run fit_aberrations() on CUDA or MPS. The browser never substitutes fewer
trials or a reduced objective.
Fit a region, reconstruct a large encoded scan#
CUDA integer ANS acquisitions with native 1024, 2048 or 4096 scan dimensions can remain encoded during SSB. Each detector-column batch is decoded exactly, transformed across every scan position, corrected with the existing SSB kernel, and accumulated before one inverse FFT. Scan tiles are not stitched. The full source and output must fit on the acquisition’s GPU; distributing one SSB session across GPUs is not supported by this API yet.
Decoded batches use a temporary scan-major uint16 buffer, then a tiled CUDA transpose writes the float32 FFT input. Counts remain exact in both buffers. The uint16 scratch uses two bytes per scan position per detector column; at 2048×2048 with 32 columns, it adds 256 MiB. Batch sizing accounts for this scratch, FFT workspace and the full output arrays before reconstruction.
from quantem.gpu import DirectPtycho, io
with io.load("acquisition.qem", backend="cuda") as data:
with DirectPtycho(
data,
voltage_kV=300,
semiangle_mrad=30,
scan_sampling_A=(0.3, 0.3),
detector_sampling_mrad=(0.55, 0.55),
scan_detector_rotation_deg=12,
) as ssb:
fitted = ptycho.fit_aberrations(
scan_region=(256, 768, 256, 768),
tilt=True,
)
result = ptycho.reconstruct(fitted, compute_loss=False)
# Native 2048 input produces 2048 output here, with unchanged field of view.
Use measured calibration; the numbers above illustrate the units. A session
borrows data, so it can also inspect the same counts through detector APIs.
DirectPtycho.open("acquisition.h5", ...) uses the normal automatic ANS loader and owns
its encoded source. No manual conversion or saved intermediate is required.
Invalid detector pixels are excluded from scientific selection; their original
stored counts remain unchanged.
The fit region is an explicit square of 128, 256, 512 or 1024 scan positions.
Its returned phase, loss and rotation diagnostics describe that region.
Reconstruction applies the selected aberrations, tilt and depth spread to the
whole scan. This assumes those optics apply across the field; regional fitting
does not establish that assumption for a specimen with varying thickness or
long-scan drift. result.fit_scan_region records the region used.
The bounded path supports a native 1024×1024 scan and even scan dimensions
above 1024 up to 4096, including rectangular grids, uint8/uint16 ANS, C10/C12,
and optional sample tilt/depth. Smaller rectangular scans retain their existing
square computational padding.
It uses complex64 forward spectra, complex128 summation/inverse FFT and
float32 phase. Its phase of the mean wave matches the existing CUDA
wave-average path within floating-point FFT rounding, not bit for bit.
Use compute_loss=False for the bounded path. Full-field per-detector
phase-variance loss and higher-order aberrations still use the existing
Fourier-stack backend for 1024 scans, retaining their prior behavior and
memory cost. Those operations are unsupported above 1024. Fit diagnostics remain available on the small
region. A full 1024 fit remains available, but an explicit smaller region avoids
keeping its entire Fourier stack during optimization.
upsampling="auto" or an explicit factor changes output sampling and
memory requirements, not the measured field of view. A 4096 output from a
512 scan is distinct from a native 4096 scan. Large output arrays can still
exceed available GPU memory. Borrowed resident inputs without a persistent
source identity recompute saved results; file-backed sessions retain exact
saved-result reuse.
Find aberrations, then reconstruct#
from quantem.gpu import DirectPtycho
ptycho = DirectPtycho.open(
"acquisition.qem",
voltage_kV=300, # kV
semiangle_mrad=30, # mrad
scan_sampling_A=(0.264, 0.264), # Å per scan pixel, (row, col)
)
aberrations = ptycho.fit_aberrations(tilt=True, save_dir="results/aberrations")
aberrations.report()
ptycho.show_trials(best=5)
result = ptycho.reconstruct(aberrations, save_dir="results/native")
shown = ptycho.reconstruct(aberrations, upsampling="auto")
shown.show()
The numbers above illustrate the units; use your acquisition’s calibration.
SSB here is model-based and tilt-corrected: fit_aberrations() (tilt=True,
the default) jointly estimates specimen tilt and model depth spread as well as
aberrations, and so does the automatic fit of reconstruct().
search="primary", tilt=False holds the tilt and depth spread at 0
(Fit methods).
Fits stay on the native scan grid; what you look at is reconstructed at
upsampling="auto", the factor the scan sampling allows (at 300 kV
and 30 mrad: 2× for the 0.264 Å step above, 3× at 0.373 Å, 4× for the 0.5 Å
gold scan), and its title names the factor. show_trials and the branch
panels of scan_rotations use the same factor. result.show()
draws the phase a result holds; for a native fit one line names the auto
factor and the call that shows it.
Keep ssb open for the examples below and call ptycho.close() when finished.
Inspect the best joint fit while the same SSB session is open:
ptycho.show() # existing interactive ShowPtycho
ptycho.show("depth", size="large") # static focus-depth landscape
ptycho.show("tilt", size="medium") # static row-column tilt landscape
The static calls reuse recorded diagnostics or measure them once on the GPU.
If no fit exists, the usual joint fit runs first. size="small", "medium"
"large", "xlarge", or an integer pixel width such as size=750 only change
figure dimensions. For explicit width and height in inches use
axsize=(6, 5) instead. Use cmap="viridis" to choose colors and save="fit.png" to export
the complete figure. Returned figures also support savefig(...) and render directly as a notebook cell’s final expression.
Argument |
Choices |
Meaning |
|---|---|---|
|
|
Focus versus depth, row versus column tilt, or both landscapes. With no view, |
|
|
Display size only; defaults to |
|
|
Full field, or an independently fitted selected region. |
|
|
Exact panel dimensions instead of |
To see both landscapes in one figure and read the numerical joint-fit values, use these expressions in separate notebook cells:
ptycho.show("parameters", size="medium", columns=2, cmap="inferno", save="fit.png")
aberrations.report()
Select a region in original scan pixels with exclusive stops:
ptycho.show("tilt", region=(64, 192, 64, 192), size="small")
This jointly fits the region’s optics, tilt and depth with one seed, 200 trials and Newton, keeping the full-field rotation and detector calibration. It leaves the full-field fit unchanged. Each bounds-based call fits independently; to inspect multiple views of the same regional fit, retain its session:
region = ptycho.select_region((64, 192, 64, 192))
region.show("depth")
Then region.show("tilt") reuses that fit and its measured slices in the next
cell. Call region.close() when finished.
depth means fitted depth spread, not measured specimen thickness. The
plots are conditional objective landscapes, not spatial depth/tilt maps.
Their marker identifies the joint fit; all unplotted parameters stay fixed.
Each colored tile is a measured parameter evaluation, not a specimen pixel.
The color scale is agreement divided by agreement at the fit: 1 at the fitted
point, with higher values better. A broad bright area means similar scores
along this conditional slice. It is not an uncertainty estimate. Each region
is normalized to its own fit, so colors do not compare absolute data quality
between regions. Use shown.show(show_parameters=False) for the specimen’s
reconstructed phase instead.
The explicit measurement API remains available, including plotting from a saved result without an open GPU session:
ptycho.measure_parameters(aberrations)
aberrations.show() # phase beside the saved parameter slices
aberrations.show("parameters")
aberrations.show(show_parameters=False) # phase alone
This evaluates up to 201 additional parameter settings on the selected GPU without changing the fitted optics or phase. The saved axes use nm and mrad; agreement is normalized to the fitted point. These are conditional objective cuts, not uncertainty estimates or proof of a global optimum. If the fit was saved, the measurements are saved with it and reopening performs no new calculations. Measure a regional fit in the same regional session, rather than against the full acquisition. Joint fitting requires measured spatial-frequency support in its fitting band; unsupported scans raise a corrective error instead of returning a zero-agreement fit.
fit_aberrations() searches on the native scan grid.
reconstruct() applies the supplied parameters without
searching. CUDA and MPS use phase_of_mean at every output factor, including
all 14 higher-order terms. Both return phase-only complex waves with unit amplitude,
recorded as amplitude_estimated=False. That amplitude is not a measured
specimen transmission. Use reconstruct(method="complex") when amplitude is needed
(Choose a direct reconstruction).
result.upsampling and result.object_sampling_A record the output factor and
(row, col) pixel spacing. Input sampling stays in the saved signature. Changing the
method, sampling, or scientific parameters invalidates saved-result reuse.
Inspect and replay search trials#
ptycho.show_trials(first=5)
ptycho.show_trials(last=5)
ptycho.show_trials(best=5)
aberrations.trials # DataFrame, indexed by stable trial ID
aberrations["C10_nm"] # defocus, nm
aberrations["C12_nm"] # twofold astigmatism, nm
aberrations["phi12_deg"] # astigmatism angle, degrees
aberrations.sample_tilt_mrad # sample tilt (row, column), mrad; None if not fitted
aberrations.depth_spread_nm # model depth spread, nm; None if not fitted
result.phase # radians
result.object_sampling_A # Å per output pixel, (row, column)
Coefficients use dictionary indexing; each key carries its unit, nm for
C10_nm/C12_nm and degrees for phi12_deg. dict(aberrations) makes a separate coefficient dictionary. Phase,
tilt, rotation and trial history remain properties of the fitted result.
The report table labels its angle column phi12 (deg).
Supply exactly one positive selector. Images share phase contrast and include
scale bars and a parameter table. They are reconstructed at
upsampling="auto" by default (the title names the factor); the search
losses in the table are native-grid values. Trials are replayed from their
recorded settings rather than stored as a large image stack. Browsing preserves the
active reconstruction. best ranks the latest search only.
result.objective names the fit’s objective, and an agreement trial table its
objective column. Local refinement is separate from the search trials and
its final result appears in report().
attempts = ptycho.reconstruct(aberrations, trial_ids=aberrations.trials.tail(5).index)
attempts.phase # radians; axes (trial, row, column)
adjusted = ptycho.reconstruct(aberrations, aberrations={"C10_nm": 12.5}) # nm
Overrides preserve unspecified coefficients, tilt, depth spread and rotation.
They leave the supplied result unchanged. Replay uses each trial’s original
rotation branch: after a branch comparison the trials of both searches are kept,
search 0 at the first angle and 1 at the angle plus 180 degrees. Saved search
results retain this history.
Scan rotations#
rotations = ptycho.scan_rotations(report=True, region=None, zoom=3, size="large")
One call compares two scan-detector angles separated by 180 degrees and shows
full fitted fields, boxed central zooms, histograms and a numerical table.
report=True is the default. Use report=False to compute the same evidence
without rendering; rotations.report() returns a pandas DataFrame.
region=None or () uses the full scan. Named regions such as "center" use
half of each scan dimension; explicit bounds are
(row_start, row_stop, col_start, col_stop), with exclusive stops. The region
controls fitting. zoom controls display only: 1 shows full fields, 3 adds
threefold central zooms. Neither zoom nor size changes sampling or fitted data.
size accepts small, medium (default), large, xlarge, or a positive pixel width.
The suggested angle is applied automatically only when the session estimated
its rotation. Recorded or explicit angles are retained unless apply=True.
The polarity evidence is conditional and does not independently certify the
physical calibration. All numerical fit controls retain their original meaning.
Rotation and defocus comparison#
comparison = ptycho.scan_rotations(defocus=True, zoom=3, size="large")
Four independent fits compare two rotations and both C10 signs, preserving the
session’s fitted state. One tqdm progress bar shows the current rotation, C10
range, preparation/fitting/reconstruction stage, completed cases and elapsed
time. Updates occur between stages; the GPU search and Newton refinement are
not interrupted for per-trial progress. Use verbose=False for quiet runs.
Reconstructed focus/depth images#
To compare the actual specimen images, use the interactive image gallery:
viewer = ptycho.show("focus")
viewer
Use the interactive stack viewer to scroll through defocus while comparing model depths side by side:
viewer = ptycho.show("focus", layout="stack")
viewer
Each panel has one depth spread; the shared defocus slider changes C10 in every
panel. It starts at the requested offset nearest zero. Labels show the actual
C10 and depth values, and the fitted depth panel is marked. The default
layout="grid" keeps the image gallery.
The default grid varies C10 by -5, 0 and +5 nm around the joint fit, and model
depth spread by -5, 0 and +5 nm around its fitted value (bounded at zero).
Every panel retains the fitted astigmatism, specimen tilt and rotation. Labels
state the applied C10 and depth spread; the fitted pair is marked joint fit.
These are conditional reconstructions, not independently fitted models or
physical depth sections. All panels share contrast and automatic output sampling.
Use mode="static" for a figure made by core QuantEM’s show_2d. Both
interactive layouts become the same compact grid containing every reconstructed
frame, with at most two columns by default:
ptycho.show("focus", mode="static", size="small")
Static figures keep the image aspect ratio and use enough pixels to retain the
native reconstruction resolution. size changes the panel dimensions, not the
data. Use save="focus.png" on this call to export at that native density.
cmap="viridis" sets colors in static, interactive grid and stack focus views.
save exports a static figure; it is separate from ShowPtycho save_dir,
which stores scientific calibration.
Focus-image option |
Default |
Meaning |
|---|---|---|
|
|
An interactive widget, or |
|
|
Image grid or |
|
Static: two columns |
Images across or down; derive the other dimension. |
|
|
Colormap in static and interactive focus views. |
|
None |
Static figure path; requires |
|
|
Defocus offsets from fitted C10, in nm. |
|
|
Fitted depth spread and ±5 nm; supply an absolute value or sequence to choose the compared depths. |
|
|
Full field, or scan-pixel bounds to fit independently and display. |
|
|
Display size only. |
For a focus sweep at one model depth, or a regional comparison:
ptycho.show("focus", focus_offsets_nm=(-10, 0, 10), depth_spread_nm=15)
ptycho.show("focus", region=(64, 192, 64, 192))
Regional focus views apply the region’s joint fit to the full parent scan,
then crop the displayed images. A retained region = ptycho.select_region(...)
session also supports region.show("focus"); keep its parent open. This does
not replace the full-field fitted parameters. Both returned widgets support
the usual zoom, FFT and viewer.export_html(...) methods.
Finer preview sampling#
Fit at native sampling, then reuse the fitted parameters for a finer preview:
result = ptycho.reconstruct(aberrations, upsampling="auto")
"auto" selects the smallest supported factor (1, 2, 3, 4 or 8) whose output
Nyquist frequency covers the ideal circular-aperture SSB bandwidth,
2 * semiangle / wavelength, on both scan axes. Equivalently, output spacing
must be at most wavelength / (4 * semiangle), with semiangle in radians.
This uses the calibrated voltage, convergence semiangle and scan spacing.
It is a numerical grid criterion, not a measured resolution: detector coverage,
noise, coherence, aberrations and alias separation can reduce useful bandwidth.
See the direct-ptychography upsampling paper.
More than 8× raises an explicit limit error. CUDA and MPS support all 14
aberration terms at every listed factor, with or without specimen tilt and
depth correction. Both reconstruct the phase of the mean wave (phase_of_mean).
The aberration search and loss remain on the native grid. Upsampling excludes
replicas of the scan mean while retaining the normalization at the true origin.
For an explicit sampling comparison:
result_2x = ptycho.reconstruct(aberrations, upsampling=2)
result_4x = ptycho.reconstruct(aberrations, upsampling=4)
Show both full fields with a marked, matching crop underneath:
result_2x.show(compare=result_4x, region="top-left", size="large")
The library outlines the same physical zoom in both full images and shows it
below them. Each zoom spans one quarter of the width and height. Use
region="center" (the default), "top-left", "top-right", "bottom-left",
or "bottom-right". Method names and regularization are included in the titles;
upsampling is labeled separately. Region selection changes only the display.
All four panels share the first result’s symmetric phase_limits.
Reconstruction carries those limits from the supplied aberration result, so its
phase, rotation diagnostics and finer outputs use the same linear mapping. The
results must cover the same physical field; this view does not align images.
fit_aberrations() has no upsampling argument: the search stays at 1×.
The Gold comparison shows 1× and 4× outputs with shared
contrast and a matched crop. That Gold result has visible bands at 4×;
the example does not establish improved resolution.
Fit a region and reconstruct the full scan#
aberrations = ptycho.fit_aberrations(
scan_region=(128, 384, 128, 384), # row start/stop, column start/stop; scan pixels
tilt=True,
)
result = ptycho.reconstruct(aberrations, upsampling="auto")
result.show()
The stop indices are exclusive. Only the selected region contributes to the
aberration search; reconstruction still uses the full scan. scan_region=None
fits the full scan. The detector calibration remains tied to the full input.
Regional fitting currently requires a dense 4D scan source and a square region
of 128, 256, 512 or 1024 pixels per side, without implicit padding or cropping.
The result records fit_scan_region, and saved-result matching includes it.
With show=True, standard/broad fits initially display full-field SSB with the
fit region boxed and a matching close-up. The final resident-CUDA view compares
three full fields above their matching fit-region zooms. These previews apply regional optics
to the full acquisition; they do not expand the fitting data. The initial and
final previews add full-field reconstruction work; show=False skips it.
Its saved search phase and branch comparison describe that region (the
dark-field columns are located on the region of the full scan’s dark field). Applying
those parameters elsewhere assumes the optics and specimen tilt are shared;
a locally good fit does not establish that the whole specimen is uniform.
Settle the 180° branch#
The centre-of-mass curl fixes the scan-detector rotation only modulo 180°: the
angle and the angle plus 180° reverse every CoM vector and both leave the field
curl-free. SSB fits both equally well, the second as the conjugate object with
C10 of the opposite sign. For a rotation the session estimated, reconstruct()
and fit_aberrations() therefore fit twice, at the estimate and at the
estimate plus 180°: two independent searches with the same settings (tilt,
ranges, trials, seed), neither started from the other. The branch is decided on
tilt-corrected SSB, by positive phase at the atomic columns located on the dark
field, with the phase tail as the fallback: correcting the sample tilt and depth
spread makes the column contrast clearer, while both branches still fit equally
well. Each fit’s phase is reconstructed at the native scan sampling, and the
branch is chosen in two steps:
Atomic columns. The columns are located on the dark field of the same scan,
detector.dffrom 1.1 times the bright-field radius to the detector edge. Dark field is incoherent, so the positions are the same on both branches. The image is smoothed by a 1-pixel gaussian and high-passed by subtracting a 2.1 Å gaussian. Its local maxima at least 2 Å apart are ranked by prominence (height above the window’s minimum) and the top 20% are the columns (an 8-pixel high pass and a 17 × 17 window at 0.264 Å). The phase minus its mean, high-passed the same way, is sampled at the columns:z = mean / (std / sqrt(n))andeffect = mean / std. Atoms push the phase forward, so when either branch has|z| >= 3and|effect| >= 0.25the branch with the more positive columns is kept. The effect floor keeps a negligible shift over many columns from deciding.Phase tail. When the column test is not decisive, or the session has no dark field, the branch with the larger mean-centred third-moment ratio
S = mean(p³) / mean(p²)^1.5,p = phase − mean(phase)(quantem.gpu.direct_ptycho.results.phase_skewness) is kept: a few bright columns over a flatter background.
One line says which branch was kept and why, for example
SSB: kept 339.4°: atomic columns positive (z +10.8, effect +0.70, 75% of 241 columns); 159.4° negative.
The comparison costs a second full search: two joint fits took 9 to 25 s in
all on 512 × 512 scans on an M-series Mac, two MPS standard fits
(tilt=False) 3 to 10 s, two phase-variance fits about 90 s.
rotations = ptycho.scan_rotations(region=None, zoom=3, size="large")
# report=True is the default: phase images, histograms and a numerical table.
# rotations.report() returns the table again without fitting.
result = ptycho.reconstruct() # the kept fit, no third search
result.show("rotation") # the same figure from the result
scan_rotations(**fit_options) accepts the numerical controls for the primary
aberration fit, such as trials, tilt, and refinement. Use region to select
the fitting area; check_rotation is not needed. A rotation given to
DirectPtycho(...) or recorded with the acquisition is the user’s calibration:
reconstruct() and fit_aberrations() fit it once and never move it, and
scan_rotations() on it reports the evidence and keeps the given
angle unless called with apply=True. fit_aberrations(check_rotation=False)
fits once at the session’s rotation, estimated or not.
fit_aberrations(refine_rotation=True) (off by default) also refines an
estimated angle after the branch decision. G(q, k) does not depend on the
rotation, only the bright-field k vectors do, so each angle costs one polish of
the fit from the nearest angle’s fit (set_rotation retargets the k geometry
without rebuilding G): 1-degree steps from the estimate until the agreement
falls on both sides, at most 10 degrees away, then bounded Brent to 0.05
degrees, 7 to 12 polishes in all. One line reports it, for example
SSB: refined rotation 358.31° → 1.24° (+2.93°, agreement +9.82%; 11 polishes, 12.5 s)
on the 512 × 512 gold scan (M5 Max). result.rotation_estimate_deg keeps the
estimate, result.rotation_refined says whether the angle moved,
result.rotation_refinement records the agreement at both angles and the
angles evaluated, and report() shows both angles. A given or recorded angle
is never refined. Use it when the centre-of-mass estimate is suspect (weak curl
contrast, thick or tilted crystals) or before iterative ptychography. It finds
the fit’s best angle within its model, which need not improve the SSB image: on
gold the phase image at the refined angle correlates 0.98 with the image at the
estimate, with the same column width, roundness and contrast. On a simulated
crystal (64 × 64, noise free) it recovers a 2-degree error to within
0.01 degrees.
Bright columns are a physical assumption, not proof of the absolute
orientation. The column test assumes the dark-field image peaks on the
columns. A dark field that collects only low angles from a strongly defocused
probe can break that: in a noise-free 64 × 64 simulation (4 Å lattice, dark
field 33 to 55 mrad, tilt-corrected fits) the column test chose the wrong
branch at C10 = −5 and −8 nm and the right one at −3 nm, while the phase tail
was right in all three.
In the figure, the top row is each branch’s fit reconstructed at
upsampling="auto" by the session (its own rotation, aberrations,
tilt and depth spread; 0.4 s for both 4× panels of the 512 × 512 gold scan),
with the dark-field columns marked at scan pixels times the factor. The
bottom row is the evidence, from the native phases the decision used: the
full-field phase distributions. The high-pass column statistic remains part of
the rotation decision and numerical report, but is not plotted.
branches.show(panels=False) draws the evidence alone, without the GPU.
View the phase and model probe#
result = ptycho.reconstruct(aberrations, upsampling="auto")
result.show() # one large phase image
In a separate notebook cell:
result.show("probe") # real-space and Fourier-space probe intensities
The returned Matplotlib figure renders once in a notebook. Phase has a
12 × 12 inch panel; each probe or trial panel is 6 × 6 inches. Use axsize to
change panel size. Probe and trial comparisons use at most two columns.
For example, replace the phase call with result.show(axsize=(14, 14)), or
use result.show("probe", axsize=(7, 7)) for larger probe panels. These are
panel widths and heights in inches; image sampling stays unchanged.
Phase and both model-probe intensities use linear contrast over their full ranges. Each probe has its own display range and scale bar: Å in real space, mrad in Fourier space. Fourier intensity shows the aperture; defocus and astigmatism are encoded in the complex Fourier probe’s phase. Plotting does not change the calculated waves.
A defocused model can have rings in real-space intensity. Twofold astigmatism can make that pattern elliptical. A uniform Fourier-intensity disk does not mean the probe is aberration-free: inspect the complex Fourier phase to see those aberrations. Neither view independently validates the fitted coefficients.
To work with the complex arrays directly:
probe = result.probe() # centered complex real-space wave
probe_fourier = result.probe(space="fourier") # centered complex reciprocal-space wave
result.probe_sampling_A # Å per probe pixel, (row, column)
result.probe_sampling_mrad # mrad per model-probe Fourier pixel, (row, column)
Voltage, semiangle and effective aberrations come from
result, including any coefficient overrides. Both waves have unit summed
intensity and scan-frame axes. The probe is the fitted circular-aperture optical
model, not an independently retrieved wave or a measured vacuum pattern.
Specimen tilt does not become beam tilt; a thick-model probe is at mid-depth.
The model grid resolves the aperture and expands to cover the defocus and
astigmatism footprint. It is independent of detector binning and object
upsampling. Finer model sampling does not add measured information.
The phase–probe cell shows the calibrated scale bars.
CUDA is physically tested for this view; Python MPS uses MLX but has not yet
been checked on hardware for this addition.
Upsampling support#
CUDA and MPS support factors 1, 2, 3, 4 and 8 with all 14 aberration terms and optional tilt/depth correction. The field of view stays fixed. The search and diagnostic loss stay on the native scan grid; changing the factor does not refit parameters or interpolate detector measurements. More output pixels do not guarantee more resolved specimen detail. These Python backend guarantees do not qualify the separate browser WebGPU or native Swift implementations.
When migrating hard-coded aberrations from releases before the nm correction, divide old C10/C12 numbers and search bounds by 10. For example, an old value of 100 represented 100 Å and should now be supplied as 10 nm. Do not rescale angles or values already recorded in nm. Saved records must declare nm explicitly and use the current schema. Rerun the aberration search for older records; the loader does not guess their units.
Errors and unsupported requests#
Missing voltage, semiangle, or scan sampling raises rather than inventing calibration.
An unsupported backend or scientific request fails explicitly; SSB never falls back silently to CPU.
DirectPtycho(data.data, ...)raisesTypeError: the encoded storage inside a loaded dataset has no bounded read of its own. Pass the dataset,DirectPtycho(data, ...), or open the file withDirectPtycho.open(path, ...).trialsis the search budget per rotation branch (default 200), before refinement.num_trialsremains a compatibility alias; supply only one keyword.trialsmust be non-negative andrefinementis"auto"orNone(asearch="primary"fit’s best trial, unrefined). Withtilt=Trueandtrials=0the session’s previous joint fit is refined. Usereconstruct(), or assignptycho.aberrations = {...}, to apply known parameters without a search.To save results from an in-memory array, provide
source_pathfor provenance. Direct arrays never reuse results from disk automatically: a path cannot identify which crop or edited array the scientist supplied.
Provenance and exact reuse#
save_dir names a results directory. fit_aberrations writes
ssb-find_aberrations.npz and .json there, reconstruct writes
ssb-reconstruct.npz and .json; a path ending in .npz or .json is
refused. It writes the complex object and a readable signature containing source
identity, source and output shapes/dtypes, calibration, physical parameters,
optimizer settings/history, bright-field geometry, loss, timings, backend,
package source identity, and Git revision. An exact signature match reopens the
saved result for a file-backed DirectPtycho.open session. Direct array sessions always
recompute when saving; no full-array hash or GPU-to-host copy is required to
check reuse. Any scientific mismatch recomputes instead of reusing stale
output. Inspect result.reused, result.saved_path, and result.metadata.
Searches start from the current session coefficients, including changes made
with reconstruct(aberrations={...}). Those starting coefficients participate
in the saved search identity, so a different refinement start cannot inherit
an older result.
Scripts and fixed aberrations#
from quantem.gpu import DirectPtycho
with DirectPtycho.open(
"acquisition.qem",
voltage_kV=300,
semiangle_mrad=30,
scan_sampling_A=(0.264, 0.264),
) as batch_ssb:
batch_aberrations = batch_ptycho.fit_aberrations(save_dir="results/aberrations")
result = batch_ptycho.reconstruct(batch_aberrations, save_dir="results/native")
The context manager closes the session after the block. In a still-open
notebook session, use reconstruct() when aberrations are known and no
optimizer should run:
result = ptycho.reconstruct(
aberrations={"C10_nm": 12.5, "C12_nm": 3.0, "phi12_deg": 15.0},
save_dir="results/fixed-ssb",
)
preview() accepts the same complete aberration mapping and returns a
transient phase array plus an optional exact loss. The array has the full
selected output resolution: “preview” means it is not saved and does not
replace the fitted calibration or stored result, not that it is lower quality.
It does not create a second public result type.
Phase averaging#
SSB reconstructs the phase of the bright-field mean wave (phase_of_mean): the
corrected spectra are combined before one inverse FFT, and the phase is taken
once. Taking a phase is nonlinear: angle(mean(waves)) is not
mean(angle(waves)). Taking each angle first discards amplitude information
and compresses reconstructed contrast, most at higher output sampling, where
detector-dependent structure in the added frequency bands enters the phase
extraction. Combining the corrected spectra before one inverse FFT gives the
same wave sum as summing separate inverse FFTs, because both operations are
linear. Stronger contrast alone does not establish quantitative phase accuracy.
QuantEM’s DirectPtychography SSB implementation uses a related but distinct
weak-phase convention: multiply by -1j * conj(gamma) / abs(gamma), inverse
transform, take the real part, normalize by BF weight, and sum contributions.
It does not average per-contribution phase angles. Do not describe our
DC-referenced angle(mean(waves)) as numerically identical to that output.
CUDA and MPS use it at every output factor, including 1x, with higher orders and with sample tilt and depth:
phase, native_loss = ptycho.preview(
dict(aberrations),
sample_tilt_mrad=aberrations.sample_tilt_mrad,
depth_spread_nm=aberrations.depth_spread_nm,
upsampling=4,
)
Fitting never runs inside preview. The diagnostic loss is the native-grid
per-detector phase variance at every output factor. Use compute_loss=False
when only the image is needed.
The per-detector phase average (phase_estimator="mean_phase") and the 1x
complex object (phase_estimator="complex_wave") were retired on 2026-10-25.
Asking for either raises an error naming its replacement, the regularized
method="phase" or method="complex" (see
Choose a direct reconstruction). Saved results and
records that name a retired estimator still load as they were saved, with a
one-line notice; reconstruct() recomputes them with SSB. phase_estimator
otherwise accepts only "phase_of_mean", the default.
Reduced contrast compression from wave averaging does not establish quantitative phase accuracy. Known-phase controls still show amplitude attenuation, and improvement is not uniform across tested noise/sampling cases. There is no fitted gain, display normalization or detector interpolation. Higher-order corrections, tilt/depth and automatic sampling can be combined on both backends. The fit still searches C10/C12 and specimen geometry; higher-order coefficients are supplied corrections, not extra fitted parameters.
Regression checks on the chosen physical CUDA device:
CUDA_VISIBLE_DEVICES=1 python -m pytest tests/hardware/cuda/test_ssb_wave_average.py -q
These compare public output against independent Torch per-detector inverse transforms at 1x/2x/3x/4x, with and without depth/tilt, unchanged native loss and memory-batch invariance. They validate the implementation, not absolute phase accuracy.
Thick, tilted crystals#
Standard SSB treats the sample as one thin plane. In a crystal a few nanometres thick that leans by a few milliradians, each atomic column walks sideways with depth (5 mrad over 10 nm is 0.5 A), which blurs the lattice along the tilt. To isolate the effect of tilt, compare the same primary aberration model with and without joint sample tilt and depth fitting. The default fifth-order fit adds more optical terms, so it is a separate comparison:
import pandas as pd
standard = ptycho.fit_aberrations(search="primary", tilt=False) # C10_nm, C12_nm, phi12_deg; same objective, tilt held at 0
tilted = ptycho.fit_aberrations(search="primary") # + specimen tilt and depth spread, jointly
tilted.sample_tilt_mrad # (row, col) mrad, scan frame
tilted.depth_spread_nm # model depth spread, not a measured thickness
tilted.sample_agreement # fraction of the band's power the model explains, 0 to 1
tilted.tilt_fit_gain # agreement over the best standard-SSB search's (> 1: tilt explains more)
pd.concat([standard.report(), tilted.report()], axis=1) # one column per fit
After a tilt fit, C10_nm is the defocus at mid-depth, so it can differ from the
standard fit’s. The search is joint on purpose: fitting the tilt after a
standard fit leaves C10 behind and stalls (0.91-0.99 of the best agreement on
two acquisitions, every seed). With 200 trials every seed reached the same tilt
within 0.02 mrad; 300 and 400 trials gave the same answer.
preview(aberrations, sample_tilt_mrad=(row, col), depth_spread_nm=d) renders the
thick-sample model for interactive viewers; with no depth spread the tilt has
no effect. CUDA and MPS both implement it.
Limits:
The phase-variance loss in
aberrations.lossdoes not reward tilt; judge a tilt bytilt_fit_gainand the lattice, not by the loss.A tilt at the search limit (
tilt_limit_mrad, default 25) or a gain close to 1 is not a measurement.One tilt direction can be loosely determined (on one film acquisition the row tilt varied by 0.6 mrad at 0.05 % of the agreement).
The tilt is in the scan frame. The ptychography object frame is the scan rotated by the same scan-detector rotation.
Evidence: SSB units and the thick-sample model.
Native Swift and Metal#
Native application authors should use the Swift/Metal integration guide.
Series reconstruction#
DirectPtycho.run_series() discovers numbered acquisitions in one directory
and returns one SSBSeriesResult. Inclusive first_frame and last_frame
values select acquisition identifiers, not positional array indices. Set
probe_reference_frame to reuse one fitted probe; omit it to fit each
acquisition independently. The result retains the phase, bright-field and
dark-field stacks, frame/dataset identifiers, source/results directories,
backend request, optimizer settings, and per-acquisition records.
save_dir names the folder of per-acquisition records (QuantEM Live’s
<output>/live/screen; default <source>/live/screen),
scan_detector_rotation_flipped gives the flip (None: the saved fit’s, else
unflipped for a recorded rotation), and an acquisition with no recorded
rotation is estimated by DirectPtycho.open. scan_sampling_A may be one number.
quantem.gpu.direct_ptycho.series.screen_record_settings(record) returns the
DirectPtycho.open keywords a QuantEM Live screening record’s stored fit used (beam,
(row, col) scan and detector sampling, physical rotation and flip,
bright-field centre and radius, aberrations), and
series.open_screen_record(record, source=None, backend="auto") opens the
session at that fit, so ptycho.show() opens ShowPtycho there. The fit is the
record’s tilt-corrected fit when it has one (series.screen_record_fit:
computed.ssb_tilt beside an ordinary fit, else computed.ssb), and the
session also takes its sample tilt and depth spread
(series.screen_record_sample).
quantem.gpu.formats.dataset_yaml.file_entry(document, master) finds an
acquisition’s files entry by QuantEM Live’s rules (its master name, a
.qem copy’s master, then the trailing number or a range key such as
"06-24" that names no other master), and
acquisition_scan_sampling_A(document, master, ...) resolves its step.
A fit stored in live/screen/<dataset>/config.json is read only when its
computed.ssb block carries "schema": "quantem.ssb-fit/7"
(quantem.gpu.direct_ptycho.persistence.FIT_SCHEMA), which reconstruct_series writes
with every fit. A block with another schema or none is treated as absent: its
aberrations, rotation and recorded settings are not read or converted, the
probe reference is fitted again, and the physical settings come from
dataset.yaml or the keyword arguments. Evidence:
stored fit schema.
That config.json is QuantEM Live’s screening record, which Live reads only
with "schema": "quantem.screen-acquisition/2" at its top level
(quantem.gpu.direct_ptycho.persistence.SCREEN_ACQUISITION_SCHEMA). A record that
reconstruct_series creates carries that key. In a record that exists it
changes nothing outside computed.ssb; a record with another top-level
schema, or none, raises ValueError before anything is written and names
QuantEM Live’s tools/rewrite_screen_config.py.
dataset.yaml is read only at schema quantem.dataset/2: the microscope
voltage and semi-angle, the file entry’s scan_detector_rotation_deg (a
per-file 0.0 counts) else microscope.scan_detector_rotation_deg, and the
[row, col] step of the file entry, its calibrations row or
calibrations.default. Another schema raises and names QuantEM Live’s
tools/rewrite_dataset_yaml.py.
Integration boundary#
QuantEM.GPU owns SSB preparation, the exact objective, optimization, reconstruction, typed results, persistence signatures, and backend parity. A consuming application owns controls, progress presentation, cache scheduling, and visualization. It must present approximate previews as previews and must not promote them to exact calibration evidence.
See Single-sideband ptychography for the complete mathematics and SSB performance reports for dated, revision- and device-qualified measurements. Performance numbers do not live in this API contract because a new benchmark must not silently change API semantics.
Inspect the launch calibration#
Load first so acquisition metadata remains available independently of SSB:
from quantem.gpu import DirectPtycho, io
data = io.load("scan.qem")
data
Voltage, convergence semiangle and scan step are read from the loaded metadata. For scan-detector rotation, explicitly use the recorded angle, or provide an independently calibrated angle when the file does not record one:
ptycho = DirectPtycho(data, scan_detector_rotation_deg=data.metadata["scan_detector_rotation_deg"])
ptycho.calibration()
The metadata lookup intentionally raises if rotation is missing. Replace it with your known calibrated angle; do not silently substitute zero. Omitting rotation estimates it from the centre-of-mass curl, labelled Estimated (centre-of-mass curl), not a measured calibration.
ptycho.calibration() returns a DataFrame with File value, File unit,
Value used, Unit used, and Source, using the session’s current values.
File values include saved calibration overrides. Source labels are Recorded,
Overridden (set on data.calibration), Fitted (bright-field disk) (data.calibration.fit_detector_sampling()),
Explicit override, Estimated,
Estimated (centre-of-mass curl) and Default, from the one label set of
quantem.gpu.io.metadata_display. Later
session changes are labeled Modified in session. Explicit keywords remain
overrides even when they equal stored values. Labels describe provenance,
not validation or confidence. Reading the table does not reconstruct or alter
source data; detector estimates remain Pending probe fit until preparation.
Detector sampling is optional: omit detector_sampling_mrad to reuse the
loaded acquisition’s saved calibration. If missing, SSB estimates sampling
from the full-field probe disk edge and the known convergence semiangle.
The working session retains the estimate for reconstruction, regional sessions
and persistence. The source QEM and its recorded metadata remain unchanged.
An explicit numerical keyword remains available for an independent calibration.
The convergence semiangle must be known; disk radius alone cannot set an
absolute angular scale.
Automatic output sampling#
Auto chooses 1×, 2×, 3× or 4× from the acquisition calibration, with a
maximum of 4×. Select 8× explicitly with ssb.show(upsampling=8) or the
ShowPtycho Upsampling menu. For a standalone reconstruction, use
ssb.reconstruct(fit, upsampling=8).
The nominal bandwidth requirement remains separate from the selected grid. When Auto is capped, the fit report and viewer settings identify the larger requirement; a 4× grid does not imply full nominal bandwidth. Fitting and source data are unchanged. Saved Auto fits reopen under the current cap; saved explicit 8× choices retain 8×.
Use scan_rotations() for both diagnostics; set defocus=True for the
four-case comparison. The old rotation-comparison method names were removed.