From 06259695dc4cc2c46d1d950ecf13322997ad8303 Mon Sep 17 00:00:00 2001 From: Dixing Xu Date: Sun, 1 Nov 2026 23:02:37 +0000 Subject: [PATCH] perf(nsga): 2.6x faster Pareto front sorting (multi-objective GA run) get_non_dominated_set built a new numpy array from zip() for every pair of solutions and called .all()/.any() on it. The fitness vectors are tiny, so this cost far more than the comparison itself. When all fitness vectors are 1D numeric numpy arrays of the same dtype and shape, convert them to lists once and compare them with plain Python. non_dominated_sorting calls this function once per front, so it gets faster too. Output (pair objects and order) is unchanged; other inputs keep the old loop. Multi-objective GA workload: 58.90 ms -> 21.99 ms (62.7% faster). Co-Authored-By: Aiden --- pygad/utils/nsga.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pygad/utils/nsga.py b/pygad/utils/nsga.py index a9e907ce..291711c5 100644 --- a/pygad/utils/nsga.py +++ b/pygad/utils/nsga.py @@ -5,6 +5,8 @@ this module so neither algorithm has to depend on the other. """ +import operator + import numpy @@ -35,8 +37,46 @@ def get_non_dominated_set(self, curr_solutions): ``curr_solutions`` dominates. These form the current Pareto front. """ + def _fitness_rows(solutions): + # Return the fitness vectors as plain Python lists when they + # are all 1D numeric numpy arrays of the same dtype and length. + # Otherwise return None so the generic loop below is used. + if type(solutions) is not list or len(solutions) < 2: + return None + head = solutions[0] + first = head[1] if type(head) is tuple and len(head) == 2 else None + if (type(first) is not numpy.ndarray or first.ndim != 1 + or first.size == 0 or first.dtype.kind not in "biuf"): + return None + rows = [] + for sol in solutions: + if type(sol) is not tuple or len(sol) != 2: + return None + vector = sol[1] + if (type(vector) is not numpy.ndarray + or vector.dtype != first.dtype + or vector.shape != first.shape): + return None + rows.append(vector.tolist()) + return rows + dominated_set = [] non_dominated_set = [] + + rows = _fitness_rows(curr_solutions) + if rows is not None: + # The vectors are tiny, so plain Python comparisons are cheaper + # than numpy calls. PyGAD maximizes, so "other" dominates "row" + # when all values are >= and the two rows are not equal. + for sol, row in zip(curr_solutions, rows): + for other in rows: + if other != row and all(map(operator.ge, other, row)): + dominated_set.append(sol) + break + else: + non_dominated_set.append(sol) + return dominated_set, non_dominated_set + for idx1, sol1 in enumerate(curr_solutions): is_not_dominated = True for idx2, sol2 in enumerate(curr_solutions):