From eeb5dd4141d3cc843cdf6358178d4c20a60c30c9 Mon Sep 17 00:00:00 2001 From: zjkree Date: Tue, 14 Jan 2025 05:48:42 +0100 Subject: [PATCH] Some modifications to the regression experiment. --- offchain/src/bin/symreg_experiment.rs | 123 ++++++++++++++++---------- offchain/src/gp/generate_spec.rs | 2 +- offchain/src/gp/mutation.rs | 61 +++++++++++++ 3 files changed, 137 insertions(+), 49 deletions(-) diff --git a/offchain/src/bin/symreg_experiment.rs b/offchain/src/bin/symreg_experiment.rs index 116c4e7..dd57de5 100644 --- a/offchain/src/bin/symreg_experiment.rs +++ b/offchain/src/bin/symreg_experiment.rs @@ -2,20 +2,25 @@ 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, - Push3InterpreterOutputs + Push3InterpreterInputs, }; -use offchain::gp::generate_spec::{ranmdom_code_fixed}; -use offchain::gp::mutation::mutate_by_index; 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 +/// 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 { @@ -25,8 +30,9 @@ fn generate_samples() -> Vec<(i32, i32)> { samples } -// 2) Evaluate a single AST on a single input `x`, returning the integer -// that ends up on top of the final int stack (or 0 if empty). +/// 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, @@ -64,7 +70,7 @@ fn evaluate_ast_on_x( } } -// 3) Evaluate an AST on all (x, y) samples => compute MSE +/// 3) Evaluate an AST on all (x, y) samples => compute MSE fn evaluate_fitness( runner: &mut EvmRunner, ast: &UntypedAst, @@ -80,30 +86,33 @@ fn evaluate_fitness( } fn main() -> Result<()> { - // A) Generate samples for f(x)=3x^2 + x + 3 + // 1) Generate samples for f(x)=3x^2 + x + 3 let samples = generate_samples(); - // B) Create an ephemeral EVM runner (Push3Interpreter) + // 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)?; - // C) Setup GP parameters - let pop_size = 1000; // population size - let max_points = 15; // max "size" for random code - let generations = 100; // how many generations we run - let early_stop_threshold = 1.0; // if error < this => we can stop + // 3) GP parameters + let pop_size = 1000; // must be divisible by 4 for our scheme + let generations = 30; + let early_stop_threshold = 1.0; + let max_points = 15; + + 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(); - // D) Initialize random population + // 4) Initialize population (random) let mut population: Vec = (0..pop_size) .map(|_| ranmdom_code_fixed(&mut rng, max_points)) .collect(); - // E) Main GP Loop + // 5) Main GP loop for gen in 0..generations { - // 1) Evaluate each => store (ast, error) + // (a) Evaluate let mut scored: Vec<(UntypedAst, f64)> = population.into_iter() .map(|ast| { let err = evaluate_fitness(&mut runner, &ast, &samples); @@ -111,59 +120,77 @@ fn main() -> Result<()> { }) .collect(); - // 2) Sort ascending by error + // (b) Sort ascending by error scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); - // 3) Print best error + // (c) Print generation summary let best_err = scored[0].1; - println!("Generation {gen}, best error = {best_err}"); + println!("\n=== Generation {gen} ==="); + println!("Best error = {best_err}"); - // 4) Check if best is below threshold => early stop + // 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; } - // 5) Keep best half - let half = scored.len() / 2; - let survivors = &scored[..half]; + // (d) Reproduction => build new population + let mut new_pop = Vec::with_capacity(pop_size); - // 6) Refill population: half mutated, half fresh random - let mut new_pop = Vec::with_capacity(scored.len()); - // a) keep survivors - for &(ref ast, _) in survivors { - new_pop.push(ast.clone()); + // keep best quarter + for i in 0..quarter { + new_pop.push(scored[i].0.clone()); } - // b) how many to add? - let remainder_size = pop_size - new_pop.len(); - // we do half mutated, half new random - let mutated_count = remainder_size / 2; - let random_count = remainder_size - mutated_count; - - // c) add mutated - for _ in 0..mutated_count { - let idx = rng.gen_range(0..half); - let mutated = mutate_by_index(&survivors[idx].0, &mut rng, max_points); - new_pop.push(mutated); - } - - // d) add brand-new random - for _ in 0..random_count { + // add random quarter + for _ in 0..quarter { let fresh = ranmdom_code_fixed(&mut rng, max_points); new_pop.push(fresh); } - // store for next generation + // 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; } - // F) Final Print + // 6) Final population => we can evaluate & print println!("\n=== Final Population ==="); for (i, ast) in population.iter().enumerate() { let err = evaluate_fitness(&mut runner, ast, &samples); - println!("Subject #{i}, err={err}, ast={:?}", ast); + println!("Subject #{i}, err={err}, AST:\n{:#?}", ast); } Ok(()) diff --git a/offchain/src/gp/generate_spec.rs b/offchain/src/gp/generate_spec.rs index 15a2100..3004403 100644 --- a/offchain/src/gp/generate_spec.rs +++ b/offchain/src/gp/generate_spec.rs @@ -47,7 +47,7 @@ impl InstructionSet { InstructionAtom::Opcode(op) => UntypedAst::Instruction(op.clone()), InstructionAtom::EphemeralInt => { // For ephemeral int, produce a random literal in some range - let val = rng.gen_range(-10..10); + let val = rng.gen_range(-100..100); UntypedAst::IntLiteral(val) } } diff --git a/offchain/src/gp/mutation.rs b/offchain/src/gp/mutation.rs index 0b02133..2e15996 100644 --- a/offchain/src/gp/mutation.rs +++ b/offchain/src/gp/mutation.rs @@ -84,3 +84,64 @@ pub fn mutate_by_index( 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) +}