Source code for gemseo_mma.opt.core.mma

# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""GCMMA-MMA-Python. This file is part of GCMMA-MMA-Python.

GCMMA-MMA-Python is licensed under the terms of GNU General Public License as published
by the Free Software Foundation. For more information and the LICENSE file,
see [here](https://github.com/arjendeetman/GCMMA-MMA-Python).
The original work is written by Krister Svanberg in MATLAB.
This is the python version of the code written Arjen Deetman. version 09-11-2019.

MMA optimizer.

Original work written by Krister Svanberg in Matlab.
This is the python version of the code written by Arjen Deetman.
"""

from __future__ import annotations

import numpy as np
from numpy import ndarray
from numpy.linalg import lstsq
from scipy.linalg import solve  # or use numpy: from numpy.linalg import solve
from scipy.sparse import diags  # or use numpy: from numpy import diag as diags


# Function for the MMA sub problem
[docs] def solve_mma_local_approximation_problem( m: int, n: int, n_iterations: int, xval: ndarray, xmin: ndarray, xmax: ndarray, xold1: ndarray, xold2: ndarray, f0val: ndarray, df0dx: ndarray, fval: ndarray, dfdx: ndarray, low: ndarray, upp: ndarray, a0: float, a: ndarray, c: ndarray, d: ndarray, move: float, external_move_limit: float = 10.0, internal_limit: float = 0.01, asyinit: float = 0.5, asyincr: float = 1.2, asydecr: float = 0.7, ) -> tuple[ ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ]: """MMA sub function. This function mmasub performs one MMA-iteration, aimed at solving the nonlinear programming problem: Minimize f_0(x) + a_0*z + sum( c_i*y_i + 0.5*d_i*(y_i)^2 ) subject to f_i(x) - a_i*z - y_i <= 0, i = 1,...,m xmin_j <= x_j <= xmax_j, j = 1,...,n z >= 0, y_i >= 0, i = 1,...,m Args: m: The number of general constraints. n: The number of variables x_j. n_iterations: The current iteration number (=1 the first time mmasub is called). xval: The column vector with the current values of the variables x_j. xmin: The column vector with the lower bounds for the variables x_j. xmax: The column vector with the upper bounds for the variables x_j. xold1: The value of xval, one iteration ago (provided that n_iterations>1). xold2: The value of xval, two iterations ago (provided that n_iterations>2). f0val: The value of the objective function f_0 at xval. df0dx: The column vector with the derivatives of the objective function f_0 with respect to the variables x_j, calculated at xval. fval: The column vector with the values of the constraint functions f_i, calculated at xval. dfdx: The (m x n)-matrix with the derivatives of the constraint functions f_i with respect to the variables x_j, calculated at xval. low: The column vector with the lower asymptotes from the previous iteration (provided that n_iterations>1). upp: The column vector with the upper asymptotes from the previous iteration (provided that n_iterations>1). a0: The constants a_0 in the term a_0*z. a: The column vector with the constants a_i in the terms a_i*z. c: The column vector with the constants c_i in the terms c_i*y_i. d: The column vector with the constants d_i in the terms 0.5*d_i*(y_i)^2. move: The maximum optimization step. external_move_limit: The maximum distance of the asymptotes from the current design variable value. internal_limit: The minimum distance of the asymptotes from the current design variable value. asyinit: The initial asymptotes distance from the current design variable value. asyincr: The incremental factor for successful iterations. asydecr: The decremental factor for unsuccessful iterations. Returns: The Column vector with the optimal values of the variables x_j in the current MMA subproblem. The Column vector with the optimal values of the variables y_i in the current MMA subproblem. The Scalar with the optimal value of the variable z in the current MMA subproblem. The Lagrange multipliers for the m general MMA constraints. The Lagrange multipliers for the n constraints alfa_j - x_j <= 0. The Lagrange multipliers for the n constraints x_j - beta_j <= 0. The Lagrange multipliers for the m constraints -y_i <= 0. The Lagrange multiplier for the single constraint -z <= 0. The Slack variables for the m general MMA constraints. The Column vector with the lower asymptotes, calculated and used in the current MMA subproblem. The Column vector with the upper asymptotes, calculated and used in the current MMA subproblem. """ epsimin = 0.0000001 raa0 = 0.00001 albefa = 0.1 eeen = np.ones((n, 1)) eeem = np.ones((m, 1)) zeron = np.zeros((n, 1)) # Calculation of the asymptotes low and upp if n_iterations <= 2: low = xval - asyinit * (xmax - xmin) upp = xval + asyinit * (xmax - xmin) else: zzz = (xval - xold1) * (xold1 - xold2) factor = eeen.copy() factor[np.where(zzz > 0)] = asyincr factor[np.where(zzz < 0)] = asydecr low = xval - factor * (xold1 - low) upp = xval + factor * (upp - xold1) lowmin = xval - external_move_limit * (xmax - xmin) lowmax = xval - internal_limit * (xmax - xmin) uppmin = xval + internal_limit * (xmax - xmin) uppmax = xval + external_move_limit * (xmax - xmin) low = np.maximum(low, lowmin) low = np.minimum(low, lowmax) upp = np.minimum(upp, uppmax) upp = np.maximum(upp, uppmin) # Calculation of the bounds alfa and beta zzz1 = low + albefa * (xval - low) zzz2 = xval - move * (xmax - xmin) zzz = np.maximum(zzz1, zzz2) alfa = np.maximum(zzz, xmin) zzz1 = upp - albefa * (upp - xval) zzz2 = xval + move * (xmax - xmin) zzz = np.minimum(zzz1, zzz2) beta = np.minimum(zzz, xmax) # Calculations of p0, q0, pp, qq and b xmami = xmax - xmin xmamieps = 0.00001 * eeen xmami = np.maximum(xmami, xmamieps) xmamiinv = eeen / xmami ux1 = upp - xval ux2 = ux1 * ux1 xl1 = xval - low xl2 = xl1 * xl1 uxinv = eeen / ux1 xlinv = eeen / xl1 p0 = zeron.copy() q0 = zeron.copy() p0 = np.maximum(df0dx, 0) q0 = np.maximum(-df0dx, 0) pq0 = 0.001 * (p0 + q0) + raa0 * xmamiinv p0 = p0 + pq0 q0 = q0 + pq0 p0 = p0 * ux2 q0 = q0 * xl2 pp = np.zeros((m, n)) qq = np.zeros((m, n)) pp = np.maximum(dfdx, 0) qq = np.maximum(-dfdx, 0) ppqq = 0.001 * (pp + qq) + raa0 * np.dot(eeem, xmamiinv.T) pp = pp + ppqq qq = qq + ppqq pp = (diags(ux2.flatten(), 0).dot(pp.T)).T qq = (diags(xl2.flatten(), 0).dot(qq.T)).T b = np.dot(pp, uxinv) + np.dot(qq, xlinv) - fval xmma, ymma, zmma, lam, xsi, eta, mu, zet, s = __subsolv( m, n, epsimin, low, upp, alfa, beta, p0, q0, pp, qq, a0, a, b, c, d ) # Return values return xmma, ymma, zmma, lam, xsi, eta, mu, zet, s, low, upp
# Function for solving the subproblem (can be used for MMA) def __subsolv( m: int, n: int, epsimin: float, low: ndarray, upp: ndarray, alfa: ndarray, beta: ndarray, p0: ndarray, q0: ndarray, pp: ndarray, qq: ndarray, a0: ndarray, a: ndarray, b: ndarray, c: ndarray, d: ndarray, ) -> tuple[ ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ]: """Solve the subproblem by a primal-dual Newton method. This function subsolv solves the MMA subproblem: minimize SUM[p0j/(uppj-xj) + q0j/(xj-lowj)] + a0*z + SUM[ci*yi + 0.5*di*(yi)^2], subject to SUM[pij/(uppj-xj) + qij/(xj-lowj)] - ai*z - yi <= bi, alfaj <= xj <= betaj, yi >= 0, z >= 0. Args: m: The number of general constraints. n: The number of variables x_j. epsimin:The convergence tolerance of the local approximation sub problem. low: The column vector with the lower asymptotes, calculated and used in the current MMA subproblem. upp: The column vector with the upper asymptotes, calculated and used in the current MMA subproblem. alfa: The column vector of local lower bound alfa_j. beta: The column vector of local upper bound beta_j. p0: The column vector of p0_j. q0: The column vector of q0_j. pp: The m x n matrix of p_ij. qq: The m x n matrix of q_ij. a0: The constants a_0 in the term a_0*z. a: The column vector with the constants a_i in the terms a_i*z. c: The column vector with the constants c_i in the terms c_i*y_i. d: The column vector with the constants d_i in the terms 0.5*d_i*(y_i)^2. b: The column vector of b_i. Returns: All the unkowns of the subproblem. """ een = np.ones((n, 1)) eem = np.ones((m, 1)) epsi = 1 x = 0.5 * (alfa + beta) y = eem.copy() z = np.array([[1.0]]) lam = eem.copy() xsi = een / (x - alfa) xsi = np.maximum(xsi, een) eta = een / (beta - x) eta = np.maximum(eta, een) mu = np.maximum(eem, 0.5 * c) zet = np.array([[1.0]]) s = eem.copy() itera = 0 # Start while epsi>epsimin while epsi > epsimin: epsvecn = epsi * een epsvecm = epsi * eem ux1 = upp - x xl1 = x - low ux2 = ux1 * ux1 xl2 = xl1 * xl1 uxinv1 = een / ux1 xlinv1 = een / xl1 plam = p0 + np.dot(pp.T, lam) qlam = q0 + np.dot(qq.T, lam) gvec = np.dot(pp, uxinv1) + np.dot(qq, xlinv1) dpsidx = plam / ux2 - qlam / xl2 rex = dpsidx - xsi + eta rey = c + d * y - mu - lam rez = a0 - zet - np.dot(a.T, lam) relam = gvec - a * z - y + s - b rexsi = xsi * (x - alfa) - epsvecn reeta = eta * (beta - x) - epsvecn remu = mu * y - epsvecm rezet = zet * z - epsi res = lam * s - epsvecm residu1 = np.concatenate((rex, rey, rez), axis=0) residu2 = np.concatenate((relam, rexsi, reeta, remu, rezet, res), axis=0) residu = np.concatenate((residu1, residu2), axis=0) residunorm = np.sqrt((np.dot(residu.T, residu)).item()) residumax = np.max(np.abs(residu)) ittt = 0 # Start while (residumax>0.9*epsi) and (ittt<200) while (residumax > 0.9 * epsi) and (ittt < 200): ittt = ittt + 1 itera = itera + 1 ux1 = upp - x xl1 = x - low ux2 = ux1 * ux1 xl2 = xl1 * xl1 ux3 = ux1 * ux2 xl3 = xl1 * xl2 uxinv1 = een / ux1 xlinv1 = een / xl1 uxinv2 = een / ux2 xlinv2 = een / xl2 plam = p0 + np.dot(pp.T, lam) qlam = q0 + np.dot(qq.T, lam) gvec = np.dot(pp, uxinv1) + np.dot(qq, xlinv1) gg = (diags(uxinv2.flatten(), 0).dot(pp.T)).T - ( diags(xlinv2.flatten(), 0).dot(qq.T) ).T dpsidx = plam / ux2 - qlam / xl2 delx = dpsidx - epsvecn / (x - alfa) + epsvecn / (beta - x) dely = c + d * y - lam - epsvecm / y delz = a0 - np.dot(a.T, lam) - epsi / z dellam = gvec - a * z - y - b + epsvecm / lam diagx = plam / ux3 + qlam / xl3 diagx = 2 * diagx + xsi / (x - alfa) + eta / (beta - x) diagxinv = een / diagx diagy = d + mu / y diagyinv = eem / diagy diaglam = s / lam diaglamyi = diaglam + diagyinv # Start if m<n if m < n: blam = dellam + dely / diagy - np.dot(gg, (delx / diagx)) bb = np.concatenate((blam, delz), axis=0) alam = np.asarray( diags(diaglamyi.flatten(), 0) + (diags(diagxinv.flatten(), 0).dot(gg.T).T).dot(gg.T) ) aar1 = np.concatenate((alam, a), axis=1) aar2 = np.concatenate((a, -zet / z), axis=0).T aa = np.concatenate((aar1, aar2), axis=0) solut = solve(aa, bb) dlam = solut[0:m] dz = solut[m : m + 1] dx = -delx / diagx - np.dot(gg.T, dlam) / diagx else: diaglamyiinv = eem / diaglamyi dellamyi = dellam + dely / diagy axx = np.asarray( diags(diagx.flatten(), 0) + (diags(diaglamyiinv.flatten(), 0).dot(gg).T).dot(gg) ) azz = zet / z + np.dot(a.T, (a / diaglamyi)) axz = np.dot(-gg.T, (a / diaglamyi)) bx = delx + np.dot(gg.T, (dellamyi / diaglamyi)) bz = delz - np.dot(a.T, (dellamyi / diaglamyi)) aar1 = np.concatenate((axx, axz), axis=1) aar2 = np.concatenate((axz.T, azz), axis=1) aa = np.concatenate((aar1, aar2), axis=0) bb = np.concatenate((-bx, -bz), axis=0) solut, _i, _j, _k = lstsq(aa, bb, rcond=-1) dx = solut[0:n] dz = solut[n : n + 1] dlam = ( np.dot(gg, dx) / diaglamyi - dz * (a / diaglamyi) + dellamyi / diaglamyi ) # End if m<n dy = -dely / diagy + dlam / diagy dxsi = -xsi + epsvecn / (x - alfa) - (xsi * dx) / (x - alfa) deta = -eta + epsvecn / (beta - x) + (eta * dx) / (beta - x) dmu = -mu + epsvecm / y - (mu * dy) / y dzet = -zet + epsi / z - zet * dz / z ds = -s + epsvecm / lam - (s * dlam) / lam xx = np.concatenate((y, z, lam, xsi, eta, mu, zet, s), axis=0) dxx = np.concatenate((dy, dz, dlam, dxsi, deta, dmu, dzet, ds), axis=0) # stepxx = -1.01 * dxx / xx stmxx = np.max(stepxx) stepalfa = -1.01 * dx / (x - alfa) stmalfa = np.max(stepalfa) stepbeta = 1.01 * dx / (beta - x) stmbeta = np.max(stepbeta) stmalbe = max(stmalfa, stmbeta) stmalbexx = max(stmalbe, stmxx) stminv = max(stmalbexx, 1.0) steg = 1.0 / stminv # xold = x.copy() yold = y.copy() zold = z.copy() lamold = lam.copy() xsiold = xsi.copy() etaold = eta.copy() muold = mu.copy() zetold = zet.copy() sold = s.copy() # itto = 0 resinew = 2 * residunorm # Start: while (resinew>residunorm) and (itto<50) while (resinew > residunorm) and (itto < 50): itto = itto + 1 x = xold + steg * dx y = yold + steg * dy z = zold + steg * dz lam = lamold + steg * dlam xsi = xsiold + steg * dxsi eta = etaold + steg * deta mu = muold + steg * dmu zet = zetold + steg * dzet s = sold + steg * ds ux1 = upp - x xl1 = x - low ux2 = ux1 * ux1 xl2 = xl1 * xl1 uxinv1 = een / ux1 xlinv1 = een / xl1 plam = p0 + np.dot(pp.T, lam) qlam = q0 + np.dot(qq.T, lam) gvec = np.dot(pp, uxinv1) + np.dot(qq, xlinv1) dpsidx = plam / ux2 - qlam / xl2 rex = dpsidx - xsi + eta rey = c + d * y - mu - lam rez = a0 - zet - np.dot(a.T, lam) relam = gvec - np.dot(a, z) - y + s - b rexsi = xsi * (x - alfa) - epsvecn reeta = eta * (beta - x) - epsvecn remu = mu * y - epsvecm rezet = np.dot(zet, z) - epsi res = lam * s - epsvecm residu1 = np.concatenate((rex, rey, rez), axis=0) residu2 = np.concatenate( (relam, rexsi, reeta, remu, rezet, res), axis=0 ) residu = np.concatenate((residu1, residu2), axis=0) resinew = np.sqrt(np.dot(residu.T, residu)) steg = steg / 2 # End: while (resinew>residunorm) and (itto<50) residunorm = resinew.copy() residumax = max(abs(residu)) steg = 2 * steg # End: while (residumax>0.9*epsi) and (ittt<200) epsi = 0.1 * epsi # End: while epsi>epsimin xmma = x.copy() ymma = y.copy() zmma = z.copy() lamma = lam xsimma = xsi etamma = eta mumma = mu zetmma = zet smma = s # Return values return xmma, ymma, zmma, lamma, xsimma, etamma, mumma, zetmma, smma # Function for Karush-Kuhn-Tucker check
[docs] def compute_kkt_residual_on_local_approximation( m: int, n: int, x: ndarray, y: ndarray, z: ndarray, lam: ndarray, xsi: ndarray, eta: ndarray, mu: ndarray, zet: ndarray, s: ndarray, xmin: ndarray, xmax: ndarray, df0dx: ndarray, fval: ndarray, dfdx: ndarray, a0: float, a: ndarray, c: ndarray, d: ndarray, ) -> tuple[ndarray, ndarray, ndarray]: """KKT residual computation. The left hand sides of the KKT conditions for the following nonlinear programming problem are calculated. Minimize f_0(x) + a_0*z + sum(c_i*y_i + 0.5*d_i*(y_i)^2) subject to f_i(x) - a_i*z - y_i <= 0, i = 1,...,m xmax_j <= x_j <= xmin_j, j = 1,...,n z >= 0, y_i >= 0, i = 1,...,m Args: m: The number of general constraints. n: The number of variables x_j. x: The current values of the n variables x_j. y: The current values of the m variables y_i. z: The current value of the single variable z. lam: The Lagrange multipliers for the m general constraints. xsi: The Lagrange multipliers for the n constraints xmin_j - x_j <= 0. eta: The Lagrange multipliers for the n constraints x_j - xmax_j <= 0. mu: The Lagrange multipliers for the m constraints -y_i <= 0. zet: The Lagrange multiplier for the single constraint -z <= 0. s: The Slack variables for the m general constraints. xmin: The Lower bounds for the variables x_j. xmax: The Upper bounds for the variables x_j. df0dx: The vector with the derivatives of the objective function f_0 with respect to the variables x_j, calculated at x. fval: The vector with the values of the constraint functions f_i, calculated at x. dfdx: The (m x n)-matrix with the derivatives of the constraint functions f_i with respect to the variables x_j, calculated at x. dfdx(i,j) = the derivative of f_i with respect to x_j. a0: The constants a_0 in the term a_0*z. a: The vector with the constants a_i in the terms a_i*z. c: The vector with the constants c_i in the terms c_i*y_i. d: The vector with the constants d_i in the terms 0.5*d_i*(y_i)^2. Returns: The vector residual, its norm and its maximum. """ rex = df0dx + np.dot(dfdx.T, lam) - xsi + eta rey = c + d * y - mu - lam rez = a0 - zet - np.dot(a.T, lam) relam = fval - a * z - y + s rexsi = xsi * (x - xmin) reeta = eta * (xmax - x) remu = mu * y rezet = zet * z res = lam * s residu1 = np.concatenate((rex, rey, rez), axis=0) residu2 = np.concatenate((relam, rexsi, reeta, remu, rezet, res), axis=0) residu = np.concatenate((residu1, residu2), axis=0) residunorm = np.sqrt((np.dot(residu.T, residu)).item()) residumax = np.max(np.abs(residu)) return residu, residunorm, residumax