scikit-hep / pyhf

pure-Python HistFactory implementation with tensors and autodiff
https://pyhf.readthedocs.io/
Apache License 2.0
276 stars 82 forks source link

How to manually override the yields inputs to the model spec #1894

Open phinate opened 2 years ago

phinate commented 2 years ago

Summary

As discussed briefly on other channels, the number one thing preventing neos from being used on a realistic use-case is the technical hurdle that comes with differentiating through pyhf model construction (#882).

A recently talked about way to circumvent this issue would be to skip the model construction entirely, and just modify the spec information in-place for a given model. I've tested that this works on a technical level, and it's super fast. However, there are problems with validating the likelihood shape for the modified model, i.e. not all information is propagating in the correct way.

Here's an example of hand-altering the nominal rates of a model to match those of a different model, but their likelihoods don't match:

import pyhf

pyhf.set_backend("jax")

def make_model(s, b, bup, bdown):
    return pyhf.simplemodels.correlated_background(s, b, bup, bdown)

s = [1,2,3]
b = [21,20,22]

bup = [22,22,24]
bdown = [20, 18, 20]

# some target model
model1 = make_model(s, b, bup, bdown)

# a skeleton model with different yields, but the same up/down variations
model2 = make_model([0,0,0], [0,0,0], bup, bdown)

# simply set the yields of the old model to the new one
model2.main_model.nominal_rates = model1.main_model.nominal_rates

# Plotting:
import numpy as np
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt

plt.rc("figure", figsize=(6, 2), dpi=200, facecolor="w")

data = [23, 23, 23] + model1.config.auxdata

def plot_model(model, ax, model_name=None):
    grid = (500, 500)
    x_range = np.linspace(0,10,grid[0])
    y_range = np.linspace(0,10,grid[1]) 
    xx, yy = np.meshgrid(x_range, y_range)
    xy = np.concatenate([xx.reshape(-1, 1), yy.reshape(-1, 1)], axis=1)
    @jax.vmap
    def logpdf(pars):
        return model.logpdf(pars, data)[0]
    z = logpdf(jnp.array(xy)).reshape(*grid)
    ax.contourf(xx, yy, z)
    if model_name is not None:
        ax.set_title(model_name)

def plot_difference(model1, model2, ax):
    grid = (500, 500)
    x_range = np.linspace(0,10,grid[0])
    y_range = np.linspace(0,10,grid[1]) 
    xx, yy = np.meshgrid(x_range, y_range)
    xy = np.concatenate([xx.reshape(-1, 1), yy.reshape(-1, 1)], axis=1)
    zs = []
    for model in [model1, model2]:
        @jax.vmap
        def logpdf(pars):
            return model.logpdf(pars, data)[0]
        zs.append(logpdf(jnp.array(xy)).reshape(*grid))
    diffs = zs[1] - zs[0]
    ax.contourf(xx, yy, diffs)
    ax.set_title('differences')

fig, axs = plt.subplots(1, 3)

plot_model(model1, axs[0], 'original model')
plot_model(model2, axs[1], 'manually set yields')
plot_difference(model1, model2, axs[2])
plt.tight_layout()

image

Additional Information

No response

Code of Conduct

phinate commented 2 years ago

For archival purposes: here's an example of a model with a histosys being succsessfully hacked to mimic another model :)

import pyhf

from math import prod
import numpy as np
from copy import deepcopy
import matplotlib.pyplot as plt
from pyhf import interpolators
pyhf.set_backend("numpy")
grid = [500, 500]
def make_model(s, b, bup, bdown):
    return pyhf.simplemodels.correlated_background(s, b, bup, bdown, batch_size=prod(grid))

s = [1,2,3]
b = [21,20,22]

bup = [22,22,24]
bdown = [20, 18, 30]

model1 = make_model(s, b, bup, bdown)
dummy = [0,0,0]
model2 = make_model(dummy, dummy, dummy, dummy)

histoset = deepcopy(model1.main_model.modifiers_appliers["histosys"]._histosys_histoset)
interpcode = model1.main_model.modifiers_appliers["histosys"].interpcode
model2.main_model.nominal_rates = deepcopy(model1.main_model.nominal_rates)
interpolator = getattr(interpolators, interpcode)(histoset)
model2.main_model.modifiers_appliers["histosys"].interpolator = interpolator

# matplotlib settings
plt.rc("figure", figsize=(6, 2), dpi=200, facecolor="w")

data = [23, 23, 23] + model1.config.auxdata

def plot_model(model, ax, model_name=None):
    grid = (500, 500)
    x_range = np.linspace(0,10,grid[0])
    y_range = np.linspace(0,10,grid[1]) 
    xx, yy = np.meshgrid(x_range, y_range)
    xy = np.concatenate([xx.reshape(-1, 1), yy.reshape(-1, 1)], axis=1)
    def logpdf(pars):
        return model.logpdf(pars, data)
    z = logpdf(xy).reshape(*grid)
    ax.contourf(xx, yy, z)
    if model_name is not None:
        ax.set_title(model_name)

def plot_difference(model1, model2, ax):

    x_range = np.linspace(0,10,grid[0])
    y_range = np.linspace(0,10,grid[1]) 
    xx, yy = np.meshgrid(x_range, y_range)
    xy = np.concatenate([xx.reshape(-1, 1), yy.reshape(-1, 1)], axis=1)
    zs = []
    for model in [model1, model2]:
        def logpdf(pars):
            return model.logpdf(pars, data)
        zs.append(logpdf(xy).reshape(*grid))
    diffs = zs[1] - zs[0]
    ax.contourf(xx, yy, diffs)
    ax.set_title('differences')

fig, axs = plt.subplots(1, 3)

plot_model(model1, axs[0], 'original model')
plot_model(model2, axs[2], 'dummy model w/ set yields')
plot_difference(model1, model2, axs[1])
plt.tight_layout()

image