| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Harmonic.Evaluation.Database.Query
Description
This module implements the Evaluation (E) component of the Creative Systems Framework. It fetches transition probabilities from Neo4j and resolves composer-weighted scores for candidate cadences.
The database is treated as abstract/pitch-agnostic. Root notes and voicings are computed at runtime from user-defined starting conditions.
Synopsis
- type ComposerWeights = Map Text Double
- parseComposerWeights :: Text -> ComposerWeights
- normalizeWeights :: ComposerWeights -> ComposerWeights
- parseWeightsJson :: Text -> ComposerWeights
- fetchTransitions :: Text -> DbActionT [(Cadence, ComposerWeights)]
- fetchTransitionsAggregate :: Text -> DbActionT [(Cadence, Double)]
- data ChangeCandidate = ChangeCandidate {}
- fetchChangeTransitions :: Text -> DbActionT [(ChangeCandidate, ComposerWeights)]
- fetchChangeAggregate :: Text -> DbActionT [(ChangeCandidate, Double)]
- fetchChangeStarts :: DbActionT [(ChangeCandidate, Double)]
- resolveChangeCue :: Text -> DbActionT (Maybe Text)
- fetchJazzComposers :: DbActionT (Set Text)
- splitSeekByCorpus :: Set Text -> ComposerWeights -> (ComposerWeights, ComposerWeights)
- resolveWeights :: ComposerWeights -> [(a, ComposerWeights)] -> [(a, Double)]
- applyComposerBlend :: ComposerWeights -> [(a, ComposerWeights)] -> [(a, Double)]
Composer Weight Parsing
type ComposerWeights = Map Text Double Source #
Map from composer name to weight (e.g., "bach" -> 0.7)
parseComposerWeights :: Text -> ComposerWeights Source #
Parse a composer selection string into normalized weights.
Composer names are matched case-insensitively against the corpus —
Bach, "bach", BACH, "bAcH" all collapse to the same key.
Names are lower-cased here at parse time; resolveWeights lowercases
corpus edge keys at lookup time so the match is robust regardless of
the case convention used during corpus ingestion.
Supported formats: "bach debussy" -> equal weights, normalized to sum 1.0 "Bach:30 Debussy:70" -> weighted, normalized; case-insensitive "bach:0.3, debussy:0.7" -> already normalized (or re-normalized if needed)
Examples: parseComposerWeights "bach debussy" == Map.fromList [("bach", 0.5), ("debussy", 0.5)] parseComposerWeights "Bach:30 DEBUSSY:70" == Map.fromList [("bach", 0.3), ("debussy", 0.7)]
normalizeWeights :: ComposerWeights -> ComposerWeights Source #
Normalize weights so they sum to 1.0
parseWeightsJson :: Text -> ComposerWeights Source #
Parse the weights JSON string from Neo4j. Format: '{Debussy:3.0,Stravinsky:2.0}'
Graph Queries
fetchTransitions :: Text -> DbActionT [(Cadence, ComposerWeights)] Source #
Fetch all outgoing transitions from a cadence node.
Returns: List of (Cadence, ComposerWeights) pairs for all [:NEXT] edges. The Cadence is reconstructed from Neo4j node properties (movement, chord).
Query: MATCH (c:Cadence {show: $show})-[r:NEXT]->(n:Cadence) RETURN n.movement, n.chord, r.weights
fetchTransitionsAggregate :: Text -> DbActionT [(Cadence, Double)] Source #
Fetch outgoing transitions with the pre-aggregated corpus score.
Fast path for the wildcard composer spec ("*", i.e. an empty blend):
every edge stores r.confidence, written at ingestion as exactly
sum (Map.elems weights) (Harmonic.Rules.Import.Graph), which is the
score resolveWeights computes for an empty blend. Projecting it directly
skips the r.weights JSON payload (all corpus composers per edge, ~1MB/
fetch on high-degree nodes) — measured ~30x faster and ~115x less
allocation per generation step.
Result contract matches
exactly: positive scores only (always true by the ingestion invariant —
zero-total edges are never written), sorted highest first.applyComposerBlend Map.empty $ fetchTransitions
Query: MATCH (c:Cadence {show: $show})-[r:NEXT]->(n:Cadence) RETURN n.movement, n.chord, r.confidence
Jazz (Change) Graph Queries
data ChangeCandidate Source #
One candidate next node in the jazz graph: its show key (the walk
state), plus the movement and anchor-relative zero-form set parsed
back from node properties.
Constructors
| ChangeCandidate | |
Instances
| Show ChangeCandidate Source # | |
Defined in Harmonic.Evaluation.Database.Query Methods showsPrec :: Int -> ChangeCandidate -> ShowS # show :: ChangeCandidate -> String # showList :: [ChangeCandidate] -> ShowS # | |
| Eq ChangeCandidate Source # | |
Defined in Harmonic.Evaluation.Database.Query Methods (==) :: ChangeCandidate -> ChangeCandidate -> Bool # (/=) :: ChangeCandidate -> ChangeCandidate -> Bool # | |
fetchChangeTransitions :: Text -> DbActionT [(ChangeCandidate, ComposerWeights)] Source #
Outgoing jazz transitions with full per-composer weights — the blend
path, mirroring fetchTransitions for the Change label.
fetchChangeAggregate :: Text -> DbActionT [(ChangeCandidate, Double)] Source #
Outgoing jazz transitions on the pre-aggregated corpus score — the
wildcard fast path, mirroring fetchTransitionsAggregate. Sorted
highest first.
fetchChangeStarts :: DbActionT [(ChangeCandidate, Double)] Source #
Every jazz start candidate with its outgoing corpus mass — the
popularity-weighted pool the uncued genJ
cue draws from (structure only: Change nodes are zero-form, the root
comes from the caller's tonal context). Nodes with no outgoing edges
are excluded — a walk could not leave them.
resolveChangeCue :: Text -> DbActionT (Maybe Text) Source #
Resolve a cue functionality to a jazz walk-start node. Exact movement+name keys are the caller's own lookup; this finds the best node for a bare functionality: prefer the pedal arrival, then the node with the most outgoing corpus mass.
fetchJazzComposers :: DbActionT (Set Text) Source #
Every composer key present in the jazz graph — the corpus membership
test that splits a mixed seek spec into its jazz-blend and
classical-steer halves. One scan of the (small) Change edge set,
keys unioned client-side.
splitSeekByCorpus :: Set Text -> ComposerWeights -> (ComposerWeights, ComposerWeights) Source #
Split a parsed seek blend into its (jazz, classical-steer) halves
against the jazz graph's key set. A token names a jazz composer when
it matches a key exactly or as a substring ("monk" matches
"theloniousmonk" and co-credits); its weight is split equally across
all matching keys. Unmatched tokens are classical steer names. Both
halves are renormalised. Pure — callers fetch the key set once
(fetchJazzComposers) and reuse it.
Weight Resolution
resolveWeights :: ComposerWeights -> [(a, ComposerWeights)] -> [(a, Double)] Source #
Resolve transition weights using the active composer blend.
For each candidate (Cadence, ComposerWeights), compute a single score by multiplying each composer's edge weight by the user's blend weight and summing.
Example: candidate weights: {"bach": 5, "debussy": 3} user blend: {"bach": 0.7, "debussy": 0.3} score = 5 * 0.7 + 3 * 0.3 = 4.4
applyComposerBlend :: ComposerWeights -> [(a, ComposerWeights)] -> [(a, Double)] Source #
Apply composer blend to filter transitions, keeping only those with score > 0