-
Notifications
You must be signed in to change notification settings - Fork 0
10. Utilities
HoloGen provides a comprehensive set of utility functions for working with optical fields, managing dataset I/O operations, and performing mathematical transformations. This document covers all utility modules and their functions, with practical examples and performance considerations.
The utility modules are organized into three main categories:
-
Field Utilities (
hologen.utils.fields): Complex field conversions and validation -
I/O Utilities (
hologen.utils.io): Dataset writers and loaders -
Math Utilities (
hologen.utils.math): Mathematical operations and transformations
Conversion Flow Diagram
The diagram above shows the relationships between different field representations and the utility functions that convert between them.
The hologen.utils.fields module provides functions for converting between different field representations and validating field data.
Convert a complex optical field to a specific representation (intensity, amplitude, phase, or complex).
Signature:
def complex_to_representation( field: ArrayComplex, representation: FieldRepresentation ) -> ArrayFloat | ArrayComplex
Parameters:
-
field: Complex-valued optical field (numpy array with dtype complex128) -
representation: Target representation type fromFieldRepresentationenum
Returns:
-
ArrayFloatfor intensity, amplitude, and phase representations -
ArrayComplexfor complex representation (returns input unchanged)
Raises:
-
FieldRepresentationError: If the representation type is invalid
Usage Examples:
from hologen.utils.fields import complex_to_representation from hologen.types import FieldRepresentation import numpy as np # Create a complex field field = np.array([[1+1j, 2+0j], [0+2j, 1-1j]], dtype=np.complex128) # Convert to intensity intensity = complex_to_representation(field, FieldRepresentation.INTENSITY) # Result: [[2.0, 4.0], [4.0, 2.0]] # Convert to amplitude amplitude = complex_to_representation(field, FieldRepresentation.AMPLITUDE) # Result: [[1.414, 2.0], [2.0, 1.414]] # Convert to phase phase = complex_to_representation(field, FieldRepresentation.PHASE) # Result: [[0.785, 0.0], [1.571, -0.785]] (in radians) # Keep as complex (no conversion) complex_field = complex_to_representation(field, FieldRepresentation.COMPLEX) # Result: same as input
See the conversion flow diagram above for a visual representation of these conversions.
Performance Considerations:
- Intensity conversion: O(n) with two operations per element (abs + square)
- Amplitude conversion: O(n) with one operation per element (abs)
- Phase conversion: O(n) with one operation per element (angle)
- Complex conversion: O(1) (no computation, returns input)
Common Use Cases:
- Converting hologram fields for visualization
- Preparing data for intensity-only ML models
- Extracting phase information for quantitative phase imaging
- Pipeline transformations between processing stages
Construct a complex field from separate amplitude and phase arrays.
Signature:
def amplitude_phase_to_complex( amplitude: ArrayFloat, phase: ArrayFloat ) -> ArrayComplex
Parameters:
-
amplitude: Amplitude values (non-negative real array) -
phase: Phase values in radians (real array)
Returns:
- Complex field with specified amplitude and phase
Mathematical Formula:
field = amplitude ×ばつ exp(i ×ばつ phase)
Usage Examples:
from hologen.utils.fields import amplitude_phase_to_complex import numpy as np # Create amplitude and phase arrays amplitude = np.array([[1.0, 2.0], [1.5, 0.5]], dtype=np.float64) phase = np.array([[0.0, np.pi/2], [np.pi, -np.pi/2]], dtype=np.float64) # Construct complex field field = amplitude_phase_to_complex(amplitude, phase) # Result: [[1+0j, 0+2j], [-1.5+0j, 0-0.5j]]
Performance Considerations:
- O(n) complexity with one exp operation per element
- Exponential computation is relatively expensive
- Consider caching results if used repeatedly with same phase values
Common Use Cases:
- Reconstructing complex fields from separate amplitude/phase measurements
- Creating synthetic fields with controlled amplitude and phase patterns
- Implementing custom phase modulation schemes
- Converting from polar to Cartesian representation
Validate that all phase values are within the valid [-π, π] range.
Signature:
def validate_phase_range(phase: ArrayFloat) -> None
Parameters:
-
phase: Phase array in radians
Returns:
- None (raises exception if validation fails)
Raises:
-
PhaseRangeError: If any phase values are outside [-π, π] or non-finite (NaN/Inf)
Usage Examples:
from hologen.utils.fields import validate_phase_range, PhaseRangeError import numpy as np # Valid phase array valid_phase = np.array([[0.0, np.pi/2], [-np.pi/2, np.pi]], dtype=np.float64) validate_phase_range(valid_phase) # No error # Invalid phase array (out of range) invalid_phase = np.array([[0.0, 4.0]], dtype=np.float64) try: validate_phase_range(invalid_phase) except PhaseRangeError as e: print(f"Validation failed: {e}") # Output: "Phase values must be in the range [-π, π] radians. Found values in range [0.0000, 4.0000]." # Invalid phase array (non-finite) nan_phase = np.array([[0.0, np.nan]], dtype=np.float64) try: validate_phase_range(nan_phase) except PhaseRangeError as e: print(f"Validation failed: {e}") # Output: "Phase array contains non-finite values (NaN or Inf). All phase values must be finite numbers."
Visual Example:
Phase Validation Example
The diagram above shows examples of valid and invalid phase arrays, illustrating what the validation function checks for.
Performance Considerations:
- O(n) complexity with two passes over the array
- First pass checks for finite values
- Second pass checks range bounds
- Minimal overhead for valid data
Common Use Cases:
- Validating phase data before saving to disk
- Debugging phase computation errors
- Ensuring phase wrapping was applied correctly
- Input validation in custom processing functions
FieldRepresentationError
Raised when a field representation is invalid or incompatible.
from hologen.utils.fields import FieldRepresentationError # Inherits from ValueError # Raised by complex_to_representation() for invalid representation types
PhaseRangeError
Raised when phase values are outside the valid [-π, π] range.
from hologen.utils.fields import PhaseRangeError # Inherits from ValueError # Raised by validate_phase_range() for out-of-range or non-finite values
The hologen.utils.io module provides classes for writing datasets to disk and loading samples from saved files.
I/O Workflow Diagram
The diagram above illustrates the complete workflow from dataset generation through storage to ML model training.
Persist holography samples in NumPy archives (.npz) with optional PNG previews.
Class Definition:
@dataclass(slots=True) class NumpyDatasetWriter: save_preview: bool = True
Parameters:
-
save_preview: Whether to generate PNG preview images for each domain (default: True)
Methods:
Write hologram samples to disk.
Signature:
def save(self, samples: Iterable[HologramSample], output_dir: Path) -> None
Parameters:
-
samples: Iterable ofHologramSampleobjects from the pipeline -
output_dir: Target directory for dataset files
Raises:
-
IOError: If files cannot be written to disk
File Format:
Each sample generates the following files:
-
sample_XXXXX_<name>.npz: NumPy archive with keys:-
object: Object-domain intensity image -
hologram: Hologram-domain intensity image -
reconstruction: Reconstructed object-domain intensity image
-
-
sample_XXXXX_<name>_object.png: Object preview (if save_preview=True) -
sample_XXXXX_<name>_hologram.png: Hologram preview (if save_preview=True) -
sample_XXXXX_<name>_reconstruction.png: Reconstruction preview (if save_preview=True)
File Format Examples:
File Format Examples
The diagram above shows the structure and contents of different file formats supported by HoloGen.
Usage Example:
from hologen.utils.io import NumpyDatasetWriter from hologen.converters import create_hologram_dataset_generator from hologen.types import GridSpec, OpticalConfig, HolographyConfig from pathlib import Path # Create dataset generator grid = GridSpec(height=512, width=512, pixel_pitch=1e-6) optical = OpticalConfig(wavelength=532e-9, distance=0.01) holography = HolographyConfig(method="inline") generator = create_hologram_dataset_generator( grid=grid, optical=optical, holography=holography, num_samples=10 ) # Write dataset with previews writer = NumpyDatasetWriter(save_preview=True) samples = generator.generate() writer.save(samples, Path("output/dataset")) # Write dataset without previews (faster, less disk space) writer_no_preview = NumpyDatasetWriter(save_preview=False) writer_no_preview.save(samples, Path("output/dataset_no_preview"))
Performance Considerations:
- PNG generation adds ~30-50% overhead to write time
- Disable previews for large-scale dataset generation
- Each sample writes 1 .npz file + 3 PNG files (if enabled)
- PNG files are 8-bit grayscale, ~10-50 KB each
- .npz files are compressed, size depends on image complexity
Common Use Cases:
- Generating training datasets for intensity-based ML models
- Creating datasets compatible with legacy HoloGen versions
- Quick dataset generation with visual verification
- Batch processing with preview disabled for speed
Persist complex holography samples in NumPy archives with optional PNG previews and phase colormaps.
Class Definition:
@dataclass(slots=True) class ComplexFieldWriter: save_preview: bool = True phase_colormap: str = "twilight"
Parameters:
-
save_preview: Whether to generate PNG preview images (default: True) -
phase_colormap: Matplotlib colormap name for phase visualization (default: "twilight")
Methods:
Write complex hologram samples to disk.
Signature:
def save(self, samples: Iterable[ComplexHologramSample], output_dir: Path) -> None
Parameters:
-
samples: Iterable ofComplexHologramSampleobjects from the pipeline -
output_dir: Target directory for dataset files
Raises:
-
IOError: If files cannot be written to disk
File Format:
Each sample generates separate files for object, hologram, and reconstruction domains:
For COMPLEX representation:
-
<prefix>_<domain>.npz: Containsreal,imag, andrepresentationkeys -
<prefix>_<domain>_amplitude.png: Amplitude preview -
<prefix>_<domain>_phase.png: Phase preview with colormap
For AMPLITUDE representation:
-
<prefix>_<domain>.npz: Containsamplitudeandrepresentationkeys -
<prefix>_<domain>.png: Amplitude preview
For PHASE representation:
-
<prefix>_<domain>.npz: Containsphaseandrepresentationkeys -
<prefix>_<domain>.png: Phase preview with colormap
For INTENSITY representation:
-
<prefix>_<domain>.npz: Containsintensityandrepresentationkeys -
<prefix>_<domain>.png: Intensity preview
Usage Example:
from hologen.utils.io import ComplexFieldWriter from hologen.converters import create_complex_hologram_generator from hologen.types import ( GridSpec, OpticalConfig, HolographyConfig, FieldRepresentation ) from pathlib import Path # Create complex field generator grid = GridSpec(height=512, width=512, pixel_pitch=1e-6) optical = OpticalConfig(wavelength=532e-9, distance=0.01) holography = HolographyConfig(method="inline") generator = create_complex_hologram_generator( grid=grid, optical=optical, holography=holography, num_samples=10, object_representation=FieldRepresentation.PHASE, hologram_representation=FieldRepresentation.COMPLEX ) # Write with default twilight colormap writer = ComplexFieldWriter(save_preview=True, phase_colormap="twilight") samples = generator.generate() writer.save(samples, Path("output/complex_dataset")) # Write with different colormap writer_hsv = ComplexFieldWriter(save_preview=True, phase_colormap="hsv") writer_hsv.save(samples, Path("output/complex_dataset_hsv")) # Write without previews writer_fast = ComplexFieldWriter(save_preview=False) writer_fast.save(samples, Path("output/complex_dataset_fast"))
Available Phase Colormaps:
-
twilight: Cyclic colormap, good for phase (default) -
hsv: Classic hue-based phase visualization -
twilight_shifted: Shifted twilight colormap - Any matplotlib colormap name
Performance Considerations:
- Complex fields require 2x storage (real + imaginary components)
- PNG generation with colormaps requires matplotlib
- Falls back to grayscale if matplotlib unavailable
- Phase colormaps add minimal overhead (~5%)
- Disable previews for maximum write speed
Common Use Cases:
- Generating datasets for physics-aware ML models
- Quantitative phase imaging applications
- Full-field holographic reconstruction training data
- Research applications requiring complete field information
Load a sample from a NumPy archive with automatic format detection.
Signature:
def load_complex_sample(path: Path) -> ComplexObjectSample | ObjectSample
Parameters:
-
path: Path to the .npz file
Returns:
-
ComplexObjectSampleif file contains complex field data -
ObjectSampleif file contains legacy intensity data
Raises:
-
ValueError: If file format is not recognized -
IOError: If file cannot be read
Supported Formats:
The function automatically detects the format based on keys in the .npz file:
-
Complex format: Contains
realandimagkeys -
Amplitude format: Contains
amplitudekey -
Phase format: Contains
phasekey -
Intensity format: Contains
intensitykey -
Legacy format: Contains
objectkey
Usage Examples:
from hologen.utils.io import load_complex_sample from hologen.types import ComplexObjectSample, ObjectSample from pathlib import Path # Load complex field data sample = load_complex_sample(Path("output/sample_00000_object.npz")) if isinstance(sample, ComplexObjectSample): print(f"Loaded complex sample: {sample.name}") print(f"Representation: {sample.representation}") print(f"Field shape: {sample.field.shape}") print(f"Field dtype: {sample.field.dtype}") # Load legacy intensity data legacy_sample = load_complex_sample(Path("output/legacy_sample.npz")) if isinstance(legacy_sample, ObjectSample): print(f"Loaded legacy sample: {legacy_sample.name}") print(f"Pixels shape: {legacy_sample.pixels.shape}") # Handle unknown format try: unknown = load_complex_sample(Path("output/unknown.npz")) except ValueError as e: print(f"Format error: {e}")
Performance Considerations:
- Lazy loading: Only reads requested file
- NumPy's load() is memory-mapped for large files
- Format detection is fast (checks dictionary keys)
- No unnecessary data copies
Common Use Cases:
- Loading samples for visualization
- Creating custom data loaders for ML frameworks
- Batch processing of generated datasets
- Validating dataset contents
- Migrating between legacy and complex field formats
The hologen.utils.math module provides mathematical operations for image processing and Fourier-domain computations.
Normalize an image to the range [0.0, 1.0] for visualization or saving.
Signature:
def normalize_image(image: ArrayFloat) -> ArrayFloat
Parameters:
-
image: Arbitrary floating-point image array
Returns:
- Normalized image in range [0.0, 1.0]
- Returns zeros array if input is constant (min == max)
Mathematical Formula:
normalized = (image - min(image)) / (max(image) - min(image))
Usage Examples:
from hologen.utils.math import normalize_image import numpy as np # Normalize arbitrary range image = np.array([[100, 200], [150, 250]], dtype=np.float64) normalized = normalize_image(image) # Result: [[0.0, 0.667], [0.333, 1.0]] # Constant image constant = np.ones((10, 10), dtype=np.float64) * 5.0 normalized_constant = normalize_image(constant) # Result: zeros array (10, 10) # Negative values negative = np.array([[-10, 0], [5, 10]], dtype=np.float64) normalized_negative = normalize_image(negative) # Result: [[0.0, 0.5], [0.75, 1.0]]
Visual Example:
Normalization Example
The diagram above demonstrates how normalize_image() transforms an image with arbitrary range to [0, 1] for visualization and PNG export.
Performance Considerations:
- O(n) complexity with two passes (min/max, then normalization)
- NumPy's vectorized operations are highly optimized
- Returns float64 for consistency
- Handles edge case of constant images gracefully
Common Use Cases:
- Preparing images for PNG export
- Visualizing hologram intensity patterns
- Normalizing before applying colormaps
- Preprocessing for display purposes
Create Fourier-domain sampling coordinates for a spatial grid.
Signature:
def make_fourier_grid(grid: GridSpec) -> FourierGrid
Parameters:
-
grid: Spatial grid specification with height, width, and pixel_pitch
Returns:
-
FourierGridobject containing:-
fx: 2D array of spatial frequencies along x-axis (cycles/meter) -
fy: 2D array of spatial frequencies along y-axis (cycles/meter)
-
Usage Examples:
from hologen.utils.math import make_fourier_grid from hologen.types import GridSpec # Create spatial grid grid = GridSpec(height=512, width=512, pixel_pitch=1e-6) # Generate Fourier grid fourier_grid = make_fourier_grid(grid) print(f"fx shape: {fourier_grid.fx.shape}") # (512, 512) print(f"fy shape: {fourier_grid.fy.shape}") # (512, 512) print(f"Max frequency: {fourier_grid.fx.max():.2e} cycles/m") # Use in propagation calculations import numpy as np wavelength = 532e-9 k = 2 * np.pi / wavelength transfer_function = np.exp( 1j * k * distance * np.sqrt(1 - (wavelength * fourier_grid.fx)**2 - (wavelength * fourier_grid.fy)**2) )
Performance Considerations:
- O(n) complexity for meshgrid generation
- Results can be cached and reused for same grid
- Memory usage: 2 ×ばつ height ×ばつ width ×ばつ 8 bytes (float64)
- Computation is fast even for large grids
Common Use Cases:
- Angular spectrum propagation
- Fourier-domain filtering
- Off-axis holography carrier frequency calculations
- Frequency-domain analysis
Apply an isotropic Gaussian blur to a 2D image.
Signature:
def gaussian_blur(image: ArrayFloat, sigma: float) -> ArrayFloat
Parameters:
-
image: Input image to filter -
sigma: Standard deviation of Gaussian kernel in pixel units
Returns:
- Blurred image with identical shape to input
Mathematical Formula:
kernel(x) = exp(-0.5 ×ばつ (x/σ)2) / Σ(kernel)
Usage Examples:
from hologen.utils.math import gaussian_blur import numpy as np # Create test image image = np.zeros((100, 100), dtype=np.float64) image[45:55, 45:55] = 1.0 # Central square # Apply different blur levels no_blur = gaussian_blur(image, sigma=0.0) # Returns copy light_blur = gaussian_blur(image, sigma=1.0) medium_blur = gaussian_blur(image, sigma=3.0) heavy_blur = gaussian_blur(image, sigma=10.0) # Use in noise simulation from hologen.noise import SpeckleNoiseModel # Speckle noise internally uses gaussian_blur for correlation
Performance Considerations:
- Separable convolution: O(n ×ばつ kernel_size) instead of O(n ×ばつ kernel_size2)
- Kernel radius = 3σ (captures 99.7% of Gaussian)
- Edge padding mode: 'edge' (extends border values)
- Returns copy if sigma ≤ 0 (no computation)
Common Use Cases:
- Speckle noise correlation
- Optical aberration simulation
- Image smoothing for preprocessing
- Defocus simulation
Dataclass containing frequency-domain sampling coordinates.
Class Definition:
@dataclass(slots=True) class FourierGrid: fx: NDArray[np.float64] # Spatial frequencies along x-axis fy: NDArray[np.float64] # Spatial frequencies along y-axis
Usage:
from hologen.utils.math import FourierGrid, make_fourier_grid from hologen.types import GridSpec grid = GridSpec(height=256, width=256, pixel_pitch=2e-6) fourier_grid = make_fourier_grid(grid) # Access frequency arrays fx = fourier_grid.fx fy = fourier_grid.fy # Compute radial frequency f_radial = np.sqrt(fx**2 + fy**2)
- Batch conversions: Convert multiple fields in a loop rather than one at a time
- Representation selection: Use INTENSITY for fastest conversion, PHASE for slowest
- Validation: Only validate phase when necessary (e.g., before saving)
- Reuse arrays: Avoid unnecessary copies by working in-place when possible
-
Disable previews: Set
save_preview=Falsefor large-scale generation - Batch writes: Write samples in batches rather than one at a time
- Compression: NumPy's .npz format automatically compresses data
- Parallel I/O: Use multiple writers for parallel dataset generation
- SSD storage: I/O performance benefits significantly from SSD vs HDD
-
Cache Fourier grids: Reuse
FourierGridobjects for same grid specifications - Normalize once: Only normalize images when needed for visualization
- Blur optimization: Use smaller sigma values when possible
- Vectorization: Leverage NumPy's vectorized operations
from hologen import * from hologen.utils.fields import complex_to_representation, validate_phase_range from hologen.utils.io import ComplexFieldWriter from hologen.utils.math import normalize_image from pathlib import Path # Generate complex field dataset grid = GridSpec(height=512, width=512, pixel_pitch=1e-6) optical = OpticalConfig(wavelength=532e-9, distance=0.01) holography = HolographyConfig(method="inline") generator = create_complex_hologram_generator( grid=grid, optical=optical, holography=holography, num_samples=100, object_representation=FieldRepresentation.PHASE, hologram_representation=FieldRepresentation.COMPLEX ) # Write dataset writer = ComplexFieldWriter(save_preview=True, phase_colormap="twilight") samples = generator.generate() writer.save(samples, Path("output/phase_dataset")) # Load and process a sample from hologen.utils.io import load_complex_sample sample = load_complex_sample(Path("output/phase_dataset/sample_00000_object.npz")) if isinstance(sample, ComplexObjectSample): # Extract phase phase = complex_to_representation(sample.field, FieldRepresentation.PHASE) # Validate phase range validate_phase_range(phase) # Normalize for visualization phase_normalized = normalize_image(phase) print(f"Phase range: [{phase.min():.3f}, {phase.max():.3f}] radians")
import torch from torch.utils.data import Dataset, DataLoader from hologen.utils.io import load_complex_sample from hologen.utils.fields import complex_to_representation from hologen.types import FieldRepresentation from pathlib import Path import numpy as np class HologenDataset(Dataset): def __init__(self, data_dir: Path, representation: FieldRepresentation): self.data_dir = data_dir self.representation = representation self.samples = sorted(data_dir.glob("*_object.npz")) def __len__(self): return len(self.samples) def __getitem__(self, idx): # Load sample sample = load_complex_sample(self.samples[idx]) # Convert to desired representation if hasattr(sample, 'field'): data = complex_to_representation(sample.field, self.representation) else: data = sample.pixels # Convert to tensor tensor = torch.from_numpy(data).float().unsqueeze(0) return tensor # Create data loader dataset = HologenDataset( Path("output/phase_dataset"), FieldRepresentation.INTENSITY ) loader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=4) # Training loop for batch in loader: # batch shape: (16, 1, 512, 512) pass
- Complex Fields Documentation - Detailed guide to field representations
- I/O Formats Documentation - Complete file format specifications
- API Reference - Full API documentation
- Pipeline Documentation - Dataset generation pipeline architecture
| Function | Purpose | Returns |
|---|---|---|
complex_to_representation() |
Convert complex field to specific representation | ArrayFloat or ArrayComplex |
amplitude_phase_to_complex() |
Construct complex field from amplitude and phase | ArrayComplex |
validate_phase_range() |
Validate phase values in [-π, π] range | None (raises on error) |
Exceptions: FieldRepresentationError, PhaseRangeError
| Class/Function | Purpose | Key Parameters |
|---|---|---|
NumpyDatasetWriter |
Write intensity-based datasets | save_preview |
ComplexFieldWriter |
Write complex field datasets |
save_preview, phase_colormap
|
load_complex_sample() |
Load sample with format detection | path |
| Function | Purpose | Returns |
|---|---|---|
normalize_image() |
Normalize image to [0, 1] | ArrayFloat |
make_fourier_grid() |
Create frequency-domain coordinates | FourierGrid |
gaussian_blur() |
Apply Gaussian blur filter | ArrayFloat |
Data Classes: FourierGrid