Source code for pyFANTOM.Optimizers.CPU.MMA

import numpy as np
from .._optimizer import Optimizer
from ...Problem._problem import Problem
from ...mma.CPU import mmasub
import time

[docs] class MMA(Optimizer): """ Method of Moving Asymptotes (MMA) nonlinear optimizer. Industry-standard gradient-based optimizer for topology optimization. Handles general nonlinear constraints efficiently via convex approximations. Most robust optimizer in pyFANTOM. Parameters ---------- problem : Problem Optimization problem (e.g., MinimumCompliance) sub_tol : float, optional Sub-problem convergence tolerance (default: 1e-7) sub_maxiter : int, optional Sub-problem max iterations (default: 100) change_tol : float, optional Design variable change convergence tolerance (default: 1e-4) fun_tol : float, optional Objective function change tolerance (default: 1e-6) move : float, optional Move limit for design variables, range [0,1] (default: 0.5) timer : bool, optional Return iteration time if True (default: False) Attributes ---------- iteration : int Current iteration number change : float L2 norm of design variable change change_f : float Relative objective function change Methods ------- iter() Perform one MMA iteration converged() Check convergence based on change_tol and fun_tol logs() Return dict of optimization metrics Notes ----- - **Best for**: General constraints, multi-material, multi-constraint problems - **Convergence**: Typically 50-200 iterations for topology optimization - **Move limit**: Smaller = more stable but slower, larger = faster but may oscillate - **Sub-problem**: Convex approximation solved each iteration - **Dual variables**: Automatically updated via Lagrange multipliers Examples -------- >>> from pyFANTOM.CPU import MMA, MinimumCompliance >>> optimizer = MMA(problem=problem, sub_tol=1e-7, fun_tol=1e-6) >>> for i in range(100): >>> optimizer.iter() >>> logs = optimizer.logs() >>> print(f"Iter {i}: C={logs['objective']:.2e}, Vol={logs['volume']:.3f}") >>> if optimizer.converged(): >>> break """
[docs] def __init__(self, problem: Problem, sub_tol=1e-7, sub_maxiter=100, change_tol=1e-4, fun_tol=1e-6, move=0.5, timer=False): super().__init__(problem) self.last_desvars = np.copy(problem.get_desvars()) self.last_f = problem.f() self.m = problem.m() self.ocP = None self.lambda_map = problem.constraint_map() self.bounds = problem.bounds() self.change = np.inf self.change_f = np.inf self.change_tol = change_tol self.fun_tol = fun_tol self.iteration = 0 self.move = move self.sub_tol = sub_tol self.sub_maxiter = sub_maxiter self.x_1 = np.copy(problem.get_desvars()) self.x_2 = np.copy(problem.get_desvars()) self.low = np.ones([problem.get_desvars().shape[0], 1]) * self.bounds[0] self.upp = np.ones([problem.get_desvars().shape[0], 1]) * self.bounds[1] self.comp_base = self.problem.f() self.N = self.problem.N() self.timer = timer
[docs] def iter(self): """ Perform one MMA optimization iteration. Updates design variables by solving a convex sub-problem approximation of the original problem. Uses moving asymptotes to control step size. Returns ------- float, optional If timer=True, returns iteration time in seconds Notes ----- - Solves convex sub-problem using mmasub() - Updates moving asymptotes (low, upp) for next iteration - Tracks design variable change and objective change - Handles NaN values by clipping to bounds """ if self.timer: start_time = time.time() desvars = self.problem.get_desvars() dg = self.problem.nabla_g() df = self.problem.nabla_f() if self.iteration == 0: self.f0_val = self.problem.f() f_val = self.problem.g().reshape(-1,1) a = np.zeros([self.m,1]) c = np.ones([self.m,1]) * 100000 d = np.zeros([self.m,1]) desvars_new, _, _, _, _, _, _, _, _, self.low, self.upp = mmasub( self.m, self.N, self.iteration+1, desvars.reshape(-1,1), np.ones_like(desvars).reshape(-1,1) * self.bounds[0], np.ones_like(desvars).reshape(-1,1) * self.bounds[1], self.x_1.reshape(-1,1), self.x_2.reshape(-1,1), 0, df.reshape(-1,1), f_val.reshape(-1,1), dg, self.low, self.upp, 1.0, a, c, d, move=self.move, sub_maxiter=self.sub_maxiter, sub_tol=self.sub_tol ) self.x_2 = np.copy(self.x_1) self.x_1 = np.copy(desvars) self.iteration += 1 if self.timer: end_time = time.time() # make small adjustments to avoid nan values if np.isnan(desvars_new).any(): desvars_new = np.clip(desvars + self.sub_tol, self.bounds[0], self.bounds[1]) self.problem.set_desvars(desvars_new.reshape(-1)) self.change = np.linalg.norm(self.last_desvars - desvars_new.reshape(-1)) self.change_f = np.abs((self.problem.f()-self.last_f)/self.problem.f()) self.last_f = self.problem.f() self.last_desvars = self.problem.get_desvars().copy() if self.timer: return end_time - start_time
[docs] def converged(self, *args, **kwargs): """ Check if optimizer has converged. Returns ------- bool True if convergence criteria are met: - Problem penalty continuation is complete (is_terminal() == True) - Design variable change <= change_tol - Objective function change <= fun_tol Notes ----- Convergence requires both design change and objective change to be below tolerances. Also checks that penalty continuation (if used) has finished. """ if not self.problem.is_terminal(): return False elif self.change <= self.change_tol and self.change_f <= self.fun_tol: return True else: return False
[docs] def logs(self): """ Return diagnostic information for current iteration. Returns ------- dict Dictionary with keys: - 'objective': Current objective function value - 'variable change': L2 norm of design variable change - 'function change': Relative objective function change - Additional keys from problem.logs() (e.g., 'iteration', 'residual') Notes ----- Used for monitoring optimization progress and convergence. """ problem_logs = self.problem.logs() return{ 'objective': float(self.last_f), 'variable change': float(self.change), 'function change': float(self.change_f), **problem_logs }