WIP: offchain-start #14
@@ -15,6 +15,10 @@ path = "src/bin/gp_spec_tester.rs"
|
|||||||
name = "symreg_experiment"
|
name = "symreg_experiment"
|
||||||
path = "src/bin/symreg_experiment.rs"
|
path = "src/bin/symreg_experiment.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "symreg_experiment_local"
|
||||||
|
path = "src/bin/symreg_experiment_local.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ fn evaluate_ast_on_x(
|
|||||||
|
|
||||||
// e) If final_int_stack is empty => 0, else top item
|
// e) If final_int_stack is empty => 0, else top item
|
||||||
if outputs.final_int_stack.is_empty() {
|
if outputs.final_int_stack.is_empty() {
|
||||||
0
|
i32::MAX
|
||||||
} else {
|
} else {
|
||||||
*outputs.final_int_stack.last().unwrap() as i32
|
*outputs.final_int_stack.last().unwrap() as i32
|
||||||
}
|
}
|
||||||
@@ -77,14 +77,22 @@ fn evaluate_fitness(
|
|||||||
samples: &[(i32, i32)]
|
samples: &[(i32, i32)]
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
let mut error_sum = 0.0;
|
let mut error_sum = 0.0;
|
||||||
for &(x, target_y) in samples {
|
for &(x, target_y32) in samples {
|
||||||
let predicted = evaluate_ast_on_x(runner, ast, x);
|
// cast to i64
|
||||||
let diff = (predicted - target_y) as f64;
|
let target_y = target_y32 as i64;
|
||||||
error_sum += diff * diff;
|
|
||||||
|
// 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
|
error_sum / samples.len() as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// 1) Generate samples for f(x)=3x^2 + x + 3
|
// 1) Generate samples for f(x)=3x^2 + x + 3
|
||||||
let samples = generate_samples();
|
let samples = generate_samples();
|
||||||
@@ -95,10 +103,10 @@ fn main() -> Result<()> {
|
|||||||
let mut runner = EvmRunner::new(creation_bytes)?;
|
let mut runner = EvmRunner::new(creation_bytes)?;
|
||||||
|
|
||||||
// 3) GP parameters
|
// 3) GP parameters
|
||||||
let pop_size = 1000; // must be divisible by 4 for our scheme
|
let pop_size = 4000; // must be divisible by 4 for our scheme
|
||||||
let generations = 30;
|
let generations = 100;
|
||||||
let early_stop_threshold = 1.0;
|
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");
|
assert_eq!(pop_size % 4, 0, "pop_size must be multiple of 4 for our scheme");
|
||||||
let quarter = pop_size / 4;
|
let quarter = pop_size / 4;
|
||||||
@@ -129,15 +137,15 @@ fn main() -> Result<()> {
|
|||||||
println!("Best error = {best_err}");
|
println!("Best error = {best_err}");
|
||||||
|
|
||||||
// Print top 5 subjects in pretty format
|
// Print top 5 subjects in pretty format
|
||||||
let top_n = 5.min(scored.len());
|
// let top_n = 5.min(scored.len());
|
||||||
println!("Top {top_n} subjects:");
|
// println!("Top {top_n} subjects:");
|
||||||
for i in 0..top_n {
|
// for i in 0..top_n {
|
||||||
println!(
|
// println!(
|
||||||
" #{i}, err={}, AST:\n{:#?}",
|
// " #{i}, err={}, AST:\n{:#?}",
|
||||||
scored[i].1,
|
// scored[i].1,
|
||||||
scored[i].0
|
// scored[i].0
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// early stop?
|
// early stop?
|
||||||
if best_err < early_stop_threshold {
|
if best_err < early_stop_threshold {
|
||||||
@@ -186,12 +194,30 @@ fn main() -> Result<()> {
|
|||||||
population = new_pop;
|
population = new_pop;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Final population => we can evaluate & print
|
// 6) Final population => evaluate & sort, then print only top 10
|
||||||
println!("\n=== Final Population ===");
|
println!("\n=== Final Population (Top 10) ===");
|
||||||
for (i, ast) in population.iter().enumerate() {
|
|
||||||
let err = evaluate_fitness(&mut runner, ast, &samples);
|
|
||||||
println!("Subject #{i}, err={err}, AST:\n{:#?}", ast);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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(())
|
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(())
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ impl InstructionSet {
|
|||||||
InstructionAtom::Opcode(op) => UntypedAst::Instruction(op.clone()),
|
InstructionAtom::Opcode(op) => UntypedAst::Instruction(op.clone()),
|
||||||
InstructionAtom::EphemeralInt => {
|
InstructionAtom::EphemeralInt => {
|
||||||
// For ephemeral int, produce a random literal in some range
|
// 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)
|
UntypedAst::IntLiteral(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ pub mod genetic_ops;
|
|||||||
pub mod generate;
|
pub mod generate;
|
||||||
pub mod generate_spec;
|
pub mod generate_spec;
|
||||||
pub mod mutation;
|
pub mod mutation;
|
||||||
|
pub mod local_mutation;
|
||||||
|
|||||||
Reference in New Issue
Block a user