Expand description
φ_struct: render-free descriptors of the term itself.
These cost nothing (no compile, no render), which is what makes the screening cascade work: a struct-only surrogate prunes candidates before the expensive render path. They also capture taste axes audio features can’t fully separate (“likes supersaws”, “likes deep modulated chains”).
§φ carries families, not one column per module
StructFeatures keeps a raw counter per kind — the Styles tab and the
auto-namer both want “two filters”, not “two subtractive stages” — but
StructFeatures::NAMES and StructFeatures::to_vec collapse the
forty-one module kinds into fourteen family counts plus seven term-level
numbers — five about modulation and the amp envelope, two about how the
term is arranged. Two reasons, and the second is the load-bearing one:
- Nothing meaningful distinguishes them.
n_fold,n_distortionandn_bitcrushall answer “how much nonlinear colour”;n_chorus,n_phaser,n_flanger,n_tremoloandn_vibratoall answer “how much periodic movement”. A user who likes drive does not first decide which drive. - Per-kind columns arrive as near-indicator variables. The prior draws bitcrush at 2.5%, ring mod at 2% and granular at 1.5%, so those columns are zero in ~19 of every 20 pool members. A coefficient fitted on a column that is almost always zero is estimated from a handful of rows, and the Styles tab renders it beside coefficients fitted on hundreds. Sixteen sparse columns also cost sixteen dimensions of posterior variance for the cold start to pay down before the model says anything at all. Five of wave 2A’s six newcomers would have arrived under 3% prevalence, and all five of wave 2B’s do. Wave 2C is the extreme case: measured over 1200 draws, each of its four CV processors appears in under 4% of patches and each of its six combiners in under 1% — a column that is zero in 99 rows of every 100, which is not a coefficient, it is a rounding error with a name in the Styles tab.
§What is deliberately not in φ
size, depth and n_mix. Every audio node increments
exactly one raw counter, so size ≡ Σ n_* — exactly, for every tree.
Including it makes the design matrix rank-deficient: the Gaussian prior
keeps the posterior proper, but there is an unidentified ridge along which
the MH chain random-walks forever. That wrecks mixing, splits each
coefficient arbitrarily between size and the counts (so the per-feature
weights shown in the Styles tab mean nothing individually), and poisons the
taste→grammar proposal tilt, which reads exactly those coefficients.
size − depth would be no better: it is still an exact linear combination
of coordinates already present. The field is kept for display and naming;
it just never reaches the model.
Dropping size alone was not enough, which a VIF sweep over 300 prior
draws caught (cargo run -p auracle-features --example pipeline_stats --release -- 300). A tree is a forest of source leaves joined by
productions that each take some number of audio children, so the leaf count
exceeds the total branch count by exactly one. Wave 2B is where that stops
being a two-term statement — the compressor, ducker, gate and vocoder each
take two audio subterms, exactly as mix and ring mod do — so the identity
generalizes to:
n_vco + n_supersaw + n_noise + n_wavetable + n_pluck + n_formant
+ n_silence
− n_mix − n_ringmod − n_comp − n_duck − n_gate − n_vocoder = 1
(exactly, for every tree)n_silence joins that sum as a source leaf, which is what it is: it has no
children, so it ends a branch exactly as a Vco does. Joining keeps this
one equation with one dropped column, and n_mix stays the column
dropped. Leaving it out instead would make the identity exact for a tree
with no holes and slack for one with them — near-exact almost always, which
is a worse thing to hold than an exact dependency, because an exact one is
visible in a VIF sweep and a near-exact one is a large number that looks
like a judgment call.
(Wave 2A added one source and five unary operators, so it only moved a
source term. Wave 2B’s Shift is unary too and does not appear here.)
— a second exact dependency, reported as VIF ≈ 10⁹ on every column in it.
That is one equation, so exactly one column has to go, and dropping
more would remove real dimensions rather than redundant ones: with both
binary counts gone, φ could not tell a crossfade from a ring modulator at
all, which are about as different as two nodes in this grammar get. n_mix
leaves (it is the one determined by the others, and its proposal tilt is
recovered from the source coefficients in auracle_session’s
biased_prior); the other five stay, but never as columns of their own:
ring mod lives inside n_drive, the vocoder inside n_filter, and the
compressor, ducker and gate inside n_dynamics.
That last one is the case worth checking rather than assuming, because
n_dynamics is exactly n_comp + n_duck + n_gate and it is a retained
column — the only family in φ whose members are all on the wrong side of the
identity. It is still safe, and the reason is that the identity needs each
binary count separately: n_ringmod is only ever visible summed with
folds, distortions and bitcrushers, and n_vocoder only summed with filters
and eqs, so no linear combination of the retained columns isolates either
and the equation cannot be reconstructed. n_dynamics on its own supplies
three of the six binary terms and nothing supplies the other three.
Confirmed empirically, not just argued: on a 1200-draw sweep every
structural coordinate came back well under 10, with n_dynamics at 1.9.
The prevalence argument points the same way independently: the prior draws ring mod into ~3.5% of patches and each of the four 2B binaries into fewer, so any of them as a standalone column would be the near-indicator variable this whole section exists to avoid.
depth goes too, on a weaker but real argument: VIF ≈ 21.7. Not exact —
the posterior stays proper — but a coefficient that unstable is not
individually meaningful, and the Styles tab renders these per-feature
weights as if they were.
Still standing, and deliberately: rolloff_mean ≈ 18.4, zcr_mean ≈ 10.4
against centroid_mean ≈ 5.9 (re-measured over 1200 draws of the 2C
prior; they were 19.4 / 11.4 / 5.9 under 2B, 19.8 / 12.0 / 8.2 under 2A
and 24.7 / 16.6 / 6.6 under v1). That is the brightness cluster — three
genuine measurements of one perceptual thing. Dropping any of them
discards real signal rather than redundancy, so the right fix is a
shared/fused prior over the cluster, which is a modelling change rather
than a feature change and is not in this pass.
Every family coordinate came back under 4 on that sweep, the highest being
mod_depth_mean at 3.8 — which is the whole reason the families exist.
Forty separate module columns is the design that would not have. The three
wave-2C additions specifically: n_mod_shape 1.6, n_mod_logic 1.3,
mod_depth_mean 3.8, with mod_density rising from 2.7 to 4.1 as the one
visible cost of adding a second modulation-shape coordinate beside it.
§The families, and why each one is one column
n_filter= filter + eq + vocoder. Spectral tilt: a resonant filter and a tone control are the same question at different sharpnesses, and a vocoder is that question with the curve drawn by a signal.n_drive= fold + distortion + bitcrush + ring mod. Nonlinear colour.n_time= delay + granular + pitch shift. Smearing a signal in time. Renamed fromn_delayin wave 2A, because the column now counts three ways of doing it and a name that says “delay” while counting granulators is a lie the Styles tab would render as if it meant something.n_mod_fx= chorus + phaser + flanger + tremolo + vibrato. Periodic movement — an LFO on a short delay, an allpass chain or a gain.n_dynamics= compressor + ducker + gate. Level shaped by a second signal. The one family whose members all sit inside the binary-node identity above, which is why that paragraph checks it rather than assuming it.n_mod_shape= quantizer + slew + rectifier + clocked hold. CV that has been worked on before it lands.n_mod_logic= euclid + min + max + and + or + xor + switch. Gate and decision CV. See below for why the euclid is counted here rather than with the other modulation leaves.
§Wave 2C: the modulation sort has its own identity, and it is not exact
Modulation became a recursive sort, so a patch now carries a forest of
modulation terms as well as one audio tree — and forests have the same
kind of leaf-versus-branch identity the audio tree does. With f filled
slots, p binary combiners and u unary processors, the forest has
exactly f + p leaves:
n_lfo + n_env + n_rand + n_follow + n_euclid
= filled_slots + (n_min + n_max + n_and + n_or + n_xor + n_switch)Two things stop that reaching φ as a dependency. filled_slots is not a
coordinate — mod_density is filled/slots, a ratio, and no linear
combination recovers the numerator without the denominator. And the euclid
is summed into n_mod_logic with the combiners rather than sitting
with the other leaves, so the two sides of the equation are not separately
visible: what φ carries is n_euclid + Σcombiners, and the identity needs
them with opposite signs. Grouping it there is also the honest reading —
what a euclidean generator emits is a gate, which is what the logic ops
consume and produce — but the identity is the reason it is not a
judgement call.
mod_depth_mean is the new term-level number, and it is the coordinate
that actually says “this person likes modulation that has been shaped”:
the counts say how many processors are in the patch, the mean depth says
how deep the chains they sit in are, and a patch with four one-deep
modulators is a different animal from one with a single three-deep chain.
It is averaged over the filled slots only, so it is not a second reading
of mod_density — measured, the two come back at 3.8 and 4.1 rather than
at the double figures a restatement would give.
§Wave 3: two coordinates that carry arrangement
Everything above counts what is in the patch. Nothing above says
how it is put together. filter(mix(vco, vco)) and
mix(filter(vco), vco) have byte-identical φ under the twenty-three
columns that shipped before this wave — one filter, two VCOs, one mixer
that is not a column anyway — and they are two different instruments: one
filters the sum, the other filters a layer and leaves the other dry. Two
coordinates fix that. Four were written. The two that were cut, and
why, are the more useful half of this section, because both were cut by
measurement rather than by argument and the arguments for them had looked
fine.
§branch_width_max: the identity, walking back in
WS-8 §4 asked for a parallelism coordinate on the reading that serial
vco→filter→drive and parallel mix(vco→filter, vco→drive) “differ only
in n_mix, which is deliberately not a φ column”. That reading is wrong,
and the identity two sections up is the reason: the leaf count is
1 + Σ binaries exactly, so a patch cannot gain a mixer without
gaining a source, and the six source counts have been in φ since v1. The
serial patch has one VCO and the parallel one has two. φ could always see
that.
branch_width_max — the largest number of audio nodes sharing one level of
the tree — was written, put through the VIF sweep, and came back at
10.4, taking n_vco from 3.1 to 9.1 and every other source count with
it. On trees this size (mean 3.1 nodes, mean depth 2.6) the widest level
is the leaf set, so the column was very nearly Σ sources, which is very
nearly 1 + Σ binaries: the same rank deficiency size and n_mix were
removed for, under a new name.
The evolution measurement said the same thing independently. A synthetic listener whose taste was “likes wide patches” was learned to Spearman 0.709 by the old feature set, with no arrangement coordinate at all — because wanting a wider tree is wanting more sources, and it was reading the source counts. So the honest statement of what was missing is not parallelism but arrangement: given the same modules, how are they wired.
§mod_at_source: a tie the measurement could not break
The modulation half of φ carries density and chain length and no
destination, so “an LFO on the filter” and “an LFO on the oscillator” are
the same patch to the model. mod_at_source — mean over the filled slots
of the owning module’s distance from the root, over the tree’s depth — is
the obvious fix, and it measured well: VIF 3.0, spread across the whole
[0, 1] range, no exact dependency anywhere near it.
It is not in φ, and the honest reason is not the one the first run
suggested. Over 8 seeds, three new columns took search_health’s pool
climb from +1.714 to +1.320, the best patch found from 8.154 to
6.503, and the seeds that climbed from 7 of 8 to 5 of 8 — which reads as
an unambiguous regression and was very nearly acted on as one. Re-run at
16 seeds with the per-seed numbers printed (--climb, added for
exactly this), the harness turned out to have a standard error of ±0.64
on that quantity, and the paired differences against the same seed list
are:
two columns: gain +0.35 ± 0.73 best patch −1.00 ± 0.63
three columns: gain −0.33 ± 0.74 best patch −0.93 ± 0.76Neither is a regression this instrument can see, and neither is an improvement. So the tie is broken on cost rather than on evidence, and the cost is real even when it is not measurable: every column is a dimension of posterior variance the cold start has to pay down, which is the general form of the near-indicator argument several sections up. Two columns answer the question this wave was asked — given the same modules, how are they wired — and the third answers a different one. It stays as a display field and is the obvious candidate for the next wave with evidence to spend, measured on its own so the attribution survives.
The first reading is written down rather than quietly deleted because the failure it represents is the expensive one: an eight-seed run of a noisy harness looks exactly like a finding.
§The two that stayed
Both are ratios of shape sums, and that is the design constraint, not a
stylistic one. Every identity in the sections above is a linear equation
over counts; a ratio of two path sums is not linear in any count, so
neither can supply a term an identity needs, and neither re-admits size,
depth or n_mix through a side door. Being ratios also keeps them
scale-free, which is why they do not simply re-measure depth (VIF 21.7,
dropped). Over 300 draws: chain_balance 2.7, frac_sidechained
2.4. The cost, stated rather than buried: the source counts rise a
little (n_vco 3.1 → 4.3, n_supersaw 2.7 → 3.5) because how a patch is
arranged is correlated with how many sources it has even though it is not
determined by it, and mod_density and mod_depth_mean move by a tenth.
Compare branch_width_max’s 10.4 and 3.1 → 9.1, which is the difference
between a correlated column and a redundant one.
chain_balance— mean source-to-root path length over the longest one. 1.0 when every source is the same distance from the amp (a serial chain, or a symmetric mix); below 1.0 when one branch is a long chain and the other is a bare oscillator. This is the coordinate that separates the two patches at the top: 1.000 forfilter(mix(vco, vco)), 0.833 formix(filter(vco), vco). It is the load-bearing one — it catches an asymmetric branch whichever side the chain is on.frac_sidechained— of the binary nodes, the fraction whose/1child is not a bare source. That is the halfchain_balancecannot say: which side./1is the ducker’s key, the compressor’s sidechain and the vocoder’s modulator, so the difference between a key that is a raw pulse and one that is a whole drum bus lands here and nowhere else. 0 when there are no binaries, on the same rule asmod_density.
§What the evolution loop said
The standing rule is that a green make check says nothing about search
health, so both columns went through search_health and
closed_loop_sweep on both sides of the change. The table lives on
auracle-session’s search_health module doc, next to the harness that
produced it; the short version is that nothing regressed, the MH acceptance
rate and the locked-refine hit rate went up slightly, and the calibration
r was flat at 0.693 → 0.688 (se ±0.018).
Two readings are worth repeating here because they are about these columns rather than about the harness:
- The model’s ranking got better, for a listener who does not care about routing. Spearman between the fitted utility and the truth went 0.318 → 0.389 and the true best patch survived its generation 48 times out of 48 rather than 47. Two more coordinates to be wrong about did not make the ranking noisier; on this evidence they made the pool easier to tell apart.
- For a listener who does care, all four numbers move together. A synthetic user who wants an asymmetric, sidechained routing is fitted at 0.705 rather than 0.662, and the search leaves them a pool that is 82% sidechained rather than 71.6%. The before arm is not blind — an asymmetric routing also sounds different, and the audio half of φ hears that — so what the columns buy is the model knowing why.
What they deliberately do not carry: node order along a serial chain.
drive(filter(vco)) and filter(drive(vco)) remain indistinguishable, and
fixing that needs a coordinate per ordered pair of families — 91 of them —
which is the near-indicator disaster the family scheme exists to avoid.
The honest reading is that φ now sees the graph, not the sequence.
Structs§
- Struct
Features - Named structural descriptors.
to_vecorder matchesStructFeatures::NAMES.
Functions§
- struct_
features - Extract
StructFeaturesfrom a term (no compile, no render).