def dominated_novelty_search(
descriptors: np.ndarray,
performances: np.ndarray,
k: int = 15,
force_feasible_only: bool = True,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""
Dominated Novelty Search (DNS)
Bahlous-Boldi, R., Faldor, M., Grillotti, L., Janmohamed, H., Coiffard, L., Spector, L., & Cully, A. (2025).
Dominated Novelty Search: Rethinking Local Competition in Quality-Diversity. 1.
https://arxiv.org/abs/2502.00593v1
Quality-Diversity algorithm that implements local competition through dynamic fitness transformations,
eliminating the need for predefined bounds or parameters. The competition fitness, also known as the dominated novelty score,
is calculated as the average distance to the k nearest neighbors with higher fitness.
The method returns a descending sorted list of instances by their competition fitness value.
For each instance ``i'' in the sequence, we calculate all the other instances that dominate it.
Then, we compute the distances between their descriptors using the norm of the difference for each dimension of the descriptors.
Novel instances will get a competition fitness of np.inf (assuring they will survive).
Less novel instances will be selected by their competition fitness value. This competition mechanism creates two complementary evolutionary
pressures: individuals must either improve their fitness or discover distinct behaviors that differ from better-performing
solutions. Solutions that have no fitter neighbors (D𝑖 = ∅) receive an infinite competition fitness, ensuring their preservation in the
population.
Args:
descriptors (np.ndarray): Numpy array with the descriptors of the instances
performances (np.ndarray): Numpy array with the performance values of the instances
k (int): Number of nearest neighbours to calculate the competition fitness. Default to 15.
force_feasible_only (bool): Allow only instances with performance >= 0 to be considered. Default True.
Raises:
ValueError: If len(d) where d is the descriptor of each instance i differs from another
ValueError: If k >= len(instances)
Returns:
Tuple[np.ndarray]: Tuple with the descriptors, performances and competition fitness values sorted, plus the sorted indexing (descending order).
"""
num_instances = len(descriptors)
if num_instances <= k:
msg = f"Trying to calculate the dominated novelty search with k({k}) > len(instances) = {num_instances}"
raise ValueError(msg)
if len(performances) != len(descriptors):
raise ValueError(
f"Array mismatch between peformances and descriptors. len(performance) = {len(performances)} != {len(descriptors)} len(descriptors)"
)
# Try to force only feasible performances to get proper biased instances
is_unfeasible = (
performances < 0.0 if force_feasible_only else (performances == -np.inf)
)
fitter = performances[:, None] <= performances[None, :]
fitter = np.where(is_unfeasible[None, :], False, fitter)
np.fill_diagonal(fitter, False)
distance = np.linalg.norm(
descriptors[:, None, :] - descriptors[None, :, :], axis=-1
)
distance = np.where(fitter, distance, np.inf)
neg_dist = -distance
indices = np.argpartition(neg_dist, -k, axis=-1)[..., -k:]
values = np.take_along_axis(neg_dist, indices, axis=-1)
indices = np.argsort(values, axis=-1)[..., ::-1]
values = np.take_along_axis(values, indices, axis=-1)
indices = np.take_along_axis(indices, indices, axis=-1)
distance = np.mean(
-values, where=np.take_along_axis(fitter, indices, axis=1), axis=-1
)
distance = np.where(np.isnan(distance), np.inf, distance)
distance = np.where(is_unfeasible, -np.inf, distance)
sorted_indices = np.argsort(-distance)
return (
descriptors[sorted_indices],
performances[sorted_indices],
distance[sorted_indices],
sorted_indices,
)