-
Notifications
You must be signed in to change notification settings - Fork 0
05. Complex Fields
HoloGen supports complex-valued optical fields throughout the hologram generation pipeline, enabling more realistic and physics-accurate synthetic data for machine learning applications. This document explains the different field representations, how to use them, and when each is appropriate.
Optical fields in holography are fundamentally complex-valued, containing both amplitude and phase information. HoloGen supports four different representations of these fields:
What it is: The squared magnitude of the complex field, representing the detected light power.
I = |E|2 = A2
When to use:
- Classical holography ML applications where only intensity is recorded
- Backward compatibility with existing intensity-only workflows
- When phase information is not needed or available
- Training models for intensity-based reconstruction methods
Characteristics:
- Real-valued (non-negative)
- Phase information is lost
- Matches what typical cameras record
- Default representation for backward compatibility
Example use case: Training a CNN to reconstruct object shapes from inline hologram intensity patterns.
What it is: The magnitude of the complex field, representing the field strength.
A = |E|
When to use:
- Amplitude-based reconstruction methods
- When you need field strength without phase
- Intermediate processing steps
- Visualizing field magnitude
Characteristics:
- Real-valued (non-negative)
- Phase information is lost
- Square root of intensity
- Preserves more dynamic range than intensity
Example use case: Training models for amplitude-based phase retrieval algorithms.
What it is: The phase angle of the complex field in radians.
φ = arg(E) ∈ [-π, π]
When to use:
- Quantitative phase imaging (QPI) applications
- Phase-contrast microscopy simulations
- When amplitude is uniform or unimportant
- Training phase unwrapping or phase retrieval models
Characteristics:
- Real-valued (range: -π to π)
- Amplitude information is lost (assumed uniform)
- Critical for transparent sample imaging
- Wraps at ±π boundaries
Example use case: Generating training data for biological cell imaging where cells are transparent and only modulate phase.
What it is: The full complex field with both amplitude and phase.
E = A·exp(iφ) = real + i·imag
When to use:
- Physics-aware ML models that process full optical fields
- When both amplitude and phase are important
- Holographic reconstruction algorithms
- Preserving complete field information through pipeline
- Advanced applications requiring full wave information
Characteristics:
- Complex-valued (real + imaginary components)
- Contains complete field information
- No information loss
- Enables full wave optics simulations
- Larger storage requirements (2x memory)
Example use case: Training neural networks for holographic autofocusing or aberration correction that operate on complex fields.
The different representations capture different aspects of the optical field:
Original Complex Field: E = 0.8·exp(i·π/4)
├─ Intensity: I = 0.64
├─ Amplitude: A = 0.8
├─ Phase: φ = 0.785 rad (45°)
└─ Complex: E = 0.566 + 0.566i
For a simple circular object:
Amplitude-only object (absorbing circle):
- Intensity: Dark circle on bright background
- Amplitude: Smooth transition from 0 to 1
- Phase: Uniform (zero everywhere)
- Complex: Real-valued field
Phase-only object (transparent circle with phase shift):
- Intensity: Uniform (no contrast!)
- Amplitude: Uniform (1.0 everywhere)
- Phase: Step function (0 outside, π/2 inside)
- Complex: Pure phase modulation
Mixed object (partially absorbing with phase shift):
- Intensity: Partial contrast
- Amplitude: Varies with absorption
- Phase: Varies with optical path length
- Complex: Full amplitude-phase modulation
HoloGen provides utilities to convert between representations:
from hologen.utils.fields import complex_to_representation from hologen.types import FieldRepresentation # Convert complex field to different representations intensity = complex_to_representation(field, FieldRepresentation.INTENSITY) amplitude = complex_to_representation(field, FieldRepresentation.AMPLITUDE) phase = complex_to_representation(field, FieldRepresentation.PHASE)
Important: Converting from intensity, amplitude, or phase back to complex results in information loss:
- Intensity → Complex: Phase is assumed zero
- Amplitude → Complex: Phase is assumed zero
- Phase → Complex: Amplitude is assumed unity (1.0)
Only complex → complex conversion is lossless.
Use this decision tree to select the appropriate representation:
-
Do you need phase information?
- No → Use Intensity (default, most compatible)
- Yes → Continue to step 2
-
Do you need amplitude information?
- No (uniform amplitude) → Use Phase
- Yes → Continue to step 3
-
Are you training physics-aware models?
- Yes → Use Complex (full information)
- No → Use Amplitude or Intensity depending on your reconstruction method
-
Storage and memory constraints?
- Tight constraints → Use Intensity or Amplitude (half the size)
- No constraints → Use Complex (preserves all information)
| Representation | Memory Usage | Information Content | Compatibility |
|---|---|---|---|
| Intensity | 1x (baseline) | Low (magnitude2) | High (legacy) |
| Amplitude | 1x (baseline) | Medium (magnitude) | Medium |
| Phase | 1x (baseline) | Medium (angle) | Low |
| Complex | 2x (baseline) | High (complete) | Low (new) |
Memory example for ×ばつ512 images:
- Intensity/Amplitude/Phase: ~2 MB (float64) or ~1 MB (float32)
- Complex: ~4 MB (complex128) or ~2 MB (complex64)
- Complex Object Generation - Learn how to generate phase-only and mixed objects
- Complex Hologram Export - Understand file formats and loading data
- CLI Usage Examples - See command-line examples
- API Reference - Detailed API documentation
HoloGen can generate three types of object domains: amplitude-only, phase-only, and complex (mixed amplitude-phase). This section explains how to generate each type and when to use them.
Amplitude-only objects modulate the amplitude of transmitted light while leaving phase unchanged (zero phase).
Physical interpretation: Absorbing or scattering samples (e.g., stained biological samples, printed patterns, metal particles)
Mathematical representation:
E(x,y) = A(x,y)·exp(i·0) = A(x,y)
Code example:
from hologen.shapes import CircleGenerator from hologen.types import GridSpec import numpy as np # Create grid and generator grid = GridSpec(height=512, width=512, pixel_pitch=6.4e-6) generator = CircleGenerator(radius_range=(20e-6, 50e-6)) rng = np.random.default_rng(42) # Generate amplitude-only object complex_field = generator.generate_complex( grid=grid, rng=rng, mode="amplitude", phase_shift=0.0 # Not used for amplitude mode ) # Result: complex field with varying amplitude, zero phase print(f"Amplitude range: [{np.abs(complex_field).min():.2f}, {np.abs(complex_field).max():.2f}]") print(f"Phase range: [{np.angle(complex_field).min():.2f}, {np.angle(complex_field).max():.2f}]") # Output: Amplitude range: [0.00, 1.00], Phase range: [0.00, 0.00]
Phase-only objects modulate the phase of transmitted light while maintaining uniform amplitude.
Physical interpretation: Transparent samples with varying refractive index or thickness (e.g., biological cells, phase masks, transparent polymers)
Mathematical representation:
E(x,y) = 1.0·exp(i·φ(x,y))
Code example:
from hologen.shapes import CircleGenerator from hologen.types import GridSpec import numpy as np # Create grid and generator grid = GridSpec(height=512, width=512, pixel_pitch=6.4e-6) generator = CircleGenerator(radius_range=(20e-6, 50e-6)) rng = np.random.default_rng(42) # Generate phase-only object with π/2 phase shift complex_field = generator.generate_complex( grid=grid, rng=rng, mode="phase", phase_shift=np.pi/2 # 90-degree phase shift inside circle ) # Result: complex field with uniform amplitude, varying phase print(f"Amplitude range: [{np.abs(complex_field).min():.2f}, {np.abs(complex_field).max():.2f}]") print(f"Phase range: [{np.angle(complex_field).min():.2f}, {np.angle(complex_field).max():.2f}]") # Output: Amplitude range: [1.00, 1.00], Phase range: [0.00, 1.57]
Complex objects modulate both amplitude and phase simultaneously.
Physical interpretation: Samples with both absorption and refractive index variation (e.g., partially stained cells, composite materials)
Note: Full mixed mode is reserved for future extension. Current implementation supports amplitude-only and phase-only modes.
The phase_shift parameter controls the phase difference between the object and background for phase-only objects.
Valid range: [0, 2π] radians (0 to 6.28)
Common values:
-
π/4(0.785 rad, 45°): Small phase shift, subtle contrast -
π/2(1.571 rad, 90°): Quarter-wave shift, good contrast -
π(3.142 rad, 180°): Half-wave shift, maximum contrast -
3π/2(4.712 rad, 270°): Three-quarter wave shift
Physical meaning: The phase shift represents the optical path difference between light passing through the object versus the background:
φ = (2π/λ) ×ばつ (n1 - n0) ×ばつ d
Where:
- λ = wavelength
- n1 = refractive index of object
- n0 = refractive index of background (typically 1.0 for air)
- d = object thickness
Example: For a biological cell in water:
- λ = 532 nm (green laser)
- n1 = 1.38 (cell cytoplasm)
- n0 = 1.33 (water)
- d = 5 μm (cell thickness)
Phase shift: φ = (2π/532e-9) ×ばつ (1.38 - 1.33) ×ばつ 5e-6 ≈ 0.59 rad ≈ π/5
Choosing phase_shift:
# Subtle phase contrast (thin samples) phase_shift = np.pi / 4 # 45 degrees # Moderate phase contrast (typical cells) phase_shift = np.pi / 2 # 90 degrees (default) # Strong phase contrast (thick samples) phase_shift = np.pi # 180 degrees # Very strong contrast (phase masks) phase_shift = 3 * np.pi / 2 # 270 degrees
All shape generators support complex field generation:
from hologen.shapes import ( CircleGenerator, RectangleGenerator, RingGenerator, CircleCheckerGenerator, RectangleCheckerGenerator, EllipseCheckerGenerator ) # Each generator has generate_complex() method generators = [ CircleGenerator(radius_range=(20e-6, 50e-6)), RectangleGenerator(width_range=(30e-6, 60e-6), height_range=(30e-6, 60e-6)), RingGenerator(inner_radius_range=(15e-6, 25e-6), outer_radius_range=(30e-6, 50e-6)), CircleCheckerGenerator(radius_range=(40e-6, 80e-6), num_divisions=8), RectangleCheckerGenerator(width_range=(50e-6, 100e-6), height_range=(50e-6, 100e-6), num_divisions=8), EllipseCheckerGenerator(semi_major_range=(40e-6, 80e-6), semi_minor_range=(30e-6, 60e-6), num_divisions=8) ] # Generate phase-only objects with each shape for generator in generators: field = generator.generate_complex( grid=grid, rng=rng, mode="phase", phase_shift=np.pi/2 )
Here's a complete example generating phase-only objects and complex holograms:
from hologen import * from hologen.converters import ObjectDomainProducer, ObjectToHologramConverter, HologramDatasetGenerator from hologen.holography.inline import InlineHolographyStrategy from hologen.shapes import CircleGenerator from hologen.types import ( GridSpec, OpticalConfig, HolographyConfig, HolographyMethod, FieldRepresentation, OutputConfig ) from hologen.utils.io import ComplexFieldWriter from pathlib import Path import numpy as np # Configuration grid = GridSpec(height=512, width=512, pixel_pitch=6.4e-6) optics = OpticalConfig(wavelength=532e-9, propagation_distance=0.05) config = HolographyConfig(grid=grid, optics=optics, method=HolographyMethod.INLINE) # Output configuration for complex fields output_config = OutputConfig( object_representation=FieldRepresentation.PHASE, hologram_representation=FieldRepresentation.COMPLEX, reconstruction_representation=FieldRepresentation.COMPLEX ) # Create pipeline components shape_generator = CircleGenerator(radius_range=(20e-6, 50e-6)) object_producer = ObjectDomainProducer( generator=shape_generator, phase_shift=np.pi/2, # 90-degree phase shift mode="phase" # Phase-only objects ) strategy = InlineHolographyStrategy() converter = ObjectToHologramConverter( strategy_mapping={HolographyMethod.INLINE: strategy}, output_config=output_config ) # Generate dataset rng = np.random.default_rng(42) dataset_generator = HologramDatasetGenerator( object_producer=object_producer, converter=converter, config=config ) # Write to disk writer = ComplexFieldWriter(save_preview=True, phase_colormap="twilight") samples = dataset_generator.generate(count=10, rng=rng) writer.save(samples, output_dir=Path("phase_objects_dataset"))
HoloGen automatically validates phase values to ensure they're in the valid range [-π, π]:
from hologen.utils.fields import validate_phase_range, PhaseRangeError # This will raise PhaseRangeError if phase is out of range try: validate_phase_range(phase_array) except PhaseRangeError as e: print(f"Invalid phase values: {e}")
-
Start with amplitude-only: If you're new to complex fields, start with amplitude-only objects (default behavior) before moving to phase-only.
-
Use moderate phase shifts: For biological samples, π/2 to π is typically realistic. Larger shifts may not be physically meaningful.
-
Consider your application:
- Amplitude-only: Classical holography, absorbing samples
- Phase-only: Quantitative phase imaging, transparent samples
- Complex: Advanced applications requiring both
-
Validate your data: Always check that generated fields have expected properties (uniform amplitude for phase-only, zero phase for amplitude-only).
-
Memory usage: Complex fields use 2x memory compared to intensity. For large datasets, consider generating in batches.
HoloGen exports complex field data in two formats: NumPy .npz archives for numerical processing and PNG images for visualization. This section explains the file structure and how to load data in ML pipelines.
The .npz format stores arrays and metadata in a compressed archive. The structure varies by field representation:
Stores real and imaginary components separately:
# File: sample_00000_circle_hologram.npz { 'real': ndarray, # Real component (float64, shape: [H, W]) 'imag': ndarray, # Imaginary component (float64, shape: [H, W]) 'representation': 'complex', # String identifier 'wavelength': 532e-9, # Optical wavelength in meters 'propagation_distance': 0.05 # Propagation distance in meters }
Loading example:
import numpy as np # Load complex hologram data = np.load('sample_00000_circle_hologram.npz') hologram = data['real'] + 1j * data['imag'] # Access metadata wavelength = float(data['wavelength']) distance = float(data['propagation_distance']) print(f"Hologram shape: {hologram.shape}") print(f"Hologram dtype: {hologram.dtype}") print(f"Wavelength: {wavelength*1e9:.1f} nm")
Stores magnitude only:
# File: sample_00000_circle_hologram.npz { 'amplitude': ndarray, # Amplitude (float64, shape: [H, W]) 'representation': 'amplitude', 'wavelength': 532e-9, 'propagation_distance': 0.05 }
Loading example:
data = np.load('sample_00000_circle_hologram.npz') amplitude = data['amplitude']
Stores phase angle in radians:
# File: sample_00000_circle_hologram.npz { 'phase': ndarray, # Phase in radians (float64, shape: [H, W]) 'representation': 'phase', 'wavelength': 532e-9, 'propagation_distance': 0.05 }
Loading example:
data = np.load('sample_00000_circle_hologram.npz') phase = data['phase'] # Range: [-π, π]
Backward-compatible format for intensity-only data:
# File: sample_00000_circle.npz { 'object': ndarray, # Object intensity (float64, shape: [H, W]) 'hologram': ndarray, # Hologram intensity (float64, shape: [H, W]) 'reconstruction': ndarray # Reconstruction intensity (float64, shape: [H, W]) }
Loading example:
data = np.load('sample_00000_circle.npz') object_intensity = data['object'] hologram_intensity = data['hologram'] reconstruction = data['reconstruction']
PNG files provide visual previews of the data. The export behavior depends on the field representation:
Generates two PNG files per field:
-
*_amplitude.png: Amplitude visualization (grayscale, 8-bit) -
*_phase.png: Phase visualization (colormap, 8-bit)
File naming example:
sample_00000_circle_object_amplitude.png
sample_00000_circle_object_phase.png
sample_00000_circle_hologram_amplitude.png
sample_00000_circle_hologram_phase.png
sample_00000_circle_reconstruction_amplitude.png
sample_00000_circle_reconstruction_phase.png
Amplitude PNG encoding:
- Amplitude values normalized to [0, 255]
- Linear mapping:
pixel = 255 * (amplitude / amplitude.max()) - Grayscale 8-bit PNG
Phase PNG encoding:
- Phase values mapped from [-π, π] to [0, 255]
- Linear mapping:
pixel = 255 * (phase + π) / (2π) - Optional colormap applied (default: "twilight")
- 8-bit PNG (grayscale or RGB if colormap used)
Generates one PNG file per field:
- Single grayscale image
- Values normalized to [0, 255]
import torch from torch.utils.data import Dataset, DataLoader import numpy as np from pathlib import Path class ComplexHologramDataset(Dataset): """PyTorch dataset for complex hologram data.""" def __init__(self, data_dir: Path): self.data_dir = Path(data_dir) self.samples = sorted(self.data_dir.glob("*_hologram.npz")) def __len__(self): return len(self.samples) def __getitem__(self, idx): # Load hologram data = np.load(self.samples[idx]) if 'real' in data and 'imag' in data: # Complex representation hologram = data['real'] + 1j * data['imag'] # Convert to 2-channel tensor (real, imag) hologram_tensor = torch.stack([ torch.from_numpy(data['real']).float(), torch.from_numpy(data['imag']).float() ], dim=0) else: # Intensity representation (legacy) hologram = data['hologram'] hologram_tensor = torch.from_numpy(hologram).float().unsqueeze(0) # Load corresponding object object_path = self.samples[idx].parent / self.samples[idx].name.replace('_hologram', '_object') object_data = np.load(object_path) if 'real' in object_data and 'imag' in object_data: object_tensor = torch.stack([ torch.from_numpy(object_data['real']).float(), torch.from_numpy(object_data['imag']).float() ], dim=0) else: object_tensor = torch.from_numpy(object_data['object']).float().unsqueeze(0) return hologram_tensor, object_tensor # Usage dataset = ComplexHologramDataset(Path("phase_objects_dataset/npz")) dataloader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=4) for hologram_batch, object_batch in dataloader: # hologram_batch shape: [batch_size, 2, height, width] for complex # or [batch_size, 1, height, width] for intensity print(f"Hologram batch shape: {hologram_batch.shape}") break
import tensorflow as tf import numpy as np from pathlib import Path def load_complex_sample(hologram_path, object_path): """Load a single complex hologram-object pair.""" # Load hologram hologram_data = np.load(hologram_path.numpy().decode()) if b'real' in hologram_data.files and b'imag' in hologram_data.files: hologram = np.stack([hologram_data['real'], hologram_data['imag']], axis=-1) else: hologram = hologram_data['hologram'][..., np.newaxis] # Load object object_data = np.load(object_path.numpy().decode()) if b'real' in object_data.files and b'imag' in object_data.files: obj = np.stack([object_data['real'], object_data['imag']], axis=-1) else: obj = object_data['object'][..., np.newaxis] return hologram.astype(np.float32), obj.astype(np.float32) # Create dataset data_dir = Path("phase_objects_dataset/npz") hologram_paths = sorted(data_dir.glob("*_hologram.npz")) object_paths = [str(p).replace('_hologram', '_object') for p in hologram_paths] dataset = tf.data.Dataset.from_tensor_slices(( [str(p) for p in hologram_paths], object_paths )) dataset = dataset.map( lambda h, o: tf.py_function( load_complex_sample, [h, o], [tf.float32, tf.float32] ), num_parallel_calls=tf.data.AUTOTUNE ) dataset = dataset.batch(16).prefetch(tf.data.AUTOTUNE) # Usage for hologram_batch, object_batch in dataset: # hologram_batch shape: [batch_size, height, width, 2] for complex # or [batch_size, height, width, 1] for intensity print(f"Hologram batch shape: {hologram_batch.shape}") break
import numpy as np from pathlib import Path from sklearn.model_selection import train_test_split def load_dataset(data_dir: Path): """Load entire dataset into memory.""" data_dir = Path(data_dir) hologram_files = sorted(data_dir.glob("*_hologram.npz")) holograms = [] objects = [] for hologram_path in hologram_files: # Load hologram h_data = np.load(hologram_path) if 'real' in h_data and 'imag' in h_data: hologram = h_data['real'] + 1j * h_data['imag'] else: hologram = h_data['hologram'] # Load object object_path = hologram_path.parent / hologram_path.name.replace('_hologram', '_object') o_data = np.load(object_path) if 'real' in o_data and 'imag' in o_data: obj = o_data['real'] + 1j * o_data['imag'] else: obj = o_data['object'] holograms.append(hologram) objects.append(obj) return np.array(holograms), np.array(objects) # Load and split X, y = load_dataset(Path("phase_objects_dataset/npz")) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) print(f"Training set: {X_train.shape}, dtype: {X_train.dtype}") print(f"Test set: {X_test.shape}, dtype: {X_test.dtype}")
HoloGen provides a utility function for loading samples:
from hologen.utils.io import load_complex_sample # Load a complex sample (auto-detects format) sample = load_complex_sample(Path("phase_objects_dataset/npz/sample_00000_circle.npz")) # Returns ComplexObjectSample or ObjectSample depending on format if hasattr(sample, 'field'): # Complex sample print(f"Complex field shape: {sample.field.shape}") print(f"Representation: {sample.representation}") else: # Legacy intensity sample print(f"Intensity shape: {sample.pixels.shape}")
ComplexFieldWriter organizes files in a structured directory:
output_dir/
├── npz/
│ ├── sample_00000_circle_object.npz
│ ├── sample_00000_circle_hologram.npz
│ ├── sample_00000_circle_reconstruction.npz
│ ├── sample_00001_ring_object.npz
│ ├── sample_00001_ring_hologram.npz
│ └── ...
└── preview/
├── object/
│ ├── sample_00000_circle_object_amplitude.png
│ ├── sample_00000_circle_object_phase.png
│ └── ...
├── hologram/
│ ├── sample_00000_circle_hologram_amplitude.png
│ ├── sample_00000_circle_hologram_phase.png
│ └── ...
└── reconstruction/
├── sample_00000_circle_reconstruction_amplitude.png
├── sample_00000_circle_reconstruction_phase.png
└── ...
-
Check representation type: Always check the 'representation' field in .npz files to determine how to load the data.
-
Handle both formats: Write loaders that can handle both legacy intensity and new complex formats for backward compatibility.
-
Normalize appropriately:
- Amplitude: Normalize by max value or use fixed normalization
- Phase: Already in [-π, π], may need wrapping for some applications
- Intensity: Normalize by max or use percentile-based normalization
-
Memory management: Complex fields use 2x memory. For large datasets, use generators or load in batches.
-
Validate loaded data:
# Check for NaN or Inf assert np.isfinite(hologram).all(), "Hologram contains non-finite values" # Check phase range phase = np.angle(hologram) assert np.all((-np.pi <= phase) & (phase <= np.pi)), "Phase out of range" # Check amplitude is non-negative amplitude = np.abs(hologram) assert np.all(amplitude >= 0), "Amplitude is negative"
-
Use appropriate dtypes:
-
complex128for high precision (default) -
complex64for memory efficiency (half the size) - Convert after loading if needed:
hologram.astype(np.complex64)
-
The generate_dataset.py script supports complex field generation through command-line arguments. This section provides examples for common use cases.
--object-type {amplitude,phase,complex}
Type of object domain representation
Default: amplitude
--output-domain {intensity,amplitude,phase,complex}
Type of hologram output representation
Default: intensity (backward compatible)
--phase-shift FLOAT
Phase shift in radians for phase-only objects
Default: 1.5708 (π/2, 90 degrees)
Valid range: [0, 2π]Generate traditional intensity-only holograms from amplitude objects:
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_intensity \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05
This is the default behavior and maintains backward compatibility.
Generate phase-only objects (transparent samples) with full complex hologram output:
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_phase_complex \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain complex \ --phase-shift 1.5708
Use case: Quantitative phase imaging, biological cell imaging
Generate phase-only objects but export only intensity (for phase contrast imaging):
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_phase_intensity \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain intensity \ --phase-shift 1.5708
Use case: Training models for phase contrast microscopy where only intensity is recorded
Generate amplitude objects with full complex hologram output:
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_amplitude_complex \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type amplitude \ --output-domain complex
Use case: Physics-aware models that need full field information even for absorbing samples
Generate off-axis holograms with complex output:
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_offaxis_complex \ --method off_axis \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --carrier-frequency-x 1e6 \ --carrier-frequency-y 0 \ --object-type phase \ --output-domain complex \ --phase-shift 1.5708
Use case: Off-axis holography with phase objects
Generate phase objects with large phase shift (π radians):
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_large_phase \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain complex \ --phase-shift 3.14159
Use case: Thick samples or large refractive index differences
Generate phase objects with small phase shift (π/4 radians):
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_small_phase \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain complex \ --phase-shift 0.7854
Use case: Thin samples or small refractive index differences
Generate holograms with amplitude-only output (no phase):
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_amplitude_only \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type amplitude \ --output-domain amplitude
Use case: Amplitude-based reconstruction methods
Generate holograms with phase-only output (no amplitude):
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_phase_only \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain phase \ --phase-shift 1.5708
Use case: Phase retrieval algorithms, quantitative phase imaging
Complex fields work seamlessly with noise models:
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_phase_noisy \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain complex \ --phase-shift 1.5708 \ --sensor-read-noise 3.0 \ --sensor-shot-noise \ --sensor-bit-depth 12
Note: Noise is applied to the intensity representation, then the complex field is reconstructed preserving phase.
python scripts/generate_dataset.py \ --samples 100 \ --output ./dataset_phase_speckle \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain complex \ --phase-shift 1.5708 \ --speckle-contrast 0.8
Combine phase objects, complex output, and comprehensive noise:
python scripts/generate_dataset.py \ --samples 1000 \ --output ./dataset_realistic \ --method inline \ --height 512 \ --width 512 \ --pixel-pitch 6.4e-6 \ --wavelength 532e-9 \ --distance 0.05 \ --object-type phase \ --output-domain complex \ --phase-shift 1.5708 \ --speckle-contrast 0.8 \ --sensor-read-noise 3.0 \ --sensor-shot-noise \ --sensor-dark-current 0.5 \ --sensor-bit-depth 12 \ --aberration-defocus 0.5 \ --aberration-astigmatism 0.3
Use case: Most realistic simulation for training robust ML models
Here's a matrix of valid combinations:
| Object Type | Output Domain | Use Case |
|---|---|---|
| amplitude | intensity | Default, backward compatible |
| amplitude | amplitude | Amplitude-based reconstruction |
| amplitude | phase | Not recommended (phase is zero) |
| amplitude | complex | Physics-aware models |
| phase | intensity | Phase contrast imaging |
| phase | amplitude | Not recommended (amplitude is uniform) |
| phase | phase | Quantitative phase imaging |
| phase | complex | Full field QPI |
# List generated files ls -lh dataset_phase_complex/npz/ # Check .npz contents python -c "import numpy as np; data = np.load('dataset_phase_complex/npz/sample_00000_circle_hologram.npz'); print(data.files)" # Verify complex data python -c " import numpy as np data = np.load('dataset_phase_complex/npz/sample_00000_circle_hologram.npz') if 'real' in data and 'imag' in data: field = data['real'] + 1j * data['imag'] print(f'Complex field shape: {field.shape}') print(f'Amplitude range: [{np.abs(field).min():.3f}, {np.abs(field).max():.3f}]') print(f'Phase range: [{np.angle(field).min():.3f}, {np.angle(field).max():.3f}]') "
# Check that phase-only objects have uniform amplitude python -c " import numpy as np data = np.load('dataset_phase_complex/npz/sample_00000_circle_object.npz') field = data['real'] + 1j * data['imag'] amplitude = np.abs(field) print(f'Amplitude std: {amplitude.std():.6f}') # Should be ~0 print(f'Amplitude mean: {amplitude.mean():.6f}') # Should be ~1.0 "
Error: ValueError: Invalid output-domain value
Solution: Use one of: intensity, amplitude, phase, complex
Error: PhaseRangeError: Phase values outside [-π, π]
Solution: Check --phase-shift is in [0, 2π] range
Error: FileNotFoundError: No such file or directory
Solution: Ensure output directory exists or script will create it
# Small dataset for testing (fast) python scripts/generate_dataset.py --samples 10 --output ./test --object-type phase --output-domain complex # Medium dataset for development (moderate) python scripts/generate_dataset.py --samples 100 --output ./dev --object-type phase --output-domain complex # Large dataset for training (slow, ~1-2 samples/sec) python scripts/generate_dataset.py --samples 10000 --output ./train --object-type phase --output-domain complex # Very large dataset (use batching) for i in {0..9}; do python scripts/generate_dataset.py \ --samples 1000 \ --output ./train_batch_$i \ --object-type phase \ --output-domain complex done
# Minimal command (defaults) python scripts/generate_dataset.py # Phase objects, complex output python scripts/generate_dataset.py --object-type phase --output-domain complex # Custom phase shift python scripts/generate_dataset.py --object-type phase --phase-shift 3.14159 # With noise python scripts/generate_dataset.py --object-type phase --output-domain complex --sensor-shot-noise # Off-axis python scripts/generate_dataset.py --method off_axis --object-type phase --output-domain complex # Help python scripts/generate_dataset.py --help
This section documents the new classes, functions, and protocols introduced for complex field support.
Enumeration of field representation types.
from enum import StrEnum, auto, unique @unique class FieldRepresentation(StrEnum): """Enumeration of optical field representation types.""" INTENSITY = auto() # |E|2 - squared magnitude AMPLITUDE = auto() # |E| - magnitude PHASE = auto() # arg(E) - phase angle in radians COMPLEX = auto() # E = real + i*imag - full complex field
Usage:
from hologen.types import FieldRepresentation # Specify representation rep = FieldRepresentation.COMPLEX print(rep) # Output: 'complex' # Compare representations if rep == FieldRepresentation.COMPLEX: print("Using complex representation")
Dataclass representing an object-domain sample with complex field.
@dataclass(slots=True) class ComplexObjectSample: """Object-domain sample with complex field representation. Attributes: name: Sample identifier (e.g., 'sample_00000_circle') field: Complex-valued 2D array representing the object field representation: Type of field representation """ name: str field: ArrayComplex # Complex128 array, shape: [H, W] representation: FieldRepresentation
Usage:
from hologen.types import ComplexObjectSample, FieldRepresentation import numpy as np # Create a complex object sample sample = ComplexObjectSample( name="my_sample", field=np.ones((512, 512), dtype=np.complex128), representation=FieldRepresentation.COMPLEX ) # Access properties print(f"Sample name: {sample.name}") print(f"Field shape: {sample.field.shape}") print(f"Field dtype: {sample.field.dtype}") print(f"Representation: {sample.representation}")
Dataclass representing a complete hologram sample with complex fields.
@dataclass(slots=True) class ComplexHologramSample: """Hologram sample with complex field support. Attributes: object_sample: The original object-domain sample hologram_field: Complex hologram field at sensor plane hologram_representation: Representation type for hologram reconstruction_field: Reconstructed object field reconstruction_representation: Representation type for reconstruction """ object_sample: ComplexObjectSample hologram_field: ArrayComplex hologram_representation: FieldRepresentation reconstruction_field: ArrayComplex reconstruction_representation: FieldRepresentation
Usage:
from hologen.types import ComplexHologramSample # Access sample components print(f"Object name: {sample.object_sample.name}") print(f"Hologram shape: {sample.hologram_field.shape}") print(f"Hologram representation: {sample.hologram_representation}") print(f"Reconstruction shape: {sample.reconstruction_field.shape}") # Extract specific representations hologram_intensity = np.abs(sample.hologram_field) ** 2 hologram_phase = np.angle(sample.hologram_field)
Configuration for output field representations.
@dataclass(slots=True) class OutputConfig: """Configuration for output field representations. Attributes: object_representation: Desired representation for object field hologram_representation: Desired representation for hologram field reconstruction_representation: Desired representation for reconstruction Default values maintain backward compatibility (intensity-only). """ object_representation: FieldRepresentation = FieldRepresentation.INTENSITY hologram_representation: FieldRepresentation = FieldRepresentation.INTENSITY reconstruction_representation: FieldRepresentation = FieldRepresentation.INTENSITY
Usage:
from hologen.types import OutputConfig, FieldRepresentation # Default configuration (backward compatible) config = OutputConfig() print(config.hologram_representation) # Output: 'intensity' # Custom configuration for complex output config = OutputConfig( object_representation=FieldRepresentation.PHASE, hologram_representation=FieldRepresentation.COMPLEX, reconstruction_representation=FieldRepresentation.COMPLEX ) # Use in converter converter = ObjectToHologramConverter( strategy_mapping=strategies, output_config=config )
Convert complex field to requested representation.
def complex_to_representation( field: ArrayComplex, representation: FieldRepresentation ) -> ArrayFloat | ArrayComplex: """Convert complex field to requested representation. Args: field: Complex-valued field array representation: Target representation type Returns: Converted field (real-valued for intensity/amplitude/phase, complex-valued for complex representation) Raises: FieldRepresentationError: If representation is invalid Examples: >>> field = np.array([[1+1j, 2+0j], [0+1j, 1-1j]]) >>> intensity = complex_to_representation(field, FieldRepresentation.INTENSITY) >>> amplitude = complex_to_representation(field, FieldRepresentation.AMPLITUDE) >>> phase = complex_to_representation(field, FieldRepresentation.PHASE) """
Usage:
from hologen.utils.fields import complex_to_representation from hologen.types import FieldRepresentation import numpy as np # Create complex field field = np.exp(1j * np.linspace(0, 2*np.pi, 100).reshape(10, 10)) # Convert to different representations intensity = complex_to_representation(field, FieldRepresentation.INTENSITY) amplitude = complex_to_representation(field, FieldRepresentation.AMPLITUDE) phase = complex_to_representation(field, FieldRepresentation.PHASE) complex_copy = complex_to_representation(field, FieldRepresentation.COMPLEX) print(f"Intensity range: [{intensity.min():.3f}, {intensity.max():.3f}]") print(f"Amplitude range: [{amplitude.min():.3f}, {amplitude.max():.3f}]") print(f"Phase range: [{phase.min():.3f}, {phase.max():.3f}]")
Construct complex field from amplitude and phase arrays.
def amplitude_phase_to_complex( amplitude: ArrayFloat, phase: ArrayFloat ) -> ArrayComplex: """Construct complex field from amplitude and phase. Args: amplitude: Amplitude array (non-negative) phase: Phase array in radians (typically [-π, π]) Returns: Complex field: amplitude * exp(i * phase) Examples: >>> amplitude = np.ones((10, 10)) >>> phase = np.random.uniform(-np.pi, np.pi, (10, 10)) >>> field = amplitude_phase_to_complex(amplitude, phase) """
Usage:
from hologen.utils.fields import amplitude_phase_to_complex import numpy as np # Create amplitude and phase separately amplitude = np.random.rand(512, 512) phase = np.random.uniform(-np.pi, np.pi, (512, 512)) # Combine into complex field field = amplitude_phase_to_complex(amplitude, phase) # Verify round-trip recovered_amplitude = np.abs(field) recovered_phase = np.angle(field) assert np.allclose(amplitude, recovered_amplitude) assert np.allclose(phase, recovered_phase)
Validate that phase values are within valid range.
def validate_phase_range(phase: ArrayFloat) -> None: """Validate phase values are in [-π, π] range. Args: phase: Phase array in radians Raises: PhaseRangeError: If any phase values are outside [-π, π] Examples: >>> phase = np.array([0, np.pi/2, np.pi, -np.pi]) >>> validate_phase_range(phase) # OK >>> >>> invalid_phase = np.array([0, 4*np.pi]) >>> validate_phase_range(invalid_phase) # Raises PhaseRangeError """
Usage:
from hologen.utils.fields import validate_phase_range, PhaseRangeError import numpy as np # Valid phase phase = np.random.uniform(-np.pi, np.pi, (512, 512)) validate_phase_range(phase) # No error # Invalid phase try: invalid_phase = np.array([0, 5*np.pi]) validate_phase_range(invalid_phase) except PhaseRangeError as e: print(f"Validation failed: {e}")
class FieldRepresentationError(ValueError): """Raised when field representation is invalid or incompatible.""" pass class PhaseRangeError(ValueError): """Raised when phase values are outside [-π, π] range.""" pass
Generate complex-valued object field.
def generate_complex( self, grid: GridSpec, rng: Generator, phase_shift: float = 0.0, mode: str = "amplitude" ) -> ArrayComplex: """Generate complex-valued object field. Args: grid: Grid specification defining spatial dimensions rng: NumPy random number generator phase_shift: Phase modulation in radians for phase-only mode mode: Generation mode - "amplitude" or "phase" Returns: Complex field array with shape [grid.height, grid.width] Raises: ValueError: If mode is invalid PhaseRangeError: If phase_shift is outside [0, 2π] Examples: >>> generator = CircleGenerator(radius_range=(20e-6, 50e-6)) >>> grid = GridSpec(height=512, width=512, pixel_pitch=6.4e-6) >>> rng = np.random.default_rng(42) >>> >>> # Amplitude-only object >>> amp_field = generator.generate_complex(grid, rng, mode="amplitude") >>> >>> # Phase-only object >>> phase_field = generator.generate_complex( ... grid, rng, phase_shift=np.pi/2, mode="phase" ... ) """
Available generators:
CircleGeneratorRectangleGeneratorRingGeneratorCircleCheckerGeneratorRectangleCheckerGeneratorEllipseCheckerGenerator
Usage:
from hologen.shapes import CircleGenerator, RectangleGenerator from hologen.types import GridSpec import numpy as np grid = GridSpec(height=512, width=512, pixel_pitch=6.4e-6) rng = np.random.default_rng(42) # Circle with amplitude modulation circle_gen = CircleGenerator(radius_range=(20e-6, 50e-6)) amp_circle = circle_gen.generate_complex(grid, rng, mode="amplitude") # Rectangle with phase modulation rect_gen = RectangleGenerator(width_range=(30e-6, 60e-6), height_range=(30e-6, 60e-6)) phase_rect = rect_gen.generate_complex(grid, rng, phase_shift=np.pi/2, mode="phase")
Generate complex object sample.
def generate_complex(self, rng: Generator) -> ComplexObjectSample: """Generate a complex object-domain sample. Args: rng: NumPy random number generator Returns: ComplexObjectSample with generated field Examples: >>> producer = ObjectDomainProducer( ... generator=CircleGenerator(radius_range=(20e-6, 50e-6)), ... phase_shift=np.pi/2, ... mode="phase" ... ) >>> sample = producer.generate_complex(rng) """
Converter now supports complex fields and OutputConfig.
@dataclass(slots=True) class ObjectToHologramConverter: """Convert object-domain samples to holograms with complex field support. Attributes: strategy_mapping: Mapping from holography method to strategy noise_model: Optional noise model to apply output_config: Configuration for output representations """ strategy_mapping: dict[HolographyMethod, HolographyStrategy] noise_model: NoiseModel | None = None output_config: OutputConfig = field(default_factory=OutputConfig)
Updated methods:
def create_hologram( self, sample: ComplexObjectSample, config: HolographyConfig, rng: Generator ) -> ArrayComplex: """Generate complex hologram from complex object. Args: sample: Complex object sample config: Holography configuration rng: Random number generator Returns: Complex hologram field """ def reconstruct( self, hologram: ArrayComplex, config: HolographyConfig ) -> ArrayComplex: """Reconstruct complex object field from complex hologram. Args: hologram: Complex hologram field config: Holography configuration Returns: Complex reconstruction field """
Writer for complex field data.
@dataclass(slots=True) class ComplexFieldWriter: """Write complex field data to NumPy archives and PNG previews. Attributes: save_preview: Whether to save PNG preview images phase_colormap: Matplotlib colormap name for phase visualization Examples: >>> writer = ComplexFieldWriter(save_preview=True, phase_colormap="twilight") >>> writer.save(samples, output_dir=Path("dataset")) """ save_preview: bool = True phase_colormap: str = "twilight" def save( self, samples: Iterable[ComplexHologramSample], output_dir: Path ) -> None: """Write complex hologram samples to disk. Args: samples: Iterable of complex hologram samples output_dir: Output directory path Creates directory structure: output_dir/ ├── npz/ │ ├── *_object.npz │ ├── *_hologram.npz │ └── *_reconstruction.npz └── preview/ ├── object/ ├── hologram/ └── reconstruction/ """
Usage:
from hologen.utils.io import ComplexFieldWriter from pathlib import Path # Create writer writer = ComplexFieldWriter( save_preview=True, phase_colormap="twilight" # or "hsv", "twilight_shifted", etc. ) # Write samples writer.save(samples, output_dir=Path("my_dataset"))
Load sample with automatic format detection.
def load_complex_sample(path: Path) -> ComplexObjectSample | ObjectSample: """Load sample with automatic format detection. Args: path: Path to .npz file Returns: ComplexObjectSample if file contains complex data, ObjectSample if file contains legacy intensity data Raises: ValueError: If file format is unrecognized Examples: >>> sample = load_complex_sample(Path("dataset/npz/sample_00000_circle_object.npz")) >>> if isinstance(sample, ComplexObjectSample): ... print(f"Complex field: {sample.field.shape}") ... else: ... print(f"Intensity field: {sample.pixels.shape}") """
Usage:
from hologen.utils.io import load_complex_sample from hologen.types import ComplexObjectSample, ObjectSample from pathlib import Path # Load sample (auto-detects format) sample = load_complex_sample(Path("dataset/npz/sample_00000_circle_object.npz")) # Handle both formats if isinstance(sample, ComplexObjectSample): print(f"Complex sample: {sample.representation}") field = sample.field else: print("Legacy intensity sample") intensity = sample.pixels
Protocol for holography strategies now uses complex fields.
class HolographyStrategy(Protocol): """Protocol for holography strategy implementations.""" def create_hologram( self, object_field: ArrayComplex, # Updated: was ArrayFloat config: HolographyConfig ) -> ArrayComplex: # Updated: was ArrayFloat """Create complex hologram field from complex object field.""" ... def reconstruct( self, hologram: ArrayComplex, # Updated: was ArrayFloat config: HolographyConfig ) -> ArrayComplex: # Updated: was ArrayFloat """Reconstruct complex object field from complex hologram.""" ...
Old code (intensity-only):
from hologen.converters import generate_dataset from hologen.utils.io import NumpyDatasetWriter # Generate intensity-only dataset generate_dataset( count=100, config=config, rng=rng, writer=NumpyDatasetWriter(save_preview=True), output_dir=Path("dataset") )
New code (complex fields):
from hologen.converters import generate_dataset from hologen.utils.io import ComplexFieldWriter from hologen.types import OutputConfig, FieldRepresentation # Configure complex output output_config = OutputConfig( object_representation=FieldRepresentation.PHASE, hologram_representation=FieldRepresentation.COMPLEX, reconstruction_representation=FieldRepresentation.COMPLEX ) # Generate complex dataset generate_dataset( count=100, config=config, rng=rng, writer=ComplexFieldWriter(save_preview=True), output_dir=Path("dataset"), output_config=output_config )
All existing code continues to work without modification:
# This still works (intensity-only, default behavior) generate_dataset( count=100, config=config, rng=rng, writer=NumpyDatasetWriter(save_preview=True), output_dir=Path("dataset") )
from numpy.typing import NDArray import numpy as np # Array type aliases ArrayFloat = NDArray[np.float64] # Real-valued arrays ArrayComplex = NDArray[np.complex128] # Complex-valued arrays
from hologen import * from hologen.converters import ObjectDomainProducer, ObjectToHologramConverter from hologen.holography.inline import InlineHolographyStrategy from hologen.shapes import CircleGenerator from hologen.types import ( GridSpec, OpticalConfig, HolographyConfig, HolographyMethod, FieldRepresentation, OutputConfig, ComplexObjectSample ) from hologen.utils.fields import complex_to_representation, validate_phase_range from hologen.utils.io import ComplexFieldWriter from pathlib import Path import numpy as np # 1. Configure system grid = GridSpec(height=512, width=512, pixel_pitch=6.4e-6) optics = OpticalConfig(wavelength=532e-9, propagation_distance=0.05) config = HolographyConfig(grid=grid, optics=optics, method=HolographyMethod.INLINE) # 2. Configure output output_config = OutputConfig( object_representation=FieldRepresentation.PHASE, hologram_representation=FieldRepresentation.COMPLEX, reconstruction_representation=FieldRepresentation.COMPLEX ) # 3. Create components generator = CircleGenerator(radius_range=(20e-6, 50e-6)) producer = ObjectDomainProducer(generator=generator, phase_shift=np.pi/2, mode="phase") strategy = InlineHolographyStrategy() converter = ObjectToHologramConverter( strategy_mapping={HolographyMethod.INLINE: strategy}, output_config=output_config ) # 4. Generate sample rng = np.random.default_rng(42) object_sample = producer.generate_complex(rng) # 5. Validate phase = np.angle(object_sample.field) validate_phase_range(phase) # 6. Create hologram hologram = converter.create_hologram(object_sample, config, rng) # 7. Reconstruct reconstruction = converter.reconstruct(hologram, config) # 8. Convert representations hologram_intensity = complex_to_representation(hologram, FieldRepresentation.INTENSITY) hologram_amplitude = complex_to_representation(hologram, FieldRepresentation.AMPLITUDE) hologram_phase = complex_to_representation(hologram, FieldRepresentation.PHASE) # 9. Save writer = ComplexFieldWriter(save_preview=True, phase_colormap="twilight") samples = [ComplexHologramSample( object_sample=object_sample, hologram_field=hologram, hologram_representation=FieldRepresentation.COMPLEX, reconstruction_field=reconstruction, reconstruction_representation=FieldRepresentation.COMPLEX )] writer.save(samples, output_dir=Path("output"))
This section provides visual examples demonstrating the differences between field representations and object types.
To generate visual comparison images, install matplotlib and run the provided script:
pip install matplotlib python scripts/generate_visual_examples.py
This will create comparison images in docs/examples/complex_fields/ showing:
- Amplitude-only vs phase-only objects
- Intensity vs complex field representations
- Complete hologram generation and reconstruction pipeline
For a phase-only circular object, the four representations show:
Intensity (|E|2):
- Uniform brightness (no contrast!)
- Phase information is lost
- Cannot distinguish object from background
- This is what a camera would record
Amplitude (|E|):
- Uniform amplitude (value = 1.0 everywhere)
- Still no contrast for phase-only objects
- Square root of intensity
Phase (arg(E)):
- Clear circular pattern visible
- Phase shift of π/2 inside circle, 0 outside
- Values range from -π to π radians
- Contains the actual object information
Complex (Real + Imaginary):
- Complete field information preserved
- Can be visualized as amplitude with phase color overlay
- Enables full wave optics processing
- Required for physics-aware ML models
An amplitude-only object modulates light intensity through absorption:
- Intensity: Dark circle on bright background (high contrast)
- Amplitude: Smooth transition from 0 (inside) to 1 (outside)
- Phase: Uniform zero everywhere (no phase modulation)
- Complex: Real-valued field (imaginary part is zero)
Physical example: Stained biological sample, printed pattern, metal particle
A phase-only object modulates light phase without absorption:
- Intensity: Uniform brightness (NO contrast - invisible!)
- Amplitude: Uniform value of 1.0 everywhere
- Phase: Step function (0 outside, π/2 inside circle)
- Complex: Pure phase modulation (|E| = 1)
Physical example: Unstained biological cell, phase mask, transparent polymer
For phase-only objects, intensity-based imaging provides no contrast:
Phase-only object: E = exp(iφ)
Intensity: I = |E|2 = |exp(iφ)|2 = 1 (uniform!)
The object is invisible in intensity! However, after propagation through holography:
After propagation: E' = F−1[F[E] ×ばつ H]
Intensity: I' = |E'|2 (now shows interference pattern)
The hologram intensity shows interference fringes that encode the phase information.
A typical hologram generation pipeline for a phase-only object:
-
Object Field: Phase-only circle (uniform amplitude, varying phase)
- Amplitude: 1.0 everywhere
- Phase: 0 outside, π/2 inside
-
Hologram Field: After propagation to sensor plane
- Amplitude: Interference pattern (fringes visible)
- Phase: Complex phase distribution
- Intensity: Shows characteristic hologram pattern
-
Reconstruction Field: After back-propagation
- Amplitude: Recovered (close to 1.0)
- Phase: Recovered (close to original 0 and π/2)
- Quality depends on propagation distance and noise
Intensity-Only Workflow (Legacy):
Phase Object → [Propagate] → Hologram Intensity → [ML Model] → Reconstruction
↑
Phase information lost here!
Complex Field Workflow (New):
Phase Object → [Propagate] → Complex Hologram → [ML Model] → Complex Reconstruction
↑
Full field information preserved!
-
Phase-only objects are invisible in intensity: You need holographic propagation to create contrast
-
Complex fields preserve all information: Both amplitude and phase are available for processing
-
Hologram patterns differ by object type:
- Amplitude objects: Fresnel diffraction pattern
- Phase objects: Interference fringes
- Mixed objects: Combination of both
-
Reconstruction quality: Complex field reconstruction can recover both amplitude and phase, while intensity-only reconstruction can only recover intensity
For intensity-only models:
- Can learn hologram → intensity reconstruction
- Cannot recover phase information
- Limited to amplitude/intensity objects
- Simpler data format (1 channel)
For complex field models:
- Can learn hologram → full field reconstruction
- Can recover both amplitude and phase
- Works with all object types
- Richer data format (2 channels: real + imaginary)
- Enables physics-informed architectures
The generate_visual_examples.py script creates the following comparison images:
- amplitude_only_object.png: Shows all four representations of an absorbing circle
- phase_only_object.png: Shows all four representations of a transparent circle
- example_object.png: Phase-only object field
- example_hologram.png: Hologram field after propagation
- example_reconstruction.png: Reconstructed field after back-propagation
- intensity_vs_complex_comparison.png: Side-by-side comparison showing why complex fields matter
These images demonstrate:
- Why phase-only objects need complex field support
- How information is preserved through the pipeline
- The difference between intensity-only and complex representations
- What ML models can learn from each representation type
For phase visualization, common colormaps include:
- twilight: Cyclic colormap, good for phase (default)
- hsv: Classic phase colormap (hue = phase)
- twilight_shifted: Shifted version of twilight
- cyclic: Generic cyclic colormap
Example of setting colormap:
writer = ComplexFieldWriter(phase_colormap="twilight")
Or for custom visualization:
import matplotlib.pyplot as plt phase = np.angle(field) plt.imshow(phase, cmap='twilight', vmin=-np.pi, vmax=np.pi) plt.colorbar(label='Phase (radians)')