Geometry Optimisation

In this example we will go through how to set up a simple geometry optimisation, including a geometric constraint.

The problem to solve is, minimise the length of our wall boundary, in the xz-plane, whilst keeping it a minimum distance from our plasma.

We will greatly simplify this problem by working with a circular plasma, we will use a PrincetonD for the wall shape, and set the minimum distance to half a meter.

Using the optimise_geometry Function

Let’s perform this geometry optimisation, utilising bluemira’s optimise_geometry function.

import numpy as np

from bluemira.display import plot_2d
from bluemira.display.plotter import PlotOptions
from bluemira.geometry.optimisation import optimise_geometry
from bluemira.geometry.parameterisations import GeometryParameterisation, PrincetonD
from bluemira.geometry.tools import distance_to, make_circle
from bluemira.geometry.wire import BluemiraWire

min_distance = 0.5
plasma = make_circle(radius=2, center=(8, 0, 0.25), axis=(0, 1, 0))
# As with any optimisation, it's important to pick a reasonable initial
# parameterisation.
wall_boundary = PrincetonD({
    "x1": {"value": 4, "upper_bound": 6},
    "x2": {"value": 12, "lower_bound": 10},
})
print("Initial parameterisation:")
print(wall_boundary.variables)
print("Length of wall    :", wall_boundary.create_shape().length)
print("Distance to plasma:", distance_to(wall_boundary.create_shape(), plasma)[0])

plot_2d([wall_boundary.create_shape(), plasma])
def f_objective(geom: GeometryParameterisation) -> float:
    """Objective function to minimise a shape's length."""
    return geom.create_shape().length


def distance_constraint(
    geom: GeometryParameterisation, boundary: BluemiraWire, min_distance: float
) -> float:
    """
    A constraint to keep a minimum distance between two shapes.

    The constraint must be in the form f(x) <= 0, i.e., constraint
    is satisfied if f(x) <= 0.

    Since what we want is 'min_distance <= distance(A, B)', we rewrite
    this in the form 'min_distance - distance(A, B) <= 0', and return
    the left-hand side from this function.
    """
    shape = geom.create_shape()
    return min_distance - distance_to(shape, boundary)[0]
result = optimise_geometry(
    wall_boundary,
    algorithm="SLSQP",
    f_objective=f_objective,
    opt_conditions={"ftol_abs": 1e-6},
    keep_history=True,
    ineq_constraints=[
        {
            "f_constraint": lambda g: distance_constraint(g, plasma, min_distance),
            "tolerance": np.array([1e-8]),
        }
    ],
)

print("Optimised parameterisation:")
print(result.geom.variables)

boundary = result.geom.create_shape()
print("Length of wall    :", boundary.length)
print("Distance to plasma:", distance_to(boundary, plasma)[0])

plot_2d([boundary, plasma])

As we passed keep_history=True into the optimisation function, we can look at how the optimiser arrived at its solution.

geom = PrincetonD()
ax = plot_2d(plasma, show=False)
for i, (x, _) in enumerate(result.history):
    geom.variables.set_values_from_norm(x)
    wire = geom.create_shape()
    wire_options = {
        "alpha": 0.5 + ((i + 1) / len(result.history)) / 2,
        "color": "red",
        "linewidth": 0.1,
    }
    ax = plot_2d(wire, options=PlotOptions(wire_options=wire_options), ax=ax, show=False)
plot_2d(boundary, ax=ax, show=True)

Using the GeomOptimisationProblem Class

Alternatively, we can take a class-based approach to defining this optimisation problem, using the GeomOptimisationProblem base class.


from bluemira.geometry.optimisation import GeomConstraintT, GeomOptimisationProblem


class ContractLengthGOP(GeomOptimisationProblem):
    """Geometry optimisation problem to minimise a shape's length."""

    def __init__(self, plasma: BluemiraWire, min_distance: float):
        self.plasma = plasma
        self.min_distance = min_distance

    def objective(self, geom: GeometryParameterisation) -> float:  # noqa: PLR6301
        """Objective function to minimise."""
        return geom.create_shape().length

    def ineq_constraints(self) -> list[GeomConstraintT]:
        """List of inequality constraints to satisfy."""
        return [
            {
                "name": "distance",
                "f_constraint": lambda geom: self._distance_constraint(
                    geom, self.plasma, self.min_distance
                ),
                "tolerance": np.array([1e-8]),
            }
        ]

    @staticmethod
    def _distance_constraint(
        geom: GeometryParameterisation, boundary: BluemiraWire, min_distance: float
    ) -> float:
        """A constraint to keep a minimum distance between two shapes."""
        shape = geom.create_shape()
        return min_distance - distance_to(shape, boundary)[0]


wall_boundary = PrincetonD({
    "x1": {"value": 4, "upper_bound": 6},
    "x2": {"value": 12, "lower_bound": 10},
})
opt_problem = ContractLengthGOP(plasma, min_distance)
result = opt_problem.optimise(
    wall_boundary, algorithm="SLSQP", opt_conditions={"ftol_abs": 1e-6}
)
print("Optimised parameterisation:")
print(result.geom.variables)

boundary = result.geom.create_shape()
print("Length of wall    :", boundary.length)
print("Distance to plasma:", distance_to(boundary, plasma)[0])