maiman 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. maiman-0.1.0/.gitattributes +8 -0
  2. maiman-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +65 -0
  3. maiman-0.1.0/.github/ISSUE_TEMPLATE/component_or_feature.yml +51 -0
  4. maiman-0.1.0/.github/ISSUE_TEMPLATE/config.yml +11 -0
  5. maiman-0.1.0/.github/ISSUE_TEMPLATE/model_is_wrong.yml +54 -0
  6. maiman-0.1.0/.github/workflows/ci.yml +79 -0
  7. maiman-0.1.0/.github/workflows/release.yml +128 -0
  8. maiman-0.1.0/.gitignore +68 -0
  9. maiman-0.1.0/CITATION.cff +44 -0
  10. maiman-0.1.0/CONTRIBUTING.md +115 -0
  11. maiman-0.1.0/DESIGN.md +320 -0
  12. maiman-0.1.0/LICENSE +202 -0
  13. maiman-0.1.0/PKG-INFO +1829 -0
  14. maiman-0.1.0/PRODUCT.md +95 -0
  15. maiman-0.1.0/README.md +1803 -0
  16. maiman-0.1.0/RELEASING.md +135 -0
  17. maiman-0.1.0/SECURITY.md +79 -0
  18. maiman-0.1.0/assets/apple-touch-icon.png +0 -0
  19. maiman-0.1.0/assets/icon-192.png +0 -0
  20. maiman-0.1.0/assets/icon-32.png +0 -0
  21. maiman-0.1.0/assets/icon-512.png +0 -0
  22. maiman-0.1.0/assets/logo-mark-tight.png +0 -0
  23. maiman-0.1.0/assets/logo-mark.png +0 -0
  24. maiman-0.1.0/assets/logo-wordmark-dark.png +0 -0
  25. maiman-0.1.0/assets/logo-wordmark.png +0 -0
  26. maiman-0.1.0/assets/og-cover.png +0 -0
  27. maiman-0.1.0/docs/ARCHITECTURE.md +562 -0
  28. maiman-0.1.0/docs/capture_screenshots.py +113 -0
  29. maiman-0.1.0/docs/images/studio-graphite.png +0 -0
  30. maiman-0.1.0/docs/images/studio-paper.png +0 -0
  31. maiman-0.1.0/docs/render_og_cover.py +366 -0
  32. maiman-0.1.0/examples/amplified_link.py +140 -0
  33. maiman-0.1.0/examples/coherent_link.py +118 -0
  34. maiman-0.1.0/examples/dispersion_link.py +245 -0
  35. maiman-0.1.0/examples/dualpol_link.py +147 -0
  36. maiman-0.1.0/examples/export_ui_data.py +514 -0
  37. maiman-0.1.0/examples/microring_filter.py +197 -0
  38. maiman-0.1.0/examples/mzi_interleaver.py +154 -0
  39. maiman-0.1.0/examples/ook_eye.maiman +209 -0
  40. maiman-0.1.0/examples/ook_link.maiman +188 -0
  41. maiman-0.1.0/examples/ook_link.py +150 -0
  42. maiman-0.1.0/examples/pdk_import.py +109 -0
  43. maiman-0.1.0/examples/reference_rates.py +321 -0
  44. maiman-0.1.0/examples/silicon_220nm.pdk.json +127 -0
  45. maiman-0.1.0/examples/ui_data.json +19500 -0
  46. maiman-0.1.0/examples/wdm_demux.py +222 -0
  47. maiman-0.1.0/examples/wdm_nonlinear.py +448 -0
  48. maiman-0.1.0/examples/wdm_osa.maiman +424 -0
  49. maiman-0.1.0/examples/zr400.maiman +662 -0
  50. maiman-0.1.0/examples/zr800.maiman +662 -0
  51. maiman-0.1.0/pyproject.toml +88 -0
  52. maiman-0.1.0/src/maiman/__init__.py +83 -0
  53. maiman-0.1.0/src/maiman/__main__.py +14 -0
  54. maiman-0.1.0/src/maiman/analysis.py +669 -0
  55. maiman-0.1.0/src/maiman/backend.py +188 -0
  56. maiman-0.1.0/src/maiman/circuit.py +257 -0
  57. maiman-0.1.0/src/maiman/cli.py +90 -0
  58. maiman-0.1.0/src/maiman/component.py +432 -0
  59. maiman-0.1.0/src/maiman/components/__init__.py +112 -0
  60. maiman-0.1.0/src/maiman/components/amplifiers.py +216 -0
  61. maiman-0.1.0/src/maiman/components/analyzers.py +224 -0
  62. maiman-0.1.0/src/maiman/components/coherent.py +452 -0
  63. maiman-0.1.0/src/maiman/components/detectors.py +234 -0
  64. maiman-0.1.0/src/maiman/components/dsp.py +573 -0
  65. maiman-0.1.0/src/maiman/components/electrical.py +654 -0
  66. maiman-0.1.0/src/maiman/components/fiber.py +677 -0
  67. maiman-0.1.0/src/maiman/components/filters.py +263 -0
  68. maiman-0.1.0/src/maiman/components/mapping.py +192 -0
  69. maiman-0.1.0/src/maiman/components/meters.py +69 -0
  70. maiman-0.1.0/src/maiman/components/modulators.py +203 -0
  71. maiman-0.1.0/src/maiman/components/passive.py +270 -0
  72. maiman-0.1.0/src/maiman/components/photonic.py +667 -0
  73. maiman-0.1.0/src/maiman/components/sources.py +183 -0
  74. maiman-0.1.0/src/maiman/context.py +95 -0
  75. maiman-0.1.0/src/maiman/dsp.py +910 -0
  76. maiman-0.1.0/src/maiman/encoding.py +446 -0
  77. maiman-0.1.0/src/maiman/fec.py +402 -0
  78. maiman-0.1.0/src/maiman/graph.py +400 -0
  79. maiman-0.1.0/src/maiman/kernels.py +1047 -0
  80. maiman-0.1.0/src/maiman/modulation.py +494 -0
  81. maiman-0.1.0/src/maiman/pdk.py +397 -0
  82. maiman-0.1.0/src/maiman/photonics.py +490 -0
  83. maiman-0.1.0/src/maiman/project.py +154 -0
  84. maiman-0.1.0/src/maiman/registry.py +80 -0
  85. maiman-0.1.0/src/maiman/server.py +556 -0
  86. maiman-0.1.0/src/maiman/signals.py +786 -0
  87. maiman-0.1.0/src/maiman/softfec.py +726 -0
  88. maiman-0.1.0/src/maiman/studio/index.html +23174 -0
  89. maiman-0.1.0/src/maiman/sweep.py +239 -0
  90. maiman-0.1.0/src/maiman/units.py +180 -0
  91. maiman-0.1.0/tests/conftest.py +15 -0
  92. maiman-0.1.0/tests/hostile_backend.py +125 -0
  93. maiman-0.1.0/tests/test_amplifier.py +485 -0
  94. maiman-0.1.0/tests/test_apd.py +224 -0
  95. maiman-0.1.0/tests/test_ase_beat.py +723 -0
  96. maiman-0.1.0/tests/test_backend.py +386 -0
  97. maiman-0.1.0/tests/test_ber.py +392 -0
  98. maiman-0.1.0/tests/test_cd_compensation.py +429 -0
  99. maiman-0.1.0/tests/test_cd_estimation.py +501 -0
  100. maiman-0.1.0/tests/test_circuit.py +306 -0
  101. maiman-0.1.0/tests/test_coherent.py +535 -0
  102. maiman-0.1.0/tests/test_core.py +180 -0
  103. maiman-0.1.0/tests/test_dispersion.py +256 -0
  104. maiman-0.1.0/tests/test_dispersion_slope.py +490 -0
  105. maiman-0.1.0/tests/test_dsp.py +677 -0
  106. maiman-0.1.0/tests/test_dualpol.py +649 -0
  107. maiman-0.1.0/tests/test_fec.py +595 -0
  108. maiman-0.1.0/tests/test_fwm_accumulation.py +392 -0
  109. maiman-0.1.0/tests/test_modulation.py +425 -0
  110. maiman-0.1.0/tests/test_nonlinear.py +361 -0
  111. maiman-0.1.0/tests/test_optical_filter.py +494 -0
  112. maiman-0.1.0/tests/test_packaging.py +336 -0
  113. maiman-0.1.0/tests/test_pdk.py +363 -0
  114. maiman-0.1.0/tests/test_photonics.py +904 -0
  115. maiman-0.1.0/tests/test_physics.py +411 -0
  116. maiman-0.1.0/tests/test_pmd.py +258 -0
  117. maiman-0.1.0/tests/test_polarization_kerr.py +302 -0
  118. maiman-0.1.0/tests/test_project.py +514 -0
  119. maiman-0.1.0/tests/test_raman.py +306 -0
  120. maiman-0.1.0/tests/test_receiver.py +212 -0
  121. maiman-0.1.0/tests/test_reference_rates.py +239 -0
  122. maiman-0.1.0/tests/test_scheduler.py +326 -0
  123. maiman-0.1.0/tests/test_server.py +1030 -0
  124. maiman-0.1.0/tests/test_shaping.py +394 -0
  125. maiman-0.1.0/tests/test_softfec.py +348 -0
  126. maiman-0.1.0/tests/test_studio.py +736 -0
  127. maiman-0.1.0/tests/test_sweep.py +345 -0
  128. maiman-0.1.0/tests/test_transmitter.py +245 -0
  129. maiman-0.1.0/tests/test_wdm_nonlinearity.py +585 -0
@@ -0,0 +1,8 @@
1
+ # Normalize line endings: LF in the repository, native in the working tree.
2
+ * text=auto eol=lf
3
+
4
+ *.png binary
5
+ *.jpg binary
6
+ *.gds binary
7
+ *.npy binary
8
+ *.npz binary
@@ -0,0 +1,65 @@
1
+ name: Bug report
2
+ description: Something crashes, refuses valid input, or does not do what it says.
3
+ title: "[bug] "
4
+ labels: ["bug"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ For a result that is *physically* wrong rather than broken, use **A model is wrong**
10
+ instead — it asks for the reference, which is what makes that kind of report actionable.
11
+
12
+ - type: textarea
13
+ id: reproduce
14
+ attributes:
15
+ label: What you ran
16
+ description: >-
17
+ The smallest script that shows it. A graph with four blocks that fails is worth ten times
18
+ a forty-block one that also fails.
19
+ render: python
20
+ validations:
21
+ required: true
22
+
23
+ - type: textarea
24
+ id: happened
25
+ attributes:
26
+ label: What happened
27
+ description: The full traceback, if there is one. Do not trim it — the frames matter.
28
+ render: shell
29
+ validations:
30
+ required: true
31
+
32
+ - type: input
33
+ id: expected
34
+ attributes:
35
+ label: What you expected instead
36
+ validations:
37
+ required: true
38
+
39
+ - type: input
40
+ id: version
41
+ attributes:
42
+ label: Commit
43
+ description: "`git rev-parse --short HEAD`"
44
+ validations:
45
+ required: true
46
+
47
+ - type: input
48
+ id: environment
49
+ attributes:
50
+ label: Python and OS
51
+ description: "`python -VV` and your platform"
52
+ placeholder: "3.12.4, Windows 11"
53
+ validations:
54
+ required: true
55
+
56
+ - type: checkboxes
57
+ id: suite
58
+ attributes:
59
+ label: Before opening
60
+ options:
61
+ - label: >-
62
+ The suite passes on my checkout
63
+ (`ruff check . && ruff format --check . && mypy && pytest`), so this is not my
64
+ environment. Tick it or say why not — a failing suite is itself worth reporting.
65
+ required: false
@@ -0,0 +1,51 @@
1
+ name: A missing component or capability
2
+ description: Something you need to model a link that this cannot express.
3
+ title: "[request] "
4
+ labels: ["enhancement"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ The most useful version of this issue describes **the link you are trying to build**, not
10
+ just the block you want. What a component has to get right is decided by what it sits
11
+ between, and that is usually clear from the system and rarely from the block alone.
12
+
13
+ - type: textarea
14
+ id: use_case
15
+ attributes:
16
+ label: What are you trying to model
17
+ description: >-
18
+ The system, the rates, the formats, what you need to read out at the end. If you currently
19
+ do this in another tool, saying which is useful — including if the answer is a spreadsheet.
20
+ validations:
21
+ required: true
22
+
23
+ - type: textarea
24
+ id: missing
25
+ attributes:
26
+ label: What is missing
27
+ placeholder: "A Raman amplifier with counter-propagating pumps; export of sweep results as CSV"
28
+ validations:
29
+ required: true
30
+
31
+ - type: textarea
32
+ id: reference
33
+ attributes:
34
+ label: A model to build it from, if you have one
35
+ description: >-
36
+ Optional, and it is what decides whether this is a weekend or a month. A component here
37
+ needs a published model and a closed-form result to be tested against; without a reference
38
+ the first work is finding one.
39
+ validations:
40
+ required: false
41
+
42
+ - type: dropdown
43
+ id: offer
44
+ attributes:
45
+ label: Would you want to implement it
46
+ options:
47
+ - "No — flagging it"
48
+ - "Maybe, with guidance on where it goes"
49
+ - "Yes"
50
+ validations:
51
+ required: true
@@ -0,0 +1,11 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Report a security vulnerability
4
+ url: https://github.com/ehsun-sh/maiman-studio/security/advisories/new
5
+ about: >-
6
+ Privately, not in the issue tracker. See SECURITY.md for what does and does not count.
7
+ - name: Does this duplicate something that already exists?
8
+ url: https://github.com/ehsun-sh/maiman-studio/issues/new
9
+ about: >-
10
+ Open a blank issue and say so. Knowing early is worth more than most feature requests —
11
+ several months could go into work someone has already done better.
@@ -0,0 +1,54 @@
1
+ name: A model is wrong
2
+ description: A physics block does not agree with the literature, a standard, or measurement.
3
+ title: "[model] "
4
+ labels: ["model", "needs-reference"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ This is the most valuable issue you can open here, and it is deliberately the first one
10
+ offered.
11
+
12
+ Every physics block in this project is validated against a closed-form result, and the
13
+ validation is only as good as the closed form it was checked against. If one of those is
14
+ the wrong expression, the wrong regime, or the right expression misapplied, everything
15
+ downstream of it inherits the error and the test suite agrees with it enthusiastically.
16
+
17
+ - type: input
18
+ id: block
19
+ attributes:
20
+ label: Which block or function
21
+ placeholder: "maiman.components.EDFA, or maiman.dsp.estimate_timing"
22
+ validations:
23
+ required: true
24
+
25
+ - type: textarea
26
+ id: expected
27
+ attributes:
28
+ label: What it should do, and where that comes from
29
+ description: >-
30
+ A paper with an equation number, a standard with a clause, or your own measurement.
31
+ This project does not accept models derived from inspection of commercial tools — not
32
+ because they are wrong, but because nobody without a licence can check them.
33
+ placeholder: |
34
+ Agrawal, Fiber-Optic Communication Systems, 4th ed., eq. (2.4.15) gives ...
35
+ ITU-T G.652 clause 8.2 specifies ...
36
+ validations:
37
+ required: true
38
+
39
+ - type: textarea
40
+ id: observed
41
+ attributes:
42
+ label: What it does instead
43
+ description: Numbers, please. A script that produces them is better still.
44
+ render: python
45
+ validations:
46
+ required: true
47
+
48
+ - type: input
49
+ id: version
50
+ attributes:
51
+ label: Commit
52
+ description: "`git rev-parse --short HEAD`. There are no releases yet, so the commit is the version."
53
+ validations:
54
+ required: true
@@ -0,0 +1,79 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ check:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.11", "3.12", "3.13"]
20
+
21
+ steps:
22
+ - uses: actions/checkout@v5
23
+
24
+ - uses: actions/setup-python@v6
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ cache: pip
28
+
29
+ - name: Install
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install -e ".[dev]"
33
+
34
+ - name: Lint
35
+ run: |
36
+ ruff check .
37
+ ruff format --check .
38
+
39
+ - name: Type check
40
+ run: mypy
41
+
42
+ - name: Test
43
+ run: pytest
44
+
45
+ # Runs only where a CUDA device is actually attached. The default job above
46
+ # tests what would break a GPU port — that the kernels never reach for NumPy
47
+ # directly, against a deliberately hostile array library; this one tests that
48
+ # the port is not broken, on hardware.
49
+ #
50
+ # Gated on a repository *variable* rather than on the runner label alone. A job
51
+ # that merely asks for a label nobody provides is not skipped, it is queued —
52
+ # for hours, holding the whole run pending and the badge with it. Set
53
+ # `HAS_GPU_RUNNER` to `true` under Settings → Secrets and variables → Actions
54
+ # once a machine labelled `gpu` is attached, and this starts running; until
55
+ # then it is skipped, which costs nothing and claims nothing.
56
+ device:
57
+ if: ${{ vars.HAS_GPU_RUNNER == 'true' }}
58
+ runs-on: [self-hosted, gpu]
59
+
60
+ steps:
61
+ - uses: actions/checkout@v5
62
+
63
+ - uses: actions/setup-python@v6
64
+ with:
65
+ python-version: "3.12"
66
+
67
+ - name: Install, with CuPy
68
+ run: |
69
+ python -m pip install --upgrade pip
70
+ pip install -e ".[dev]"
71
+ pip install cupy-cuda12x
72
+
73
+ # One soliton period on the device against the same span on NumPy. Exits
74
+ # non-zero only if a back-end is present and disagrees.
75
+ - name: Cross-check the device against NumPy
76
+ run: maiman devices
77
+
78
+ - name: Test
79
+ run: pytest
@@ -0,0 +1,128 @@
1
+ # Publish to PyPI.
2
+ #
3
+ # **No API token anywhere.** This uses PyPI's trusted publishing: GitHub mints a
4
+ # short-lived OIDC identity for this workflow, PyPI checks it against a publisher
5
+ # it was told to trust, and the upload is authorised without a secret existing at
6
+ # all. A token in repository secrets is a long-lived credential that can be
7
+ # leaked, has to be rotated, and grants upload rights to anyone who obtains it.
8
+ # This has none of those properties, which is why it is worth the one-time setup
9
+ # described in RELEASING.md.
10
+ #
11
+ # It runs on a published GitHub release rather than on a tag push, so the release
12
+ # notes and the artifacts are one act. The build is checked before it is
13
+ # uploaded, and the tag has to agree with the version in pyproject.toml -- a
14
+ # mismatch there is unrecoverable once uploaded, because PyPI never lets a
15
+ # version number be reused.
16
+
17
+ name: Release
18
+
19
+ on:
20
+ release:
21
+ types: [published]
22
+ workflow_dispatch:
23
+ inputs:
24
+ target:
25
+ description: "Where to publish"
26
+ required: true
27
+ default: testpypi
28
+ type: choice
29
+ options: [testpypi, pypi]
30
+
31
+ permissions:
32
+ contents: read
33
+
34
+ jobs:
35
+ build:
36
+ name: Build and check
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@v5
40
+
41
+ - uses: actions/setup-python@v6
42
+ with:
43
+ python-version: "3.12"
44
+
45
+ - name: Install the build tools
46
+ run: python -m pip install --upgrade build twine
47
+
48
+ - name: Refuse a tag that disagrees with the version
49
+ if: github.event_name == 'release'
50
+ run: |
51
+ declared=$(python -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
52
+ tag="${GITHUB_REF_NAME#v}"
53
+ echo "pyproject says $declared, tag says $tag"
54
+ if [ "$declared" != "$tag" ]; then
55
+ echo "::error::tag $GITHUB_REF_NAME does not match version $declared in pyproject.toml"
56
+ exit 1
57
+ fi
58
+
59
+ - name: Build
60
+ run: python -m build
61
+
62
+ - name: Check the metadata renders
63
+ run: python -m twine check --strict dist/*
64
+
65
+ - name: Install the wheel somewhere clean and run a link through it
66
+ run: |
67
+ python -m venv /tmp/fresh
68
+ /tmp/fresh/bin/python -m pip install --quiet dist/*.whl
69
+ cd /tmp
70
+ /tmp/fresh/bin/python - <<'PY'
71
+ import pathlib
72
+
73
+ import maiman
74
+ from maiman import Graph, SimulationContext
75
+ from maiman.components import CWLaser, PowerMeter
76
+
77
+ ctx = SimulationContext(bit_rate=10e9, samples_per_symbol=8, sequence_length=64)
78
+ graph = Graph(ctx)
79
+ laser = graph.add(CWLaser(power=0.0))
80
+ meter = graph.add(PowerMeter())
81
+ graph.chain(laser, meter)
82
+ # 1e-6, not 1e-9: converting watts to dBm and back costs a couple of
83
+ # parts in 1e7 on its own. The tighter bound passed when I wrote it
84
+ # only because I printed the number rounded to six places.
85
+ assert abs(graph.run()[meter].power_dbm) < 1e-6
86
+
87
+ page = pathlib.Path(maiman.__file__).parent / "studio" / "index.html"
88
+ assert page.exists(), "the wheel does not carry the studio page"
89
+ assert len(maiman.registered_names()) > 40
90
+ print(f"maiman {maiman.__version__}: a link ran and the page is present")
91
+ PY
92
+
93
+ - uses: actions/upload-artifact@v7
94
+ with:
95
+ name: distributions
96
+ path: dist/
97
+
98
+ testpypi:
99
+ name: Publish to TestPyPI
100
+ needs: build
101
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
102
+ runs-on: ubuntu-latest
103
+ environment: testpypi
104
+ permissions:
105
+ id-token: write # the OIDC identity, and the only credential involved
106
+ steps:
107
+ - uses: actions/download-artifact@v8
108
+ with:
109
+ name: distributions
110
+ path: dist/
111
+ - uses: pypa/gh-action-pypi-publish@release/v1
112
+ with:
113
+ repository-url: https://test.pypi.org/legacy/
114
+
115
+ pypi:
116
+ name: Publish to PyPI
117
+ needs: build
118
+ if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
119
+ runs-on: ubuntu-latest
120
+ environment: pypi
121
+ permissions:
122
+ id-token: write
123
+ steps:
124
+ - uses: actions/download-artifact@v8
125
+ with:
126
+ name: distributions
127
+ path: dist/
128
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,68 @@
1
+ # Byte-compiled / optimized
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.pyd
7
+
8
+ # Packaging / build
9
+ build/
10
+ dist/
11
+ sdist/
12
+ wheels/
13
+ *.egg-info/
14
+ *.egg
15
+ .eggs/
16
+ MANIFEST
17
+
18
+ # Virtual environments
19
+ .venv/
20
+ venv/
21
+ env/
22
+ ENV/
23
+
24
+ # Testing / coverage / type checking
25
+ .pytest_cache/
26
+ .tox/
27
+ .nox/
28
+ .coverage
29
+ .coverage.*
30
+ coverage.xml
31
+ htmlcov/
32
+ .hypothesis/
33
+ .mypy_cache/
34
+ .pyright/
35
+ .ruff_cache/
36
+
37
+ # Jupyter
38
+ .ipynb_checkpoints/
39
+ profile_default/
40
+ ipython_config.py
41
+
42
+ # Simulation output — results are reproducible from the project file + seed,
43
+ # so they are regenerated, never committed.
44
+ results/
45
+ *.npy
46
+ *.npz
47
+ *.h5
48
+ *.hdf5
49
+
50
+ # Node / web UI (from Phase 2 onward)
51
+ node_modules/
52
+ .next/
53
+ .vite/
54
+ *.tsbuildinfo
55
+
56
+ # Editors / IDEs
57
+ .vscode/
58
+ .idea/
59
+ *.swp
60
+ *~
61
+
62
+ # OS
63
+ .DS_Store
64
+ Thumbs.db
65
+ desktop.ini
66
+
67
+ # Local agent/tooling config
68
+ .claude/
@@ -0,0 +1,44 @@
1
+ cff-version: 1.2.0
2
+ title: "Maiman Studio"
3
+ message: >-
4
+ If you use this software in work you publish, please cite it as below.
5
+ Please also cite the primary sources for whichever physics you relied on:
6
+ every component's docstring names the paper or standard its model comes
7
+ from, and those authors did the work this only implements.
8
+ type: software
9
+ authors:
10
+ - family-names: Shahmohammadi
11
+ given-names: Ehsan
12
+ email: ehsun.ca@gmail.com
13
+ repository-code: "https://github.com/ehsun-sh/maiman-studio"
14
+ url: "https://github.com/ehsun-sh/maiman-studio"
15
+ abstract: >-
16
+ An open-source simulator for optical communication links and photonic
17
+ systems. Links are built as block diagrams — laser, modulator, fiber,
18
+ amplifier, detector, receiver DSP — and read back as eye diagrams, bit
19
+ error rates, OSNR figures and constellation measurements. Every physics
20
+ block is validated against a closed-form result in continuous
21
+ integration, and models are derived from published literature and
22
+ standards rather than from inspection of commercial tools.
23
+
24
+ The project is named after Theodore Maiman, who built the first working
25
+ laser at Hughes Research Laboratories in May 1960.
26
+ keywords:
27
+ - photonics
28
+ - optical communications
29
+ - fiber optics
30
+ - simulation
31
+ - digital signal processing
32
+ - forward error correction
33
+ - photonic integrated circuits
34
+ license: Apache-2.0
35
+ version: 0.1.0
36
+
37
+ # No `date-released`, deliberately. There has been no release: the version
38
+ # above is a development version, the package is not on PyPI, and the field
39
+ # would have to be invented to be present. Cite the repository and, if the
40
+ # result matters, the commit you ran — `git rev-parse HEAD` — because a
41
+ # pre-alpha tree is not a stable thing to cite by name alone.
42
+ #
43
+ # No ORCID and no affiliation either, for the same reason: neither is on
44
+ # hand, and a citation file is exactly the wrong place to approximate.
@@ -0,0 +1,115 @@
1
+ # Contributing
2
+
3
+ The core is small enough that changing it is still cheap, which makes right now the most useful
4
+ time to push back on it. The [README's Contributing section](README.md#contributing) says where
5
+ help is most valuable; this file says how the project actually works, and what it refuses.
6
+
7
+ ## The one command that decides
8
+
9
+ ```bash
10
+ pip install -e ".[dev]" && ruff check . && ruff format --check . && mypy && pytest
11
+ ```
12
+
13
+ Run **that**, not a narrower version of it. Two red builds in this repository's short history came
14
+ from running a subset: `mypy src tests examples` instead of bare `mypy`, and `ruff format tests`
15
+ instead of `ruff format .`. Both passed locally and both failed in CI, because the command that
16
+ decides is the whole one. `mypy` takes no arguments here on purpose — its configuration already
17
+ names what to check.
18
+
19
+ ## What this project refuses
20
+
21
+ These are not style preferences. They are the reason the simulator is worth trusting, and a change
22
+ that breaks one will not be merged however good the code is.
23
+
24
+ **Every number shown must come from the engine.** Not from a plausible-looking constant, not from
25
+ a curve fitted to a screenshot, not from a figure remembered from a datasheet. If the interface
26
+ displays it, a component computed it.
27
+
28
+ **Every physics block ships with a test against a closed-form result.** Not "the code runs" — a
29
+ comparison against an analytical answer the block cannot have been written to agree with by
30
+ accident. See [`tests/test_physics.py`](tests/test_physics.py). A component without one will not be
31
+ merged, and this is the rule most often argued with.
32
+
33
+ **Models come from published literature and standards, cited in the docstring — never from
34
+ inspection of commercial tools.** Comparing against a licensed tool is not reproducible in CI and
35
+ proves nothing to anyone who cannot afford the licence. Name the paper, the equation number, and
36
+ the edition.
37
+
38
+ **Derive rather than transcribe, where you can.** A generator polynomial's *roots* are the claim; a
39
+ table of its coefficients is a copy that no test can check. Where a table genuinely has to be
40
+ present — the primitive polynomials in [`src/maiman/softfec.py`](src/maiman/softfec.py) — the code
41
+ verifies it at construction, so the table is an assertion rather than an assumption.
42
+
43
+ **Never ship a claim you cannot back.** If a model does not work, say so in the docstring, write a
44
+ test that pins the failure, and do not tune it until a curve looks plausible. There is precedent in
45
+ this repository for exactly that, including the commit that had to correct its own diagnosis.
46
+
47
+ **The interface never shows a control it cannot back.** A parameter that moves nothing is worse
48
+ than a missing feature, because the user cannot tell. If a model stops using a parameter, retire it
49
+ — see `retired_parameters` in [`src/maiman/component.py`](src/maiman/component.py) — and say in the
50
+ comment whether dropping it changes the link a saved project describes. Usually it does not. Once,
51
+ it did, and the comment says so.
52
+
53
+ **An idealisation is declared, not delivered by accident.** A laser with no intensity noise, an
54
+ amplifier that never compresses — these are fine and useful, and they are flags with defaults, not
55
+ silence. What is not fine is a clamp that calls itself a saturation model.
56
+
57
+ ## Adding a component
58
+
59
+ A component is a Python class with declared parameters and typed ports. Read a few in
60
+ [`src/maiman/components/`](src/maiman/components/) first — `CWLaser` for a source, `Fiber` for a
61
+ channel, `TimingRecovery` for a DSP block with a diagnostics port.
62
+
63
+ 1. Subclass `Component`. Set `display_name`, `category`, `inputs` and `outputs`.
64
+ 2. Declare parameters with `Param` / `BoolParam`, each with its real unit and a `doc` that says
65
+ what it does. Units convert once, at the boundary, via `si()`; see
66
+ [`src/maiman/units.py`](src/maiman/units.py).
67
+ 3. Implement `run(ctx, inputs) -> dict[str, Signal]`.
68
+ 4. Export it from [`src/maiman/components/__init__.py`](src/maiman/components/__init__.py).
69
+ 5. Write the closed-form test.
70
+ 6. Regenerate the interface's baked data and splice it back into the page:
71
+
72
+ ```bash
73
+ python examples/export_ui_data.py
74
+ ```
75
+
76
+ Then copy `examples/ui_data.json` verbatim into the `<script id="maiman-data">` block of
77
+ `src/maiman/studio/index.html`. The tests below will tell you if you forget.
78
+
79
+ If your component emits a new kind of measurement, it also needs an encoder in
80
+ [`src/maiman/encoding.py`](src/maiman/encoding.py) and a `case` in the studio page's `describe()`
81
+ so the log prints a fact rather than the name of a type.
82
+
83
+ ## The guards, and what they mean when they fire
84
+
85
+ Several tests exist only to catch drift between parts that have no other way to disagree. They are
86
+ not obstacles; each one has caught a real mistake, most of them more than once.
87
+
88
+ | Test | Fires when | Do this |
89
+ | :--- | :--- | :--- |
90
+ | `test_the_palette_is_the_whole_library` | A component is registered but not in the baked export | Re-run the export and splice |
91
+ | `test_the_baked_manifests_are_the_live_ones` | A *parameter* changed since the last export | Same — this one catches what the palette test cannot |
92
+ | `test_every_kind_the_engine_can_send_has_a_line_to_print` | A new encoded kind has no `case` in `describe()` | Give the log the fact the payload carries |
93
+ | `test_no_metric_port_in_the_library_encodes_as_opaque` | A new metric port is never exercised | Add a graph in `tests/test_server.py` that runs it |
94
+ | `test_every_block_in_the_project_has_somewhere_to_be_drawn` | A block in the shipped project has no canvas position | Add it to `LAYOUT` in the studio page |
95
+ | `test_the_schematic_on_the_canvas_is_a_graph_that_runs` | The shipped project does not execute | The page is showing a link that cannot exist — fix the project |
96
+ | The test-count floor in `test_packaging.py` | The suite shrank below what the docs claim | Either you deleted tests, or a doc needs updating |
97
+
98
+ ## Documentation
99
+
100
+ `README.md` is the argument for the project and carries its measured results. `PRODUCT.md` is who
101
+ it is for. `DESIGN.md` is the interface's reasoning, including a "Not done" section that is kept
102
+ honest. `docs/ARCHITECTURE.md` is the engine's.
103
+
104
+ Numbers in those files are **measured**, and several of them are pinned by tests. If you change a
105
+ model, re-measure rather than adjusting the prose to match your intuition.
106
+
107
+ ## Commits
108
+
109
+ Explain *why*, and what you learned. A commit here is expected to say what was measured, what was
110
+ surprising, and what was wrong before — including when the thing that was wrong was the previous
111
+ commit's own explanation. That is not ceremony; it is the only durable record of why a model looks
112
+ the way it does.
113
+
114
+ Open an issue before a large change, so nobody spends a weekend on something that was already
115
+ decided against.