Harmonic Geometry

Contents

Harmonic Geometry#

The biotuner.harmonic_geometry subpackage produces pure-data geometric structures (curves, fields, point clouds, surfaces, polygons, fractals) derived from harmonic inputs — ratios, peaks, amplitudes, phases. It is organized as a set of submodules, each focused on one family of structures. Rendering is the responsibility of downstream layers.

biotuner.harmonic_geometry#

Module type: Subpackage

Pure-data geometric structures derived from harmonic inputs (ratios, peaks, amplitudes, phases). This module produces structured numpy / dataclass output; rendering is the responsibility of downstream layers.

Submodules: chladni, extensions, fractal, generative, geometry_3d, geometry_data, harmonograph, inputs, interference_patterns, lissajous, metrics, plotting, polygon_circular, spherical_harmonics, transitions.

class GeometryData(geom_type: str, coordinates: ~numpy.ndarray | ~typing.List[~numpy.ndarray], edges: ~numpy.ndarray | None = None, faces: ~numpy.ndarray | None = None, weights: ~numpy.ndarray | None = None, field_grid: ~typing.Tuple[~numpy.ndarray, ...] | None = None, parameters: ~typing.Dict[str, ~typing.Any] = <factory>, metadata: ~typing.Dict[str, ~typing.Any] = <factory>)[source]#

Bases: object

Container for one piece of geometric output.

Parameters:
  • geom_type (str) – One of GEOM_TYPES. Describes the layout of coordinates and which optional fields are populated.

  • coordinates (ndarray or list of ndarray) – The primary geometric data. Exact shape depends on geom_type; see module docstring.

  • edges (ndarray, optional) – Integer index pairs of shape (E, 2) for graph and tree types. Indices are into coordinates along axis 0.

  • faces (ndarray, optional) – Integer triangle indices of shape (F, 3) for mesh_3d.

  • weights (ndarray, optional) – Per-point or per-edge scalar weights. Shape must be broadcast- compatible with the entity it annotates.

  • field_grid (tuple of ndarray, optional) – (X, Y) or (X, Y, Z) meshgrid arrays for field_2d / field_3d types.

  • parameters (dict) – Snapshot of the inputs that produced this geometry (e.g. the HarmonicInput fields). Free-form; serialized via pickle.

  • metadata (dict) – Geom-specific annotations (e.g. lobe count for Lissajous, mode multiplicity for Chladni). Free-form; serialized via pickle.

Notes

Files written by save() are pickle-backed. Do not load GeometryData files from untrusted sources.

geom_type: str#
coordinates: ndarray | List[ndarray]#
edges: ndarray | None = None#
faces: ndarray | None = None#
weights: ndarray | None = None#
field_grid: Tuple[ndarray, ...] | None = None#
parameters: Dict[str, Any]#
metadata: Dict[str, Any]#
save(path: str) None[source]#

Persist to path as a numpy .npz file.

parameters and metadata are pickled into a 0-d object array. Lists of arrays (e.g. for curve_set_2d) are flattened with index suffixes so they round-trip through np.savez.

Loading is the inverse via load(). Because pickled data is used, only load files from sources you trust.

classmethod load(path: str) GeometryData[source]#

Load a GeometryData previously written by save().

Only load files from trusted sources — parameters and metadata are unpickled.

GeomType#

alias of str

class HarmonicInput(ratios: ~typing.List[~fractions.Fraction | float] | None = None, peaks: ~typing.List[float] | None = None, amplitudes: ~typing.List[float] | None = None, phases: ~typing.List[float] | None = None, damping: ~typing.List[float] | None = None, base_freq: float = 1.0, equave: float = 2.0, metadata: ~typing.Dict[str, ~typing.Any] = <factory>)[source]#

Bases: object

Unified harmonic input.

At least one of ratios or peaks must be provided. All list-typed optional fields, if given, must have lengths matching the number of components (len(ratios) or len(peaks)).

Parameters:
  • ratios (list of Fraction or float, optional) – Frequency ratios. Coerced to Fraction when a rational approximation within DEFAULT_MAX_DENOMINATOR exists.

  • peaks (list of float, optional) – Peak frequencies in Hz.

  • amplitudes (list of float, optional) – Linear (not dB) amplitudes per component. Defaults to uniform.

  • phases (list of float, optional) – Phase per component, in radians. Defaults to zeros.

  • damping (list of float, optional) – Decay rate (1/s) per component. Defaults to zeros.

  • base_freq (float, default=1.0) – Reference frequency in Hz. Used when ratios are given without peaks to recover absolute frequencies.

  • equave (float, default=2.0) – Equave width: 2.0 for octaves, 3.0 for tritaves, etc. Must be greater than 1.

  • metadata (dict) – Free-form annotations preserved across constructors and validation.

Notes

Validation is invoked by __post_init__(); constructing an inconsistent HarmonicInput raises ValueError.

ratios: List[Fraction | float] | None = None#
peaks: List[float] | None = None#
amplitudes: List[float] | None = None#
phases: List[float] | None = None#
damping: List[float] | None = None#
base_freq: float = 1.0#
equave: float = 2.0#
metadata: Dict[str, Any]#
n_components() int[source]#

Return the number of harmonic components.

Resolved from ratios first, then peaks. At least one of the two is guaranteed to be present after validate().

to_peaks() ndarray[source]#

Return absolute peak frequencies in Hz as a 1-D float64 array.

If peaks is set, those values are returned directly. Otherwise peaks are reconstructed as base_freq * ratios.

to_ratios() List[Fraction | float][source]#

Return ratios.

If ratios is set, those values are returned. Otherwise ratios are derived as peaks / base_freq and coerced to Fraction.

normalized_amplitudes() ndarray[source]#

Return amplitudes scaled to sum to 1.

If amplitudes is None, returns a uniform distribution over the components.

validate() None[source]#

Raise ValueError if the input is internally inconsistent.

Checked invariants:

  • at least one of ratios / peaks is given,

  • equave > 1 and base_freq > 0,

  • all list-typed fields have matching lengths,

  • all amplitudes are non-negative,

  • all peaks are positive,

  • if both ratios and peaks are given, they agree up to base_freq within a small relative tolerance.

classmethod from_ratios(ratios: Iterable[Fraction | int | float | tuple], base_freq: float = 1.0, amplitudes: Sequence[float] | None = None, phases: Sequence[float] | None = None, damping: Sequence[float] | None = None, equave: float = 2.0, metadata: Dict[str, Any] | None = None) HarmonicInput[source]#

Build from a sequence of ratios.

classmethod from_peaks(peaks: Iterable[float], base_freq: float | None = None, amplitudes: Sequence[float] | None = None, phases: Sequence[float] | None = None, damping: Sequence[float] | None = None, equave: float = 2.0, metadata: Dict[str, Any] | None = None) HarmonicInput[source]#

Build from a sequence of peak frequencies in Hz.

If base_freq is None it is set to the smallest peak so that the implied ratio of the lowest component is exactly 1.

classmethod from_biotuner(bt: Any, equave: float = 2.0) HarmonicInput[source]#

Build from a fitted compute_biotuner (a.k.a. BiotunerObject) instance.

Pulls peaks (required), amps, and peaks_ratios when available. Phase / damping are not currently exposed by biotuner and are left at their defaults.

Parameters:
  • bt (compute_biotuner) – A biotuner object on which peaks_extraction has been called.

  • equave (float, default=2.0)

Raises:
  • AttributeError – If bt lacks a peaks attribute.

  • ValueError – If bt.peaks is empty.

class HarmonicSequence(frames: List[HarmonicInput], times: ndarray | None = None)[source]#

Bases: object

Time-resolved sequence of HarmonicInput frames.

Pairs naturally with the output of biotuner.transitional_harmony and biotuner.harmonic_sequence: each window’s peaks become a frame here, and downstream geometry functions can be applied frame-by-frame via transformations.geometry_sequence.

Parameters:
  • frames (list of HarmonicInput) – At least one frame is required.

  • times (ndarray, optional) – Time of each frame in seconds. If None, frames are assumed to be uniformly spaced at unit intervals.

frames: List[HarmonicInput]#
times: ndarray | None = None#
n_frames() int[source]#

Number of frames in the sequence.

at(t: float) HarmonicInput[source]#

Return the frame nearest to time t.

interpolate(t: float, mode: str = 'log') HarmonicInput[source]#

Return a HarmonicInput interpolated to time t.

Currently supports two-frame interpolation between the bracketing frames. mode selects the space in which ratios / peaks are blended:

  • 'log' (default) — logarithmic, musically correct,

  • 'linear' — straight linear interpolation,

  • 'nearest' — return the nearest frame (no blending).

Frames must have equal n_components; if they do not, this raises ValueError. Richer interpolation (mismatched component counts, phase wrapping, etc.) is the job of transformations.interpolate_input in Phase 6.

classmethod from_biotuner_list(bt_list: Sequence[Any], times: Sequence[float] | None = None, equave: float = 2.0) HarmonicSequence[source]#

Build from a sequence of fitted compute_biotuner objects.

Each biotuner object becomes one frame. Objects with no peaks are skipped; if every object is empty, ValueError is raised.

Parameters:
  • bt_list (sequence of compute_biotuner) – Fitted biotuner objects (peaks_extraction already called).

  • times (sequence of float, optional) – One time per kept frame. If None, frames are uniformly spaced. If provided, must match the number of non-empty biotuner objects.

  • equave (float, default=2.0)

classmethod from_biotuner_group(btg: Any, times: Sequence[float] | None = None, equave: float = 2.0) HarmonicSequence[source]#

Build from a biotuner.biotuner_group.BiotunerGroup instance.

Uses btg.objects (the per-series compute_biotuner instances). BiotunerGroup must have been constructed with store_objects=True (the default).

Parameters:
  • btg (BiotunerGroup) – A group whose compute_peaks has been run.

  • times (sequence of float, optional) – One time per non-empty frame; see from_biotuner_list().

  • equave (float, default=2.0)

Raises:

AttributeError – If btg has no objects attribute, or it is None (e.g. store_objects=False was used).

lissajous_2d(ratio: Fraction | int | float | Tuple[int, int], phase: float = 1.5707963267948966, amps: Tuple[float, float] = (1.0, 1.0), n_points: int = 1000, n_periods: int = 1) GeometryData[source]#

A single 2-D Lissajous curve.

Samples x(t) = A_x · sin(a · t + phase) and y(t) = A_y · sin(b · t) over t [0, · n_periods], where (a, b) is a coprime representation of ratio.

Parameters:
  • ratio (Fraction, int, float, or (int, int)) – Frequency ratio a / b of the x-component to the y-component.

  • phase (float, default=π/2) – Phase shift δ in radians applied to the x-component.

  • amps (tuple of float, default=(1.0, 1.0)) – Amplitudes (A_x, A_y).

  • n_points (int, default=1000) – Number of samples along the curve.

  • n_periods (int, default=1) – Number of fundamental periods to sample. For coprime (a, b) the curve closes after one period.

Returns:

GeometryDatageom_type='curve_2d' with shape (n_points, 2). Metadata includes the coprime (a, b) pair, closure flag, and phase.

lissajous_3d(ratios: Sequence[Fraction | int | float | Tuple[int, int]], phases: Sequence[float] = (0.0, 0.0, 0.0), amps: Sequence[float] = (1.0, 1.0, 1.0), n_points: int = 2000) GeometryData[source]#

A 3-D Lissajous curve.

Samples x_i(t) = A_i · sin(f_i · t + φ_i) for i {0, 1, 2}, where f_i is a coprime integer derived from ratios[i].

When all three f_i are pairwise coprime the resulting curve is a Lissajous knot; this is flagged in metadata['knot'].

Parameters:
  • ratios (sequence of length 3) – Frequencies for x, y, z.

  • phases (sequence of length 3, default=(0, 0, 0)) – Phase per axis in radians.

  • amps (sequence of length 3, default=(1, 1, 1)) – Amplitude per axis.

  • n_points (int, default=2000)

Returns:

GeometryDatageom_type='curve_3d' with shape (n_points, 3).

lissajous_compound(input: HarmonicInput, n_points: int = 2000, n_periods: int = 1) GeometryData[source]#

Sum-of-sinusoids Lissajous from a HarmonicInput.

Treats the input components as a chord. The x-coordinate is the sum of all components phase-shifted by π/2; the y-coordinate is the sum without the phase shift. This collapses an N-component HarmonicInput into a single 2-D Lissajous-like curve.

Parameters:
  • input (HarmonicInput)

  • n_points (int, default=2000)

  • n_periods (int, default=1)

Returns:

GeometryDatageom_type='curve_2d'.

lissajous_pairwise_grid(input: HarmonicInput, n_points: int = 500, phase: float = 1.5707963267948966) List[List[GeometryData]][source]#

Build a 2-D grid of pairwise Lissajous curves from a HarmonicInput.

For an input with N components, returns an N×N nested list where entry [i][j] is the 2-D Lissajous of component i (x-axis) against component j (y-axis). The diagonal contains 1:1 unison curves.

Parameters:
  • input (HarmonicInput)

  • n_points (int, default=500)

  • phase (float, default=π/2)

Returns:

list of list of GeometryDataN × N matrix of curve_2d geometries.

lissajous_phase_drift(ratio: Fraction | int | float | Tuple[int, int], drift_rate: float, duration: float, sr: int = 1000, amps: Tuple[float, float] = (1.0, 1.0)) GeometryData[source]#

Lissajous with a linearly-drifting phase.

The phase δ(t) = drift_rate · t evolves linearly with time, producing the classic “spinning” Lissajous animation when rendered.

Parameters:
  • ratio (Fraction, int, float, or (int, int))

  • drift_rate (float) – Phase drift in radians per second.

  • duration (float) – Total duration in seconds.

  • sr (int, default=1000) – Sample rate (samples per second).

  • amps (tuple of float, default=(1.0, 1.0))

Returns:

GeometryDatageom_type='curve_2d' with shape (int(sr * duration), 2).

lissajous_topology(geom: GeometryData) dict[source]#

Inspect a Lissajous-style curve_2d and return topological summary.

Parameters:

geom (GeometryData) – Must have geom_type='curve_2d'.

Returns:

dict – Keys:

  • 'lobes_x', 'lobes_y' — integer lobe counts (read from metadata when available; otherwise estimated by counting zero crossings on each axis).

  • 'closed' — whether the first and last points coincide within a small tolerance.

  • 'self_intersections' — count of polyline self-intersections (brute-force, O(N²)).

  • 'period_ratio'Fraction representing the a / b ratio when known, else None.

derive_damping_from_linewidth(linewidths: Sequence[float], default: float = 0.01) ndarray[source]#

Convert spectral linewidths (FWHM, Hz) to damping coefficients (1/s).

For a Lorentzian peak with full width at half maximum Δf, the underlying decay rate is π · Δf (since the Lorentzian is the FT of a decaying exponential e^(-π Δf t)).

Non-positive or non-finite linewidths fall back to default.

harmonograph_3d(input: HarmonicInput, duration: float = 30.0, sr: int = 200, axis_assignment: str = 'cyclic') GeometryData[source]#

3-D harmonograph trace.

Parameters:
  • input (HarmonicInput)

  • duration (float, default=30.0)

  • sr (int, default=200)

  • axis_assignment ({‘cyclic’, ‘split’}, default=’cyclic’) –

    • 'cyclic': component i is assigned to axis i % 3.

    • 'split': components are split contiguously into three near-equal blocks for x, y, z.

Returns:

GeometryDatageom_type='curve_3d'.

harmonograph_from_peaks(peaks: Sequence[float], amps: Sequence[float] | None = None, phases: Sequence[float] | None = None, damping: Sequence[float] | None = None, duration: float = 30.0, sr: int = 200) GeometryData[source]#

Convenience: build a lateral harmonograph directly from peak Hz values.

Internally constructs a HarmonicInput and delegates to harmonograph_lateral(). If damping is None, a uniform default of DEFAULT_DAMPING is used.

harmonograph_lateral(input: HarmonicInput, duration: float = 30.0, sr: int = 200, x_components: Sequence[int] | None = None, y_components: Sequence[int] | None = None) GeometryData[source]#

A two-pendulum lateral harmonograph trace.

Parameters:
  • input (HarmonicInput) – Provides peak frequencies, amplitudes, phases, and (optionally) damping per component.

  • duration (float, default=30.0) – Duration of the trace in seconds.

  • sr (int, default=200) – Sample rate in samples per second. Downstream renderers may resample.

  • x_components, y_components (sequence of int, optional) – Indices of components assigned to each axis. If both are None, components alternate (even indices → x, odd → y).

Returns:

GeometryDatageom_type='curve_2d' with shape (int(sr * duration), 2).

harmonograph_rotary(input: HarmonicInput, duration: float = 30.0, sr: int = 200, rotation_freq: float = 0.1) GeometryData[source]#

Lateral harmonograph with an additional slow rotation about the origin.

The lateral trace is rotated by an angle θ(t) = · rotation_freq · t, producing the rosette-like rotary patterns of a circular harmonograph.

Parameters:
  • input (HarmonicInput)

  • duration (float, default=30.0)

  • sr (int, default=200)

  • rotation_freq (float, default=0.1) – Angular drift in Hz applied to the entire trace.

Returns:

GeometryDatageom_type='curve_2d'.

chladni_field_3d_box(modes_3d: Sequence[Tuple[int, int, int]], amps: Sequence[float] | None = None, phases: Sequence[float] | None = None, dimensions: Tuple[float, float, float] = (1.0, 1.0, 1.0), resolution: int = 48) GeometryData[source]#

Standing-wave displacement field in a 3-D box (Dirichlet BC).

u(x, y, z) = Σ_k A_k · sin(l_k π x / Lx) · sin(m_k π y / Ly) · sin(n_k π z / Lz) · cos(φ_k)

Parameters:
  • modes_3d (sequence of (int, int, int)) – Mode triples (l, m, n) with all entries >= 1.

  • amps, phases (sequences of float, optional)

  • dimensions ((float, float, float), default=(1, 1, 1)) – (Lx, Ly, Lz) box dimensions.

  • resolution (int, default=48) – Grid resolution per axis. Total memory scales as O(R³); the default 48³ ≈ 110k cells is a balance for a quick eigen-sum.

Returns:

GeometryDatageom_type='field_3d' with coordinates the (R, R, R) scalar field and field_grid=(X, Y, Z).

chladni_field_circular(modes_radial: Sequence[int], modes_angular: Sequence[int], amps: Sequence[float] | None = None, phases: Sequence[float] | None = None, R: float = 1.0, resolution: int = 256) GeometryData[source]#

Clamped circular membrane displacement.

For each mode k: u_k(r, θ) = J_{n_k}(α_{n_k, m_k} · r / R) · cos(n_k · θ), where α_{n, m} is the m-th positive zero of J_n (m is 1-indexed). Outside the disk the field is set to NaN.

Parameters:
  • modes_radial (sequence of int) – Radial mode index (m, 1-indexed). Same length as modes_angular.

  • modes_angular (sequence of int) – Angular mode index (n, 0). Same length as modes_radial.

  • amps, phases (sequences of float, optional)

  • R (float, default=1.0) – Disk radius.

  • resolution (int, default=256) – Square-grid resolution covering [-R, R]^2.

Returns:

GeometryDatageom_type='field_2d'. Values outside the disk are NaN.

chladni_field_polygon(modes: Sequence[int], n_sides: int, amps: Sequence[float] | None = None, phases: Sequence[float] | None = None, radius: float = 1.0, resolution: int = 128, solver: str = 'fdm') GeometryData[source]#

Clamped regular polygon membrane displacement.

Solves the Dirichlet eigenproblem -∇²ψ = λψ numerically on a rasterized polygon mask, then sums a chosen subset of eigenmodes.

Parameters:
  • modes (sequence of int) – 0-indexed eigenmode indices (0 is the fundamental).

  • n_sides (int) – Polygon side count, >= 3.

  • amps, phases (sequences of float, optional)

  • radius (float, default=1.0) – Circumradius of the polygon.

  • resolution (int, default=128) – Bounding-square grid resolution. Lower than the rectangular default because the FDM eigenproblem is O(n²) to O(n³) in the interior cell count.

  • solver ({‘fdm’, ‘fem’}, default=’fdm’) – 'fem' requires the optional scikit-fem dependency and is not yet wired through; selecting it raises NotImplementedError.

Returns:

GeometryDatageom_type='field_2d'. Cells outside the polygon are NaN.

chladni_field_rectangular(modes: Sequence[Tuple[int, int]], amps: Sequence[float] | None = None, phases: Sequence[float] | None = None, Lx: float = 1.0, Ly: float = 1.0, resolution: int = 256) GeometryData[source]#

Free-edge rectangular plate displacement.

u(x, y) = Σ_k A_k · cos(m_k π x / Lx) · cos(n_k π y / Ly) · cos(φ_k)

Parameters:
  • modes (sequence of (int, int)) – Mode pairs (m, n).

  • amps (sequence of float, optional) – Amplitudes per mode. Defaults to uniform 1 / n_modes.

  • phases (sequence of float, optional) – Phase per mode in radians. Defaults to zeros.

  • Lx, Ly (float, default=1.0) – Plate dimensions.

  • resolution (int, default=256) – Grid resolution along each axis. The output field is (resolution, resolution).

Returns:

GeometryDatageom_type='field_2d' with coordinates the (R, R) field and field_grid=(X, Y) meshgrid arrays.

chladni_from_input(input: HarmonicInput, plate: str = 'rectangular', plate_kwargs: dict | None = None, mode_strategy: str = 'stern_brocot', max_mode: int = 20) GeometryData[source]#

Build a Chladni field from a HarmonicInput.

The input’s ratios are mapped to mode pairs (or triples, for box_3d) via ratios_to_modes(); amplitudes are taken from input.normalized_amplitudes(); phases default to the input’s phases (or zero).

Parameters:
  • input (HarmonicInput)

  • plate ({‘rectangular’, ‘circular’, ‘polygon’, ‘box_3d’}, default=’rectangular’)

  • plate_kwargs (dict, optional) – Extra keyword arguments forwarded to the underlying field builder (e.g. Lx, Ly, resolution, n_sides).

  • mode_strategy (str, default=’stern_brocot’)

  • max_mode (int, default=20)

chladni_nodal_lines(field_data: GeometryData, threshold: float = 0.001) GeometryData[source]#

Extract nodal lines from a 2-D Chladni field via marching squares.

Parameters:
  • field_data (GeometryData) – Must have geom_type='field_2d'.

  • threshold (float, default=1e-3) – Iso-value at which to extract contours. Cells where the field is NaN (e.g. outside a circular plate) are treated as the boundary and skipped during extraction.

Returns:

GeometryDatageom_type='curve_set_2d' — one (N_i, 2) array per contour, with coordinates in the same units as the input field_grid.

chladni_nodal_surfaces(field_3d: GeometryData, threshold: float = 0.001) GeometryData[source]#

Extract a 2-D nodal surface mesh from a 3-D Chladni field.

Parameters:
  • field_3d (GeometryData) – Must have geom_type='field_3d'.

  • threshold (float, default=1e-3) – Iso-value of the marching-cubes extraction.

Returns:

GeometryDatageom_type='mesh_3d' with vertex coordinates in the same units as the input field_grid and triangle faces.

chladni_temporal(input: HarmonicInput, t: float, plate: str = 'rectangular', mode_strategy: str = 'stern_brocot', max_mode: int = 20, **plate_kwargs: Any) GeometryData[source]#

Chladni field at a specific time t, using input as the mode source.

The phases used in the field are shifted by · f_k · t for each component (with f_k taken from input.peaks), implementing the standing-wave time evolution. Suitable for assembling a time sequence of frames.

Parameters:
  • input (HarmonicInput)

  • t (float) – Time in seconds.

  • plate ({‘rectangular’, ‘circular’, ‘polygon’, ‘box_3d’}, default=’rectangular’)

  • mode_strategy (str, default=’stern_brocot’) – Forwarded to ratios_to_modes().

  • max_mode (int, default=20)

  • **plate_kwargs – Forwarded to the underlying field builder.

Returns:

GeometryData

ratios_to_modes(ratios: Iterable[Fraction | int | float | Tuple[int, int]], strategy: str = 'stern_brocot', max_mode: int = 20) List[Tuple[int, int]][source]#

Map a list of ratios to Chladni mode pairs (m, n).

Each ratio r is approximated by a coprime pair (m, n) with m, n max_mode (when feasible).

Parameters:
  • ratios (iterable of Fraction, int, float, or (int, int))

  • strategy ({‘stern_brocot’, ‘continued_fraction’, ‘rounded’, ‘best_simple’}) – Algorithm used to pick (m, n). 'stern_brocot' uses Fraction.limit_denominator() (Stern-Brocot mediant search); 'continued_fraction' walks the continued-fraction convergents and stops at the last one with both terms max_mode; 'rounded' returns (round(r), 1) for each ratio (cheap and coarse); 'best_simple' brute-forces the closest (m, n) pair in [1, max_mode]^2.

  • max_mode (int, default=20)

Returns:

list of (int, int)

ratios_to_modes_lm(ratios: Sequence[float], mode_rule: str = 'zonal', max_l: int = 10, l_rule: str = 'numerator') List[Tuple[int, int]][source]#

Map a sequence of frequency ratios to spherical-harmonic (l, m) pairs.

Each ratio is converted to an integer degree l (via l_rule) and then assigned an order m [-l, l] (via mode_rule).

Parameters:
  • ratios (sequence of float) – Frequency ratios. Negative or zero values raise ValueError.

  • mode_rule ({‘zonal’, ‘sectoral’, ‘chord_balanced’, ‘rounded’}, default=’zonal’) – How to pick the order m for each component:

    • 'zonal'm = 0 for every harmonic. Banded patterns parallel to the equator. Simplest, most legible.

    • 'sectoral'|m| = l. Vertical orange-segment lobes; alternating sign per harmonic so the chord doesn’t reduce to a single mode.

    • 'chord_balanced' — cycles m through {0, ±l, ±l/2} across the harmonics. Mixes zonal / tesseral / sectoral modes for visual variety.

    • 'rounded' — alias of 'zonal'; kept for API parity with chladni.ratios_to_modes().

  • max_l (int, default=10) – Cap on the degree l.

  • l_rule ({‘numerator’, ‘rounded’}, default=’numerator’) – How to convert each ratio r to a degree l:

    • 'numerator' — rationalise r with Fraction.limit_denominator(max_l)() and use the numerator. Maps musical chords cleanly: 4 : 5 : 6 (i.e. ratios 1, 5/4, 3/2) yields l = 4, 5, 6. This is the natural “harmonic-index” mapping and the default.

    • 'rounded'l = int(round(r)), clamped to [0, max_l]. Cheap and coarse; collapses ratios in [1, 2] to l {1, 2}.

Returns:

list of (int, int) – One (l, m) tuple per input ratio.

Notes

Unlike Chladni’s (m, n) pair, where both indices are positive and near-symmetric, here l is degree and m is order with m [-l, l]. The two indices have very different geometric meanings: l controls how many nodal lines the mode has; m controls how those lines are split between latitude and longitude.

single_spherical_harmonic(l: int, m: int, n_theta: int = 128, n_phi: int = 256, real: bool = True) GeometryData[source]#

One spherical-harmonic mode Y_l^m evaluated on a (θ, φ) grid.

Parameters:
  • l (int) – Degree, l >= 0.

  • m (int) – Order, -l <= m <= l.

  • n_theta (int, default=128) – Polar samples in [0, π].

  • n_phi (int, default=256) – Azimuthal samples in [0, 2π].

  • real (bool, default=True) – If True, return the real spherical harmonic; otherwise the complex form (the field is then complex-valued).

Returns:

GeometryDatageom_type='field_2d' with coordinates shape (n_theta, n_phi) and field_grid=(THETA, PHI) meshgrids.

spherical_harmonic_field(modes_lm: Sequence[Tuple[int, int]], amps: Sequence[float] | None = None, phases: Sequence[float] | None = None, n_theta: int = 128, n_phi: int = 256, real: bool = True) GeometryData[source]#

Superposition of spherical-harmonic modes on a (θ, φ) grid.

Ψ(θ, φ) = Σ_k  A_k · cos(φ_k) · Y_{l_k}^{m_k}(θ, φ)

Parameters:
  • modes_lm (sequence of (int, int)) – (l, m) mode pairs, with l >= 0 and |m| <= l.

  • amps (sequence of float, optional) – Per-mode amplitude. Defaults to uniform 1 / n_modes.

  • phases (sequence of float, optional) – Per-mode phase in radians. Defaults to zeros (so cos(φ_k) = 1).

  • n_theta, n_phi (int) – Grid resolution in polar / azimuthal axes.

  • real (bool, default=True) – Use real spherical harmonics (the orthonormal real basis). Set False to keep the complex-valued Y_l^m and let the field be complex.

Returns:

GeometryDatageom_type='field_2d'. coordinates is the (n_theta, n_phi) field; field_grid is the meshgrid of the polar and azimuthal angles.

spherical_harmonic_from_input(input: HarmonicInput, mode_rule: str = 'zonal', max_l: int = 10, l_rule: str = 'numerator', n_theta: int = 128, n_phi: int = 256, real: bool = True) GeometryData[source]#

Build a spherical-harmonic field from a HarmonicInput.

Equivalent to chladni.chladni_from_input() but rendered onto a sphere instead of a bounded plate.

Ratios are mapped to (l, m) modes via ratios_to_modes_lm(), amplitudes are taken from input.normalized_amplitudes(), phases default to input.phases or zeros.

Parameters:
  • input (HarmonicInput)

  • mode_rule ({‘zonal’, ‘sectoral’, ‘chord_balanced’, ‘rounded’})

  • max_l (int, default=10)

  • l_rule ({‘numerator’, ‘rounded’}, default=’numerator’) – See ratios_to_modes_lm().

  • n_theta, n_phi (int) – Grid resolution.

  • real (bool, default=True) – Use real spherical harmonics.

spherical_harmonic_mesh(input: HarmonicInput, epsilon: float = 0.18, mode_rule: str = 'zonal', max_l: int = 10, l_rule: str = 'numerator', n_theta: int = 96, n_phi: int = 192) GeometryData[source]#

Wobbled-radius mesh of a chord’s spherical-harmonic superposition.

Each vertex sits on the unit sphere displaced radially by

r(θ, φ) = 1 + ε · Ψ̂(θ, φ)

where Ψ̂ is the chord’s real-valued spherical-harmonic field rescaled so its peak absolute value is 1. The resulting mesh is the chord-shaped “blob” — concave at nodal lines, convex at antinodes.

Parameters:
  • input (HarmonicInput)

  • epsilon (float, default=0.18) – Maximum radial displacement (fraction of the unit radius). Smaller values keep the mesh close to a sphere; larger values emphasise the modal structure.

  • mode_rule (str, default=’zonal’)

  • max_l (int, default=10)

  • n_theta (int, default=96)

  • n_phi (int, default=192)

Returns:

GeometryDatageom_type='mesh_3d' with

  • coordinates shape (V, 3): vertex positions in . V = n_theta * n_phi.

  • faces shape (F, 3): triangle indices into coordinates. The mesh is a UV-sphere triangulation of the (n_theta - 1) × (n_phi - 1) quad grid (two triangles per quad).

  • weights shape (V,): the field amplitude at each vertex, useful for renderer colouring.

spherical_harmonic_temporal(input: HarmonicInput, t: float, mode_rule: str = 'zonal', max_l: int = 10, l_rule: str = 'numerator', n_theta: int = 128, n_phi: int = 256, real: bool = True) GeometryData[source]#

Spherical-harmonic field at a specific time t.

Each component’s phase is shifted by · f_k · t (with f_k drawn from input.to_peaks()), so iterating over a sequence of times produces a beating standing-wave evolution suitable for an animation. Mirrors the API of chladni.chladni_temporal().

Parameters:
Returns:

GeometryDatageom_type='field_2d'. The parameters['phases'] field carries the time-shifted phase vector (so a renderer can label which frame it received without re-running the math).

harmonic_interference_field_2d(input: HarmonicInput, *, n_directions: int = 24, base_period: float = 1.0, extent: float = 1.5, resolution: int = 384, output: str = 'amplitude_pow', power: float = 0.5) GeometryData[source]#

Rich 2-D interference field with soft rotational symmetry.

For each chord component i, project n_directions coherent plane waves at evenly-spaced angles around the unit circle, then sum the complex field everywhere. Amplitudes are divided by n_directions to average the projections — the result has approximate continuous rotational symmetry (concentric rings), overlaid with a discrete N-fold lattice from the finite direction count.

Mathematical form#

Ψ(x, y) = Σ_i  (a_i / N_dir) · Σ_θ  exp(i · k_i · (x·cos θ + y·sin θ)  +  i · φ_i)

Where k_i = · r_i / L and θ ranges over N_dir angles in [0, 2π).

Parameters:
  • input (HarmonicInput)

  • n_directions (int, default=24) – Plane-wave directions per component. Larger values approach full rotational symmetry.

  • base_period (float, default=1.0)

  • extent (float, default=1.5)

  • resolution (int, default=384)

  • output ({‘amplitude_pow’, ‘amplitude’, ‘intensity’, ‘real’})

  • power (float, default=0.5)

returns:

GeometryDatageom_type='field_2d', metadata.kind='harmonic_interference_field_2d'.

interference_field_2d(input: HarmonicInput, *, layout: str = 'line', spacing: float = 1.0, extent: float = 4.0, resolution: int = 384, base_wavelength: float = 0.6, output: str = 'amplitude_pow', power: float = 0.5) GeometryData[source]#

N-source interference field from a chord, in free space.

Each chord component is associated with a wavelength λ_i = base_wavelength / r_i and emits an idealised (no 1/r falloff) 2-D wave from the i-th source position. The composite field is

u(x, y) = Σ_i  a_i · exp(i · k_i · |r - r_i|  +  i · φ_i)

The classical Young’s two-slit case is recovered with a 2-component equal-ratio input and layout='line'.

Returns:

GeometryDatametadata.kind='interference_field_2d'.

quasicrystal_field_2d(input: HarmonicInput, *, n_fold: int = 5, base_period: float = 1.0, extent: float = 1.5, resolution: int = 384, output: str = 'amplitude_pow', power: float = 0.5, direction_phase_step: float = 0.0) GeometryData[source]#

Quasi-crystal field with exact discrete N-fold symmetry.

Same idea as harmonic_interference_field_2d() but the plane-wave projections are kept separate (no averaging) and N is small. For non-crystallographic n_fold (5, 7, 11, 13) the pattern is a quasi-crystal: exact n_fold-rotational symmetry, no translational period — a 2-D analogue of Penrose tilings.

Mathematical form#

Ψ(x, y) = Σ_i Σ_{k=0..n_fold-1}  a_i · exp(i · k_i · (x·cos α_k + y·sin α_k)  +  i · ψ_{i,k})

where α_k = 2π·k/n_fold and ψ_{i,k} = φ_i + k · ε (per-direction phase step ε defaults to 0).

Parameters:
  • input (HarmonicInput)

  • n_fold (int, default=5) – Number of plane-wave directions per chord component. Choose non-crystallographic values (5, 7, 11, 13) for a true quasi-crystal; crystallographic values (3, 4, 6) yield ordinary periodic lattices.

  • base_period (float, default=1.0)

  • extent (float, default=1.5)

  • resolution (int, default=384)

  • output ({‘amplitude_pow’, ‘amplitude’, ‘intensity’, ‘real’})

  • power (float, default=0.5)

  • direction_phase_step (float, default=0.0) – Optional phase advance ε applied per direction. Setting ε = 2π/n_fold makes each direction’s contribution chirally rotated, producing pinwheel-type quasicrystals.

returns:

GeometryDatametadata.kind='quasicrystal_field_2d', symmetry='discrete_nfold'.

standing_wave_lattice_2d(input: HarmonicInput, *, base_period: float = 1.0, extent: float = 2.0, resolution: int = 384, output: str = 'amplitude_pow', power: float = 0.5, cross_phase: bool = False) GeometryData[source]#

Cartesian 2-D Fourier lattice from the chord’s outer product.

Builds an -mode 2-D field where the chord’s ratios appear as both x- and y-direction frequencies, with intermodulation cross-peaks at every (r_i, r_j) combination.

Mathematical form#

Ψ(x, y) = Σ_i Σ_j  a_i · a_j · exp(i · 2π · (r_i · x + r_j · y) / L  +  i · φ_{i,j})

where φ_{i,j} = φ_i + φ_j (or φ_i · φ_j when cross_phase=True, introducing additional asymmetry).

The result is square-lattice symmetric (90° rotation invariant if all input phases are zero). With N chord components, the lattice has peaks. Extension multiplies this dramatically — a 10-component chord paints a 100-peak lattice with rich IMD-style sub-peaks between them.

Parameters:
  • input (HarmonicInput)

  • base_period (float, default=1.0)

  • extent (float, default=2.0)

  • resolution (int, default=384)

  • output ({‘amplitude_pow’, ‘amplitude’, ‘intensity’, ‘real’})

  • power (float, default=0.5)

  • cross_phase (bool, default=False) – If True, use the antisymmetric combination φ_i φ_j for the cross-term phase instead of the symmetric φ_i + φ_j. This breaks the (i, j) (j, i) exchange symmetry — and so the field’s x y swap symmetry — introducing chirality in the lattice.

returns:

GeometryDatametadata.kind='standing_wave_lattice_2d', symmetry='cartesian'.

vortex_field_2d(input: HarmonicInput, *, radial_kind: str = 'bessel', beam_waist: float = 1.0, extent: float = 2.0, resolution: int = 384, output: str = 'amplitude_pow', power: float = 0.5, charge_scale: float = 1.0, use_numerator_charges: bool = True, p_index_rule: str = 'denominator', radial_indices: Sequence[int] | None = None) GeometryData[source]#

Coherent superposition of chord-driven optical vortex modes.

Each chord component contributes one vortex mode with topological charge l_i derived from the ratio’s rationalised numerator (so Major’s ratio 5/4 → l = 5, ratio 3/2 → l = 3, etc.). The composite field has chord-driven spiral arms and phase singularities — the modulus shows interleaving rotational lobes braided around each other.

Mathematical form#

The field is

Ψ(r, θ) = Σ_i  a_i · radial_i(r) · exp(i · l_i · θ + i · φ_i)

where the radial factor depends on radial_kind. See _vortex_radial_factor() for the four supported families.

The default radial_kind='bessel' uses the natural cylindrical eigenmode J_{|l|}(k·r) — its oscillatory radial profile gives a rich ring pattern that scales properly with extension (more chord components ⇒ more interlocking rings + spirals). The simpler Gaussian-envelope variants are visually softer and deliberately don’t grow with extension.

Parameters:
  • input (HarmonicInput)

  • radial_kind ({‘bessel’, ‘laguerre_gauss’, ‘propagating’, ‘gaussian’}, default=’bessel’) – How to build the radial factor of each vortex mode. See _vortex_radial_factor() for details.

  • beam_waist (float, default=1.0) – Reference radial scale w. Wavenumbers are k_i = · r_i / w.

  • extent (float, default=2.0)

  • resolution (int, default=384)

  • output ({‘amplitude_pow’, ‘amplitude’, ‘intensity’, ‘real’})

  • power (float, default=0.5)

  • charge_scale (float, default=1.0) – Multiplies the chord-derived azimuthal charges. charge_scale=2 doubles every component’s spiral-arm count.

  • use_numerator_charges (bool, default=True) – If True, l_i is the numerator of Fraction(r_i).limit_denominator(20) (musical chord ratios produce small spread-out integer charges this way). If False, l_i = round(charge_scale · r_i).

  • p_index_rule ({‘denominator’, ‘index’, ‘zero’}, default=’denominator’) – Only used when radial_kind='laguerre_gauss'. Picks the radial index p_i per component:

    • 'denominator' — denominator of the rationalised ratio (Major’s 5/4, 3/2 → p = 4, 2). Chord-distinct ring counts.

    • 'index' — sequential p = 0, 1, 2, per component. Useful when the chord ratios all rationalise to the same denominator.

    • 'zero'p = 0, no radial nodes (collapses LG to its zeroth-order Gaussian-modulated form).

  • radial_indices (sequence of int, optional) – If given, overrides p_index_rule and uses these p_i values directly. Length must match the number of components.

returns:

GeometryDatametadata.kind='vortex_field_2d', symmetry='spiral', parameters['radial_kind'] records which radial flavour was used.

extend_harmonic_fit(input: HarmonicInput, n_harm: int = 10, bounds: float = 0.1) HarmonicInput[source]#

Add the common harmonics of the chord — its consonance lattice.

Wraps biotuner.peaks_extension.harmonic_fit(). For every pair of peaks, this finds the harmonic positions where their multiplicative series coincide (within bounds Hz). The returned chord is augmented with these matched harmonics — peaks that resolve the chord because they sit at a common harmonic of two or more components.

Parameters:
  • input (HarmonicInput)

  • n_harm (int, default=10) – Number of harmonics computed per peak before pairwise matching.

  • bounds (float, default=0.1) – Hz tolerance for considering two harmonics as matching.

Returns:

HarmonicInputmetadata['extension'] = 'harmonic_fit'.

extend_harmonic_tuning(input: HarmonicInput, n_harmonics: int = 10, *, octave: float = 2.0, min_ratio: float = 1.0, max_ratio: float = 2.0) HarmonicInput[source]#

Replace the input’s ratios with a generated harmonic-series tuning.

Wraps biotuner.scale_construction.harmonic_tuning(). The chord’s peaks are first turned into integer harmonic indices, then the harmonic_tuning builder generates a tuning whose ratios are derived from those harmonics within [min_ratio, max_ratio].

Useful for the report: the same chord rendered with n_harmonics=4 looks coarse, with n_harmonics=10 shows mid-detail, with n_harmonics=20+ shows full intricate carpets.

Parameters:
  • input (HarmonicInput)

  • n_harmonics (int, default=10) – Number of harmonics fed into harmonic_tuning. Higher = more ratios, more visual richness.

  • octave, min_ratio, max_ratio – Forwarded to harmonic_tuning.

Returns:

HarmonicInputmetadata['extension'] = 'harmonic_tuning'.

extend_harmonics(input: HarmonicInput, n_harmonics: int = 4, *, include_fundamental: bool = True, decay: float = 1.0) HarmonicInput[source]#

Add multiplicative harmonics 2f, 3f, …, nf for every peak.

Wraps biotuner.peaks_extension.EEG_harmonics_mult() and rebuilds a HarmonicInput whose peaks are the union of the originals and their harmonics. Per-harmonic amplitude follows a_n = a_0 / n^decay where a_0 is the fundamental’s amplitude.

Parameters:
  • input (HarmonicInput)

  • n_harmonics (int, default=4) – Number of harmonics to compute per peak (excludes the fundamental when include_fundamental=True; the fundamental is always included when True).

  • include_fundamental (bool, default=True) – If False, drop the fundamental frequencies and keep only the harmonics. Useful for showing the harmonic content alone.

  • decay (float, default=1.0) – Amplitude falloff exponent: harmonic n of a peak is given amplitude a_0 / n^decay. decay=0 keeps all harmonics at the same amplitude; decay=1 gives the natural 1/n law.

Returns:

HarmonicInput – A new input carrying the extended peak set and per-peak amplitudes, with metadata['extension'] = 'harmonics'.

extend_subharmonics(input: HarmonicInput, n_harmonics: int = 4, *, include_fundamental: bool = True, decay: float = 1.0) HarmonicInput[source]#

Add divisive sub-harmonics f/2, f/3, …, f/n for every peak.

Wraps biotuner.peaks_extension.EEG_harmonics_div() semantics but stays peak-array native to avoid lifting a non-trivial dependency. The peak set is the union of originals and their sub-harmonics.

Parameters:
  • input (HarmonicInput)

  • n_harmonics (int, default=4)

  • include_fundamental (bool, default=True)

  • decay (float, default=1.0)

Returns:

HarmonicInputmetadata['extension'] = 'subharmonics'.

blend_fields(geom_a: GeometryData, geom_b: GeometryData, t: float, *, require_same_grid: bool = True) GeometryData[source]#

Pixel-space crossfade between two field_2d geometries.

Used for algorithm morphing (e.g., harmonic_interference → quasicrystal): render two paradigms on the same grid, then blend them. No physical interpretation — purely a visual transition.

Parameters:
  • geom_a, geom_b (GeometryData) – Both must have geom_type='field_2d' and matching coordinate shapes.

  • t (float) – In [0, 1]. Clamped.

  • require_same_grid (bool, default=True) – If True, additionally require that the field_grid arrays match within float tolerance, so the blended field has a well-defined coordinate system. Set False to allow blending fields whose grids differ (the resulting field_grid is taken from geom_a).

Returns:

GeometryDatageom_type='field_2d', metadata.kind='blended', with the constituent paradigm names recorded in parameters.

fade_in_components(base: HarmonicInput, extended: HarmonicInput, t: float, *, match_tol: float = 1e-06) HarmonicInput[source]#

Smoothly grow base into extended as t goes 0 → 1.

For every component present in extended:

  • If its ratio matches a base component (within match_tol), the amplitude is linearly interpolated from the base value (at t=0) to the extended value (at t=1).

  • Otherwise (the component only exists in extended), the amplitude ramps from 0 (at t=0) to its full extended value (at t=1).

At t=0 the output reproduces base exactly (every new component has amplitude 0); at t=1 it reproduces extended exactly (shared components have switched to extended amps).

Designed for animating “chord at extension level 0 → 4 → 8” without components abruptly popping in.

Parameters:
  • base (HarmonicInput) – The starting (smaller) chord. All its components must also appear (within match_tol) in extended.

  • extended (HarmonicInput) – The target chord — typically extend_harmonics(base, …).

  • t (float) – In [0, 1]. Clamped.

  • match_tol (float, default=1e-6) – Tolerance for matching ratios when deciding which extended components are “new” vs “shared”.

Returns:

HarmonicInput

interpolate_chords(a: HarmonicInput, b: HarmonicInput, t: float, *, base_freq: float | None = None, equave: float | None = None) HarmonicInput[source]#

Smoothly morph chord a into chord b at parameter t [0, 1].

Pairing strategy#

Both chords are sorted by ratio (ascending). The first min(N_a, N_b) components are paired index-by-index and linearly interpolated in ratio, amplitude, and phase. Any extra components in the longer chord are kept at their full ratio + phase, but their amplitudes ramp from 0 (at the opposite endpoint) to their full value (at their own endpoint) — so when t=0 the output reproduces a exactly, and when t=1 it reproduces b.

Parameters:
  • a, b (HarmonicInput) – Source and target chords.

  • t (float) – Interpolation parameter. Clamped to [0, 1].

  • base_freq, equave (float, optional) – Override the corresponding fields on the returned input. By default they are linearly interpolated between a and b.

returns:

HarmonicInputmetadata['transition'] = {'kind': 'interpolate_chords', 't': t}.

Examples

>>> major = HarmonicInput(ratios=[1, 5/4, 3/2])
>>> minor = HarmonicInput(ratios=[1, 6/5, 3/2])
>>> midpoint = interpolate_chords(major, minor, 0.5)
>>> [float(r) for r in midpoint.to_ratios()]
[1.0, 1.225, 1.5]
consonance_polygon(input: HarmonicInput, metric: str | Callable[[float], float] = 'dyad_similarity', radius: float = 1.0) GeometryData[source]#

Convex polygon whose vertex angles encode each ratio’s consonance share.

Each component’s “consonance share” is the sum of pairwise metric scores against all other components. Vertices are then placed at cumulative-angle positions: the polygon’s angular density spikes around the most-connected ratios and thins around outliers.

Parameters:
  • input (HarmonicInput)

  • metric (str or callable, default=’dyad_similarity’)

  • radius (float, default=1.0)

Returns:

GeometryDatageom_type='polygon' with weights carrying the per-vertex consonance share. The first vertex is placed at angle 0.

epicycloid(ratio: Fraction | int | float | Tuple[int, int], R: float = 1.0, n_points: int = 2000) GeometryData[source]#

Epicycloid traced by a point on a small circle rolling outside a large one.

With ratio = R / r = p / q (coprime), the curve has p cusps and closes after the small circle completes q revolutions.

Parameters:
  • ratio (Fraction, int, float, or (int, int)) – Ratio of fixed circle radius to rolling circle radius, R / r.

  • R (float, default=1.0) – Radius of the fixed (large) circle.

  • n_points (int, default=2000)

Returns:

GeometryDatageom_type='curve_2d'.

hypocycloid(ratio: Fraction | int | float | Tuple[int, int], R: float = 1.0, n_points: int = 2000) GeometryData[source]#

Hypocycloid traced by a point on a small circle rolling inside a large one.

With ratio = R / r = p / q (coprime, p > q), the curve has p - q cusps. For p = q the trace is a degenerate point.

Parameters:
  • ratio (Fraction, int, float, or (int, int)) – R / r. Must satisfy R > r > 0 for a non-degenerate curve; i.e., the coprime form must have p > q.

  • R (float, default=1.0)

  • n_points (int, default=2000)

Returns:

GeometryDatageom_type='curve_2d'.

interval_vector_diagram(input: HarmonicInput, radius: float = 1.0, bin_cents: float = 50.0) GeometryData[source]#

Graph of pairwise intervals binned into cents-classes.

Nodes are placed on the tuning circle. For each pair (i, j), the interval in cents is computed (modulo equave). Intervals are bucketed in bin_cents-wide classes; an edge’s weight is the interval-class count — how many other pairs share that bucket. Edges thus highlight the chord’s interval-vector multiplicities.

Parameters:
  • input (HarmonicInput)

  • radius (float, default=1.0)

  • bin_cents (float, default=50.0) – Bucket width for grouping intervals into classes.

Returns:

GeometryDatageom_type='graph' with edges of shape (E, 2) and per-edge weights.

polygon_chord_pattern(input: HarmonicInput, metric: str | Callable[[float], float] = 'dyad_similarity', threshold: float | None = None, radius: float = 1.0) GeometryData[source]#

Chord-pattern graph weighted by a biotuner harmonicity metric.

Nodes are placed on the tuning circle. Every pair of distinct components has an edge weighted by metric(ratio_j / ratio_i) (after rebounding the quotient into [1, ∞)). Optionally threshold to keep only the strongest pairs — yields the polygonal “chord skeleton” of the most consonant relationships.

Parameters:
  • input (HarmonicInput)

  • metric (str or callable, default=’dyad_similarity’) – Either a single-ratio biotuner metric name (one of 'dyad_similarity', 'compute_consonance', 'tenneyHeight', 'log_distance') or a callable ratio -> score. tenneyHeight is sign-flipped so higher always means “more consonant” downstream.

  • threshold (float, optional) – Drop edges with weight below threshold. None keeps all.

  • radius (float, default=1.0)

Returns:

GeometryDatageom_type='graph'.

rose_curve(ratio: Fraction | int | float | Tuple[int, int], n_points: int = 2000, n_periods: int | None = None, radius: float = 1.0) GeometryData[source]#

Polar rose: r(θ) = radius · cos((p/q) · θ).

For coprime (p, q), the curve closes after: - θ [0, q · π] if p + q is even, - θ [0, 2 · q · π] if p + q is odd.

If n_periods is given, the curve is sampled over [0, n_periods · π] explicitly; otherwise the closure-aware default above is used.

Parameters:
  • ratio (Fraction, int, float, or (int, int))

  • n_points (int, default=2000)

  • n_periods (int, optional) – Override the auto-computed sampling range (in units of π).

  • radius (float, default=1.0)

Returns:

GeometryDatageom_type='curve_2d'.

star_polygon(n: int, k: int, radius: float = 1.0) GeometryData[source]#

Schläfli star polygon {n/k}.

Vertices are placed on a circle of given radius. Edges connect vertex i to vertex (i + k) mod n. When gcd(n, k) > 1 the figure decomposes into gcd(n, k) disjoint compound polygons; the return type is then polygon_set.

Parameters:
  • n (int) – Number of vertices on the circle, n >= 3.

  • k (int) – Step size, 1 <= k < n.

  • radius (float, default=1.0)

Returns:

GeometryData

  • geom_type='polygon' when gcd(n, k) == 1

  • geom_type='polygon_set' when gcd(n, k) > 1

times_table_circle(n_points: int, multiplier: float, radius: float = 1.0) GeometryData[source]#

Modular-multiplication “times-table” pattern on a circle.

n_points points are placed evenly on a circle of given radius. For each i [0, n_points), an edge is drawn from i to int(round(i * multiplier)) mod n_points. Self-loops (the i==j case) are dropped.

Parameters:
  • n_points (int, must be >= 2)

  • multiplier (float) – Modular multiplier. Integer multipliers produce the classic Mardi-Gras patterns; non-integer values produce richer textures.

  • radius (float, default=1.0)

Returns:

GeometryDatageom_type='graph'.

times_table_from_input(input: HarmonicInput, n_points: int = 360, mode: str = 'ratio', radius: float = 1.0) GeometryData[source]#

Chord-driven times-table: one edge family per harmonic ratio.

Each component of input contributes its own multiplier; all edge families share the same n_points-vertex circle and are returned in a single GeometryData so they can be drawn as overlaid colour layers.

Parameters:
  • input (HarmonicInput)

  • n_points (int, default=360) – Number of points on the circle. 360 is a convenient default because most rational chord ratios give clean modular periods.

  • mode ({‘ratio’, ‘pitch_class’, ‘integer’}, default=’ratio’) – How each ratio is converted into a multiplier:

    • 'ratio' — multiplier = ratio (float). i int(round(i * ratio)) mod n_points.

    • 'pitch_class' — multiplier = round(n_points * log2(ratio)). One “octave” wraps the circle once.

    • 'integer' — multiplier = Fraction(ratio).limit_denominator(32).numerator.

  • radius (float, default=1.0)

Returns:

GeometryDatageom_type='graph'. edges carries every edge across all ratio families; metadata['ratio_index'] is an int array aligned with edges mapping each edge to the originating ratio index (so a renderer can colour each family separately). metadata['multipliers'] lists the resolved multiplier per ratio.

tuning_circle(input: HarmonicInput, radius: float = 1.0) GeometryData[source]#

Place input components on a circle by their log-equave pitch class.

For each ratio r, the angle is · log_equave(r), wrapped into [0, 2π). Amplitudes are exposed as per-point weights.

Parameters:
  • input (HarmonicInput)

  • radius (float, default=1.0)

Returns:

GeometryDatageom_type='point_cloud_2d' with shape (n_components, 2) and weights of length n_components.

continued_fraction_rectangles(ratio: Fraction | int | float | Tuple[int, int], depth: int = 10) GeometryData[source]#

Recursive Euclid-algorithm square / rectangle decomposition of a ratio.

Visualizes the continued-fraction expansion of p/q (assumed > 1; smaller values are inverted internally and the output is flagged in metadata). The starting rectangle is p × q; the largest possible squares of side min(p, q) are stripped off repeatedly, each time rotating the residual strip by 90°. The sequence of squares is the continued-fraction expansion of p/q.

Parameters:
  • ratio (Fraction, int, float, or (int, int))

  • depth (int, default=10) – Maximum number of squares to record. The full expansion terminates earlier if p/q is rational.

Returns:

GeometryDatageom_type='polygon_set' — one rectangular polygon per square, in original-rectangle units (the bounding rectangle is the unit-area rectangle [0, 1] × [0, q/p]).

farey_sequence_layout(order: int, layout: str = 'circle') GeometryData[source]#

Farey sequence F_n placed on a circle, line, or as Ford circles.

Parameters:
  • order (int) – Sequence order, >= 1.

  • layout ({‘circle’, ‘line’, ‘ford’}, default=’circle’) –

    • 'circle' / 'line' — points only; weight encodes 1 / denominator.

    • 'ford' — each fraction p/q F_n becomes a circle of radius 1 / (2 q²) tangent to the x-axis at x = p/q. Adjacent Farey fractions correspond to tangent Ford circles — the classic visual of the Farey structure.

Returns:

GeometryData – For 'circle' / 'line': geom_type='point_cloud_2d'. For 'ford': geom_type='polygon_set' — each entry is a polyline approximation of one Ford circle. metadata['radii'] and metadata['centers'] carry the analytic geometry.

ifs_harmonic(input: HarmonicInput, n_points: int = 50000, contraction: str = 'ratio_inverse', transient: int = 200, rng: Generator | None = None) GeometryData[source]#

Iterated-function-system attractor driven by harmonic ratios.

Each input ratio defines an affine contraction z -> z · s_i + v_i, where s_i is the contraction factor (derived from the ratio per contraction) and v_i is the i-th vertex of an N-gon scaled to the unit disk. The classic chaos game then samples the attractor.

Parameters:
  • input (HarmonicInput) – Provides the N ratios.

  • n_points (int, default=50_000)

  • contraction ({‘ratio_inverse’, ‘log_ratio’, ‘fixed_half’}, default=’ratio_inverse’) –

    • 'ratio_inverse': s_i = 1 / r_i (rebound to < 1).

    • 'log_ratio': s_i = 1 / (1 + log(r_i)).

    • 'fixed_half': s_i = 0.5 for all i (Sierpinski-like).

  • transient (int, default=200) – Number of warm-up iterations to discard before recording points.

  • rng (np.random.Generator, optional) – Source of randomness. Default: np.random.default_rng().

Returns:

GeometryDatageom_type='point_cloud_2d' with (n_points, 2) coordinates.

stern_brocot_tree(input: HarmonicInput | None = None, max_depth: int = 6, layout: str = 'hyperbolic') GeometryData[source]#

Stern-Brocot mediant tree to max_depth levels.

Starts from the canonical bounds 0/1 and 1/0; each node is the mediant of its bracketing pair. The tree at depth d has exactly 2^d - 1 interior nodes (the bounds are excluded from the output).

Each node is annotated with a harmonicity score in metadata['harmonicity'] — by default dyad_similarity(p/q). If input is provided, an additional metadata['nearest_input_dist_cents'] array records the cents distance from each tree node to the closest ratio in input, helpful for highlighting where the chord lives in the rational lattice.

Parameters:
  • input (HarmonicInput, optional) – If given, used only for the nearest-input-distance annotation.

  • max_depth (int, default=6) – Tree depth. The number of nodes grows as 2^depth - 1.

  • layout ({‘hyperbolic’, ‘tree’}, default=’hyperbolic’) – 'hyperbolic' places nodes on the Poincaré disk by traversal position and depth; 'tree' uses a flat dendrogram layout.

Returns:

GeometryDatageom_type='tree'.

subharmonic_tree(input: HarmonicInput, depth: int = 4, n_harmonics: int = 5, min_freq: float = 0.1, layout: str = 'depth') GeometryData[source]#

Recursive subharmonic expansion as a tree.

Each input peak f is the root of a sub-tree whose children are its first n_harmonics subharmonics f / 2, f / 3, ..., f / (k + 1). Each child is expanded the same way to depth levels. Nodes with frequency below min_freq are pruned.

Notes

The plan originally suggested using biotuner.metrics.compute_subharmonics, which finds common subharmonics across a chord. That’s a different operation from the per-peak subharmonic series this tree visualizes; we use the classical f / k definition directly.

Parameters:
  • input (HarmonicInput) – Source peaks for the root level.

  • depth (int, default=4) – Number of expansion levels below the root.

  • n_harmonics (int, default=5) – Number of subharmonics per node.

  • min_freq (float, default=0.1) – Frequencies below this are not expanded further.

  • layout ({‘depth’, ‘polar’}, default=’depth’) – 'depth' — original dendrogram layout (depth on Y, sorted on X). 'polar' — each input peak gets its own angular sector; depth becomes radial distance, so different chords produce visibly different fan-out shapes instead of identical depth-stacks.

Returns:

GeometryDatageom_type='tree'. metadata['root_index_per_node'] tags every node with its originating root-peak index, useful for colour-coding.

geometry_sequence(input_seq: HarmonicSequence | Iterable[HarmonicInput], fn: Callable[[...], GeometryData], **kwargs) List[GeometryData][source]#

Map a geometry function over every frame of a HarmonicSequence.

Parameters:
  • input_seq (HarmonicSequence or Iterable[HarmonicInput])

  • fn (callable taking a HarmonicInput as first argument)

  • **kwargs (forwarded to fn for every frame)

Returns:

list of GeometryData

Raises:

ValueError – If input_seq contains no frames.

lsystem_from_ratios(input: HarmonicInput, depth: int = 4, axiom: str = 'F', rules: Dict[str, str] | None = None, step_size: float = 1.0) GeometryData[source]#

L-system branching plant parameterised by harmonic ratios.

The base turning angle θ is derived from the first non-unison ratio as 360 / (p + q) for ratio p/q. The number of side-branches equals n_components - 1, mapping each interval of the chord to a branch direction. Override both with explicit rules.

Parameters:
  • input (HarmonicInput)

  • depth (int, default=4) – Rewriting depth. Keep ≤ 6 to avoid very large strings.

  • axiom (str, default=’F’) – Starting L-system string.

  • rules (dict, optional) – Symbol → replacement string. Defaults to a ratio-derived plant rule.

  • step_size (float, default=1.0) – Forward step length per F symbol.

Returns:

GeometryDatageom_type='graph' with all turtle segments as edges. metadata['lstring_preview'] holds the first 120 characters of the rewritten string.

recursive_polygon(input: HarmonicInput, depth: int = 4, n_sides: int | None = None, scale_factor: float | None = None) GeometryData[source]#

Koch-like self-similar polygon boundary driven by harmonic ratios.

Each edge is recursively replaced by four sub-edges forming an outward triangular bump. The bump scale and rotation angle are derived from the first non-unison ratio.

Parameters:
  • input (HarmonicInput)

  • depth (int, default=4) – Subdivision steps. Edge count grows as 4^depth * n_sides.

  • n_sides (int, optional) – Number of polygon sides. Defaults to n_components (min 3).

  • scale_factor (float, optional) – Fraction of the edge length occupied by each outer sub-edge. Defaults to 1 / (p + 1) for the first non-trivial ratio p/q.

Returns:

GeometryDatageom_type='curve_2d' with the closed fractal boundary.

self_similar_tuning(input: HarmonicInput, n_levels: int = 4, equave: float = 2.0) GeometryData[source]#

Self-similar pitch lattice at multiple equave levels.

Starting from the input ratios as generators, each subsequent level is formed by multiplying every pitch in the previous level by every generator and reducing back to [1, equave). The result is the free abelian group generated by the ratios, truncated to n_levels generations.

Pitches are arranged on concentric circles — the k-th circle has radius (k + 1) / n_levels — at angular positions proportional to log_equave(pitch). Edges connect each pitch to its parent (the closest pitch at the previous level that generated it).

Parameters:
  • input (HarmonicInput)

  • n_levels (int, default=4) – Number of generative levels (generations). Level 0 is the seed.

  • equave (float, default=2.0) – Interval of equivalence (2.0 = octave).

Returns:

GeometryDatageom_type='graph' with nodes on concentric circles and edges tracing the generative lineage.

harmonic_knot(input: HarmonicInput, n_points: int = 600, tube_radius: float = 0.06, n_sides: int = 16, major_radius: float = 2.0, minor_radius: float = 0.7) GeometryData[source]#

Torus knot T(p, q) derived from the dominant harmonic ratio.

The simplest ratio p/q in the input determines the winding numbers. A 3/2 input (perfect fifth) gives a trefoil knot T(3, 2); 5/4 gives T(5, 4). Amplitude modulates the tube radius so louder harmonics thicken the knot.

Parameters:
  • input (HarmonicInput)

  • n_points (int, default=600) – Sample points along the knot curve.

  • tube_radius (float, default=0.06) – Base tube radius.

  • n_sides (int, default=16) – Cross-section polygon sides.

  • major_radius (float, default=2.0) – Torus major radius (distance from torus centre to tube centre).

  • minor_radius (float, default=0.7) – Torus minor radius (tube radius of the underlying torus).

Returns:

GeometryDatageom_type='mesh_3d'.

harmonic_point_cloud(input: HarmonicInput, n_points: int = 2000, surface: str = 'sphere') GeometryData[source]#

Point cloud on a sphere or torus with harmonic-phase density modulation.

Base points are distributed via Fibonacci spiral (golden-angle method) for uniform coverage, then the field value at each point is computed as a superposition of ratio-frequency waves. Points where the field exceeds its median are retained; the remainder are discarded (up to n_points survivors).

Parameters:
  • input (HarmonicInput)

  • n_points (int, default=2000) – Number of points in the output cloud.

  • surface ({‘sphere’, ‘torus’, ‘klein’, ‘hyperbolic’, ‘mos’}, default=’sphere’) –

    • 'sphere' / 'torus' — classic surfaces, see Phase 7.

    • 'klein' — Klein bottle (immersion in R³ via Lawson form).

    • 'hyperbolic' — Poincaré disk lifted to a saddle / pseudosphere.

    • 'mos' — moment-of-symmetry helical curve at log-equave height.

  • oversample (int, default=3) – Multiplier for the candidate-point pool before field-based selection. Higher values give finer-grained density at the cost of more compute.

Returns:

GeometryDatageom_type='point_cloud_3d'. weights carries the field value at each point (useful for colouring); metadata['surface'] and metadata['field_range'] describe the underlying scalar field.

harmonic_surface(input: HarmonicInput, mode: str = 'torus', resolution: int = 64) GeometryData[source]#

Deformed parametric surface driven by harmonic ratio frequencies.

Each ratio p/q contributes a standing-wave ripple whose angular frequency is (p, q) in the two surface parameters. Amplitude controls the ripple depth.

Parameters:
  • input (HarmonicInput)

  • mode ({‘torus’, ‘sphere’, ‘cylinder’}, default=’torus’) – Base surface geometry.

  • resolution (int, default=64) – Grid resolution per parameter axis (total vertices ≈ resolution²).

Returns:

GeometryDatageom_type='mesh_3d'.

lissajous_tube(input: HarmonicInput, n_points: int = 800, n_periods: int = 6, tube_radius: float = 0.05, n_sides: int = 12) GeometryData[source]#

Tube mesh extruded around a 3-D Lissajous curve.

The first three ratio components drive the x, y, z frequencies. Amplitude is mapped to tube-radius variation so louder components swell the tube.

Parameters:
  • input (HarmonicInput)

  • n_points (int, default=800) – Number of sample points along the curve.

  • n_periods (int, default=6) – Number of full periods of the base (lowest-ratio) oscillation.

  • tube_radius (float, default=0.05) – Base tube radius.

  • n_sides (int, default=12) – Polygon sides for the tube cross-section.

Returns:

GeometryDatageom_type='mesh_3d'.

lsystem_3d(input: HarmonicInput, depth: int = 3, step_length: float = 1.0, rules: Dict[str, str] | None = None, axiom: str = 'F') GeometryData[source]#

3-D turtle L-system branching tree driven by harmonic ratios.

The branch angle θ is derived from the dominant ratio 360 / (p + q). The turtle supports the full six-degree-of-freedom rotation set: +/- yaw around U, ^/& pitch around L, </> roll around H, | for a 180-degree U-turn, and [/] for state push/pop.

Parameters:
  • input (HarmonicInput)

  • depth (int, default=3) – L-system rewriting depth (keep ≤ 5 to avoid memory issues).

  • step_length (float, default=1.0) – Length of each forward (F) segment.

  • rules (dict, optional) – Custom symbol→replacement rules; defaults to a ratio-derived 3-D grammar.

  • axiom (str, default=’F’) – Starting string.

Returns:

GeometryDatageom_type='tree' with 3-D node coordinates (N, 3).

recursive_polyhedron(input: HarmonicInput, depth: int = 2, solid: str | None = None, per_face_bump: bool = True, apex_twist: bool = True) GeometryData[source]#

Koch-style recursively stellated Platonic solid, ratio-differentiated.

At each depth level every triangular face is subdivided into four smaller triangles and a tetrahedral bump is raised at the centre face. By default each face’s bump scale is keyed to the closest harmonic ratio (the ratio whose log2 value best matches the face’s normal-vector polar angle), so chord-tones literally sculpt their own surface region.

Parameters:
  • input (HarmonicInput)

  • depth (int, default=2) – Recursion levels. Faces quadruple each level; keep ≤ 4.

  • solid ({‘tetrahedron’, ‘cube’, ‘icosahedron’} or None) – Base solid. When None (default), picks based on n_components: ≤3 → tetrahedron, 4 → cube, ≥5 → icosahedron, so chord size also differentiates the silhouette.

  • per_face_bump (bool, default=True) – If True, each face’s bump scale interpolates between component amplitudes weighted by alignment of the face normal with each ratio’s pitch-class direction. If False, all faces share a single global scale (the legacy behaviour).

  • apex_twist (bool, default=True) – If True, each bump apex is shifted laterally in the face’s tangent plane by an amount proportional to input.phases[k], where k is the nearest-ratio index. The lateral offset is scaled by edge length and clamped so the apex stays above the face. This makes the bump visibly tilted (rather than just rotating about the normal, which would be invisible).

Returns:

GeometryDatageom_type='mesh_3d'. metadata['face_ratio_index'] is a per-face int array mapping each face to its nearest-ratio index — renderers can use it to colour the surface by chord-tone.

class MetricsLog(rows: ~typing.List[~typing.Dict[str, ~typing.Any]] = <factory>)[source]#

Bases: object

Append-only log of metric measurements with CSV / JSON export.

Each row is a dict of metric values plus optional metadata fields (label, timestamp, anything user-supplied via log()). Suitable for accumulating measurements across many chords / frames / experiments and exporting them for downstream analysis.

rows: List[Dict[str, Any]]#
log(**fields: Any) None[source]#

Append one row. fields is the per-row data.

log_geometry(geom: GeometryData, label: str | None = None, **extra: Any) None[source]#

Convenience: compute geometry_metrics() and append the row.

log_sequence(seq: HarmonicSequence, generator: Callable[[...], GeometryData], label_prefix: str = 'frame', generator_kwargs: Dict[str, Any] | None = None, **extra: Any) None[source]#

Apply generator to each frame and log the resulting geometry-metrics row.

Parameters:
  • seq (HarmonicSequence)

  • generator (callable(HarmonicInput, **kw) -> GeometryData)

  • label_prefix (str, default "frame")

  • generator_kwargs (dict, optional) – Forwarded to generator for every frame.

  • **extra (Any) – Constant fields appended to every row (e.g. trial=1).

to_dict() Dict[str, List[Any]][source]#

Return a column-oriented dict (DataFrame-like).

to_csv(path: str | Path) Path[source]#

Write the log as CSV (header = column names, one row per entry).

to_json(path: str | Path) Path[source]#

Write the log as JSON (a list of row dicts).

compare(geometries: Sequence[GeometryData], labels: Sequence[str] | None = None) Dict[str, List[float]][source]#

Side-by-side metric comparison across a list of GeometryData.

Parameters:
  • geometries (sequence of GeometryData)

  • labels (sequence of str, optional) – One label per geometry. Defaults to geom_0, geom_1, .

Returns:

dict{metric_name: [v_for_item_1, …]} plus a special '__labels__' key carrying the user-supplied or auto labels.

geometry_metrics(geom: GeometryData) Dict[str, float][source]#

Structural + per-method metrics for any GeometryData.

Always returns the generic stats (n_vertices, n_faces / n_edges when applicable, spatial spans, edge-length and field stats). When the geometry’s metadata['kind'] matches a registered generator (lissajous_2d, chladni_field_*, stern_brocot_tree, ifs_harmonic, recursive_polyhedron, harmonic_point_cloud, …), method-specific scalars are merged on top.

The list of recognised kinds is _GEOM_EXTRACTORS. Geometries without a recognised kind still receive the generic stats.

Parameters:

geom (GeometryData)

Returns:

dict[str, float]

list_supported_kinds() List[str][source]#

Return every metadata[‘kind’] string recognised by geometry_metrics().

Useful for sanity-checking instrumentation coverage across the module.

normalize_metrics(rows: Sequence[Dict[str, float]], metrics: Sequence[str] | None = None, bounds: Dict[str, tuple] | None = None, senses: Dict[str, int] | None = None) List[Dict[str, float]][source]#

Map metric values to [0, 1] for radar plotting.

Bounds are data-driven by default (per-metric min/max across rows). Pass bounds={name: (lo, hi)} to override.

Parameters:
  • rows (sequence of dict)

  • metrics (sequence of str, optional) – Subset of metric keys to normalise; defaults to rows[0] keys.

  • bounds (dict, optional) – Explicit {metric: (lo, hi)} per-metric scales.

  • senses (dict, optional) – {metric: +1 | -1}. +1 (default) keeps the data-driven mapping; -1 inverts (i.e. higher raw value → lower normalised value). Useful for “lower is better” metrics like edge irregularity.

  • Behaviour notes

  • —————

  • * Non-finite raw values stay ``nan`` (not rendered on a radar).

  • * Zero-variance metrics (all rows agree) map to ``0.5`` — interpreted as – “consensus / no information” rather than misleadingly collapsing to 0.

sequence_metrics(seq: HarmonicSequence, generator: Callable[[...], GeometryData], **generator_kwargs: Any) Dict[str, ndarray][source]#

Apply generator to every frame of seq and return per-frame geometry-metric trajectories.

Parameters:
  • seq (HarmonicSequence)

  • generator (callable) – Any harmonic-geometry generator that takes a HarmonicInput as its first positional argument and returns a GeometryData (e.g. harmonic_knot, chladni_from_input, recursive_polyhedron, …).

  • **generator_kwargs – Forwarded to generator on every frame.

Returns:

dict[str, ndarray]{metric_name: 1-D ndarray of length n_frames}. The metric set is the union over all frames; missing values are nan.

Examples

>>> traj = sequence_metrics(seq, harmonic_knot, n_points=400)
>>> traj["winding_p"].shape
(T,)

Submodules#