Source code for einsteinpy.utils.dual

import numpy as np


[docs] class DualNumber: """ Numbers of the form, :math:`a + b\\epsilon`, where :math:`\\epsilon^2 = 0` and :math:`\\epsilon \\ne 0`. Their addition and multiplication properties make them suitable for Automatic Differentiation (AD). EinsteinPy uses AD for solving Geodesics in arbitrary spacetimes. This module is based on [1]_. References ---------- .. [1] Christian, Pierre and Chan, Chi-Kwan; "FANTASY: User-Friendly Symplectic Geodesic Integrator for Arbitrary Metrics with Automatic Differentiation"; `2021 ApJ 909 67 <https://doi.org/10.3847/1538-4357/abdc28>`__ """ def __init__(self, val, deriv): """ Constructor Parameters ---------- val : float Value deriv : float Directional Derivative """ self.val = float(val) self.deriv = float(deriv) def __str__(self): return f"DualNumber({self.val}, {self.deriv})" def __repr__(self): return self.__str__() def __add__(self, other): if isinstance(other, DualNumber): return DualNumber(self.val + other.val, self.deriv + other.deriv) return DualNumber(self.val + other, self.deriv) __radd__ = __add__ def __sub__(self, other): if isinstance(other, DualNumber): return DualNumber(self.val - other.val, self.deriv - other.deriv) return DualNumber(self.val - other, self.deriv) def __rsub__(self, other): if isinstance(other, DualNumber): return DualNumber(other.val - self.val, other.deriv - self.deriv) return DualNumber(other, 0) - self def __mul__(self, other): if isinstance(other, DualNumber): return DualNumber( self.val * other.val, self.deriv * other.val + self.val * other.deriv ) return DualNumber(self.val * other, self.deriv * other) __rmul__ = __mul__ def __truediv__(self, other): if isinstance(other, DualNumber): if self.val == 0 and other.val == 0: return DualNumber(self.deriv / other.deriv, 0.0) return DualNumber( self.val / other.val, (self.deriv * other.val - self.val * other.deriv) / (other.val**2), ) return DualNumber(self.val / other, self.deriv / other) def __rtruediv__(self, other): if isinstance(other, DualNumber): if self.val == 0 and other.val == 0: return DualNumber(other.deriv / self.deriv, 0.0) return DualNumber( other.val / self.val, (other.deriv * self.val - other.val * self.deriv) / (self.val**2), ) return DualNumber(other, 0).__truediv__(self) def __eq__(self, other): return (self.val == other.val) and (self.deriv == other.deriv) def __ne__(self, other): return not (self == other) def __neg__(self): return DualNumber(-self.val, -self.deriv) def __pow__(self, power): return DualNumber( self.val**power, self.deriv * power * self.val ** (power - 1) ) def sin(self): return DualNumber(np.sin(self.val), self.deriv * np.cos(self.val)) def cos(self): return DualNumber(np.cos(self.val), -self.deriv * np.sin(self.val)) def tan(self): return np.sin(self) / np.cos(self) def log(self): return DualNumber(np.log(self.val), self.deriv / self.val) def exp(self): return DualNumber(np.exp(self.val), self.deriv * np.exp(self.val))
def _deriv(func, x): """ Calculates first (partial) derivative of ``func`` at ``x`` Parameters ---------- func : callable Function to differentiate x : float Point, at which, the derivative will be evaluated Returns _______ float First partial derivative of ``func`` at ``x`` """ funcdual = func(DualNumber(x, 1.0)) if isinstance(funcdual, DualNumber): return funcdual.deriv return 0.0 def _diff_g(g, g_prms, coords, indices, wrt): """ Computes derivative of metric elements Parameters ---------- g : callable Metric (Contravariant) Function g_prms : array_like Tuple of parameters to pass to the metric E.g., ``(a,)`` for Kerr coords : array_like 4-Position indices : array_like 2-tuple, containing indices, indexing a metric element, whose derivative will be calculated wrt : int Coordinate, with respect to which, the derivative will be calculated Takes values from ``[0, 1, 2, 3]`` Returns ------- float Value of derivative of metric element at ``coords`` Raises ------ ValueError If ``wrt`` is not in [1, 2, 3, 4] or ``len(indices) != 2`` """ if wrt not in [0, 1, 2, 3]: raise ValueError(f"wrt takes values from [0, 1, 2, 3]. Supplied value: {wrt}") if len(indices) != 2: raise ValueError("indices must be a 2-tuple containing indices for the metric.") dual_coords = [ DualNumber(coords[0], 0.0), DualNumber(coords[1], 0.0), DualNumber(coords[2], 0.0), DualNumber(coords[3], 0.0), ] # Coordinate, against which, derivative will be propagated dual_coords[wrt].deriv = 1.0 return _deriv(lambda q: g(dual_coords, *g_prms)[indices], coords[wrt]) def _jacobian_g(g, g_prms, coords, wrt): """ Part of Jacobian of Metric Parameters ---------- g : callable Metric (Contravariant) Function g_prms : array_like Tuple of parameters to pass to the metric E.g., ``(a,)`` for Kerr coords : array_like 4-Position wrt : int Coordinate, with respect to which, the derivative will be calculated Takes values from ``[0, 1, 2, 3]`` Returns ------- numpy.ndarray Value of derivative of metric elements, w.r.t a particular coordinate, at ``coords`` """ J = np.zeros((4, 4)) for i in range(4): for j in range(4): if i <= j: J[i, j] = _diff_g(g, g_prms, coords, (i, j), wrt) J = J + J.T - np.diag(np.diag(J)) return J