Skip to content

Example: A Real-Valued Multi-Objective Optimization Problem

Below is a simple two-variable multi-objective problem to illustrate real-valued optimization with pymoors. We have two continuous decision variables, \(x_1\) and \(x_2\), both within a given range. We define two objective functions to be minimized simultaneously, and we solve this using the popular NSGA2 algorithm.

Mathematical Formulation

Let \(\mathbf{x} = (x_1, x_2)\) be our decision variables, each constrained to the interval \([-2, 2]\). We define the following objectives:

\[ \begin{aligned} \min_{\mathbf{x}} \quad &f_1(x_1, x_2) = x_1^2 + x_2^2 \\ \min_{\mathbf{x}} \quad &f_2(x_1, x_2) = (x_1 - 1)^2 + x_2^2 \\ \text{subject to} \quad & -2 \le x_1 \le 2, \\ & -2 \le x_2 \le 2. \end{aligned} \]

Interpretation

  1. \(f_1\) measures the distance of \(\mathbf{x}\) from the origin \((0,0)\) in the 2D plane.
  2. \(f_2\) measures the distance of \(\mathbf{x}\) from the point \((1,0)\).

Thus, \(\mathbf{x}\) must compromise between being close to \((0,0)\) and being close to \((1,0)\). There is no single point in \([-2,2]^2\) that simultaneously minimizes both distances perfectly (other than at the boundary of these trade-offs), so we end up with a Pareto front rather than a single best solution.

:dep ndarray = "*"
:dep moors = "0.2.6"
:dep plotters = "0.3.6"

use ndarray::{Array2, Ix2};
use moors::{
    NoConstraints,
    algorithms::Nsga2Builder,
    duplicates::CloseDuplicatesCleaner,
    operators::{GaussianMutation, SimulatedBinaryCrossover, RandomSamplingFloat},
    genetic::Population
};
use plotters::prelude::*;

// General: compute two-objective fitness (squared distances).
fn fitness_fn(genes: &Array2<f64>) -> Array2<f64> {
    let n = genes.nrows();
    let mut result = Array2::<f64>::zeros((n, 2));

    let x1 = genes.column(0);
    let x2 = genes.column(1);

    let f1 = &x1.mapv(|v| v.powi(2)) + &x2.mapv(|v| v.powi(2));
    let f2 = &x1.mapv(|v| (v - 1.0).powi(2)) + &x2.mapv(|v| v.powi(2));

    result.column_mut(0).assign(&f1);
    result.column_mut(1).assign(&f2);
    result
}

// General: run NSGA-II and collect population.
let population: Population<Ix2, Ix2> = {
    let mut algorithm = Nsga2Builder::default()
        .sampler(RandomSamplingFloat::new(-2.0, 2.0))
        .crossover(SimulatedBinaryCrossover::new(15.0))
        .mutation(GaussianMutation::new(0.1, 0.01))
        .duplicates_cleaner(CloseDuplicatesCleaner::new(1e-16))
        .fitness_fn(fitness_fn)
        .constraints_fn(NoConstraints)
        .num_vars(2)
        .population_size(200)
        .num_offsprings(200)
        .num_iterations(100)
        .mutation_rate(0.2)
        .crossover_rate(0.9)
        .keep_infeasible(false)
        .verbose(false)
        .seed(42)
        .build()
        .unwrap();

    algorithm.run().expect("NSGA2 run failed");
    algorithm.population().unwrap().clone()
};

// General: theoretical Pareto front curve.
let n = 200usize;
let t: Vec<f64> = (0..n).map(|i| i as f64 / (n as f64 - 1.0)).collect();
let f1_theo: Vec<f64> = t.iter().map(|&x| x * x).collect();
let f2_theo: Vec<f64> = t.iter().map(|&x| (x - 1.0).powi(2)).collect();

// General: obtained front from the algorithm.
let fitness = population.fitness;
let f1: Vec<f64> = fitness.column(0).to_vec();
let f2: Vec<f64> = fitness.column(1).to_vec();

// General: define axes with headroom.
let mut x_max = f1.iter().copied().chain(f1_theo.iter().copied()).fold(0.0_f64, f64::max);
let mut y_max = f2.iter().copied().chain(f2_theo.iter().copied()).fold(0.0_f64, f64::max);
x_max = (x_max * 1.05).max(1.0);
y_max = (y_max * 1.05).max(1.0);

// General: render to in-memory SVG and emit as rich output (no files).
let mut svg = String::new();
{
    let backend = SVGBackend::with_string(&mut svg, (800, 600));
    let root = backend.into_drawing_area();
    root.fill(&WHITE).unwrap();

    let mut chart = ChartBuilder::on(&root)
        .caption("Two-Target Distance Problem · Pareto Front", ("DejaVu Sans", 22))
        .margin(10)
        .x_label_area_size(40)
        .y_label_area_size(60)
        .build_cartesian_2d(0f64..x_max, 0f64..y_max)
        .unwrap();

    chart.configure_mesh()
        .x_desc("f1")
        .y_desc("f2")
        .axis_desc_style(("DejaVu Sans", 14))
        .light_line_style(&RGBColor(220, 220, 220))
        .draw()
        .unwrap();

    chart.draw_series(
        f1_theo.iter().zip(f2_theo.iter()).map(|(&x, &y)| {
            Circle::new((x, y), 5, RGBColor(31, 119, 180).filled())
        })
    ).unwrap()
     .label("Theoretical Pareto Front")
     .legend(|(x, y)| Circle::new((x, y), 5, RGBColor(31, 119, 180).filled()));

    chart.draw_series(
        f1.iter().zip(f2.iter()).map(|(&x, &y)| {
            Circle::new((x, y), 3, RGBColor(255, 0, 0).filled())
        })
    ).unwrap()
     .label("Obtained Front")
     .legend(|(x, y)| Circle::new((x, y), 3, RGBColor(255, 0, 0).filled()));

    chart.configure_series_labels()
        .border_style(&RGBAColor(0, 0, 0, 0.3))
        .background_style(&WHITE.mix(0.9))
        .label_font(("DejaVu Sans", 13))
        .draw()
        .unwrap();

    root.present().unwrap();
}

println!("EVCXR_BEGIN_CONTENT image/svg+xml\n{}\nEVCXR_END_CONTENT", svg);

svg


import numpy as np
import matplotlib.pyplot as plt

from pymoors import (
    Nsga2,
    RandomSamplingFloat,
    GaussianMutation,
    SimulatedBinaryCrossover,
    CloseDuplicatesCleaner,
    Constraints
)
from pymoors.typing import TwoDArray


def fitness(genes: TwoDArray) -> TwoDArray:
    x1 = genes[:, 0]
    x2 = genes[:, 1]
    # Objective 1: Distance to (0,0)
    f1 = x1**2 + x2**2
    # Objective 2: Distance to (1,0)
    f2 = (x1 - 1) ** 2 + x2**2
    # Combine the two objectives into a single array
    return np.column_stack([f1, f2])


algorithm = Nsga2(
    sampler=RandomSamplingFloat(min=-2, max=2),
    crossover=SimulatedBinaryCrossover(distribution_index=15),
    mutation=GaussianMutation(gene_mutation_rate=0.1, sigma=0.01),
    fitness_fn=fitness,
    constraints_fn = Constraints(lower_bound = -2.0, upper_bound = 2.0),
    duplicates_cleaner=CloseDuplicatesCleaner(epsilon=1e-16),
    num_vars=2,
    population_size=200,
    num_offsprings=200,
    num_iterations=100,
    mutation_rate=0.2,
    crossover_rate=0.9,
    keep_infeasible=False,
    seed=42,
    verbose=False,
)

algorithm.run()
population = algorithm.population

# Plot the results
t = np.linspace(0.0, 1.0, 200)
f1_theo = t**2
f2_theo = (t - 1.0) ** 2


plt.figure(figsize=(8, 6))
plt.scatter(f1_theo, f2_theo, marker="D", s=18, label="Theoretical Pareto Front")
plt.scatter(
    population.fitness[:, 0],
    population.fitness[:, 1],
    c="r",
    s=8,
    marker="o",
    label="Obtained Front",
)
plt.xlabel("$f_1$")
plt.ylabel("$f_2$")
plt.title("Two-Target Distance Problem · Pareto Front")
plt.grid(True)
plt.legend()
plt.show()

png