# DPO: Debt-Paying Optimizer

DPO is a population-based metaheuristic optimizer designed for **Neural Architecture Search (NAS)** and generalized to solve **continuous**, **combinatoric**, and **hybrid** optimization problems.

It combines deliberate acceptance of worse candidates (debt accumulation) with aggressive repayment and overshoot to escape local optima while converging toward strong global solutions.

## Highlights

- Unified engine for NAS, HPO, resource allocation, TSP/pathfinding, and custom problems.
- Preset-driven workflows via `DPO_Presets` for fast setup.
- Works with custom `Problem` implementations for domain-specific constraints.
- Supports both `DPO_NAS` (low-level) and `DPO_Universal` (recommended high-level).

## Benchmark Snapshot (Feb 2026)

The latest comprehensive benchmark run (`495` total runs across NASBench201, HPOBench, and HPOLib) produced the following overall summary:

| Method | Mean Rank (↓ better) | #1 Wins | Mean AUC |
|---|---:|---:|---:|
| JADE | **2.27** | **7** | 0.8919 |
| DE | 3.33 | 2 | 0.8228 |
| FA | 3.60 | 5 | 0.8070 |
| GWO | 4.53 | 0 | 0.9077 |
| ACO | 4.80 | 0 | 0.8266 |
| DPO | 6.07 | 1 | **0.9459** |
| PSO | 6.73 | 0 | 0.8531 |
| GA | 6.80 | 0 | 0.9360 |
| ABC | 7.60 | 0 | 0.7658 |
| WOA | 9.33 | 0 | 0.9269 |
| SA | 10.93 | 0 | 0.7772 |

Interpretation:

- DPO is highly competitive on convergence quality (`AUC`) and stability during search.
- On this specific benchmark mix, JADE/FA/DE lead final-score rank averages.
- This indicates DPO currently favors fast/robust trajectory quality over best final score on some datasets.

Repro command used:

```bash
python -m dpo.benchmarks.hpo_comprehensive_benchmark --seeds 3 --population 40 --iterations 60
```

Generated artifacts:

- JSON: `hpo_benchmark_results/results.json`
- Plots: `hpo_benchmark_results/*.png`

## Installation

```bash
pip install -e .
```

Optional extras:

```bash
pip install -e .[dev]
pip install -e .[docs]
pip install -e .[gpu]
```

## Quick Start

### 1) One-line NAS optimization

```python
from dpo import dpo

result = dpo(preset="nas")
print(result["best_fitness"])
print(result.get("best_accuracy"))
```

### 2) Recommended explicit universal interface

```python
from dpo.core.universal import DPO_Universal, DPO_Presets

config = DPO_Presets.NAS_Config(population_size=40, max_iterations=80)
optimizer = DPO_Universal(config=config)
result = optimizer.optimize()

best_solution = optimizer.get_best_solution()
print(result["best_fitness"], result.get("best_accuracy"))
print(best_solution)
```

---

## How to Use DPO in Detail

## Core API Layers

### Layer A: `DPO_Universal` (best default)
Use this for almost all projects.

```python
from dpo.core.universal import DPO_Universal, DPO_Presets

config = DPO_Presets.Pathfinding_Config(population_size=50, max_iterations=100)
optimizer = DPO_Universal(problem=my_problem, config=config)
result = optimizer.optimize()
```

### Layer B: `DPO_NAS` (low-level / manual control)
Use this when you need direct control over evaluator, constraints, or internals.

```python
from dpo.core.optimizer import DPO_NAS
from dpo.core.config import DPO_Config

config = DPO_Config.balanced()
optimizer = DPO_NAS(config=config)
result = optimizer.optimize()
```

### Layer C: convenience helpers
Kept for easy onboarding:

- `dpo(...)`
- `dpo_optimize(...)`
- `dpo_solve_tsp(...)`
- `dpo_solve_nas(...)`

---

## Problem Types

## 1) Continuous optimization (HPO, calibration, scalar black-box)

```python
from dpo import dpo_optimize


def objective(params):
    x = params["x"]
    y = params["y"]
    fitness = (x - 2.0) ** 2 + (y + 1.5) ** 2
    return fitness, {
        "accuracy": 1.0 / (1.0 + fitness),
        "latency_ms": abs(x) + abs(y),
        "memory_mb": 1.0,
        "flops_m": 1.0,
    }

result = dpo_optimize(
    objective=objective,
    bounds=[(-5.0, 5.0), (-5.0, 5.0)],
    names=["x", "y"],
    preset="continuous",
    max_iterations=80,
)
print(result["best_solution"])
```

## 2) Combinatoric optimization (TSP, routing, scheduling)

```python
import numpy as np
from dpo import dpo_solve_tsp

rng = np.random.default_rng(7)
n = 20
coords = rng.uniform(0.0, 100.0, (n, 2))
dist = np.linalg.norm(coords[:, None, :] - coords[None, :, :], axis=2)

result = dpo_solve_tsp(
    distance_matrix=dist,
    preset="balanced",
    max_iterations=120,
    population_size=60,
)
print(result["best_fitness"])
```

## 3) NAS with custom estimator

```python
from dpo import dpo_solve_nas


class MyEstimator:
    def estimate(self, arch_dict, **kwargs):
        acc = 0.80  # replace with real evaluation
        return 1.0 - acc, {
            "accuracy": acc,
            "latency_ms": 50.0,
            "memory_mb": 20.0,
            "flops_m": 180.0,
        }

result = dpo_solve_nas(
    estimator=MyEstimator(),
    constraints={"latency": 100.0, "memory": 50.0, "flops": 300.0},
    preset="nas",
)
print(result["best_fitness"], result.get("best_accuracy"))
```

## 4) Custom `Problem` class (full extensibility)

```python
import numpy as np
from dpo.core.problem import Problem
from dpo.core.solution import NumericSolution
from dpo.core.universal import DPO_Universal


class SphereProblem(Problem):
    def evaluate(self, solution, **kwargs):
        params = solution.to_dict()
        x, y = params["x"], params["y"]
        fitness = x * x + y * y
        return fitness, {
            "accuracy": 1.0 / (1.0 + fitness),
            "latency_ms": abs(x) + abs(y),
            "memory_mb": 1.0,
            "flops_m": 1.0,
        }

    def create_solution(self, **kwargs):
        values = np.random.uniform(-5.0, 5.0, size=2)
        return NumericSolution(values, [(-5.0, 5.0), (-5.0, 5.0)], ["x", "y"])

problem = SphereProblem()
result = DPO_Universal(problem=problem, preset="balanced").optimize()
print(result["best_fitness"])
```

---

## Output Contract

Common result keys:

- `best_fitness`: best objective value (lower is better).
- `best_solution`: best solution as dict-like structure.
- `best_accuracy`: optional metric when provided by evaluator/problem.
- `best_metrics`: metrics dictionary for the best solution.
- `history`: convergence and run history.
- `elapsed_time`: optimization wall time in seconds.
- `total_evaluations`: function evaluations performed.

---

## Presets and Tuning

Use problem-tailored presets:

- `DPO_Presets.NAS_Config(...)`
- `DPO_Presets.ResourceAllocation_Config(...)`
- `DPO_Presets.Pathfinding_Config(...)`
- `DPO_Presets.HyperparameterTuning_Config(...)`
- `DPO_Presets.Scheduling_Config(...)`

For low-dimensional continuous problems, start with:

```python
from dpo.core.config import DPO_Config
config = DPO_Config.continuous_analytic()
```

---

## Examples

See runnable scripts in `dpo/examples/`:

- `example_nas.py`
- `example_hpo.py`
- `example_tsp.py`
- `example_pathfinding.py`
- `example_resource_allocation.py`
- `example_hybrid.py`

---

## Migration Notes

Legacy modules `dpo/api.py` and `dpo/api_universal.py` have been removed to simplify the API surface.

Use either:

- `from dpo import dpo, dpo_optimize, dpo_solve_tsp, dpo_solve_nas`, or
- `from dpo.core.universal import DPO_Universal, DPO_Presets` (recommended for long-term projects).

---

## Documentation

Full docs are available in `docs/`:

- `docs/index.md`
- `docs/installation.md`
- `docs/quickstart.md`
- `docs/api_reference.md`
- `docs/examples.md`
- `docs/methodology.md`

## PyPI README Visibility

This project is configured to publish this `README.md` as the PyPI project description via `pyproject.toml` (`[project].readme`).

To publish/update on PyPI:

```bash
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
python -m twine upload dist/*
```

## License

MIT
