Repository navigation
Releases: ehsun-sh/maiman-studio
Release list
0.17.0
- A Maiman App shortcut with the app icon that opens the studio with Python alone (run
python tools/make_shortcut.pyonce). - Examples in seven phases, from first light to real-world systems, now with distributed Raman amplification. The Examples menu is grouped by phase.
- Lessons, notes, frames and arrows on the canvas.
- Opening or starting a project now clears every instrument.
0.16.0
pip install maiman==0.16.0
Four-wave mixing is solved exactly now, where it used to be a truncated series.
Constant tones: every order at once
tone_solver integrates the coupled equations of the launched tones and every product above
mixing_floor through the span (kernels.fwm_tone_solve, a Dormand–Prince pair). Cross-phase,
depletion and every order of mixing are in them. It lands on the split-step to five digits where
the series was 25 % off at 30 mW over 20 km, or 2.3 times out over four spans. It carries:
- two polarizations, coupled by silica's isotropic Kerr tensor;
- PMD, applied at each tone's own frequency, with the waveplates at the pieces' midpoints;
- stimulated Raman scattering, as a power coupling that leaves the mixing phases alone.
Modulated channels: one wide grid
composite_fwm split-steps every band on one composite grid, so modulated channels mix
exactly. It carries two polarizations and PMD (measured from the first band), lets outer slots keep
a wide product's skirt, and adds Raman inside the step. The step is Yoshida's fourth-order
composition, so a step several times longer gives the same answer; products agree to about 2e-4.
Phases that are carried, not drawn
A mixing product's carrier phase is carried from span to span, expanded about the first band with
the β2 the mixing uses, and a coherent receiver beats its local oscillator by the phase both paths
carry. With carry_phase and cascaded_fwm, second-order products add across spans as the
split-step's do.
The second-order series stays as the cheap path. A new test pins why it stops there: split by
order against the exact tone equations, one 10 km span at 10 mW is 0.82 of the exact power at
γ², 1.15 with γ³ and 1.03 with γ⁴. The model's folded pump phase (0.89) already beats the next
truncation.
Around it
- Pump loop: proportional and derivative terms, a loop filter, a delay, a noisy detector, a
dither, and back-calculation anti-windup (Åström & Hägglund). - EDFA: ASE shaped by a measured erbium spectrum, held to Giles and Desurvire's closed forms.
- Gratings: solved in TM as well as TE, with Li's inverse rule for the field across the
walls. A finite grating coupler is solved with its guided light carried off, and converges with
a stronger absorber and room for the beam. - W-Port receivers: real log-likelihood ratios, with the shaper's prior on the amplitude bit;
802.3 error blocks mark what a failed CRC32 covered. - Lasers: junction heating through a Foster network of package stages. Fibres take a
dispersive surrounding medium from Sellmeier terms.
65 components, 2274 tests, every physics block checked against a closed-form or independently
computed result in CI.
🤖 Generated with Claude Code
0.15.0
pip install maiman==0.15.0
The shaped path can be read back, which closes the W-Port: all eight of its interfaces, DO and DPO,
now run TP0 to TP7 and back, against the specification's own test vectors.
Through the encoder's door
Clause 9.2.4 permutes the last 35 bits of every codeword — eighteen information bits and all
seventeen parity ones — after the parity is computed. There are four orderings and a codeword takes
the one its index selects, r % 4. It is applied to the encoder's matrix, not to its output,
and that distinction is the whole of it: a row twenty back is read as this one's front half, so by
the time the parity that follows is computed it is parity over permuted bits, and no reordering of
the output can put that right.
Figure 27's table did not survive the document's conversion into a page of run-together digits, so
the four orderings were read off the specification's own TP4 and TP5 — for every mode and every
encoder — by matching each tail bit's values across the blocks where no front half has been read
yet and nothing but a permutation can have happened.
With it, dpo_transmit goes TP0 to TP7 bit for bit for FlexO-6(e), FlexO-8e and FlexO-8, reusing
the interleavers, the symbol mapper and the DSP frame the DO modes use, unchanged.
And back: two views of one matrix
The same feedback that makes the permute work makes it interesting to undo. Un-permuting the whole
stream restores every back half and breaks every front half; leaving it alone breaks the backs.
Neither single view decodes.
So the decoder holds both. A codeword's front half is what the line carried, because that is what
its parity was taken over; its back half is that same line untangled, because the permute happened
after. ofec_decode_stream takes a tail and does exactly that — the gather untangles, the scatter
re-tangles, the information comes out of the untangled view — and nothing else about the iteration
changes.
The measurement that settles it, and the tests hold all three:
a clean shaped stream, decoded ... bits "corrected"
as the line carried it some hundreds
untangled wholesale first some hundreds, a different set
with both views none
dpo_receive runs the whole shaped path backwards — deframe, demap, deinterleave, decode with the
tail, unshape, descramble, de-adapt — and hands back the FlexO information with its CRC32 flags. It
round-trips every mode exactly, and repairs four hundred flipped symbols with the CRC32s agreeing
afterwards.
What is left
On the W-Port, nothing. The shaping tables are still normative data loaded from
MAIMAN_WPORT_TABLES rather than shipped, error marking stops at a flag per CRC32 because this
project has no client layer to hand it to, and the decoder is this project's own throughout — the
specification leaves decoding open. Each of those is stated where it applies.
65 components, 1910 tests, every physics block checked against a closed-form or independently
computed result in CI.
🤖 Generated with Claude Code
0.14.0
pip install maiman==0.14.0
The W-Port's DPO path, which the last release wrote off as unreachable and which turned out not to be.
Probabilistic constellation shaping
maiman.pcs is clause 9: the transcoder that makes FlexO-6(e), FlexO-8e and FlexO-8 DPO
different from their DO namesakes. It does not change the constellation — the line is DP-16QAM
either way — it changes which of its points come up. Each 16QAM amplitude is one bit of a pair,
and shaping makes that bit the inner amplitude more often than the outer one:
mode b shaping rate P(inner) net bits/symbol
FlexO-6(e) 72 33.7 % 0.8265 2.594
FlexO-8e 106 11.0 % 0.681 3.125
FlexO-8 116 5.71 % 0.623 3.281
Those probabilities are written nowhere in the code. The two lookup tables are bijections on
their words ordered by falling weight, so a field of nine bits reaches only the heavy half of a
ten-bit table — fewer indices, all of them low-energy words — and the measured probability falls
out of how far in a field can reach. The tests read it back off the shaper's output and compare
with the specification's Table 4.
The chain: the scrambled stream is split a bit at a time between the four encoders, each share is
cut into 84 coder blocks, and each block alternates chunks of shaping bits with runs of sign bits.
Every chunk feeds four lookup arrays round robin; each array cuts its b bits into twelve fields
that index the tables least significant bit first; each 128-bit result is rewired — a different
permutation per polarization, quadrature and encoder — and the four are bit-interleaved into a
512-bit column. Test points TP3 and TP4 come out bit for bit, for all three modes, and the shaping
inverts exactly.
The adaptation and the scrambler needed only a mode table: the DPO modes read the FlexO structure
in 2,056-bit columns instead of 10,280-bit ones, and twenty narrow rows are four wide ones, so the
CRC32 covers the same 41,120 bits either way. 433 rows and 22 CRC32s for FlexO-6(e), 522 and 27 for
FlexO-8e, 548 and 28 for FlexO-8. Verified against TP0, TP1 and TP2 for each.
Why it was reachable after all
0.13.0 said the shaping lookup tables were published only inside the document and could not be had.
That was true of the PDF. The MSA wiki also carries the specification as a .docx, and the .docx
still has the OLE attachments the PDF dropped — SCS_LUT10, SCS_LUT11 and the rewiring table. The
same wiki's test vector archive has b72, b106 and b116 members beside the qpsk and 16qam
ones this project already used, so every DPO test point is checkable.
The tables are not in this repository. They are normative data, and their within-weight ordering
is not an enumeration anything here could reproduce — colex, lexicographic and numeric were all
tried and none of them is it. So they are handled the way the OFEC vectors are: point
MAIMAN_WPORT_TABLES at a directory holding them and the shaper runs; without it, it raises and
says what is missing. What does ship is the block order — 3,552 numbers saying where each bit of an
encoder's block comes from — because that is a fact about the block's own bit classes rather than a
table quoted from the document, and it is the same permutation for every mode and every encoder.
Where the DPO path stops
At the encoder's door. That path's encoder output ordering is not the DO path's, and clause 9.2.4
permutes the last 35 bits of each codeword back through the code, so TP4 to TP5 is not modelled and
a DPO transmitter cannot yet be run to TP7. That is the one thing left on the Open list, and it is
smaller than what it replaced.
65 components, 1891 tests, every physics block checked against a closed-form or independently
computed result in CI.
🤖 Generated with Claude Code
0.13.0
pip install maiman==0.13.0
The last two items on the Open list, and one of them had been there since the list was written.
Mode partition noise, made by the link
It used to be computed at the laser, by dispersed_power, and the reason was written down as the
open item: every band here is an envelope in its own retarded frame, so the constant group delay a
span gives it has been divided out, and a detector summing several bands adds them as though they
had all arrived at once — and reports the laser's quiet total, which is the one thing that is not
true a span later.
So the delay is carried rather than applied. WalkoffHistory rides on the signal beside the
dispersion and Kerr histories, one number per carrier; a span with carry_walkoff adds each band's
D L dlambda to it; and the detector spends it once, delaying each band's power before the sum.
FabryPerotLaser is the source that needs it — seven longitudinal modes on one reservoir, each a
band, from the multimode rate equations that were already here.
7 modes, 1.1 nm apart, 17 ps/nm/km, a 12.8 ns window
span modes arrive over received noise (relative variance)
0 km 0 ps 1.58e-2 the partition cancelled in the sum
5 km 561 ps 1.29e-2 still largely correlated
20 km 2244 ps 3.33e-2
50 km 5610 ps 6.47e-2 against a ceiling of 8.58e-2
The ceiling is the sum of the modes' own variances — what a receiver sees once nothing cancels at
all — and it is approached from below. The dip at five kilometres is not a glitch: a delay shorter
than the fluctuations' own correlation time leaves them partly aligned.
Held against dispersed_power, which does the whole thing at the laser and shares no code with
this, to a thousandth of the mean. carry_walkoff starts false, so a link that does not ask sees
exactly what it saw before.
The W-Port framing around OFEC, TP0 to TP7
maiman.ofec ended where the code ends and said so. maiman.wport is everything either side of
it — clause 8 before, clauses 11 and 12 after — written with the same document open and held to the
same standard, which is the specification's own test vectors. The compliance point is TP7 from TP0
and every point between is an implementation choice; this reproduces all of them, for DP-QPSK and
DP-16QAM alike.
TP0 58 x 10,280 bits of FlexO-4(e) 116 rows for FlexO-8(e)
a CRC32 every four rows, then a CRC32 and zero pad
TP1 596,736 bits 1,193,472
scrambled, x^16 + x^12 + x^3 + x + 1, reset to 0xFFFF each block
TP2 the codec payload maiman.ofec takes it from here
TP6 688,128 line bits 1,376,256
four bits to a DP-QPSK symbol, eight to a DP-16QAM one
FAW, training, reserved and pilots inserted
TP7 175,104 symbols in each polarization
A DSP frame is 24 subframes of 7,296 symbols, and its budget closes exactly: 2,736 pilots, 240
training symbols that are not pilots, 22 + 74 of alignment and reserve, and 172,032 payload — which
is precisely what the symbol mapper made.
Two readings the text leaves open, which only one answer to reproduces the vectors. The reserved
symbols straddle the pilot at slot 64 rather than displacing it, because the overhead goes in
first and the pilot grid second. And the pilot PRBS10's seeds appear at its output least significant
bit first, which makes its recurrence the stated polynomial's reciprocal — the same register read
from the other end, and the only reading that gives Table 15.
The CRC32 has one more hold on it than the vectors: 0xFC891918, the check value catalogued for
CRC-32/BZIP2, which is what this clause's procedure is over a byte string's bits.
Not covered, and said so: the DPO path, FlexO-6(e)/8(e) with probabilistic constellation shaping,
whose shaping lookup table the published vectors do not exercise. Writing one from memory is the
thing maiman.ofec was careful not to do — and it is now the only thing left on the Open list.
65 components, 1872 tests, every physics block checked against a closed-form or independently
computed result in CI.
🤖 Generated with Claude Code
0.12.0
pip install maiman==0.12.0
Two more items off the Open list. Both are behind flags that start false, so no result from an earlier release moves.
A grating coupler, from its own geometry
Four things that were numbers or were missing.
What its teeth send back. The same corrugation that radiates on its first order couples the forward mode to the backward one on its second — a Bragg grating written in the chip. Set from_teeth and it is solved rather than quoted:
| fibre angle | period that radiates there | reflection |
|---|---|---|
| 0° | 572.0 nm | −1.6 dB |
| 5° | 591.0 nm | −19.4 dB |
| 10° | 611.1 nm | −21.5 dB |
| 15° | 632.3 nm | −26.2 dB |
- The angle is the whole defence: a vertical coupler reflects its own Bragg line, and the detuning that keeps a tilted one quiet is the same
2π n sin θ / λthat sets the emission angle. - A duty cycle of exactly one half has no second harmonic, and reflects nothing.
- Checked against this library's own transfer-matrix grating, which shares no algebra with the closed form, to 1e-9.
The height the fibre sits at. fibre_height widens the beam where it lands — 150 µm up costs more than a decibel — curves it, which the overlap integral carries as a phase, and makes the gap an etalon ringing at λ²/(2nh), measured against that to 5 %. The tilt walks each round trip 2h tan θ sideways, so the ripple falls from 0.81 dB at normal incidence to 0.06 dB at 12°.
A bottom mirror. substrate_extinction makes the substrate absorbing — aluminium is 1.44 + 16j — and the share leaving upward rises from 0.59 to 0.99. An index with the other sign is a gain medium under this convention and is now refused rather than quietly flipped.
Apodization. grating_strength_end tapers the radiation along the grating; the emitted profile follows from energy conservation alone:
uniform, 0.14 /um -3.19 dB
tapered, 0.05 to 0.4 /um -2.63 dB
with a mirror as well -0.48 dB
A laser's junction warms, and its line moves
Most of a laser's drive becomes heat: I(V_j + I R_s) in, and only the light carries any away. Set thermal and the junction warms by R_th times what is left, moving the line by 0.09 nm/K. Two halves, on two time axes:
The bias sets where the line sits.
| bias | junction | wavelength | against cold |
|---|---|---|---|
| 20 mA | 0.90 K | 1310.0810 nm | −14.1 GHz |
| 35 mA | 1.60 K | 1310.1437 nm | −25.1 GHz |
| 50 mA | 2.36 K | 1310.2128 nm | −37.2 GHz |
| 80 mA | 4.20 K | 1310.3782 nm | −66.1 GHz |
That is 4 to 6 pm per milliamp, what a DFB is specified at, and it belongs to the band's own centre frequency: a window of a few hundred nanoseconds cannot hold 25 GHz as a phase ramp.
The pattern moves it, slowly. The junction follows its dissipation through one pole of about a microsecond, so a long mark warms it and the line drifts red — thermal chirp, the low-frequency tail under the carriers' adiabatic chirp, and the reason a line code's run length matters here:
mark, 30 to 50 mA junction line drifts
100 ns +0.095 K -1.34 GHz
500 ns +0.394 K -6.03 GHz
2000 ns +0.865 K -13.45 GHz
The temperature is a third state beside the carriers and the photons, started settled as they are. One test runs it to rest against R_th P_diss — two code paths, one closed form — and another measures the pole's time constant back out of a step, to 5 %.
64 components, 1815 tests, every physics block checked against a closed-form or independently computed result in CI.
🤖 Generated with Claude Code
v0.11.0 — the pumps' own phase, a comb split by polarization, and a grating written twice
pip install maiman==0.11.0
Three more items off the Open list. Each is behind a flag that starts false, so no result from an earlier release moves.
Four-wave mixing feels the pumps' own phase
Self- and cross-phase modulation turn a mixing product's drive, A_i A_j A_k*, and the carrier the product lands on by different amounts — the textbook −γ(P_i + P_j − P_k − P_F), since the cross-phase of everything else turns every carrier alike and cancels. Set pump_phase on the fibre and that enters in three places:
- Inside a span, as a rate the mixing integral carries down the span as the pumps decay,
L_eff(z)rather thanz. Summed as a series inr/α, or by quadrature past it; the two agree to 1e-12. - Between spans, through a Kerr history the signal now carries — each carrier's angle, and the angle a weak carrier on the same path would have. It is the nonlinear counterpart of the accumulated dispersion, and every optical block carries it through.
- In the product's frame. The split-step keeps turning the band a product was added to, so a new span's share has to arrive turned the same way.
What checks it is the split-step solution of the same fibre with every tone in one band, which has no mixing model at all. At 20 mW, after four amplified 80 km spans:
| D [ps/(nm·km)] | linear mismatch ÷ split-step | with pump_phase ÷ split-step |
|---|---|---|
| +4 | 23.9 | 1.04 |
| −4 | 0.044 | 1.01 |
| +17 | 0.021 | 0.99 |
A join of two paths that went through different Kerr fibre is recorded rather than refused; only a span that asks for pump_phase refuses it.
A tilted grating's comb, polarization by polarization
Set vector on a TiltedFiberBraggGrating and it solves the cladding's true HE, EH, TE and TM modes and couples each input polarization on its own. With the fringes tilted in x–z, each mode of order ν has two orientations of one effective index, and one sign separates the two polarizations:
π ∫_core [ J_{ν−1}(K_t r) S ∓ J_{ν+1}(K_t r) D ] r dr
S = e_r e_r' + e_φ e_φ' D = e_r e_r' − e_φ e_φ' minus for p, plus for s
- p reaches TM0m and never TE0m; s the reverse, exactly.
- Untilted it is the long-period grating's vector coupling to 1e-12, and the core's own reflection matches the scalar core's to 0.2 %.
- In weak guidance each LP family's vector modes together carry what the scalar LP mode does, to 2 %.
Near the cladding's cutoff the split is largest: five nanometres below the Bragg line, a 30 mm grating's two polarizations differ by up to 14 dB. The block sends the field's x through p's comb and y through s's. The vector modes cost about ten times the scalar ones, and the two polarizations share one mode solve.
Two long-period gratings, and the cladding light comes back
A single grating's cladding light is lost under the coating. Set pair and the grating is written twice, separation apart over bare fibre: the first splits the core's light, the two run at different speeds, the second recombines them. That is a Mach-Zehnder interferometer inside one fibre, and the notch fills with fringes.
- No gap is one grating twice as long, to 1e-12.
- At a 3 dB split the fringes swing between
(1 + g)²/4and(1 − g)²/4,gthe cladding amplitude surviving the gap — a perfect null with no loss, matched to 1e-4 at 0, 3 and 10 dB. - The fringe spacing is
λ²/(Δn_g d)withΔn_gfrom the mode solver: within 5 % at a 20 cm gap, 2 % at 80 cm. - At a full crossover the pair passes back everything one grating cuts.
64 components, 1782 tests, every physics block checked against a closed-form or independently computed result in CI.
🤖 Generated with Claude Code
v0.10.0 — an amplifier's own noise empties it, and the glass disperses
pip install maiman==0.10.0
This release removes two items from the Open list. Both are opt-in, so no result from an earlier release moves.
An amplifier's own noise empties it
Saleh's equation is the reservoir's energy balance: P_sat ln(G₀/G) is everything the inversion hands out, over every field in the fibre. The amplifier's own spontaneous emission is one of those fields. It has no input, and it leaves by both ends, each carrying NF·G·hν·B — exactly what the block emits into the link. Set EDFA(saturate=True, self_saturation=True) and that load is counted:
ln(G₀/G) = [(G − 1)·P_in + 2·NF·G·hν·B] / P_sat
With no input the gain is W(βG₀)/β in Lambert's W. That is the gain a coil can hold before its own noise empties it. The test solves W by Halley's iteration, which shares nothing with the component's own solver, and the two agree to 1e-12. With the defaults:
| small-signal | gain in the dark | forward ASE | cost at −40 dBm in |
|---|---|---|---|
| 20 dB | 19.98 dB | −7.9 dBm | 0.02 dB |
| 30 dB | 29.81 dB | +1.9 dBm | 0.19 dB |
| 40 dB | 38.59 dB | +10.7 dBm | 1.38 dB |
At 0 dBm input, every one of them is within 0.02 dB of the model without it. The transient integrators carry the same term:
- an amplifier whose channels are all dropped comes to rest on Lambert's gain, not on
G₀; - it relaxes at
τ/(1 + (P_out + P_ASE)/P_sat), which a test measures to a part in a thousand.
The glass disperses
StepIndexFibre, LongPeriodGrating and TiltedFiberBraggGrating take material_dispersion:
- the cladding follows Malitson's fused silica;
- the core is germania-doped silica, interpolated in mole fraction toward Fleming's GeO₂, at the fraction that gives the quoted index step (3.5 mol % for 0.0052).
The quoted indices hold at 1550 nm. Every mode solver, scalar and vector, takes the fibre at the wavelength it is solving.
With nothing fitted, the default fibre becomes a G.652 fibre:
| constant indices | dispersing glass | G.652 | |
|---|---|---|---|
| D at 1550 nm | −4.8 ps/(nm·km) | 16.7 ps/(nm·km) | ≤ 18 |
| zero dispersion | none | 1308.3 nm | 1300 – 1324 nm |
Silica's own dispersion zero comes out at 1272.7 nm.
A long-period grating's notches are set by the difference of two indices that disperse differently, so they move by nanometres: the first moves 4.2 nm shortward and the fourth 1.4 nm longward. Each notch still phase-matches on the dispersive indices to 1e-9. A tilted grating's comb is read against indices that hold at 1550 nm, where the comb is, so it moves by picometres.
64 components, 1737 tests, every physics block checked against a closed-form or independently computed result in CI.
🤖 Generated with Claude Code
v0.9.0 — OFEC bit for bit, and a block's own spectrum in the studio
pip install maiman==0.9.0
Two things 0.8.0 did not have: the FEC that coherent pluggables actually run, and a way to look inside a single photonic block. Both are opt-in, so no result from an earlier release moves.
OFEC, bit for bit
maiman.ofec implements the Open ROADM MSA 6.0 W-Port encoder from the normative text. It has four encoders, each a semi-infinite staircase of 16 × 16 blocks, in which every bit is the front of one extended BCH(256, 239) codeword and the back of another twenty block-rows later. After them come the intra-block permutation of Table 8 and the inter-block interleaver.
It is checked against the specification's own test vectors, and no bit differs:
- the encoders turn TP2 into TP5;
- the interleavers turn TP5 into TP6.
That covers 2,752,512 bits for DP-QPSK and 5,505,024 for DP-16QAM. The text leaves two things open: which encoder each bit of a codec block feeds, and whether TP6 is merged from the two interleavers. Only one reading of each reproduces the vectors, and that is the one implemented.
The vectors come with no licence to redistribute, so they are not in the repository:
- setting
MAIMAN_OFEC_VECTORSto where they unzip runs them; - without them, a fingerprint of the output, taken while it matched, keeps it from drifting.
The specification leaves decoding open, so the decoder is this project's own. It is soft-in soft-out Chase on each bit's two codewords:
- at 1 % raw BER the output is clean;
- at the specification's 2.0e-2 threshold, three passes of Chase-4 leave one bit in eighty wrong;
- six passes of Chase-6 leave 17 in 1e5.
The gap to the threshold is the decoder's, not the code's, and the documentation says so.
This release also corrects what the project said about OFEC: it belongs to Open ROADM, G.709.6, OpenZR+ and 800ZR, not to OIF 400ZR, which uses another code.
A block's own spectrum in the studio
Select a grating, a ring, a coupler or any other photonic block. Its properties now offer Show its S-matrix spectrum. The new S-matrix tab plots every entry of the block's scattering matrix across a wavelength window, as power, phase or group delay, with no link run through it.
- Each device opens on the window worth looking at, such as a grating's line or four free spectral ranges of a ring, and the window can be moved.
POST /api/spectrumbuilds the block from the same node a run would, then callsScatteringDevice.spectrum(). A script can make the same call.- Group delay is a local derivative, not a differenced unwrap of the sampled phase. The unwrap turned a straight waveguide's 14 ps into −1 ps once the grid was coarser than the delay.
- Where an entry is 40 dB below its own peak, its phase means nothing. The delay is left undefined there, and the plot shows a gap instead of a spike.
A 10 mm Bragg grating reads 48 ps away from its line, which is one transit, and 72 ps at its band edge.
64 components, 1714 tests, every physics block checked against a closed-form or independently computed result in CI.
🤖 Generated with Claude Code
v0.8.0 — a cavity at the facet, a passband from the stack, a laser's own noise, and a receiver's equaliser
pip install maiman==0.8.0
Four pieces of physics that were compact numbers or missing in 0.7.0. Every one is opt-in, so no result from an earlier release moves.
An edge coupler's facets are a cavity
EdgeCoupler(etalon=True) sums the reflections between the fibre's facet and the chip's bounce by bounce, each beam diffracting over its own path with its own Gouy phase. Where nothing diffracts it is the Airy function of a thin film (5e-5); at zero gap it is one interface (1e-12); against a brute-force angular-spectrum propagation of the same cavity it agrees to 2e-4, and to 7e-3 at six degrees of fibre tilt.
Fifty microns of air ripples by 0.49 dB, fringes c / 2g apart. Tilting the fibre walks each bounce off the chip mode, and eight degrees takes the ripple to 0.04 dB — which is why fibre arrays are polished at an angle.
A grating coupler's passband from its stack
GratingCoupler(from_stack=True) computes what the PDK's two numbers stood for. The share of light going up rather than into the substrate comes from the silicon, the buried oxide and the substrate as a thin-film stack, checked against every layer's boundary conditions solved at once to 1e-12. The grating's exponential beam meets the fibre's Gaussian at best 80.1 %, the literature's number; and as the emission angle turns with wavelength while the fibre's does not, the overlap falls — which is the passband.
220 nm of silicon on 2 µm of oxide comes to 3.19 dB and 35.7 nm from its geometry alone. A 2.2 µm oxide buys back 1.1 dB.
A laser's own noise
DirectlyModulatedLaser(noise=True) and maiman.laser carry the textbook Langevin forces and nothing else. What the textbooks derive from them comes out:
| Schawlow–Townes | Henry | measured | |
|---|---|---|---|
| α = 0 | 3.23 MHz | 3.23 MHz | 3.08 MHz |
| α = 4 | 54.9 MHz | 56.2 MHz |
Henry's factor of seventeen is written nowhere in the integration. The intensity noise peaks at 5.07 GHz, where the laser rings (5.09).
integrate_multimode gives a Fabry–Perot laser its longitudinal modes on one carrier reservoir. The modes trade power, so the total is quiet — a twentieth of the sum of their noise for seven modes — and dispersed_power shows the cancellation undoing itself after a span: seven times the laser's own noise at 60 km. Mode partition is computed at the laser rather than in a link, because bands here are carried in their own retarded frames; the README says so where it applies.
An equaliser for a receiver in the field
FFEDFEEqualizer takes two samples a symbol with fractional: sampled half a symbol late, a symbol-spaced FFE is left with six percent of its symbols wrong; a T/2-spaced one decides every symbol at any phase. With blind it trains on its own decisions — landing exactly on the reference-trained taps up to a postcursor of 0.4, and failing at 0.5, which the tests keep in view.
64 components, 1686 tests, every physics block checked against a closed-form or independently computed result in CI.
🤖 Generated with Claude Code