def test_except_inherit(self): """Don't re-use an inherited parameter""" src1 = Source([]) src1.set_expression('a') src2 = Source([], shapeof=src1) self.assertRaises(ValueError, src2.set_expression, 'a') self.assertRaises(ValueError, src2.set_expression, 'a', ['a'], ['1']) self.assertRaises(ValueError, src2.set_expression, 'a*b', ['a', 'b'], ['b', 'a'])
def test_zero(self): builder = SpecBuilder('SpectrumZero') sig = [10., 11.] src_sig = Source(sig) src_sig.use_stats(.5 * (2 * np.array(sig))**0.5) src_sig.set_expression('lumi*xsec_sig', ['lumi', 'xsec_sig'], ['xsec_sig', 'lumi']) builder.add_source(src_sig) builder.set_prior('xsec_sig', 1, 0.9, 1.2, 'normal') builder.set_prior('lumi', 1, 0.95, 1.05, 'lognormal') sig_syst1 = [-5, 0] src_sig_syst1_up = Source(sig_syst1, shapeof=src_sig) src_sig_syst1_up.set_expression('syst1', polarity='up') builder.add_source(src_sig_syst1_up) builder.set_prior('syst1', 0, -1, 1, 'normal') spec = builder.build() pars = list(spec.central) data = spec(pars) isyst = spec.ipar('syst1') pars[isyst] = 2 # ensure syst made bin go to zero self.assertAlmostEqual(spec(pars)[0], 0) # ensure not NaN (0 data so bin is ignored) self.assertTrue(spec.ll(pars) == spec.ll(pars)) # try again with negative bin value pars[isyst] = 3 self.assertAlmostEqual(spec(pars)[0], -5) self.assertTrue(spec.ll(pars) == spec.ll(pars)) # now set the data and check that ll goes to -inf spec.set_data(data) # check also grads, so need memory arrays pars = np.array(pars, dtype=np.float64) grads = pars * 0 pars[isyst] = 2 self.assertEqual(spec.ll(pars), float('-inf')) spec._obj.Gradient(pars, grads) self.assertEqual(grads[isyst], float('inf')) pars[isyst] = 3 self.assertEqual(spec.ll(pars), float('-inf')) spec._obj.Gradient(pars, grads) self.assertEqual(grads[isyst], float('inf'))
def test_except_polarity(self): """Reject bad polarity values""" src = Source([]) self.assertRaises(ValueError, src.set_expression, 'a', polarity='invalid')
def test_expression(self): """Set an expression, parameters and gradients""" src = Source([]) src.set_expression('a*b*b', ['a', 'b'], ['b*b', '2*a*b']) self.assertEqual(['a', 'b'], src._pars) self.assertEqual(['b*b', '2*a*b'], src._grads) # Should convert numerical gradients src = Source([]) src.set_expression('a', ['a'], [1]) self.assertEqual(['1'], src._grads)
def test_except_infer_pars(self): """Try to infer bad expression""" src = Source([]) self.assertRaises(RuntimeError, src.set_expression, 'a+a') self.assertRaises(RuntimeError, src.set_expression, '2*a') self.assertRaises(ValueError, src.set_expression, '2*a', ['a']) self.assertRaises(ValueError, src.set_expression, '2*a', grads=['2']) self.assertRaises(ValueError, src.set_expression, 'a*b', ['a', 'b'], ['b'])
def test_infer(self): """Infer parameters and gradients from expression""" src = Source([]) src.set_expression('a') self.assertEqual(['a'], src._pars) self.assertEqual(['1'], src._grads) src = Source([]) src.set_expression('a+b') self.assertEqual(['a', 'b'], src._pars) self.assertEqual(['1', '1'], src._grads)
def test_inherit(self): """Test inheriting from parent sources""" # Setup a source which inherits from two others src1 = Source([]) src1.set_expression('a+b') src2 = Source([], shapeof=src1) src2.set_expression('5*c*c', ['c'], ['10*c']) src3 = Source([], shapeof=src2) src3.set_expression('d+e') # Check the correct compound expression self.assertEqual('((a+b) * (5*c*c)) * (d+e)', src3._expr) # Ensure paramters correctly ammended self.assertEqual(['a', 'b', 'c', 'd', 'e'], src3._pars) # Check that the gradients are correctly propagated self.assertEqual('((1) * (5*c*c)) * (d+e)', src3._grads[0]) self.assertEqual('((1) * (5*c*c)) * (d+e)', src3._grads[1]) self.assertEqual('((10*c) * (a+b)) * (d+e)', src3._grads[2]) self.assertEqual('(1) * ((a+b) * (5*c*c))', src3._grads[3]) self.assertEqual('(1) * ((a+b) * (5*c*c))', src3._grads[4])
def setUpClass(cls): """Setup a single spectrum object for all tests""" # Builder accumulates data and builds the spectrum builder = SpecBuilder('Spectrum') ### Add a signal ### # Add a trinagular signal sig = [1000., 1100., 1200., 1100., 1000.] src_sig = Source(sig) # Indicate the bin contents in sig are subject to statistical # uncertainty, based on double the count (as if 2x MC was generated # then scaled down by 0.5) src_sig.use_stats(.5 * (2 * np.array(sig))**0.5) # Allow its scale to vary src_sig.set_expression( 'lumi*xsec_sig', # scale factor ['lumi', 'xsec_sig'], # parameters are lumi and xsec ['xsec_sig', 'lumi']) # dn/dlumi and dn/dxsec # Add to builder once configured builder.add_source(src_sig) # Constrain xsec with an asymmeric prior builder.set_prior('xsec_sig', 1, 0.9, 1.2, 'normal') # Constrain lumi with 5% uncertainty builder.set_prior('lumi', 1, 0.95, 1.05, 'lognormal') ### Add two systematic uncertinaties ### # Add systematic shape variation (a top hat) sig_syst1 = [0, 50, 50, 50, 0] # This is a shape which inherits the normalization from the signal src_sig_syst1_up = Source(sig_syst1, shapeof=src_sig) # Assume 1:1 statistical uncertainty on this shape src_sig_syst1_up.use_stats(np.array(sig_syst1)**0.5) # Control the amount of this variation with the parameter syst1, and # indicate that the shape applies only if syst1 >= 0. Note that # parameter list and gradients can be omitted for simple sums src_sig_syst1_up.set_expression('syst1', polarity='up') # Make syst1 fully asymmetric: it has the same effect on the spectrum # when the parameter is positive as negative src_sig_syst1_down = Source(sig_syst1, shapeof=src_sig) src_sig_syst1_down.set_expression('syst1', polarity='down') builder.add_source(src_sig_syst1_up) builder.add_source(src_sig_syst1_down) # 1 sigma penality when this parameter gets to values +/- 1 builder.set_prior('syst1', 0, -1, 1, 'normal') # Add a linear systematic variant sig_syst2 = [-100, -50, 0, 50, 100] src_sig_syst2 = Source(sig_syst2, shapeof=src_sig) # This one is symmetrized: the value of syst2 simply scales src_sig_syst2.set_expression('syst2') builder.add_source(src_sig_syst2) builder.set_prior('syst2', 0, -1, 1, 'normal') ### Add a template (the parameter of interest) ### # Add shape to th3 signal, but won't be constrained sig_temp1 = [0, 0, 10, 100, 0] src_poi = Source(sig_temp1, shapeof=src_sig) # The parameter of interest is called p, and scales the template by # a factof of 5 src_poi.set_expression('5*p', ['p'], ['5']) builder.add_source(src_poi) ### Add a background ### bg = [110, 100, 100, 100, 105] src_bg = Source(bg) src_bg.set_expression('lumi*xsec_bg', ['lumi', 'xsec_bg'], ['xsec_bg', 'lumi']) builder.add_source(src_bg) builder.set_prior('xsec_bg', 1, 0.9, 1.1, 'normal') ### Share one of the systematics with the background ### bg_syst2 = [10, 20, 10, 20, 10] src_bg_syst2 = Source(bg_syst2, shapeof=src_bg) src_bg_syst2.set_expression('syst2') builder.add_source(src_bg_syst2) # Note that this parameter is already constrained ### Add a custom regularization for the free parameter ### builder.add_regularization('std::pow(p-syst1, 2)', ['p', 'syst1'], ['2*(p-syst1)', '-2*(p-syst1)']) # Store the builder so that tests can use it or its contents cls.builder = builder cls.spec = builder.build()
def test_except_reset(self): """Don't allow re-setting expression""" src = Source([]) src.set_expression('a') self.assertRaises(RuntimeError, src.set_expression, 'a')
def test_except_par_name(self): """Reject bad parameter names""" src = Source([]) self.assertRaises(ValueError, src.set_expression, '_a', ['_a'], ['1']) self.assertRaises(ValueError, src.set_expression, '1a', ['1a'], ['1'])
def setUpClass(cls): """Setup a single spectrum object for all tests""" # Builder accumulates data and builds the spectrum builder = SpecBuilder('Spectrum') ### Add a signal ### # Add a trinagular signal sig = [1000., 1100., 1200., 1100., 1000.] src_sig = Source(sig) # Indicate the bin contents in sig are subject to statistical # uncertainty, based on double the count (as if 2x MC was generated # then scaled down by 0.5) src_sig.use_stats(.5*(2*np.array(sig))**0.5) # Allow its scale to vary src_sig.set_expression( 'lumi*xsec_sig', # scale factor ['lumi', 'xsec_sig'], # parameters are lumi and xsec ['xsec_sig', 'lumi']) # dn/dlumi and dn/dxsec # Add to builder once configured builder.add_source(src_sig) # Constrain xsec with an asymmeric prior builder.set_prior('xsec_sig', 1, 0.9, 1.2, 'normal') # Constrain lumi with 5% uncertainty builder.set_prior('lumi', 1, 0.95, 1.05, 'lognormal') ### Add two systematic uncertinaties ### # Add systematic shape variation (a top hat) sig_syst1 = [0, 50, 50 , 50, 0] # This is a shape which inherits the normalization from the signal src_sig_syst1_up = Source(sig_syst1, shapeof=src_sig) # Assume 1:1 statistical uncertainty on this shape src_sig_syst1_up.use_stats(np.array(sig_syst1)**0.5) # Control the amount of this variation with the parameter syst1, and # indicate that the shape applies only if syst1 >= 0. Note that # parameter list and gradients can be omitted for simple sums src_sig_syst1_up.set_expression('syst1', polarity='up') # Make syst1 fully asymmetric: it has the same effect on the spectrum # when the parameter is positive as negative src_sig_syst1_down = Source(sig_syst1, shapeof=src_sig) src_sig_syst1_down.set_expression('syst1', polarity='down') builder.add_source(src_sig_syst1_up) builder.add_source(src_sig_syst1_down) # 1 sigma penality when this parameter gets to values +/- 1 builder.set_prior('syst1', 0, -1, 1, 'normal') # Add a linear systematic variant sig_syst2 = [-100, -50, 0 , 50, 100] src_sig_syst2 = Source(sig_syst2, shapeof=src_sig) # This one is symmetrized: the value of syst2 simply scales src_sig_syst2.set_expression('syst2') builder.add_source(src_sig_syst2) builder.set_prior('syst2', 0, -1, 1, 'normal') ### Add a template (the parameter of interest) ### # Add shape to th3 signal, but won't be constrained sig_temp1 = [0, 0, 10, 100, 0] src_poi = Source(sig_temp1, shapeof=src_sig) # The parameter of interest is called p, and scales the template by # a factof of 5 src_poi.set_expression('5*p', ['p'], ['5']) builder.add_source(src_poi) ### Add a background ### bg = [110, 100, 100, 100, 105] src_bg = Source(bg) src_bg.set_expression( 'lumi*xsec_bg', ['lumi', 'xsec_bg'], ['xsec_bg', 'lumi']) builder.add_source(src_bg) builder.set_prior('xsec_bg', 1, 0.9, 1.1, 'normal') ### Share one of the systematics with the background ### bg_syst2 = [10, 20, 10, 20, 10] src_bg_syst2 = Source(bg_syst2, shapeof=src_bg) src_bg_syst2.set_expression('syst2') builder.add_source(src_bg_syst2) # Note that this parameter is already constrained ### Add a custom regularization for the free parameter ### builder.add_regularization( 'std::pow(p-syst1, 2)', ['p', 'syst1'], ['2*(p-syst1)', '-2*(p-syst1)']) # Store the builder so that tests can use it or its contents cls.builder = builder cls.spec = builder.build()
def test_data(self): """Data is correctly propagated""" src = Source([1, 2, 3]) np.testing.assert_array_almost_equal([1, 2, 3], src._data, 15)