pyLeman2000
Python wrapper for Leman's (2000) tonal contextuality model.
The original model was published in a 2000 Music Perception paper, and was shown to provide a psychoacoustic account of the Krumhansl-Kessler probe-tone data (Leman, 2000). Leman and colleagues released this model as part of the IPEM Toolbox, which now only runs on old MATLAB versions. This package runs a license-free GNU Octave port of the model in Docker (via the Docker SDK for Python) for cross-platform use.
The default image is
ghcr.io/cms-cambridge/pyleman2000-octave
(linux/amd64), built from docker/octave/ against a pinned
cms-cambridge/IPEMToolbox
commit. On 44.1 kHz input, running correlations typically agree with the
archived MATLAB/R snapshots to about 3e-6 (not bit-identical). Feed
22.05 kHz audio if you need closer cross-implementation agreement.
This is a Python port of leman2000R.
Requirements
- Python 3.10+
- Docker installed and running
(
docker infoshould succeed) - On first use, the default GHCR image is pulled automatically (progress on
stderr). Silence with
leman2000(..., show_progress=False).
Input and output files are copied in and out of the container rather than
bind-mounted, so no Docker file sharing configuration is needed. Analyses work
regardless of where the audio lives, including paths that Docker Desktop does
not share by default (such as WAV files inside site-packages).
Note: The image targets
linux/amd64. On Apple Silicon, enable Docker Desktop's amd64/Rosetta or QEMU emulation, then verify withdocker run --platform=linux/amd64 --rm hello-world. Emulated runs are slower than native amd64 hardware. For many analyses in one process, reuse a warm container withLeman2000Session(see below).
Installation
python3 -m pip install git+https://github.com/cms-cambridge/pyLeman2000.git
For local development:
python3 -m pip install -e ".[dev]"
Optional: pre-pull the default image (otherwise the package pulls it on first
leman2000(...) call):
docker pull "$(python3 -c 'from pyleman2000 import DEFAULT_IMAGE; print(DEFAULT_IMAGE)')"
Building the image locally
Contributors can build from this repository (pins IPEM at the commit in
docker/octave/Dockerfile):
./scripts/build_octave_image.sh
Then pass docker_image="pyleman2000-octave:dev" (or another tag) to the API.
Local pyleman2000-octave:* tags are never pulled from a registry; if missing
you get an error pointing at the build script.
Publishing to GHCR is handled by .github/workflows/docker-publish.yml
(linux/amd64). Pushes to main refresh
ghcr.io/cms-cambridge/pyleman2000-octave:dev; manual workflow dispatch
publishes version tags such as :0.1.0 (and :latest). The package default
is a digest pin of the 0.1.0 image (DEFAULT_IMAGE), so installs stay
reproducible while :dev keeps moving.
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, which is what this package facilitates: pass sequences for
local_decay_sec and global_decay_sec to obtain all pairwise results. See
Bigand et al. (2014) for an example of using Leman's model across parameter
settings when relating auditory short-term memory accounts to musical syntax
findings.
local_global_comparison gives the running correlation between local and
global tonal images over time. Higher values mean the short-term (local) and
longer-term (global) representations are more similar at that moment. Optional
windows summarise those correlations over closed time intervals of interest
(for example chord or phrase spans). This differs from leman2000R, which uses
half-open intervals [start, end): pyLeman2000 includes both endpoints, so a
sample that falls exactly on a shared boundary contributes to both adjacent
windows.
Input files must use a .wav extension. The Dockerised model inherits the
IPEM WAV constraints of the original toolbox; if a file fails inside the
container, try a standard PCM WAV (for example mono or stereo, 16-bit,
44.1 kHz).
Example
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)
Repeated analyses (warm container)
Each one-shot leman2000(...) call starts a fresh container. When analysing
many files, keep one container alive:
from pyleman2000 import Leman2000Session, example_wav_path
with Leman2000Session() as session:
first = session.run(
input_file=example_wav_path(),
local_decay_sec=0.1,
global_decay_sec=1.0,
)
second = session.run(
input_file=example_wav_path(),
local_decay_sec=[0.1, 0.5],
global_decay_sec=[1.0, 2.0],
)
Octave still starts on every run, but later calls in the same session are
typically faster (filesystem caches stay warm), especially on Apple Silicon.
Optional compiled MATLAB backend
Pass backend="matlab" to keep a compiled MATLAB Runtime worker alive across
runs. The package default (DEFAULT_MATLAB_IMAGE) is a digest pin of the
published 0.1.0 worker; override with docker_image= for a local :dev build:
with Leman2000Session(backend="matlab") as session:
result = session.run(
input_file=example_wav_path(),
local_decay_sec=0.1,
global_decay_sec=1.0,
)
The default remains backend="octave". Build and publish the MATLAB image on
a Linux host with MATLAB Compiler:
./scripts/build_matlab_image.sh # local pyleman2000-matlab:dev
./scripts/build_matlab_image.sh --tag 0.1.0 --push
See docker/matlab/README.md for pins, provenance, and the worker protocol.
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
result.windowed_local_global_comparison
local_decay_sec global_decay_sec window_id window_start window_end local_global_correlation
0 0.1 1.0 1 0.0 0.1 0.999977
1 0.5 1.0 1 0.0 0.1 1.000000
2 0.1 2.0 1 0.0 0.1 0.999974
3 0.5 2.0 1 0.0 0.1 0.999999
4 0.1 1.0 2 0.1 0.2 0.998674
5 0.5 1.0 2 0.1 0.2 0.999988
6 0.1 2.0 2 0.1 0.2 0.998546
7 0.5 2.0 2 0.1 0.2 0.999972
Window intervals include both endpoints. A timestamp on a boundary shared by
adjacent windows contributes to both windows. This is an intentional divergence
from leman2000R, which uses half-open [start, end). window_id is 1-based,
and windowed rows are ordered window-major (all parameter combinations for
window 1, then window 2, and so on).
leman2000 returns a Leman2000Result dataclass with:
audio_length_sec,num_channels,sample_ratelocal_global_comparison— pandas DataFrame of running local/global correlationswindowed_local_global_comparison— optional windowed averagesauditory_nerve/periodicity_pitch— optional heavy intermediate outputs (nested dictionaries from the Octave model; omit unless you need them)
The result object itself 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. Result equality is identity-based; compare DataFrames or fields explicitly when needed.
Tests
Unit tests do not require Docker:
python3 -m pip install -e ".[dev]"
python3 -m pytest -v -m "not integration and not matlab"
Octave integration and R-snapshot tests require Docker. The package pulls
DEFAULT_IMAGE from GHCR on first use (or build locally if you prefer):
# optional pre-pull; otherwise leman2000() pulls automatically
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 pull DEFAULT_MATLAB_IMAGE (no Compiler needed):
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). Octave and MATLAB CI
jobs compare against those archives with a looser tolerance (~1e-5).
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyleman2000-0.1.0.tar.gz.
File metadata
- Download URL: pyleman2000-0.1.0.tar.gz
- Upload date:
- Size: 84.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85720350ce55b1dd41d06e0665aa9575a17d2f4912dd4613263bbdb5bd116d5d
|
|
| MD5 |
286114c375c8095cc6ab6fd625776439
|
|
| BLAKE2b-256 |
2d26c139ac6bc9144070ed31713d7d670dc6f7373b845c1f9ea8a37dfabd1439
|
Provenance
The following attestation bundles were made for pyleman2000-0.1.0.tar.gz:
Publisher:
publish-pypi.yml on cms-cambridge/pyLeman2000
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyleman2000-0.1.0.tar.gz -
Subject digest:
85720350ce55b1dd41d06e0665aa9575a17d2f4912dd4613263bbdb5bd116d5d - Sigstore transparency entry: 2369393110
- Sigstore integration time:
-
Permalink:
cms-cambridge/pyLeman2000@88c5788a37d940492520d71e1e2f8fe63e390688 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cms-cambridge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@88c5788a37d940492520d71e1e2f8fe63e390688 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file pyleman2000-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyleman2000-0.1.0-py3-none-any.whl
- Upload date:
- Size: 52.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1165c8afd4fea0ce2be3ce0cd96e9948023b39b23c1cfbb6da70d86a3b8fda8
|
|
| MD5 |
ea1ae58f91be5588066b0af158e73615
|
|
| BLAKE2b-256 |
67dd93194413ed766f1567557f37c772319ccfa545c87b64982009053b23ec91
|
Provenance
The following attestation bundles were made for pyleman2000-0.1.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on cms-cambridge/pyLeman2000
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyleman2000-0.1.0-py3-none-any.whl -
Subject digest:
f1165c8afd4fea0ce2be3ce0cd96e9948023b39b23c1cfbb6da70d86a3b8fda8 - Sigstore transparency entry: 2369393220
- Sigstore integration time:
-
Permalink:
cms-cambridge/pyLeman2000@88c5788a37d940492520d71e1e2f8fe63e390688 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cms-cambridge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@88c5788a37d940492520d71e1e2f8fe63e390688 -
Trigger Event:
workflow_dispatch
-
Statement type: