Skip to content

Conversation

@thomas-hermann
Copy link
Member

Summary

Introduces a new ChainableArray class that provides a fluent, chainable interface for array transformations. This allows more expressive functional operations on arrays while maintaining readability. ChainableArray integrates both existing numpy ufuncs and custom mapping functions for chainable operations. The pyamapping-examples.ipyng Jupyter notebook provides extensive examples.

Motivation

  • pyamapping was intended from the beginning to grow into a comprehensive library of mapping functions (particularly for audio coding, computer music and sonification). The package initially contained only few functions, such as linlin, midi_to_cps, amp_to_db, ... This PR takes the next step of implementing the initial vision.
  • Method invocation such as amp_to_db(clip(linlin(myarray, x1, x2, y1, y2), c1, c2)) leads to reverse reading order, argument fragmentation and parenthesis clutter. A chain-like syntax, as introduced with pya enhances readability. ChainableArray allows to rewrite the example above as myarray.linlin(x1, x2, y1, y2).clip(c1, c2).amp_to_db().
  • Ultimately the new mapping functions added plus numpy functions should by made available for Asigs and AGens in pya. By introducing ChainableArray to pyamapping, the first step is taken - the next would be to auto-register Asig functions and AGens for functions defined here in pyamapping.

Changes

  • Added ChainableArray class under mappings.py
  • Implemented new mapping functions: bilin, cps_to_octave, curvelin, distort, ecdf_to_lin, ecdf, explin, fermi, fold, gain, interp_spline, interp, lcurve, lin_to_ecdf, lincurve, linexp, linpoly, midi_to_ratio, norm_peak, norm_rms, normalize, octave_to_cps, ratio_to_midi, remove_dc, scurve, softclip, wrap. Many of these are defined in close analogy to counterpart functions in Supercollider3 and librosa.
  • Added numpy ufuncs as chainable methods (106 methods = 58 ufuncs with one argument (from absolute() to trunc()) and 48 ufuncs with two arguments (from add() to vecmat()). These are made available via auto-generation using pyamapping.registering_numpy_ufunc()).
  • Extended pyamapping-examples.ipynb with an introduction to ChainableArray, and examples for the new mapping functions added to pyamapping. Examples show method use and mapping function plots and give hints for using them practically.

Testing

  • Testing is so far implicit, merely via the code/examples provided in the pyamapping-examples.ipynb notebook.
  • A separate development notebook (not added to avoid sc3nb dependency, yet available on demand) was used to verify results agains supercollider outputs for parity.
  • Addition of unit tests remains to be done, but could probably be postponed to accelerate releasing the functionality.

Review notes

  • Feedback on naming conventions and method signatures, particularly type hints (ArrayLike or numpy.ndarray?) is welcome before merging into main.

@sherlockhommel
Copy link
Collaborator

sherlockhommel commented Nov 5, 2025

Hi Thomas,

wow, lots of mappings, very cool!

Looking through it, I think the ChainableArray + utils should live in their own module to keep the semantics of the mappings module clean 🤔 However, this could also be done later, since it is abstracted away from importers by the top-level init.

Also, let's add those unit tests as soon as possible.

@sherlockhommel
Copy link
Collaborator

I updated the maximum python version to be tested during the github workflow to 3.13, so we have 3.7 to 3.13 for now

@thomas-hermann
Copy link
Member Author

I followed sherlockhommel's suggestion and restructured pyamapping, moving ChainableArray to chainable_array.py. The idea is that mappings.py should be reserved for mapping functions. Registration of numpy ufuncs and pyamapping functions were moved to init.py.

@sherlockhommel
Copy link
Collaborator

sherlockhommel commented Nov 6, 2025

I spent just a little bit of time adding a few very basic tests for some mappings - planning to expand on those later. I noticed that the bilin mapping requires scipy to work, so I added scipy as a new dependency (in setup.cfg).

@thomas-hermann
Copy link
Member Author

I added basic tests for most mapping functions (few still missing, such as ecdf, interp, interp_spline...)

@thomas-hermann
Copy link
Member Author

thomas-hermann commented Nov 6, 2025

I spent just a little bit of time adding a few very basic tests for some mappings - planning to expand on those later. I noticed that the bilin mapping requires scipy to work, so I added scipy as a new dependency (in setup.cfg).

so far scipy is only required for interp_spline (which, however is used by bilin()). So the question is whether we should have scipy as optional dependency to keep pyamapping lightweight, and issue a (scipy missing) warning in case bilin() or interp_spline() are used. Ideas?

related issue: ChainableArray features to_asig() which imports pya.Asig and plot() which imports matplotlib: makes sense to have these as optional dependencies. Maybe we should use module import flags to avoid repeated import checks?

@sherlockhommel
Copy link
Collaborator

I think for now it would work to have matplotlib, pya and scipy as optional dependencies and then do a dynamic import that checks whether the module has been imported before. Maybe something like this:

import sys, importlib


def get_scipy():
    if "scipy" in sys.modules:
        return sys.modules["scipy"]

    try:
        return importlib.import_module("scipy")
    except ImportError as e:
        raise ImportError(
            "This function requires SciPy. Please install it via ..."
        ) from e

but a little more refined to the specific scipy / matplotlib / pya submodule that is needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants