Multi-objective optimization#

OMADS can approximate the Pareto front of problems with several objectives. The blackbox returns a list of objective values, and three parameters switch on the Pareto study:

def blackbox(x):
    return [[f1, f2], [c1, c2]]          # objectives first, then constraints

param = {..., "nobj": 2, "is_pareto": True, "fun_names": ["f1", "f2"]}

Internally the adaptive barrier keeps the whole set of non-dominated points, each with its own mesh, and the poll picks its frame centre where the front has the largest gaps (see Algorithm 2). The quality of the front is measured with the hypervolume indicator.

The Binh and Korn problem#

\[\min_{x \in [0,5]\times[0,3]} \; \bigl(4x_1^2 + 4x_2^2,\; (x_1-5)^2 + (x_2-5)^2\bigr).\]
Listing 12 multiobjective.py#
"""Bi-objective Binh and Korn problem, solved with a Pareto poll study."""
import numpy as np
import pandas as pd
from OMADS import poll


def binh_korn(x):
  f1 = 4.0 * x[0] ** 2 + 4.0 * x[1] ** 2
  f2 = (x[0] - 5.0) ** 2 + (x[1] - 5.0) ** 2
  return [[f1, f2], [0]]


data = {
    "evaluator": {"blackbox": binh_korn},
    "param": {
        "name": "binh_korn",
        "baseline": [2.5, 1.5],
        "lb": [0.0, 0.0],
        "ub": [5.0, 3.0],
        "scaling": [5.0, 3.0],
        "var_names": ["x1", "x2"],
        "fun_names": ["f1", "f2"],
        "nobj": 2,
        "is_pareto": True,
        "ref_point": [140.0, 50.0],
        "post_dir": "./post",
    },
    "options": {"seed": 0, "budget": 1500, "tol": 1e-9,
                "check_cache": True, "store_cache": True, "rich_direction": True,
                "save_results": True},
}


def pareto_front(csv_path, objectives):
  """Non-dominated feasible rows of an OMADS results file."""
  df = pd.read_csv(csv_path, skipinitialspace=True)
  df.columns = df.columns.str.strip()
  df = df[df["Status"].str.strip() == "FEASIBLE"].drop_duplicates(objectives)
  F = df[objectives].to_numpy()
  keep = [not np.any(np.all(F <= f, axis=1) & np.any(F < f, axis=1)) for f in F]
  return df[keep].sort_values(objectives[0])


if __name__ == "__main__":
  out, _ = poll.main(data)
  print("hypervolume =", round(out["HV"], 4))

  front = pareto_front("post/binh_korn_run/binh_korn_all.out", ["f1", "f2"])
  print(len(front), "non-dominated points, e.g.")
  print(front[["x1", "x2", "f1", "f2"]].iloc[::len(front) // 5].to_string(index=False))
hypervolume = 0.8435
380 non-dominated points, e.g.
      x1       x2        f1        f2
0.000000 0.000000  0.000000 50.000000
0.750000 0.718750  4.316406 36.391602
1.593750 1.515625 19.348633 23.743408
2.500000 2.453125 49.071289 12.736572
3.484375 3.000000 84.563477  6.297119

The front runs from \((0, 50)\) at \(x = (0, 0)\) to the region around \(x_2 = 3\), where the upper bound becomes active. out["xmin"] and out["fmin"] hold one point of the front; the whole front is in the results file.

Important

In this release, use poll.main for multi-objective problems. mads.main stops with an IndexError while writing the Pareto file, and the ACTIVE sampling search fails on multi-objective problems. Also, <name>_Pareto.out only keeps the starting point, which is why the script extracts the front from <name>_all.out.