From 73ec7a00b302e86ccd3b7d5a5c674592594fde00 Mon Sep 17 00:00:00 2001 From: Urmzd Mukhammadnaim Date: Sun, 29 Mar 2026 01:38:32 -0500 Subject: [PATCH 1/5] refactor: rename lgp-cli to lgp and lgp to lgp-core for simpler cargo install --- .github/workflows/experiments.yml | 4 ++-- .github/workflows/release.yml | 6 ++--- Cargo.lock | 38 +++++++++++++++---------------- crates/lgp-cli/Cargo.toml | 4 ++-- crates/lgp/Cargo.toml | 13 ++++++++++- teasr.toml | 2 +- 6 files changed, 39 insertions(+), 28 deletions(-) diff --git a/.github/workflows/experiments.yml b/.github/workflows/experiments.yml index 9a99a729..d5a0185c 100644 --- a/.github/workflows/experiments.yml +++ b/.github/workflows/experiments.yml @@ -48,11 +48,11 @@ jobs: - name: Run baseline if: inputs.experiment_type == 'baseline' - run: cargo run -p lgp-cli --release -- experiment iris_baseline --skip-search + run: cargo run -p lgp --release -- experiment iris_baseline --skip-search - name: Run experiments if: inputs.experiment_type == 'experiments' - run: cargo run -p lgp-cli --release -- experiment --skip-search -n ${{ inputs.iterations }} + run: cargo run -p lgp --release -- experiment --skip-search -n ${{ inputs.iterations }} - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 28819c22..4ab8126a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,8 +85,8 @@ jobs: - name: Wait for crates.io index update run: sleep 30 - - name: Publish lgp-cli to crates.io - run: cargo publish -p lgp-cli + - name: Publish lgp to crates.io + run: cargo publish -p lgp env: CARGO_REGISTRY_TOKEN: ${{ steps.crates-token.outputs.token }} @@ -127,7 +127,7 @@ jobs: echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml - name: Build binary - run: cargo build --release --target ${{ matrix.target }} -p lgp-cli + run: cargo build --release --target ${{ matrix.target }} -p lgp - name: Package binary run: | diff --git a/Cargo.lock b/Cargo.lock index 40e7ef0e..7c795784 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,54 +1082,54 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lgp" -version = "1.5.0" +version = "1.6.0" dependencies = [ "chrono", "clap 4.5.58", - "config", - "criterion", + "crossterm", "csv", - "derivative", - "derive_builder", - "derive_more", - "glob", "gymnasia", + "indicatif", "itertools", - "lazy_static", + "lgp-core", + "plotters", "rand", - "rand_xoshiro", "rayon", "serde", "serde_json", - "strum", - "strum_macros", "toml 0.8.23", "tracing", - "tracing-appender", "tracing-subscriber", - "uuid", ] [[package]] -name = "lgp-cli" -version = "1.5.0" +name = "lgp-core" +version = "1.6.0" dependencies = [ "chrono", "clap 4.5.58", - "crossterm", + "config", + "criterion", "csv", + "derivative", + "derive_builder", + "derive_more", + "glob", "gymnasia", - "indicatif", "itertools", - "lgp", - "plotters", + "lazy_static", "rand", + "rand_xoshiro", "rayon", "serde", "serde_json", + "strum", + "strum_macros", "toml 0.8.23", "tracing", + "tracing-appender", "tracing-subscriber", + "uuid", ] [[package]] diff --git a/crates/lgp-cli/Cargo.toml b/crates/lgp-cli/Cargo.toml index 714da0be..31155f0d 100644 --- a/crates/lgp-cli/Cargo.toml +++ b/crates/lgp-cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "lgp-cli" +name = "lgp" version.workspace = true edition.workspace = true authors.workspace = true @@ -12,7 +12,7 @@ name = "lgp" path = "src/main.rs" [dependencies] -lgp = { path = "../lgp", version = "1.1", features = ["gym"] } +lgp = { package = "lgp-core", path = "../lgp", version = "1.1", features = ["gym"] } clap = { version = "4.1.8", features = ["derive"] } chrono = "0.4" toml = "0.8" diff --git a/crates/lgp/Cargo.toml b/crates/lgp/Cargo.toml index 9f676dac..5402614d 100644 --- a/crates/lgp/Cargo.toml +++ b/crates/lgp/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "lgp" +name = "lgp-core" version.workspace = true edition.workspace = true authors.workspace = true @@ -7,6 +7,9 @@ description = "A library to solve problems using linear genetic programming" license = "Apache-2.0" repository = "https://github.com/urmzd/linear-gp" +[lib] +name = "lgp" + [dependencies] csv = "1.1" serde = { version = "1.0", features = ["derive"] } @@ -37,6 +40,7 @@ gym = ["dep:gymnasia"] [dev-dependencies] criterion = "0.4.0" +itertools = "0.10" [[bench]] name = "performance_after_training" @@ -46,3 +50,10 @@ required-features = ["gym"] [[bench]] name = "parallel_fitness" harness = false + +[[example]] +name = "cart_pole" +required-features = ["gym"] + +[[example]] +name = "iris_classification" diff --git a/teasr.toml b/teasr.toml index d0ea8177..adc88572 100644 --- a/teasr.toml +++ b/teasr.toml @@ -17,7 +17,7 @@ rows = 50 [[scenes.interactions]] type = "type" -text = "RUSTUP_HOME=/Users/urmzd/.rustup CARGO_HOME=/Users/urmzd/.cargo /Users/urmzd/.cargo/bin/cargo run -p lgp-cli -- run iris_baseline 2>&1" +text = "RUSTUP_HOME=/Users/urmzd/.rustup CARGO_HOME=/Users/urmzd/.cargo /Users/urmzd/.cargo/bin/cargo run -p lgp -- run iris_baseline 2>&1" speed = 50 [[scenes.interactions]] type = "key" From 9466be3f1d3c4eec82f0ca35ebb73855a376886e Mon Sep 17 00:00:00 2001 From: Urmzd Mukhammadnaim Date: Sun, 29 Mar 2026 01:51:34 -0500 Subject: [PATCH 2/5] refactor(cli): remove example command Remove the 'example' CLI command as examples are now better served by running them directly with 'cargo run' from the lgp crate. This simplifies the CLI surface and leverages standard Cargo tooling. --- crates/lgp-cli/src/commands/example.rs | 98 -------------------------- crates/lgp-cli/src/commands/mod.rs | 1 - crates/lgp-cli/src/main.rs | 4 -- 3 files changed, 103 deletions(-) delete mode 100644 crates/lgp-cli/src/commands/example.rs diff --git a/crates/lgp-cli/src/commands/example.rs b/crates/lgp-cli/src/commands/example.rs deleted file mode 100644 index 510cfb81..00000000 --- a/crates/lgp-cli/src/commands/example.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Example command: run Rust examples - -use clap::Args; -use std::fs; -use std::path::Path; -use std::process::Command; - -use crate::ui; - -#[derive(Args)] -pub struct ExampleArgs { - /// Example name to run (without .rs extension) - #[arg(required_unless_present = "list")] - pub name: Option, - - /// List available examples - #[arg(short, long)] - pub list: bool, -} - -pub fn execute(args: &ExampleArgs) -> Result<(), Box> { - if args.list { - list_examples() - } else if let Some(name) = &args.name { - run_example(name) - } else { - Err("Either provide an example name or use --list".into()) - } -} - -fn list_examples() -> Result<(), Box> { - let examples_dir = Path::new("examples"); - - if !examples_dir.exists() { - ui::warn("No examples directory found"); - return Ok(()); - } - - let mut examples: Vec = fs::read_dir(examples_dir)? - .filter_map(|entry| { - let entry = entry.ok()?; - let path = entry.path(); - if path.extension()?.to_str()? == "rs" { - path.file_stem()?.to_str().map(String::from) - } else { - None - } - }) - .collect(); - - if examples.is_empty() { - ui::warn("No examples found in examples/"); - return Ok(()); - } - - examples.sort(); - - ui::header("Available examples"); - for example in examples { - ui::line(&example); - } - ui::info("Run with: lgp example "); - - Ok(()) -} - -fn run_example(name: &str) -> Result<(), Box> { - let example_path = Path::new("examples").join(format!("{}.rs", name)); - - if !example_path.exists() { - return Err(format!( - "Example '{}' not found. Use 'lgp example --list' to see available examples.", - name - ) - .into()); - } - - ui::header(&format!("Running example: {}", name)); - - let sp = ui::spinner("Compiling and running..."); - let status = Command::new("cargo") - .args(["run", "--example", name, "--release"]) - .status()?; - sp.finish_and_clear(); - - if !status.success() { - return Err(format!( - "Example '{}' failed with exit code: {:?}", - name, - status.code() - ) - .into()); - } - - ui::phase_ok(&format!("Example '{}' completed", name)); - - Ok(()) -} diff --git a/crates/lgp-cli/src/commands/mod.rs b/crates/lgp-cli/src/commands/mod.rs index e157583a..1fc01e5b 100644 --- a/crates/lgp-cli/src/commands/mod.rs +++ b/crates/lgp-cli/src/commands/mod.rs @@ -1,5 +1,4 @@ pub mod analyze; -pub mod example; pub mod experiment; pub mod list; pub mod run; diff --git a/crates/lgp-cli/src/main.rs b/crates/lgp-cli/src/main.rs index 7abd7b8f..8e1e655c 100644 --- a/crates/lgp-cli/src/main.rs +++ b/crates/lgp-cli/src/main.rs @@ -61,9 +61,6 @@ enum Commands { /// Run an experiment from config Run(commands::run::RunArgs), - /// Run a Rust example - Example(commands::example::ExampleArgs), - /// Analyze experiment results (generate tables and optional plots) Analyze(commands::analyze::AnalyzeArgs), @@ -103,7 +100,6 @@ fn main() { let result: Result<(), Box> = match cli.command { Commands::List(args) => commands::list::execute(&args), Commands::Run(args) => commands::run::execute(&args), - Commands::Example(args) => commands::example::execute(&args), Commands::Analyze(args) => commands::analyze::execute(&args), Commands::Search(args) => commands::search::execute(&args), Commands::Experiment(args) => commands::experiment::execute(&args), From 4600b734419625ba118b87c5e2ca4b69cc95c0d6 Mon Sep 17 00:00:00 2001 From: Urmzd Mukhammadnaim Date: Sun, 29 Mar 2026 01:51:36 -0500 Subject: [PATCH 3/5] docs(readme): update example usage instructions Update README to document the new way to run examples using 'cargo run' directly from the lgp crate. Remove references to the removed 'lgp example' CLI command and its --list option. --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9842e531..0f81aaad 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,15 @@ lgp run cart_pole_lgp # Run Iris classification lgp run iris_baseline -# Run a Rust example -lgp example cart_pole +``` + +### Examples + +Run the standalone Rust examples directly with cargo: + +```bash +cargo run -p lgp --example cart_pole --features gym +cargo run -p lgp --example iris_classification ``` ## CLI Reference @@ -109,12 +116,6 @@ lgp analyze --input outputs --output outputs lgp experiment iris_baseline lgp experiment iris_baseline --iterations 20 lgp experiment --skip-search - -# Run a Rust example -lgp example cart_pole - -# List available examples -lgp example --list ``` ### Available Experiments From 94850df56df13ce636c29cab42335cffef215579 Mon Sep 17 00:00:00 2001 From: Urmzd Mukhammadnaim Date: Sun, 29 Mar 2026 01:52:01 -0500 Subject: [PATCH 4/5] refactor(examples): move examples to lgp crate directory Move CartPole and Iris classification examples from the top-level examples directory to crates/lgp/examples. This colocates examples with the crate they demonstrate, improving project organization and making dependencies clearer. Update run instructions in example documentation to specify the lgp crate package explicitly. --- examples/cart_pole.rs | 102 ----------------------------- examples/iris_classification.rs | 112 -------------------------------- 2 files changed, 214 deletions(-) delete mode 100644 examples/cart_pole.rs delete mode 100644 examples/iris_classification.rs diff --git a/examples/cart_pole.rs b/examples/cart_pole.rs deleted file mode 100644 index eaed7732..00000000 --- a/examples/cart_pole.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! CartPole Example -//! -//! This example demonstrates how to use the LGP framework to evolve programs -//! that can balance a pole on a cart (the classic CartPole control problem). -//! -//! Run with: `cargo run --example cart_pole` - -use itertools::Itertools; - -use gymnasia::envs::classical_control::cartpole::CartPoleEnv; - -use lgp::core::engines::core_engine::HyperParametersBuilder; -use lgp::core::engines::status_engine::{Status, StatusEngine}; -use lgp::core::instruction::InstructionGeneratorParametersBuilder; -use lgp::core::program::ProgramGeneratorParametersBuilder; -use lgp::problems::gym::GymRsEngine; - -fn main() { - println!("=== CartPole LGP Example ===\n"); - - // Step 1: Configure instruction parameters - // CartPole has 4 inputs (cart position, velocity, pole angle, angular velocity) - // and 2 actions (push left or right) - let instruction_parameters = InstructionGeneratorParametersBuilder::default() - .n_actions(2) - .n_inputs(4) - .n_extras(1) // Extra working registers - .external_factor(10.0) // Scaling for input values - .build() - .expect("Failed to build instruction parameters"); - - // Step 2: Configure program parameters - let program_parameters = ProgramGeneratorParametersBuilder::default() - .max_instructions(20) // Maximum instructions per program - .instruction_generator_parameters(instruction_parameters) - .build() - .expect("Failed to build program parameters"); - - // Step 3: Configure hyperparameters for the evolutionary algorithm - let parameters = HyperParametersBuilder::>::default() - .program_parameters(program_parameters) - .population_size(50) // Number of programs per generation - .n_generations(20) // Number of generations to evolve - .n_trials(5) // Episodes per fitness evaluation - .mutation_percent(0.5) // 50% of offspring via mutation - .crossover_percent(0.5) // 50% of offspring via crossover - .gap(0.5) // 50% of population survives each generation - .default_fitness(500.0) // Max possible score for CartPole - .build() - .expect("Failed to build hyperparameters"); - - println!("Configuration:"); - println!(" Population size: {}", parameters.population_size); - println!(" Generations: {}", parameters.n_generations); - println!(" Trials per evaluation: {}", parameters.n_trials); - println!(); - - // Step 4: Run evolution - println!("Starting evolution...\n"); - - let populations: Vec<_> = parameters - .build_engine() - .take(parameters.n_generations) - .collect_vec(); - - // Step 5: Report results - println!("Generation | Best Fitness | Median Fitness | Worst Fitness"); - println!("-----------+--------------+----------------+--------------"); - - for (gen, population) in populations.iter().enumerate() { - let best = StatusEngine::get_fitness(population.first().unwrap()); - let worst = StatusEngine::get_fitness(population.last().unwrap()); - let median = StatusEngine::get_fitness(&population[population.len() / 2]); - - println!( - "{:>10} | {:>12.2} | {:>14.2} | {:>13.2}", - gen + 1, - best, - median, - worst - ); - } - - // Final summary - let final_population = populations.last().unwrap(); - let best_program = final_population.first().unwrap(); - let best_fitness = StatusEngine::get_fitness(best_program); - - println!(); - println!("=== Evolution Complete ==="); - println!("Best fitness achieved: {:.2}", best_fitness); - - if best_fitness >= 475.0 { - println!("Success! The evolved program can balance the pole effectively."); - } else if best_fitness >= 200.0 { - println!("Partial success. The program shows improvement but may need more generations."); - } else { - println!( - "The program needs more evolution. Try increasing generations or population size." - ); - } -} diff --git a/examples/iris_classification.rs b/examples/iris_classification.rs deleted file mode 100644 index 7ed5f31e..00000000 --- a/examples/iris_classification.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Iris Classification Example -//! -//! This example demonstrates how to use the LGP framework for a classification task -//! using the classic Iris flower dataset. -//! -//! Run with: `cargo run --example iris_classification` - -use itertools::Itertools; - -use lgp::core::engines::core_engine::HyperParametersBuilder; -use lgp::core::engines::status_engine::{Status, StatusEngine}; -use lgp::core::instruction::InstructionGeneratorParametersBuilder; -use lgp::core::program::ProgramGeneratorParametersBuilder; -use lgp::problems::iris::IrisEngine; - -fn main() { - println!("=== Iris Classification LGP Example ===\n"); - - // Step 1: Configure instruction parameters - // Iris has 4 input features (sepal length/width, petal length/width) - // and 3 classes (Setosa, Versicolor, Virginica) - let instruction_parameters = InstructionGeneratorParametersBuilder::default() - .n_actions(3) // 3 classes to predict - .n_inputs(4) // 4 input features - .n_extras(2) // Extra working registers for intermediate calculations - .external_factor(1.0) // Iris features are already normalized-ish - .build() - .expect("Failed to build instruction parameters"); - - // Step 2: Configure program parameters - let program_parameters = ProgramGeneratorParametersBuilder::default() - .max_instructions(50) // Allow more complex programs for classification - .instruction_generator_parameters(instruction_parameters) - .build() - .expect("Failed to build program parameters"); - - // Step 3: Configure hyperparameters - let parameters = HyperParametersBuilder::::default() - .program_parameters(program_parameters) - .population_size(100) // Larger population for classification - .n_generations(50) // More generations for convergence - .n_trials(1) // Single pass through dataset per evaluation - .mutation_percent(0.5) - .crossover_percent(0.5) - .gap(0.5) - .default_fitness(0.0) // Minimum accuracy - .build() - .expect("Failed to build hyperparameters"); - - println!("Configuration:"); - println!(" Population size: {}", parameters.population_size); - println!(" Generations: {}", parameters.n_generations); - println!( - " Max instructions: {}", - parameters.program_parameters.max_instructions - ); - println!(); - - // Step 4: Run evolution - println!("Starting evolution...\n"); - println!("(Note: Iris dataset is downloaded on first run)\n"); - - let populations: Vec<_> = parameters - .build_engine() - .take(parameters.n_generations) - .collect_vec(); - - // Step 5: Report results (every 10 generations) - println!("Generation | Best Accuracy | Median Accuracy"); - println!("-----------+---------------+----------------"); - - for (gen, population) in populations.iter().enumerate() { - if (gen + 1) % 10 == 0 || gen == 0 || gen == populations.len() - 1 { - let best = StatusEngine::get_fitness(population.first().unwrap()); - let median = StatusEngine::get_fitness(&population[population.len() / 2]); - - // Convert to percentage (Iris has 150 samples) - let best_pct = (best / 150.0) * 100.0; - let median_pct = (median / 150.0) * 100.0; - - println!( - "{:>10} | {:>12.1}% | {:>14.1}%", - gen + 1, - best_pct, - median_pct - ); - } - } - - // Final summary - let final_population = populations.last().unwrap(); - let best_program = final_population.first().unwrap(); - let best_fitness = StatusEngine::get_fitness(best_program); - let accuracy = (best_fitness / 150.0) * 100.0; - - println!(); - println!("=== Evolution Complete ==="); - println!( - "Best accuracy: {:.1}% ({:.0}/150 correct)", - accuracy, best_fitness - ); - - if accuracy >= 95.0 { - println!("Excellent! Near-perfect classification achieved."); - } else if accuracy >= 90.0 { - println!("Great result! Strong classification performance."); - } else if accuracy >= 80.0 { - println!("Good result. Consider more generations for improvement."); - } else { - println!("The classifier needs more evolution or parameter tuning."); - } -} From 11bbcc744c633de389c54a2fc8c8519961935442 Mon Sep 17 00:00:00 2001 From: Urmzd Mukhammadnaim Date: Sun, 29 Mar 2026 01:52:16 -0500 Subject: [PATCH 5/5] fix: add examples --- crates/lgp/examples/cart_pole.rs | 102 +++++++++++++++++++ crates/lgp/examples/iris_classification.rs | 112 +++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 crates/lgp/examples/cart_pole.rs create mode 100644 crates/lgp/examples/iris_classification.rs diff --git a/crates/lgp/examples/cart_pole.rs b/crates/lgp/examples/cart_pole.rs new file mode 100644 index 00000000..b5066328 --- /dev/null +++ b/crates/lgp/examples/cart_pole.rs @@ -0,0 +1,102 @@ +//! CartPole Example +//! +//! This example demonstrates how to use the LGP framework to evolve programs +//! that can balance a pole on a cart (the classic CartPole control problem). +//! +//! Run with: `cargo run -p lgp --example cart_pole --features gym` + +use itertools::Itertools; + +use gymnasia::envs::classical_control::cartpole::CartPoleEnv; + +use lgp::core::engines::core_engine::HyperParametersBuilder; +use lgp::core::engines::status_engine::{Status, StatusEngine}; +use lgp::core::instruction::InstructionGeneratorParametersBuilder; +use lgp::core::program::ProgramGeneratorParametersBuilder; +use lgp::problems::gym::GymRsEngine; + +fn main() { + println!("=== CartPole LGP Example ===\n"); + + // Step 1: Configure instruction parameters + // CartPole has 4 inputs (cart position, velocity, pole angle, angular velocity) + // and 2 actions (push left or right) + let instruction_parameters = InstructionGeneratorParametersBuilder::default() + .n_actions(2) + .n_inputs(4) + .n_extras(1) // Extra working registers + .external_factor(10.0) // Scaling for input values + .build() + .expect("Failed to build instruction parameters"); + + // Step 2: Configure program parameters + let program_parameters = ProgramGeneratorParametersBuilder::default() + .max_instructions(20) // Maximum instructions per program + .instruction_generator_parameters(instruction_parameters) + .build() + .expect("Failed to build program parameters"); + + // Step 3: Configure hyperparameters for the evolutionary algorithm + let parameters = HyperParametersBuilder::>::default() + .program_parameters(program_parameters) + .population_size(50) // Number of programs per generation + .n_generations(20) // Number of generations to evolve + .n_trials(5) // Episodes per fitness evaluation + .mutation_percent(0.5) // 50% of offspring via mutation + .crossover_percent(0.5) // 50% of offspring via crossover + .gap(0.5) // 50% of population survives each generation + .default_fitness(500.0) // Max possible score for CartPole + .build() + .expect("Failed to build hyperparameters"); + + println!("Configuration:"); + println!(" Population size: {}", parameters.population_size); + println!(" Generations: {}", parameters.n_generations); + println!(" Trials per evaluation: {}", parameters.n_trials); + println!(); + + // Step 4: Run evolution + println!("Starting evolution...\n"); + + let populations: Vec<_> = parameters + .build_engine() + .take(parameters.n_generations) + .collect_vec(); + + // Step 5: Report results + println!("Generation | Best Fitness | Median Fitness | Worst Fitness"); + println!("-----------+--------------+----------------+--------------"); + + for (gen, population) in populations.iter().enumerate() { + let best = StatusEngine::get_fitness(population.first().unwrap()); + let worst = StatusEngine::get_fitness(population.last().unwrap()); + let median = StatusEngine::get_fitness(&population[population.len() / 2]); + + println!( + "{:>10} | {:>12.2} | {:>14.2} | {:>13.2}", + gen + 1, + best, + median, + worst + ); + } + + // Final summary + let final_population = populations.last().unwrap(); + let best_program = final_population.first().unwrap(); + let best_fitness = StatusEngine::get_fitness(best_program); + + println!(); + println!("=== Evolution Complete ==="); + println!("Best fitness achieved: {:.2}", best_fitness); + + if best_fitness >= 475.0 { + println!("Success! The evolved program can balance the pole effectively."); + } else if best_fitness >= 200.0 { + println!("Partial success. The program shows improvement but may need more generations."); + } else { + println!( + "The program needs more evolution. Try increasing generations or population size." + ); + } +} diff --git a/crates/lgp/examples/iris_classification.rs b/crates/lgp/examples/iris_classification.rs new file mode 100644 index 00000000..3e13cac3 --- /dev/null +++ b/crates/lgp/examples/iris_classification.rs @@ -0,0 +1,112 @@ +//! Iris Classification Example +//! +//! This example demonstrates how to use the LGP framework for a classification task +//! using the classic Iris flower dataset. +//! +//! Run with: `cargo run -p lgp --example iris_classification` + +use itertools::Itertools; + +use lgp::core::engines::core_engine::HyperParametersBuilder; +use lgp::core::engines::status_engine::{Status, StatusEngine}; +use lgp::core::instruction::InstructionGeneratorParametersBuilder; +use lgp::core::program::ProgramGeneratorParametersBuilder; +use lgp::problems::iris::IrisEngine; + +fn main() { + println!("=== Iris Classification LGP Example ===\n"); + + // Step 1: Configure instruction parameters + // Iris has 4 input features (sepal length/width, petal length/width) + // and 3 classes (Setosa, Versicolor, Virginica) + let instruction_parameters = InstructionGeneratorParametersBuilder::default() + .n_actions(3) // 3 classes to predict + .n_inputs(4) // 4 input features + .n_extras(2) // Extra working registers for intermediate calculations + .external_factor(1.0) // Iris features are already normalized-ish + .build() + .expect("Failed to build instruction parameters"); + + // Step 2: Configure program parameters + let program_parameters = ProgramGeneratorParametersBuilder::default() + .max_instructions(50) // Allow more complex programs for classification + .instruction_generator_parameters(instruction_parameters) + .build() + .expect("Failed to build program parameters"); + + // Step 3: Configure hyperparameters + let parameters = HyperParametersBuilder::::default() + .program_parameters(program_parameters) + .population_size(100) // Larger population for classification + .n_generations(50) // More generations for convergence + .n_trials(1) // Single pass through dataset per evaluation + .mutation_percent(0.5) + .crossover_percent(0.5) + .gap(0.5) + .default_fitness(0.0) // Minimum accuracy + .build() + .expect("Failed to build hyperparameters"); + + println!("Configuration:"); + println!(" Population size: {}", parameters.population_size); + println!(" Generations: {}", parameters.n_generations); + println!( + " Max instructions: {}", + parameters.program_parameters.max_instructions + ); + println!(); + + // Step 4: Run evolution + println!("Starting evolution...\n"); + println!("(Note: Iris dataset is downloaded on first run)\n"); + + let populations: Vec<_> = parameters + .build_engine() + .take(parameters.n_generations) + .collect_vec(); + + // Step 5: Report results (every 10 generations) + println!("Generation | Best Accuracy | Median Accuracy"); + println!("-----------+---------------+----------------"); + + for (gen, population) in populations.iter().enumerate() { + if (gen + 1) % 10 == 0 || gen == 0 || gen == populations.len() - 1 { + let best = StatusEngine::get_fitness(population.first().unwrap()); + let median = StatusEngine::get_fitness(&population[population.len() / 2]); + + // Convert to percentage (Iris has 150 samples) + let best_pct = (best / 150.0) * 100.0; + let median_pct = (median / 150.0) * 100.0; + + println!( + "{:>10} | {:>12.1}% | {:>14.1}%", + gen + 1, + best_pct, + median_pct + ); + } + } + + // Final summary + let final_population = populations.last().unwrap(); + let best_program = final_population.first().unwrap(); + let best_fitness = StatusEngine::get_fitness(best_program); + let accuracy = (best_fitness / 150.0) * 100.0; + + println!(); + println!("=== Evolution Complete ==="); + println!( + "Best accuracy: {:.1}% ({:.0}/150 correct)", + accuracy, best_fitness + ); + + if accuracy >= 95.0 { + println!("Excellent! Near-perfect classification achieved."); + } else if accuracy >= 90.0 { + println!("Great result! Strong classification performance."); + } else if accuracy >= 80.0 { + println!("Good result. Consider more generations for improvement."); + } else { + println!("The classifier needs more evolution or parameter tuning."); + } +}