Lesson 2: OPTIC-K embeddings
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
An embedding is an array of numbers that describes an object, built so that similar objects get similar arrays. Text embeddings come out of a neural network and nobody can say what dimension 417 means. OPTIC-K is the opposite: every one of its 240 numbers is computed by C# code from music theory, and has a name. That makes it a good first embedding to study, because each number can be checked. This lesson computes the vectors of six chords with GA’s own classes and checks them.
All GA links point to commit a826864. The outputs come from:
dotnet run --project code/ga-ai/GaAi -c Release -- l2Where the name comes from
Section titled “Where the name comes from”In 2008, Clifton Callender, Ian Quinn and Dmitri Tymoczko described musical objects by the equivalences you choose to ignore: Octave, Permutation (the order of the notes), Transposition, Inversion and Cardinality (doubled notes) (Generalized Voice-Leading Spaces). Ignore octave and order, and a chord becomes a set of pitch classes; ignore transposition and inversion too, and it becomes a set class, the subject of lesson 4 of the music theory course. A K, for complementarity, comes from Harmonious, Jared Updike’s chord and scale reference: its Equivalence Groups page adds the letter and notes that it isn’t standard terminology. GA names its embedding after the full list, and its OpticKClass cites that page. The comments of TheoryVectorService map each block of numbers to the letters it is meant to cover.
The schema
Section titled “The schema”EmbeddingSchema.Partitions is the registry every consumer is meant to read: a name, a range of the 240 slots, a weight and a role for each partition.
== EmbeddingSchema.Partitions (OPTIC-K-v1.8)partition raw dims role weightIDENTITY 0-5 6 Identity 0.00STRUCTURE 6-29 24 Similarity 0.45MORPHOLOGY 30-53 24 Similarity 0.25CONTEXT 54-65 12 Similarity 0.20SYMBOLIC 66-77 12 Similarity 0.10EXTENSIONS 78-95 18 Info 0.00SPECTRAL 96-108 13 Info 0.00MODAL 109-148 40 Similarity 0.10HIERARCHY 149-163 15 Info 0.00ATONAL_MODAL 164-227 64 Info 0.00ROOT 228-239 12 Similarity 0.05TotalDimension 240sum of dims 240sum of weights 1.15CompactDimension 124CompactLayoutV4 optk-v4-pp-r:STRUCTURE:0-23,MORPHOLOGY:24-47,CONTEXT:48-59,SYMBOLIC:60-71,MODAL:72-111,ROOT:112-123SchemaHashV4 0x37CD8ECFWhat each partition describes, from the generator and its services:
| Partition | Describes | Computed from |
|---|---|---|
| STRUCTURE | which pitch classes, how many, the interval-class vector | the set of notes, octave and order ignored |
| MORPHOLOGY | the shape on the fretboard: bass and top note, span, fret position, barre | the physical voicing |
| CONTEXT | harmonic function, tension, motion | the analysis of the chord |
| SYMBOLIC | tags such as technique and style | tags from the analysis |
| MODAL | how strongly the chord suggests each mode: 40 slots, 33 of them named after a mode | the notes, relative to a root |
| ROOT | the root pitch class, one-hot | the root |
| IDENTITY, EXTENSIONS, SPECTRAL, HIERARCHY, ATONAL_MODAL | object kind, psychoacoustic and spectral features, complexity, set-theoretic families | stored, never scored |
Three things to notice:
- Only the six “Similarity” partitions are searched. Together they hold 124 numbers, the
CompactDimension. The five others are information for other tools; the index file doesn’t even store them (lesson 3). - The weights add up to 1.15, not 1. A vector compared with itself therefore scores 1.15, as the output below shows. Nothing breaks, since only the ranking matters, but a score is not a cosine in the usual 0 to 1 sense.
- The layout string is hashed.
SchemaHashV4is the CRC-32 ofCompactLayoutV4(lines 154-186). Issue #616 reports0x37CD8ECFfor GA’s live index of 313,047 voicings: the course and that index use the same layout.
Next to the registry, EmbeddingSchema keeps older constants for each partition. Three of them no longer match:
== Loose constants next to the registryconstant value registryHierarchyDim 8 15AtonalModalDim 17 64ExtensionsEnd 96 240ExtensionsEnd is right, 96 is where EXTENSIONS ends, but its comment says it equals TotalDimension, which was true when the vector had 96 numbers. The two others matter more: ModalVectorService allocates its ATONAL_MODAL array with AtonalModalDim, 17, and WriteInto copies whatever length it’s given without complaint, so 47 of that partition’s 64 slots can never be anything but zero. Lesson 3 finds exactly 47 on a whole corpus.
Versions
Section titled “Versions”The schema grew by appending partitions. The history below comes from GA’s schema documents, the version comments in EmbeddingSchema.cs and two commits:
| Version | Dimensions | What changed | Source |
|---|---|---|---|
| v1.2.1 | 96 | EXTENSIONS appended; “indices 0–77 are unchanged” | OPTIC-K_Embedding_Schema_v1.2.1.md, dated 2026-01-11 |
| v1.3, v1.3.1 | 108, 109 | SPECTRAL, then spectral entropy at index 108 | OPTIC-K_Embedding_Schema_v1.3.md, _v1.3.1.md |
| v1.4 | 216 | defined indices 0-135, the rest reserved | OPTIC-K_Embedding_Schema_v1.4.1.md, dated 2026-01-21 |
| v1.6 | MODAL and HIERARCHY | comments in EmbeddingSchema.cs |
|
| v1.7 | 228 | ATONAL_MODAL | comments in EmbeddingSchema.cs |
| v1.8 | 240 | ROOT appended; the root bonus removed from STRUCTURE | commit 95b5a9d9, 2026-04-19 |
The index format has its own history: v4 normalized the whole compact vector at once, v4-pp normalizes each partition separately (commit 3ba35365, 2026-04-19), and v4-pp-r adds ROOT, which moved the compact size from 112 to 124.
The documentation has not followed. The newest schema document stops at v1.4.1, and the one without a version in its name, OPTIC-K_Embedding_Schema.md, describes v1.3.1 and 109 dimensions. In the code, the generator’s summary still says “228-dimensional canonical musical embedding (v1.7)” (line 13) and its Dimension property “216 for v1.4/v1.5” (line 55), while both return 240. When a comment and the registry disagree, trust the registry, and better, print it.
From a chord shape to a document
Section titled “From a chord shape to a document”Guitarists write chord shapes from the low E string to the high E: x32010 is open C, with the low E muted. GA numbers strings the other way, from the high E, string 1, and its voicing generator builds positions in that order. The course’s Voicings.FromShape converts a chart into GA’s order, then Voicings.Embed runs the three steps GA’s own tools run:
var analysis = VoicingAnalyzer.Analyze(voicing);var doc = VoicingDocumentFactory.FromAnalysis(voicing, analysis, tuningId: "guitar");var raw = Generator.GenerateEmbeddingAsync(doc).GetAwaiter().GetResult();The middle step builds a ChordVoicingRagDocument, a record of about fifty properties that the generator reads. Six chords:
== From a chord shape to GA's voicing documentshape GA diagram GA name pcs MIDI doc root inv rootless functionx32010 0-1-0-2-3-x C 0 4 7 64 60 55 52 48 4 E 1 yes Functional Harmonyx35553 3-5-5-5-3-x C 0 4 7 67 64 60 55 48 7 G 2 yes Functional Harmony032010 0-1-0-2-3-0 C/E 0 4 7 64 60 55 52 48 40 4 E 1 yes Functional Harmonyx32000 0-0-0-2-3-x Cmaj7 0 4 7 11 64 59 55 52 48 4 E 1 yes Functional Harmonyx02210 0-1-2-2-0-x Am 0 4 9 64 60 57 52 45 4 E 2 yes Functional Harmony320003 3-0-0-0-2-3 G 2 7 11 67 59 55 50 47 43 7 G 0 yes Functional HarmonyThe names are right. The document’s root is not: it is E for open C and G for the barre C. VoicingDocumentFactory takes the first MIDI note as both the root and the bass:
RootPitchClass = analysis.MidiNotes.Length > 0 ? analysis.MidiNotes[0] % 12 : 0,MidiBassNote = analysis.MidiNotes.Length > 0 ? analysis.MidiNotes[0] : 0,Because the notes come string 1 first, MidiNotes[0] is the highest note, the melody note, not the bass and not the root. The inversion column is computed from that note too: open C in root position is reported as a first inversion. The chord root it is compared with is read by PitchClass.Parse, which takes the note names A and E for the numbers 10 and 11 (lines 43-44): outside the course program, a root-position E minor, 022000, came out with the inversion -1. Every partition that reads RootPitchClass or MidiBassNote inherits the error: ROOT, MODAL, part of MORPHOLOGY.
Two positional records
Section titled “Two positional records”The “rootless” column says yes for all six chords, each of which contains its root. The cause is a pattern every C# developer should recognise. VoicingCharacteristics is a positional record of eleven parameters, and VoicingHarmonicAnalyzer builds it with positional arguments:
// The record: (ChordId, DissonanceScore, Consonance, IntervalSpread, NoteCount,// IntervalClassVector, IsRootless, DropVoicing, IsOpenVoicing, Features, SemanticTags)return new( chordId, dissonanceScore, consonance, intervalSpread, pitchClasses.Count, pcSet.IntervalClassVector.ToString(), intervalSpread > 12, dropVoicing, false, [], semanticTags);intervalSpread > 12, “the notes span more than an octave”, is what an open voicing means, and it lands in IsRootless; the constant false lands in IsOpenVoicing. The compiler can’t object: both are bool. The same thing happens one level up, in VoicingAnalyzer, with a record declared as PerceptualQualities(double Brightness, double ConsonanceScore, double Roughness, string TexturalDescription, string Register):
var perceptualQualities = new PerceptualQualities(curVoiceChars.Consonance, 0, 0, "Neutral", "Medium");The consonance goes into Brightness, and ConsonanceScore is always 0. The program prints both records for the six chords:
== VoicingCharacteristics and PerceptualQualities, as VoicingAnalyzer fills themshape span IsRootless IsOpen Consonance Brightness ConsonanceScorex32010 16 True False 1.000 1.000 0.000x35553 19 True False 1.000 1.000 0.000032010 24 True False 1.000 1.000 0.000x32000 16 True False 1.000 1.000 0.000x02210 19 True False 1.000 1.000 0.000320003 24 True False 1.000 1.000 0.000The document copies ConsonanceScore into its Consonance property, and the generator computes CONTEXT’s tension as 1.0 - doc.Consonance (line 102). So the tension is 1 for every chord ever indexed. Issue #616 measured the CONTEXT partition as “empty” with one constant dimension and blamed the literals passed next to it; the constant dimension is this one, and its cause is the record above. Named arguments, new PerceptualQualities(Brightness: ..., ConsonanceScore: ...), would have made both slips visible at the call site.
The 240 numbers of open C
Section titled “The 240 numbers of open C”== The 240 values of x32010 (C), partition by partitionIDENTITY 3/6 1 0 1 0 0 1STRUCTURE 10/24 1 0 0 0 1 0 0 1 0 0 0 0 0.5 0 0 1 1 1 0 0 1 0.83 1 0MORPHOLOGY 7/24 0 0 0 0 1 0 0 0 0 0 0 0 0.17 0.83 1 0.87 -0.5 0.33 0 0 0 0 0 0CONTEXT 1/12 0 0 0 0 1 0 0 0 0 0 0 0SYMBOLIC 4/12 1 1 0 0 0 0 0 0 0 1 0 1EXTENSIONS 9/18 0 1 0.4 0 0 0.57 0.33 0.47 0.2 0 0 0 0.33 0 0 1 0.6 0SPECTRAL 13/13 0.2 0.26 0.61 0.37 0.4 0.47 0.29 0.6 0.53 0.29 0.64 0.5 0.16MODAL 2/40 0 0 0 0 0 0.43 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.67 (+13 zeros)HIERARCHY 1/15 0.33 0 0 0 0 0 0 0 0 0 0 0 0 0 0ATONAL_MODAL 11/64 0.2 0.5 0 1 0 0 0.33 0.33 0.33 0 0.03 0.44 0.67 1 0 0.88 (+48 zeros)ROOT 1/12 0 0 0 0 1 0 0 0 0 0 0 0MODAL slots set: Aeolian=0.43, LydianAugmentedSharp2=0.67STRUCTURE can be read by hand with TheoryVectorService.ComputeEmbedding open next to it:
| Slots | Values | Meaning |
|---|---|---|
| 0-11 | 1 0 0 0 1 0 0 1 0 0 0 0 |
the chroma: pitch classes 0, 4 and 7, C E G |
| 12 | 0.5 |
cardinality, 3 ÷ 12 × 2 |
| 13-18 | 0 0 1 1 1 0 |
the interval-class vector <001110> of a major triad |
| 19 | 0 |
complementarity, always passed as 0.0 |
| 20, 21 | 1, 0.83 |
consonance and brightness derived from the vector |
| 22 | 1 |
“a root is known” |
| 23 | 0 |
reserved |
The CONTEXT line is the constant tension of the previous section, at slot 4 of ContextVectorService; slots 6 to 11 are “reserved” and never written. The ROOT line has its 1 at pitch class 4, E, the top note. The MODAL slots are computed relative to a root as well, which would explain why a C major triad lights up Aeolian, a minor mode: read from E, C E G is E, G, C (an interpretation, to verify line by line in ModalVectorService).
Nine of MODAL’s named slots are never set by any of the 15,360 voicings of lesson 3’s corpus.
Similarity by hand
Section titled “Similarity by hand”GA compares two vectors partition by partition: the cosine of each similarity partition, multiplied by its weight, summed. WeightedPartitionCosine does it with TensorPrimitives.CosineSimilarity, and skips a partition that is all zeros. The course prints each cosine against open C:
== Cosine per partition, and GA's weighted score, against open Cpair structure morphology context symbolic modal root weighted compact dotx32010 x35553 1.000 0.348 1.000 1.000 0.000 0.000 0.837 0.837x32010 032010 1.000 0.997 1.000 0.750 1.000 1.000 1.124 1.124x32010 x32000 0.883 0.998 1.000 0.816 0.541 1.000 1.033 1.033x32010 x02210 0.888 0.998 1.000 0.816 0.153 1.000 0.996 0.996x32010 320003 0.776 0.496 1.000 0.671 0.000 0.000 0.740 0.740open C with itself: 1.150The first row, open C against the barre C at the third fret, by hand:
0.45 × 1.000 + 0.25 × 0.348 + 0.20 × 1.000 + 0.10 × 1.000 + 0.10 × 0.000 + 0.05 × 0.000 = 0.837
Read musically, the table is surprising. The barre C has exactly the notes of open C, yet it scores lower than Am (0.996) and Cmaj7 (1.033). Its STRUCTURE is identical; what sinks it is ROOT and MODAL, both computed from the top note, G instead of E, and its different hand shape. Open C and C/E, which share their top five strings, score 1.124. And CONTEXT is 1.000 on every row: 0.20 of every score is a constant.
The last column is the dot product of the two compact vectors. ExtractCompact keeps the six similarity partitions, divides each by its Euclidean norm and multiplies it by the square root of its weight. Then for a partition p, (√w · a/|a|) · (√w · b/|b|) = w · cos(a, b), and the dot product of two compact vectors is the weighted score. That’s why the index stores compact vectors: a single dot product per voicing replaces six cosines.
What the vector is invariant to
Section titled “What the vector is invariant to”STRUCTURE’s chroma ignores octaves and note order: any voicing of C E G gets the same twelve slots, which is why the barre C scores 1.000 there. Transposition is another matter. The course compares the STRUCTURE of C major with its eleven transpositions, and separately the slots 12 to 18, cardinality and interval-class vector:
== STRUCTURE of C major (0 4 7) against its 11 transpositions (TheoryVectorService)T pcs structure icv dims1 1 5 8 0.665 1.0002 2 6 9 0.665 1.0003 3 7 10 0.776 1.0004 4 8 11 0.776 1.0005 0 5 9 0.776 1.0006 1 6 10 0.665 1.0007 2 7 11 0.776 1.0008 0 3 8 0.776 1.0009 1 4 9 0.776 1.00010 2 5 10 0.665 1.00011 3 6 11 0.665 1.000set classes swept 222invariant (min cosine > 0.9999) 1mean of the minimum cosines 0.881worst minimum cosine 0.514The interval-class dimensions don’t move; the whole partition does, because the chroma names the literal notes. Over every set class of two notes or more, one is invariant, the twelve-note chromatic set, the only set equal to all its transpositions. These are the numbers of GA’s own sweep, state/quality/domain-invariants/README.md: “1 of 222 set classes is T-invariant”, mean 0.88, worst 0.51. TheoryVectorService records the finding in a comment; RootVectorService still says that moving the root out made STRUCTURE “genuinely O+P+T+I-invariant”.
So OPTIC-K is invariant to octave and order, as the name promises, and not to transposition or inversion: a search for “this shape in another key” can’t rely on STRUCTURE.
One MCP tool, two diagram orders
Section titled “One MCP tool, two diagram orders”GA exposes the generator to AI assistants through an MCP tool, ga_generate_voicing_embedding. Its description promises “a 228-dim OPTIC-K embedding vector” and gives the example 'x-3-2-0-1-0' for Cmaj7. Its parser reads the first element as string 1, the high E, like GA’s generator. The course copies that parser and feeds it the example:
== ga_generate_voicing_embedding's example diagram, x-3-2-0-1-0parsed by GA diagram MIDI GA name pcsthe MCP tool's parser x-3-2-0-1-0 62 57 50 46 40 Dsus2/E 2 4 9 10chord chart x32010 0-1-0-2-3-x 64 60 55 52 48 C 0 4 7The example is written like a chord chart, low E first, and parsed high E first: an assistant following the description gets the vector of a different chord. Written low E first it would be open C, and still not Cmaj7. The agentic coding course makes the general point: an MCP tool’s description is part of its interface, and the model believes it.
Exercises
Section titled “Exercises”- Using the table of cosines, compute the weighted score of open C against G (
320003) by hand, and check it against the output. - Why does open C against Am score 1.000 on ROOT, when their roots are C and A?
- The weights add up to 1.15. Change nothing in the code: how would you turn a GA score into a number between 0 and 1, and would it change any search result?
- Rewrite the
PerceptualQualitiescall ofVoicingAnalyzerwith named arguments, keeping the author’s evident intent.
Solutions
0.45 × 0.776 + 0.25 × 0.496 + 0.20 × 1.000 + 0.10 × 0.671 + 0.10 × 0.000 + 0.05 × 0.000 = 0.3492 + 0.124 + 0.20 + 0.0671 = 0.740, the value printed.- Because the document’s root is the top note, and both shapes have E on top,
64in their MIDI lists. The ROOT one-hot of both has its 1 at pitch class 4. - Divide by 1.15, the sum of the weights, or by the score of a vector with itself. It changes no ranking, since every score is divided by the same constant. It matters only where a score is compared with a fixed threshold.
new PerceptualQualities(Brightness: 0, ConsonanceScore: curVoiceChars.Consonance, Roughness: 0, TexturalDescription: "Neutral", Register: "Medium"). “Keeping the intent” is a guess: the brightness was perhaps meant to be computed too. With named arguments, the question is at least visible in review.
Key takeaways
Section titled “Key takeaways”- OPTIC-K is a hand-built embedding: 240 named numbers in 11 partitions, of which 6 partitions and 124 numbers are used for similarity, with weights summing to 1.15.
- Similarity is a weighted sum of per-partition cosines; storing each partition normalized and scaled by the square root of its weight turns it into a single dot product.
- The vector is invariant to octave and note order, not to transposition: STRUCTURE’s chroma names the notes.
- At commit
a826864, the document’s root is the top note, two positional records put values in the wrong slots, and CONTEXT is a constant: the vectors are deterministic, but several partitions don’t mean what their names say. - Comments and documents describe older versions (96, 109, 216, 228 dimensions); the registry and a program that prints it are the reliable source.
Sources
Section titled “Sources”- Clifton Callender, Ian Quinn and Dmitri Tymoczko, “Generalized Voice-Leading Spaces”, Science 320 (5874), 2008, pp. 346-348.
- GuitarAlchemist/ga at
a826864:Common/GA.Business.ML/Embeddings(EmbeddingSchema.cs,MusicalEmbeddingGenerator.cs,Services/TheoryVectorService.cs,Services/ContextVectorService.cs,Services/ModalVectorService.cs,Services/RootVectorService.cs),Common/GA.Business.ML/Rag/VoicingDocumentFactory.cs,Common/GA.Domain.Services/Fretboard/Voicings/Analysis(VoicingAnalyzer.cs,VoicingHarmonicAnalyzer.cs),Common/GA.Business.Core/Analysis/Voicings,Common/GA.Business.ML/Documentation/Schema,GaMcpServer/Tools/VoicingEmbeddingTool.cs,state/quality/domain-invariants/README.md; commits95b5a9d9and3ba35365. - GA issue #616, read on 2026-09-14.
- Microsoft Learn: TensorPrimitives.CosineSimilarity, Records, Named and optional arguments, Crc32.
- Wikipedia, Cosine similarity.