Skip to main content

forge_engine/lab/
emitters.rs

1use rand::Rng;
2use serde::{Deserialize, Serialize};
3
4/// AlgebraSpec — a candidate's parameter specification for MindState compilation.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct AlgebraSpec {
7    /// The primary evidence retrieval parameter.
8    pub k: f64,
9    /// Operator weights for combining signals.
10    pub operator_weights: OperatorWeights,
11    /// Delta amplitudes for stabilization phases.
12    pub delta_amp_default: f64,
13    pub delta_amp_stabilize1: f64,
14    pub delta_amp_stabilize2: f64,
15    pub delta_amp_clamp: f64,
16    /// Evidence budget.
17    pub evidence_budget: usize,
18    /// Orthogonality target for novelty.
19    pub orthogonality_target: f64,
20    /// Token budget for MindState.
21    pub token_budget: usize,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct OperatorWeights {
26    pub correctness: f64,
27    pub novelty: f64,
28    pub stability: f64,
29}
30
31impl Default for AlgebraSpec {
32    fn default() -> Self {
33        Self {
34            k: 5.0,
35            operator_weights: OperatorWeights {
36                correctness: 0.7,
37                novelty: 0.2,
38                stability: 0.1,
39            },
40            delta_amp_default: 0.7,
41            delta_amp_stabilize1: 0.2,
42            delta_amp_stabilize2: 0.1,
43            delta_amp_clamp: 0.0,
44            evidence_budget: 8,
45            orthogonality_target: 0.10,
46            token_budget: 1800,
47        }
48    }
49}
50
51/// E1: Param mutation — perturb numeric parameters.
52/// `perturbation_scale` controls magnitude; `rng` must be seeded by caller for reproducibility.
53pub fn mutate_params<R: Rng>(
54    parent: &AlgebraSpec,
55    perturbation_scale: f64,
56    rng: &mut R,
57) -> AlgebraSpec {
58    let mut child = parent.clone();
59
60    child.k = clamp_positive(child.k * (1.0 + perturbation_scale * signed(rng)));
61    child.delta_amp_default =
62        (child.delta_amp_default + perturbation_scale * signed(rng)).clamp(0.0, 1.0);
63    child.delta_amp_stabilize1 =
64        (child.delta_amp_stabilize1 + perturbation_scale * signed(rng)).clamp(0.0, 1.0);
65    child.delta_amp_stabilize2 =
66        (child.delta_amp_stabilize2 + perturbation_scale * signed(rng)).clamp(0.0, 1.0);
67    child.orthogonality_target =
68        (child.orthogonality_target + perturbation_scale * 0.1 * signed(rng)).clamp(0.0, 1.0);
69    child.evidence_budget = ((child.evidence_budget as f64
70        + perturbation_scale * 2.0 * signed(rng))
71    .round()
72    .max(1.0)) as usize;
73
74    // Perturb operator weights then normalize to sum to 1.0
75    child.operator_weights.correctness =
76        (child.operator_weights.correctness + perturbation_scale * 0.1 * signed(rng)).max(0.01);
77    child.operator_weights.novelty =
78        (child.operator_weights.novelty + perturbation_scale * 0.1 * signed(rng)).max(0.01);
79    child.operator_weights.stability =
80        (child.operator_weights.stability + perturbation_scale * 0.1 * signed(rng)).max(0.01);
81    normalize_weights(&mut child.operator_weights);
82
83    child
84}
85
86/// E2: Crossover — merge two parent specs.
87pub fn crossover(parent_a: &AlgebraSpec, parent_b: &AlgebraSpec) -> AlgebraSpec {
88    let mut child = AlgebraSpec {
89        // From parent A
90        k: parent_a.k,
91        token_budget: parent_a.token_budget,
92        evidence_budget: parent_a.evidence_budget,
93
94        // From parent B (delta policy)
95        delta_amp_default: parent_b.delta_amp_default,
96        delta_amp_stabilize1: parent_b.delta_amp_stabilize1,
97        delta_amp_stabilize2: parent_b.delta_amp_stabilize2,
98        delta_amp_clamp: parent_b.delta_amp_clamp,
99
100        // Average other numeric parameters
101        orthogonality_target: (parent_a.orthogonality_target + parent_b.orthogonality_target) / 2.0,
102        operator_weights: OperatorWeights {
103            correctness: (parent_a.operator_weights.correctness
104                + parent_b.operator_weights.correctness)
105                / 2.0,
106            novelty: (parent_a.operator_weights.novelty + parent_b.operator_weights.novelty) / 2.0,
107            stability: (parent_a.operator_weights.stability + parent_b.operator_weights.stability)
108                / 2.0,
109        },
110    };
111    normalize_weights(&mut child.operator_weights);
112    child
113}
114
115/// Normalize operator weights so they sum to 1.0.
116fn normalize_weights(w: &mut OperatorWeights) {
117    let sum = w.correctness + w.novelty + w.stability;
118    if sum > 0.0 {
119        w.correctness /= sum;
120        w.novelty /= sum;
121        w.stability /= sum;
122    }
123}
124
125fn clamp_positive(v: f64) -> f64 {
126    if v < 0.1 {
127        0.1
128    } else {
129        v
130    }
131}
132
133/// Signed random direction: -1.0 or +1.0.
134fn signed<R: Rng>(rng: &mut R) -> f64 {
135    if rng.random::<bool>() {
136        1.0
137    } else {
138        -1.0
139    }
140}