|
| 1 | +"""Forward model for simulating diffraction patterns from a ptychography data product.""" |
| 2 | + |
| 3 | +import numpy |
| 4 | + |
| 5 | +from scipy.fft import fft2, fftfreq, ifft2 |
| 6 | + |
| 7 | +from .diffraction import BadPixels, DiffractionIndexes, DiffractionPatterns |
| 8 | +from .geometry import PixelGeometry |
| 9 | +from .io import AssembledDiffractionData |
| 10 | +from .product import Product |
| 11 | +from .propagator import AngularSpectrumPropagator, FraunhoferPropagator, PropagatorParameters |
| 12 | + |
| 13 | + |
| 14 | +def generate_diffraction_data( |
| 15 | + product: Product, rng: numpy.random.Generator | None = None |
| 16 | +) -> AssembledDiffractionData: |
| 17 | + """Simulate diffraction patterns for all scan positions in *product* using a multislice forward model. |
| 18 | +
|
| 19 | + If *rng* is provided, Poisson noise is added to the intensity patterns. |
| 20 | + """ |
| 21 | + object_ = product.object_ |
| 22 | + probe_geometry = product.probes.get_geometry() |
| 23 | + |
| 24 | + propagator_parameters = PropagatorParameters( |
| 25 | + wavelength_m=product.metadata.probe_wavelength_m, |
| 26 | + width_px=probe_geometry.width_px, |
| 27 | + height_px=probe_geometry.height_px, |
| 28 | + pixel_width_m=probe_geometry.pixel_width_m, |
| 29 | + pixel_height_m=probe_geometry.pixel_height_m, |
| 30 | + propagation_distance_m=product.metadata.detector_distance_m, |
| 31 | + ) |
| 32 | + |
| 33 | + # TODO also support near-field propagation |
| 34 | + propagator = FraunhoferPropagator(propagator_parameters) |
| 35 | + |
| 36 | + # One angular-spectrum propagator per inter-layer gap |
| 37 | + interlayer_propagators = [ |
| 38 | + AngularSpectrumPropagator( |
| 39 | + PropagatorParameters( |
| 40 | + wavelength_m=product.metadata.probe_wavelength_m, |
| 41 | + width_px=probe_geometry.width_px, |
| 42 | + height_px=probe_geometry.height_px, |
| 43 | + pixel_width_m=probe_geometry.pixel_width_m, |
| 44 | + pixel_height_m=probe_geometry.pixel_height_m, |
| 45 | + propagation_distance_m=spacing_m, |
| 46 | + ) |
| 47 | + ) |
| 48 | + for spacing_m in object_.layer_spacing_m |
| 49 | + ] |
| 50 | + |
| 51 | + num_positions = len(product.probe_positions) |
| 52 | + indexes: DiffractionIndexes = numpy.zeros(num_positions, dtype=int) |
| 53 | + patterns: DiffractionPatterns = numpy.zeros( |
| 54 | + (num_positions, probe_geometry.height_px, probe_geometry.width_px), |
| 55 | + dtype=float, |
| 56 | + ) |
| 57 | + lambda_z_m2 = propagator_parameters.wavelength_m * propagator_parameters.propagation_distance_m |
| 58 | + pixel_geometry = PixelGeometry( |
| 59 | + width_m=lambda_z_m2 / probe_geometry.width_m, |
| 60 | + height_m=lambda_z_m2 / probe_geometry.height_m, |
| 61 | + ) |
| 62 | + bad_pixels: BadPixels = numpy.full((probe_geometry.height_px, probe_geometry.width_px), False) |
| 63 | + |
| 64 | + # Precompute frequency grids for Fourier shifting the probe |
| 65 | + freq_y = fftfreq(probe_geometry.height_px) |
| 66 | + freq_x = fftfreq(probe_geometry.width_px) |
| 67 | + fy, fx = numpy.meshgrid(freq_y, freq_x, indexing='ij') |
| 68 | + |
| 69 | + object_geometry = object_.get_geometry() |
| 70 | + |
| 71 | + for index, (probe_position, probe) in enumerate(zip(product.probe_positions, product.probes)): |
| 72 | + object_position = object_geometry.map_coordinates_probe_to_object(probe_position) |
| 73 | + |
| 74 | + cx = object_position.coordinate_x_px |
| 75 | + cy = object_position.coordinate_y_px |
| 76 | + |
| 77 | + x_lower = int(cx - probe_geometry.width_px / 2) |
| 78 | + y_lower = int(cy - probe_geometry.height_px / 2) |
| 79 | + |
| 80 | + # Extract patches from all layers at the same integer position |
| 81 | + object_patches = [ |
| 82 | + object_.get_layer(ilayer)[ |
| 83 | + y_lower : y_lower + probe_geometry.height_px, |
| 84 | + x_lower : x_lower + probe_geometry.width_px, |
| 85 | + ] |
| 86 | + for ilayer in range(object_.num_layers) |
| 87 | + ] |
| 88 | + |
| 89 | + # Subpixel offsets between the true position and the integer extraction position |
| 90 | + dx = cx - (x_lower + probe_geometry.width_px / 2) |
| 91 | + dy = cy - (y_lower + probe_geometry.height_px / 2) |
| 92 | + |
| 93 | + # Shift all incoherent modes at once via a single batched FFT pair |
| 94 | + shift_phase = numpy.exp(-2j * numpy.pi * (fy * dy + fx * dx)) |
| 95 | + shifted_modes = ifft2(fft2(probe.get_array()) * shift_phase) |
| 96 | + |
| 97 | + for wavefield in shifted_modes: |
| 98 | + # Multislice: apply each layer then propagate to the next; last layer has no propagation |
| 99 | + for object_patch, interlayer_propagator in zip(object_patches, interlayer_propagators): |
| 100 | + wavefield = wavefield * object_patch |
| 101 | + wavefield = interlayer_propagator.propagate(wavefield) |
| 102 | + wavefield = wavefield * object_patches[-1] |
| 103 | + |
| 104 | + wavefield = propagator.propagate(wavefield) |
| 105 | + patterns[index] += numpy.square(numpy.abs(wavefield)) |
| 106 | + |
| 107 | + indexes[index] = object_position.index |
| 108 | + |
| 109 | + if rng is not None: |
| 110 | + # NOTE: object and probe scaling influence how much noise is added |
| 111 | + patterns = rng.poisson(patterns).astype(float) |
| 112 | + |
| 113 | + return AssembledDiffractionData(indexes, patterns, pixel_geometry, bad_pixels) |
0 commit comments