xptycho blueprints › Software Architecture

Software Architecture Blueprint

How the five kinds of quantities from the algorithm blueprint map onto objects. The layout is the mbirtorch pattern: one model holds the parameters and the forward model, and the unknowns pass through it as arguments and results. The unknowns are held in a Sample: an object and the probe it was seen with. A ground truth and a reconstruction are both a Sample; simulate takes one and recon returns one. Position refinement is a method the user calls (section 7).

1. The three things a user holds

Scan

The measurement. Never modified.
  • data the frames, held in memory. They are intensities; their square roots are the amplitudes \(y_j\).
  • facts from the file positions, wavelength, distance, detector pitch, as recorded. Read once, to build a model.
  • Made by Scan.load(path), by model.simulate, or by Scan(frames, probe_positions, …) after preprocessing raw frames.

PtychoModel

The parameters and the forward model. Holds no image and no probe.
  • forward-model parameters fixed when the model is built, except the positions, which set_params can replace.
  • reconstruction parameters set and read by name. device settings the devices, chosen by configure_devices, and the batch size, set by name.
  • Computes the forward model, simulates data, and reconstructs. Drawn in full below.

Sample

The unknowns: an object and the probe it was seen with. A truth or an estimate.
  • unknowns the object image \(x\), complex, on the object grid; the probe modes \(d\), complex \((K, N_p, N_p)\). Both on one grid, with its pixel_pitch.
  • A ground truth is a Sample, given to simulate. recon returns a Sample that also carries run, the record of how it was made. Drawn in section 3.
  • It saves itself to one HDF5 file and loads back (section 4), so a cluster job's output is one thing.

The run state is the fourth kind and nobody holds it: it exists inside recon and is gone when recon returns.

2. The model, in full

PtychoModel

Forward-model parameters

Given to the constructor, or read from a scan by from_scan. Fixed for the life of the model, except the scan positions, which set_params(probe_positions=...) can replace (section 7).
  • wavelength (or energy)
  • object-to-detector distance
  • detector pixel pitch
  • frame size \(N_p\)
  • scan positions, in meters
  • \(K\), the number of probe modes

Reconstruction parameters

set_params(...) and get_params(...), by name. The defaults are in the algorithm blueprint, section 7.
  • object_data_fit \(\alpha_1\), probe_data_fit \(\alpha_2\)
  • probe_weight_exponent \(\kappa\), relaxation \(\rho\)
  • mode_schedule, mode_energy_fraction, orthogonalize_modes
  • probe_fresnel_radius_pixels
  • object_shape, object_origin: the object grid, which must hold every patch; derived from the positions unless set

Device settings

They change where the arithmetic runs, and change the result only by rounding.
  • the devices: a count, or a list; configure_devices(...)
  • the batch size: automatic unless set with set_params(batch_size=...)

Derived, read-only: the object pixel pitch \(\lambda L / (N_p \Delta_{\mathrm{det}})\) (sample_pixel_pitch), the positions in pixels, the patch operators \(P_j\). The object grid shape and origin are derived from the positions unless set with object_shape and object_origin.

Methods

Written as a user calls them: PtychoModel(…) builds one, and model.name(…) is a method of the instance model. The one classmethod is marked.

PtychoModel(…) constructorBuild from the forward-model parameters.
PtychoModel.from_scan(scan, num_probe_modes=1, **overrides) classmethodBuild from the facts a scan file recorded. Any parameter passed overrides the file's value.
model.set_params(**p), model.get_params(names)Set and read the reconstruction parameters by name, batch_size among them, as in mbirtorch. set_params also takes probe_positions, to replace the positions (section 7). get_params also reads the forward-model parameters and sample_pixel_pitch.
model.configure_devices(num_devices=1, devices=None)Choose the devices, as in mbirtorch: a count of GPUs, or an explicit list. Without a call the model chooses automatically. See Hardware Mapping, section 4.
model.print_params()Print every parameter with its value, units, and origin (given, file, derived, default).
model.forward(x, d)The forward model. For every scan position: cut the patch out of x, multiply by each probe mode, far-field transform, sum the modes in intensity. Returns the noiseless amplitudes \(y_j\), shape \((J, N_p, N_p)\). Stores nothing.
model.simulate(sample, peak_photons=None, dark_rate=0, seed=0)Calls forward on the sample's object and probe, after checking that the sample's pixel_pitch equals the model's, scales the intensities so the brightest detector pixel is peak_photons, draws Poisson counts, and returns a Scan carrying this model's positions and instrument facts. Without peak_photons, the exact intensities.
model.recon(scan, probe=None, init_probe=None, init=None, iterations=100, verbose=1)The reconstruction. With probe=, the probe is known and held fixed. Without it, the probe is estimated: from init_probe= when given, else from the data-driven initialization. A given probe= must have all \(K\) modes. An estimate may start with fewer: one mode from the initialization, or however many init_probe= has, and reaches \(K\) by adding a mode at each iteration of mode_schedule (algorithm blueprint, section 5). The schedule must have enough entries to reach \(K\), or recon refuses before it starts. init= warm-starts from a Sample, usually a previous result: its object, and its probe when the probe is estimated and init_probe= is not given. iterations is the one setting that changes on every call, so it is an argument here, as mbirtorch's max_iterations is. verbose=1 prints one line per iteration; 0 prints nothing. Returns a Sample with its run record. The run state lives only inside this call.
model.data_error(scan, x, d)The normalized distance between forward(x, d) and the scan's amplitudes. The curve recon reports, available on its own.
model.refine_probe_positions(scan, x, d, max_shift=1)One refinement pass: for each position, the integer shift within max_shift whose forward best matches the data, given x and d. Returns the refined positions and the per-position misfit. Changes nothing. Section 7.
model.initial_probe(scan), model.initial_object(scan, d)The two initialization formulas of the algorithm blueprint, exposed so a user can look at the starting point. recon calls them when nothing is given.

3. Two directions through one model

The forward model is written once, in forward. Everything else that needs it calls it.

simulation: a known sample in, data out
Samplex, d: a known object and probe
→
model.simulate(sample, …)forward, then Poisson counts
→
Scanframes, positions, facts
reconstruction: data in, a sample out
Scanframes, positions, facts
→
model.recon(scan, probe=…)PMACE; the run state lives here
→
Samplex̂, d̂, and the record of the run

Two more uses share the same forward: data_error, and refine_probe_positions, which compares forward at shifted positions against the data. A continued run is another recon call on the same model with init= the previous Sample. It rebuilds the per-position copies from the previous object and probe, so it is not identical to one uninterrupted run of the same length.

The Sample

A Sample holds the unknowns and what is needed to read them. One that recon returns also holds run, a record of the run; for a ground truth run is absent. The record is not a new kind of quantity; it is copies and derived values of what the model holds, frozen at the time of the run. Nothing of the run state and nothing of the measured data is in a Sample.

Sample

The unknowns

True, or estimated. A probe given to recon is passed back, so the sample is complete on its own.
  • object: \(\hat x\), complex, on the object grid
  • probe: \(\hat d\), complex \((K, N_p, N_p)\)

To read the image

Where the grid sits.
  • pixel_pitch: the spacing of the pixels, in meters
  • origin: the position of the center of the first pixel, in meters
  • mode_energies: the share of the probe energy in each mode
  • name: a label

run: the record of the run

Only for a reconstruction. What model.print_params() showed, plus what happened.
  • run.parameters: every forward-model parameter, reconstruction parameter, and device setting, each with its value, units, and origin; also the iterations, whether the probe was given or estimated, and the run time in seconds
  • run.data_error: the data error at each iteration
  • run.iterations: the iterations run
  • run.probe_positions: the positions the run used
  • run.coverage: the accumulated probe weight; zero where no probe reached

Methods

sample.summary()The sizes and the pixel pitch as text; for a reconstruction, also the parameter table, the iteration count, and the final data error.
sample.save(path)Write the groups /sample and, for a reconstruction, /run of an HDF5 file (section 4).
Sample.load(path) classmethodRead the sample, and the record of its run when there is one, from an HDF5 file.

How the probe reaches \(K\) modes

Mode addition is inside recon. The user sets the target and the timing: \(K\) on the model (num_probe_modes=2) is how many modes the run ends with; mode_schedule=[20] is the iterations at which a mode is added; mode_energy_fraction is the energy a new mode starts with. recon starts with one mode (or the modes of init_probe= or init=) and at each scheduled iteration runs the six steps of theory section 5. The user sees it in three places: the progress line printed at that iteration ("iteration 20: mode 2 added, 5% of the energy"); the convergence figure, which marks that iteration; and the result, recon.probe of shape \((K, N_p, N_p)\) with recon.mode_energies, the final share of each mode.

4. The file format

A scan and a sample are groups of an HDF5 file. One file may hold either or both. Each class reads and writes only its own groups and keeps the others, so a simulated dataset can be one file holding the scan and its truth. Lengths are in meters. Only functions named for it touch files: load, save, load_raw, download, and save_figures. Every other function takes and returns objects in memory, and the package holds no address, folder, or file name.

file.h5                 attribute: format_version
    /scan               attributes: wavelength, det_distance, det_pixel_pitch, name
        frames          (J, Np, Np) intensities
        probe_positions (J, 2), row and column of each probe center; attributes: units, order
    /sample             attributes: pixel_pitch, origin, name, units
        object          complex64 (rows, cols)
        probe           complex64 (K, Np, Np)
    /run                attribute: iterations.  Present for a reconstruction.
        parameters      (N, 4) text: name, value, units, origin
        data_error      (iterations,)
        probe_positions (J, 2), the positions the run used
        coverage        (rows, cols), the accumulated probe weight
classreads and writes
Scan/scan
Sample/sample, and /run when it has a record

5. The scripts

# 1. simulated scan, known probe (demo 1, TCI 2023)
truth = xpt.Sample.load(xpt.download(TRUTH_URL, DATA_DIR))         # a Sample: truth.object, truth.probe, truth.pixel_pitch
pixel_pitch = truth.pixel_pitch
probe_positions = xpt.scan_positions((12, 12), 68 * pixel_pitch, 5 * pixel_pitch, seed=0)
det_distance = pixel_pitch * 256 * 75e-6 / xpt.energy_to_wavelength(8.8)   # object pixel = the truth's pixel pitch
model = xpt.PtychoModel(energy=8.8, det_distance=det_distance, det_pixel_pitch=75e-6,
                        frame_size=256, probe_positions=probe_positions)
model.set_params(object_shape=truth.object.shape)                  # the object grid of the truth
scan = model.simulate(truth, peak_photons=1e4, dark_rate=0.5, seed=0)
model.set_params(object_data_fit=0.7, probe_weight_exponent=1.5, relaxation=0.5)
model.print_params()
recon = model.recon(scan, probe=truth.probe, iterations=100)      # probe given, held fixed
xpt.view_sample(recon, compare_to=truth)                          # the estimate beside the truth
recon.save(OUTPUT_DIR + '/recon.h5')

# 2. measured scan, blind, two modes (demo 3, TCI 2025); xpp is xptycho.preprocess
raw = xpp.cxi.load_raw(xpt.download(RAW_URL, DATA_DIR))
frames = xpp.subtract_dark(raw['frames'], raw['dark_frames'])
outlier = xpp.find_outlier_frames(frames, 2.0)
frames, translations = frames[~outlier], raw['translations'][~outlier]
frames = xpp.crop_frames(frames, xpp.diffraction_center(frames), 512)
frames = frames * xpp.tukey_window(512, 0.5) ** 2
scan = xpt.Scan(frames, translations[:, [1, 0]], wavelength=raw['wavelength'],
                det_distance=raw['det_distance'], det_pixel_pitch=raw['det_pixel_pitch'])
model = xpt.PtychoModel.from_scan(scan, num_probe_modes=2)        # forward-model parameters from the scan
model.set_params(object_data_fit=0.5, probe_data_fit=0.6, probe_weight_exponent=1.25, relaxation=0.5,
                 mode_schedule=[10], mode_energy_fraction=0.05, orthogonalize_modes=True,
                 probe_fresnel_radius_pixels=2.5)
model.configure_devices(num_devices=4)                            # four GPUs; without this call, every GPU present
model.set_params(batch_size=512)                                  # optional; chosen from the free memory if not set
recon = model.recon(scan, iterations=100)                         # probe not given, so estimated
recon.probe                                                       # (2, 512, 512)
recon = model.recon(scan, init=recon, iterations=100)             # continue from the previous result
recon.save(OUTPUT_DIR + '/recon.h5')

6. Where each kind lives

kindlives in
measured dataScan
forward-model parametersPtychoModel, fixed at construction except the positions, which set_params can replace; a copy of the recorded ones in Scan
unknownsSample: given to simulate, returned by recon
reconstruction parametersPtychoModel, set_params
device settingsPtychoModel: the devices by configure_devices, the batch size by set_params
run stateinside recon
Why the unknowns are not in the model. The same probe is fixed in one run and estimated in the next; an argument states which, an attribute cannot. The object's size depends on the object grid, a reconstruction parameter, so it cannot exist when the model is built. And a truth and an estimate are then the same kind of object, so they are compared, saved, and shown in the same way.

7. Position refinement

Positions are a forward-model parameter, read from the scan file, and in most runs they are accurate enough to leave alone. Sometimes they are not, and the user usually knows which from the instrument. So refinement is a method the user calls, never something recon does on its own.

The method is Qiuchen's: for each position, try every integer shift within max_shift, run forward at each against the current object and probe, and keep the shift that best matches the measurement. It trusts the current image, so it is called after some iterations, and it costs 9 to 25 forward projections per position per pass.

recon = model.recon(scan, iterations=50)
model.set_params(object_shape=recon.object.shape, object_origin=recon.origin)   # keep the object grid fixed
new_pos, misfit = model.refine_probe_positions(scan, recon.object, recon.probe, max_shift=1)
model.set_params(probe_positions=new_pos)                       # accept the refined positions
recon = model.recon(scan, init=recon, iterations=150)     # continue with them