Skip to content

Releases: ehsun-sh/maiman-studio

0.17.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 17:07
9b9ea6d
  • A Maiman App shortcut with the app icon that opens the studio with Python alone (run python tools/make_shortcut.py once).
  • 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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 02 Oct 04:41

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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 23 Sep 15:19

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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 23 Sep 04:39

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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 23 Sep 02:04

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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 22 Sep 22:48

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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 20 Sep 01:02

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:

  1. Inside a span, as a rate the mixing integral carries down the span as the pumps decay, L_eff(z) rather than z. Summed as a series in r/α, or by quadrature past it; the two agree to 1e-12.
  2. 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.
  3. 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)²/4 and (1 − g)²/4, g the 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_g from 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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 19 Sep 03:55

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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 19 Sep 02:23

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_VECTORS to 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/spectrum builds the block from the same node a run would, then calls ScatteringDevice.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

Choose a tag to compare

@ehsun-sh ehsun-sh released this 18 Sep 18:36

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