Fields#

The Field class is the central object in TorchOptics: a complex-valued wavefront sampled on a 2D planar grid at a position along the optical axis.

Creating a Field#

Construct a Field from a 2D complex tensor. If spacing or wavelength are omitted, the global defaults are used (see Configuration).

import torch
import torchoptics
from torchoptics import Field
from torchoptics.profiles import gaussian, circle

torchoptics.set_default_spacing(10e-6)
torchoptics.set_default_wavelength(700e-9)

field = Field(circle(300, radius=1e-3))
field.visualize(title="Circular Aperture")
print(field)
../_images/fields-1.png
Field(shape=(300, 300), z=0.00e+00, spacing=(1.00e-05, 1.00e-05), offset=(0.00e+00, 0.00e+00), wavelength=7.00e-07)

The data tensor must have at least 2 dimensions (H × W). Leading dimensions are treated as batch dimensions. Fields can be created from profiles functions (see Profiles) or from arbitrary tensors:

gaussian_field = Field(gaussian(300, waist_radius=500e-6))
gaussian_field.visualize(title="Gaussian Beam")
../_images/fields-2.png

Grid Geometry#

Every Field inherits from PlanarGrid, which defines its spatial layout through four properties:

Property

Type

Description

shape

(int, int)

Number of grid points (H, W).

spacing

Tensor

Physical distance between adjacent grid points (m). Can differ along the two axes.

offset

Tensor

The (y, x) coordinates of the grid center (m). Default: (0, 0).

z

Tensor

Position along the optical axis (m). Default: 0.

Retrieve spatial coordinates and bounds:

x, y = field.meshgrid()     # 2D coordinate arrays
bounds = field.bounds()     # [y_min, y_max, x_min, x_max]
length = field.length()     # Physical extent [Ly, Lx]

Propagation#

Three methods handle free-space propagation (see Propagation for the underlying algorithms):

propagate_to_z() — propagate to a new z while preserving grid geometry:

propagated = field.propagate_to_z(0.5)
propagated.visualize(title="Propagated to z = 0.5 m")
../_images/fields-3.png

propagate() — full control over the output grid:

output = field.propagate(shape=(512, 512), z=1.0, spacing=5e-6, offset=(100e-6, 0))

propagate_to_plane() — propagate to a PlanarGrid or element:

from torchoptics import PlanarGrid

target = PlanarGrid(shape=400, z=0.3, spacing=8e-6)
output = field.propagate_to_plane(target)

All three accept optional propagation_method, asm_pad, and interpolation_mode keyword arguments.

Analysis#

Method

Description

intensity()

Squared magnitude \(|\psi|^2\).

power()

Integrated intensity: \(P = \sum I_{ij}\,\Delta A\).

centroid()

Intensity-weighted center of mass \((\bar{y}, \bar{x})\).

std()

Intensity-weighted standard deviation along each axis.

g = Field(gaussian(300, waist_radius=500e-6, offset=(200e-6, 300e-6)))
print(f"Power:    {g.power().item():.4e}")
print(f"Centroid: ({g.centroid()[0].item():.2e}, {g.centroid()[1].item():.2e})")
print(f"Std:      ({g.std()[0].item():.2e}, {g.std()[1].item():.2e})")
Power:    1.0000e+00
Centroid: (2.00e-04, 3.00e-04)
Std:      (2.50e-04, 2.50e-04)

Inner Product#

The overlap integral between two fields is:

\[\eta = \sum_{i,j} \psi_1(i,j) \, \psi_2^*(i,j) \, \Delta A\]

inner() returns this as a complex scalar, where \(\psi_1\) is self and \(\psi_2\) is the argument. Taking the squared magnitude gives the mode overlap \(|\eta|^2\): a value in \([0, 1]\) when both fields are normalized to unit power (by Cauchy–Schwarz), and a natural fidelity metric for inverse design (see Inverse Design):

overlap = field_a.inner(field_b).abs().square()  # |η|² in [0, 1] if both normalized
loss = 1 - overlap

Both fields must share the same geometry (shape, spacing, offset, and z).

Modulation#

Point-wise complex multiplication:

\[\psi'(x, y) = \mathcal{M}(x, y) \cdot \psi(x, y)\]

This is the mechanism by which elements transform fields, and can be used to apply arbitrary complex-valued masks directly. The profile may be real (amplitude mask) or complex (phase and amplitude):

# Complex phase mask
profile = torch.exp(1j * torch.randn(300, 300, dtype=torch.double))
modulated = field.modulate(profile)

# Real amplitude mask
from torchoptics.profiles import circle
apertured = field.modulate(circle(300, radius=1e-3))

Normalization and Copying#

normalized = field.normalize()           # Scale to unit power
scaled = field.normalize(2.5)            # Scale to power = 2.5
shifted = field.copy(z=0.5)              # Copy with updated z
rescaled = field.copy(spacing=5e-6)      # Copy with new spacing

Updating Properties#

All registered properties can be updated by direct assignment. Assignments are validated automatically; invalid values raise errors immediately.

field.z = 0.5
field.wavelength = 532e-9
field.spacing = (8e-6, 8e-6)
field.offset = (100e-6, 0)

Element properties work the same way.

Visualization#

visualize() displays intensity and phase for complex fields:

from torchoptics.profiles import laguerre_gaussian

lg = Field(laguerre_gaussian(300, p=1, l=2, waist_radius=500e-6))
lg.visualize(title="LG$_{1}^{2}$ Mode")
../_images/fields-5.png

The standalone visualize_tensor() and animate_tensor() functions work with arbitrary 2D and 3D tensors respectively.

Batched Fields#

The data tensor supports batch dimensions with shape (..., H, W):

batch_data = torch.randn(4, 300, 300, dtype=torch.cdouble)
batch_field = Field(batch_data)

propagated = batch_field.propagate_to_z(0.5)
print(propagated.data.shape)  # torch.Size([4, 300, 300])