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 the model in Docker (via the Docker SDK for Python): by default a compiled MATLAB Runtime worker, with an optional license-free GNU Octave backend.

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. 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 pyLeman2000

From a specific Git tag (or before the PyPI upload lands):

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

For local development:

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

Optional: pre-pull the default MATLAB image (otherwise the package pulls it on first leman2000(...) call):

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

Building images locally

Octave (optional backend):

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

MATLAB (default backend; needs MATLAB Compiler on Linux amd64):

./scripts/build_matlab_image.sh                 # local pyleman2000-matlab:dev
./scripts/build_matlab_image.sh --tag 0.1.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.1.0 (and :latest). Package defaults are digest pins of the 0.1.0 images, so installs stay reproducible while :dev keeps moving. See docker/matlab/README.md for the MATLAB worker protocol.

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 worker)

Each one-shot leman2000(...) call starts a fresh container. When analysing many files, keep one worker alive (default MATLAB backend reuses the compiled Runtime process):

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],
    )

Optional Octave backend

Pass backend="octave" for the license-free Octave image (DEFAULT_IMAGE). Sessions still help on Apple Silicon by warming filesystem caches, even though Octave restarts inside the container on each run:

with Leman2000Session(backend="octave") as session:
    result = session.run(
        input_file=example_wav_path(),
        local_decay_sec=0.1,
        global_decay_sec=1.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
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.2.0.tar.gz (84.5 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.2.0-py3-none-any.whl (51.9 kB view details)

Uploaded Python 3

File details

Details for the file pyleman2000-0.2.0.tar.gz.

File metadata

  • Download URL: pyleman2000-0.2.0.tar.gz
  • Upload date:
  • Size: 84.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for pyleman2000-0.2.0.tar.gz
Algorithm Hash digest
SHA256 9999e18b7110cfe9a38d8212ef7a9c4cabe3e1db47fe642cd04034c2aa4a55ec
MD5 31f4366528fab05e3734bfe85cd81779
BLAKE2b-256 6b5d5af22e6fe1a442039047f79c5084e4c286822bbbc4021508d5de4b8a026e

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyleman2000-0.2.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.2.0-py3-none-any.whl.

File metadata

  • Download URL: pyleman2000-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 51.9 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 590a5d4aac11edf47c37a3bb46143c141d612106ee4d5f14ade1ccadad9d3416
MD5 79138c87f9fb9b0450881736db10a554
BLAKE2b-256 a711a05e358b74f27d9e5af45da0935d54231e6c9819840e88ab0e8ff4d5ebd5

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyleman2000-0.2.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

This release

0.2.0 This release

2 files

0.1.0

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