Skip to content

Rework the test suite and add a CI workflow - #35

Merged
edsonportosilva merged 2 commits into
edsonportosilva:mainfrom
Hewei603:tests/pytest-suite-and-ci
Aug 1, 2026
Merged

Rework the test suite and add a CI workflow#35
edsonportosilva merged 2 commits into
edsonportosilva:mainfrom
Hewei603:tests/pytest-suite-and-ci

Conversation

@Hewei603

Copy link
Copy Markdown
Contributor

Following up on your suggestion in #34 — this reworks the test suite and adds a CI workflow.

Why

tests/test_metrics.py::test_fastBERcalc was failing on main. It calls

symbRx = awgn(symbTx, snrdB)

passing a float, while awgn() reads its configuration through getattr(param, "snr", 20). The SNR therefore silently fell back to the 20 dB default and the measured BER no longer followed the EbN0 values being swept, so it could never match theoryBER(). Building a proper parameters object makes the measured curve match the theory to within 20%.

The rest of the suite mixed unittest and pytest styles and covered only a handful of functions, so I extended it while I was there.

What is in this PR

File Contents
tests/test_utils.py new — unit conversions, bit array helpers, dotNumba, llr2bitProb (including its numerical stability for very large LLRs)
tests/test_dsp.py extended — pnorm, signalPower, sigPow, firFilter, pulseShape, upsample, decimate, resample, quantizer, lowPassFIR, freqShift, movingAverage, delaySignal, gaussianNoise, phaseNoise, symbolSync
tests/test_modulation.py ported to pytest and extended — constellation generators, detector, and the Gray mapping property that nearest-neighbour symbols differ in a single bit
tests/test_metrics.py AWGN setup fixed, ported to pytest, plus theoryBER, Qfunc, calcEVM, calcLLR and the GMI/MI estimators
tests/test_channels.py new — awgn, linearFiberChannel, its inversion by edc, and the reduction of ssfm to the linear channel for gamma = 0
tests/test_imports.py new — every module must import; GPU modules are skipped when CuPy is missing
.github/workflows/tests.yml new — pytest on Python 3.10–3.13 for pushes to main and pull requests

Wherever possible the assertions check a property rather than a hardcoded number: RRC pulses are verified through the Nyquist criterion after matched filtering, linearFiberChannel through the fiber loss coefficient and energy conservation, phaseNoise through the variance of its increments, awgn through the SNR measured at its output. Random number generators are seeded, and the whole suite runs in about 12 s (182 tests, 3 skipped without a GPU).

tests/test_imports.py exists specifically because of #34: an import of a name that does not exist leaves the whole package unusable, and running it in CI makes that visible immediately.

Things I noticed while writing the tests

None of these are addressed here, since they are outside the scope of a test PR. Happy to open issues or separate PRs for any of them.

  1. edc() shifts the signal by one sample. Compensating 200 km of dispersion on an RRC-shaped QPSK signal leaves a normalized MSE of 2.1e-1; realigning the output by a single sample brings it down to 5.8e-3. The offset comes from blockwiseFFTConv()delaySignal() works around the same effect with np.roll(delayedSig, -1), but edc() does not. In a full receiver the adaptive equalizer absorbs it, which is probably why it went unnoticed. The test aligns the sequences before comparing, so it passes either way.

  2. delaySignal(sig, 0) corrupts the last sample. With delay = 0 there is no zero padding, so the final np.roll(..., -1) shifts a sample out of the signal. Delays greater than zero are unaffected.

  3. pnorm() docstring does not match its behaviour. It says the average power of each component is normalized, but the implementation is x / sqrt(mean(x * conj(x))), where the mean runs over the whole array. For a dual-polarization signal the total average power becomes 1 per sample rather than 1 per mode. The behaviour looks intentional — it preserves the power ratio between polarizations — so this may just be a documentation fix.

  4. calcEVM() returns the squared EVM. It computes mean(|error|^2) / mean(|reference|^2), whereas EVM is usually reported as the square root of that ratio. The example notebooks print the returned value as a percentage, so the reported figure is currently the error power ratio, not the RMS EVM.

  5. Two deprecated imports that will eventually break. optic/plot.py imports gaussian_filter from scipy.ndimage.filters, a namespace scheduled for removal in SciPy 2.0, and optic/models/amplification.py imports numpy.matlib, deprecated since NumPy 1.19. Both currently emit warnings.

  6. quantizer() only accepts 2D arrays, since it indexes x.shape[1]. Every internal caller reshapes first, so this is just worth a note in the docstring.

Hewei603 added 2 commits July 30, 2026 20:09
The existing tests mixed unittest and pytest styles and covered only a few
functions of optic.dsp.core, optic.comm.modulation and optic.comm.metrics.

test_metrics.py::test_fastBERcalc was also failing: it called
`awgn(symbTx, snrdB)` passing a float, but awgn() reads its configuration
through `getattr(param, "snr", 20)`, so the SNR silently fell back to the
20 dB default and the measured BER no longer followed the EbN0 values being
swept. The test now builds a proper parameters object, which makes the
measured curve match theoryBER() to within 20%.

Changes:

- Port every test to pytest, grouped in classes by the function under test.
- tests/test_dsp.py: add coverage for pnorm, signalPower, sigPow, firFilter,
  pulseShape, upsample, decimate, resample, quantizer, lowPassFIR, freqShift,
  movingAverage, delaySignal, gaussianNoise, phaseNoise and symbolSync.
- tests/test_modulation.py: add coverage for the constellation generators,
  the detector and the Gray mapping property that nearest-neighbour symbols
  differ in a single bit.
- tests/test_metrics.py: fix the AWGN setup and add coverage for theoryBER,
  Qfunc, calcEVM, calcLLR and the GMI/MI estimators.
- tests/test_channels.py: new, covering awgn, linearFiberChannel, its
  inversion by edc, and the reduction of ssfm to the linear channel when the
  nonlinear coefficient is zero.
- tests/test_utils.py: new, covering the unit conversions, the bit array
  helpers, dotNumba and llr2bitProb.
- tests/test_imports.py: new, checking that every module can be imported.
  GPU modules are skipped when CuPy is unavailable.
- Ignore __pycache__ and .pytest_cache directories.

The suite runs in about 12 s and passes with 182 tests (3 skipped without a
GPU).
Runs pytest on Python 3.10 to 3.13 for pushes to main and for pull requests.

Together with tests/test_imports.py this catches breakages such as an import
of a name that does not exist, which otherwise leave the whole package
unusable until someone runs the code by hand.
@edsonportosilva

Copy link
Copy Markdown
Owner

Hello, @Hewei603! I have reviewed your changes and I believe they make sense for all the tests that were improved and the new added ones. Thanks for contributing with the repository! Please, open an issue listing these last findings you mentioned. I can have a look on them later. Perhaps, a few of them are already being looked at in some upcoming pull requests.

@edsonportosilva
edsonportosilva merged commit 4b81cc9 into edsonportosilva:main Aug 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants