def get_time_deriv(self, t, q, y): """ Evaluates dydt at a given time t by interpolating dydt at 4 nearby nodes with cubic interpolation. Use get_time_deriv_from_index when possible. """ if t < self.t[0] or t > self.t[-1]: raise Exception("Cannot extrapolate time derivative!") i0 = np.argmin(abs(self.t - t)) if t > self.t[i0]: imin = i0 - 1 else: imin = i0 - 2 imin = min(max(0, imin), len(self.t) - 4) dydts = np.array( [self.get_time_deriv_from_index(imin + i, q, y) for i in range(4)]) ts = self.t[imin:imin + 4] dydt = np.array([_splinterp_Cwrapper(np.array([t]), ts, x)[0] \ for x in dydts.T]) return dydt
def __call__(self, x, fM_low=None, fM_ref=None, dtM=None, timesM=None, dfM=None, freqsM=None, mode_list=None, ellMax=None, precessing_opts=None, tidal_opts=None, par_dict=None): """ Evaluates a precessing surrogate model. Arguments: x: Parameters, x = q, chiA0, chiB0. Where, q: The mass ratio mA/mB, where A (B) refers to the heavier BH. chiA0, chiB0: The initial dimensionless spins given in the coprecessing frame. They should be length 3 lists or numpy arrays. These are $\\vec{\chi_{1,2}^\mathrm{copr}(t_0)$ in THE PAPER. chiA0: The chiA vector at the reference time. chiB0: The chiB vector at the reference time. The above spins use lalsimulation conventions, where \chi_z = \chi \cdot \hat{L}, where L is the orbital angular momentum vector at the epoch. \chi_x = \chi \cdot \hat{n}, where n = body2 -> body1 is the separation vector at the epoch. body1 is the heavier body. \chi_y = \chi \cdot \hat{L \cross n}. These spin components are frame-independent as they are defined using vector inner products. This is equivalent to specifying the spins in the coorbital frame used in the surrogate papers. fM_low: Initial frequency in dimensionless units. Currently only fM_low = 0 is allowed, in which case the full surrogate is returned. fM_ref: Reference frequency in dimensionless units, at which the reference frame and spins are defined. dtM: Time step in dimensionless units. timesM: Dimensionless times at which the output should be sampled. Give at most one of dtM and timesM. If neither is given returns the results sampled at self.t_coorb. dfM, freqsM: These should be None, as we only have time domain models so far. mode_list: This should be None, use ellMax instead. ellMax: The maximum ell modes to use. The NRSur7dq4 surrogate model contains modes up to L=4. Using ellMax=2 or ellMax=3 reduces the evaluation time. precessing_opts: A dictionary containing optional parameters for a precessing surrogate model. Default: None. Allowed keys are: init_orbphase: The orbital phase in the coprecessing frame at the reference epoch. Default: None, in which case the coorbital frame and coprecessing frame are the same. init_quat: The unit quaternion (length 4 vector) giving the rotation from the coprecessing frame to the inertial frame at the reference epoch. Default: None, in which case the coprecessing frame is the same as the inertial frame. return_dynamics: Return the frame dynamics and spin evolution along with the waveform. Default: False. Example: precessing_opts = { 'init_orbphase': 0, 'init_quat': [1,0,0,0], 'return_dynamics': True } tidal_opts: Should be None for this model. par_dict: Should be None for this model. Returns: domain, h, dynamics. """ if dfM is not None: raise ValueError('Expected dfM to be None for a Time domain model') if freqsM is not None: raise ValueError('Expected freqsM to be None for a Time domain' ' model') if par_dict is not None: raise ValueError('par_dict should be None for this model') if precessing_opts is None: precessing_opts = {} init_orbphase = precessing_opts.pop('init_orbphase', 0) init_quat = precessing_opts.pop('init_quat', None) return_dynamics = precessing_opts.pop('return_dynamics', False) self._check_unused_opts(precessing_opts) if ellMax is None: ellMax = 4 if ellMax > 4: raise ValueError("NRSur7dq4 only allows ellMax<=4.") q, chiA0, chiB0 = x chiA_norm = np.sqrt(np.sum(chiA0**2)) chiB_norm = np.sqrt(np.sum(chiB0**2)) # Get dimensionless omega_ref if fM_ref is None or fM_ref == 0: omega_ref = None else: omega_ref = fM_ref * np.pi # Get dimensionless omega_low if fM_low is None or fM_low == 0: omega_low = None else: omega_low = fM_low * np.pi ## Get dynamics quat_dyn, orbphase_dyn, chiA_copr_dyn, chiB_copr_dyn, t0 \ = self.dynamics_sur(q, chiA0, chiB0, init_orbphase=init_orbphase, \ init_quat=init_quat, t_ref=None, omega_ref=omega_ref, \ omega_low=omega_low) # If init_orbphase != 0, chiA0 and chiB0 get transformed in # self.dynamics_sur. To avoid accidental usage without this # transformation, we set chiA0 and chiB0 to None here. chiA0 = None chiB0 = None # Interpolate to the coorbital time grid, and transform to coorb frame. # Interpolate first since coorbital spins oscillate faster than # coprecessing spins chiA_copr = splinterp_many(self.t_coorb, self.tds, chiA_copr_dyn.T).T chiB_copr = splinterp_many(self.t_coorb, self.tds, chiB_copr_dyn.T).T chiA_copr = normalize_spin(chiA_copr, chiA_norm) chiB_copr = normalize_spin(chiB_copr, chiB_norm) orbphase = _splinterp_Cwrapper(self.t_coorb, self.tds, orbphase_dyn) quat = splinterp_many(self.t_coorb, self.tds, quat_dyn) quat = quat / np.sqrt(np.sum(abs(quat)**2, 0)) chiA_coorb, chiB_coorb = coorb_spins_from_copr_spins( chiA_copr, chiB_copr, orbphase) # Evaluate coorbital waveform surrogate h_coorb = self.coorb_sur(q, chiA_coorb, chiB_coorb, \ ellMax=ellMax) # Transform the sparsely sampled waveform h_inertial = inertial_waveform_modes(self.t_coorb, orbphase, quat, h_coorb) if timesM is not None: if timesM[-1] > self.t_coorb[-1] + 0.01: raise Exception("'times' includes times larger than the" " maximum time value in domain.") if timesM[0] < self.t_coorb[0]: raise Exception( "'times' starts before start of domain. Try" " increasing initial value of times or reducing f_low.") return_times = True if dtM is None and timesM is None: # Use the sparse domain. Python normally copies numpy arrays by # reference, so we do a deep copy so as to not overwrite # self.t_coorb. timesM = np.copy(self.t_coorb) do_interp = False else: ## Interpolate onto uniform domain if needed do_interp = True if dtM is not None: # If omega_low=0 or None, t0 would have been set to None, # in which case we use the full surrogate length if t0 is None: t0 = self.t_coorb[0] tf = self.t_coorb[-1] num_times = int(np.ceil((tf - t0) / dtM)) timesM = t0 + dtM * np.arange(num_times) else: return_times = False if do_interp: hre = splinterp_many(timesM, self.t_coorb, np.real(h_inertial)) him = splinterp_many(timesM, self.t_coorb, np.imag(h_inertial)) h_inertial = hre + 1.j * him # Make mode dict h = {} i = 0 for ell in range(2, ellMax + 1): for m in range(-ell, ell + 1): h[(ell, m)] = h_inertial[i] i += 1 # Transform and interpolate spins if needed if return_dynamics: if do_interp: ## Interpolate from self.tds to timesM because that is what ## is done in the LAL code. chiA_copr = splinterp_many(timesM, self.tds, chiA_copr_dyn.T).T chiB_copr = splinterp_many(timesM, self.tds, chiB_copr_dyn.T).T chiA_copr = normalize_spin(chiA_copr, chiA_norm) chiB_copr = normalize_spin(chiB_copr, chiB_norm) orbphase = _splinterp_Cwrapper(timesM, self.tds, orbphase_dyn) quat = splinterp_many(timesM, self.tds, quat_dyn) quat = quat / np.sqrt(np.sum(abs(quat)**2, 0)) chiA_inertial = transformTimeDependentVector(quat, chiA_copr.T).T chiB_inertial = transformTimeDependentVector(quat, chiB_copr.T).T dynamics = { 'chiA': chiA_inertial, 'chiB': chiB_inertial, 'q_copr': quat, 'orbphase': orbphase, } else: dynamics = None return timesM, h, dynamics
def splinterp_many(t_out, t_in, many_things): return np.array([_splinterp_Cwrapper(t_out, t_in, thing) \ for thing in many_things])