Metrics#

biotuner.harmonic_geometry.metrics#

Quantitative measurements over GeometryData (and time-resolved HarmonicSequence after a generator is applied) — the entry point for running science on top of the harmonic-geometry module.

This module is strictly geometry-side. For harmonic-content metrics on the underlying signal (consonance, dyad_similarity, euler, tenney, subharmonic_tension), use biotuner.metrics and biotuner.biotuner_group.BiotunerGroup directly — those tools already exist and are not duplicated here.

Public API#

geometry_metrics(geom) – scalar dict of structural +

per-method stats for a GeometryData

list_supported_kinds() – names of every recognised

metadata[‘kind’]

sequence_metrics(seq, generator, **kw) – apply generator to each

HarmonicInput frame and return per-frame geometry_metrics() as a dict of 1-D ndarrays

compare(geometries, labels=None) – N-way comparison table

(column-oriented dict)

normalize_metrics(rows, …) – min-max scale rows to [0, 1]

(data-driven; optional senses argument inverts metric direction)

MetricsLog – append-only log of metric rows

(geometry / generator output); CSV / JSON export

All extractors fall back gracefully — missing fields produce nan rather than raising.

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.

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,)
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.

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.

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).