Skip to content

API reference

The public API is curated in melanopy.__all__. Everything below is importable directly from the top-level package, e.g. import melanopy as mp; mp.rate_colormap(...).

Rating

rate_colormap

rate_colormap(colors, panel='representative', profile=False)

Rate an sRGB ramp on the melanopic axis (display white = 1.0).

Both summary numbers are luminance-weighted and ignore near-black pixels (which emit almost nothing), so neither is dominated by the dark end of the ramp.

Parameters:

Name Type Description Default
colors array - like

An (N, 3) sRGB ramp, values in [0, 1].

required
panel str

Display archetype selecting the per-primary coefficients (see :data:coeffs.PANELS).

'representative'
profile bool

When True, also return the per-position arrays behind the summary numbers.

False

Returns:

Type Description
dict

melanopic_ratio — the M/P mean (luminance-weighted axis position; < 1 protective,

1 alerting); mp_spread — the luminance-weighted spread of the per-position ratio (a tight spread reads as a "pure" ramp); and range — its (min, max) over the emitting ramp. With profile=True the dict also carries positions (the [0, 1] data grid), ratios (per-position M/P, NaN where the colour emits ~nothing), and luminance (the per-position photopic luminance, i.e. the weights) — enough to plot the M/P profile.

Examples:

>>> import matplotlib.pyplot as plt, numpy as np, melanopy as mp
>>> c = plt.get_cmap("viridis")(np.linspace(0, 1, 256))[:, :3]
>>> mp.rate_colormap(c)
{'melanopic_ratio': 0.834, 'mp_spread': 0.556, 'range': (0.395, 3.069)}

melanopic_ratio

melanopic_ratio(rgb, panel='representative')

Melanopic ratio (M/P) of one or more sRGB colours, normalized so display white = 1.0.

< 1 is protective (warm, low melatonin drive); > 1 is alerting (cool, blue-rich).

Parameters:

Name Type Description Default
rgb array - like

A single (3,) sRGB colour or an (N, 3) array, values in [0, 1].

required
panel str

Display archetype selecting the per-primary coefficients (see :data:coeffs.PANELS).

'representative'

Returns:

Type Description
ndarray

The melanopic ratio per input colour.

Examples:

>>> import melanopy as mp
>>> mp.melanopic_ratio([1, 1, 1])  # display white is the unit
array([1.])
>>> mp.melanopic_ratio([1, 0, 0])  # pure red — protective
array([0.002])
>>> mp.melanopic_ratio([0, 0, 1])  # pure blue — strongly alerting
array([8.695])

circadia_rating

circadia_rating(alpha, *, panel='representative')

Rated melanopic ratio of a Circadia map at alpha — the physical number behind the dial.

alpha is a geometric position on the OKLab morph, not a melanopic ratio, and the M/P a viewer actually receives is panel-dependent. This composes the generator and the rater so a live UI (e.g. a labelled slider) can show the rated M/P for its configured panel rather than bare alpha. The recompute is cheap (a vectorized 256-point rating); a hot UI may memoize on (round(alpha, 3), panel).

Parameters:

Name Type Description Default
alpha float

Position on the Circadia axis in [0, 1] (0 protective/warm, 1 alerting/cool).

required
panel str

Display archetype selecting the per-primary coefficients (see :data:coeffs.PANELS).

'representative'

Returns:

Type Description
tuple of float

(melanopic_ratio, mp_spread) — the M/P mean (axis position; < 1 protective, > 1 alerting) and its luminance-weighted spread, both for panel.

Examples:

>>> import melanopy as mp
>>> mp.circadia_rating(0.0)  # Sodium endpoint — protective
(0.288, 0.067)
>>> mp.circadia_rating(0.55)  # Equilux — circadian-neutral (M/P ≈ 1)
(0.999, 0.16)
>>> mp.circadia_rating(1.0)  # Xenon endpoint — alerting
(1.728, 0.423)

Generator — the Circadia family

The circadia(alpha) family walks from protective (warm) to alerting (cool) while holding lightness and CVD-recoverability fixed. Each member is a drop-in matplotlib colormap:

import matplotlib.pyplot as plt
import numpy as np

import melanopy as mp

grad = np.linspace(0, 1, 256).reshape(1, -1)
fig, axes = plt.subplots(5, 1, figsize=(7, 3))
for ax, alpha in zip(axes, [0.0, 0.25, 0.55, 0.75, 1.0]):
    ax.imshow(grad, aspect="auto", cmap=mp.circadia(alpha, as_cmap=True))
    ax.set_ylabel(f"α = {alpha}", rotation=0, ha="right", va="center")
    ax.set_xticks([])
    ax.set_yticks([])
plt.show()
2026-07-10T15:21:21.970676 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/

circadia

circadia(alpha, n=256, as_cmap=False, name=None)

One member of the Circadia family — a perceptually-uniform, CVD-safe ramp at dial alpha.

alpha sets the warm→cool colour temperature: 0 is protective (Sodium, warm, low melanopic), 1 is alerting (Xenon, cool, high melanopic), and ~0.55 is the M/P ≈ 1 crossover. It is the design input; read the emergent melanopic ratio back with :func:circadia_rating or :func:rate_colormap.

Parameters:

Name Type Description Default
alpha float

Position on the Circadia axis in [0, 1].

required
n int

Number of samples in the ramp.

256
as_cmap bool

Return a :class:matplotlib.colors.ListedColormap instead of the raw array.

False
name str

Name for the colormap when as_cmap=True (default "circadia_<alpha>").

None

Returns:

Type Description
ndarray or ListedColormap

An (n, 3) sRGB array, or a colormap when as_cmap=True.

Examples:

>>> import melanopy as mp
>>> mp.circadia(0.3).shape
(256, 3)
>>> cmap = mp.circadia(0.3, as_cmap=True)  # drop-in matplotlib colormap
>>> cmap.name
'circadia_0.30'

circadian_cmap is an alias of circadia.

circadia_sweep

circadia_sweep(n=256, as_cmap=False, name=None)

Full-axis sequential map sweeping protective (Sodium, warm) -> alerting (Xenon, cool), with the melanopic ratio rising ~linearly along the ramp -- the "data axis is the melanopic axis" teaching map. The shared monotone OKLab lightness keeps it ordered and CVD-recoverable; the positions are calibrated against the rater so M/P is linear in the data value. This is the one melanopic-aware generator (via a local import); circadia itself stays pure OKLab geometry.

Examples:

>>> import melanopy as mp
>>> mp.circadia_sweep().shape
(256, 3)
>>> mp.circadia_sweep(as_cmap=True).name
'circadia_sweep'

circadia_diverging

circadia_diverging(n=256, as_cmap=False, name=None)

Diverging map for signed data: warm Sodium (protective, M/P<1) <- light neutral centre (M/P=1) -> cool Xenon (alerting, M/P>1). Each arm has monotone lightness, but -- like most diverging maps -- the arms are told apart across zero by hue, so it is NOT CVD-order-recoverable; use the sequential maps where CVD-safety matters.

Examples:

>>> import melanopy as mp
>>> mp.circadia_diverging(as_cmap=True).name  # pass to imshow(..., vmin=-x, vmax=x)
'circadia_diverging'

circadia_sweep walks the whole axis along one ramp (a teaching map whose melanopic ratio rises ~linearly with the data value); circadia_diverging is a warm↔cool diverging map, neutral at the zero crossing:

import matplotlib.pyplot as plt
import numpy as np

import melanopy as mp

grad = np.linspace(0, 1, 256).reshape(1, -1)
maps = [("circadia_sweep", mp.circadia_sweep(as_cmap=True)),
        ("circadia_diverging", mp.circadia_diverging(as_cmap=True))]
fig, axes = plt.subplots(2, 1, figsize=(7, 1.2))
for ax, (name, cmap) in zip(axes, maps):
    ax.imshow(grad, aspect="auto", cmap=cmap)
    ax.set_title(name, loc="left", fontsize=9)
    ax.set_axis_off()
plt.show()
2026-07-10T15:21:22.098066 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/

The named anchors SODIUM (alpha=0.0), EQUILUX (alpha=0.55, the M/P = 1 crossover), and XENON (alpha=1.0) are exported as ready-made matplotlib.colors.ListedColormap objects.

matplotlib integration

register

register()

Register sodium/xenon/equilux + circadia_sweep/circadia_diverging as matplotlib colormaps.

After calling this, the maps are available by name to any matplotlib call. Safe to call more than once.

Examples:

>>> import matplotlib.pyplot as plt, numpy as np, melanopy as mp
>>> mp.register()
>>> Z = np.add.outer(np.linspace(0, 1, 200), np.linspace(0, 1, 200))
>>> plt.imshow(Z, cmap="sodium")

Accent palette

CIRCADIA_ACCENT (with CIRCADIA_ACCENT_NAMES) is a small set of vivid colours for qualitative marks — ticks, points, event lines — drawn over a Circadia colormap fill, where ordinary qualitative colours can be lost against the fill or clash with it. They are chosen to sit outside the family's colour footprint across the whole axis (so they contrast with warm and cool fills) and to stay mutually distinct under colour-vision deficiency — which confines them to the magenta/violet/green arc. Vivid by design.

>>> import melanopy as mp
>>> mp.CIRCADIA_ACCENT_NAMES
['orchid', 'grape', 'emerald', 'lime']
import matplotlib.pyplot as plt

import melanopy as mp

fig, ax = plt.subplots(figsize=(7, 1.2))
for i, (color, name) in enumerate(zip(mp.CIRCADIA_ACCENT, mp.CIRCADIA_ACCENT_NAMES)):
    ax.add_patch(plt.Rectangle((i, 0), 0.9, 1, color=color))
    ax.text(i + 0.45, -0.12, name, ha="center", va="top")
ax.set(xlim=(0, 4), ylim=(0, 1))
ax.set_axis_off()
plt.show()
2026-07-10T15:21:22.128119 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/

For qualitative marks that are not over a fill, the melanopic axis does not apply (small marks emit negligibly) — any CVD-safe qualitative palette (e.g. ColorBrewer, Okabe–Ito) serves.

Adding a measured panel

The built-in panels (representative, led_lcd, oled, wide_gamut) are spectral archetypes, not measurements — so a reported M/P is indicative, not metrological. The protective ↔ alerting ranking is stable across panels (Spearman ρ ≥ 0.99), so relative comparisons hold on any panel; only the absolute M/P needs a measured one.

For research-grade work, measure the three primary SPDs (R, G, B at full drive) on the package's 1 nm grid (melanopy.spectra.WL, 380–780 nm) and derive the coefficients:

>>> from melanopy.spectra import coefficients_from_primaries
>>> coeffs = coefficients_from_primaries({"R": spd_r, "G": spd_g, "B": spd_b})  # doctest: +SKIP

Add the result to melanopy.coeffs.PANELS as a new keyed row (e.g. "lab_monitor": {...}); the panel="lab_monitor" argument then threads through rate_colormap, melanopic_ratio, and circadia_rating.

coefficients_from_primaries

coefficients_from_primaries(primaries, smel=None, vlam=None)

Derive the three per-primary M/P coefficients from measured primary SPDs.

Parameters:

Name Type Description Default
primaries dict

{'R', 'G', 'B'} -> SPD array on :data:WL (the 380–780 nm, 1 nm grid).

required
smel array - like

Override the melanopic action spectrum / V(λ); default to the shipped CIE tables.

None
vlam array - like

Override the melanopic action spectrum / V(λ); default to the shipped CIE tables.

None

Returns:

Type Description
dict

{'R', 'G', 'B'} -> the melanopic/photopic ratio of each primary.

Examples:

>>> from melanopy.spectra import coefficients_from_primaries, panel_primaries
>>> coefficients_from_primaries(panel_primaries("representative"))
{'R': 0.003, 'G': 0.656, 'B': 10.968}