Skip to content

Python-side dimension-limit checks, large-dimension tests, and 1-column igd support (#72)#73

Open
blankjul wants to merge 3 commits into
multi-objective:v0.3.2from
blankjul:dimension-limit-checks
Open

Python-side dimension-limit checks, large-dimension tests, and 1-column igd support (#72)#73
blankjul wants to merge 3 commits into
multi-objective:v0.3.2from
blankjul:dimension-limit-checks

Conversation

@blankjul

@blankjul blankjul commented Jul 2, 2026

Copy link
Copy Markdown

Closes the remaining checklist items from #72.

Commit 1 — Python-side dimension-limit checks + tests

Adds a _check_nobj() helper and per-function ValueErrors so that inputs beyond what the C library supports are rejected in Python instead of reaching C as undefined behavior:

Functions Limit Before this PR
igd, igd_plus, avg_hausdorff_dist, epsilon_additive, epsilon_mult, pareto_rank, is_nondominated, any_dominated 255 (range of dimension_t) 256 columns segfaulted (dimension_t wraparound)
hypervolume (function + class), hv_contributions, hv_approx 31 32 columns silently returned a wrong value (release build)
epsilon_additive, epsilon_mult at least 2 1 column read out of bounds and returned garbage (see below)

New test_dimension_limits.py:

  • large-dimension correctness at 32/64/128/255 columns for igd, igd_plus, epsilon_additive, pareto_rank, is_nondominated — verified against independent pure-numpy references (~0.3 s total),
  • error-raising tests at each limit (at-limit succeeds, limit+1 raises).

Note on epsilon: while testing the lower bound we found that epsilon_helper_ is unrolled over the first two dimensions (MAX(eps_value_agree_(0), eps_value_agree_(1)) unconditionally), so a 1-column input performs an out-of-bounds read and returns garbage — e.g. epsilon_additive returned 0.616 where the correct value is 0.009 (formula cross-checked against moocore at 2–3 columns). Reachable from Python in all released versions; now rejected with a clear error.

Commit 2 — relax igd.h to ASSUME(1 <= dim) (drop if you disagree)

The gd_common_helper_ loop is fully general in dim, so the 2 <= dim lower bound is purely conservative there: with 1 <= dim, igd/igd_plus/avg_hausdorff_dist compute exact results for 1-column input (tests included in test_dim1_metrics.py) and higher dimensions are unchanged.

Why legalize rather than reject: pymoo 0.6.2 (current release) calls moocore.igd on design-space data, so any single-variable problem passes a 1-column array in production. Today that violates the ASSUME (it happens to work on GCC release builds but would assert under DEBUG=1). A ValueError here would retroactively break those installs; relaxing the assume makes the existing behavior well-defined. epsilon.h is deliberately untouched — its 2 <= dim is load-bearing (see above).

This is a separate commit so it's easy to drop if you'd rather handle the lower bound differently.

Verification

  • New tests: 44 (test_dimension_limits.py 40, test_dim1_metrics.py 4)
  • Full suite on this branch: 115 passed, 2 xfailed (Linux/GCC, Python 3.11)

Open questions (not in this PR)

  • Would you prefer the limit constants exposed from C (via cffi) instead of mirrored in _moocore.py? Happy to change.
  • NaN/inf checking (your earlier point): confirmed igd with NaN silently returns 0.0 today. A SciPy-style opt-out check_finite=True kwarg would avoid always paying the extra pass — can do as a follow-up if you want it.
  • Numpy fallback above 255: skipped for now per your comment; can contribute later if demand appears.

Downstream context: anyoptimization/pymoo#793.

blankjul and others added 3 commits July 2, 2026 07:45
Add checks to the Python wrappers so that inputs beyond what the C
library supports are rejected with a clear ValueError instead of
reaching C as undefined behavior (multi-objective#72):

- 255 columns (the range of dimension_t) for igd, igd_plus,
  avg_hausdorff_dist, epsilon_additive, epsilon_mult, pareto_rank,
  is_nondominated and any_dominated. Before this, 256 columns
  segfaulted via dimension_t wraparound.
- 31 columns for hypervolume (function and class), hv_contributions
  and hv_approx. Before this, 32 columns silently returned a wrong
  value (or crashed, depending on the compiler).
- at least 2 columns for epsilon_additive/epsilon_mult: their C kernel
  is unrolled over the first two dimensions, so 1-column input read
  out of bounds and returned garbage.

Add test_dimension_limits.py covering large-dimension correctness
(32-255 columns, verified against independent numpy references) and
the error behavior at each limit.
The gd_common_helper_ loop is fully general in dim, so the 2 <= dim
lower bound is purely conservative: with 1 <= dim, igd, igd_plus and
avg_hausdorff_dist compute exact results for 1-column input (tests
included) and higher dimensions are unchanged.

This case is exercised in the wild: pymoo 0.6.2 calls moocore.igd on
design-space data, so any single-variable problem passes a 1-column
array. Legalizing it keeps those installs working; rejecting it would
break them from a bugfix release.

Note this deliberately does not touch epsilon.h: its kernel is
unrolled over the first two dimensions, so 2 <= dim is load-bearing
there (1-column input is now rejected in Python instead).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant