Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3d09358
One name for the function that builds a chain
claude Aug 12, 2026
7740652
Write down where derived state lives (D50)
claude Aug 13, 2026
b484394
Let the link budget its own noise, and fix what direct assignment broke
claude Aug 13, 2026
a45b25d
Repaste the back-propagation tables from one run
claude Aug 13, 2026
2816d51
Count Monte-Carlo errors in one place, and leave the loops alone
claude Aug 13, 2026
273f758
Rename sweep to monte_carlo
claude Aug 13, 2026
298e6ad
Put the Hager reproduction back, in validation/
claude Aug 13, 2026
2e4f11e
Type budget() like the other info dictionaries
claude Aug 13, 2026
95777cd
Choose the step count by measuring it, not by asserting it
claude Aug 13, 2026
40328eb
Repaste the launch-power sweep at fifty steps
claude Aug 13, 2026
2a9d208
Delete two metric helpers the library already provides
claude Aug 13, 2026
eb73f30
Make the batch axis of a fibre real: documented, guaranteed, discover…
claude Aug 13, 2026
d76131c
Split the linear step into what it is and what it does
claude Aug 13, 2026
7dfd477
Repaste the two optical pages with the transfer cache in place
claude Aug 13, 2026
2071580
Run the slow examples, say when a curve is not reproducible, count sy…
claude Aug 13, 2026
ad4c7bf
Name the transfer-table type, and stop pretending the cache may be mi…
claude Aug 13, 2026
b46c0dc
Narrow all three cache fields, and say which NumPy the ratchet is rea…
claude Aug 13, 2026
43bb248
Derive the AWGN reference curve, then price four constellations again…
claude Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions .claude/skills/comnumpy-tutorial/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ then learns the wrong lesson -- they came to see how the library is used.
`compute_ccdf`, `compute_metric_awgn_theo` and
`compute_metric_rayleigh_theo` for the closed forms,
`constellation_capacity` and `bicm_capacity` for the rates.
- Monte-Carlo: `sweep(chain, param, values, metrics, stimulus, seed=)`.
- Monte-Carlo: `monte_carlo(chain, param, values, metrics, stimulus, seed=)`.
- Figures: `plot_iq`, `plot_error_rate`, `plot_spectrum`, `plot_time`,
`plot_channel_response`, and the channels' own `info()` / `plot()`.
- If the library makes something awkward, that is a finding about the
Expand All @@ -63,9 +63,39 @@ then learns the wrong lesson -- they came to see how the library is used.

## Structuring the example script

- One `get_*` function per assembly the page talks about, so that what
varies is an argument. `get_full_chain(n_spans)` beats building a chain
inline six times.
### Naming the chain

Three rules, and they are about where the reader looks and what the
signature promises.

1. **A function only when the chain is built more than once** -- several
values of a parameter, several variants, a Monte-Carlo. A chain built
once and used once is written inline: wrapping four blocks in a
function called once, with no argument, is ceremony, and it puts an
indirection between the reader and their first chain.
2. **Always `get_<thing>()`**, one prefix, the noun saying what comes
back. Which noun follows from how the page is organized:

| the page… | names |
|---|---|
| has one chain | `get_chain(...)` |
| cuts one chain in two | `get_channel()` and `get_receiver()` |
| compares two structurally different chains | `get_uncoded_chain()`, `get_coded_chain(soft)` |
| builds a sub-assembly, not a whole chain | the part: `get_transmitter()` |

Two chains that differ by a block or two are **one** function with a
parameter, not two functions. Two chains that differ in structure --
four blocks against six -- are two functions, because merging them
would hide behind a flag exactly what the page is comparing.
3. **What varies is a parameter, never a module global.** Constants of
the page (the fibre, the constellation, the roll-off) may stay
global; anything the page *varies* goes through the signature. A
function whose signature announces three arguments and silently reads
eleven more is a closure wearing a `def`, and the reader cannot tell
from the call site what changed.

### The rest of the script

- **Prefer one chain, transmitter to decision.** A full chain is what the
reader will write. Cutting the chain into pieces is justified only when a
Monte-Carlo re-runs an expensive stage that could be run once -- and then
Expand Down
94 changes: 94 additions & 0 deletions .github/scripts/run_slow_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Run the examples `tests/test_examples_run.py` marks as SLOW.

The smoke test skips them so that a pull request does not wait minutes
per script. Skipping is the right call there and the wrong one
everywhere: for a while nothing ran them at all, and a regression lived
in `CD_compensation_part1.py` for two commits -- it raised `TypeError:
'Constellation' object is not subscriptable` on its first pass while
every check stayed green.

The list is not repeated here. It is imported from the test module, so
adding an entry to `SLOW` is enough to have it covered, and an entry
that is removed stops being run without anyone editing a workflow.

Each script runs in its own process, from its own directory, with the
same sandbox the smoke test uses: the examples hardcode
`../../docs/tutorials/img/`, and a scheduled job has no business
rewriting figures that are committed. Every script runs even if an
earlier one failed -- one broken example must not hide the state of the
other eight.
"""
import os
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO))

from tests.test_examples_run import SLOW # noqa: E402 -- after sys.path

TIMEOUT_S = 1800


def sandbox() -> Path:
"""A throwaway copy of `examples/`, with the directories it writes to."""
root = Path(tempfile.mkdtemp(prefix="comnumpy_slow_"))
shutil.copytree(REPO / "examples", root / "examples",
ignore=shutil.ignore_patterns("__pycache__"))
for pattern in ("docs/**/img", "docs/**/mermaid"):
for directory in REPO.glob(pattern):
(root / directory.relative_to(REPO)).mkdir(parents=True,
exist_ok=True)
(root / "validation" / "figures").mkdir(parents=True, exist_ok=True)
return root


def main() -> int:
only = os.environ.get("ONLY", "").strip()
names = sorted(name for name in SLOW if only in name)
if not names:
print(f"no SLOW example matches {only!r}; known: {sorted(SLOW)}")
return 1

root = sandbox()
env = os.environ.copy()
env["MPLBACKEND"] = "Agg"
env["PYTHONPATH"] = os.pathsep.join(
p for p in (str(REPO / "src"), env.get("PYTHONPATH", "")) if p)

failures = []
for name in names:
script = root / "examples" / name
print(f"::group::{name}", flush=True)
start = time.perf_counter()
try:
done = subprocess.run([sys.executable, script.name],
cwd=script.parent, env=env,
capture_output=True, text=True,
timeout=TIMEOUT_S)
code, output = done.returncode, done.stdout + done.stderr
except subprocess.TimeoutExpired:
code, output = 1, f"did not finish within {TIMEOUT_S} s"
elapsed = time.perf_counter() - start
print(output[-4000:])
print("::endgroup::", flush=True)
status = "ok" if code == 0 else "FAILED"
print(f"{status:6s} {name:42s} {elapsed:7.1f} s", flush=True)
if code != 0:
failures.append((name, output[-2000:]))

if failures:
print(f"\n{len(failures)} of {len(names)} slow examples failed:")
for name, output in failures:
print(f"\n--- {name} ---\n{output}")
return 1
print(f"\nall {len(names)} slow examples ran")
return 0


if __name__ == "__main__":
raise SystemExit(main())
46 changes: 46 additions & 0 deletions .github/workflows/slow-examples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Slow examples

# The nine scripts in `SLOW` are skipped by the smoke test, which is what
# `SLOW` is for: they cost minutes each and a pull request should not wait
# for them. But nothing else ran them either, so for a while nothing ran
# them at all -- and a regression lived there for two commits, invisible.
# `CD_compensation_part1.py` raised `TypeError: 'Constellation' object is
# not subscriptable` on its first pass and every check stayed green.
#
# So they run here instead: once a week, and on demand. Weekly is the
# right cadence for a guard against drift rather than against a specific
# change -- the whole set is about 15 minutes of runner time, and a
# failure names the script and the traceback.

on:
schedule:
- cron: "17 4 * * 1" # Monday, 04:17 UTC
workflow_dispatch:
inputs:
example:
description: "Substring of one example to run; empty runs all of them"
required: false
default: ""

jobs:
slow-examples:
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install package
run: |
python -m pip install --upgrade pip
pip install -e .[test]

- name: Run the examples the smoke test skips
env:
MPLBACKEND: Agg
ONLY: ${{ inputs.example }}
run: python .github/scripts/run_slow_examples.py
Loading
Loading