Skip to content

Commit b7572e1

Browse files
ehsun-shclaude
andcommitted
Add .oosim project format, component registry, and sweeps
Completes Phase 0. Registry: Component subclasses register themselves on definition, and project files resolve names by lookup only. Resolving a name by importing a dotted path out of a file would make opening someone else's project equivalent to running their code, since importing a module executes it. An unregistered name is an error naming the package to import. `manifests()` generates the GUI palette from the classes, so it cannot drift from what the engine does. .oosim format: versioned JSON. Three rules shape it — UI data is segregated from the physics so a diff shows model changes and not moved boxes; only explicitly set parameters are stored, so a file records the choices its author made rather than the defaults they accepted; and a project is always runnable headless. `structural_config()` carries constructor arguments that change a component's shape rather than a value (a combiner's port count), because a GUI has to treat those differently: editing one invalidates the connections drawn to it. The defaults decision has a real trade-off and the docstring states it: if a model default changes in a later release, a file that never overrode it will simulate slightly differently. `oosim_version` is recorded so that is diagnosable. Sweeps: Graph.run() takes per-run parameter overrides and a seed override, both rolled back afterwards including when a run raises — a leaked override would silently contaminate every later point and nothing would report it. sweep() covers the cartesian product of any number of axes with optional repeated runs; run seeds are derived by hash rather than incremented. Points are independent so parallel execution is straightforward, but is not implemented; the docstring says so rather than implying otherwise. 34 new tests (205 total): round-trip identity down to identical Q and error counts, structural config survival, UI segregation, rejection of missing/future schema versions, unknown component types, dangling edges, out-of-range parameters and mistyped connections on load; sweep equivalence to a manual loop, override restoration, cartesian product ordering, and reproducible repeats. examples/ook_link.py now uses sweep() and writes examples/ook_link.oosim. The Monte-Carlo section shows why repeats exist: at -20 dBm, eight runs of the same link give 37 to 58 errors — a 50% spread — while Q is stable to ~1%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b9b1df1 commit b7572e1

12 files changed

Lines changed: 1291 additions & 43 deletions

File tree

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
> PRBS → NRZ → CW laser → MZM → fiber (loss + dispersion) → PIN → filter → eye/Q/BER.
1616
> Every physics block is validated against a closed-form result in CI.
1717
>
18+
> Projects save to versioned JSON and sweeps are first-class, so a curve is one call rather than
19+
> a hand-written loop that mutates the graph.
20+
>
1821
> **Not implemented yet:** SSFM/nonlinearity, PMD, amplifiers, equalisers, coherent detection,
19-
> WDM crosstalk, the project file format, and the GUI. See the [roadmap](#roadmap).
22+
> WDM crosstalk, and the GUI. See the [roadmap](#roadmap).
2023
>
2124
> This is not yet a useful simulator. It is a foundation with the expensive decisions made and
2225
> tested. Criticism of those decisions is worth more right now than any feature —
@@ -87,10 +90,22 @@ with dispersion switched off the same 60 km span gives Q = 15.4 instead of 6.5.
8790
textbook result for uncompensated 10 G NRZ on standard fiber.
8891

8992
The two columns are also a cross-check on each other. 120 km of 0.2 dB/km is 24 dB, and launching
90-
0 dBm through it gives Q = 1.01 — the same Q as launching −24 dBm back to back. Modulator, fiber,
91-
detector, filter and analyzer all have to agree for that to hold; it is
93+
0 dBm through it gives the same Q as launching −24 dBm back to back. Modulator, fiber, detector,
94+
filter and analyzer all have to agree for that to hold; it is
9295
[a test](tests/test_ber.py), not a coincidence.
9396

97+
Both curves come from `sweep()`, and the same script writes the schematic to
98+
[`examples/ook_link.oosim`](examples/ook_link.oosim) — versioned JSON, diffable, runnable headless.
99+
100+
```python
101+
result = sweep(graph, {("laser", "power"): [-24.0, -21.0, -18.0]}, runs=8)
102+
q = result.metric(analyzer, lambda m: m.q_factor) # shape (points, runs)
103+
```
104+
105+
Repeats matter more than they look. At −20 dBm, eight runs of the same link give error counts of
106+
37 to 58 — a 50% spread on the thing being measured, while Q itself is stable to ±1%. A single
107+
BER at a marginal operating point is one sample, not an answer.
108+
94109
## What this is
95110

96111
A block-diagram simulator for optical systems: drop components on a canvas, wire a link, run it,
@@ -221,7 +236,7 @@ time window, and results are reproducible.
221236

222237
| Phase | Scope | Estimate¹ |
223238
| :--- | :--- | :--- |
224-
| **0 — Foundations** *(in progress)* |Signal model, context, port types, component base, scheduler, CI · ⬜ project file format, sweeps | ~1 month |
239+
| **0 — Foundations** | Signal model, context, port types, component base, registry, scheduler, `.oosim` project format, sweeps, CI | ~1 month |
225240
| **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 |
226241
| **1.5 — Nonlinear & amplified** | Adaptive-step SSFM, Kerr, PMD, EDFA (gain/NF/saturation/ASE), APD | ~2 months |
227242
| **2 — GUI & DSP** | Graph editor, plots, pulse shaping, FIR, equalizers (LMS/CMA), OSA, constellation, sweeps | ~3–4 months |

examples/ook_link.oosim

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
{
2+
"schema_version": 1,
3+
"oosim_version": "0.0.1.dev0",
4+
"context": {
5+
"bit_rate": 10000000000.0,
6+
"samples_per_symbol": 8,
7+
"sequence_length": 8192,
8+
"seed": 2026,
9+
"precision": "single"
10+
},
11+
"nodes": [
12+
{
13+
"id": "prbs",
14+
"type": "PRBSGenerator",
15+
"params": {
16+
"order": 15.0
17+
},
18+
"ui": {
19+
"x": 40.0,
20+
"y": 40.0
21+
}
22+
},
23+
{
24+
"id": "driver",
25+
"type": "NRZDriver",
26+
"params": {
27+
"v_low": 4.0,
28+
"v_high": 0.0
29+
},
30+
"ui": {
31+
"x": 200.0,
32+
"y": 40.0
33+
}
34+
},
35+
{
36+
"id": "laser",
37+
"type": "CWLaser",
38+
"params": {
39+
"power": 0.0,
40+
"wavelength": 1550.0
41+
},
42+
"ui": {
43+
"x": 40.0,
44+
"y": 160.0
45+
}
46+
},
47+
{
48+
"id": "mzm",
49+
"type": "MachZehnderModulator",
50+
"params": {
51+
"v_pi": 4.0,
52+
"extinction_ratio": 30.0
53+
},
54+
"ui": {
55+
"x": 360.0,
56+
"y": 100.0
57+
}
58+
},
59+
{
60+
"id": "fiber",
61+
"type": "Fiber",
62+
"params": {
63+
"length": 0.0,
64+
"attenuation": 0.2,
65+
"dispersion": 17.0
66+
},
67+
"ui": {
68+
"x": 520.0,
69+
"y": 100.0
70+
}
71+
},
72+
{
73+
"id": "pin",
74+
"type": "PINPhotodiode",
75+
"params": {
76+
"responsivity": 0.8,
77+
"shot_noise": true,
78+
"thermal_noise": true
79+
},
80+
"ui": {
81+
"x": 680.0,
82+
"y": 100.0
83+
}
84+
},
85+
{
86+
"id": "lpf",
87+
"type": "ElectricalFilter",
88+
"params": {
89+
"bandwidth": 7.0
90+
},
91+
"ui": {
92+
"x": 840.0,
93+
"y": 100.0
94+
}
95+
},
96+
{
97+
"id": "ber",
98+
"type": "BERAnalyzer",
99+
"params": {},
100+
"ui": {
101+
"x": 1000.0,
102+
"y": 100.0
103+
}
104+
}
105+
],
106+
"edges": [
107+
{
108+
"from": [
109+
"lpf",
110+
"out"
111+
],
112+
"to": [
113+
"ber",
114+
"in"
115+
]
116+
},
117+
{
118+
"from": [
119+
"prbs",
120+
"out"
121+
],
122+
"to": [
123+
"ber",
124+
"reference"
125+
]
126+
},
127+
{
128+
"from": [
129+
"prbs",
130+
"out"
131+
],
132+
"to": [
133+
"driver",
134+
"in"
135+
]
136+
},
137+
{
138+
"from": [
139+
"mzm",
140+
"out"
141+
],
142+
"to": [
143+
"fiber",
144+
"in"
145+
]
146+
},
147+
{
148+
"from": [
149+
"pin",
150+
"out"
151+
],
152+
"to": [
153+
"lpf",
154+
"in"
155+
]
156+
},
157+
{
158+
"from": [
159+
"driver",
160+
"out"
161+
],
162+
"to": [
163+
"mzm",
164+
"electrical_in"
165+
]
166+
},
167+
{
168+
"from": [
169+
"laser",
170+
"out"
171+
],
172+
"to": [
173+
"mzm",
174+
"optical_in"
175+
]
176+
},
177+
{
178+
"from": [
179+
"fiber",
180+
"out"
181+
],
182+
"to": [
183+
"pin",
184+
"in"
185+
]
186+
}
187+
]
188+
}

0 commit comments

Comments
 (0)