Reading the output of a modal run#
Every run returns an output handler — in memory, or an HDF5 file with output= — and
everything is read from it through the accessors of solprop.processing and the
figures of solprop.plotting, exactly as for any other run (analysing a
run, output). This
chapter covers what is different about a capillary with a coherent wall and about the
general modal route: which quantities a basis defines, what the record holds, and how the
physical fields are rebuilt. A saved plane read as a field plane, with its reference,
metric and provenance, for transport or for launching another run, is
field_plane() (coupling simulations).
A capillary with a coherent wall#
Nothing changes. The coupled flow couples the modes; it does not change the basis the
state is written in, so each saved channel is still a Marcatili mode’s true-power
amplitude, and every ordinary accessor — energy per mode, peak_power, fwhm_t,
get_spectral_density, the propagation maps — reads it as on any modal run. On a taper each
mode’s amplitude carries that mode’s own normalisation at every position, so a mode’s
energy is that mode’s energy there. What the wall adds is the record’s flow entry
(the capillary chapter).
The general modal route#
A general modal run’s state is a set of channel amplitudes in √W, one per channel of the guide, normalised to unit Poynting flux. On the provider’s own basis they are orthonormal in true power; on a declared basis (a circular pair, a rotated split block) they are not orthogonal in the same sense, and the basis carries a dense metric \(W\). Two consequences decide what a run reports.
The statistics are the physical ones, and they are scalars. physical_energy is the
energy in the physical field, \(\int \mathbf a^{\mathsf H}W\mathbf a\,d\omega\) summed
over the channels; analytic_flux is the peak over time of the flux built from the
same pairing of each frequency with itself; spectral_flux, requested by name, is the
spectral density of physical_energy, in J/(rad/s). On the
provider’s own basis these are exactly sum(energy) and peak_power_total; on a
declared one they are what those would have to become. The names that promise a
per-channel energy or power (energy, peak_power, peak_power_total,
energy_window, peak_power_window, peak_intensity) are refused by name on this
route, each with its replacement, rather than redefined; quadrature_error is refused
too, because a provider’s node rule has no embedded coarse rule to compare with — there
is no automatic transverse-resolution alarm during a general modal run. fwhm_t,
fwhm_t_outer and omega0 stay, taking the metric. zdw stays, except beside a split
block, which has no per-channel propagation constant.
import numpy as np
from solprop.driver import prop_waveguide
from solprop.exceptions import OutputError
from solprop.grid import FieldGrid
from solprop.modes import (
IndexMaterial,
MaterialKerr,
MaterialRegion,
ModalGuide,
StepIndexMode,
)
from solprop.processing import get_spectral_density
from solprop.processing import energy as energy_per_mode
from solprop.processing.modal import electric_field, physical_energy, reconstructed_flux
from solprop.stats import StatsSpec
from solprop.translate.general import general_modal_route
core, cladding = IndexMaterial(1.45), IndexMaterial(1.44)
pair = [
StepIndexMode(6e-6, core=1.45, cladding=1.44, profile_wavelength=835e-9, parity=p)
for p in ("even", "odd")
]
regions = (
MaterialRegion("core", 0.0, 6e-6, core, (MaterialKerr(n2=2.6e-20, at=835e-9),)),
MaterialRegion("cladding", 6e-6, None, cladding),
)
provider = ModalGuide.from_step_index(
pair, core=core, cladding=cladding, regions=regions
).field_provider
grid = FieldGrid.from_limits(1e-3, 835e-9, (790e-9, 890e-9), 200e-15)
active = grid.omega[grid.active_mask]
low = 0.99 * float(active[0])
guide = ModalGuide.from_provider(
provider,
np.linspace(low, 1.01 * float(active[-1]), 25),
nodes=provider.node_rule(low, n_r=24),
degenerate_blocks=((0, 1),),
)
run = prop_waveguide(
(),
1e-3,
provider=guide,
grid=grid,
lambda0=835e-9,
fwhm=40e-15,
energy=1e-9,
n_save=3,
dz_max=1e-4,
dz_init=1e-4,
progress="none",
stats=StatsSpec(names=("physical_energy", "analytic_flux", "spectral_flux")),
)
print(run.metadata.modal["accessors"])
print("physical energy (J):", physical_energy(run))
try:
energy_per_mode(run)
except OutputError as refusal:
print("refused:", str(refusal)[:100], "...")
The spectra are read as for any run — get_spectral_density sums the channels’ true-power
spectral densities:
wavelength, density = get_spectral_density(run, "wavelength", plane=-1, modes="sum")
print(f"spectral peak at {wavelength[np.argmax(density)] * 1e9:.1f} nm")
The file holds amplitudes, not fields. The run’s record names the guide — the
provider, the sampling, the materials and their responses, the channel labels row by row,
the table and metric identities with their measured Gram departures, and the basis — but
not its tens of megabytes of stored E and H. The physical fields are rebuilt from the
guide: general_modal_route() gives the run’s own tables,
and every reconstruction accessor takes them and the guide, and checks both against
the identity the file carries, path by path, before it reads anything:
route = general_modal_route(guide, grid, z_max=1e-3, omega_reference=float(grid.omega0))
field = electric_field(run, route.tables, plane=-1, guide=guide) # V/m, (nodes, 3, t)
flux = reconstructed_flux(run, route.tables, plane=-1, guide=guide) # W, (t,)
density = reconstructed_flux(run, route.tables, plane=-1, density=True, guide=guide)
print(field.shape, f"peak flux {flux.max():.3e} W")
print(f"peak flux density {density.max() * 1e-4:.3e} W/cm^2")
reconstructed_flux is the flux of the reconstructed fields, a trace in time, which
carries the guide’s cross-frequency flux kernel as well; the run’s own analytic_flux
statistic is the peak of the same-frequency one, and the two peaks differ by at most
\(\sum_{ij}|B_i||B_j||K_{ij}-1|\), where \(K_{ij}\) is the guide’s flux kernel between
frequencies \(\omega_i\) and \(\omega_j\), one on its diagonal (measured: 1.3e-5 of the peak
on a strongly guiding strand). Their time integrals agree to far better than that,
because the cross-frequency terms integrate away. instantaneous_flux (the real fields’
flux, with its sum-frequency term), magnetic_field and coordinate_intensity (the
flux density each channel would carry alone, which is not additive) are the others,
and plane_nodes returns the node positions and area weights they are all given on;
output and
statistics have the definitions.
What the file’s key protects. A general modal file resumes only against the same guide, table, materials, channel order and basis — every one of them is in the resume key — and a reconstruction is refused against a guide that is not the one the file names. Keep the script that builds the guide beside the file:
from solprop.output import open_file
run.to_file("fibre.h5")
with open_file("fibre.h5") as saved:
again = reconstructed_flux(saved, route.tables, plane=-1, guide=guide)
print("the same flux from the file:", bool(np.allclose(again, flux)))