Add symbolic regression experiment.

This commit is contained in:
zjkree
2025-01-14 05:22:08 +01:00
parent c5538e35bb
commit 64a81e4110
6 changed files with 423 additions and 0 deletions
+121
View File
@@ -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(-10..10);
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
}
+2
View File
@@ -1,3 +1,5 @@
pub mod population;
pub mod genetic_ops;
pub mod generate;
pub mod generate_spec;
pub mod mutation;
+86
View File
@@ -0,0 +1,86 @@
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)
}