Skip to main content

lgp/core/
config.rs

1#[cfg(feature = "gym")]
2use crate::core::engines::reset_engine::{Reset, ResetEngine};
3use crate::core::engines::status_engine::{Status, StatusEngine};
4#[cfg(feature = "gym")]
5use crate::problems::gym::{GymRsEngine, GymRsQEngine};
6use crate::{core::engines::core_engine::HyperParameters, problems::iris::IrisEngine};
7use clap::{Parser, Subcommand, ValueEnum};
8use config::{Config, Environment, File};
9#[cfg(feature = "gym")]
10use gymnasia::envs::classical_control::{cartpole::CartPoleEnv, mountain_car::MountainCarEnv};
11use serde::{Deserialize, Serialize};
12
13use super::engines::core_engine::Core;
14use super::instruction::InstructionGeneratorParameters;
15use super::program::ProgramGeneratorParameters;
16#[cfg(feature = "gym")]
17use crate::extensions::q_learning::{QConsts, QProgramGeneratorParameters};
18
19/// Environment types supported by the framework
20#[derive(Debug, Clone, Copy, ValueEnum, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub enum EnvironmentType {
23    /// CartPole with pure Linear Genetic Programming
24    #[cfg(feature = "gym")]
25    CartPoleLgp,
26    /// CartPole with LGP + Q-Learning
27    #[cfg(feature = "gym")]
28    CartPoleQ,
29    /// MountainCar with pure Linear Genetic Programming
30    #[cfg(feature = "gym")]
31    MountainCarLgp,
32    /// MountainCar with LGP + Q-Learning
33    #[cfg(feature = "gym")]
34    MountainCarQ,
35    /// Iris classification with Linear Genetic Programming
36    IrisLgp,
37}
38
39/// Experiment parameters for running LGP experiments
40#[derive(Debug, Parser, Serialize, Deserialize)]
41pub struct ExperimentParams {
42    /// Environment to run
43    #[arg(value_enum)]
44    pub env: EnvironmentType,
45
46    // === GA Parameters ===
47    /// Number of individuals per generation
48    #[arg(long, default_value = "100")]
49    pub population_size: usize,
50
51    /// Number of generations to evolve
52    #[arg(long, default_value = "100")]
53    pub n_generations: usize,
54
55    /// Proportion of offspring created by mutation
56    #[arg(long, default_value = "0.5")]
57    pub mutation_percent: f64,
58
59    /// Proportion of offspring created by crossover
60    #[arg(long, default_value = "0.5")]
61    pub crossover_percent: f64,
62
63    /// Survival rate (fraction of population that survives)
64    #[arg(long, default_value = "0.5")]
65    pub gap: f64,
66
67    /// Number of trial episodes for fitness evaluation
68    #[arg(long, default_value = "100")]
69    pub n_trials: usize,
70
71    /// Random seed for reproducibility
72    #[arg(long)]
73    pub seed: Option<u64>,
74
75    /// Number of threads for parallel evaluation (defaults to all available cores)
76    #[arg(long)]
77    pub n_threads: Option<usize>,
78
79    /// Fitness assigned to invalid programs (overridden per environment if not set)
80    #[arg(long)]
81    pub default_fitness: Option<f64>,
82
83    // === Program Parameters ===
84    /// Maximum instructions per program
85    #[arg(long, default_value = "12")]
86    pub max_instructions: usize,
87
88    /// Number of extra working registers
89    #[arg(long, default_value = "1")]
90    pub n_extras: usize,
91
92    /// Scaling factor for external inputs
93    #[arg(long, default_value = "10.0")]
94    pub external_factor: f64,
95
96    // === Q-Learning Parameters (only for Q variants) ===
97    /// Learning rate (Q-Learning only)
98    #[arg(long, default_value = "0.1")]
99    pub alpha: f64,
100
101    /// Discount factor (Q-Learning only)
102    #[arg(long, default_value = "0.9")]
103    pub gamma: f64,
104
105    /// Exploration rate (Q-Learning only)
106    #[arg(long, default_value = "0.05")]
107    pub epsilon: f64,
108
109    /// Learning rate decay per trial (Q-Learning only)
110    #[arg(long, default_value = "0.01")]
111    pub alpha_decay: f64,
112
113    /// Exploration rate decay per trial (Q-Learning only)
114    #[arg(long, default_value = "0.001")]
115    pub epsilon_decay: f64,
116}
117
118/// CLI structure for the LGP framework
119#[derive(Parser)]
120#[command(
121    name = "lgp",
122    author,
123    version,
124    about = "Linear Genetic Programming Framework"
125)]
126pub struct Cli {
127    #[command(subcommand)]
128    pub command: Commands,
129}
130
131/// Available CLI commands
132#[derive(Subcommand)]
133pub enum Commands {
134    /// Run an experiment with the specified environment
135    Experiment(ExperimentParams),
136}
137
138// Generate a macro which takes hyperparameters, builds the necessary engine and runs it,
139// outputting the best score for each generation
140macro_rules! run_experiment {
141    ($hyperparameters:ident) => {
142        for population in $hyperparameters
143            .build_engine()
144            .take($hyperparameters.n_generations)
145        {
146            println!("{}", StatusEngine::get_fitness(population.first().unwrap()));
147        }
148        println!("{}", serde_json::to_string(&$hyperparameters).unwrap());
149    };
150}
151
152impl ExperimentParams {
153    /// Get the number of inputs for the environment
154    fn n_inputs(&self) -> usize {
155        match self.env {
156            #[cfg(feature = "gym")]
157            EnvironmentType::CartPoleLgp | EnvironmentType::CartPoleQ => 4,
158            #[cfg(feature = "gym")]
159            EnvironmentType::MountainCarLgp | EnvironmentType::MountainCarQ => 2,
160            EnvironmentType::IrisLgp => 4,
161        }
162    }
163
164    /// Get the number of actions for the environment
165    fn n_actions(&self) -> usize {
166        match self.env {
167            #[cfg(feature = "gym")]
168            EnvironmentType::CartPoleLgp | EnvironmentType::CartPoleQ => 2,
169            #[cfg(feature = "gym")]
170            EnvironmentType::MountainCarLgp | EnvironmentType::MountainCarQ => 3,
171            EnvironmentType::IrisLgp => 3,
172        }
173    }
174
175    /// Get the default fitness for the environment
176    fn env_default_fitness(&self) -> f64 {
177        match self.env {
178            #[cfg(feature = "gym")]
179            EnvironmentType::CartPoleLgp | EnvironmentType::CartPoleQ => 500.0,
180            #[cfg(feature = "gym")]
181            EnvironmentType::MountainCarLgp | EnvironmentType::MountainCarQ => -200.0,
182            EnvironmentType::IrisLgp => 0.0,
183        }
184    }
185
186    /// Build instruction generator parameters
187    fn build_instruction_params(&self) -> InstructionGeneratorParameters {
188        InstructionGeneratorParameters {
189            n_extras: self.n_extras,
190            external_factor: self.external_factor,
191            n_actions: self.n_actions(),
192            n_inputs: self.n_inputs(),
193        }
194    }
195
196    /// Build program generator parameters
197    fn build_program_params(&self) -> ProgramGeneratorParameters {
198        ProgramGeneratorParameters {
199            max_instructions: self.max_instructions,
200            instruction_generator_parameters: self.build_instruction_params(),
201        }
202    }
203
204    /// Build Q-Learning constants
205    #[cfg(feature = "gym")]
206    fn build_q_consts(&self) -> QConsts {
207        QConsts::new(
208            self.alpha,
209            self.gamma,
210            self.epsilon,
211            self.alpha_decay,
212            self.epsilon_decay,
213        )
214    }
215
216    /// Build Q-Program generator parameters
217    #[cfg(feature = "gym")]
218    fn build_q_program_params(&self) -> QProgramGeneratorParameters {
219        QProgramGeneratorParameters {
220            program_parameters: self.build_program_params(),
221            consts: self.build_q_consts(),
222        }
223    }
224
225    /// Run the experiment based on the selected environment
226    pub fn run(&self) {
227        let default_fitness = self
228            .default_fitness
229            .unwrap_or_else(|| self.env_default_fitness());
230
231        match self.env {
232            #[cfg(feature = "gym")]
233            EnvironmentType::CartPoleLgp => {
234                let hyperparameters: HyperParameters<GymRsEngine<CartPoleEnv>> = HyperParameters {
235                    default_fitness,
236                    population_size: self.population_size,
237                    gap: self.gap,
238                    mutation_percent: self.mutation_percent,
239                    crossover_percent: self.crossover_percent,
240                    n_generations: self.n_generations,
241                    n_trials: self.n_trials,
242                    seed: self.seed,
243                    n_threads: self.n_threads,
244                    program_parameters: self.build_program_params(),
245                };
246                run_experiment!(hyperparameters);
247            }
248            #[cfg(feature = "gym")]
249            EnvironmentType::CartPoleQ => {
250                let mut hyperparameters: HyperParameters<GymRsQEngine<CartPoleEnv>> =
251                    HyperParameters {
252                        default_fitness,
253                        population_size: self.population_size,
254                        gap: self.gap,
255                        mutation_percent: self.mutation_percent,
256                        crossover_percent: self.crossover_percent,
257                        n_generations: self.n_generations,
258                        n_trials: self.n_trials,
259                        seed: self.seed,
260                        n_threads: self.n_threads,
261                        program_parameters: self.build_q_program_params(),
262                    };
263                ResetEngine::reset(&mut hyperparameters.program_parameters.consts);
264                run_experiment!(hyperparameters);
265            }
266            #[cfg(feature = "gym")]
267            EnvironmentType::MountainCarLgp => {
268                let hyperparameters: HyperParameters<GymRsEngine<MountainCarEnv>> =
269                    HyperParameters {
270                        default_fitness,
271                        population_size: self.population_size,
272                        gap: self.gap,
273                        mutation_percent: self.mutation_percent,
274                        crossover_percent: self.crossover_percent,
275                        n_generations: self.n_generations,
276                        n_trials: self.n_trials,
277                        seed: self.seed,
278                        n_threads: self.n_threads,
279                        program_parameters: self.build_program_params(),
280                    };
281                run_experiment!(hyperparameters);
282            }
283            #[cfg(feature = "gym")]
284            EnvironmentType::MountainCarQ => {
285                let mut hyperparameters: HyperParameters<GymRsQEngine<MountainCarEnv>> =
286                    HyperParameters {
287                        default_fitness,
288                        population_size: self.population_size,
289                        gap: self.gap,
290                        mutation_percent: self.mutation_percent,
291                        crossover_percent: self.crossover_percent,
292                        n_generations: self.n_generations,
293                        n_trials: self.n_trials,
294                        seed: self.seed,
295                        n_threads: self.n_threads,
296                        program_parameters: self.build_q_program_params(),
297                    };
298                ResetEngine::reset(&mut hyperparameters.program_parameters.consts);
299                run_experiment!(hyperparameters);
300            }
301            EnvironmentType::IrisLgp => {
302                let hyperparameters: HyperParameters<IrisEngine> = HyperParameters {
303                    default_fitness,
304                    population_size: self.population_size,
305                    gap: self.gap,
306                    mutation_percent: self.mutation_percent,
307                    crossover_percent: self.crossover_percent,
308                    n_generations: self.n_generations,
309                    n_trials: self.n_trials,
310                    seed: self.seed,
311                    n_threads: self.n_threads,
312                    program_parameters: self.build_program_params(),
313                };
314                run_experiment!(hyperparameters);
315            }
316        }
317    }
318}
319
320pub fn load_hyper_parameters<C>(
321    filename: &str,
322) -> Result<HyperParameters<C>, Box<dyn std::error::Error>>
323where
324    C: Core,
325{
326    let settings = Config::builder()
327        .add_source(File::with_name(filename))
328        .add_source(Environment::default())
329        .build()?;
330
331    let parameters: HyperParameters<C> = settings.try_deserialize()?;
332    Ok(parameters)
333}