java-topology/defects/optuna/unit/OptunaNonDomRankAlgorithm.java

293 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* optuna-0001 — O(N³) non-dominated sort in _calculate_nondomination_rank
*
* Simulates the defective pattern from:
* optuna/study/_multi_objective.py lines 187216 (_calculate_nondomination_rank)
* optuna/study/_multi_objective.py lines 127148 (_is_pareto_front_nd)
*
* Defect:
* Outer while loop runs once per front level F.
* Each iteration calls _is_pareto_front which re-scans ALL remaining trials.
* _is_pareto_front_nd itself is an O(N²) while loop in worst case.
* Total: O(F × N²) = O(N³) worst case (all trials in distinct fronts).
*
* Fix:
* Build dominance graph once in O(N²), then extract fronts via degree-counting in O(N+E).
* Total: O(N² × M) — eliminates the outer loop's redundant re-scanning.
*
* Op-count: number of pairwise trial comparisons (dominance checks).
* In N-objective space, each comparison examines M values; we count pairwise checks.
*/
public class OptunaNonDomRankAlgorithm {
static void check(String desc, boolean cond) {
System.out.println((cond ? "PASS" : "FAIL") + ": " + desc);
if (!cond) throw new AssertionError("FAIL: " + desc);
}
// Simulates a trial with M objective values (lower is better = minimize)
static class Trial {
final int id;
final double[] values; // M-dimensional objective values
Trial(int id, double... values) {
this.id = id;
this.values = values;
}
// Does this trial dominate other? (lower is better)
boolean dominates(Trial other) {
boolean strictlyBetter = false;
for (int i = 0; i < values.length; i++) {
if (values[i] > other.values[i]) return false; // not dominated in this dimension
if (values[i] < other.values[i]) strictlyBetter = true;
}
return strictlyBetter;
}
}
static class Result {
final int[] ranks;
final long comparisons;
Result(int[] ranks, long comparisons) {
this.ranks = ranks;
this.comparisons = comparisons;
}
}
// -----------------------------------------------------------------------
// DEFECTIVE: re-scans all remaining trials once per front level
// Simulates: _calculate_nondomination_rank + _is_pareto_front_nd pattern
// -----------------------------------------------------------------------
static long defectiveComparisons;
// Returns true if trial at index `idx` is on the Pareto front of `remaining`
static boolean isOnFrontDefective(List<Trial> remaining, int idx) {
Trial t = remaining.get(idx);
for (int j = 0; j < remaining.size(); j++) {
if (j == idx) continue;
defectiveComparisons++;
if (remaining.get(j).dominates(t)) return false; // dominated — not on front
}
return true;
}
static Result runDefective(List<Trial> trials) {
defectiveComparisons = 0;
int n = trials.size();
int[] ranks = new int[n];
// Map original index for rank assignment
List<Integer> remaining = new ArrayList<>();
for (int i = 0; i < n; i++) remaining.add(i);
int rank = 0;
while (!remaining.isEmpty()) {
List<Integer> front = new ArrayList<>();
List<Trial> remainingTrials = new ArrayList<>();
for (int idx : remaining) remainingTrials.add(trials.get(idx));
// O(N) scan per remaining trial to check if it's on front
for (int li = 0; li < remaining.size(); li++) {
if (isOnFrontDefective(remainingTrials, li)) {
front.add(remaining.get(li));
}
}
for (int idx : front) ranks[idx] = rank;
remaining.removeAll(front);
rank++;
}
return new Result(ranks, defectiveComparisons);
}
// -----------------------------------------------------------------------
// FIXED: build dominance graph once, extract fronts via degree-counting
// Simulates: Deb NSGA-II fast non-dominated sort
// -----------------------------------------------------------------------
static long fixedComparisons;
@SuppressWarnings("unchecked")
static Result runFixed(List<Trial> trials) {
fixedComparisons = 0;
int n = trials.size();
int[] ranks = new int[n];
int[] dominationCount = new int[n]; // number of trials that dominate trial i
List<Integer>[] dominated = new List[n]; // trials dominated by i
for (int i = 0; i < n; i++) dominated[i] = new ArrayList<>();
// Build dominance graph: O(N²) total comparisons, done once
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
fixedComparisons++;
if (trials.get(i).dominates(trials.get(j))) {
dominated[i].add(j);
dominationCount[j]++;
} else if (trials.get(j).dominates(trials.get(i))) {
dominated[j].add(i);
dominationCount[i]++;
}
// else: non-dominated pair (no relationship)
}
}
// Kahn-style front extraction: O(N + E)
List<Integer> currentFront = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (dominationCount[i] == 0) currentFront.add(i);
}
int rank = 0;
while (!currentFront.isEmpty()) {
List<Integer> nextFront = new ArrayList<>();
for (int i : currentFront) {
ranks[i] = rank;
for (int j : dominated[i]) {
dominationCount[j]--;
if (dominationCount[j] == 0) nextFront.add(j);
}
}
rank++;
currentFront = nextFront;
}
return new Result(ranks, fixedComparisons);
}
// -----------------------------------------------------------------------
// Build test populations
// -----------------------------------------------------------------------
// All trials in a single front (incomparable): values along a Pareto front
// trial i: (i/(N-1), (N-1-i)/(N-1)) — all on Pareto front
static List<Trial> singleFrontPopulation(int n) {
List<Trial> trials = new ArrayList<>();
for (int i = 0; i < n; i++) {
double v0 = (double) i / (n - 1);
double v1 = (double) (n - 1 - i) / (n - 1);
trials.add(new Trial(i, v0, v1));
}
return trials;
}
// All trials in distinct fronts (total order): trial i dominates all j > i
// trial i: (i, i) — completely ordered
static List<Trial> totalOrderPopulation(int n) {
List<Trial> trials = new ArrayList<>();
for (int i = 0; i < n; i++) {
trials.add(new Trial(i, (double) i, (double) i));
}
return trials;
}
// Mixed population with known structure
static List<Trial> mixedPopulation() {
return Arrays.asList(
new Trial(0, 0.1, 0.9), // front 0
new Trial(1, 0.5, 0.5), // front 0
new Trial(2, 0.9, 0.1), // front 0
new Trial(3, 0.3, 0.8), // dominated by (0.1, 0.9)? No — 0.3>0.1, 0.8<0.9 → incomparable
new Trial(4, 0.2, 1.0), // dominated by trial0: 0.2>0.1, 1.0>0.9 → NOT dominated
new Trial(5, 0.5, 0.6), // dominated by trial1: 0.5=0.5, 0.6>0.5 → dominated by trial1
new Trial(6, 0.8, 0.3) // dominated by trial2: 0.8<0.9, 0.3>0.1 → not dominated by trial2
);
}
public static void main(String[] args) {
System.out.println("=== optuna-0001: O(N³) non-dominated sort in _calculate_nondomination_rank ===");
// ------ Test 1: correctness on known population ------
{
List<Trial> pop = mixedPopulation();
Result def = runDefective(pop);
Result fix = runFixed(pop);
System.out.println(" Mixed population ranks (defective): " + Arrays.toString(def.ranks));
System.out.println(" Mixed population ranks (fixed): " + Arrays.toString(fix.ranks));
check("mixed: both assign same ranks", Arrays.equals(def.ranks, fix.ranks));
// trial5 should be dominated (rank > 0): trial1 (0.5,0.5) dominates trial5 (0.5,0.6)
check("mixed: trial5 is not on front 0", def.ranks[5] > 0);
}
// ------ Test 2: single front — all rank 0 ------
{
int N = 20;
List<Trial> pop = singleFrontPopulation(N);
Result def = runDefective(pop);
Result fix = runFixed(pop);
check("single-front-20: all rank 0 (defective)", Arrays.stream(def.ranks).allMatch(r -> r == 0));
check("single-front-20: all rank 0 (fixed)", Arrays.stream(fix.ranks).allMatch(r -> r == 0));
check("single-front-20: same ranks", Arrays.equals(def.ranks, fix.ranks));
}
// ------ Test 3: total order — rank i for trial i ------
{
int N = 20;
List<Trial> pop = totalOrderPopulation(N);
Result def = runDefective(pop);
Result fix = runFixed(pop);
for (int i = 0; i < N; i++) {
check("total-order-20: trial " + i + " has rank " + i + " (defective)", def.ranks[i] == i);
check("total-order-20: trial " + i + " has rank " + i + " (fixed)", fix.ranks[i] == i);
}
check("total-order-20: same ranks", Arrays.equals(def.ranks, fix.ranks));
}
// ------ Test 4: O(N³) vs O(N²) scaling on total-order population ------
{
int N = 100;
List<Trial> pop = totalOrderPopulation(N);
Result def = runDefective(pop);
Result fix = runFixed(pop);
long ratio = def.comparisons / Math.max(fix.comparisons, 1);
System.out.printf(" N=%d total-order: defective comparisons=%d, fixed comparisons=%d, ratio=%dx%n",
N, def.comparisons, fix.comparisons, ratio);
// Defective on total order (worst case): F=N fronts, each scan is O(remaining²)
// Total: sum_{f=0}^{N-1} (N-f)² ≈ N³/3
// For N=100: ~333,333 comparisons
check("N=100 total-order: defective comparisons > N*(N-1)/2 (super-linear evidence)",
def.comparisons > (long) N * (N - 1) / 2);
// Fixed: exactly N*(N-1)/2 pairwise comparisons (build graph once)
check("N=100 total-order: fixed comparisons == N*(N-1)/2",
fix.comparisons == (long) N * (N - 1) / 2);
check("N=100: fixed uses fewer comparisons than defective",
fix.comparisons <= def.comparisons);
check("N=100: same ranks", Arrays.equals(def.ranks, fix.ranks));
}
// ------ Test 5: cubic growth evidence ------
{
// Compare N=30 and N=60 on total-order (worst case for defective)
List<Trial> pop30 = totalOrderPopulation(30);
List<Trial> pop60 = totalOrderPopulation(60);
Result def30 = runDefective(pop30);
Result def60 = runDefective(pop60);
double growthRatio = (double) def60.comparisons / Math.max(def30.comparisons, 1);
System.out.printf(" Defective: N=30 comparisons=%d, N=60 comparisons=%d, growth=%.1fx%n",
def30.comparisons, def60.comparisons, growthRatio);
// Cubic: 2^3 = 8× growth expected
check("cubic growth: def60.comparisons > 4x def30.comparisons", growthRatio > 4.0);
Result fix30 = runFixed(pop30);
Result fix60 = runFixed(pop60);
double fixGrowth = (double) fix60.comparisons / Math.max(fix30.comparisons, 1);
System.out.printf(" Fixed: N=30 comparisons=%d, N=60 comparisons=%d, growth=%.1fx%n",
fix30.comparisons, fix60.comparisons, fixGrowth);
// Quadratic: 2^2 = 4× growth expected
check("quadratic growth: fix60.comparisons ≈ 4x fix30.comparisons",
fixGrowth >= 3.5 && fixGrowth <= 4.5);
}
System.out.println("All tests PASS.");
}
}