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: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"docfx": {
"version": "2.81.0",
"commands": [
"docfx"
]
}
}
}
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ jobs:

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
dotnet-version: |
8.0.x
10.0.x

- name: Restore
run: dotnet restore Unsga3.slnx
Expand Down Expand Up @@ -53,3 +55,20 @@ jobs:

- name: Smoke (mock)
run: python tools/typesafe-pareto/score_pareto.py --smoke --force-mock --metrics metrics/typesafe-runs.jsonl

api-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- name: Restore DocFX
run: dotnet tool restore

- name: Build API docs
run: dotnet docfx docfx.json
4 changes: 3 additions & 1 deletion .github/workflows/publish-github-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ jobs:

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
dotnet-version: |
8.0.x
10.0.x
source-url: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
env:
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ metrics/typesafe-runs.jsonl
appsettings.*.local.json
.env
.env.*

# DocFX (regenerate with `dotnet tool restore && dotnet docfx docfx.json`)
_site/
/api/
4 changes: 2 additions & 2 deletions .grok/skills/unsga3-oracle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ Repo root: Unsga3. Confirm `Unsga3.slnx` / `tools/OracleCompare` exist before ru
| zdt2 | 12 | 52 | **250** | `--pymoo-mode` (`PymooCompatible`) |
| dtlz2 | 12 | 92 | 150 | `--pymoo-mode` (`PymooCompatible`) |

DTLZ2: C# `Dtlz2Problem(k: 10)` ⇒ **n_var=12**. `run_pymoo_oracle.py` passes `n_var=12`. pymoo’s own default is n_var=10 (k=8). Do not treat the published 15-seed pymoo column as that matched run, and do not rewrite `docs/WILCOXON-RESULTS.md` until the seeds are re-run.
DTLZ2: C# `Dtlz2Problem(k: 10)` ⇒ **n_var=12**. `run_pymoo_oracle.py` and `run_multiseed_wilcoxon.py` pass `n_var=12`. pymoo’s own default is n_var=10 (k=8). The checked-in `docs/WILCOXON-RESULTS.md` DTLZ2 columns are that matched re-run. Rewrite that file only by re-running the harness. Do not paste a remembered n_var=10 column back in.

ZDT2 **gens=100** is an early-stress snapshot (collapse on Bend, C#, and pymoo), not the quality bar. Quality protocol matches unsga3-bend A/B (gens=250, PymooCompatible). `RankNicheDistance` is an optional unpublished Wilcoxon ZDT2 mating mode — do not silently switch all ZDT defaults to it. ZDT1 and DTLZ2 unchanged.

IGD = **mean** nearest Euclidean distance (pymoo-compatible). C# scores the full non-dominated front; `run_pymoo_oracle.py` scores pymoo `res.F` (niche optimum). Compare them only on a shared front definition and a shared reference set. Docs: `docs/EQUIVALENCE.md`, `docs/RESEARCH-STANDARDS.md`.

Requires: .NET 10 SDK; Python 3 + `pip install pymoo` for pymoo side / multi-seed.
Requires: .NET 10 SDK to run `tools/OracleCompare`. A full `dotnet test` of both library TFMs also needs the .NET 8 SDK. Python 3 + `pip install pymoo` for the pymoo side / multi-seed.

## Modes

Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Added

- Measured 15-seed tables for OSY, TNK, and C1-DTLZ1 (feasible non-dominated IGD vs pymoo `UNSGA3`) and for mating opt-ins (`TwoShuffledPasses`, `Algorithm2`, and both) in `docs/NEW-SURFACES-RESULTS.md`. Protocol is in `docs/EQUIVALENCE.md`. Package defaults are unchanged. `docs/WILCOXON-RESULTS.md` is not rewritten.
- `PerformanceIndicators.GenerationalDistancePlus` (Ishibuchi GD+): mean modified distance from each obtained point to the nearest reference point. Hand-case unit tests. No new benchmark IGD table.
- API reference via DocFX. From a clean checkout: `dotnet tool restore && dotnet docfx docfx.json`. CI job `api-docs` builds `_site/`.
- Measured 15-seed tables for OSY, TNK, and C1-DTLZ1 (feasible non-dominated IGD vs pymoo `UNSGA3`) and for mating opt-ins (`TwoShuffledPasses`, `Algorithm2`, and both) in `docs/NEW-SURFACES-RESULTS.md`. Protocol is in `docs/EQUIVALENCE.md`. Package defaults are unchanged.
- 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`.

### Changed

- The library, tests, and BasicUsage sample multi-target `net8.0` and `net10.0`. `tools/OracleCompare` stays `net10.0`. GitHub Packages is still the only publish target.
- `docs/WILCOXON-RESULTS.md` is the matched DTLZ2 re-run (`n_var=12` on C# and pymoo). ZDT2 stays out of that table (quality bar is gens=250 + `PymooCompatible`). No invented ZDT2 Wilcoxon numbers.
- Docs and XML comments describe `initialPopulation` and hybrid loops in generic terms (domain-adapter warm-start / grid-seed). No product-repo names.

## [0.2.0] — 2026-09-30
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ Thanks for your interest. This library aims to be a **faithful, well-tested** U-

## Development setup

- **.NET 10 SDK** ([download](https://dotnet.microsoft.com/download))
- **.NET 8 SDK and .NET 10 SDK** ([download](https://dotnet.microsoft.com/download)). The library, tests, and sample target both. `tools/OracleCompare` is `net10.0` only.
- Optional (oracle / multi-seed stats): Python 3.10+ with `pip install pymoo`
- Optional (API docs): `dotnet tool restore` then `dotnet docfx docfx.json` (DocFX is pinned in `.config/dotnet-tools.json`)
- Optional (TypeSafe / Jev Pareto scores): Python 3.10+; live calls need `TYPESAFE_API_KEY` (never commit it)

```bash
git clone https://github.com/AppSprout-dev/Unsga3.git
cd Unsga3
dotnet build Unsga3.slnx -c Release
dotnet test Unsga3.slnx -c Release
dotnet run --project samples/BasicUsage -c Release
dotnet run --project samples/BasicUsage -c Release -f net10.0
```

### Oracle / equivalence (optional)
Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

[![ci](https://github.com/AppSprout-dev/Unsga3/actions/workflows/ci.yml/badge.svg)](https://github.com/AppSprout-dev/Unsga3/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![.NET](https://img.shields.io/badge/.NET-10.0-purple.svg)](https://dotnet.microsoft.com/)
[![.NET](https://img.shields.io/badge/.NET-8.0%20%7C%2010.0-purple.svg)](https://dotnet.microsoft.com/)

**U-NSGA-III** (Unified NSGA-III) for .NET — single-, multi-, and many-objective evolutionary optimization with Das–Dennis reference directions, SBX crossover, polynomial mutation, and **niching-based tournament selection** ([Seada & Deb, 2016](https://ieeexplore.ieee.org/document/7271063)).

> **v0.2.0** — opt-in two-shuffle mating, Algorithm 2 tournament, odd-N pairing, feasible-only niching, equal-CV Deb fix, two-layer Das–Dennis, and OSY/TNK/C1-DTLZ1. Defaults stay unconstrained `RankNicheDistance`, independent mating, and single-layer Das–Dennis. The published 15-seed table is not rewritten.
> **15-seed IGD vs pymoo `UNSGA3`:** ZDT1 **median 0.053 vs 0.070** (MWU *p*≈0.05) compares the full C# non-dominated front with pymoo `res.F`. DTLZ2 **median 0.0045 vs 0.0028** (~1.6×) compares C# n_var=12 with pymoo's default n_var=10. Neither pair is a same-set, same-problem ranking. Notes: [`docs/ORACLE-RESULTS.md`](docs/ORACLE-RESULTS.md).
> **v0.2.0** — opt-in two-shuffle mating, Algorithm 2 tournament, odd-N pairing, feasible-only niching, equal-CV Deb fix, two-layer Das–Dennis, and OSY/TNK/C1-DTLZ1. Defaults stay unconstrained `RankNicheDistance`, independent mating, and single-layer Das–Dennis. Unreleased work adds `net8.0`+`net10.0`, GD+, and DocFX.
> **15-seed IGD vs pymoo `UNSGA3`:** ZDT1 **median 0.053173 vs 0.0695884** (MWU *p*=0.0512394) compares the full C# non-dominated front with pymoo `res.F`. DTLZ2 **median 0.00451168 vs 0.00388327** (ratio 1.16183, MWU *p*=0.0225311) uses **n_var=12** on both sides. ZDT2 has no Wilcoxon table. Notes: [`docs/WILCOXON-RESULTS.md`](docs/WILCOXON-RESULTS.md).
> Details: [`docs/WILCOXON-RESULTS.md`](docs/WILCOXON-RESULTS.md) · single-seed notes: [`docs/ORACLE-RESULTS.md`](docs/ORACLE-RESULTS.md) · constrained + mating opt-ins: [`docs/NEW-SURFACES-RESULTS.md`](docs/NEW-SURFACES-RESULTS.md)

```text
Expand Down Expand Up @@ -76,7 +76,7 @@ var algo = new Unsga3Algorithm(dirs, populationSize: 92, seed: 1,
| `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 |
| `PerformanceIndicators` | IGD, IGD+, GD, 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) |

Expand All @@ -87,10 +87,19 @@ Built-in problems: ZDT1–4/6, DTLZ1–4/7, Sphere, Ackley, Rosenbrock, and cons
```bash
dotnet build Unsga3.slnx -c Release
dotnet test Unsga3.slnx -c Release
dotnet run --project samples/BasicUsage -c Release
dotnet run --project samples/BasicUsage -c Release -f net10.0
```

Requires **.NET 10** SDK. Optional oracle: Python 3 + `pip install pymoo` (see [CONTRIBUTING.md](CONTRIBUTING.md)).
The library, tests, and sample target **`net8.0` and `net10.0`**. Building both needs the .NET 8 and .NET 10 SDKs. `tools/OracleCompare` is `net10.0` only. Optional oracle: Python 3 + `pip install pymoo` (see [CONTRIBUTING.md](CONTRIBUTING.md)).

API reference (DocFX, local tool):

```bash
dotnet tool restore
dotnet docfx docfx.json
```

That writes `_site/` and does not publish a package.

## TypeSafe / Jev Pareto scoring (optional)

Expand Down Expand Up @@ -141,6 +150,7 @@ Unsga3/
├── tools/typesafe-pareto/ # optional TypeSafe / Jev Score+Choice on a front sample
├── metrics/ # typesafe-runs.jsonl (local; gitignored)
├── docs/
├── docfx.json # API docs (`dotnet docfx docfx.json`)
└── .github/workflows/ # CI + GitHub Packages publish
```

Expand Down
47 changes: 47 additions & 0 deletions docfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"metadata": [
{
"src": [
{
"src": ".",
"files": [
"src/Unsga3/Unsga3.csproj"
]
}
],
"dest": "api",
"includePrivateMembers": false,
"properties": {
"TargetFramework": "net10.0"
}
}
],
"build": {
"content": [
{
"files": [
"toc.yml",
"docs/docfx/index.md",
"docs/docfx/toc.yml"
]
},
{
"files": [
"api/**.yml",
"api/index.md"
]
}
],
"output": "_site",
"template": [
"default",
"modern"
],
"globalMetadata": {
"_appName": "Unsga3",
"_appTitle": "Unsga3",
"_enableSearch": true,
"_disableContribution": true
}
}
}
10 changes: 5 additions & 5 deletions docs/EQUIVALENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ See also **[RESEARCH-STANDARDS.md](RESEARCH-STANDARDS.md)** for the literature +
- [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)
- Indicators: [pymoo performance indicators](https://pymoo.org/misc/indicators.html) (GD, GD+, IGD, IGD+, HV)

## Protocol

Expand All @@ -24,7 +24,7 @@ See also **[RESEARCH-STANDARDS.md](RESEARCH-STANDARDS.md)** for the literature +
Score the **same front definition** and the **same reference set**. C# reports the full non-dominated front. pymoo `res.F` is the survival niche set (about one point per filled direction). `ReferenceDirectionThinning.OnePerDirection` can match cardinality; it does not reproduce `res.F` and is not a parity claim.
6. **Tolerance:** do not read the published table as median IGD within 1–2% of pymoo.
ZDT1 median ratio 0.764107, Mann–Whitney U = 65, p = 0.0512394 (the pymoo column is `res.F`).
DTLZ2 median ratio 1.58638, U = 222, p = 6.15164×10⁻⁶ (the pymoo column is n_var=10). That comparison rejects equal distributions at α = 0.05.
DTLZ2 median ratio 1.16183, U = 168, p = 0.0225311 (both columns are n_var=12). That comparison rejects equal distributions at α = 0.05.
The CI guard is 2× the published mismatched DTLZ2 scalar 0.00350, which fails a regression to about 2.9×. It is not a same-problem equivalence claim.

Published A/B budgets (ZDT1 / DTLZ2 unchanged; ZDT2 matches unsga3-bend protocol honesty):
Expand Down Expand Up @@ -55,7 +55,7 @@ ZDT2 **gens=100** is an early-stress snapshot (collapse on Bend, C#, and pymoo),
- **Normalization intercepts / ASF axis extremes** (`NormalizationTests`)
- Tournament pressure
- `populationSize: null` ⇒ `|refs|`
- IGD/HV formulas on hand-checked fronts (`MetricsTests`)
- IGD/GD/GD+/HV formulas on hand-checked fronts (`MetricsTests`)

## Automated IGD smoke (CI)

Expand Down Expand Up @@ -126,7 +126,7 @@ Same populations and generation counts as the quality protocols above. ZDT1 gens
| algorithm2 | `Algorithm2` | `IndependentWithReplacement` |
| both | `Algorithm2` | `TwoShuffledPasses` |

The ZDT2 and DTLZ2 default columns in that table are `RankNicheDistance`. The published DTLZ2 Wilcoxon column is `PymooCompatible`. ZDT2 has no published Wilcoxon column. Neither published table is rewritten.
The ZDT2 and DTLZ2 default columns in that table are `RankNicheDistance`. The published DTLZ2 Wilcoxon column is `PymooCompatible` at n_var=12. ZDT2 has no published Wilcoxon column. `run_new_surfaces.py` does not write `docs/WILCOXON-RESULTS.md`.

## DTLZ2 gap history

Expand All @@ -136,4 +136,4 @@ The ZDT2 and DTLZ2 default columns in that table are `RankNicheDistance`. The pu
| ASF + persistent ideal | 0.0052 | ~1.5× |
| + duplicate elimination | **0.0040** | **~1.15× vs pymoo n_var=10** |

The ~1.15× denominator is pymoo at **n_var=10** (k=8), not the C# problem (n_var=12, k=10). A matched seed-1 pair is recorded in [ORACLE-RESULTS.md](ORACLE-RESULTS.md). The 15-seed table is still the mismatched-k run.
The ~1.15× denominator is pymoo at **n_var=10** (k=8), not the C# problem (n_var=12, k=10). That history row is not the current 15-seed table. The current table is the matched n_var=12 re-run in [WILCOXON-RESULTS.md](WILCOXON-RESULTS.md) (median ratio 1.16183). A matched seed-1 pair is also in [ORACLE-RESULTS.md](ORACLE-RESULTS.md).
2 changes: 1 addition & 1 deletion docs/NEW-SURFACES-RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ IGD is the C# feasible non-dominated front against `ParetoFronts` (ZDT: 500 poin
| algorithm2 | Algorithm2 | IndependentWithReplacement |
| both | Algorithm2 | TwoShuffledPasses |

ZDT2 uses gens=250. Its default column is `RankNicheDistance`, not `PymooCompatible`. There is no published ZDT2 Wilcoxon column. DTLZ2's default column is also `RankNicheDistance`. The published DTLZ2 column is `PymooCompatible` and is not rewritten here.
ZDT2 uses gens=250. Its default column is `RankNicheDistance`, not `PymooCompatible`. There is no published ZDT2 Wilcoxon column. DTLZ2's default column is also `RankNicheDistance`. The published DTLZ2 Wilcoxon column is `PymooCompatible` at n_var=12. This file does not rewrite it.

Paired tests compare each opt-in with the default column (same seed index). Lower IGD is better. p-values are two-sided and not multiplicity-adjusted.

Expand Down
6 changes: 3 additions & 3 deletions docs/ORACLE-RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ On the shared 100-point PF, the full-front pair is C# 0.05119280568479224 and py
| 5 | 0.00466 |
| **mean** | **~0.00485** |

Those five C# seeds are `Dtlz2Problem(k: 10)` (n_var=12). The 0.0035 figure they were compared with is pymoo at **n_var=10** (k=8). That is not a same-problem band. The 15-seed file is unchanged until a matched re-run (see below).
Those five C# seeds are `Dtlz2Problem(k: 10)` (n_var=12). The 0.0035 figure they were compared with is pymoo at **n_var=10** (k=8). That is not a same-problem band. The current 15-seed file is the matched n_var=12 re-run (see below).

### DTLZ2 n_var (known mismatch, seed 1 remeasured)

`Dtlz2Problem(nObjectives: 3, k: 10)` builds **n = 12**. Deb et al. suggest k = 10. pymoo 0.6.2 `get_problem("dtlz2", n_obj=3)` defaults to **n_var=10** (k = 8). The harness used to omit `n_var`, so the published seed-1 pair and `docs/WILCOXON-RESULTS.md` compare those two dimensions. `tools/oracle/run_pymoo_oracle.py` now passes **n_var=12**.
`Dtlz2Problem(nObjectives: 3, k: 10)` builds **n = 12**. Deb et al. suggest k = 10. pymoo 0.6.2 `get_problem("dtlz2", n_obj=3)` defaults to **n_var=10** (k = 8). The harness used to omit `n_var`, so the historical seed-1 pair below compares those two dimensions. `tools/oracle/run_pymoo_oracle.py` and `tools/oracle/run_multiseed_wilcoxon.py` pass **n_var=12**. [WILCOXON-RESULTS.md](WILCOXON-RESULTS.md) is that matched 15-seed re-run.

Published mismatched seed 1 (already in the Wilcoxon table; not re-interpreted as parity):

Expand All @@ -94,7 +94,7 @@ Seed 1 remeasured **2026-09-22** with pymoo 0.6.2 after the oracle passes `n_var
| C# `PymooCompatible` | 12 | 10 | 0.00403168 | 0.004031675764658275 | 92 |
| pymoo `n_var=12` | 12 | 10 | 0.00308392 | 0.003083921253245871 | 91 |

Ratio of the two meta IGDs: 0.004031675764658275 / 0.003083921253245871 = **1.30732**. That is one seed, and the fronts still differ by one point (92 vs 91). It is not a 15-seed ranking and it does not replace the Wilcoxon table.
Ratio of the two meta IGDs: 0.004031675764658275 / 0.003083921253245871 = **1.30732**. That is one seed, and the fronts still differ by one point (92 vs 91). Seed 1 of the matched 15-seed table is this console pair. The 15-seed median ratio is **1.16183** ([WILCOXON-RESULTS.md](WILCOXON-RESULTS.md)).

## Root cause of the old ~5× DTLZ2 gap (fixed)

Expand Down
Loading
Loading