Note
Go to the end to download the full example code.
Mode conversion and loss in a taper with a coherent wall#
A capillary whose bore varies along z is written in a basis attached to the local radius, and a moving basis costs a term: the connection
C_mn = (a'/a) 2 s_m s_n u_m u_n / (u_m^2 - u_n^2), C_nn = 0,
real and antisymmetric in the true-power amplitude, so on its own it moves no power out of the retained set – it moves it between the channels, and the channels it fills are the lossier ones (Mode conversion in a capillary taper).
prop_capillary(taper, ..., wall=CoherentWall()) carries it, and so does a
multimode taper without a wall, by default, beside each channel’s own Marcatili loss
(the taper coupling, Multimode capillaries: the coherent wall, tapers and gradients); taper_coupling=False
is the
adiabatic route, on which every channel follows its local mode and linear taper
conversion is
omitted – the comparison drawn here. The run is cut into
sections on the linear operator’s own intervals; inside each, the complete wall
generator is frozen at the midpoint and integrated exactly, and everything the
freezing leaves out – the generator’s variation and the connection – is put back
as an explicit term in the Runge-Kutta source. This example runs a 50 to 40 um
transition over a fifth of a millimetre, launched into HE_11 alone, and checks
three numbers:
the walled taper keeps
0.9287of the arriving energy inHE_11and puts0.038inHE_12and0.033inHE_13, where the adiabatic route leaves5e-18there;it therefore loses
1.87e-3of the launched energy, more than either of the two straight bores it runs between, where the adiabatic taper loses6.4e-4;freezing is a rearrangement and not an approximation: one frozen section per interval and four of them agree to
1.9e-10of the state.
Every channel keeps its own normalisation. The cumulative operator beside a
coupled block must act on every channel alike, so it renormalises a vacuum amplitude
along the varying index by the lowest channel’s effective index; the flow’s own
diagonal carries the rest of each channel’s, -(1/2) d ln(w_n/w_1)/dz, so the
upper channels’ amplitudes are good to the step error.
import warnings
import matplotlib.pyplot as plt
import numpy as np
from solprop.boundaries import absorber
from solprop.driver import prop_capillary
from solprop.grid import FieldGrid
from solprop.modes import CoherentWall
from solprop.profiles import PiecewiseLinear
from solprop.stepper import FixedSchedule
The taper: straight, a linear transition, straight – the shape of a 125 to 62.5 um capillary taper, scaled to the radius and length a ten-step run allows. The corners of the profile are where the sections start and end, and the fixed schedule lands on every one of them (a schedule that comes near a corner without landing on it is refused by name).
length, steps = 2e-4, 10
taper = PiecewiseLinear.from_stages(
(0.0, 6e-5, 1.4e-4, 2e-4), [50e-6, 50e-6, 40e-6, 40e-6]
)
grid = FieldGrid.from_limits(length, 800e-9, (400e-9, 1.6e-6), 300e-15)
def run(wall, radius=taper, taper_coupling=True):
"""Propagate three channels ten steps along `radius`, with or without a wall."""
# The band runs past helium's and silica's fitted ranges and the geometry is a
# marginal one on purpose, so that a fifth of a millimetre shows the wall: both
# warn, and both are silenced here, as is the advisory step of the coupled flow.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
return prop_capillary(
radius,
length,
"He",
1.0,
lambda0=800e-9,
grid=grid,
absorber=absorber(grid, boundary_length=1e3),
fwhm=30e-15,
energy=1e-9,
modes=3,
n_save=11,
backend="numpy_ref",
wall=wall,
taper_coupling=taper_coupling,
schedule=FixedSchedule(np.full(steps, length / steps)),
dz_max=length / steps,
dz_init=length / steps,
plasma=False,
raman=False,
progress="none",
)
def channel_energy(output):
"""Return the positions and the energy in each channel at every save plane."""
planes = output.planes()
energy = np.array(
[np.asarray(plane.stats["energy"]).reshape(3, -1)[:, 0] for plane in planes]
)
return np.array([plane.z for plane in planes]), energy
walled, plain = run(CoherentWall()), run(None, taper_coupling=False)
coupled = run(None)
straight = {radius: run(CoherentWall(), radius) for radius in (50e-6, 40e-6)}
for name, output in (
("taper, CoherentWall()", walled),
("taper, no wall", coupled),
("taper, adiabatic", plain),
("50 um straight, wall", straight[50e-6]),
("40 um straight, wall", straight[40e-6]),
):
_, energy = channel_energy(output)
lost = 1.0 - energy[-1].sum() / energy[0].sum()
shares = energy[-1] / energy[-1].sum()
print(
f"{name:22s} lost {lost:.3e}; of what arrives: HE_11 {shares[0]:.5f}, "
f"HE_12 {shares[1]:.2e}, HE_13 {shares[2]:.2e}"
)
taper, CoherentWall() lost 1.667e-03; of what arrives: HE_11 0.92811, HE_12 3.86e-02, HE_13 3.33e-02
taper, no wall lost 6.265e-04; of what arrives: HE_11 0.92733, HE_12 3.88e-02, HE_13 3.38e-02
taper, adiabatic lost 6.423e-04; of what arrives: HE_11 1.00000, HE_12 4.85e-18, HE_13 1.02e-20
50 um straight, wall lost 4.409e-04; of what arrives: HE_11 1.00000, HE_12 2.57e-07, HE_13 6.27e-07
40 um straight, wall lost 8.575e-04; of what arrives: HE_11 1.00000, HE_12 9.76e-07, HE_13 2.34e-06
What the record says about the sectioning: how many frozen sections the run was
cut into, the Chebyshev degree along z the generator was resolved at, whether a
connection was carried, and the advisory step a declared dz_max is warned
against. Ten steps of 20 um sit inside it.
4 sections, n_deg 17, connection True, advisory step 1.053e-04 m
Freezing is a rearrangement. Four frozen sections per interval freeze four
different blocks and carry four different residuals, and the answer does not
move: the residual is evaluated exactly, so what sections buys is a smaller
explicit term, not a new physical model. At finite step size, changing this split can
still change the numerical error, so section and step refinement are distinct checks.
finer = run(CoherentWall(sections=4))
coarse_field = walled.planes()[-1].field
fine_field = finer.planes()[-1].field
departure = np.max(np.abs(fine_field - coarse_field)) / np.max(np.abs(coarse_field))
print(f"one section per interval against four: {departure:.2e} of the state")
fig, ax = plt.subplots(figsize=(6.4, 3.8), constrained_layout=True)
for output, style, label in (
(plain, ":", "adiabatic"),
(walled, "-", "CoherentWall()"),
):
z, energy = channel_energy(output)
shares = energy / energy[0, 0]
shares = np.where(shares > 0.0, shares, np.nan)
for channel in range(3):
ax.semilogy(
z * 1e6,
shares[:, channel],
style,
color=f"C{channel}",
label=f"HE_1{channel + 1}, {label}",
)
for corner in (60.0, 140.0):
ax.axvline(corner, color="grey", linewidth=0.6, linestyle="--")
ax.set(
xlabel="z (um); the taper runs between the dashed lines",
ylabel="Fraction of the launched energy",
title="A 50 to 40 um taper converts HE_11 into the lossier orders",
ylim=(1e-22, 3.0),
)
ax.legend(frameon=False, fontsize="small", ncol=2)
plt.show()

one section per interval against four: 3.42e-05 of the state