Beams and imported sources¶
Excitations built from phased source planes: focused Gaussian beams, thin-lens fields, and fields imported from another calculation.
Gaussian and other scalar beams¶
gaussian_beam
¶
Gaussian-beam excitation source, a free-space / lensed-fibre beam launched as a per-cell source-current (Huygens) source plane.
This is the excitation twin of gaussian_mode
(which builds an analysis-side Gaussian for a coupling-efficiency overlap).
Where that one is a scalar profile on its own grid, gaussian_beam builds
the full-vector paraxial beam on the simulation's own Yee-staggered injection
plane, E and H sampled at their true intra-cell locations, with the beam's
complex phase, so it drops straight into
equivalence_current_source and
launches one-sided (forward only) exactly like a solved waveguide mode does.
Why the Huygens source plane and not a ModeSource:
a Gaussian beam is only real (flat-phase) at its waist and at normal incidence.
Move the waist off the injection plane, or tilt the beam, and the transverse
profile picks up the wavefront-curvature, Gouy and transverse-k phases, which
the wire (real signed profile) cannot carry. The eq-current source plane stamps
one PointDipole per cell per component with
its own amplitude AND phase, so an arbitrary complex profile is exact on the
existing wire and engine, no schema change, CPU and GPU alike.
The beam¶
Fundamental (TEM₀₀) Gaussian, generally elliptical. Per transverse axis j, with
field 1/e radius w0ⱼ at the waist and Rayleigh range zRⱼ = π n w0ⱼ²/λ::
wⱼ(z) = w0ⱼ √(1 + (z/zRⱼ)²) spot growth
1/Rⱼ(z) = z / (z² + zRⱼ²) wavefront curvature (0 at the waist)
ψⱼ(z) = atan(z / zRⱼ) Gouy phase
E(ρ₁, ρ₂) = √(w0₁w0₂ / w₁w₂) · exp(-ρ₁²/w₁² - ρ₂²/w₂²)
· exp(-i[ k·(r·k̂) + k(ρ₁²/2R₁ + ρ₂²/2R₂) - (ψ₁+ψ₂)/2 ])
evaluated at the beam-frame coordinates of each Yee point on the injection
plane, with k = 2πn/λ. The paired magnetic field is the exact plane-wave
pairing about the beam axis, H = (n/η₀) k̂ × E, which is the correct paired
H to paraxial order (the neglected term is O(1/(k w₀)²), 3e-4 at the NA ≈ 0.06
of a lensed-fibre facet). Because E and H are supplied as a consistent
Huygens pair, the source plane radiates FORWARD only; the backward residual is the
paraxial error, not a launch artifact.
Phasor sign. The exp(-i…) above is the source plane's
convention: the builder drives every dipole as cos(ωt + arg A), so the
field it realizes is Re{A e^{+iωt}} and a forward-travelling wave carries
e^{-i k·r}. RECORDED field_dft phasors run the other way (e^{-iωt}:
a forward wave there is e^{+i k·r}). gaussian_beam returns the beam
in the RECORDED convention, the conjugate of the formula above, so it compares
directly with monitor data: as the reference of a
mode_monitor, in
mode_overlap, or against a recorded
plane by hand. gaussian_beam_source conjugates it back for the source plane.
The distinction is invisible for a real profile (a beam at its waist at normal
incidence); for a tilted or off-waist beam it decides which way a tilt steers
and whether an offset waist focuses. Verified on the engine; see
test_phasor_convention.py and
test_gaussian_beam.py::test_offset_waist_focuses_inside_the_domain.
Off-normal injection tilts the whole beam frame: β = k cos θ (carried as the
mode's n_eff = n cos θ, which is what phases the source plane's half-cell straddle)
and the transverse k shows up as the e^{-i k (r·k̂)} ramp above, whose
angular-spectrum centroid is exactly k sin θ. The beam's elliptical axes and
its transverse coordinates are measured in the plane perpendicular to k̂, not
on the (tilted) injection plane, so a tilted beam is sampled correctly rather
than merely phase-ramped.
Note
A beam is only as paraxial as w₀/λ makes it. At w₀ ≈ 0.8 λ the
textbook w(z) under-predicts the real (exact-diffraction) spot by ~7% one
Rayleigh range out, and a tilted beam's amplitude centroid walks at
⟨kₓ/k_z⟩, noticeably faster than tan θ. Both are properties of a
tightly-focused Gaussian, not of this launch, compare against exact
angular-spectrum propagation, not against the paraxial formulas, when
validating at small w₀/λ.
Usage¶
::
from photonhub.analysis import gaussian_beam_source
sim = sim.model_copy(update={"sources": gaussian_beam_source(
geometry_sim, axis="x", position_um=2.0, source_time=pulse,
mfd_um=10.4, # SMF-28 at 1550 nm
polarization="Ez", n=1.45, power_watts=1.0)})
gaussian_beam alone returns the beam as a
VectorMode in the recorded
e^{-iωt} convention, which is also what you want as the reference mode of
a mode_monitor for a chip-to-fibre
coupling readout.
gaussian_beam
¶
gaussian_beam(
sim,
*,
axis: str,
waist_um: Optional[
Union[float, Sequence[float]]
] = None,
mfd_um: Optional[Union[float, Sequence[float]]] = None,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
source_time=None,
center_um: Optional[Tuple[float, float]] = None,
n: Optional[float] = None,
polarization: Optional[str] = None,
pol_angle_rad: Optional[float] = None,
waist_distance_um: float = 0.0,
angle_theta_rad: float = 0.0,
angle_phi_rad: float = 0.0,
direction: str = "+",
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
window_sigmas: float = 3.0,
) -> VectorMode
The analytic fundamental-Gaussian beam on sim's axis-normal Yee
plane, as a full-vector VectorMode.
Launch it with gaussian_beam_source (which is this call plus the
Huygens source plane in one step); use it directly when you want the beam object
itself, e.g. as the reference of a
mode_monitor for a chip-to-fibre
coupling readout, or of mode_overlap.
Parameters¶
sim:
The simulation whose grid, size, and symmetry the beam is sampled
on. A cheap placeholder geometry-only simulation (same grid/size/symmetry) is fine.
axis:
Propagation axis, "x"/"y"/"z", the injection plane's normal.
waist_um, mfd_um:
Beam size, exactly one of: waist_um = the field 1/e radius w₀;
mfd_um = the 1/e² intensity mode-field DIAMETER vendors quote
(w₀ = MFD/2). Scalar for a round beam, (along h, along v) for an
elliptical one (a lensed fibre), where (h, v) are the two in-plane
axes in ascending order, (y, z) for an x-cut, (x, z) for a
y-cut, (x, y) for a z-cut.
wlen_um, freq_hz, source_time:
The frequency the beam is built at, at most one of the first two; with
neither, it is taken from source_time.freq0_hz (the pulse centre).
Only the phase terms are wavelength-dependent, at the waist, at normal
incidence, the Gaussian's SHAPE is wavelength-independent.
center_um:
Transverse beam centre as (h, v) in the in-plane-axis order above.
Default: the domain centre. Under a symmetry plane the folded axis'
centre is 0 (the plane sits on the domain min face).
n:
Refractive index of the medium the beam propagates in. Default:
sqrt(sim.background.permittivity), right for a beam launched in an
unpatterned background (air n=1, an oxide cladding n≈1.45). Pass
it explicitly if the launch plane sits in a different homogeneous medium.
polarization, pol_angle_rad:
Linear polarization, at most one of: polarization names an in-plane
E component ("Ez", or bare "z"); pol_angle_rad is the angle in
radians from the first in-plane axis toward the second. Default: E along
the first in-plane axis.
waist_distance_um:
Signed distance from the beam WAIST to the injection plane, along the
propagation direction. 0 (default) launches the beam at its waist
(flat phase). Positive puts the waist BEHIND the plane, so the beam
is already diverging when injected; negative puts it ahead, so the
beam converges to its waist |waist_distance_um| into the domain.
angle_theta_rad, angle_phi_rad:
Off-normal injection (radians). angle_theta_rad tilts the beam off the
propagation direction; angle_phi_rad is the azimuth of that tilt in the
transverse plane, measured from the first in-plane axis. Both default to
0 (normal incidence). The tilt is applied about the propagation
direction implied by direction, so angle_theta_rad always means "off
the launch direction".
direction:
"+" (default) launches toward increasing axis, "-" toward
decreasing.
half_w_um, half_v_um, window_sigmas:
Half-extents of the sampled window. Default: window_sigmas (3) times
the beam's 1/e field radius ON the plane, i.e. the field is down to
e⁻⁹ at the window edge, so truncation costs ~1e-8 of the power.
Widen it (or set the halves explicitly) if you deliberately clip the
beam; the window is clipped to the domain and to any symmetry plane.
Returns¶
VectorMode
In the RECORDED e^{-iωt} phasor convention of monitor data (a beam
tilted toward +h carries e^{+i k_h h}), so it is directly an
overlap or mode-monitor reference; gaussian_beam_source
conjugates it for the source plane. yee_staggered, with the six field
components sampled at their true
in-plane Yee locations over the window, the transverse-E pair jointly
L2-normalized (all six scaled together, so E/H stay a consistent
Huygens pair), n_eff = n cos(angle_theta_rad) (the phase constant along
axis, which is what phases the launch), and center_offset_um
recording the window's grid snap.
Notes¶
The profile is generally COMPLEX. Launch it through gaussian_beam_source,
which conjugates it for the source plane and carries per-cell phase. To hand it to
equivalence_current_source
directly (a per-frequency mode mapping, say), pass conjugate_fields(beam)
(from photonhub.analysis.gaussian_beam import conjugate_fields): the
source plane stamps e^{+iωt} phasors. The aux-line path
(mode_source) keeps only the real part
and would silently mis-launch anything but an at-waist, normal-incidence beam.
conjugate_fields
¶
conjugate_fields(mode: VectorMode) -> VectorMode
The mode with all six field components conjugated.
This is the switch between the recorded e^{-iωt} phasor convention of
gaussian_beam, import_field
and thin_lens_beam modes (and of
monitor data) and the e^{+iωt} one
equivalence_current_source
stamps. Pass a beam or imported mode through it before handing it to that
source builder directly; the *_source launchers already do. It is exact,
so converting twice returns the original bits. Import it as
from photonhub.analysis.gaussian_beam import conjugate_fields: in
photonhub.analysis, gaussian_beam is the function, not this
module.
Only for beam and imported-field modes, whose n_eff is real: it
leaves n_eff, k_eff and every other field alone, so it is not a
convention switch for a complex (lossy, bend or PML) solver mode.
scalar_beam
¶
scalar_beam(
sim,
*,
axis: str,
profile,
half_w_um: float,
half_v_um: float,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
source_time=None,
center_um: Optional[Tuple[float, float]] = None,
n: Optional[float] = None,
polarization: Optional[str] = None,
pol_angle_rad: Optional[float] = None,
direction: str = "+",
) -> VectorMode
A beam with any scalar transverse profile on sim's axis-normal
Yee plane, as a VectorMode for
equivalence_current_source.
profile(dh, dv) returns the complex scalar field ON the injection plane
at in-plane offsets (dh, dv) from center_um (arrays of one shape, in
µm), in the source plane's e^{+iωt} phasor convention (a flat phase for a beam
at its waist; a field that converges into the domain has already been
propagated back to the plane by the caller). The fibre mode a chip is
coupled from, a top-hat, a measured near field: anything the Gaussian of
gaussian_beam is not. The beam is linearly polarized at normal
incidence and the paired H comes from the scalar-limit admittance n/η₀,
exact for a plane wave and right to (λ/πw)² for a beam w wide.
The returned mode stays in the source plane's e^{+iωt} convention, because it
is built to be handed to
equivalence_current_source
directly. That is the opposite of gaussian_beam, whose mode is in
the recorded e^{-iωt} convention of monitor data: conjugate this one's
fields before using it as an overlap or mode-monitor reference.
half_w_um/half_v_um are the window's half-extents and have no
default: the profile's reach is the caller's to know. The window is clipped
to the domain and to a symmetry plane as the Gaussian's is. The other
arguments are as for gaussian_beam.
gaussian_beam_source
¶
gaussian_beam_source(
sim,
*,
axis: str,
position_um: float,
source_time,
waist_um: Optional[
Union[float, Sequence[float]]
] = None,
mfd_um: Optional[Union[float, Sequence[float]]] = None,
direction: str = "+",
power_watts: float = 1.0,
center_um: Optional[Tuple[float, float]] = None,
n: Optional[float] = None,
polarization: Optional[str] = None,
pol_angle_rad: Optional[float] = None,
waist_distance_um: float = 0.0,
angle_theta_rad: float = 0.0,
angle_phi_rad: float = 0.0,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
freqs_hz: Optional[Sequence[float]] = None,
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
window_sigmas: float = 3.0,
amplitude_threshold: float = 1e-06,
) -> List[PointDipole]
Launch a Gaussian beam. Return a list
of PointDipole sources to put in
Simulation.sources.
The beam (gaussian_beam, whose parameters this shares) is injected as
a plane of phased dipoles
(equivalence_current_source):
J = n̂ × H on the E plane at position_um and M = -n̂ × E on the H
nodes half a cell upstream, each dipole carrying the beam's own complex
amplitude and phase. That is what lets an offset waist or an off-normal beam
be exact rather than approximated, and it makes the launch one-sided
(forward) with no TF/SF plane to keep clear of structures.
power_watts (default 1 W) is the beam power through the injection plane,
normalized on the engine's own discrete Poynting quadrature over the
dipoles actually stamped, so a full-plane PowerMonitor below the
source plane reads it back in watts and a
transmission monitor reads an absolute fraction of the launch. Under symmetry planes it is the power of the whole, unfolded device. A beam
centered on k planes puts power_watts / 2^k into the modeled part.
A full-plane PowerMonitor reports power_watts.
On a one-cell periodic
(quasi-2-D) axis it is the power through that one cell's width;
transmission ratios are normalization-invariant either way.
Extra parameters beyond gaussian_beam¶
position_um:
Where the injection plane sits along axis. Keep it clear of the PML.
source_time:
The shared GaussianPulse; its
phase is overridden per dipole (that is where the beam profile's
phase goes), and its freq0_hz sets the beam's frequency unless
wlen_um/freq_hz says otherwise.
freqs_hz:
Optional broadband launch: build one beam and one dipole source plane
per frequency. They are driven by partition-of-unity
windowed carriers that sum back to the source pulse. Worth it only when
the beam's profile actually moves across the band, i.e. an offset waist,
an off-normal beam, or a dispersive n; at the waist at normal
incidence the Gaussian's shape is wavelength-independent and the extra
source planes buy nothing. None (default) or a single entry launches the
single band-centre beam.
amplitude_threshold:
Finite fraction in [0, 1), relative to each field's own peak (default
1e-6). At each sample, keep the electric and magnetic currents together
when either field reaches the threshold. Power is normalized on the
kept samples.
gaussian_beam_source
¶
gaussian_beam_source(
sim,
*,
axis: str,
position_um: float,
source_time,
waist_um: Optional[
Union[float, Sequence[float]]
] = None,
mfd_um: Optional[Union[float, Sequence[float]]] = None,
direction: str = "+",
power_watts: float = 1.0,
center_um: Optional[Tuple[float, float]] = None,
n: Optional[float] = None,
polarization: Optional[str] = None,
pol_angle_rad: Optional[float] = None,
waist_distance_um: float = 0.0,
angle_theta_rad: float = 0.0,
angle_phi_rad: float = 0.0,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
freqs_hz: Optional[Sequence[float]] = None,
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
window_sigmas: float = 3.0,
amplitude_threshold: float = 1e-06,
) -> List[PointDipole]
Launch a Gaussian beam. Return a list
of PointDipole sources to put in
Simulation.sources.
The beam (gaussian_beam, whose parameters this shares) is injected as
a plane of phased dipoles
(equivalence_current_source):
J = n̂ × H on the E plane at position_um and M = -n̂ × E on the H
nodes half a cell upstream, each dipole carrying the beam's own complex
amplitude and phase. That is what lets an offset waist or an off-normal beam
be exact rather than approximated, and it makes the launch one-sided
(forward) with no TF/SF plane to keep clear of structures.
power_watts (default 1 W) is the beam power through the injection plane,
normalized on the engine's own discrete Poynting quadrature over the
dipoles actually stamped, so a full-plane PowerMonitor below the
source plane reads it back in watts and a
transmission monitor reads an absolute fraction of the launch. Under symmetry planes it is the power of the whole, unfolded device. A beam
centered on k planes puts power_watts / 2^k into the modeled part.
A full-plane PowerMonitor reports power_watts.
On a one-cell periodic
(quasi-2-D) axis it is the power through that one cell's width;
transmission ratios are normalization-invariant either way.
Extra parameters beyond gaussian_beam¶
position_um:
Where the injection plane sits along axis. Keep it clear of the PML.
source_time:
The shared GaussianPulse; its
phase is overridden per dipole (that is where the beam profile's
phase goes), and its freq0_hz sets the beam's frequency unless
wlen_um/freq_hz says otherwise.
freqs_hz:
Optional broadband launch: build one beam and one dipole source plane
per frequency. They are driven by partition-of-unity
windowed carriers that sum back to the source pulse. Worth it only when
the beam's profile actually moves across the band, i.e. an offset waist,
an off-normal beam, or a dispersive n; at the waist at normal
incidence the Gaussian's shape is wavelength-independent and the extra
source planes buy nothing. None (default) or a single entry launches the
single band-centre beam.
amplitude_threshold:
Finite fraction in [0, 1), relative to each field's own peak (default
1e-6). At each sample, keep the electric and magnetic currents together
when either field reaches the threshold. Power is normalized on the
kept samples.
scalar_beam
¶
scalar_beam(
sim,
*,
axis: str,
profile,
half_w_um: float,
half_v_um: float,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
source_time=None,
center_um: Optional[Tuple[float, float]] = None,
n: Optional[float] = None,
polarization: Optional[str] = None,
pol_angle_rad: Optional[float] = None,
direction: str = "+",
) -> VectorMode
A beam with any scalar transverse profile on sim's axis-normal
Yee plane, as a VectorMode for
equivalence_current_source.
profile(dh, dv) returns the complex scalar field ON the injection plane
at in-plane offsets (dh, dv) from center_um (arrays of one shape, in
µm), in the source plane's e^{+iωt} phasor convention (a flat phase for a beam
at its waist; a field that converges into the domain has already been
propagated back to the plane by the caller). The fibre mode a chip is
coupled from, a top-hat, a measured near field: anything the Gaussian of
gaussian_beam is not. The beam is linearly polarized at normal
incidence and the paired H comes from the scalar-limit admittance n/η₀,
exact for a plane wave and right to (λ/πw)² for a beam w wide.
The returned mode stays in the source plane's e^{+iωt} convention, because it
is built to be handed to
equivalence_current_source
directly. That is the opposite of gaussian_beam, whose mode is in
the recorded e^{-iωt} convention of monitor data: conjugate this one's
fields before using it as an overlap or mode-monitor reference.
half_w_um/half_v_um are the window's half-extents and have no
default: the profile's reach is the caller's to know. The window is clipped
to the domain and to a symmetry plane as the Gaussian's is. The other
arguments are as for gaussian_beam.
Thin lens¶
thin_lens_beam
¶
thin_lens_beam(
sim,
*,
axis: str,
na: float,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
source_time=None,
n: Optional[float] = None,
center_um: Optional[Tuple[float, float]] = None,
polarization: Optional[str] = None,
pol_angle_rad: Optional[float] = None,
focus_distance_um: float = 0.0,
pupil=None,
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
window_airy_units: float = 12.0,
) -> VectorMode
The Richards–Wolf focused beam on sim's axis-normal Yee plane.
Parameters¶
sim:
Simulation whose grid, size and symmetry the beam is sampled on (a
placeholder geometry-only simulation is fine).
axis:
Propagation axis ('x' | 'y' | 'z'), the injection plane's normal.
na:
Numerical aperture n sin(theta_max) of the focusing cone, in the
LAUNCH medium (0 < na < n).
wlen_um, freq_hz, source_time:
Frequency the beam is built at (at most one of the first two; else
source_time.freq0_hz).
n:
Launch-medium index. Default sqrt(sim.background.permittivity).
center_um:
Transverse focus centre (h, v) in the in-plane-axis order;
default the domain centre (0, the symmetry plane, on an axis with one).
polarization, pol_angle_rad:
Incident linear polarization before the lens (component name or
angle from the first in-plane axis). Default: along the first
in-plane axis.
focus_distance_um:
Distance from the injection plane to the focus, along propagation
(>= 0; 0 = focus on the plane).
pupil:
Optional complex pupil function pupil(theta, phi) -> complex
(vectorized over numpy arrays), multiplied onto the aplanatic
sqrt(cos theta). Default: uniform (clipped Airy-type focus).
half_w_um, half_v_um, window_airy_units:
Sampled-window half-extents. Default: window_airy_units (12)
Airy radii 0.61 lambda / NA, wide enough that the discarded
tail carries ~<1e-3 of the power for a uniform pupil, widened by
the defocus cone focus_distance * tan(theta_max), clipped to
the domain.
Returns¶
VectorMode
In the recorded e^{-i omega t} convention of monitor data, so it
is directly a mode-monitor or overlap reference (for
equivalence_current_source
pass conjugate_fields of it, imported as
from photonhub.analysis.gaussian_beam import conjugate_fields;
thin_lens_source does). yee_staggered; six components
sampled at their true in-plane Yee locations;
n_eff = n <cos theta> (power-weighted).
thin_lens_source
¶
thin_lens_source(
sim,
*,
axis: str,
position_um: float,
source_time,
na: float,
direction: str = "+",
power_watts: float = 1.0,
n: Optional[float] = None,
center_um: Optional[Tuple[float, float]] = None,
polarization: Optional[str] = None,
pol_angle_rad: Optional[float] = None,
focus_distance_um: float = 0.0,
pupil=None,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
window_airy_units: float = 12.0,
amplitude_threshold: float = 1e-06,
) -> List[PointDipole]
Launch a high-NA focused beam, thin_lens_beam plus the
per-cell source-current Huygens source plane. power_watts (default 1 W)
normalizes the launched power on the engine's discrete Poynting
quadrature like every other launch, as the whole, unfolded device's
power (a beam centred on k symmetry planes puts power_watts / 2^k
into the modeled part); parameters match
thin_lens_beam / gaussian_beam_source.
The beam converges onto focus_distance_um past the source plane: its
recorded-convention mode is conjugated once for the source plane, which stamps
e^{+i omega t} phasors. amplitude_threshold compares each field
with its own peak (default fraction 1e-6). Electric and magnetic currents
at a sample are kept or dropped together, and power is normalized on the
kept samples.
Imported fields¶
import_field
¶
import_field(
sim,
*,
axis: str,
e_h,
e_v,
coords_h_um,
coords_v_um,
h_h=None,
h_v=None,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
source_time=None,
n: Optional[float] = None,
n_eff: Optional[float] = None,
center_um: Optional[Tuple[float, float]] = None,
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
) -> VectorMode
Resample a user field map onto the axis-normal Yee plane.
Parameters¶
sim:
The simulation whose grid, size and symmetry the profile is sampled on
(a placeholder geometry-only simulation with the same grid is fine).
axis:
The injection plane's normal, "x"/"y"/"z".
e_h, e_v:
Complex transverse E components along the plane's (h, v) in-plane
axes, shape (len(coords_v_um), len(coords_h_um)).
coords_h_um, coords_v_um:
Strictly-ascending sample coordinates of the arrays, RELATIVE to
center_um.
h_h, h_v:
Optional transverse H (same shape/grid). Give both or neither;
omitted -> forward quasi-plane-wave fill (module docstring).
wlen_um, freq_hz, source_time:
The frequency the profile is phased at (at most one of the first
two; else source_time.freq0_hz).
n:
Index of the launch medium. Default
sqrt(sim.background.permittivity).
n_eff:
Effective longitudinal phase index used by the source plane's half-cell
straddle (a beam tilted by theta has n cos(theta)). Default: n.
center_um:
Transverse placement of the profile origin as (h, v); default the
domain centre (0 on a folded axis).
half_w_um, half_v_um:
Half-extents of the sampled window; default the data's own extent,
clipped to the domain.
Returns¶
VectorMode
In the recorded e^{-i w t} convention of the input arrays.
yee_staggered, six components on the window (e_a = h_a = 0),
transverse-E jointly L2-normalized, ready for
mode_monitor or overlaps.
To hand it to
equivalence_current_source
directly, pass conjugate_fields of it
(from photonhub.analysis.gaussian_beam import conjugate_fields; import_source does).
import_source
¶
Import source, launch a user-supplied field profile as a Huygens source plane.
Any transverse field map, from another solver, an analytic model, a
measurement, or a previous run, becomes an excitation by
resampling it onto the simulation's Yee injection plane and stamping the
per-cell source plane
(equivalence_current_source,
J = n̂ × H, M = -n̂ × E), the same machinery behind
mode_launch and
gaussian_beam_source.
Two layers, mirroring the Gaussian-beam plugin:
import_field, resample the user arrays onto the plane's true Yee sample locations and package them as ayee_staggeredVectorMode(also usable as a monitor/overlap reference);import_source, that plus the source plane, returning thePointDipolelist forSimulation.sources.
Field conventions¶
Arrays are indexed [iv, ih] over the plane's natural in-plane axes
(h, v) (x-cut -> (y, z), y-cut -> (x, z), z-cut -> (x, y)), on the
caller's own rectilinear grid coords_h_um x coords_v_um measured
RELATIVE to center_um (so a profile exported centred on 0 drops in
unchanged). Values are complex phasors in the engine's e^{-i w t}
convention, the one monitor data is recorded in (a field tilted toward +h
carries e^{+i k_h h}); points outside the supplied grid are taken as 0.
import_field returns the mode in that same convention, so it is
directly an overlap or mode-monitor reference; import_source
conjugates it for the source plane, which stamps e^{+i w t}
phasors.
H is optional: when only e_h/e_v are given, the paired magnetic
field is filled in the forward quasi-plane-wave limit
(h_h, h_v) = (n / eta0) (-e_v, +e_h), exact for a normal-incidence plane
wave, the scalar limit otherwise (same approximation the scalar mode stack
uses). Supply all four components for tilted/structured fields where the
true E/H relation matters; direction is applied by the source plane either way.
The transverse-E pair is jointly L2-normalized (all components scaled
together, keeping E/H a consistent Huygens pair); absolute launch strength is
set by power_watts at import_source (None keeps the imported
units).
What's not handled¶
- Broadband profile mode mappings, one profile, phased at the pulse centre. For
a per-frequency profile mode mapping call
equivalence_current_sourcedirectly withmodes_by_freq={freq: conjugate_fields(import_field(...))}(from photonhub.analysis.gaussian_beam import conjugate_fields: the source plane stampse^{+i w t}phasors). - Recorded-monitor import, a
field_dftplane stores its four tangential components on ONE base-index coordinate set while each component physically sits half a cell off it; feeding those arrays here treats them as co-located (a <= half-cell registration blur). A stagger-exactfrom_monitorimporter is a follow-up.
import_field
¶
import_field(
sim,
*,
axis: str,
e_h,
e_v,
coords_h_um,
coords_v_um,
h_h=None,
h_v=None,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
source_time=None,
n: Optional[float] = None,
n_eff: Optional[float] = None,
center_um: Optional[Tuple[float, float]] = None,
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
) -> VectorMode
Resample a user field map onto the axis-normal Yee plane.
Parameters¶
sim:
The simulation whose grid, size and symmetry the profile is sampled on
(a placeholder geometry-only simulation with the same grid is fine).
axis:
The injection plane's normal, "x"/"y"/"z".
e_h, e_v:
Complex transverse E components along the plane's (h, v) in-plane
axes, shape (len(coords_v_um), len(coords_h_um)).
coords_h_um, coords_v_um:
Strictly-ascending sample coordinates of the arrays, RELATIVE to
center_um.
h_h, h_v:
Optional transverse H (same shape/grid). Give both or neither;
omitted -> forward quasi-plane-wave fill (module docstring).
wlen_um, freq_hz, source_time:
The frequency the profile is phased at (at most one of the first
two; else source_time.freq0_hz).
n:
Index of the launch medium. Default
sqrt(sim.background.permittivity).
n_eff:
Effective longitudinal phase index used by the source plane's half-cell
straddle (a beam tilted by theta has n cos(theta)). Default: n.
center_um:
Transverse placement of the profile origin as (h, v); default the
domain centre (0 on a folded axis).
half_w_um, half_v_um:
Half-extents of the sampled window; default the data's own extent,
clipped to the domain.
Returns¶
VectorMode
In the recorded e^{-i w t} convention of the input arrays.
yee_staggered, six components on the window (e_a = h_a = 0),
transverse-E jointly L2-normalized, ready for
mode_monitor or overlaps.
To hand it to
equivalence_current_source
directly, pass conjugate_fields of it
(from photonhub.analysis.gaussian_beam import conjugate_fields; import_source does).
import_source
¶
import_source(
sim,
*,
axis: str,
position_um: float,
source_time,
e_h,
e_v,
coords_h_um,
coords_v_um,
h_h=None,
h_v=None,
direction: str = "+",
power_watts: Optional[float] = 1.0,
n: Optional[float] = None,
n_eff: Optional[float] = None,
wlen_um: Optional[float] = None,
freq_hz: Optional[float] = None,
center_um: Optional[Tuple[float, float]] = None,
half_w_um: Optional[float] = None,
half_v_um: Optional[float] = None,
amplitude_threshold: float = 1e-06,
) -> List[PointDipole]
Launch an imported field map, the one-call custom excitation.
import_field (whose parameters this shares) plus the Huygens
source plane. power_watts (default 1 W) normalizes the launched power on the
engine's discrete Poynting quadrature exactly like the mode and beam
launches: the whole, unfolded device's power, so a profile centred on k
symmetry planes puts power_watts / 2^k into the modeled part;
None keeps the imported amplitude scale. Returns the PointDipole list for
Simulation.sources. amplitude_threshold compares each field with
its own peak (default fraction 1e-6). Electric and magnetic currents at a
sample are kept or dropped together, and power is normalized on the kept
samples.
Source construction¶
equivalence_current_source
¶
equivalence_current_source(
sim,
mode,
*,
axis: str,
position_um: float,
source_time,
direction: str = "+",
h_center_um: float,
v_center_um: float,
half_w_um: float,
half_v_um: float,
power_watts: Optional[float] = 1.0,
amplitude_threshold: float = 1e-06,
extra_j_phase: float = 0.0,
_window_origin: Optional[Tuple[float, float]] = None,
modes_by_freq: Optional[Mapping[float, object]] = None,
_clip_to_interior: Tuple[bool, bool] = (False, False),
) -> List[PointDipole]
Build the phased-dipole Huygens source planes launching mode along axis.
mode must be the discrete Yee eigenmode from solve_yee_mode
solved with the SAME window arguments (h_center_um/v_center_um/
half_w_um/half_v_um/the simulation's dl), they are used here to
re-derive the window's grid registration exactly. source_time is the
shared GaussianPulse envelope (its phase is overridden per
dipole). power_watts scales the launched modal power (None = leave
the mode's own units): the forward Poynting power, on the engine's own
discrete quadrature, of the dipoles the source plane actually stamps. Window nodes
the domain clips (the interior margin of a quarter cell at each wall) are
left out of the normalization, so the placed source plane carries exactly
power_watts. A plain periodic in-plane axis has no such margin: its
window is the whole period and every column is stamped, the single
column of a one-cell (quasi-2-D) axis included. The normalization is the
power the Yee scheme carries through the planes downstream, the continuum
integral times cos(beta*dl/2) of the source plane's own cell,
so without symmetry a PowerMonitor or a port's mode_power downstream
reads power_watts back in watts. Under symmetry planes this builder is
the primitive: power_watts is the power into the modeled part, so a
source plane centered on k planes launches 2^k * power_watts into the whole
device, which is what the PowerMonitor and the port report. The
launches built on it (mode_launch, gaussian_beam_source,
import_source, thin_lens_source) take the whole device's power
and pass power_watts / 2^k here. amplitude_threshold compares
each field with its own peak; a sample is kept when either field reaches
that fraction, and its electric and magnetic currents are kept or dropped
together. Power is normalized on the kept samples. Entries outside the
domain are dropped.
Returns the list of PointDipole (electric + magnetic) to put in
Simulation.sources. Single-frequency profile (band-centre), like a
single-frequency (num_freqs=1) mode source.
Broadband (num_freqs analogue). Pass
modes_by_freq ({freq_hz: Yee mode}, N >= 2, each solved AT that
frequency with the SAME window arguments) to launch a BROADBAND mode: this
builds one dipole source plane per frequency, source plane k stamped from the mode at
freqs[k] (its own n_eff/fields drive the geometric + half-step
phases) and driven by the partition-of-unity WINDOWED carrier for that
frequency (source_time copied with band_freqs_hz=sorted_freqs,
carrier_index=k). Because the windows sum to 1, the source planes reconstruct
the full source pulse while the per-frequency mode dominates near its own
sample, the eq-current twin of the broadband ModeSource, now on the
graded grid too. Each source plane is normalized to the full power_watts (the
window, not an amplitude split, apportions the band, exactly like the broadband ModeSource). The
concatenation of all N source planes is returned. modes_by_freq None or with a
single entry falls through to the single-frequency path (mode is used),
bit-identical to the pre-broadband build.
Symmetry planes (half-domain sims) are honored automatically, matching
solve_yee_mode's window rule: the source plane is clipped at the plane,
ON-plane dipoles are kept at 1x amplitude (they are self-mirror, the
boundary supplies the image; the engine's own half==full tests use
unchanged amplitudes, and odd-parity components are exactly 0 there by the
mode's parity BC), and half-cell-offset dipoles get their mirror partner
from the boundary. power_watts is the modeled-part launch power.
Full-plane public flux and modal readouts return 2^k * power_watts for
k in-plane symmetry planes. RunResult.wire() retains the recorded
modeled-part flux. Transmission ratios are normalization-invariant,
so T needs no factor bookkeeping.
cvcs_sections
¶
cvcs_sections(
key_planes: Sequence[Sequence[VectorMode]],
key_z_um: Sequence[float],
n_subslices: int,
min_confidence: float = 0.0,
) -> List[Section]
Build a dense interpolated EME section list from a few solved key planes, keeping a fixed multimode basis so inter-mode conversion is represented.
The key planes are put in correspondence with a full permutation match (every
mode is matched to one on the previous plane, nothing is dropped), so a mode
that the fundamental converts into (even one that is near-cutoff / "born" only
at the wider planes) stays in the basis and the conversion is captured. This is
what lets CVCS reproduce a multimode staircase (e.g. a symmetric taper's
TE0->TE2 conversion) at K << N solves, not just the adiabatic limit.
Parameters¶
key_planes:
K mode lists (K >= 2), the FDE modes solved at the key
cross-sections, all on the same transverse grid and with the same
number of modes (solve a num_modes every key plane supports).
key_z_um:
The K monotonically increasing z-positions (microns) of the key
planes. The device spans [key_z_um[0], key_z_um[-1]].
n_subslices:
Number of interpolated, uniformly spaced propagation slices spanning the
device. Choose n_subslices >> K (the point of CVCS).
min_confidence:
Optional guard (default 0.0 = off): if > 0, reject when any
adjacent key-plane match is weaker than this overlap, a coarse check that
the key planes are dense enough to correspond. It is conservative (a
near-cutoff higher mode can have a weak link without spoiling the
result), so the real validation is convergence in the number of key
planes; leave it off unless you want a hard floor.
Returns¶
list[Section]
The first/last sections are the (solved) key endpoints as length-0 ports;
the interior is n_subslices interpolated sections. Feed directly to
photonhub.analysis.eme.run_eme.
interpolate_mode
¶
interpolate_mode(
mode_a: VectorMode, mode_b: VectorMode, s: float
) -> VectorMode
Linear blend of two corresponding modes at fraction s in [0, 1]
(s = 0 -> mode_a, s = 1 -> mode_b).
mode_b is sign/phase aligned to mode_a (via their transverse-E
overlap) before blending, so the two add constructively. All six field
components are interpolated and the result is renormalized to the
VectorMode convention (transverse-E L2 = 1). The full complex modal
index n_eff + i*k_eff is linearly interpolated, preserving both phase
propagation and attenuation. Intended for adjacent tracked ordinary guided
modes (see module docstring). Endpoint solve_params provenance is cleared
because an interpolated field cannot be reproduced by replaying either
endpoint eigensolve.
interpolate_plane
¶
interpolate_plane(
modes_a: Sequence[VectorMode],
modes_b: Sequence[VectorMode],
s: float,
) -> List[VectorMode]
Interpolate a whole corresponding (tracked, same-order, same-count)
mode set at fraction s. Mode m of the result blends modes_a[m]
with modes_b[m].
Propagation¶
propagate_plane
¶
propagate_plane(
simulation,
data,
monitor,
dz_um,
*,
n_medium: Optional[Union[float, ndarray]] = None,
direction: str = "+",
) -> Dict[str, ndarray]
Reconstruct the node-collocated complex E at offset(s) dz_um
downstream of a recorded plane.
direction is the way the recorded field travels: "+" toward
+axis, "-" toward -axis (a reflected or downward beam). dz_um is
measured along that travel direction, so a positive offset is always
downstream: the plane at axis coordinate a + dz for "+" and
a - dz for "-", where a is the recorded plane. A negative
offset looks upstream (the propagating part is exact; evanescent parts
are only ever damped).
n_medium is the index of the region swept (a scalar, or one value per
recorded frequency). Default: read from the scene at the plane; in a
dispersive (pole) medium that is sqrt(Re eps(f)) per frequency, and
the returned "n_medium" is then that array. data is a
RunResult or a plain name -> DataArray mapping (wire frame); the
plane's position is read through frame.wire_array.
Returns {"e1", "e2", "en", "coords1_um", "coords2_um", "dz_um",
"freqs_hz"}, each field shaped (n_dz, nf, n1, n2) with e1/e2
the in-plane components along the plane's cyclic transverse axes and
en the normal component completed from div E = 0.
focal_scan
¶
focal_scan(
simulation,
data,
monitor,
*,
dz_um,
freq_hz: Optional[float] = None,
n_medium: Optional[Union[float, ndarray]] = None,
direction: str = "+",
) -> FocalScan
Sweep dz_um downstream of a recorded plane and return the
intensity volume + spot metrics for ONE recorded frequency (freq_hz
defaults to the monitor's single frequency; required when it records
several). n_medium and direction are as in
propagate_plane; FocalScan.n_medium is the index at
freq_hz.
FocalScan
dataclass
¶
FocalScan(
dz_um: ndarray,
coords1_um: ndarray,
coords2_um: ndarray,
intensity: ndarray,
freq_hz: float,
n_medium: float,
)
Result of focal_scan: intensity through a z-sweep of one
recorded frequency. intensity is |E|^2 (all three components)
shaped (n_dz, n1, n2).
peak_intensity_per_z
property
¶
Maximum intensity across each sampled transverse plane.
peak_dz_um
property
¶
Propagation offset in microns with the largest sampled intensity.
If peaks tie, return the first offset in scan order.
fwhm_um
¶
FWHM of the focal-plane spot along the two in-plane axes, through the peak pixel (linear interpolation between samples).
focal_metrics
¶
focal_metrics(
simulation,
data,
monitor,
*,
dz_um,
incident_power: float,
freq_hz: float | None = None,
n_medium: float | ndarray | None = None,
aperture_fwhm: float = 3.0,
disc_radius_um: float | None = None,
center_um: tuple[float, float] | None = None,
refine_um: float = 0.5,
refine_steps: int = 41,
) -> FocalMetrics
Focal-spot metrics of a lens from its recorded transmitted plane.
The plane (a full-period ProfileMonitor with the tangential E pair)
is decomposed into plane waves, each propagated dz downstream with
its paired H, and the Poynting flux S_z is rebuilt: the plane of
focus is the plane of peak S_z over dz_um (then refined within
refine_um), the FWHM is a 2-D Gaussian fit to S_z there, and the
focusing efficiency is the power through a circular aperture of radius
aperture_fwhm x FWHM around the peak over incident_power, the
definitions used by the metalens literature. transmission is the
power through a disc of disc_radius_um about center_um at the
recorded plane over incident_power (the lens's own transmission).
incident_power must be read in the same units, e.g. the forward power
of an input plane from diffraction_orders.
n_medium defaults to the scene's index at the plane at freq_hz
(sqrt(Re eps(f)) in a dispersive medium), as in
propagate_plane.
FocalMetrics
dataclass
¶
FocalMetrics(
z_peak_um: float,
peak_um: tuple[float, float],
fwhm_um: float,
fwhm_x_um: float,
fwhm_y_um: float,
fwhm_method: str,
efficiency: float,
aperture_radius_um: float,
transmission: float | None,
incident_power: float,
power_aperture: float,
power_forward_total: float,
intensity: ndarray,
coords1_um: ndarray,
coords2_um: ndarray,
cut1: ndarray,
cut2: ndarray,
scan_dz_um: ndarray,
scan_peak: ndarray,
)
Result of focal_metrics. Powers are in the recorded phasors'
own units (E^2 / (2 eta0) times um^2), the same convention as
diffraction_orders, so ratios
against a forward power read the same way are the physical numbers.
focal_cut
property
¶
(offset_um, normalized S_z) along the first in-plane axis through the peak.