diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bbb5b3f..282aefc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + BILBY_ALLOW_PARAMETERS_AS_STATE: FALSE + jobs: unittests: name: Unit tests - Python ${{ matrix.python-version }} (${{ matrix.os }}) @@ -36,7 +39,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install .[test] + pip install .[test] "numpy<2.4" # numpy 2.4 causes issues with bilby + - name: Install bilby>=2.7 + if: matrix.python-version != '3.9' + run: | + pip install "bilby>=2.7" - name: Set MPL backend on Windows if: runner.os == 'Windows' run: | @@ -44,3 +51,7 @@ jobs: - name: Test with pytest run: | python -m pytest + - name: Test with bilby<2.7 + run: | + pip install "bilby<2.7" + python -m pytest diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..354c3cf --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,11 @@ +# Contributors + +This file preserves contributor attribution from bilby history, including commits from git.ligo.org that are not present in this plugin repository. + +It is a legacy attribution snapshot from before the sampler was moved to this plugin repository; new plugin-era contributions are tracked in this repository's git history. + + +- Colm Talbot +- Gregory Ashton +- Michael J. Williams +- Moritz Huebner diff --git a/pyproject.toml b/pyproject.toml index b672695..2fd27df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,11 @@ build-backend = "setuptools.build_meta" [project] name = "nessai-bilby" authors = [ - {name = "Michael J. Williams", email = "michaeljw1@googlemail.com"}, + {name = "Bilby Developers"}, +] +maintainers = [ + {name = "Colm Talbot", email = "colm.talbot@ligo.org"}, + {name = "Michael J. Williams", email = "michael.williams@ligo.org"}, ] description = "Interface and plugin for using nessai in bilby" readme = "README.md" diff --git a/src/nessai_bilby/model.py b/src/nessai_bilby/model.py index 5d3d222..57abee5 100644 --- a/src/nessai_bilby/model.py +++ b/src/nessai_bilby/model.py @@ -42,6 +42,7 @@ def __init__( self.use_ratio = use_ratio self.names = self.bilby_priors.non_fixed_keys self._update_bounds() + self.fixed_parameters = self._fixed_parameters_dict(self.bilby_priors) if self.use_ratio: self.bilby_log_likelihood_fn = ( @@ -52,6 +53,14 @@ def __init__( self.validate_bilby_likelihood() + @staticmethod + def _fixed_parameters_dict(bilby_priors: "PriorDict") -> dict: + """Dictionary of fixed parameters.""" + theta = {} + for key in bilby_priors.fixed_keys: + theta[key] = bilby_priors[key].value + return theta + def _update_bounds(self): self.bounds = { key: [ @@ -61,17 +70,27 @@ def _update_bounds(self): for key in self.names } + def _try_bilby_likelihood(self, theta): + """Try to evaluate the bilby likelihood with the given parameters.""" + try: + return self.bilby_log_likelihood_fn(theta) + except TypeError: + self.bilby_likelihood.parameters.update(theta) + return self.bilby_log_likelihood_fn() + def validate_bilby_likelihood(self) -> None: """Validate the bilby likelihood object""" - theta = self.bilby_priors.sample() - self.bilby_likelihood.parameters.update(theta) - self.bilby_log_likelihood_fn() + theta = self.fixed_parameters.copy() + theta_new = self.bilby_priors.sample() + theta.update(theta_new) + return self._try_bilby_likelihood(theta) def log_likelihood(self, x): """Compute the log likelihood""" - theta = {n: x[n].item() for n in self.names} - self.bilby_likelihood.parameters.update(theta) - return self.bilby_log_likelihood_fn() + theta = self.fixed_parameters.copy() + for n in self.names: + theta[n] = x[n].item() + return self._try_bilby_likelihood(theta) def log_prior(self, x): """Compute the log prior. @@ -79,9 +98,7 @@ def log_prior(self, x): Also evaluates the likelihood constraints. """ theta = {n: x[n] for n in self.names} - return self.bilby_priors.ln_prob(theta, axis=0) + np.log( - self.bilby_priors.evaluate_constraints(theta) - ) + return self.bilby_priors.ln_prob(theta, axis=0) def new_point(self, N=1): """Draw a point from the prior""" @@ -116,11 +133,12 @@ def log_likelihood(self, x): Also evaluates the likelihood constraints. """ - theta = {n: x[n].item() for n in self.names} + theta = self.fixed_parameters.copy() + for n in self.names: + theta[n] = x[n].item() if not self.bilby_priors.evaluate_constraints(theta): return -np.inf - self.bilby_likelihood.parameters.update(theta) - return self.bilby_log_likelihood_fn() + return self._try_bilby_likelihood(theta) def log_prior(self, x): """Compute the log prior.""" diff --git a/src/nessai_bilby/plugin.py b/src/nessai_bilby/plugin.py index f3b96aa..54e17b5 100644 --- a/src/nessai_bilby/plugin.py +++ b/src/nessai_bilby/plugin.py @@ -214,6 +214,20 @@ def run_sampler(self): "n_pool=1, overriding n_pool to None to disable multiprocessing" ) n_pool = None + self._npool = None + + if n_pool is not None: + logger.info( + f"Using bilby pool for multiprocessing with n_pool={n_pool}" + ) + self._setup_pool(model) + else: + # bilby doesn't define pool by default + self.pool = None + + # Remove pool if it exists since it will have been used in `_setup_pool` + # we instead pass the pool to the FlowSampler directly. + kwargs.pop("pool", None) # Configure the sampler self.fs = FlowSampler( @@ -221,11 +235,15 @@ def run_sampler(self): signal_handling=False, # Disable signal handling so it can be handled by bilby importance_nested_sampler=self._importance_nested_sampler, n_pool=n_pool, + pool=self.pool, + close_pool=False, # Don't close the pool since it is managed by bilby **kwargs, ) # Run the sampler self.fs.run(**run_kwargs) + self._close_pool() + # Update the result self.update_result() @@ -306,8 +324,13 @@ def get_expected_outputs(cls, outdir=None, label=None): filenames = [] return filenames, dirs - def _setup_pool(self): - pass + def _setup_pool(self, model): + from nessai.utils.multiprocessing import ( + initialise_pool_variables, + ) + + initialise_pool_variables(model) + super()._setup_pool() class ImportanceNessai(Nessai): diff --git a/tests/conftest.py b/tests/conftest.py index 7814eec..a4a6e02 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,26 +4,33 @@ from nessai.livepoint import reset_extra_live_points_parameters -class GaussianLikelihood(bilby.Likelihood): - def __init__(self): - """A very simple Gaussian likelihood""" - super().__init__(parameters={"x": None, "y": None}) - - def log_likelihood(self): - """Log-likelihood.""" - return -0.5 * ( - self.parameters["x"] ** 2.0 + self.parameters["y"] ** 2.0 - ) - np.log(2.0 * np.pi) - - -@pytest.fixture -def bilby_gaussian_likelihood_and_priors(): - likelihood = GaussianLikelihood() - priors = dict( - x=bilby.core.prior.Uniform(-10, 10, "x"), - y=bilby.core.prior.Uniform(-10, 10, "y"), - ) - return likelihood, priors +def model(x, m, c): + return m * x + c + + +def conversion_func(parameters): + # d = |m| + |c| + parameters["d"] = abs(parameters["m"]) + abs(parameters["c"]) + return parameters + + +@pytest.fixture() +def bilby_likelihood(rng): + x = np.linspace(0, 10, 100) + injection_parameters = dict(m=0.5, c=0.2) + sigma = 1.0 + y = model(x, **injection_parameters) + rng.normal(0.0, sigma, len(x)) + likelihood = bilby.likelihood.GaussianLikelihood(x, y, model, sigma) + return likelihood + + +@pytest.fixture() +def bilby_priors(): + priors = bilby.core.prior.PriorDict(conversion_function=conversion_func) + priors["m"] = bilby.core.prior.Uniform(0, 5, boundary="periodic") + priors["c"] = bilby.core.prior.Uniform(-2, 2, boundary="reflective") + priors["d"] = bilby.core.prior.Constraint(name="d", minimum=0, maximum=5) + return priors @pytest.fixture(autouse=True) diff --git a/tests/test_bilby_integration.py b/tests/test_bilby_integration.py index b4c95f6..c7ad899 100644 --- a/tests/test_bilby_integration.py +++ b/tests/test_bilby_integration.py @@ -1,92 +1,198 @@ """Test the integration with bilby""" +import os +import signal + import bilby import pytest +from nessai_bilby.model import BilbyModel + @pytest.fixture(params=[False, True]) def likelihood_constraint(request): return request.param +@pytest.fixture +def conversion_function(): + def _conversion_function(parameters, likelihood, prior): + converted = parameters.copy() + if "derived" not in converted: + converted["derived"] = converted["m"] * converted["c"] + return converted + + return _conversion_function + + +def run_sampler( + likelihood, + priors, + outdir, + conversion_function, + sampler, + n_pool=None, + **kwargs, +): + result = bilby.run_sampler( + likelihood=likelihood, + priors=priors, + sampler=sampler, + outdir=str(outdir), + save="hdf5", + n_pool=n_pool, + conversion_function=conversion_function, + **kwargs, + ) + return result + + def test_sampling_nessai( - bilby_gaussian_likelihood_and_priors, + bilby_likelihood, + bilby_priors, + conversion_function, tmp_path, likelihood_constraint, n_pool, ): - likelihood, priors = bilby_gaussian_likelihood_and_priors - outdir = tmp_path / "test_sampling_nessai" - bilby.run_sampler( - outdir=outdir, - resume=False, - plot=False, - likelihood=likelihood, - priors=priors, + run_sampler( + bilby_likelihood, + bilby_priors, + outdir, + conversion_function, + sampler="nessai", nlive=100, stopping=5.0, - sampler="nessai", - injection_parameters={"x": 0.0, "y": 0.0}, analytic_priors=True, seed=1234, nessai_likelihood_constraint=likelihood_constraint, n_pool=n_pool, ) - # Assert plots are made + assert list(outdir.glob("*_nessai/*.png")) +def test_ensure_pool_is_used( + bilby_likelihood, bilby_priors, tmp_path, caplog, monkeypatch +): + # Patch the ModelClass to check if the pool is used + from nessai_bilby import plugin + + class TestModel(BilbyModel): + pool_used = False + + def batch_evaluate_log_likelihood(self, parameters): + if self.pool is not None: + TestModel.pool_used = True + else: + assert False, "Pool was not set in the model" + return super().batch_evaluate_log_likelihood(parameters) + + monkeypatch.setattr(plugin, "BilbyModel", TestModel) + + with caplog.at_level("DEBUG", logger="nessai"): + run_sampler( + bilby_likelihood, + bilby_priors, + tmp_path / "test_pool_is_used", + None, + "nessai", + n_pool=2, + condition=10, + nlive=100, + nessai_likelihood_constraint=False, + ) + assert "Using user specified pool" in caplog.text + assert TestModel.pool_used, "Pool was not used during sampling" + + def test_sampling_inessai( - bilby_gaussian_likelihood_and_priors, + bilby_likelihood, + bilby_priors, + conversion_function, tmp_path, likelihood_constraint, n_pool, ): - likelihood, priors = bilby_gaussian_likelihood_and_priors - outdir = tmp_path / "test_sampling_inessai" - bilby.run_sampler( - outdir=outdir, - resume=False, - plot=False, - likelihood=likelihood, - priors=priors, + run_sampler( + bilby_likelihood, + bilby_priors, + outdir, + conversion_function, + sampler="inessai", + n_pool=n_pool, nlive=100, min_samples=10, - sampler="inessai", - injection_parameters={"x": 0.0, "y": 0.0}, seed=1234, nessai_likelihood_constraint=likelihood_constraint, - n_pool=n_pool, ) def test_sampling_nessai_plot( - bilby_gaussian_likelihood_and_priors, + bilby_likelihood, + bilby_priors, + conversion_function, tmp_path, ): - likelihood, priors = bilby_gaussian_likelihood_and_priors - outdir = tmp_path / "test_sampling_nessai_plot" - - bilby.run_sampler( - outdir=outdir, - resume=False, - plot=True, - likelihood=likelihood, - priors=priors, + run_sampler( + bilby_likelihood, + bilby_priors, + outdir, + conversion_function, + sampler="nessai", nlive=100, stopping=5.0, - sampler="nessai", - injection_parameters={"x": 0.0, "y": 0.0}, analytic_priors=True, seed=1234, nessai_likelihood_constraint=False, nessai_plot=False, - n_pool=None, ) + # Assert no png files in the output directory assert not list(outdir.glob("*_nessai/*.png")) + + +def test_interrupt_sampler( + bilby_likelihood, + bilby_priors, + conversion_function, + n_pool, + tmp_path, +): + outdir = tmp_path / "test_interrupt_sampler" + interrupted = False + + def interrupt_once(_sampler): + nonlocal interrupted + if not interrupted: + interrupted = True + os.kill(os.getpid(), signal.SIGINT) + + label = "test_interrupt" + + with pytest.raises((SystemExit, KeyboardInterrupt)) as exc: + run_sampler( + bilby_likelihood, + bilby_priors, + outdir, + conversion_function, + "nessai", + exit_code=5, + resume=True, + label=label, + n_pool=n_pool, + nlive=100, + checkpoint_on_iteration=True, + checkpoint_interval=1, + checkpoint_callback=interrupt_once, + ) + + if isinstance(exc.value, SystemExit): + assert exc.value.code == 5 + + assert outdir.glob(f"{label}_nessai/*.pkl") diff --git a/tests/test_model.py b/tests/test_model.py index bde08da..237df31 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -11,10 +11,9 @@ def ModelClass(request): return request.param -def test_create_model(ModelClass, bilby_gaussian_likelihood_and_priors, rng): - likelihood, priors = bilby_gaussian_likelihood_and_priors - priors = bilby.core.prior.PriorDict(priors) - model = ModelClass(priors=priors, likelihood=likelihood) +def test_create_model(ModelClass, bilby_likelihood, bilby_priors, rng): + priors = bilby.core.prior.PriorDict(bilby_priors) + model = ModelClass(priors=priors, likelihood=bilby_likelihood) # Do this rather than using `set_rng` to ensure backwards compatibility model.set_rng(rng) model.validate_bilby_likelihood() @@ -22,13 +21,13 @@ def test_create_model(ModelClass, bilby_gaussian_likelihood_and_priors, rng): def test_sample_model_with_nessai( - bilby_gaussian_likelihood_and_priors, + bilby_likelihood, + bilby_priors, tmp_path, ModelClass, ): - likelihood, priors = bilby_gaussian_likelihood_and_priors - priors = bilby.core.prior.PriorDict(priors) - model = ModelClass(priors=priors, likelihood=likelihood) + priors = bilby.core.prior.PriorDict(bilby_priors) + model = ModelClass(priors=priors, likelihood=bilby_likelihood) fs = FlowSampler( model=model, diff --git a/tests/test_sampler_class.py b/tests/test_sampler_class.py index 033577a..8341408 100644 --- a/tests/test_sampler_class.py +++ b/tests/test_sampler_class.py @@ -14,15 +14,11 @@ def SamplerClass(request): @pytest.fixture() -def create_sampler( - SamplerClass, bilby_gaussian_likelihood_and_priors, tmp_path -): - likelihood, priors = bilby_gaussian_likelihood_and_priors - +def create_sampler(SamplerClass, bilby_likelihood, bilby_priors, tmp_path): def create_fn(**kwargs): return SamplerClass( - likelihood, - priors, + likelihood=bilby_likelihood, + priors=bilby_priors, outdir=tmp_path / "outdir", label="test", use_ratio=False,