Skip to content
learn

Journal

  • Lab scaffold: mission, outline of seven prototypes, CI workflow
  • P1, voicing space explorer: build step, page, measurements, lesson
  • P2, play a chord, see the universe
  • P3, album covers
  • P4, AI render pass
  • P5, 3D-printable bracelets
  • P6, full chain
  • P7, playability model

What the lab found in GuitarAlchemist/ga while measuring, not while reading. Line numbers point at 66bdd049, the commit the index was exported from. None of these is a bug report yet: they are what the measurements showed.

Expected What happens Where Measure Status
The index and the two FretDiagram renderers agree on which string comes first The index writes a diagram high E first; both renderers read it low E first FretDiagram.cs:14, FretDiagram.tsx:8-11 Row 1000 is 1-2-x-5-x-1 for MIDI 65, 61, 55, 41; 30,000 sampled rows read high E first, 0 mismatches Fixed upstream by #696, merged 2026-09-23: the React renderer honours the high-E-first order the API sends, with a test; the C# FretDiagram now documents its low-to-high tokens
The React FretDiagram shows every fretted note It drops a dot when the grid starts at the nut and the voicing reaches a fret below the last row FretDiagram.tsx lines 31 and 104 15,853 of 297,910 guitar voicings, 5.32 % Fixed upstream by #696, merged 2026-09-23: the grid’s fret window is computed so every fretted note has a row, with a test
OptickIndexReader’s summary describes the vectors it returns It says “L2-normalized”; each partition is normalized, then scaled by the square root of its weight OptickIndexReader.cs Squared length 1.15 or 1.05, never 1 Fixed upstream by #696, merged 2026-09-23: the summary now gives the squared length as the sum of the partition weights, 1.15 or 1.05, and a test checks it on the vectors. The behaviour was right and is unchanged
Two voicings that sound and look different are two points Voicings with the same pitch classes, the same top note and a similar shape collapse onto one vector OptickIndexWriter.cs 121,768 of 297,910 guitar vectors are bit-for-bit copies of another, 40.9 %; the largest group holds 12 voicings of F♯ diminished with A on top Reproduced. A consequence of the embedding rather than an obvious defect — to arbitrate before changing anything
The same commit exports the same index Deduplication keeps the cheapest voicing of each group, generation runs in parallel, and ties go to whichever arrives first OptickIndexWriter.cs Two exports of one build: every shared vector identical, 70 voicings present in one and missing from the other Fixed upstream by #730, merged 2026-09-24: generation is still parallel, but the voicings come out in fret order, so ties go to the same voicing every run. Two guitar exports of GA’s main hashed 776c849e… and d0a7d1d9… before, 138603e8… both times after
--export-max N samples the index It keeps the first N raw voicings in generation order, which starts high on the neck OptickIndexWriter.cs --export-max 3000 produced 2,641 vectors, 0 of them in the full index Changed by #730: the first N voicings are now the same on every run and come from the nut up, in fret order; they are still not a sample of the index. Not re-measured (to verify)
A slash chord names its bass with the spelling of the key Some names use the wrong enharmonic, for example Gmaj7/Gb instead of Gmaj7/F♯ index chord names Seen while reading the largest duplicate groups Reproduced, not yet counted
barreRequired is true for a barre chord It asks for the same fret on three adjacent strings, so the E-shape F barre 133211 — strings 1, 2 and 6 at fret 1 — is not a barre, while the A-shape x13331 is VoicingPhysicalAnalyzer.cs:219-233 133211 and the four-string xx3211 both score exactly 4.50 Reproduced (P7)
minimumFingers counts fingers It counts distinct frets, capped at 4 VoicingPhysicalAnalyzer.cs:102-103 Am x02210 and Am7 x02010 both score 2.29; same for E against E7 and D against Dsus4 Reproduced (P7)
The CAGED shape names the shape It reads a voicing’s first string as string 6, but GA stores string 1 (high E) first; and it gives up as soon as string 6 is muted, before trying the A and C shapes VoicingPhysicalAnalyzer.cs, DetectCagedShape Over GA’s 667,125 generated guitar voicings: 92 tagged E-Shape, all wrong (0-2-2-1-x-x, which is xx1220), no A or C shape, nothing for 133211 or open E. With the fix: 92 E, 46 A, 152 C shapes Found while reviewing a fix for P7; fixed upstream by #735, merged 2026-09-25 2026-09-24
The Difficulty label and the DifficultyScore agree The label’s “Beginner” band asks for a span of at most 64 mm, under two frets at the nut, so the open C chord is “Intermediate”; and 141404, labelled “Advanced”, scores 4.65, the 26th percentile of the index VoicingPhysicalAnalyzer.cs:105-112 3.50 and “Intermediate” for x32010 Reproduced (P7)
A voicing nobody can play scores high 101,967 of 297,883 guitar voicings (34.2 %) have no legal fingering under a four-finger model, and the highest score GA gives one of them is 7.73, inside the range of ordinary chords, so maxDifficulty: 8 returns them VoicingFilterService.cs:53-70 The search of fingering.mjs over the whole index Reproduced (P7). The hand model is mine and has no thumb, so 34.2 % is an upper bound
ga_chord_to_set('Cdim7') returns GA’s own diminished-7, 0 3 6 9 It returns 0 3 6 10, a half-diminished seventh, Forte 4-27; ga_parse_chord('Cdim7') gives quality diminished with component ext:7, so the seventh is added as a minor one GA’s running MCP server against CanonicalChordPatternCatalog.cs:66 The exchange in results/ga-check.json, 2026-09-17 Reproduced (P5), not reported. Fixed upstream by #681, merged 2026-09-23; re-run on 27a1257: 0 3 6 9, 4-28 2026-09-24
ga_chord_to_set('Cm7b5') returns GA’s own half-diminished-7, 0 3 6 10 It returns 0 3 7 10, a plain minor seventh: the b5 alteration is dropped GA’s running MCP server against CanonicalChordPatternCatalog.cs:65 The same exchange; get_scale_notes and Cmaj7 agree with the files, these two do not Reproduced (P5), not reported. Fixed upstream by #681; re-run on 27a1257: 0 3 6 10, 4-27 2026-09-24

Each prototype writes its hypotheses before the measuring script exists, and they are committed first. A refuted hypothesis stays here: it is the result that cost the most to get.

Question Hypothesis, written before measuring Result Verdict
How many dimensions does the voicing space really need? Committed in hypotheses.md at 03f2bbb; the file says which one was not blind 3 components explain 22.2 % of the variance, 10 explain 48.0 %, 32 explain 84.7 %, 64 explain 99.9 % The first two axes are mostly MORPHOLOGY, the third STRUCTURE, CONTEXT weighs nothing (2026-09-17)
Does a 3D view keep the neighbours the exact search finds? H2 set a recall@10 threshold of 0.15; H3 predicted the full index would do worse than the sample Recall@10 of 0.168 within the 30,000 sample, 0.348 against all guitar voicings; with 32 components, 0.693 and 0.787 H2 missed its threshold, H3 was wrong — both refuted (2026-09-17)
Is WebGPU faster than WebGL 2 for 300,000 points? A first series said 3.5 ms against 0.5 ms and looked like a finding With trackTimestamp, the render pass takes 0.066 ms on WebGPU and 0.071 ms on WebGL 2 at 30,000 points, 0.26 ms for all 297,910, 2.3 ms for 3 million random points The first series was a measuring mistake: 3.5 ms is how long onSubmittedWorkDone takes to resolve, not GPU work. The lesson quotes the second series (2026-09-17)
Does picking on the CPU hold at full index size? H6 expected more than 16 ms at 297,910 points 0.2 ms for 30,000, 1.4 ms for 297,910, 14 ms for 3 million H6 refuted (2026-09-17)
Can the browser hold the whole index? — 156 MB loaded and decoded in 341 ms, 180 MB of JavaScript heap, 28 ms to find the ten exact neighbours It can, and the lesson ships that mode (2026-09-17)
Can a chromagram in a page name the chord you play? Eight figures predicted before any recording 79.3 % of 624 synthetic strums named exactly — 89.1 % in root position, 50 % for inversions — and 11 of 17 CC0 recordings; estimating the notes before the chromagram was worth 39 points Seven of the eight figures held; the oracle, the noise conditions and the recorded failures did not (2026-09-17)
Can a small model trained on one CPU core beat GA’s hand-written playability cost? Twelve figures committed in hypotheses.md at c6a9937 Gradient boosting reaches a Spearman of 0.964 against the target and a pairwise accuracy of 0.921, where GA’s score reaches 0.596 and 0.715; a least-squares line through the physical span alone reaches 0.794 Yes, but the finding is that GA’s three extra flags cost it more than they earn (2026-09-17)
How many of GA’s guitar voicings can four fingers play? H11: more than 98 % 65.8 %: the search finds no legal fingering for 101,967 of 297,883 Badly refuted, and it split the prototype in two: a ranking task on what can be played, a yes-or-no question on the rest (2026-09-17)
Does splitting by voicing instead of by chord flatter the model? H6: the naive split overstates the boosted trees by 0.005 to 0.05 of Spearman 0.0005; the split by transposed shape moves it by 0.002 Refuted. Group splitting only costs you when the group carries information the features do not, and here both sides are read off the same diagram (2026-09-17)
Is the fingering search expensive enough to be worth replacing with a model? H12: the model scores a voicing at least 50 times faster 3.8 times: 0.0145 ms against 0.0558 ms Refuted. The premise of the prototype was wrong; what the model buys is 949 KB that ships without the hand model (2026-09-17)
Can a pitch-class bracelet become an object a slicer accepts? Twelve figures committed in hypotheses.md at 8a2ed53 30 solids out of 30 watertight, one body, Euler characteristic 0; 5.2473 cm3, bore 64.9913 mm, thinnest wall 1.9954 mm, no face below the 45-degree support threshold; PrusaSlicer 2.9.6 gives 6.42 g and 35 min 40 s Yes, and four of the twelve predictions were refuted (2026-09-17)
Does a mesh written without a boolean union fail its checks? H2: the fourteen parts concatenated are not watertight They are watertight, manifold3d accepts them, and they are 14 bodies; volume 2.6 % high, surface 10.8 % high, wall measured at 1.40 mm — and PrusaSlicer slices them to the same 6.42 g with no repair message Refuted twice: neither the mesh checks nor the slicer notice (2026-09-17)
How much of a bracelet needs support if its beads are spheres? H6: between 3 % and 8 % of the surface 0.35 %, worst slope 4.87 degrees Refuted in magnitude, right that supports are needed (2026-09-17)
Does the bead prototype 2 draws survive a 2 mm wall? H4: the bore stays at 64.98 mm or more A sphere of radius 3.0 mm centred on the outer surface takes the bore to 63.0287 mm, 1.96 mm lost The spherical variant failed and is published as a failure; the cone ships (2026-09-17)

2026-09-16 — P1: where the data comes from

Section titled “2026-09-16 — P1: where the data comes from”
  • GA’s main was at 66bdd049 (“chore(quality): snapshot 2026-09-16”). The index isn’t in the repository: FretboardVoicingsCLI --export-embeddings generates it into state/voicings/optick.index.
  • I didn’t rebuild it. The session measuring GA’s performance had built FretboardVoicingsCLI in a clean clone at that commit and exported the index between 23:00 and 23:02 local time, in 149 seconds. Its log says “688,351 raw, 313,047 unique”. I copied that file and pinned it by hash: 183,770,270 bytes, SHA-256 1e91e4695bab8aa4bb451fdd1f199a4afaa4c4ad913f08acc5163c6ab07caa23, 313,047 voicings (297,910 guitar, 7,795 bass, 7,342 ukulele), 124 dimensions, schema hash 0x37cd8ecf, the one my CRC-32 of EmbeddingSchema.CompactLayoutV4 gives.
  • Row 1000 settles the string order. Its diagram is 1-2-x-5-x-1 and its MIDI notes are 65, 61, 55, 41: read string 1 first (high E + 1 = F4, 65), not low E first. On a chord chart, that shape is 1x5x21. The build step checks all 30,000 sampled rows the same way: 0 mismatches.
  • GA disagrees with itself here. FretDiagram.cs and the React FretDiagram.tsx take frets low E first; the index writes them high E first. The GA AI course found the same split in an MCP tool’s example.
  • The index’s vectors don’t have unit length, although OptickIndexReader’s summary says “L2-normalized”: each partition is normalized, then scaled by the square root of its weight, so a vector’s squared length is the sum of the weights of its non-empty partitions, 1.15 or 1.05. The dot product is still the weighted partition cosine, which is what the search needs.
  • Written in results/hypotheses.md and committed in 03f2bbb, before the measurement script existed.
  • One of them isn’t blind, and the file says so: a smoke test of the build step on 2,000 voicings had already printed that three components explain 22.2 % of the variance.
  • Three principal components explain 22.2 % of the variance, ten 48.0 %, 32 84.7 %, 64 99.9 %. The first two axes are mostly MORPHOLOGY, the third STRUCTURE; CONTEXT weighs nothing.
  • Recall@10 of the 3D view against the exact neighbours: 0.168 within the 30,000 sample, 0.348 against all guitar voicings. With 32 components: 0.693 and 0.787. H2 missed its 0.15 threshold; H3, which predicted the full index would do worse, was wrong.
  • 121,768 of the 297,910 guitar vectors are bit-for-bit copies of another voicing’s vector. The largest group holds 12 voicings of F♯ diminished with A on top. Voicings with the same pitch classes, the same top note and a similar shape are the same point for OPTIC-K.
  • GA’s React FretDiagram drops a dot for 15,853 guitar voicings (5.32 %): the grid starts at the nut when the lowest fretted note is on fret 2, and a voicing reaching fret 6 needs a sixth row.

2026-09-17 — P1: two exports, one commit

Section titled “2026-09-17 — P1: two exports, one commit”
  • The performance session exported the index a second time from the same build. Matched by instrument and diagram, every shared vector is identical, but 70 voicings of one export are missing from the other: deduplication keeps the cheapest voicing of each group, the generator runs in parallel, and ties go to whichever arrives first. The file’s hash pins the data; the commit doesn’t.
  • I tried to confirm the provenance by regenerating 3,000 guitar voicings with GA’s CLI (--export-max 3000, 4.1 s under the heavy-work lock). It found 0 of their 2,641 vectors in the full index. The limit keeps the first 3,000 raw voicings in generation order, which started at fret 19; within that small set, deduplication kept high-neck voicings that lose to cheaper shapes in the full export. A partial export can’t check a full one.

2026-09-17 — P1: frame times, measured twice

Section titled “2026-09-17 — P1: frame times, measured twice”
  • First series: render() plus onSubmittedWorkDone, 120 frames at 1280 × 720 in headless Chromium 153 on the RTX 5080. WebGPU came out at 3.5 ms for 30,000 points and WebGL 2 at 0.5 ms. That looked like a finding, and it was a measuring mistake: WebGPU’s number didn’t change between 30,000 and 3 million points.
  • Second series, with three.js’s trackTimestamp and resolveTimestampsAsync: the render pass takes 0.066 ms on WebGPU and 0.071 ms on WebGL 2 for 30,000 points, 0.26 ms for all 297,910, 2.3 ms for 3 million random points. The 3.5 ms is the time onSubmittedWorkDone takes to resolve, not GPU work.
  • renderer.info.render.drawCalls read 434 and then negative values in the probe, because three.js resets it at each animation frame and the probe awaits across frames. The lesson doesn’t quote it.
  • Picking by projecting every point on the CPU: 0.2 ms for 30,000, 1.4 ms for 297,910, 14 ms for 3 million. H6 expected more than 16 ms at 297,910.
  • Local full-index mode: 156 MB loaded and decoded in 341 ms, 180 MB of JavaScript heap, 28 ms to find the ten exact neighbours of a voicing in the browser.
  • No pipeline for live demos existed on the site yet. scripts/publish.mjs builds the page with Vite (base: './') into public/ga-lab/p1/, which Astro copies as is: 3.65 MB, plus a 241 KB screenshot. The lesson shows the screenshot and a button, and loads the page in an iframe only on click, through a small LazyDemo component that prefixes the site’s base path.
  • scripts/check-published.mjs serves public/ under /learn/ like GitHub Pages and opens the published page: WebGPU, 30,000 points, no error.

2026-09-17 — P2: play a chord, see the universe

Section titled “2026-09-17 — P2: play a chord, see the universe”
  • P2 published. The chord recognizer names 79.3 % of 624 synthetic strums exactly (89.1 % in root position, 50 % for inversions) and 11 of 17 CC0 recordings; estimating notes before the chromagram was worth 39 points. Seven of eight predicted figures held; the oracle, the noise conditions and the recorded failures did not. The audio never leaves the page (connect-src ‘none’). Lesson

2026-09-17 — P7: the label that does not exist

Section titled “2026-09-17 — P7: the label that does not exist”
  • The question is whether a model beats GA’s DifficultyScore, and GA’s score cannot also be the answer sheet. So P7 builds a target: fingering.mjs enumerates every legal way of putting fingers 1 to 4 on the fretted notes — any finger may barre, a barre may hold part of a fret and pass under higher notes — and keeps the cheapest. The fret and string geometry is GA’s own; every weight is a constant I chose and listed in WEIGHTS.
  • Three checks on that target, none of them proof: 32 pairwise judgements on canonical shapes in annotations.json, whose annotator is named in the file (this model, from written rules, not a guitarist); a checklist of what method books say; and GA’s own score. The search agrees with 25 of the 25 high-confidence pairs, GA with 22, the checklist with 19. The search’s perfect score measures internal consistency — the same head wrote the hand model and the protocol — and the file says so.
  • Reading GA’s five lines before measuring anything found four things, all in the QA table above: the barre flag misses the F barre, minimumFingers counts frets, a full barre and a four-string F score identically, and the Difficulty label contradicts the DifficultyScore.
  • Twelve numbered predictions in results/hypotheses.md, committed in c6a9937 before train.mjs had been run on any data, synthetic or real.
  • H0 is marked “seen, not blind”: the annotation check had already run when the file was written, and its three numbers are recorded as measurements, not predictions. They did not move when the search was later generalised.

2026-09-17 — P7: what the search rejects

Section titled “2026-09-17 — P7: what the search rejects”
  • The first version of the search let only the index finger barre, and only across every note of the lowest fret. It rejected 61.9 % of the guitar index. Letting any finger barre, over part of a fret, passing under higher notes, brought that to 34.2 % — 101,967 of 297,883 voicings with no legal fingering at all.
  • What is left is genuinely unplayable: x12345 needs five fingers on five frets, and 103212 has two notes at fret 1 on either side of an open string, so neither of the two barres it would need is possible.
  • Those rows have no rank, so the experiment splits in two: the ranking task drops them, and a second model answers whether a voicing can be fingered at all. Split by chord, gradient boosting reaches an accuracy of 0.903 against a majority baseline of 0.663, and an AUC of 0.973; GA’s score, read as a ranker for the same question, reaches 0.677.
  • One core of an Intel Core Ultra 9 285K, Node.js v24.12.0, no GPU, no dependencies. 297,883 voicings, 195,916 of them fingerable, split by chord into 118,243 / 38,287 / 39,386. The whole run — three splits, six predictors each, bootstrap intervals over 200 rounds resampled by chord, and the feasibility model — took 302 seconds; building the dataset from the 183 MB index took 7.8 seconds.
  • Spearman against the target, held-out rows: gradient boosting 0.964, MLP 0.959, random forest 0.957, ridge 0.906, a line through the physical span alone 0.794, GA’s score 0.596, the rule checklist 0.449, the mean 0. Pairwise accuracy across chords: 0.921 against GA’s 0.715.
  • The boosted trees put 0.723 of their gain on diagonalMm, the widest pair of fretted notes measured across the strings as well as along the neck — not GA’s along-the-neck span, which holds 0.028. GA’s barre flag holds 0.011; the model rebuilds barres from maxAtMinFret, which counts strings at the lowest fret without asking whether they are adjacent, exactly the test GA’s flag gets wrong.
  • Sizes: 949 KB for 300 boosted trees, 11.9 MB for 60 deep forest trees, 15.4 KB for the MLP, 1.24 KB for ridge.
  • Four predictions wrong (H2, H6, H11, H12), two half right (H9, H10). Lesson
  • A bug worth remembering, found by a failing split: the first group hash was plain FNV-1a, whose high bits barely move between chord0 and chord39. A split reads exactly those high bits, so all forty chords landed in the training set and the test set came out empty. A murmur3 finalizer fixed it, and a test now checks the three sides get 60/20/20 of three thousand near-identical keys.

2026-09-17 — P5: hypotheses, then a bangle

Section titled “2026-09-17 — P5: hypotheses, then a bangle”
  • Twelve predictions in results/hypotheses.md, committed in 8a2ed53 before any mesh existed. Four came out wrong: H2, H6, H11’s repair prediction and H7’s premise.
  • The pitch classes are parsed out of GA at a826864, from CanonicalChordPatternCatalog.cs and Scale.cs, and cross-checked against the catalog prototype 2 already ported. 58 chord patterns, 19 scales, SHA-256 of both blobs recorded.
  • 31 solids built, unioned with manifold3d and measured in 5.95 s: all watertight, one body, Euler characteristic 0, manifold3d NoError. A 65 mm bangle with cone beads is 5.2473 cm3 and 6.507 g of PLA, its bore 64.9913 mm, its thinnest wall 1.9954 mm, and no face of it below the 45-degree support threshold.
  • PrusaSlicer 2.9.6 was downloaded and run in console mode: 6.42 g and 35 min 40 s for a triad, 6.47 g and 39 min 56 s for a seven-note scale, 6.15 g and 26 min 43 s for the bare band. Lesson

2026-09-17 — P5: three things that were not true

Section titled “2026-09-17 — P5: three things that were not true”
  • “Watertight” is about edges. H2 said the fourteen parts written into one file without a boolean would fail the test. They pass it, and manifold3d accepts them too — fourteen closed solids in one bounding box. What they cost is the volume (2.6 % high), the surface area (10.8 % high) and the wall measurement, which reads 1.40 mm because the ribs’ buried faces are still real surfaces.
  • The slicer does not care either. PrusaSlicer sliced the un-unioned mesh to 2,154.16 mm of filament against the union’s 2,154.05, the same 6.42 g, and reported no repair. The case for doing booleans properly has to be made on other grounds than the slicer.
  • The keel’s chamfer was 36 degrees, not the 45 the hypotheses claimed. min_slope_deg printed 36.027 and nothing else would have caught it. Making the ramp a real 45 degrees removed the overhang and left the mark 0.4 mm proud at 1 mm above the bed, half an ordinary rib. Deleting the ramp fixed both: a vertical feature on a vertical wall, printed axis up, overhangs nothing.
  • The bead from prototype 2 eats the bracelet. A sphere of radius 3.0 mm centred on the outer surface of a 2.0 mm wall reaches 1.0 mm into the bore: the inner diameter drops from 64.99 to 63.03 mm, and 0.35 % of the surface needs support, with a worst slope of 4.87 degrees. H6 had predicted 3 % to 8 %, an order of magnitude out.

2026-09-17 — P5: GA’s server disagrees with GA’s source

Section titled “2026-09-17 — P5: GA’s server disagrees with GA’s source”
  • Asked through GA’s MCP tools, ga_chord_to_set('Cdim7') answers C, E flat, F sharp, B flat — Forte 4-27, a half-diminished seventh — while CanonicalChordPatternCatalog.cs has diminished-7 as 0 3 6 9, Forte 4-28. ga_parse_chord('Cdim7') returns quality diminished with component ext:7, so the seventh is added as a minor seventh.
  • ga_chord_to_set('Cm7b5') answers C, E flat, G, B flat, a plain minor seventh: the b5 alteration is dropped. GA’s file has half-diminished-7 as 0 3 6 10.
  • get_scale_notes for C major and A minor, and ga_chord_to_set('Cmaj7'), agree with the files. The exchange is recorded in results/ga-check.json. Two of the thirteen bracelets would have been wrong if they had been built from the running parser instead of the pinned files. For the author to decide whether this becomes an issue on GuitarAlchemist/ga.

2026-09-24 — Upstream fixes checked again

Section titled “2026-09-24 — Upstream fixes checked again”
  • The two ga_chord_to_set rows of P5 are fixed upstream by #681, merged 2026-09-23: the F# closure behind the tool now applies the alterations and implies the seventh. Re-run on GA 27a1257 by music-theory-ga’s recheck/probe.cs, which calls the tool method directly: Cdim7 → {C, Eb, F#, A}, 0 3 6 9, 4-28; Cm7b5 → {C, Eb, F#, Bb}, 4-27, “Half Diminished Seventh”. GA’s server and GA’s source now agree on both chords.
  • A blank line before those two rows ended the QA table early, so they rendered as plain text; removed.
  • The three rows about FretDiagram and OptickIndexReader were fixed upstream by #696, merged 2026-09-23, each commit with its test: FretDiagram.test.tsx for the string order, fretWindow.test.ts for the dropped dots, CompactVectorNormTests.cs for the vector length.
  • #730, merged 2026-09-24, makes the export reproducible: the parallel generator now emits its windows in fret order, so the export’s tie-break always keeps the same voicing. The two exports compared here, from one build, would no longer differ; the course’s own index, exported from 66bdd049, is unchanged.
  • A finding outside the lab’s plan, while reviewing a fix for the P7 rows: DetectCagedShape read the strings backwards. A probe over GA’s own 667,125 generated voicings found 92 voicings tagged E-Shape, none of them one, and no A or C shape at all, because the check returned as soon as string 6 was muted. #735, merged 2026-09-25, reads the strings in the order the voicing stores them; its test looks the shapes up among the generator’s voicings and fails on 6 of its 7 cases with the old code. The shape feeds the embedding’s shape tag and the CAGED filter, so the course’s index, exported from 66bdd049, still carries the old tags.
  • The explorer on macOS Safari and Firefox, with and without WebGPU: tested only in Chromium on Windows.
  • Audio on mobile browsers, which may block the AudioContext until a tap.
  • P7’s target: the 32 pairwise judgements were made by this model from written rules, not by a guitarist, and the hand model’s constants — comfortable spans, barre and mute penalties — are chosen, not measured on a player. A guitarist ranking a few hundred pairs would replace both.
  • P7’s 34.2 % of unfingerable voicings is an upper bound: the search has no thumb over the top, no string damped by the fretting hand, and no note held by two fingers.
  • P5 has never been printed. Whether 65 mm fits a wrist, whether a 1.2 mm rib can be told from a 2.5 mm keel by touch, whether a 45-degree cone underside droops on a real machine, and whether a 2.0 mm PLA band survives being pushed over a hand are all open.
  • P5’s eight degenerate, zero-area triangles left by manifold3d in the cone unions: PrusaSlicer says nothing about them, and no other slicer has been tried.