class Metric(object): """ Abstract metric object. Metrics are numpy arrays of callable functions representing each components of a metric tensor. Parameters ========== coords: iterable List of Symbols denoting the coordinates used for argument bookkeeping. matrix: Matrix | ndarray | list | tuple Matrix representing representing the components of the metric. *args: Symbols Symbols representing additional dependencies of the metric. Examples ======== >>> from numpy import pi >>> from sympy import symbols, trace >>> from gravispy.geom.metric import Schwarzschild >>> t, r, th, ph, M = symbols('t r theta phi M', real=True) >>> S = Schwarzschild([t, r, th, ph], M, timelike=True) >>> S.as_Matrix() Matrix([ [-2*M/r + 1, 0, 0, 0], [ 0,-1/(-2*M/r + 1), 0, 0], [ 0, 0, -r**2, 0], [ 0, 0, 0, -r**2*sin(theta)**2]]) >>> S.args (r, theta, M) >>> S(10, pi/2, 1) array([[ 0.8 , 0. , 0. , 0. ], [ 0. , -1.25 , 0. , 0. ], [ 0. , 0. , -100. , 0. ], [ 0. , 0. , 0. , -100. ]]) >>> S[0,0](10, pi/2, 1) 0.8 >>> S(10, pi/2, 1)[1,1] -1.25 >>> S.set_conditions((M, 1)) >>> S.as_Matrix() Matrix([ [-2/r + 1, 0, 0, 0], [ 0,-1/(-2/r + 1), 0, 0], [ 0, 0, -r**2, 0], [ 0, 0, 0, -r**2*sin(theta)**2]]) >>> S.args (r, theta) >>> S.vars {'M': 1} >>> S.set_conditions((th, pi/2)) >>> S.as_Matrix() Matrix([ [-2/r + 1, 0, 0, 0], [ 0,-1/(-2/r + 1), 0, 0], [ 0, 0, 0, 0], [ 0, 0, 0, -1.0*r**2]]) >>> S.args (r,) >>> S.coords {'t': t, 'r': r, 'theta': 1.5707963267948966, 'phi': phi} >>> S.conditions {'M': 1, 'theta': 1.5707963267948966} >>> S(20) # only requires one argument under these conditions. array([[ 0.9 , 0. , 0. , 0. ], [ 0. , -1.11111111, 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , -400. ]]) >>> S.applyfunc(trace) -1.0*r**2 + 1 - 1/(1 - 2/r) - 2/r >>> Si = S.inv() >>> Si.as_Matrix() Matrix([ [1/(2/r + 1), 0, 0, 0], [ 0,-1 + 2/r, 0, 0], [ 0, 0, 0, 0], [ 0, 0, 0, -1.0/r**2]]) >>> Si.conditions {'M': 1, 'theta': 1.5707963267948966} """ def __init__(self, coords, matrix, *args, **kwargs): self.__args = args self.__kwargs = kwargs self._inv_method = self.__kwargs.get('inv_method', None) self._lambdify_modules = self.__kwargs.get('lambdify_modules', None) self.is_metric = True self.is_spacetime = False self._A = Matrix(matrix) if not self._A.is_square: raise ValueError('Matrix must be square.') if not all(map(isinstance, coords, len(coords) * [Symbol])): raise TypeError('coordinates must be Symbols') self.shape = self._A.shape self.dims = self.shape[0] # the basis remains constant self.basis = tuple(coords) # coordinates are used as arguments to lambdified metric compononents. # self._coords is subject to change depending on conditions # set by the user. self._coords = self.basis # variables are used as arguments to lambdified metric compononents. # self._vars is subject to change depending on conditions # set by the user. self._vars = tuple(v for v in self.__args if isinstance(v, Symbol)) # self.coords and self.vars are the canonical representation for the # metric arguments. they retain their initial values as keys to the # dictionary but their values are subject to change based on conditions # set by the user. self.coords = dict(zip(map(str, self._coords), self._coords)) self.vars = dict(zip(map(str, self._vars), self._vars)) # self.args is the tuple representing what will be used in lambdified # metric components. their order is to remain constant but elements may # be removed by conditions. coord_args = (x for x in self._coords if x in self._A.free_symbols) var_args = (v for v in self._vars if v in self._A.free_symbols) self.args = tuple(coord_args) + tuple(var_args) if len(self.basis) is not self.shape[0]: raise ValueError('coordinates do not match metric dimensions') elif len(self.args) < len(self._A.free_symbols): raise ValueError('coordinates and variables given do not '\ 'sufficiently describe the metric') self.conditions = self.__kwargs.get('conditions', {}) self.assumptions = { 'spherical': False, 'axial': False, 'static': False, } self._T = None self._I = None self._Agenerator = None self._Tgenerator = None self._Igenerator = None self._set_generators() def _set_generators(self): self._T = self._A.T # the reduced metric is a nonsingular version of the metric. # singular metrics may arise from conditions set by the user. reduced_metric = self._A.copy() zero_idxs = [ idx for idx in range(reduced_metric.rows) if (reduced_metric.row(idx).is_zero and reduced_metric.col(idx).is_zero) ] for offset, idx in enumerate(zero_idxs): reduced_metric.row_del(idx - offset) reduced_metric.col_del(idx - offset) self._I = reduced_metric.inv(method=self._inv_method) for idx in zero_idxs: # replace the missing row and column. self._I = self._I.col_insert(idx, zeros(self._I.shape[0], 1))\ .row_insert(idx, zeros(1, self._I.shape[1]+1)) self._Agenerator = lambdify(self.args, self._A, modules=self._lambdify_modules) self._Tgenerator = lambdify(self.args, self._T, modules=self._lambdify_modules) self._Igenerator = lambdify(self.args, self._I, modules=self._lambdify_modules) def A(self, *args): return self._Agenerator(*args) def T(self, *args): return self._Tgenerator(*args) def I(self, *args): return self._Igenerator(*args) def as_Matrix(self): return self._A.copy() def as_ndarray(self): return np.asarray(self._A) def applyfunc(self, func=Matrix.applyfunc, return_metric=False, *args, **kwargs): res = self._A.__getattribute__(func.__name__)(*args, **kwargs) if not isinstance(res, Matrix) or not return_metric: return res else: return Metric(self.basis, res, conditions=self.conditions, *self.__args, **self.__kwargs) def inv(self): return Metric(self.basis, self._I, conditions=self.conditions, *self.__args, **self.__kwargs) def transpose(self): return Metric(self.basis, self._T, conditions=self.conditions, *self.__args, **self.__kwargs) def set_conditions(self, *args): if (not all(map(isinstance, args, len(args) * [tuple])) or any([len(arg) is not 2 for arg in args])): raise TypeError('arguments must be tuples of length 2') if not all( map(lambda a: isinstance(a[1], (int, float)) or a[1].is_number, args)): raise ValueError('conditionals must be constants') sub_dict = dict(args) self._A = self._A.subs(sub_dict) self.conditions.update(sub_dict) coords_selector = [coord in sub_dict.keys() for coord in self._coords] vars_selector = [var in sub_dict.keys() for var in self._vars] coord_keys = tuple(it.compress(self._coords, coords_selector)) var_keys = tuple(it.compress(self._vars, vars_selector)) for idx in range(len(self._coords)): if coords_selector[idx]: # replace the metric column and row of the coordinate # with 0 since the coordinate's differential is 0 self._A.row_del(idx) self._A.col_del(idx) self._A = self._A.col_insert(idx, zeros(self._A.shape[0], 1))\ .row_insert(idx, zeros(1, self._A.shape[1]+1)) self.coords.update( dict( zip(map(str, coord_keys), [sub_dict[key] for key in coord_keys]))) self.vars.update( dict(zip(map(str, var_keys), [sub_dict[key] for key in var_keys]))) self._coords = tuple( it.compress(self._coords, np.logical_not(coords_selector))) self._vars = tuple( it.compress(self._vars, np.logical_not(vars_selector))) # update the args such that they preserve their ordering coord_args = (x for x in self._coords if x in self._A.free_symbols) var_args = (v for v in self._vars if v in self._A.free_symbols) self.args = tuple(coord_args) + tuple(var_args) self._set_generators() def __getitem__(self, key): elem = np.asarray(self._A).__getitem__(key) return lambdify(self.args, elem, modules=self._lambdify_modules) def __call__(self, *args): return self.A(*args) def __str__(self): return self.as_ndarray().__str__() def __repr__(self): return str(self.__class__)