diff --git a/AUTHORS.md b/AUTHORS.md index 32f41f250997c53eaa77e2217a7a7326c3be4081..42d22edbc19da2e09d4d7d1d98581acde8fddfb7 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -78,3 +78,4 @@ Tathagata Ghosh Virginia d'Emilio Vivien Raymond Ka-Lok Lo +Isaac Legred \ No newline at end of file diff --git a/bilby/gw/conversion.py b/bilby/gw/conversion.py index b494dca9f4a947f63cce12004bae181c478e5c7e..24ae47ba406f2d46e10d7c781c1dcc1f5083f45f 100644 --- a/bilby/gw/conversion.py +++ b/bilby/gw/conversion.py @@ -210,67 +210,8 @@ def convert_to_lal_binary_black_hole_parameters(parameters): converted_parameters[key[:-7]] = converted_parameters[key] * ( 1 + converted_parameters['redshift']) - if 'chirp_mass' in converted_parameters.keys(): - if "mass_1" in converted_parameters.keys(): - converted_parameters["mass_ratio"] = chirp_mass_and_primary_mass_to_mass_ratio( - converted_parameters["chirp_mass"], converted_parameters["mass_1"]) - if 'total_mass' in converted_parameters.keys(): - converted_parameters['symmetric_mass_ratio'] =\ - chirp_mass_and_total_mass_to_symmetric_mass_ratio( - converted_parameters['chirp_mass'], - converted_parameters['total_mass']) - if 'symmetric_mass_ratio' in converted_parameters.keys() and "mass_ratio" not in converted_parameters: - converted_parameters['mass_ratio'] =\ - symmetric_mass_ratio_to_mass_ratio( - converted_parameters['symmetric_mass_ratio']) - if 'total_mass' not in converted_parameters.keys(): - converted_parameters['total_mass'] =\ - chirp_mass_and_mass_ratio_to_total_mass( - converted_parameters['chirp_mass'], - converted_parameters['mass_ratio']) - converted_parameters['mass_1'], converted_parameters['mass_2'] = \ - total_mass_and_mass_ratio_to_component_masses( - converted_parameters['mass_ratio'], - converted_parameters['total_mass']) - elif 'total_mass' in converted_parameters.keys(): - if 'symmetric_mass_ratio' in converted_parameters.keys(): - converted_parameters['mass_ratio'] = \ - symmetric_mass_ratio_to_mass_ratio( - converted_parameters['symmetric_mass_ratio']) - if 'mass_ratio' in converted_parameters.keys(): - converted_parameters['mass_1'], converted_parameters['mass_2'] =\ - total_mass_and_mass_ratio_to_component_masses( - converted_parameters['mass_ratio'], - converted_parameters['total_mass']) - elif 'mass_1' in converted_parameters.keys(): - converted_parameters['mass_2'] =\ - converted_parameters['total_mass'] -\ - converted_parameters['mass_1'] - elif 'mass_2' in converted_parameters.keys(): - converted_parameters['mass_1'] = \ - converted_parameters['total_mass'] - \ - converted_parameters['mass_2'] - elif 'symmetric_mass_ratio' in converted_parameters.keys(): - converted_parameters['mass_ratio'] =\ - symmetric_mass_ratio_to_mass_ratio( - converted_parameters['symmetric_mass_ratio']) - if 'mass_1' in converted_parameters.keys(): - converted_parameters['mass_2'] =\ - converted_parameters['mass_1'] *\ - converted_parameters['mass_ratio'] - elif 'mass_2' in converted_parameters.keys(): - converted_parameters['mass_1'] =\ - converted_parameters['mass_2'] /\ - converted_parameters['mass_ratio'] - elif 'mass_ratio' in converted_parameters.keys(): - if 'mass_1' in converted_parameters.keys(): - converted_parameters['mass_2'] =\ - converted_parameters['mass_1'] *\ - converted_parameters['mass_ratio'] - if 'mass_2' in converted_parameters.keys(): - converted_parameters['mass_1'] = \ - converted_parameters['mass_2'] /\ - converted_parameters['mass_ratio'] + # we do not require the component masses be added if no mass parameters are present + converted_parameters = generate_component_masses(converted_parameters, require_add=False) for idx in ['1', '2']: key = 'chi_{}'.format(idx) @@ -480,6 +421,33 @@ def total_mass_and_mass_ratio_to_component_masses(mass_ratio, total_mass): return mass_1, mass_2 +def chirp_mass_and_mass_ratio_to_component_masses(chirp_mass, mass_ratio): + """ + Convert total mass and mass ratio of a binary to its component masses. + + Parameters + ========== + chirp_mass: float + Chirp mass of the binary + mass_ratio: float + Mass ratio (mass_2/mass_1) of the binary + + Returns + ======= + mass_1: float + Mass of the heavier object + mass_2: float + Mass of the lighter object + """ + total_mass = chirp_mass_and_mass_ratio_to_total_mass(chirp_mass=chirp_mass, + mass_ratio=mass_ratio) + mass_1, mass_2 = ( + total_mass_and_mass_ratio_to_component_masses( + total_mass=total_mass, mass_ratio=mass_ratio) + ) + return mass_1, mass_2 + + def symmetric_mass_ratio_to_mass_ratio(symmetric_mass_ratio): """ Convert the symmetric mass ratio to the normal mass ratio. @@ -678,6 +646,30 @@ def mass_1_and_chirp_mass_to_mass_ratio(mass_1, chirp_mass): return mass_ratio +def mass_2_and_chirp_mass_to_mass_ratio(mass_2, chirp_mass): + """ + Calculate mass ratio from mass_1 and chirp_mass. + + This involves solving mc = m2 * (1/q)**(3/5) / (1 + (1/q))**(1/5). + + Parameters + ========== + mass_2: float + Mass of the lighter object + chirp_mass: float + Chirp mass of the binary + + Returns + ======= + mass_ratio: float + Mass ratio of the binary + """ + # Passing mass_2, the expression from the function above + # returns 1/q (because chirp mass is invariant under + # mass_1 <-> mass_2) + return 1 / mass_1_and_chirp_mass_to_mass_ratio(mass_2, chirp_mass) + + def lambda_1_lambda_2_to_lambda_tilde(lambda_1, lambda_2, mass_1, mass_2): """ Convert from individual tidal parameters to domainant tidal term. @@ -1009,33 +1001,192 @@ def fill_from_fixed_priors(sample, priors): return output_sample +def generate_component_masses(sample, require_add=False): + """" + Add the component masses to the dataframe/dictionary + We add: + mass_1, mass_2 + We also add any other masses which may be necessary for + intermediate steps, i.e. typically the total mass is necessary, along + with the mass ratio, so these will usually be added to the dictionary + + If `require_add` is True, then having an incomplete set of mass + parameters (so that the component mass parameters cannot be added) + will throw an error, otherwise it will quietly add nothing to the + dictionary. + + Parameters + ========= + sample : dict + The input dictionary with at least one + component with overall mass scaling (i.e. + chirp_mass, mass_1, mass_2, total_mass) and + then any other mass parameter. + + Returns + dict : the updated dictionary + """ + def check_and_return_quietly(require_add, sample): + if require_add: + raise KeyError("Insufficient mass parameters in input dictionary") + else: + return sample + output_sample = sample.copy() + if "mass_1" in sample.keys(): + if "mass_2" in sample.keys(): + return output_sample + if "total_mass" in sample.keys(): + output_sample["mass_2"] = output_sample["total_mass"] - ( + output_sample["mass_1"] + ) + return output_sample + + elif "mass_ratio" in sample.keys(): + pass + elif "symmetric_mass_ratio" in sample.keys(): + output_sample["mass_ratio"] = ( + symmetric_mass_ratio_to_mass_ratio( + output_sample["symmetric_mass_ratio"]) + ) + elif "chirp_mass" in sample.keys(): + output_sample["mass_ratio"] = ( + mass_1_and_chirp_mass_to_mass_ratio( + mass_1=output_sample["mass_1"], + chirp_mass=output_sample["chirp_mass"]) + ) + else: + return check_and_return_quietly(require_add, sample) + + output_sample["mass_2"] = ( + output_sample["mass_ratio"] * output_sample["mass_1"] + ) + + return output_sample + + elif "mass_2" in sample.keys(): + # mass_1 is not in the dict + if "total_mass" in sample.keys(): + output_sample["mass_1"] = ( + output_sample["total_mass"] - output_sample["mass_2"] + ) + return output_sample + elif "mass_ratio" in sample.keys(): + pass + elif "symmetric_mass_ratio" in sample.keys(): + output_sample["mass_ratio"] = ( + symmetric_mass_ratio_to_mass_ratio( + output_sample["symmetric_mass_ratio"]) + ) + elif "chirp_mass" in sample.keys(): + output_sample["mass_ratio"] = ( + mass_2_and_chirp_mass_to_mass_ratio( + mass_2=output_sample["mass_2"], + chirp_mass=output_sample["chirp_mass"]) + ) + else: + check_and_return_quietly(require_add, sample) + + output_sample["mass_1"] = 1 / output_sample["mass_ratio"] * ( + output_sample["mass_2"] + ) + + return output_sample + + # Only if neither mass_1 or mass_2 is in the input sample + if "total_mass" in sample.keys(): + if "mass_ratio" in sample.keys(): + pass # We have everything we need already + elif "symmetric_mass_ratio" in sample.keys(): + output_sample["mass_ratio"] = ( + symmetric_mass_ratio_to_mass_ratio( + output_sample["symmetric_mass_ratio"]) + ) + elif "chirp_mass" in sample.keys(): + output_sample["symmetric_mass_ratio"] = ( + chirp_mass_and_total_mass_to_symmetric_mass_ratio( + chirp_mass=output_sample["chirp_mass"], + total_mass=output_sample["total_mass"]) + ) + output_sample["mass_ratio"] = ( + symmetric_mass_ratio_to_mass_ratio( + output_sample["symmetric_mass_ratio"]) + ) + else: + return check_and_return_quietly(require_add, sample) + + elif "chirp_mass" in sample.keys(): + if "mass_ratio" in sample.keys(): + pass + elif "symmetric_mass_ratio" in sample.keys(): + output_sample["mass_ratio"] = ( + symmetric_mass_ratio_to_mass_ratio( + sample["symmetric_mass_ratio"]) + ) + else: + return check_and_return_quietly(require_add, sample) + + output_sample["total_mass"] = ( + chirp_mass_and_mass_ratio_to_total_mass( + chirp_mass=output_sample["chirp_mass"], + mass_ratio=output_sample["mass_ratio"]) + ) + + # We haven't matched any of the criteria + if "total_mass" not in output_sample.keys() or ( + "mass_ratio" not in output_sample.keys()): + return check_and_return_quietly(require_add, sample) + mass_1, mass_2 = ( + total_mass_and_mass_ratio_to_component_masses( + total_mass=output_sample["total_mass"], + mass_ratio=output_sample["mass_ratio"]) + ) + output_sample["mass_1"] = mass_1 + output_sample["mass_2"] = mass_2 + return output_sample + + def generate_mass_parameters(sample): """ - Add the known mass parameters to the data frame/dictionary. + Add the known mass parameters to the data frame/dictionary. We do + not recompute keys already present in the dictionary - We add: - chirp mass, total mass, symmetric mass ratio, mass ratio + We add, potentially: + chirp mass, total mass, symmetric mass ratio, mass ratio, mass_1, mass_2 Parameters ========== sample : dict - The input dictionary with component masses 'mass_1' and 'mass_2' - + The input dictionary with two "spanning" mass parameters + e.g. (mass_1, mass_2), or (chirp_mass, mass_ratio), but not e.g. only + (mass_ratio, symmetric_mass_ratio) Returns ======= dict: The updated dictionary """ - output_sample = sample.copy() - output_sample['chirp_mass'] =\ - component_masses_to_chirp_mass(sample['mass_1'], sample['mass_2']) - output_sample['total_mass'] =\ - component_masses_to_total_mass(sample['mass_1'], sample['mass_2']) - output_sample['symmetric_mass_ratio'] =\ - component_masses_to_symmetric_mass_ratio(sample['mass_1'], - sample['mass_2']) - output_sample['mass_ratio'] =\ - component_masses_to_mass_ratio(sample['mass_1'], sample['mass_2']) + # Only add the parameters if they're not already present + intermediate_sample = generate_component_masses(sample) + output_sample = intermediate_sample.copy() + if "chirp_mass" not in output_sample.keys(): + output_sample['chirp_mass'] = ( + component_masses_to_chirp_mass(output_sample['mass_1'], + output_sample['mass_2']) + ) + if "total_mass" not in output_sample.keys(): + output_sample['total_mass'] = ( + component_masses_to_total_mass(output_sample['mass_1'], + output_sample['mass_2']) + ) + if "symmetric_mass_ratio" not in output_sample.keys(): + output_sample['symmetric_mass_ratio'] = ( + component_masses_to_symmetric_mass_ratio(output_sample['mass_1'], + output_sample['mass_2']) + ) + if "mass_ratio" not in output_sample.keys(): + output_sample['mass_ratio'] = ( + component_masses_to_mass_ratio(output_sample['mass_1'], + output_sample['mass_2']) + ) return output_sample diff --git a/test/gw/conversion_test.py b/test/gw/conversion_test.py index e89bdce00164bc24a2440f18dd2a21dc25b41c02..a85cbf8e5dad58fbdba38a8b0a9e2f51d5d72343 100644 --- a/test/gw/conversion_test.py +++ b/test/gw/conversion_test.py @@ -3,6 +3,7 @@ import unittest import numpy as np import pandas as pd + import bilby from bilby.gw import conversion @@ -99,6 +100,13 @@ class TestBasicConversions(unittest.TestCase): ) self.assertAlmostEqual(self.total_mass, total_mass) + def test_chirp_mass_and_mass_ratio_to_component_masses(self): + mass_1, mass_2 = \ + conversion.chirp_mass_and_mass_ratio_to_component_masses( + self.chirp_mass, self.mass_ratio) + self.assertAlmostEqual(self.mass_1, mass_1) + self.assertAlmostEqual(self.mass_2, mass_2) + def test_component_masses_to_chirp_mass(self): chirp_mass = conversion.component_masses_to_chirp_mass(self.mass_1, self.mass_2) self.assertAlmostEqual(self.chirp_mass, chirp_mass) @@ -523,5 +531,68 @@ class TestDistanceTransformations(unittest.TestCase): self.assertAlmostEqual(max(abs(dl - self.distances)), 0, 4) +class TestGenerateMassParameters(unittest.TestCase): + def setUp(self): + self.expected_values = {'mass_1': 2.0, + 'mass_2': 1.0, + 'chirp_mass': 1.2167286837864113, + 'total_mass': 3.0, + 'symmetric_mass_ratio': 0.2222222222222222, + 'mass_ratio': 0.5} + + def helper_generation_from_keys(self, keys, expected_values,): + # Explicitly test the helper generate_component_masses + local_test_vars = \ + {key: expected_values[key] for key in keys} + local_test_vars_with_component_masses = \ + conversion.generate_component_masses(local_test_vars) + self.assertTrue("mass_1" in local_test_vars_with_component_masses.keys()) + self.assertTrue("mass_2" in local_test_vars_with_component_masses.keys()) + for key in local_test_vars_with_component_masses.keys(): + self.assertAlmostEqual( + local_test_vars_with_component_masses[key], + self.expected_values[key]) + + # Test the function more generally + local_all_mass_parameters = \ + conversion.generate_mass_parameters(local_test_vars) + self.assertEqual(local_all_mass_parameters.keys(), + self.expected_values.keys()) + for key in expected_values.keys(): + self.assertAlmostEqual(expected_values[key], local_all_mass_parameters[key]) + + def test_from_mass_1_and_mass_2(self): + self.helper_generation_from_keys(["mass_1", "mass_2"], + self.expected_values) + + def test_from_mass_1_and_mass_ratio(self): + self.helper_generation_from_keys(["mass_1", "mass_ratio"], + self.expected_values) + + def test_from_mass_2_and_mass_ratio(self): + self.helper_generation_from_keys(["mass_2", "mass_ratio"], + self.expected_values) + + def test_from_mass_1_and_total_mass(self): + self.helper_generation_from_keys(["mass_2", "total_mass"], + self.expected_values) + + def test_from_chirp_mass_and_mass_ratio(self): + self.helper_generation_from_keys(["chirp_mass", "mass_ratio"], + self.expected_values) + + def test_from_chirp_mass_and_symmetric_mass_ratio(self): + self.helper_generation_from_keys(["chirp_mass", "symmetric_mass_ratio"], + self.expected_values) + + def test_from_chirp_mass_and_symmetric_mass_1(self): + self.helper_generation_from_keys(["chirp_mass", "mass_1"], + self.expected_values) + + def test_from_chirp_mass_and_symmetric_mass_2(self): + self.helper_generation_from_keys(["chirp_mass", "mass_2"], + self.expected_values) + + if __name__ == "__main__": unittest.main()