WIP: offchain-start #14
@@ -2,20 +2,25 @@
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
|
// Our GP + compiler modules (adjust paths as needed)
|
||||||
use offchain::compiler::ast::{UntypedAst, Push3Ast};
|
use offchain::compiler::ast::{UntypedAst, Push3Ast};
|
||||||
use offchain::helpers::artifact::get_creation_code;
|
use offchain::helpers::artifact::get_creation_code;
|
||||||
use offchain::runner::revm_runner::{
|
use offchain::runner::revm_runner::{
|
||||||
EvmRunner,
|
EvmRunner,
|
||||||
Push3InterpreterInputs,
|
Push3InterpreterInputs,
|
||||||
Push3InterpreterOutputs
|
|
||||||
};
|
};
|
||||||
use offchain::gp::generate_spec::{ranmdom_code_fixed};
|
|
||||||
use offchain::gp::mutation::mutate_by_index;
|
|
||||||
use offchain::compiler::push3_describtor::make_sublist_descriptor;
|
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::types::U256;
|
||||||
use ethers::abi::{encode, Token};
|
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)> {
|
fn generate_samples() -> Vec<(i32, i32)> {
|
||||||
let mut samples = Vec::new();
|
let mut samples = Vec::new();
|
||||||
for x in -5..=5 {
|
for x in -5..=5 {
|
||||||
@@ -25,8 +30,9 @@ fn generate_samples() -> Vec<(i32, i32)> {
|
|||||||
samples
|
samples
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Evaluate a single AST on a single input `x`, returning the integer
|
/// 2) Evaluate a single AST on a single input `x` by
|
||||||
// that ends up on top of the final int stack (or 0 if empty).
|
/// pushing `x` onto the int stack, running in EVM,
|
||||||
|
/// and reading the final top of the int stack.
|
||||||
fn evaluate_ast_on_x(
|
fn evaluate_ast_on_x(
|
||||||
runner: &mut EvmRunner,
|
runner: &mut EvmRunner,
|
||||||
ast: &UntypedAst,
|
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(
|
fn evaluate_fitness(
|
||||||
runner: &mut EvmRunner,
|
runner: &mut EvmRunner,
|
||||||
ast: &UntypedAst,
|
ast: &UntypedAst,
|
||||||
@@ -80,30 +86,33 @@ fn evaluate_fitness(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
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();
|
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_hex_filename = "../onchain/out/Push3Interpreter.sol/Push3Interpreter.json";
|
||||||
let creation_bytes = get_creation_code(creation_hex_filename)?;
|
let creation_bytes = get_creation_code(creation_hex_filename)?;
|
||||||
let mut runner = EvmRunner::new(creation_bytes)?;
|
let mut runner = EvmRunner::new(creation_bytes)?;
|
||||||
|
|
||||||
// C) Setup GP parameters
|
// 3) GP parameters
|
||||||
let pop_size = 1000; // population size
|
let pop_size = 1000; // must be divisible by 4 for our scheme
|
||||||
let max_points = 15; // max "size" for random code
|
let generations = 30;
|
||||||
let generations = 100; // how many generations we run
|
let early_stop_threshold = 1.0;
|
||||||
let early_stop_threshold = 1.0; // if error < this => we can stop
|
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();
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
// D) Initialize random population
|
// 4) Initialize population (random)
|
||||||
let mut population: Vec<UntypedAst> = (0..pop_size)
|
let mut population: Vec<UntypedAst> = (0..pop_size)
|
||||||
.map(|_| ranmdom_code_fixed(&mut rng, max_points))
|
.map(|_| ranmdom_code_fixed(&mut rng, max_points))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// E) Main GP Loop
|
// 5) Main GP loop
|
||||||
for gen in 0..generations {
|
for gen in 0..generations {
|
||||||
// 1) Evaluate each => store (ast, error)
|
// (a) Evaluate
|
||||||
let mut scored: Vec<(UntypedAst, f64)> = population.into_iter()
|
let mut scored: Vec<(UntypedAst, f64)> = population.into_iter()
|
||||||
.map(|ast| {
|
.map(|ast| {
|
||||||
let err = evaluate_fitness(&mut runner, &ast, &samples);
|
let err = evaluate_fitness(&mut runner, &ast, &samples);
|
||||||
@@ -111,59 +120,77 @@ fn main() -> Result<()> {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// 2) Sort ascending by error
|
// (b) Sort ascending by error
|
||||||
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
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;
|
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 {
|
if best_err < early_stop_threshold {
|
||||||
println!("Best error < {early_stop_threshold}, stopping early!");
|
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();
|
population = scored.into_iter().map(|(a, _)| a).collect();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Keep best half
|
// (d) Reproduction => build new population
|
||||||
let half = scored.len() / 2;
|
let mut new_pop = Vec::with_capacity(pop_size);
|
||||||
let survivors = &scored[..half];
|
|
||||||
|
|
||||||
// 6) Refill population: half mutated, half fresh random
|
// keep best quarter
|
||||||
let mut new_pop = Vec::with_capacity(scored.len());
|
for i in 0..quarter {
|
||||||
// a) keep survivors
|
new_pop.push(scored[i].0.clone());
|
||||||
for &(ref ast, _) in survivors {
|
|
||||||
new_pop.push(ast.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// b) how many to add?
|
// add random quarter
|
||||||
let remainder_size = pop_size - new_pop.len();
|
for _ in 0..quarter {
|
||||||
// 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 {
|
|
||||||
let fresh = ranmdom_code_fixed(&mut rng, max_points);
|
let fresh = ranmdom_code_fixed(&mut rng, max_points);
|
||||||
new_pop.push(fresh);
|
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;
|
population = new_pop;
|
||||||
}
|
}
|
||||||
|
|
||||||
// F) Final Print
|
// 6) Final population => we can evaluate & print
|
||||||
println!("\n=== Final Population ===");
|
println!("\n=== Final Population ===");
|
||||||
for (i, ast) in population.iter().enumerate() {
|
for (i, ast) in population.iter().enumerate() {
|
||||||
let err = evaluate_fitness(&mut runner, ast, &samples);
|
let err = evaluate_fitness(&mut runner, ast, &samples);
|
||||||
println!("Subject #{i}, err={err}, ast={:?}", ast);
|
println!("Subject #{i}, err={err}, AST:\n{:#?}", ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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(-10..10);
|
let val = rng.gen_range(-100..100);
|
||||||
UntypedAst::IntLiteral(val)
|
UntypedAst::IntLiteral(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,3 +84,64 @@ pub fn mutate_by_index(
|
|||||||
replace_subtree(original, chosen_path, 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