numrs2 0.3.3

A Rust implementation inspired by NumPy for numerical computing (NumRS2)
Documentation
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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
//! Quality metrics for multi-objective optimization.
//!
//! This module provides convergence and diversity metrics for evaluating
//! Pareto fronts in multi-objective optimization:
//!
//! ## Convergence Metrics
//! - **IGD (Inverted Generational Distance)**: Coverage of reference front
//! - **GD (Generational Distance)**: Convergence to reference front
//!
//! ## Diversity Metrics
//! - **Spacing (S)**: Uniformity of distribution
//! - **Spread (Delta)**: Extent and uniformity of spread

use crate::error::{NumRs2Error, Result};
use num_traits::Float;
use std::cmp::Ordering;

// =============================================================================
// Utility Functions
// =============================================================================

/// Calculate Euclidean distance between two points
///
/// # Arguments
///
/// * `a` - First point
/// * `b` - Second point
///
/// # Returns
///
/// Euclidean distance between a and b
pub(crate) fn euclidean_distance<T: Float + std::iter::Sum>(a: &[T], b: &[T]) -> T {
    a.iter()
        .zip(b.iter())
        .map(|(ai, bi)| (*ai - *bi) * (*ai - *bi))
        .sum::<T>()
        .sqrt()
}

/// Calculate minimum distance from a point to a Pareto front
///
/// # Arguments
///
/// * `point` - Point to measure distance from
/// * `front` - Pareto front to measure distance to
///
/// # Returns
///
/// Minimum Euclidean distance to any point in the front
fn min_distance_to_front<T: Float + std::iter::Sum>(point: &[T], front: &[Vec<T>]) -> T {
    front
        .iter()
        .map(|front_point| euclidean_distance(point, front_point))
        .fold(
            T::infinity(),
            |min_dist, dist| {
                if dist < min_dist {
                    dist
                } else {
                    min_dist
                }
            },
        )
}

// =============================================================================
// Convergence Metrics
// =============================================================================

/// Calculate Inverted Generational Distance (IGD)
///
/// IGD measures how well the obtained front covers the reference front.
/// It calculates the average distance from reference points to the obtained front.
/// Lower values indicate better convergence and coverage.
///
/// # Formula
///
/// IGD = (1/|P_ref|) * sqrt(sum(d_i^2))
///
/// where d_i is the minimum distance from reference point i to the obtained front
///
/// # Arguments
///
/// * `obtained_front` - Obtained Pareto front objective values
/// * `reference_front` - True/reference Pareto front objective values
///
/// # Returns
///
/// IGD metric value
///
/// # Errors
///
/// Returns error if:
/// - Either front is empty
/// - Points have inconsistent dimensions
///
/// # Interpretation
///
/// - IGD = 0: Perfect convergence to reference front
/// - Lower values: Better convergence and coverage
/// - Higher values: Poor convergence or incomplete coverage
/// - Typical range: [0, infinity)
///
/// # Example
///
/// ```
/// use numrs2::optimize::nsga2::calculate_igd;
///
/// let obtained = vec![
///     vec![1.0, 3.0],
///     vec![2.0, 2.0],
///     vec![3.0, 1.0],
/// ];
///
/// let reference = vec![
///     vec![1.0, 3.0],
///     vec![2.0, 2.0],
///     vec![3.0, 1.0],
/// ];
///
/// let igd = calculate_igd(&obtained, &reference).expect("IGD calculation should succeed");
/// assert!(igd >= 0.0);
/// ```
pub fn calculate_igd<T: Float + std::fmt::Display + std::iter::Sum>(
    obtained_front: &[Vec<T>],
    reference_front: &[Vec<T>],
) -> Result<T> {
    if obtained_front.is_empty() {
        return Err(NumRs2Error::ValueError(
            "Obtained front cannot be empty".to_string(),
        ));
    }

    if reference_front.is_empty() {
        return Err(NumRs2Error::ValueError(
            "Reference front cannot be empty".to_string(),
        ));
    }

    let n_obj = obtained_front[0].len();

    // Validate dimensions
    for point in obtained_front {
        if point.len() != n_obj {
            return Err(NumRs2Error::ValueError(
                "All obtained points must have the same dimension".to_string(),
            ));
        }
    }

    for point in reference_front {
        if point.len() != n_obj {
            return Err(NumRs2Error::ValueError(
                "All reference points must have the same dimension".to_string(),
            ));
        }
    }

    // Calculate sum of squared minimum distances from reference to obtained
    let sum_squared_distances: T = reference_front
        .iter()
        .map(|ref_point| {
            let min_dist = min_distance_to_front(ref_point, obtained_front);
            min_dist * min_dist
        })
        .sum();

    // Calculate IGD
    let n_ref = T::from(reference_front.len()).ok_or_else(|| {
        NumRs2Error::ConversionError("Failed to convert reference front size to Float".to_string())
    })?;

    Ok((sum_squared_distances / n_ref).sqrt())
}

/// Calculate Generational Distance (GD)
///
/// GD measures the convergence of the obtained front to the reference front.
/// It calculates the average distance from obtained points to the reference front.
/// Lower values indicate better convergence.
///
/// # Formula
///
/// GD = (1/n)^(1/p) * (sum(d_i^p))^(1/p)
///
/// where:
/// - d_i is the minimum distance from obtained point i to the reference front
/// - p is typically 2 (default)
///
/// # Arguments
///
/// * `obtained_front` - Obtained Pareto front objective values
/// * `reference_front` - True/reference Pareto front objective values
/// * `p` - Power parameter (typically 2); if None, defaults to 2
///
/// # Returns
///
/// GD metric value
///
/// # Errors
///
/// Returns error if:
/// - Either front is empty
/// - Points have inconsistent dimensions
/// - p <= 0
///
/// # Interpretation
///
/// - GD = 0: Perfect convergence to reference front
/// - Lower values: Better convergence
/// - Higher values: Poor convergence
/// - Typical range: [0, infinity)
///
/// # Example
///
/// ```
/// use numrs2::optimize::nsga2::calculate_gd;
///
/// let obtained = vec![
///     vec![1.0, 3.0],
///     vec![2.0, 2.0],
///     vec![3.0, 1.0],
/// ];
///
/// let reference = vec![
///     vec![1.0, 3.0],
///     vec![2.0, 2.0],
///     vec![3.0, 1.0],
/// ];
///
/// let gd = calculate_gd(&obtained, &reference, None).expect("GD calculation should succeed");
/// assert!(gd >= 0.0);
/// ```
pub fn calculate_gd<T: Float + std::fmt::Display + std::iter::Sum>(
    obtained_front: &[Vec<T>],
    reference_front: &[Vec<T>],
    p: Option<T>,
) -> Result<T> {
    if obtained_front.is_empty() {
        return Err(NumRs2Error::ValueError(
            "Obtained front cannot be empty".to_string(),
        ));
    }

    if reference_front.is_empty() {
        return Err(NumRs2Error::ValueError(
            "Reference front cannot be empty".to_string(),
        ));
    }

    let p_val = p.unwrap_or_else(|| T::from(2.0).expect("Default p=2.0 should convert to Float"));

    if p_val <= T::zero() {
        return Err(NumRs2Error::ValueError(
            "Power parameter p must be positive".to_string(),
        ));
    }

    let n_obj = obtained_front[0].len();

    // Validate dimensions
    for point in obtained_front {
        if point.len() != n_obj {
            return Err(NumRs2Error::ValueError(
                "All obtained points must have the same dimension".to_string(),
            ));
        }
    }

    for point in reference_front {
        if point.len() != n_obj {
            return Err(NumRs2Error::ValueError(
                "All reference points must have the same dimension".to_string(),
            ));
        }
    }

    // Calculate sum of powered minimum distances from obtained to reference
    let sum_powered_distances: T = obtained_front
        .iter()
        .map(|obtained_point| {
            let min_dist = min_distance_to_front(obtained_point, reference_front);
            min_dist.powf(p_val)
        })
        .sum();

    // Calculate GD
    let n_obtained = T::from(obtained_front.len()).ok_or_else(|| {
        NumRs2Error::ConversionError("Failed to convert obtained front size to Float".to_string())
    })?;

    Ok((sum_powered_distances / n_obtained).powf(T::one() / p_val))
}

// =============================================================================
// Diversity Metrics
// =============================================================================

/// Calculate spacing metric for Pareto front
///
/// Spacing (S) measures the uniformity of distribution in the Pareto front.
/// Lower values indicate better uniformity.
///
/// # Formula
///
/// S = sqrt(1/(n-1) * sum((d_i - d_mean)^2))
///
/// where d_i is the minimum Euclidean distance from point i to other points
///
/// # Arguments
///
/// * `front` - Pareto front objective values
///
/// # Returns
///
/// Spacing metric value
///
/// # Errors
///
/// Returns error if:
/// - Front has fewer than 2 points
/// - Points have inconsistent dimensions
///
/// # Interpretation
///
/// - S = 0: Perfectly uniform distribution
/// - Lower values: Better uniformity
/// - Higher values: Clustered or uneven distribution
///
/// # Example
///
/// ```
/// use numrs2::optimize::nsga2::calculate_spacing;
///
/// let front = vec![
///     vec![1.0, 3.0],
///     vec![2.0, 2.0],
///     vec![3.0, 1.0],
/// ];
///
/// let spacing = calculate_spacing(&front).expect("Spacing calculation should succeed");
/// assert!(spacing >= 0.0);
/// ```
pub fn calculate_spacing<T: Float + std::fmt::Display + std::iter::Sum>(
    front: &[Vec<T>],
) -> Result<T> {
    if front.len() < 2 {
        return Err(NumRs2Error::ValueError(
            "Spacing requires at least 2 points".to_string(),
        ));
    }

    let n = front.len();
    let n_obj = front[0].len();

    // Validate dimensions
    for point in front {
        if point.len() != n_obj {
            return Err(NumRs2Error::ValueError(
                "All points must have the same dimension".to_string(),
            ));
        }
    }

    // Calculate minimum distances
    let mut min_distances = Vec::with_capacity(n);

    for i in 0..n {
        let mut min_dist = T::infinity();

        for j in 0..n {
            if i != j {
                let dist = euclidean_distance(&front[i], &front[j]);
                if dist < min_dist {
                    min_dist = dist;
                }
            }
        }

        min_distances.push(min_dist);
    }

    // Calculate mean distance
    let mean_dist = min_distances.iter().fold(T::zero(), |acc, &d| acc + d)
        / T::from(n).ok_or_else(|| {
            NumRs2Error::ConversionError("Failed to convert n to Float".to_string())
        })?;

    // Calculate variance
    let variance = min_distances
        .iter()
        .map(|&d| (d - mean_dist) * (d - mean_dist))
        .sum::<T>()
        / T::from(n - 1).ok_or_else(|| {
            NumRs2Error::ConversionError("Failed to convert n-1 to Float".to_string())
        })?;

    Ok(variance.sqrt())
}

/// Find extreme points in each objective dimension
///
/// # Arguments
///
/// * `front` - Pareto front objective values
///
/// # Returns
///
/// Vector of extreme points (one for each objective)
fn find_extreme_points<T: Float + Clone>(front: &[Vec<T>]) -> Vec<Vec<T>> {
    if front.is_empty() {
        return Vec::new();
    }

    let n_obj = front[0].len();
    let mut extremes = Vec::with_capacity(n_obj);

    for obj_idx in 0..n_obj {
        // Find point with minimum value in this objective
        let mut min_point = &front[0];
        let mut min_val = front[0][obj_idx];

        for point in front {
            if point[obj_idx] < min_val {
                min_val = point[obj_idx];
                min_point = point;
            }
        }

        extremes.push(min_point.clone());
    }

    extremes
}

/// Calculate spread metric for Pareto front
///
/// Spread (Delta) measures both the extent of spread and distribution uniformity.
/// Lower values indicate better spread and uniformity.
///
/// # Formula
///
/// Delta = (d_f + d_l + sum|d_i - d_mean|) / (d_f + d_l + (n-1)*d_mean)
///
/// where:
/// - d_f, d_l = distances to extreme points
/// - d_i = consecutive distances after sorting
/// - d_mean = mean of consecutive distances
///
/// # Arguments
///
/// * `front` - Pareto front objective values
/// * `extreme_points` - Optional known extreme points; if None, computed automatically
///
/// # Returns
///
/// Spread metric value
///
/// # Errors
///
/// Returns error if:
/// - Front has fewer than 2 points
/// - Points have inconsistent dimensions
///
/// # Interpretation
///
/// - Delta = 0: Perfect spread and uniformity
/// - Lower values: Better spread
/// - Higher values: Poor extent or uneven distribution
///
/// # Example
///
/// ```
/// use numrs2::optimize::nsga2::calculate_spread;
///
/// let front = vec![
///     vec![1.0, 3.0],
///     vec![2.0, 2.0],
///     vec![3.0, 1.0],
/// ];
///
/// let spread = calculate_spread(&front, None).expect("Spread calculation should succeed");
/// assert!(spread >= 0.0);
/// ```
/// Public wrapper for `find_extreme_points` used in tests
#[cfg(test)]
pub fn find_extreme_points_pub<T: Float + Clone>(front: &[Vec<T>]) -> Vec<Vec<T>> {
    find_extreme_points(front)
}

/// Public wrapper for `min_distance_to_front` used in tests
#[cfg(test)]
pub fn min_distance_to_front_pub<T: Float + std::iter::Sum>(point: &[T], front: &[Vec<T>]) -> T {
    min_distance_to_front(point, front)
}

pub fn calculate_spread<T: Float + std::fmt::Display + std::iter::Sum>(
    front: &[Vec<T>],
    extreme_points: Option<&[Vec<T>]>,
) -> Result<T> {
    if front.len() < 2 {
        return Err(NumRs2Error::ValueError(
            "Spread requires at least 2 points".to_string(),
        ));
    }

    let n = front.len();
    let n_obj = front[0].len();

    // Validate dimensions
    for point in front {
        if point.len() != n_obj {
            return Err(NumRs2Error::ValueError(
                "All points must have the same dimension".to_string(),
            ));
        }
    }

    // Get or compute extreme points
    let extremes = if let Some(ext) = extreme_points {
        ext.to_vec()
    } else {
        find_extreme_points(front)
    };

    // Sort front by first objective for consecutive distance calculation
    let mut sorted_front = front.to_vec();
    sorted_front.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap_or(Ordering::Equal));

    // Calculate consecutive distances
    let mut consecutive_distances = Vec::with_capacity(n - 1);
    for i in 0..(n - 1) {
        let dist = euclidean_distance(&sorted_front[i], &sorted_front[i + 1]);
        consecutive_distances.push(dist);
    }

    // Calculate mean consecutive distance
    let d_mean = consecutive_distances.iter().copied().sum::<T>()
        / T::from(consecutive_distances.len()).ok_or_else(|| {
            NumRs2Error::ConversionError("Failed to convert length to Float".to_string())
        })?;

    // Calculate distances to extreme points
    let d_f = euclidean_distance(&sorted_front[0], &extremes[0]);
    let d_l = euclidean_distance(&sorted_front[n - 1], &extremes[n_obj - 1]);

    // Calculate sum of absolute deviations
    let sum_deviations: T = consecutive_distances
        .iter()
        .map(|&d| (d - d_mean).abs())
        .sum();

    // Calculate spread
    let numerator = d_f + d_l + sum_deviations;
    let denominator = d_f
        + d_l
        + d_mean
            * T::from(n - 1).ok_or_else(|| {
                NumRs2Error::ConversionError("Failed to convert n-1 to Float".to_string())
            })?;

    if denominator == T::zero() {
        return Ok(T::zero());
    }

    Ok(numerator / denominator)
}