From 761883a622a103d28c67442fd2e7e2b72aa6ccce Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 25 Oct 2025 19:05:52 +0200 Subject: [PATCH 01/67] add nbstripout pre-commit rule --- .pre-commit-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f331c47..8a753d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,3 +65,9 @@ repos: # rev: v2.2.5 # hooks: # - id: codespell + +- repo: https://github.com/kynan/nbstripout + rev: 0.8.1 + hooks: + - id: nbstripout + args: [--extra-keys=metadata.kernelspec metadata.language_info] From f01c4951860642933a63d8458708cee6557b8f09 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 25 Oct 2025 22:34:01 +0200 Subject: [PATCH 02/67] Add ChainableArray class, 28 new mapping functions and numpy ufunc registration --- LICENSE.txt | 2 +- pyproject.toml | 7 + src/pyamapping/__init__.py | 78 ++- src/pyamapping/mappings.py | 1052 +++++++++++++++++++++++++++++++++++- 4 files changed, 1114 insertions(+), 25 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index a1a87e4..f31a526 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023 Dennis Reinsch +Copyright (c) 2023-2025 Thomas Hermann and Dennis Reinsch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pyproject.toml b/pyproject.toml index 89a5bed..19cf8da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,3 +7,10 @@ build-backend = "setuptools.build_meta" # For smarter version schemes and other configuration options, # check out https://github.com/pypa/setuptools_scm version_scheme = "no-guess-dev" + +[tool.ruff] +line-length = 88 +lint.select = ["D"] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" diff --git a/src/pyamapping/__init__.py b/src/pyamapping/__init__.py index 0ae280d..e696760 100644 --- a/src/pyamapping/__init__.py +++ b/src/pyamapping/__init__.py @@ -16,32 +16,96 @@ del version, PackageNotFoundError -from pyamapping.mappings import ( +from pyamapping.mappings import ( # some synonyms; the class and helper functions + ChainableArray, amp_to_db, ampdb, + bilin, + chain, clip, cps_to_midi, + cps_to_octave, cpsmidi, + curvelin, db_to_amp, dbamp, + distort, + ecdf, + ecdf_to_lin, + fermi, + fold, + gain, hz_to_mel, + interp, + interp_spline, + lcurve, + lin_to_ecdf, + lincurve, + linexp, linlin, + linlog, + linpoly, + linspace, mel_to_hz, midi_to_cps, + midi_to_ratio, midicps, + norm_peak, + norm_rms, + normalize, + octave_to_cps, + ratio_to_midi, + register_chain_fn, + remove_dc, + scurve, + softclip, + wrap, ) __all__ = [ "amp_to_db", - "ampdb", + "bilin", "clip", "cps_to_midi", - "cpsmidi", + "cps_to_octave", + "curvelin", "db_to_amp", - "dbamp", - "hz_to_mel", + "distort", + "ecdf_to_lin", + "ecdf", + "fermi", + "fold", + "gain", + "interp_spline", + "interp", + "lcurve", + "lin_to_ecdf", + "lincurve", + "linexp", "linlin", - "mel_to_hz", + "linlog", + "linpoly", + "linspace", "midi_to_cps", + "midi_to_ratio", + "norm_peak", + "norm_rms", + "normalize", + "octave_to_cps", + "ratio_to_midi", + "remove_dc", + "scurve", + "softclip", + "wrap", + "mel_to_hz", + "hz_to_mel", + # some synonyms + "ampdb", + "cpsmidi", + "dbamp", "midicps", -] + # class and helper functions + "ChainableArray", + "chain", + "register_chain_fn", +] # type: ignore diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 4b665f1..d78575f 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -1,18 +1,22 @@ -"""Collection of audio related mapping functions""" -from typing import Optional, Union +"""Collection of audio related mapping functions.""" + +from typing import Any, Callable, List, Optional, TypeVar, Union import numpy as np +from numpy.typing import ArrayLike + +NDArrayType = TypeVar("NDArrayType", bound=np.ndarray) def linlin( - value: Union[float, np.ndarray], + value: Union[float, ArrayLike], x1: float, x2: float, y1: float, y2: float, clip: Optional[str] = None, ) -> Union[float, np.ndarray]: - """Map value linearly so that [x1, x2] is mapped to [y1, y2] + """Map value linearly so that [x1, x2] is mapped to [y1, y2]. linlin is implemented in analogy to the SC3 linlin, yet this function extrapolates by default. @@ -21,7 +25,7 @@ def linlin( Parameters ---------- - value : float or np.ndarray + value : float or np.ndarray (ArrayLike) value(s) to be mapped x1 : float source value 1 @@ -54,16 +58,382 @@ def linlin( return np.minimum(np.maximum(z, y1), y2) +def linexp( + value: Union[float, ArrayLike], + x1: float, + x2: float, + y1: float, + y2: float, + clip: Optional[str] = None, +) -> Union[float, np.ndarray]: + """Map value exponentially so that [x1, x2] is mapped to [y1, y2]. + + linexp is implemented in analogy to the SC3 linexp, yet this + function extrapolates by default. + A frequently used invocation is with x1 < x2, i.e. thinking + of them as a range [x1,x2] + + Parameters + ---------- + value : float or np.ndarray (ArrayLike) + value(s) to be mapped + x1 : float + source value 1 + x2 : float + source value 2 + y1 : float + destination value to be reached for value == x1 + y2 : float + destination value to be reached for value == x2 + clip: None or string + None extrapolates, "min" or "max" clip at floor resp. ceiling + of the destination range, any other value defaults to "minmax", + i.e. it clips on both sides. + + Returns + ------- + float or np.ndarray + the mapping result + """ + z = np.exp((value - x1) / (x2 - x1) * (np.log(y2) - np.log(y1)) + np.log(y1)) + if clip is None: + return z + if y1 > y2: + x1, x2, y1, y2 = x2, x1, y2, y1 + if clip == "max": + return np.minimum(z, y2) + elif clip == "min": + return np.maximum(z, y1) + else: # imply clip to be "minmax" + return np.minimum(np.maximum(z, y1), y2) + + +def linlog( + value: Union[float, ArrayLike], + x1: float, + x2: float, + y1: float, + y2: float, + clip: Optional[str] = None, +) -> Union[float, np.ndarray]: + """Map value logarithmically so that [x1, x2] is mapped to [y1, y2]. + + linlog is implemented in analogy to the SC3 linlog, yet this + function extrapolates by default. + A frequently used invocation is with x1 < x2, i.e. thinking + of them as a range [x1,x2] + + Parameters + ---------- + value : float or np.ndarray (ArrayLike) + value(s) to be mapped + x1 : float + source value 1 + x2 : float + source value 2 + y1 : float + destination value to be reached for value == x1 + y2 : float + destination value to be reached for value == x2 + clip: None or string + None extrapolates, "min" or "max" clip at floor resp. ceiling + of the destination range, any other value defaults to "minmax", + i.e. it clips on both sides. + + Returns + ------- + float or np.ndarray + the mapping result + """ + z = np.log((value - x1) / (x2 - x1) * (np.exp(y2) - np.exp(y1)) + np.exp(y1)) + if clip is None: + return z + if y1 > y2: + x1, x2, y1, y2 = x2, x1, y2, y1 + if clip == "max": + return np.minimum(z, y2) + elif clip == "min": + return np.maximum(z, y1) + else: # imply clip to be "minmax" + return np.minimum(np.maximum(z, y1), y2) + + +def lincurve( + x: Union[float, ArrayLike], + x1: float, + x2: float, + y1: float = -1.0, + y2: float = 1.0, + curve: float = -2.0, + clip: Optional[str] = None, +) -> Union[float, np.ndarray]: + """Map value exponentially so that [x1, x2] is mapped to [y1, y2]. + + lincurve is implemented in analogy to the SC3 lincurve, yet this + function extrapolates by default. + A frequently used invocation is with x1 < x2, + i.e. thinking of them as a range [x1, x2] + x1 is mapped to y1. Use y2 < y1 for polarity inversion, i.e. curve = -curve + returns y1 + (y2 - y1) / + (1.0 - exp(curve)) * (1 - exp(curve) ** ((x - x1) / (x2 - x1))) + yoffset + yrange * (this goes from 0 =(1-grow**0) to (1-grow**1) + + Parameters + ---------- + value : float or np.ndarray (ArrayLike) + value(s) to be mapped + x1 : float + source value 1 + x2 : float + source value 2 + y1 : float + destination value to be reached for value == x1 + y2 : float + destination value to be reached for value == x2 + curve : float + specification of the curvature. TBA + clip: None or string + None extrapolates, "min" or "max" clip at floor resp. ceiling + of the destination range, any other value defaults to "minmax", + i.e. it clips on both sides. + + Returns + ------- + float or np.ndarray + the mapping result + """ + if abs(curve) < 0.001: + z = (x - x1) / (x2 - x1) * (y2 - y1) + y1 + else: + grow = np.exp(curve) + a = (y2 - y1) / (1.0 - grow) + b = y1 + a + z = b - a * grow ** ((x - x1) / (x2 - x1)) + + if y1 > y2: + y1, y2 = y2, y1 + if clip: + if clip == "max": + z = np.minimum(z, y2) + elif clip == "min": + z = np.maximum(z, y1) + else: # imply clip to be "minmax" + z = np.minimum(np.maximum(z, y1), y2) + return z + + +def curvelin( + x: Union[float, ArrayLike], + x1: float, + x2: float, + y1: float = -1.0, + y2: float = 1.0, + curve: float = -2.0, + clip: Optional[str] = None, +) -> Union[float, np.ndarray]: + """Map (assumedly exponentially curved) x from [x1, x2] linearly to [y1, y2]. + + This is done by applying a curve parameter as in sc3. the input range can include 0, + different from explin a clipping is performed according to the clip argument. + + curvelin is implemented in analogy to the SC3 curvelin, yet extrapolates by default. + A frequently used invocation is with x1 y2: + y1, y2 = y2, y1 + if clip: + if clip == "max": + z = np.minimum(z, y2) + elif clip == "min": + z = np.maximum(z, y1) + else: # imply clip to be "minmax" + z = np.minimum(np.maximum(z, y1), y2) + return z + + +def linpoly( + x: Union[float, ArrayLike], + xmax: float = 1.0, + y1: float = -1.0, + y2: float = 1.0, + curve: float = 2.0, + clip: Optional[str] = None, +) -> Union[float, np.ndarray]: + """Map x between [-xmax, xmax] to [y1, y2] using a polynomial mapping. + + The mapping is y1 + (y2 - y1) * (1 + (x/xmax)**order) / 2 + where order = 1 + curve if curve>0 else (1 - 1 / (1 + curve)) + + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): values to be mapped + xmax (float): source scale, defaults to 1.0 + y1 (float, optional): target range low value, Defaults to -1.0. + y2 (float, optional): target ragne 2nd value. Defaults to 1.0. + curve (float, optional): defaults to 2 + if curve > 0: polynomial order is curve + 1 + if curve < 0: polynomial order is 1 - 1/curve + clip (Optional[str], optional): clip flags (min / max / minmax). + Defaults to None. + + Returns + ------- + Union[float, np.ndarray]: mapping result for x + """ + order = 1 + curve if curve >= 0 else (1 / (1 - curve)) + z = y1 + (y2 - y1) * (1 + np.sign(x) * (np.abs(x) / xmax) ** order) / 2 + if clip is None: + return z + if y1 > y2: + y1, y2 = y2, y1 + if clip == "max": + return np.minimum(z, y2) + elif clip == "min": + return np.maximum(z, y1) + else: # imply clip to be "minmax" + return np.minimum(np.maximum(z, y1), y2) + + +def interp_spline( + x: Union[float, ArrayLike], + xc: Union[List[float], np.ndarray], + yc: Union[List[float], np.ndarray] = [-1, 0, 1], + k=1, + **kwarg, +) -> Union[float, np.ndarray]: + """Apply scipy.interpolate.interp_spline interpolation. + + applicable for piecewise linear mappings, with extrapolation. + interp_spline is slower than numpy.interp() for smaller data sets (e.g. <5000) + however, it allows extrapolation. + + Parameters + ---------- + x : float or np.ndarray (ArrayLike) + value(s) to be mapped + xc : Union[List[float], np.ndarray] + x coordinates of line segment function, must be sorted + yc : Union[List[float], np.ndarray] + y coordinates of line segment function, requires len(yc)==len(xc) + k : interpolation order (see interp_spline, defaults to 1 = linear) + + Returns + ------- + float or np.ndarray + the mapping result, extrapolating beyond bounds + + """ + from scipy.interpolate import make_interp_spline + + spl = make_interp_spline(xc, yc, k=k) # k=1: linear + return spl(x) if isinstance(x, ChainableArray) else chain(spl(x)) + + +def interp( + x: Union[float, ArrayLike], + xc: Union[List[float], np.ndarray], + yc: Union[List[float], np.ndarray] = [-1, 0, 1], + **kwarg, +) -> Union[float, np.ndarray]: + """Apply numpy.interp interpolation. + + applicable for piecewise linear mappings, with extrapolation + interp is faster than interp_spline() for small x (e.g. <5000), + but it clips by default. + + Parameters + ---------- + x : float or np.ndarray (ArrayLike) + value(s) to be mapped + xc : Union[List[float], np.ndarray] + x coordinates of line segment function, must be sorted + yc : Union[List[float], np.ndarray] + y coordinates of line segment function, requires len(yc)==len(xc) + + Returns + ------- + float or np.ndarray + the mapping result, clipping beyond bounds + + """ + return np.interp(x, xc, yc, **kwarg) + + +def bilin( + x: Union[float, ArrayLike], + xcenter: float, + xmin: float, + xmax: float, + ycenter: float = 0, + ymin: float = -1, + ymax: float = 1, +) -> Union[float, np.ndarray]: + """Bilin compatibility function. implements sc3 bilin function. + + This maps x in 2 linear segments as given by coordinates. + + Parameter: + --------- + x (Union[float, np.typing.ArrayLike]): _description_ + xcenter (float): _description_ + xmin (float): _description_ + xmax (float): _description_ + ycenter (float): _description_ + ymin (float): _description_ + ymax (float): _description_ + + Returns + ------- + Union[float, np.ndarray], + the mapping result + """ + return interp_spline(x, [xmin, xcenter, xmax], [ymin, ycenter, ymax]) + + def clip( - value: Union[float, np.ndarray], + value: Union[float, ArrayLike], minimum: float = -float("inf"), maximum: float = float("inf"), ) -> Union[float, np.ndarray]: - """Clips a value to a certain range + """Clips a value to a certain range. Parameters ---------- - value : float or np.ndarray + value : float or np.ndarray (ArrayLike) Value(s) to clip minimum : float, optional Minimum output value, by default -float("inf") @@ -75,7 +445,7 @@ def clip( float clipped value """ - if type(value) == np.ndarray: + if isinstance(value, np.ndarray): return np.maximum(np.minimum(value, maximum), minimum) else: # ToDo: check if better performance than above numpy code - if not: delete if value < minimum: @@ -86,7 +456,7 @@ def clip( def midi_to_cps(midi_note: float) -> float: - """Convert MIDI note to cycles per second + """Convert MIDI note to cycles per second. Parameters ---------- @@ -105,7 +475,7 @@ def midi_to_cps(midi_note: float) -> float: def cps_to_midi(cps: float) -> float: - """Convert cycles per second to MIDI note + """Convert cycles per second to MIDI note. Parameters ---------- @@ -123,16 +493,96 @@ def cps_to_midi(cps: float) -> float: cpsmidi = cps_to_midi +def midi_to_ratio(midi_note: float) -> float: + """Convert MIDI difference to ratio. + + Parameters + ---------- + m : float + MIDI note + + Returns + ------- + float + corresponding ratio + """ + return 2 ** (midi_note / 12.0) + + +midiratio = midi_to_ratio + + +def ratio_to_midi(ratio: float) -> float: + """Convert ratio to MIDI difference. + + Parameters + ---------- + ratio : float + ratio (e.g. of frequencies) + + Returns + ------- + float + corresponding MIDI difference + """ + return 12 * np.log2(ratio) + + +ratiomidi = ratio_to_midi + + +def cps_to_octave(cps: float) -> float: + """Convert cycles per second into decimal octaves. + + reference Middle C (i.e. MIDI 60, C_4, 261.626 Hz) yields 4 (octaves). + + Parameters + ---------- + cps : float + cycles per second + + Returns + ------- + float + octaves relative to Middle C (MIDI 60 C_4, 261.626 HZ) + """ + return np.log2(cps / 440) + 4.75 + + +cpsoct = cps_to_octave + + +def octave_to_cps(octave: float) -> float: + """Convert octaves to cps. + + reference 4.75 yields 440 Hz, i.e. 4 -> freq of Middle C (C4) + + Parameters + ---------- + octave : float + octave + + Returns + ------- + float + cycles per second + """ + return 440 * 2 ** (octave - 4.75) + + +octcps = octave_to_cps + + def hz_to_mel(hz): - """Convert a value in Hertz to Mels + """Convert a value in Hertz to Mels. Parameters ---------- hz : number of array value in Hz, can be an array - Returns: - -------- + Returns + ------- _ : number of array value in Mels, same type as the input. """ @@ -140,15 +590,15 @@ def hz_to_mel(hz): def mel_to_hz(mel): - """Convert a value in Hertz to Mels + """Convert a value in Hertz to Mels. Parameters ---------- hz : number of array value in Hz, can be an array - Returns: - -------- + Returns + ------- _ : number of array value in Mels, same type as the input. """ @@ -191,3 +641,571 @@ def amp_to_db(amp: float) -> float: ampdb = amp_to_db + + +def distort( + x: Union[float, ArrayLike], threshold: float = 1.0 +) -> Union[float, np.ndarray]: + """Apply value distortion x/(threshold + |x|). + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + threshold (float, optional): defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: distorted value / array + """ + return x / (threshold + np.abs(x)) + + +def softclip( + x: Union[float, ArrayLike], threshold: float = 1.0 +) -> Union[float, np.ndarray]: + """Apply softclip distortion to x. + + This yields a perfectly linear region within [-0.5, 0.5], + outside values computed by (|x| - 0.25) / x + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + threshold (float, optional): defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: softclip distorted value / array + """ + condition = np.abs(x) > 0.5 + return (np.abs(x) - 0.25) / x * condition + (1 - condition) * x + # return (np.abs(x) - 0.25)/x if np.abs(x)<0.5 else x + + +def scurve( + x: Union[float, ArrayLike], threshold: float = 1.0 +) -> Union[float, np.ndarray]: + """Map value onto an S-curve bound to [0,1]. + + Implements v * v * (3-(2*v)) mit v = x.clip(0,1) + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + threshold (float, optional): defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: scurve distorted value / array + """ + v = clip(x, 0, 1) + return v**2 * (3 - 2 * v) + + +def lcurve( + x: Union[float, ArrayLike], m: float = 0.0, n: float = 1.0, tau: float = 1.0 +) -> Union[float, np.ndarray]: + """Map value or array onto an L-curve. + + Implements (1 + m * exp(-x/tau) + 1) / (1 + n * exp(-x/tau)) + - equal to fermi function with default parameters + - note that different to the sc3 implementation, tau is inside + the exp function (unclear tau placement in sc3...) + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + m (float, optional): numerator factor defaults to 0.0. + n (float, optional): denumerator factor defaults to 1.0. + tau (float, optional): scale constant, defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: lcurve distorted value / array + """ + return (1 + m * np.exp(-x / tau)) / (1 + n * np.exp(-x / tau)) + + +def fermi( + x: Union[float, ArrayLike], tau: float = 1.0, mu: float = 0.0 +) -> Union[float, np.ndarray]: + """Apply fermi function to value or array. + + Implements 1 / (1 + exp(-x/tau)) + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + tau (float, optional): scale constant, defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: fermi distorted value / array + """ + return 1.0 / (1 + np.exp(-(x - mu) / tau)) + + +def normalize( + x: Union[float, ArrayLike], y1: float = -1.0, y2: float = 1.0 +) -> Union[float, np.ndarray]: + """Normalize array to target range [y1, y2]. + + Linear mapping [min(x), max(x)] to [y1, y2]. Use y1 > y2 to change polarity. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + y1 (float, optional): mapping target for min(x). Defaults to -1.0. + y2 (float, optional): mapping target for max(x). Defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: normalized / scaled array + """ + x1, x2 = np.amin(x), np.amax(x) + return (x - x1) / (x2 - x1) * (y2 - y1) + y1 + + +def wrap( + x: Union[float, ArrayLike], y1: float = -1.0, y2: float = 1.0 +) -> Union[float, np.ndarray]: + """Wrap array around target range [y1, y2]. + + This implements the mapping y1 + x % (y2 - y1). + The order of y1, y2 is irrelevant. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + y1 (float, optional): 1st wrap bound. Defaults to -1.0. + y2 (float, optional): 2nd wrap bound. Defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: wraped array + """ + return y1 + x % (y2 - y1) + + +def fold( + x: Union[float, ArrayLike], y1: float = -1.0, y2: float = 1.0 +) -> Union[float, np.ndarray]: + """Fold array around target range [y1, y2]. + + This implements (np.abs((x - y2) % (2 * L) - L) + y1), + ordering bounds so that y1 < y2. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + y1 (float, optional): 1st fold bound. Defaults to -1.0. + y2 (float, optional): 2nd fold bound. Defaults to 1.0. + + Returns + ------- + Union[float, np.ndarray]: folded array + """ + if y2 < y1: + y1, y2 = y2, y1 + L = y2 - y1 + return np.abs((x - y2) % (2 * L) - L) + y1 + + +def remove_dc( + x: Union[float, ArrayLike], +) -> Union[float, np.ndarray]: + """Remove DC bias. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + + Returns + ------- + Union[float, np.ndarray]: normalized / scaled array + """ + return x - np.mean(x) + + +def norm_peak(x: Union[float, ArrayLike], peak=1.0): + """Normalize array so that max(abs(x)) = peak. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + peak (float): target peak + + Returns + ------- + Union[float, np.ndarray]: normalized (scaled) array + """ + peak_of_x = np.max(np.abs(x)) + return (x / peak_of_x) * peak if peak_of_x != 0 else x + + +def norm_rms(x: Union[float, ArrayLike], rms=1.0): + """Normalize array so that its RMS value equals `rms`. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + rms (float): target rms of array + + Returns + ------- + Union[float, np.ndarray]: rms normalized (scaled) array + """ + rms_of_x = np.sqrt(np.mean(x**2)) + return (x / rms_of_x) * rms if rms_of_x != 0 else x + + +def gain( + x: Union[float, ArrayLike], db: Optional[float] = None, amp: Optional[float] = None +): + """Apply gain, either as dB (SPL) or scalar factor amp. + + No operation done if neither argument is given, it applies both if both are given. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): input value or array + db (None or float): dB SPL = gain 10**(db/20), e.g. -6 dB ~ factor 0.5 + amp (None or float): gain factor + + Returns + ------- + Union[float, np.ndarray]: scaled (amplified / attenuated) array + """ + if db: + sig = x * dbamp(db) + else: + sig = x.copy() + if amp: + sig *= amp + return sig + + +def linspace( + x: Union[float, int, ArrayLike], x1: float, x2: float, endpoint: bool = True +) -> np.ndarray: + """Create np.linspace from x1 to x2 in int(x) resp len(x) steps. + + Parameters + ---------- + x (Union[float, int, ArrayLike]): length or array of which only shape is used + x1 (float): target interval one side + x2 (float): target interval other side + endpoint (bool): forwarded to np.linspace + + Returns + ------- + Union[float, np.ndarray]: array of length len(x) + (resp. int(x) if x is float) of numbers between x1 and x2 + """ + if isinstance(x, np.ndarray): + return np.linspace(x1, x2, x.shape[0], endpoint=endpoint) + else: + return np.linspace(x1, x2, int(abs(x)), endpoint=endpoint) + + +def lin_to_ecdf( + x: Union[float, ArrayLike], ref_data: np.ndarray, sorted: bool = False +) -> Union[float, np.ndarray]: + """Map data using empiric cumulative distribution function as mapping. + + This meann feature values are mapped to quantiles. + if sorted==True: ref_data is regarded as sorted, speeding repeated invocations. + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): value or array to map + ref_data (np.ndarray): reference data used to create ecdf. + sorted (bool): whether ref_data is sorted. + Defaults to False, i.e. by default data will be sorted. + + Returns + ------- + np.ndarray: resulting mapped data + """ + if sorted: + return interp( + x, ref_data, np.arange(1, len(ref_data) + 1) / float(len(ref_data)) + ) + else: + return interp(x, *ecdf(ref_data)) + + +def ecdf_to_lin( + x: Union[float, ArrayLike], ref_data: np.ndarray, sorted: bool = False +) -> Union[float, np.ndarray]: + """Map data using inverse empiric cumulative distribution function. + + This means that quantiles are mapped back to estimated feature values. + - if ref_data is omitted, x is used instead + - if sorted==True: ref_data is regarded as sorted, speeding repeated invocations + + Parameters + ---------- + x (Union[float, np.typing.ArrayLike]): value or array to map + ref_data (np.ndarray): reference data used to create ecdf. + sorted (bool): whether ref_data is sorted. + Defaults to False, i.e. data will be sorted. + + Returns + ------- + np.ndarray: resulting mapped data + """ + if sorted: + return interp( + x, np.arange(1, len(ref_data) + 1) / float(len(ref_data)), ref_data + ) + else: + xc, yc = ecdf(ref_data) + return interp(x, yc, xc) + + +# create subclass of numpy.ndarray + + +class ChainableArray(np.ndarray): + """subclass for simpler numpy mapping by chaining syntax.""" + + def __new__(cls, input_array, *args, **kwargs): + """Create new instance.""" + obj = np.asarray(input_array).view(cls) + return obj + + def __array_finalize__(self, obj): + """Finalize array.""" + if obj is None: + return + + def to_array(self): + """Convert self to np.ndarray.""" + return np.array(self) + + def to_asig(self, sr=44100): + """Convert self to pya.Asig.""" + from pya import Asig + + return Asig(self, sr=sr) + + def plot(self, *args, **kwargs): + """Plot self via matplotlib.""" + import matplotlib.pyplot as plt + + sr = kwargs.pop("sr", None) + if sr: + xs = np.arange(0, self.shape[0]) / sr + plt.plot(xs, self, *args, **kwargs) + plt.xlabel("time [s]") + else: + xs = kwargs.pop("xs", None) + if xs is not None: + plt.plot(xs, self, *args, **kwargs) + else: + plt.plot(self, *args, **kwargs) + + return self + + def mapvec( + self: NDArrayType, fn: Callable[..., Any], *args: Any, **kwargs: Any + ) -> NDArrayType: + """Map fn on self by using np.vectorize(). + + Parameters + ---------- + self (NDArrayType): array to map + fn (Callable[..., Any]): function to call on each element + + Returns + ------- + NDArrayType: mapping result as ChainableArray + """ + return np.vectorize(fn)(self, *args, **kwargs) + + def map( + self: NDArrayType, fn: Callable[..., Any], *args: Any, **kwargs: Any + ) -> NDArrayType: + """Apply function fn directly to self; on fail suggest to use mapvec(). + + Parameters + ---------- + self (np.ndarray): array used as input of fn + fn (Callable[..., Any]): mapping function + args and kwargs are passed on to fn + + Raises + ------ + TypeError: if fn fails to operate on np.ndarray as first argument. + mapvec() is then proposed as alternative. + + Returns + ------- + ChainableArray: the mapping result as ChainableArray + """ + try: + return chain(fn(self, *args, **kwargs)) + except (TypeError, ValueError, AttributeError) as e: + raise TypeError( + f"Function {fn.__name__} does not support NumPy arrays directly. " + "Use .mapvec() instead for np.vectorize elementwise mapping." + ) from e + + def __getattr__(self, name: str) -> Callable: + """Dynamically handle method calls.""" + if name.startswith("dynamic_"): + return lambda *args: f"Called {name} with {args}" + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + + +def ecdf( + x: np.ndarray, selection: slice = slice(None, None, None) +) -> tuple[np.ndarray, np.ndarray]: + """Empirical cumulative distribution function. + + Usable for handcrafted mapping functions such as with using ChainableArray.interp() + + Example 1: (compute once - use many) + >>> myecdf = ecdf(data); chain(otherdata).interp(*ecdf) + + Example 2: (compute and map in one go) + >>> chain(otherdata).interp(*ecdf(data)) + + Example 3: (use a sparser (more smooth) ecdf mapping) + >>> chain(otherdata).interp(*ecdf(data, np.s_[::5])) + + Parameters + ---------- + x (np.ndarray): array + selection (slice): slice applied to x and y coordinates of the resulting tuple + + Returns + ------- + tuple[np.ndarray, np.ndarray]: sorted array of x and y coordinates of the ecdf + """ + xs = np.sort(x) + ys = np.arange(1, len(xs) + 1) / float(len(xs)) + return xs[selection], ys[selection] + + +def register_numpy_ufunc(fn: np.ufunc, name: Union[None, str] = None) -> None: + """Register numpy ufunc with one or two ndarray arguments.""" + nin = fn.nin + if nin == 1: + + def method1(self, *args, **kwargs): + return ChainableArray(fn(self, *args, **kwargs)) + + method = method1 + + elif nin == 2: + + def method2(self, other, *args, **kwargs): + return ChainableArray(fn(self, other, *args, **kwargs)) + + method = method2 + + else: + print("warning: np.ufunc fn has nin not in [1,2]") + + def default_method(x): + return None + + method = default_method + + method.__name__ = fn.__name__ if not name else name + method.__doc__ = ( + f"{method.__name__} implements numpy.{method.__name__}" + + f"function for ChainableArray. See help(np.{fn.__name__})" + ) + + setattr(ChainableArray, method.__name__, method) + + +def register_chain_fn(fn: Callable, name: Union[None, str] = None) -> None: + """Register function fn for chaining, optionally under given name.""" + + def method(self, *args, **kwargs): + return ChainableArray(fn(self, *args, **kwargs)) + + method.__name__ = fn.__name__ if not name else name + method.__doc__ = ( + f"{method.__name__} implements the {method.__name__}" + + "operation for ChainableArray. Argument: np.ndarray" + ) + + setattr(ChainableArray, method.__name__, method) + + +def _list_numpy_ufuncs(): + """Return all numpy ufuncs with 1 or 2 ndarray arguments.""" + ufunc_list = [] + for attr_name in dir(np): # all attributes in numpy + attr = getattr(np, attr_name) + if isinstance(attr, np.ufunc): + if attr.nin <= 2: + ufunc_list.append(attr) + else: + print(attr, attr.nin) + return ufunc_list + + +# create class methods for numpy functions and pyamapping functions +for fn in _list_numpy_ufuncs(): # numpy_mapping_functions: + name = "abs" if fn.__name__ == "absolute" else None + register_numpy_ufunc(fn, name) + +# register some non-ufunc which nontheless should workd +register_chain_fn(np.angle, "angle") +ChainableArray.magnitude = ChainableArray.abs + +pyamapping_functions = [ + amp_to_db, + bilin, + clip, + cps_to_midi, + cps_to_octave, + curvelin, + db_to_amp, + distort, + ecdf_to_lin, + ecdf, + fermi, + fold, + gain, + hz_to_mel, + interp_spline, + interp, + lcurve, + lin_to_ecdf, + lincurve, + linexp, + linlin, + linlog, + linpoly, + linspace, + mel_to_hz, + midi_to_cps, + midi_to_ratio, + norm_peak, + norm_rms, + normalize, + octave_to_cps, + ratio_to_midi, + remove_dc, + scurve, + softclip, + wrap, +] + +for fn in pyamapping_functions: + register_chain_fn(fn, name) + + +def chain(input_array: ArrayLike) -> ChainableArray: + """Turn np.ndarray into ChainableArray.""" + # ToDo: check difference to input_array.view(ChainableArray) + return ChainableArray(input_array) From 956153330ea15c2e33e0c19f6fde8993acc0722e Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 25 Oct 2025 22:46:08 +0200 Subject: [PATCH 03/67] add pyamapping example notebook --- notebooks/pyamapping-examples.ipynb | 246 ++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 notebooks/pyamapping-examples.ipynb diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb new file mode 100644 index 0000000..3119c72 --- /dev/null +++ b/notebooks/pyamapping-examples.ipynb @@ -0,0 +1,246 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# pyamapping - mapping functions for audio computing (and beyond)\n", + "\n", + "by Thomas Hermann and Dennis Reinsch, 2023++\n", + "\n", + "## Introduction \n", + "\n", + "### Background / History\n", + "\n", + "- pyamapping bundles frequently used mapping functions for audio computing\n", + "- the earliest functions were reimplementations of ampdb, dbamp, midicps, cpsmidi, linlin, coded in analogy to SuperCollider3 functions to be used within the sc3nb package. (coded by TH)\n", + "- later, when I started pya, the same functions were needed, yet importing sc3nb would have caused unwanted dependencies, so we created pyamapping as a very lean package that both sc3nb and pya depend on (created by DR)\n", + "- now in 2025 pyamapping grows strongly (additions by TH) \n", + " - firstly by adding many mapping functions available in sc3 which were beforehand not copied\n", + " - secondly, by introducing ChainableArray, a class that wraps numpy.ndarrays, allowing to daisy chain operations on numpy arrays, similar to how we offer it for pya.\n", + "- This notebook introduces the available mapping functions with examples.\n", + "\n", + "### Overview\n", + "\n", + "**pyamapping** offers a set of mapping functions often used \n", + "\n", + "- in the context of sound and computer music \n", + "- in the context of auditory display and sonification (e.g. parameter mapping sonifications)\n", + "- ...\n", + "\n", + "A source of inspiration is librosa and Supercollider3. This package reimplements them and adds mappings used in the interactive sonification stack (cf. ), including the following packages that all make use of pyamapping:\n", + "\n", + "- **sc3nb** - sc3 interface for Python and Jupyter notebooks\n", + "- **pya** - the python Audio Coding Package\n", + "- **mesonic** - a middleware for sonification and auditory display \n", + "- **sonecules** - a high-level class library for sonification and auditory display\n", + "\n", + "**Chainable numpy arrays**\n", + "\n", + "Method chaining offers concise syntax and proved helpful in pya.\n", + "Numpy offers method chaining only for few functions.\n", + "This package extends method chaining \n", + "\n", + "- by inheriting the class `ChainableArray` from `numpy.ndarray`\n", + "- by adding wrappers to enable a method chain syntax for most `ufuncs`\n", + "- by providing a general `map()` method for direct vectorized mapping\n", + "- by providing helper functions to vectorize Python functions into methods\n", + "\n", + "Furthermore it offers some convenience functions, e.g.\n", + "\n", + "- to plot arrays (optionally as signal with given sample rate)\n", + "\n", + "We hope that pyamapping will help to write signal transformations and manipulations in a more concise, compact and readible manner." + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "**Imports and Headers**\n", + "\n", + "- as pyamapping is a long name, importing as `pam` is a suggested abbreviation \n", + "- matplotlib and pprint imports are merely for showing example output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib as mpl\n", + "import matplotlib.pyplot as plt\n", + "from pprint import pprint\n", + "import pyamapping as pam\n", + "\n", + "mpl.rcParams['figure.figsize'] = (9,2.5)" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Available pyamapping functions - Overview\n", + "\n", + "- in import of pyamapping, wrappers are automatically created for numpy ufuncs.\n", + " - later versions may have them created verbatim to enable code completion\n", + "- the following code simply lists those numpy functions plus special dedicated/ new pyamapping functions that do not have their origin in numpy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from pyamapping.mappings import _list_numpy_ufuncs, pyamapping_functions\n", + "\n", + "# print (i) a compact list of all unary and binary numpy functions \n", + "# and (ii) all pyamapping functions\n", + "u_lists = [[], []]\n", + "\n", + "for ufunc in _list_numpy_ufuncs():\n", + " u_lists[ufunc.nin - 1].append(ufunc.__name__)\n", + "\n", + "# compact list numpy functions\n", + "for i, li in enumerate(u_lists):\n", + " print(f\"\\n=== numpy functions with {i+1} argument ===\")\n", + " pprint(li, compact=True, width=80)\n", + "\n", + "# compact list of pyamapping functions\n", + "li = [el.__name__ for el in pyamapping_functions]\n", + "print(f\"\\n=== pyamapping functions: ===\")\n", + "pprint(li, compact=True, width=80)" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## pyamapping - Demonstration and Examples" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "### ChainableArray - Basics" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "Any numpy array can be turned into a chainable array by using the `ChainableArray` class defined in `pyamapping`.\n", + "- the chain() function provides a shortcut, making this construction shorter.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "from pyamapping import ChainableArray, chain\n", + "\n", + "# some data\n", + "data = np.random.random(100)\n", + "\n", + "# create ChainableArray\n", + "dch = ChainableArray(data)\n", + "\n", + "# the same can be obtained shorter by\n", + "dch = chain(data)" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "ChainableArray offer the following methods:\n", + "\n", + "- `to_array` - back to numpy ndarray\n", + "- `to_asig` - convert into pya.Asig\n", + "- `plot` - plot signal(s) as time series\n", + "- `mapvec` - map function on self by using numpy.vectorize\n", + "- `map` - apply function directly to the array itself\n", + "\n", + "Here is a quick demonstration:\n", + "\n", + "- let us\n", + " - map $x \\to (5x)^2 + 0.1$, \n", + " - plot as signal assuming sampling rate 100 Hz, \n", + " - convert into decibel, \n", + " - turn that into an audio signal (i.e. pya.Asig) \n", + " - and plot it in the same figure created above.\n", + " - finally transform the ChainableArray back to a regular numpy.ndarray.\n", + "\n", + "Using pyamapping, the code is both shorter and more concise than the above description:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "dch2 = dch.map(lambda x: (5*x)**2+0.1).plot(sr=100, color=\"r\").mapvec(pam.amp_to_db)\n", + "a1 = dch2.to_asig(sr=100).plot(color=\"c\", lw=0.8)\n", + "\n", + "# conversion back to numpy array is rarely needed but if...\n", + "dd = dch2.to_array()\n", + "type(dd)" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "ChainableArray is a recent addition to pyamapping, yet introduced here as it makes demonstrations of mapping functions extremely readable..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "isonpy13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From e3f866c50ea8b29144cfb92d87afa22f4f0f262c Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 27 Oct 2025 17:28:53 +0100 Subject: [PATCH 04/67] add linlin() examples --- notebooks/pyamapping-examples.ipynb | 98 +++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 20 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 3119c72..a6e3c72 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -77,6 +77,7 @@ "import matplotlib.pyplot as plt\n", "from pprint import pprint\n", "import pyamapping as pam\n", + "from pyamapping import chain\n", "\n", "mpl.rcParams['figure.figsize'] = (9,2.5)" ] @@ -213,34 +214,91 @@ "ChainableArray is a recent addition to pyamapping, yet introduced here as it makes demonstrations of mapping functions extremely readable..." ] }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Tour of Mapping Functions: Functions from SuperCollider3 and librosa" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "### `linlin`\n", + "\n", + "- linlin is implemented in analogy to the SC3 linlin\n", + "- `linlin(v, x1, x2, y1, y2)` maps values v (scalar or arraylike) affine linearly so that [x1, x2] is mapped to [y1, y2].\n", + "- note that this linlin function extrapolates by default\n", + "- clipping can be controlled via the clip argument (values None (default), \"min\", \"max\", or anything else for \"minmax\")\n", + "- A frequently used invocation is with `x1 < x2`, i.e. thinking of them as a range $[x_1, x_2]$" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "pam.linlin(7, 0, 10, 100, 300)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "pam.linlin(7, 0, 5, 100, 300, \"max\") # clip result to maximum input range" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "- ChainableArray.linlin uses self as input v.\n", + "- Here are some mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "vch = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", + "vch.plot(\"k-\", label=\"input data\") # plot input data\n", + "vch.linlin(0, 1, 1, 3).plot(\"r-\", label=\"linlin\")\n", + "vch.linlin(0.2, 0.7, -2, 2, \"minmix\").plot(\"g-\", label=\"linlin with clip\")\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", "metadata": {}, "outputs": [], + "source": [ + "# plot mapping function (output vs input)\n", + "plt.figure(figsize=(3,1.5)); plt.grid()\n", + "plt.plot(vch, vch.linlin(0.25, 0.75, -1, 1, \"minmax\"));" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, "source": [] } ], - "metadata": { - "kernelspec": { - "display_name": "isonpy13", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.2" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } From dae8f724462850fbf8d2c30df14a3c97bee0f01f Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 27 Oct 2025 20:21:30 +0100 Subject: [PATCH 05/67] improve linlin example --- notebooks/pyamapping-examples.ipynb | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index a6e3c72..d4013ff 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -219,7 +219,9 @@ "id": "12", "metadata": {}, "source": [ - "## Tour of Mapping Functions: Functions from SuperCollider3 and librosa" + "## Tour of Mapping Functions: Functions from SuperCollider3 and librosa\n", + "\n", + "For the following functions, we use `xs` to refer to the input array and `ys` for the outputs." ] }, { @@ -230,10 +232,11 @@ "### `linlin`\n", "\n", "- linlin is implemented in analogy to the SC3 linlin\n", - "- `linlin(v, x1, x2, y1, y2)` maps values v (scalar or arraylike) affine linearly so that [x1, x2] is mapped to [y1, y2].\n", + "- `linlin(v, x1, x2, y1, y2)` maps values v (scalar or arraylike) affine linearly so that [x1, x2] is mapped to [y1, y2]:\n", + " $$ z = y_1 + \\frac{v - x_1}{x_2 - x_1} \\cdot (y_2 - y_1) $$\n", "- note that this linlin function extrapolates by default\n", "- clipping can be controlled via the clip argument (values None (default), \"min\", \"max\", or anything else for \"minmax\")\n", - "- A frequently used invocation is with `x1 < x2`, i.e. thinking of them as a range $[x_1, x_2]$" + "- A frequently used invocation is with $x_1 < x_2$, i.e. thinking of them as a range $[x_1, x_2]$" ] }, { @@ -261,7 +264,7 @@ "id": "16", "metadata": {}, "source": [ - "- ChainableArray.linlin uses self as input v.\n", + "- ChainableArray.linlin uses self as input.\n", "- Here are some mapping examples and plots" ] }, @@ -272,10 +275,10 @@ "metadata": {}, "outputs": [], "source": [ - "vch = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", - "vch.plot(\"k-\", label=\"input data\") # plot input data\n", - "vch.linlin(0, 1, 1, 3).plot(\"r-\", label=\"linlin\")\n", - "vch.linlin(0.2, 0.7, -2, 2, \"minmix\").plot(\"g-\", label=\"linlin with clip\")\n", + "xs = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", + "xs.plot(\"k-\", label=\"input data\") # plot input data\n", + "xs.linlin(0, 1, 1, 3).plot(\"r-\", label=\"linlin\")\n", + "xs.linlin(0.2, 0.7, -2, 2, \"minmix\").plot(\"g-\", label=\"linlin with clip\")\n", "plt.legend(); plt.grid()" ] }, @@ -287,8 +290,8 @@ "outputs": [], "source": [ "# plot mapping function (output vs input)\n", - "plt.figure(figsize=(3,1.5)); plt.grid()\n", - "plt.plot(vch, vch.linlin(0.25, 0.75, -1, 1, \"minmax\"));" + "ys = xs.linlin(0.25, 0.75, -1, 1, \"minmax\")\n", + "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); " ] }, { @@ -298,7 +301,11 @@ "source": [] } ], - "metadata": {}, + "metadata": { + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } From f87bb01e8367be70ffbdb54593d942d10127de5b Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 27 Oct 2025 20:38:42 +0100 Subject: [PATCH 06/67] add linexp() example --- notebooks/pyamapping-examples.ipynb | 67 ++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index d4013ff..aa24f34 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -298,7 +298,72 @@ "cell_type": "markdown", "id": "19", "metadata": {}, - "source": [] + "source": [ + "### `linexp`\n", + "\n", + "- linexp is implemented in analogy to the SC3 linexp\n", + "- `linexp(v, x1, x2, y1, y2)` maps values v (scalar or arraylike) exponentially so that [x1, x2] is mapped to [y1, y2]:\n", + " $$ z = y_1 \\text{exp}\\left(\\frac{v - x_1}{x_2 - x_1}\\cdot (\\log(y_2) - \\log(y_1))\\right)\n", + "- note that this linexp function extrapolates by default\n", + "- clipping can be controlled via the clip argument (values None (default), \"min\", \"max\", or anything else for \"minmax\")\n", + "- A frequently used invocation is with $x_1 < x_2$, i.e. thinking of them as a range $[x_1, x_2]$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "pam.linexp(5, 1, 8, 2, 256)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "pam.linexp(7, 0, 5, 100, 300, \"max\") # clip result to maximum input range" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "- ChainableArray.linexp uses self as input.\n", + "- Here are some mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", + "xs.plot(\"k-\", label=\"input data\") # plot input data\n", + "xs.linexp(0, 1, 0.01, 1).plot(\"r-\", label=\"linlin\")\n", + "xs.linexp(0.2, 0.7, 2, 0.2, \"minmix\").plot(\"g-\", label=\"linlin with clip\")\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "# plot linexp mapping function (output vs input)\n", + "ys = xs.linexp(0.25, 0.85, 0.2, 2)\n", + "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); \n", + "plt.plot(xs, ys); " + ] } ], "metadata": { From 6b3637a5412b2daf6ef83a001ace959392cdcf5b Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 27 Oct 2025 21:44:45 +0100 Subject: [PATCH 07/67] refactor linlog to explin --- src/pyamapping/__init__.py | 4 ++-- src/pyamapping/mappings.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pyamapping/__init__.py b/src/pyamapping/__init__.py index e696760..e37069d 100644 --- a/src/pyamapping/__init__.py +++ b/src/pyamapping/__init__.py @@ -32,6 +32,7 @@ distort, ecdf, ecdf_to_lin, + explin, fermi, fold, gain, @@ -43,7 +44,6 @@ lincurve, linexp, linlin, - linlog, linpoly, linspace, mel_to_hz, @@ -73,6 +73,7 @@ "distort", "ecdf_to_lin", "ecdf", + "explin", "fermi", "fold", "gain", @@ -83,7 +84,6 @@ "lincurve", "linexp", "linlin", - "linlog", "linpoly", "linspace", "midi_to_cps", diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index d78575f..13995e5 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -108,7 +108,7 @@ def linexp( return np.minimum(np.maximum(z, y1), y2) -def linlog( +def explin( value: Union[float, ArrayLike], x1: float, x2: float, @@ -118,7 +118,7 @@ def linlog( ) -> Union[float, np.ndarray]: """Map value logarithmically so that [x1, x2] is mapped to [y1, y2]. - linlog is implemented in analogy to the SC3 linlog, yet this + explin is implemented in analogy to the SC3 explin, yet this function extrapolates by default. A frequently used invocation is with x1 < x2, i.e. thinking of them as a range [x1,x2] @@ -145,7 +145,8 @@ def linlog( float or np.ndarray the mapping result """ - z = np.log((value - x1) / (x2 - x1) * (np.exp(y2) - np.exp(y1)) + np.exp(y1)) + z = np.log(value / x1) / np.log(x2 / x1) * (y2 - y1) + y1 + if clip is None: return z if y1 > y2: @@ -1173,6 +1174,7 @@ def _list_numpy_ufuncs(): distort, ecdf_to_lin, ecdf, + explin, fermi, fold, gain, @@ -1184,7 +1186,6 @@ def _list_numpy_ufuncs(): lincurve, linexp, linlin, - linlog, linpoly, linspace, mel_to_hz, From 09851be3a3bac092875e5298fd8958f06572d875 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 27 Oct 2025 21:46:23 +0100 Subject: [PATCH 08/67] add explin() example --- notebooks/pyamapping-examples.ipynb | 82 +++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index aa24f34..f160020 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -79,7 +79,7 @@ "import pyamapping as pam\n", "from pyamapping import chain\n", "\n", - "mpl.rcParams['figure.figsize'] = (9,2.5)" + "mpl.rcParams['figure.figsize'] = (9, 2.5)" ] }, { @@ -347,8 +347,8 @@ "source": [ "xs = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", "xs.plot(\"k-\", label=\"input data\") # plot input data\n", - "xs.linexp(0, 1, 0.01, 1).plot(\"r-\", label=\"linlin\")\n", - "xs.linexp(0.2, 0.7, 2, 0.2, \"minmix\").plot(\"g-\", label=\"linlin with clip\")\n", + "xs.linexp(0, 1, 0.01, 1).plot(\"r-\", label=\"linexp\")\n", + "xs.linexp(0.2, 0.7, 2, 0.2, \"minmix\").plot(\"g-\", label=\"linexp with clip\")\n", "plt.legend(); plt.grid()" ] }, @@ -364,6 +364,82 @@ "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); \n", "plt.plot(xs, ys); " ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "### `explin`\n", + "\n", + "- explin is implemented in analogy to the SC3 function\n", + "- `explin(v, x1, x2, y1, y2)` maps values v (scalar or arraylike) logarithmically so that [x1, x2] is mapped to [y1, y2]:\n", + "\n", + " $$ y = y_1 + (y_2-y_1) \\frac{\\log(v / x_1)}{\\log(x_2 / x_1)} $$\n", + "\n", + "- note that this `explin` function extrapolates by default\n", + "- clipping can be controlled via the clip argument (values None (default), `\"min\"`, `\"max\"`, or anything else for `\"minmax\"`)\n", + "- A frequently used invocation is with $x_1 < x_2$, i.e. thinking of them as a range $[x_1, x_2]$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "# example: unmap a frequency to midi note with explin\n", + "f = 220 * 2**(-5/12) # 5 semitones higher than 220 Hz\n", + "pam.explin(f, 220, 440, 0, 12)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "# example: unmap amplitude to level in decibel\n", + "pam.explin(0.01, 0.001, 1.0, -30, 0, \"max\") # clip result to maximum input range" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "- ChainableArray.explin uses self as input.\n", + "- Here are some mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0.01, 1, 100)) # turn data into ChainableArray\n", + "xs.plot(\"k-\", label=\"input data\") # plot input data\n", + "xs.explin(0.1, 1, 0, 1).plot(\"r-\", label=\"explin\")\n", + "xs.explin(0.1, 0.5, 1, 0, \"minmix\").plot(\"g-\", label=\"explin with clip\")\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "# plot linexp mapping function (output vs input)\n", + "ys = xs.explin(0.01, 1, 0, 20)\n", + "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); \n", + "plt.plot(xs, ys); " + ] } ], "metadata": { From c0482aa4dccd599351f1b0b40a6fcb5aa0c6f7a1 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 14:43:14 +0100 Subject: [PATCH 09/67] improve lincurve mapping function --- src/pyamapping/mappings.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 13995e5..fb6bfd8 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -206,10 +206,9 @@ def lincurve( if abs(curve) < 0.001: z = (x - x1) / (x2 - x1) * (y2 - y1) + y1 else: - grow = np.exp(curve) - a = (y2 - y1) / (1.0 - grow) - b = y1 + a - z = b - a * grow ** ((x - x1) / (x2 - x1)) + z = y1 + (y2 - y1) / (1.0 - np.exp(curve)) * ( + 1 - np.exp((curve * (x - x1) / (x2 - x1))) + ) if y1 > y2: y1, y2 = y2, y1 From 30e7c9da1928132eb315ba1eee0c87beb739b09f Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 14:48:39 +0100 Subject: [PATCH 10/67] add lincurve example --- notebooks/pyamapping-examples.ipynb | 71 ++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index f160020..2c8b400 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -278,7 +278,7 @@ "xs = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", "xs.plot(\"k-\", label=\"input data\") # plot input data\n", "xs.linlin(0, 1, 1, 3).plot(\"r-\", label=\"linlin\")\n", - "xs.linlin(0.2, 0.7, -2, 2, \"minmix\").plot(\"g-\", label=\"linlin with clip\")\n", + "xs.linlin(0.2, 0.7, -2, 2, \"minmax\").plot(\"g-\", label=\"linlin with clip\")\n", "plt.legend(); plt.grid()" ] }, @@ -389,7 +389,7 @@ "metadata": {}, "outputs": [], "source": [ - "# example: unmap a frequency to midi note with explin\n", + "# example: unmap a frequency to MIDI note with explin\n", "f = 220 * 2**(-5/12) # 5 semitones higher than 220 Hz\n", "pam.explin(f, 220, 440, 0, 12)\n" ] @@ -440,13 +440,70 @@ "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); \n", "plt.plot(xs, ys); " ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "### `lincurve`\n", + "\n", + "- lincurve is implemented in analogy to the SC3 function\n", + "- `lincurve(v, x1, x2, y1, y2, curve=-2)` maps v (scalar or arraylike) from [x1, x2] to [y1, y2] using the following function (with c=curve): \n", + "\n", + "$$ y_1 + \\frac{y_2 - y_1}{1.0 - e^c} \\left(1 - \\exp\\left(c \\frac{v - x_1}{x_2 - x_1}\\right) \\right) $$\n", + "- in contrast to `explin` (resp. `linexp`) this allows source (resp. target) range to include 0.\n", + "- note that this `lincurve` function extrapolates by default\n", + "- clipping can be controlled via the clip argument (values None (default), `\"min\"`, `\"max\"`, or anything else for `\"minmax\"`)\n", + "- A frequently used invocation is with $x_1 < x_2$, i.e. thinking of them as a range $[x_1, x_2]$" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "- ChainableArray.lincurve uses self as input.\n", + "- Here are some mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", + "xs.plot(\"k-\", label=\"input data\") # plot input data\n", + "xs.lincurve(0, 1, 0, 0.4, 5).plot(\"r-\", label=\"lincurve\")\n", + "xs.lincurve(0.2, 0.5, 1, 0, 2.5, \"minmax\").plot(\"g-\", label=\"lincurve with clip\")\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "the following plot shows how the curve parameter influences the mapping function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 100, 100))\n", + "for i, curve in enumerate(range(-9, 10, 3)):\n", + " xs.lincurve(0, 100, -10, 10, curve).plot('-', label=f\"curve={curve}\")\n", + "plt.legend(fontsize=6); plt.grid();" + ] } ], - "metadata": { - "language_info": { - "name": "python" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } From 5b9306e79d6ab0bb7aeedeb0102f23521269a82c Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 15:34:21 +0100 Subject: [PATCH 11/67] improve curvelin mapping function --- src/pyamapping/mappings.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index fb6bfd8..b46fd56 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -267,10 +267,8 @@ def curvelin( if abs(curve) < 0.001: z = (x - x1) / (x2 - x1) * (y2 - y1) + y1 else: - grow = np.exp(curve) - a = (x2 - x1) / (1.0 - grow) - b = x1 + a - z = np.log((b - x) / a) * (y2 - y1) / curve + y1 + a = (x2 - x1) / (1.0 - np.exp(curve)) + z = np.log((x1 + a - x) / a) * (y2 - y1) / curve + y1 if y1 > y2: y1, y2 = y2, y1 @@ -1202,7 +1200,7 @@ def _list_numpy_ufuncs(): ] for fn in pyamapping_functions: - register_chain_fn(fn, name) + register_chain_fn(fn, None) def chain(input_array: ArrayLike) -> ChainableArray: From 5f24f5e35d71526be8579b73b4806078024d246d Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 15:35:24 +0100 Subject: [PATCH 12/67] add curvelin() example --- notebooks/pyamapping-examples.ipynb | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 2c8b400..8087068 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -501,6 +501,69 @@ " xs.lincurve(0, 100, -10, 10, curve).plot('-', label=f\"curve={curve}\")\n", "plt.legend(fontsize=6); plt.grid();" ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### `curvelin`\n", + "\n", + "- curvelin is implemented in analogy to the SC3 function\n", + "- `curvelin(v, x1, x2, y1, y2, curve=-2)` maps v (scalar or arraylike) from an assumed curve-exponential input range [x1, x2] to a linear output range [y1, y2] using the following function (with c=curve): \n", + "$$ f(x) = y_1 + \\frac{y_2 - y_1}{c}\\log\\left(\\frac{a + x_1 - x}{a}\\right) ~~~\\text{with}~~~ a = \\frac{x_2 - x_1}{1 - e^c}$$\n", + "\n", + "- This is the opposite transformation to `lincurve`.\n", + "- note that this `curvelin` function extrapolates by default.\n", + "- clipping can be controlled via the clip argument (values None (default), `\"min\"`, `\"max\"`, or anything else for minmax clipping.\n", + "- A frequently used invocation is with $x_1 < x_2$, i.e. thinking of them as a range $[x_1, x_2]$" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "- ChainableArray.curvelin uses self as input.\n", + "- Here are some mapping examples and plots\n", + "- The first shows how curvelin unmaps or reverts a lincurve warped interval when using the same curve argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", + "xs.plot(\"k-\", label=\"input data for lincurve\") # plot input data\n", + "curve = 10\n", + "ys = xs.lincurve(0, 1, 0, 0.6, curve).plot(\"r-\", label=\"lincurve output\")\n", + "xs = ys.curvelin(0, 0.6, 0, 1, curve).plot(\"b-.\", lw=3, alpha=0.5, label=\"curvelin to undo lincurve\")\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "markdown", + "id": "39", + "metadata": {}, + "source": [ + "the following plot shows how the curve parameter influences the mapping function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 100, 500))\n", + "for i, curve in enumerate(range(-9, 10, 3)):\n", + " xs.curvelin(0, 100, -10, 10, curve).plot('-', label=f\"curve={curve}\")\n", + "plt.legend(fontsize=6); plt.grid();" + ] } ], "metadata": {}, From 241b14c274e3a548ccd67567bf00c0cadcb70dc8 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 16:18:01 +0100 Subject: [PATCH 13/67] add linpoly() example --- notebooks/pyamapping-examples.ipynb | 73 ++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 8087068..68f4d62 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -564,9 +564,80 @@ " xs.curvelin(0, 100, -10, 10, curve).plot('-', label=f\"curve={curve}\")\n", "plt.legend(fontsize=6); plt.grid();" ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "### `linpoly`\n", + "\n", + "- linpoly has no corresponding SC3 function, it provides a polynomial mapping\n", + "- `linpoly(v, xmax, y1, y2, curve=2, clip)` maps v (scalar or arraylike) from an assumed linear input range [-xmax, xmax] to an output range [y1, y2] using the following polynomial mapping function, using a polynomial order m\n", + "\n", + "$$ m = \\left\\{\\begin{align*} 1 + \\text{curve} & ~~~\\text{if} & \\text{curve} \\ge 0\\\\\n", + " \\frac{1}{1-\\text{curve}} & ~~~\\text{else} & \n", + " \\end{align*} \\right.\n", + "$$\n", + "using the mapping function\n", + "$$ f(x) = y_1 + \\frac{y_2 - y_1}{2} \\cdot \\left(1 + \\text{sign}(x) \\left|\\frac{x}{x_{\\max}}\\right|^m\\right)$$\n", + "\n", + "- note that np.sign is used\n", + "- note that this `linpoly` function extrapolates by default.\n", + "- clipping can be controlled via the clip argument (values: None as default, `\"min\"`, `\"max\"`, or anything else for minmax clipping.)\n", + "- It can be used to provide a sensitivity magnification (or reduction) around 0, the center of the input interval." + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "- ChainableArray.linpoly uses self as input.\n", + "- Here are some mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(-3, 3, 200)) # turn data into ChainableArray\n", + "xs.plot(\"k-\", label=\"input data for linpoly\") # plot input data\n", + "xs.linpoly(3, 0, 20, curve=1).plot(\"r-\", label=\"linpoly output\")\n", + "xs.linpoly(3, 0, 20, curve=-1).plot(\"b-\", label=\"linpoly output\")\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "the following plot shows how the curve parameter influences the mapping function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(-1, 1, 200))\n", + "for i, curve in enumerate(range(-3, 3, 1)):\n", + " xs.linpoly(1, -10, 10, curve).plot('-', label=f\"curve={curve}\")\n", + "plt.legend(fontsize=6); plt.grid();" + ] } ], - "metadata": {}, + "metadata": { + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } From 59aa1fce96bca167b640215de19d38f43139b124 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 16:38:09 +0100 Subject: [PATCH 14/67] improve structure and explanations --- notebooks/pyamapping-examples.ipynb | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 68f4d62..58c7734 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -221,7 +221,9 @@ "source": [ "## Tour of Mapping Functions: Functions from SuperCollider3 and librosa\n", "\n", - "For the following functions, we use `xs` to refer to the input array and `ys` for the outputs." + "- The following function have been implemented in analogy to their versions in Supercollider3 resp. librosa.\n", + "- Please note that some defaults may be different, e.g. functions extrapolate by default\n", + "- For the following demonstrations, we use `xs` to refer to the input array and `ys` for the outputs." ] }, { @@ -569,6 +571,17 @@ "cell_type": "markdown", "id": "41", "metadata": {}, + "source": [ + "## Tour of Mapping Functions: additional/novel mapping functions\n", + "\n", + "- The following function are new additions (i.e. there is no counterpart in Supercollider3 resp. librosa).\n", + "- For the following demonstrations, we use `xs` to refer to the input array and `ys` for the outputs." + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, "source": [ "### `linpoly`\n", "\n", @@ -590,7 +603,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "43", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -600,7 +613,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -613,7 +626,7 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "45", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -622,7 +635,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -633,11 +646,7 @@ ] } ], - "metadata": { - "language_info": { - "name": "python" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } From a433b15f0e7e24812bd7398f5215cee2a2ad3802 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 17:13:54 +0100 Subject: [PATCH 15/67] forward kwargs for interp_spline() --- src/pyamapping/mappings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index b46fd56..2250773 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -399,6 +399,7 @@ def bilin( ycenter: float = 0, ymin: float = -1, ymax: float = 1, + **kwargs, ) -> Union[float, np.ndarray]: """Bilin compatibility function. implements sc3 bilin function. @@ -419,7 +420,7 @@ def bilin( Union[float, np.ndarray], the mapping result """ - return interp_spline(x, [xmin, xcenter, xmax], [ymin, ycenter, ymax]) + return interp_spline(x, [xmin, xcenter, xmax], [ymin, ycenter, ymax], **kwargs) def clip( From 02cbbcb79b2c3c17c1bb2bdc5b62bd3a2a1d2dc2 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 17:17:56 +0100 Subject: [PATCH 16/67] add bilin() example --- notebooks/pyamapping-examples.ipynb | 58 ++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 58c7734..10d7642 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -571,6 +571,48 @@ "cell_type": "markdown", "id": "41", "metadata": {}, + "source": [ + "### `bilin`\n", + "\n", + "- bilin is implemented similar to the SC3 function, yet with an API change:\n", + "- `bilin(v, xcenter, xmin, xmax, ycenter, ymin, ymax)` maps v (scalar or arraylike) \n", + " according to two linear segments:\n", + " - [xmin, xcenter] to [ymin, ycenter] with default extrapolation beyond xmin\n", + " - [xcenter, xmax] to [ycenter, ymax] with default extrapolation beyond xmax\n", + "- this mapping is achieved using `pyamapping.interp_spline()`.\n", + "- kwargs are passed on to `interp_spline()` if needed.\n", + "- in case, no extrapolation is wanted, pyampapping.interp() offers an alternative\n", + " with a different interface." + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "- ChainableArray.bilin uses self as input.\n", + "- Here are some mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(0, 100)) # turn data into ChainableArray\n", + "xs.bilin(60, 20, 80, 0, -20, 60).plot(\"b.\", ms=1, label='bilin');\n", + "for (x, y, t) in [[20, -20, \"(xmin, ymin)\"], [80, 60, \"(xmax, ymax)\"], [60, 0, \"(xcenter, ycenter)\"]]:\n", + " plt.plot([x], [y], \"ro\")\n", + " plt.text(x+1, y-5, t, fontsize=6)\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -580,7 +622,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "45", "metadata": {}, "source": [ "### `linpoly`\n", @@ -603,7 +645,7 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "46", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -613,7 +655,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "47", "metadata": {}, "outputs": [], "source": [ @@ -626,7 +668,7 @@ }, { "cell_type": "markdown", - "id": "45", + "id": "48", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -635,7 +677,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "49", "metadata": {}, "outputs": [], "source": [ @@ -646,7 +688,11 @@ ] } ], - "metadata": {}, + "metadata": { + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } From 9592b2d60836f47e605e5fe92727393f3936c6cb Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 17:43:24 +0100 Subject: [PATCH 17/67] add interp_spline() example --- notebooks/pyamapping-examples.ipynb | 39 +++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 10d7642..28f6409 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -686,13 +686,42 @@ " xs.linpoly(1, -10, 10, curve).plot('-', label=f\"curve={curve}\")\n", "plt.legend(fontsize=6); plt.grid();" ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "### `interp_spline`\n", + "\n", + "- interp_spline provides an interface to `scipy.interpolate.make_interp_spline` for line segment interpolation.\n", + "- `interp_spline(v, xc, yc, k)` maps v (scalar or arraylike) along the spline \n", + " - defined by the input coordinates in array xc \n", + " - and corresponding output coordinates in yc\n", + " - using interpolation order k (default 1=linear)\n", + "- note that interp_spline extrapolation beyond segments.\n", + "- interp_spline is called from `bilin()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 8.2, 1000)) # turn data into ChainableArray\n", + "xc = [1, 2, 5, 7, 8]\n", + "yc = [3, 1, 2, 9, 1]\n", + "plt.plot(xc, yc, \"ro\", label=\"given points\")\n", + "for k in [0, 1, 2]:\n", + " ys = xs.interp_spline(xc, yc, k=k)\n", + " plt.plot(xs,ys, label=f\"spline with k={k}\")\n", + "plt.legend(fontsize=7); plt.grid()" + ] } ], - "metadata": { - "language_info": { - "name": "python" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } From 19d394255ad3510543466502e07766ca8d89844b Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 30 Oct 2025 18:01:26 +0100 Subject: [PATCH 18/67] add interp() example --- notebooks/pyamapping-examples.ipynb | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 28f6409..af0ffe4 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -719,6 +719,52 @@ " plt.plot(xs,ys, label=f\"spline with k={k}\")\n", "plt.legend(fontsize=7); plt.grid()" ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "### `interp`\n", + "\n", + "- interp provides an interface to `np.interp` for line segment interpolation.\n", + "- `interp(v, xc, yc)` maps v (scalar or arraylike) along the sample points \n", + " - defined by the input coordinates in array xc (monotonically increasing)\n", + " - and corresponding output coordinates in yc\n", + "- note that interp clips beyond xc limits.\n", + "- interp can be used as alternative to bilin in case clipped output is needed.\n", + " - some may prefer the more tidy API with x and y values in their arrays." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 8.2, 1000)) # turn data into ChainableArray\n", + "xc = [1, 2, 5, 7, 8]\n", + "yc = [7, 1, 2, 9, 1]\n", + "plt.plot(xc, yc, \"ro\", ms=3, label=\"given points\")\n", + "ys = xs.interp(xc, yc)\n", + "plt.plot(xs, ys, \"b,\", label=f\"interp\")\n", + "plt.legend(fontsize=7); plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-100, 150)) # turn data into ChainableArray\n", + "# use interp if clipping is wanted (faster for small datasets)\n", + "xs.interp([-50, 50, 100],[0, 0.4, 1]).plot(\"r-.\", label=\"interp with clipping\"); \n", + "xs.bilin(50, -50, 100, 0.4, 0, 1).plot(\"b:\", label='bilin with extrapolation');\n", + "plt.legend();plt.grid();" + ] } ], "metadata": {}, From 1d058a28bab60e06ba911b44e9dd678634d18e98 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 17:34:49 +0100 Subject: [PATCH 19/67] add clip() example --- notebooks/pyamapping-examples.ipynb | 66 ++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index af0ffe4..66c94a3 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -613,6 +613,52 @@ "cell_type": "markdown", "id": "44", "metadata": {}, + "source": [ + "### `clip`\n", + "\n", + "- clip is implemented in analogy to the SC3 clip\n", + "- `clip(value, minimum, maximum)` clips value (scalar or arraylike) to a certain range [minimum, maximum]\n", + "- default values for minium and maximum are so that no clipping occurs, i.e. specifying only minimum or maximum allows one-sided clipping." + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, + "source": [ + "- ChainableArray.clip uses self as input.\n", + "- Here are some mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.linspace(0, 1, 100)) # turn data into ChainableArray\n", + "xs.plot(\"k:\", label=\"input data\") # plot input data\n", + "xs.clip(0.2, 0.7).plot(\"r-\", label=\"linlin with clip\")\n", + "plt.legend(); plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "# plot mapping function (output vs input)\n", + "ys = xs.clip(0.25, 0.75)\n", + "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys, label=''); plt.grid(); " + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -622,7 +668,7 @@ }, { "cell_type": "markdown", - "id": "45", + "id": "49", "metadata": {}, "source": [ "### `linpoly`\n", @@ -645,7 +691,7 @@ }, { "cell_type": "markdown", - "id": "46", + "id": "50", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -655,7 +701,7 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "51", "metadata": {}, "outputs": [], "source": [ @@ -668,7 +714,7 @@ }, { "cell_type": "markdown", - "id": "48", + "id": "52", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -677,7 +723,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "53", "metadata": {}, "outputs": [], "source": [ @@ -689,7 +735,7 @@ }, { "cell_type": "markdown", - "id": "50", + "id": "54", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -706,7 +752,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "55", "metadata": {}, "outputs": [], "source": [ @@ -722,7 +768,7 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "56", "metadata": {}, "source": [ "### `interp`\n", @@ -739,7 +785,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "57", "metadata": {}, "outputs": [], "source": [ @@ -755,7 +801,7 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "58", "metadata": {}, "outputs": [], "source": [ From ccb5286dcb4bafaa6f27154ea70ae982730e0b61 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 17:55:34 +0100 Subject: [PATCH 20/67] add method shortcuts for midicps and cpsmidi --- src/pyamapping/mappings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 2250773..4691272 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -1203,6 +1203,9 @@ def _list_numpy_ufuncs(): for fn in pyamapping_functions: register_chain_fn(fn, None) +register_chain_fn(cpsmidi, "cpsmidi") +register_chain_fn(midicps, "midicps") + def chain(input_array: ArrayLike) -> ChainableArray: """Turn np.ndarray into ChainableArray.""" From 131c5c38e732bf892a4e969f634ffa9901f33d6e Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 17:56:34 +0100 Subject: [PATCH 21/67] add example for midi_to_cps() --- notebooks/pyamapping-examples.ipynb | 77 +++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 66c94a3..c33d339 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -659,6 +659,63 @@ "cell_type": "markdown", "id": "48", "metadata": {}, + "source": [ + "### `midi_to_cps`\n", + "\n", + "- midi_to_cps is implemented in analogy to the SC3 midicps function\n", + "- the shorter (less pythonic name) midicps can be used as well\n", + "- `midi_to_cps(midi_note)` converts MIDI note midi_note (value or arraylike) to cycles per second (aka Hz).\n", + "- The mapping function is\n", + " $$ f(x) = 440\\cdot 2^{\\frac{x-69}{12}} $$\n", + " which obviously maps MIDI 69 to 440 Hz, the reference for definition. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "pam.midi_to_cps(69+12) # should be 880, i.e. one octave above MIDI 69" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "chain(np.arange(60, 74, 2)).midicps().round(1) # rounded frequencies of full tone scale ('c-d-e-f#-g#-a#-c')" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "- ChainableArray.midi_to_cps uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(21, 109)) # the standard MIDI range\n", + "xs.plot(\"k-\", label=\"input data (MIDI notes)\") # plot input data\n", + "xs.midi_to_cps().plot(\"r-\", label=\"midi_to_cps result [in Hz]\")\n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -668,7 +725,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "54", "metadata": {}, "source": [ "### `linpoly`\n", @@ -691,7 +748,7 @@ }, { "cell_type": "markdown", - "id": "50", + "id": "55", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -701,7 +758,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "56", "metadata": {}, "outputs": [], "source": [ @@ -714,7 +771,7 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "57", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -723,7 +780,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "58", "metadata": {}, "outputs": [], "source": [ @@ -735,7 +792,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "59", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -752,7 +809,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "60", "metadata": {}, "outputs": [], "source": [ @@ -768,7 +825,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "61", "metadata": {}, "source": [ "### `interp`\n", @@ -785,7 +842,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "62", "metadata": {}, "outputs": [], "source": [ @@ -801,7 +858,7 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "63", "metadata": {}, "outputs": [], "source": [ From 43fb2242e579d586d1b8c19c7fe0a65377963c13 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 18:10:15 +0100 Subject: [PATCH 22/67] add example for cps_to_midi() --- notebooks/pyamapping-examples.ipynb | 79 +++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index c33d339..2891fd7 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -716,6 +716,65 @@ "cell_type": "markdown", "id": "53", "metadata": {}, + "source": [ + "### `cps_to_midi`\n", + "\n", + "- cps_to_midi is implemented in analogy to the SC3 cpsmidi function\n", + "- the shorter (less pythonic name) cpsmidi can be used as well\n", + "- `cps_to_midi(cps)` converts a frequency cps in Hz (value or arraylike) to a MIDI note (in float, resp. Arraylike of float).\n", + "- The mapping function is\n", + " $$ f(x) = 69 + 12 \\log_2\\left(\\frac{x}{440}\\right) $$\n", + " which obviously maps 440 Hz to MIDI 69, the reference by definition. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "pam.cps_to_midi(440*2) # should be 81=69+12" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "chain([110, 220, 330, 440, 550, 660, 770, 880]).cpsmidi().round(2) # rounded MIDI notes for the harmonics series over low A" + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "- ChainableArray.midi_to_cps uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(100, 1000, 100)) # frequencies with 50 Hz spacing\n", + "ys = xs.cps_to_midi() \n", + "\n", + "plt.plot(xs, ys, \"o-\", label=\"MIDI note for harmonics series\"); \n", + "plt.xlabel(\"frequency [Hz]\"); plt.ylabel(\"MIDI note\"); \n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -725,7 +784,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "59", "metadata": {}, "source": [ "### `linpoly`\n", @@ -748,7 +807,7 @@ }, { "cell_type": "markdown", - "id": "55", + "id": "60", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -758,7 +817,7 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -771,7 +830,7 @@ }, { "cell_type": "markdown", - "id": "57", + "id": "62", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -780,7 +839,7 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -792,7 +851,7 @@ }, { "cell_type": "markdown", - "id": "59", + "id": "64", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -809,7 +868,7 @@ { "cell_type": "code", "execution_count": null, - "id": "60", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -825,7 +884,7 @@ }, { "cell_type": "markdown", - "id": "61", + "id": "66", "metadata": {}, "source": [ "### `interp`\n", @@ -842,7 +901,7 @@ { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "67", "metadata": {}, "outputs": [], "source": [ @@ -858,7 +917,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "68", "metadata": {}, "outputs": [], "source": [ From 178edbdd7bca1727b3398784f1721d7bf77c6605 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 22:16:20 +0100 Subject: [PATCH 23/67] add chain fn shortcuts midiratio and ratiomidi --- src/pyamapping/mappings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 4691272..4fc02de 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -1205,6 +1205,8 @@ def _list_numpy_ufuncs(): register_chain_fn(cpsmidi, "cpsmidi") register_chain_fn(midicps, "midicps") +register_chain_fn(ratiomidi, "ratiomidi") +register_chain_fn(midiratio, "midiratio") def chain(input_array: ArrayLike) -> ChainableArray: From f57e2c6a7e937e1942c75922fa1d4401af63db4e Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 22:18:22 +0100 Subject: [PATCH 24/67] add midi_to_ratio() example --- notebooks/pyamapping-examples.ipynb | 80 +++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 2891fd7..76f3b7b 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -687,7 +687,7 @@ "metadata": {}, "outputs": [], "source": [ - "chain(np.arange(60, 74, 2)).midicps().round(1) # rounded frequencies of full tone scale ('c-d-e-f#-g#-a#-c')" + "chain(np.arange(60, 74, 2)).midicps().round(1) # rounded frequencies of whole-tone scale ('c-d-e-f#-g#-a#-c')" ] }, { @@ -775,6 +775,64 @@ "cell_type": "markdown", "id": "58", "metadata": {}, + "source": [ + "### `midi_to_ratio`\n", + "\n", + "- midi_to_ratio is implemented in analogy to the SC3 midiratio function\n", + "- the shorter (less pythonic name) midiratio can be used as well\n", + "- `midi_to_ratio(midi_note)` converts MIDI note difference midi_note (value or arraylike) to the ratio of their corresponding frequencies.\n", + "- The mapping function is\n", + " $$ f(x) = 2^{(x/\\tiny 12)} $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "pam.midi_to_ratio(7) # a fifth is ~3/2 (in equal tuning)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60", + "metadata": {}, + "outputs": [], + "source": [ + "chain(np.arange(0, 12, 2)).midi_to_ratio().round(2) # ratio of tones in the whole-tone scale ('c-d-e-f#-g#-a#-c')" + ] + }, + { + "cell_type": "markdown", + "id": "61", + "metadata": {}, + "source": [ + "- ChainableArray.midi_to_cps uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-12, 13)) # one octave around any note\n", + "ys = xs.midi_to_ratio()\n", + "\n", + "plt.plot(xs, ys, \"r-\", label=\"midi_to_ratio result\")\n", + "plt.plot(0, 1, \"ro\", label=\"midi_ratio of 0\")\n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -784,7 +842,7 @@ }, { "cell_type": "markdown", - "id": "59", + "id": "64", "metadata": {}, "source": [ "### `linpoly`\n", @@ -807,7 +865,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "65", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -817,7 +875,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "66", "metadata": {}, "outputs": [], "source": [ @@ -830,7 +888,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "67", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -839,7 +897,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "68", "metadata": {}, "outputs": [], "source": [ @@ -851,7 +909,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "69", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -868,7 +926,7 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -884,7 +942,7 @@ }, { "cell_type": "markdown", - "id": "66", + "id": "71", "metadata": {}, "source": [ "### `interp`\n", @@ -901,7 +959,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "72", "metadata": {}, "outputs": [], "source": [ @@ -917,7 +975,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "73", "metadata": {}, "outputs": [], "source": [ From e8677efcb394e4e4eb0325b78f90f2341d9e58c9 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 22:26:26 +0100 Subject: [PATCH 25/67] add ratio_to_midi() example --- notebooks/pyamapping-examples.ipynb | 80 +++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 76f3b7b..e51c681 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -752,7 +752,7 @@ "id": "56", "metadata": {}, "source": [ - "- ChainableArray.midi_to_cps uses self as input.\n", + "- ChainableArray.cps_to_midi uses self as input.\n", "- Here a mapping examples and plots" ] }, @@ -833,6 +833,64 @@ "cell_type": "markdown", "id": "63", "metadata": {}, + "source": [ + "### `ratio_to_midi`\n", + "\n", + "- radio_to_midi is implemented in analogy to the SC3 ratiomidi function\n", + "- the shorter (less pythonic name) ratiomidi can be used as well\n", + "- `ratio_to_midi(ratio)` converts a frequency ratio (value or arraylike) to a MIDI note difference (in float, resp. Arraylike of float).\n", + "- The mapping function is\n", + " $$ f(x) = 12 \\log_2(x) $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "pam.ratio_to_midi(2) # should be 12 for ratio=2 (i.e. an octave)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65", + "metadata": {}, + "outputs": [], + "source": [ + "chain(np.arange(1, 10)).ratio_to_midi().round(2) # rounded MIDI offsets for the harmonics series" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "- ChainableArray.ratio_to_midi uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(1, 10)) # integer frequency ratios / harmonics\n", + "ys = xs.ratio_to_midi() # likewise ratiomidi()\n", + "\n", + "plt.plot(xs, ys, \"o-\", label=\"MIDI offsets for harmonics series\"); \n", + "plt.xlabel(\"ratio to fundamental frequency\"); plt.ylabel(\"MIDI offset\"); \n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -842,7 +900,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "69", "metadata": {}, "source": [ "### `linpoly`\n", @@ -865,7 +923,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "70", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -875,7 +933,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -888,7 +946,7 @@ }, { "cell_type": "markdown", - "id": "67", + "id": "72", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -897,7 +955,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "73", "metadata": {}, "outputs": [], "source": [ @@ -909,7 +967,7 @@ }, { "cell_type": "markdown", - "id": "69", + "id": "74", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -926,7 +984,7 @@ { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "75", "metadata": {}, "outputs": [], "source": [ @@ -942,7 +1000,7 @@ }, { "cell_type": "markdown", - "id": "71", + "id": "76", "metadata": {}, "source": [ "### `interp`\n", @@ -959,7 +1017,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "77", "metadata": {}, "outputs": [], "source": [ @@ -975,7 +1033,7 @@ { "cell_type": "code", "execution_count": null, - "id": "73", + "id": "78", "metadata": {}, "outputs": [], "source": [ From 9461c7e7a6398fe593290eea84d0cf92905436dd Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 22:48:41 +0100 Subject: [PATCH 26/67] register chain fn for cpsoct and octcps --- src/pyamapping/mappings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 4fc02de..2e2bf99 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -1207,6 +1207,8 @@ def _list_numpy_ufuncs(): register_chain_fn(midicps, "midicps") register_chain_fn(ratiomidi, "ratiomidi") register_chain_fn(midiratio, "midiratio") +register_chain_fn(cpsoct, "cpsoct") +register_chain_fn(octcps, "octcps") def chain(input_array: ArrayLike) -> ChainableArray: From 348504b1ba723f748c0074c36ef4ad620d95615d Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 22:49:46 +0100 Subject: [PATCH 27/67] add octave_to_cps() and cps_to_octave() examples --- notebooks/pyamapping-examples.ipynb | 134 +++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 10 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index e51c681..00b2996 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -891,6 +891,120 @@ "cell_type": "markdown", "id": "68", "metadata": {}, + "source": [ + "### `octave_to_cps`\n", + "\n", + "- octave_to_cps is implemented in analogy to the SC3 octcps function\n", + "- the shorter (less pythonic name) octcps can be used as well\n", + "- `octave_to_cps(octave)` converts octaves (value or arraylike) to cycles per second (aka Hz).\n", + "- The mapping function is\n", + " $$ f(x) = 440\\cdot 2^{(x - 4.75)} $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": {}, + "outputs": [], + "source": [ + "pam.octave_to_cps(3.75) # one octave below 440" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70", + "metadata": {}, + "outputs": [], + "source": [ + "chain(np.arange(2, 9)).octcps().round(1) # rounded frequencies of c-tones" + ] + }, + { + "cell_type": "markdown", + "id": "71", + "metadata": {}, + "source": [ + "- ChainableArray.octave_to_cps uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(0, 8, 1/4)) # 8 octaves in minor thirds (3 semitone steps)\n", + "ys = xs.octave_to_cps()\n", + "plt.plot(xs, ys, \"ro-\", ms=2,label=\"octave_to_cps result [in Hz]\")\n", + "plt.legend(); plt.grid(); plt.semilogy()" + ] + }, + { + "cell_type": "markdown", + "id": "73", + "metadata": {}, + "source": [ + "### `cps_to_octave`\n", + "\n", + "- cps_to_octave is implemented in analogy to the SC3 cpsoct function\n", + "- the shorter (less pythonic name) cpsoct can be used as well\n", + "- `cps_to_oct(cps)` converts a frequency cps in Hz (value or arraylike) to an octave value (in float, resp. Arraylike of float).\n", + "- The mapping function is\n", + " $$ f(x) = 4.75 + \\log_2\\left(\\frac{x}{440}\\right) $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74", + "metadata": {}, + "outputs": [], + "source": [ + "pam.cps_to_octave(440*2) # one octave above reference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": {}, + "outputs": [], + "source": [ + "chain(pam.midicps(12) * np.arange(1, 10)).cpsoct().round(2) # harmonics series over low C in octaves" + ] + }, + { + "cell_type": "markdown", + "id": "76", + "metadata": {}, + "source": [ + "- ChainableArray.cps_to_octave uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(100, 1000, 100)) # frequencies with 50 Hz spacing\n", + "ys = xs.cps_to_octave() \n", + "\n", + "plt.plot(xs, ys, \"o-\", label=\"octaves for harmonics series\"); \n", + "plt.xlabel(\"frequency [Hz]\"); plt.ylabel(\"octave\"); \n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "78", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -900,7 +1014,7 @@ }, { "cell_type": "markdown", - "id": "69", + "id": "79", "metadata": {}, "source": [ "### `linpoly`\n", @@ -923,7 +1037,7 @@ }, { "cell_type": "markdown", - "id": "70", + "id": "80", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -933,7 +1047,7 @@ { "cell_type": "code", "execution_count": null, - "id": "71", + "id": "81", "metadata": {}, "outputs": [], "source": [ @@ -946,7 +1060,7 @@ }, { "cell_type": "markdown", - "id": "72", + "id": "82", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -955,7 +1069,7 @@ { "cell_type": "code", "execution_count": null, - "id": "73", + "id": "83", "metadata": {}, "outputs": [], "source": [ @@ -967,7 +1081,7 @@ }, { "cell_type": "markdown", - "id": "74", + "id": "84", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -984,7 +1098,7 @@ { "cell_type": "code", "execution_count": null, - "id": "75", + "id": "85", "metadata": {}, "outputs": [], "source": [ @@ -1000,7 +1114,7 @@ }, { "cell_type": "markdown", - "id": "76", + "id": "86", "metadata": {}, "source": [ "### `interp`\n", @@ -1017,7 +1131,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "87", "metadata": {}, "outputs": [], "source": [ @@ -1033,7 +1147,7 @@ { "cell_type": "code", "execution_count": null, - "id": "78", + "id": "88", "metadata": {}, "outputs": [], "source": [ From 499ef14755c57bffd30b698c7762e2039599c24a Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 23:07:44 +0100 Subject: [PATCH 28/67] register chain fn for ampdb and dbamp --- src/pyamapping/mappings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 2e2bf99..f868ba1 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -1209,6 +1209,8 @@ def _list_numpy_ufuncs(): register_chain_fn(midiratio, "midiratio") register_chain_fn(cpsoct, "cpsoct") register_chain_fn(octcps, "octcps") +register_chain_fn(ampdb, "ampdb") +register_chain_fn(dbamp, "dbamp") def chain(input_array: ArrayLike) -> ChainableArray: From de86068d0ea1c448825a7363119c656404026592 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Fri, 31 Oct 2025 23:08:13 +0100 Subject: [PATCH 29/67] add db_to_amp() and amp_to_db() examples --- notebooks/pyamapping-examples.ipynb | 123 +++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 10 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 00b2996..58c614e 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1005,6 +1005,109 @@ "cell_type": "markdown", "id": "78", "metadata": {}, + "source": [ + "### `db_to_amp`\n", + "\n", + "- db_to_amp is implemented in analogy to the SC3 dbamp function\n", + "- the shorter (less pythonic name) dbamp can be used as well\n", + "- `db_to_amp(decibels)` converts decibels (value or arraylike) to amplitudes.\n", + "- The mapping function is\n", + " $$ f(x) = 10^{(x/20)}$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79", + "metadata": {}, + "outputs": [], + "source": [ + "pam.db_to_amp(np.array([-6, -12, -20]))" + ] + }, + { + "cell_type": "markdown", + "id": "80", + "metadata": {}, + "source": [ + "- ChainableArray.db_to_amp uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-60, 0, 6)) # some 6 dB steps\n", + "ys = xs.dbamp()\n", + "plt.plot(xs, ys, \"ro-\", ms=2,label=\"db_to_amp result [arb. unit]\")\n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "82", + "metadata": {}, + "source": [ + "### `amp_to_db`\n", + "\n", + "- amp_to_db is implemented in analogy to the SC3 ampdb function\n", + "- the shorter (less pythonic name) ampdb can be used as well\n", + "- `amp_to_db(amp)` converts amplitude(s) amp (in arb. units) (value or arraylike) to decibel values (in float, resp. Arraylike of float).\n", + "- The mapping function is\n", + " $$ f(x) = 20 * \\log_{10}(x) $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83", + "metadata": {}, + "outputs": [], + "source": [ + "pam.amp_to_db(0.01) # 10^(-2) will be 10^(-4) for energy, aka -40 dB" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84", + "metadata": {}, + "outputs": [], + "source": [ + "chain(np.arange(0.1, 1, 0.1)).ampdb().round(2) # harmonics series over low C in octaves" + ] + }, + { + "cell_type": "markdown", + "id": "85", + "metadata": {}, + "source": [ + "- ChainableArray.amp_to_db uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(0.1, 2.1, 0.1)) # ampltitudes from 0.1 to 2.0\n", + "ys = xs.ampdb() # likewise amp_to_db()\n", + "plt.plot(xs, ys, \"o-\", label=\"decibel\"); \n", + "plt.xlabel(\"amplitudes\"); plt.ylabel(\"dB\"); \n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "87", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1014,7 +1117,7 @@ }, { "cell_type": "markdown", - "id": "79", + "id": "88", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1037,7 +1140,7 @@ }, { "cell_type": "markdown", - "id": "80", + "id": "89", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1047,7 +1150,7 @@ { "cell_type": "code", "execution_count": null, - "id": "81", + "id": "90", "metadata": {}, "outputs": [], "source": [ @@ -1060,7 +1163,7 @@ }, { "cell_type": "markdown", - "id": "82", + "id": "91", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1069,7 +1172,7 @@ { "cell_type": "code", "execution_count": null, - "id": "83", + "id": "92", "metadata": {}, "outputs": [], "source": [ @@ -1081,7 +1184,7 @@ }, { "cell_type": "markdown", - "id": "84", + "id": "93", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1098,7 +1201,7 @@ { "cell_type": "code", "execution_count": null, - "id": "85", + "id": "94", "metadata": {}, "outputs": [], "source": [ @@ -1114,7 +1217,7 @@ }, { "cell_type": "markdown", - "id": "86", + "id": "95", "metadata": {}, "source": [ "### `interp`\n", @@ -1131,7 +1234,7 @@ { "cell_type": "code", "execution_count": null, - "id": "87", + "id": "96", "metadata": {}, "outputs": [], "source": [ @@ -1147,7 +1250,7 @@ { "cell_type": "code", "execution_count": null, - "id": "88", + "id": "97", "metadata": {}, "outputs": [], "source": [ From 959efcbc2372768474376b8bc9f908f4c223b30f Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 11:06:28 +0100 Subject: [PATCH 30/67] update hz_to_mel(), mel_to_hz() docstring --- src/pyamapping/mappings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index f868ba1..75261c8 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -578,28 +578,28 @@ def hz_to_mel(hz): Parameters ---------- hz : number of array - value in Hz, can be an array + frequencies in Hz, can be an array Returns ------- _ : number of array - value in Mels, same type as the input. + mel scale value, same type as the input. """ return 2595 * np.log10(1 + hz / 700.0) def mel_to_hz(mel): - """Convert a value in Hertz to Mels. + """Convert a frequency in Hz to mel using . Parameters ---------- - hz : number of array - value in Hz, can be an array + mel : number of array + melody value Returns ------- _ : number of array - value in Mels, same type as the input. + cps in Hz, same type as the input. """ return 700 * (10 ** (mel / 2595.0) - 1) From 7af357043872496043e5711e122f6985d9402255 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 11:07:46 +0100 Subject: [PATCH 31/67] add mel_to_hz() and hz_to_mel() examples --- notebooks/pyamapping-examples.ipynb | 127 +++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 11 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 58c614e..857ad1b 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1105,9 +1105,114 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "87", "metadata": {}, + "outputs": [], + "source": [ + "import librosa\n", + "help(librosa.mel_to_hz)" + ] + }, + { + "cell_type": "markdown", + "id": "88", + "metadata": {}, + "source": [ + "### `mel_to_hz`\n", + "\n", + "- mel_to_hz is implemented in analogy to its librosa counterpart, but instaed of Slanays formula using the formula from O'Shaughnessy (1987) (available in librosa via argument htk=True)\n", + "- `mel_to_hz(mel)` converts mel (value or arraylike) to cycles per second (aka Hz). 1000 mel roughly matches 1000 Hz.\n", + "- The mapping function is\n", + " $$ f(x) = 700 \\cdot (10^{\\frac{x}{2595}} - 1) $$\n", + "- ToDo: alternative methods could be added via a method argument, e.g. 'htk' (default), 'slaney', ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89", + "metadata": {}, + "outputs": [], + "source": [ + "pam.mel_to_hz(1000)" + ] + }, + { + "cell_type": "markdown", + "id": "90", + "metadata": {}, + "source": [ + "- ChainableArray.mel_to_hz uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(0, 2500, 100)) \n", + "ys = xs.mel_to_hz()\n", + "plt.plot(xs, ys, \"r-\", label=\"mel_to_hz\")\n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "markdown", + "id": "92", + "metadata": {}, + "source": [ + "### `hz_to_mel`\n", + "\n", + "- hz_to_mel is implemented in analogy to its librosa counterpart, assuming the htk=True flag, i.e. using the formula from O'Shaughnessy (1987).\n", + "- `hz_to_mel(hz)` converts a frequency in Hz (value or arraylike) to a mel scale value (in float, resp. Arraylike of float).\n", + "- The mapping function is\n", + " $$ f(x) = 2595 \\cdot \\log_{10}\\left(1 + \\frac{x}{700}\\right) $$\n", + "- ToDo: alternative methods could be added via a method argument, e.g. 'htk' (default), 'slaney', ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93", + "metadata": {}, + "outputs": [], + "source": [ + "pam.hz_to_mel(1000) " + ] + }, + { + "cell_type": "markdown", + "id": "94", + "metadata": {}, + "source": [ + "- ChainableArray.hz_to_mel uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(21, 108, 7)).midicps() # frequencies of fifth series\n", + "ys = xs.hz_to_mel() \n", + "plt.plot(xs, ys, \"o-\", label=\"mel values for frequencies\"); \n", + "plt.xlabel(\"frequency [Hz]\"); plt.ylabel(\"mel scale\"); \n", + "plt.legend(); plt.grid(); plt.loglog()\n", + "plt.plot([1000], [1000], \"ro-\", label=\"a reference point\");" + ] + }, + { + "cell_type": "markdown", + "id": "96", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1117,7 +1222,7 @@ }, { "cell_type": "markdown", - "id": "88", + "id": "97", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1140,7 +1245,7 @@ }, { "cell_type": "markdown", - "id": "89", + "id": "98", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1150,7 +1255,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "99", "metadata": {}, "outputs": [], "source": [ @@ -1163,7 +1268,7 @@ }, { "cell_type": "markdown", - "id": "91", + "id": "100", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1172,7 +1277,7 @@ { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "101", "metadata": {}, "outputs": [], "source": [ @@ -1184,7 +1289,7 @@ }, { "cell_type": "markdown", - "id": "93", + "id": "102", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1201,7 +1306,7 @@ { "cell_type": "code", "execution_count": null, - "id": "94", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1217,7 +1322,7 @@ }, { "cell_type": "markdown", - "id": "95", + "id": "104", "metadata": {}, "source": [ "### `interp`\n", @@ -1234,7 +1339,7 @@ { "cell_type": "code", "execution_count": null, - "id": "96", + "id": "105", "metadata": {}, "outputs": [], "source": [ @@ -1250,7 +1355,7 @@ { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "106", "metadata": {}, "outputs": [], "source": [ From 7493c86f275aa0a64e56d7a70d354f7e0bb1e19f Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 17:28:23 +0100 Subject: [PATCH 32/67] add Slaney mel scale to mel_to_hz() and hz_to_mel() --- src/pyamapping/mappings.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 75261c8..c18725e 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -572,36 +572,61 @@ def octave_to_cps(octave: float) -> float: octcps = octave_to_cps -def hz_to_mel(hz): - """Convert a value in Hertz to Mels. +def hz_to_mel(hz, htk=False): + """Convert frequencies [Hz] to mel scale. Parameters ---------- hz : number of array frequencies in Hz, can be an array + htk: bool + flag: if True use O'Shaughnessy (1987) formula + if False use Slaney's matlab formula + Returns ------- _ : number of array mel scale value, same type as the input. """ - return 2595 * np.log10(1 + hz / 700.0) + if htk: + return 2595 * np.log10(1 + hz / 700.0) + else: + hz = np.asanyarray(hz) # supports both scalars and arrays + mel = np.where( + hz < 1000, # point between linear and log scale + 3.0 * hz / 200, # linear law + 15 + 27 * np.log(hz / 1000) / np.log(6.4), # log law + ) + return mel if mel.ndim > 0 else float(mel) -def mel_to_hz(mel): - """Convert a frequency in Hz to mel using . +def mel_to_hz(mel, htk=False): + """Convert mel from mel scale to frequency [Hz]. Parameters ---------- mel : number of array melody value + htk: bool + flag: if True use O'Shaughnessy (1987) formula + if False use Slaney's matlab formula Returns ------- _ : number of array cps in Hz, same type as the input. """ - return 700 * (10 ** (mel / 2595.0) - 1) + if htk: + return 700 * (10 ** (mel / 2595.0) - 1) + else: + mel = np.asanyarray(mel) + hz = np.where( + mel < 15, # border between lin/exp regime + (200.0 / 3) * mel, # linear regime + 1000 * (6.4 ** ((mel - 15) / 27)), + ) # exp. regime + return hz if hz.ndim > 0 else float(hz) def db_to_amp(decibels: float) -> float: From db1a25e8c4d223193239c43c6aca70d41adbdeae Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 17:29:21 +0100 Subject: [PATCH 33/67] improve mel_to_hz and hz_to_mel examples --- notebooks/pyamapping-examples.ipynb | 99 +++++++++++++++++++---------- 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 857ad1b..5bf0889 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1122,11 +1122,17 @@ "source": [ "### `mel_to_hz`\n", "\n", - "- mel_to_hz is implemented in analogy to its librosa counterpart, but instaed of Slanays formula using the formula from O'Shaughnessy (1987) (available in librosa via argument htk=True)\n", - "- `mel_to_hz(mel)` converts mel (value or arraylike) to cycles per second (aka Hz). 1000 mel roughly matches 1000 Hz.\n", - "- The mapping function is\n", - " $$ f(x) = 700 \\cdot (10^{\\frac{x}{2595}} - 1) $$\n", - "- ToDo: alternative methods could be added via a method argument, e.g. 'htk' (default), 'slaney', ..." + "- mel_to_hz is implemented in analogy to its librosa counterpart, i.e. using Slanay's formula with a linear and logarithmic part by default, and alternatively the formula from O'Shaughnessy (1987) via the argument `htk=True`.\n", + "- `mel_to_hz(mel)` converts mel (value or arraylike) to cycles per second (aka Hz). \n", + "- For the default (Slaney) the mapping function is\n", + " - linear part (for $mel<15$): \n", + " $$ f(\\text{mel}) = \\frac{200}{3} \\cdot\\text{mel}$$\n", + " - exponential part (for $\\text{mel}>15$):\n", + " $$ f(\\text{mel}) = 1000 \\cdot 6.4 ^{(\\frac{mel - 15}{27})}$$\n", + " - Note that this mel scale has another range: 15 mel = 1000 Hz, and ~10kHz is obtained for mel = 48.5.\n", + "- if `htk==True`, the mapping function is the O'Shaughnessy (1987) formula\n", + " $$ f(\\text{mel}) = 700 \\cdot (10^{\\frac{\\text{mel}}{2595}} - 1) $$\n", + "- Note that here 1000 mel roughly matches 1000 Hz.\n" ] }, { @@ -1136,13 +1142,23 @@ "metadata": {}, "outputs": [], "source": [ - "pam.mel_to_hz(1000)" + "pam.mel_to_hz(1000, htk=True) # using the O'Shaughnessy (1987) formula" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "90", "metadata": {}, + "outputs": [], + "source": [ + "pam.mel_to_hz(15) # using the Slaney formula" + ] + }, + { + "cell_type": "markdown", + "id": "91", + "metadata": {}, "source": [ "- ChainableArray.mel_to_hz uses self as input.\n", "- Here a mapping examples and plots" @@ -1151,43 +1167,62 @@ { "cell_type": "code", "execution_count": null, - "id": "91", + "id": "92", "metadata": {}, "outputs": [], "source": [ "xs = chain(np.arange(0, 2500, 100)) \n", - "ys = xs.mel_to_hz()\n", - "plt.plot(xs, ys, \"r-\", label=\"mel_to_hz\")\n", + "ys = xs.mel_to_hz(htk=True)\n", + "plt.plot(xs, ys, \"r-\", label=\"mel_to_hz (O'Shaughnessy)\")\n", + "plt.legend(); plt.grid();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(0, 40, 1)) \n", + "ys = xs.mel_to_hz() # i.e. Slaney\n", + "plt.plot(xs, ys, \"r-\", label=\"mel_to_hz (Slaney)\")\n", "plt.legend(); plt.grid();" ] }, { "cell_type": "markdown", - "id": "92", + "id": "94", "metadata": {}, "source": [ "### `hz_to_mel`\n", "\n", - "- hz_to_mel is implemented in analogy to its librosa counterpart, assuming the htk=True flag, i.e. using the formula from O'Shaughnessy (1987).\n", + "- hz_to_mel is implemented in analogy to its librosa counterpart, using Slaney method as default, allowing to set htk=True flag for the formula from O'Shaughnessy (1987).\n", "- `hz_to_mel(hz)` converts a frequency in Hz (value or arraylike) to a mel scale value (in float, resp. Arraylike of float).\n", - "- The mapping function is\n", - " $$ f(x) = 2595 \\cdot \\log_{10}\\left(1 + \\frac{x}{700}\\right) $$\n", - "- ToDo: alternative methods could be added via a method argument, e.g. 'htk' (default), 'slaney', ..." + "- The default (Slaney) mapping is:\n", + " $$ \\text{mel}(f) = \\left\\{ \\begin{align*} 3f/200 &~\\text{if}~& f < 1000\\\\\n", + " 15 + 27 \\log_{6.4}(f/1000) &~\\text{if}~& f \\ge 1000\\\\\n", + " \\end{align*}\\right.$$ \n", + "- The alternative (htk=True, O'Shaughnessy (1987) formula) mapping is:\n", + " $$ f(\\text{mel}) = 2595 \\cdot \\log_{10}\\left(1 + \\frac{\\text{mel}}{700}\\right) $$\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "93", + "id": "95", "metadata": {}, "outputs": [], "source": [ - "pam.hz_to_mel(1000) " + "xs = chain(np.arange(1, 10000))\n", + "plt.plot(xs, pam.hz_to_mel(xs));\n", + "plt.xlabel('frequencies [Hz]'); plt.ylabel('mel scale (Slaney)');\n", + "plt.grid()" ] }, { "cell_type": "markdown", - "id": "94", + "id": "96", "metadata": {}, "source": [ "- ChainableArray.hz_to_mel uses self as input.\n", @@ -1197,12 +1232,12 @@ { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "97", "metadata": {}, "outputs": [], "source": [ - "xs = chain(np.arange(21, 108, 7)).midicps() # frequencies of fifth series\n", - "ys = xs.hz_to_mel() \n", + "xs = chain(np.arange(21, 120, 7)).midicps() # frequencies of fifth series\n", + "ys = xs.hz_to_mel(htk=True) \n", "plt.plot(xs, ys, \"o-\", label=\"mel values for frequencies\"); \n", "plt.xlabel(\"frequency [Hz]\"); plt.ylabel(\"mel scale\"); \n", "plt.legend(); plt.grid(); plt.loglog()\n", @@ -1211,7 +1246,7 @@ }, { "cell_type": "markdown", - "id": "96", + "id": "98", "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", @@ -1222,7 +1257,7 @@ }, { "cell_type": "markdown", - "id": "97", + "id": "99", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1245,7 +1280,7 @@ }, { "cell_type": "markdown", - "id": "98", + "id": "100", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1255,7 +1290,7 @@ { "cell_type": "code", "execution_count": null, - "id": "99", + "id": "101", "metadata": {}, "outputs": [], "source": [ @@ -1268,7 +1303,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "102", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1277,7 +1312,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1289,7 +1324,7 @@ }, { "cell_type": "markdown", - "id": "102", + "id": "104", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1306,7 +1341,7 @@ { "cell_type": "code", "execution_count": null, - "id": "103", + "id": "105", "metadata": {}, "outputs": [], "source": [ @@ -1322,7 +1357,7 @@ }, { "cell_type": "markdown", - "id": "104", + "id": "106", "metadata": {}, "source": [ "### `interp`\n", @@ -1339,7 +1374,7 @@ { "cell_type": "code", "execution_count": null, - "id": "105", + "id": "107", "metadata": {}, "outputs": [], "source": [ @@ -1355,7 +1390,7 @@ { "cell_type": "code", "execution_count": null, - "id": "106", + "id": "108", "metadata": {}, "outputs": [], "source": [ From f691c09899207cde095fa550361deaa2e53aad4d Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 17:52:49 +0100 Subject: [PATCH 34/67] add distort() example --- notebooks/pyamapping-examples.ipynb | 90 +++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 5bf0889..cff2631 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1245,9 +1245,77 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "98", "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "99", + "metadata": {}, + "source": [ + "### `distort`\n", + "\n", + "- `distort(x, threshold=1)` is implemented in analogy to its sc3 counterpart `.distort`\n", + "\n", + "- It applies a distortion to x (float, resp. Arraylike of float).\n", + "- the threshold parameter controls the non-linearity\n", + "- The mapping function is:\n", + " $$ f(x, \\theta) = \\frac{x}{\\theta + |x|}$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "100", + "metadata": {}, + "outputs": [], + "source": [ + "pam.distort([0, 1, 2, 3], 1)" + ] + }, + { + "cell_type": "markdown", + "id": "101", + "metadata": {}, + "source": [ + "- ChainableArray.distort uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "102", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-3, 3, 0.01))\n", + "for theta in [0.1, 0.5, 1, 3]:\n", + " plt.plot(xs, xs.distort(theta), label=f\"threshold = {theta}\")\n", + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", + "plt.legend(); plt.title(\"distort mapping function\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "103", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts)\n", + "xs.plot(label='sine').distort(0.3).plot(label='distorted');" + ] + }, + { + "cell_type": "markdown", + "id": "104", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1257,7 +1325,7 @@ }, { "cell_type": "markdown", - "id": "99", + "id": "105", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1280,7 +1348,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "106", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1290,7 +1358,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "107", "metadata": {}, "outputs": [], "source": [ @@ -1303,7 +1371,7 @@ }, { "cell_type": "markdown", - "id": "102", + "id": "108", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1312,7 +1380,7 @@ { "cell_type": "code", "execution_count": null, - "id": "103", + "id": "109", "metadata": {}, "outputs": [], "source": [ @@ -1324,7 +1392,7 @@ }, { "cell_type": "markdown", - "id": "104", + "id": "110", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1341,7 +1409,7 @@ { "cell_type": "code", "execution_count": null, - "id": "105", + "id": "111", "metadata": {}, "outputs": [], "source": [ @@ -1357,7 +1425,7 @@ }, { "cell_type": "markdown", - "id": "106", + "id": "112", "metadata": {}, "source": [ "### `interp`\n", @@ -1374,7 +1442,7 @@ { "cell_type": "code", "execution_count": null, - "id": "107", + "id": "113", "metadata": {}, "outputs": [], "source": [ @@ -1390,7 +1458,7 @@ { "cell_type": "code", "execution_count": null, - "id": "108", + "id": "114", "metadata": {}, "outputs": [], "source": [ From 3cddd5b7f9b40ba1cb6cb1d6b6f4a1a8e9710836 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 18:11:52 +0100 Subject: [PATCH 35/67] improve softclip() code and docstring --- src/pyamapping/mappings.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index c18725e..87da1b0 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -695,15 +695,18 @@ def softclip( Parameters ---------- x (Union[float, np.typing.ArrayLike]): input value or array - threshold (float, optional): defaults to 1.0. Returns ------- Union[float, np.ndarray]: softclip distorted value / array """ - condition = np.abs(x) > 0.5 - return (np.abs(x) - 0.25) / x * condition + (1 - condition) * x - # return (np.abs(x) - 0.25)/x if np.abs(x)<0.5 else x + x = np.asarray(x) # ensure numpy array for elementwise operations + y = np.where( + np.abs(x) > 0.5, # condition + (np.abs(x) - 0.25) / x, # if condition + x, # else + ) + return y if y.ndim > 0 else float(y) def scurve( From ae451d3a48665ba9fb210bbc9c502606d7983a94 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 18:12:39 +0100 Subject: [PATCH 36/67] add softclip() example --- notebooks/pyamapping-examples.ipynb | 103 +++++++++++++++++++++------- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index cff2631..cc01e50 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1244,17 +1244,9 @@ "plt.plot([1000], [1000], \"ro-\", label=\"a reference point\");" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "98", - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "markdown", - "id": "99", + "id": "98", "metadata": {}, "source": [ "### `distort`\n", @@ -1270,7 +1262,7 @@ { "cell_type": "code", "execution_count": null, - "id": "100", + "id": "99", "metadata": {}, "outputs": [], "source": [ @@ -1279,7 +1271,7 @@ }, { "cell_type": "markdown", - "id": "101", + "id": "100", "metadata": {}, "source": [ "- ChainableArray.distort uses self as input.\n", @@ -1289,7 +1281,7 @@ { "cell_type": "code", "execution_count": null, - "id": "102", + "id": "101", "metadata": {}, "outputs": [], "source": [ @@ -1303,19 +1295,82 @@ { "cell_type": "code", "execution_count": null, - "id": "103", + "id": "102", "metadata": {}, "outputs": [], "source": [ "ts = chain(np.linspace(0, 0.1, 500))\n", "xs = np.sin(2*np.pi*50*ts)\n", - "xs.plot(label='sine').distort(0.3).plot(label='distorted');" + "xs.plot(label='sine').distort(0.3).plot(label='distorted');\n", + "plt.legend(loc='upper right');" ] }, { "cell_type": "markdown", + "id": "103", + "metadata": {}, + "source": [ + "### `softclip`\n", + "\n", + "- `softclip(x)` is implemented in analogy to its sc3 counterpart `.softclip`\n", + "\n", + "- It applies a softclip distortion to x (float, resp. Arraylike of float).\n", + "- The mapping function is:\n", + " $$ f(x, \\theta) = \\left\\{\\begin{align*}\n", + " x & ~~~\\text{if}~~~ & |x| \\le 0.5 \\\\\n", + " \\frac{|x| - 0.25}{x} & ~~~\\text{else}~~~ & \\\\\n", + " \\end{align*}\\right.$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "104", "metadata": {}, + "outputs": [], + "source": [ + "pam.softclip([0, 1, 2, 3])" + ] + }, + { + "cell_type": "markdown", + "id": "105", + "metadata": {}, + "source": [ + "- ChainableArray.distort uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "106", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-3, 3, 0.01))\n", + "plt.plot(xs, xs.softclip(), label=f\"threshold = {theta}\")\n", + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", + "plt.legend(); plt.title(\"softclip mapping function\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "107", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts)\n", + "xs.plot(label='sine').softclip().plot(label='softclip distorted');\n", + "plt.legend(loc='upper right');" + ] + }, + { + "cell_type": "markdown", + "id": "108", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1325,7 +1380,7 @@ }, { "cell_type": "markdown", - "id": "105", + "id": "109", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1348,7 +1403,7 @@ }, { "cell_type": "markdown", - "id": "106", + "id": "110", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1358,7 +1413,7 @@ { "cell_type": "code", "execution_count": null, - "id": "107", + "id": "111", "metadata": {}, "outputs": [], "source": [ @@ -1371,7 +1426,7 @@ }, { "cell_type": "markdown", - "id": "108", + "id": "112", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1380,7 +1435,7 @@ { "cell_type": "code", "execution_count": null, - "id": "109", + "id": "113", "metadata": {}, "outputs": [], "source": [ @@ -1392,7 +1447,7 @@ }, { "cell_type": "markdown", - "id": "110", + "id": "114", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1409,7 +1464,7 @@ { "cell_type": "code", "execution_count": null, - "id": "111", + "id": "115", "metadata": {}, "outputs": [], "source": [ @@ -1425,7 +1480,7 @@ }, { "cell_type": "markdown", - "id": "112", + "id": "116", "metadata": {}, "source": [ "### `interp`\n", @@ -1442,7 +1497,7 @@ { "cell_type": "code", "execution_count": null, - "id": "113", + "id": "117", "metadata": {}, "outputs": [], "source": [ @@ -1458,7 +1513,7 @@ { "cell_type": "code", "execution_count": null, - "id": "114", + "id": "118", "metadata": {}, "outputs": [], "source": [ From 2eb1fc1ef17501b9a083fc00484c0cd028785e22 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 18:42:24 +0100 Subject: [PATCH 37/67] fix dtype bug in softclip(), improve scurve() --- src/pyamapping/mappings.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 87da1b0..c8582ef 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -684,9 +684,7 @@ def distort( return x / (threshold + np.abs(x)) -def softclip( - x: Union[float, ArrayLike], threshold: float = 1.0 -) -> Union[float, np.ndarray]: +def softclip(x: Union[float, ArrayLike]) -> Union[float, np.ndarray]: """Apply softclip distortion to x. This yields a perfectly linear region within [-0.5, 0.5], @@ -700,26 +698,22 @@ def softclip( ------- Union[float, np.ndarray]: softclip distorted value / array """ - x = np.asarray(x) # ensure numpy array for elementwise operations - y = np.where( - np.abs(x) > 0.5, # condition - (np.abs(x) - 0.25) / x, # if condition - x, # else - ) + x = np.asarray(x, dtype=float) # ensure numpy array for elementwise operations + y = np.empty_like(x) + mask = np.abs(x) <= 0.5 + y[mask] = x[mask] + y[~mask] = (np.abs(x[~mask]) - 0.25) / x[~mask] return y if y.ndim > 0 else float(y) -def scurve( - x: Union[float, ArrayLike], threshold: float = 1.0 -) -> Union[float, np.ndarray]: +def scurve(x: Union[float, ArrayLike]) -> Union[float, np.ndarray]: """Map value onto an S-curve bound to [0,1]. - Implements v * v * (3-(2*v)) mit v = x.clip(0,1) + Implements v * v * (3-(2*v)) mit v = x.clip(0, 1) Parameters ---------- x (Union[float, np.typing.ArrayLike]): input value or array - threshold (float, optional): defaults to 1.0. Returns ------- From ce80c0ce952b7c3b83974007f00bcdd49ebe77f2 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 18:43:37 +0100 Subject: [PATCH 38/67] add scurve() example --- notebooks/pyamapping-examples.ipynb | 89 ++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 15 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index cc01e50..771e9de 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1316,7 +1316,7 @@ "\n", "- It applies a softclip distortion to x (float, resp. Arraylike of float).\n", "- The mapping function is:\n", - " $$ f(x, \\theta) = \\left\\{\\begin{align*}\n", + " $$ f(x) = \\left\\{\\begin{align*}\n", " x & ~~~\\text{if}~~~ & |x| \\le 0.5 \\\\\n", " \\frac{|x| - 0.25}{x} & ~~~\\text{else}~~~ & \\\\\n", " \\end{align*}\\right.$$\n" @@ -1329,7 +1329,7 @@ "metadata": {}, "outputs": [], "source": [ - "pam.softclip([0, 1, 2, 3])" + "pam.softclip(np.arange(1, 5))" ] }, { @@ -1337,7 +1337,7 @@ "id": "105", "metadata": {}, "source": [ - "- ChainableArray.distort uses self as input.\n", + "- ChainableArray.softclip uses self as input.\n", "- Here a mapping examples and plots" ] }, @@ -1349,9 +1349,9 @@ "outputs": [], "source": [ "xs = chain(np.arange(-3, 3, 0.01))\n", - "plt.plot(xs, xs.softclip(), label=f\"threshold = {theta}\")\n", + "plt.plot(xs, xs.softclip())\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", - "plt.legend(); plt.title(\"softclip mapping function\");" + "plt.title(\"softclip mapping function\");" ] }, { @@ -1371,6 +1371,65 @@ "cell_type": "markdown", "id": "108", "metadata": {}, + "source": [ + "### `scurve`\n", + "\n", + "- `scurve(x)` is implemented in analogy to its sc3 counterpart `.scurve`\n", + "\n", + "- It applies an scurve distortion to x (float, resp. Arraylike of float).\n", + "- The mapping function is:\n", + " $$ f(x) = v^2 (3-2v) ~~\\text{with}~~ v = \\min(\\max(x, 0), 1)~~~\\text{i.e. v=clip(x, 0, 1)}$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "109", + "metadata": {}, + "outputs": [], + "source": [ + "pam.scurve(np.arange(0, 1, 0.25))" + ] + }, + { + "cell_type": "markdown", + "id": "110", + "metadata": {}, + "source": [ + "- ChainableArray.scurve uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "111", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(0, 1, 0.01))\n", + "plt.plot(xs, xs.scurve())\n", + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", + "plt.legend(); plt.title(\"scurve mapping function\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "112", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts).linlin(-1, 1, 0, 1)\n", + "xs.plot(label='sine with offset').scurve().plot(label='scurve distorted');\n", + "plt.legend(loc='upper right');" + ] + }, + { + "cell_type": "markdown", + "id": "113", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1380,7 +1439,7 @@ }, { "cell_type": "markdown", - "id": "109", + "id": "114", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1403,7 +1462,7 @@ }, { "cell_type": "markdown", - "id": "110", + "id": "115", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1413,7 +1472,7 @@ { "cell_type": "code", "execution_count": null, - "id": "111", + "id": "116", "metadata": {}, "outputs": [], "source": [ @@ -1426,7 +1485,7 @@ }, { "cell_type": "markdown", - "id": "112", + "id": "117", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1435,7 +1494,7 @@ { "cell_type": "code", "execution_count": null, - "id": "113", + "id": "118", "metadata": {}, "outputs": [], "source": [ @@ -1447,7 +1506,7 @@ }, { "cell_type": "markdown", - "id": "114", + "id": "119", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1464,7 +1523,7 @@ { "cell_type": "code", "execution_count": null, - "id": "115", + "id": "120", "metadata": {}, "outputs": [], "source": [ @@ -1480,7 +1539,7 @@ }, { "cell_type": "markdown", - "id": "116", + "id": "121", "metadata": {}, "source": [ "### `interp`\n", @@ -1497,7 +1556,7 @@ { "cell_type": "code", "execution_count": null, - "id": "117", + "id": "122", "metadata": {}, "outputs": [], "source": [ @@ -1513,7 +1572,7 @@ { "cell_type": "code", "execution_count": null, - "id": "118", + "id": "123", "metadata": {}, "outputs": [], "source": [ From d9dc43321cd759d986b7fdca14d663010d92f208 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 18:58:33 +0100 Subject: [PATCH 39/67] fix error in lcurve() docstring --- src/pyamapping/mappings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index c8582ef..f304c7e 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -728,10 +728,10 @@ def lcurve( ) -> Union[float, np.ndarray]: """Map value or array onto an L-curve. - Implements (1 + m * exp(-x/tau) + 1) / (1 + n * exp(-x/tau)) + Implements (1 + m * exp(-x/tau)) / (1 + n * exp(-x/tau)) - equal to fermi function with default parameters - note that different to the sc3 implementation, tau is inside - the exp function (unclear tau placement in sc3...) + the exp function (...unclear tau placement in sc3...) Parameters ---------- From 0a1050f8bd109bcba4a46024fb79c6d75dfad0e5 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 18:59:14 +0100 Subject: [PATCH 40/67] add lcurve() example --- notebooks/pyamapping-examples.ipynb | 81 +++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 771e9de..2523ffc 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1430,6 +1430,67 @@ "cell_type": "markdown", "id": "113", "metadata": {}, + "source": [ + "### `lcurve`\n", + "\n", + "- `lcurve(x, m=0.0, n=1.0, tau=1.0)` is implemented in analogy to its sc3 counterpart `.lcurve`\n", + "\n", + "- It applies an l-curve distortion to x (float, resp. Arraylike of float).\n", + "- The mapping function is:\n", + " $$ f(x) = \n", + " \\frac{1 + m e^{-x/\\tau}}{1 + n e^{-x/\\tau}} $$\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "114", + "metadata": {}, + "outputs": [], + "source": [ + "pam.lcurve(np.array([-1, -0.5, 0, 0.5, 1]))" + ] + }, + { + "cell_type": "markdown", + "id": "115", + "metadata": {}, + "source": [ + "- ChainableArray.lcurve uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "116", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-10, 10, 0.005))\n", + "for tau in [0.2, 0.5, 1, 2]:\n", + " plt.plot(xs, xs.lcurve(tau=tau), label=f'lcurve for tau={tau}')\n", + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", + "plt.legend(); plt.title(\"scurve mapping function\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "117", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts)\n", + "xs.plot(label='sine').lcurve(tau=0.25).plot(label='lcurve distorted');\n", + "plt.legend(loc='upper right');" + ] + }, + { + "cell_type": "markdown", + "id": "118", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1439,7 +1500,7 @@ }, { "cell_type": "markdown", - "id": "114", + "id": "119", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1462,7 +1523,7 @@ }, { "cell_type": "markdown", - "id": "115", + "id": "120", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1472,7 +1533,7 @@ { "cell_type": "code", "execution_count": null, - "id": "116", + "id": "121", "metadata": {}, "outputs": [], "source": [ @@ -1485,7 +1546,7 @@ }, { "cell_type": "markdown", - "id": "117", + "id": "122", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1494,7 +1555,7 @@ { "cell_type": "code", "execution_count": null, - "id": "118", + "id": "123", "metadata": {}, "outputs": [], "source": [ @@ -1506,7 +1567,7 @@ }, { "cell_type": "markdown", - "id": "119", + "id": "124", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1523,7 +1584,7 @@ { "cell_type": "code", "execution_count": null, - "id": "120", + "id": "125", "metadata": {}, "outputs": [], "source": [ @@ -1539,7 +1600,7 @@ }, { "cell_type": "markdown", - "id": "121", + "id": "126", "metadata": {}, "source": [ "### `interp`\n", @@ -1556,7 +1617,7 @@ { "cell_type": "code", "execution_count": null, - "id": "122", + "id": "127", "metadata": {}, "outputs": [], "source": [ @@ -1572,7 +1633,7 @@ { "cell_type": "code", "execution_count": null, - "id": "123", + "id": "128", "metadata": {}, "outputs": [], "source": [ From 080ddc156fa522ade21648de7e77226177bc7d21 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 19:38:26 +0100 Subject: [PATCH 41/67] fixed bug in wrap() --- src/pyamapping/mappings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index f304c7e..47f144e 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -792,7 +792,7 @@ def wrap( ) -> Union[float, np.ndarray]: """Wrap array around target range [y1, y2]. - This implements the mapping y1 + x % (y2 - y1). + This implements the mapping y1 + np.mod(x - y1, y2 - y1). The order of y1, y2 is irrelevant. Parameters @@ -805,7 +805,7 @@ def wrap( ------- Union[float, np.ndarray]: wraped array """ - return y1 + x % (y2 - y1) + return y1 + np.mod(x - y1, y2 - y1) def fold( From 992ba82f7592ca87a168d3c73f5fef4c79849a8c Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 19:38:54 +0100 Subject: [PATCH 42/67] add wrap() example --- notebooks/pyamapping-examples.ipynb | 83 +++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 2523ffc..0acbc6a 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1471,7 +1471,7 @@ "for tau in [0.2, 0.5, 1, 2]:\n", " plt.plot(xs, xs.lcurve(tau=tau), label=f'lcurve for tau={tau}')\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", - "plt.legend(); plt.title(\"scurve mapping function\");" + "plt.legend(); plt.title(\"lcurve mapping function\");" ] }, { @@ -1491,6 +1491,67 @@ "cell_type": "markdown", "id": "118", "metadata": {}, + "source": [ + "### `wrap`\n", + "\n", + "- `wrap(x, y1=-1.0, y2=1.0)` is implemented in analogy to its sc3 counterpart `.wrap`\n", + "\n", + "- It wraps x (float, resp. Arraylike of float) around target range [y1, y2].\n", + "- The mapping function is:\n", + " $$ f(x) = (x - y_1) \\mod (y_2 - y_1) $$\n", + "- the order of y1 and y2 is irrelevant\n", + "- wrap delivers the quantization error when quantizing a signal in units of the interval $|y_2-y_1|$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "119", + "metadata": {}, + "outputs": [], + "source": [ + "pam.wrap(np.arange(0, 13), 0, 3)" + ] + }, + { + "cell_type": "markdown", + "id": "120", + "metadata": {}, + "source": [ + "- ChainableArray.wrap uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "121", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-5, 5, 0.01))\n", + "plt.plot(xs, xs.wrap(y1=-1, y2=1))\n", + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", + "plt.title(\"wrap mapping function\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "122", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*30*ts)\n", + "xs.plot(label='sine').wrap(-0.8, 0.5).plot(label='wrapped sine');\n", + "plt.legend(loc='upper right');" + ] + }, + { + "cell_type": "markdown", + "id": "123", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1500,7 +1561,7 @@ }, { "cell_type": "markdown", - "id": "119", + "id": "124", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1523,7 +1584,7 @@ }, { "cell_type": "markdown", - "id": "120", + "id": "125", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1533,7 +1594,7 @@ { "cell_type": "code", "execution_count": null, - "id": "121", + "id": "126", "metadata": {}, "outputs": [], "source": [ @@ -1546,7 +1607,7 @@ }, { "cell_type": "markdown", - "id": "122", + "id": "127", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1555,7 +1616,7 @@ { "cell_type": "code", "execution_count": null, - "id": "123", + "id": "128", "metadata": {}, "outputs": [], "source": [ @@ -1567,7 +1628,7 @@ }, { "cell_type": "markdown", - "id": "124", + "id": "129", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1584,7 +1645,7 @@ { "cell_type": "code", "execution_count": null, - "id": "125", + "id": "130", "metadata": {}, "outputs": [], "source": [ @@ -1600,7 +1661,7 @@ }, { "cell_type": "markdown", - "id": "126", + "id": "131", "metadata": {}, "source": [ "### `interp`\n", @@ -1617,7 +1678,7 @@ { "cell_type": "code", "execution_count": null, - "id": "127", + "id": "132", "metadata": {}, "outputs": [], "source": [ @@ -1633,7 +1694,7 @@ { "cell_type": "code", "execution_count": null, - "id": "128", + "id": "133", "metadata": {}, "outputs": [], "source": [ From f0817f33e217927a6b37700011a57fc97b9c9650 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Sat, 1 Nov 2025 19:48:07 +0100 Subject: [PATCH 43/67] add fold() example --- notebooks/pyamapping-examples.ipynb | 82 +++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 0acbc6a..5dd1eff 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1498,7 +1498,7 @@ "\n", "- It wraps x (float, resp. Arraylike of float) around target range [y1, y2].\n", "- The mapping function is:\n", - " $$ f(x) = (x - y_1) \\mod (y_2 - y_1) $$\n", + " $$ f(x) = y1 + ((x - y_1) \\mod (y_2 - y_1)) $$\n", "- the order of y1 and y2 is irrelevant\n", "- wrap delivers the quantization error when quantizing a signal in units of the interval $|y_2-y_1|$" ] @@ -1552,6 +1552,66 @@ "cell_type": "markdown", "id": "123", "metadata": {}, + "source": [ + "### `fold`\n", + "\n", + "- `fold(x, y1=-1.0, y2=1.0)` is implemented in analogy to its sc3 counterpart `.fold`\n", + "\n", + "- It folds x (float, resp. Arraylike of float) beyond limits [y1, y2] back by mirroring the signal.\n", + "- The mapping function is:\n", + " $$ f(x) = y_1 + |(x - y_2) \\mod (2L) - L| \\text{~~with~~} L = y_2 -y_1 $$\n", + "- the order of y1 and y2 is irrelevant: a swap is done internally " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "124", + "metadata": {}, + "outputs": [], + "source": [ + "pam.fold(np.arange(0, 13), 0, 4)" + ] + }, + { + "cell_type": "markdown", + "id": "125", + "metadata": {}, + "source": [ + "- ChainableArray.wrap uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "126", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-5, 5, 0.01))\n", + "plt.plot(xs, xs.fold(y1=-1, y2=1))\n", + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", + "plt.title(\"wrap mapping function\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "127", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*30*ts)\n", + "xs.plot(label='sine').fold(-0.75, 0.5).plot(label='folded sine');\n", + "plt.legend(loc='upper right'); plt.grid()" + ] + }, + { + "cell_type": "markdown", + "id": "128", + "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", "\n", @@ -1561,7 +1621,7 @@ }, { "cell_type": "markdown", - "id": "124", + "id": "129", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1584,7 +1644,7 @@ }, { "cell_type": "markdown", - "id": "125", + "id": "130", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1594,7 +1654,7 @@ { "cell_type": "code", "execution_count": null, - "id": "126", + "id": "131", "metadata": {}, "outputs": [], "source": [ @@ -1607,7 +1667,7 @@ }, { "cell_type": "markdown", - "id": "127", + "id": "132", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1616,7 +1676,7 @@ { "cell_type": "code", "execution_count": null, - "id": "128", + "id": "133", "metadata": {}, "outputs": [], "source": [ @@ -1628,7 +1688,7 @@ }, { "cell_type": "markdown", - "id": "129", + "id": "134", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1645,7 +1705,7 @@ { "cell_type": "code", "execution_count": null, - "id": "130", + "id": "135", "metadata": {}, "outputs": [], "source": [ @@ -1661,7 +1721,7 @@ }, { "cell_type": "markdown", - "id": "131", + "id": "136", "metadata": {}, "source": [ "### `interp`\n", @@ -1678,7 +1738,7 @@ { "cell_type": "code", "execution_count": null, - "id": "132", + "id": "137", "metadata": {}, "outputs": [], "source": [ @@ -1694,7 +1754,7 @@ { "cell_type": "code", "execution_count": null, - "id": "133", + "id": "138", "metadata": {}, "outputs": [], "source": [ From 24bb5c0edfbed51a0ab2e93b3a2d6513239a16c9 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 16:15:03 +0100 Subject: [PATCH 44/67] improve/correct fermi() docstring --- src/pyamapping/mappings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 47f144e..65e0d2f 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -752,12 +752,13 @@ def fermi( ) -> Union[float, np.ndarray]: """Apply fermi function to value or array. - Implements 1 / (1 + exp(-x/tau)) + Implements 1 / (1 + exp(-(x-mu)/tau)) Parameters ---------- x (Union[float, np.typing.ArrayLike]): input value or array tau (float, optional): scale constant, defaults to 1.0. + mu (float, optional): shift, defaults to 0.0 Returns ------- From ce45ea880d1ba8b485f15b0904fcf1a07bf522c6 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 16:15:59 +0100 Subject: [PATCH 45/67] add fermi() example --- notebooks/pyamapping-examples.ipynb | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 5dd1eff..022dd9f 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1764,6 +1764,77 @@ "xs.bilin(50, -50, 100, 0.4, 0, 1).plot(\"b:\", label='bilin with extrapolation');\n", "plt.legend();plt.grid();" ] + }, + { + "cell_type": "markdown", + "id": "139", + "metadata": {}, + "source": [ + "### `fermi`\n", + "\n", + "- `fermi(x, tau=1.0, mu=0.0)` implements a (shiftable) fermi function.\n", + "\n", + "- It applies a Fermi function to x (float, resp. Arraylike of float).\n", + "- The mapping function is:\n", + " $$ f(x) = \n", + " \\frac{1}{1 + n e^{-(x-\\mu)/\\tau}} $$\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "140", + "metadata": {}, + "outputs": [], + "source": [ + "pam.fermi(np.array([-1, -0.5, 0, 0.5, 1]))" + ] + }, + { + "cell_type": "markdown", + "id": "141", + "metadata": {}, + "source": [ + "- ChainableArray.fermi uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "142", + "metadata": {}, + "outputs": [], + "source": [ + "xs = chain(np.arange(-10, 10, 0.005))\n", + "for i, mu in enumerate([-2, 0, 2]):\n", + " for j, tau in enumerate([0.2, 0.5, 1]):\n", + " plt.plot(xs, xs.fermi(tau, mu), color=['r','g','b'][i], \n", + " lw=j+1, label=f'lcurve for tau={tau}')\n", + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", + "plt.legend(fontsize=8); plt.title(\"fermi curve mapping functions\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "143", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts)\n", + "xs.plot(label='sine').fermi(tau=0.25, mu=1).plot(label='fermi distorted');\n", + "plt.legend(loc='upper right'); plt.grid()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "144", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": {}, From 558303c81648af921118f365e9d99ef50f9fc832 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 16:33:04 +0100 Subject: [PATCH 46/67] fix normalize() input type --- src/pyamapping/mappings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 65e0d2f..c1807c5 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -767,9 +767,7 @@ def fermi( return 1.0 / (1 + np.exp(-(x - mu) / tau)) -def normalize( - x: Union[float, ArrayLike], y1: float = -1.0, y2: float = 1.0 -) -> Union[float, np.ndarray]: +def normalize(x: np.ndarray, y1: float = -1.0, y2: float = 1.0) -> np.ndarray: """Normalize array to target range [y1, y2]. Linear mapping [min(x), max(x)] to [y1, y2]. Use y1 > y2 to change polarity. From 9b50b9f79a52c4b15352fc219c817d3831d80b70 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 16:33:32 +0100 Subject: [PATCH 47/67] add normalize() example --- notebooks/pyamapping-examples.ipynb | 46 +++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 022dd9f..c438b70 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1777,7 +1777,7 @@ "- It applies a Fermi function to x (float, resp. Arraylike of float).\n", "- The mapping function is:\n", " $$ f(x) = \n", - " \\frac{1}{1 + n e^{-(x-\\mu)/\\tau}} $$\n" + " \\frac{1}{1 + e^{-(x-\\mu)/\\tau}} $$\n" ] }, { @@ -1828,13 +1828,53 @@ "plt.legend(loc='upper right'); plt.grid()" ] }, + { + "cell_type": "markdown", + "id": "144", + "metadata": {}, + "source": [ + "### `normalize`\n", + "\n", + "- `normalize(x, y1=-1.0, y2=1.0)` implements a signal normalization to [y1,y2].\n", + "- A linear mapping from input range [min(x) to max(x)] to output range [y1, y2]\n", + " is applied to argument x (Arraylike of float).\n", + "- Note that this won't work for min(x) = max(x)\n", + "- Note that an implicit polarity change can be achieved by choosing y1>y2.\n", + "- The mapping function is:\n", + " $$ f(x) = y_1 + \\frac{x - x_1}{x_2 - x_1} (y_2 - y_1) $$\n" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "144", + "id": "145", + "metadata": {}, + "outputs": [], + "source": [ + "pam.normalize(np.random.rand(10)) # you'll find a 1 and a -1" + ] + }, + { + "cell_type": "markdown", + "id": "146", + "metadata": {}, + "source": [ + "- ChainableArray.normalize uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "147", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts)\n", + "xs.plot(label='sine').normalize(0.5,1).plot(label='normalized to [0.5, 1]');\n", + "plt.legend(loc='upper right'); plt.grid()" + ] } ], "metadata": {}, From 148ad035350e28fd03187a217ed7e02209de8a6c Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 16:53:41 +0100 Subject: [PATCH 48/67] fixed norm_peak() type definition --- src/pyamapping/mappings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index c1807c5..b0a32ec 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -847,17 +847,17 @@ def remove_dc( return x - np.mean(x) -def norm_peak(x: Union[float, ArrayLike], peak=1.0): - """Normalize array so that max(abs(x)) = peak. +def norm_peak(x: np.ndarray, peak=1.0): + """Normalize by scaling array so that max(abs(x)) = peak. Parameters ---------- - x (Union[float, np.typing.ArrayLike]): input value or array + x (np.ndarray]): input array peak (float): target peak Returns ------- - Union[float, np.ndarray]: normalized (scaled) array + np.ndarray: normalized (scaled) array """ peak_of_x = np.max(np.abs(x)) return (x / peak_of_x) * peak if peak_of_x != 0 else x From 4a7a42eca06f03f0498b4e51cf074acacc2d9590 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 16:55:23 +0100 Subject: [PATCH 49/67] add norm_peak() example --- notebooks/pyamapping-examples.ipynb | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index c438b70..9f2af20 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1840,6 +1840,7 @@ " is applied to argument x (Arraylike of float).\n", "- Note that this won't work for min(x) = max(x)\n", "- Note that an implicit polarity change can be achieved by choosing y1>y2.\n", + "- Note that normalize is different from sc3 normalize (see `pyamapping.norm_peak()`). \n", "- The mapping function is:\n", " $$ f(x) = y_1 + \\frac{x - x_1}{x_2 - x_1} (y_2 - y_1) $$\n" ] @@ -1875,6 +1876,54 @@ "xs.plot(label='sine').normalize(0.5,1).plot(label='normalized to [0.5, 1]');\n", "plt.legend(loc='upper right'); plt.grid()" ] + }, + { + "cell_type": "markdown", + "id": "148", + "metadata": {}, + "source": [ + "### `norm_peak`\n", + "\n", + "- `norm_peak(x, peak=1.0)` implements a signal normalization by scaling to new peak.\n", + "- The signal is scaled by peak/max(abs(x)).\n", + "- note that a polarity change results in negative values of `peak`.\n", + "- Note that `norm_peak` is resembles .normalize from SuperCollider. \n", + "- The mapping function is:\n", + " $$ f(x, \\text{peak}) = \\text{peak}\\cdot\\frac{x}{\\max|x|} $$\n", + "- i.e. if the signal is DC-free, it remains so as it is merely scaled. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "149", + "metadata": {}, + "outputs": [], + "source": [ + "pam.norm_peak(np.random.rand(10), 5) # you'll find a 5 (not not a -5)" + ] + }, + { + "cell_type": "markdown", + "id": "150", + "metadata": {}, + "source": [ + "- ChainableArray.norm_peak uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "151", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts)\n", + "xs.plot(label='sine').norm_peak(0.5).plot(label='norm_peak to 0.5');\n", + "plt.legend(loc='upper right'); plt.grid()" + ] } ], "metadata": {}, From 763c2cb08b23e1b001c4eebdfe26a7a90c890d74 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 17:08:35 +0100 Subject: [PATCH 50/67] fix norm_rms() type definiton --- src/pyamapping/mappings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index b0a32ec..0461ea4 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -863,17 +863,17 @@ def norm_peak(x: np.ndarray, peak=1.0): return (x / peak_of_x) * peak if peak_of_x != 0 else x -def norm_rms(x: Union[float, ArrayLike], rms=1.0): +def norm_rms(x: np.ndarray, rms=1.0): """Normalize array so that its RMS value equals `rms`. Parameters ---------- - x (Union[float, np.typing.ArrayLike]): input value or array + x (np.ndarray): input array rms (float): target rms of array Returns ------- - Union[float, np.ndarray]: rms normalized (scaled) array + np.ndarray: rms normalized (scaled) array """ rms_of_x = np.sqrt(np.mean(x**2)) return (x / rms_of_x) * rms if rms_of_x != 0 else x From d97d53352b7405fb3e90fc3625ca0587ff1171ad Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 17:09:21 +0100 Subject: [PATCH 51/67] add norm_rms() example --- notebooks/pyamapping-examples.ipynb | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 9f2af20..51cec71 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1924,6 +1924,54 @@ "xs.plot(label='sine').norm_peak(0.5).plot(label='norm_peak to 0.5');\n", "plt.legend(loc='upper right'); plt.grid()" ] + }, + { + "cell_type": "markdown", + "id": "152", + "metadata": {}, + "source": [ + "### `norm_rms`\n", + "\n", + "- `norm_rms(x, rms=1.0)` implements a signal normalization by scaling to target RMS.\n", + "- The signal is scaled, not shifted.\n", + "- Note that negative `rms` result in a polarity change.\n", + "- The mapping function is:\n", + " $$ f(x, \\text{rms}) = \\text{rms}\\cdot\\frac{x}{\\sqrt{\\langle x^2 \\rangle}} \n", + " = \\text{rms}\\cdot\\frac{x}{\\sqrt{\\frac{1}{n}\\sum\\limits_{i=1}^n x_i^2}} $$\n", + "- i.e. if the signal is DC-free, it remains so as it is merely scaled. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "153", + "metadata": {}, + "outputs": [], + "source": [ + "pam.norm_rms(np.array([1,0,0,-1]), 1) # scale by sqrt(2) to magnify RMS" + ] + }, + { + "cell_type": "markdown", + "id": "154", + "metadata": {}, + "source": [ + "- ChainableArray.norm_rms uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "155", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*50*ts)\n", + "xs.plot(label='sine').norm_rms(0.5).plot(label='norm_rms to 0.5');\n", + "plt.legend(loc='upper right'); plt.grid()" + ] } ], "metadata": {}, From c667391e382a9d688a88dc5a1afb64798d5e1f89 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 23:00:43 +0100 Subject: [PATCH 52/67] fix remove_dc() type hints --- src/pyamapping/mappings.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 0461ea4..b73ff59 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -831,18 +831,16 @@ def fold( return np.abs((x - y2) % (2 * L) - L) + y1 -def remove_dc( - x: Union[float, ArrayLike], -) -> Union[float, np.ndarray]: +def remove_dc(x: np.darray) -> np.ndarray: """Remove DC bias. Parameters ---------- - x (Union[float, np.typing.ArrayLike]): input value or array + x (np.ndarray): input array Returns ------- - Union[float, np.ndarray]: normalized / scaled array + np.ndarray: mean-free array """ return x - np.mean(x) From c869f41438c1767619297bf4a1f4640e72ab6c00 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 23:01:37 +0100 Subject: [PATCH 53/67] add remove_dc() example --- notebooks/pyamapping-examples.ipynb | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 51cec71..626b128 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1972,6 +1972,52 @@ "xs.plot(label='sine').norm_rms(0.5).plot(label='norm_rms to 0.5');\n", "plt.legend(loc='upper right'); plt.grid()" ] + }, + { + "cell_type": "markdown", + "id": "156", + "metadata": {}, + "source": [ + "### `remove_dc`\n", + "\n", + "- `remove_dc(x)` removes the signal's mean.\n", + "- The mapping function is:\n", + " $$ f(x) = x - \\left< x \\right> $$\n", + "- i.e. the signals mean is shifted to zero.\n", + "- Note that this could cause a signal to clip [-1,1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "157", + "metadata": {}, + "outputs": [], + "source": [ + "pam.remove_dc(np.array([1,2,3,4]))" + ] + }, + { + "cell_type": "markdown", + "id": "158", + "metadata": {}, + "source": [ + "- ChainableArray.remove_dc uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "159", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*25*ts)**2\n", + "xs.plot(label='sine').remove_dc().plot(label='remove_dc output');\n", + "plt.legend(loc='upper right'); plt.grid()" + ] } ], "metadata": {}, From fbeb49f71512b96cb722998696e2c8b73f5726fe Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 23:21:50 +0100 Subject: [PATCH 54/67] fix gain() type hints --- src/pyamapping/mappings.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index b73ff59..9941262 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -877,22 +877,20 @@ def norm_rms(x: np.ndarray, rms=1.0): return (x / rms_of_x) * rms if rms_of_x != 0 else x -def gain( - x: Union[float, ArrayLike], db: Optional[float] = None, amp: Optional[float] = None -): +def gain(x: np.ndarray, db: Optional[float] = None, amp: Optional[float] = None): """Apply gain, either as dB (SPL) or scalar factor amp. No operation done if neither argument is given, it applies both if both are given. Parameters ---------- - x (Union[float, np.typing.ArrayLike]): input value or array + x (np.ndarray): input array db (None or float): dB SPL = gain 10**(db/20), e.g. -6 dB ~ factor 0.5 amp (None or float): gain factor Returns ------- - Union[float, np.ndarray]: scaled (amplified / attenuated) array + np.ndarray: scaled (amplified / attenuated) array """ if db: sig = x * dbamp(db) From 703ebd2695c9949a713ca66f79d873d65c19409e Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Mon, 3 Nov 2025 23:22:43 +0100 Subject: [PATCH 55/67] add gain() example --- notebooks/pyamapping-examples.ipynb | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 626b128..1fd8e25 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -2018,6 +2018,54 @@ "xs.plot(label='sine').remove_dc().plot(label='remove_dc output');\n", "plt.legend(loc='upper right'); plt.grid()" ] + }, + { + "cell_type": "markdown", + "id": "160", + "metadata": {}, + "source": [ + "### `gain`\n", + "\n", + "- `gain(x, db=None, amp=None` applies gain in either dB or amp.\n", + "- The mapping function is:\n", + " $$ f(x) = \\left\\{\\begin{align*}\n", + " x\\cdot \\text{db\\_to\\_amp}(\\text{db}) & ~~\\text{if~ } & \\text{db} \\neq \\text{None}\\\\\n", + " x\\cdot \\text{amp} & ~~\\text{elif~} & \\text{amp} \\neq \\text{None}\\\\\n", + " x & ~~\\text{else~} & ~ \\\\\n", + " \\end{align*}\\right. $$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "161", + "metadata": {}, + "outputs": [], + "source": [ + "pam.gain(np.array([1,2,3,4]), amp=2)" + ] + }, + { + "cell_type": "markdown", + "id": "162", + "metadata": {}, + "source": [ + "- ChainableArray.gain uses self as input.\n", + "- Here a mapping examples and plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "163", + "metadata": {}, + "outputs": [], + "source": [ + "ts = chain(np.linspace(0, 0.1, 500))\n", + "xs = np.sin(2*np.pi*25*ts)\n", + "xs.plot(label='sine').gain(db=-6).plot(label='-6 dB gain');\n", + "plt.legend(loc='upper right'); plt.grid()" + ] } ], "metadata": {}, From ee852c45a4afaa2c05024731858ae3722d029591 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Tue, 4 Nov 2025 13:58:03 +0100 Subject: [PATCH 56/67] fix remove_dc() type hint error --- src/pyamapping/mappings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index 9941262..bc265c5 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -831,7 +831,7 @@ def fold( return np.abs((x - y2) % (2 * L) - L) + y1 -def remove_dc(x: np.darray) -> np.ndarray: +def remove_dc(x: np.ndarray) -> np.ndarray: """Remove DC bias. Parameters From e54692f839e9f3dd782d0db56311c1ea7054fe19 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Tue, 4 Nov 2025 15:09:13 +0100 Subject: [PATCH 57/67] remove obsolete linspace() --- src/pyamapping/mappings.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index bc265c5..efb8a7e 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -901,35 +901,12 @@ def gain(x: np.ndarray, db: Optional[float] = None, amp: Optional[float] = None) return sig -def linspace( - x: Union[float, int, ArrayLike], x1: float, x2: float, endpoint: bool = True -) -> np.ndarray: - """Create np.linspace from x1 to x2 in int(x) resp len(x) steps. - - Parameters - ---------- - x (Union[float, int, ArrayLike]): length or array of which only shape is used - x1 (float): target interval one side - x2 (float): target interval other side - endpoint (bool): forwarded to np.linspace - - Returns - ------- - Union[float, np.ndarray]: array of length len(x) - (resp. int(x) if x is float) of numbers between x1 and x2 - """ - if isinstance(x, np.ndarray): - return np.linspace(x1, x2, x.shape[0], endpoint=endpoint) - else: - return np.linspace(x1, x2, int(abs(x)), endpoint=endpoint) - - def lin_to_ecdf( x: Union[float, ArrayLike], ref_data: np.ndarray, sorted: bool = False ) -> Union[float, np.ndarray]: """Map data using empiric cumulative distribution function as mapping. - This meann feature values are mapped to quantiles. + This means feature values are mapped to quantiles. if sorted==True: ref_data is regarded as sorted, speeding repeated invocations. Parameters @@ -1202,7 +1179,6 @@ def _list_numpy_ufuncs(): linexp, linlin, linpoly, - linspace, mel_to_hz, midi_to_cps, midi_to_ratio, From 39f4abaf1fb2a0602da0ad7a8d12956ab4a98734 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Tue, 4 Nov 2025 15:28:56 +0100 Subject: [PATCH 58/67] remove linspace(), fix lin_ecdf() extrapolation --- src/pyamapping/__init__.py | 1 - src/pyamapping/mappings.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pyamapping/__init__.py b/src/pyamapping/__init__.py index e37069d..09d7893 100644 --- a/src/pyamapping/__init__.py +++ b/src/pyamapping/__init__.py @@ -45,7 +45,6 @@ linexp, linlin, linpoly, - linspace, mel_to_hz, midi_to_cps, midi_to_ratio, diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index efb8a7e..d5baf7e 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -922,7 +922,7 @@ def lin_to_ecdf( """ if sorted: return interp( - x, ref_data, np.arange(1, len(ref_data) + 1) / float(len(ref_data)) + x, ref_data, np.arange(1, len(ref_data) + 1) / float(len(ref_data), left=0) ) else: return interp(x, *ecdf(ref_data)) From d72f1e599af0d4dec162888ce670b13aaa667828 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Tue, 4 Nov 2025 15:30:11 +0100 Subject: [PATCH 59/67] add ecdf(), lin_to_ecdf(), ecdf_to_lin() examples --- notebooks/pyamapping-examples.ipynb | 156 ++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 1fd8e25..835ea73 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -2066,6 +2066,162 @@ "xs.plot(label='sine').gain(db=-6).plot(label='-6 dB gain');\n", "plt.legend(loc='upper right'); plt.grid()" ] + }, + { + "cell_type": "markdown", + "id": "164", + "metadata": {}, + "source": [ + "### `ecdf`\n", + "\n", + "- `ecdf(x, selection=slice())` computes the empirical cumulative distribution function for x.\n", + "- it basically:\n", + " - sorts x to obtain locations for the steps (step_x)\n", + " - creates a step function with len(x)+1 steps (these values become step_y)\n", + " - returns the step_x and step_y coordinates for the given selection\n", + "- Applications:\n", + " - This enables handcrafted mapping functions such as for using `ChainableArray.interp()`.\n", + " - it is used in `lin_to_ecdf()` and `ecdf_to_lin()`\n", + "- Remarks:\n", + " - cdf steps by 1/n occur at points in the sorted data.\n", + " - the values are the cdf at (i.e. including) the data point\n", + " - in consequence the correct cdf for any point left of min(x) is 0\n", + " - however, as there are no data points left of min(x), `interp()` would rather hold, i.e. stay on value 1/n\n", + " - use left=0 as remedy to get cdf=0 for values < min(x)\n", + " - there is no extrapolation problem on the right side: hold on 1 is correct for any v > max(x)\n", + " - note that interp would interpolate between these points, so not generate a step function" + ] + }, + { + "cell_type": "markdown", + "id": "165", + "metadata": {}, + "source": [ + "**Example 1**: (compute once - use many)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "166", + "metadata": {}, + "outputs": [], + "source": [ + "from pyamapping import ecdf\n", + "\n", + "data = np.array([1, 3, 1.5, 3.5, 2, 5, 8, 9, 16]) # your data, unsorted\n", + "myecdf = ecdf(data) # compute the (xc, yc) for interp()\n", + "\n", + "# now myecdf may be used many times\n", + "xn = chain(np.linspace(0, 20, 50)) # your custom x (at which you need ecdf)\n", + "yn = xn.interp(*myecdf, left=0) # extra argument to specify left extrapolation\n", + "\n", + "# plot data, ecdf and results of interp\n", + "plt.plot(data, np.zeros_like(data), \"bx\", label=\"data\")\n", + "plt.plot(*myecdf, \"ro\", ms=5, label=\"ecdf for data\")\n", + "plt.plot(xn, yn, \"ko-\", lw=0.5, ms=2, label=\"your applied ecdf to custom data\")\n", + "plt.grid()\n" + ] + }, + { + "cell_type": "markdown", + "id": "167", + "metadata": {}, + "source": [ + "**Example 2:** (compute and map in one go)\n", + "\n", + "```chain(otherdata).interp(*ecdf(data))```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "168", + "metadata": {}, + "outputs": [], + "source": [ + "newdata = chain(np.array([0.5, 3.25, 20]))\n", + "newdata.interp(*ecdf(data), left=0) # turn newdata into ecdfs of data " + ] + }, + { + "cell_type": "markdown", + "id": "169", + "metadata": {}, + "source": [ + "### `lin_to_ecdf`\n", + "\n", + "- `lin_to_ecdf(x, ref_data, sorted=False)` maps data using the empiric cumulative \n", + " distribution function as mapping.\n", + " - This means feature values are mapped to quantiles.\n", + " - if `sorted==True`, `ref_data` is regarded as sorted, speeding repeated invocations.\n", + "- Note that left=0 argument to interp() is used to make sure cdf=0 for values < min(ref_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "170", + "metadata": {}, + "outputs": [], + "source": [ + "data_feat = np.random.randn(200) # data used for crafting the mapping\n", + "test_data = np.linspace(-3, 3, 200) # data to apply mapping to\n", + "chain(test_data).lin_to_ecdf(data_feat, sorted=False).plot(xs=test_data);" + ] + }, + { + "cell_type": "markdown", + "id": "171", + "metadata": {}, + "source": [ + "### `ecdf_to_lin`\n", + "\n", + "- `ecdf_to_lin(x, ref_data, sorted=False)` maps data using the inverse empiric cumulative \n", + " distribution function as mapping.\n", + " - if ref_data is omitted, x is used instead.\n", + " - if `sorted==True`, `ref_data` is regarded as sorted, speeding repeated invocations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "172", + "metadata": {}, + "outputs": [], + "source": [ + "# ecdf_to_lin\n", + "data = chain(np.random.randn(500)).normalize(0,10) # some quantiles, i.e. in [0,1]\n", + "test_data = np.linspace(0, 1, 100) # data to apply mapping to\n", + "chain(test_data).ecdf_to_lin(data).plot(xs=test_data)\n", + "\n", + "plt.axvline(0.5, ls=\":\", color='k'); \n", + "plt.xlabel('test data, resp. quantile'); plt.ylabel('feature values')\n", + "plt.axhline(np.median(data), ls=\":\", color='k');" + ] + }, + { + "cell_type": "markdown", + "id": "173", + "metadata": {}, + "source": [ + "- quantiles mapping should pass a cdf array \n", + " - so that this does not need to be computed each invocation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "174", + "metadata": {}, + "outputs": [], + "source": [ + "data = chain(np.random.randn(200)) # data used for crafting the mapping\n", + "test_data = np.linspace(-3, 3, 200) # data to apply mapping to\n", + "\n", + "chain(test_data).interp(*ecdf(data)).plot(xs=test_data, label='full data ecdf');\n", + "chain(test_data).interp(*ecdf(data, np.s_[20:-15:10])).plot(\"r-\", xs=test_data, ms=1, label='ecdf sliced');" + ] } ], "metadata": {}, From e36bb858101f52ee9b62524820ce3fb87f3e099f Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Tue, 4 Nov 2025 18:50:33 +0100 Subject: [PATCH 60/67] update .plot() for mapping with xs arg --- notebooks/pyamapping-examples.ipynb | 54 ++++++++++++----------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 835ea73..1d0c949 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -175,6 +175,7 @@ "- `to_array` - back to numpy ndarray\n", "- `to_asig` - convert into pya.Asig\n", "- `plot` - plot signal(s) as time series\n", + " - optional kwarg 'xs' allows to pass x values for data.\n", "- `mapvec` - map function on self by using numpy.vectorize\n", "- `map` - apply function directly to the array itself\n", "\n", @@ -292,8 +293,8 @@ "outputs": [], "source": [ "# plot mapping function (output vs input)\n", - "ys = xs.linlin(0.25, 0.75, -1, 1, \"minmax\")\n", - "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); " + "plt.figure(figsize=(3, 1.5)); plt.grid();\n", + "xs.linlin(0.25, 0.75, -1, 1, \"minmax\").plot(xs=xs); " ] }, { @@ -362,9 +363,7 @@ "outputs": [], "source": [ "# plot linexp mapping function (output vs input)\n", - "ys = xs.linexp(0.25, 0.85, 0.2, 2)\n", - "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); \n", - "plt.plot(xs, ys); " + "ys = xs.linexp(0.25, 0.85, 0.2, 2).plot(xs=xs)" ] }, { @@ -438,9 +437,7 @@ "outputs": [], "source": [ "# plot linexp mapping function (output vs input)\n", - "ys = xs.explin(0.01, 1, 0, 20)\n", - "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys); plt.grid(); \n", - "plt.plot(xs, ys); " + "ys = xs.explin(0.01, 1, 0, 20).plot(xs=xs)" ] }, { @@ -651,8 +648,7 @@ "outputs": [], "source": [ "# plot mapping function (output vs input)\n", - "ys = xs.clip(0.25, 0.75)\n", - "plt.figure(figsize=(3, 1.5)); plt.plot(xs, ys, label=''); plt.grid(); " + "ys = xs.clip(0.25, 0.75).plot(xs=xs, label=''); plt.grid(); " ] }, { @@ -822,9 +818,7 @@ "outputs": [], "source": [ "xs = chain(np.arange(-12, 13)) # one octave around any note\n", - "ys = xs.midi_to_ratio()\n", - "\n", - "plt.plot(xs, ys, \"r-\", label=\"midi_to_ratio result\")\n", + "ys = xs.midi_to_ratio().plot(xs=xs, c=\"r\", ls=\"-\", label=\"midi_to_ratio result\")\n", "plt.plot(0, 1, \"ro\", label=\"midi_ratio of 0\")\n", "plt.legend(); plt.grid();" ] @@ -1042,8 +1036,7 @@ "outputs": [], "source": [ "xs = chain(np.arange(-60, 0, 6)) # some 6 dB steps\n", - "ys = xs.dbamp()\n", - "plt.plot(xs, ys, \"ro-\", ms=2,label=\"db_to_amp result [arb. unit]\")\n", + "ys = xs.dbamp().plot(xs=xs, marker=\"o\", ms=2, label=\"db_to_amp result [arb. unit]\")\n", "plt.legend(); plt.grid();" ] }, @@ -1215,9 +1208,8 @@ "outputs": [], "source": [ "xs = chain(np.arange(1, 10000))\n", - "plt.plot(xs, pam.hz_to_mel(xs));\n", - "plt.xlabel('frequencies [Hz]'); plt.ylabel('mel scale (Slaney)');\n", - "plt.grid()" + "xs.hz_to_mel().plot(xs=xs)\n", + "plt.xlabel('frequencies [Hz]'); plt.ylabel('mel scale (Slaney)'); plt.grid();" ] }, { @@ -1237,8 +1229,7 @@ "outputs": [], "source": [ "xs = chain(np.arange(21, 120, 7)).midicps() # frequencies of fifth series\n", - "ys = xs.hz_to_mel(htk=True) \n", - "plt.plot(xs, ys, \"o-\", label=\"mel values for frequencies\"); \n", + "xs.hz_to_mel(htk=True).plot(xs=xs, marker=\"o\", label=\"mel values for frequencies\"); \n", "plt.xlabel(\"frequency [Hz]\"); plt.ylabel(\"mel scale\"); \n", "plt.legend(); plt.grid(); plt.loglog()\n", "plt.plot([1000], [1000], \"ro-\", label=\"a reference point\");" @@ -1287,7 +1278,7 @@ "source": [ "xs = chain(np.arange(-3, 3, 0.01))\n", "for theta in [0.1, 0.5, 1, 3]:\n", - " plt.plot(xs, xs.distort(theta), label=f\"threshold = {theta}\")\n", + " xs.distort(theta).plot(xs=xs, label=f\"threshold = {theta}\")\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", "plt.legend(); plt.title(\"distort mapping function\");" ] @@ -1300,8 +1291,7 @@ "outputs": [], "source": [ "ts = chain(np.linspace(0, 0.1, 500))\n", - "xs = np.sin(2*np.pi*50*ts)\n", - "xs.plot(label='sine').distort(0.3).plot(label='distorted');\n", + "np.sin(2*np.pi*50*ts).plot(label='sine').distort(0.3).plot(label='distorted');\n", "plt.legend(loc='upper right');" ] }, @@ -1349,7 +1339,7 @@ "outputs": [], "source": [ "xs = chain(np.arange(-3, 3, 0.01))\n", - "plt.plot(xs, xs.softclip())\n", + "xs.softclip().plot(xs=xs)\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", "plt.title(\"softclip mapping function\");" ] @@ -1408,9 +1398,9 @@ "outputs": [], "source": [ "xs = chain(np.arange(0, 1, 0.01))\n", - "plt.plot(xs, xs.scurve())\n", + "xs.scurve().plot(xs=xs)\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", - "plt.legend(); plt.title(\"scurve mapping function\");" + "plt.title(\"scurve mapping function\");" ] }, { @@ -1421,7 +1411,7 @@ "outputs": [], "source": [ "ts = chain(np.linspace(0, 0.1, 500))\n", - "xs = np.sin(2*np.pi*50*ts).linlin(-1, 1, 0, 1)\n", + "xs = (2*np.pi*50*ts).sin().linlin(-1, 1, 0, 1)\n", "xs.plot(label='sine with offset').scurve().plot(label='scurve distorted');\n", "plt.legend(loc='upper right');" ] @@ -1469,7 +1459,7 @@ "source": [ "xs = chain(np.arange(-10, 10, 0.005))\n", "for tau in [0.2, 0.5, 1, 2]:\n", - " plt.plot(xs, xs.lcurve(tau=tau), label=f'lcurve for tau={tau}')\n", + " xs.lcurve(tau=tau).plot(xs=xs, label=f'lcurve for tau={tau}')\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", "plt.legend(); plt.title(\"lcurve mapping function\");" ] @@ -1530,7 +1520,7 @@ "outputs": [], "source": [ "xs = chain(np.arange(-5, 5, 0.01))\n", - "plt.plot(xs, xs.wrap(y1=-1, y2=1))\n", + "xs.wrap(y1=-1, y2=1).plot(xs=xs)\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", "plt.title(\"wrap mapping function\");" ] @@ -1590,7 +1580,7 @@ "outputs": [], "source": [ "xs = chain(np.arange(-5, 5, 0.01))\n", - "plt.plot(xs, xs.fold(y1=-1, y2=1))\n", + "xs.fold(y1=-1, y2=1).plot(xs=xs)\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", "plt.title(\"wrap mapping function\");" ] @@ -1809,7 +1799,7 @@ "xs = chain(np.arange(-10, 10, 0.005))\n", "for i, mu in enumerate([-2, 0, 2]):\n", " for j, tau in enumerate([0.2, 0.5, 1]):\n", - " plt.plot(xs, xs.fermi(tau, mu), color=['r','g','b'][i], \n", + " xs.fermi(tau, mu).plot(xs=xs, color=['r','g','b'][i], \n", " lw=j+1, label=f'lcurve for tau={tau}')\n", "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", "plt.legend(fontsize=8); plt.title(\"fermi curve mapping functions\");" @@ -1872,7 +1862,7 @@ "outputs": [], "source": [ "ts = chain(np.linspace(0, 0.1, 500))\n", - "xs = np.sin(2*np.pi*50*ts)\n", + "xs = (2*np.pi*50*ts).sin()\n", "xs.plot(label='sine').normalize(0.5,1).plot(label='normalized to [0.5, 1]');\n", "plt.legend(loc='upper right'); plt.grid()" ] From 02781780b029d3ff9f3566bb8b4abe4a854566a8 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Tue, 4 Nov 2025 21:06:47 +0100 Subject: [PATCH 61/67] fix linspace removal bug --- src/pyamapping/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyamapping/__init__.py b/src/pyamapping/__init__.py index 09d7893..cb83831 100644 --- a/src/pyamapping/__init__.py +++ b/src/pyamapping/__init__.py @@ -84,7 +84,6 @@ "linexp", "linlin", "linpoly", - "linspace", "midi_to_cps", "midi_to_ratio", "norm_peak", From 76c7ac4f172ba8e67d2932983a6736827a04a132 Mon Sep 17 00:00:00 2001 From: Fabian Hommel Date: Wed, 5 Nov 2025 10:27:51 +0100 Subject: [PATCH 62/67] Updated maximum python version for testing matrix in github workflow. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5c6825..e6e0739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: matrix: python: - "3.7" # oldest Python supported by PSF - - "3.11" # newest Python that is stable + - "3.13" # newest Python that is stable platform: - ubuntu-latest - macos-latest @@ -110,7 +110,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 - with: {python-version: "3.11"} + with: {python-version: "3.12"} - name: Retrieve pre-built distribution files uses: actions/download-artifact@v3 with: {name: python-distribution-files, path: dist/} From bf3b9825246cae61eef6be03c42db271423cd86d Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Wed, 5 Nov 2025 17:26:58 +0100 Subject: [PATCH 63/67] correct wrap() example plot title --- notebooks/pyamapping-examples.ipynb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 1d0c949..60237d7 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -1581,8 +1581,7 @@ "source": [ "xs = chain(np.arange(-5, 5, 0.01))\n", "xs.fold(y1=-1, y2=1).plot(xs=xs)\n", - "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); \n", - "plt.title(\"wrap mapping function\");" + "plt.xlabel('input'); plt.ylabel('output'); plt.grid(); " ] }, { From d57f6cb0fb7d9bd16bf5e970fd7d2332401cd9d3 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Wed, 5 Nov 2025 19:15:18 +0100 Subject: [PATCH 64/67] move ChainableArray to separate module --- notebooks/pyamapping-examples.ipynb | 187 +++++++++++------------ src/pyamapping/__init__.py | 136 ++++++++++++++++- src/pyamapping/chainable_array.py | 108 +++++++++++++ src/pyamapping/mappings.py | 229 +--------------------------- 4 files changed, 331 insertions(+), 329 deletions(-) create mode 100644 src/pyamapping/chainable_array.py diff --git a/notebooks/pyamapping-examples.ipynb b/notebooks/pyamapping-examples.ipynb index 60237d7..c27d591 100644 --- a/notebooks/pyamapping-examples.ipynb +++ b/notebooks/pyamapping-examples.ipynb @@ -101,7 +101,7 @@ "metadata": {}, "outputs": [], "source": [ - "from pyamapping.mappings import _list_numpy_ufuncs, pyamapping_functions\n", + "from pyamapping import _list_numpy_ufuncs, pyamapping_functions\n", "\n", "# print (i) a compact list of all unary and binary numpy functions \n", "# and (ii) all pyamapping functions\n", @@ -1097,20 +1097,9 @@ "plt.legend(); plt.grid();" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "87", - "metadata": {}, - "outputs": [], - "source": [ - "import librosa\n", - "help(librosa.mel_to_hz)" - ] - }, { "cell_type": "markdown", - "id": "88", + "id": "87", "metadata": {}, "source": [ "### `mel_to_hz`\n", @@ -1131,7 +1120,7 @@ { "cell_type": "code", "execution_count": null, - "id": "89", + "id": "88", "metadata": {}, "outputs": [], "source": [ @@ -1141,7 +1130,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "89", "metadata": {}, "outputs": [], "source": [ @@ -1150,7 +1139,7 @@ }, { "cell_type": "markdown", - "id": "91", + "id": "90", "metadata": {}, "source": [ "- ChainableArray.mel_to_hz uses self as input.\n", @@ -1160,7 +1149,7 @@ { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "91", "metadata": {}, "outputs": [], "source": [ @@ -1173,7 +1162,7 @@ { "cell_type": "code", "execution_count": null, - "id": "93", + "id": "92", "metadata": {}, "outputs": [], "source": [ @@ -1185,7 +1174,7 @@ }, { "cell_type": "markdown", - "id": "94", + "id": "93", "metadata": {}, "source": [ "### `hz_to_mel`\n", @@ -1203,7 +1192,7 @@ { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "94", "metadata": {}, "outputs": [], "source": [ @@ -1214,7 +1203,7 @@ }, { "cell_type": "markdown", - "id": "96", + "id": "95", "metadata": {}, "source": [ "- ChainableArray.hz_to_mel uses self as input.\n", @@ -1224,7 +1213,7 @@ { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "96", "metadata": {}, "outputs": [], "source": [ @@ -1237,7 +1226,7 @@ }, { "cell_type": "markdown", - "id": "98", + "id": "97", "metadata": {}, "source": [ "### `distort`\n", @@ -1253,7 +1242,7 @@ { "cell_type": "code", "execution_count": null, - "id": "99", + "id": "98", "metadata": {}, "outputs": [], "source": [ @@ -1262,7 +1251,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "99", "metadata": {}, "source": [ "- ChainableArray.distort uses self as input.\n", @@ -1272,7 +1261,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "100", "metadata": {}, "outputs": [], "source": [ @@ -1286,7 +1275,7 @@ { "cell_type": "code", "execution_count": null, - "id": "102", + "id": "101", "metadata": {}, "outputs": [], "source": [ @@ -1297,7 +1286,7 @@ }, { "cell_type": "markdown", - "id": "103", + "id": "102", "metadata": {}, "source": [ "### `softclip`\n", @@ -1315,7 +1304,7 @@ { "cell_type": "code", "execution_count": null, - "id": "104", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1324,7 +1313,7 @@ }, { "cell_type": "markdown", - "id": "105", + "id": "104", "metadata": {}, "source": [ "- ChainableArray.softclip uses self as input.\n", @@ -1334,7 +1323,7 @@ { "cell_type": "code", "execution_count": null, - "id": "106", + "id": "105", "metadata": {}, "outputs": [], "source": [ @@ -1347,7 +1336,7 @@ { "cell_type": "code", "execution_count": null, - "id": "107", + "id": "106", "metadata": {}, "outputs": [], "source": [ @@ -1359,7 +1348,7 @@ }, { "cell_type": "markdown", - "id": "108", + "id": "107", "metadata": {}, "source": [ "### `scurve`\n", @@ -1374,7 +1363,7 @@ { "cell_type": "code", "execution_count": null, - "id": "109", + "id": "108", "metadata": {}, "outputs": [], "source": [ @@ -1383,7 +1372,7 @@ }, { "cell_type": "markdown", - "id": "110", + "id": "109", "metadata": {}, "source": [ "- ChainableArray.scurve uses self as input.\n", @@ -1393,7 +1382,7 @@ { "cell_type": "code", "execution_count": null, - "id": "111", + "id": "110", "metadata": {}, "outputs": [], "source": [ @@ -1406,7 +1395,7 @@ { "cell_type": "code", "execution_count": null, - "id": "112", + "id": "111", "metadata": {}, "outputs": [], "source": [ @@ -1418,7 +1407,7 @@ }, { "cell_type": "markdown", - "id": "113", + "id": "112", "metadata": {}, "source": [ "### `lcurve`\n", @@ -1434,7 +1423,7 @@ { "cell_type": "code", "execution_count": null, - "id": "114", + "id": "113", "metadata": {}, "outputs": [], "source": [ @@ -1443,7 +1432,7 @@ }, { "cell_type": "markdown", - "id": "115", + "id": "114", "metadata": {}, "source": [ "- ChainableArray.lcurve uses self as input.\n", @@ -1453,7 +1442,7 @@ { "cell_type": "code", "execution_count": null, - "id": "116", + "id": "115", "metadata": {}, "outputs": [], "source": [ @@ -1467,7 +1456,7 @@ { "cell_type": "code", "execution_count": null, - "id": "117", + "id": "116", "metadata": {}, "outputs": [], "source": [ @@ -1479,7 +1468,7 @@ }, { "cell_type": "markdown", - "id": "118", + "id": "117", "metadata": {}, "source": [ "### `wrap`\n", @@ -1496,7 +1485,7 @@ { "cell_type": "code", "execution_count": null, - "id": "119", + "id": "118", "metadata": {}, "outputs": [], "source": [ @@ -1505,7 +1494,7 @@ }, { "cell_type": "markdown", - "id": "120", + "id": "119", "metadata": {}, "source": [ "- ChainableArray.wrap uses self as input.\n", @@ -1515,7 +1504,7 @@ { "cell_type": "code", "execution_count": null, - "id": "121", + "id": "120", "metadata": {}, "outputs": [], "source": [ @@ -1528,7 +1517,7 @@ { "cell_type": "code", "execution_count": null, - "id": "122", + "id": "121", "metadata": {}, "outputs": [], "source": [ @@ -1540,7 +1529,7 @@ }, { "cell_type": "markdown", - "id": "123", + "id": "122", "metadata": {}, "source": [ "### `fold`\n", @@ -1556,7 +1545,7 @@ { "cell_type": "code", "execution_count": null, - "id": "124", + "id": "123", "metadata": {}, "outputs": [], "source": [ @@ -1565,7 +1554,7 @@ }, { "cell_type": "markdown", - "id": "125", + "id": "124", "metadata": {}, "source": [ "- ChainableArray.wrap uses self as input.\n", @@ -1575,7 +1564,7 @@ { "cell_type": "code", "execution_count": null, - "id": "126", + "id": "125", "metadata": {}, "outputs": [], "source": [ @@ -1587,7 +1576,7 @@ { "cell_type": "code", "execution_count": null, - "id": "127", + "id": "126", "metadata": {}, "outputs": [], "source": [ @@ -1599,7 +1588,7 @@ }, { "cell_type": "markdown", - "id": "128", + "id": "127", "metadata": {}, "source": [ "## Tour of Mapping Functions: additional/novel mapping functions\n", @@ -1610,7 +1599,7 @@ }, { "cell_type": "markdown", - "id": "129", + "id": "128", "metadata": {}, "source": [ "### `linpoly`\n", @@ -1633,7 +1622,7 @@ }, { "cell_type": "markdown", - "id": "130", + "id": "129", "metadata": {}, "source": [ "- ChainableArray.linpoly uses self as input.\n", @@ -1643,7 +1632,7 @@ { "cell_type": "code", "execution_count": null, - "id": "131", + "id": "130", "metadata": {}, "outputs": [], "source": [ @@ -1656,7 +1645,7 @@ }, { "cell_type": "markdown", - "id": "132", + "id": "131", "metadata": {}, "source": [ "the following plot shows how the curve parameter influences the mapping function" @@ -1665,7 +1654,7 @@ { "cell_type": "code", "execution_count": null, - "id": "133", + "id": "132", "metadata": {}, "outputs": [], "source": [ @@ -1677,7 +1666,7 @@ }, { "cell_type": "markdown", - "id": "134", + "id": "133", "metadata": {}, "source": [ "### `interp_spline`\n", @@ -1694,7 +1683,7 @@ { "cell_type": "code", "execution_count": null, - "id": "135", + "id": "134", "metadata": {}, "outputs": [], "source": [ @@ -1710,7 +1699,7 @@ }, { "cell_type": "markdown", - "id": "136", + "id": "135", "metadata": {}, "source": [ "### `interp`\n", @@ -1727,7 +1716,7 @@ { "cell_type": "code", "execution_count": null, - "id": "137", + "id": "136", "metadata": {}, "outputs": [], "source": [ @@ -1743,7 +1732,7 @@ { "cell_type": "code", "execution_count": null, - "id": "138", + "id": "137", "metadata": {}, "outputs": [], "source": [ @@ -1756,7 +1745,7 @@ }, { "cell_type": "markdown", - "id": "139", + "id": "138", "metadata": {}, "source": [ "### `fermi`\n", @@ -1772,7 +1761,7 @@ { "cell_type": "code", "execution_count": null, - "id": "140", + "id": "139", "metadata": {}, "outputs": [], "source": [ @@ -1781,7 +1770,7 @@ }, { "cell_type": "markdown", - "id": "141", + "id": "140", "metadata": {}, "source": [ "- ChainableArray.fermi uses self as input.\n", @@ -1791,7 +1780,7 @@ { "cell_type": "code", "execution_count": null, - "id": "142", + "id": "141", "metadata": {}, "outputs": [], "source": [ @@ -1807,7 +1796,7 @@ { "cell_type": "code", "execution_count": null, - "id": "143", + "id": "142", "metadata": {}, "outputs": [], "source": [ @@ -1819,7 +1808,7 @@ }, { "cell_type": "markdown", - "id": "144", + "id": "143", "metadata": {}, "source": [ "### `normalize`\n", @@ -1837,7 +1826,7 @@ { "cell_type": "code", "execution_count": null, - "id": "145", + "id": "144", "metadata": {}, "outputs": [], "source": [ @@ -1846,7 +1835,7 @@ }, { "cell_type": "markdown", - "id": "146", + "id": "145", "metadata": {}, "source": [ "- ChainableArray.normalize uses self as input.\n", @@ -1856,7 +1845,7 @@ { "cell_type": "code", "execution_count": null, - "id": "147", + "id": "146", "metadata": {}, "outputs": [], "source": [ @@ -1868,7 +1857,7 @@ }, { "cell_type": "markdown", - "id": "148", + "id": "147", "metadata": {}, "source": [ "### `norm_peak`\n", @@ -1885,7 +1874,7 @@ { "cell_type": "code", "execution_count": null, - "id": "149", + "id": "148", "metadata": {}, "outputs": [], "source": [ @@ -1894,7 +1883,7 @@ }, { "cell_type": "markdown", - "id": "150", + "id": "149", "metadata": {}, "source": [ "- ChainableArray.norm_peak uses self as input.\n", @@ -1904,7 +1893,7 @@ { "cell_type": "code", "execution_count": null, - "id": "151", + "id": "150", "metadata": {}, "outputs": [], "source": [ @@ -1916,7 +1905,7 @@ }, { "cell_type": "markdown", - "id": "152", + "id": "151", "metadata": {}, "source": [ "### `norm_rms`\n", @@ -1933,7 +1922,7 @@ { "cell_type": "code", "execution_count": null, - "id": "153", + "id": "152", "metadata": {}, "outputs": [], "source": [ @@ -1942,7 +1931,7 @@ }, { "cell_type": "markdown", - "id": "154", + "id": "153", "metadata": {}, "source": [ "- ChainableArray.norm_rms uses self as input.\n", @@ -1952,7 +1941,7 @@ { "cell_type": "code", "execution_count": null, - "id": "155", + "id": "154", "metadata": {}, "outputs": [], "source": [ @@ -1964,7 +1953,7 @@ }, { "cell_type": "markdown", - "id": "156", + "id": "155", "metadata": {}, "source": [ "### `remove_dc`\n", @@ -1979,7 +1968,7 @@ { "cell_type": "code", "execution_count": null, - "id": "157", + "id": "156", "metadata": {}, "outputs": [], "source": [ @@ -1988,7 +1977,7 @@ }, { "cell_type": "markdown", - "id": "158", + "id": "157", "metadata": {}, "source": [ "- ChainableArray.remove_dc uses self as input.\n", @@ -1998,7 +1987,7 @@ { "cell_type": "code", "execution_count": null, - "id": "159", + "id": "158", "metadata": {}, "outputs": [], "source": [ @@ -2010,7 +1999,7 @@ }, { "cell_type": "markdown", - "id": "160", + "id": "159", "metadata": {}, "source": [ "### `gain`\n", @@ -2027,7 +2016,7 @@ { "cell_type": "code", "execution_count": null, - "id": "161", + "id": "160", "metadata": {}, "outputs": [], "source": [ @@ -2036,7 +2025,7 @@ }, { "cell_type": "markdown", - "id": "162", + "id": "161", "metadata": {}, "source": [ "- ChainableArray.gain uses self as input.\n", @@ -2046,7 +2035,7 @@ { "cell_type": "code", "execution_count": null, - "id": "163", + "id": "162", "metadata": {}, "outputs": [], "source": [ @@ -2058,7 +2047,7 @@ }, { "cell_type": "markdown", - "id": "164", + "id": "163", "metadata": {}, "source": [ "### `ecdf`\n", @@ -2083,7 +2072,7 @@ }, { "cell_type": "markdown", - "id": "165", + "id": "164", "metadata": {}, "source": [ "**Example 1**: (compute once - use many)\n" @@ -2092,7 +2081,7 @@ { "cell_type": "code", "execution_count": null, - "id": "166", + "id": "165", "metadata": {}, "outputs": [], "source": [ @@ -2114,7 +2103,7 @@ }, { "cell_type": "markdown", - "id": "167", + "id": "166", "metadata": {}, "source": [ "**Example 2:** (compute and map in one go)\n", @@ -2125,7 +2114,7 @@ { "cell_type": "code", "execution_count": null, - "id": "168", + "id": "167", "metadata": {}, "outputs": [], "source": [ @@ -2135,7 +2124,7 @@ }, { "cell_type": "markdown", - "id": "169", + "id": "168", "metadata": {}, "source": [ "### `lin_to_ecdf`\n", @@ -2150,7 +2139,7 @@ { "cell_type": "code", "execution_count": null, - "id": "170", + "id": "169", "metadata": {}, "outputs": [], "source": [ @@ -2161,7 +2150,7 @@ }, { "cell_type": "markdown", - "id": "171", + "id": "170", "metadata": {}, "source": [ "### `ecdf_to_lin`\n", @@ -2175,7 +2164,7 @@ { "cell_type": "code", "execution_count": null, - "id": "172", + "id": "171", "metadata": {}, "outputs": [], "source": [ @@ -2191,7 +2180,7 @@ }, { "cell_type": "markdown", - "id": "173", + "id": "172", "metadata": {}, "source": [ "- quantiles mapping should pass a cdf array \n", @@ -2201,7 +2190,7 @@ { "cell_type": "code", "execution_count": null, - "id": "174", + "id": "173", "metadata": {}, "outputs": [], "source": [ diff --git a/src/pyamapping/__init__.py b/src/pyamapping/__init__.py index cb83831..b750b6c 100644 --- a/src/pyamapping/__init__.py +++ b/src/pyamapping/__init__.py @@ -15,17 +15,20 @@ finally: del version, PackageNotFoundError +from typing import Callable, Union +import numpy as np + +from pyamapping.chainable_array import ChainableArray, chain from pyamapping.mappings import ( # some synonyms; the class and helper functions - ChainableArray, amp_to_db, ampdb, bilin, - chain, clip, cps_to_midi, cps_to_octave, cpsmidi, + cpsoct, curvelin, db_to_amp, dbamp, @@ -49,18 +52,23 @@ midi_to_cps, midi_to_ratio, midicps, + midiratio, norm_peak, norm_rms, normalize, octave_to_cps, + octcps, ratio_to_midi, - register_chain_fn, + ratiomidi, remove_dc, scurve, softclip, wrap, ) +# defined here: register_chain_fn, + + __all__ = [ "amp_to_db", "bilin", @@ -107,3 +115,125 @@ "chain", "register_chain_fn", ] # type: ignore + + +def register_numpy_ufunc(fn: np.ufunc, name: Union[None, str] = None) -> None: + """Register numpy ufunc with one or two ndarray arguments.""" + nin = fn.nin + if nin == 1: + + def method1(self, *args, **kwargs): + return ChainableArray(fn(self, *args, **kwargs)) + + method = method1 + + elif nin == 2: + + def method2(self, other, *args, **kwargs): + return ChainableArray(fn(self, other, *args, **kwargs)) + + method = method2 + + else: + print("warning: np.ufunc fn has nin not in [1,2]") + + def default_method(x): + return None + + method = default_method + + method.__name__ = fn.__name__ if not name else name + method.__doc__ = ( + f"{method.__name__} implements numpy.{method.__name__}" + + f"function for ChainableArray. See help(np.{fn.__name__})" + ) + + setattr(ChainableArray, method.__name__, method) + + +def register_chain_fn(fn: Callable, name: Union[None, str] = None) -> None: + """Register function fn for chaining, optionally under given name.""" + + def method(self, *args, **kwargs): + return ChainableArray(fn(self, *args, **kwargs)) + + method.__name__ = fn.__name__ if not name else name + method.__doc__ = ( + f"{method.__name__} implements the {method.__name__}" + + "operation for ChainableArray. Argument: np.ndarray" + ) + + setattr(ChainableArray, method.__name__, method) + + +def _list_numpy_ufuncs(): + """Return all numpy ufuncs with 1 or 2 ndarray arguments.""" + ufunc_list = [] + for attr_name in dir(np): # all attributes in numpy + attr = getattr(np, attr_name) + if isinstance(attr, np.ufunc): + if attr.nin <= 2: + ufunc_list.append(attr) + else: + print(attr, attr.nin) + return ufunc_list + + +# create class methods for numpy functions and pyamapping functions +for fn in _list_numpy_ufuncs(): # numpy_mapping_functions: + name = "abs" if fn.__name__ == "absolute" else None + register_numpy_ufunc(fn, name) + +# register some non-ufunc which nontheless should workd +register_chain_fn(np.angle, "angle") +ChainableArray.magnitude = ChainableArray.abs + +pyamapping_functions = [ + amp_to_db, + bilin, + clip, + cps_to_midi, + cps_to_octave, + curvelin, + db_to_amp, + distort, + ecdf_to_lin, + ecdf, + explin, + fermi, + fold, + gain, + hz_to_mel, + interp_spline, + interp, + lcurve, + lin_to_ecdf, + lincurve, + linexp, + linlin, + linpoly, + mel_to_hz, + midi_to_cps, + midi_to_ratio, + norm_peak, + norm_rms, + normalize, + octave_to_cps, + ratio_to_midi, + remove_dc, + scurve, + softclip, + wrap, +] + +for fn in pyamapping_functions: + register_chain_fn(fn, None) + +register_chain_fn(cpsmidi, "cpsmidi") +register_chain_fn(midicps, "midicps") +register_chain_fn(ratiomidi, "ratiomidi") +register_chain_fn(midiratio, "midiratio") +register_chain_fn(cpsoct, "cpsoct") +register_chain_fn(octcps, "octcps") +register_chain_fn(ampdb, "ampdb") +register_chain_fn(dbamp, "dbamp") diff --git a/src/pyamapping/chainable_array.py b/src/pyamapping/chainable_array.py new file mode 100644 index 0000000..599d7c6 --- /dev/null +++ b/src/pyamapping/chainable_array.py @@ -0,0 +1,108 @@ +"""ChainableArray - a subclass of numpy.ndarray.""" + +from typing import Any, Callable, TypeVar + +import numpy as np +from numpy.typing import ArrayLike + +NDArrayType = TypeVar("NDArrayType", bound=np.ndarray) + + +class ChainableArray(np.ndarray): + """subclass for simpler numpy mapping by chaining syntax.""" + + def __new__(cls, input_array, *args, **kwargs): + """Create new instance.""" + obj = np.asarray(input_array).view(cls) + return obj + + def __array_finalize__(self, obj): + """Finalize array.""" + if obj is None: + return + + def to_array(self): + """Convert self to np.ndarray.""" + return np.array(self) + + def to_asig(self, sr=44100): + """Convert self to pya.Asig.""" + from pya import Asig + + return Asig(self, sr=sr) + + def plot(self, *args, **kwargs): + """Plot self via matplotlib.""" + import matplotlib.pyplot as plt + + sr = kwargs.pop("sr", None) + if sr: + xs = np.arange(0, self.shape[0]) / sr + plt.plot(xs, self, *args, **kwargs) + plt.xlabel("time [s]") + else: + xs = kwargs.pop("xs", None) + if xs is not None: + plt.plot(xs, self, *args, **kwargs) + else: + plt.plot(self, *args, **kwargs) + + return self + + def mapvec( + self: NDArrayType, fn: Callable[..., Any], *args: Any, **kwargs: Any + ) -> NDArrayType: + """Map fn on self by using np.vectorize(). + + Parameters + ---------- + self (NDArrayType): array to map + fn (Callable[..., Any]): function to call on each element + + Returns + ------- + NDArrayType: mapping result as ChainableArray + """ + return np.vectorize(fn)(self, *args, **kwargs) + + def map( + self: NDArrayType, fn: Callable[..., Any], *args: Any, **kwargs: Any + ) -> NDArrayType: + """Apply function fn directly to self; on fail suggest to use mapvec(). + + Parameters + ---------- + self (np.ndarray): array used as input of fn + fn (Callable[..., Any]): mapping function + args and kwargs are passed on to fn + + Raises + ------ + TypeError: if fn fails to operate on np.ndarray as first argument. + mapvec() is then proposed as alternative. + + Returns + ------- + ChainableArray: the mapping result as ChainableArray + """ + try: + return chain(fn(self, *args, **kwargs)) + except (TypeError, ValueError, AttributeError) as e: + raise TypeError( + f"Function {fn.__name__} does not support NumPy arrays directly. " + "Use .mapvec() instead for np.vectorize elementwise mapping." + ) from e + + def __getattr__(self, name: str) -> Callable: + """Dynamically handle method calls.""" + if name.startswith("dynamic_"): + return lambda *args: f"Called {name} with {args}" + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + + +def chain(input_array: ArrayLike) -> ChainableArray: + """Turn np.ndarray into ChainableArray.""" + # ToDo: check difference to input_array.view(ChainableArray) + return ChainableArray(input_array) diff --git a/src/pyamapping/mappings.py b/src/pyamapping/mappings.py index d5baf7e..0286d0f 100644 --- a/src/pyamapping/mappings.py +++ b/src/pyamapping/mappings.py @@ -1,11 +1,11 @@ """Collection of audio related mapping functions.""" -from typing import Any, Callable, List, Optional, TypeVar, Union +from typing import List, Optional, Union import numpy as np from numpy.typing import ArrayLike -NDArrayType = TypeVar("NDArrayType", bound=np.ndarray) +from .chainable_array import ChainableArray, chain def linlin( @@ -957,103 +957,6 @@ def ecdf_to_lin( return interp(x, yc, xc) -# create subclass of numpy.ndarray - - -class ChainableArray(np.ndarray): - """subclass for simpler numpy mapping by chaining syntax.""" - - def __new__(cls, input_array, *args, **kwargs): - """Create new instance.""" - obj = np.asarray(input_array).view(cls) - return obj - - def __array_finalize__(self, obj): - """Finalize array.""" - if obj is None: - return - - def to_array(self): - """Convert self to np.ndarray.""" - return np.array(self) - - def to_asig(self, sr=44100): - """Convert self to pya.Asig.""" - from pya import Asig - - return Asig(self, sr=sr) - - def plot(self, *args, **kwargs): - """Plot self via matplotlib.""" - import matplotlib.pyplot as plt - - sr = kwargs.pop("sr", None) - if sr: - xs = np.arange(0, self.shape[0]) / sr - plt.plot(xs, self, *args, **kwargs) - plt.xlabel("time [s]") - else: - xs = kwargs.pop("xs", None) - if xs is not None: - plt.plot(xs, self, *args, **kwargs) - else: - plt.plot(self, *args, **kwargs) - - return self - - def mapvec( - self: NDArrayType, fn: Callable[..., Any], *args: Any, **kwargs: Any - ) -> NDArrayType: - """Map fn on self by using np.vectorize(). - - Parameters - ---------- - self (NDArrayType): array to map - fn (Callable[..., Any]): function to call on each element - - Returns - ------- - NDArrayType: mapping result as ChainableArray - """ - return np.vectorize(fn)(self, *args, **kwargs) - - def map( - self: NDArrayType, fn: Callable[..., Any], *args: Any, **kwargs: Any - ) -> NDArrayType: - """Apply function fn directly to self; on fail suggest to use mapvec(). - - Parameters - ---------- - self (np.ndarray): array used as input of fn - fn (Callable[..., Any]): mapping function - args and kwargs are passed on to fn - - Raises - ------ - TypeError: if fn fails to operate on np.ndarray as first argument. - mapvec() is then proposed as alternative. - - Returns - ------- - ChainableArray: the mapping result as ChainableArray - """ - try: - return chain(fn(self, *args, **kwargs)) - except (TypeError, ValueError, AttributeError) as e: - raise TypeError( - f"Function {fn.__name__} does not support NumPy arrays directly. " - "Use .mapvec() instead for np.vectorize elementwise mapping." - ) from e - - def __getattr__(self, name: str) -> Callable: - """Dynamically handle method calls.""" - if name.startswith("dynamic_"): - return lambda *args: f"Called {name} with {args}" - raise AttributeError( - f"'{self.__class__.__name__}' object has no attribute '{name}'" - ) - - def ecdf( x: np.ndarray, selection: slice = slice(None, None, None) ) -> tuple[np.ndarray, np.ndarray]: @@ -1082,131 +985,3 @@ def ecdf( xs = np.sort(x) ys = np.arange(1, len(xs) + 1) / float(len(xs)) return xs[selection], ys[selection] - - -def register_numpy_ufunc(fn: np.ufunc, name: Union[None, str] = None) -> None: - """Register numpy ufunc with one or two ndarray arguments.""" - nin = fn.nin - if nin == 1: - - def method1(self, *args, **kwargs): - return ChainableArray(fn(self, *args, **kwargs)) - - method = method1 - - elif nin == 2: - - def method2(self, other, *args, **kwargs): - return ChainableArray(fn(self, other, *args, **kwargs)) - - method = method2 - - else: - print("warning: np.ufunc fn has nin not in [1,2]") - - def default_method(x): - return None - - method = default_method - - method.__name__ = fn.__name__ if not name else name - method.__doc__ = ( - f"{method.__name__} implements numpy.{method.__name__}" - + f"function for ChainableArray. See help(np.{fn.__name__})" - ) - - setattr(ChainableArray, method.__name__, method) - - -def register_chain_fn(fn: Callable, name: Union[None, str] = None) -> None: - """Register function fn for chaining, optionally under given name.""" - - def method(self, *args, **kwargs): - return ChainableArray(fn(self, *args, **kwargs)) - - method.__name__ = fn.__name__ if not name else name - method.__doc__ = ( - f"{method.__name__} implements the {method.__name__}" - + "operation for ChainableArray. Argument: np.ndarray" - ) - - setattr(ChainableArray, method.__name__, method) - - -def _list_numpy_ufuncs(): - """Return all numpy ufuncs with 1 or 2 ndarray arguments.""" - ufunc_list = [] - for attr_name in dir(np): # all attributes in numpy - attr = getattr(np, attr_name) - if isinstance(attr, np.ufunc): - if attr.nin <= 2: - ufunc_list.append(attr) - else: - print(attr, attr.nin) - return ufunc_list - - -# create class methods for numpy functions and pyamapping functions -for fn in _list_numpy_ufuncs(): # numpy_mapping_functions: - name = "abs" if fn.__name__ == "absolute" else None - register_numpy_ufunc(fn, name) - -# register some non-ufunc which nontheless should workd -register_chain_fn(np.angle, "angle") -ChainableArray.magnitude = ChainableArray.abs - -pyamapping_functions = [ - amp_to_db, - bilin, - clip, - cps_to_midi, - cps_to_octave, - curvelin, - db_to_amp, - distort, - ecdf_to_lin, - ecdf, - explin, - fermi, - fold, - gain, - hz_to_mel, - interp_spline, - interp, - lcurve, - lin_to_ecdf, - lincurve, - linexp, - linlin, - linpoly, - mel_to_hz, - midi_to_cps, - midi_to_ratio, - norm_peak, - norm_rms, - normalize, - octave_to_cps, - ratio_to_midi, - remove_dc, - scurve, - softclip, - wrap, -] - -for fn in pyamapping_functions: - register_chain_fn(fn, None) - -register_chain_fn(cpsmidi, "cpsmidi") -register_chain_fn(midicps, "midicps") -register_chain_fn(ratiomidi, "ratiomidi") -register_chain_fn(midiratio, "midiratio") -register_chain_fn(cpsoct, "cpsoct") -register_chain_fn(octcps, "octcps") -register_chain_fn(ampdb, "ampdb") -register_chain_fn(dbamp, "dbamp") - - -def chain(input_array: ArrayLike) -> ChainableArray: - """Turn np.ndarray into ChainableArray.""" - # ToDo: check difference to input_array.view(ChainableArray) - return ChainableArray(input_array) From a4b0d82b26d08e59e0c30fbd2e7bd4c769a91237 Mon Sep 17 00:00:00 2001 From: Fabian Hommel Date: Thu, 6 Nov 2025 10:45:59 +0100 Subject: [PATCH 65/67] Added a few tests for mappings, also added scipy as a dependency to the setup.cfg --- setup.cfg | 1 + tests/test_mappings.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/setup.cfg b/setup.cfg index 333c4f6..cf14e4b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ package_dir = # For more information, check out https://semver.org/. install_requires = numpy + scipy importlib-metadata; python_version<"3.8" diff --git a/tests/test_mappings.py b/tests/test_mappings.py index 21182be..02518af 100644 --- a/tests/test_mappings.py +++ b/tests/test_mappings.py @@ -1,4 +1,5 @@ import numpy as np +from pyamapping.mappings import bilin, curvelin, explin, lincurve, midi_to_ratio, ratio_to_midi import pytest from pyamapping import ( @@ -50,6 +51,11 @@ def test_midi_cps(): assert x == cps_to_midi(midi_to_cps(x)) +def test_midi_ratio(): + pytest.approx(midi_to_ratio(7), 1.4983070768766815) + pytest.approx(ratio_to_midi(2), 12.0) + + def test_db_amp(): for x in range(128): assert x == pytest.approx(amp_to_db(db_to_amp(x))) @@ -60,3 +66,30 @@ def test_hz_mel(): pytest.approx(mel_to_hz(549.64), 440) for x in range(128): assert x == pytest.approx(hz_to_mel(mel_to_hz(x))) + + +def test_explin(): + f = 220 * 2**(-5/12) + pytest.approx(explin(f, 220, 440, 0, 12), -5.0) + pytest.approx(explin(0.01, 0.001, 1.0, -30, 0, "max"), -20.0) + + +def test_lincurve(): + pytest.approx( + lincurve(np.array([0.0, 0.1, 0.4, 0.7, 1.0]), 0, 1, 0, 0.4), + np.array([0., 0.08385643, 0.25474431, 0.34852956, 0.4]) + ) + + +def test_curvelin(): + pytest.approx( + curvelin(np.array([0, 0.1, 0.3, 0.5]), 0, 0.5, 0, 10), + np.array([ 0., 0.94934752, 3.65734932, 10.]) + ) + + +def test_bilin(): + pytest.approx( + bilin(np.array([0, 20, 40, 60, 80, 100]), 60, 20, 80, 0, -20, 60), + np.array([-30., -20., -10., 0., 60., 120.]) + ) \ No newline at end of file From 49b8cdb12d8930a649ecd886f0990d832d726ea9 Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 6 Nov 2025 13:57:52 +0100 Subject: [PATCH 66/67] add tests and update test function names --- tests/test_mappings.py | 74 +++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/tests/test_mappings.py b/tests/test_mappings.py index 02518af..f4b0a31 100644 --- a/tests/test_mappings.py +++ b/tests/test_mappings.py @@ -1,5 +1,4 @@ import numpy as np -from pyamapping.mappings import bilin, curvelin, explin, lincurve, midi_to_ratio, ratio_to_midi import pytest from pyamapping import ( @@ -12,6 +11,21 @@ mel_to_hz, midi_to_cps, ) +from pyamapping.mappings import ( + bilin, + cps_to_octave, + curvelin, + distort, + explin, + lcurve, + lincurve, + linexp, + midi_to_ratio, + octave_to_cps, + ratio_to_midi, + scurve, + softclip, +) def test_linlin(): @@ -44,32 +58,42 @@ def test_clip(): assert np.array_equal(clip(a1, 2, 4), a2) -def test_midi_cps(): +def test_midi_to_cps_to_midi(): assert midi_to_cps(69) == 440 assert cps_to_midi(440) == 69 for x in range(128): assert x == cps_to_midi(midi_to_cps(x)) -def test_midi_ratio(): +def test_midi_ratio_midi(): pytest.approx(midi_to_ratio(7), 1.4983070768766815) pytest.approx(ratio_to_midi(2), 12.0) -def test_db_amp(): +def test_cps_octave_cps(): + pytest.approx(octave_to_cps(5.75), 880) + pytest.approx(cps_to_octave(220), 3.75) + + +def test_db_amp_db(): for x in range(128): assert x == pytest.approx(amp_to_db(db_to_amp(x))) -def test_hz_mel(): +def test_hz_mel_hz(): pytest.approx(hz_to_mel(440), 549.64) pytest.approx(mel_to_hz(549.64), 440) - for x in range(128): + for x in np.arange(1, 128): assert x == pytest.approx(hz_to_mel(mel_to_hz(x))) +def test_linexp(): + pytest.approx(linexp(5, 1, 8, 2, 256), 32.0) + pytest.approx(linexp(7, 0, 5, 100, 300, "max"), 300) + + def test_explin(): - f = 220 * 2**(-5/12) + f = 220 * 2 ** (-5 / 12) pytest.approx(explin(f, 220, 440, 0, 12), -5.0) pytest.approx(explin(0.01, 0.001, 1.0, -30, 0, "max"), -20.0) @@ -77,19 +101,47 @@ def test_explin(): def test_lincurve(): pytest.approx( lincurve(np.array([0.0, 0.1, 0.4, 0.7, 1.0]), 0, 1, 0, 0.4), - np.array([0., 0.08385643, 0.25474431, 0.34852956, 0.4]) + np.array([0.0, 0.08385643, 0.25474431, 0.34852956, 0.4]), ) def test_curvelin(): pytest.approx( curvelin(np.array([0, 0.1, 0.3, 0.5]), 0, 0.5, 0, 10), - np.array([ 0., 0.94934752, 3.65734932, 10.]) + np.array([0.0, 0.94934752, 3.65734932, 10.0]), ) def test_bilin(): pytest.approx( bilin(np.array([0, 20, 40, 60, 80, 100]), 60, 20, 80, 0, -20, 60), - np.array([-30., -20., -10., 0., 60., 120.]) - ) \ No newline at end of file + np.array([-30.0, -20.0, -10.0, 0.0, 60.0, 120.0]), + ) + + +def test_distort(): + pytest.approx( + distort([0, 1, 2, 3], 1), + np.array([0.0, 0.5, 0.66666667, 0.75]), + ) + + +def test_softclip(): + pytest.approx( + softclip(np.arange(1, 5)), + np.array([0.75, 0.875, 0.91666667, 0.9375]), + ) + + +def test_scurve(): + pytest.approx( + scurve(np.arange(0, 1, 0.25)), + np.array([0.0, 0.15625, 0.5, 0.84375]), + ) + + +def test_lcurve(): + pytest.approx( + lcurve(np.array([-1, -0.5, 0, 0.5, 1])), + np.array([0.26894142, 0.37754067, 0.5, 0.62245933, 0.73105858]), + ) From 417f0e945f5661fa1901e0df79c95192654136bf Mon Sep 17 00:00:00 2001 From: Thomas Hermann Date: Thu, 6 Nov 2025 14:21:58 +0100 Subject: [PATCH 67/67] add more tests --- tests/test_mappings.py | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/test_mappings.py b/tests/test_mappings.py index f4b0a31..a421495 100644 --- a/tests/test_mappings.py +++ b/tests/test_mappings.py @@ -17,14 +17,23 @@ curvelin, distort, explin, + fermi, + fold, + gain, lcurve, lincurve, linexp, + linpoly, midi_to_ratio, + norm_peak, + norm_rms, + normalize, octave_to_cps, ratio_to_midi, + remove_dc, scurve, softclip, + wrap, ) @@ -140,8 +149,71 @@ def test_scurve(): ) +def test_fermi(): + pytest.approx( + fermi(np.array([-1, -0.5, 0, 0.5, 1])), + np.array([0.26894142, 0.37754067, 0.5, 0.62245933, 0.73105858]), + ) + + def test_lcurve(): pytest.approx( lcurve(np.array([-1, -0.5, 0, 0.5, 1])), np.array([0.26894142, 0.37754067, 0.5, 0.62245933, 0.73105858]), ) + + +def test_wrap(): + pytest.approx( + wrap(np.arange(-3, 5), 0, 3), + np.array([0, 1, 2, 0, 1, 2, 0, 1]), + ) + + +def test_fold(): + pytest.approx( + fold(np.arange(0, 13), 0, 4), + np.array([0, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4]), + ) + + +def test_linpoly(): + pytest.approx( + linpoly(np.arange(-2, 3), 2.5, 100, 500, curve=1), + np.array([172.0, 268.0, 300.0, 332.0, 428.0]), + ) + + +def test_normalize(): + pytest.approx( + np.sort(normalize(np.random.rand(10)))[[0, -1]], + np.array([-1, 1]), + ) + + +def test_norm_peak(): + pytest.approx( + np.max(norm_peak(np.random.rand(10), 5)), + 5, + ) + + +def test_norm_rms(): + pytest.approx( + norm_rms(np.array([1, 0, 0, -1]), 1), + np.array([1.41421356, 0.0, 0.0, -1.41421356]), + ) + + +def test_remove_dc(): + pytest.approx( + remove_dc(np.array([1, 2, 3, 4])), + np.array([-1.5, -0.5, 0.5, 1.5]), + ) + + +def test_gain(): + pytest.approx( + gain(np.array([1, 2, 3, 4]), amp=2), + np.array([2, 4, 6, 8]), + )