Examples
This is a short
notebook
to illustrate some of the key features of the precession
module.
There much more than this in the code but hopefully this is a good
starting point.
>>> import numpy as np
>>> from importlib.machinery import SourceFileLoader
>>> precession = SourceFileLoader("precession", "../precession/precession.py").load_module()
(The import syntax above is to make sure the online documentation grabs
the latest version. For you it should just be import precession
)
1. Select a binary configuration.
First, set the constants of motion. These include mass ratio and spin magnitudes:
>>> q = 0.8
>>> q
0.8
>>> chi1 = 0.5
>>> chi1
0.5
>>> chi2 = 0.9
>>> chi2
0.9
From these, find a suitable value of the effective spin within its geometrical limits
>>> chieff_minus,chieff_plus=precession.chiefflimits(q=q, chi1=chi1, chi2=chi2)
>>> chieff = 0.1
>>> assert chieff>=chieff_minus and chieff<=chieff_plus
>>> chieff
0.1
Now the quantities that vary on the radiation-reaction timescale. Specify the orbital separation
>>> r = 1000
>>> r
1000
Find a suitable value of the asymptotic angular momentum (its boundaries are the spin-orbit resonances):
>>> kappa_minus,kappa_plus=precession.kappalimits(r=r,chieff=chieff, q=q, chi1=chi1, chi2=chi2)
>>> kappa = 0.02
>>> assert kappa>=kappa_minus and kappa<=kappa_plus
This is more conventiently done by specifying a dimensionless number, e.g.
>>> kappa = precession.kapparescaling(kappatilde=0.5, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi2)
>>> kappa
array([0.04563644])
Finally, the precession-timescale variation is encoded in the weighted spin difference. Its limits are given by the solutions of a cubic polynomial:
>>> deltachi_minus,deltachi_plus = precession.deltachilimits(kappa=kappa,r=r,chieff=chieff,q=q,chi1=chi1,chi2=chi2)
>>> deltachi=-0.1
>>> assert deltachi>=deltachi_minus and deltachi<=deltachi_plus
You can also sample deltachi from its PN-weighted porability density function
>>> deltachi = precession.deltachisampling(kappa=kappa, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi1, N=1)
Or, as before, specify a dimensionless number, e.g.
>>> deltachi = precession.deltachirescaling(deltachitilde=0.4, kappa=kappa, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi2)
>>> deltachi
array([-0.15027382])
From these quantities, one can compute the spin angles
>>> theta1, theta2, deltaphi = precession.conserved_to_angles(deltachi=deltachi, kappa=kappa, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi2)
>>> theta1,theta2,deltaphi
(array([1.66141316]), array([1.25261228]), array([1.4089439]))
The inverse operation is
>>> deltachi_, kappa_, chieff_ = precession.angles_to_conserved(theta1=theta1, theta2=theta2, deltaphi=deltaphi, r=r, q=q, chi1=chi1, chi2=chi2)
>>> assert np.isclose(deltachi_,deltachi) and np.isclose(kappa_,kappa) and np.isclose(chieff_,chieff)
One can now compute various derived quantities including:
The precession period
The total precession angle
Two flavors of the precession estimator chip
The spin morphology
… and many more!
>>> tau = precession.eval_tau(kappa=kappa, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi2)
>>> tau
array([1.14179292e+09])
>>> alpha = precession.eval_alpha(kappa=kappa, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi2)
>>> alpha
array([34.7076951])
>>> chip_averaged = precession.eval_chip(deltachi=deltachi,kappa=kappa,r=r,chieff=chieff,q=q,chi1=chi1,chi2=chi2,which="averaged")
>>> chip_averaged
array([0.74912589])
>>> chip_rms = precession.eval_chip(deltachi=deltachi,kappa=kappa,r=r,chieff=chieff,q=q,chi1=chi1,chi2=chi2,which="rms")
>>> chip_rms
array([0.81485269])
>>> morph = precession.morphology(kappa=kappa, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi2 )
>>> morph
array(['C+'], dtype='<U32')
2. Inspiral
Let’s now evolve that binary configuration down to r=10M.
>>> rvals = [r, 10] # Specify all the outputs you want here
>>> outputs = precession.inspiral_precav(kappa=kappa,r=rvals,chieff=chieff,q=q,chi1=chi1,chi2=chi2)
>>> outputs
{'theta1': array([[1.54408963, 2.16297419]]),
'theta2': array([[1.33722239, 0.87939461]]),
'deltaphi': array([[2.78560692, 0.51251691]]),
'deltachi': array([[-0.08516471, -0.4100941 ]]),
'kappa': array([[0.04563644, 0.06977371]]),
'r': array([[1000, 10]]),
'u': array([[0.06403612, 0.64036123]]),
'deltachiminus': array([[-0.19622919, -0.42562113]]),
'deltachiplus': array([[-0.08134075, 0.26825594]]),
'deltachi3': array([[0.74325701, 0.1801658 ]]),
'chieff': array([0.1]),
'q': array([0.8]),
'chi1': array([0.5]),
'chi2': array([0.9])}
The same evolution can also be done with an orbit-averaged scheme (note that now you have to specify deltachi as well). This is slow:
>>> outputs = precession.inspiral_orbav(deltachi=deltachi, kappa=kappa,r=rvals,chieff=chieff,q=q,chi1=chi1,chi2=chi2)
>>> outputs
{'t': array([[0.000000e+00, 7.935974e+10]]),
'theta1': array([[1.66141316, 0.88395959]]),
'theta2': array([[1.25261228, 1.76230812]]),
'deltaphi': array([[1.4089439 , 3.07301953]]),
'Lh': array([[ 3.13179178e-02, 5.33207833e-18, 9.99509474e-01],
[ 1.24542216e-03, -6.64332678e-02, 9.97790094e-01]]),
'S1h': array([[-0.73494006, 0.67476338, -0.06750919],
[ 0.1569423 , -0.79776376, 0.58218746]]),
'S2h': array([[-0.73753054, -0.5857321 , 0.33610506],
[-0.13214013, 0.98330162, -0.12513137]]),
'deltachi': array([[-0.15027382, 0.25227467]]),
'kappa': array([[0.04563644, 0.06865318]]),
'r': array([[1000, 10]]),
'u': array([[0.06403612, 0.64036123]]),
'chieff': array([[0.1 , 0.09999999]]),
'q': array([0.8]),
'chi1': array([0.5]),
'chi2': array([0.9]),
'cyclesign': array([[1., 1.]])}
Let’s now take this binary at r=10M, and propagate it back to past time infinity:
>>> rvals = [outputs['r'][0,-1], np.inf] # Specify all the outputs you want here
>>> outputs = precession.inspiral_precav(kappa=outputs['kappa'][0,-1],r=rvals,chieff=chieff,q=q,chi1=chi1,chi2=chi2)
>>> outputs
{'theta1': array([[1.48961071, 1.66496014]]),
'theta2': array([[1.37588146, 1.25002889]]),
'deltaphi': array([[-2.00118805, -2.58471431]]),
'deltachi': array([[-0.05494641, -0.15223596]]),
'kappa': array([[0.06865318, 0.04154245]]),
'r': array([[10., inf]]),
'u': array([[0.64036123, 0. ]]),
'deltachiminus': array([[-0.43153997, -0.15223596]]),
'deltachiplus': array([[ 0.25313089, -0.15223596]]),
'deltachi3': array([[0.18034069, nan]]),
'chieff': array([0.1]),
'q': array([0.8]),
'chi1': array([0.5]),
'chi2': array([0.9])}
3. Precession average what you like
The code also allow you to precession-average quantities specified by the users. For instance, let’s compute the average of deltachi.
>>> def func(deltachi):
... return deltachi
...
>>> deltachiav_ = precession.precession_average(kappa=kappa, r=r, chieff=chieff, q=q, chi1=chi1, chi2=chi2, func=func)
>>> deltachiav_
array([-0.13857097])
For this specific case, the averaged can also be done semi-analytically in terms of elliptic integrals (though in general this is not possible). Let’s check we get the same result:
>>> u = precession.eval_u(r, q)
>>> deltachiminus,deltachiplus,deltachi3 = precession.deltachiroots(kappa=kappa, u=u, chieff=chieff, q=q, chi1=chi1, chi2=chi2)
>>> m = precession.elliptic_parameter(kappa=kappa, u=u, chieff=chieff, q=q, chi1=chi1, chi2=chi2,
... precomputedroots=np.stack([deltachiminus, deltachiplus, deltachi3]))
>>> deltachiav = precession.inverseaffine( precession.deltachitildeav(m), deltachiminus, deltachiplus)
>>> deltachiav
array([-0.13857097])
>>> (deltachiav_-deltachiav)/deltachiav
array([-8.99340873e-14])
This is also an example of how providing the optional quantity
precomputedroots
speeds up the computation.