auracle_session/surrogate.rs
1//! The learned taste as a fugue-evo fitness: the surrogate that lets the
2//! machine evolve thousands of candidates silently (the reference: *The two
3//! loops*).
4//!
5//! `SurrogateFitness` plugs the taste posterior's expected utility into
6//! fugue-evo's `Fitness`, so the Boltzmann target
7//! `π_β(x) ∝ p_grammar(x) · exp(β · E[u_θ(φ(x))])` becomes an ordinary
8//! `EvolutionModel` and typed-MH / SMC drivers apply unchanged. Quarantined
9//! candidates score a large negative fitness — safety layer 2: evolution
10//! learns to avoid the pathological region.
11
12use std::sync::Arc;
13
14use auracle_features::{featurize_memo, PhraseSpec, RenderMemo};
15use auracle_grammar::PatchTree;
16use auracle_taste::{Standardizer, TastePosterior};
17use fugue_evo::fitness::traits::Fitness;
18
19/// Fitness a quarantined (unrenderable/unlistenable) candidate receives.
20pub const QUARANTINE_FITNESS: f64 = -50.0;
21
22/// Expected posterior utility as a scalar fitness over patch terms.
23#[derive(Clone, Debug)]
24pub struct SurrogateFitness {
25 /// The fitted taste posterior.
26 pub posterior: Arc<TastePosterior>,
27 /// The standardizer the posterior's observations were made under.
28 pub standardizer: Arc<Standardizer>,
29 /// The audition stimulus (must match the one used for observations).
30 pub phrase: PhraseSpec,
31 /// The engine's featurization memo.
32 ///
33 /// Not an optimization detail — it is what makes the MH walk affordable.
34 /// `adaptive_single_site_mh` executes the model **twice per step**: once
35 /// to re-score the current trace, which is bit-identically the tree the
36 /// previous step accepted and therefore already featurized, and once for
37 /// the proposal. Without a memo, one render in two is a recomputation of a
38 /// number the walk already has.
39 pub memo: RenderMemo,
40}
41
42impl Fitness for SurrogateFitness {
43 type Genome = PatchTree;
44 type Value = f64;
45
46 fn evaluate(&self, genome: &PatchTree) -> f64 {
47 // `want_audio: false` — the surrogate only ever wants φ, and nothing
48 // in a refinement generation is ever played. Asking for samples here
49 // would undo the memo: a miss would convert 141 k f64s it then drops,
50 // and a hit would copy a ~565 KB buffer out of the audio tier. Twice
51 // per MH step, ~96 times per seed, that is tens of megabytes of churn
52 // for a value discarded on the next line.
53 match featurize_memo(genome, &self.phrase, &self.memo, false) {
54 Ok((cf, _)) => {
55 let phi = self.standardizer.transform(&cf.features.phi());
56 self.posterior.utility_mix(&phi).0
57 }
58 Err(_) => QUARANTINE_FITNESS,
59 }
60 }
61}