Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2343edc6b | ||
|
|
eeb5dd4141 | ||
|
|
64a81e4110 |
@@ -7,6 +7,18 @@ edition = "2021"
|
||||
name = "gp_tester"
|
||||
path = "src/bin/gp_tester.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "gp_spec_tester"
|
||||
path = "src/bin/gp_spec_tester.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "symreg_experiment"
|
||||
path = "src/bin/symreg_experiment.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "symreg_experiment_local"
|
||||
path = "src/bin/symreg_experiment_local.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::Result;
|
||||
use rand::thread_rng;
|
||||
use rand::Rng;
|
||||
|
||||
// Suppose you have these in your library:
|
||||
use offchain::gp::generate_spec::ranmdom_code_fixed;
|
||||
use offchain::gp::mutation::mutate_by_index;
|
||||
use offchain::compiler::ast::UntypedAst;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// We'll produce, say, 3 random ASTs ("subjects").
|
||||
let num_subjects = 3;
|
||||
let max_points = 6;
|
||||
|
||||
for i in 0..num_subjects {
|
||||
// Generate a random AST
|
||||
let original = ranmdom_code_fixed(&mut rng, max_points);
|
||||
println!("=== Subject #{} ===", i);
|
||||
println!("Original AST:\n{:#?}", original);
|
||||
|
||||
// Let's do 2 successive mutations on it
|
||||
let mut prev = original.clone();
|
||||
for mut_idx in 0..2 {
|
||||
let mutated = mutate_by_index(&prev, &mut rng, max_points);
|
||||
println!("Mutation #{} result:\n{:#?}", mut_idx, mutated);
|
||||
prev = mutated; // so the next iteration mutates from the last result
|
||||
}
|
||||
|
||||
println!("=== End of Subject #{} ===\n", i);
|
||||
}
|
||||
|
||||
println!("Done generating & mutating random ASTs!");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// src/bin/symreg_experiment.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
// Our GP + compiler modules (adjust paths as needed)
|
||||
use offchain::compiler::ast::{UntypedAst, Push3Ast};
|
||||
use offchain::helpers::artifact::get_creation_code;
|
||||
use offchain::runner::revm_runner::{
|
||||
EvmRunner,
|
||||
Push3InterpreterInputs,
|
||||
};
|
||||
use offchain::compiler::push3_describtor::make_sublist_descriptor;
|
||||
|
||||
// Our GP operators
|
||||
use offchain::gp::generate_spec::ranmdom_code_fixed;
|
||||
use offchain::gp::mutation::{mutate_by_index, crossover_by_index};
|
||||
|
||||
// Ethers + ABI
|
||||
use ethers::types::U256;
|
||||
use ethers::abi::{encode, Token};
|
||||
|
||||
/// 1) Generate (x, y) samples for f(x)=3x^2 + x + 3
|
||||
fn generate_samples() -> Vec<(i32, i32)> {
|
||||
let mut samples = Vec::new();
|
||||
for x in -5..=5 {
|
||||
let y = 3 * x * x + x + 3;
|
||||
samples.push((x, y));
|
||||
}
|
||||
samples
|
||||
}
|
||||
|
||||
/// 2) Evaluate a single AST on a single input `x` by
|
||||
/// pushing `x` onto the int stack, running in EVM,
|
||||
/// and reading the final top of the int stack.
|
||||
fn evaluate_ast_on_x(
|
||||
runner: &mut EvmRunner,
|
||||
ast: &UntypedAst,
|
||||
x: i32,
|
||||
) -> i32 {
|
||||
// a) Convert AST => push3 bytecode
|
||||
let code_bytes = ast.to_bytecode();
|
||||
let code_len = code_bytes.len() as u32;
|
||||
|
||||
// b) Build a sublist descriptor
|
||||
let descriptor = make_sublist_descriptor(0, code_len);
|
||||
|
||||
// c) Provide x in `init_int_stack`
|
||||
let inputs = Push3InterpreterInputs {
|
||||
code: code_bytes,
|
||||
init_code_stack: Vec::new(),
|
||||
init_exec_stack: vec![descriptor],
|
||||
init_int_stack: vec![x as i128],
|
||||
};
|
||||
|
||||
// d) Actually run the interpreter
|
||||
let outputs = match runner.run_interpreter(&inputs) {
|
||||
Ok(o) => o,
|
||||
Err(_) => {
|
||||
// If the call fails or reverts, return 0
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// e) If final_int_stack is empty => 0, else top item
|
||||
if outputs.final_int_stack.is_empty() {
|
||||
i32::MAX
|
||||
} else {
|
||||
*outputs.final_int_stack.last().unwrap() as i32
|
||||
}
|
||||
}
|
||||
|
||||
/// 3) Evaluate an AST on all (x, y) samples => compute MSE
|
||||
fn evaluate_fitness(
|
||||
runner: &mut EvmRunner,
|
||||
ast: &UntypedAst,
|
||||
samples: &[(i32, i32)]
|
||||
) -> f64 {
|
||||
let mut error_sum = 0.0;
|
||||
for &(x, target_y32) in samples {
|
||||
// cast to i64
|
||||
let target_y = target_y32 as i64;
|
||||
|
||||
// evaluate in i64 as well
|
||||
let predicted_i64 = evaluate_ast_on_x(runner, ast, x) as i64;
|
||||
|
||||
let diff = predicted_i64.wrapping_sub(target_y);
|
||||
// or you could saturating_sub if you prefer
|
||||
|
||||
error_sum += (diff as f64) * (diff as f64);
|
||||
}
|
||||
error_sum / samples.len() as f64
|
||||
}
|
||||
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// 1) Generate samples for f(x)=3x^2 + x + 3
|
||||
let samples = generate_samples();
|
||||
|
||||
// 2) Create ephemeral EVM runner for the interpreter
|
||||
let creation_hex_filename = "../onchain/out/Push3Interpreter.sol/Push3Interpreter.json";
|
||||
let creation_bytes = get_creation_code(creation_hex_filename)?;
|
||||
let mut runner = EvmRunner::new(creation_bytes)?;
|
||||
|
||||
// 3) GP parameters
|
||||
let pop_size = 4000; // must be divisible by 4 for our scheme
|
||||
let generations = 100;
|
||||
let early_stop_threshold = 1.0;
|
||||
let max_points = 20;
|
||||
|
||||
assert_eq!(pop_size % 4, 0, "pop_size must be multiple of 4 for our scheme");
|
||||
let quarter = pop_size / 4;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// 4) Initialize population (random)
|
||||
let mut population: Vec<UntypedAst> = (0..pop_size)
|
||||
.map(|_| ranmdom_code_fixed(&mut rng, max_points))
|
||||
.collect();
|
||||
|
||||
// 5) Main GP loop
|
||||
for gen in 0..generations {
|
||||
// (a) Evaluate
|
||||
let mut scored: Vec<(UntypedAst, f64)> = population.into_iter()
|
||||
.map(|ast| {
|
||||
let err = evaluate_fitness(&mut runner, &ast, &samples);
|
||||
(ast, err)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// (b) Sort ascending by error
|
||||
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
|
||||
// (c) Print generation summary
|
||||
let best_err = scored[0].1;
|
||||
println!("\n=== Generation {gen} ===");
|
||||
println!("Best error = {best_err}");
|
||||
|
||||
// Print top 5 subjects in pretty format
|
||||
// let top_n = 5.min(scored.len());
|
||||
// println!("Top {top_n} subjects:");
|
||||
// for i in 0..top_n {
|
||||
// println!(
|
||||
// " #{i}, err={}, AST:\n{:#?}",
|
||||
// scored[i].1,
|
||||
// scored[i].0
|
||||
// );
|
||||
// }
|
||||
|
||||
// early stop?
|
||||
if best_err < early_stop_threshold {
|
||||
println!("Best error < {early_stop_threshold}, stopping early!");
|
||||
// we can keep 'scored' for final if we want
|
||||
population = scored.into_iter().map(|(a, _)| a).collect();
|
||||
break;
|
||||
}
|
||||
|
||||
// (d) Reproduction => build new population
|
||||
let mut new_pop = Vec::with_capacity(pop_size);
|
||||
|
||||
// keep best quarter
|
||||
for i in 0..quarter {
|
||||
new_pop.push(scored[i].0.clone());
|
||||
}
|
||||
|
||||
// add random quarter
|
||||
for _ in 0..quarter {
|
||||
let fresh = ranmdom_code_fixed(&mut rng, max_points);
|
||||
new_pop.push(fresh);
|
||||
}
|
||||
|
||||
// crossover quarter
|
||||
let top_half_count = scored.len() / 2;
|
||||
let crossover_pairs = quarter / 2; // each yields 2 kids => quarter total
|
||||
for _ in 0..crossover_pairs {
|
||||
let idx1 = rng.gen_range(0..top_half_count);
|
||||
let idx2 = rng.gen_range(0..top_half_count);
|
||||
let (child1, child2) = crossover_by_index(
|
||||
&scored[idx1].0,
|
||||
&scored[idx2].0,
|
||||
&mut rng
|
||||
);
|
||||
new_pop.push(child1);
|
||||
new_pop.push(child2);
|
||||
}
|
||||
|
||||
// mutate quarter
|
||||
while new_pop.len() < pop_size {
|
||||
let idx = rng.gen_range(0..top_half_count);
|
||||
let mutated = mutate_by_index(&scored[idx].0, &mut rng, max_points);
|
||||
new_pop.push(mutated);
|
||||
}
|
||||
|
||||
population = new_pop;
|
||||
}
|
||||
|
||||
// 6) Final population => evaluate & sort, then print only top 10
|
||||
println!("\n=== Final Population (Top 10) ===");
|
||||
|
||||
// a) Evaluate each item in the final population
|
||||
let mut final_scored: Vec<(UntypedAst, f64)> = population
|
||||
.into_iter()
|
||||
.map(|ast| {
|
||||
let err = evaluate_fitness(&mut runner, &ast, &samples);
|
||||
(ast, err)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// b) Sort ascending by error
|
||||
final_scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
|
||||
// c) Print the top 10 (or fewer if the population is smaller)
|
||||
let top_n = 10.min(final_scored.len());
|
||||
for i in 0..top_n {
|
||||
let (ref ast, err) = final_scored[i];
|
||||
println!(
|
||||
"Subject #{i}, err={err}, AST:\n{:#?}",
|
||||
ast
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// src/bin/symreg_experiment_local.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
// Our GP + compiler modules (adjust paths if needed)
|
||||
use offchain::compiler::ast::{UntypedAst, Push3Ast};
|
||||
use offchain::helpers::artifact::get_creation_code;
|
||||
use offchain::runner::revm_runner::{EvmRunner, Push3InterpreterInputs};
|
||||
use offchain::compiler::push3_describtor::make_sublist_descriptor;
|
||||
|
||||
// Our random code and local mutation
|
||||
use offchain::gp::generate_spec::ranmdom_code_fixed;
|
||||
use offchain::gp::local_mutation::local_mutation_fixed;
|
||||
|
||||
// Ethers + ABI
|
||||
use ethers::types::U256;
|
||||
use ethers::abi::{encode, Token};
|
||||
|
||||
/// 1) Generate (x, y) samples for f(x) = 3x^2 + x + 3
|
||||
fn generate_samples() -> Vec<(i32, i32)> {
|
||||
let mut samples = Vec::new();
|
||||
for x in -5..=5 {
|
||||
let y = 3 * x * x + x + 3;
|
||||
samples.push((x, y));
|
||||
}
|
||||
samples
|
||||
}
|
||||
|
||||
/// 2) Evaluate a single AST on a single input `x`, returning the final top of the int stack.
|
||||
fn evaluate_ast_on_x(
|
||||
runner: &mut EvmRunner,
|
||||
ast: &UntypedAst,
|
||||
x: i32,
|
||||
) -> i32 {
|
||||
// Convert AST => push3 bytecode
|
||||
let code_bytes = ast.to_bytecode();
|
||||
let code_len = code_bytes.len() as u32;
|
||||
|
||||
// Build a sublist descriptor
|
||||
let descriptor = make_sublist_descriptor(0, code_len);
|
||||
|
||||
// Provide x in `init_int_stack`
|
||||
let inputs = Push3InterpreterInputs {
|
||||
code: code_bytes,
|
||||
init_code_stack: Vec::new(),
|
||||
init_exec_stack: vec![descriptor],
|
||||
init_int_stack: vec![x as i128],
|
||||
};
|
||||
|
||||
// Run
|
||||
let outputs = match runner.run_interpreter(&inputs) {
|
||||
Ok(o) => o,
|
||||
Err(_) => {
|
||||
// If revert => 0
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
if outputs.final_int_stack.is_empty() {
|
||||
i32::MAX // or 0, or i32::MAX to strongly penalize weird runs
|
||||
} else {
|
||||
*outputs.final_int_stack.last().unwrap() as i32
|
||||
}
|
||||
}
|
||||
|
||||
/// 3) Evaluate an AST on all samples => compute MSE
|
||||
fn evaluate_fitness(
|
||||
runner: &mut EvmRunner,
|
||||
ast: &UntypedAst,
|
||||
samples: &[(i32, i32)]
|
||||
) -> f64 {
|
||||
let mut error_sum = 0.0;
|
||||
for &(x, target_y32) in samples {
|
||||
// cast to i64
|
||||
let target_y = target_y32 as i64;
|
||||
|
||||
// evaluate in i64
|
||||
let predicted_i64 = evaluate_ast_on_x(runner, ast, x) as i64;
|
||||
|
||||
// wrapping_sub to avoid panic
|
||||
let diff = predicted_i64.wrapping_sub(target_y);
|
||||
error_sum += (diff as f64) * (diff as f64);
|
||||
}
|
||||
error_sum / samples.len() as f64
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// 1) Generate samples
|
||||
let samples = generate_samples();
|
||||
|
||||
// 2) Setup ephemeral EVM runner
|
||||
let creation_hex_filename = "../onchain/out/Push3Interpreter.sol/Push3Interpreter.json";
|
||||
let creation_bytes = get_creation_code(creation_hex_filename)?;
|
||||
let mut runner = EvmRunner::new(creation_bytes)?;
|
||||
|
||||
// 3) GP parameters
|
||||
let pop_size = 3600; // must be multiple of 3 for our scheme
|
||||
let generations = 100;
|
||||
let early_stop_threshold = 1.0;
|
||||
let max_points = 12; // random code size limit
|
||||
|
||||
// Ensure pop_size is multiple of 3
|
||||
assert_eq!(pop_size % 3, 0, "pop_size must be multiple of 3 for our scheme");
|
||||
let third = pop_size / 3;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// 4) Initialize random population
|
||||
let mut population: Vec<UntypedAst> = (0..pop_size)
|
||||
.map(|_| ranmdom_code_fixed(&mut rng, max_points))
|
||||
.collect();
|
||||
|
||||
// 5) Main loop
|
||||
for gen in 0..generations {
|
||||
// a) Evaluate
|
||||
let mut scored: Vec<(UntypedAst, f64)> = population.into_iter()
|
||||
.map(|ast| {
|
||||
let err = evaluate_fitness(&mut runner, &ast, &samples);
|
||||
(ast, err)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// b) Sort ascending by error
|
||||
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
|
||||
let best_err = scored[0].1;
|
||||
println!("\n=== Generation {gen} ===");
|
||||
println!("Best error = {best_err}");
|
||||
|
||||
// (Optionally) Print top 5 in pretty format
|
||||
let top_n = 5.min(scored.len());
|
||||
println!("Top {top_n} subjects:");
|
||||
for i in 0..top_n {
|
||||
println!(
|
||||
" #{i}, err={}, AST:\n{:#?}",
|
||||
scored[i].1,
|
||||
scored[i].0
|
||||
);
|
||||
}
|
||||
|
||||
// check early stop
|
||||
if best_err < early_stop_threshold {
|
||||
println!("Best error < {early_stop_threshold}, stopping early!");
|
||||
population = scored.into_iter().map(|(a, _)| a).collect();
|
||||
break;
|
||||
}
|
||||
|
||||
// c) Build new population:
|
||||
// - 1/3 = best survivors
|
||||
// - 1/3 = fresh random
|
||||
// - 1/3 = local mutation
|
||||
let mut new_pop = Vec::with_capacity(pop_size);
|
||||
|
||||
// keep best third
|
||||
for i in 0..third {
|
||||
new_pop.push(scored[i].0.clone());
|
||||
}
|
||||
|
||||
// random third
|
||||
for _ in 0..third {
|
||||
let fresh = ranmdom_code_fixed(&mut rng, max_points);
|
||||
new_pop.push(fresh);
|
||||
}
|
||||
|
||||
// local mutation third
|
||||
let top_half_count = scored.len() / 2;
|
||||
// we'll pick parents from top half for local mutation
|
||||
while new_pop.len() < pop_size {
|
||||
let idx = rng.gen_range(0..top_half_count);
|
||||
// we assume you have local_mutation_fixed
|
||||
// if you prefer passing a custom InstructionSet, do local_mutation(..., &instr_set)
|
||||
let mutated = offchain::gp::local_mutation::local_mutation_fixed(
|
||||
&scored[idx].0,
|
||||
&mut rng
|
||||
);
|
||||
new_pop.push(mutated);
|
||||
}
|
||||
|
||||
// store for next generation
|
||||
population = new_pop;
|
||||
}
|
||||
|
||||
// 6) Final => evaluate & sort => top 10
|
||||
println!("\n=== Final Population (Top 10) ===");
|
||||
let mut final_scored: Vec<(UntypedAst, f64)> = population
|
||||
.into_iter()
|
||||
.map(|ast| {
|
||||
let err = evaluate_fitness(&mut runner, &ast, &samples);
|
||||
(ast, err)
|
||||
})
|
||||
.collect();
|
||||
final_scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
|
||||
let top_n = 10.min(final_scored.len());
|
||||
for i in 0..top_n {
|
||||
let (ref ast, err) = final_scored[i];
|
||||
println!("Subject #{i}, err={err}, AST:\n{:#?}", ast);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::compiler::ast::{UntypedAst, OpCode};
|
||||
use rand::Rng;
|
||||
use rand::prelude::SliceRandom;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InstructionAtom {
|
||||
/// A normal opcode, e.g. `Plus`, `Minus`, `Dup`, etc.
|
||||
Opcode(OpCode),
|
||||
/// An ephemeral random constant placeholder.
|
||||
/// If chosen, we generate a random literal on the spot.
|
||||
EphemeralInt,
|
||||
// If you want ephemeral floats, booleans, etc., add more variants
|
||||
}
|
||||
|
||||
/// A small struct to hold our entire “instruction set.”
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InstructionSet {
|
||||
pub atoms: Vec<InstructionAtom>,
|
||||
}
|
||||
|
||||
impl InstructionSet {
|
||||
pub fn new_default() -> Self {
|
||||
// Example: Some opcodes + ephemeral constant
|
||||
// Tweak as you like
|
||||
use InstructionAtom::*;
|
||||
use OpCode::*;
|
||||
|
||||
Self {
|
||||
atoms: vec![
|
||||
Opcode(Noop),
|
||||
Opcode(Plus),
|
||||
Opcode(Minus),
|
||||
Opcode(Mult),
|
||||
Opcode(Dup),
|
||||
Opcode(Pop),
|
||||
EphemeralInt,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick a random atom from this set.
|
||||
/// If it's `EphemeralInt`, we produce `UntypedAst::IntLiteral(...)`.
|
||||
/// If it's `Opcode(...)`, we produce `UntypedAst::Instruction(...)`.
|
||||
pub fn random_atom_as_ast(&self, rng: &mut impl Rng) -> UntypedAst {
|
||||
let idx = rng.gen_range(0..self.atoms.len());
|
||||
match &self.atoms[idx] {
|
||||
InstructionAtom::Opcode(op) => UntypedAst::Instruction(op.clone()),
|
||||
InstructionAtom::EphemeralInt => {
|
||||
// For ephemeral int, produce a random literal in some range
|
||||
let val = rng.gen_range(-30..30);
|
||||
UntypedAst::IntLiteral(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ranmdom_code_fixed(rng: &mut impl Rng, max_points: usize) -> UntypedAst {
|
||||
let instr_set = InstructionSet::new_default();
|
||||
return random_code(rng, &instr_set, max_points);
|
||||
}
|
||||
|
||||
/// The "entry point": random_code(max_points).
|
||||
/// 1) Choose actual_points in [1..=max_points].
|
||||
/// 2) Return the result of random_code_with_size.
|
||||
pub fn random_code(rng: &mut impl Rng, instr_set: &InstructionSet, max_points: usize) -> UntypedAst {
|
||||
let actual_points = rng.gen_range(1..=max_points);
|
||||
random_code_with_size(rng, instr_set, actual_points)
|
||||
}
|
||||
|
||||
/// The main logic:
|
||||
/// If points == 1 => pick a single instruction/ephemeral
|
||||
/// Else =>
|
||||
/// - We "decompose" (points - 1) among sub-codes
|
||||
/// - For each sub-points, we recursively call random_code_with_size
|
||||
/// - We return a Sublist of those children, possibly in random order
|
||||
pub fn random_code_with_size(
|
||||
rng: &mut impl Rng,
|
||||
instr_set: &InstructionSet,
|
||||
points: usize,
|
||||
) -> UntypedAst {
|
||||
use UntypedAst::*;
|
||||
|
||||
// Base case
|
||||
if points == 1 {
|
||||
// Choose a random "atom" from instr_set
|
||||
return instr_set.random_atom_as_ast(rng);
|
||||
}
|
||||
|
||||
// If `points > 1`, let's produce a Sublist
|
||||
// We'll break (points - 1) into sub-points via `decompose`
|
||||
let subpoints_list = decompose(rng, points - 1, points - 1);
|
||||
// subpoints_list is e.g. [2, 3, 5] and sums to (points-1)
|
||||
|
||||
// Then for each "subpoints", we do random_code_with_size
|
||||
let mut sub_asts: Vec<UntypedAst> = subpoints_list
|
||||
.into_iter()
|
||||
.map(|sp| random_code_with_size(rng, instr_set, sp))
|
||||
.collect();
|
||||
|
||||
// The spec says "Return a list containing the results, in random order"
|
||||
// so let's shuffle sub_asts
|
||||
sub_asts.shuffle(rng);
|
||||
|
||||
Sublist(sub_asts)
|
||||
}
|
||||
|
||||
/// Decompose a number into random parts.
|
||||
///
|
||||
/// - If number is 1 or max_parts is 1 => return [number].
|
||||
/// - Otherwise pick a random split "this_part" and recurse on the remainder.
|
||||
fn decompose(rng: &mut impl Rng, number: usize, max_parts: usize) -> Vec<usize> {
|
||||
if number == 1 || max_parts == 1 {
|
||||
return vec![number];
|
||||
}
|
||||
// pick a random "this_part" in [1 .. (number-1)]
|
||||
let this_part = rng.gen_range(1..number);
|
||||
let mut remainder = decompose(rng, number - this_part, max_parts - 1);
|
||||
let mut result = vec![this_part];
|
||||
result.append(&mut remainder);
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//! src/gp/local_mutation.rs
|
||||
//! A localized mutation operator that makes small tweaks depending on node type.
|
||||
|
||||
use rand::Rng;
|
||||
use crate::compiler::ast::{UntypedAst, OpCode};
|
||||
use crate::gp::mutation::{
|
||||
enum_nodes_dfs, get_subtree, replace_subtree,
|
||||
};
|
||||
use crate::gp::generate_spec::{
|
||||
InstructionAtom, InstructionSet,
|
||||
};
|
||||
|
||||
/// A top-level function to perform a localized mutation with a chosen `InstructionSet`.
|
||||
pub fn local_mutation(
|
||||
original: &UntypedAst,
|
||||
rng: &mut impl Rng,
|
||||
instr_set: &InstructionSet,
|
||||
) -> UntypedAst {
|
||||
let all_paths = enum_nodes_dfs(original);
|
||||
let idx = rng.gen_range(0..all_paths.len());
|
||||
let chosen_path = &all_paths[idx];
|
||||
|
||||
let old_subtree = get_subtree(original, chosen_path);
|
||||
let new_subtree = local_mutation_node(&old_subtree, rng, instr_set);
|
||||
replace_subtree(original, chosen_path, new_subtree)
|
||||
}
|
||||
|
||||
/// A convenience wrapper that always uses `InstructionSet::new_default()`.
|
||||
pub fn local_mutation_fixed(
|
||||
original: &UntypedAst,
|
||||
rng: &mut impl Rng,
|
||||
) -> UntypedAst {
|
||||
let instr_set = InstructionSet::new_default();
|
||||
local_mutation(original, rng, &instr_set)
|
||||
}
|
||||
|
||||
fn local_mutation_node(
|
||||
subtree: &UntypedAst,
|
||||
rng: &mut impl Rng,
|
||||
instr_set: &InstructionSet,
|
||||
) -> UntypedAst {
|
||||
match subtree {
|
||||
UntypedAst::IntLiteral(val) => {
|
||||
let delta = rng.gen_range(-5..=5);
|
||||
let new_val = val.saturating_add(delta);
|
||||
UntypedAst::IntLiteral(new_val)
|
||||
}
|
||||
UntypedAst::Instruction(op) => {
|
||||
let mut new_op = pick_random_opcode(rng, instr_set);
|
||||
for _ in 0..3 {
|
||||
if &new_op != op {
|
||||
break;
|
||||
}
|
||||
new_op = pick_random_opcode(rng, instr_set);
|
||||
}
|
||||
UntypedAst::Instruction(new_op)
|
||||
}
|
||||
UntypedAst::Sublist(children) => {
|
||||
let choice = rng.gen_range(0..3);
|
||||
let mut new_children = children.clone();
|
||||
|
||||
match choice {
|
||||
0 => {
|
||||
// remove one child if possible
|
||||
if !new_children.is_empty() {
|
||||
let i = rng.gen_range(0..new_children.len());
|
||||
new_children.remove(i);
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
// insert a new small node
|
||||
let i = rng.gen_range(0..=new_children.len());
|
||||
let node = create_small_node(rng, instr_set);
|
||||
new_children.insert(i, node);
|
||||
}
|
||||
2 => {
|
||||
// reorder two children
|
||||
if new_children.len() > 1 {
|
||||
let i = rng.gen_range(0..new_children.len());
|
||||
let j = rng.gen_range(0..new_children.len());
|
||||
new_children.swap(i, j);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
UntypedAst::Sublist(new_children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pick_random_opcode(rng: &mut impl Rng, instr_set: &InstructionSet) -> OpCode {
|
||||
use crate::compiler::ast::OpCode;
|
||||
|
||||
let mut opcode_list: Vec<&OpCode> = Vec::new();
|
||||
for atom in &instr_set.atoms {
|
||||
if let InstructionAtom::Opcode(ref op) = atom {
|
||||
opcode_list.push(op);
|
||||
}
|
||||
}
|
||||
if opcode_list.is_empty() {
|
||||
// fallback => default to Noop
|
||||
return OpCode::Noop;
|
||||
}
|
||||
let i = rng.gen_range(0..opcode_list.len());
|
||||
opcode_list[i].clone()
|
||||
}
|
||||
|
||||
fn create_small_node(
|
||||
rng: &mut impl Rng,
|
||||
instr_set: &InstructionSet,
|
||||
) -> UntypedAst {
|
||||
instr_set.random_atom_as_ast(rng)
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod population;
|
||||
pub mod genetic_ops;
|
||||
pub mod generate;
|
||||
pub mod generate_spec;
|
||||
pub mod mutation;
|
||||
pub mod local_mutation;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
use rand::Rng;
|
||||
use crate::compiler::ast::UntypedAst;
|
||||
use crate::gp::generate_spec::ranmdom_code_fixed; // or push3-based generator
|
||||
|
||||
/// A "path" is a list of indices leading from the root to a child.
|
||||
/// For example, [] is the root, [0] is the root's first child, [0,1] is that child's second child, etc.
|
||||
pub type Path = Vec<usize>;
|
||||
|
||||
/// Enumerate all nodes in a DFS order (root first).
|
||||
/// For each node, we store the `Path` (list of child indices).
|
||||
pub fn enum_nodes_dfs(ast: &UntypedAst) -> Vec<Path> {
|
||||
let mut paths = Vec::new();
|
||||
dfs_helper(ast, &mut paths, &mut vec![]);
|
||||
paths
|
||||
}
|
||||
|
||||
/// Recursive helper: `current_path` is a stack we push child indices onto.
|
||||
/// `paths` is the global list of final results.
|
||||
fn dfs_helper(ast: &UntypedAst, paths: &mut Vec<Path>, current_path: &mut Vec<usize>) {
|
||||
// Record the current path as a valid node
|
||||
paths.push(current_path.clone());
|
||||
|
||||
match ast {
|
||||
UntypedAst::Sublist(children) => {
|
||||
// For each child, push its index, recurse, then pop
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
current_path.push(i);
|
||||
dfs_helper(child, paths, current_path);
|
||||
current_path.pop();
|
||||
}
|
||||
}
|
||||
UntypedAst::IntLiteral(_) | UntypedAst::Instruction(_) => {
|
||||
// Leaf node => no children
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the node at `path` in `original` with `replacement`, returning a new AST.
|
||||
/// If `path` is empty => we replace the root entirely.
|
||||
pub fn replace_subtree(
|
||||
original: &UntypedAst,
|
||||
path: &[usize],
|
||||
replacement: UntypedAst
|
||||
) -> UntypedAst {
|
||||
if path.is_empty() {
|
||||
// means we’re replacing the root
|
||||
return replacement;
|
||||
}
|
||||
// otherwise, we need to walk the sublist chain
|
||||
match original {
|
||||
UntypedAst::Sublist(children) => {
|
||||
// let path_head = path[0], path_tail = path[1..]
|
||||
let (first_idx, tail_path) = (path[0], &path[1..]);
|
||||
let mut new_children = children.clone();
|
||||
new_children[first_idx] = replace_subtree(&children[first_idx], tail_path, replacement);
|
||||
UntypedAst::Sublist(new_children)
|
||||
}
|
||||
// If the path is not empty but we’re not in a Sublist => error or do nothing
|
||||
UntypedAst::IntLiteral(_) | UntypedAst::Instruction(_) => {
|
||||
// Possibly, if the path is invalid, we ignore. Or we treat it as "replace anyway."
|
||||
// Here, we might treat it as an error or a no-op.
|
||||
original.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a new AST by choosing exactly one node in `original` at random
|
||||
/// (by enumerating them), and replacing it with a new random subtree.
|
||||
pub fn mutate_by_index(
|
||||
original: &UntypedAst,
|
||||
rng: &mut impl Rng,
|
||||
max_points: usize,
|
||||
) -> UntypedAst {
|
||||
// 1) Enumerate all nodes => get a vector of `Path`
|
||||
let all_paths = enum_nodes_dfs(original);
|
||||
// pick a random path
|
||||
let idx = rng.gen_range(0..all_paths.len());
|
||||
let chosen_path = &all_paths[idx];
|
||||
|
||||
// 2) Generate a new subtree with our "fixed" random code generator
|
||||
let new_subtree = ranmdom_code_fixed(rng, max_points);
|
||||
|
||||
// 3) Replace the subtree at `chosen_path` in `original` with `new_subtree`
|
||||
replace_subtree(original, chosen_path, new_subtree)
|
||||
}
|
||||
|
||||
/// Return the subtree of `original` at `path`,
|
||||
/// cloning it as a `UntypedAst`.
|
||||
/// If `path` is empty => returns the entire `original`.
|
||||
pub fn get_subtree(original: &UntypedAst, path: &[usize]) -> UntypedAst {
|
||||
if path.is_empty() {
|
||||
// The entire AST is the subtree
|
||||
return original.clone();
|
||||
}
|
||||
|
||||
match original {
|
||||
UntypedAst::Sublist(children) => {
|
||||
let (first_idx, tail_path) = (path[0], &path[1..]);
|
||||
if first_idx >= children.len() {
|
||||
// If path index is out of range, just clone the entire AST or
|
||||
// do something fallback
|
||||
return original.clone();
|
||||
}
|
||||
get_subtree(&children[first_idx], tail_path)
|
||||
}
|
||||
UntypedAst::IntLiteral(_) | UntypedAst::Instruction(_) => {
|
||||
// If path is non-empty but this is a leaf,
|
||||
// fallback: just return the leaf.
|
||||
original.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a subtree crossover between two ASTs.
|
||||
/// 1) We pick a random node in `a` and a random node in `b`,
|
||||
/// 2) We swap those subtrees,
|
||||
/// 3) Return the two new ASTs.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```rust
|
||||
/// let (childA, childB) = crossover_by_index(&parentA, &parentB, &mut rng);
|
||||
/// ```
|
||||
pub fn crossover_by_index(
|
||||
a: &UntypedAst,
|
||||
b: &UntypedAst,
|
||||
rng: &mut impl Rng,
|
||||
) -> (UntypedAst, UntypedAst) {
|
||||
// 1) enumerate nodes in a
|
||||
let paths_a = enum_nodes_dfs(a);
|
||||
let idx_a = rng.gen_range(0..paths_a.len());
|
||||
let chosen_a = &paths_a[idx_a];
|
||||
|
||||
// 2) enumerate nodes in b
|
||||
let paths_b = enum_nodes_dfs(b);
|
||||
let idx_b = rng.gen_range(0..paths_b.len());
|
||||
let chosen_b = &paths_b[idx_b];
|
||||
|
||||
// 3) get subtree from a, from b
|
||||
let subtree_a = get_subtree(a, chosen_a);
|
||||
let subtree_b = get_subtree(b, chosen_b);
|
||||
|
||||
// 4) replace them
|
||||
let new_a = replace_subtree(a, chosen_a, subtree_b);
|
||||
let new_b = replace_subtree(b, chosen_b, subtree_a);
|
||||
|
||||
(new_a, new_b)
|
||||
}
|
||||
Reference in New Issue
Block a user