From 508dd5c5fa461fa051e0bb2acf889179a36a6371 Mon Sep 17 00:00:00 2001
From: Raffi Enficiaud <raffi.enficiaud@mines-paris.org>
Date: Tue, 10 Dec 2024 17:38:11 +0100
Subject: [PATCH 1/3] Support for eccentricity and mean_per_ano parameters

* add the new parameters
* set new parameters to zero if not specified
* unit tests
---
 bilby/gw/source.py     | 86 ++++++++++++++++++++++++++++++++++++++----
 test/gw/source_test.py | 84 +++++++++++++++++++++++++++++++++++++++--
 2 files changed, 159 insertions(+), 11 deletions(-)

diff --git a/bilby/gw/source.py b/bilby/gw/source.py
index bd2cd1b64..08b7e5f22 100644
--- a/bilby/gw/source.py
+++ b/bilby/gw/source.py
@@ -16,8 +16,22 @@
 """
 
 
-def gwsignal_binary_black_hole(frequency_array, mass_1, mass_2, luminosity_distance, a_1, tilt_1,
-                               phi_12, a_2, tilt_2, phi_jl, theta_jn, phase, **kwargs):
+def _base_gwsignal_binary_black_hole(
+        frequency_array,
+        mass_1,
+        mass_2,
+        luminosity_distance,
+        a_1,
+        tilt_1,
+        phi_12,
+        a_2,
+        tilt_2,
+        phi_jl,
+        theta_jn,
+        phase,
+        eccentricity,
+        mean_per_ano,
+        **kwargs):
     """
     A binary black hole waveform model using GWsignal
 
@@ -48,6 +62,10 @@ def gwsignal_binary_black_hole(frequency_array, mass_1, mass_2, luminosity_dista
         Angle between the total binary angular momentum and the line of sight
     phase: float
         The phase at coalescence
+    eccentricity: float
+        Orbital eccentricity
+    mean_per_ano: float
+        Mean anomaly
     kwargs: dict
         Optional keyword arguments
         Supported arguments:
@@ -83,8 +101,8 @@ def gwsignal_binary_black_hole(frequency_array, mass_1, mass_2, luminosity_dista
     =====
     This function is a temporary wrapper to the interface that will
     likely be significantly changed or removed in a future release.
-    This version is only intended to be used with `SEOBNRv5HM` and `SEOBNRv5PHM` and
-    does not have full functionality for other waveform models.
+    This version is only intended to be used with ``SEOBNRv5HM``, ``SEOBNRv5EHM``
+    and ``SEOBNRv5PHM`` and does not have full functionality for other waveform models.
     """
 
     from lalsimulation.gwsignal import GenerateFDWaveform
@@ -103,7 +121,7 @@ def gwsignal_binary_black_hole(frequency_array, mass_1, mass_2, luminosity_dista
     waveform_kwargs.update(kwargs)
 
     waveform_approximant = waveform_kwargs['waveform_approximant']
-    if waveform_approximant not in ["SEOBNRv5HM", "SEOBNRv5PHM"]:
+    if waveform_approximant not in ["SEOBNRv5HM", "SEOBNRv5EHM", "SEOBNRv5PHM"]:
         if waveform_approximant == "IMRPhenomXPHM":
             logger.warning("The new waveform interface is unreviewed for this model" +
                            "and it is only intended for testing.")
@@ -141,9 +159,7 @@ def gwsignal_binary_black_hole(frequency_array, mass_1, mass_2, luminosity_dista
         phi_12=phi_12, a_1=a_1, a_2=a_2, mass_1=mass_1 * utils.solar_mass, mass_2=mass_2 * utils.solar_mass,
         reference_frequency=reference_frequency, phase=phase)
 
-    eccentricity = 0.0
     longitude_ascending_nodes = 0.0
-    mean_per_ano = 0.0
 
     # Check if conditioning is needed
     condition = 0
@@ -252,6 +268,62 @@ def gwsignal_binary_black_hole(frequency_array, mass_1, mass_2, luminosity_dista
     return dict(plus=h_plus, cross=h_cross)
 
 
+def gwsignal_binary_black_hole(frequency_array, mass_1, mass_2, luminosity_distance, a_1, tilt_1,
+                               phi_12, a_2, tilt_2, phi_jl, theta_jn, phase, **kwargs):
+
+    return _base_gwsignal_binary_black_hole(
+        frequency_array=frequency_array,
+        mass_1=mass_1,
+        mass_2=mass_2,
+        luminosity_distance=luminosity_distance,
+        a_1=a_1,
+        tilt_1=tilt_1,
+        phi_12=phi_12,
+        a_2=a_2,
+        tilt_2=tilt_2,
+        phi_jl=phi_jl,
+        theta_jn=theta_jn,
+        phase=phase,
+        eccentricity=0,
+        mean_per_ano=0,
+        **kwargs)
+
+
+def gwsignal_eccentric_binary_black_hole(
+        frequency_array,
+        mass_1,
+        mass_2,
+        luminosity_distance,
+        a_1,
+        tilt_1,
+        phi_12,
+        a_2,
+        tilt_2,
+        phi_jl,
+        theta_jn,
+        phase,
+        eccentricity,
+        mean_per_ano,
+        **kwargs):
+
+    return _base_gwsignal_binary_black_hole(
+        frequency_array=frequency_array,
+        mass_1=mass_1,
+        mass_2=mass_2,
+        luminosity_distance=luminosity_distance,
+        a_1=a_1,
+        tilt_1=tilt_1,
+        phi_12=phi_12,
+        a_2=a_2,
+        tilt_2=tilt_2,
+        phi_jl=phi_jl,
+        theta_jn=theta_jn,
+        phase=phase,
+        eccentricity=eccentricity,
+        mean_per_ano=mean_per_ano,
+        **kwargs)
+
+
 def lal_binary_black_hole(
         frequency_array, mass_1, mass_2, luminosity_distance, a_1, tilt_1,
         phi_12, a_2, tilt_2, phi_jl, theta_jn, phase, **kwargs):
diff --git a/test/gw/source_test.py b/test/gw/source_test.py
index be9072fb8..191082380 100644
--- a/test/gw/source_test.py
+++ b/test/gw/source_test.py
@@ -1,13 +1,15 @@
-import unittest
 import logging
-import pytest
+import random
+import unittest
+from copy import copy
+from unittest.mock import patch
 
+import astropy.units as u
 import bilby
 import lal
 import lalsimulation
-
 import numpy as np
-from copy import copy
+import pytest
 
 
 class TestLalBBH(unittest.TestCase):
@@ -170,6 +172,7 @@ def test_waveform_error_raising(self):
             bilby.gw.source.gwsignal_binary_black_hole(
                 self.frequency_array, **raise_error_parameters
             )
+
     # def test_gwsignal_bbh_works_without_waveform_parameters(self):
     #    self.assertIsInstance(
     #        bilby.gw.source.gwsignal_binary_black_hole(
@@ -193,6 +196,79 @@ def test_gwsignal_lal_bbh_consistency(self):
             np.allclose(hpc_gwsignal["cross"], hpc_lal["cross"], atol=0, rtol=1e-7)
         )
 
+    def test_argument_passed_to_generate_waveform(self):
+        # here we test the behaviour of the function "gwsignal_binary_black_hole"
+        # until the execution of the "generate_fd_waveform" in gwsignal. In particular
+        # the actual generate_fd_waveform is not called and only the parameters passed to
+        # the function are checked.
+        # The test does not require gwsignal to support any of the approximants or parameters.
+        from lalsimulation.gwsignal.models.pyseobnr_model import SEOBNRv5PHM as wf_gen
+
+        class MyException(Exception):
+            pass
+
+        with patch.object(
+            lalsimulation.gwsignal.models,
+            "gwsignal_get_waveform_generator",
+            autospec=True,
+        ) as mock_gwsignal_get_waveform_generator:
+
+            with patch.object(
+                wf_gen, "generate_fd_waveform", autospec=True
+            ) as mock_wgen_gen_fd:
+                mock_wgen_gen_fd.side_effect = MyException(
+                    "__not_the_string_input_domain_error__"
+                )
+                mock_gwsignal_get_waveform_generator.return_value = wf_gen()
+
+                for current_param, gwsignal_target_param in (
+                    "eccentricity",
+                    "eccentricity",
+                ), ("mean_per_ano", "meanPerAno"):
+                    mock_wgen_gen_fd.reset_mock()
+                    mock_gwsignal_get_waveform_generator.reset_mock()
+
+                    parameters = self.parameters.copy()
+                    parameters["waveform_approximant"] = "SEOBNRv5PHM"
+                    parameters.update(self.waveform_kwargs | {"eccentricity": 0, "mean_per_ano": 0})
+                    parameters[current_param] = random.uniform(0, 0.3)
+
+                    with self.assertRaises(MyException):
+                        bilby.gw.source.gwsignal_eccentric_binary_black_hole(
+                            self.frequency_array, **parameters
+                        )
+
+                    # check we are calling the mock generator
+                    mock_gwsignal_get_waveform_generator.assert_called_once_with(
+                        parameters["waveform_approximant"]
+                    )
+
+                    # checks generated_fd_waveform is called
+                    mock_wgen_gen_fd.assert_called_once()
+
+                    # checks parameters are passed: this is done inside the
+                    self.assertIsInstance(
+                        mock_wgen_gen_fd.call_args_list[0].args[0], wf_gen
+                    )
+
+                    # check if the currently modified parameter is dA22, dtau32 etc
+                    self.assertIn(
+                        gwsignal_target_param, mock_wgen_gen_fd.call_args_list[0].kwargs
+                    )
+                    converted_param = mock_wgen_gen_fd.call_args_list[0].kwargs[
+                        gwsignal_target_param
+                    ]
+
+                    if converted_param.unit == u.rad:
+                        self.assertEqual(
+                            converted_param.value, parameters[current_param]
+                        )
+
+                    elif converted_param.unit == u.dimensionless_unscaled:
+                        self.assertEqual(
+                            float(converted_param), parameters[current_param]
+                        )
+
 
 class TestLalBNS(unittest.TestCase):
     def setUp(self):

From b6007a6bb60b2afecfc2a6297acb02f69e3e14fc Mon Sep 17 00:00:00 2001
From: AntoniRamosBuades <arbuades@nikhef.nl>
Date: Fri, 21 Feb 2025 12:57:00 +0100
Subject: [PATCH 2/3] Update for usage with latest scipy version

---
 bilby/core/prior/analytical.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/bilby/core/prior/analytical.py b/bilby/core/prior/analytical.py
index 5e7b3099f..4032d9113 100644
--- a/bilby/core/prior/analytical.py
+++ b/bilby/core/prior/analytical.py
@@ -2,6 +2,12 @@
 from scipy.special import erfinv
 from scipy.special._ufuncs import xlogy, erf, log1p, stdtrit, gammaln, stdtr, \
     btdtri, betaln, btdtr, gammaincinv, gammainc
+try:
+    from scipy.special._ufuncs import betaincinv as btdtri
+    from scipy.special._ufuncs import betainc as btdtr
+    
+except: 
+    from scipy.special._ufuncs import  btdtri,btdtr 
 
 from .base import Prior
 from ..utils import logger

From 87be26ff47f41fd50089e1b4357f293ed4e62648 Mon Sep 17 00:00:00 2001
From: AntoniRamosBuades <arbuades@nikhef.nl>
Date: Fri, 21 Feb 2025 12:59:29 +0100
Subject: [PATCH 3/3] Remove btdtri and btdtr from main import

---
 bilby/core/prior/analytical.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bilby/core/prior/analytical.py b/bilby/core/prior/analytical.py
index 4032d9113..9636d0adb 100644
--- a/bilby/core/prior/analytical.py
+++ b/bilby/core/prior/analytical.py
@@ -1,7 +1,7 @@
 import numpy as np
 from scipy.special import erfinv
 from scipy.special._ufuncs import xlogy, erf, log1p, stdtrit, gammaln, stdtr, \
-    btdtri, betaln, btdtr, gammaincinv, gammainc
+    betaln,gammaincinv, gammainc
 try:
     from scipy.special._ufuncs import betaincinv as btdtri
     from scipy.special._ufuncs import betainc as btdtr