Auxiliary Samplers

Along with sampling the spatial and luminosity distributions, auxiliary properties and be sampled that both depend on and/or influence the luminosity as well each other. This allows you to build up arbitrailiy complex dependencies between parameters which can lead to diverse populations.

[1]:

import networkx as nx import numpy as np import matplotlib.pyplot as plt %matplotlib inline from jupyterthemes import jtplot jtplot.style(context="notebook", fscale=1, grid=False) purple = "#B833FF" yellow = "#F6EF5B" import warnings warnings.simplefilter("ignore") import popsynth popsynth.update_logging_level("INFO")

Built in auxiliary samplers

There are several built in auxiliary samplers that allow you to quickly add on auxiliary parameters.

[2]:
popsynth.list_available_auxiliary_samplers()
DeltaAuxSampler
LogNormalAuxSampler
Log10NormalAuxSampler
NormalAuxSampler
ParetoAuxSampler
PowerLawAuxSampler
BrokenPowerLawAuxSampler
TruncatedNormalAuxSampler
ViewingAngleSampler

We can add these on to the populations, but let’s have a look at how to use them.

[3]:
x = popsynth.NormalAuxSampler(name="aux_param", observed=True)

x.mu = 0
x.sigma = 1

# draws the observed values from normal distribution with std equal to tau
x.tau = 1

If value of x is observed (generates data), then we can set the width of the normal distribtuion from which the observed values are sampled from the latent values. Otherwise, only the latent values are stored. This applies to any of the built in auxiliary samplers. However, this can all be customized by adding our own:

Creating a custom auxiliary sampler

Let’s create two auxiliary samplers that sample values from normal distributions with some dependency on each other.

First, we specify the main population. This time, we will chose a SFR-like redshift distribution and a Schecter luminosity function

[4]:
pop_gen = popsynth.populations.SchechterSFRPopulation(
    r0=100,a=0.0157, rise=1.0, decay=1.0, peak=1.0, Lmin=1e50, alpha=2.0
)

Suppose we have a property “demo” that we want to sample as well. For this property, we do not observe it directly. We will get to that. This means that our property latent and could influence other parameters but we can not measure it directly. If you are familiar with Bayesian hierarchical models, this concept may be more familiar to you. As an example, this could be the temperature of a star, which influences its spectrum. The spectrum creates an observable, but the tempreature is imply a random latent variable sampled from a distribution.

We create an AuxiliarySampler child class, and define the true_sampler for the latent values:

[5]:
class DemoSampler(popsynth.AuxiliarySampler):
    _auxiliary_sampler_name = "DemoSampler"

    mu = popsynth.auxiliary_sampler.AuxiliaryParameter(default=2)
    tau = popsynth.auxiliary_sampler.AuxiliaryParameter(default=1, vmin=0)

    def __init__(self):

        # pass up to the super class
        super(DemoSampler, self).__init__("demo", observed=False)

    def true_sampler(self, size):

        # sample the latent values for this property

        self._true_values = np.random.normal(self.mu, self.tau, size=size)

Now we instantiate it and then assign it our pop_gen object. Then we draw out survey

[6]:
demo1 = DemoSampler()

pop_gen.add_observed_quantity(demo1)


flux_selector = popsynth.HardFluxSelection()
flux_selector.boundary = 1e-9

pop_gen.set_flux_selection(flux_selector)

population = pop_gen.draw_survey()



## plot it
options = {"node_color": purple, "node_size": 3000, "width": 0.5}
pos = nx.drawing.nx_agraph.graphviz_layout(population.graph, prog="dot")
nx.draw(population.graph, with_labels=True, pos=pos, **options)
 INFO     |  registering auxilary sampler: demo 
 INFO     |  The volume integral is 4760.754853877658 
 INFO     |  Expecting 4693 total objects 
 INFO     |  Sampling: demo 
 INFO     |  applying selection to fluxes 
 INFO     |  Detected 3223 distances 
 INFO     |  Detected 3223 objects out to a distance of 8.44 
../_images/notebooks_aux_12_3.png
[7]:
fig = population.display_fluxes(obs_color=purple, true_color=yellow, s=15)
../_images/notebooks_aux_13_0.png

We can see that the population has stored out demo auxiliary property.

[8]:
all_demo = population.demo

selected_demo = population.demo.selected

We can also see that our demo sampler is now known which is important when creating populations from YAML files. This registering happens when we add the property _auxiliary_sampler_name = "DemoSampler" which must be name of the class!

[9]:
popsynth.list_available_auxiliary_samplers()
DeltaAuxSampler
LogNormalAuxSampler
Log10NormalAuxSampler
NormalAuxSampler
ParetoAuxSampler
PowerLawAuxSampler
BrokenPowerLawAuxSampler
TruncatedNormalAuxSampler
ViewingAngleSampler
DemoSampler

Observed auxiliary properties and dependent parameters

Suppose now we want to simulate a property that is observed by an instrument but depends on latent parameters.

We will create a second demo sampler and tell it what the observational error is as well as how to read from a secondary sampler:

[10]:
class DemoSampler2(popsynth.AuxiliarySampler):
    _auxiliary_sampler_name = "DemoSampler2"
    mu = popsynth.auxiliary_sampler.AuxiliaryParameter(default=2)
    tau = popsynth.auxiliary_sampler.AuxiliaryParameter(default=1, vmin=0)
    sigma = popsynth.auxiliary_sampler.AuxiliaryParameter(default=1, vmin=0)

    def __init__(
        self,
    ):

        # this time set observed=True
        super(DemoSampler2, self).__init__("demo2", observed=True, uses_distance=True)

    def true_sampler(self, size):

        # we access the secondary sampler dictionary. In this
        # case "demo". This itself is a sampler with
        # <>.true_values as a parameter
        secondary = self._secondary_samplers['demo']

        # now we sample the demo2 latent values and add on the dependence of "demo"

        tmp = np.random.normal(self.mu, self.tau, size=size)

        # for fun, we can substract the log of the distance as all
        # auxiliary samples know about their distances

        self._true_values = tmp + secondary.true_values - np.log10(1 + self._distance)

    def observation_sampler(self, size):

        # here we define the "observed" values, i.e., the latened values
        # with observational error

        self._obs_values = self._true_values + np.random.normal(
            0, self.sigma, size=size
        )

We recreate our base sampler:

[11]:
pop_gen = popsynth.populations.SchechterSFRPopulation(
    r0=100, a=0.0157, rise=1.0, decay=1.0, peak=1.0, Lmin=1e50, alpha=2.0
)

Now, make a new demo1, but this time we do not have to attach it to the base sampler. Instead, we will assign it as a secondary sampler to demo2 and popsynth is smart enough to search for it when it draws a survey.

[12]:
demo1 = DemoSampler()


demo2 = DemoSampler2()

demo2.set_secondary_sampler(demo1)

# attach to the base sampler
pop_gen.add_observed_quantity(demo2)
 INFO     |  registering auxilary sampler: demo2 
[13]:
pos = nx.drawing.nx_agraph.graphviz_layout(pop_gen.graph, prog="dot")


fig, ax = plt.subplots()


nx.draw(pop_gen.graph, with_labels=True, pos=pos, ax=ax, **options)
../_images/notebooks_aux_24_0.png
[14]:

flux_selector = popsynth.HardFluxSelection() flux_selector.boundary = 1e-8 pop_gen.set_flux_selection(flux_selector) population = pop_gen.draw_survey(flux_sigma=0.1)
 INFO     |  The volume integral is 4760.754853877658 
 INFO     |  Expecting 4693 total objects 
 INFO     |  Sampling: demo2 
 INFO     |  demo2 is sampling its secondary quantities 
 INFO     |  Sampling: demo 
 INFO     |  applying selection to fluxes 
 INFO     |  Detected 1124 distances 
 INFO     |  Detected 1124 objects out to a distance of 3.18 
[15]:
fig, ax = plt.subplots()

ax.scatter(population.demo2.selected, population.demo.selected, c=purple, s=40)

ax.scatter(population.demo2, population.demo, c=yellow, s=20)
[15]:
<matplotlib.collections.PathCollection at 0x7f33439c6c70>
../_images/notebooks_aux_26_1.png

Derived Luminosity sampler

Sometimes, the luminosity does not come directly from a distribution. Rather, it is computed from other quantities. In these cases, we want to use the DerivedLumAuxSampler class.

This allows you to sample auxiliary parameters and compute a luminosity from those.

[16]:
class DemoSampler3(popsynth.DerivedLumAuxSampler):
    _auxiliary_sampler_name = "DemoSampler3"
    mu = popsynth.auxiliary_sampler.AuxiliaryParameter(default=1)
    tau = popsynth.auxiliary_sampler.AuxiliaryParameter(default=1, vmin=0)

    def __init__(self, mu=2, tau=1.0, sigma=1):

        # this time set observed=True
        super(DemoSampler3, self).__init__("demo3", uses_distance=False)

    def true_sampler(self, size):

        # draw a random number
        tmp = np.random.normal(self.mu, self.tau, size=size)

        self._true_values = tmp

    def compute_luminosity(self):

        # compute the luminosity
        secondary = self._secondary_samplers["demo"]

        return (10 ** (self._true_values + 54)) + secondary.true_values
[17]:
pop_gen = popsynth.populations.SchechterSFRPopulation(
    r0=100,a=0.0157, rise=1.0, decay=1.0, peak=1.0, Lmin=1e50, alpha=2.0
)
[18]:
demo1 = DemoSampler()


demo3 = DemoSampler3()

demo3.set_secondary_sampler(demo1)

# attach to the base sampler
pop_gen.add_observed_quantity(demo3)


pos = nx.drawing.nx_agraph.graphviz_layout(pop_gen.graph, prog="dot")

fig, ax = plt.subplots()

nx.draw(pop_gen.graph, with_labels=True, pos=pos, **options, ax=ax)
 INFO     |  registering derived luminosity sampler: demo3 
../_images/notebooks_aux_30_1.png
[19]:
flux_selector = popsynth.HardFluxSelection()
flux_selector.boundary = 1e-5
pop_gen.set_flux_selection(flux_selector)
population = pop_gen.draw_survey(flux_sigma=0.1)
 INFO     |  The volume integral is 4760.754853877658 
 INFO     |  Expecting 4693 total objects 
 INFO     |  Sampling: demo3 
 INFO     |  demo3 is sampling its secondary quantities 
 INFO     |  Sampling: demo 
 INFO     |  Getting luminosity from derived sampler 
 INFO     |  applying selection to fluxes 
 INFO     |  Detected 3752 distances 
 INFO     |  Detected 3752 objects out to a distance of 9.99 
[20]:
population.display_fluxes(obs_color=purple, true_color=yellow, s=15)
[20]:
../_images/notebooks_aux_32_0.png
../_images/notebooks_aux_32_1.png