Skip to content

Commit b7dbf13

Browse files
committed
Couple the channels: cross-phase modulation and four-wave mixing
Bands propagated independently through the fiber, which made this a good model of one channel and an optimistic one of a comb. The same |A|^2 A term that gives a channel its own self-phase modulation also lets every other channel rotate its phase, and lets triplets of them generate light at frequencies nobody launched. propagate_coupled_ssfm steps every channel together. The whole extension is a factor of two -- a channel's own power appears once in the expansion of |A|^2 A and a neighbour's appears twice -- written as 2*total - |A_k|^2 so the cost stays linear in the channel count and one field collapses to plain SPM identically. propagate_ssfm is now a call to it rather than a second implementation, and a test asserts they agree bit for bit. Walk-off is not a new parameter. It is the group-delay term of the same expansion of beta(omega) that produces the dispersion, so setting D to zero removes both at once -- which is the correct coupling, and the reason a dispersion-shifted fiber operated at its zero is the worst place to put a WDM comb. propagate_dispersion's docstring warned that adding a group-delay term ends the sign-convention immunity that beta2 alone enjoys; the operator is therefore derived from the beta2 term rather than guessed. Each field comes back in its own retarded frame, so the constant delay does not slide every channel off its own sampler. Four-wave mixing is solved in closed form from the band powers and injected as tones, because the products land at frequencies no band is sampled at. Not putting the channels on one grid is what makes a comb affordable, and that choice has to be paid for somewhere. A product landing on a channel is folded into it with a drawn phase, for the same reason PMD is drawn. Measured, on channel 1 of a four-channel QPSK comb over 4 x 80 km, as EVM after carrier recovery: launch D = 0 D = 17 ps/nm/km -3 dBm +0.18 % +0.00 % 0 dBm +0.49 % +0.03 % +3 dBm +2.84 % +0.29 % Ten times less penalty for having dispersion in the fiber. What walk-off removes is not the cross-phase modulation but its variation: the mean phase is fixed at 2*gamma*<P>*L_eff across a 16x change in slip while its spread falls 5.7x, and it is the variation that closes an eye. Validated against closed forms derived outside the code. n equal channels give exactly (2n-1) times one channel's nonlinear phase; the peak-to-peak swing is 2*gamma*P*L_eff; walk-off is D*delta_lambda; FWM efficiency is 1 phase matched and sinc^2 lossless; the component reproduces d^2 gamma^2 P^3 L_eff^2 eta exp(-alpha L) to 1e-7, cubic in power, with non-degenerate products exactly 6.02 dB above degenerate ones. 16 sabotages, 16 caught. Two rounds were needed: the first reported the doubled-walk-off sabotage as surviving, and it had. pytest.approx applies a default absolute tolerance of 1e-12 alongside rel, and a walk-off is ~1e-14 s/m, so the assertion accepted every value in a band five orders of magnitude wide. The same defect was already in test_amplifier: an ASE density is ~1e-17 W/Hz, and that assertion had been testing nothing at all, zero included. Both now pass abs=0.0, and the ASE one was confirmed to bite. A stale .pyc also served a sabotaged module after it had been restored -- the substitution leaves the file the same size -- so the harness clears bytecode between runs. The UI export had been stale since the filter commit and is regenerated: 33 manifests to 35, with the fiber's four new parameters, synced into the mockup and the screenshots. 589 tests.
1 parent 4cce684 commit b7dbf13

10 files changed

Lines changed: 1900 additions & 80 deletions

File tree

README.md

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@
2828
> from it** — signal-ASE and ASE-ASE beat noise are modelled in both detector families — and PMD
2929
> is drawn as a random realisation with the right Maxwellian statistics.
3030
>
31-
> **Not implemented yet:** cross-phase modulation and four-wave mixing between channels,
32-
> and the GUI. See the [roadmap](#roadmap).
31+
> Channels interact: the split-step propagates them coupled, so a neighbour's power modulates
32+
> each channel's phase at twice the rate its own does, sliding past under walk-off derived from
33+
> the dispersion — and triplets of channels mix to put light where nobody launched it.
34+
>
35+
> **Not implemented yet:** the GUI. See the [roadmap](#roadmap).
3336
>
3437
> This is not yet a useful simulator. It is a foundation with the expensive decisions made and
3538
> tested. Criticism of those decisions is worth more right now than any feature —
@@ -376,6 +379,71 @@ noise bins rendered onto one grid, the way an instrument shows them. Its resolut
376379
not cosmetic — widening it raises the ASE trace decibel for decibel and leaves a carrier exactly
377380
where it is, which is the clearest demonstration of why OSNR needs a stated reference bandwidth.
378381

382+
## What channels do to each other
383+
384+
Until recently bands propagated independently through the fiber, which made this a good model of
385+
one channel and an optimistic model of a comb. They no longer do. The same `|A|²A` term that gives
386+
a channel its own self-phase modulation lets every *other* channel rotate its phase — and the
387+
coefficient is not free. Expanding the term for a sum of carriers, a channel's own power appears
388+
once and a neighbour's appears twice, because there are two ways to choose which un-conjugated
389+
factor belongs to the neighbour and one way when it is the channel itself.
390+
391+
That factor of two is measurable, and it is exact:
392+
393+
| co-propagating channels | mean nonlinear phase | vs one channel | closed form |
394+
| :--- | ---: | ---: | ---: |
395+
| 1 | 0.027518 rad | 1.000× ||
396+
| 2 | 0.082559 rad | 3.000× ||
397+
| 3 | 0.137600 rad | 5.000× ||
398+
| 4 | 0.192640 rad | 7.000× ||
399+
400+
**Dispersion is the cure here, not the disease.** Chromatic dispersion is normally introduced as
401+
something to compensate. Between channels it is the only thing keeping them apart: it makes them
402+
travel at different speeds, so a neighbour's bit pattern *slides past* instead of sitting on top of
403+
the channel it is modulating. Walk-off is therefore not a separate parameter — it is the
404+
group-delay term of the same expansion of β(ω) that produces the dispersion, so setting D to zero
405+
removes both at once. At 17 ps/nm/km a 100 GHz neighbour separates by 13.62 ps/km, and has slid 140
406+
symbols by the end of a 320 km link.
407+
408+
What walk-off removes is not the cross-phase modulation but its *variation*. The mean phase shift
409+
is fixed by the neighbour's average power and no amount of sliding changes it — sliding
410+
redistributes in time, it does not destroy. That split is the whole mechanism, because a constant
411+
phase offset is absorbed by carrier recovery for free and it is the variation that closes an eye.
412+
Measured on channel 1 of a four-channel QPSK comb over 4 × 80 km, as EVM after carrier recovery:
413+
414+
| launch/channel | D = 0 | D = 17 ps/nm/km |
415+
| :--- | ---: | ---: |
416+
| −3 dBm | +0.18 % | +0.00 % |
417+
| 0 dBm | +0.49 % | +0.03 % |
418+
| +3 dBm | **+2.84 %** | **+0.29 %** |
419+
420+
Ten times less penalty for having dispersion in the fiber.
421+
422+
**Four-wave mixing lands on the channels.** Products appear at `f_i + f_j − f_k`, and on an equally
423+
spaced grid those frequencies *are* channel frequencies — so the crosstalk arrives in band, where
424+
no filter downstream can reach it. The model folds such a product into the channel it lands on,
425+
with a phase drawn from the run's generator for the same reason PMD is drawn: it is set by fiber
426+
details nobody measures. Dispersion suppresses mixing too, by dephasing it, and the phase mismatch
427+
grows as the *square* of the channel spacing:
428+
429+
| channel spacing | D = 0 | D = 2 | D = 17 |
430+
| :--- | ---: | ---: | ---: |
431+
| 25 GHz | 0.0 dB | −4.4 dB | −21.2 dB |
432+
| 50 GHz | 0.0 dB | −14.7 dB | −33.1 dB |
433+
| 100 GHz | 0.0 dB | −26.7 dB | −45.4 dB |
434+
| 200 GHz | 0.0 dB | −38.6 dB | −57.4 dB |
435+
436+
Zero-dispersion fiber is perfectly phase matched at every spacing, which is the whole reason
437+
dispersion-shifted fiber was abandoned for WDM.
438+
439+
The two effects are computed differently, and the difference is worth knowing before reading a
440+
number off the block. Cross-phase modulation is solved on the waveform inside the split-step,
441+
because it depends on the neighbour's instantaneous power sliding past; four-wave mixing is solved
442+
in closed form from the band powers and injected as tones, because the products land at frequencies
443+
no band is sampled at. Not putting the channels on one grid is what makes a WDM comb affordable at
444+
all, and that choice has to be paid for somewhere. See
445+
[`examples/wdm_nonlinear.py`](examples/wdm_nonlinear.py).
446+
379447
## What this is
380448

381449
A block-diagram simulator for optical systems: drop components on a canvas, wire a link, run it,
@@ -510,7 +578,7 @@ time window, and results are reproducible.
510578
| **1 — MVP: linear link** *(essentially done)* | ✅ PRBS → NRZ → laser → MZM → fiber (α + CD) → PIN → filter → eye/Q/BER, validated end to end. **Python only, no GUI.** | ~2–3 months |
511579
| **1.5 — Nonlinear & amplified**| Adaptive-step SSFM, Kerr, EDFA with ASE, OSNR, PMD, APD | ~2 months |
512580
| **2 — Coherent transceiver**| Gray-coded M-QAM to 256, IQ modulator with bias and quadrature error, 90° hybrid, balanced detection, blind carrier phase recovery, dual polarization with a blind butterfly equaliser, root-raised-cosine shaping and matched filtering, differential quadrant encoding, receiver-side dispersion compensation over spans to 1000 km, EVM/MER, constellation diagram, validated against closed-form SER | ~3 months |
513-
| **3 — GUI & WDM** | ✅ Wavelength-selective filters and an OSA. Session server, React Flow graph editor · DWDM + XPM/FWM crosstalk, 400G/800G references, CuPy back-end | ~6 months |
581+
| **3 — GUI & WDM** | ✅ Wavelength-selective filters, an OSA, and coupled-channel propagation: cross-phase modulation with walk-off, and four-wave mixing. Session server, React Flow graph editor · 400G/800G references, CuPy back-end | ~6 months |
514582
| **4 — PIC** | Waveguides, ring resonators, MMI, MZI via integration with an existing S-matrix solver; PDK import ||
515583

516584
¹ One developer, part-time. Estimates, not commitments.
@@ -561,7 +629,13 @@ Every physics block ships with a test against a closed-form result, run in CI
561629
| Matched filtering | Costs `10·log10(f_s/R_s)` to omit — the receiver integrates noise it cannot use ||
562630
| **PMD** | DGD Maxwellian: `⟨τ²⟩/⟨τ⟩² = 3π/8`, mean `∝√L`, spread `0.42·mean` ||
563631
| APD | `F(M) = kM + (2−1/M)(1−k)`; an **interior optimum gain** exists ||
564-
| XPM / FWM | Cross-channel nonlinearity ||
632+
| **Cross-phase modulation** | `n` equal channels give `(2n−1)×` one channel's nonlinear phase — exact to 1e-3 ||
633+
| XPM swing | Peak-to-peak `2·γ·P·L_eff` on a probe beside an on/off pump, with no walk-off ||
634+
| Walk-off | `D·Δλ` per unit length, derived from β₂ and not declared beside it ||
635+
| Walk-off conserves the mean | Mean XPM phase fixed at `2·γ·⟨P⟩·L_eff` across a 16× change in slip, while its spread falls 5.7× ||
636+
| FWM efficiency | `η → 1` phase matched; `→ sinc²(Δβ·L/2)` lossless; even in Δβ ||
637+
| FWM phase mismatch | `Δβ = −β₂(ω_i−ω_k)(ω_j−ω_k)` — quadratic in spacing, zero at zero dispersion ||
638+
| FWM product power | Component reproduces `d²γ²P_iP_jP_k·L_eff²·η·e^{−αL}` to 1e-7; cubic in power; `d = 2−δ_ij` gives non-degenerate products exactly 6.02 dB ||
565639

566640
Component models are derived from published literature and standards (Agrawal, *Nonlinear Fiber
567641
Optics*; ITU-T G.652 / G.694.1; relevant IEEE 802.3 clauses), cited in each component's

docs/images/studio-graphite.png

100 Bytes
Loading

docs/images/studio-paper.png

96 Bytes
Loading

docs/ui-mockup.html

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,11 @@
854854
"default": true,
855855
"doc": "Add thermal (Johnson) noise"
856856
},
857+
"ase_beat_noise": {
858+
"type": "bool",
859+
"default": true,
860+
"doc": "Add signal-ASE and ASE-ASE beat noise"
861+
},
857862
"gain": {
858863
"type": "float",
859864
"default": 10.0,
@@ -1087,6 +1092,11 @@
10871092
"type": "bool",
10881093
"default": true,
10891094
"doc": "Add thermal (Johnson) noise"
1095+
},
1096+
"ase_beat_noise": {
1097+
"type": "bool",
1098+
"default": true,
1099+
"doc": "Add LO-ASE beat noise"
10901100
}
10911101
},
10921102
"ports": {
@@ -1290,6 +1300,11 @@
12901300
"type": "bool",
12911301
"default": true,
12921302
"doc": "Add thermal (Johnson) noise"
1303+
},
1304+
"ase_beat_noise": {
1305+
"type": "bool",
1306+
"default": true,
1307+
"doc": "Add LO-ASE beat noise"
12931308
}
12941309
},
12951310
"ports": {
@@ -1460,6 +1475,30 @@
14601475
"min": 1e-06,
14611476
"doc": "Largest nonlinear phase rotation allowed per split-step [rad]"
14621477
},
1478+
"cross_phase_modulation": {
1479+
"type": "bool",
1480+
"default": true,
1481+
"doc": "Couple the bands: each is phase-modulated by the others' power"
1482+
},
1483+
"max_walkoff_slip": {
1484+
"type": "float",
1485+
"default": 0.5,
1486+
"unit": "",
1487+
"min": 0.001,
1488+
"doc": "Largest relative slip between bands allowed per split-step [samples]"
1489+
},
1490+
"four_wave_mixing": {
1491+
"type": "bool",
1492+
"default": true,
1493+
"doc": "Generate mixing products between bands"
1494+
},
1495+
"mixing_floor": {
1496+
"type": "float",
1497+
"default": 70.0,
1498+
"unit": "dB",
1499+
"min": 0.0,
1500+
"doc": "Discard mixing products this far below the strongest band"
1501+
},
14631502
"pmd_coefficient": {
14641503
"type": "float",
14651504
"default": 0.0,
@@ -1786,6 +1825,107 @@
17861825
}
17871826
}
17881827
},
1828+
"OpticalFilter": {
1829+
"name": "Optical Filter",
1830+
"type": "OpticalFilter",
1831+
"class": "oosim.components.filters.OpticalFilter",
1832+
"category": "Passive",
1833+
"version": "0.1.0",
1834+
"parameters": {
1835+
"center_wavelength": {
1836+
"type": "float",
1837+
"default": 1550.0,
1838+
"unit": "nm",
1839+
"min": 1200.0,
1840+
"max": 1700.0,
1841+
"doc": "Centre of the passband"
1842+
},
1843+
"bandwidth": {
1844+
"type": "float",
1845+
"default": 50.0,
1846+
"unit": "GHz",
1847+
"min": 0.0,
1848+
"doc": "3 dB full width"
1849+
},
1850+
"order": {
1851+
"type": "float",
1852+
"default": 3.0,
1853+
"unit": "",
1854+
"min": 1.0,
1855+
"max": 10.0,
1856+
"doc": "Super-Gaussian order; 1 is Gaussian"
1857+
},
1858+
"insertion_loss": {
1859+
"type": "float",
1860+
"default": 0.0,
1861+
"unit": "dB",
1862+
"min": 0.0,
1863+
"doc": "Loss at peak transmission"
1864+
},
1865+
"extinction": {
1866+
"type": "float",
1867+
"default": 40.0,
1868+
"unit": "dB",
1869+
"min": 0.0,
1870+
"doc": "Out-of-band rejection floor; 0 disables the floor"
1871+
}
1872+
},
1873+
"ports": {
1874+
"inputs": {
1875+
"in": "optical"
1876+
},
1877+
"outputs": {
1878+
"out": "optical"
1879+
}
1880+
}
1881+
},
1882+
"OpticalSpectrumAnalyzer": {
1883+
"name": "Optical Spectrum Analyzer",
1884+
"type": "OpticalSpectrumAnalyzer",
1885+
"class": "oosim.components.filters.OpticalSpectrumAnalyzer",
1886+
"category": "Measurements",
1887+
"version": "0.1.0",
1888+
"parameters": {
1889+
"center_wavelength": {
1890+
"type": "float",
1891+
"default": 1550.0,
1892+
"unit": "nm",
1893+
"min": 1200.0,
1894+
"max": 1700.0,
1895+
"doc": "Centre of the displayed span"
1896+
},
1897+
"span": {
1898+
"type": "float",
1899+
"default": 1000.0,
1900+
"unit": "GHz",
1901+
"min": 1.0,
1902+
"doc": "Displayed frequency span"
1903+
},
1904+
"points": {
1905+
"type": "float",
1906+
"default": 1024.0,
1907+
"unit": "",
1908+
"min": 16.0,
1909+
"max": 16384.0,
1910+
"doc": "Trace points"
1911+
},
1912+
"resolution_bandwidth": {
1913+
"type": "float",
1914+
"default": 12.5,
1915+
"unit": "GHz",
1916+
"min": 0.001,
1917+
"doc": "Instrument resolution; 12.5 GHz is 0.1 nm at 1550 nm"
1918+
}
1919+
},
1920+
"ports": {
1921+
"inputs": {
1922+
"in": "optical"
1923+
},
1924+
"outputs": {
1925+
"out": "metric"
1926+
}
1927+
}
1928+
},
17891929
"PINPhotodiode": {
17901930
"name": "PIN Photodiode",
17911931
"type": "PINPhotodiode",
@@ -1830,6 +1970,11 @@
18301970
"type": "bool",
18311971
"default": true,
18321972
"doc": "Add thermal (Johnson) noise"
1973+
},
1974+
"ase_beat_noise": {
1975+
"type": "bool",
1976+
"default": true,
1977+
"doc": "Add signal-ASE and ASE-ASE beat noise"
18331978
}
18341979
},
18351980
"ports": {

0 commit comments

Comments
 (0)