LALSimulation  5.4.0.1-b72065a
NRSur7dq4Remnant.py
Go to the documentation of this file.
1 """
2 Class for NRSur7dq4Remnant model for the mass, spin and kick of the final BH of
3 generically precessing binary black holes.
4 
5 Vijay Varma, 2019.
6 """
7 
8 import numpy as np
9 import lalsimulation as lalsim
10 import lal
11 
12 from . import pn_spin_evolution_wrapper
13 from . import quaternion_utils
14 from .nrfits import NRFits
15 
17  r"""
18  Class for NRSur7dq4Remnant model for the mass, spin and kick of the final BH
19  of generically precessing binary black holes.
20 
21  Paper: arxiv:1905.09300
22 
23  Parameter ranges for usage: \n
24  q = [1, 6.01] \n
25  \f$\chi_{A}, \chi_{B}\f$ = [-1, 1]
26 
27  Training parameter ranges: \n
28  q = [1, 4.01] \n
29  \f$\chi_{A}, \chi_{B}\f$ = [-0.81, 0.81]
30 
31  But extrapolates reasonably to the above mass ratios and spins. However,
32  if a guarantee of accuracy is required, this model should be used within
33  the training parameter range.
34  """
35 
36 
37  #-------------------------------------------------------------------------
38  def _get_coorbital_frame_spins_at_idx(self, chiA, chiB, omega, lNhat, phi, \
39  idx):
40  """ Computes PN spins and dynamics at a given idx.
41 
42  Inputs:
43  chiA: Dimless spin evolution of BhA in inertial frame.
44  chiB: Dimless spin evolution of BhB in inertial frame.
45  omega: Orbital frequency evolution (this is total-mass times
46  the angular orbital frequency).
47  lNhat: Orbital angular momentum direction evolution.
48  phi: Orbital phase evolution.
49  idx: Index for output.
50 
51  Outputs (all are time series):
52  chiA_at_idx_coorb: Spin of BhA at idx, in coorbital frame.
53  chiB_at_idx_coorb: Spin of BhB at idx, in coorbital frame.
54  quat_copr_at_idx: Coprecessing frame quaternion at idx.
55  phi_at_idx: Orbital phase in the coprecessing frame at idx.
56  omega_at_idx Orbital frequency at idx (this is total-mass
57  times the angular orbital frequency).
58 
59  The inertial frame is assumed to be aligned to the coorbital frame at
60  the first index.
61  """
62 
63  # Get omega, inertial spins, angular momentum direction and orbital
64  # phase at idx
65  omega_at_idx = omega[idx]
66  chiA_at_idx = chiA[idx]
67  chiB_at_idx = chiB[idx]
68  lNhat_at_idx = lNhat[idx]
69  phi_at_idx = phi[idx]
70 
71  # Align the z-direction along orbital angular momentum direction
72  # at idx. This moves us into the coprecessing frame.
73  quat_copr_at_idx = quaternion_utils.align_vec_quat(lNhat_at_idx)
74  # The zeroth element is taken because transform_time_dependent_vector
75  # is designed for arrays, but here we only have one element
76  chiA_at_idx_copr = quaternion_utils.transform_time_dependent_vector(
77  np.array([quat_copr_at_idx]).T,
78  np.array([chiA_at_idx]).T, inverse=1).T[0]
79  chiB_at_idx_copr = quaternion_utils.transform_time_dependent_vector(
80  np.array([quat_copr_at_idx]).T,
81  np.array([chiB_at_idx]).T, inverse=1).T[0]
82 
83  # get coorbital frame spins at idx
84  chiA_at_idx_coorb = quaternion_utils.rotate_in_plane(
85  chiA_at_idx_copr, phi_at_idx)
86  chiB_at_idx_coorb = quaternion_utils.rotate_in_plane(
87  chiB_at_idx_copr, phi_at_idx)
88 
89  return chiA_at_idx_coorb, chiB_at_idx_coorb, quat_copr_at_idx, \
90  phi_at_idx, omega_at_idx
91 
92  #-------------------------------------------------------------------------
93  def _get_surrogate_dynamics(self, q, chiA0, chiB0, init_quat, \
94  init_orbphase, omega_ref, unlimited_extrapolation):
95  """ A wrapper for NRSur7dq4 dynamics.
96 
97  Inputs:
98  q: Mass ratio, mA/mB >= 1.
99  chiA0: Dimless spin of BhA in the coorbital frame at omega_ref.
100  chiB0: Dimless spin of BhB in the coorbital frame at omega_ref.
101  init_quat: Coprecessing frame quaternion at omega_ref.
102  init_orbphase:
103  Orbital phase in the coprecessing frame at omega_ref.
104  omega_ref: Orbital frequency in the coprecessing frame at the
105  reference epoch where the input spins are given. Note:
106  This is total-mass times the angular orbital frequency.
107  unlimited_extrapolation:
108  If True, allows unlimited extrapolation to regions well
109  outside the surrogate's training region. Else, raises
110  an error.
111 
112  Outputs:
113  t_dyn: Time values at which the dynamics are returned. These
114  are nonuniform and sparse.
115  copr_quat: Time series of coprecessing frame quaternions.
116  orbphase: Orbital phase time series in the coprecessing frame.
117  chiA_copr: Time series of spin of BhA in the coprecessing frame.
118  chiB_copr: Time series of spin of BhB in the coprecessing frame.
119  """
120 
121  approxTag = lalsim.SimInspiralGetApproximantFromString("NRSur7dq4")
122  LALParams = lal.CreateDict()
123  if unlimited_extrapolation:
124  lal.DictInsertUINT4Value(LALParams, "unlimited_extrapolation", 1)
125 
126  t_dyn, quat0, quat1, quat2, quat3, orbphase, chiAx, chiAy, chiAz, \
127  chiBx, chiBy, chiBz = lalsim.PrecessingNRSurDynamics(q, \
128  chiA0[0], chiA0[1], chiA0[2], chiB0[0], chiB0[1], chiB0[2], \
129  omega_ref, init_quat[0], init_quat[1], init_quat[2], init_quat[3], \
130  init_orbphase, LALParams, approxTag)
131 
132  t_dyn = t_dyn.data
133  orbphase = orbphase.data
134  copr_quat = np.array([quat0.data, quat1.data, quat2.data, quat3.data])
135  chiA_copr = np.array([chiAx.data, chiAy.data, chiAz.data]).T
136  chiB_copr = np.array([chiBx.data, chiBy.data, chiBz.data]).T
137 
138  return t_dyn, copr_quat, orbphase, chiA_copr, chiB_copr
139 
140  #-------------------------------------------------------------------------
141  def _get_pn_spins_at_surrogate_start(self, q, \
142  chiA0, chiB0, omega0, omega_switch_IG, t_sur_switch,
143  unlimited_extrapolation):
144  """ Gets PN spins and frame dynamics at a time close to the start
145  of the surrogate waveform model.
146 
147  Inputs:
148  q: Mass ratio, mA/mB >= 1.
149  chiA0: Initial dimless spin of BhA in the LAL inertial frame.
150  chiB0: Initial dimless spin of BhB in the LAL inertial frame.
151  omega0: Initial orbital frequency. The spins are specified at
152  this frequency. Note: This is total-mass times the
153  angular orbital frequency.
154  omega_switch_IG:
155  Initial guess for the orbital frequency at which to
156  switch from PN to surrogate dynamics. We use PN to
157  evolve the spins until the surrogate becomes valid. The
158  surrogate is in time domain, so it is not
159  straightforward to determine the frequency at which it
160  is valid. So, we evolve with PN until omega_switch_IG.
161  However, this is chosen to be overly conservative such
162  that this initial guess works for all q<=6 cases.
163  Ideally, we would like to use as much as possible of
164  the surrogate spin evolution. So, using the PN spins at
165  omega_switch_IG, the surrogate dynamics are evolved
166  both backwards and forwards in time. This lets us get
167  surrogate orbital frequency at t_sur_switch, as well as
168  the corresponding PN spins at this time.
169  t_sur_switch:
170  The time at which the PN spins are returned so that
171  surrogate dynamics can be regenerated starting as early
172  as possible.
173  unlimited_extrapolation:
174  If True, Allow unlimited extrapolation to regions well
175  outside the surrogate's training region. Else, raises
176  an error.
177 
178  Outputs:
179  PN spin and frame dynamics at t_sur_switch, as well as the spins
180  and orbital frequency evolution.
181 
182  chiA_PN_at_idx_coorb:
183  PN spin of BhA at t_sur_switch, in the coorbital frame.
184  chiB_PN_at_idx_coorb:
185  PN spin of BhB at t_sur_switch, in the coorbital frame.
186  quat_PN_copr_at_idx:
187  PN coprecessing frame quaternions at t_sur_switch.
188  phi_PN_at_idx:
189  PN orbital phase at t_sur_switch.
190  omega_PN_at_idx:
191  PN orbital frequency at t_sur_switch.
192  chiA_PN: PN Spin evolution of BhA in the inertial frame.
193  chiB_PN: PN Spin evolution of BhB in the inertial frame.
194  omega_PN: PN orbital frequency evolution. Note: This is total-mass
195  times the angular orbital frequency.
196  """
197 
198  # Get PN spin evolution starting at omega0
199  omega_PN, phi_PN, chiA_PN, chiB_PN, lNhat_PN \
200  = pn_spin_evolution_wrapper.spin_evolution(q, chiA0, chiB0, omega0)
201 
202  # Get PN coorbital frame spins and frame dynamics at
203  # omega_PN=omega_switch_IG
204  idx = np.argmin(np.abs(omega_PN - omega_switch_IG))
205  chiA_PN_at_idx_coorb, chiB_PN_at_idx_coorb, quat_PN_copr_at_idx, \
206  phi_PN_at_idx, omega_PN_at_idx \
207  = self._get_coorbital_frame_spins_at_idx_get_coorbital_frame_spins_at_idx(chiA_PN, chiB_PN, \
208  omega_PN, lNhat_PN, phi_PN, idx)
209 
210  # Now evaluate the surrogate dynamics (both forwards and backwards)
211  # using PN spins at omega_switch_IG
212  dyn_times, quat_sur, orbphase_sur, chiA_copr_sur, chiB_copr_sur \
213  = self._get_surrogate_dynamics_get_surrogate_dynamics(q, chiA_PN_at_idx_coorb, \
214  chiB_PN_at_idx_coorb, quat_PN_copr_at_idx, phi_PN_at_idx, \
215  omega_switch_IG, unlimited_extrapolation)
216  omega_sur = np.gradient(orbphase_sur, dyn_times)
217 
218  # Get surrogate orbital frequency at t_sur_switch, which is
219  # close to the start of the surrogate data
220  omega_init_sur = omega_sur[np.argmin(np.abs(dyn_times - t_sur_switch))]
221 
222  # Get PN coorbital frame spins and frame dynamics at omega_init_sur.
223  # These must be in the coorbital frame as the surrogate dynamics
224  # expects the inputs in the LAL convention which agrees with the
225  # coorbital frame.
226  idx = np.argmin(np.abs(omega_PN - omega_init_sur))
227  chiA_PN_at_idx_coorb, chiB_PN_at_idx_coorb, quat_PN_copr_at_idx, \
228  phi_PN_at_idx, omega_PN_at_idx \
229  = self._get_coorbital_frame_spins_at_idx_get_coorbital_frame_spins_at_idx(chiA_PN, chiB_PN, \
230  omega_PN, lNhat_PN, phi_PN, idx)
231 
232  return chiA_PN_at_idx_coorb, chiB_PN_at_idx_coorb, \
233  quat_PN_copr_at_idx, phi_PN_at_idx, omega_PN_at_idx, \
234  chiA_PN, chiB_PN, omega_PN
235 
236  #-------------------------------------------------------------------------
237  def _evolve_spins(self, q, chiA0, chiB0, omega0, omega_switch_IG=0.03, \
238  t_sur_switch=-4000, return_spin_evolution=False, \
239  unlimited_extrapolation=False):
240  """ Uses PN and surrogate spin evolution to evolve spins of the
241  component BHs from an initial orbital frequency = omega0 until t=-100 M
242  from the peak of the waveform.
243 
244  Inputs:
245  q: Mass ratio, mA/mB >= 1.
246  chiA0: Initial dimless spin of BhA in the inertial frame.
247  chiB0: Initial dimless spin of BhB in the inertial frame. Note
248  that the LAL inertial frame coincides with the
249  coorbital frame used in the surrogate models.
250  omega0: Initial orbital frequency in the coprecessing frame.
251  The spins are specified at this frequency. Note: This
252  is total-mass times the angular orbital frequency.
253  omega_switch_IG:
254  Initial guess for orbital frequency used to determine
255  if PN spin evolution is required, and where to switch
256  from PN spin evolution to the surrogate.
257  If omega0 >= omega_switch_IG:
258  PN evolution is not required and the surrogate dynamics
259  are used to evolve the spin until t=-100M. Default
260  value for omega_switch_IG is 0.03 which is expected to
261  be large enough for q<=6. NOTE: If you get errors
262  about omega0 being too small for the surrogate, try
263  increasing omega_switch_IG.
264  If omega0 < omega_switch_IG:
265  Use PN to evolve the spins until the surrogate becomes
266  valid. The surrogate is in time domain, so it is not
267  straightforward to determine the frequency at which it
268  is valid. So, we evolve with PN until omega_switch_IG.
269  However, this is chosen to be overly conservative such
270  that this initial guess works for all q<=6 cases.
271  Ideally, we would like to use as much as possible of
272  the surrogate spin evolution. So, using the PN spins at
273  omega_switch_IG, the surrogate dynamics are evolved
274  both backwards and forwards in time. Then the PN spins
275  at omega = omega_sur[t_sur_switch] are used to
276  reevaluate the surrogate dynamics. This way,
277  omega_switch_IG is only used as an initial guess for
278  where to switch to the surrogate, and we switch to the
279  surrogate as early as possible.
280  t_sur_switch:
281  The time at which we switch to the surrogate evolution.
282  This should be slightly larger than the surrogate start
283  time of -4300M. Since we use the PN spins at
284  omega_sur[t_sur_switch] to reevaluate the surrogate
285  dynamics, it is possible that for the given PN spins at
286  omega_sur[t_sur_switch], the surrogate is not long
287  enough. Default: -4000.
288  return_spin_evolution:
289  Also return the spin evolution and dynamics as a
290  dictionary.
291  unlimited_extrapolation:
292  If True, Allow unlimited extrapolation to regions well
293  outside the surrogate's training region. Else, raises
294  an error.
295  Outputs:
296  chiA_coorb_fitnode:
297  Spin of BhA in the coorbital frame at t=-100M.
298  chiB_coorb_fitnode:
299  Spin of BhB in the coorbital frame at t=-100M.
300  quat_fitnode:
301  Coprecessing frame quaternions at t=-100M.
302  orbphase_fitnode:
303  Orbital phase in the coprecessing frame at t=-100M.
304  spin_evolution:
305  Dictionary containing the PN and surrogate spin
306  evolution (in the inertial frame) and dynamics.
307  Default: None, unless return_spin_evolution=True.
308  """
309 
310  # If omega0 is below the NRSur7dq4 initial guess frequency, we use PN
311  # to evolve the spins. We get the initial PN spins and omega_init_sur
312  # that should go into the surrogate such that the initial time is
313  # t_sur_switch.
314  if omega0 < omega_switch_IG:
315  chiA0_nrsur_coorb, chiB0_nrsur_coorb, quat0_nrsur_copr, \
316  phi0_nrsur, omega_init_sur, chiA_PN, chiB_PN, omega_PN \
317  = self._get_pn_spins_at_surrogate_start_get_pn_spins_at_surrogate_start(q, \
318  chiA0, chiB0, omega0, omega_switch_IG, t_sur_switch,
319  unlimited_extrapolation)
320 
321  # If omega0 >= omega_switch_IG, we evolve spins directly with NRSur7dq4
322  # waveform model. We set the coprecessing frame quaternion to identity
323  # and orbital phase to 0 at omega=omega0, hence the coorbital frame is
324  # the same as the inertial frame here.
325  else:
326  # Note that here we set omega_init_sur to omega0
327  chiA0_nrsur_coorb, chiB0_nrsur_coorb, quat0_nrsur_copr, \
328  phi0_nrsur, omega_init_sur, chiA_PN, chiB_PN, omega_PN \
329  = chiA0, chiB0, [1,0,0,0], 0, omega0, None, None, None
330 
331  # Now evaluate the surrogate dynamics using PN spins at omega_init_sur
332  dyn_times, quat_sur, orbphase_sur, chiA_copr_sur, chiB_copr_sur \
333  = self._get_surrogate_dynamics_get_surrogate_dynamics(q, chiA0_nrsur_coorb, \
334  chiB0_nrsur_coorb, quat0_nrsur_copr, phi0_nrsur, \
335  omega_init_sur, unlimited_extrapolation)
336 
337  # get data at time node where remnant fits are done
338  fitnode_time = -100
339  nodeIdx = np.argmin(np.abs(dyn_times - fitnode_time))
340  quat_fitnode = quat_sur.T[nodeIdx]
341  orbphase_fitnode = orbphase_sur[nodeIdx]
342 
343  # get coorbital frame spins at the time node
344  chiA_coorb_fitnode = quaternion_utils.rotate_in_plane(
345  chiA_copr_sur[nodeIdx], orbphase_fitnode)
346  chiB_coorb_fitnode = quaternion_utils.rotate_in_plane(
347  chiB_copr_sur[nodeIdx], orbphase_fitnode)
348 
349  if return_spin_evolution:
350  # Transform spins to the reference inertial frame
351  chiA_sur = quaternion_utils.transform_time_dependent_vector(
352  quat_sur, chiA_copr_sur.T).T
353  chiB_sur = quaternion_utils.transform_time_dependent_vector(
354  quat_sur, chiB_copr_sur.T).T
355  spin_evolution = {
356  't_sur': dyn_times,
357  'chiA_sur': chiA_sur,
358  'chiB_sur': chiB_sur,
359  'orbphase_sur': orbphase_sur,
360  'quat_sur': quat_sur,
361  'omega_PN': omega_PN,
362  'chiA_PN': chiA_PN,
363  'chiB_PN': chiB_PN,
364  'omega_init_sur': omega_init_sur,
365  }
366  else:
367  spin_evolution = None
368 
369  return chiA_coorb_fitnode, chiB_coorb_fitnode, quat_fitnode, \
370  orbphase_fitnode, spin_evolution
371 
372  # ------------------------------------------------------------------------
373  def _get_fit_params(self, m1, m2, chiA_vec, chiB_vec, f_ref,
374  extra_params_dict):
375  """
376  If f_ref = -1, assumes the reference epoch is at t=-100M and
377  just returns the input spins, which are expected to be in
378  the coorbital frame at t=-100M.
379  Else, takes the spins at f_ref and evolves them using a combination of
380  PN and NRSur7dq4 until t=-100M. The returned spins are in the coorbital
381  frame at t=-100M.
382 
383  See eval_fits.eval_nrfit() for the definitions of the arguments of
384  this function.
385  """
386 
387  unlimited_extrapolation = extra_params_dict["unlimited_extrapolation"]
388 
389  q = m1/m2
390  M = m1 + m2 # m1, m2 are assumed to be in kgs
391 
392  if f_ref == -1:
393  # If f_ref = -1, assume the reference epoch is at t=-100M from the
394  # total waveform amplitude peak. All input and output quantities
395  # are in the coorbital frame at this time.
396  chiA_coorb_fitnode = chiA_vec
397  chiB_coorb_fitnode = chiB_vec
398  # We set this to None, and this will be used in _eval_fit() to
399  # determine if the remnant vectors need any further transformations.
400  quat_fitnode = None
401  orbphase_fitnode = None
402  else:
403  # Else, evolve the spins from f_ref to t = -100 M from the peak.
404 
405  # reference orbital angular frequency times the total mass (M*omega)
406  omega_ref = f_ref*np.pi*M*lal.G_SI/lal.C_SI**3
407 
408  chiA_coorb_fitnode, chiB_coorb_fitnode, quat_fitnode, \
409  orbphase_fitnode, spin_evolution = self._evolve_spins_evolve_spins(q, \
410  chiA_vec, chiB_vec, omega_ref, \
411  unlimited_extrapolation=unlimited_extrapolation)
412 
413  fit_params = [q, chiA_coorb_fitnode, chiB_coorb_fitnode, quat_fitnode, \
414  orbphase_fitnode]
415 
416  return fit_params
417 
418  # ------------------------------------------------------------------------
419  def _eval_fit(self, fit_params, fit_type, extra_params_dict):
420  """ Evaluates a particular fit for NRSur7dq4Remnant using the fit_params
421  returned by _get_fit_params().
422 
423  fit_type can be one of "FinalMass", "FinalSpin" or "RecoilKick".
424 
425  Passing extra_params_dict = {"unlimited_extrapolation": True}, will
426  ignore any extrapolation errors. USE AT YOUR OWN RISK!! Default: False.
427  """
428 
429  q, chiA_vec, chiB_vec, quat_fitnode, orbphase_fitnode = fit_params
430  LALParams = lal.CreateDict()
431  if extra_params_dict["unlimited_extrapolation"]:
432  lal.DictInsertUINT4Value(LALParams, "unlimited_extrapolation", 1)
433 
434  if fit_type == "FinalMass":
435  # FinalMass is given as a fraction of total mass
436  val = lalsim.NRSur7dq4Remnant(q,
437  chiA_vec[0], chiA_vec[1], chiA_vec[2],
438  chiB_vec[0], chiB_vec[1], chiB_vec[2], "mf",
439  LALParams).data[0]
440  else:
441  if fit_type == "FinalSpin":
442  val = lalsim.NRSur7dq4Remnant(q,
443  chiA_vec[0], chiA_vec[1], chiA_vec[2],
444  chiB_vec[0], chiB_vec[1], chiB_vec[2], "chif",
445  LALParams).data
446  elif fit_type == "RecoilKick":
447  val = lalsim.NRSur7dq4Remnant(q,
448  chiA_vec[0], chiA_vec[1], chiA_vec[2],
449  chiB_vec[0], chiB_vec[1], chiB_vec[2], "vf",
450  LALParams).data
451  else:
452  raise ValueError("Invalid fit_type=%s. This model only allows "
453  "'FinalMass', 'FinalSpin' and 'RecoilKick'."%fit_type)
454 
455  # The fits are constructed in the coorbital frame at t=-100M.
456  # If quat_fitnode is None, this means that no spin evolution
457  # was done and the return values should be in the coorbital frame
458  # at t=-100. So, we don't need any further transformations.
459  if quat_fitnode is not None:
460  # If quat_fitnode is not None, we transform the remnant vectors
461  # into the LAL inertial frame, which is the same as the
462  # coorbital frame at omega0. This is done using the
463  # coprecessing frame quaternion and orbital phase at t=-100M.
464  # These are defined w.r.t. the reference LAL inertial frame
465  # defined at omega0.
466  val = quaternion_utils.transform_vector_coorb_to_inertial(val, \
467  orbphase_fitnode, quat_fitnode)
468 
469  return np.atleast_1d(val)
Class for NRSur7dq4Remnant model for the mass, spin and kick of the final BH of generically precessin...
def _get_surrogate_dynamics(self, q, chiA0, chiB0, init_quat, init_orbphase, omega_ref, unlimited_extrapolation)
def _get_coorbital_frame_spins_at_idx(self, chiA, chiB, omega, lNhat, phi, idx)
def _evolve_spins(self, q, chiA0, chiB0, omega0, omega_switch_IG=0.03, t_sur_switch=-4000, return_spin_evolution=False, unlimited_extrapolation=False)
def _get_pn_spins_at_surrogate_start(self, q, chiA0, chiB0, omega0, omega_switch_IG, t_sur_switch, unlimited_extrapolation)
Base class for Numerical Relativity fits such as remnant BH mass, spin, etc.
Definition: nrfits.py:19