"""
mergen.criteria
===============
Optimisation criteria for space-filling design.
Each criterion exposes four methods:
evaluate(X, space)
Full computation on a normalised design matrix. Used once
per algorithm restart to obtain the initial score.
incremental(X, i, new_pt, space, current_score)
O(n*d) update when the entire point *i* is replaced by
``new_pt``. Used by single-point swap operators.
begin_1d(X, i, current_score)
Initialise a per-point cache for fast 1D coordinate-exchange
trials.
try_1d(cache, axis, new_value, space)
O(n) update when only one coordinate changes. Inner-loop
operation of the SCE algorithm.
All criteria operate on **normalised** coordinates in [0, 1]^d.
Available criteria
------------------
'umaxpro' UMaxPro — Vorechovsky & Masek (2026) (default)
'maxpro' MaxPro — Joseph, Gul & Ba (2015)
'phi_p' PhiP (p=15) — Morris & Mitchell (1995)
'cd2' CD2 — Hickernell (1998)
'stratified' StratifiedL2 — Tian & Xu (2025)
Usage
-----
from mergen.criteria import get_criterion
crit = get_criterion('umaxpro')
score = crit.evaluate(X_norm, space)
"""
from __future__ import annotations
from .base import BaseCriterion, _EPS # noqa: F401
from .umaxpro import UMaxPro
from .maxpro import MaxPro
from .maxproqq import MaxProQQ
from .phi_p import PhiP
from .cd2 import CD2
from .qqd import QQD
from .stratified import StratifiedL2
__all__ = [
"BaseCriterion",
"UMaxPro",
"MaxPro",
"MaxProQQ",
"PhiP",
"CD2",
"QQD",
"StratifiedL2",
"get_criterion",
"list_criteria",
"criterion_latex",
]
# ── Registry of built-in criterion names -> class ────────────────────
_REGISTRY: dict = {
'umaxpro' : UMaxPro,
'maxpro' : MaxPro,
'maxproqq' : MaxProQQ,
'maxpro_qq' : MaxProQQ, # alias
'phi_p' : PhiP,
'phip' : PhiP, # alias
'cd2' : CD2,
'qqd' : QQD,
'stratified' : StratifiedL2,
'stratified_l2' : StratifiedL2, # alias
}
# LaTeX math labels for the criteria, for use in rendering targets
# (plots, LaTeX / Markdown / HTML exports). Plain-text targets
# (terminal, CSV, Excel) keep the registry name. Notation follows the
# source papers: phi_p (Morris & Mitchell 1995); MaxPro (Joseph, Gul &
# Ba 2015); centered L2-discrepancy CD_2 (Hickernell 1998); stratified
# L2-discrepancy SL_2 (Tian & Xu 2025).
_CRITERION_LATEX = {
'umaxpro' : r'$\mathrm{UMaxPro}$',
'maxpro' : r'$\mathrm{MaxPro}$',
'maxproqq' : r'$\mathrm{MaxPro}_{\mathrm{QQ}}$',
'phi_p' : r'$\phi_p$',
'cd2' : r'$\mathrm{CD}_2$',
'qqd' : r'$\mathrm{QQD}$',
'stratified' : r'$\mathrm{SL}_2$',
}
[docs]
def criterion_latex(name: str) -> str:
"""Return the LaTeX math label for a criterion, or the name itself.
Aliases resolve to their canonical label. Plain-text output targets
(terminal, CSV) should use the raw name instead of this label.
"""
aliases = {'maxpro_qq': 'maxproqq', 'phip': 'phi_p',
'stratified_l2': 'stratified'}
key = aliases.get(name, name)
return _CRITERION_LATEX.get(key, name)
[docs]
def get_criterion(name: str) -> BaseCriterion:
"""
Instantiate a criterion by name.
Parameters
----------
name : str
One of: ``'umaxpro'``, ``'maxpro'``, ``'phi_p'``, ``'cd2'``,
``'stratified'`` (or ``'stratified_l2'``).
Case-insensitive.
Returns
-------
BaseCriterion instance
Raises
------
ValueError if *name* is not recognised.
Examples
--------
>>> crit = get_criterion('umaxpro')
>>> type(crit).__name__
'UMaxPro'
"""
key = name.lower().strip()
if key not in _REGISTRY:
available = ', '.join(
f"'{k}'" for k in sorted(
set(_REGISTRY.keys())
- {'phip', 'stratified_l2', 'maxpro_qq'}
)
)
raise ValueError(
f"\n\033[0;31m[MERGEN ERROR]\033[0m "
f"Unknown criterion '{name}'.\n"
f" Available: {available}"
)
return _REGISTRY[key]()
[docs]
def list_criteria() -> list:
"""Return the list of available criterion names (canonical, no aliases)."""
return sorted(set(_REGISTRY.keys())
- {'phip', 'stratified_l2', 'maxpro_qq'})
def nominal_supporting_criteria() -> list:
"""
Return the canonical names of criteria that support nominal
(unordered categorical) factors.
Used by :class:`~mergen.sampler.Sampler` to build the diagnostic
message when a user requests a criterion incompatible with a
space that contains nominal columns.
"""
seen = set()
out = []
for name, cls in _REGISTRY.items():
if name in {'phip', 'stratified_l2', 'maxpro_qq'}:
continue # aliases
if cls in seen:
continue
if getattr(cls, 'supports_nominal', False):
out.append(name)
seen.add(cls)
return sorted(out)