Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/cip-startup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ jobs:
run: |
python -m pytest -q \
MonteCarloMarginalizeCode/Code/test/test_cip_startup.py \
MonteCarloMarginalizeCode/Code/test/test_lalsim_eos_compat.py
MonteCarloMarginalizeCode/Code/test/test_lalsim_eos_compat.py \
MonteCarloMarginalizeCode/Code/test/test_ring_coordinates.py
2 changes: 1 addition & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ unit_tests:
script:
- python -m pytest -q MonteCarloMarginalizeCode/Code/test/test_time_marginalization_perrow_offset.py
- python -m pytest -q MonteCarloMarginalizeCode/Code/test/test_sky_rotations.py
- python -m pytest -q MonteCarloMarginalizeCode/Code/test/test_cip_startup.py MonteCarloMarginalizeCode/Code/test/test_lalsim_eos_compat.py
- python -m pytest -q MonteCarloMarginalizeCode/Code/test/test_cip_startup.py MonteCarloMarginalizeCode/Code/test/test_lalsim_eos_compat.py MonteCarloMarginalizeCode/Code/test/test_ring_coordinates.py

test_run:
stage: system tests
Expand Down
48 changes: 47 additions & 1 deletion MonteCarloMarginalizeCode/Code/RIFT/lalsimutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ def lsu_StringFromPNOrder(order):

# so far, used for puffball, to prevent insanity (infinite growth) and/or death to downselect
# - note we also provide for extrinsic: RA (phi), phiref, psi, just in case we need it in the future
periodic_params = {'phi1':2*np.pi, 'phi2':2*np.pi, 'phiref':2*np.pi, 'psi':np.pi, 'meanPerAno':2*np.pi, 'phi':2*np.pi, 'phiJL':2*np.pi, 'psiJ':2*np.pi}
periodic_params = {'phi1':2*np.pi, 'phi2':2*np.pi, 'phi12':2*np.pi, 'phiref':2*np.pi, 'psi':np.pi, 'meanPerAno':2*np.pi, 'phi':2*np.pi, 'phiJL':2*np.pi, 'psiJ':2*np.pi}

tex_dictionary = {
"mtot": r'$M$',
Expand All @@ -371,6 +371,7 @@ def lsu_StringFromPNOrder(order):
"DeltaOverM2_perp" : r"$\Delta_\perp$",
"DeltaOverM2_L" : r"$\Delta_{||}$",
"SOverM2_perp" : r"$S_\perp$",
"chi_p_vec" : r"$\chi_{p,{\rm vec}}$",
"SOverM2_L" : r"$S_{||}$",
"eta": r"$\eta$",
"chi_eff": r"$\chi_{eff}$",
Expand Down Expand Up @@ -1199,6 +1200,20 @@ def extract_param(self,p):
S2p = (m2**2 * chi2)[:2]
Sp = np.max([np.linalg.norm( A1*S1p), np.linalg.norm(A2*S2p)])
return Sp/(A1*m1**2) # divide by term for *larger* BH
if p == 'phi12':
# azimuth of spin 2's in-plane component relative to spin 1's, in [0, 2 pi), L frame.
# Undefined if either in-plane component vanishes; 0 is returned then (same as the vectorized path).
if np.hypot(self.s1x, self.s1y) == 0 or np.hypot(self.s2x, self.s2y) == 0:
return 0.
val = np.mod(np.arctan2(self.s2y, self.s2x) - np.arctan2(self.s1y, self.s1x), 2*np.pi)
return 0. if val >= 2*np.pi else val # np.mod of a tiny negative number rounds to 2 pi
if p == 'chi_p_vec':
# vector-sum (ring) analogue of chi_p: |A1 S1perp + A2 S2perp| / (A1 m1^2), same A1, A2 as chi_p.
# chi_p keeps the larger of the two terms; this keeps their vector sum, so it depends on phi12
# (in-plane spins that cancel give a small value). L frame.
q = self.m2/self.m1
A1 = (2+ 3.*q/2); A2 = (2+3./(2*q))
return np.abs( (self.s1x + 1j*self.s1y) + (A2/A1)*q**2*(self.s2x + 1j*self.s2y) )
if p == 'chi_pavg':
if (abs(self.s1x) < 1e-4 and abs(self.s1y) < 1e-4 and abs(self.s2x) < 1e-4 and abs(self.s2y) < 1e-4):
chipavg = 0.0
Expand Down Expand Up @@ -5155,6 +5170,7 @@ def convert_waveform_coordinates(x_in,coord_names=['mc', 'eta'],low_level_coord_
- source_redshift: if nonzero, convert m1 -> m1 (1+z)=m_z, as fit is done in the detector frame. We are **assuming source-frame sampling**
"""
x_out = np.zeros( (len(x_in), len(coord_names) ) )
kerr_violation_ring = None # set by the vectorized in-plane block, which can end the conversion early
# Check for trivial identity transformations and do those by direct copy, then remove those from the list of output coord names
coord_names_reduced = coord_names.copy()
for p in low_level_coord_names:
Expand Down Expand Up @@ -5368,6 +5384,34 @@ def convert_waveform_coordinates(x_in,coord_names=['mc', 'eta'],low_level_coord_
x_out[:,indx_pout_s2y] = x_in[:,indx_chi2]*sintheta2*sinphi2
coord_names_reduced.remove('s2x')
coord_names_reduced.remove('s2y')
# in-plane magnitudes, relative azimuth, and ring coordinates (L frame), vectorized
ring_names = ['chi1_perp', 'chi2_perp', 'phi12', 'SOverM2_perp', 'DeltaOverM2_perp', 'chi_p_vec']
if any(p in coord_names_reduced for p in ring_names):
# CIP's default sampler passes an object array of python floats; ufuncs need a float array
xf = np.asarray(x_in, dtype=float)
m1f = np.asarray(m1_vals, dtype=float); m2f = np.asarray(m2_vals, dtype=float)
indx_phi1 = low_level_coord_names.index('phi1')
indx_phi2 = low_level_coord_names.index('phi2')
chi1_perp = xf[:,indx_chi1]*np.sqrt(1-xf[:,indx_ct1]**2)
chi2_perp = xf[:,indx_chi2]*np.sqrt(1-xf[:,indx_ct2]**2)
v1 = chi1_perp*np.exp(1j*xf[:,indx_phi1])
v2 = chi2_perp*np.exp(1j*xf[:,indx_phi2])
mtot_vals = m1f + m2f
q_vals = m2f/m1f
A1 = 2 + 1.5*q_vals; A2 = 2 + 1.5/q_vals
ring_vals = {'chi1_perp': chi1_perp, 'chi2_perp': chi2_perp,
'phi12': np.where((chi1_perp > 0) & (chi2_perp > 0), np.mod(xf[:,indx_phi2] - xf[:,indx_phi1], 2*np.pi), 0.),
'SOverM2_perp': np.abs(v1*m1f**2 + v2*m2f**2)/mtot_vals**2,
'DeltaOverM2_perp': np.abs(v1*m1f - v2*m2f)/mtot_vals,
'chi_p_vec': np.abs(v1 + (A2/A1)*q_vals**2*v2)}
ring_vals['phi12'] = np.where(ring_vals['phi12'] >= 2*np.pi, 0., ring_vals['phi12'])
for p in ring_names:
if p in coord_names_reduced:
x_out[:,coord_names.index(p)] = ring_vals[p]
coord_names_reduced.remove(p)
if enforce_kerr:
# same rule as the per-row fallthrough below, which this block can bypass
kerr_violation_ring = (xf[:,indx_chi1] > 1) | (xf[:,indx_chi2] > 1)

# Spin pseudo-cylindrical coordinate names, standard framing
if ('s1z_bar' in low_level_coord_names) and ('phi1' in low_level_coord_names) and ('s2z_bar' in low_level_coord_names) and ('phi2' in low_level_coord_names) and ('mc' in low_level_coord_names) and ('eta' in low_level_coord_names or 'delta_mc' in low_level_coord_names):
Expand Down Expand Up @@ -5585,6 +5629,8 @@ def convert_waveform_coordinates(x_in,coord_names=['mc', 'eta'],low_level_coord_
x_out[indx_name] *= (1+source_redshift)

# return if we don't need to do any more conversions (e.g., if we only have --parameter specification)
if kerr_violation_ring is not None:
x_out[kerr_violation_ring] = -np.inf
if len(coord_names_reduced)<1:
return x_out

Expand Down
100 changes: 100 additions & 0 deletions MonteCarloMarginalizeCode/Code/test/test_ring_coordinates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""phi12 and the ring coordinates (chi1_perp, chi2_perp, SOverM2_perp, DeltaOverM2_perp, chi_p_vec):
the vectorized spherical path of convert_waveform_coordinates must agree with extract_param."""
import numpy as np
import lal
import RIFT.lalsimutils as lsu

RING = ['chi1_perp', 'chi2_perp', 'phi12', 'SOverM2_perp', 'DeltaOverM2_perp', 'chi_p_vec']
LOW = ['mc', 'delta_mc', 'chi1', 'cos_theta1', 'phi1', 'chi2', 'cos_theta2', 'phi2']


def _draws(n=300, seed=4):
rng = np.random.default_rng(seed)
x = np.column_stack([rng.uniform(5, 30, n), rng.uniform(0.01, 0.8, n), rng.uniform(0, 0.99, n),
rng.uniform(-1, 1, n), rng.uniform(0, 2 * np.pi, n), rng.uniform(0, 0.99, n),
rng.uniform(-1, 1, n), rng.uniform(0, 2 * np.pi, n)])
return x


def test_vectorized_matches_extract_param():
x = _draws()
y = lsu.convert_waveform_coordinates(x, coord_names=RING, low_level_coord_names=LOW)
for i, row in enumerate(x):
mc, dmc, c1, ct1, p1, c2, ct2, p2 = row
eta = 0.25 * (1 - dmc ** 2)
m1, m2 = lsu.m1m2(mc, eta)
P = lsu.ChooseWaveformParams()
P.m1, P.m2 = m1 * lal.MSUN_SI, m2 * lal.MSUN_SI
s1 = c1 * np.sqrt(1 - ct1 ** 2); s2 = c2 * np.sqrt(1 - ct2 ** 2)
P.s1x, P.s1y, P.s1z = s1 * np.cos(p1), s1 * np.sin(p1), c1 * ct1
P.s2x, P.s2y, P.s2z = s2 * np.cos(p2), s2 * np.sin(p2), c2 * ct2
for j, name in enumerate(RING):
ref = P.extract_param(name)
d = abs(y[i, j] - ref)
if name == 'phi12':
d = min(d, 2 * np.pi - d)
assert d < 1e-9, (name, y[i, j], ref)


def test_chi_p_vec_limits():
# one spin: chi_p_vec equals chi1_perp; opposite in-plane spins of the weighted size cancel
P = lsu.ChooseWaveformParams()
P.m1, P.m2 = 10 * lal.MSUN_SI, 5 * lal.MSUN_SI
P.s1x, P.s1y, P.s2x, P.s2y = 0.4, 0.0, 0.0, 0.0
assert abs(P.extract_param('chi_p_vec') - 0.4) < 1e-12
q = 0.5; A1 = 2 + 1.5 * q; A2 = 2 + 1.5 / q
P.s2x = -0.4 / ((A2 / A1) * q ** 2)
if abs(P.s2x) < 1:
assert P.extract_param('chi_p_vec') < 1e-12
assert abs(P.extract_param('phi12') - np.pi) < 1e-12


def _one(mc, dmc, c1, ct1, p1, c2, ct2, p2):
return np.array([[mc, dmc, c1, ct1, p1, c2, ct2, p2]])


def test_phi12_sign_both_paths():
# spin 2 60 degrees ahead of spin 1: phi12 = pi/3, not 5 pi/3 (a sign flip in both paths fails here)
x = _one(10., 0.3, 0.5, 0., 0.2, 0.5, 0., 0.2 + np.pi / 3)
y = lsu.convert_waveform_coordinates(x, coord_names=['phi12'], low_level_coord_names=LOW)
assert abs(y[0, 0] - np.pi / 3) < 1e-12
P = lsu.ChooseWaveformParams()
P.m1, P.m2 = 10 * lal.MSUN_SI, 5 * lal.MSUN_SI
P.s1x, P.s1y = 0.5 * np.cos(0.2), 0.5 * np.sin(0.2)
P.s2x, P.s2y = 0.5 * np.cos(0.2 + np.pi / 3), 0.5 * np.sin(0.2 + np.pi / 3)
assert abs(P.extract_param('phi12') - np.pi / 3) < 1e-12


def test_object_array_input():
# CIP's default sampler (adaptive_cartesian) passes an object array of python floats
x = _draws(20).astype(object)
y = lsu.convert_waveform_coordinates(x, coord_names=RING, low_level_coord_names=LOW)
y_ref = lsu.convert_waveform_coordinates(x.astype(float), coord_names=RING, low_level_coord_names=LOW)
assert np.allclose(np.asarray(y, dtype=float), y_ref, atol=1e-12)


def test_phi12_zero_inplane_spin():
# phi12 is undefined without an in-plane component; both paths return 0
for x in (_one(10., 0.3, 0.0, 0.5, 1.5, 0.5, 0.2, 2.5), _one(10., 0.3, 0.5, 0.2, 1.5, 0.5, -1.0, 2.5)):
y = lsu.convert_waveform_coordinates(x, coord_names=['phi12'], low_level_coord_names=LOW)
assert y[0, 0] == 0.
P = lsu.ChooseWaveformParams()
P.m1, P.m2 = 10 * lal.MSUN_SI, 5 * lal.MSUN_SI
P.s1x = P.s1y = 0.
P.s2x, P.s2y = 0.3, 0.1
assert P.extract_param('phi12') == 0.


def test_enforce_kerr_in_ring_block():
# the vectorized block can end the conversion early; it applies the fallthrough's Kerr rule itself
x = np.vstack([_one(10., 0.3, 1.2, 0.2, 1.5, 0.5, 0.2, 2.5), _one(10., 0.3, 0.5, 0.2, 1.5, 0.5, 0.2, 2.5)])
y = lsu.convert_waveform_coordinates(x, coord_names=['mc', 'delta_mc', 'chi1_perp', 'chi_p_vec'],
low_level_coord_names=LOW, enforce_kerr=True)
assert np.all(y[0] == -np.inf) and np.all(np.isfinite(y[1]))
y = lsu.convert_waveform_coordinates(x, coord_names=['chi1_perp'], low_level_coord_names=LOW, enforce_kerr=False)
assert np.all(np.isfinite(y))


def test_chi_p_vec_not_assignable():
# grid readers call assign_param on every column named in valid_params; chi_p_vec is derived only
assert 'chi_p_vec' not in lsu.valid_params
Loading