Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenOptiSim

An open-source, modular simulator for optical communication links and photonic systems.

CI status license python


⚠️ Project status: pre-alpha — Phases 0, 1 and 1.5 complete

A complete 10 Gb/s OOK link runs end to end and produces numbers that match theory: PRBS → NRZ → CW laser → MZM → fiber (loss + dispersion) → PIN → filter → eye/Q/BER. Every physics block is validated against a closed-form result in CI.

Projects save to versioned JSON and sweeps are first-class, so a curve is one call rather than a hand-written loop that mutates the graph.

Fiber nonlinearity is solved by adaptive-step split-step Fourier, EDFAs emit ASE into the noise-bin model so amplified multi-span links give correct OSNR, and PMD is drawn as a random realisation with the right Maxwellian statistics.

Not implemented yet: cross-phase modulation and four-wave mixing between channels, equalisers, coherent detection, and the GUI. See the roadmap.

This is not yet a useful simulator. It is a foundation with the expensive decisions made and tested. Criticism of those decisions is worth more right now than any feature — the architecture document is where they are argued out.


Try it

pip install -e ".[dev]" && pytest
from oosim import SimulationContext, Graph
from oosim.components import CWLaser, Combiner, Fiber, PowerMeter

ctx = SimulationContext(bit_rate=10e9, samples_per_symbol=16, sequence_length=64)
g = Graph(ctx)

laser = g.add(CWLaser(power=0.0, wavelength=1550.0))  # 0 dBm
fiber = g.add(Fiber(length=80.0, attenuation=0.2))  # 80 km, 0.2 dB/km
meter = g.add(PowerMeter())
g.chain(laser, fiber, meter)

print(g.run()[meter])  # PowerReading(-16.000 dBm; 1550.00nm=-16.000dBm)

Two carriers stay two independently sampled bands, which is the point of the signal model:

g = Graph(ctx)
ch1 = g.add(CWLaser(wavelength=1550.0, label="ch1"))
ch2 = g.add(CWLaser(wavelength=1551.0, label="ch2"))
mux = g.add(Combiner(2))
fiber = g.add(Fiber(length=80.0, attenuation=0.2))
meter = g.add(PowerMeter())

g.connect(ch1, mux["in0"])
g.connect(ch2, mux["in1"])
g.chain(mux, fiber, meter)

print(g.run()[meter])
# PowerReading(-12.990 dBm; 1551.00nm=-16.000dBm, 1550.00nm=-16.000dBm)

Each band carries its own centre frequency, so channel spacing never enters the sample rate. Put those two lasers 6 THz apart instead of 125 GHz and nothing about the run changes — which is exactly what a single-carrier signal model cannot do.

Results

python examples/ook_link.py builds a 10 Gb/s OOK link and characterises it. Abridged output:

Receiver sensitivity (back to back)        Dispersion-limited reach (0 dBm launch)
  launch      Q    BER (from Q)  counted     distance     Q    BER (from Q)
  -22 dBm   1.59     5.61e-02    460/8184        0 km   94.75    0.00e+00
  -20 dBm   2.51     6.09e-03     49/8184       40 km    9.26    1.03e-20
  -19 dBm   3.15     8.17e-04      7/8184       60 km    6.48    4.63e-11
  -16 dBm   6.25     2.11e-10   none counted    80 km    3.76    8.58e-05
  -14 dBm   9.83     3.99e-23   none counted   120 km    0.63    2.65e-01

Two things are worth reading off that. Sensitivity is −16 dBm for a Q of 6 — the right figure for a PIN into a plain 50 Ω load. Reach is ~62 km, and it is set by dispersion, not by loss: with dispersion switched off the same 60 km span gives Q = 15.4 instead of 6.5. That is the textbook result for uncompensated 10 G NRZ on standard fiber.

The two columns are also a cross-check on each other. 120 km of 0.2 dB/km is 24 dB, and launching 0 dBm through it gives the same Q as launching −24 dBm back to back. Modulator, fiber, detector, filter and analyzer all have to agree for that to hold; it is a test, not a coincidence.

Both curves come from sweep(), and the same script writes the schematic to examples/ook_link.oosim — versioned JSON, diffable, runnable headless.

result = sweep(graph, {("laser", "power"): [-24.0, -21.0, -18.0]}, runs=8)
q = result.metric(analyzer, lambda m: m.q_factor)     # shape (points, runs)

Repeats matter more than they look. At −20 dBm, eight runs of the same link give error counts of 37 to 58 — a 50% spread on the thing being measured, while Q itself is stable to ±1%. A single BER at a marginal operating point is one sample, not an answer.

Amplified and nonlinear

python examples/amplified_link.py runs a chain of 80 km spans, each amplified back to transparency:

  spans   reach     OSNR      vs. one span            Sech pulse over 4 soliton periods
      1     80 km   36.95 dB    +0.00  (theory -0.00)   configuration      width   peak
      2    160 km   33.94 dB    -3.01  (theory -3.01)   soliton (N = 1)    x1.00   x1.00
      4    320 km   30.93 dB    -6.02  (theory -6.02)   half the power     x2.62   x0.40
      8    640 km   27.92 dB    -9.03  (theory -9.03)   no nonlinearity    x4.12   x0.23
     16   1280 km   24.91 dB   -12.04  (theory -12.04)  no dispersion      x1.00   x1.00

The left table tracks 10·log10(N) to a hundredth of a dB over sixteen spans, and nothing in the model is written in those terms — the noise bins just accumulate. The single-span figure of 36.95 dB is 58 − 16 − 5: the quantum floor, the span loss the amplifier has to make up, and its noise figure.

The right table is the soliton. At N = 1 the chirp the Kerr effect imposes cancels the one dispersion imposes and the pulse is unchanged after 29 km; halve the power and the balance breaks. The last row is the honest caveat — self-phase modulation alone also preserves |A(T)|, so shape invariance proves nothing by itself. It is invariance with both effects active that is the result.

What this is

A block-diagram simulator for optical systems: drop components on a canvas, wire a link, run it, get an eye diagram and a BER number — with correct optical noise accounting. And the same model, scriptable from Python.

Think OptiSystem's workflow, VPIphotonics' ambition, open source, and verifiable.

  PRBS ──▶ NRZ ──▶ CW Laser ──▶ MZM ──▶ Fiber ──▶ PIN ──▶ BER / Eye
                                 ▲
                            (V_π, ER, IL)

Why

There is a real gap in open-source photonics tooling, but it is not where it first appears to be.

The physics kernels already exist. SSFM, coherent DSP, and S-matrix circuit solvers are all available in open source — as libraries, in fragments, each with its own incompatible data model. What does not exist is a single coherent tool that puts them behind a usable interface with a sound signal representation underneath.

So the gap is integration and user experience, not numerics. That shapes the whole strategy: where a mature open-source kernel exists, OpenOptiSim wraps or depends on it rather than rewriting it. The value added here is the data model, the execution engine, the component library, and the UI.

Existing tool What it does Relationship
OptiCommPy Python: SSFM, coherent DSP, BER Reference & cross-validation target
GNPy Optical network planning / OSNR budgets Complementary — network layer, not waveform layer
QAMPy Coherent DSP algorithms Reference for Phase 3
SAX S-matrix photonic circuit solver This is Phase 4 — integrate, don't reimplement
gdsfactory Photonic layout & PDK ecosystem PDK path for Phase 4
Meep FDTD / full-wave EM Feeds component models in; not a competitor
GNU Radio Block-based SDR Architectural reference for dataflow scheduling

Design decisions

These are the choices that matter, stated up front so they can be argued with. Full reasoning is in the architecture document.

Decision Rationale
Engine before GUI A wrong engine forces a total rewrite; a wrong GUI does not.
Multi-band optical signal from day one A single scalar carrier frequency cannot express a 40-channel DWDM system without a physically impossible sample rate. Discovering that in Phase 3 means rewriting the core.
Noise carried in spectral bins, separate from sampled fields ASE spans the amplifier bandwidth; the signal does not. Sampling both together makes realistic runs impossible.
Block-mode execution (whole time window per call) Every component becomes a pure function of its inputs. Vectorizes naturally; not a streaming scheduler.
Python-first, behind a narrow kernel boundary ~90–95% of SSFM runtime is inside FFT — library code in any language. GPU via CuPy is nearly free. Contributors who write fiber models write Python. Native kernels stay an option, not a prerequisite.
Typed ports (Optical / Electrical / Binary / Symbol / Metric) An MZM has an electrical input. Invalid wiring is rejected at edit time, not at run time.
Immutable signals A WDM link is hundreds of MB. Value-copying between blocks makes the tool unusable regardless of language.
Python-package plugins, parameters declared once Requiring contributors to match a C++ ABI suppresses exactly the contributions the plugin system exists to attract.
Every physics block validated against a closed-form result Comparison against commercial tools needs a licence and is not reproducible in CI. A simulator nobody can verify has no scientific value.
No FFTW FFTW is GPL-2.0-or-later. Linking it would make the project GPL. pocketfft (BSD) is used instead.

Architecture

                    ┌──────────────────────────────────────┐
                    │        Visual Designer (Web UI)      │
                    │   graph editor · plots (WebGL)       │
                    └───────────────────┬──────────────────┘
                                        │  project JSON + WebSocket
                                        │  (data reduced engine-side)
                    ┌───────────────────▼──────────────────┐
                    │            Session Server            │
                    └───────────────────┬──────────────────┘
 ┌──────────────────────────────────────▼───────────────────────────────────────┐
 │                             Public Python API                                 │
 │        oosim.Graph · Component · run() · sweep()                              │
 └──────────────┬─────────────────────────────────────┬─────────────────────────┘
 ┌──────────────▼───────────────┐    ┌────────────────▼─────────────────┐
 │      Component Library       │◄──►│         Execution Engine         │
 │  plugins · registry · schema │    │  scheduler · sweeps · run graph  │
 └──────────────┬───────────────┘    └────────────────┬─────────────────┘
 ┌──────────────▼─────────────────────────────────────▼─────────────────────────┐
 │                     Core Data Model + Numerical Kernels                       │
 │       SimulationContext · Signals · FFT / SSFM / filters / noise              │
 │              back-ends: NumPy → CuPy → (optional) native                      │
 └──────────────────────────────────────────────────────────────────────────────┘

The GUI is a client of the public Python API with no privileged access. If a feature is not reachable from Python, it does not exist.

The core data model

The part most worth reviewing — see src/oosim/signals.py. An optical signal is not one array of numbers:

@dataclass(frozen=True)
class Band:
    """One sampled band: complex envelope in two orthogonal polarizations (Jones vector)."""

    Ex: np.ndarray  # complex64, shape (N,), read-only
    Ey: np.ndarray
    f0: float  # band centre frequency [Hz]
    fs: float  # band sample rate [Hz]


@dataclass(frozen=True)
class NoiseBin:
    """Spectrally-resolved noise, carried separately from the sampled bands."""

    f_start: float
    f_end: float
    psd_x: float  # [W/Hz] per polarization
    psd_y: float


@dataclass(frozen=True)
class OpticalSignal:
    bands: tuple[Band, ...]
    noise: tuple[NoiseBin, ...]

Fields are sqrt(W), so instantaneous power is |Ex|**2 + |Ey|**2. Arrays are read-only, which is what lets metadata-only blocks share buffers instead of copying a span at a time.

Global run parameters (bit rate, oversampling, sequence length, RNG seed) live in a shared SimulationContext, not in individual signals — so blocks cannot silently disagree about the time window, and results are reproducible.

Roadmap

Phase Scope Estimate¹
0 — Foundations Signal model, context, port types, component base, registry, scheduler, .oosim project format, sweeps, CI ~1 month
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
1.5 — Nonlinear & amplified Adaptive-step SSFM, Kerr, EDFA with ASE, OSNR, PMD, APD ~2 months
2 — GUI & DSP (design started) ✅ Layout agreed, interactive mockup built on real engine output · ⬜ session server, graph editor, DSP blocks, OSA, constellation ~3–4 months
3 — Coherent & WDM IQ mod, M-QAM, LO, 90° hybrid, balanced detection, coherent DSP, DWDM + crosstalk, 400G/800G references, CuPy back-end ~6 months
4 — PIC Waveguides, ring resonators, MMI, MZI via integration with an existing S-matrix solver; PDK import

¹ One developer, part-time. Estimates, not commitments.

Phase 1 is deliberately smaller than a first instinct suggests: SSFM, PMD, Kerr, APD and the GUI are all pushed out of it. Shipping a validated linear link quickly matters more than breadth.

Validation

Every physics block ships with a test against a closed-form result, run in CI (tests/test_physics.py):

Case Expected
Attenuation P_out = P_in · 10^(-αL/10)
Cascaded spans Loss is additive in dB
Source power Independent of the simulated time window
Phase noise Broadens the line, conserves average power
Multi-carrier Channels stay separate bands; spacing does not drive Fs
Gaussian pulse, CD only T₁/T₀ = √(1 + (z/L_D)²), L_D = T₀²/|β₂|
Chirped Gaussian T₁/T₀ = √((1 + Cβ₂z/T₀²)² + (β₂z/T₀²)²) — pins the sign of β₂
Dispersion compensation +D then −D restores the input sample-for-sample
GVD Energy conserved (Parseval); β₂ = −Dλ²/2πc per band
PRBS Period 2ⁿ−1; 2ⁿ⁻¹ marks; every n-bit window appears once
Ideal push-pull MZM P_out/P_in = cos²(πV / 2V_π); null depth equals the declared ER
PIN detector I = R·P; shot σ² = 2qIB; thermal σ² = 4kTB/R_L
Receiver filter 3 dB at B; noise bandwidth B·√(π/4ln2); zero group delay
BER ½·erfc(Q/√2) matched against directly counted errors, 10⁻⁴–10⁻¹
Link consistency L km of span ≡ launching α·L dB lower, end to end
Lossless SSFM Energy conserved with nonlinearity; γ=0 reproduces the exact linear solution
Self-phase modulation |A(T)| exactly unchanged; spectrum broadens
Fundamental soliton (N=1) Envelope invariant over 4 soliton periods — the only test that pins the sign of γ against β₂
Higher-order soliton (N=2) Compresses at half a period, recovers at a full one
EDFA P_ASE = 2·n_sp·hν·(G−1)·B_o; n_sp = NF·G/2(G−1)
OSNR 58 + P_launch − NF − 10·log10(spans), over 16 spans
PMD DGD Maxwellian: ⟨τ²⟩/⟨τ⟩² = 3π/8, mean ∝√L, spread 0.42·mean
APD F(M) = kM + (2−1/M)(1−k); an interior optimum gain exists
XPM / FWM Cross-channel nonlinearity

Component models are derived from published literature and standards (Agrawal, Nonlinear Fiber Optics; ITU-T G.652 / G.694.1; relevant IEEE 802.3 clauses), cited in each component's docstring — never from inspection of commercial tools.

Contributing

The core is small enough that changing it is still cheap, which makes right now the most useful time to push back on it. Most valuable first:

  • Review the signal model and schedulersrc/oosim/signals.py, src/oosim/graph.py, and §3–§4 of the architecture document. If something there is wrong, it is far cheaper to fix now than after fifty components depend on it.
  • Tell us if this duplicates existing work. If a project already does this well, that is worth knowing before several months go into it.
  • Describe your use case. Which components, which measurements, what you currently use and what frustrates you about it.
  • Add a component. A component is a Python class with declared parameters and typed ports — see src/oosim/components/ for the pattern. Every physics block needs a test against a closed-form result; a component without one will not be merged.

Open an issue for any of the above.

pip install -e ".[dev]" && ruff check . && ruff format --check . && mypy && pytest

License

Apache-2.0 — permissive enough for industrial adoption, with an explicit patent grant.

Dependency licences are checked before adoption, not after. The concrete case already identified: FFTW is GPL-2.0-or-later, so it (and pyFFTW) cannot be linked without making the whole project GPL — pocketfft/scipy.fft (BSD) is used instead.


→ Full architecture & roadmap

About

Open-source, modular simulator for optical communication links and photonic systems. Design phase — architecture under review.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages