theHarmonicAlgorithm-3.1.1: Real-time harmonic progression generation for TidalCycles live performance
Safe HaskellNone
LanguageGHC2021

Harmonic.Evaluation.Scoring.Progression

Description

Composable score of a ProgressionContext along four axes:

  • psRootMotion — per-edge root motion smoothness (Hindemith-derived)
  • psVoiceLeading — cyclic voice-leading cost over the triad layer
  • psCadenceFav — Neo4j-backed cadence transition favourability (0.0 if computed offline; online callers override)
  • psModeValidity — fraction of bars whose mode layer carries the expected 7-PC chroma (Phase 1 invariant guarantees walk-generated progressions score 1.0)

Each component lives in [0, 1] with higher = better. totalScore combines components via a weighted sum; defaultWeights makes cadence-favourability the dominant axis (the user's stated preference).

The normalisations applied to per-component raw measurements are intentionally simple placeholders — to be refined from observed distributions once multi-attempt generation is live (see the project's data-driven tuning memory). The function signatures are stable; only the internal transforms inside scoreRootMotion / scoreVoiceLeading / scoreModeValidity are subject to retuning.

Synopsis

Score record

data ProgressionScore Source #

Per-progression scoring breakdown. Each component is in [0, 1]; higher is better.

Default weights

defaultWeights :: ProgressionScoreWeights Source #

Cadence-favourability dominant: 0.5 fav, 0.25 each on root motion and voice leading. Mode validity carries NO weight: it is structurally 1.0 on every walk-generated progression (proven — see CLAUDE.md's mode-invariant note), so weighting it only compressed the usable score range to [0.2, 1.0]. It remains a hard viability gate (psModeValidity >= 1.0) in the attempt loop, which is the one job it can actually do. The three live weights are the old 0.2/0.2/0.4 renormalised — rankings are order-identical to the previous scale via new = (old − 0.2) / 0.8.

defaultWeightsOffline :: ProgressionScoreWeights Source #

Offline-mode weights — cadence favourability dropped (no graph), mode validity gate-only as in defaultWeights, the two live axes renormalised. Offline totals now span the full [0, 1] instead of having a 1/3 floor.

Scoring (offline)

scoreProgression :: ProgressionContext -> ProgressionScore Source #

Pure score: computes root-motion, voice-leading, and mode-validity components. psCadenceFav is left at 0.0 — online callers override this field after consulting the graph.

totalScore :: ProgressionScoreWeights -> ProgressionScore -> Double Source #

Weighted sum of components. Conventional weights sum to 1.0 → total in [0, 1].

Cadence-favourability (online + pure helper)

type TransitionMap = Map Text [(Text, Double)] Source #

A pre-fetched, composer-blend-resolved map from source cadence (keyed by its show representation) to its outgoing transitions. Each transition carries the destination Cadence and the blended weight (output of applyComposerBlend).

cadenceFavFromMap :: TransitionMap -> Progression -> Double Source #

Compute psCadenceFav from a pre-fetched transition map. Pure — no IO.

The progression is treated as a cyclic loop: edges are (C₀ → C₁), …, (C_{N-2} → C_{N-1}), (C_{N-1} → C₀). Each edge is scored by edgeScore (hybrid presence + share). The per-progression score is the mean of per-edge scores — length-independent and in [0, 1].

Matching by show (not Eq) matches the DB's identity convention (MATCH (c:Cadence {show: $show}) at Query.hs:113-137). The DB-side Cadence is reconstructed via constructCadence (movement, chord), which may differ from a generated Cadences cadenceIntervals field; the show instance projects to (movement, functionality) only.

scoreProgressionOnline Source #

Arguments

:: TransitionCache

Cross-attempt fetch cache (newTransitionCache).

-> Text

Seek string (composer blend; same format as _gcSeek).

-> ProgressionContext 
-> DbActionT ProgressionScore 

Online variant of scoreProgression. Pure components match the offline version exactly; psCadenceFav is populated from Neo4j edge weights under the composer blend parsed from the supplied seek string.

Runs inside DbActionT so the caller controls connection lifecycle (typically a single shared pipe across a multi-attempt loop).

scoreProgressionJazz Source #

Arguments

:: TransitionCache

Cross-attempt fetch cache (newTransitionCache).

-> ComposerWeights

Resolved jazz blend (resolveJazzBlend); empty = wildcard.

-> ProgressionContext 
-> DbActionT ProgressionScore 

Jazz-family variant of scoreProgressionOnline: pure components are identical (they are arity-agnostic); psCadenceFav is computed against the Change graph under the seek spec's JAZZ half — the spec is split by splitSeekByCorpus exactly as the genJ walk splits it, so an attempt loop ranks by the same musical priorities the walk sampled under. Node keys are the bars' own show identities (jazz names) — no triad projection.

Polytonal scoring (genE)

data PolyScore Source #

Composite score for a polytonal (FPoly) context. The other three families are one progression wearing three hats; genE is genuinely three progressions, so ranking its attempts on the foundation alone would leave the partner chains — the whole point of the family — unselected.

Constructors

PolyScore 

Fields

Instances

Instances details
Show PolyScore Source # 
Instance details

Defined in Harmonic.Evaluation.Scoring.Progression

Eq PolyScore Source # 
Instance details

Defined in Harmonic.Evaluation.Scoring.Progression

polyLayerWeights :: (Double, Double, Double) Source #

Layer weights for pyQuality: foundation, then each partner. The foundation owns the bass and the harmonic spine, so it carries half; the partners colour it and split the rest.

polyDivergenceWeight :: Double Source #

Share of pyTotal carried by pyDivergence rather than pyQuality.

A weighted sum rather than a multiplier: quality still leads, and divergence discriminates between walks that are otherwise equally good.

Calibrated 2026-08-29 against the requirement that raising attempt's K must raise BOTH the mean quality and the mean divergence of the selection. Two independent offline pools of 60 genE walks (len 6, entropy 0.4), 20k selection trials per cell, means of the selected attempt:

weight   divergence K=1 -> 4 -> 12      quality gain K=1->12
 0.00    0.685  0.696  0.682  (FALLS)          +0.143
 0.25    0.690  0.714  0.730  (+0.040)         +0.137
 0.40    0.689  0.733  0.752  (+0.063)         +0.128
 0.50    0.691  0.744  0.764  (+0.074)         +0.118

At zero the selection's divergence actually falls as K rises — quality alone decides, which is the failure this axis exists to prevent. 0.25 rises but flattens between K=4 and K=12. 0.40 is the knee: it buys 57% more divergence gain than 0.25 for less than a hundredth of quality gain, and both means climb monotonically. Past 0.50 the quality gain erodes faster than divergence improves.

polyDivergence :: ProgressionContext -> Double Source #

How far a polytonal context's three layers stand apart, [0, 1], averaged over bars. Two equally-weighted readings:

  • GEOMETRY — the partners sharing exactly two tones is the common-dyad shape; anything else is base-anchored, where S and M sound the full pentad against each other. Deliberately the same test as PolyGen's pdGeometry, so the number the ranker optimises and the label the diagnostics print can never disagree.
  • ROOT SPREAD — summed interval-class distance between the three roots, normalised by its maximum. Three triads on distant roots read as more polytonal than three sharing a neighbourhood.

scorePoly :: ProgressionScoreWeights -> ProgressionContext -> PolyScore Source #

Pure polytonal score — all three layers on the offline components.

scorePolyOnline :: TransitionCache -> Text -> ProgressionScoreWeights -> ProgressionContext -> DbActionT PolyScore Source #

Online polytonal score. Each partner bar is a real graph continuation of its own chain, so cadence favourability is as meaningful for the partners as for the foundation and all three are fetched. The shared TransitionCache keeps the extra passes cheap across a K-attempt loop.

jazzChangeKey :: CadenceState -> Text Source #

Jazz analogue of walkTriadCadence: project a bar onto its Change-graph node key. Identity for walk-generated bars (their functionality was already set by the jazz namer); maps a lead-produced cue onto the node the walk itself started from, so the cyclic favourability score no longer charges both cue edges as misses. Sets outside the jazz vocabulary keep their raw key and score 0 — honest.

type TransitionCache = IORef TransitionMap Source #

Mutable fetch cache threaded through a multi-attempt loop: source key → blend-resolved outgoing transitions. Attempts share source cadences heavily (same cue, same key, same filters), so attempts 2..K score almost query-free. Valid for ONE blend — create a fresh cache per generateBest call, never share across seek strings.

resolveJazzBlend :: Text -> DbActionT ComposerWeights Source #

Resolve a seek string's jazz half once (one fetchJazzComposers round trip), for threading through a multi-attempt loop. An empty seek blend (wildcard) resolves to the empty map without touching the graph.

cadenceFavFromKeys :: TransitionMap -> [Text] -> Double Source #

Key-level core of cadenceFavFromMap: cyclic per-edge mean over an arbitrary node-key sequence. The jazz scorer passes jazz node keys (whose show identity is the Change graph's) through the same arithmetic.

computeCadenceFav :: TransitionCache -> Text -> Progression -> DbActionT Double Source #

Cyclic per-edge favourability mean, computed against Neo4j. Builds the TransitionMap by fetching each unique source cadence's outgoing transitions once, applying the composer blend, then delegating to the pure cadenceFavFromMap.

Number of graph queries = number of distinct source-cadence show keys in the progression NOT already in the cache (≤ N for an N-bar progression on the first attempt; near zero on later attempts).