Skip to main content

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 info should 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 with docker 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 with Leman2000Session (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_rate
  • local_global_comparison — pandas DataFrame of running local/global correlations
  • windowed_local_global_comparison — optional windowed averages
  • auditory_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

pyleman2000-0.1.0.tar.gz (84.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyleman2000-0.1.0-py3-none-any.whl (52.0 kB view details)

Uploaded Python 3

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

Hashes for pyleman2000-0.1.0.tar.gz
Algorithm Hash digest
SHA256 85720350ce55b1dd41d06e0665aa9575a17d2f4912dd4613263bbdb5bd116d5d
MD5 286114c375c8095cc6ab6fd625776439
BLAKE2b-256 2d26c139ac6bc9144070ed31713d7d670dc6f7373b845c1f9ea8a37dfabd1439

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyleman2000-0.1.0.tar.gz:

Publisher: publish-pypi.yml on cms-cambridge/pyLeman2000

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pyleman2000-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f1165c8afd4fea0ce2be3ce0cd96e9948023b39b23c1cfbb6da70d86a3b8fda8
MD5 ea1ae58f91be5588066b0af158e73615
BLAKE2b-256 67dd93194413ed766f1567557f37c772319ccfa545c87b64982009053b23ec91

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyleman2000-0.1.0-py3-none-any.whl:

Publisher: publish-pypi.yml on cms-cambridge/pyLeman2000

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.0

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page