Metadata-Version: 2.4
Name: mace-eda-bo
Version: 0.1.2
Summary: Cold-start MACE Bayesian optimization for mixed-variable EDA design spaces.
Author: MACE EDA BO contributors
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23
Requires-Dist: scipy>=1.8
Requires-Dist: scikit-learn>=1.2
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# MACE EDA BO

Cold-start Bayesian optimization for mixed-variable EDA design spaces.

This first version keeps the HEBO-style MACE acquisition idea (`LCB`, `EI`, and `PI`) but changes the surrounding loop for discrete EDA-style spaces:

- mixed variables: continuous, integer, categorical, boolean
- optional `legalize(config)` hook for repairing invalid designs
- optional `canonicalize(config)` hook for de-duplicating equivalent designs
- optional `perturb(anchor, rng, strength)` hook for domain-aware local search
- optional `global_sampler(rng, n)` hook for domain-aware global candidates
- tree-ensemble surrogate with uncertainty from estimator disagreement
- candidate-pool MACE selection instead of raw-space acquisition gradients
- ask/tell API suitable for expensive external EDA evaluators

## Install

For regular use, install the released package from PyPI:

```powershell
python -m pip install mace-eda-bo
```

For local development, clone the repository and install it in editable mode:

```powershell
cd mace-eda-bo
python -m pip install -e .[dev]
```

## Minimal Example

```python
from mace_eda_bo import MACEBO, DesignSpace

space = DesignSpace([
    {"name": "x", "type": "num", "lb": -5.0, "ub": 5.0},
    {"name": "n", "type": "int", "lb": 1, "ub": 8},
    {"name": "kind", "type": "cat", "categories": ["a", "b", "c"]},
    {"name": "flag", "type": "bool"},
])

opt = MACEBO(space, init_samples=8, seed=0, maximize=False)

for _ in range(40):
    cfg = opt.ask()
    y = (cfg["x"] - 1.5) ** 2 + (cfg["n"] - 3) ** 2
    opt.tell(cfg, y)

print(opt.best_config, opt.best_y)
```

For EDA flows, pass shell-backed evaluators outside the optimizer and call `tell()` with the measured metric.

Available surrogate plugins:

```python
MACEBO(space, surrogate="extra_trees")
MACEBO(space, surrogate="random_forest")
MACEBO(space, surrogate="gaussian_process")
MACEBO(space, surrogate="tpe")
```

## Domain-Aware Candidate Generation

For structured EDA spaces, many raw configurations may map to the same legal
design. `MACEBO` accepts optional hooks so users can keep the optimizer generic
while injecting domain-specific search behavior:

```python
def legalize(cfg):
    return repair_to_legal_design(cfg)

def canonicalize(cfg):
    return tuple(sorted(legalize(cfg).items()))

def perturb(anchor, rng, strength):
    # Mutate only parameters that are effective for this design family.
    return domain_local_neighbor(anchor, rng, strength)

def global_sampler(rng, n):
    # Optional: draw global candidates from a domain-specific distribution.
    return [sample_domain_design(rng) for _ in range(n)]

opt = MACEBO(
    space,
    legalize=legalize,
    canonicalize=canonicalize,
    perturb=perturb,
    global_sampler=global_sampler,
    init_samples=96,
    local_fraction=0.35,
    kappa=2.5,
    initial_sampler="sobol",
    maximize=True,
)
```

The `perturb` hook is useful for trust-region style local search in effective
design spaces, while `global_sampler` is optional and should be validated for
the target problem because a poor global distribution can hurt early search.

## Trace and Resume

```python
opt = MACEBO(space, initial_sampler="sobol", seed=0)
cfg = opt.ask()

try:
    y = run_expensive_flow(cfg)
    opt.tell(cfg, y)
except RuntimeError as exc:
    opt.tell_failed(cfg, reason=str(exc))

opt.save_trace("trace.csv")

restored = MACEBO.from_trace("trace.csv", space, initial_sampler="sobol", seed=0)
```

## Examples

```powershell
python examples/chiplet_like.py
python examples/chiplet_benchmark.py
python examples/shell_evaluator_demo.py
python examples/flow_adapter_demo.py
python examples/multi_objective_archive.py
python examples/parego_demo.py
python examples/repeatability_check.py
python examples/analyze_pareto.py
mace-eda-bo chiplet-like --budget 50
mace-eda-bo benchmark-chiplet-like --budget 40 --seeds 0 1 2
mace-eda-bo benchmark --config configs/chiplet_like.json
mace-eda-bo benchmark --config configs/parego_chiplet_like.json
```

The chiplet-like example demonstrates `legalize`, `canonicalize`, equivalent-design de-duplication, and a throughput-like maximization loop.
The benchmark example compares random search with several `MACEBO` surrogate plugins across multiple seeds and writes `trace.csv`, `summary.csv`, `convergence.csv`, and `convergence_mean.csv`.
The shell evaluator demo shows the same interface you would use for OpenROAD/Yosys-style command-line flows: write `config.json`, run an external command, parse `metrics.json`, and keep per-run artifacts.
The flow adapter demo also renders a per-run `flow.tcl` template before launching the external command.
The multi-objective archive example tracks Pareto-optimal designs from raw EDA metrics without collapsing them into a weighted score.
The ParEGO demo uses the existing single-objective MACEBO loop to explore a multi-objective Pareto front with changing scalarizations.
The repeatability check example estimates metric noise from repeated evaluations and recommends absolute epsilon values for Pareto pruning.
The Pareto analyzer reads `pareto_front.csv`, writes `pareto_summary.csv` with count, coverage, and 2D hypervolume metrics, and plots common metric trade-offs when `matplotlib` is available.
The `parego_chiplet_like` config compares random search, scalar MACEBO, and ParEGO on a toy multi-objective chiplet-like benchmark.
ParEGO supports `weight_sampler="random"` and `weight_sampler="sobol"`; Sobol weights give a more even coverage of objective preferences.
ParEGO also supports `normalization="online"` and `normalization="fixed"`; fixed normalization uses user-provided `objective_bounds` to keep scalarization scales stable across the run.

## Config-Driven Benchmarks

Benchmark configs are JSON by default:

```json
{
  "problem": "chiplet_like",
  "budget": 40,
  "seeds": [0, 1, 2],
  "output_dir": "examples/output/chiplet_benchmark_config",
  "optimizers": [
    {"name": "random", "type": "random"},
    {
      "name": "macebo_extra_trees",
      "type": "macebo",
      "init_samples": 10,
      "candidate_pool_size": 512,
      "local_fraction": 0.6,
      "initial_sampler": "sobol",
      "surrogate": "extra_trees"
    }
  ]
}
```

Run:

```powershell
mace-eda-bo benchmark --config configs/chiplet_like.json
```

When a benchmark is configured with multi-objective metrics, it also writes
`pareto_front.csv` with non-dominated designs per optimizer and seed.

```json
{
  "objectives": [
    {"name": "frequency_mhz", "direction": "max"},
    {"name": "timing_slack_ns", "direction": "max"},
    {"name": "area_um2", "direction": "min"},
    {"name": "power_mw", "direction": "min"}
  ]
}
```

Near-duplicate Pareto points can be pruned explicitly with absolute or relative
tolerances. By default, no pruning is applied.

```json
{
  "pareto_abs_eps": {
    "frequency_mhz": 5.0,
    "timing_slack_ns": 0.005,
    "area_um2": 1000.0,
    "power_mw": 1.0
  },
  "pareto_rel_eps": {
    "area_um2": 0.01,
    "power_mw": 0.01
  }
}
```

Absolute epsilon values can be estimated from repeated evaluations of the same
design:

```python
from mace_eda_bo import recommend_abs_epsilons

pareto_abs_eps = recommend_abs_epsilons(repeated_metrics, multiplier=3.0)
```

## Evaluators

See [docs/eda_flow_contract.md](docs/eda_flow_contract.md) for the expected
input/output contract for real Yosys/OpenROAD-style flows.

For real EDA flows, return an `EvaluationResult` instead of a bare float when you want traces to keep metrics, runtime, artifacts, or failure reasons:

```python
from mace_eda_bo import EvaluationResult

def evaluate(config):
    try:
        metrics = run_openroad_flow(config)
    except RuntimeError as exc:
        return EvaluationResult.failed(str(exc), artifacts_dir="runs/design_001")

    score = metrics["frequency_mhz"] - 0.01 * metrics["area_um2"]
    return EvaluationResult.ok(
        score,
        metrics=metrics,
        artifacts_dir="runs/design_001",
        runtime_sec=metrics.get("runtime_sec"),
    )
```

Benchmark traces include `metrics_json`, `artifacts_dir`, and `runtime_sec` columns.

For command-line EDA tools, use `ShellEvaluator`:

```python
from mace_eda_bo import ShellEvaluator

evaluator = ShellEvaluator(
    ["python", "flow.py", "--config", "{config_json}", "--out", "{run_dir}"],
    root_dir="runs/openroad",
    metrics_file="metrics.json",
    score_key="score",
    timeout_sec=3600,
)

result = evaluator.evaluate({"clock_period": 2.0, "utilization": 0.65})
```

Each run directory contains `config.json`, `stdout.txt`, `stderr.txt`, and the metrics file produced by the external command.

For template-driven Yosys/OpenROAD-style flows, use `EDAFlowEvaluator`:

```python
from mace_eda_bo import EDAFlowEvaluator

evaluator = EDAFlowEvaluator(
    ["openroad", "flow.tcl"],
    root_dir="runs/openroad",
    template_texts={
        "flow.tcl": """
set clock_period_ns ${clock_period_ns}
set core_utilization ${core_utilization}
# write metrics to ${metrics_json}
"""
    },
    metrics_file="metrics.json",
    score_key="score",
    timeout_sec=3600,
)
```

Templates use `${name}` placeholders. Available names include the config keys plus `${run_dir}`, `${config_json}`, and `${metrics_json}`.
