diff --git a/offchain/Cargo.toml b/offchain/Cargo.toml index 62a6f91..561dd5a 100644 --- a/offchain/Cargo.toml +++ b/offchain/Cargo.toml @@ -15,6 +15,10 @@ path = "src/bin/gp_spec_tester.rs" 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" diff --git a/offchain/src/bin/symreg_experiment.rs b/offchain/src/bin/symreg_experiment.rs index dd57de5..1c7e1b9 100644 --- a/offchain/src/bin/symreg_experiment.rs +++ b/offchain/src/bin/symreg_experiment.rs @@ -64,7 +64,7 @@ fn evaluate_ast_on_x( // e) If final_int_stack is empty => 0, else top item if outputs.final_int_stack.is_empty() { - 0 + i32::MAX } else { *outputs.final_int_stack.last().unwrap() as i32 } @@ -77,14 +77,22 @@ fn evaluate_fitness( samples: &[(i32, i32)] ) -> f64 { let mut error_sum = 0.0; - for &(x, target_y) in samples { - let predicted = evaluate_ast_on_x(runner, ast, x); - let diff = (predicted - target_y) as f64; - error_sum += diff * diff; + 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(); @@ -95,10 +103,10 @@ fn main() -> Result<()> { let mut runner = EvmRunner::new(creation_bytes)?; // 3) GP parameters - let pop_size = 1000; // must be divisible by 4 for our scheme - let generations = 30; + let pop_size = 4000; // must be divisible by 4 for our scheme + let generations = 100; let early_stop_threshold = 1.0; - let max_points = 15; + 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; @@ -129,15 +137,15 @@ fn main() -> Result<()> { 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 - ); - } + // 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 { @@ -186,12 +194,30 @@ fn main() -> Result<()> { population = new_pop; } - // 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:\n{:#?}", ast); - } + // 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(()) } diff --git a/offchain/src/bin/symreg_experiment_local.rs b/offchain/src/bin/symreg_experiment_local.rs new file mode 100644 index 0000000..58df871 --- /dev/null +++ b/offchain/src/bin/symreg_experiment_local.rs @@ -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 = (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(()) +} diff --git a/offchain/src/gp/generate_spec.rs b/offchain/src/gp/generate_spec.rs index 3004403..0ac4976 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(-100..100); + let val = rng.gen_range(-30..30); UntypedAst::IntLiteral(val) } } diff --git a/offchain/src/gp/local_mutation.rs b/offchain/src/gp/local_mutation.rs new file mode 100644 index 0000000..5170924 --- /dev/null +++ b/offchain/src/gp/local_mutation.rs @@ -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) +} diff --git a/offchain/src/gp/mod.rs b/offchain/src/gp/mod.rs index 3b107c3..91c726c 100644 --- a/offchain/src/gp/mod.rs +++ b/offchain/src/gp/mod.rs @@ -3,3 +3,4 @@ pub mod genetic_ops; pub mod generate; pub mod generate_spec; pub mod mutation; +pub mod local_mutation;