Skip to content

Commit 84e411b

Browse files
ehsun-shclaude
andcommitted
Let the polarizations see each other
The Kerr coupling was scalar per polarization: a channel was modulated by its neighbours' co-polarized power and by nothing else, so a channel polarized across it counted for zero. In an isotropic medium orthogonal power counts for exactly two thirds — which, because co-polarized cross-phase modulation already carries its factor of two, makes orthogonal cross-phase modulation exactly one third of co-polarized. Measured on unmodulated carriers over a lossless dispersionless span, where the phase is a closed form: 2.000 gPL against 0.667. One coefficient, three consequences. A channel's own orthogonal component modulates it at two thirds the rate, so power split evenly across the axes turns each by 0.5 + (2/3)*0.5 instead of 0.5. The two axes stop accumulating the same phase, so nonlinear birefringence falls by a factor of three. And that difference is then driven by a *neighbour's* power, which is cross-polarization modulation — it arrives with the coupling rather than being added beside it. The solver takes a polarization label per field rather than a second field list, so the flat argument and the flat return survive and an unlabelled call is the scalar model term for term. The step-size bound had to learn the same weighting, or the two paths picked different steps and a single-axis run stopped being bit-identical to itself; written per field it is now exact rather than an upper bound, and reduces to the old expression algebraically for co-polarized fields. The value is the fixed-axis one from the chi(3) tensor, not the Manakov 8/9: this block applies PMD as a separate element rather than interleaving it, so the fixed-axis form is what is consistent with the rest of it. The coherent A_x* A_y**2 term is left out, and a test asserts each axis keeps its power to twelve digits, so that omission is a number rather than a sentence. Off by default. With the light on one axis it changes nothing, on the samples. 10 of 10 behaviour-changing sabotages caught. An eleventh — keeping an unused axis as zeros instead of leaving it absent — changed nothing, correctly: the docstring claimed that distinction was about exactness and it is about one array allocation per step, so the docstring was wrong and is now right. 743 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 2484481 commit 84e411b

7 files changed

Lines changed: 562 additions & 33 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,38 @@ noise bins rendered onto one grid, the way an instrument shows them. Its resolut
447447
not cosmetic — widening it raises the ASE trace decibel for decibel and leaves a carrier exactly
448448
where it is, which is the clearest demonstration of why OSNR needs a stated reference bandwidth.
449449

450+
## An orthogonal neighbour is not an absent one
451+
452+
The Kerr coupling used to be scalar per polarization: a channel was modulated by its neighbours'
453+
co-polarized power and by nothing else, so a channel polarized across it counted for zero. In an
454+
isotropic medium orthogonal power counts for exactly two thirds — which, since co-polarized
455+
cross-phase modulation already carries its factor of two, makes orthogonal cross-phase modulation
456+
exactly **one third** of co-polarized. Set `cross_polarization` on the fibre:
457+
458+
| neighbour | axes uncoupled | axes coupled |
459+
| :--- | ---: | ---: |
460+
| co-polarized | 2.000 γPL | 2.000 γPL |
461+
| orthogonal | 0.000 γPL | 0.667 γPL |
462+
463+
The same coefficient does two more things, because it is the same coefficient. A channel's own
464+
orthogonal component modulates it at two thirds the rate, so power split evenly between the axes
465+
turns each of them by `(0.5 + ⅔·0.5)·γPL` instead of `0.5·γPL`. And the two axes, no longer
466+
accumulating the same phase, rotate the state of polarization as the power moves — nonlinear
467+
birefringence, which falls by a factor of three, and cross-polarization modulation, which is that
468+
rotation being driven by a *different* channel's power.
469+
470+
The value is the fixed-axis one, from the χ⁽³⁾ tensor rather than from any averaging. A fibre whose
471+
birefringence scrambles faster than the nonlinearity acts is the Manakov regime instead, where the
472+
distinction washes into a single 8/9 on the total power; this block applies PMD as a separate
473+
element rather than interleaving it, so the fixed-axis form is the one consistent with the rest of
474+
it. The coherent `A_x* A_y²` term, which would exchange power between the axes rather than only
475+
dephase them, is left out — it is the part that averages away first — and the tests assert the
476+
axes' powers are unchanged to twelve digits, so that omission is a number rather than a sentence.
477+
478+
Off by default, and with all the light on one axis it changes nothing at all — on the samples,
479+
which is what makes it safe to leave on for a dual-polarization link and pointless for a
480+
single-polarization one.
481+
450482
## D is not one number
451483

452484
Standard fibre gains 0.058 ps/nm/km of dispersion for every nanometre up the

examples/ui_data.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,11 @@
724724
"default": true,
725725
"doc": "Couple the bands: each is phase-modulated by the others' power"
726726
},
727+
"cross_polarization": {
728+
"type": "bool",
729+
"default": false,
730+
"doc": "Couple the two polarizations: orthogonal power modulates at two thirds"
731+
},
727732
"max_walkoff_slip": {
728733
"type": "float",
729734
"default": 0.5,

examples/wdm_nonlinear.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
fwm_phase_mismatch,
6868
walkoff_from_dispersion,
6969
)
70-
from maiman.units import C_LIGHT, w_to_dbm, wavelength_to_frequency
70+
from maiman.signals import Band, OpticalSignal
71+
from maiman.units import C_LIGHT, dbm_to_w, w_to_dbm, wavelength_to_frequency
7172

7273
ANCHOR = 1550.0 # nm — channel 0
7374
SPACING = 100e9 # Hz
@@ -308,6 +309,51 @@ def main() -> None:
308309
print(" where no filter downstream can reach it. That is what a uniform grid")
309310
print(" costs, and why unequal channel spacing was once a serious proposal.")
310311

312+
# ------------------------------------------------------------------
313+
print("\n 6. Polarization: an orthogonal neighbour is not an absent one")
314+
print(" Two unmodulated channels, 100 km, no loss, no dispersion.")
315+
print(" The phase channel 0 picks up beyond its own self-phase modulation:\n")
316+
ctx = SimulationContext(bit_rate=10e9, samples_per_symbol=16, sequence_length=64, seed=3)
317+
power = dbm_to_w(0.0)
318+
unit = GAMMA * 1e-3 * power * 100e3 # gamma * P * L, radians
319+
320+
def carrier(px: float, py: float, index: int) -> Band:
321+
samples = ctx.num_samples
322+
return Band(
323+
Ex=np.full(samples, np.sqrt(px), dtype=np.complex128),
324+
Ey=np.full(samples, np.sqrt(py), dtype=np.complex128),
325+
f0=wavelength_to_frequency(channel_wavelength(index) * 1e-9),
326+
fs=ctx.sample_rate,
327+
)
328+
329+
def turned(bands: tuple[Band, ...], *, coupled: bool) -> float:
330+
span = Fiber(
331+
length=100.0,
332+
attenuation=0.0,
333+
dispersion=0.0,
334+
nonlinearity=GAMMA,
335+
four_wave_mixing=False,
336+
cross_polarization=coupled,
337+
label="fib",
338+
)
339+
out = span.run(ctx, {"in": OpticalSignal(bands=bands, noise=())})["out"]
340+
return float(np.angle(out.bands[0].Ex[0])) / unit
341+
342+
print(f" {'neighbour':>22} {'axes uncoupled':>15} {'axes coupled':>13}")
343+
print(" " + "-" * 54)
344+
for label, neighbour in (
345+
("co-polarized", carrier(power, 0.0, 1)),
346+
("orthogonal", carrier(0.0, power, 1)),
347+
):
348+
alone_off = turned((carrier(power, 0.0, 0),), coupled=False)
349+
alone_on = turned((carrier(power, 0.0, 0),), coupled=True)
350+
off = turned((carrier(power, 0.0, 0), neighbour), coupled=False) - alone_off
351+
on = turned((carrier(power, 0.0, 0), neighbour), coupled=True) - alone_on
352+
print(f" {label:>22} {off:12.3f} gPL {on:10.3f} gPL")
353+
print(" Two and two thirds: orthogonal cross-phase modulation is exactly a")
354+
print(" third of co-polarized. Uncoupled it is zero, which is the model")
355+
print(" saying a channel across the polarization is not there at all.")
356+
311357

312358
if __name__ == "__main__":
313359
main()

src/maiman/components/fiber.py

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,23 @@ class Fiber(Component):
102102
the run's generator, which makes a single run reproducible and a sweep with
103103
repeats an exploration of the distribution.
104104
105-
Not yet modelled: Raman scattering. The
106-
Kerr coupling is scalar per polarization throughout, so a channel is
107-
modulated by its neighbours' co-polarized power and not by their orthogonal
108-
power, which really contributes a third as much; cross-polarization
109-
modulation is absent for the same reason. Mixing products accumulate from
110-
span to span in power rather than in field, which understates the coherent
111-
build-up a dispersion-managed link produces. Pump depletion is not modelled,
112-
which matters only at powers no link is operated at.
105+
**The polarizations couple only if asked.** By default each is propagated as
106+
its own scalar problem, which is what every result in this project was taken
107+
with. Set ``cross_polarization`` and orthogonal power enters the Kerr term at
108+
two thirds the co-polarized weight — so a neighbour polarized across the
109+
channel modulates it at exactly one third of the rate a co-polarized one
110+
does, and the two axes of one channel, no longer accumulating the same phase,
111+
rotate the state of polarization as the power moves. With all the light on
112+
one axis the setting changes nothing, which is why it is safe to leave on for
113+
a dual-polarization link and pointless for a single-polarization one.
114+
115+
Not yet modelled: Raman scattering. The coherent polarization term
116+
``A_x* A_y**2``, which exchanges power between the axes rather than only
117+
dephasing them, is left out — it is the part that averages away first under
118+
real birefringence. Mixing products accumulate from span to span in power
119+
rather than in field, which understates the coherent build-up a
120+
dispersion-managed link produces. Pump depletion is not modelled, which
121+
matters only at powers no link is operated at.
113122
"""
114123

115124
display_name = "Optical Fiber"
@@ -140,6 +149,9 @@ class Fiber(Component):
140149
cross_phase_modulation = BoolParam(
141150
True, doc="Couple the bands: each is phase-modulated by the others' power"
142151
)
152+
cross_polarization = BoolParam(
153+
False, doc="Couple the two polarizations: orthogonal power modulates at two thirds"
154+
)
143155
max_walkoff_slip = Param(
144156
0.5,
145157
unit="",
@@ -316,9 +328,16 @@ def _propagate_kerr(
316328
) -> tuple[list[tuple[np.ndarray, np.ndarray]], PropagationDiagnostics]:
317329
"""Split-step propagation, with the bands coupled unless told otherwise.
318330
319-
The two polarizations are propagated as two separate coupled systems,
320-
which is what makes the model scalar per polarization: a band's X field
321-
is modulated by every other band's X power and by nothing else.
331+
By default the two polarizations are propagated as two separate coupled
332+
systems, which is what makes the model scalar per polarization: a band's
333+
X field is modulated by every other band's X power and by nothing else.
334+
Set ``cross_polarization`` and both axes go into one call, labelled, so
335+
that orthogonal power enters at :data:`maiman.kernels.ORTHOGONAL_KERR_WEIGHT`.
336+
337+
The two axes of one band share a linear operator — the same beta2, the
338+
same slope, the same walk-off — because birefringence is this block's
339+
other business and is applied as a separate element afterwards. What the
340+
coupling adds is entirely in the nonlinear step.
322341
"""
323342
bands = signal.bands
324343
coupled = self.cross_phase_modulation and len(bands) > 1
@@ -340,26 +359,74 @@ def _propagate_kerr(
340359
beta2 = [self.beta2_at(band.wavelength) for band in group]
341360
beta3 = [self.beta3_at(band.wavelength) for band in group]
342361
walkoff = [self.walkoff_of(band, reference) for band in group]
362+
363+
if self.cross_polarization:
364+
# One system, both axes, labelled. The per-field lists are
365+
# doubled rather than special-cased in the solver: X and Y of the
366+
# same band see the same linear operator.
367+
out, diagnostics = self._solve(
368+
[band.Ex for band in group] + [band.Ey for band in group],
369+
reference.fs,
370+
beta2=beta2 * 2,
371+
beta3=beta3 * 2,
372+
walkoff=walkoff * 2,
373+
polarization=[0] * len(group) + [1] * len(group),
374+
gamma=gamma,
375+
alpha=alpha,
376+
distance=distance,
377+
best=diagnostics,
378+
)
379+
fields.extend(zip(out[: len(group)], out[len(group) :], strict=True))
380+
continue
381+
343382
solved = []
344383
for axis in ("Ex", "Ey"):
345-
out, diag = propagate_coupled_ssfm(
384+
out, diagnostics = self._solve(
346385
[getattr(band, axis) for band in group],
347386
reference.fs,
348387
beta2=beta2,
388+
beta3=beta3,
349389
walkoff=walkoff,
390+
polarization=None,
350391
gamma=gamma,
351-
beta3=beta3,
352392
alpha=alpha,
353393
distance=distance,
354-
max_nonlinear_phase=self.max_nonlinear_phase,
355-
max_walkoff_slip=self.max_walkoff_slip,
394+
best=diagnostics,
356395
)
357396
solved.append(out)
358-
if diag.steps > diagnostics.steps:
359-
diagnostics = diag
360397
fields.extend(zip(solved[0], solved[1], strict=True))
361398
return fields, diagnostics
362399

400+
def _solve(
401+
self,
402+
fields: list[np.ndarray],
403+
sample_rate: float,
404+
*,
405+
beta2: list[float],
406+
beta3: list[float],
407+
walkoff: list[float],
408+
polarization: list[int] | None,
409+
gamma: float,
410+
alpha: float,
411+
distance: float,
412+
best: PropagationDiagnostics,
413+
) -> tuple[list[np.ndarray], PropagationDiagnostics]:
414+
"""One call into the solver, keeping whichever diagnostics took more steps."""
415+
out, diag = propagate_coupled_ssfm(
416+
fields,
417+
sample_rate,
418+
beta2=beta2,
419+
walkoff=walkoff,
420+
gamma=gamma,
421+
beta3=beta3,
422+
polarization=polarization,
423+
alpha=alpha,
424+
distance=distance,
425+
max_nonlinear_phase=self.max_nonlinear_phase,
426+
max_walkoff_slip=self.max_walkoff_slip,
427+
)
428+
return out, diag if diag.steps > best.steps else best
429+
363430
# -- four-wave mixing -------------------------------------------------
364431

365432
def _mix(

0 commit comments

Comments
 (0)