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 187–216 (_calculate_nondomination_rank) * optuna/study/_multi_objective.py lines 127–148 (_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 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 trials) { defectiveComparisons = 0; int n = trials.size(); int[] ranks = new int[n]; // Map original index for rank assignment List remaining = new ArrayList<>(); for (int i = 0; i < n; i++) remaining.add(i); int rank = 0; while (!remaining.isEmpty()) { List front = new ArrayList<>(); List 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 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[] 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 currentFront = new ArrayList<>(); for (int i = 0; i < n; i++) { if (dominationCount[i] == 0) currentFront.add(i); } int rank = 0; while (!currentFront.isEmpty()) { List 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 singleFrontPopulation(int n) { List 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 totalOrderPopulation(int n) { List 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 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 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 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 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 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 pop30 = totalOrderPopulation(30); List 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."); } }