Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
### Added

- Optional **TypeSafe / Jev System One** pass (`tools/typesafe-pareto`) for Score + Choice judgments over a small Pareto candidate sample. Additive semantic layer — does not replace NSGA-III / U-NSGA-III objectives. Live calls only when `TYPESAFE_API_KEY` is set; CI uses mocks. Metrics append to `metrics/typesafe-runs.jsonl`.
- `ReferenceDirections.TwoLayerDasDennis`: outer Das–Dennis layer plus an inside layer scaled toward the centroid (Deb & Jain, NSGA-III Part I; default scale 0.5). Duplicates are removed. Single-layer `DasDennis` / `WithDasDennis` stays the default. M≤3 oracles do not need the second layer.
- Constrained problems `OsyProblem`, `TnkProblem`, and `C1Dtlz1Problem`, with formulation and survival self-tests. No IGD table.
- Opt-in `MatingPoolMode.TwoShuffledPasses`: two shuffled passes of consecutive tournaments (Seada & Deb Algorithm 1 and pymoo 0.6.2 `TournamentSelection`, pressure 2). When N parents are drawn from N members, each member is a contestant twice. The default pool stays N independent tournaments with replacement, which is the published ZDT1 Wilcoxon protocol.
- Opt-in `TournamentMode.Algorithm2`: a same-niche perpendicular-distance tie keeps the second parent. `PymooCompatible` still coin-flips. `RankNicheDistance` is unchanged.

### Fixed

- Odd population sizes no longer skip `parents[N-1]` in SBX. The leftover parent is paired with parent 0. Even-N pair order is unchanged (ZDT1 pop 52, DTLZ2 pop 92).

### Changed

- Environmental selection follows pymoo `Survival.filter_infeasible` (Jain & Deb, NSGA-III Part II). Feasible members are niched. If fewer than N are feasible, the rest are filled by ascending constraint violation and are not assigned a niche. When any member is feasible, ideal, worst, and ASF extremes come from feasible objectives. An all-infeasible generation does not move the hyperplane. This replaces the locked whole-pool ideal fixture.
- Docs and XML comments describe `initialPopulation` and hybrid loops in generic terms (domain-adapter warm-start / grid-seed). No product-repo names.

### Fixed

- `CompareConstraintDominated` no longer falls through to Pareto when two infeasible individuals share a constraint violation. Deb's constraint-domination leaves them mutually non-dominated. Tournament comparators are unchanged.
- Odd population sizes no longer skip `parents[N-1]` in SBX. The leftover parent is paired with parent 0. Even-N pair order is unchanged (ZDT1 pop 52, DTLZ2 pop 92).

## [0.1.5] — 2026-09-22

Forensic-audit wave (G1–G10): document known gaps against pymoo and Seada & Deb, and lock them with tests. The published 15-seed table is not rewritten. **Default tournament stays `RankNicheDistance`.**
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,14 @@ var algo = new Unsga3Algorithm(dirs, populationSize: 92, seed: 1,
| `Unsga3Algorithm` | Main entry — `Run(problem, gens)` |
| `Individual` | Variables / Objectives / Constraints |
| `OptimizationResult` | Final population + non-dominated set |
| `ReferenceDirections.DasDennis` | Structured reference points |
| `ReferenceDirections.DasDennis` | Single-layer reference points (default) |
| `ReferenceDirections.TwoLayerDasDennis` | Outer layer plus an inside layer for larger M |
| `SimulatedBinaryCrossover` / `PolynomialMutation` | Variation operators |
| `PerformanceIndicators` | IGD, GD, 2-D hypervolume |
| `TournamentMode` | Default rank→niche, `PymooCompatible` (distance-tie coin flip), or opt-in `Algorithm2` (keep the second parent) |
| `MatingPoolMode` | Default independent tournaments; opt-in `TwoShuffledPasses` (paper / pymoo 0.6.2) |

Built-in problems: ZDT1–4/6, DTLZ1–4/7, Sphere, Ackley, Rosenbrock.
Built-in problems: ZDT1–4/6, DTLZ1–4/7, Sphere, Ackley, Rosenbrock, and constrained OSY, TNK, and C1-DTLZ1 (self-tests only; no IGD table yet).

## Build & test

Expand Down Expand Up @@ -154,6 +155,7 @@ Algorithm papers (please cite these when publishing results):

- Seada, H. & Deb, K. (2016). *A Unified Evolutionary Optimization Procedure for Single, Multiple, and Many Objectives.* IEEE Trans. Evol. Comput.
- Deb, K. & Jain, H. (2014). *An Evolutionary Many-Objective Optimization Algorithm Using Reference-Point-Based Nondominated Sorting Approach (NSGA-III), Part I.* IEEE Trans. Evol. Comput.
- Jain, H. & Deb, K. (2014). *NSGA-III, Part II: Handling Constraints and Extending to an Adaptive Approach.* IEEE Trans. Evol. Comput.
- Das, I. & Dennis, J. E. (1998). *Normal-Boundary Intersection.* SIAM J. Optim.

## License
Expand Down
13 changes: 9 additions & 4 deletions docs/EQUIVALENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ See also **[RESEARCH-STANDARDS.md](RESEARCH-STANDARDS.md)** for the literature +
- COIN Report 2014022; Seada & Deb, IEEE TEVC 2016
- [pymoo `UNSGA3`](https://pymoo.org/algorithms/moo/unsga3.html)
- pymoo `nsga3.py` — `HyperplaneNormalization`, `associate_to_niches`, `niching`
- pymoo `Survival.filter_infeasible` — feasible niching, then ascending-CV fill (Jain & Deb, NSGA-III Part II)
- Indicators: [pymoo performance indicators](https://pymoo.org/misc/indicators.html) (GD, IGD, IGD+, HV)

## Protocol
Expand Down Expand Up @@ -44,12 +45,13 @@ ZDT2 **gens=100** is an early-stress snapshot (collapse on Bend, C#, and pymoo),
| Bi | ZDT1–4, ZDT6 | 2 | yes |
| Many | DTLZ1–4, DTLZ7 | 3+ | yes |
| Hard | WFG1, WFG2, WFG9 | 3–5 | planned |
| Constrained | OSY, TNK, C1-DTLZ1 | 2–3 | planned |
| Constrained | OSY, TNK, C1-DTLZ1 | 2–3 | yes (self-tests; no IGD table) |

## Unit checks

- Reference association & niche counts
- Non-dominated ranks / constraint domination
- Non-dominated ranks / constraint domination (equal CV is mutual non-domination)
- Feasible niching, CV fill, and OSY / TNK / C1-DTLZ1 (`ConstrainedSurvivalTests`, `ConstrainedProblemTests`). No IGD table.
- **Normalization intercepts / ASF axis extremes** (`NormalizationTests`)
- Tournament pressure
- `populationSize: null` ⇒ `|refs|`
Expand All @@ -71,14 +73,17 @@ ZDT2 **gens=100** is an early-stress snapshot (collapse on Bend, C#, and pymoo),
| Mating pool (`TwoShuffledPasses`, opt-in) | two shuffled passes of consecutive pairs. When N parents are drawn from N members, each index is a contestant twice | pymoo 0.6.2 `TournamentSelection` (pressure 2) **is** this pool: `n_perms = ceil(2N / N)` permutations, then consecutive pairs |
| Niche ids at mating | `PrepareForSelection` re-normalizes survivors and re-associates | ids written during survival are kept |
| `WithDasDennis(1, 1)` | throws. One objective has a single direction, and N must be ≥ 2 | pass `populationSize` ≥ 2 for the single-objective degeneration |
| Reference layers | single-layer Das–Dennis only. Two-layer directions are absent | many-objective NSGA-III adds an inside layer for larger M |
| Reference layers | `DasDennis` / `WithDasDennis` stay single-layer (the M≤3 oracles). `TwoLayerDasDennis` adds Part I's inside layer, default scale 0.5, duplicates removed | multi-layer factory; NSGA-III consumes whatever `ref_dirs` it is given |
| Duplicate elimination | default **on**. Key is `G12` (12 significant digits), not 12 decimal places. Attempts are capped when mutation cannot produce a new key; remaining slots may be duplicates | `eliminate_duplicates=True` |
| Survival RNG | optional RNG niche pick | random among equal niches |
| IGD | **mean** nearest distance | same (verified pymoo 0.6.2) |
| Scored set | full non-dominated front | `res.F` niche optimum |
| Hyperplane norm | persistent ideal, ND extremes, correct ASF | `HyperplaneNormalization` |
| Collapsed nadir | if the span is still ≤ 1e-6, nadir = ideal + 1 | stop at worst-of-population |
| Infeasible points in the hyperplane | ideal and worst from the whole pool, including infeasible points. ASF extremes use the ND index set when supplied | pymoo niching can restrict the normalized set to feasible members |
| Constrained survival | Niche feasible members only. If fewer than N are feasible, fill by ascending CV (original index breaks ties) with no niche assignment. All-infeasible generations truncate by CV | `Survival.filter_infeasible` |
| Feasible hyperplane | Ideal, worst, and ASF use feasible objectives when any feasible member exists. An all-infeasible generation does not move the hyperplane | `HyperplaneNormalization.update` sees the feasible subset passed by `filter_infeasible` |
| Equal CV in the sort | Two infeasible individuals with the same violation are mutually non-dominated (Deb). Objectives are not compared | constraint-domination; CV ordering is separate from Pareto |
| Tournament equal CV | unchanged. `RankNicheDistance` still falls through to rank, niche, and distance. `PymooCompatible` still coin-flips | pymoo coin-flips |

## Mating pool

Expand Down
3 changes: 2 additions & 1 deletion docs/NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ It is **not** affiliated with, endorsed by, or derived from the source code of p
| Work | Role in Unsga3 |
|------|----------------|
| Seada, H. & Deb, K. (2016). *A Unified Evolutionary Optimization Procedure for Single, Multiple, and Many Objectives.* IEEE TEVC. | U-NSGA-III algorithm |
| Deb, K. & Jain, H. (2014). *NSGA-III.* IEEE TEVC. | Reference-point survival, hyperplane normalization |
| Deb, K. & Jain, H. (2014). *An Evolutionary Many-Objective Optimization Algorithm Using Reference-Point-Based Nondominated Sorting Approach, Part I: Solving Problems With Box Constraints.* IEEE TEVC. | Reference directions (including the inside layer), hyperplane normalization, niching |
| Jain, H. & Deb, K. (2014). *An Evolutionary Many-Objective Optimization Algorithm Using Reference-Point-Based Nondominated Sorting Approach, Part II: Handling Constraints and Extending to an Adaptive Approach.* IEEE TEVC. | Feasible-only niching and constraint-violation fill |
| Das, I. & Dennis, J. E. (1998). *Normal-Boundary Intersection.* SIAM J. Optim. | Das–Dennis reference directions |
| Deb, K. et al. ZDT / DTLZ suites | Standard test problems |
| Deb & Agrawal — SBX; Deb et al. — polynomial mutation | Variation operators |
Expand Down
2 changes: 1 addition & 1 deletion docs/ORACLE-RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ ZDT2 quality A/B is gens=250 + `PymooCompatible` (not the loose smoke bar). ZDT1
| IGD mean-distance | **aligned** |
| ASF / axis intercepts | **aligned** |
| Collapsed nadir (span ≤ 1e-6) | **delta**: nadir = ideal + 1 after the worst-of-pop fallback. pymoo 0.6.2 stops at worst-of-pop. Locked by `Collapsed_span_sets_nadir_to_ideal_plus_one` (`{2, 2+1e-8}` → nadir 3). |
| Infeasible points | **current rule, locked**: ideal and worst include them. Fixture is feasible (1, 1) vs infeasible (0, 0) → ideal (0, 0). No constrained benchmark yet. |
| Infeasible points | **feasible hyperplane**. Ideal, worst, and ASF use feasible objectives when any feasible member exists. An all-infeasible generation does not move the hyperplane. Survival niches the feasible set and fills a shortfall by ascending CV. The old whole-pool fixture (feasible (1, 1) vs infeasible (0, 0) → ideal (0, 0)) is replaced. OSY, TNK, and C1-DTLZ1 have self-tests. No constrained IGD table. |
| Mating re-association | **delta**: after survival, `PrepareForSelection` normalizes the survivors again and overwrites niche ids. pymoo keeps the survival ids. Fixture: `PrepareForSelection_overwrites_survival_niche_ids`. |
| Persistent ideal + ND extremes | **aligned** |
| `TournamentMode.PymooCompatible` | **implemented** |
Expand Down
4 changes: 2 additions & 2 deletions docs/RESEARCH-STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Sources consulted (2026-08):
| Bi-objective | ZDT1–4, ZDT6 | Convex / non-convex / disconnected / multimodal / biased density |
| Many-objective | DTLZ1–4, DTLZ7 | Linear simplex, sphere, multimodal sphere, bias, disconnected |
| Hard (later) | WFG1–9 | Non-separable, scaled, deceptive |
| Constrained (later) | OSY, TNK, C1-DTLZ1 | Constraint-domination path |
| Constrained | OSY, TNK, C1-DTLZ1 | In the library, with survival self-tests. No IGD table until measured runs exist |

**Seada selection paper** stresses ZDT4 (many local fronts) and DTLZ1 for M=3,5.

Expand All @@ -35,7 +35,7 @@ Default dimensions (Deb / pymoo convention):
|------|----------------|
| Crossover | SBX, η_c = **30**, p_c = **1.0** (pymoo). Paper section 4 uses p_c = **0.9** |
| Mutation | Polynomial, η_m = **20**, p_m = **1/n** |
| Reference set | **Das–Dennis** (uniform) on the unit simplex, **single layer**. Two-layer directions for larger M are absent |
| Reference set | **Das–Dennis** on the unit simplex. **Single layer is the default** (`DasDennis`, `WithDasDennis`, M≤3 oracles). `TwoLayerDasDennis` adds Part I's inside layer (default scale 1/2) for larger M |
| Population size | Often = #reference directions (or slightly larger). N ≥ 2. `WithDasDennis(1, 1)` throws because |H| = 1; pass an explicit population size for single-objective runs |
| Selection | U-NSGA-III **tournament** (not NSGA-III random mating) |

Expand Down
3 changes: 2 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Living plan for Unsga3. Issues track concrete work; this page is the narrative.
- [ ] `net8.0` (+ `net10.0`) multi-target for broader NuGet consumers
- [ ] nuget.org publish (in addition to GitHub Packages)
- [ ] GD+ indicator (IGD+ has a hand-case test; GD and IGD were already implemented)
- [ ] Constrained demos (OSY / TNK) with self-tests
- [x] Constrained demos (OSY, TNK, C1-DTLZ1) with formulation and survival self-tests
- [ ] Constrained IGD / Wilcoxon vs pymoo (no table until measured runs exist)
- [ ] API docs site (DocFX or similar)

## Medium term (0.3+)
Expand Down
4 changes: 3 additions & 1 deletion src/Unsga3/Algorithm/Unsga3Algorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@
}

/// <summary>
/// Convenience: build Das–Dennis directions then construct the algorithm.
/// Convenience: build single-layer Das–Dennis directions then construct the algorithm.
/// Two-layer directions are <see cref="ReferenceDirections.TwoLayerDasDennis"/>, passed
/// to the constructor; M≤3 oracles stay on this single-layer factory.
/// One objective produces a single direction, so the default population size is 1.
/// The constructor requires N ≥ 2, and <c>WithDasDennis(1, 1)</c> throws.
/// Single-objective runs must pass <paramref name="populationSize"/> ≥ 2
Expand Down Expand Up @@ -118,8 +120,8 @@
/// are pad-filled randomly.
/// </param>
public OptimizationResult Run(
IProblem problem,

Check warning on line 123 in src/Unsga3/Algorithm/Unsga3Algorithm.cs

View workflow job for this annotation

GitHub Actions / build-test

Parameter 'problem' has no matching param tag in the XML comment for 'Unsga3Algorithm.Run(IProblem, int, IReadOnlyList<Individual>?)' (but other parameters do)

Check warning on line 123 in src/Unsga3/Algorithm/Unsga3Algorithm.cs

View workflow job for this annotation

GitHub Actions / build-test

Parameter 'problem' has no matching param tag in the XML comment for 'Unsga3Algorithm.Run(IProblem, int, IReadOnlyList<Individual>?)' (but other parameters do)
int maxGenerations,

Check warning on line 124 in src/Unsga3/Algorithm/Unsga3Algorithm.cs

View workflow job for this annotation

GitHub Actions / build-test

Parameter 'maxGenerations' has no matching param tag in the XML comment for 'Unsga3Algorithm.Run(IProblem, int, IReadOnlyList<Individual>?)' (but other parameters do)

Check warning on line 124 in src/Unsga3/Algorithm/Unsga3Algorithm.cs

View workflow job for this annotation

GitHub Actions / build-test

Parameter 'maxGenerations' has no matching param tag in the XML comment for 'Unsga3Algorithm.Run(IProblem, int, IReadOnlyList<Individual>?)' (but other parameters do)
IReadOnlyList<Individual>? initialPopulation) =>
Run(problem, TerminationCriterion.MaxGenerations(maxGenerations), initialPopulation);

Expand Down
5 changes: 4 additions & 1 deletion src/Unsga3/Core/NonDominatedSort.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ public static int CompareConstraintDominated(Individual a, Individual b)
{
if (a.ConstraintViolation < b.ConstraintViolation) return -1;
if (a.ConstraintViolation > b.ConstraintViolation) return 1;
// equal violation → fall through to objective comparison
// Deb constraint-domination: equal violation ⇒ neither dominates.
// Objectives are not consulted. Tournament comparators are separate
// and still break an equal-CV tie their own way.
return 0;
}

return ComparePareto(a.Objectives, b.Objectives);
Expand Down
Loading
Loading