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:
objectContainer for one piece of geometric output.
- Parameters:
geom_type (str) – One of
GEOM_TYPES. Describes the layout ofcoordinatesand 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)forgraphandtreetypes. Indices are intocoordinatesalong axis 0.faces (ndarray, optional) – Integer triangle indices of shape
(F, 3)formesh_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 forfield_2d/field_3dtypes.parameters (dict) – Snapshot of the inputs that produced this geometry (e.g. the
HarmonicInputfields). 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
pathas a numpy.npzfile.parametersandmetadataare pickled into a 0-d object array. Lists of arrays (e.g. forcurve_set_2d) are flattened with index suffixes so they round-trip throughnp.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
GeometryDatapreviously written bysave().Only load files from trusted sources —
parametersandmetadataare 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:
objectUnified harmonic input.
At least one of
ratiosorpeaksmust be provided. All list-typed optional fields, if given, must have lengths matching the number of components (len(ratios)orlen(peaks)).- Parameters:
ratios (list of Fraction or float, optional) – Frequency ratios. Coerced to
Fractionwhen a rational approximation withinDEFAULT_MAX_DENOMINATORexists.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
ratiosare given withoutpeaksto recover absolute frequencies.equave (float, default=2.0) – Equave width:
2.0for octaves,3.0for 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 inconsistentHarmonicInputraisesValueError.- 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
ratiosfirst, thenpeaks. At least one of the two is guaranteed to be present aftervalidate().
- to_peaks() ndarray[source]#
Return absolute peak frequencies in Hz as a 1-D
float64array.If
peaksis set, those values are returned directly. Otherwise peaks are reconstructed asbase_freq * ratios.
- to_ratios() List[Fraction | float][source]#
Return ratios.
If
ratiosis set, those values are returned. Otherwise ratios are derived aspeaks / base_freqand coerced toFraction.
- normalized_amplitudes() ndarray[source]#
Return amplitudes scaled to sum to 1.
If
amplitudesisNone, returns a uniform distribution over the components.
- validate() None[source]#
Raise
ValueErrorif the input is internally inconsistent.Checked invariants:
at least one of
ratios/peaksis given,equave > 1andbase_freq > 0,all list-typed fields have matching lengths,
all amplitudes are non-negative,
all peaks are positive,
if both
ratiosandpeaksare given, they agree up tobase_freqwithin 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_freqisNoneit 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, andpeaks_ratioswhen 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_extractionhas been called.equave (float, default=2.0)
- Raises:
AttributeError – If
btlacks apeaksattribute.ValueError – If
bt.peaksis empty.
- class HarmonicSequence(frames: List[HarmonicInput], times: ndarray | None = None)[source]#
Bases:
objectTime-resolved sequence of
HarmonicInputframes.Pairs naturally with the output of
biotuner.transitional_harmonyandbiotuner.harmonic_sequence: each window’s peaks become a frame here, and downstream geometry functions can be applied frame-by-frame viatransformations.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#
- at(t: float) HarmonicInput[source]#
Return the frame nearest to time
t.
- interpolate(t: float, mode: str = 'log') HarmonicInput[source]#
Return a
HarmonicInputinterpolated to timet.Currently supports two-frame interpolation between the bracketing frames.
modeselects 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 raisesValueError. Richer interpolation (mismatched component counts, phase wrapping, etc.) is the job oftransformations.interpolate_inputin 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_biotunerobjects.Each biotuner object becomes one frame. Objects with no peaks are skipped; if every object is empty,
ValueErroris raised.- Parameters:
bt_list (sequence of compute_biotuner) – Fitted biotuner objects (
peaks_extractionalready 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.BiotunerGroupinstance.Uses
btg.objects(the per-seriescompute_biotunerinstances).BiotunerGroupmust have been constructed withstore_objects=True(the default).- Parameters:
btg (BiotunerGroup) – A group whose
compute_peakshas 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
btghas noobjectsattribute, or it isNone(e.g.store_objects=Falsewas 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)andy(t) = A_y · sin(b · t)overt ∈ [0, 2π · n_periods], where(a, b)is a coprime representation ofratio.- Parameters:
ratio (Fraction, int, float, or (int, int)) – Frequency ratio
a / bof 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:
GeometryData –
geom_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)fori ∈ {0, 1, 2}, wheref_iis a coprime integer derived fromratios[i].When all three
f_iare pairwise coprime the resulting curve is a Lissajous knot; this is flagged inmetadata['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:
GeometryData –
geom_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:
GeometryData –
geom_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 GeometryData –
N × Nmatrix ofcurve_2dgeometries.
- 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 · tevolves 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:
GeometryData –
geom_type='curve_2d'with shape(int(sr * duration), 2).
- lissajous_topology(geom: GeometryData) dict[source]#
Inspect a Lissajous-style
curve_2dand 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'—Fractionrepresenting thea / bratio when known, elseNone.
- 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 exponentiale^(-π Δ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 axisi % 3.'split': components are split contiguously into three near-equal blocks for x, y, z.
- Returns:
GeometryData –
geom_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
HarmonicInputand delegates toharmonograph_lateral(). IfdampingisNone, a uniform default ofDEFAULT_DAMPINGis 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:
GeometryData –
geom_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) = 2π · 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:
GeometryData –
geom_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:
GeometryData –
geom_type='field_3d'withcoordinatesthe(R, R, R)scalar field andfield_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 ofJ_n(m is 1-indexed). Outside the disk the field is set toNaN.- Parameters:
modes_radial (sequence of int) – Radial mode index (
m, 1-indexed). Same length asmodes_angular.modes_angular (sequence of int) – Angular mode index (
n,≥ 0). Same length asmodes_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:
GeometryData –
geom_type='field_2d'. Values outside the disk areNaN.
- 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 (
0is 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²)toO(n³)in the interior cell count.solver ({‘fdm’, ‘fem’}, default=’fdm’) –
'fem'requires the optionalscikit-femdependency and is not yet wired through; selecting it raisesNotImplementedError.
- Returns:
GeometryData –
geom_type='field_2d'. Cells outside the polygon areNaN.
- 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:
GeometryData –
geom_type='field_2d'withcoordinatesthe(R, R)field andfield_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) viaratios_to_modes(); amplitudes are taken frominput.normalized_amplitudes(); phases default to the input’sphases(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:
GeometryData –
geom_type='curve_set_2d'— one(N_i, 2)array per contour, with coordinates in the same units as the inputfield_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:
GeometryData –
geom_type='mesh_3d'with vertex coordinates in the same units as the inputfield_gridand trianglefaces.
- 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, usinginputas the mode source.The phases used in the field are shifted by
2π · f_k · tfor each component (withf_ktaken frominput.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
ris approximated by a coprime pair(m, n)withm, 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'usesFraction.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(vial_rule) and then assigned an orderm ∈ [-l, l](viamode_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
mfor each component:'zonal'—m = 0for 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'— cyclesmthrough{0, ±l, ±l/2}across the harmonics. Mixes zonal / tesseral / sectoral modes for visual variety.'rounded'— alias of'zonal'; kept for API parity withchladni.ratios_to_modes().
max_l (int, default=10) – Cap on the degree
l.l_rule ({‘numerator’, ‘rounded’}, default=’numerator’) – How to convert each ratio
rto a degreel:'numerator'— rationaliserwithFraction.limit_denominator(max_l)()and use the numerator. Maps musical chords cleanly:4 : 5 : 6(i.e. ratios1, 5/4, 3/2) yieldsl = 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]tol ∈ {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, herelis degree andmis order withm ∈ [-l, l]. The two indices have very different geometric meanings:lcontrols how many nodal lines the mode has;mcontrols 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^mevaluated 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:
GeometryData –
geom_type='field_2d'withcoordinatesshape(n_theta, n_phi)andfield_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, withl >= 0and|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
Falseto keep the complex-valuedY_l^mand let the field be complex.
- Returns:
GeometryData –
geom_type='field_2d'.coordinatesis the(n_theta, n_phi)field;field_gridis 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 viaratios_to_modes_lm(), amplitudes are taken frominput.normalized_amplitudes(), phases default toinput.phasesor 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:
GeometryData –
geom_type='mesh_3d'withcoordinatesshape(V, 3): vertex positions inR³.V = n_theta * n_phi.facesshape(F, 3): triangle indices intocoordinates. The mesh is a UV-sphere triangulation of the(n_theta - 1) × (n_phi - 1)quad grid (two triangles per quad).weightsshape(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
2π · f_k · t(withf_kdrawn frominput.to_peaks()), so iterating over a sequence of times produces a beating standing-wave evolution suitable for an animation. Mirrors the API ofchladni.chladni_temporal().- Parameters:
input (HarmonicInput)
t (float) – Time in seconds.
mode_rule, max_l, n_theta, n_phi, real – Forwarded to
spherical_harmonic_field(). Seespherical_harmonic_from_input()for descriptions.
- Returns:
GeometryData –
geom_type='field_2d'. Theparameters['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, projectn_directionscoherent plane waves at evenly-spaced angles around the unit circle, then sum the complex field everywhere. Amplitudes are divided byn_directionsto 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 = 2π · r_i / Landθranges overN_dirangles 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:
GeometryData –
geom_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_iand emits an idealised (no1/rfalloff) 2-D wave from thei-th source position. The composite field isu(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:
GeometryData –
metadata.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) andNis small. For non-crystallographicn_fold(5, 7, 11, 13) the pattern is a quasi-crystal: exactn_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_foldandψ_{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_foldmakes each direction’s contribution chirally rotated, producing pinwheel-type quasicrystals.
- returns:
GeometryData –
metadata.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
N²-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 · φ_jwhencross_phase=True, introducing additional asymmetry).The result is square-lattice symmetric (90° rotation invariant if all input phases are zero). With
Nchord components, the lattice hasN²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 − φ_jfor the cross-term phase instead of the symmetricφ_i + φ_j. This breaks the(i, j) ↔ (j, i)exchange symmetry — and so the field’sx ↔ yswap symmetry — introducing chirality in the lattice.
- returns:
GeometryData –
metadata.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_iderived 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 eigenmodeJ_{|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 arek_i = 2π · 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=2doubles every component’s spiral-arm count.use_numerator_charges (bool, default=True) – If True,
l_iis the numerator ofFraction(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 indexp_iper component:'denominator'— denominator of the rationalised ratio (Major’s 5/4, 3/2 → p = 4, 2). Chord-distinct ring counts.'index'— sequentialp = 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_ruleand uses thesep_ivalues directly. Length must match the number of components.
- returns:
GeometryData –
metadata.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 (withinboundsHz). 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:
HarmonicInput –
metadata['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:
HarmonicInput –
metadata['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, …, nffor every peak.Wraps
biotuner.peaks_extension.EEG_harmonics_mult()and rebuilds aHarmonicInputwhose peaks are the union of the originals and their harmonics. Per-harmonic amplitude followsa_n = a_0 / n^decaywherea_0is 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 whenTrue).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
nof a peak is given amplitudea_0 / n^decay.decay=0keeps all harmonics at the same amplitude;decay=1gives the natural1/nlaw.
- 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/nfor 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:
HarmonicInput –
metadata['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_2dgeometries.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_gridarrays match within float tolerance, so the blended field has a well-defined coordinate system. Set False to allow blending fields whose grids differ (the resultingfield_gridis taken fromgeom_a).
- Returns:
GeometryData –
geom_type='field_2d',metadata.kind='blended', with the constituent paradigm names recorded inparameters.
- fade_in_components(base: HarmonicInput, extended: HarmonicInput, t: float, *, match_tol: float = 1e-06) HarmonicInput[source]#
Smoothly grow
baseintoextendedastgoes 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 (att=0) to the extended value (att=1).Otherwise (the component only exists in
extended), the amplitude ramps from 0 (att=0) to its full extended value (att=1).
At
t=0the output reproducesbaseexactly (every new component has amplitude 0); att=1it reproducesextendedexactly (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) inextended.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
ainto chordbat parametert ∈ [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 whent=0the output reproducesaexactly, and whent=1it reproducesb.- 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
aandb.
- returns:
HarmonicInput –
metadata['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
metricscores 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:
GeometryData –
geom_type='polygon'withweightscarrying 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 haspcusps and closes after the small circle completesqrevolutions.- 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:
GeometryData –
geom_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 hasp - qcusps. Forp = qthe trace is a degenerate point.- Parameters:
ratio (Fraction, int, float, or (int, int)) –
R / r. Must satisfyR > r > 0for a non-degenerate curve; i.e., the coprime form must havep > q.R (float, default=1.0)
n_points (int, default=2000)
- Returns:
GeometryData –
geom_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 inbin_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:
GeometryData –
geom_type='graph'withedgesof shape(E, 2)and per-edgeweights.
- 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 callableratio -> score.tenneyHeightis sign-flipped so higher always means “more consonant” downstream.threshold (float, optional) – Drop edges with weight below
threshold.Nonekeeps all.radius (float, default=1.0)
- Returns:
GeometryData –
geom_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 · π]ifp + qis even, -θ ∈ [0, 2 · q · π]ifp + qis odd.If
n_periodsis 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:
GeometryData –
geom_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 vertexito vertex(i + k) mod n. Whengcd(n, k) > 1the figure decomposes intogcd(n, k)disjoint compound polygons; the return type is thenpolygon_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'whengcd(n, k) == 1geom_type='polygon_set'whengcd(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_pointspoints are placed evenly on a circle of givenradius. For eachi ∈ [0, n_points), an edge is drawn fromitoint(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:
GeometryData –
geom_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
inputcontributes its own multiplier; all edge families share the samen_points-vertex circle and are returned in a singleGeometryDataso 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:
GeometryData –
geom_type='graph'.edgescarries every edge across all ratio families;metadata['ratio_index']is an int array aligned withedgesmapping 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 is2π · log_equave(r), wrapped into[0, 2π). Amplitudes are exposed as per-point weights.- Parameters:
input (HarmonicInput)
radius (float, default=1.0)
- Returns:
GeometryData –
geom_type='point_cloud_2d'with shape(n_components, 2)andweightsof lengthn_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 isp × q; the largest possible squares of sidemin(p, q)are stripped off repeatedly, each time rotating the residual strip by 90°. The sequence of squares is the continued-fraction expansion ofp/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/qis rational.
- Returns:
GeometryData –
geom_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_nplaced 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 encodes1 / denominator.'ford'— each fractionp/q ∈ F_nbecomes a circle of radius1 / (2 q²)tangent to the x-axis atx = 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']andmetadata['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, wheres_iis the contraction factor (derived from the ratio percontraction) andv_iis 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.5for 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:
GeometryData –
geom_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_depthlevels.Starts from the canonical bounds
0/1and1/0; each node is the mediant of its bracketing pair. The tree at depthdhas exactly2^d - 1interior nodes (the bounds are excluded from the output).Each node is annotated with a harmonicity score in
metadata['harmonicity']— by defaultdyad_similarity(p/q). Ifinputis provided, an additionalmetadata['nearest_input_dist_cents']array records the cents distance from each tree node to the closest ratio ininput, 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:
GeometryData –
geom_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
fis the root of a sub-tree whose children are its firstn_harmonicssubharmonicsf / 2, f / 3, ..., f / (k + 1). Each child is expanded the same way todepthlevels. Nodes with frequency belowmin_freqare 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 classicalf / kdefinition 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:
GeometryData –
geom_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
fnfor every frame)
- Returns:
list of GeometryData
- Raises:
ValueError – If
input_seqcontains 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 ratiop/q. The number of side-branches equalsn_components - 1, mapping each interval of the chord to a branch direction. Override both with explicitrules.- 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
Fsymbol.
- Returns:
GeometryData –
geom_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 ratiop/q.
- Returns:
GeometryData –
geom_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 ton_levelsgenerations.Pitches are arranged on concentric circles — the k-th circle has radius
(k + 1) / n_levels— at angular positions proportional tolog_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:
GeometryData –
geom_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/qin 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:
GeometryData –
geom_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_pointssurvivors).- 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:
GeometryData –
geom_type='point_cloud_3d'.weightscarries the field value at each point (useful for colouring);metadata['surface']andmetadata['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/qcontributes 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:
GeometryData –
geom_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:
GeometryData –
geom_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:
GeometryData –
geom_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. WhenNone(default), picks based onn_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. IfFalse, all faces share a single globalscale(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 toinput.phases[k], wherekis 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:
GeometryData –
geom_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:
objectAppend-only log of metric measurements with CSV / JSON export.
Each row is a
dictof metric values plus optional metadata fields (label,timestamp, anything user-supplied vialog()). Suitable for accumulating measurements across many chords / frames / experiments and exporting them for downstream analysis.- rows: List[Dict[str, Any]]#
- 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
generatorto 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
generatorfor every frame.**extra (Any) – Constant fields appended to every row (e.g.
trial=1).
- 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_edgeswhen applicable, spatial spans, edge-length and field stats). When the geometry’smetadata['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;-1inverts (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
generatorto every frame ofseqand return per-frame geometry-metric trajectories.- Parameters:
seq (HarmonicSequence)
generator (callable) – Any harmonic-geometry generator that takes a
HarmonicInputas its first positional argument and returns aGeometryData(e.g.harmonic_knot,chladni_from_input,recursive_polyhedron, …).**generator_kwargs – Forwarded to
generatoron 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 arenan.
Examples
>>> traj = sequence_metrics(seq, harmonic_knot, n_points=400) >>> traj["winding_p"].shape (T,)
Submodules#
- Inputs
- Geometry Data
- Lissajous
- Chladni
- Harmonograph
- Polygon & Circular
- Fractal
- Generative
- Geometry 3D
- Metrics
- Plotting
- biotuner.harmonic_geometry.plotting
axis_clean()title_ax()make_axis_3d()save_figure()draw_curve_2d()draw_polygon()draw_polygon_set()draw_graph_2d()draw_hyperbolic_graph()draw_point_cloud_2d()draw_field_2d()draw_image()draw_tree_2d()draw_rectangles()draw_mesh_3d()draw_tree_3d()draw_point_cloud_3d()draw_curve_3d()plot_geometry()gallery()sweep_strip()rotation_strip()animate_rotation()animate_geometry_sequence()plot_metric_radar()plot_metric_trajectory()