| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
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 layerpsCadenceFav— 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
- data ProgressionScore = ProgressionScore {
- psRootMotion :: !Double
- psVoiceLeading :: !Double
- psCadenceFav :: !Double
- psModeValidity :: !Double
- data ProgressionScoreWeights = ProgressionScoreWeights {
- wRootMotion :: !Double
- wVoiceLeading :: !Double
- wCadenceFav :: !Double
- wModeValidity :: !Double
- defaultWeights :: ProgressionScoreWeights
- defaultWeightsOffline :: ProgressionScoreWeights
- scoreProgression :: ProgressionContext -> ProgressionScore
- totalScore :: ProgressionScoreWeights -> ProgressionScore -> Double
- type TransitionMap = Map Text [(Text, Double)]
- cadenceFavFromMap :: TransitionMap -> Progression -> Double
- scoreProgressionOnline :: TransitionCache -> Text -> ProgressionContext -> DbActionT ProgressionScore
- scoreProgressionJazz :: TransitionCache -> ComposerWeights -> ProgressionContext -> DbActionT ProgressionScore
- data PolyScore = PolyScore {
- pyT :: !ProgressionScore
- pyS :: !ProgressionScore
- pyM :: !ProgressionScore
- pyQuality :: !Double
- pyDivergence :: !Double
- pyTotal :: !Double
- polyLayerWeights :: (Double, Double, Double)
- polyDivergenceWeight :: Double
- polyDivergence :: ProgressionContext -> Double
- scorePoly :: ProgressionScoreWeights -> ProgressionContext -> PolyScore
- scorePolyOnline :: TransitionCache -> Text -> ProgressionScoreWeights -> ProgressionContext -> DbActionT PolyScore
- jazzChangeKey :: CadenceState -> Text
- type TransitionCache = IORef TransitionMap
- newTransitionCache :: IO TransitionCache
- resolveJazzBlend :: Text -> DbActionT ComposerWeights
- cadenceFavFromKeys :: TransitionMap -> [Text] -> Double
- computeCadenceFav :: TransitionCache -> Text -> Progression -> DbActionT Double
Score record
data ProgressionScore Source #
Per-progression scoring breakdown. Each component is in [0, 1]; higher
is better.
Constructors
| ProgressionScore | |
Fields
| |
Instances
| Show ProgressionScore Source # | |
Defined in Harmonic.Evaluation.Scoring.Progression Methods showsPrec :: Int -> ProgressionScore -> ShowS # show :: ProgressionScore -> String # showList :: [ProgressionScore] -> ShowS # | |
| Eq ProgressionScore Source # | |
Defined in Harmonic.Evaluation.Scoring.Progression Methods (==) :: ProgressionScore -> ProgressionScore -> Bool # (/=) :: ProgressionScore -> ProgressionScore -> Bool # | |
data ProgressionScoreWeights Source #
Weights applied to each scoring axis in totalScore. Conventionally
sum to 1.0 so the resulting total stays in [0, 1].
Constructors
| ProgressionScoreWeights | |
Fields
| |
Instances
| Show ProgressionScoreWeights Source # | |
Defined in Harmonic.Evaluation.Scoring.Progression Methods showsPrec :: Int -> ProgressionScoreWeights -> ShowS # show :: ProgressionScoreWeights -> String # showList :: [ProgressionScoreWeights] -> ShowS # | |
| Eq ProgressionScoreWeights Source # | |
Defined in Harmonic.Evaluation.Scoring.Progression Methods (==) :: ProgressionScoreWeights -> ProgressionScoreWeights -> Bool # (/=) :: ProgressionScoreWeights -> ProgressionScoreWeights -> Bool # | |
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 ( |
| -> Text | Seek string (composer blend; same format as |
| -> 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).
Arguments
| :: TransitionCache | Cross-attempt fetch cache ( |
| -> ComposerWeights | Resolved jazz blend ( |
| -> 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)
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
| |
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'spdGeometry, 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.
newTransitionCache :: IO TransitionCache Source #
A fresh, empty TransitionCache.
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).