|
| 1 | +"""A 256 Gb/s dual-polarization coherent link through a rotated channel. |
| 2 | +
|
| 3 | +Two independent 16-QAM tributaries share one wavelength on orthogonal |
| 4 | +polarizations. A fibre rotates the launched state arbitrarily, so what the |
| 5 | +receiver's two branches carry is a *mixture* of both tributaries rather than one |
| 6 | +each — and past a small angle neither is recoverable at all. The butterfly |
| 7 | +equaliser is what separates them again. |
| 8 | +
|
| 9 | +Run: ``python examples/dualpol_link.py`` |
| 10 | +""" |
| 11 | + |
| 12 | +from __future__ import annotations |
| 13 | + |
| 14 | +from oosim import Graph, SimulationContext |
| 15 | +from oosim.components import ( |
| 16 | + ButterflyEqualizer, |
| 17 | + CarrierRecovery, |
| 18 | + ConstellationAnalyzer, |
| 19 | + CWLaser, |
| 20 | + DualPolarizationReceiver, |
| 21 | + IQDriver, |
| 22 | + IQModulator, |
| 23 | + IQSampler, |
| 24 | + PolarizationCombiner, |
| 25 | + PolarizationRotator, |
| 26 | + PRBSGenerator, |
| 27 | + QAMMapper, |
| 28 | + Splitter, |
| 29 | +) |
| 30 | + |
| 31 | +SYMBOL_RATE = 32e9 |
| 32 | +BITS_PER_SYMBOL = 4 |
| 33 | + |
| 34 | + |
| 35 | +def build(rotation: float, *, equalize: bool, sequence_length: int = 4096) -> tuple: |
| 36 | + ctx = SimulationContext( |
| 37 | + bit_rate=SYMBOL_RATE, |
| 38 | + samples_per_symbol=4, |
| 39 | + sequence_length=sequence_length, |
| 40 | + seed=2026, |
| 41 | + precision="double", |
| 42 | + ) |
| 43 | + graph = Graph(ctx) |
| 44 | + laser = graph.add(CWLaser(power=0.0, linewidth=100.0, label="tx")) |
| 45 | + splitter = graph.add(Splitter(2, label="sp")) |
| 46 | + graph.connect(laser, splitter["in"]) |
| 47 | + |
| 48 | + mappers, modulators = {}, {} |
| 49 | + for index, axis in enumerate(("x", "y")): |
| 50 | + prbs = graph.add( |
| 51 | + PRBSGenerator( |
| 52 | + order=23.0 if axis == "x" else 15.0, |
| 53 | + bits_per_symbol=float(BITS_PER_SYMBOL), |
| 54 | + label=f"prbs_{axis}", |
| 55 | + ) |
| 56 | + ) |
| 57 | + mapper = graph.add(QAMMapper(bits_per_symbol=float(BITS_PER_SYMBOL), label=f"map_{axis}")) |
| 58 | + driver = graph.add(IQDriver(label=f"drv_{axis}")) |
| 59 | + modulator = graph.add(IQModulator(label=f"mod_{axis}")) |
| 60 | + graph.chain(prbs, mapper, driver) |
| 61 | + graph.connect(splitter[f"out{index}"], modulator["optical_in"]) |
| 62 | + graph.connect(driver["i"], modulator["i"]) |
| 63 | + graph.connect(driver["q"], modulator["q"]) |
| 64 | + mappers[axis], modulators[axis] = mapper, modulator |
| 65 | + |
| 66 | + combiner = graph.add(PolarizationCombiner(label="pbc")) |
| 67 | + graph.connect(modulators["x"], combiner["x"]) |
| 68 | + graph.connect(modulators["y"], combiner["y"]) |
| 69 | + rotator = graph.add(PolarizationRotator(angle=rotation, phase=25.0, label="rot")) |
| 70 | + graph.connect(combiner, rotator["in"]) |
| 71 | + |
| 72 | + lo = graph.add(CWLaser(power=13.0, linewidth=100.0, label="lo")) |
| 73 | + receiver = graph.add(DualPolarizationReceiver(label="rx")) |
| 74 | + graph.connect(rotator, receiver["in"]) |
| 75 | + graph.connect(lo, receiver["lo"]) |
| 76 | + |
| 77 | + samplers = {} |
| 78 | + for axis in ("x", "y"): |
| 79 | + sampler = graph.add(IQSampler(label=f"smp_{axis}")) |
| 80 | + graph.connect(receiver[f"{axis}i"], sampler["i"]) |
| 81 | + graph.connect(receiver[f"{axis}q"], sampler["q"]) |
| 82 | + graph.connect(mappers[axis]["out"], sampler["reference"]) |
| 83 | + samplers[axis] = sampler |
| 84 | + |
| 85 | + if equalize: |
| 86 | + equalizer = graph.add(ButterflyEqualizer(label="eq")) |
| 87 | + graph.connect(samplers["x"]["out"], equalizer["x"]) |
| 88 | + graph.connect(samplers["y"]["out"], equalizer["y"]) |
| 89 | + sources = {"x": equalizer["x_out"], "y": equalizer["y_out"]} |
| 90 | + else: |
| 91 | + sources = {axis: samplers[axis]["out"] for axis in ("x", "y")} |
| 92 | + |
| 93 | + analyzers = {} |
| 94 | + for axis in ("x", "y"): |
| 95 | + recovery = graph.add(CarrierRecovery(label=f"cr_{axis}")) |
| 96 | + graph.connect(sources[axis], recovery["in"]) |
| 97 | + for reference in ("x", "y"): |
| 98 | + analyzer = graph.add( |
| 99 | + ConstellationAnalyzer(ignore_edges=128.0, label=f"vsa_{axis}{reference}") |
| 100 | + ) |
| 101 | + graph.connect(recovery["out"], analyzer["in"]) |
| 102 | + graph.connect(mappers[reference]["out"], analyzer["reference"]) |
| 103 | + analyzers[axis + reference] = analyzer |
| 104 | + return graph, analyzers |
| 105 | + |
| 106 | + |
| 107 | +def measure(rotation: float, *, equalize: bool) -> tuple: |
| 108 | + graph, analyzers = build(rotation, equalize=equalize) |
| 109 | + results = graph.run(keep=[]) |
| 110 | + taken = {key: results[a] for key, a in analyzers.items()} |
| 111 | + # Nothing blind labels the tributaries, so a channel that swaps them is |
| 112 | + # separated correctly and delivered the other way round. Framing resolves |
| 113 | + # this in a real link; both references resolve it here. |
| 114 | + direct = taken["xx"].symbol_errors + taken["yy"].symbol_errors |
| 115 | + swapped = taken["xy"].symbol_errors + taken["yx"].symbol_errors |
| 116 | + if direct <= swapped: |
| 117 | + return taken["xx"], taken["yy"], False |
| 118 | + return taken["xy"], taken["yx"], True |
| 119 | + |
| 120 | + |
| 121 | +def main() -> None: |
| 122 | + rate = SYMBOL_RATE * BITS_PER_SYMBOL * 2 / 1e9 |
| 123 | + print( |
| 124 | + f"Dual-polarization 16-QAM, {SYMBOL_RATE / 1e9:.0f} GBd x 2 pol = {rate:.0f} Gb/s\n" |
| 125 | + f"Two independent tributaries on one wavelength, through a rotated channel.\n" |
| 126 | + ) |
| 127 | + print("rotation without equaliser with equaliser") |
| 128 | + print("-" * 70) |
| 129 | + for rotation in (0.0, 15.0, 30.0, 45.0, 72.0, 90.0): |
| 130 | + off_x, off_y, _ = measure(rotation, equalize=False) |
| 131 | + on_x, on_y, swapped = measure(rotation, equalize=True) |
| 132 | + note = " (tributaries swapped)" if swapped else "" |
| 133 | + print( |
| 134 | + f"{rotation:5.0f} deg EVM {off_x.evm * 100:6.1f} / {off_y.evm * 100:6.1f} %" |
| 135 | + f" {off_x.symbol_errors + off_y.symbol_errors:5} err" |
| 136 | + f" EVM {on_x.evm * 100:4.2f} / {on_y.evm * 100:4.2f} %" |
| 137 | + f" {on_x.symbol_errors + on_y.symbol_errors:3} err{note}" |
| 138 | + ) |
| 139 | + print( |
| 140 | + "\nPast a few degrees the unequalised branches are not degraded — they carry\n" |
| 141 | + "no recoverable data at all. Separating them is linear algebra the receiver\n" |
| 142 | + "has to learn blind, with no training sequence anywhere in the link." |
| 143 | + ) |
| 144 | + |
| 145 | + |
| 146 | +if __name__ == "__main__": |
| 147 | + main() |
0 commit comments