diff --git a/.gitignore b/.gitignore index 48e4c01a1387a87c5af4a7381f1f5d157171e3c0..437f5654e8ab5c75ef9e57427ae65d39c467b37c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,6 @@ MANIFEST *.dat *.version *.ipynb_checkpoints -outdir/* +**/outdir .idea/* bilby/_version.py diff --git a/bilby/core/sampler/dynesty.py b/bilby/core/sampler/dynesty.py index d9c6ab6c01180226ce3bb91e58e0928df9cc00d7..a4e459f59089b723fc9a0747dd6c1aa49edce10b 100644 --- a/bilby/core/sampler/dynesty.py +++ b/bilby/core/sampler/dynesty.py @@ -725,7 +725,7 @@ class Dynesty(NestedSampler): def _run_test(self): import pandas as pd - self.sampler = self.sampler_class( + self.sampler = self.sampler_init( loglikelihood=self.log_likelihood, prior_transform=self.prior_transform, ndim=self.ndim, @@ -734,7 +734,14 @@ class Dynesty(NestedSampler): sampler_kwargs = self.sampler_function_kwargs.copy() sampler_kwargs["maxiter"] = 2 + if self.print_method == "tqdm" and self.kwargs["print_progress"]: + from tqdm.auto import tqdm + + self.pbar = tqdm(file=sys.stdout, initial=self.sampler.it) self.sampler.run_nested(**sampler_kwargs) + if self.pbar is not None: + self.pbar = self.pbar.close() + print("") N = 100 self.result.samples = pd.DataFrame(self.priors.sample(N))[ self.search_parameter_keys diff --git a/examples/core_examples/conditional_prior.py b/examples/core_examples/conditional_prior.py deleted file mode 100644 index 9dc236a87c5c2986c9bb5320def58aed1c182c39..0000000000000000000000000000000000000000 --- a/examples/core_examples/conditional_prior.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -This tutorial demonstrates how we can sample a prior in the shape of a ball. -Note that this will not end up sampling uniformly in that space, constraint -priors are more suitable for that. -This implementation will draw a value for the x-coordinate from p(x), and -given that draw a value for the -y-coordinate from p(y|x), and given that draw a value for the z-coordinate -from p(z|x,y). -Only the x-coordinate will end up being uniform for this -""" -import bilby -import numpy as np - - -class ZeroLikelihood(bilby.core.likelihood.Likelihood): - """Flat likelihood. This always returns 0. - This way our posterior distribution is exactly the prior distribution.""" - - def log_likelihood(self): - return 0 - - -def condition_func_y(reference_params, x): - """Condition function for our p(y|x) prior.""" - radius = 0.5 * (reference_params["maximum"] - reference_params["minimum"]) - y_max = np.sqrt(radius**2 - x**2) - return dict(minimum=-y_max, maximum=y_max) - - -def condition_func_z(reference_params, x, y): - """Condition function for our p(z|x, y) prior.""" - radius = 0.5 * (reference_params["maximum"] - reference_params["minimum"]) - z_max = np.sqrt(radius**2 - x**2 - y**2) - return dict(minimum=-z_max, maximum=z_max) - - -# Set up the conditional priors and the flat likelihood -priors = bilby.core.prior.ConditionalPriorDict() -priors["x"] = bilby.core.prior.Uniform(minimum=-1, maximum=1, latex_label="$x$") -priors["y"] = bilby.core.prior.ConditionalUniform( - condition_func=condition_func_y, minimum=-1, maximum=1, latex_label="$y$" -) -priors["z"] = bilby.core.prior.ConditionalUniform( - condition_func=condition_func_z, minimum=-1, maximum=1, latex_label="$z$" -) -likelihood = ZeroLikelihood(parameters=dict(x=0, y=0, z=0)) - -# Sample the prior distribution -res = bilby.run_sampler( - likelihood=likelihood, - priors=priors, - sampler="dynesty", - nlive=5000, - label="conditional_prior", - outdir="outdir", - resume=False, - clean=True, -) -res.plot_corner() diff --git a/examples/gw_examples/injection_examples/relative_binning.py b/examples/gw_examples/injection_examples/relative_binning.py index 5a848f003b0a88262d0380272c85b624ef306111..bc224ca6208a7734b2495c7291830ba4bd640427 100644 --- a/examples/gw_examples/injection_examples/relative_binning.py +++ b/examples/gw_examples/injection_examples/relative_binning.py @@ -7,6 +7,7 @@ This example estimates the masses using a uniform prior in both component masses and distance using a uniform in comoving volume prior on luminosity distance between luminosity distances of 100Mpc and 5Gpc, the cosmology is Planck15. """ +from copy import deepcopy import bilby import numpy as np @@ -109,6 +110,17 @@ priors["fiducial"] = 0 # Perform a check that the prior does not extend to a parameter space longer than the data priors.validate_prior(duration, minimum_frequency) +# Set up the fiducial parameters for the relative binning likelihood to be the +# injected parameters. Note that because we sample in chirp mass and mass ratio +# but injected with mass_1 and mass_2, we need to convert the mass parameters +fiducial_parameters = injection_parameters.copy() +m1 = fiducial_parameters.pop("mass_1") +m2 = fiducial_parameters.pop("mass_2") +fiducial_parameters["chirp_mass"] = bilby.gw.conversion.component_masses_to_chirp_mass( + m1, m2 +) +fiducial_parameters["mass_ratio"] = m2 / m1 + # Initialise the likelihood by passing in the interferometer data (ifos) and # the waveform generator likelihood = bilby.gw.likelihood.RelativeBinningGravitationalWaveTransient( @@ -116,15 +128,15 @@ likelihood = bilby.gw.likelihood.RelativeBinningGravitationalWaveTransient( waveform_generator=waveform_generator, priors=priors, distance_marginalization=True, - fiducial_parameters=injection_parameters, + fiducial_parameters=fiducial_parameters, ) -# Run sampler. In this case we're going to use the `dynesty` sampler +# Run sampler. In this case, we're going to use the `nestle` sampler result = bilby.run_sampler( likelihood=likelihood, priors=priors, sampler="nestle", - npoints=100, + npoints=1000, injection_parameters=injection_parameters, outdir=outdir, label=label, @@ -158,5 +170,15 @@ print( ) print(f"Binned vs unbinned log Bayes factor {np.log(np.mean(weights)):.2f}") -# Make a corner plot. -# result.plot_corner() +# Generate result object with the posterior for the regular likelihood using +# rejection sampling +alt_result = deepcopy(result) +keep = weights > np.random.uniform(0, max(weights), len(weights)) +alt_result.posterior = result.posterior.iloc[keep] + +# Make a comparison corner plot. +bilby.core.result.plot_multiple( + [result, alt_result], + labels=["Binned", "Reweighted"], + filename=f"{outdir}/{label}_corner.png", +) diff --git a/examples/tutorials/conditional_priors.ipynb b/examples/tutorials/conditional_priors.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..0838e5da9cb205f467fcf58c94acef9aea36e08c --- /dev/null +++ b/examples/tutorials/conditional_priors.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Conditional prior demonstration\n", + "\n", + "Conditional priors enable inference to be performed with priors that correlate different parameters.\n", + "In this notebook, we demonstrate two uses of this: maintaining a two-dimensional distribution while changing the parameterization to be more efficient for sampling, and enforcing an ordering between parameters.\n", + "\n", + "Many cases where `Conditional` priors are useful can also be expressed with `Constraint` priors, however the conditional approach can improve sampling efficiency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from bilby.core.prior import (\n", + " Prior, PriorDict, ConditionalPriorDict,\n", + " Uniform, ConditionalUniform, Constraint, \n", + ")\n", + "from corner import corner\n", + "from scipy.stats import semicircular\n", + "\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sampling from a disc\n", + "\n", + "Our first example is sampling uniformly from a disc\n", + "\n", + "$$p(x, y) = \\frac{1}{\\pi}; x^2 + y+2 \\leq 1.$$\n", + "\n", + "A naive implementation of this would define a uniform prior over a square over `[-1, 1]` and then reject points that don't satisfy the radius constraint.\n", + "Naive sampling from this parameterization would have an efficiency of $\\pi / 4$.\n", + "\n", + "If we instead consider the marginal distribution $p(x)$ and conditional distribution $p(y | x)$, we can achieve a sampling efficiency of 100%.\n", + "\n", + "$$\n", + "p(x) = \\int_{-1 + \\sqrt{x^2 + y^2}}^{1 - \\sqrt{x^2 + y^2}} dy p(x, y) = \\frac{2 (1 - \\sqrt{x^2 + y^2})}{\\pi} \\\\\n", + "p(y | x) = \\frac{1}{2 (1 - \\sqrt{x^2})}\n", + "$$\n", + "\n", + "The marginal distribution for $x$ is the [Wigner semicircle distribution](https://en.wikipedia.org/wiki/Wigner_semicircle_distribution), this distribution is not currently defined in `Bilby`, but we can wrap the `scipy` implementation.\n", + "The conditional distribution for $y$ is implemented in `Bilby` as the `ConditionUnifrom`, we just need to define the condition function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class SemiCircular(Prior):\n", + "\n", + " def __init__(self, radius=1, center=0, name=None, latex_label=None, unit=None, boundary=None):\n", + " super(SemiCircular, self).__init__(\n", + " minimum=center - radius,\n", + " maximum=center + radius,\n", + " name=name,\n", + " latex_label=latex_label,\n", + " unit=unit,\n", + " boundary=boundary,\n", + " )\n", + " self.radius = radius\n", + " self.center = center\n", + " self._dist = semicircular(loc=center, scale=radius)\n", + "\n", + " def prob(self, val):\n", + " return self._dist.pdf(val)\n", + "\n", + " def ln_prob(self, val):\n", + " return self._dist.logpdf(val)\n", + "\n", + " def cdf(self, val):\n", + " return self._dist.cdf(val)\n", + "\n", + " def rescale(self, val):\n", + " return self._dist.ppf(val)\n", + "\n", + "\n", + "def conditional_func_y(reference_parameters, x):\n", + " condition = np.sqrt(reference_parameters[\"maximum\"]-x**2)\n", + " return dict(minimum=-condition, maximum=condition)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Sample from the distribution\n", + "\n", + "To demonstrate the equivalence of the two methods, we will draw samples from the distribution using the two methods and verify that they agree." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "N = int(2e4)\n", + "\n", + "CORNER_KWARGS = dict(\n", + " plot_contours=False,\n", + " plot_density=False,\n", + " fill_contours=False,\n", + " max_n_ticks=3,\n", + " verbose=False,\n", + " use_math_text=True,\n", + ")\n", + "\n", + "\n", + "def convert_to_radial(parameters):\n", + " p = parameters.copy()\n", + " p['r'] = p['x']**2 + p['y']**2\n", + " return p\n", + "\n", + "def sample_circle_with_constraint():\n", + " d = PriorDict(\n", + " dictionary=dict(\n", + " x=Uniform(-1, 1),\n", + " y=Uniform(-1, 1),\n", + " r=Constraint(0, 1),\n", + " ),\n", + " conversion_function=convert_to_radial\n", + " )\n", + " return pd.DataFrame(d.sample(N))\n", + "\n", + "\n", + "def sample_circle_with_conditional():\n", + " d = ConditionalPriorDict(\n", + " dictionary=dict(\n", + " x=SemiCircular(),\n", + " y=ConditionalUniform(\n", + " condition_func=conditional_func_y, \n", + " minimum=-1, maximum=1\n", + " )\n", + " )\n", + " )\n", + " return pd.DataFrame(d.sample(N))\n", + "\n", + "\n", + "s1 = sample_circle_with_constraint()\n", + "s2 = sample_circle_with_conditional()\n", + "fig = corner(s1.values, **CORNER_KWARGS, color=\"tab:blue\", labels=[\"$x$\", \"$y$\"])\n", + "corner(s2.values, **CORNER_KWARGS, color=\"tab:green\", fig=fig)\n", + "plt.show()\n", + "plt.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sampling from ordered distributions\n", + "\n", + "As our second example, we demonstrate defining a prior distribution over a set of strictly ordered parameters.\n", + "\n", + "We note that in this case, we do not require that the marginal distributions for each of the parameters are independently and identically disributed, although this can be fairly simply remedied." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class BoundedUniform(ConditionalUniform):\n", + " \"\"\"Conditional Uniform prior where prior sample < previous prior sample\n", + " \n", + " This is ensured by fixing the maximum bound to be the previous prior sample value.\n", + " \"\"\"\n", + " def __init__(self, idx: int, minimum, maximum, name=None, latex_label=None,\n", + " unit=None, boundary=None):\n", + " super(BoundedUniform, self).__init__(\n", + " minimum=minimum, maximum=maximum, name=name, \n", + " latex_label=latex_label, unit=unit,\n", + " boundary=boundary, condition_func=self.bounds_condition\n", + " )\n", + " self.idx = idx\n", + " self.previous_name = f\"{name[:-1]}{self.idx - 1}\"\n", + " self._required_variables = [self.previous_name] \n", + " # this is used in prior.sample(... **required_variables)\n", + "\n", + "\n", + " def bounds_condition(self, reference_params, **required_variables):\n", + " previous_sample = required_variables[self.previous_name]\n", + " return dict(maximum=previous_sample)\n", + "\n", + "\n", + "def make_uniform_conditonal_priordict(n_priors=3):\n", + " priors = ConditionalPriorDict()\n", + " for i in range(n_priors):\n", + " if i==0:\n", + " priors[f\"uni{i}\"] = Uniform(minimum=0, maximum=1, name=f\"uni{i}\")\n", + " else:\n", + " priors[f\"uni{i}\"] = BoundedUniform(idx=i, minimum=0, maximum=1, name=f\"uni{i}\")\n", + " return priors\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "samples = pd.DataFrame(make_uniform_conditonal_priordict(3).sample(10000))\n", + "fig = corner(samples.values, **CORNER_KWARGS, color=\"tab:blue\", labels=[f\"$A_{ii}$\" for ii in range(3)])\n", + "plt.show()\n", + "plt.close()" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +}