Repository navigation
The blind coherent receiver, and batch axes as a machine-checked contract - #41
Merged
Merged
Conversation
…nt themselves The expert review named the two absences that dated the optical layer: no polarization impairment, and no time-varying carrier recovery. Both close here. PMDEmulator is the standard section emulator -- K Haar-uniform Jones rotations, each followed by a DGD of tau/sqrt(K) applied exactly in the frequency domain, so the declared tau is the RMS DGD of a Maxwellian ensemble (Poole & Wagner); a 300-seed test measures the DGD from the eigenvalues of the group-delay operator and pins both moments within 10 %. BlindPhaseSearchCompensator is Pfau's feedforward search, tested against a Wiener walk within its grid, its quadrant ambiguity documented as unresolved. The optical_pdm page runs PDM-QPSK through laser, PMD and amplifier, then undoes it blindly -- CMA butterfly, BPS, and the three ambiguities every blind receiver leaves resolved explicitly by known data: zero errors over 123072 symbols. PhaseNoise now draws one walk shared across leading axes: one laser. The Gray labelling that every BER curve leaned on is locked by deterministic tests: geometric nearest neighbours differ by exactly one index bit for QAM 4-256 and PSK 4-16, a nearest-neighbour symbol error costs exactly one bit through compute_ber, and a seeded 16-QAM run lands on the Gray closed form. print(obj) now renders info() -- one key: value line per entry, repr when there is no info() -- and the tutorials' items() loops become print(channel). The AWGN page drops its interpolation coda. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Leading axes ahead of a block's event axes are batch axes -- independent trials run in one call -- and each of the three block families owes the batch something different. Deterministic blocks broadcast their one configuration: FIRChannel now reshapes its kernel so scipy convolves along the last axis, and LinearEqualizer applies its matrix as a column product; both crashed on a batch before. Stochastic blocks draw independently per event: PhaseNoise declares its event with per= -- "pair" by default, one laser per polarization pair and independent draws across trials -- and refuses the shape it cannot read one way, naming the resolutions. Adaptive blocks carry independent state per event: BlindDualMIMOCompensator on (..., 2, N) adapts one butterfly per pair and exposes H_ with the batch axes in front, so partial_fit resumes each pair from its own state. CONVENTIONS.md states the contract (D51) and tests/core/test_batch_axes.py locks one block of each family. Two silent traps died on the way. compute_ser and compute_ber used to ravel before truncating to the common length, so a batch whose rows carried a convolution tail compared misaligned rows -- SER 0.47 where the truth was 0.02; both now truncate along the last axis first, and the pooled rate is exactly the mean of the per-row rates. The OFDM tutorial's repeat-loop became one batched monte_carlo call over (n_trials, N), and its page re-pastes the run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A sweep of the block catalogue against the D51 contract found four more deterministic blocks that crashed on a batch; all now broadcast over leading axes. BWFilter's 1-D guard was refusing what already worked -- the FFT mask acts on the last axis. CFO read len(x), which on a batch is the number of trials, as the signal length. Delay sliced and padded axis 0. Both chromatic-dispersion FIR compensators get the same kernel-rank fix as FIRChannel. BlindCFOCompensator keeps its documented event -- one oscillator per signal or per polarization pair (2, N), estimated jointly, as test_estimand_scope pins -- and now refuses a wider batch instead of silently smearing one scalar estimate over independent trials. tests/core/test_batch_axes.py locks each of these, and the data-aided estimators' existing refusals already conform. What batching buys is now measured, not asserted, and the CHANGELOG says all three numbers: x4.1 on the single-carrier chain whose block ZF equalizer rebuilds its pseudo-inverse on every call, x2.2 on the OFDM chain over 16 trials, and x0.5 -- slower -- on a plain AWGN chain where Python overhead was never the cost. Batch for correctness and to amortize per-call operator builds, not as a blanket speed knob. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MIMO question the batch contract had to answer: the channel is redrawn every frame, and every detector needs that frame's matrix. The answer is a stack. rayleigh_channel(size=K) returns (K, N_r, N_t), drawn sequentially from one generator so draw k equals the k-th draw of the equivalent Python loop; FlatMIMOChannel applies channel k to frame k, since matmul batches over leading axes; and a detector built with the same stack decides frame k against channel k. The linear detectors batch through numpy's stacked linear algebra -- mmse_estimator now transposes with swapaxes(-1, -2) so its normal equations batch too -- while ML, the sphere decoder and OSIC loop per draw internally: an enumeration, a tree and a cancellation order each depend on the one matrix in front of them, so their batch is convenience and correctness, not a speedup, and their docstrings say so. A stack of K channels refuses K+1 frames. tests/mimo/test_stacked_channel.py locks the draw equivalence, the per-frame alignment of the channel and all five detectors, and the refusal. Both MIMO studies now run on the stack. one_shot_mimo replaces its zipped monte_carlo sweep -- 200 chain runs per detector per SNR point -- with one batched pass, 10.9 s to 9.0 s, the sphere decoder's per-sample search dominating what remains; its page re-pastes the run. monte_carlo_simulation_1 drops the 500-draw Python loop for one stacked pass per point and reads the BLER directly off the batch as np.mean(np.any(S_est != S_ref, axis=(-2, -1))): 4.7 s to 2.5 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A convention over 91 blocks is only worth the sweep that checks it. tests/test_batch_contract.py discovers every Processor subclass in the library and fails unless each one is declared and verified: BROADCAST (row i of a batch equals the block on row i alone -- 36 blocks checked by equality), INDEPENDENT (trials do not share a realization), REFUSES (the ambiguous batch raises), or EXEMPT with the reason written next to it -- an exemption is visible debt, not a pass. Like the pyright ratchet, the contract is now self-enforcing: a new block cannot land without saying, and proving, what a batch means for it. Its first sweep caught two more silent couplings, both fixed. DCCorrector's default axis=0 averaged across the batch -- one converter offset smeared over independent trials; the default is now -1, the canonical serial layout, one offset per row. HardClipper computed its threshold from the mean power of the whole array, so the weakest trial of a batch was clipped against the strongest; the power is now per row, one transmitter per trial. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five commits, two threads.
The blind coherent receiver (PMD, BPS), and locks the library owed
PMDEmulator(optical.channels): the standard section emulator — K Haar-uniform Jones rotations, each followed by a DGD of τ/√K applied exactly in the frequency domain, so the declared τ is the RMS DGD of a Maxwellian ensemble (Poole & Wagner). A 300-seed test measures the DGD from the eigenvalues of the group-delay operator and pins both moments within 10 %.BlindPhaseSearchCompensator(core.compensators): Pfau's feedforward blind phase search, tested against a Wiener walk within its grid; the quadrant ambiguity is documented as unresolved, and tested as such.PhaseNoisenow draws one walk per polarization pair — one laser.optical_pdmtutorial: PDM-QPSK through laser, PMD and amplifier, undone blindly (CMA butterfly, BPS), the three receiver ambiguities — permutation, quadrant, equalizer delay — resolved explicitly by known data. Zero errors over 123 072 symbols, and the page says what that measures.compute_ber; a seeded run lands on the Gray closed form.print(obj)rendersinfo()— onekey: valueline per entry,reprwhen there is none.Batch axes are a contract (D51), enforced by a ratchet
Leading axes ahead of a block's event axes are batch axes: independent trials in one call. Three families — deterministic blocks broadcast, stochastic blocks draw independently per event (
PhaseNoise(per=)names the event; the ambiguous shape is refused), adaptive blocks carry state per event (one CMA butterfly per pair,H_with batch axes in front). Stated in CONVENTIONS.md.compute_ser/compute_berraveled before truncating and compared misaligned rows (SER 0.47 for a truth of 0.02);FIRChannel,LinearEqualizer,BWFilter,CFO,Delayand both CD FIR compensators crashed on a batch;DCCorrectoraveraged across trials;HardClipperclipped every trial against the batch's pooled power;BlindCFOCompensatorsmeared one estimate over independent trials and now refuses, naming the loop.rayleigh_channel(size=K)→(K, N_r, N_t), draw-for-draw equal to the Python loop;FlatMIMOChannelapplies channel k to frame k; a detector built with the same stack decides frame k against channel k — ZF/MMSE through numpy's stacked linalg, ML/sphere/OSIC through an internal per-draw loop that their docstrings call what it is.tests/test_batch_contract.py): discovers all 91Processorsubclasses and fails unless each is verified (BROADCASTby row equality — 36 blocks,INDEPENDENT,REFUSES) or exempted with a written reason. A new block cannot land without declaring what a batch means for it.Full suite green: 938 tests + 1218 subtests, ruff, pyright strict (0 errors), sphinx -W, example smoke tests.
🤖 Generated with Claude Code
Generated by Claude Code