Skip to main content

pyLeman2000

Python wrapper for Leman's (2000) tonal contextuality model.

The model was published in Music Perception and accounts for the Krumhansl–Kessler probe-tone data (Leman, 2000). This package runs it in Docker: by default a compiled MATLAB Runtime worker, with an optional license-free GNU Octave backend. It is a Python port of leman2000R.

Requirements

  • Python 3.10+
  • Docker installed and running (docker info should succeed)

The default image is pulled automatically on first use.

Installation

python3 -m pip install pyLeman2000

Quick start

from pyleman2000 import example_wav_path, leman2000

result = leman2000(
    input_file=example_wav_path(),
    local_decay_sec=[0.1, 0.5],
    global_decay_sec=[1.0, 2.0],
    windows=[(0.0, 0.1), (0.1, 0.2)],
)

result.audio_length_sec, result.num_channels, result.sample_rate
(0.3707936508, 1, 44100.0)
result.local_global_comparison.head()
   local_decay_sec  global_decay_sec  time_sec  running_correlation
0              0.1               1.0  0.000000             1.000000
1              0.1               1.0  0.014832             0.999999
2              0.1               1.0  0.029663             0.999998
3              0.1               1.0  0.044495             0.999996
4              0.1               1.0  0.059327             0.999993

leman2000 returns a Leman2000Result with:

  • audio_length_sec, num_channels, sample_rate
  • local_global_comparison — running local/global correlations over time
  • windowed_local_global_comparison — optional windowed averages
  • auditory_nerve / periodicity_pitch — optional heavy intermediates (omit unless you need them)

Input files must use a .wav extension (standard PCM WAV works best, for example mono or stereo, 16-bit, 44.1 kHz).

Choosing parameters

There is not much clarity in the literature on which local/global decay parameters are best. Previous work has therefore grid-searched many combinations: pass sequences for local_decay_sec and global_decay_sec to obtain all pairwise results. See Bigand et al. (2014) for an example.

local_global_comparison is the running correlation between local and global tonal images. Higher values mean the short-term and longer-term representations are more similar at that moment. Optional windows summarise those correlations over closed time intervals (for example chord or phrase spans). Unlike leman2000R's half-open [start, end), both endpoints are included here.

Analysing many files

For many files, use leman2000_batch: it opens a worker pool, picks a worker count from the backend, total audio duration, CPU count, and available RAM (override with workers= or PYLEMAN2000_WORKERS), shows progress, and returns stacked DataFrames:

from pyleman2000 import example_wav_path, leman2000_batch

paths = [example_wav_path(), example_wav_path()]
batch = leman2000_batch(
    paths,
    local_decay_sec=0.1,
    global_decay_sec=1.0,
)

batch.files
batch.local_global_comparison.head()

Batch progress is shown by default. Pass progress=False to disable it, or provide an object implementing start(n_files, n_workers), update(completed), and close() to bridge progress into another UI.

batch.files always has one row per input and an ok or error status. Its audio metadata columns use pandas nullable numeric dtypes, so their schema stays stable when metadata is unavailable. batch.results is input-aligned and contains each per-file Leman2000Result when you need keep_* payloads.

By default, an error in any file stops the batch. Pass continue_on_error=True to finish the remaining files instead. The failed file keeps its input-aligned position as None in batch.results, and batch.failures records its file ID, path, exception type, message, and original exception object. Aggregate correlation tables contain successful files and retain their original file IDs. When batch progress is enabled, a continued batch also prints a short failure summary to standard error, so the completed N/N count does not conceal errors.

For lower-level pool use, Leman2000Pool.map() remains fail-fast, while Leman2000Pool.map_with_errors() processes every file and returns aligned (results, errors) lists. A timeout, dead container, or other Leman2000WorkerError restarts that worker before reuse; ordinary request errors keep the healthy warm worker running.

Extra workers only pay off once each has enough audio to offset the ~5 s MATLAB worker startup, so short MATLAB files stay sequential by default. Octave starts for every file and parallelizes even short batches, but its memory grows much faster with audio length. See artifacts/benchmark/batch_scaling.md and artifacts/benchmark/batch_scaling_octave.md for the measurements behind these choices. Passing workers= (or PYLEMAN2000_WORKERS) overrides the automatic choice and is honoured as given, so it can exceed what memory comfortably allows.

Octave backend

Pass backend="octave" for the license-free image:

from pyleman2000 import example_wav_path, leman2000

result = leman2000(
    input_file=example_wav_path(),
    local_decay_sec=0.1,
    global_decay_sec=1.0,
    backend="octave",
)

Developer notes

Local development

python3 -m pip install -e ".[dev]"

From a specific Git tag:

python3 -m pip install git+https://github.com/cms-cambridge/pyLeman2000.git@v0.3.0

Optional pre-pull of the default MATLAB image:

docker pull "$(python3 -c 'from pyleman2000 import DEFAULT_MATLAB_IMAGE; print(DEFAULT_MATLAB_IMAGE)')"

Platform and Docker details

The default image is ghcr.io/cms-cambridge/pyleman2000-matlab (linux/amd64), digest-pinned as DEFAULT_MATLAB_IMAGE. Pass backend="octave" for the Octave image built from docker/octave/ against a pinned cms-cambridge/IPEMToolbox commit.

Input and output files are copied in and out of the container rather than bind-mounted, so no Docker file-sharing configuration is needed.

On Apple Silicon, enable Docker Desktop's amd64/Rosetta or QEMU emulation, then verify with docker run --platform=linux/amd64 --rm hello-world. Emulated runs are slower than native amd64. On 44.1 kHz input, running correlations typically agree with archived MATLAB/R snapshots to about 3e-6 (not bit-identical); feed 22.05 kHz audio for closer cross-implementation agreement.

Silence single-analysis pull/run progress with show_progress=False.

Building images locally

Octave:

./scripts/build_octave_image.sh
# then: leman2000(..., backend="octave", docker_image="pyleman2000-octave:dev")

MATLAB (needs MATLAB Compiler on Linux amd64):

./scripts/build_matlab_image.sh                 # local pyleman2000-matlab:dev
./scripts/build_matlab_image.sh --tag 0.2.0 --push

Local pyleman2000-octave:* / pyleman2000-matlab:* tags are never pulled from a registry; if missing you get an error pointing at the build script.

Octave publishing to GHCR is handled by .github/workflows/docker-publish.yml (linux/amd64). Pushes to main refresh :dev; manual workflow dispatch publishes version tags such as :0.2.0 (and :latest). Package defaults are digest pins of the 0.2.0 images. See docker/matlab/README.md for the MATLAB worker protocol.

Tests

Unit tests do not require Docker:

python3 -m pytest -v -m "not integration and not matlab"

Octave integration tests:

docker pull "$(python3 -c 'from pyleman2000 import DEFAULT_IMAGE; print(DEFAULT_IMAGE)')"
# or: ./scripts/build_octave_image.sh && use docker_image='pyleman2000-octave:dev'
python3 -m pytest -v -m integration

Compiled MATLAB smoke tests (no Compiler needed to run them):

docker pull "$(python3 -c 'from pyleman2000 import DEFAULT_MATLAB_IMAGE; print(DEFAULT_MATLAB_IMAGE)')"
python3 -m pytest -v -m matlab

Snapshot CSVs under tests/snapshots/ were generated from leman2000R via scripts/generate_r_snapshots.R (MATLAB backend). CI compares against those archives with a looser tolerance (~1e-5).

Result object notes

Leman2000Result is frozen (attribute reassignment is blocked), but embedded DataFrames remain mutable through pandas APIs. Constructor inputs are copied so later edits to caller-owned objects do not alter the result. Equality is identity-based; compare DataFrames or fields explicitly when needed. window_id is 1-based, and windowed rows are ordered window-major.

References

Bigand, E., Delbé, C., Poulin-Charronnat, B., Leman, M., & Tillmann, B. (2014). Empirical evidence for musical syntax processing? Computer simulations reveal the contribution of auditory short-term memory. Frontiers in Systems Neuroscience, 8, 94. https://doi.org/10.3389/fnsys.2014.00094

Leman, M. (2000). An auditory model of the role of short-term memory in probe-tone ratings. Music Perception, 17(4), 481–509.

Metadata

Release files for pyLeman2000 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyLeman2000 0.3.0
File Size Uploaded
pyleman2000-0.3.0.tar.gz 134.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyLeman2000 0.3.0
File Interpreter ABI Platform
pyleman2000-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 197.6 kB

Release files / pyleman2000-0.3.0.tar.gz

Download URL pyleman2000-0.3.0.tar.gz
Size 134.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a8d4a76fb96ecca778269a366643545e34f171b43540d054646720f69d532b92
BLAKE2b-256 checksum
How to use checksums
81819e44f2e0846d3897ced292e60d35e2682335ecaa2f06da9262a3d4220cd8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Release files / pyleman2000-0.3.0-py3-none-any.whl

Download URL pyleman2000-0.3.0-py3-none-any.whl
Size 63.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
981201f136f3d63b060eb8c2c83f4934c1387865286a239838d29869786f0141
BLAKE2b-256 checksum
How to use checksums
d2d6e827b0345c1ea5d5cc3491a7f6b321e68894255d6e923c0aea45b4f94b3f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page