Releases: questrail/siganalysis
Release list
v0.9.0
Added
- Python 3.14 to the classifiers and the CI matrix. The suite passes on
it, and nothing in the dependencies held it back; it was left out only
because the classifiers did not already name it. - Continuous integration on GitHub Actions, which this project had none
of. Every push and pull request now lints, checks formatting, type
checks, and runs the suite on 3.12 and 3.13, the versions the
pyproject.tomlclassifiers claim. A second job installs the oldest
numpy, scipy, and matplotlib thatpyproject.tomlallows, with
--resolution lowest-direct, so that the version floors are a
tested promise rather than a hopeful one; the lock file pins the
newest of each, so nothing else exercises them. A third audits the
workflows with zizmor, the part of the repository that can mint a
PyPI credential having otherwise been read by eye alone. That job
restores the uv cache without saving it, since it installs what the
3.13 leg installs and the two would otherwise race to write one key.
Coverage goes to Coveralls from the 3.13 leg. - Releases publish from a tag rather than from a laptop.
just release
refuses a dirty tree, a branch other thanmaster, amasterbehind
its upstream, an empty Unreleased section, or an existing tag; then
lints and tests; then shows the entries waiting to ship beside the
version each kind of bump would produce, and asks which to cut. It
bumps the version, closes out the CHANGELOG, commits, and tags.
Pushing the tag publishes.just release-checkruns the refusals on
their own. - The release workflow waits on the whole CI run before it uploads
anything, confirms the tag sits onmasterand matches the version
inpyproject.toml, and authenticates to PyPI with
trusted publishing, so there is no API token to paste, store, or
leak. It signs a PEP 740 attestation for each distribution
against the same identity, and creates a GitHub release carrying the
CHANGELOG section for that version as its notes. - Dependabot keeps the pinned actions and the lock file moving. The
actions in both workflows are pinned to commit SHAs, which a fix
published upstream does not reach on its own the way a moving tag
would, so pinning without something to move it would amount to
staying on one commit forever. Dependabot readspyproject.tomland
uv.locktogether as well, so a dependency update arrives as a lock
file change that CI checks withuv sync --lockedrather than as a
resolution done on the runner. scripts/smoke_test_wheel.py, which installs the built wheel where
src/cannot be reached and without theplottingextra, then
checks the version, every public name, andpy.typed. Every other
check runs against the source tree with matplotlib installed, so this
is the only one that can catch a packaging mistake, and the only one
that can tell whether matplotlib is still optional.just buildand
the release workflow run the same command.py.typed, so that the type hints already written reach anyone
installing the package rather than stopping at this repository.- A coverage floor of 95%, which is what the suite covers today. Below
that the run fails, so uncovered code has to arrive with either a
test or a deliberate edit to the floor. just doc,just up-all, andjust lint, and aJustfilethat is
otherwise line for line the one in applyaf, so that moving
between the two projects does not mean learning a second set of
recipe names.
Changed
- Type checking is done by pyright rather than ty, which is
still a 0.0.x release, and it runs insidejust lintrather than as
a separatejust checkthat every caller had to remember. Reaching
it throughlintmeansjust buildandjust releasecannot skip
it. scipy-stubs is a new dev dependency: without it pyright
cannot see the window functions inscipy.signal.windows. plot_spectrogram()andplot_peak_hold()are declared to a type
checker underTYPE_CHECKING, so an editor offers their real
signatures instead of names that appear from nowhere. They still
arrive through the module's__getattr__at run time, so a plain
import siganalysisstill does not import matplotlib.- The ruff rule set is selected explicitly in
pyproject.tomlrather
than left at the default, so that it is a deliberate choice. The
handful of places the new rules object to are deliberate, and each
carries anoqasaying so. - The license is declared as an SPDX expression with
license-files,
which is what replaced theLicense ::classifier that used to
carry it. .python-versionis tracked rather than ignored. It decides the
interpreter a contributor'suv syncbuilds against, and the file
was being read while being excluded from the repository.
Removed
just deploy, which published from a laptop against a pasted PyPI
token, andjust test-all, which ran the suite against each
supported Python locally because there was no CI to do it. The
release workflow and the CI matrix replace them.AUTHORS.md, along with the pointer to it in the copyright notice.
The repository history is the record of who wrote what.
v0.8.0
A choice of STFT window, matplotlib made optional, and a correction to the
amplitude of every windowed STFT.
Upgrading
Two changes need attention before this drops into an existing project.
stft() takes window in place of use_hamming_window. The argument
names a window, or is None for no window. Passing the old argument raises a
TypeError, so nothing fails quietly:
stft(data, fs, frame, hop, use_hamming_window=True) # before
stft(data, fs, frame, hop, window="hamming") # after, and the default
stft(data, fs, frame, hop, use_hamming_window=False) # before
stft(data, fs, frame, hop, window=None) # aftermatplotlib is no longer installed by default. It is now an optional
dependency, so it is only installed for those who plot:
pip install siganalysis[plotting]
siganalysis.plot_spectrogram() and siganalysis.plot_peak_hold() still
resolve, so no import has to change. The package imports the plotting module
on the first use of either name, which keeps matplotlib out of a plain
import siganalysis. Plotting without it installed raises an ImportError
naming the extra.
Your numbers will change
stft() had scaled a Hamming windowed frame by 2 rather than by the gain of
the window, leaving every windowed amplitude 7.08% (0.594 dB) high. That is
corrected here, so the same code returns different amplitudes: about 7%
lower, and now equal to what the same tone reads with the window off. A 1.0
amplitude tone on the center of a bin reads 1.0 rather than 1.0708.
Nothing raises to tell you this happened. If you are comparing new output
against stored spectrograms, or against a limit line, expect a 0.594 dB shift
that is the fix working rather than a regression.
plot_spectrogram() also draws a slightly different range for a plot range
that does not land on a bin: the bin holding a value is now the one nearest
to it, rather than the one found by truncating (#11).
A choice of window
stft() accepts any of the windows named in STFT_WINDOWS: hamming,
hann (also spelled hanning, as smooth() spells it), blackman,
blackmanharris, and flattop. This closes #6, which asked for the Hann
window that the Agilent 35670A applies.
flattop is worth knowing about: it is the window a spectrum analyzer offers
for accurate amplitude. For a tone falling exactly between two bins, the worst
case, it reports 0.999 of a 1.0 amplitude, against 0.821 for hamming and
0.650 for no window, at the cost of resolving the neighboring bins.
Better failures
The functions taking STFT data now check it against the vectors describing its
axes and say what does not line up, rather than failing further down with an
error about numpy indexing (#12). plot_spectrogram() had accepted a time or
frequency vector that did not match the data at all and plotted it against
mislabelled axes. smooth2() gained the checks smooth() already had, having
quietly returned an empty array for a window longer than the signal.
plot_peak_hold() checks that a limit_array carries the fields it reads.
time_slice_zip() rejects fewer than one sample per time slice, which had put
it into an endless loop.
Also new
time_slice_zip()takes an optionalminimum_samples_in_last_slice. A last
slice shorter than that is folded into the slice before it, so a sample count
just past a multiple of the slice size no longer leaves a last slice too
short to process. The samples are kept, so the last slice grows (#20).- The docstring examples run as doctests under pytest, so an example that stops
working fails the suite (#10). The one insmooth()had been broken for some
time:np.linspace(-2, 2, 0.1)raises a TypeError, since the third argument
is a count rather than a step. - Tests for
smooth(),smooth2(), andplot_peak_hold(), the three
functions that still had none (#4). The suite has grown from 53 to 154 tests. WINDOW_FUNCTIONS, the windowssmooth()accepts, is exported alongside
STFT_WINDOWS.siganalysis.__version__reports the installed version. It was only
reachable assiganalysis.siganalysis.__version__before.
Fixed
stft()scaled a Hamming windowed frame by 2 rather than by the coherent
gain of the window. Applying a window scales every amplitude by the mean of
its samples, 0.5354 for a Hamming window, so the correction needed is
1/0.5354 = 1.8678 rather than 2._bin_holding()truncated whilefreq_bin()rounded to nearest, so the
module held two different answers to which bin holds a value. For 10 Hz bins,
6 Hz gave the 0 Hz bin one way and the 10 Hz bin the other. Both round to
nearest now, which is also exactly the set of bins overlapping a requested
plot range.smooth()returned a signal one sample short of the one given when the
window length was even and equal to the length of the signal. The window is
made odd before the length is checked now, so such a call raises IndexError
rather than quietly returning the wrong length.
Upgrading from v0.5.1 also picks up the v0.6.0 and v0.7.0 changes, which
include two further corrections that change results silently: stft() built
its time vector from the requested hop rather than the hop actually taken, and
plot_spectrogram() sliced its plot ranges exclusively while documenting them
as inclusive. See the CHANGELOG for the full history.