If you find any mistakes here or anywhere else on my website, email website@CoryHufford.com.
October 31, 2026
This is the same experiment as in Part I, but in three dimensions. Here's a uniformly distributed example Pareto front of 10,000 points that lies on the surface of an octant of the unit sphere:

This is a histogram of the crowding distances of the Pareto front:

The following three plots are this Pareto front downselected from 10,000 points to 2,500 points using the same three methods used in Part I:



The following three plots are histograms of the crowding distances for each of the three downselected Pareto fronts:



Here's another example Pareto front of 10,000 points on the same octant of the unit sphere, but normally distributed:

Here's the histogram of its crowding distances:

Here's the normally distributed front downselected from 10,000 to 2,500 points using the same three methods:



Here are the crowding distance histogram for the downselected normal Pareto fronts:



Here are the four crowding distance histograms (original and three downselections) for the normally distributed Pareto front on a logarithmic scale:




All results cam from this Rust and Python code using random number generator
seed 3588435413156785030. Use this code at your own risk.
All results came from this Rust and Python code using Rust edition 2024,
Python 3.13.5, and random number generator seed
3588435413156785030. Use this code at your own risk.
Ran with Rust 1.99.1, edition 2024, and dependencies:
[dependencies]
chrono = { version = "0.4.45", default-features = false, features = ["clock"] }
rand = { version = "0.10.3", default-features = false, features = ["thread_rng"] }
rand_distr = { version = "0.6.0", default-features = false }
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cc"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "chacha20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"num-traits",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crowd"
version = "0.1.0"
dependencies = [
"chrono",
"rand",
"rand_distr",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "js-sys"
version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
dependencies = [
"chacha20",
"getrandom",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_distr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8"
dependencies = [
"num-traits",
"rand",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "wasm-bindgen"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.9",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
#[must_use]
fn format_duration(duration: std::time::Duration) -> String {
let seconds = duration.as_secs_f64();
let (value, unit) = if seconds >= 3600.0 {
(seconds / 3600.0, "hr")
} else if seconds >= 60.0 {
(seconds / 60.0, "min")
} else if seconds < 1.0e-3 {
(seconds * 1.0e6, "us")
} else if seconds < 1.0 {
(seconds * 1.0e3, "ms")
} else {
(seconds, "s")
};
let precision = if value >= 100.0 {0} else if value >= 10.0 {1} else {2};
format!("{value:.precision$} {unit}")
}
mod euclidean {
#[must_use]
pub fn norm<const N: usize>(a: &[f64; N]) -> f64 {
debug_assert!(a.iter().all(|x| x.is_finite()));
a.iter().map(|x| x.powi(2)).sum::<f64>().sqrt()
}
}
struct TimedStatus {
start_time: std::time::Instant,
}
impl TimedStatus {
pub fn new(message: &str) -> Self {
use std::io::Write;
print!("{message}");
std::io::stdout().flush().unwrap();
Self{start_time: std::time::Instant::now()}
}
}
impl Drop for TimedStatus {
fn drop(&mut self) {
use std::io::Write;
// Shouldn't panic in drop, but it'll be fine
println!(" ({})", format_duration(self.start_time.elapsed()));
std::io::stdout().flush().unwrap();
}
}
struct Dtlz2<const NUM_PARAMETERS: usize, const NUM_METRICS: usize> {
}
impl<
const NUM_PARAMETERS: usize,
const NUM_METRICS: usize
> Dtlz2<NUM_PARAMETERS, NUM_METRICS> {
#[must_use]
pub fn new() -> Self {
const {
assert!(NUM_METRICS >= 2);
assert!(NUM_PARAMETERS >= NUM_METRICS);
}
Self{}
}
#[allow(dead_code)]
#[must_use]
pub fn evaluate(
&self,
arguments: [f64; NUM_PARAMETERS],
) -> [f64; NUM_METRICS] {
const HALF_PI: f64 = 0.5 * std::f64::consts::PI;
debug_assert!(arguments.iter().all(|f| f.is_finite()));
if arguments.iter().any(|&a| a < 0.0 || a > 1.0) {
return std::array::repeat(f64::INFINITY);
}
let m = NUM_METRICS - 1;
let g_plus_one =
arguments[m..].iter().map(|x| (x - 0.5).powi(2)).sum::<f64>()
+ 1.0;
std::array::from_fn(|i| {
let j = m - i;
g_plus_one
* arguments[..j].iter().map(|x| (HALF_PI * x).cos()).product::<f64>()
* if i == 0 { 1.0 } else { (HALF_PI * arguments[j]).sin() }
})
}
#[must_use]
pub fn first_dominates_second(
&self,
measurements_a: [f64; NUM_METRICS],
measurements_b: [f64; NUM_METRICS],
) -> bool {
debug_assert!(measurements_a.iter().all(|x| x.is_finite()));
debug_assert!(measurements_b.iter().all(|x| x.is_finite()));
let mut one_better = false;
for (a, b) in measurements_a.into_iter().zip(measurements_b) {
if b < a { return false; }
if a < b { one_better = true; }
}
one_better
}
/// If `members` is empty, returns `None`. Otherwise, returns `true` if all
/// pairs of elements in `members` are not equal and non-dominating, or
/// `false` otherwise.
#[must_use]
pub fn is_one_pareto_front(
&self,
members: &[[f64; NUM_METRICS]],
) -> Option<bool> {
if members.is_empty() {
return None;
}
for i in 0..(members.len() - 1) {
for j in (i + 1)..members.len() {
let a = members[i];
let b = members[j];
if
a == b
|| self.first_dominates_second(a, b)
|| self.first_dominates_second(b, a)
{
return Some(false);
}
}
}
Some(true)
}
#[must_use]
pub fn random_optimal_uniform_measurements(
&self,
rng: &mut rand::rngs::Xoshiro256PlusPlus,
) -> [f64; NUM_METRICS] {
use rand::RngExt;
let distribution = rand_distr::StandardNormal;
loop {
let measurements = std::array::from_fn(|_| {
rng.sample::<f64, _>(distribution).abs()
});
let norm = euclidean::norm(&measurements);
if norm > 1.0e-15 { // Arbitrary threshold
return measurements.map(|m| m / norm);
}
}
}
#[must_use]
pub fn random_optimal_normal_measurements(
&self,
rng: &mut rand::rngs::Xoshiro256PlusPlus,
) -> [f64; NUM_METRICS] {
use rand::RngExt;
let distribution = rand_distr::StandardNormal;
let center = 1.0 / 3.0_f64.sqrt();
let width = 0.2;
loop {
let measurements = std::array::from_fn(|_| {
(center + width * rng.sample::<f64, _>(distribution)).abs()
});
let norm = euclidean::norm(&measurements);
if norm > 1.0e-15 { // Arbitrary threshold
return measurements.map(|m| m / norm);
}
}
}
}
#[must_use]
fn calculate_crowding_distances<const N: usize>(
points: &[[f64; N]],
) -> Vec<f64> {
debug_assert!(points.iter().all(|p| p.iter().all(|f| f.is_finite())));
if points.len() <= 2 {
return (0..points.len()).map(|_| f64::INFINITY).collect();
}
let mut distances: Vec<_> = (0..points.len()).map(|i| (i, 0.0)).collect();
for i in 0..N {
distances.sort_by(|a, b| points[a.0][i].total_cmp(&points[b.0][i]));
let span =
points[distances.last().unwrap().0][i]
- points[distances.first().unwrap().0][i];
if span == 0.0 {
continue;
}
distances.first_mut().unwrap().1 = f64::INFINITY;
distances.last_mut().unwrap().1 = f64::INFINITY;
for j in 1..(distances.len() - 1) {
let ai = points[distances[j - 1].0][i];
let bi = points[distances[j + 1].0][i];
distances[j].1 += (bi - ai) / span;
}
}
distances.sort_unstable_by_key(|(i, _)| *i);
distances.into_iter().map(|(_, d)| d).collect()
}
fn create_new_datetime_directory(suffix: &str) -> Result<String, String> {
loop {
let directory_path = format!(
"{}_{suffix}/",
chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"),
);
let directory_exists =
std::fs::exists(&directory_path)
.map_err(|e| format!(
"couldn't check for existing datetime directory: {e}"
))?;
if directory_exists {
std::thread::sleep(std::time::Duration::from_secs(1));
continue;
}
std::fs::create_dir(&directory_path).map_err(|e| format!(
"couldn't create datetime directory \"{directory_path}\": {e}"
))?;
return Ok(directory_path);
}
}
fn save_points_and_distances(
points: &[[f64; 3]],
file_path: &str,
) -> Result<(), String> {
use std::io::Write;
let crowding_distances = calculate_crowding_distances(points);
let file =
std::fs::File::create(file_path)
.map_err(|e| format!(
"couldn't create points and distances file \"{file_path}\": {e}"
))?;
let mut writer = std::io::BufWriter::new(file);
writeln!(writer, "x,y,z,crowding distance")
.map_err(|e| format!(
"couldn't write header to points and distances file \
\"{file_path}\": {e}"
))?;
for ([x, y, z], crowding) in points.iter().zip(crowding_distances) {
writeln!(writer, "{x},{y},{z},{crowding}").map_err(|e| format!(
"couldn't write line to points and distances file \"{file_path}\": \
{e}"
))?;
}
writer.flush().map_err(|e| format!(
"couldn't flush points and distances file \"{file_path}\": {e}"
))
}
fn run_test(
original_pareto_front: &[[f64; 3]],
downselection_size: usize,
output_directory_path: &str,
output_file_prefix: &str,
rng: &mut rand::rngs::Xoshiro256PlusPlus,
) -> Result<(), String> {
let random_subset: Vec<[f64; 3]> = {
use rand::prelude::SliceRandom;
let _status = TimedStatus::new(" creating random subset");
let mut v = original_pareto_front.to_vec();
v.shuffle(rng);
v.truncate(downselection_size);
v
};
let one_shot_crowding_subset: Vec<[f64; 3]> = {
let _status = TimedStatus::new(" creating one-shot subset");
let mut v: Vec<_> =
original_pareto_front
.iter()
.zip(calculate_crowding_distances(original_pareto_front))
.collect();
v.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
v.truncate(downselection_size);
v.into_iter().map(|(p, _)| *p).collect()
};
let iterative_crowding_subset: Vec<[f64; 3]> = {
let _status = TimedStatus::new(" creating iterative subset");
let mut subset = original_pareto_front.to_vec();
while subset.len() > downselection_size {
subset.remove(
calculate_crowding_distances(&subset)
.into_iter()
.enumerate()
.min_by(|a, b| a.1.total_cmp(&b.1))
.unwrap()
.0
);
}
subset
};
for (index, (front, name)) in [
(original_pareto_front, "original"),
(&random_subset, "random"),
(&one_shot_crowding_subset, "one-shot"),
(&iterative_crowding_subset, "iterative"),
].into_iter().enumerate() {
assert!(!front.is_empty(), "{name}");
let _status = TimedStatus::new(&format!(" saving {name}"));
save_points_and_distances(
front,
&format!(
"{output_directory_path}/\
{output_file_prefix}_{index}-{name}.csv"
),
)?;
}
Ok(())
}
fn main() {
use rand::SeedableRng;
const LARGE_SIZE: usize = 10_000;
const SMALL_SIZE: usize = LARGE_SIZE / 4;
const {
assert!(SMALL_SIZE < LARGE_SIZE);
assert!(0 < SMALL_SIZE);
}
let usage = format!("{} [rng-seed]", env!("CARGO_BIN_NAME"));
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "-h" || a == "--help") {
println!("usage: {usage}");
std::process::exit(0);
}
if args.len() < 1 {
eprintln!("error: too few arguments\nusage: {usage}");
std::process::exit(1);
}
if args.len() > 2 {
eprintln!("error: too many arguments\nusage: {usage}");
std::process::exit(2);
}
let rng_seed = args.get(1).map_or_else(
rand::random::<u64>,
|s| match s.parse::<u64>() {
Ok(u) => u,
Err(e) => {
eprintln!(
"error: invalid random number generator seed: {e}\nusage: \
{usage}"
);
std::process::exit(3);
},
},
);
let mut rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
let output_directory_path = match create_new_datetime_directory(
&rng_seed.to_string()
) {
Ok(s) => s,
Err(e) => {
eprintln!("error: couldn't create output directory: {e}");
std::process::exit(4);
},
};
let problem = Dtlz2::<3, 3>::new();
let uniform_front: Vec<[f64; 3]> = {
let _status = TimedStatus::new("creating uniform front");
let front: Vec<[f64; 3]> =
(0..LARGE_SIZE)
.map(|_| problem.random_optimal_uniform_measurements(&mut rng))
.collect();
assert_eq!(problem.is_one_pareto_front(&front), Some(true));
front
};
let normal_front: Vec<[f64; 3]> = {
let _status = TimedStatus::new("creating normal front");
let front: Vec<[f64; 3]> =
(0..LARGE_SIZE)
.map(|_| problem.random_optimal_normal_measurements(&mut rng))
.collect();
assert_eq!(problem.is_one_pareto_front(&front), Some(true));
front
};
for (i, (name, front)) in [
("uniform", uniform_front),
("normal", normal_front),
].into_iter().enumerate() {
use std::io::Write;
println!("running {name} test");
std::io::stdout().flush().unwrap();
if let Err(e) = run_test(
&front,
SMALL_SIZE,
&output_directory_path,
name,
&mut rng,
) {
eprintln!("error: {e}");
std::process::exit(5 + i as i32);
}
}
}
Ran with Python 3.13.5.
#!/usr/bin/env python3
import cycler
import matplotlib.pyplot
import matplotlib.ticker
import matplotlib.typing
import argparse
import collections.abc
import math
import os
import subprocess
import time
import types
import typing
PLOT_THEME: dict[matplotlib.typing.RcKeyType, typing.Any] = {
"axes.axisbelow": True,
"axes.edgecolor": "#e0cdb0",
"axes.facecolor": "#000000",
"axes.grid": True,
"axes.grid.which": "both",
"axes.labelcolor": "#e0cdb0",
"axes.prop_cycle": cycler.cycler(
color = ["#35b779", "#a878de", "#398fe5", "#e65c64"],
),
"axes3d.xaxis.panecolor": "#000000",
"axes3d.yaxis.panecolor": "#000000",
"axes3d.zaxis.panecolor": "#000000",
"figure.facecolor": "#000000",
"grid.major.color": "#706658",
"grid.major.linewidth": 0.8,
"grid.minor.color": "#4e483e",
"grid.minor.linewidth": 0.5,
"patch.edgecolor": "#000000",
"patch.force_edgecolor": True,
"patch.linewidth": 0.4,
"scatter.edgecolors": "#000000",
"text.color": "#e0cdb0",
"xtick.color": "#e0cdb0",
"ytick.color": "#e0cdb0",
}
def format_duration(seconds: float) -> str:
value: float = seconds
unit: str = "s"
if value >= 3600.0:
value /= 3600.0
unit = "hr"
elif value >= 60.0:
value /= 60.0
unit = "min"
elif value < 1.0e-3:
value *= 1.0e6
unit = "us"
elif value < 1.0:
value *= 1.0e3
unit = "ms"
num_decimal_places: int = 2
if value >= 100.0:
num_decimal_places = 0
elif value >= 10.0:
num_decimal_places = 1
return f"{value:.{num_decimal_places}f} {unit}"
class TimedStatus:
def __init__(self, message: str) -> None:
self.__message: str = message
def __enter__(self) -> "TimedStatus":
print(self.__message, end = "", flush = True)
self.__start_time: float = time.monotonic()
return self
def __exit__(
self,
exception_type: type[BaseException] | None,
exception_value: BaseException | None,
traceback: types.TracebackType | None,
) -> None:
elapsed: str = format_duration(time.monotonic() - self.__start_time)
print(f" ({elapsed})", flush = True)
def read_dsv_file_rowwise(
file_path: str,
*,
delimiter: str = ",",
column_types: collections.abc.Mapping[
str,
collections.abc.Callable[[str], typing.Any]
] = {},
) -> list[dict[str, typing.Any]]:
def split_str(s: str) -> list[str]:
return [sub.strip() for sub in s.split(delimiter)]
with open(file_path, "r") as file:
column_names: list[str] = split_str(file.readline().rstrip("\r\n"))
rows: list[dict[str, typing.Any]] = []
while line := file.readline():
if len(line := line.rstrip("\r\n")) == 0:
continue
rows.append({
name: column_types.get(name, lambda x: x)(part)
for name, part in zip(column_names, split_str(line))
})
return rows
def compress_png(oxipng_path: str, png_file_path: str) -> None:
subprocess.run(
[oxipng_path, "--opt", "max", "--strip", "safe", png_file_path],
capture_output = True,
check = True,
)
def main(
*,
directory_path: str,
oxipng_path: str | None,
) -> None:
file_names: list[str] = []
for name in os.listdir(directory_path):
if name.endswith(".csv"):
file_names.append(name)
file_names.sort()
print(f"found {len(file_names)} files", flush = True)
if oxipng_path == "":
oxipng_path = "oxipng"
plot_file_paths: list[str] = []
for i, file_name in enumerate(file_names, start = 1):
print(f"\n{i} of {len(file_names)}: {file_name}")
with TimedStatus(f" parsing"):
rows: list[dict[str, typing.Any]] = read_dsv_file_rowwise(
file_path = f"{directory_path}/{file_name}",
column_types = {
"x": float,
"y": float,
"z": float,
"crowding distance": float,
},
)
with matplotlib.pyplot.rc_context(PLOT_THEME):
with TimedStatus(" plotting front"):
figure = matplotlib.pyplot.figure(
figsize = (12.0, 12.0),
dpi = 512,
layout = "tight",
)
axes = figure.add_subplot(1, 1, 1, projection = "3d")
axes.set_xlabel("x")
axes.set_ylabel("y")
axes.set_zlabel("z")
axes.set_xlim((-0.05, 1.05))
axes.set_ylim((-0.05, 1.05))
axes.set_zlim((-0.05, 1.05))
axes.view_init(20.0, 30.0, 0.0)
axes.scatter(
[row["x"] for row in rows],
[row["y"] for row in rows],
[row["z"] for row in rows],
)
plot_file_paths.append(
f"{directory_path}/plot_front_{file_name}.png"
)
title: str = "Pareto Front"
if file_name.startswith("normal"):
title = "Normally Distributed " + title
elif file_name.startswith("uniform"):
title = "Uniformly Distributed " + title
if "0-original" in file_name:
title += "\nOriginal"
elif "1-random" in file_name:
title += "\nQuartered Randomly"
elif "2-one-shot" in file_name:
title += "\nQuartered with One-Shot Crowding Distance"
elif "3-iterative" in file_name:
title += "\nQuartered with Iterative Crowding Distance"
axes.set_title(title, y = 0.95)
figure.savefig(
plot_file_paths[-1],
bbox_inches = figure.bbox_inches.from_bounds(
x0 = 0.0,
y0 = 0.75,
width = 12.0,
height = 10.5,
)
)
matplotlib.pyplot.close(figure)
with TimedStatus(" plotting crowding"):
values: list[float] = [
d
for row in rows
if math.isfinite(d := row["crowding distance"])
]
figure = matplotlib.pyplot.figure(
figsize = (12.0, 10.0),
dpi = 512,
layout = "tight",
)
axes = figure.add_subplot(1, 1, 1)
title: str = "Crowding Distance Histogram"
if file_name.startswith("normal"):
title += "\nNormally Distributed Pareto Front"
bins: list[float] = [x / 1000.0 for x in range(51)]
padding_x: float = 0.005 * (bins[-1] - bins[0])
assert min(values) >= bins[ 0]
assert max(values) <= bins[-1]
axes.hist(x = values, bins = bins)
axes.set_xlim((bins[0] - padding_x, bins[-1] + padding_x))
if "0-original" in file_name:
title += "\nOriginal"
axes.set_ylim((0, 9000))
else:
axes.set_ylim((0, 1400))
if "1-random" in file_name:
title += "\nQuartered Randomly"
elif "2-one-shot" in file_name:
title += "\nQuartered with One-Shot Crowding Distance"
elif "3-iterative" in file_name:
title += "\nQuartered with Iterative Crowding Distance"
axes.xaxis.set_minor_locator(
matplotlib.ticker.MultipleLocator(0.002)
)
axes.yaxis.set_major_locator(
matplotlib.ticker.MultipleLocator(200)
)
axes.yaxis.set_minor_locator(
matplotlib.ticker.MultipleLocator(100)
)
elif file_name.startswith("uniform"):
title += "\nUniformly Distributed Pareto Front"
bins: list[float] = [x / 5000.0 for x in range(41)]
padding_x: float = 0.005 * (bins[-1] - bins[0])
assert min(values) >= bins[ 0]
assert max(values) <= bins[-1]
axes.hist(x = values, bins = bins)
axes.set_xlim((bins[0] - padding_x, bins[-1] + padding_x))
if "0-original" in file_name:
title += "\nOriginal"
axes.set_ylim((0, 3500))
else:
axes.set_ylim((0, 600))
if "1-random" in file_name:
title += "\nQuartered Randomly"
elif "2-one-shot" in file_name:
title += "\nQuartered with One-Shot Crowding Distance"
elif "3-iterative" in file_name:
title += "\nQuartered with Iterative Crowding Distance"
axes.xaxis.set_major_locator(
matplotlib.ticker.MultipleLocator(0.001)
)
axes.xaxis.set_minor_locator(
matplotlib.ticker.MultipleLocator(0.0005)
)
axes.yaxis.set_major_locator(
matplotlib.ticker.MultipleLocator(100)
)
axes.yaxis.set_minor_locator(
matplotlib.ticker.MultipleLocator(50)
)
else:
axes.hist(x = values)
axes.set_title(title)
plot_file_paths.append(
f"{directory_path}/plot_crowding_{file_name}.png"
)
figure.savefig(plot_file_paths[-1])
matplotlib.pyplot.close(figure)
if file_name.startswith("normal"):
with TimedStatus(" plotting crowding log"):
figure = matplotlib.pyplot.figure(
figsize = (12.0, 12.0),
dpi = 512,
layout = "tight",
)
axes = figure.add_subplot(1, 1, 1)
axes.hist(x = values, bins = bins)
axes.set_yscale("log")
axes.set_ylim((0.9, 10 ** 4))
axes.set_xlim((bins[0] - padding_x, bins[-1] + padding_x))
axes.xaxis.set_minor_locator(
matplotlib.ticker.MultipleLocator(0.002)
)
plot_file_paths.append(
f"{directory_path}/plot_crowding-log_{file_name}.png"
)
axes.set_title(title)
figure.savefig(plot_file_paths[-1])
matplotlib.pyplot.close(figure)
print()
if oxipng_path is not None:
print("\ncompressing")
starting_bytes: int = sum(os.path.getsize(f) for f in plot_file_paths)
for i, file_path in enumerate(sorted(plot_file_paths), start = 1):
file_name: str = file_path.split("/")[-1]
with TimedStatus(f" {i} of {len(plot_file_paths)}: {file_name}"):
compress_png(oxipng_path, file_path)
ending_bytes: int = sum(os.path.getsize(f) for f in plot_file_paths)
compression_percent: float = (
100.0 * (starting_bytes - ending_bytes) / starting_bytes
)
print(
f"\n {starting_bytes:,} bytes -> {ending_bytes:,} bytes "
f"({compression_percent:.1f}% smaller)\n"
)
def make_cli() -> argparse.ArgumentParser:
class FixedWidthFormatter(argparse.ArgumentDefaultsHelpFormatter):
def __init__(self, prog: str) -> None:
super().__init__(prog, width = 80)
parser = argparse.ArgumentParser(formatter_class = FixedWidthFormatter)
parser.add_argument(
"-c",
"--compress",
metavar = "oxipng-path",
dest = "oxipng_path",
nargs = "?",
const = "",
help = (
"Compress the plot PNGs with Oxipng."
)
)
parser.add_argument(
"directory_path",
metavar = "<directory-path>",
help = (
"Path to directory containing results .csv files from the "
"accompanying Rust program."
),
)
return parser
if __name__ == "__main__":
main(**vars(make_cli().parse_args()))