Skip to content

compas_cem.optimization ¤

The constrained form-finding solver, its goals and its optimization parameters.

Classes¤

Optimizer ¤

Optimizer(**kwargs)

An object that modifies a form diagram to meet multiple goals.

Source code in src/compas_cem/optimization/optimizer.py
def __init__(self, **kwargs):
    super(Optimizer, self).__init__(**kwargs)

    self.parameters = {}
    self.goals = {}

    self.x_opt = None
    self.time_opt = None
    self.penalty = None
    self.evals = None
    self.gradient_norm = None
    self.status = None

    self._gkey = -1
    self._pkey = -1

Methods:¤

__repr__ ¤
__repr__()
Source code in src/compas_cem/optimization/optimizer.py
def __repr__(self):
    """ """
    tpl = "{} with {} parameters and {} goals. Status: {}"
    return tpl.format(
        self.__class__.__name__,
        self.number_of_parameters(),
        self.number_of_goals(),
        self.status,
    )
add_goal ¤
add_goal(goal)

Adds a goal goal.

Source code in src/compas_cem/optimization/optimizer.py
def add_goal(self, goal):
    """
    Adds a goal goal.
    """
    self._gkey += 1
    self.goals[self._gkey] = goal
add_parameter ¤
add_parameter(parameter)

Adds a parameter to the optimization problem.

Source code in src/compas_cem/optimization/optimizer.py
def add_parameter(self, parameter):
    """
    Adds a parameter to the optimization problem.
    """
    self._pkey += 1
    self.parameters[self._pkey] = parameter
check_optimization_sanity ¤
check_optimization_sanity()

Verify the optimization problem is in its sane mind.

Source code in src/compas_cem/optimization/optimizer.py
def check_optimization_sanity(self):
    """
    Verify the optimization problem is in its sane mind.
    """
    if len(self.parameters) == 0:
        msg = "No parameters defined. Optimization not possible."
        raise ValueError(msg)

    if len(self.goals) == 0:
        msg = "No goals defined. Optimization not possible."
        raise ValueError(msg)
gradient_func ¤
gradient_func(grad_f, topology, tmax, eta, step_size)

The objective function to calculate gradients from.

Source code in src/compas_cem/optimization/optimizer.py
def gradient_func(self, grad_f, topology, tmax, eta, step_size):
    """
    The objective function to calculate gradients from.
    """
    x_func = partial(self._optimize_form, topology=topology, tmax=tmax, eta=eta)
    return partial(grad_f, x_func=x_func, step_size=step_size)
number_of_goals ¤
number_of_goals()

The number of goals added to the optimizer.

Source code in src/compas_cem/optimization/optimizer.py
def number_of_goals(self):
    """
    The number of goals added to the optimizer.
    """
    return len(self.goals)
number_of_parameters ¤
number_of_parameters()

The number of optimization parameters.

Source code in src/compas_cem/optimization/optimizer.py
def number_of_parameters(self):
    """
    The number of optimization parameters.
    """
    return len(self.parameters)
objective_func ¤
objective_func(topology, grad_func, tmax, eta)

The objective function to minimize.

Source code in src/compas_cem/optimization/optimizer.py
def objective_func(self, topology, grad_func, tmax, eta):
    """
    The objective function to minimize.
    """
    f = objective_function_numpy
    x_func = partial(self._optimize_form, topology=topology, tmax=tmax, eta=eta)
    return partial(f, x_func=x_func, grad_func=grad_func)
optimization_bounds ¤
optimization_bounds(topology)

Creates optimization bounds array. Only one entry in the array per goal.

Source code in src/compas_cem/optimization/optimizer.py
def optimization_bounds(self, topology):
    """
    Creates optimization bounds array.
    Only one entry in the array per goal.
    """
    bounds_low = np.zeros(self.number_of_parameters())
    bounds_up = np.zeros(self.number_of_parameters())

    for pkey, parameter in self.parameters.items():
        bounds_low[pkey] = parameter.bound_low(topology)
        bounds_up[pkey] = parameter.bound_up(topology)

    return bounds_low, bounds_up
optimization_parameters ¤
optimization_parameters(topology)

Creates optimization paremeters array. Only one entry in the array per goal. Takes care of keeping the ordering.

Source code in src/compas_cem/optimization/optimizer.py
def optimization_parameters(self, topology):
    """
    Creates optimization paremeters array.
    Only one entry in the array per goal.
    Takes care of keeping the ordering.
    """
    parameters = np.zeros(self.number_of_parameters())

    for pkey, parameter in self.parameters.items():
        parameters[pkey] = parameter.start_value(topology)

    return parameters
remove_goal ¤
remove_goal(gkey)

Removes a goal from the optimizer.

Source code in src/compas_cem/optimization/optimizer.py
def remove_goal(self, gkey):
    """
    Removes a goal from the optimizer.
    """
    if gkey not in self.goals:
        raise KeyError("Goals not found on object key: {}".format(gkey))
    del self.goals[gkey]
remove_parameter ¤
remove_parameter(pkey)

Removes an optimization parameter.

Source code in src/compas_cem/optimization/optimizer.py
def remove_parameter(self, pkey):
    """
    Removes an optimization parameter.
    """
    if pkey not in self.parameters:
        raise KeyError("Parameter not found at object key: {}".format(pkey))
    del self.parameters[pkey]
solve ¤
solve(
    topology,
    algorithm="SLSQP",
    grad="AD",
    step_size=1e-06,
    iters=100,
    eps=1e-06,
    kappa=1e-08,
    tmax=100,
    eta=1e-06,
    verbose=False,
)

Solve a constrained form-finding problem using gradient-based optimization.

Parameters:

  • topology (:class:`compas_cem.diagrams.TopologyDiagram`) –

    A topology diagram.

  • algorithm (``str``, default: 'SLSQP' ) –

    The name of the gradient-based local optimization algorithm to use. Only the following local gradient-based optimization algorithms are supported:

    • SLSQP: Sequential Least Squares Programming
    • LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
    • MMA: Method of Moving Asymptotes
    • TNEWTON: Preconditioned Truncated Newton
    • AUGLAG: Augmented Lagrangian
    • VAR: Limited-Memory Variable-Metric Algorithm

    Defaults to "SLSQP". Refer to the NLopt documentation <https://nlopt.readthedocs.io/en/latest/>_ for more details on their theoretical underpinnings.

  • grad (``str``, default: 'AD' ) –

    The method to compute the gradient of the objective function. The currently available methods are:

    • AD: Automatic differentiation
    • FD: Finite differences

    Defaults to "AD".

  • iters (``int``, default: 100 ) –

    The maximum number of iterations to run the optimization algorithm for. Defaults to 100.

  • eps (``float``, default: 1e-06 ) –

    The convergence threshold for the output value of the objective function. Defaults to 1e-6.

  • kappa (``float``, default: 1e-08 ) –

    The convergence threshold for the norm of the gradient of the objective function. Defaults to 1e-8.

  • step_size (``float``, default: 1e-06 ) –

    The step size to calculate the gradient of the objective function via finite differences. It becomes active only if grad="FD". It is otherwise ignored by this function. Defaults to 1e-3.

  • tmax (``int``, default: 100 ) –

    The maximum number of iterations the CEM form-finding algorithm will run for. If eta is hit first, the form-finding algorithm will stop early. Defaults to 100.

  • eta (``float``, default: 1e-06 ) –

    The numerical converge threshold of the CEM form-finding algorithm. If tmax is hit first, the form-finding algorithm will stop early. Defaults to 1e-6.

  • verbose (``bool``, default: False ) –

    A flag to prints statistics of the optimization process. Defaults to True.

Returns:

  • form ( :class:`compas_cem.diagrams.FormDiagram` ) –

    A form diagram.

Source code in src/compas_cem/optimization/optimizer.py
def solve(
    self,
    topology,
    algorithm="SLSQP",
    grad="AD",
    step_size=1e-6,
    iters=100,
    eps=1e-6,
    kappa=1e-8,
    tmax=100,
    eta=1e-6,
    verbose=False,
):
    """
    Solve a constrained form-finding problem using gradient-based optimization.

    Parameters
    ----------
    topology : :class:`compas_cem.diagrams.TopologyDiagram`
        A topology diagram.
    algorithm : ``str``, optional
        The name of the gradient-based local optimization algorithm to use.
        Only the following local gradient-based optimization algorithms are supported:

        - SLSQP: Sequential Least Squares Programming
        - LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
        - MMA: Method of Moving Asymptotes
        - TNEWTON: Preconditioned Truncated Newton
        - AUGLAG: Augmented Lagrangian
        - VAR: Limited-Memory Variable-Metric Algorithm

        Defaults to "SLSQP".
        Refer to the NLopt `documentation <https://nlopt.readthedocs.io/en/latest/>`_ for more details on their theoretical underpinnings.
    grad : ``str``, optional
        The method to compute the gradient of the objective function.
        The currently available methods are:

        - AD: Automatic differentiation
        - FD: Finite differences

        Defaults to "AD".
    iters : ``int``, optional
        The maximum number of iterations to run the optimization algorithm for.
        Defaults to ``100``.
    eps : ``float``, optional
        The convergence threshold for the output value of the objective function.
        Defaults to ``1e-6``.
    kappa : ``float``, optional
        The convergence threshold for the norm of the gradient of the objective function.
        Defaults to ``1e-8``.
    step_size : ``float``, optional
        The step size to calculate the gradient of the objective function via finite differences.
        It becomes active only if ``grad="FD"``. It is otherwise ignored by this function.
        Defaults to ``1e-3``.
    tmax : ``int``, optional
        The maximum number of iterations the CEM form-finding algorithm will run for.
        If ``eta`` is hit first, the form-finding algorithm will stop early.
        Defaults to ``100``.
    eta : ``float``, optional
        The numerical converge threshold of the CEM form-finding algorithm.
        If ``tmax`` is hit first, the form-finding algorithm will stop early.
        Defaults to ``1e-6``.
    verbose : ``bool``, optional
        A flag to prints statistics of the optimization process.
        Defaults to ``True``.

    Returns
    -------
    form : :class:`compas_cem.diagrams.FormDiagram`
        A form diagram.
    """
    if verbose:
        print("----------")
        print("Optimization with {} started!".format(algorithm))
        print(
            f"# Parameters: {self.number_of_parameters()}, # Goals {self.number_of_goals()}"
        )

    # test for bad stuff before going any further
    self.check_optimization_sanity()

    # compose gradient and objective functions
    if grad not in ("AD", "FD"):
        raise ValueError(f"Gradient method {grad} is not supported!")
    if grad == "AD":
        if verbose:
            print("Computing gradients using automatic differentiation!")
        x_func = partial(
            self._optimize_form, topology=topology.copy(), tmax=tmax, eta=eta
        )
        grad_func = partial(
            grad_autograd, grad_func=agrad(x_func)
        )  # x, grad, x_func

    elif grad == "FD":
        if verbose:
            print(
                f"Warning: Calculating gradients using finite differences with step size {step_size}. This may take a while..."
            )
        grad_func = self.gradient_func(
            grad_finite_differences, topology.copy(), tmax, eta, step_size
        )

    # grad_func = self.gradient_func(grad_func, topology.copy(), tmax, eta, step_size)
    obj_func = self.objective_func(topology, grad_func, tmax, eta)

    # generate optimization variables
    x = self.optimization_parameters(topology)

    # extract the lower and upper bounds to optimization variables
    bounds_low, bounds_up = self.optimization_bounds(topology)

    # stack keyword arguments
    hyper_parameters = {
        "f": obj_func,
        "algorithm": algorithm,
        "dims": self.number_of_parameters(),
        "bounds_low": bounds_low,
        "bounds_up": bounds_up,
        "iters": iters,
        "eps": eps,
        "ftol": kappa,
    }

    # assemble optimization solver
    solver = nlopt_solver(**hyper_parameters)

    # solve optimization problem
    x_opt = None
    start = time()
    try:
        x_opt = solver.optimize(x)
        if verbose:
            print("Optimization ended correctly!")
    except RoundoffLimited:
        print("Optimization was halted because roundoff errors limited progress")
        print("Results may still be useful though!")
        x_opt = self.optimization_parameters(topology)
    except RuntimeError:
        print("Optimization failed due to a runtime error!")
        print(f"Optimization total runtime: {round(time() - start, 4)} seconds")
        return static_equilibrium(topology)

    # fetch last optimum value of loss function
    time_opt = time() - start
    loss_opt = solver.last_optimum_value()
    evals = solver.get_numevals()
    status = nlopt_status(solver.last_optimize_result())

    # set optimizer attributes
    self.time_opt = time_opt
    self.x_opt = x_opt
    self.penalty = loss_opt
    self.evals = evals
    self.status = status

    # set norm of the gradient
    # NOTE: np.zeros is a dummy array (signature requirement set by nlopt)
    self.gradient = grad_func(x_opt, np.zeros(x_opt.size))
    self.gradient_norm = np.linalg.norm(self.gradient)

    if verbose:
        print(f"Optimization total runtime: {round(time_opt, 6)} seconds")
        print("Number of evaluations incurred: {}".format(evals))
        print(f"Final value of the objective function: {round(loss_opt, 6)}")
        print(
            f"Norm of the gradient of the objective function: {round(self.gradient_norm, 6)}"
        )
        print(f"Optimization status: {status}".format(status))
        print("----------")

    # exit like a champion
    return static_equilibrium(topology)

Functions:¤

grad_autograd ¤

grad_autograd(x, grad, grad_func, **kwargs)

Calculate the gradient with automatic differentiation. This function updates grad in place.

Source code in src/compas_cem/optimization/grad.py
def grad_autograd(x, grad, grad_func, **kwargs):
    """
    Calculate the gradient with automatic differentiation.
    This function updates grad in place.
    """
    grad[:] = grad_func(x)

    return grad

grad_finite_differences ¤

grad_finite_differences(x, grad, x_func, step_size, **kwargs)

Approximate the gradient of a blackbox function using forward finite differences. This function updates grad in place.

Source code in src/compas_cem/optimization/grad.py
def grad_finite_differences(x, grad, x_func, step_size, **kwargs):
    """
    Approximate the gradient of a blackbox function using forward finite differences.
    This function updates grad in place.
    """
    fx0 = x_func(x)
    # NOTE: We make an editable copy of x because NLOpt makes x a read-only vector
    _x = np.copy(x)

    for i in range(len(x)):
        _xi = _x[i]
        _x[i] += step_size

        fx1 = x_func(_x)

        delta_fx = (fx1 - fx0) / step_size
        grad[i] = delta_fx
        _x[i] = _xi

    return grad

nlopt_algorithm ¤

nlopt_algorithm(name)

Fetches an optimization algorithm from the nlopt library by name.

Parameters:

  • name (``str``) –

    The name of the algorithm to search for.

Returns:

  • algorithm ( ``nlopt.algorithm`` ) –

    An nlopt algorithm object.

Notes

Only the following local gradient-based optimization algorithms are supported:

  • SLSQP: Sequential Least Squares Programming
  • LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
  • MMA: Method of Moving Asymptotes
  • TNEWTON: Preconditioned Truncated Newton
  • AUGLAG: Augmented Lagrangian
  • VAR: Limited-Memory Variable-Metric Algorithm

Refer to the NLopt documentation <https://nlopt.readthedocs.io/en/latest/>_ for more details on their theoretical underpinnings.

Source code in src/compas_cem/optimization/nlopt.py
def nlopt_algorithm(name):
    """
    Fetches an optimization algorithm from the nlopt library by name.

    Parameters
    ----------
    name : ``str``
        The name of the algorithm to search for.

    Returns
    -------
    algorithm : ``nlopt.algorithm``
        An nlopt algorithm object.

    Notes
    -----
    Only the following local gradient-based optimization algorithms are supported:

    - SLSQP: Sequential Least Squares Programming
    - LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
    - MMA: Method of Moving Asymptotes
    - TNEWTON: Preconditioned Truncated Newton
    - AUGLAG: Augmented Lagrangian
    - VAR: Limited-Memory Variable-Metric Algorithm

    Refer to the NLopt `documentation <https://nlopt.readthedocs.io/en/latest/>`_ for more details on their theoretical underpinnings.
    """
    algorithms = nlopt_algorithms()
    return algorithms[name]

nlopt_algorithms ¤

nlopt_algorithms()

A dictionary with all the supported nlopt algorithms.

Returns:

  • algorithms ( ``dict`` ) –

    A dictionary that maps algorithm names to nlopt algorithm objects.

Notes

Only the following local gradient-based optimization algorithms are supported:

  • SLSQP: Sequential Least Squares Programming
  • LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
  • MMA: Method of Moving Asymptotes
  • TNEWTON: Preconditioned Truncated Newton
  • AUGLAG: Augmented Lagrangian
  • VAR: Limited-Memory Variable-Metric Algorithm

Refer to the NLopt documentation <https://nlopt.readthedocs.io/en/latest/>_ for more details on their theoretical underpinnings.

Source code in src/compas_cem/optimization/nlopt.py
def nlopt_algorithms():
    """
    A dictionary with all the supported nlopt algorithms.

    Returns
    -------
    algorithms : ``dict``
        A dictionary that maps algorithm names to nlopt algorithm objects.

    Notes
    -----
    Only the following local gradient-based optimization algorithms are supported:

    - SLSQP: Sequential Least Squares Programming
    - LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
    - MMA: Method of Moving Asymptotes
    - TNEWTON: Preconditioned Truncated Newton
    - AUGLAG: Augmented Lagrangian
    - VAR: Limited-Memory Variable-Metric Algorithm

    Refer to the NLopt `documentation <https://nlopt.readthedocs.io/en/latest/>`_ for more details on their theoretical underpinnings.
    """
    algorithms = {}
    gradient_based = {
        "SLSQP": LD_SLSQP,
        "MMA": LD_MMA,
        "LBFGS": LD_LBFGS,
        "TNEWTON": LD_TNEWTON,
        "VAR": LD_VAR2,
        "AUGLAG": LD_AUGLAG,
    }

    algorithms.update(gradient_based)

    return algorithms

nlopt_solver ¤

nlopt_solver(f, algorithm, dims, bounds_up, bounds_low, iters, eps, ftol)

Wrapper around a typical nlopt solver routine.

Source code in src/compas_cem/optimization/nlopt.py
def nlopt_solver(f, algorithm, dims, bounds_up, bounds_low, iters, eps, ftol):
    """
    Wrapper around a typical nlopt solver routine.
    """
    solver = opt(nlopt_algorithm(algorithm), dims)

    if algorithm == "AUGLAG":
        solver.set_local_optimizer(opt(nlopt_algorithm("VAR"), dims))

    if algorithm in ("VAR", "TNEWTON"):
        solver.set_vector_storage(100)  # Defaults to 10 or to 10 MiB of data

    if algorithm == "MMA":
        solver.set_param("inner_maxeval", 5)

    solver.set_lower_bounds(bounds_low)
    solver.set_upper_bounds(bounds_up)

    solver.set_maxeval(iters)

    if ftol is not None:
        # relative difference between two consecutive iterations
        # ftol_abs as per recommendation in the NLOpt docs
        solver.set_ftol_abs(ftol)

    if eps is not None:
        solver.set_stopval(eps)

    solver.set_min_objective(f)

    return solver

nlopt_status ¤

nlopt_status(constant)

Convert the number constant returned by the optimization process into a human-readable string.

Input

constant : int The constant returned by the optimization algorithm as result.

Returns:

  • status ( ``str`` ) –

    A human-readable string.

Source code in src/compas_cem/optimization/nlopt.py
def nlopt_status(constant):
    """
    Convert the number constant returned by the optimization process into a human-readable string.

    Input
    -----
    constant : ``int``
        The constant returned by the optimization algorithm as result.

    Returns
    -------
    status : ``str``
        A human-readable string.
    """
    results = {}

    success = {
        1: "NLOPT_SUCCESS",
        2: "NLOPT_EPSVAL_REACHED",
        3: "NLOPT_FTOL_REACHED",
        4: "NLOPT_XTOL_REACHED",
        5: "NLOPT_ITERSMAX_REACHED",
        6: "NLOPT_MAXTIME_REACHED",
    }

    failure = {
        -1: "NLOPT_GENERIC_FAILURE",
        -2: "NLOPT_INVALID_ARGS",
        -3: "NLOPT_OUT_OF_MEMORY",
        -4: "NLOPT_ROUNDOFF_LIMITED",
        -5: "NLOPT_FORCED_STOP",
    }

    results.update(success)
    results.update(failure)

    return results[constant]

objective_function_numpy ¤

objective_function_numpy(x, grad, x_func, grad_func)
Source code in src/compas_cem/optimization/objective_func.py
def objective_function_numpy(x, grad, x_func, grad_func):
    """ """
    fx = x_func(x)

    if grad.size > 0:
        grad_func(x, grad)

    return fx

solve_proxy ¤

solve_proxy(
    topology,
    goals,
    parameters,
    algorithm,
    iters,
    eps=1e-06,
    kappa=1e-08,
    tmax=100,
    eta=1e-06,
)

Solve a constrained form-finding problem through a Proxy hyperspace tunnel.

Parameters:

  • topology (:class:`compas_cem.diagrams.TopologyDiagram`) –

    A topology diagram.

  • goals (``list``) –

    A list with the goals to optimize for.

  • parameters (``list``) –

    A list of optimization parameters.

  • algorithm (``str``) –

    The name of the gradient-based local optimization algorithm to use. Only the following local gradient-based optimization algorithms are supported:

    • SLSQP: Sequential Least Squares Programming
    • LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
    • MMA: Method of Moving Asymptotes
    • TNEWTON: Preconditioned Truncated Newton
    • AUGLAG: Augmented Lagrangian
    • VAR: Limited-Memory Variable-Metric Algorithm

    Defaults to "SLSQP". Refer to the NLopt documentation <https://nlopt.readthedocs.io/en/latest/>_ for more details on their theoretical underpinnings.

  • iters (``int``) –

    The maximum number of iterations to run the optimization algorithm for. Defaults to 100.

  • eps (``float``, default: 1e-06 ) –

    The convergence threshold for the output value of the objective function. Defaults to 1e-6.

  • kappa (``float``, default: 1e-08 ) –

    The convergence threshold for the norm of the gradient of the objective function. Defaults to 1e-8.

  • tmax (``int``, default: 100 ) –

    The maximum number of iterations the CEM form-finding algorithm will run for. If eta is hit first, the form-finding algorithm will stop early. Defaults to 100.

  • eta (``float``, default: 1e-06 ) –

    The numerical converge threshold of the CEM form-finding algorithm. If tmax is hit first, the form-finding algorithm will stop early. Defaults to 1e-6.

Returns:

  • topology ( :class:`compas_cem.diagrams.TopologyDiagram` ) –

    The topology diagram with optimal parameters as found by the optimization algorithm.

  • form ( :class:`compas_cem.diagrams.FormDiagram` ) –

    The constrained form diagram.

  • objective ( `float` ) –

    The final value of the objective function.

  • grad_norm ( `float` ) –

    The cummulative norm of the gradients.

  • iters ( `int` ) –

    The elapsed number of iterations.

  • duration ( `float` ) –

    The total optimization time in milliseconds.

  • status ( `str` ) –

    The final status of the optimization problem as per NLOpt.

Source code in src/compas_cem/optimization/proxy.py
def solve_proxy(
    topology,
    goals,
    parameters,
    algorithm,
    iters,
    eps=1e-6,
    kappa=1e-8,
    tmax=100,
    eta=1e-6,
):
    """
    Solve a constrained form-finding problem through a Proxy hyperspace tunnel.

    Parameters
    ----------
    topology : :class:`compas_cem.diagrams.TopologyDiagram`
        A topology diagram.
    goals : ``list``
        A list with the goals to optimize for.
    parameters : ``list``
        A list of optimization parameters.

    algorithm : ``str``, optional
        The name of the gradient-based local optimization algorithm to use.
        Only the following local gradient-based optimization algorithms are supported:

        - SLSQP: Sequential Least Squares Programming
        - LBFGS: Low-Storage Broyden-Fletcher-Goldfarb-Shanno
        - MMA: Method of Moving Asymptotes
        - TNEWTON: Preconditioned Truncated Newton
        - AUGLAG: Augmented Lagrangian
        - VAR: Limited-Memory Variable-Metric Algorithm

        Defaults to "SLSQP".
        Refer to the NLopt `documentation <https://nlopt.readthedocs.io/en/latest/>`_ for more details on their theoretical underpinnings.
    iters : ``int``, optional
        The maximum number of iterations to run the optimization algorithm for.
        Defaults to ``100``.
    eps : ``float``, optional
        The convergence threshold for the output value of the objective function.
        Defaults to ``1e-6``.
    kappa : ``float``, optional
        The convergence threshold for the norm of the gradient of the objective function.
        Defaults to ``1e-8``.
    tmax : ``int``, optional
        The maximum number of iterations the CEM form-finding algorithm will run for.
        If ``eta`` is hit first, the form-finding algorithm will stop early.
        Defaults to ``100``.
    eta : ``float``, optional
        The numerical converge threshold of the CEM form-finding algorithm.
        If ``tmax`` is hit first, the form-finding algorithm will stop early.
        Defaults to ``1e-6``.

    Returns
    -------
    topology : :class:`compas_cem.diagrams.TopologyDiagram`
        The topology diagram with optimal parameters as found by the optimization algorithm.
    form : :class:`compas_cem.diagrams.FormDiagram`
        The constrained form diagram.
    objective : `float`
        The final value of the objective function.
    grad_norm : `float`
        The cummulative norm of the gradients.
    iters : `int`
        The elapsed number of iterations.
    duration : `float`
        The total optimization time in milliseconds.
    status : `str`
        The final status of the optimization problem as per NLOpt.
    """
    # TODO: the optimizer import statement should be handled more elegantly
    from compas_cem.optimization import Optimizer

    optimizer = Optimizer()

    # add goals
    for goal in goals:
        optimizer.add_goal(goal)

    # add parameters
    for parameter in parameters:
        optimizer.add_parameter(parameter)

    form = optimizer.solve(
        topology=topology,
        algorithm=algorithm,
        iters=iters,
        eps=eps,
        kappa=kappa,
        tmax=tmax,
        eta=eta,
    )

    duration = optimizer.time_opt
    objective = optimizer.penalty
    evals = optimizer.evals
    grad_norm = optimizer.gradient_norm
    status = optimizer.status

    return topology, form, objective, grad_norm, evals, duration, status