Coupling simulations#
A simulated pulse is often the input to another optical component: a second capillary,
a fibre, a gas cell. Carrying only its spectrum or duration loses its wavefront, its
modal coherence and the frequency dependence of its transverse profile; carrying a bare
array leaves its basis, units, frame and energy meaning implicit.
solprop.coupling connects simulations through a coherent field at a
reference plane instead (SEP 31).
This page covers reading a saved plane as a field plane, moving it between temporal
grids, and coupling it into another simulation: a relaunch into the same basis, a
beam or a guide’s exit into a capillary or another fibre, a guide’s exit field
synthesised on a spatial grid, and transport of a spatial plane through free space,
lenses, apertures, mirrors, interfaces and spectral optics.
A plane, and what it means#
A FieldPlane holds a field’s native coefficients
exactly as an output stores them – the optics-convention raw spectrum of each
column, referenced to the centre of the time window – together with everything
needed to use them:
part |
what it says |
|---|---|
|
the representation: ordinary true-power mode coefficients ( |
|
the temporal grid: field or envelope layout, time samples, the absolute frequency of every bin, the carrier |
|
the affine reference of the stored spectrum (below) |
|
the plane’s position, the side of a declared index jump, and its oriented right-handed transverse frame |
|
how energy is measured there: the identity for true-power coefficients, a general basis’s Gram, or the medium’s axial admittance for a spatial plane (medium flux) |
|
whether the record is zero outside its samples or unknown there |
|
the scoped path certificate of a run through a gas gradient: it bounds the linear background path, not the whole nonlinear run |
|
the source run’s configuration hash, the plane, its lineage and what was done to it |
The plane owns read-only copies of its arrays, so it cannot be changed through an
alias. plane.energy() is the physical energy crossing the plane in joules,
plane.spectral_energy() the energy per bin.
The reference. A run’s spectrum is stored in its moving frame: the linear operator removed \(R(\omega,z) = \beta_0(z) + \beta_1(z)(\omega - \omega_o)\) (the frame, with \(\omega_o\) the reference frequency on an envelope grid and zero on a field grid). The absolute optics-convention spectrum is
with ω the absolute frequency. τ is the absolute time of the record’s \(t = 0\) and φ a
carrier phase; plane.absolute() applies them. Keeping the pair as numbers, rather
than shifting the samples, means that a long path never translates a femtosecond pulse
out of its window. Each run records its frame integral, so any saved plane’s reference
is rebuilt from the file alone (output schema 24,
provenance).
Reading a plane back#
field_plane() reads one saved plane; by default the last.
A position must name a stored plane: a nearby plane that was not saved is refused,
never substituted.
from solprop.driver import Simulation
from solprop.processing import field_plane
simulation = Simulation.from_capillary(
100e-6,
1e-3,
"Ar",
1.0,
lambda0=800e-9,
wavelength_limits=(400e-9, 1.6e-6),
trange=200e-15,
fwhm=20e-15,
energy=20e-6,
modes=2,
n_save=3,
noise=None,
)
run = simulation.run(progress="none")
plane = field_plane(run) # the exit plane
middle = field_plane(run, z=0.5e-3) # a stored plane by position
print(plane, plane.reference.delay, plane.energy())
A general modal file records the identities of its E/H reference and not the
reference’s arrays, so its physical fields (plane.nodal) and a dense basis Gram are
read with the simulation that ran it, field_plane(run, source=simulation); the
identities are checked first, as solprop.processing.modal checks them. A
spatial plane’s measure is rebuilt from the file’s plane measure; with source= it
also carries the medium. A hybrid planar run’s planes are refused by name, and so are
the planes of a run with the modified noise model (noise="modified"): its saved field
leaves the one-photon background to an auxiliary noise phase, which is not reconstructed
into a plane, so the stored samples alone would drop it in the next stage. A source run
meant for coupling uses noise="input", whose background travels in the saved field,
or noise=None.
Moving a plane between grids#
regrid() puts a plane on another temporal grid – a longer
or finer window, a field grid instead of an envelope one, another carrier – or into
another reference. Copying raw DFT amplitudes between grids of different size or
spacing is not this operation. The plane’s analytic signal is its finite record, zero
outside its window, and each target bin receives its finite-time Fourier sum
with \(q = 2\) on interior bins of a field grid and 1 otherwise, and \(\Delta\), \(\Delta\phi\) the change of reference, so that the absolute spectrum is unchanged. The direct sum is the implementation; complex spectral zeros and separated bands need no special case, since no phase is unwrapped and nothing is interpolated.
from solprop.coupling import regrid
from solprop.grid import FieldGrid
wider = FieldGrid.from_limits(1e-3, 800e-9, (300e-9, 2e-6), 400e-15)
moved, report = regrid(plane, wider)
print(report.method, report.band_loss, report.window_loss)
print(moved.energy() / plane.energy()) # no renormalisation, and none needed here
What cannot be represented is refused, and what is removed is reported. Light outside
the target’s active band is removed only with crop_band=True, light outside its time
window only with truncate=True, each with its share in the
RegridReport. A record whose edges carry light, or whose
exterior is unknown, is not extended into a larger window unless truncate=True
declares it zero outside. Nothing is ever renormalised: a conversion does not recover
clipped, aliased or absorbed light. delay= and phase= change the reference: a later
delay moves the record earlier in its window, with the absolute field unchanged.
Relaunching into the same basis#
A receiving simulation describes its entrance with
input_port(): the route, the temporal lattice and
basis with their identities, the entrance plane and the frame accumulated there, the
native metric at the entrance, the reference index of its entrance medium and the
coupling models it admits. couple() computes a launch
bound to that port under a declared CouplingModel.
The identity model, same_basis, launches a plane into a port of the same basis on
the same lattice, at an identically oriented plane in the same medium: the coefficients
are copied as they stand, and the plane’s reference becomes the launched run’s origin.
The launched run therefore continues the source’s absolute time and carrier phase
without moving the pulse in its window.
from solprop.coupling import CouplingModel, couple
second = Simulation.from_capillary(
100e-6,
1e-3,
"Ar",
1.0,
lambda0=800e-9,
wavelength_limits=(400e-9, 1.6e-6),
trange=200e-15,
fwhm=20e-15,
energy=20e-6,
modes=2,
n_save=3,
noise=None,
)
relaunch = couple(
field_plane(run), second.input_port(), model=CouplingModel("same_basis")
)
continued = second.with_input(relaunch.launch).run(progress="none")
print(field_plane(continued).reference.delay) # the whole 2 mm path's
Two 1 mm capillaries relaunched this way reproduce one 2 mm capillary: the comparison
is an accuracy envelope, since the second run restarts its step controller, and the
tests hold it to the tolerance’s level on every pair of the three CPU backends. The
basis of an ordinary mode set is its value at the plane – each mode’s class, label,
effective area and effective index on the active band – so the exit of one capillary
and the entrance of an identical one of another length are the same basis, and a
different radius is not. A simulation can also continue from inside its own path:
simulation.input_port(z) is an interior entrance of a modal run.
same_basis also takes a declared invertible change of basis or gauge,
CouplingModel("same_basis", basis_change=BasisChange(matrix, source, target)), which
must carry the plane’s metric onto the port’s – it relabels the same field – and, for
a general modal plane read with its simulation, recovers the coordinates in a port of
the same guide with the flux pairing, \(\mathbf a = F^{-1}\mathbf B\), refusing a field
that is not in the port’s span.
with_input() refuses a launch computed for any other
port – another basis, lattice, entrance, orientation or medium – whatever the shapes of
the arrays. It records the coupling in configuration["coupling"] (the model, the port,
the inherited origin and the source plane’s provenance, so a run launched from a plane
names its parent), and it draws no new noise: the launch carries the realised field of
its source, and a realisation is added only when one is given explicitly. The receiving
run keeps its ionisation tables and their ranges, as every derived run does.
Coupling between different representations#
A different guide, radius or medium is not a relaunch. Three quantities must then be kept apart (SEP 31, section 4.3): the Hermitian E/H flux pairing \(F\) of a modal basis, the Gram \(G = S^\dagger MS\) of a scalar least-squares fit in a common measure \(M\), and a representation’s native power metric \(Q\) – the identity for ordinary true-power coefficients, a dense Gram for a declared general basis, the axial admittance for a spatial plane. They coincide only where a model establishes it, and the uncaptured energy is a physical, nonnegative loss only in a common positive metric. The models are:
model |
what it admits |
what it computes |
|---|---|---|
|
an identical basis, a declared basis change, a general modal plane into its own guide |
the coordinates exactly; no budget |
|
a radial or Cartesian beam (Gaussian, analytic or sampled) into a capillary or a weakly guiding step-index fibre; a capillary’s exit into another capillary of any radius and mode set; one weakly guiding step-index fibre into another |
the fit \(\mathbf a = G^{-1}S^\dagger M\mathbf f\) in \(M_0 = Y_0\,dA/2\), \(Y_0 = n_{\rm ref}\varepsilon_0c\), then \(b = \sqrt{\operatorname{Re}n_{\rm eff}}\,a\) |
|
a beam, or an ordinary guide’s exit, into a general modal guide that carries its E/H at fixed nodes |
\(\mathbf a = F^{-1}\mathbf B\) with the incident E and the forward closure \(\mathbf H_t = Y_0\hat z\times\mathbf E_t\) |
Every other pair is refused by name. The reference index \(n_{\rm ref}\) is the port’s:
the core gas of a capillary, the core of a fibre. The scalar profiles are the run’s own:
a capillary’s \(HE_{1m}\) modes are \(J_0(u_mr/a)\) inside the core and zero outside, and a
step-index fibre’s \(HE_{1m}\) modes are taken in the weak-guidance limit as \(LP_{0m}\),
\(J_0(Ur/a)\) inside and \(J_0(U)K_0(Wr/a)/K_0(W)\) outside, frozen at the mode’s declared
profile wavelength as the run’s profile is
(ScalarModeBasis). A family with angular structure
(TE, TM, EH, \(HE_{nm}\) with \(n \ne 1\)) has no scalar profile, and its port admits
same_basis alone.
Overlaps. A spatial plane’s columns are the Hankel or Fourier spectrum of its field, so a profile’s overlap is taken by Parseval in that same spectral quadrature, with the profile’s closed-form transform: the incident norm, the overlaps and the Gram all use one measure, the orthogonal-residual identity \(\|f\|_M^2 = \mathbf a^\dagger G\mathbf a + \|f - S\mathbf a\|_M^2\) holds, and a profile’s kink at the core radius is never sampled. Two modal planes are overlapped on a Gauss–Legendre rule broken at both core radii.
The admission budget and the report#
model_error is a required energy budget \(\epsilon_m\) in (0, 1) for the two approximate
models: the relative energy discrepancy the approximation, its tails and its numerical
errors together may cause. It is not a coupling-efficiency floor – a poorly captured
beam is physics – and it is separate from numerical accuracy. A cone half-angle
follows from it, \(1 - \cos\theta_* = \epsilon_m/8\), and each coupling is admitted against
these items, every one a relative energy discrepancy:
item |
what it bounds |
limit |
|---|---|---|
|
\([(1 - \cos\theta_*)U_{\rm cone} + U_{\rm tail}]/U\) of the incident field, and of the worst unit field in the retained target subspace; the cone term is the cone rule’s allocation, about \(\epsilon_m/8\) of the cone share, not a measured defect |
the total |
|
the weak-guidance screen $\Delta = \max |
n^2 - n_{\rm ref}^2 |
|
\(G\) (or \(F\)) against the target’s native \(Q\), whitened |
\(\epsilon_m/8\) |
|
the common measure \(M_0\) against the source’s native metric |
\(\epsilon_m/8\) |
|
\(2\delta + \delta^2\) of the H-closure’s field error \(\delta\) (vector model) |
\(\epsilon_m/8\) |
|
the whitened certificate \(C^\dagger Q_BC \preceq Q_A\): an apparent gain within the model’s own defects is reported here, never rescaled or clipped; a larger one refuses |
the total |
|
the recorded provider-pair envelope’s allowance, plus the incident share of any bins outside its range |
the total |
|
the source’s content at its spectral edge, an analytic field’s domain and resolution checks, the overlap quadrature (an amplitude error \(\delta\) counted as \(2\delta + \delta^2\)) |
the total |
The counted items must sum to at most \(\epsilon_m\). The
CouplingReport carries the incident energy over the
whole plane in \(M_0\), the launched energy in the target’s native metric, the captured
and uncaptured energy where a common positive metric exists (None, with the reason,
for the vector model), every item with its scope – this field, the whole map, or the
model – the source plane’s exterior, and the source’s SEP 34 path certificate,
separately and with its scope. A zero field is a valid zero launch whose efficiency is
None. The recorded envelopes (solprop.coupling.envelopes) bound what the
approximate models omit, measured by the test suite against independent physics:
Capillary. The scalar profile \(J_0(u_mr/a)\) against Marcatili’s leaky core field \(J_0(u_m(1 - i\delta)r/a)\), \(\delta = |\nu_n|c/(\omega a)\) with \(\nu_n\) the wall term (Marcatili and Schmeltzer 1964). Over waists 0.3a–1.0a, displacements to 0.2a, tilts to \(0.59/ka\), 1–8 modes and radius ratios 0.5–2 the captured shares differ by at most \(2.9\times10^{-3}\) at \(\delta = 10^{-2}\), the range’s top (an 18.8 µm silica capillary at 800 nm); the bound is twice that, \(6\times10^{-3}\). The difference falls at least linearly with \(\delta\), so the reported allowance is the bound scaled by the pair’s energy-weighted \(\delta\): about \(1.5\times10^{-3}\) for a 75 µm capillary at 800 nm.
Step index. The LP01 overlap against the full-vector flux overlap of the exact \(HE_{11}\) fields: \(3.0\times10^{-4}\) over contrasts to 0.01, V from 1.5 to 2.4 and radius ratios 0.7–1.43. V is checked at the profile wavelength and at every occupied bin, since it grows with frequency.
A beam into a step-index fibre. The scalar LP01 overlap against the beam’s full-vector coupling into the exact \(HE_{11}\) pair: the flux pairing \(\frac14\int(\mathbf E\times\mathbf h^* + \mathbf e^*\times\mathbf H)\cdot\hat z\,dA\) with the beam’s \(\mathbf H\) and \(E_z\) from its own plane-wave spectrum in the core index. The bound is not sampled over beams but maximised over every incident field inside the widest admitted cone (both captures are rank-two quadratic forms, so the largest eigenvalue of their difference bounds them all): \(0.467\Delta\) at V 1.5, falling to \(0.284\Delta\) at V 2.4 (conservative there, where the cone’s edge truncates the widening \(HE_{11}\) spectrum); at V 1.5 it is linear in \(\Delta\) and independent of the core index and the wavelength. Gaussian beams of waist 0.5a–2.6a, displaced to 0.5a and tilted to half the acceptance angle reach \(3.3\times10^{-3}\) at \(\Delta = 0.01\) and V 1.5, a centred beam of waist 0.8a. The bound is \(10^{-2}\) at contrast 0.01, scaled by the fibre’s contrast (it is first order in \(\Delta\)): about \(5\times10^{-3}\) for a 0.005 step. It holds for any beam shape, alignment and polarisation, for V from 1.5 to 2.4 (LP01 alone); the angular item counts what lies outside the cone.
Vector model into a step-index guide. The flux pairing with the incident field against an independent full-vector mode-matching solution of the planar entrance facet: the incident and reflected fields in the input medium, the fibre’s forward guided and radiation fields, tangential \(\mathbf E\) and \(\mathbf H\) continuous, solved per azimuthal order in a large conducting cylinder on its own TE/TM basis. The exact amplitude differs from the model’s by \(-\tfrac14\langle\mathbf u_i, D(Y_0 + Y_f)^{-1}D\,\mathbf e_m\rangle\) with \(D = Y_f - Y_0\) the difference of the fibre’s and the medium’s admittance operators, so the omission is second order in the index step; at weak guidance it is the local Fresnel estimate \(-(n_{\rm cl} - n_{\rm co})^2/(8n_{\rm co})\) times the beam’s cladding overlap with the mode, which the solution matches to a few per cent. Summed over an azimuthal order’s guided modes the omission is a quadratic form of the incident field, so, as for the beam into a fibre, the bound is maximised over every field inside the widest admitted cone, of any shape and polarisation. At contrast 0.05 it rises towards every cutoff, where a newborn mode’s power moves into the cladding: the largest resolved value is \(1.37\times10^{-3}\) (V = 3.905, just above the \(HE_{12}\) cutoff), against \(7.4\times10^{-4}\) at V = 1.5. A mode wholly in the cladding approaches the normal-incidence Fresnel ceiling \(R(2-R)/(1-R) \approx \Delta^2/2 = 1.39\times10^{-3}\), R the core–cladding reflectance; the bound is twice that, \(3.0\times10^{-3}\), which also covers the narrow bands just above the \(HE_{1m}\) cutoffs that the reference cannot resolve. It bounds the energy launched into all guided modes together and is scaled by the squared weighted contrast over \(0.05^2\): about \(1.2\times10^{-4}\) at contrast 0.01. The beam is in the core’s index: a beam in air meets a bulk step first (a separate planar interface), and the model refuses it through its source metric. The incident beam’s own \(\mathbf H\) closure is the angular item’s, not the envelope’s. A graded-index guide has no recorded envelope; its item has no value and is not counted.
A pair outside its envelope’s range is refused, not extrapolated; bins of a covered pair whose \(\delta\), V or contrast leave the range carry an unbounded discrepancy, so their share of the incident energy is added to the item in full.
A Gaussian beam into a capillary#
A beam is a field plane: from_pulse() builds
one from the package’s SpatialPulse, and
from_analytic() samples any analytic field,
a callable of the node positions and the absolute angular frequency returning the
optics-convention spectrum \(\tilde E(\mathbf r,\omega)\) in V s/m, with a declared
support and exterior; it records how the plane’s energy changes when the window is
doubled and when the spacing is halved, and a coupling counts both as numerical error.
import numpy as np
from solprop.grid import RadialGrid
from solprop.pulses import GaussianPulse, SpatialPulse
from solprop.spatial import SpatialMedium
from solprop.coupling import FieldPlane, PlaneGeometry
capillary = Simulation.from_capillary(
75e-6,
1e-3,
"Ar",
1.0,
lambda0=800e-9,
wavelength_limits=(400e-9, 1.6e-6),
trange=200e-15,
fwhm=20e-15,
energy=20e-6,
modes=4,
n_save=3,
noise=None,
)
port = capillary.input_port()
beam = SpatialPulse(
GaussianPulse(wavelength=800e-9, fwhm=20e-15, energy=10e-6), 37.5e-6
)
incident = FieldPlane.from_pulse(
beam,
port.grid,
PlaneGeometry(0.0),
space=RadialGrid(400e-6, 256),
medium=SpatialMedium("Ar", 1.0),
)
coupled = couple(
incident, port, model=CouplingModel("scalar_paraxial", model_error=0.05)
)
print(coupled.report) # incident, launched, uncaptured, the budget
entered = capillary.with_input(coupled.launch)
At \(w/a = 0.5\) the fundamental takes about 0.906 of the incident energy and the next
radial mode 0.092. The launch carries what the modes capture: nothing is rescaled to
the requested energy. A displaced, tilted or curved beam is a Cartesian plane
(space=FreeGrid(...)); a radial plane cannot carry them. The
capillary coupling tutorial
couples the entrance field of a beam focused through 20 mm of argon, with and without
the Kerr effect, and follows both launches through a capillary.
A change of capillary radius#
A modal plane read with its simulation, field_plane(run, source=simulation), carries
the run’s scalar profiles; its coefficients are coupled into another capillary’s port
by direct overlap, without a transverse grid:
exit_plane = field_plane(run, source=simulation) # 100 um, two modes
wider = Simulation.from_capillary(
120e-6,
1e-3,
"Ar",
1.0,
lambda0=800e-9,
wavelength_limits=(400e-9, 1.6e-6),
trange=200e-15,
fwhm=20e-15,
energy=20e-6,
modes=4,
n_save=3,
noise=None,
)
into_wider = couple(
exit_plane,
wider.input_port(),
model=CouplingModel("scalar_paraxial", model_error=0.05),
)
print(into_wider.report.efficiency)
The same result comes from synthesising the exit field and projecting it:
Outcouple writes the modal field onto a radial or
Cartesian grid under an explicitly declared forward interface model. The one model is
"matched", an exit into a medium of the core’s own index where the transverse field is
continuous; a change of index is a separate planar interface, applied once, never an
implied Fresnel factor.
from solprop.coupling import Outcouple
synthesised = Outcouple(RadialGrid(600e-6, 512)).apply(exit_plane)
through = couple(
synthesised.plane,
wider.input_port(),
model=CouplingModel("scalar_paraxial", model_error=0.05),
)
difference = np.abs(through.launch.samples - into_wider.launch.samples)
print(np.max(difference) / np.max(np.abs(into_wider.launch.samples)))
The tests hold the two routes together to the synthesis window’s numerical error.
A solid fibre overlap#
A step-index fibre’s mode-averaged run carries its waveguide’s profile, taken in the weak-guidance limit; two fibres of one material pair and different radii are coupled by the same direct overlap. The pair is admitted within the recorded envelope’s range; the contrast screen applies too, and the envelope stands in for it at a budget whose eighth is below the contrast. Small cores are not very paraxial: the LP01 field of a 2 µm core spreads over a tenth of a radian, so such a pair needs a budget of about 0.1. A beam enters a fibre the same way, as a radial or Cartesian plane sampled in the core index (a plane in another medium is a change of medium, which the source-metric item refuses: a bulk entrance face is a separate planar interface), within its own envelope above.
from solprop.modes import StepIndexMode
def fibre(radius):
mode = StepIndexMode(
radius, core=1.45, cladding=1.45 * np.sqrt(0.99), profile_wavelength=1064e-9
)
return Simulation.from_gnlse(
None,
1e-3,
fibre=mode,
n2=2.6e-20,
lambda0=1064e-9,
wavelength_limits=(900e-9, 1300e-9),
trange=400e-15,
fwhm=100e-15,
energy=1e-12,
n_save=2,
)
source_fibre, target_fibre = fibre(2.18e-6), fibre(2.57e-6)
fibre_run = source_fibre.run(progress="none")
spliced = couple(
field_plane(fibre_run, source=source_fibre),
target_fibre.input_port(),
model=CouplingModel("scalar_paraxial", model_error=0.1),
)
print(spliced.report.efficiency)
fibre_port = target_fibre.input_port()
fibre_beam = FieldPlane.from_pulse(
SpatialPulse(GaussianPulse(wavelength=1064e-9, fwhm=100e-15, energy=1e-12), 3e-6),
fibre_port.grid,
PlaneGeometry(0.0),
space=RadialGrid(100e-6, 256),
index=np.asarray(fibre_port.reference_index),
)
launched = couple(
fibre_beam, fibre_port, model=CouplingModel("scalar_paraxial", model_error=0.1)
)
print(launched.report.efficiency) # about 0.992: a 3 um waist nearly matches LP01
A beam into a general modal guide#
The vector model couples into a guide that solves its own E/H. Its entry point is
Simulation.from_waveguide(provider=..., incident=SpatialPulse(...), coupling=CouplingModel("vector_paraxial", model_error=...)), which couples the beam
through couple and records the report as configuration["launch"]
(general modal propagation). A strongly guiding core
fails the contrast screen and is refused; the closure is qualified against the
provider’s own H, and for a step-index guide the facet reflection the model omits is
counted through its recorded envelope (above).
Transporting a plane between planes#
transport() carries a radial or Cartesian field
plane through an ordered sequence of linear optical elements and returns the plane at
the last element’s output together with a
TransportReport. A modal plane holds guide
coefficients, not a field on a transverse grid, and is refused: its exit field is
synthesised first by Outcouple. The plane needs its medium, so a saved plane is read
with field_plane(run, source=simulation).
element |
what it does |
|---|---|
homogeneous propagation \(e^{ik_zL}\) with the exact \(k_z=\sqrt{(\omega n/c)^2-\kappa^2}\), on the plane’s own grid or onto another output plane |
|
the paraxial phase screen \(e^{-ik(\omega)r^2/2f(\omega)}\), with a constant or chromatic focal length |
|
the edge profile \(W\) of |
|
a complex field transmission \(T(x, y, \omega)\), as a function or as samples |
|
a lateral displacement and a tilt of a Cartesian field |
|
a uniform Jones matrix in the plane’s local axes |
|
spectral p/s reflection and the change of frame |
|
the normal-incidence Fresnel interface between two lossless media |
|
any |
Order. The elements are applied exactly in the order given. An
Chain or any other known transfer function may stand in the
sequence directly: it is one frequency-only block, its factors commute among
themselves, and it is never moved past focusing, clipping, propagation or a Jones map.
Every element reports its input and output energies in joules (the axial Poynting flux
of the medium, medium flux), the per-frequency spectra of both,
the declared loss, any intentional removal and the signed approximation error that
closes its ledger. The report of the sequence chains them: each element’s input energy
is the previous one’s output, losses add in joules, and broadband efficiencies are never
multiplied. A source plane’s path certificate is carried as its own item,
report.source_certificate, because it bounds the producing run, not the transport.
The plane’s record follows the field. Each FreeSpace removes a frame term from the
whole field so that the pulse stays in its window, and adds it to the plane’s
reference, so plane.absolute() stays exact over any distance. Pass the source
simulation’s frame, transport(plane, elements, frame=simulation.frame), and a gap
removes the same affine term \(R L\) the run would have removed: the transported plane
then has the same stored spectrum and reference as continuing the run. Without a frame a
gap removes only the group delay \(\beta_1(\omega_0)L\) of its medium, and the report says
so. A spectral element that drops its own constant phase or group delay (a grating or
prism pair, a chirped mirror’s data) leaves that term of the reference uncalibrated. An
interface changes the plane’s medium, a mirror turns its axes, and the power metric is
resolved again in the output medium and grid. Every operation is appended to
plane.provenance.history. geometry= declares the output plane in the receiving
system’s coordinates; its axes must be the ones the field arrives with.
Free space and its output plane. Without output=, the field stays on its grid,
propagated exactly in its periodic (Cartesian) or reflecting (radial) representation.
A column at angle θ moves its rays by \(L\tan\theta\); content whose rays would leave the
window wraps back, which is the same as sampling the propagator’s phase below its
Nyquist rate, and is refused beyond sampling_tolerance. beyond_window="remove"
declares the removal of that angular band instead, and the removed energy is reported.
Significant nonpropagating content is refused too, or removed explicitly with
band_limit=. The ray test does not see a field that has already spread into the
window’s outer band (the declared collar, or the outer tenth of each half-width), where
the representation wraps or reflects it; a propagated field whose share there exceeds
edge_tolerance (default \(10^{-4}\)) is refused too, and the refusal suggests a larger
window, beyond_window="remove" when the removal is not yet declared, or a Fresnel
output plane. The share is reported as edge_share either way. An output plane of
another radius or sampling is the caller’s explicit choice of method; nothing switches
between them automatically:
output_method="angular"sums the propagated spectrum at the output nodes. It is exact and nonparaxial but lives inside the source window: a field that reaches the source window’s edge is refused, and the refusal suggests the Fresnel route.output_method="fresnel"evaluates the paraxial Fresnel integral of the source samples at the output nodes. It reaches any observation window, including the far field; it reports its paraxial phase bound \(k(\rho_{\max}+r_{\rm occ})^4/8L^3\) and refuses a kernel that would alias on the source nodes.
Both build their kernels in blocks; no transverse-by-transverse-by-frequency array is formed.
The thin-screen certificate. A lens, an aperture, a mask and a tilt are local
electric-field screens at each frequency. A screen is exact in the local reference
measure \(U_0\), which weighs every plane wave by \(\operatorname{Re} n\,\varepsilon_0 c\);
the physical axial energy \(U_Y\) weighs it by a further \(\cos\theta\). With the required
model_error \(\epsilon_m\) and the cone \(1-\cos\theta_*=\epsilon_m/8\), the defect
\(d(E)=(1-\cos\theta_*)U_{0,\rm cone}+U_{0,\rm tail}\) bounds \(U_0-U_Y\) for the input
and for the output field, and the screen is admitted only when
\([d(E_{\rm in})+d(E_{\rm out})]/U_Y(E_{\rm in})\), plus the transform’s round-trip and
edge error, is at most \(\epsilon_m\) (SEP 31 §4.6). The certificate,
report.elements[i].certificate, applies to that one field. A screen whose phase changes
by π or more between neighbouring nodes where the field is, or one with gain, is
refused. An aperture’s weighting has the meaning it has for the in-run aperture
statistics: "amplitude" (the default) applies \(W\) to the field, so the power goes as
\(W^2\); "quadrature" takes \(W\) as a power weight and applies \(\sqrt W\). The
intensity-defined aperture of SEP 31 is the "quadrature" case; a hard aperture is the
same either way.
Mirrors and Jones frames. A mirror takes its incoming and outgoing propagation directions and an s axis perpendicular to both (at normal incidence the s axis resolves the degeneracy). Each beam has the right-handed frame \((\hat p, \hat s, \hat k)\) with \(\hat p = \hat s\times\hat k\); the field leaves as \((r_pE_p, r_sE_s)\) with no conjugation, and the outgoing plane’s axes keep the p/s components of the incoming ones, so the reflection appears as a mirror image of the transverse pattern along p. In this convention a perfect conductor has \(r_p=+1\), \(r_s=-1\) at any incidence; coefficients calibrated in another convention are converted first. Two reflections restore the field and its frame.
The Fresnel interface. PlanarInterface
applies \(t_E=2n_1/(n_1+n_2)\) per frequency, with power transmission
\(T=(n_2/n_1)|t_E|^2\) and the reflected \(R=|r_E|^2\) reported as loss; \(R+T=1\). It is
the normal-incidence model: oblique columns’ departure from \(T\) is reported as
approximation error. The output plane is in the second medium, so applying the same
factor twice – in an element and again in a launch – is refused.
Spectral optics. SpectralTransfer adapts
Material, ChirpedMirror, GratingPair, PrismPair, TaylorPhase and a Chain of
them, keeping each factor’s affine-phase record (report.calibration). Losses are kept
per frequency; bins a transfer function sets exactly to zero are reported as removed. A
phase-only model asked for a lossy prediction needs an explicit efficiency=, and an
efficiency on a lossy model is refused because it would count the loss twice. A
negative material length or an inverse mirror phase is flagged as a reconstruction,
not a passive element. A callable that is not a known optics element states its
calibration with
SpectralCalibration.
Example: a Gaussian focused by a lens.
from solprop.coupling import FieldPlane, FreeSpace, PlaneGeometry, ThinLens, transport
from solprop.grid import EnvelopeGrid, RadialGrid
from solprop.pulses import GaussianPulse, SpatialPulse
from solprop.spatial import SpatialMedium
grid = EnvelopeGrid.from_limits(0.1, 800e-9, (750e-9, 850e-9), 200e-15)
air = SpatialMedium()
beam = SpatialPulse(GaussianPulse(wavelength=800e-9, fwhm=30e-15, energy=1e-6), 1e-3)
plane = FieldPlane.from_pulse(
beam, grid, PlaneGeometry(0.0), space=RadialGrid(4e-3, 256), medium=air
)
small = RadialGrid(0.5e-3, 128) # the focal plane, sampled finely
focus, report = transport(
plane,
[
ThinLens(0.5, model_error=1e-3),
FreeSpace(0.5, air, output=small, output_method="fresnel"),
],
)
print(focus.geometry.z, focus.energy(), report.elements[0].certificate.ratio)
The second-moment radius at the focus is the Fourier-plane waist \(\lambda f/\pi w_0 \approx 127\) µm to about \(10^{-6}\) here (the tests hold the lens and the free-space output to their analytic references); the focal plane carries the launched microjoule less \(10^{-6}\) of it. That is the lens’s own axial-flux defect: the converging plane waves cross the plane obliquely, carrying \(\cos\theta\) of their local measure, and the report books it as the lens’s approximation error, inside its certificate (ratio \(2.5\times10^{-4}\) against the budget of \(10^{-3}\)).
Composing simulations#
The blocks above compose into the calculations
SEP 31 set out to support. Each is an
ordinary chain of runs and planes; nothing schedules them, and each run keeps its own
step control. The test suite runs every one of them as a short integrated case
(tests/coupling/test_workflows.py), asserting its energy ledger.
calculation |
composition |
|---|---|
a capillary’s output into another capillary |
|
a nonlinear beam into a capillary |
a radial (or Cartesian) gas gap with the ordinary Kerr, plasma and harmonic responses; its last plane read with |
a solid-core fibre into another fibre |
the exit plane of a step-index run coupled into another step-index run, within the recorded envelope (a solid fibre overlap) |
a Gaussian beam into a guide |
|
an arbitrary analytic field into a guide |
|
an arbitrary Cartesian beam into a guide |
a Cartesian plane, aligned with |
a capillary’s output at a distant plane |
|
SPM, a compressor and another capillary |
the first capillary’s exit, |
Three things to keep in mind when chaining:
Lattices.
coupleneeds the plane on the port’s temporal lattice. Build the receiving run on the source’s grid (grid=of the constructors), or move the plane withregridfirst. A gap whose output carries a harmonic needs a receiving band that holds it: the active band is checked, and light outside it is refused unless cropped explicitly.Planes in the receiver’s coordinates. A transported plane moves along its normal, so its origin is where the optics put it. Pass
geometry=port.geometrytotransportto declare the last plane in the receiving run’s own coordinates; its axes must be the ones the field arrives with, or the coupling is refused as oriented differently.Budgets. Every approximate step has its own budget: a coupling’s
model_error, a thin screen’smodel_error, a free-space gap’ssampling_tolerance(orbeyond_window="remove", which books the removed angular band). They are reported separately and never multiplied into one efficiency.
For example, a capillary’s output relayed 1:1 by two lenses, compressed on the way, into an identical capillary:
from solprop.coupling import Aperture, SpectralTransfer
from solprop.optics import TaylorPhase
argon = SpatialMedium("Ar", 1.0)
receiving = Simulation.from_capillary(
100e-6,
1e-3,
"Ar",
1.0,
lambda0=800e-9,
wavelength_limits=(400e-9, 1.6e-6),
trange=200e-15,
fwhm=20e-15,
energy=20e-6,
modes=2,
n_save=3,
noise=None,
)
port = receiving.input_port()
exit_field = Outcouple(RadialGrid(1.2e-3, 512), medium=argon).apply(
field_plane(run, source=simulation)
)
gap = FreeSpace(0.05, argon, beyond_window="remove")
relayed, ledger = transport(
exit_field.plane,
[
SpectralTransfer(TaylorPhase((0.0, 0.0, -20e-30), 800e-9)),
gap,
ThinLens(0.05, model_error=0.05),
gap,
Aperture(600e-6, model_error=0.05, edge=20e-6, kind="tanh"),
gap,
ThinLens(0.05, model_error=0.05),
gap,
],
frame=simulation.frame,
geometry=port.geometry,
)
second = couple(relayed, port, model=CouplingModel("scalar_paraxial", model_error=0.05))
print(ledger.specified_loss, ledger.removed, second.report.efficiency)
Field planes, coupling models and their budgets, and transport between planes were proposed and carried out by SEP 31.