293 lines
12 KiB
Java
293 lines
12 KiB
Java
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<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.");
|
||
}
|
||
}
|