Simulation Module#
The cdiutils.simulation module provides tools for simulating BCDI data.
Simulation sub-package for BCDI experiments.
This sub-package provides comprehensive tools for simulating BCDI measurements, from creating synthetic 3D objects to generating realistic detector data with noise and experimental geometry.
Modules#
- objectsObject creation, phase manipulation, and diffraction
simulation.
noise : Noise models for realistic detector simulation. detector : End-to-end BCDI measurement simulator with detector
geometry.
Main Components#
- Object creation (from objects):
make_box : Create a 3D box (parallelepiped/cube). make_ellipsoid : Create a 3D ellipsoid or sphere. make_cylinder : Create a 3D cylinder.
- Phase manipulation (from objects):
add_linear_phase : Add linear phase gradient. add_quadratic_phase : Add quadratic phase (defocus, strain). add_displacement_field : Add phase from displacement field. add_random_phase : Add random phase noise.
- Diffraction simulation (from objects):
simulate_diffraction : Compute diffraction pattern via FFT.
- Noise models (from noise):
add_noise : Add Gaussian and/or Poisson noise to data.
- BCDI measurement simulation (from detector):
BCDISimulator : Complete BCDI measurement simulator.
Notes
The diffraction simulation uses forward FFT convention (optics/signal processing). For crystallographic conventions, manage phase signs in your object definition rather than changing the FFT direction.
For realistic noise modelling, the BCDISimulator
class provides end-to-end simulation including diffractometer
geometry, coordinate transformations, and detector effects.
Example
>>> from cdiutils.simulation import (
... make_box,
... add_random_phase,
... simulate_diffraction,
... BCDISimulator,
... )
>>>
>>> # simple diffraction simulation
>>> obj = make_box((64, 64, 64), dimensions=20)
>>> obj = add_random_phase(obj, amplitude=0.1)
>>> intensity = simulate_diffraction(obj, photon_budget=1e9)
>>>
>>> # full BCDI measurement simulation
>>> sim = BCDISimulator(
... energy=9000,
... lattice_parameter=4.08e-10,
... )
>>> sim.simulate_object(shape=(100, 100, 100))
>>> detector_data = sim.to_detector_frame()
- cdiutils.simulation.make_box(shape, dimensions=15, centre=None, rotation=None, value=1.0)[source]#
Create a 3D parallelepiped (rectangular cuboid) binary array.
A cube is a special case where all dimensions are equal.
- Parameters:
shape (tuple[int, int, int]) – 3D array shape (nz, ny, nx).
dimensions (int | tuple[int, int, int]) – Side lengths in pixels. If scalar, creates a cube. If tuple, order is (length_z, length_y, length_x).
centre (tuple[int, int, int] | None) – Centre position (z, y, x). If None, uses array centre.
rotation (ndarray | tuple[float, float, float] | None) –
Rotation to apply. Can be: - None: no rotation - (3,3) array: rotation matrix - tuple of 3 floats: Euler angles (deg) as
(alpha, beta, gamma) combined as Rz(alpha) @ Ry(beta) @ Rx(gamma)
value (float) – Value to fill inside the parallelepiped.
- Returns:
Binary array with value inside parallelepiped and 0 outside.
- Raises:
ValueError – If shape is not 3D, dimensions invalid, or rotation has invalid shape.
- Return type:
ndarray
- cdiutils.simulation.make_ellipsoid(shape, radii=15, centre=None, rotation=None, value=1.0)[source]#
Create a 3D ellipsoid binary array.
A sphere is a special case where all radii are equal.
- Parameters:
shape (tuple[int, int, int]) – 3D array shape (nz, ny, nx).
radii (float | tuple[float, float, float]) – Radii in pixels. If scalar, creates a sphere. If tuple, order is (rz, ry, rx).
centre (tuple[float, float, float] | None) – Centre position (z, y, x). If None, uses array centre.
rotation (ndarray | tuple[float, float, float] | None) –
Rotation to apply. Can be: - None: no rotation - (3,3) array: rotation matrix - tuple of 3 floats: Euler angles (deg) as
(alpha, beta, gamma) combined as Rz(alpha) @ Ry(beta) @ Rx(gamma)
value (float) – Value to fill inside the ellipsoid.
- Returns:
3D array with value inside ellipsoid and 0 outside.
- Raises:
ValueError – If radii or rotation have invalid shape.
- Return type:
ndarray
- cdiutils.simulation.make_cylinder(shape, radius=10.0, height=25.0, centre=None, axis=0, rotation=None, value=1.0)[source]#
Create a 3D cylinder binary array.
- Parameters:
shape (tuple[int, int, int]) – 3D array shape (nz, ny, nx).
radius (float) – Cylinder radius in pixels.
height (float) – Cylinder height in pixels.
centre (tuple[float, float, float] | None) – Centre position (z, y, x). If None, uses array centre.
axis (int) – Cylinder axis direction as integer (0=z, 1=y, 2=x).
rotation (ndarray | tuple[float, float, float] | None) – Additional rotation to apply after axis alignment. Same format as other shape functions.
value (float) – Value to fill inside the cylinder.
- Returns:
3D array with value inside cylinder and 0 outside.
- Raises:
ValueError – If axis is invalid or parameters negative.
- Return type:
ndarray
- cdiutils.simulation.add_linear_phase(obj, phase_gradient=(1.0, 1.0, 1.0), apply_to_support=True)[source]#
Add a linear phase gradient to an object.
- Parameters:
obj (ndarray) – Real-valued object (amplitude).
phase_gradient (tuple[float, float, float]) – Phase gradient (radians/pixel) along (z, y, x) directions.
apply_to_support (bool) – If True, apply phase only where obj > 0.
- Returns:
Complex object with linear phase applied.
- Return type:
ndarray
- cdiutils.simulation.add_quadratic_phase(obj, curvature=(2, 2, 2), apply_to_support=True)[source]#
Add quadratic phase (e.g., defocus, strain) to an object.
- Parameters:
obj (ndarray) – Real-valued object (amplitude).
curvature (tuple[float, float, float]) – Phase curvature coefficients (radians/pixel²) along (z, y, x) directions.
apply_to_support (bool) – If True, apply phase only where obj > 0.
- Returns:
Complex object with quadratic phase applied.
- Return type:
ndarray
- cdiutils.simulation.add_displacement_field(obj, displacement_field, q_bragg)[source]#
Add phase from a 3D displacement field.
The phase is computed as φ = 2π * Q · u(r), where Q is the Bragg vector and u(r) is the displacement field.
- Parameters:
obj (ndarray) – Real-valued object (amplitude).
displacement_field (ndarray) – 3D displacement vector field with shape (3, nz, ny, nx) where first axis is (uz, uy, ux).
q_bragg (tuple[float, float, float]) – Bragg vector (qz, qy, qx) in reciprocal space units.
- Returns:
Complex object with displacement-induced phase.
- Raises:
ValueError – If displacement_field shape is incompatible.
- Return type:
ndarray
- cdiutils.simulation.add_random_phase(obj, phase_std=2.0, correlation_length=10, apply_to_support=True)[source]#
Add random phase noise to an object.
- Parameters:
obj (ndarray) – Real-valued object (amplitude).
phase_std (float) – Standard deviation of phase noise (radians).
correlation_length (float | None) – Correlation length for spatially correlated noise (pixels). If None, uses uncorrelated noise.
apply_to_support (bool) – If True, apply phase only where obj > 0.
- Returns:
Complex object with random phase noise.
- Return type:
ndarray
- cdiutils.simulation.simulate_diffraction(obj, photon_budget=None, max_intensity=None, scale=1.0, poisson_statistics=False, convention=None)[source]#
Simulate diffraction pattern from a real-space object.
This uses a single, consistent numerical convention: a forward n-dimensional FFT followed by fftshift to place the Bragg peak at the centre of the array:
reciprocal_obj = fftshift(fftn(obj))
The
conventionargument is kept for future extension (e.g. axis re-ordering or additional phase factors) but does not change the underlying FFT operator. This keeps the implementation simple and avoids mixingfftn/ifftnconventions. If you need to match a sign convention used in analytical Bragg-CDI derivations or in external tools, do so via the definition of the q-grid (e.g. flipping an axis or using-phaseconsistently), not by swapping FFT directions here.Intensity is computed as
|reciprocal_obj|**2. Optionally, the result can be scaled tophoton_budgetand/ormax_intensity. Note that first scaling tomax_intensitywill discard photon budget scaling if both are provided. Scale is applied in any case as a multiplicative factor.For noise modelling refer to
add_noise().- Parameters:
obj (ndarray) – Real-space complex object to simulate.
photon_budget (float | None) – Total photon budget for the exposure. If provided, intensity is scaled so that the sum equals this value (before Poisson sampling).
max_intensity (float | None) – Maximum intensity value for final scaling. If None, no scaling is applied.
scale (float) – Multiplicative scale factor applied to intensity. Defaults to 1.0.
poisson_statistics (bool) – If True, apply Poisson statistics to the final intensity (photon counting). Default is False.
convention (str | None) – Placeholder for future FFT/q-space conventions. Currently ignored except for being accepted for API compatibility.
- Returns:
Simulated diffraction pattern (intensity).
- Raises:
ValueError – If max_intensity or photon_budget are negative.
- Return type:
ndarray
Example
>>> # simulate diffraction from a 3D object >>> obj = make_box((64, 64, 64), dimensions=20) >>> obj = add_random_phase(obj, amplitude=0.1) >>> intensity = simulate_diffraction(obj, photon_budget=1e9) >>> intensity.shape (64, 64, 64)
Notes
The FFT convention is fixed to avoid confusion. The forward FFT is always used. For crystallographic sign conventions, adjust the phase of your object (e.g., use
np.conj(obj)if needed) rather than changing the FFT direction.
- cdiutils.simulation.add_noise(data, gaussian_mean=0.0, gaussian_std=0.0, poisson_background=None, poisson_statistics=False, scale=1.0)[source]#
Add noise to data with configurable Gaussian and Poisson components.
This general-purpose function allows flexible noise modelling by combining Gaussian and Poisson noise sources. It can be called multiple times to build up complex noise models from different physical sources (dark current, readout noise, air scattering, fluorescence, etc.).
- Parameters:
data (ndarray) – Input data array (any shape).
gaussian_mean (float) – Mean of Gaussian noise to add. Default is 0.0.
gaussian_std (float) – Standard deviation of Gaussian noise. Default is 0.0.
poisson_background (ndarray | float | None) –
Background for Poisson sampling. Can be: - None: no Poisson background added - float: uniform background value - np.ndarray: spatially-varying background (must match
data shape)
Default is None.
poisson_statistics (bool) – If True, apply Poisson statistics to the data itself (photon counting). Default is False.
scale (float) – Multiplicative factor applied to data before adding noise. Useful for unit conversions or intensity scaling. Default is 1.0.
- Returns:
Data with added noise, same shape as input. Values are converted to float64 for accumulation and clipped to non-negative.
- Raises:
TypeError – If data is not a numpy array or if poisson_background has invalid type.
ValueError – If gaussian_std is negative, or if poisson_background array shape doesn’t match data shape, or if scale is not positive, or if background values are negative.
- Return type:
ndarray
Example
>>> # add dark current (Gaussian thermal noise) >>> noisy = add_noise( ... data, ... gaussian_mean=0.5, ... gaussian_std=0.75, ... ) >>> >>> # add readout noise (Gaussian electronics noise) >>> noisy = add_noise(noisy, gaussian_std=0.5) >>> >>> # add uniform air scattering (Poisson background) >>> noisy = add_noise(noisy, poisson_background=2.0) >>> >>> # add spatially-varying air scattering >>> beam_profile = create_beam_profile(data.shape) >>> noisy = add_noise( ... noisy, ... poisson_background=beam_profile, ... ) >>> >>> # apply Poisson statistics to signal >>> noisy = add_noise(data, poisson_statistics=True) >>> >>> # scale data and add noise >>> noisy = add_noise( ... data, ... scale=1.5, ... gaussian_std=1.0, ... ) >>> >>> # combine multiple sources in one call >>> noisy = add_noise( ... data, ... gaussian_mean=0.5, ... gaussian_std=0.75, ... poisson_background=2.0, ... scale=1.2, ... )
Notes
This function can be called multiple times sequentially to build up complex noise models. Each call adds additional noise to the input data. The order matters when combining Poisson statistics with other noise sources.
For frame-by-frame noise in rocking curves, use this function within a loop or use the
BCDISimulatorclass which handles realistic detector simulation automatically.
- class cdiutils.simulation.BCDISimulator(geometry=None, energy=None, wavelength=None, structure=None, hkl=None, lattice_parameter=None, det_calib_params=None, detector_name=None, num_frames=256, target_peak_position=(258, 258))[source]#
Bases:
objectSimulator for BCDI measurement with realistic detector geometry.
This class provides end-to-end simulation of a BCDI measurement, including: - Sample object creation with customisable geometry and phase - Diffraction pattern computation - Diffractometer angle calculations - Coordinate transformations (Q-space ↔ detector frame) - Realistic detector effects (noise, masking, binning)
The simulator handles the full measurement geometry including Bragg angle, detector angles, and rocking curves, making it suitable for testing reconstruction algorithms and planning experiments.
- geometry#
Diffractometer geometry configuration.
- energy#
X-ray energy in eV.
- wavelength#
X-ray wavelength in metres.
- structure#
Crystal structure (‘cubic’, etc.).
- hkl#
Miller indices of the Bragg reflection.
- lattice_parameter#
Lattice parameter in metres.
- det_calib_params#
Detector calibration parameters (distance, pixel size, centre position).
- detector_name#
Name of the detector (‘maxipix’, etc.).
- mask#
Detector mask (dead pixels, gaps).
- detector_shape#
Shape of the detector (height, width).
- num_frames#
Number of frames in rocking curve.
- target_peak_position#
Target pixel position for Bragg peak (row, col).
- obj#
Simulated 3D object (complex array).
- intensity#
Diffraction intensity in reciprocal space.
- voxel_size#
Real-space voxel sizes (z, y, x) in metres.
- q_voxel_size#
Reciprocal-space voxel sizes (qz, qy, qx) in inverse metres.
- all_angles#
Dictionary of computed diffractometer angles.
- diffractometer_angles#
Dictionary of measurement angles (sample + detector).
- detector_to_q_matrix#
Transformation matrix from detector to Q-space.
- q_to_detector_matrix#
Transformation matrix from Q-space to detector.
- phase_factor#
Phase modulation for shear FFT.
Example
>>> # create simulator for ID01 beamline >>> sim = BCDISimulator( ... energy=9000, # 9 keV ... structure='cubic', ... hkl=[1, 1, 1], ... lattice_parameter=4.08e-10, # gold ... detector_name='maxipix', ... num_frames=200, ... ) >>> >>> # simulate a box-shaped particle with random phase >>> sim.simulate_object( ... shape=(100, 100, 100), ... voxel_size=5e-9, ... geometric_shape='box', ... geometric_shape_params={'dimensions': 30}, ... phase_type='random', ... phase_params={'amplitude': 0.2}, ... ) >>> >>> # set up measurement geometry >>> bragg = sim.lattice_parameter_to_bragg_angle() >>> detector_angles = sim.get_detector_angles( ... scattering_angle=2 * bragg ... ) >>> sim.set_measurement_params( ... bragg_angle=bragg, ... rocking_range=0.5, ... detector_angles=detector_angles, ... ) >>> >>> # transform to detector frame and add noise >>> detector_data = sim.to_detector_frame() >>> realistic_data = sim.get_realistic_detector_data( ... detector_data, ... photon_budget=1e10, ... )
- __init__(geometry=None, energy=None, wavelength=None, structure=None, hkl=None, lattice_parameter=None, det_calib_params=None, detector_name=None, num_frames=256, target_peak_position=(258, 258))[source]#
Initialise BCDI measurement simulator.
- Parameters:
geometry (Geometry | None) – Diffractometer geometry. If None, uses ID01 default geometry.
energy (float | None) – X-ray energy in eV. Mutually exclusive with wavelength.
wavelength (float | None) – X-ray wavelength in metres. Mutually exclusive with energy.
structure (str | None) – Crystal structure type. Default is ‘cubic’.
hkl (list[int] | None) – Miller indices [h, k, l]. Default is [1, 1, 1].
lattice_parameter (float | None) – Lattice parameter in metres.
det_calib_params (dict | None) – Dictionary with detector calibration parameters: ‘distance’ (m), ‘pwidth1’, ‘pwidth2’ (m), ‘cch1’, ‘cch2’ (pixels).
detector_name (str | None) – Detector name for loading mask. Default is ‘maxipix’.
num_frames (int) – Number of frames in rocking curve. Default is 256.
target_peak_position (tuple[int, int]) – Target pixel position (row, col) for Bragg peak. Default is (258, 258).
- Raises:
ValueError – If both energy and wavelength are provided and don’t match, or if neither is provided.
- lattice_parameter_to_bragg_angle(lattice_parameter=None)[source]#
Calculate Bragg angle from lattice parameter.
Uses Bragg’s law (2d sin(θ) = λ) to compute the Bragg angle for the specified reflection in specular geometry.
- Parameters:
lattice_parameter (float | None) – Lattice parameter in metres. If None, uses the instance attribute.
- Returns:
Bragg angle in degrees.
- Raises:
NotImplementedError – If structure is not ‘cubic’.
- Return type:
float
Example
>>> sim = BCDISimulator( ... energy=9000, ... structure='cubic', ... hkl=[1, 1, 1], ... lattice_parameter=4.08e-10, ... ) >>> bragg = sim.lattice_parameter_to_bragg_angle() >>> print(f"{bragg:.2f}°")
- bragg_angle_to_lattice_parameter(bragg_angle)[source]#
Calculate lattice parameter from Bragg angle.
Inverts Bragg’s law to compute lattice parameter from the measured Bragg angle.
- Parameters:
bragg_angle (float) – Bragg angle in degrees.
- Returns:
Lattice parameter in metres.
- Raises:
NotImplementedError – If structure is not ‘cubic’.
- Return type:
float
Example
>>> sim = BCDISimulator( ... energy=9000, ... structure='cubic', ... hkl=[1, 1, 1], ... ) >>> a = sim.bragg_angle_to_lattice_parameter(20.5) >>> print(f"{a*1e10:.3f} Å")
- get_angular_offsets(target_peak_position=None)[source]#
Compute angular offsets to centre Bragg peak at target pixel.
Calculates the detector angle corrections needed to place the Bragg peak at the specified pixel position, accounting for the difference from the calibrated detector centre.
Note that the sign convention here is specific to ID01 geometry, the pixel count increases opposite to delta and nu angles.
- Parameters:
target_peak_position (tuple[int, int] | None) – Target pixel position (row, col). If None, uses instance attribute.
- Returns:
Angular offsets (delta_offset, nu_offset) in degrees.
- Return type:
tuple[float, float]
Example
>>> sim = BCDISimulator( ... energy=9000, ... det_calib_params={ ... 'distance': 1.0, ... 'pwidth1': 55e-6, ... 'pwidth2': 55e-6, ... 'cch1': 256, ... 'cch2': 256, ... }, ... ) >>> offsets = sim.get_angular_offsets((280, 240)) >>> print(f"Delta: {offsets[0]:.3f}°, Nu: {offsets[1]:.3f}°")
Notes
This is specific to ID01 geometry where pixel count increases opposite to delta angle.
- compute_angles(target_peak_position=None, scattering_angle=None, detector_outofplane_angle=None, detector_inplane_angle=None)[source]#
Compute consistent diffractometer angles.
Given partial angle information, compute all related angles in a self-consistent way, accounting for detector offset from calibrated centre. Results are stored in
self.all_angles.Here we consider the effective angles to be the physical angles at the desired self.peak_position. They are the actual angles corresponding to that position. They are defined as: effective angles = detector angles - angular offsets The detector calibration gives the direct beam position on the detector, which corresponds to where the diffractometer angles are effective (at this position the effective angles = detector angles). Refer to
get_angular_offsets()for more details.- Parameters:
target_peak_position (tuple[int, int] | None) – Target pixel position (row, col). If None, uses instance attribute.
scattering_angle (float | None) – Total scattering angle (2θ) in degrees.
detector_outofplane_angle (float | None) – Detector out-of-plane angle (delta at ID01) in degrees.
detector_inplane_angle (float | None) – Detector in-plane angle (nu at ID01) in degrees.
- Raises:
ValueError – If angles are inconsistent or insufficient information is provided.
Example
>>> sim = BCDISimulator(energy=9000) >>> # compute detector angles from scattering angle >>> sim.compute_angles(scattering_angle=41.0) >>> print(sim.all_angles)
Notes
At least one angle must be provided. The function will compute missing angles from the provided information.
- get_detector_angles(target_peak_position=None, scattering_angle=None, detector_outofplane_angle=None, detector_inplane_angle=None)[source]#
Get detector angles for measurement.
Convenience method that computes angles and returns only the detector-specific angles (out-of-plane and in-plane).
- Parameters:
target_peak_position (tuple[int, int] | None) – Target pixel position (row, col). If None, uses instance attribute.
scattering_angle (float | None) – Total scattering angle (2θ) in degrees.
detector_outofplane_angle (float | None) – Detector out-of-plane angle (delta at ID01) in degrees.
detector_inplane_angle (float | None) – Detector in-plane angle (nu at ID01) in degrees.
- Returns:
Dictionary with keys ‘detector_outofplane_angle’ and ‘detector_inplane_angle’ in degrees.
- Return type:
dict[str, float]
Example
>>> sim = BCDISimulator(energy=9000) >>> angles = sim.get_detector_angles( ... scattering_angle=41.0 ... ) >>> print(f"Delta: {angles['detector_outofplane_angle']:.3f}°") >>> print(f"Nu: {angles['detector_inplane_angle']:.3f}°")
- simulate_object(shape=(100, 100, 100), voxel_size=1e-08, geometric_shape=None, geometric_shape_params=None, phase_type=None, phase_params=None, swap_convention=False, plot=True)[source]#
Create simulated object and compute its diffraction pattern.
Generates a 3D object with specified geometry and phase, computes its diffraction pattern, and optionally plots the results. The object and intensity are stored as instance attributes.
- Parameters:
shape (tuple | list | ndarray) – Shape of 3D array (nz, ny, nx).
voxel_size (float | tuple[float, float, float]) – Real-space voxel size in metres. Can be a single value (isotropic) or tuple (z, y, x).
geometric_shape (str | None) – Shape type: ‘box’, ‘cylinder’, or ‘ellipsoid’. Default is ‘box’.
geometric_shape_params (dict | None) – Parameters for shape function (e.g., {‘dimensions’: 30, ‘rotation’: (0, 0, 45)}).
phase_type (str | None) – Phase type: ‘linear’, ‘quadratic’, or ‘random’. Default is ‘random’.
phase_params (dict | None) – Parameters for phase function (e.g., {‘amplitude’: 0.2}).
swap_convention (bool) – If True, swap axes to match detector convention. Default is False.
plot (bool) – If True, plot object and diffraction pattern. Default is True.
- Raises:
ValueError – If geometric_shape is not recognised.
Example
>>> sim = BCDISimulator(energy=9000) >>> sim.simulate_object( ... shape=(80, 80, 80), ... voxel_size=8e-9, ... geometric_shape='ellipsoid', ... geometric_shape_params={ ... 'semi_axes': (25, 20, 20) ... }, ... phase_type='random', ... phase_params={'amplitude': 0.15}, ... )
Notes
The diffraction is computed with conjugate of the object to match crystallographic convention (Bragg CDI).
- set_object(obj, voxel_size)[source]#
Set object directly and compute diffraction pattern.
Use this method to provide a custom object instead of using
simulate_object(). The diffraction pattern is computed automatically.- Parameters:
obj (ndarray) – Complex 3D object array.
voxel_size (float | tuple[float, float, float]) – Real-space voxel size in metres. Can be a single value (isotropic) or tuple (z, y, x).
Example
>>> # create custom object >>> obj = np.ones((64, 64, 64), dtype=complex) >>> obj *= np.exp(1j * np.random.rand(64, 64, 64)) >>> >>> sim = BCDISimulator(energy=9000) >>> sim.set_object(obj, voxel_size=10e-9)
- static get_rocking_angles(bragg_angle, rocking_range, num_frames)[source]#
Generate linearly-spaced rocking angles.
Creates an array of angles for a rocking curve centred on the Bragg angle.
- Parameters:
bragg_angle (float) – Centre angle (Bragg angle) in degrees.
rocking_range (float) – Total angular range to cover in degrees.
num_frames (int) – Number of angular steps.
- Returns:
Array of rocking angles in degrees.
- Return type:
ndarray
Example
>>> angles = BCDISimulator.get_rocking_angles( ... bragg_angle=20.5, ... rocking_range=0.5, ... num_frames=200, ... ) >>> angles.shape (200,) >>> print(f"Range: {angles[0]:.3f}° to {angles[-1]:.3f}°")
- set_measurement_params(bragg_angle, rocking_range, detector_angles, rocking_angle=None)[source]#
Set measurement parameters and compute transformation matrices.
Configures the rocking curve angles and detector position, then computes the transformation matrices needed to convert between Q-space and detector frame.
- Parameters:
bragg_angle (float) – Bragg angle in degrees.
rocking_range (float) – Total rocking range in degrees.
detector_angles (dict[str, float]) – Dictionary with ‘detector_outofplane_angle’ and ‘detector_inplane_angle’ in degrees.
rocking_angle (str | None) – Rocking axis (‘outofplane’ or ‘inplane’). Default is ‘outofplane’.
- Raises:
NotImplementedError – If rocking_angle is not ‘outofplane’.
Example
>>> sim = BCDISimulator(energy=9000) >>> # ... simulate object ... >>> detector_angles = sim.get_detector_angles( ... scattering_angle=41.0 ... ) >>> sim.set_measurement_params( ... bragg_angle=20.5, ... rocking_range=0.5, ... detector_angles=detector_angles, ... )
Notes
This method must be called after
simulate_object()orset_object()as it requires the object’s reciprocal space voxel sizes.
- to_detector_frame(method=None, output_shape=None, plot=True)[source]#
Transform reciprocal space intensity to detector frame.
Applies coordinate transformation to map the simulated diffraction pattern from reciprocal space coordinates to detector pixel coordinates.
- Parameters:
method (str | None) – Transformation method: ‘matrix_transform’ or ‘shear_fft’. Default is ‘matrix_transform’. For now, only ‘matrix_transform’ is implemented.
output_shape (tuple[int, int, int] | None) – Desired output shape. Default is same as detector frame shape.
plot (bool) – If True, plot the transformed intensity. Default is True.
- Returns:
Intensity in detector frame coordinates.
- Raises:
ValueError – If method is not recognised or if ‘shear_fft’ is requested (not yet implemented).
- Return type:
ndarray
Example
>>> sim = BCDISimulator(energy=9000) >>> # ... simulate object and set measurement params ... >>> detector_data = sim.to_detector_frame()
Notes
The ‘shear_fft’ method is faster but currently has implementation issues. Use ‘matrix_transform’ for now.
- shift_to_target_pixel(intensity)[source]#
Shift and pad intensity to place peak at target pixel.
Adjusts the intensity array to match the detector shape and shifts it so the Bragg peak appears at the target pixel position.
- Parameters:
intensity (ndarray) – Intensity array to shift.
- Returns:
Shifted and padded intensity with shape (num_frames, detector_height, detector_width).
- Return type:
ndarray
Example
>>> # ... compute detector_frame_intensity ... >>> shifted = sim.shift_to_target_pixel( ... detector_frame_intensity ... )
- get_realistic_detector_data(intensity, photon_budget=None, max_intensity=None, shift=True, noise_params=None)[source]#
Apply realistic detector effects to simulated data.
Adds scaling, noise, masking, and other detector effects to create realistic measurement data.
- Parameters:
intensity (ndarray) – Simulated intensity array.
photon_budget (float | None) – Total photon count for scaling. Default is 1e8 if neither photon_budget nor max_intensity is provided.
max_intensity (float | None) – Maximum intensity value for scaling. Ignored if photon_budget is also provided.
shift (bool) – If True, shift peak to target pixel position. Default is True.
noise_params (list[dict] | dict | None) – Noise model parameters. Can be a single dict or list of dicts for multiple noise sources. Default is [{‘gaussian_mean’: 0.5, ‘gaussian_std’: 1.0}, {‘poisson_statistics’: True}].
- Returns:
Realistic detector data as integer array with dead pixels masked.
- Return type:
ndarray
Example
>>> detector_data = sim.to_detector_frame() >>> realistic_data = sim.get_realistic_detector_data( ... detector_data, ... photon_budget=1e10, ... noise_params=[ ... {'gaussian_mean': 0.5, 'gaussian_std': 1.0}, ... {'poisson_background': 2.0}, ... {'poisson_statistics': True}, ... ], ... )
Notes
Noise is applied sequentially if multiple noise_params dicts are provided. The final result is clipped to non-negative integers and masked.
Key Functions#
|
Add noise to data with configurable Gaussian and Poisson components. |
See Also#
../examples/simulation : Simulation tutorials and examples