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.
- maiman-0.1.0/.gitattributes +8 -0
- maiman-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +65 -0
- maiman-0.1.0/.github/ISSUE_TEMPLATE/component_or_feature.yml +51 -0
- maiman-0.1.0/.github/ISSUE_TEMPLATE/config.yml +11 -0
- maiman-0.1.0/.github/ISSUE_TEMPLATE/model_is_wrong.yml +54 -0
- maiman-0.1.0/.github/workflows/ci.yml +79 -0
- maiman-0.1.0/.github/workflows/release.yml +128 -0
- maiman-0.1.0/.gitignore +68 -0
- maiman-0.1.0/CITATION.cff +44 -0
- maiman-0.1.0/CONTRIBUTING.md +115 -0
- maiman-0.1.0/DESIGN.md +320 -0
- maiman-0.1.0/LICENSE +202 -0
- maiman-0.1.0/PKG-INFO +1829 -0
- maiman-0.1.0/PRODUCT.md +95 -0
- maiman-0.1.0/README.md +1803 -0
- maiman-0.1.0/RELEASING.md +135 -0
- maiman-0.1.0/SECURITY.md +79 -0
- maiman-0.1.0/assets/apple-touch-icon.png +0 -0
- maiman-0.1.0/assets/icon-192.png +0 -0
- maiman-0.1.0/assets/icon-32.png +0 -0
- maiman-0.1.0/assets/icon-512.png +0 -0
- maiman-0.1.0/assets/logo-mark-tight.png +0 -0
- maiman-0.1.0/assets/logo-mark.png +0 -0
- maiman-0.1.0/assets/logo-wordmark-dark.png +0 -0
- maiman-0.1.0/assets/logo-wordmark.png +0 -0
- maiman-0.1.0/assets/og-cover.png +0 -0
- maiman-0.1.0/docs/ARCHITECTURE.md +562 -0
- maiman-0.1.0/docs/capture_screenshots.py +113 -0
- maiman-0.1.0/docs/images/studio-graphite.png +0 -0
- maiman-0.1.0/docs/images/studio-paper.png +0 -0
- maiman-0.1.0/docs/render_og_cover.py +366 -0
- maiman-0.1.0/examples/amplified_link.py +140 -0
- maiman-0.1.0/examples/coherent_link.py +118 -0
- maiman-0.1.0/examples/dispersion_link.py +245 -0
- maiman-0.1.0/examples/dualpol_link.py +147 -0
- maiman-0.1.0/examples/export_ui_data.py +514 -0
- maiman-0.1.0/examples/microring_filter.py +197 -0
- maiman-0.1.0/examples/mzi_interleaver.py +154 -0
- maiman-0.1.0/examples/ook_eye.maiman +209 -0
- maiman-0.1.0/examples/ook_link.maiman +188 -0
- maiman-0.1.0/examples/ook_link.py +150 -0
- maiman-0.1.0/examples/pdk_import.py +109 -0
- maiman-0.1.0/examples/reference_rates.py +321 -0
- maiman-0.1.0/examples/silicon_220nm.pdk.json +127 -0
- maiman-0.1.0/examples/ui_data.json +19500 -0
- maiman-0.1.0/examples/wdm_demux.py +222 -0
- maiman-0.1.0/examples/wdm_nonlinear.py +448 -0
- maiman-0.1.0/examples/wdm_osa.maiman +424 -0
- maiman-0.1.0/examples/zr400.maiman +662 -0
- maiman-0.1.0/examples/zr800.maiman +662 -0
- maiman-0.1.0/pyproject.toml +88 -0
- maiman-0.1.0/src/maiman/__init__.py +83 -0
- maiman-0.1.0/src/maiman/__main__.py +14 -0
- maiman-0.1.0/src/maiman/analysis.py +669 -0
- maiman-0.1.0/src/maiman/backend.py +188 -0
- maiman-0.1.0/src/maiman/circuit.py +257 -0
- maiman-0.1.0/src/maiman/cli.py +90 -0
- maiman-0.1.0/src/maiman/component.py +432 -0
- maiman-0.1.0/src/maiman/components/__init__.py +112 -0
- maiman-0.1.0/src/maiman/components/amplifiers.py +216 -0
- maiman-0.1.0/src/maiman/components/analyzers.py +224 -0
- maiman-0.1.0/src/maiman/components/coherent.py +452 -0
- maiman-0.1.0/src/maiman/components/detectors.py +234 -0
- maiman-0.1.0/src/maiman/components/dsp.py +573 -0
- maiman-0.1.0/src/maiman/components/electrical.py +654 -0
- maiman-0.1.0/src/maiman/components/fiber.py +677 -0
- maiman-0.1.0/src/maiman/components/filters.py +263 -0
- maiman-0.1.0/src/maiman/components/mapping.py +192 -0
- maiman-0.1.0/src/maiman/components/meters.py +69 -0
- maiman-0.1.0/src/maiman/components/modulators.py +203 -0
- maiman-0.1.0/src/maiman/components/passive.py +270 -0
- maiman-0.1.0/src/maiman/components/photonic.py +667 -0
- maiman-0.1.0/src/maiman/components/sources.py +183 -0
- maiman-0.1.0/src/maiman/context.py +95 -0
- maiman-0.1.0/src/maiman/dsp.py +910 -0
- maiman-0.1.0/src/maiman/encoding.py +446 -0
- maiman-0.1.0/src/maiman/fec.py +402 -0
- maiman-0.1.0/src/maiman/graph.py +400 -0
- maiman-0.1.0/src/maiman/kernels.py +1047 -0
- maiman-0.1.0/src/maiman/modulation.py +494 -0
- maiman-0.1.0/src/maiman/pdk.py +397 -0
- maiman-0.1.0/src/maiman/photonics.py +490 -0
- maiman-0.1.0/src/maiman/project.py +154 -0
- maiman-0.1.0/src/maiman/registry.py +80 -0
- maiman-0.1.0/src/maiman/server.py +556 -0
- maiman-0.1.0/src/maiman/signals.py +786 -0
- maiman-0.1.0/src/maiman/softfec.py +726 -0
- maiman-0.1.0/src/maiman/studio/index.html +23174 -0
- maiman-0.1.0/src/maiman/sweep.py +239 -0
- maiman-0.1.0/src/maiman/units.py +180 -0
- maiman-0.1.0/tests/conftest.py +15 -0
- maiman-0.1.0/tests/hostile_backend.py +125 -0
- maiman-0.1.0/tests/test_amplifier.py +485 -0
- maiman-0.1.0/tests/test_apd.py +224 -0
- maiman-0.1.0/tests/test_ase_beat.py +723 -0
- maiman-0.1.0/tests/test_backend.py +386 -0
- maiman-0.1.0/tests/test_ber.py +392 -0
- maiman-0.1.0/tests/test_cd_compensation.py +429 -0
- maiman-0.1.0/tests/test_cd_estimation.py +501 -0
- maiman-0.1.0/tests/test_circuit.py +306 -0
- maiman-0.1.0/tests/test_coherent.py +535 -0
- maiman-0.1.0/tests/test_core.py +180 -0
- maiman-0.1.0/tests/test_dispersion.py +256 -0
- maiman-0.1.0/tests/test_dispersion_slope.py +490 -0
- maiman-0.1.0/tests/test_dsp.py +677 -0
- maiman-0.1.0/tests/test_dualpol.py +649 -0
- maiman-0.1.0/tests/test_fec.py +595 -0
- maiman-0.1.0/tests/test_fwm_accumulation.py +392 -0
- maiman-0.1.0/tests/test_modulation.py +425 -0
- maiman-0.1.0/tests/test_nonlinear.py +361 -0
- maiman-0.1.0/tests/test_optical_filter.py +494 -0
- maiman-0.1.0/tests/test_packaging.py +336 -0
- maiman-0.1.0/tests/test_pdk.py +363 -0
- maiman-0.1.0/tests/test_photonics.py +904 -0
- maiman-0.1.0/tests/test_physics.py +411 -0
- maiman-0.1.0/tests/test_pmd.py +258 -0
- maiman-0.1.0/tests/test_polarization_kerr.py +302 -0
- maiman-0.1.0/tests/test_project.py +514 -0
- maiman-0.1.0/tests/test_raman.py +306 -0
- maiman-0.1.0/tests/test_receiver.py +212 -0
- maiman-0.1.0/tests/test_reference_rates.py +239 -0
- maiman-0.1.0/tests/test_scheduler.py +326 -0
- maiman-0.1.0/tests/test_server.py +1030 -0
- maiman-0.1.0/tests/test_shaping.py +394 -0
- maiman-0.1.0/tests/test_softfec.py +348 -0
- maiman-0.1.0/tests/test_studio.py +736 -0
- maiman-0.1.0/tests/test_sweep.py +345 -0
- maiman-0.1.0/tests/test_transmitter.py +245 -0
- maiman-0.1.0/tests/test_wdm_nonlinearity.py +585 -0
|
@@ -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
|
maiman-0.1.0/.gitignore
ADDED
|
@@ -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.
|