-
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|² = A²
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 (magnitude²) | 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×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π/λ) × (n₁ - n₀) × d
Where:
- λ = wavelength
- n₁ = refractive index of object
- n₀ = refractive index of background (typically 1.0 for air)
- d = object thickness
Example: For a biological cell in water:
- λ = 532 nm (green laser)
- n₁ = 1.38 (cell cytoplasm)
- n₀ = 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 degreesAll 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}")
breakimport 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}")
breakimport 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.05This 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.5708Use 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.5708Use 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 complexUse 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.5708Use 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.14159Use 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.7854Use 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 amplitudeUse 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.5708Use 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 12Note: 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.8Combine 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.3Use 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 --helpThis 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|² - squared magnitude
AMPLITUDE = auto() # |E| - magnitude
PHASE = auto() # arg(E) - phase angle in radians
COMPLEX = auto() # E = real + i*imag - full complex fieldUsage:
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: FieldRepresentationUsage:
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: FieldRepresentationUsage:
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.INTENSITYUsage:
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."""
passGenerate 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.pixelsProtocol 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 arraysfrom 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.pyThis 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|²):
- 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|² = |exp(iφ)|² = 1 (uniform!)
The object is invisible in intensity! However, after propagation through holography:
After propagation: E' = F⁻¹[F[E] × H]
Intensity: I' = |E'|² (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)')