From 73f37bb8348248e860b1d0695804532f2989c3d9 Mon Sep 17 00:00:00 2001 From: Alex Nitz Date: Fri, 30 Jun 2023 15:54:17 -0400 Subject: [PATCH] add high frequency sky location dependent response for long detectors (#4377) * reset commit, add in high freqeuncy response to detector * update * fixes --- bin/hwinj/pycbc_generate_hwinj_from_xml | 2 +- examples/detector/loc.py | 4 +- pycbc/detector.py | 133 +++++++++++++++++------- test/test_detector.py | 2 +- test/test_injection.py | 1 - 5 files changed, 98 insertions(+), 44 deletions(-) diff --git a/bin/hwinj/pycbc_generate_hwinj_from_xml b/bin/hwinj/pycbc_generate_hwinj_from_xml index b3f7fb3bfcc..2561b78a096 100644 --- a/bin/hwinj/pycbc_generate_hwinj_from_xml +++ b/bin/hwinj/pycbc_generate_hwinj_from_xml @@ -49,7 +49,7 @@ parser.add_argument('--sample-rate', type=int, required=True, parser.add_argument("--tag", type=str, default='hwinjcbcsimid', help="Prefix added to output filenames.") parser.add_argument('--ifos', nargs='+', default=['H1', 'L1'], required=True, - choices=list(zip(*get_available_detectors()))[0], + choices=get_available_detectors(), help='List of IFOs to generate injections for.') # parse command line opts = parser.parse_args() diff --git a/examples/detector/loc.py b/examples/detector/loc.py index 4e9188bb25e..4016229660d 100644 --- a/examples/detector/loc.py +++ b/examples/detector/loc.py @@ -4,10 +4,10 @@ # along with a longer name. Note that some of these are not physical detectors # but may be useful for testing or study purposes -for abv, long_name in get_available_detectors(): +for abv in get_available_detectors(): d = Detector(abv) # Note that units are all in radians - print("{} {} Latitude {} Longitude {}".format(long_name, abv, + print("{} Latitude {} Longitude {}".format(abv, d.latitude, d.longitude)) diff --git a/pycbc/detector.py b/pycbc/detector.py index 35c3dd3d15c..b1cbcf2cfc0 100644 --- a/pycbc/detector.py +++ b/pycbc/detector.py @@ -48,8 +48,14 @@ def gmst_accurate(gps_time): location=(0, 0)).sidereal_time('mean').rad return gmst - def get_available_detectors(): + """ List the available detectors """ + dets = list(_ground_detectors.keys()) + for pfx, name in get_available_lal_detectors(): + dets += [pfx] + return dets + +def get_available_lal_detectors(): """Return list of detectors known in the currently sourced lalsuite. This function will query lalsuite about which detectors are known to lalsuite. Detectors are identified by a two character string e.g. 'K1', @@ -66,10 +72,11 @@ def get_available_detectors(): known_names = [ld[k.replace('PREFIX', 'NAME')] for k in known_lal_names] return list(zip(known_prefixes, known_names)) +_ground_detectors = {} -_custom_ground_detectors = {} def add_detector_on_earth(name, longitude, latitude, - yangle=0, xangle=None, height=0): + yangle=0, xangle=None, height=0, + xlength=4000, ylength=4000): """ Add a new detector on the earth Parameters @@ -94,36 +101,61 @@ def add_detector_on_earth(name, longitude, latitude, # assume right angle detector if no separate xarm direction given xangle = yangle + np.pi / 2.0 + # Rotation matrix to move detector to correct orientation + rm1 = rotation_matrix(longitude * units.rad, 'z') + rm2 = rotation_matrix((np.pi / 2.0 - latitude) * units.rad, 'y') + rm = np.matmul(rm2, rm1) + # Calculate response in earth centered coordinates # by rotation of response in coordinates aligned # with the detector arms - a, b = cos(2*xangle), sin(2*xangle) - xresp = np.array([[-a, b, 0], [b, a, 0], [0, 0, 0]]) - a, b = cos(2*yangle), sin(2*yangle) - yresp = np.array([[-a, b, 0], [b, a, 0], [0, 0, 0]]) - resp = (yresp - xresp) / 4.0 + resps = [] + vecs = [] + for angle in [yangle, xangle]: + a, b = cos(2 * angle), sin(2 * angle) + resp = np.array([[-a, b, 0], [b, a, 0], [0, 0, 0]]) - rm1 = rotation_matrix(longitude * units.rad, 'z') - rm2 = rotation_matrix((np.pi / 2.0 - latitude) * units.rad, 'y') - rm = np.matmul(rm2, rm1) + # apply rotation + resp = np.matmul(resp, rm) + resp = np.matmul(rm.T, resp) / 4.0 + resps.append(resp) - resp = np.matmul(resp, rm) - resp = np.matmul(rm.T, resp) + vec = np.matmul(rm.T, np.array([-np.cos(angle), np.sin(angle), 0])) + vecs.append(vec) + full_resp = (resps[0] - resps[1]) loc = coordinates.EarthLocation.from_geodetic(longitude * units.rad, latitude * units.rad, height=height*units.meter) - loc = np.array([loc.x.value, - loc.y.value, - loc.z.value]) - _custom_ground_detectors[name] = {'location': loc, - 'response': resp, - 'yangle': yangle, - 'xangle': xangle, - 'height': height, - 'xaltitude': 0.0, - 'yaltitude': 0.0, - } + loc = np.array([loc.x.value, loc.y.value, loc.z.value]) + _ground_detectors[name] = {'location': loc, + 'response': full_resp, + 'xresp': resps[1], + 'yresp': resps[0], + 'xvec': vecs[1], + 'yvec': vecs[0], + 'yangle': yangle, + 'xangle': xangle, + 'height': height, + 'xaltitude': 0.0, + 'yaltitude': 0.0, + 'ylength': ylength, + 'xlength': xlength, + } + +# Notation matches +# Eq 4 of https://link.aps.org/accepted/10.1103/PhysRevD.96.084004 +def single_arm_frequency_response(f, n, arm_length): + """ The relative amplitude factor of the arm response due to + signal delay. This is relevant where the long-wavelength + approximation no longer applies) + """ + n = np.clip(n, -0.999, 0.999) + phase = arm_length / constants.c.value * 2.0j * np.pi * f + a = 1.0 / 4.0 / phase + b = (1 - np.exp(-phase * (1 - n))) / (1 - n) + c = np.exp(-2.0 * phase) * (1 - np.exp(phase * (1 + n))) / (1 + n) + return a * (b - c) * 2.0 # We'll make this relative to the static resp def load_detector_config(config_files): """ Add custom detectors from a configuration file @@ -175,16 +207,17 @@ def __init__(self, detector_name, reference_time=1126259462.0): using a slower but higher precision method. """ self.name = str(detector_name) - - if detector_name in [pfx for pfx, name in get_available_detectors()]: + + lal_detectors = [pfx for pfx, name in get_available_lal_detectors()] + if detector_name in _ground_detectors: + self.info = _ground_detectors[detector_name] + self.response = self.info['response'] + self.location = self.info['location'] + elif detector_name in lal_detectors: lalsim = pycbc.libutils.import_optional('lalsimulation') self._lal = lalsim.DetectorPrefixToLALDetector(self.name) self.response = self._lal.response self.location = self._lal.location - elif detector_name in _custom_ground_detectors: - self.info = _custom_ground_detectors[detector_name] - self.response = self.info['response'] - self.location = self.info['location'] else: raise ValueError("Unkown detector {}".format(detector_name)) @@ -257,7 +290,9 @@ def light_travel_time_to_detector(self, det): d = self.location - det.location return float(d.dot(d)**0.5 / constants.c.value) - def antenna_pattern(self, right_ascension, declination, polarization, t_gps, polarization_type='tensor'): + def antenna_pattern(self, right_ascension, declination, polarization, t_gps, + frequency=0, + polarization_type='tensor'): """Return the detector response. Parameters @@ -289,31 +324,50 @@ def antenna_pattern(self, right_ascension, declination, polarization, t_gps, pol cospsi = cos(polarization) sinpsi = sin(polarization) + if frequency: + e0 = cosdec * cosgha + e1 = cosdec * -singha + e2 = sin(declination) + nhat = np.array([e0, e1, e2], dtype=object) + + nx = nhat.dot(self.info['xvec']) + ny = nhat.dot(self.info['yvec']) + + rx = single_arm_frequency_response(frequency, nx, + self.info['xlength']) + ry = single_arm_frequency_response(frequency, ny, + self.info['ylength']) + resp = ry * self.info['yresp'] - rx * self.info['xresp'] + ttype = np.complex128 + else: + resp = self.response + ttype = np.float64 + x0 = -cospsi * singha - sinpsi * cosgha * sindec x1 = -cospsi * cosgha + sinpsi * singha * sindec x2 = sinpsi * cosdec x = np.array([x0, x1, x2], dtype=object) - dx = self.response.dot(x) + dx = resp.dot(x) y0 = sinpsi * singha - cospsi * cosgha * sindec y1 = sinpsi * cosgha + cospsi * singha * sindec y2 = cospsi * cosdec y = np.array([y0, y1, y2], dtype=object) - dy = self.response.dot(y) + dy = resp.dot(y) if polarization_type != 'tensor': z0 = -cosdec * cosgha z1 = cosdec * singha z2 = -sindec z = np.array([z0, z1, z2], dtype=object) - dz = self.response.dot(z) + dz = resp.dot(z) if polarization_type == 'tensor': if hasattr(dx, 'shape'): - fplus = (x * dx - y * dy).sum(axis=0).astype(np.float64) - fcross = (x * dy + y * dx).sum(axis=0).astype(np.float64) + fplus = (x * dx - y * dy).sum(axis=0).astype(ttype) + fcross = (x * dy + y * dx).sum(axis=0).astype(ttype) else: fplus = (x * dx - y * dy).sum() fcross = (x * dy + y * dx).sum() @@ -321,16 +375,17 @@ def antenna_pattern(self, right_ascension, declination, polarization, t_gps, pol elif polarization_type == 'vector': if hasattr(dx, 'shape'): - fx = (z * dx + x * dz).sum(axis=0).astype(np.float64) - fy = (z * dy + y * dz).sum(axis=0).astype(np.float64) + fx = (z * dx + x * dz).sum(axis=0).astype(ttype) + fy = (z * dy + y * dz).sum(axis=0).astype(ttype) else: fx = (z * dx + x * dz).sum() fy = (z * dy + y * dz).sum() + return fx, fy elif polarization_type == 'scalar': if hasattr(dx, 'shape'): - fb = (x * dx + y * dy).sum(axis=0).astype(np.float64) + fb = (x * dx + y * dy).sum(axis=0).astype(ttype) fl = (z * dz).sum(axis=0) else: fb = (x * dx + y * dy).sum() diff --git a/test/test_detector.py b/test/test_detector.py index ac8be6c4cca..f11e12527cb 100644 --- a/test/test_detector.py +++ b/test/test_detector.py @@ -38,7 +38,7 @@ class TestDetector(unittest.TestCase): def setUp(self): self.d = [det.Detector(ifo) - for ifo, name in det.get_available_detectors()] + for ifo in det.get_available_detectors()] # not distributed sanely, but should provide some good coverage N = 1000 diff --git a/test/test_injection.py b/test/test_injection.py index d9e6b91abce..2ffb8479bfa 100644 --- a/test/test_injection.py +++ b/test/test_injection.py @@ -85,7 +85,6 @@ def fill_sim_inspiral_row(self, row): class TestInjection(unittest.TestCase): def setUp(self): available_detectors = get_available_detectors() - available_detectors = [a[0] for a in available_detectors] self.assertTrue('H1' in available_detectors) self.assertTrue('L1' in available_detectors) self.assertTrue('V1' in available_detectors)