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])