Geometry metrics and chord transitions#

Every generator in harmonic_geometry returns a GeometryData that geometry_metrics() summarises into a scalar dict — span, energy, active fraction, edge statistics, mode counts, etc. The same scalars feed the radar plot and the trajectory plot in this notebook, so you can compare chords on the same plate or watch metrics evolve along a chord morph.

The transitions submodule provides interpolate_chords, fade_in_components, and blend_fields to drive animation frames — used in the GeometryV2 reel and reproducible here on still frames.

import warnings
from fractions import Fraction

import numpy as np
import matplotlib.pyplot as plt

from biotuner.harmonic_geometry import HarmonicInput, plotting

warnings.filterwarnings("ignore")
plt.rcParams["figure.dpi"] = 110

Radar — six chords on a rectangular Chladni plate#

from biotuner.harmonic_geometry import chladni_from_input, geometry_metrics

chords = {
    "Major": HarmonicInput(ratios=[Fraction(1), Fraction(5, 4), Fraction(3, 2)]),
    "Minor": HarmonicInput(ratios=[Fraction(1), Fraction(6, 5), Fraction(3, 2)]),
    "Sus4":  HarmonicInput(ratios=[Fraction(1), Fraction(4, 3), Fraction(3, 2)]),
    "Aug":   HarmonicInput(ratios=[Fraction(1), Fraction(5, 4), Fraction(8, 5)]),
    "Dom7":  HarmonicInput(ratios=[Fraction(1), Fraction(5, 4),
                                    Fraction(3, 2), Fraction(7, 4)]),
    "Dim7":  HarmonicInput(ratios=[Fraction(1), Fraction(6, 5),
                                    Fraction(7, 5), Fraction(12, 7)]),
}
rows = [
    geometry_metrics(chladni_from_input(
        inp, plate="rectangular", plate_kwargs={"resolution": 129},
    ))
    for inp in chords.values()
]
fig, _ = plotting.plot_metric_radar(
    rows, labels=list(chords.keys()),
    metrics=["n_modes", "energy", "peak_abs",
             "active_frac", "field_std", "zero_crossing_frac"],
    title="geometry_metrics — chladni_from_input across six chords",
);
../../_images/97f7131c9bebdaa338b63922aca9c45c2283cce0e6f1c54a35a417d99f0ce007.png

Trajectory — recursive polygon along a chord morph#

recursive_polygon was chosen because its scalar metrics (perimeter, scale_factor, bump_angle, area) vary continuously with the chord — every interpolation step changes the output. Topology-driven generators jump in discrete steps and are less informative here.

from biotuner.harmonic_geometry import interpolate_chords, recursive_polygon

major7 = HarmonicInput(ratios=[Fraction(1), Fraction(5, 4),
                                Fraction(3, 2), Fraction(15, 8)])
dom7   = HarmonicInput(ratios=[Fraction(1), Fraction(5, 4),
                                Fraction(3, 2), Fraction(7, 4)])
dim7   = HarmonicInput(ratios=[Fraction(1), Fraction(6, 5),
                                Fraction(7, 5), Fraction(12, 7)])
def morph(a, b, n):
    return [interpolate_chords(a, b, i/(n-1)) for i in range(n)]

frames = morph(major7, dom7, 12) + morph(dom7, dim7, 12)
metrics_per_frame = [geometry_metrics(recursive_polygon(f, depth=3))
                     for f in frames]

# plot_metric_trajectory expects a dict {name: array(T)} — transpose the
# per-frame list of dicts to the column-oriented layout it wants.
keys_to_plot = ["perimeter", "scale_factor", "bump_angle", "area"]
metric_arrays = {
    k: np.array([m.get(k, np.nan) for m in metrics_per_frame], dtype=float)
    for k in keys_to_plot
}

fig, _ = plotting.plot_metric_trajectory(
    metric_arrays,
    metrics=keys_to_plot,
    title="recursive_polygon metrics across Major7 → Dom7 → Dim7",
);
../../_images/a0bcb18932a7e719f1a3b2f5f42c7bd9d2f0782394ec7f08925039f58e6c85f4.png

interpolate_chords — visualising the morph#

Sampling the morph at fixed t and rendering with recursive_polygon gives a visual preview of what the animation does between two chords.

ts     = np.linspace(0.0, 1.0, 6)
frames = [interpolate_chords(major7, dom7, float(t)) for t in ts]
geoms  = [recursive_polygon(f, depth=3) for f in frames]
plotting.gallery(geoms, titles=[f"t={t:.2f}" for t in ts], n_cols=6,
                 suptitle="interpolate_chords: Major7 → Dom7 (depth-3 polygon)");
../../_images/81c0bc85d7266f86c0049c862dff7087dbf7330551d1a7498d874deb8a1d0959.png

fade_in_components — growing a chord by extension#

Useful for animations that build up a chord one component at a time — ramp t from 0 to 1 and the extra components appear without disturbing the ratio of the existing ones.

from biotuner.harmonic_geometry import fade_in_components

base = HarmonicInput(ratios=[Fraction(1), Fraction(5, 4), Fraction(3, 2)],
                     amplitudes=[1.0, 0.8, 0.7])
ext  = HarmonicInput(ratios=[Fraction(1), Fraction(5, 4), Fraction(3, 2),
                              Fraction(7, 4), Fraction(15, 8)],
                     amplitudes=[1.0, 0.8, 0.7, 0.6, 0.5])
ts     = np.linspace(0.0, 1.0, 5)
frames = [fade_in_components(base, ext, float(t)) for t in ts]
geoms  = [recursive_polygon(f, depth=3) for f in frames]
plotting.gallery(geoms, titles=[f"t={t:.2f}" for t in ts], n_cols=5,
                 suptitle="fade_in_components: triad → extended pentad");
../../_images/31ff32b2a0d2d5cbc4f2aaa0bd43633b7d5155d6d8f4155fc3949a6c7e094bd7.png

blend_fields — pixel-space crossfade between two algorithms#

Used in the reel to morph between two paradigms on a shared grid — for instance a Chladni plate fading into a quasicrystal field. The two geometries must share field_grid shape.

from biotuner.harmonic_geometry import (
    blend_fields, chladni_field_rectangular,
    harmonic_interference_field_2d,
)
a = chladni_field_rectangular([(2, 3), (3, 5), (4, 1)], resolution=129)
b = harmonic_interference_field_2d(
    HarmonicInput(ratios=[Fraction(1), Fraction(5, 4), Fraction(3, 2)]),
    resolution=129, extent=1.0,
)
ts     = np.linspace(0.0, 1.0, 5)
frames = [blend_fields(a, b, float(t), require_same_grid=False) for t in ts]
plotting.gallery(frames, titles=[f"t={t:.2f}" for t in ts], n_cols=5,
                 suptitle="blend_fields: Chladni → interference (pixel crossfade)");
../../_images/5682b92acec45d4e72a842292eaa1bc8f50f6faa9bcedfa9a071f253a51d5601.png