1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
use std::collections::HashMap;
use log::debug;
use rand::prelude::IndexedRandom;
use rand::RngCore;
use crate::algorithms::nsga3::{
MIN_DISTANCE, NORMALISED_OBJECTIVE_KEY, REF_POINT, REF_POINT_INDEX,
};
use crate::core::{DataValue, Individual, OError, Population};
use crate::utils::{argmin_by, index_of};
/// This implements "Algorithm 4" in the paper which adds individuals from the last front to the new
/// population based on the reference point association and minimum distance.
pub(crate) struct Niching<'a> {
/// The population being created at the current evolution with the new selected individuals.
/// This is `$P_{t+1}$` from the paper and is populated with individuals from [`self.potential_individuals`].
selected_individuals: &'a mut Population,
/// Individuals from the last front $F_l$ to add to [`self.selected_individuals`] based on reference
/// point association and minimum distance.
potential_individuals: &'a mut Vec<Individual>,
/// The number of individuals to add to [`self.selected_individuals`] to complete the evolution.
missing_item_count: usize,
/// The map mapping the reference point index to the number of individuals already associated in
/// [`self.selected_individuals`]
rho_j: &'a mut HashMap<usize, usize>,
/// The random number generator
rng: &'a mut Box<dyn RngCore>,
}
impl<'a> Niching<'a> {
/// Niching algorithm.
///
/// # Arguments
///
/// * `selected_individuals`: The population P_{t+1} without the last front. This will be
/// populated with individuals from `potential_individuals`.
/// * `potential_individuals`: The potential individuals from the last front.
/// * `number_of_individuals_to_add`: The number of individuals to add to `selected_individuals`
/// from `potential_individuals`.
/// * `rho_j`: The map containing the reference point indexes as keys and the number of associated
/// points from P_{t+1}.
/// * `rng`: The random number generator.
///
/// returns: `Result<Niching, OError>`
pub fn new(
selected_individuals: &'a mut Population,
potential_individuals: &'a mut Vec<Individual>,
number_of_individuals_to_add: usize,
rho_j: &'a mut HashMap<usize, usize>,
rng: &'a mut Box<dyn RngCore>,
) -> Result<Self, OError> {
let name = "NSGA3-Niching".to_string();
if rho_j.is_empty() {
return Err(OError::AlgorithmRun(
name,
"The rho_j set is empty".to_string(),
));
}
if potential_individuals.len() < number_of_individuals_to_add {
return Err(OError::AlgorithmRun(
name,
format!("The number of individuals to add ({number_of_individuals_to_add}) is larger than the number of potential individuals ({})", potential_individuals.len()),
));
}
Ok(Self {
selected_individuals,
potential_individuals,
missing_item_count: number_of_individuals_to_add,
rho_j,
rng,
})
}
/// Add new individuals to the population. This updates [`self.new_population`] by draining
/// items from [`self.potential_individuals`]. Reference points not associated with any point in
/// [`self.potential_individuals`] and excluded from the current evolution are removed from
/// [`self.rho_j`].
///
/// return: `Result<(), OError>`
pub fn calculate(&mut self) -> Result<(), OError> {
let mut k = 1;
let name = "NSGA3-Niching".to_string();
debug!(
"Number of individuals to choose {}",
self.missing_item_count
);
while k <= self.missing_item_count {
debug!(
"Adding point {k}/{} to new population",
self.missing_item_count
);
// step 3 - select the reference point with the minimum rho_j counter. Reference points
// that have no association with individuals in F_l (Z_r = Z_r/{j_hat}, step 15) are
// excluded by removing them from rho_j later
let min_rho_j = *self
.rho_j
.iter()
.min_by(|(_, v1), (_, v2)| v1.cmp(v2))
.unwrap()
.1;
debug!("min_rho_j = {min_rho_j}");
// step 3 - collect all reference point indexes j with minimum rho_j
let j_min_set: Vec<usize> = self
.rho_j
.iter()
.filter_map(|(ref_index, ref_counter)| {
if *ref_counter == min_rho_j {
Some(*ref_index)
} else {
None
}
})
.collect();
// step 4 - get reference point with minimum association counter
let j_hat = match j_min_set.len() {
0 => return Err(OError::AlgorithmRun(name, "Empty j_min_set".to_string())),
1 => *j_min_set.first().unwrap(),
// select point randomly when set size is > 1
_ => *j_min_set.choose(self.rng.as_mut()).unwrap(),
};
debug!("Selected reference point j_hat=#{j_hat}");
// step 5 - individuals in F_j linked to current reference point index j_hat
let i_j: Vec<&Individual> = self
.potential_individuals
.iter()
.filter(|ind| ind.get_data(REF_POINT_INDEX).unwrap() == DataValue::USize(j_hat))
.collect();
debug!(
"Found {} potential individuals associated with it",
i_j.len()
);
// step 6 - select points from front F_l
if !i_j.is_empty() {
// let (_, ind) = argmin_by(&i_j, |(_, ind)| {
// ind.get_data(MIN_DISTANCE).unwrap().as_real().unwrap()
// })
// .unwrap();
// let new_ind_index = index_of(self.potential_individuals, ind);
// let method = "min_distance";
let (new_ind_index, method) = if min_rho_j == 0 {
// step 7 - no point from P_{t+1} is associated with the selected reference point
// j_hat. There's at least one point from F_l that can be linked (I_j is not empty)
// step 8 - find individual in F_l with the shortest distance
let (_, ind) = argmin_by(&i_j, |(_, ind)| {
ind.get_data(MIN_DISTANCE).unwrap().as_real().unwrap()
})
.unwrap();
let new_ind_index = index_of(self.potential_individuals, ind);
(new_ind_index, "min_distance")
} else {
// step 10 - choose random point from F_l
let ind = i_j.choose(self.rng).unwrap();
let new_ind_index = index_of(self.potential_individuals, ind);
(new_ind_index, "random")
};
// step 12a - mark reference point as associated to a new F_l's individual
*self.rho_j.get_mut(&j_hat).unwrap() += 1;
// step 12b - Add new individual and remove it from F_l
match new_ind_index {
None => {
return Err(OError::AlgorithmRun(
name,
"Cannot find individual's index".to_string(),
))
}
Some(index) => {
let ind = self.potential_individuals.remove(index);
debug!(
"Added individual #{index} {:?} to population ({method}) - reference point #{j_hat} {:?}",
ind.get_data(NORMALISED_OBJECTIVE_KEY)?,
ind.get_data(REF_POINT),
);
self.selected_individuals.add_individual(ind);
// step 13
k += 1;
}
}
} else {
// step 15 - no point in F_l is associated with reference point indexed by j_hat.
// j_hat will have no linked individual at this evolution. Exclude it.
debug!("Excluding ref point index {j_hat} - no candidates associated with it");
self.rho_j.remove(&j_hat);
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use crate::algorithms::nsga3::niching::Niching;
use crate::algorithms::nsga3::{MIN_DISTANCE, REF_POINT_INDEX};
use crate::core::test_utils::individuals_from_obj_values_dummy;
use crate::core::utils::get_rng;
use crate::core::{DataValue, Individual, ObjectiveDirection, Population};
#[test]
/// Check niching that (1) adds point with min distance when there reference point is not
/// already associated with an objective; (2) reference points not linked to potential individuals
/// are excluded from the algorithm.
fn test_niching_rho0() {
// create dummy population with 4 individuals
let dummy_objectives = vec![vec![0.0, 0.0]; 2];
let mut individuals = individuals_from_obj_values_dummy(
&dummy_objectives,
&[ObjectiveDirection::Minimise; 2],
None,
);
let problem = individuals[0].problem().clone();
let mut rho_j: HashMap<usize, usize> = HashMap::new();
// link 2 individuals to 2 out of 4 reference points
individuals[0].set_data(REF_POINT_INDEX, DataValue::USize(0));
individuals[0].set_data(MIN_DISTANCE, DataValue::Real(0.1));
rho_j.entry(0).or_insert(1);
individuals[1].set_data(REF_POINT_INDEX, DataValue::USize(1));
individuals[1].set_data(MIN_DISTANCE, DataValue::Real(0.2));
rho_j.entry(1).or_insert(1);
let mut pop = Population::new_with(individuals);
// potential individuals - both are linked to ref_point #3 but ind_3 is closer
let mut ind_3 = Individual::new(problem.clone());
ind_3.set_data(REF_POINT_INDEX, DataValue::USize(2));
ind_3.set_data(MIN_DISTANCE, DataValue::Real(0.4));
let mut ind_4 = Individual::new(problem);
ind_4.set_data(REF_POINT_INDEX, DataValue::USize(2));
ind_4.set_data(MIN_DISTANCE, DataValue::Real(0.9));
// counter is 0 for all other ref_points
rho_j.entry(2).or_insert(0);
rho_j.entry(3).or_insert(0);
let mut potential_individuals = vec![ind_3, ind_4];
let selected_ind = potential_individuals[0].clone();
let mut rng = get_rng(Some(1));
let mut n = Niching::new(
&mut pop,
&mut potential_individuals,
1,
&mut rho_j,
&mut rng,
)
.unwrap();
n.calculate().unwrap();
// let excluded = n.excluded_ref_point_index;
// counter for ref_point #3 has increased
assert_eq!(rho_j[&2_usize], 1_usize);
// 3rd individual is added to the population
assert_eq!(pop.len(), 3);
assert_eq!(pop.individual(2).unwrap(), &selected_ind);
// 4th reference point should be excluded because has no association
// NOTE: do not check this due to random ref_point selection (if point 4 is picked first,
// this is excluded otherwise it will not).
// assert!(
// !rho_j.contains(&3_usize),
// "excluded = {:?} does not contain ref_point #4",
// rho_j
// );
}
#[test]
/// Check niching that adds point with min distance when there reference point is already
/// associated with another objective.
fn test_niching_rho1() {
// create dummy population with 4 individuals
let dummy_objectives = vec![vec![0.0, 0.0]; 2];
let mut individuals = individuals_from_obj_values_dummy(
&dummy_objectives,
&[ObjectiveDirection::Minimise; 2],
None,
);
let problem = individuals[0].problem().clone();
let mut rho_j: HashMap<usize, usize> = HashMap::new();
// link 2 individuals to 2 out of 4 reference points
individuals[0].set_data(REF_POINT_INDEX, DataValue::USize(0));
individuals[0].set_data(MIN_DISTANCE, DataValue::Real(0.1));
rho_j.entry(0).or_insert(1);
individuals[1].set_data(REF_POINT_INDEX, DataValue::USize(1));
individuals[1].set_data(MIN_DISTANCE, DataValue::Real(0.2));
rho_j.entry(1).or_insert(1);
let mut pop = Population::new_with(individuals);
// potential individuals - both are linked to ref_point #2 but ind_4 is closer
let mut ind_3 = Individual::new(problem.clone());
ind_3.set_data(REF_POINT_INDEX, DataValue::USize(1));
ind_3.set_data(MIN_DISTANCE, DataValue::Real(99.0));
let mut ind_4 = Individual::new(problem);
ind_4.set_data(REF_POINT_INDEX, DataValue::USize(1));
ind_4.set_data(MIN_DISTANCE, DataValue::Real(0.9));
// counter is 0 for all other ref_points
rho_j.entry(2).or_insert(0);
let mut potential_individuals = vec![ind_3, ind_4];
// let selected_ind = potential_individuals[1].clone();
let mut rng = get_rng(Some(1));
let mut n = Niching::new(
&mut pop,
&mut potential_individuals,
1,
&mut rho_j,
&mut rng,
)
.unwrap();
n.calculate().unwrap();
// counter for ref_point #3 has increased
assert_eq!(rho_j[&1_usize], 2_usize);
// 3rd individual is added to the population
assert_eq!(pop.len(), 3);
// NOTE: do not check this due to rng when a ref point has already one individual assigned
// assert_eq!(pop.individual(2).unwrap(), &selected_ind);
}
}