Skip to content

Releases: questrail/siganalysis

Release list

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 16:13
cf2e0f1

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.toml classifiers claim. A second job installs the oldest
    numpy, scipy, and matplotlib that pyproject.toml allows, 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 than master, a master behind
    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-check runs the refusals on
    their own.
  • The release workflow waits on the whole CI run before it uploads
    anything, confirms the tag sits on master and matches the version
    in pyproject.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 reads pyproject.toml and
    uv.lock together as well, so a dependency update arrives as a lock
    file change that CI checks with uv sync --locked rather 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 the plotting extra, then
    checks the version, every public name, and py.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 build and
    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, and just lint, and a Justfile that 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 inside just lint rather than as
    a separate just check that every caller had to remember. Reaching
    it through lint means just build and just release cannot skip
    it. scipy-stubs is a new dev dependency: without it pyright
    cannot see the window functions in scipy.signal.windows.
  • plot_spectrogram() and plot_peak_hold() are declared to a type
    checker under TYPE_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 siganalysis still does not import matplotlib.
  • The ruff rule set is selected explicitly in pyproject.toml rather
    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 a noqa saying so.
  • The license is declared as an SPDX expression with license-files,
    which is what replaced the License :: classifier that used to
    carry it.
  • .python-version is tracked rather than ignored. It decides the
    interpreter a contributor's uv sync builds 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, and just 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

Choose a tag to compare

@matthewrankin matthewrankin released this 31 Aug 17:01
f183e2a

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)               # after

matplotlib 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 optional minimum_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 in smooth() 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(), and plot_peak_hold(), the three
    functions that still had none (#4). The suite has grown from 53 to 154 tests.
  • WINDOW_FUNCTIONS, the windows smooth() accepts, is exported alongside
    STFT_WINDOWS.
  • siganalysis.__version__ reports the installed version. It was only
    reachable as siganalysis.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 while freq_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.