Source code for noads.gam_jax.utils.math

#!/usr/bin/env python3
# Copyright 2025 ISAE-SUPAERO, https://www.isae-supaero.fr/en/
# Copyright 2025 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 Lesser 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser 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.

"""A math toolbox for MARILib.

:author: DRUOT Thierry, original Scilab implementation

:author: ROCHES Pascal, portage to Python
"""

# ruff: noqa: D205, E501, N806, D103, N803, F821

import warnings

import numpy as np
from numpy.linalg import solve
from numpy.linalg.linalg import LinAlgError


[docs] def great_circle_distance(p1, p2): """Compute the great circle distance between 2 points p : (lat, long). """ r = 6371000.0 v1 = np.array([ np.cos(p1[0]) * np.sin(p1[1]), np.cos(p1[0]) * np.cos(p1[1]), np.sin(p1[0]), ]) v2 = np.array([ np.cos(p2[0]) * np.sin(p2[1]), np.cos(p2[0]) * np.cos(p2[1]), np.sin(p2[0]), ]) s = angle( np.linalg.norm(np.cross(v1, v2)), np.dot(v1, v2), typ=1 ) # Get an angle between -pi and pi return r * abs(s)
[docs] def angle(sin_a, cos_a, typ=2): """Calculate an angle from its sine and cosine coordinates if typ=1 then -180<a_<180, if typ=2 then 0<a_<360. """ epsilon = 1.0e-15 if abs(cos_a - 1) < epsilon: a = 0.0 elif abs(cos_a + 1) < epsilon: a = np.pi elif abs(sin_a - 1) < epsilon: a = 0.5 * np.pi elif abs(sin_a + 1) < epsilon: a = -0.5 * np.pi else: a = np.sign(sin_a) * abs(np.arccos(cos_a)) if typ == 2: a = a + (1 - np.sign(a)) * np.pi if a > 2 * np.pi: a = a - 2.0 * np.pi return a
[docs] def rotate_vector(vec, axe, cos_a, sin_a): """Retrieve the vector vec rotated by the angle a around axe, using Olinde-Rodrigues formula v_star = vec + sin_a * np.cross(axe, vec) + (1 - cos_a) * np.cross(axe, np.cross(axe, vec)). """ return ( cos_a * vec + (1 - cos_a) * np.dot(vec, axe) * axe + sin_a * np.cross(axe, vec) )
[docs] def rotate_point(pivot, axe, angle, p0): """Retrieve the point coming from the rotation of p0 of angle, around the axis defined by pivot and axe.""" vec = p0 - pivot r0 = vec - np.dot(vec, axe) * axe Q = np.array([[0, -axe[2], axe[1]], [axe[2], 0, -axe[0]], [-axe[1], axe[0], 0]]) RmI = np.sin(angle) * Q + (1 - np.cos(angle)) * np.matmul(Q, Q) return p0 + np.matmul(RmI, r0)
[docs] def renorm(v): n = np.linalg.norm(v) return v / n if n > 0 else np.zeros_like(v)
[docs] def cross_op(v): """Build the skew-symmetric matrix of vector v.""" return np.array([[0, v[2], -v[1]], [-v[2], 0, v[0]], [v[1], -v[0], 0]])
[docs] def lin_interp_1d(x, X, Y): """Linear interpolation without any control. :param x: current position :param X: array of the abscissa of the known points :param Y: array of the known values at given abscissa :return: y the interpolated value of Y at x """ n = np.size(X) for j in range(1, n): if x < X[j]: return Y[j - 1] + (Y[j] - Y[j - 1]) * (x - X[j - 1]) / (X[j] - X[j - 1]) return Y[n - 2] + (Y[n - 1] - Y[n - 2]) * (x - X[n - 2]) / (X[n - 1] - X[n - 2])
[docs] def vander3(X): """Return the vandermonde matrix of a dim 3 array A = [X^2, X, 1].""" return np.array([ [X[0] ** 2, X[0], 1.0], [X[1] ** 2, X[1], 1.0], [X[2] ** 2, X[2], 1.0], ])
[docs] def trinome(A, Y): """Calculate trinome coefficients from 3 given points A = [X2, X, 1] (Vandermonde matrix). """ X = np.array([A[0][1], A[1][1], A[2][1]]) X2 = np.array([A[0][0], A[1][0], A[2][0]]) det = X2[0] * (X[1] - X[2]) - X2[1] * (X[0] - X[2]) + X2[2] * (X[0] - X[1]) adet = Y[0] * (X[1] - X[2]) - Y[1] * (X[0] - X[2]) + Y[2] * (X[0] - X[1]) bdet = X2[0] * (Y[1] - Y[2]) - X2[1] * (Y[0] - Y[2]) + X2[2] * (Y[0] - Y[1]) cdet = ( X2[0] * (X[1] * Y[2] - X[2] * Y[1]) - X2[1] * (X[0] * Y[2] - X[2] * Y[0]) + X2[2] * (X[0] * Y[1] - X[1] * Y[0]) ) if det != 0: C = np.array([adet / det, bdet / det, cdet / det]) elif X[0] != X[2]: C = np.array([0.0, Y[0] - Y[2], Y[2] * X[0] - Y[0] * X[2] / (X[0] - X[2])]) else: C = np.array([0.0, 0.0, (Y[0] + Y[1] + Y[2]) / 3.0]) return C
[docs] def maximize_1d(xini, dx, *fct): """Optimize 1 single variable, no constraint. :param xini: initial value of the variable. :param dx: fixed search step. :param fct: function with the signature : ['function_name',a1,a2,a3,...,an] and function_name(x,a1,a2,a3,...,an). """ n = len(fct[0]) X0 = xini Y0 = fct[0][0](X0, *fct[0][1:n]) X1 = X0 + dx Y1 = fct[0][0](X1, *fct[0][1:n]) if Y0 > Y1: dx = -dx X0, X1 = X1, X0 X2 = X1 + dx Y2 = fct[0][0](X2, *fct[0][1:n]) while Y1 < Y2: X0 = X1 X1 = X2 X2 = X2 + dx Y0 = Y1 Y1 = Y2 Y2 = fct[0][0](X2, *fct[0][1:n]) X = np.array([X0, X1, X2]) Y = np.array([Y0, Y1, Y2]) A = vander3(X) # [X**2, X, numpy.ones(3)] C = trinome(A, Y) xres = -C[1] / (2.0 * C[0]) yres = fct[0][0](xres, *fct[0][1:n]) rc = 1 return (xres, yres, rc)
[docs] def look_ahead_4_zero(xini, dx, *fct): """Look ahead for a zero of the function. :param xini: initial value of the variable. :param dx: fixed search step. :param fct: function with the signature : ['function_name',a1,a2,a3,...,an] and function_name(x,a1,a2,a3,...,an). """ n = len(fct[0]) X0 = xini Y0 = fct[0][0](X0, *fct[0][1:n]) X1 = X0 + dx Y1 = fct[0][0](X1, *fct[0][1:n]) while Y0 * Y1 > 0: X0 = X1 Y0 = Y1 X1 = X1 + dx Y1 = fct[0][0](X1, *fct[0][1:n]) xres = X0 + (X1 - X0) * (-Y0) / (Y1 - Y0) yres = 0 rc = 1 return (xres, yres, rc)
[docs] def newton_solve( res_func, x0, dres_dy=None, args=(), res_max=1e-12, relax=0.995, max_iter=50, full_output=False, ): if res_max <= 0: msg = f"res_max too small ({res_max:g} <= 0)" raise ValueError(msg) if max_iter < 1: msg = "max_iter must be greater than 0" raise ValueError(msg) if dres_dy is None: res_func, dres_dy = get_jac_func(res_func, args) msg = "" ier = 1 k = 0 y_curr = np.atleast_1d(1.0 * x0) myargs = (y_curr, *args) curr_res = res_func(*myargs) n0 = norm(curr_res) if n0 == 0.0: return y_curr, n0, 1 if not np.any(dres_dy(*myargs)): msg = "jacobian was zero." if marilib.is_driven_by_gems: warnings.warn(msg, RuntimeWarning, stacklevel=2) if full_output: infodict = {"stop_crit": n0, "niter": 1} return y_curr, infodict, ier, msg return y_curr stop_crit = norm(curr_res) / n0 while k < max_iter and stop_crit > res_max: drdy = dres_dy(*myargs) if marilib.is_using_autograd: from autograd.np.numpy_boxes import ArrayBox if isinstance(drdy, ArrayBox): drdy = drdy._value if isinstance(curr_res, ArrayBox): curr_res = curr_res._value newt_step = solve( np.atleast_2d(drdy).astype("float64"), curr_res.astype("float64") ) y_curr -= relax * newt_step myargs = (y_curr, *args) curr_res = res_func(*myargs) stop_crit = norm(curr_res) / n0 k += 1 if k == max_iter and stop_crit > res_max: ier = 0 msg = ( "Failed to converge Newton solver within " + str(max_iter) + " iterations " ) if marilib.is_driven_by_gems: raise LinAlgError(msg) if full_output: infodict = {"stop_crit": stop_crit, "niter": k} return y_curr, infodict, ier, msg return y_curr
[docs] def norm(x): return np.dot(x, x.T) ** 0.5
[docs] def approx_jac(res, y, args=(), step=1e-7): if not hasattr(y, "__len__"): y = np.array([y]) n = 1 else: n = len(y) jac = [] myargs = (y, *args) res_ref = res(*myargs) pert = np.zeros_like(y) for i in range(n): yi = y[i] if hasattr(yi, "_value"): yi = yi._value pert[i] = step * 10 ** int(np.log10(abs(yi))) y += pert myargs_loc = (y, *args) res_pert = res(*myargs_loc) jac.append((res_pert - res_ref) / pert[i]) y -= pert pert[i] = 0.0 if len(jac) == 1: return jac[0] return np.concatenate(jac)
[docs] def get_jac_func(res, args=(), step=1e-6): if marilib.is_using_autograd: from autograd import jacobian from autograd.np.numpy_boxes import ArrayBox jac = jacobian(res, argnum=0) def j_func(*args): val = jac(*args) if isinstance(val, np.ndarray): return val if isinstance(val, ArrayBox): return val._value return None def my_res(*args): val = res(*args) if isinstance(val, np.ndarray): return val if isinstance(val, ArrayBox): return val._value return None return my_res, j_func def apprx(y, *args): return approx_jac(res, y, args, step) return res, apprx