Skip to content

Printing and Display

Figure export, axis labelling with SI prefixes, colour maps, and figure layout helpers.

Figure export and layout

v_fig2pdf

V_FIG2PDF - Save a figure to PDF/EPS/PS format.

v_fig2pdf

v_fig2pdf(h=None, s=None, p=None, f='p', fig=None) -> None

Save a matplotlib figure to PDF, EPS or PS format.

Parameters:

Name Type Description Default
h Figure or str

Figure handle, or file path string (for convenience). If None, uses the current figure.

None
s str

File name. Can include '' for the calling function name and '' for the figure number. If ending with '/' or '\', '' is appended. Default is ''.

None
p array_like

If provided, call v_figbolden(p) before saving.

None
f str

Output format string: 'p' for PDF (default), 'e' for EPS, 's' for PS.

'p'
fig Figure

Alternative figure handle (overrides h if both provided).

None
Notes

Unlike the MATLAB version, this does not require MikTeX/pdfcrop. Uses matplotlib's built-in savefig with tight_layout.

Source code in pyvoicebox/v_fig2pdf.py
def v_fig2pdf(h=None, s=None, p=None, f='p', fig=None) -> None:
    """Save a matplotlib figure to PDF, EPS or PS format.

    Parameters
    ----------
    h : matplotlib.figure.Figure or str, optional
        Figure handle, or file path string (for convenience).
        If None, uses the current figure.
    s : str, optional
        File name. Can include '<m>' for the calling function name
        and '<n>' for the figure number. If ending with '/' or '\\',
        '<m>_<n>' is appended. Default is '<m>_<n>'.
    p : array_like, optional
        If provided, call v_figbolden(p) before saving.
    f : str, optional
        Output format string: 'p' for PDF (default), 'e' for EPS, 's' for PS.
    fig : matplotlib.figure.Figure, optional
        Alternative figure handle (overrides h if both provided).

    Notes
    -----
    Unlike the MATLAB version, this does not require MikTeX/pdfcrop.
    Uses matplotlib's built-in savefig with tight_layout.
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_fig2pdf")
    # Handle flexible argument parsing like MATLAB version
    if isinstance(h, str):
        s = h
        h = None

    if fig is not None:
        figure = fig
    elif h is not None:
        figure = h
    else:
        figure = plt.gcf()

    if s is None or s == '':
        s = '<m>_<n>'
    elif s.endswith('/') or s.endswith('\\'):
        s = s + '<m>_<n>'

    # Replace <m> with calling function name
    stack = inspect.stack()
    if len(stack) > 1:
        mfn = stack[-1].function
        if mfn == '<module>':
            frame_file = stack[-1].filename
            mfn = os.path.splitext(os.path.basename(frame_file))[0]
    else:
        mfn = 'Figure'
    s = s.replace('<m>', mfn)

    # Replace <n> with figure number
    fn = str(figure.number)
    s = s.replace('<n>', fn)

    if s == '.':
        return  # suppress save

    # Apply figbolden if requested
    if p is not None:
        from pyvoicebox.v_figbolden import v_figbolden
        if isinstance(p, (int, float)) and p == 0:
            v_figbolden(fig=figure)
        else:
            v_figbolden(pos=p, fig=figure)

    figure.set_layout_engine('tight')

    # Save in requested formats
    if not f:
        f = 'p'

    if 'p' in f:
        figure.savefig(s + '.pdf', format='pdf', bbox_inches='tight')
    if 'e' in f:
        figure.savefig(s + '.eps', format='eps', bbox_inches='tight')
    if 's' in f:
        figure.savefig(s + '.ps', format='ps', bbox_inches='tight')

v_fig2emf

V_FIG2EMF - Save a figure in various image formats.

v_fig2emf

v_fig2emf(
    h=None, s=None, p=None, f="svg", fig=None
) -> None

Save a matplotlib figure in various image formats.

Parameters:

Name Type Description Default
h Figure or str

Figure handle, or file path string. If None, uses the current figure.

None
s str

File name. Can include '' for calling function name and '' for figure number. Default is '_'.

None
p array_like

If provided, call v_figbolden(p) before saving.

None
f str

Output format. One of: 'svg', 'pdf', 'eps', 'ps', 'png', 'jpeg', 'tiff', 'meta' (saved as SVG). Default is 'svg'.

'svg'
fig Figure

Alternative figure handle.

None
Notes

The MATLAB 'meta' (EMF) format is not natively supported by matplotlib. We use SVG as a substitute which is also a vector format.

Source code in pyvoicebox/v_fig2emf.py
def v_fig2emf(h=None, s=None, p=None, f='svg', fig=None) -> None:
    """Save a matplotlib figure in various image formats.

    Parameters
    ----------
    h : matplotlib.figure.Figure or str, optional
        Figure handle, or file path string.
        If None, uses the current figure.
    s : str, optional
        File name. Can include '<m>' for calling function name
        and '<n>' for figure number. Default is '<m>_<n>'.
    p : array_like, optional
        If provided, call v_figbolden(p) before saving.
    f : str, optional
        Output format. One of: 'svg', 'pdf', 'eps', 'ps', 'png', 'jpeg',
        'tiff', 'meta' (saved as SVG). Default is 'svg'.
    fig : matplotlib.figure.Figure, optional
        Alternative figure handle.

    Notes
    -----
    The MATLAB 'meta' (EMF) format is not natively supported by matplotlib.
    We use SVG as a substitute which is also a vector format.
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_fig2emf")
    # Handle flexible argument parsing
    if isinstance(h, str):
        s = h
        h = None

    if fig is not None:
        figure = fig
    elif h is not None:
        figure = h
    else:
        figure = plt.gcf()

    if s is None or s == '':
        s = '<m>_<n>'
    elif s.endswith('/') or s.endswith('\\'):
        s = s + '<m>_<n>'

    # Replace <m> with calling function name
    stack = inspect.stack()
    if len(stack) > 1:
        mfn = stack[-1].function
        if mfn == '<module>':
            frame_file = stack[-1].filename
            mfn = os.path.splitext(os.path.basename(frame_file))[0]
    else:
        mfn = 'Figure'
    s = s.replace('<m>', mfn)

    # Replace <n> with figure number
    fn = str(figure.number)
    s = s.replace('<n>', fn)

    if s == '.':
        return  # suppress save

    # Apply figbolden if requested
    if p is not None:
        from pyvoicebox.v_figbolden import v_figbolden
        if isinstance(p, (int, float)) and p == 0:
            v_figbolden(fig=figure)
        else:
            v_figbolden(pos=p, fig=figure)

    figure.set_layout_engine('tight')

    # Map the format
    fmt = _FORMAT_MAP.get(f, f)

    # Determine file extension
    ext_map = {'svg': '.svg', 'pdf': '.pdf', 'eps': '.eps', 'ps': '.ps',
               'png': '.png', 'jpg': '.jpg', 'jpeg': '.jpg', 'tiff': '.tiff'}
    ext = ext_map.get(fmt, '.' + fmt)

    figure.savefig(s + ext, format=fmt, bbox_inches='tight')

v_figbolden

V_FIGBOLDEN - Embolden, resize and recolour the current figure.

v_figbolden

v_figbolden(pos=None, pv=None, m='', fig=None) -> None

Embolden, resize and recolour a matplotlib figure.

Parameters:

Name Type Description Default
pos array_like

Figure size as [width, height] in pixels, or [xmin, ymin, width, height]. If a single negative number, fix aspect ratio to -width/height preserving area. If a single positive number, use 4:3 aspect ratio.

None
pv dict

Dictionary of property-value pairs to apply. Default is:

None
m str

Mode string: 'l' - list changes made (print to stdout) 'd' - use default pv settings 'c' - change default colours for better contrast 'x' - suppress all changes

''
fig Figure

Figure handle. Default is plt.gcf().

None
Source code in pyvoicebox/v_figbolden.py
def v_figbolden(pos=None, pv=None, m='', fig=None) -> None:
    """Embolden, resize and recolour a matplotlib figure.

    Parameters
    ----------
    pos : array_like, optional
        Figure size as [width, height] in pixels, or [xmin, ymin, width, height].
        If a single negative number, fix aspect ratio to -width/height preserving area.
        If a single positive number, use 4:3 aspect ratio.
    pv : dict, optional
        Dictionary of property-value pairs to apply. Default is:
        {'fontname': 'Arial', 'fontsize': 16, 'linewidth': 2, 'markersize': 8}
    m : str, optional
        Mode string:
        'l' - list changes made (print to stdout)
        'd' - use default pv settings
        'c' - change default colours for better contrast
        'x' - suppress all changes
    fig : matplotlib.figure.Figure, optional
        Figure handle. Default is plt.gcf().
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_figbolden")
    if fig is None:
        fig = plt.gcf()

    if pv is None or 'd' in m:
        pv = {'fontname': 'Arial', 'fontsize': 16, 'linewidth': 2, 'markersize': 8}

    do_list = 'l' in m
    do_changes = 'x' not in m

    # Resize figure if pos is specified
    if pos is not None:
        pos = np.atleast_1d(np.asarray(pos, dtype=float)).ravel()
        if len(pos) >= 4:
            fig.set_size_inches(pos[2] / fig.dpi, pos[3] / fig.dpi)
        elif len(pos) >= 2:
            fig.set_size_inches(pos[0] / fig.dpi, pos[1] / fig.dpi)
        elif len(pos) == 1:
            if pos[0] > 0:
                fig.set_size_inches(pos[0] / fig.dpi, 0.75 * pos[0] / fig.dpi)
            else:
                w, h = fig.get_size_inches()
                area = w * h
                ratio = -pos[0]
                new_h = np.sqrt(area / ratio)
                new_w = ratio * new_h
                fig.set_size_inches(new_w, new_h)

    # Change default colours for better contrast
    if 'c' in m:
        color_map = {
            (0, 0.5, 0): (0, 0.7, 0),          # green
            (0, 0.75, 0.75): (0, 0.7, 0.7),     # cyan
            (0.75, 0.75, 0): (0.83, 0.83, 0),    # yellow
        }
        for ax in fig.get_axes():
            for line in ax.get_lines():
                c = line.get_color()
                if isinstance(c, str):
                    continue
                c_tuple = tuple(np.round(c[:3], 2))
                for old_c, new_c in color_map.items():
                    if np.allclose(c_tuple, old_c, atol=0.05):
                        if do_changes:
                            line.set_color(new_c)
                        if do_list:
                            print(f'  change Color: {old_c} -> {new_c}')

    # Apply property-value pairs to all text and line objects
    if do_changes:
        for ax in fig.get_axes():
            # Apply to axes labels and title
            for text_obj in [ax.title, ax.xaxis.label, ax.yaxis.label]:
                if 'fontsize' in pv:
                    text_obj.set_fontsize(pv['fontsize'])
                if 'fontname' in pv:
                    text_obj.set_fontname(pv['fontname'])

            # Apply to tick labels
            for label in ax.get_xticklabels() + ax.get_yticklabels():
                if 'fontsize' in pv:
                    label.set_fontsize(pv['fontsize'])
                if 'fontname' in pv:
                    label.set_fontname(pv['fontname'])

            # Apply to lines
            for line in ax.get_lines():
                if 'linewidth' in pv:
                    line.set_linewidth(pv['linewidth'])
                if 'markersize' in pv:
                    line.set_markersize(pv['markersize'])

            # Apply to legend if present
            legend = ax.get_legend()
            if legend is not None:
                for text_obj in legend.get_texts():
                    if 'fontsize' in pv:
                        text_obj.set_fontsize(pv['fontsize'])
                    if 'fontname' in pv:
                        text_obj.set_fontname(pv['fontname'])

v_axisenlarge

V_AXISENLARGE - Enlarge the axes of a figure.

v_axisenlarge

v_axisenlarge(f=None, ax=None) -> None

Enlarge the axes of a matplotlib plot.

Parameters:

Name Type Description Default
f float or array_like

Enlarge axis by a factor f relative to current size. If negative, shrink to fit content before enlarging by abs(f). Can be scalar, [fx, fy], [fx, fy, fz], [fleft, fright, fbottom, ftop], or [fleft, fright, fbottom, ftop, fdown, fup]. Default is -1.02.

None
ax Axes

Axes handle. Default is current axes (plt.gca()).

None
Source code in pyvoicebox/v_axisenlarge.py
def v_axisenlarge(f=None, ax=None) -> None:
    """Enlarge the axes of a matplotlib plot.

    Parameters
    ----------
    f : float or array_like, optional
        Enlarge axis by a factor f relative to current size.
        If negative, shrink to fit content before enlarging by abs(f).
        Can be scalar, [fx, fy], [fx, fy, fz],
        [fleft, fright, fbottom, ftop], or [fleft, fright, fbottom, ftop, fdown, fup].
        Default is -1.02.
    ax : matplotlib.axes.Axes, optional
        Axes handle. Default is current axes (plt.gca()).
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_axisenlarge")
    if ax is None:
        ax = plt.gca()
    if f is None:
        f = -1.02

    f = np.atleast_1d(np.asarray(f, dtype=float)).ravel()

    # Expansion table (1-indexed in MATLAB, 0-indexed here)
    fpt = np.array([
        [0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 1],
        [0, 0, 1, 1, 2, 2],
        [0, 1, 2, 3, 2, 3],
        [0, 1, 2, 3, 4, 4],
        [0, 1, 2, 3, 4, 5],
    ])
    nf = min(len(f), 6)
    f = f[fpt[nf - 1, :]]  # expand f to 6 elements

    # Get current limits
    xlim = list(ax.get_xlim())
    ylim = list(ax.get_ylim())
    # Check if 3D
    is_3d = hasattr(ax, 'get_zlim')
    if is_3d:
        zlim = list(ax.get_zlim())
    else:
        zlim = [0.0, 1.0]

    ax0 = np.array([xlim[0], xlim[1], ylim[0], ylim[1], zlim[0], zlim[1]])

    if np.any(f >= 0):
        # Keep current limits where f >= 0
        pass
    else:
        ax0 = np.zeros(6)

    if np.any(f < 0):
        # Auto-fit to tight limits
        ax.autoscale(enable=True, tight=True)
        ax.autoscale_view(tight=True)
        xlim_t = ax.get_xlim()
        ylim_t = ax.get_ylim()
        if is_3d:
            zlim_t = ax.get_zlim()
        else:
            zlim_t = (0.0, 1.0)
        ax1 = np.array([xlim_t[0], xlim_t[1], ylim_t[0], ylim_t[1], zlim_t[0], zlim_t[1]])
        mask = f < 0
        ax0[mask] = ax1[mask]
        f = np.abs(f)

    # ax1 = ax0 * f + ax0([1,0,3,2,5,4]) * (1 - f)
    ax0_swap = ax0[np.array([1, 0, 3, 2, 5, 4])]
    ax1 = ax0 * f + ax0_swap * (1 - f)

    ax.set_xlim(ax1[0], ax1[1])
    ax.set_ylim(ax1[2], ax1[3])
    if is_3d:
        ax.set_zlim(ax1[4], ax1[5])

v_tilefigs

V_TILEFIGS - Tile current figure windows.

v_tilefigs

v_tilefigs(pos=None) -> None

Tile current matplotlib figure windows on screen.

Parameters:

Name Type Description Default
pos array_like

Virtual screen region [xmin, ymin, width, height]. Values >= 1 are pixels, values in (0,1) are normalized. Default uses the full screen minus taskbar.

None
Notes

This function works with matplotlib backends that support window management (e.g., TkAgg, Qt5Agg). It will have no effect with non-interactive backends (e.g., Agg).

Source code in pyvoicebox/v_tilefigs.py
def v_tilefigs(pos=None) -> None:
    """Tile current matplotlib figure windows on screen.

    Parameters
    ----------
    pos : array_like, optional
        Virtual screen region [xmin, ymin, width, height].
        Values >= 1 are pixels, values in (0,1) are normalized.
        Default uses the full screen minus taskbar.

    Notes
    -----
    This function works with matplotlib backends that support
    window management (e.g., TkAgg, Qt5Agg). It will have no
    effect with non-interactive backends (e.g., Agg).
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_tilefigs")
    # Get all open figure numbers sorted
    fig_nums = sorted(plt.get_fignums())
    nf = len(fig_nums)
    if nf == 0:
        return

    # Try to get screen size
    try:
        # For backends with window managers
        root = plt.get_current_fig_manager()
        # Default screen size guess
        scr_w, scr_h = 1920, 1080
    except Exception:
        scr_w, scr_h = 1920, 1080

    if pos is not None:
        pos = np.atleast_1d(np.asarray(pos, dtype=float))
        if np.all(pos < 2):
            # Normalized units
            pos = np.array([
                int(pos[0] * scr_w), int(pos[1] * scr_h),
                int(pos[2] * scr_w), int(pos[3] * scr_h)
            ])
        scr_x, scr_y = int(pos[0]), int(pos[1])
        scr_w, scr_h = int(pos[2]), int(pos[3])
    else:
        scr_x, scr_y = 0, 35  # leave space for taskbar
        scr_h -= 35

    # Find best grid layout (closest to 4:3 aspect ratio)
    best_cols = 1
    best_asp_diff = float('inf')
    for cols in range(1, nf + 1):
        rows = int(np.ceil(nf / cols))
        w = scr_w / cols
        h = scr_h / rows
        asp = w / max(h, 1)
        diff = abs(asp - 4 / 3)
        if diff < best_asp_diff:
            best_asp_diff = diff
            best_cols = cols

    nfh = best_cols
    nfv = int(np.ceil(nf / nfh))
    nfh = int(np.ceil(nf / nfv))

    hpix = scr_w // nfh
    vpix = scr_h // nfv

    for i, fignum in enumerate(fig_nums):
        fig = plt.figure(fignum)
        row = i // nfh
        col = i % nfh
        x = scr_x + col * hpix
        y = scr_y + row * vpix

        try:
            mgr = fig.canvas.manager
            if hasattr(mgr, 'window'):
                # TkAgg backend
                mgr.window.geometry(f'{hpix}x{vpix}+{x}+{y}')
            elif hasattr(mgr, 'resize'):
                mgr.resize(hpix, vpix)
        except Exception:
            # Non-interactive backend -- just set figure size
            dpi = fig.dpi
            fig.set_size_inches(hpix / dpi, vpix / dpi)

Axes and colour

v_colormap

V_COLORMAP - Set and create custom color maps.

Provides custom colormaps and luminance linearization for matplotlib.

v_colormap

v_colormap(
    map_input=None, m="", n=None, p=None
) -> tuple[ndarray, ndarray, ndarray]

Create or modify a color map.

Parameters:

Name Type Description Default
map_input str or ndarray

Either an (r, 3) RGB array or a string naming a colormap. Custom maps: 'v_thermliny', 'v_bipliny', 'v_bipveey', 'v_circrby'. Standard matplotlib maps can also be specified by name. If None, returns the default 'viridis' colormap.

None
m str

Mode string: 'y' - force luminance^p to be linear or V-shaped 'l' - force lightness^p to be linear or V-shaped 'Y' - like 'y' but with default p=⅔ 'L' - like 'l' but with default p=2 'f' - flip the map 'b'/'B' - force minimum luminance >= 0.05/0.10 'w'/'W' - force maximum luminance <= 0.95/0.90 'g' - plot information about the colormap 'k' - keep the current colormap

''
n int or array_like

Number of entries in the colormap, or number per segment.

None
p float

Power law exponent for luminance/lightness linearization.

None

Returns:

Name Type Description
rgb ndarray

(N, 3) RGB colormap array with values in [0, 1].

y ndarray

Column vector of luminance values.

l ndarray

Column vector of lightness values.

Source code in pyvoicebox/v_colormap.py
def v_colormap(map_input=None, m='', n=None, p=None) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Create or modify a color map.

    Parameters
    ----------
    map_input : str or ndarray, optional
        Either an (r, 3) RGB array or a string naming a colormap.
        Custom maps: 'v_thermliny', 'v_bipliny', 'v_bipveey', 'v_circrby'.
        Standard matplotlib maps can also be specified by name.
        If None, returns the default 'viridis' colormap.
    m : str, optional
        Mode string:
        'y' - force luminance^p to be linear or V-shaped
        'l' - force lightness^p to be linear or V-shaped
        'Y' - like 'y' but with default p=2/3
        'L' - like 'l' but with default p=2
        'f' - flip the map
        'b'/'B' - force minimum luminance >= 0.05/0.10
        'w'/'W' - force maximum luminance <= 0.95/0.90
        'g' - plot information about the colormap
        'k' - keep the current colormap
    n : int or array_like, optional
        Number of entries in the colormap, or number per segment.
    p : float, optional
        Power law exponent for luminance/lightness linearization.

    Returns
    -------
    rgb : ndarray
        (N, 3) RGB colormap array with values in [0, 1].
    y : ndarray
        Column vector of luminance values.
    l : ndarray
        Column vector of lightness values.
    """
    global _computed_maps

    # Handle power law defaults
    if p is None:
        if 'Y' in m:
            p = 2 / 3
        elif 'L' in m:
            p = 2
        else:
            p = 1
    pr = 1 / p

    um = m  # preserve original case
    m_lower = m.lower()

    # Get the base RGB map
    if map_input is None:
        # Default: use a standard 64-entry viridis-like map
        from pyvoicebox._compat import _require_matplotlib
        plt = _require_matplotlib("v_colormap")
        cmap = plt.cm.get_cmap('viridis', 64)
        rgb = cmap(np.linspace(0, 1, 64))[:, :3]
    elif isinstance(map_input, str):
        name_lower = map_input.lower()
        # Check custom maps
        matched_name = None
        for k in _CUSTOM_MAPS:
            if k.lower() == name_lower:
                matched_name = k
                break

        if matched_name is not None:
            if matched_name in _computed_maps:
                rgb = _computed_maps[matched_name].copy()
            else:
                spec = _CUSTOM_MAPS[matched_name]
                rgb = v_colormap(spec['colors'], spec['mode'], spec['nszs'], spec['power'])[0]
                nmap = spec['nmap']
                rgb = rgb[:nmap, :]
                _computed_maps[matched_name] = rgb.copy()
        else:
            # Try matplotlib built-in colormap
            from pyvoicebox._compat import _require_matplotlib
            plt = _require_matplotlib("v_colormap")
            try:
                cmap = plt.cm.get_cmap(map_input, 64)
                rgb = cmap(np.linspace(0, 1, 64))[:, :3]
            except ValueError:
                raise ValueError(f'Unknown colormap: {map_input}')
    else:
        rgb = np.asarray(map_input, dtype=float).copy()
        if rgb.ndim == 1:
            rgb = rgb.reshape(-1, 3)

    # Linear interpolation and/or luminance linearization
    if ('y' in m_lower or 'l' in m_lower) or (n is not None):
        nm = rgb.shape[0]

        if 'y' in m_lower or 'l' in m_lower:
            y_lum = rgb @ _yv  # luminance

            # Find monotonic segments
            up = y_lum[1:] > y_lum[:-1]
            if nm > 2:
                ex = up[:-1] != up[1:]
                n_extrema = int(np.sum(ex))
            else:
                n_extrema = 0

            if n_extrema == 0:
                # Monotonic
                if n is None:
                    r = nm
                else:
                    n_arr = np.atleast_1d(np.asarray(n, dtype=int)).ravel()
                    r = n_arr[0]

                if 'y' in m_lower:
                    l_vals = y_lum[np.array([0, nm - 1])] ** p
                    tt = (l_vals[0] + np.arange(r) * (l_vals[1] - l_vals[0]) / (r - 1)) ** pr
                else:
                    tt_y = y_lum[np.array([0, nm - 1])]
                    l_vals = _luminance_to_lightness(tt_y) ** p
                    tt_l = (l_vals[0] + np.arange(r) * (l_vals[1] - l_vals[0]) / (r - 1)) ** pr
                    tt = _lightness_to_luminance(tt_l)

                yd = 1 if (y_lum[1] > y_lum[0]) else -1
                combined = np.concatenate([tt, y_lum])
                ix = np.argsort(combined * yd)

            elif n_extrema == 1:
                # V-shaped
                ipk = int(np.where(ex)[0][0]) + 1

                if n is None:
                    n_segs = [ipk, nm - ipk]
                else:
                    n_arr = np.atleast_1d(np.asarray(n, dtype=int)).ravel()
                    n_segs = [int(n_arr[0]), int(n_arr[1])]

                r = n_segs[0] + n_segs[1]

                if 'y' in m_lower:
                    l_vals = y_lum[np.array([0, ipk, nm - 1])] ** p
                    seg1 = l_vals[1] + np.arange(1 - n_segs[0], 1) * (l_vals[1] - l_vals[0]) / (n_segs[0] - 1)
                    seg2 = l_vals[1] + np.arange(1, n_segs[1] + 1) * (l_vals[2] - l_vals[1]) / n_segs[1]
                    tt = np.concatenate([seg1, seg2]) ** pr
                else:
                    tt_y = y_lum[np.array([0, ipk, nm - 1])]
                    l_vals = _luminance_to_lightness(tt_y) ** p
                    seg1 = l_vals[1] + np.arange(1 - n_segs[0], 1) * (l_vals[1] - l_vals[0]) / (n_segs[0] - 1)
                    seg2 = l_vals[1] + np.arange(1, n_segs[1] + 1) * (l_vals[2] - l_vals[1]) / n_segs[1]
                    tt_l = np.concatenate([seg1, seg2]) ** pr
                    tt = _lightness_to_luminance(tt_l)

                yd = 1 if (y_lum[1] > y_lum[0]) else -1
                y_pk = y_lum[ipk]
                combined = np.concatenate([
                    tt[:n_segs[0]] - y_pk,
                    y_pk - tt[n_segs[0]:r],
                    y_lum[:ipk + 1] - y_pk,
                    y_pk - y_lum[ipk + 1:],
                ])
                ix = np.argsort(combined * yd)

            else:
                raise ValueError('luminance has more than two monotonic segments')

        else:
            # Just linearly interpolate
            n_arr = np.atleast_1d(np.asarray(n, dtype=int)).ravel()
            if len(n_arr) == nm - 1:
                r = int(np.sum(n_arr))
                y_interp = np.concatenate([[1], np.cumsum(n_arr, dtype=float)])
            else:
                r = n_arr[0]
                y_interp = 1 + np.arange(nm) * (r - 1) / (nm - 1)

            tt = np.arange(1, r + 1, dtype=float)
            combined = np.concatenate([tt, y_interp])
            ix = np.argsort(combined)

        # Perform the interpolation
        jx = np.zeros(len(ix), dtype=int)
        jx[ix] = np.arange(len(ix))
        jx = jx[:r]
        jx = jx - np.arange(r)
        jx = np.clip(jx, 1, nm - 1) - 1  # convert to 0-indexed lower bound

        if 'y' in m_lower or 'l' in m_lower:
            al = (tt - y_lum[jx]) / (y_lum[jx + 1] - y_lum[jx] + 1e-300)
        else:
            al = (tt - y_interp[jx]) / (y_interp[jx + 1] - y_interp[jx] + 1e-300)
        al = al[:, np.newaxis]
        rgb = rgb[jx, :] + (rgb[jx + 1, :] - rgb[jx, :]) * al

    # Flip if requested
    if 'f' in m_lower:
        rgb = rgb[::-1, :]

    # Compute luminance
    y_out = rgb @ _yv

    # Constrain luminance if requested
    if 'b' in m_lower or 'w' in m_lower:
        minyt = 0.05 * (('b' in m_lower) + ('B' in um))
        maxyt = 1 - 0.05 * (('w' in m_lower) + ('W' in um))
        maxy = np.max(y_out)
        miny = np.min(y_out)
        if maxy > maxyt or miny < minyt:
            maxy = max(maxy, maxyt)
            miny = min(miny, minyt)
            rgb = (rgb - miny) * (maxyt - minyt) / (maxy - miny) + minyt
            y_out = rgb @ _yv

    # Compute lightness
    l_out = _luminance_to_lightness(y_out)

    # Clip to valid range
    rgb = np.clip(rgb, 0, 1)

    return rgb, y_out, l_out

v_colormap_to_mpl

v_colormap_to_mpl(name_or_rgb, m='', n=None, p=None)

Create a matplotlib LinearSegmentedColormap from a v_colormap specification.

Parameters:

Name Type Description Default
name_or_rgb str or ndarray

Colormap name or RGB array (same as v_colormap).

required
m str

Mode string (same as v_colormap).

''
n int or array_like

Number of entries (same as v_colormap).

None
p float

Power law (same as v_colormap).

None

Returns:

Name Type Description
cmap ListedColormap

A matplotlib colormap object.

Source code in pyvoicebox/v_colormap.py
def v_colormap_to_mpl(name_or_rgb, m='', n=None, p=None):
    """Create a matplotlib LinearSegmentedColormap from a v_colormap specification.

    Parameters
    ----------
    name_or_rgb : str or ndarray
        Colormap name or RGB array (same as v_colormap).
    m : str, optional
        Mode string (same as v_colormap).
    n : int or array_like, optional
        Number of entries (same as v_colormap).
    p : float, optional
        Power law (same as v_colormap).

    Returns
    -------
    cmap : matplotlib.colors.ListedColormap
        A matplotlib colormap object.
    """
    try:
        from matplotlib.colors import ListedColormap
    except ImportError as e:
        raise ImportError(
            "v_colormap_to_mpl requires matplotlib, which is an optional dependency. "
            "Install it with: pip install 'pyvoicebox-sap[plot]'"
        ) from e

    rgb, _, _ = v_colormap(name_or_rgb, m, n, p)
    if isinstance(name_or_rgb, str):
        cmap_name = name_or_rgb
    else:
        cmap_name = 'v_custom'
    return ListedColormap(rgb, name=cmap_name)

v_lambda2rgb

V_LAMBDA2RGB - Convert wavelength to XYZ or RGB colour space.

v_lambda2rgb

v_lambda2rgb(l, m='r') -> ndarray

Convert wavelength to XYZ or RGB colour space.

Parameters:

Name Type Description Default
l array_like

Wavelength(s) in nanometres.

required
m str

Mode: 'r' - output is [R G B] using 1931 observer with negatives clipped (default) 's' - output is [R G B] using 1931 observer with signed values 'x' - output is [X Y Z] using 1931 observer Use uppercase 'R', 'S', 'X' for 1964 observer.

'r'

Returns:

Name Type Description
x ndarray

Tristimulus values, shape (n, 3).

Source code in pyvoicebox/v_lambda2rgb.py
def v_lambda2rgb(l, m='r') -> np.ndarray:
    """Convert wavelength to XYZ or RGB colour space.

    Parameters
    ----------
    l : array_like
        Wavelength(s) in nanometres.
    m : str, optional
        Mode:
        'r' - output is [R G B] using 1931 observer with negatives clipped (default)
        's' - output is [R G B] using 1931 observer with signed values
        'x' - output is [X Y Z] using 1931 observer
        Use uppercase 'R', 'S', 'X' for 1964 observer.

    Returns
    -------
    x : ndarray
        Tristimulus values, shape (n, 3).
    """
    l = np.atleast_1d(np.asarray(l, dtype=float))
    lv = l.ravel()
    lm = m.lower()

    if m == lm:  # lowercase = 1931 standard observer
        ll = np.log(lv)
        x1 = _c[0] * np.exp(_c[1] * (lv - _c[2])**2) + _c[3] * np.exp(_c[4] * (lv - _c[5])**2)
        x2 = _c[6] * np.exp(_c[7] * (ll - _c[8])**2)
        x3 = _c[9] * np.exp(_c[10] * (ll - _c[11])**2)
        x = np.column_stack([x1, x2, x3])
    else:  # uppercase = 1964 standard observer
        x1 = (_d[0] * np.exp(_d[1] * (np.log(lv - _d[2]) + _d[3])**2) +
              _d[4] * np.exp(_d[5] * (np.log(_d[6] - np.minimum(lv, _d[7])) + _d[8])**2))
        x2 = _d[9] * np.exp(_d[10] * (lv - _d[11])**2)
        x3 = _d[12] * np.exp(_d[13] * (np.log(lv - _d[14]) + _d[15])**2)
        x = np.column_stack([x1, x2, x3])

    if lm == 's':
        x = x @ _rx_mat
    elif lm == 'r':
        x = np.maximum(x @ _rx_mat, 0)
    # else 'x' mode: return XYZ directly

    return x

v_xticksi

V_XTICKSI - Label the x-axis of a plot using SI multipliers.

v_xticksi

v_xticksi(ax=None, return_prefix=False) -> str

Label the x-axis of a plot using SI multipliers.

Parameters:

Name Type Description Default
ax Axes

Axes handle. Default is plt.gca().

None
return_prefix bool

If True, return a global SI prefix string that can be incorporated into the axis label.

False

Returns:

Name Type Description
prefix str

The global SI prefix (only if return_prefix=True).

Examples:

>>> import matplotlib.pyplot as plt
>>> plt.plot([0, 1000, 2000], [0, 1, 2])
>>> v_xticksi()
>>> # Or with a global prefix:
>>> prefix = v_xticksi(return_prefix=True)
>>> plt.xlabel(f'Frequency ({prefix}Hz)')
Source code in pyvoicebox/v_xticksi.py
def v_xticksi(ax=None, return_prefix=False) -> str:
    """Label the x-axis of a plot using SI multipliers.

    Parameters
    ----------
    ax : matplotlib.axes.Axes, optional
        Axes handle. Default is plt.gca().
    return_prefix : bool, optional
        If True, return a global SI prefix string that can be
        incorporated into the axis label.

    Returns
    -------
    prefix : str
        The global SI prefix (only if return_prefix=True).

    Examples
    --------
    >>> import matplotlib.pyplot as plt
    >>> plt.plot([0, 1000, 2000], [0, 1, 2])
    >>> v_xticksi()
    >>> # Or with a global prefix:
    >>> prefix = v_xticksi(return_prefix=True)
    >>> plt.xlabel(f'Frequency ({prefix}Hz)')
    """
    return v_xyzticksi(1, ax=ax, return_prefix=return_prefix)

v_yticksi

V_YTICKSI - Label the y-axis of a plot using SI multipliers.

v_yticksi

v_yticksi(ax=None, return_prefix=False) -> str

Label the y-axis of a plot using SI multipliers.

Parameters:

Name Type Description Default
ax Axes

Axes handle. Default is plt.gca().

None
return_prefix bool

If True, return a global SI prefix string that can be incorporated into the axis label.

False

Returns:

Name Type Description
prefix str

The global SI prefix (only if return_prefix=True).

Examples:

>>> import matplotlib.pyplot as plt
>>> plt.plot([0, 1, 2], [0, 1000, 2000])
>>> v_yticksi()
Source code in pyvoicebox/v_yticksi.py
def v_yticksi(ax=None, return_prefix=False) -> str:
    """Label the y-axis of a plot using SI multipliers.

    Parameters
    ----------
    ax : matplotlib.axes.Axes, optional
        Axes handle. Default is plt.gca().
    return_prefix : bool, optional
        If True, return a global SI prefix string that can be
        incorporated into the axis label.

    Returns
    -------
    prefix : str
        The global SI prefix (only if return_prefix=True).

    Examples
    --------
    >>> import matplotlib.pyplot as plt
    >>> plt.plot([0, 1, 2], [0, 1000, 2000])
    >>> v_yticksi()
    """
    return v_xyzticksi(2, ax=ax, return_prefix=return_prefix)

v_xyzticksi

V_XYZTICKSI - Label an axis of a plot using SI multipliers.

v_xyzticksi

v_xyzticksi(ax_idx, ax=None, return_prefix=False) -> str

Label an axis of a plot using SI multipliers.

This is the core implementation called by v_xticksi and v_yticksi.

Parameters:

Name Type Description Default
ax_idx int

Axis index: 1 for x-axis, 2 for y-axis, 3 for z-axis.

required
ax Axes

Axes handle. Default is plt.gca().

None
return_prefix bool

If True, try to use a global SI prefix and return it.

False

Returns:

Name Type Description
prefix str

The global SI prefix string (only if return_prefix is True). Empty string if no global prefix could be used.

Source code in pyvoicebox/v_xyzticksi.py
def v_xyzticksi(ax_idx, ax=None, return_prefix=False) -> str:
    """Label an axis of a plot using SI multipliers.

    This is the core implementation called by v_xticksi and v_yticksi.

    Parameters
    ----------
    ax_idx : int
        Axis index: 1 for x-axis, 2 for y-axis, 3 for z-axis.
    ax : matplotlib.axes.Axes, optional
        Axes handle. Default is plt.gca().
    return_prefix : bool, optional
        If True, try to use a global SI prefix and return it.

    Returns
    -------
    prefix : str
        The global SI prefix string (only if return_prefix is True).
        Empty string if no global prefix could be used.
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_xyzticksi")
    import matplotlib.ticker as ticker

    if ax is None:
        ax = plt.gca()

    # Get axis properties
    if ax_idx == 1:
        a, b = ax.get_xlim()
        scale = ax.get_xscale()
        axis_obj = ax.xaxis
    elif ax_idx == 2:
        a, b = ax.get_ylim()
        scale = ax.get_yscale()
        axis_obj = ax.yaxis
    else:
        if hasattr(ax, 'get_zlim'):
            a, b = ax.get_zlim()
            scale = ax.get_zscale() if hasattr(ax, 'get_zscale') else 'linear'
            axis_obj = ax.zaxis
        else:
            return ''

    if a >= b:
        return ''

    # Get current tick positions
    ticks = axis_obj.get_ticklocs()
    ticks = ticks[(ticks >= a) & (ticks <= b)]

    if len(ticks) == 0:
        return ''

    # Determine if we can use a global SI prefix
    max_abs = max(abs(a), abs(b))
    if max_abs == 0:
        return ''

    if return_prefix:
        # Try to find a global SI prefix
        e = int(np.floor(np.log10(max_abs)))
        gi = 3 * (e // 3)
        gi = max(-24, min(24, gi))
        prefix = _SI_PREFIXES[gi // 3 + 8]

        # Scale all ticks
        g = 10.0 ** gi
        scaled_ticks = ticks / g

        # Create labels
        labels = []
        for t in scaled_ticks:
            if t == 0:
                labels.append('0')
            else:
                # Determine appropriate decimal places
                if t == int(t):
                    labels.append(f'{int(t)}')
                else:
                    labels.append(f'{t:g}')

        if ax_idx == 1:
            ax.set_xticks(ticks)
            ax.set_xticklabels(labels)
        elif ax_idx == 2:
            ax.set_yticks(ticks)
            ax.set_yticklabels(labels)
        else:
            ax.set_zticks(ticks)
            ax.set_zticklabels(labels)

        return prefix
    else:
        # Format each tick with its own SI prefix
        labels = []
        for t in ticks:
            if t == 0:
                labels.append('0')
            else:
                abs_t = abs(t)
                e = int(np.floor(np.log10(abs_t)))
                si = 3 * (e // 3)
                si = max(-24, min(24, si))
                scaled = t / (10.0 ** si)
                prefix = _SI_PREFIXES[si // 3 + 8]
                if scaled == int(scaled):
                    label = f'{int(scaled)}'
                else:
                    label = f'{scaled:g}'
                if prefix:
                    label += prefix
                labels.append(label)

        if ax_idx == 1:
            ax.set_xticks(ticks)
            ax.set_xticklabels(labels)
        elif ax_idx == 2:
            ax.set_yticks(ticks)
            ax.set_yticklabels(labels)
        else:
            ax.set_zticks(ticks)
            ax.set_zticklabels(labels)

        return ''

v_xtickint

V_XTICKINT - Remove non-integer ticks from x-axis.

v_xtickint

v_xtickint(ax=None) -> ndarray

Remove non-integer tick marks from the x-axis.

Parameters:

Name Type Description Default
ax Axes

Axes handle. Default is plt.gca().

None

Returns:

Name Type Description
xtick ndarray

Array of remaining integer tick positions.

Source code in pyvoicebox/v_xtickint.py
def v_xtickint(ax=None) -> np.ndarray:
    """Remove non-integer tick marks from the x-axis.

    Parameters
    ----------
    ax : matplotlib.axes.Axes, optional
        Axes handle. Default is plt.gca().

    Returns
    -------
    xtick : ndarray
        Array of remaining integer tick positions.
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_xtickint")
    if ax is None:
        ax = plt.gca()

    xtick = ax.get_xticks()
    int_ticks = xtick[np.round(xtick) == xtick]
    ax.set_xticks(int_ticks)
    return int_ticks

v_ytickint

V_YTICKINT - Remove non-integer ticks from y-axis.

v_ytickint

v_ytickint(ax=None) -> ndarray

Remove non-integer tick marks from the y-axis.

Parameters:

Name Type Description Default
ax Axes

Axes handle. Default is plt.gca().

None

Returns:

Name Type Description
ytick ndarray

Array of remaining integer tick positions.

Source code in pyvoicebox/v_ytickint.py
def v_ytickint(ax=None) -> np.ndarray:
    """Remove non-integer tick marks from the y-axis.

    Parameters
    ----------
    ax : matplotlib.axes.Axes, optional
        Axes handle. Default is plt.gca().

    Returns
    -------
    ytick : ndarray
        Array of remaining integer tick positions.
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_ytickint")
    if ax is None:
        ax = plt.gca()

    ytick = ax.get_yticks()
    int_ticks = ytick[np.round(ytick) == ytick]
    ax.set_yticks(int_ticks)
    return int_ticks

v_texthvc

V_TEXTHVC - Write text on graph with specified alignment and colour.

v_texthvc

v_texthvc(
    x, y, t, p=None, q=None, r=None, ax=None
) -> ndarray

Write text on graph with specified alignment and colour.

Parameters:

Name Type Description Default
x float or array_like

X-position. If length-2 array [x0, k], positions at x0 + k*axis_width.

required
y float or array_like

Y-position. If length-2 array [y0, k], positions at y0 + k*axis_height.

required
t str

Text string to display.

required
p str

Alignment/colour string 'hvc' where: h = horizontal: l=left, c/m=center, r=right v = vertical: t=top, p=cap, c/m=middle, l=baseline, b=bottom c = colour: r,g,b,c,m,y,k,w If h or v is uppercase, position is normalized (0 to 1).

None
q array_like or str

Alternative colour as [r, g, b] or a colour string.

None
r optional

Legacy colour argument (for 6-argument compatibility).

None
ax Axes

Axes handle. Default is plt.gca().

None

Returns:

Name Type Description
text_obj Text

The text object.

Source code in pyvoicebox/v_texthvc.py
def v_texthvc(x, y, t, p=None, q=None, r=None, ax=None) -> np.ndarray:
    """Write text on graph with specified alignment and colour.

    Parameters
    ----------
    x : float or array_like
        X-position. If length-2 array [x0, k], positions at x0 + k*axis_width.
    y : float or array_like
        Y-position. If length-2 array [y0, k], positions at y0 + k*axis_height.
    t : str
        Text string to display.
    p : str, optional
        Alignment/colour string 'hvc' where:
        h = horizontal: l=left, c/m=center, r=right
        v = vertical: t=top, p=cap, c/m=middle, l=baseline, b=bottom
        c = colour: r,g,b,c,m,y,k,w
        If h or v is uppercase, position is normalized (0 to 1).
    q : array_like or str, optional
        Alternative colour as [r, g, b] or a colour string.
    r : optional
        Legacy colour argument (for 6-argument compatibility).
    ax : matplotlib.axes.Axes, optional
        Axes handle. Default is plt.gca().

    Returns
    -------
    text_obj : matplotlib.text.Text
        The text object.
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_texthvc")
    if ax is None:
        ax = plt.gca()

    ha_map = {'l': 'left', 'c': 'center', 'm': 'center', 'r': 'right'}
    va_map = {'t': 'top', 'p': 'top', 'c': 'center', 'm': 'center', 'l': 'baseline', 'b': 'bottom'}

    kwargs = {}

    if p is None:
        # No alignment or colour specified
        return ax.text(x, y, t)

    if r is not None and q is not None and p is not None:
        # 6-argument legacy: p=ha, q=va, r=color
        kwargs['horizontalalignment'] = p
        kwargs['verticalalignment'] = q
        kwargs['color'] = r
        return ax.text(x, y, t, **kwargs)

    # Determine colour
    if q is not None:
        color = q
    elif len(p) >= 3:
        c_char = p[2:]
        if len(c_char) == 1 and c_char in _COLOR_MAP:
            color = _COLOR_MAP[c_char]
        else:
            color = c_char
    else:
        color = 'black'

    # Horizontal alignment
    h_char = p[0]
    h_lower = h_char.lower()
    if h_lower not in ha_map:
        raise ValueError(f'Invalid horizontal spec: {h_char}')
    kwargs['horizontalalignment'] = ha_map[h_lower]

    # Vertical alignment
    v_char = p[1]
    v_lower = v_char.lower()
    if v_lower not in va_map:
        raise ValueError(f'Invalid vertical spec: {v_char}')
    kwargs['verticalalignment'] = va_map[v_lower]

    kwargs['color'] = color

    # Handle x positioning
    x = np.atleast_1d(np.asarray(x, dtype=float))
    if len(x) > 1:
        xlim = ax.get_xlim()
        if ax.get_xscale() == 'log':
            x_val = np.exp(np.log(x[0]) + np.log(xlim[1] / xlim[0]) * x[1])
        else:
            x_val = x[0] + (xlim[1] - xlim[0]) * x[1]
    else:
        x_val = x[0]
        if h_char == h_char.upper() and h_char.upper() != h_char.lower():
            xlim = ax.get_xlim()
            if ax.get_xscale() == 'log':
                x_val = np.exp(np.log(xlim[0]) * (1 - x_val) + np.log(xlim[1]) * x_val)
            else:
                x_val = xlim[0] * (1 - x_val) + xlim[1] * x_val

    # Handle y positioning
    y = np.atleast_1d(np.asarray(y, dtype=float))
    if len(y) > 1:
        ylim = ax.get_ylim()
        if ax.get_yscale() == 'log':
            y_val = np.exp(np.log(y[0]) + np.log(ylim[1] / ylim[0]) * y[1])
        else:
            y_val = y[0] + (ylim[1] - ylim[0]) * y[1]
    else:
        y_val = y[0]
        if v_char == v_char.upper() and v_char.upper() != v_char.lower():
            ylim = ax.get_ylim()
            if ax.get_yscale() == 'log':
                y_val = np.exp(np.log(ylim[0]) * (1 - y_val) + np.log(ylim[1]) * y_val)
            else:
                y_val = ylim[0] * (1 - y_val) + ylim[1] * y_val

    return ax.text(x_val, y_val, t, **kwargs)

v_cblabel

V_CBLABEL - Add a label to a colorbar.

v_cblabel

v_cblabel(label, h=None, ax=None) -> ndarray

Add a label to a colorbar.

Parameters:

Name Type Description Default
label str

Label string for the colorbar.

required
h matplotlib colorbar, axes, or figure

Handle of the colorbar, axis, or figure. If None, searches the current figure for a colorbar.

None
ax Axes

The axes to search for a nearby colorbar (alternative to h).

None

Returns:

Name Type Description
cb matplotlib colorbar or axes

Handle of the colorbar that was labelled.

Source code in pyvoicebox/v_cblabel.py
def v_cblabel(label, h=None, ax=None) -> np.ndarray:
    """Add a label to a colorbar.

    Parameters
    ----------
    label : str
        Label string for the colorbar.
    h : matplotlib colorbar, axes, or figure, optional
        Handle of the colorbar, axis, or figure.
        If None, searches the current figure for a colorbar.
    ax : matplotlib.axes.Axes, optional
        The axes to search for a nearby colorbar (alternative to h).

    Returns
    -------
    cb : matplotlib colorbar or axes
        Handle of the colorbar that was labelled.
    """
    from pyvoicebox._compat import _require_matplotlib
    plt = _require_matplotlib("v_cblabel")
    import numpy as np

    if h is None and ax is None:
        fig = plt.gcf()
    elif h is not None:
        fig = h
    else:
        fig = ax

    # Helper to find centre of axes position
    def _centre(pos):
        return np.array([pos.x0 + pos.width / 2, pos.y0 + pos.height / 2])

    # If given a colorbar axes directly
    if hasattr(fig, 'get_position') and hasattr(fig, 'colorbar'):
        # This is already a colorbar mappable axes -- just label it
        fig.set_label(label)
        return fig

    # If it's an Axes object
    if hasattr(fig, 'get_position') and hasattr(fig, 'get_lines'):
        # It's an axes -- find colorbars on its parent figure
        target_centre = _centre(fig.get_position())
        parent = fig.get_figure()
        if parent is None:
            raise ValueError('Cannot find parent figure')

        # Find all colorbar axes
        cb_axes = [a for a in parent.get_axes()
                   if hasattr(a, '_colorbar_info') or
                   (hasattr(a, 'get_label') and a.get_label() == '<colorbar>')]
        if not cb_axes:
            raise ValueError('There is no colour bar on this figure')

        if len(cb_axes) == 1:
            cb = cb_axes[0]
        else:
            # Find nearest colorbar
            best = None
            best_dist = float('inf')
            for cba in cb_axes:
                c = _centre(cba.get_position())
                d = np.sum((c - target_centre) ** 2)
                if d < best_dist:
                    best_dist = d
                    best = cba
            cb = best

        cb.set_ylabel(label)
        return cb

    # If it's a Figure object
    if hasattr(fig, 'get_axes'):
        cb_axes = [a for a in fig.get_axes()
                   if hasattr(a, '_colorbar_info') or
                   (hasattr(a, 'get_label') and a.get_label() == '<colorbar>')]
        if not cb_axes:
            raise ValueError('There is no colour bar on this figure')

        if len(cb_axes) == 1:
            cb = cb_axes[0]
        else:
            # Find nearest to current axes
            try:
                cur_ax = plt.gca()
                target_centre = _centre(cur_ax.get_position())
            except Exception:
                target_centre = np.array([0.5, 0.5])

            best = None
            best_dist = float('inf')
            for cba in cb_axes:
                c = _centre(cba.get_position())
                d = np.sum((c - target_centre) ** 2)
                if d < best_dist:
                    best_dist = d
                    best = cba
            cb = best

        cb.set_ylabel(label)
        return cb

    raise ValueError(f'h argument must be colorbar, axis or figure handle')

Number formatting

v_sprintsi

V_SPRINTSI - Print value with SI multiplier.

v_sprintsi

v_sprintsi(x, d=-3, w=0, u=' ') -> str

Format a number with an SI multiplier prefix.

Parameters:

Name Type Description Default
x float

Value to print.

required
d int

Decimal places (+ve) or significant digits (-ve). Default -3.

-3
w int

Minimum total width. Default 0.

0
u str

Unit string. Default ' '.

' '

Returns:

Name Type Description
s str

Formatted string.

Source code in pyvoicebox/v_sprintsi.py
def v_sprintsi(x, d=-3, w=0, u=' ') -> str:
    """Format a number with an SI multiplier prefix.

    Parameters
    ----------
    x : float
        Value to print.
    d : int, optional
        Decimal places (+ve) or significant digits (-ve). Default -3.
    w : int, optional
        Minimum total width. Default 0.
    u : str, optional
        Unit string. Default ' '.

    Returns
    -------
    s : str
        Formatted string.
    """
    f = 'qryzafpnum kMGTPEZYRQ'
    f0 = f.index(' ')
    emin = 3 - 3 * f0
    emax = 3 * (len(f) - f0)

    if x == 0:
        e = 0
    else:
        e = int(np.floor(np.log10(abs(x))))

    k = int(np.floor(max(emin, min(emax, e)) / 3))
    dp = max(0, d, 3 * k - d - e - 1)

    if w <= 0 and dp:
        w = abs(w)
        scaled = round(x * 10 ** (dp - 3 * k))
        remainder = scaled % (10 ** dp)
        # Find trailing zeros to eliminate
        for i in range(dp, 0, -1):
            if remainder % (10 ** i) == 0 and i < dp:
                continue
            else:
                break
        # Count how many trailing zeros
        trail = 0
        for i in range(dp, 0, -1):
            if int(remainder) % (10 ** i) == 0:
                trail = i
                break
        dp = dp - trail

    scaled_val = x * 10 ** (-3 * k)
    prefix = f[k + f0] if k != 0 else ''

    if u and u[0] == ' ':
        if k != 0:
            num_str = f'{scaled_val:{max(w - 2, 0)}.{dp}f}'
            s = f'{num_str} {prefix}{u[1:]}'
        else:
            num_str = f'{scaled_val:{max(w - 1, 0)}.{dp}f}'
            s = f'{num_str} {u[1:]}'
    else:
        if k != 0:
            num_str = f'{scaled_val:{max(w - 2, 0)}.{dp}f}'
            s = f'{num_str}{prefix}{u}'
        else:
            num_str = f'{scaled_val:{max(w - 1, 0)}.{dp}f}'
            s = f'{num_str}{u}'

    return s

v_sprintcpx

V_SPRINTCPX - Format a complex number for printing.

v_sprintcpx

v_sprintcpx(z, f='g') -> str

Format a complex number for printing.

Parameters:

Name Type Description Default
z complex

Complex number to format.

required
f str

Format string. May include 'i' or 'j' for sqrt(-1) symbol. Default 'g'.

'g'

Returns:

Name Type Description
s str

Formatted string.

Source code in pyvoicebox/v_sprintcpx.py
def v_sprintcpx(z, f='g') -> str:
    """Format a complex number for printing.

    Parameters
    ----------
    z : complex
        Complex number to format.
    f : str, optional
        Format string. May include 'i' or 'j' for sqrt(-1) symbol.
        Default 'g'.

    Returns
    -------
    s : str
        Formatted string.
    """
    if not f:
        f = 'g'

    # Determine sqrt(-1) symbol
    if 'i' in f:
        ij = 'i'
    else:
        ij = 'j'

    # Remove i/j from format
    f = f.replace('i', '').replace('j', '')
    if not f:
        f = 'g'

    a = np.real(z)
    b = np.imag(z)

    fmt = f'{f}'

    if a == 0 and b == 0:
        s = format(0, fmt)
    elif b == 0:
        s = format(a, fmt)
    elif a == 0:
        s = f'{format(b, fmt)}{ij}'
    else:
        if b > 0:
            s = f'{format(a, fmt)}+{format(b, fmt)}{ij}'
        else:
            s = f'{format(a, fmt)}{format(b, fmt)}{ij}'

    return s

v_bitsprec

V_BITSPREC - Round values to a specified fixed or floating precision.

v_bitsprec

v_bitsprec(x, n=None, mode='sne') -> ndarray

Round values to a specified number of bits precision.

Parameters:

Name Type Description Default
x array_like

Input values.

required
n int

Number of bits.

None
mode str

Mode string 'uvw' where: u: 's' = significant bits (default), 'f' = fixed point v: 'n' = nearest, 'p' = toward +inf, 'm' = toward -inf, 'z' = toward zero w: 'e' = even (default), 'o' = odd, 'a' = away from zero, 'p'/'m' = as v

'sne'

Returns:

Name Type Description
y ndarray

Rounded values.

Source code in pyvoicebox/v_bitsprec.py
def v_bitsprec(x, n=None, mode='sne') -> np.ndarray:
    """Round values to a specified number of bits precision.

    Parameters
    ----------
    x : array_like
        Input values.
    n : int, optional
        Number of bits.
    mode : str, optional
        Mode string 'uvw' where:
        u: 's' = significant bits (default), 'f' = fixed point
        v: 'n' = nearest, 'p' = toward +inf, 'm' = toward -inf, 'z' = toward zero
        w: 'e' = even (default), 'o' = odd, 'a' = away from zero, 'p'/'m' = as v

    Returns
    -------
    y : ndarray
        Rounded values.
    """
    x = np.asarray(x, dtype=float)

    if mode[0] == 'f':
        e = np.zeros_like(x, dtype=int)
    else:
        # Decompose x = f * 2^e where 0.5 <= |f| < 1
        f, e = np.frexp(x)
        x = f
        e = e.astype(int)

    n = int(n) if n is not None else 0
    xn = np.ldexp(x, n)

    en = (e - n).astype(int)

    if mode[1] == 'p':
        y = np.ldexp(np.ceil(xn), en)
    elif mode[1] == 'm':
        y = np.ldexp(np.floor(xn), en)
    elif mode[1] == 'z':
        y = np.ldexp(np.fix(xn), en)
    else:  # 'n' - round to nearest
        w = mode[2] if len(mode) > 2 else 'e'
        if w == 'a':
            y = np.ldexp(np.round(xn), en)
        elif w == 'p':
            y = np.ldexp(np.floor(xn + 0.5), en)
        elif w == 'm':
            y = np.ldexp(np.ceil(xn - 0.5), en)
        elif w == 'e':
            z = np.ldexp(x, n - 1)
            y = np.ldexp(
                np.floor(xn + 0.5) - np.floor(z + 0.75) + np.ceil(z - 0.25),
                en
            )
        elif w == 'o':
            z = np.ldexp(x, n - 1)
            y = np.ldexp(
                np.ceil(xn - 0.5) + np.floor(z + 0.75) - np.ceil(z - 0.25),
                en
            )
        else:
            y = np.ldexp(np.round(xn), en)

    return y