Mixed problems

The pymoo interface supports the solution of mixed integer/float problems via the genetic algorithm MixedVariableGA. Here we demonstrate this by maximizing the number if points that fit inside a circle and lie on a regular grid.

These are the imports:

import matplotlib.pyplot as plt
import numpy as np

from iwopy import Objective, Problem, create_optimizer

The problem describes points on a regular grid of size nx * ny. Points that lie outside of the given radius will be marked by a False value in a boolean array called valid:

class GridProblem(Problem):
    def __init__(self, n_row_max, radius, min_dist, ctol=1e-3):
        super().__init__(name="grid_problem")

        self.n_row_max = n_row_max
        self.radius = float(radius)
        self.min_dist = float(min_dist)
        self.max_dist = 2 * radius

        self.xy = None
        self.valid = None

    def initialize(self, verbosity=1):
        super().initialize(verbosity)
        self.apply_individual(self.initial_values_int(), self.initial_values_float())

    def var_names_int(self):
        return ["nx", "ny"]

    def initial_values_int(self):
        return [2, 2]

    def min_values_int(self):
        return [1, 1]

    def max_values_int(self):
        return [self.n_row_max, self.n_row_max]

    def var_names_float(self):
        return ["x0", "y0", "dx", "dy", "alpha"]

    def initial_values_float(self):
        return [0.0, 0.0, self.min_dist, self.min_dist, 0.0]

    def min_values_float(self):
        return [-2 * self.radius, -2 * self.radius, self.min_dist, self.min_dist, 0.0]

    def max_values_float(self):
        return [self.radius, self.radius, self.max_dist, self.max_dist, 90.0]

    def apply_individual(self, vars_int, vars_float):
        """Create points on a regular grid, and evaluate their validity"""

        nx, ny = vars_int
        x0, y0, dx, dy, alpha = vars_float

        a = np.deg2rad(alpha)
        nax = np.array([np.cos(a), np.sin(a), 0.0])
        naz = np.array([0.0, 0.0, 1.0])
        nay = np.cross(naz, nax)

        self.xy = np.zeros((nx, ny, 2))
        self.xy[:] = np.array([x0, y0])[None, None, :]
        self.xy[:] += np.arange(nx)[:, None, None] * dx * nax[None, None, :2]
        self.xy[:] += np.arange(ny)[None, :, None] * dy * nay[None, None, :2]

        self.valid = np.linalg.norm(self.xy, axis=-1) <= self.radius

        return self.xy, self.valid

    def apply_population(self, vars_int, vars_float):
        """Create points on a regular grid, and evaluate their validity"""

        n_pop = vars_int.shape[0]
        nx = vars_int[:, 0]
        ny = vars_int[:, 1]
        x0 = vars_float[:, 0]
        y0 = vars_float[:, 1]
        dx = vars_float[:, 2]
        dy = vars_float[:, 3]
        alpha = vars_float[:, 4]

        a = np.deg2rad(alpha)
        nax = np.stack([np.cos(a), np.sin(a), np.zeros(a.shape)], axis=-1)
        naz = np.zeros_like(nax)
        naz[:, 2] = 1
        nay = np.cross(naz, nax)

        mx = np.max(nx)
        my = np.max(ny)
        self.xy = np.full((n_pop, mx, my, 2), -2 * self.radius)
        for i in range(n_pop):
            self.xy[i, : nx[i], : ny[i]] = np.array([x0[i], y0[i]])[None, None, :]
            self.xy[i, : nx[i], : ny[i]] += (
                np.arange(nx[i])[:, None, None]
                * dx[i, None, None, None]
                * nax[i, None, None, :2]
            )
            self.xy[i, : nx[i], : ny[i]] += (
                np.arange(ny[i])[None, :, None]
                * dy[i, None, None, None]
                * nay[i, None, None, :2]
            )

        self.valid = np.linalg.norm(self.xy, axis=-1) <= self.radius

        return self.xy, self.valid

    def get_fig(self, xy=None, valid=None):
        if xy is None:
            xy = self.xy
        if valid is None:
            valid = self.valid

        nx, ny = xy.shape[:2]
        xy = xy.reshape(nx * ny, 2)[valid.reshape(nx * ny)]

        fig, ax = plt.subplots()
        ax.scatter(xy[:, 0], xy[:, 1], color="orange")
        ax.add_patch(plt.Circle((0, 0), self.radius, color="darkred", fill=False))
        ax.set_aspect("equal", adjustable="box")
        ax.set_xlabel("x")
        ax.set_ylabel("y")
        ax.set_title(f"N = {len(xy)}, min_dist = {self.min_dist}")

        return fig

Notice that the calculate_individual and calculate_population functions return the current grid points and the validity array. This problem has two integer variables nx and ny, determining the number of grid points in x and y directions, respectively, and 5 float type variables.

Our objective is the maximization of the total number of points within the circle:

class MaxN(Objective):
    def __init__(self, problem):
        super().__init__(
            problem,
            "maxN",
            vnames_int=problem.var_names_int(),
            vnames_float=problem.var_names_float(),
        )

    def n_components(self):
        return 1

    def maximize(self):
        return [True]

    def calc_individual(self, vars_int, vars_float, problem_results, cmpnts=None):
        _xy, valid = problem_results
        return np.sum(valid)

    def calc_population(self, vars_int, vars_float, problem_results, cmpnts=None):
        _xy, valid = problem_results
        return np.sum(valid, axis=(1, 2))[:, None]

This objective makes use of the returned validity array. We can now solve this with the appropriate algorithm from pymoo:

problem = GridProblem(n_row_max=1000, radius=5, min_dist=0.5)
problem.add_objective(MaxN(problem))
problem.initialize()
Problem 'grid_problem' (GridProblem): Initializing
--------------------------------------------------
solver = create_optimizer(
    family="pymoo",  # Backend family.
    optimizer="MixedVariableGA",  # Mixed integer/continuous search.
    problem=problem,
    vectorize=True,  # Evaluate each population together.
    pop_size=40,  # Population size; start around 100 in production.
    seed=42,  # Reproducible search.
    termination="evaluations",  # Stop on the native evaluation count.
    max_evaluations=1000,  # Notebook limit; start around 10000 in production.
)
solver.initialize()
solver.print_info()
Loading pymoo
pymoo successfully loaded
Initializing Optimizer_pymoo
Selecting algorithm: MixedVariableGA (MixedVariableGA)
Selecting termination: evaluations (MaximumFunctionCallTermination)

Problem:
--------
  name         : grid_problem
  n_vars_int   : 2
  n_vars_float : 5
  n_objectives : 1
  n_obj_cmptns : 1
  n_constraints: 0
  n_con_cmptns : 0

Algorithm:
----------
  pop_size: 40

Termination:
------------
  type: evaluations
  n_max_evals: 1000

Here tol is an IPOPT parameter that defines the convergence tolerance. Now we are finally ready - let’s solve the problem!

results = solver.solve()
solver.finalize(results)
=================================================
n_gen  |  n_eval  |     f_avg     |     f_min    
=================================================
     1 |       40 | -4.775000E+00 | -4.900000E+01
     2 |       80 | -7.475000E+00 | -4.900000E+01
     3 |      120 | -1.135000E+01 | -4.900000E+01
     4 |      160 | -2.442500E+01 | -1.420000E+02
     5 |      200 | -4.220000E+01 | -1.420000E+02
     6 |      240 | -6.440000E+01 | -1.490000E+02
     7 |      280 | -8.945000E+01 | -1.490000E+02
     8 |      320 | -1.178250E+02 | -2.230000E+02
     9 |      360 | -1.426750E+02 | -2.230000E+02
    10 |      400 | -1.587500E+02 | -2.550000E+02
    11 |      440 | -1.735000E+02 | -2.550000E+02
    12 |      480 | -1.895750E+02 | -2.550000E+02
    13 |      520 | -2.078750E+02 | -2.550000E+02
    14 |      560 | -2.233750E+02 | -2.550000E+02
    15 |      600 | -2.329500E+02 | -2.610000E+02
    16 |      640 | -2.466750E+02 | -2.850000E+02
    17 |      680 | -2.581500E+02 | -2.900000E+02
    18 |      720 | -2.695500E+02 | -3.040000E+02
    19 |      760 | -2.797500E+02 | -3.050000E+02
    20 |      800 | -2.900750E+02 | -3.050000E+02
    21 |      840 | -2.955250E+02 | -3.090000E+02
    22 |      880 | -3.000250E+02 | -3.090000E+02
    23 |      920 | -3.041750E+02 | -3.090000E+02
    24 |      960 | -3.058750E+02 | -3.100000E+02
    25 |     1000 | -3.073500E+02 | -3.120000E+02
Optimizer_pymoo: Optimization run finished
  Success: True
  Best maxN = 312.0

These are the results:

print(results)

fig = problem.get_fig()
plt.show()
plt.close(fig)
Results problem 'grid_problem':
--------------------------------
  Integer variables:
    0: nx = 864
    1: ny = 519
--------------------------------
  Float variables:
    0: x0    = -6.128757e+00
    1: y0    = -6.402713e+00
    2: dx    = 5.013868e-01
    3: dy    = 5.033444e-01
    4: alpha = 1.286230e+00
--------------------------------
  Objectives:
    0: maxN = 3.120000e+02
--------------------------------
  Success: True
--------------------------------
../_images/acdc9ae1fec8e9eae3d1903bf6689d9052c3cd20ccb24150670b853dc59c923e.png

Clearly the circle is fully filled with points on a regular grid.