optirustic/operators/mutation.rs
1use rand::{Rng, RngCore};
2use serde::{Deserialize, Serialize};
3
4#[cfg(feature = "python")]
5use pyo3::prelude::*;
6
7use crate::core::{Individual, OError, Problem, VariableType, VariableValue};
8
9/// The trait to implement a mutation operator to modify the genetic material of an individual.
10pub trait Mutation {
11 /// Mutate a population individual.
12 ///
13 /// # Arguments
14 ///
15 /// * `individual`: The individual to mutate.
16 /// * `rng`: The random number generator.
17 ///
18 /// returns: `Result<Individual, OError>`. The mutated individual.
19 fn mutate_offspring(
20 &self,
21 individual: &Individual,
22 rng: &mut dyn RngCore,
23 ) -> Result<Individual, OError>;
24}
25
26/// Input arguments for [`PolynomialMutation`].
27#[cfg_attr(feature = "python", pyclass(get_all))]
28#[derive(Serialize, Deserialize, Clone)]
29pub struct PolynomialMutationArgs {
30 /// A user-defined parameter to control the mutation. This is eta_m in the paper, and it is
31 /// suggested its value to be in the [20, 100] range.
32 pub index_parameter: f64,
33 /// The probability of mutating a parent variable.
34 pub variable_probability: f64,
35}
36
37impl PolynomialMutationArgs {
38 /// Initialise the Polynomial mutation (PM) operator with the default parameters. With a
39 /// distribution index or index parameter of `20` and variable probability equal `1` divided by
40 /// the number of real variables in the problem (i.e. each variable will have the same
41 /// probability of being mutated).
42 ///
43 /// # Arguments
44 ///
45 /// * `problem`: THe problem being solved.
46 ///
47 /// returns: `Self`
48 pub fn default(problem: &Problem) -> Self {
49 let num_real_vars = problem
50 .variables()
51 .iter()
52 .filter(|(_, v)| v.is_real())
53 .count() as f64;
54 let variable_probability = 1.0 / num_real_vars;
55 Self {
56 index_parameter: 20.0,
57 variable_probability,
58 }
59 }
60}
61
62#[cfg(feature = "python")]
63#[pymethods]
64impl PolynomialMutationArgs {
65 #[new]
66 #[pyo3(signature = (variable_probability, index_parameter=None))]
67 fn new(variable_probability: f64, index_parameter: Option<f64>) -> Self {
68 PolynomialMutationArgs {
69 index_parameter: index_parameter.unwrap_or(20.0),
70 variable_probability,
71 }
72 }
73
74 pub fn __repr__(&self) -> PyResult<String> {
75 Ok(format!(
76 "PolynomialMutationArgs(index_parameter={}, variable_probability={})",
77 self.index_parameter, self.variable_probability
78 ))
79 }
80
81 fn __str__(&self) -> String {
82 self.__repr__().unwrap()
83 }
84}
85
86/// The Polynomial mutation (PM) operator.
87///
88/// Adapted from [Deb & Deb (2014)](https://dl.acm.org/doi/10.1504/IJAISC.2014.059280), full
89/// text available at <https://www.egr.msu.edu/~kdeb/papers/k2012016.pdf>.
90///
91/// # Integer support
92/// Since the original method does not provide support for integer variables, this has been added by
93/// using the approach proposed in:
94/// > Deep, Kusum & Singh, Krishna & Kansal, M. & Mohan, Chander. (2009). A real coded genetic
95/// > algorithm for solving integer and mixed integer optimization problems. Applied Mathematics
96/// > and Computation. 212. 505-518. 10.1016/j.amc.2009.02.044.
97///
98/// See the truncation procedure in section 2.4 in the [full text](https://www.researchgate.net/publication/220557819_A_real_coded_genetic_algorithm_for_solving_integer_and_mixed_integer_optimization_problems),
99/// where a probability of `0.5` is applied to ensure randomness in the integer crossover.
100///
101/// # Example
102///
103/// ```
104/// use std::error::Error;
105/// use optirustic::core::{BoundedNumber, Individual, Problem, VariableType, VariableValue,
106/// Objective, Constraint, ObjectiveDirection, RelationalOperator, EvaluationResult, Evaluator};
107/// use optirustic::operators::{Crossover, PolynomialMutationArgs, PolynomialMutation, Mutation};
108/// use std::sync::Arc;
109/// use rand_chacha::ChaCha8Rng;
110/// use rand::SeedableRng;
111///
112/// fn main() -> Result<(), Box<dyn Error>> {
113/// // create a new one-variable
114/// let objectives = vec![Objective::new("obj1", ObjectiveDirection::Minimise)];
115/// let variables = vec![
116/// VariableType::Real(BoundedNumber::new("var1", 0.0, 1000.0)?),
117/// VariableType::Integer(BoundedNumber::new("var2", -1, 1)?)
118/// ];
119///
120/// // dummy evaluator function
121/// #[derive(Debug)]
122/// struct UserEvaluator;
123/// impl Evaluator for UserEvaluator {
124/// fn evaluate(&self, _: &Individual) -> Result<EvaluationResult, Box<dyn Error>> {
125/// Ok(EvaluationResult {
126/// constraints: Default::default(),
127/// objectives: Default::default(),
128/// })
129/// }
130/// }
131/// let problem = Arc::new(Problem::new(objectives, variables, None, Box::new(UserEvaluator))?);
132///
133/// // add new individuals
134/// let mut a = Individual::new(problem.clone());
135/// a.update_variable("var1", VariableValue::Real(0.2))?;
136/// a.update_variable("var2", VariableValue::Integer(1))?;
137///
138/// // crossover
139/// let parameters = PolynomialMutationArgs {
140/// index_parameter: 1.0 ,
141/// variable_probability: 1.0,
142/// };
143/// let pm = PolynomialMutation::new(parameters)?;
144/// let mut rng = ChaCha8Rng::from_seed(Default::default());
145/// let out = pm.mutate_offspring(&a, &mut rng)?;
146/// println!("{}", out);
147/// Ok(())
148/// }
149/// ```
150pub struct PolynomialMutation {
151 /// The user-defined parameter to control the mutation.
152 index_parameter: f64,
153 /// The probability of mutating a parent variable.
154 variable_probability: f64,
155}
156
157impl PolynomialMutation {
158 /// Initialise the Polynomial mutation (PM) operator. This returns an error if the probability
159 /// is outside the [0, 1] range.
160 ///
161 /// # Arguments
162 ///
163 /// * `index_parameter`:
164 /// * `variable_probability`: The probability of mutating a parent variable.
165 ///
166 /// returns: `Result<PolynomialMutation, OError>`
167 pub fn new(args: PolynomialMutationArgs) -> Result<Self, OError> {
168 if !(0.0..=1.0).contains(&args.variable_probability) {
169 return Err(OError::MutationOperator(
170 "PolynomialMutation".to_string(),
171 format!(
172 "The variable probability {} must be a number between 0 and 1",
173 args.variable_probability
174 ),
175 ));
176 }
177 Ok(Self {
178 index_parameter: args.index_parameter,
179 variable_probability: args.variable_probability,
180 })
181 }
182
183 /// Perform the mutation of a real variable for an offspring.
184 ///
185 /// # Arguments
186 ///
187 /// * `y`: The real variable value to mutate.
188 /// * `y_lower`: The variable lower bound.
189 /// * `y_upper`: The variable lower bound.
190 /// * `rng`: The random number generator reference.
191 ///
192 /// returns: `f64`
193 fn mutate_variable(&self, y: f64, y_lower: f64, y_upper: f64, rng: &mut dyn RngCore) -> f64 {
194 let delta_y = y_upper - y_lower;
195 let prob = rng.random_range(0.0..=1.0);
196
197 // this is delta_l or delta_r
198 let delta = if prob <= 0.5 {
199 let bl = (y - y_lower) / delta_y;
200 let b =
201 2.0 * prob + (1.0 - 2.0 * prob) * f64::powf(1.0 - bl, self.index_parameter + 1.0);
202 f64::powf(b, 1.0 / (self.index_parameter + 1.0)) - 1.0
203 } else {
204 let bu = (y_upper - y) / delta_y;
205 let b = 2.0 * (1.0 - prob)
206 + 2.0 * (prob - 0.5) * f64::powf(1.0 - bu, self.index_parameter + 1.0);
207 1.0 - f64::powf(b, 1.0 / (self.index_parameter + 1.0))
208 };
209
210 // adjust the variable
211 let new_y = y + delta * delta_y;
212 f64::min(f64::max(new_y, y_lower), y_upper)
213 }
214}
215
216impl Mutation for PolynomialMutation {
217 fn mutate_offspring(
218 &self,
219 individual: &Individual,
220 rng: &mut dyn RngCore,
221 ) -> Result<Individual, OError> {
222 let mut mutated_individual = individual.clone_variables();
223 let problem = individual.problem();
224
225 // return error if variable is not a number
226 if !problem
227 .variables()
228 .iter()
229 .all(|(_, v)| v.is_real() | v.is_integer())
230 {
231 return Err(OError::CrossoverOperator(
232 "PolynomialMutation".to_string(),
233 "The PM operator only works with real or integer variables".to_string(),
234 ));
235 }
236
237 // do not apply crossover if probability is not reached
238 for (var_name, var_type) in problem.variables() {
239 if rng.random_range(0.0..=1.0) <= self.variable_probability {
240 let y = individual.get_variable_value(&var_name)?;
241 if let (VariableValue::Real(y), VariableType::Real(vt)) = (y, &var_type) {
242 let (y_lower, y_upper) = vt.bounds();
243 let new_y = self.mutate_variable(*y, y_lower, y_upper, rng);
244 mutated_individual.update_variable(&var_name, VariableValue::Real(new_y))?;
245 } else if let (VariableValue::Integer(y), VariableType::Integer(vt)) = (y, var_type)
246 {
247 let (y_lower, y_upper) = vt.bounds();
248 let new_y =
249 self.mutate_variable(*y as f64, y_lower as f64, y_upper as f64, rng);
250
251 // truncate
252 let mut new_y = new_y.trunc() as i64;
253 if rng.random_range(0.0..=1.0) < 0.5 {
254 new_y += 1;
255 // ensure the new integer is below the variable upper bound
256 new_y = new_y.min(y_upper);
257 }
258 mutated_individual.update_variable(&var_name, VariableValue::Integer(new_y))?;
259 }
260 }
261 }
262
263 Ok(mutated_individual)
264 }
265}
266
267#[cfg(test)]
268mod test {
269 use std::sync::Arc;
270
271 use crate::core::utils::{dummy_evaluator, get_rng};
272 use crate::core::{
273 BoundedNumber, Individual, Objective, ObjectiveDirection, Problem, VariableType,
274 VariableValue,
275 };
276 use crate::operators::{Mutation, PolynomialMutation, PolynomialMutationArgs};
277
278 #[test]
279 /// Test that the PM operator mutates variables
280 fn test_sbx_crossover() {
281 let objectives = vec![Objective::new("obj1", ObjectiveDirection::Minimise)];
282
283 let variables = vec![
284 VariableType::Real(BoundedNumber::new("var1", 0.0, 1000.0).unwrap()),
285 VariableType::Integer(BoundedNumber::new("var2", -10, 20).unwrap()),
286 ];
287
288 let problem =
289 Arc::new(Problem::new(objectives, variables, None, dummy_evaluator()).unwrap());
290
291 // add new individuals
292 let mut a = Individual::new(problem.clone());
293 a.update_variable("var1", VariableValue::Real(0.2)).unwrap();
294 a.update_variable("var2", VariableValue::Integer(0))
295 .unwrap();
296 let mut b = Individual::new(problem.clone());
297 b.update_variable("var1", VariableValue::Real(0.8)).unwrap();
298 b.update_variable("var2", VariableValue::Integer(3))
299 .unwrap();
300
301 let args = PolynomialMutationArgs {
302 // ensure different variable value (with integers)
303 index_parameter: 1.0,
304 // always force mutation
305 variable_probability: 1.0,
306 };
307 let pm = PolynomialMutation::new(args).unwrap();
308 let mut rng = get_rng(Some(1));
309 let mutated_offspring = pm.mutate_offspring(&a, &mut rng).unwrap();
310
311 // Mutation always performed because variable_probability is 1
312 assert_ne!(
313 *mutated_offspring.get_variable_value("var1").unwrap(),
314 VariableValue::Real(0.2)
315 );
316 assert_ne!(
317 *mutated_offspring.get_variable_value("var2").unwrap(),
318 VariableValue::Integer(0)
319 );
320 }
321}