Skip to main content

graphmind_optimization/algorithms/
motlbo.rs

1use crate::common::{
2    MultiObjectiveIndividual, MultiObjectiveProblem, MultiObjectiveResult, SolverConfig,
3};
4use ndarray::Array1;
5use rand::prelude::*;
6
7pub struct MOTLBOSolver {
8    pub config: SolverConfig,
9}
10
11impl MOTLBOSolver {
12    pub fn new(config: SolverConfig) -> Self {
13        Self { config }
14    }
15
16    pub fn solve<P: MultiObjectiveProblem>(&self, problem: &P) -> MultiObjectiveResult {
17        let mut rng = thread_rng();
18        let dim = problem.dim();
19        let (lower, upper) = problem.bounds();
20        let pop_size = self.config.population_size;
21
22        // 1. Initialize Population
23        let mut population: Vec<MultiObjectiveIndividual> = (0..pop_size)
24            .map(|_| {
25                let mut vars = Array1::zeros(dim);
26                for i in 0..dim {
27                    vars[i] = rng.gen_range(lower[i]..upper[i]);
28                }
29                let fitness = problem.objectives(&vars);
30                MultiObjectiveIndividual::new(vars, fitness)
31            })
32            .collect();
33
34        self.evaluate_population(&mut population);
35
36        let mut history = Vec::with_capacity(self.config.max_iterations);
37
38        for _iter in 0..self.config.max_iterations {
39            let mut offspring = Vec::with_capacity(pop_size * 2);
40
41            // Teacher Phase
42            let teacher_idx = self.select_teacher(&population);
43            let teacher_vars = population[teacher_idx].variables.clone();
44            let mean_vars = self.calculate_mean(&population, dim);
45
46            for ind in &population {
47                let mut local_rng = thread_rng();
48                let tf: f64 = local_rng.gen_range(1..3) as f64;
49                let mut new_vars = Array1::zeros(dim);
50                for j in 0..dim {
51                    let r: f64 = local_rng.gen();
52                    new_vars[j] = (ind.variables[j] + r * (teacher_vars[j] - tf * mean_vars[j]))
53                        .clamp(lower[j], upper[j]);
54                }
55                offspring.push(MultiObjectiveIndividual::new(
56                    new_vars.clone(),
57                    problem.objectives(&new_vars),
58                ));
59            }
60
61            // Learner Phase
62            for i in 0..pop_size {
63                let mut local_rng = thread_rng();
64                let mut j;
65                loop {
66                    j = local_rng.gen_range(0..pop_size);
67                    if j != i {
68                        break;
69                    }
70                }
71
72                let mut new_vars = Array1::zeros(dim);
73                let dominates_i_j = self.dominates(&population[i].fitness, &population[j].fitness);
74                let dominates_j_i = self.dominates(&population[j].fitness, &population[i].fitness);
75
76                for k in 0..dim {
77                    let r: f64 = local_rng.gen();
78                    if dominates_i_j {
79                        new_vars[k] = (population[i].variables[k]
80                            + r * (population[i].variables[k] - population[j].variables[k]))
81                            .clamp(lower[k], upper[k]);
82                    } else if dominates_j_i {
83                        new_vars[k] = (population[i].variables[k]
84                            + r * (population[j].variables[k] - population[i].variables[k]))
85                            .clamp(lower[k], upper[k]);
86                    } else {
87                        // Random movement if non-dominated
88                        new_vars[k] = population[i].variables[k];
89                    }
90                }
91                offspring.push(MultiObjectiveIndividual::new(
92                    new_vars.clone(),
93                    problem.objectives(&new_vars),
94                ));
95            }
96
97            // Merge and Rank
98            let mut combined = population;
99            combined.extend(offspring);
100            self.evaluate_population(&mut combined);
101
102            // Select Best N
103            combined.sort_by(|a, b| {
104                if a.rank != b.rank {
105                    a.rank.cmp(&b.rank)
106                } else {
107                    b.crowding_distance
108                        .partial_cmp(&a.crowding_distance)
109                        .unwrap()
110                }
111            });
112
113            combined.truncate(pop_size);
114            population = combined;
115
116            history.push(population[0].fitness[0]);
117        }
118
119        MultiObjectiveResult {
120            pareto_front: population.into_iter().filter(|ind| ind.rank == 0).collect(),
121            history,
122        }
123    }
124
125    fn select_teacher(&self, population: &[MultiObjectiveIndividual]) -> usize {
126        // Teacher is chosen from the first rank (best non-dominated front)
127        let first_rank: Vec<usize> = population
128            .iter()
129            .enumerate()
130            .filter(|(_, ind)| ind.rank == 0)
131            .map(|(i, _)| i)
132            .collect();
133
134        let mut rng = thread_rng();
135        *first_rank.choose(&mut rng).unwrap_or(&0)
136    }
137
138    fn calculate_mean(&self, population: &[MultiObjectiveIndividual], dim: usize) -> Array1<f64> {
139        let mut mean = Array1::zeros(dim);
140        for ind in population {
141            mean += &ind.variables;
142        }
143        mean / (population.len() as f64)
144    }
145
146    // Reuse NSGA-II sorting logic
147    fn evaluate_population(&self, population: &mut [MultiObjectiveIndividual]) {
148        self.non_dominated_sort(population);
149        let mut rank = 0;
150        loop {
151            let indices: Vec<usize> = population
152                .iter()
153                .enumerate()
154                .filter(|(_, ind)| ind.rank == rank)
155                .map(|(i, _)| i)
156                .collect();
157            if indices.is_empty() {
158                break;
159            }
160            self.calculate_crowding_distance(population, &indices);
161            rank += 1;
162        }
163    }
164
165    fn non_dominated_sort(&self, population: &mut [MultiObjectiveIndividual]) {
166        let n = population.len();
167        let mut dominance_counts = vec![0; n];
168        let mut dominated_sets = vec![Vec::new(); n];
169        let mut fronts = vec![Vec::new()];
170
171        for i in 0..n {
172            for j in 0..n {
173                if i == j {
174                    continue;
175                }
176                if self.dominates(&population[i].fitness, &population[j].fitness) {
177                    dominated_sets[i].push(j);
178                } else if self.dominates(&population[j].fitness, &population[i].fitness) {
179                    dominance_counts[i] += 1;
180                }
181            }
182            if dominance_counts[i] == 0 {
183                population[i].rank = 0;
184                fronts[0].push(i);
185            }
186        }
187
188        let mut i = 0;
189        while !fronts[i].is_empty() {
190            let mut next_front = Vec::new();
191            for &p in &fronts[i] {
192                for &q in &dominated_sets[p] {
193                    dominance_counts[q] -= 1;
194                    if dominance_counts[q] == 0 {
195                        population[q].rank = i + 1;
196                        next_front.push(q);
197                    }
198                }
199            }
200            i += 1;
201            fronts.push(next_front);
202        }
203    }
204
205    fn dominates(&self, f1: &[f64], f2: &[f64]) -> bool {
206        let mut better = false;
207        for i in 0..f1.len() {
208            if f1[i] > f2[i] {
209                return false;
210            }
211            if f1[i] < f2[i] {
212                better = true;
213            }
214        }
215        better
216    }
217
218    fn calculate_crowding_distance(
219        &self,
220        population: &mut [MultiObjectiveIndividual],
221        indices: &[usize],
222    ) {
223        let num_objectives = population[0].fitness.len();
224        for &idx in indices {
225            population[idx].crowding_distance = 0.0;
226        }
227
228        for m in 0..num_objectives {
229            let mut sorted_indices = indices.to_vec();
230            sorted_indices.sort_by(|&a, &b| {
231                population[a].fitness[m]
232                    .partial_cmp(&population[b].fitness[m])
233                    .unwrap()
234            });
235
236            let min_val = population[*sorted_indices.first().unwrap()].fitness[m];
237            let max_val = population[*sorted_indices.last().unwrap()].fitness[m];
238            let range = max_val - min_val;
239
240            population[*sorted_indices.first().unwrap()].crowding_distance = f64::INFINITY;
241            population[*sorted_indices.last().unwrap()].crowding_distance = f64::INFINITY;
242
243            if range > 1e-9 {
244                for i in 1..(sorted_indices.len() - 1) {
245                    let prev = population[sorted_indices[i - 1]].fitness[m];
246                    let next = population[sorted_indices[i + 1]].fitness[m];
247                    population[sorted_indices[i]].crowding_distance += (next - prev) / range;
248                }
249            }
250        }
251    }
252}