|
4 | 4 |
|
5 | 5 | #include <RooStats/HistFactory/Measurement.h> |
6 | 6 | #include <RooStats/HistFactory/MakeModelAndMeasurementsFast.h> |
| 7 | +#include <RooStats/HistFactory/ConfigParser.h> |
7 | 8 | #include <RooFit/ModelConfig.h> |
8 | 9 |
|
9 | 10 | #include <RooFitHS3/JSONIO.h> |
|
24 | 25 | #include <TROOT.h> |
25 | 26 | #include <TFile.h> |
26 | 27 | #include <TCanvas.h> |
| 28 | +#include <TSystem.h> |
27 | 29 | #include <gtest/gtest.h> |
28 | 30 |
|
29 | 31 | #include "../../roofitcore/test/gtest_wrapper.h" |
@@ -794,3 +796,135 @@ TEST(HistFactory, HS3ImportShapeFactorModifier) |
794 | 796 | const std::string js2 = RooJSONFactoryWSTool{wsFromJson}.exportJSONtoString(); |
795 | 797 | EXPECT_EQ(js, js2) << "JSON -> WS -> JSON roundtrip changed the JSON"; |
796 | 798 | } |
| 799 | + |
| 800 | +// Issue #20697: Sample::AddShapeFactor() now allows to set the initial value |
| 801 | +// and the range of the ShapeFactor gammas (just like AddNormFactor() does for |
| 802 | +// the NormFactors). This is important e.g. for ABCD estimates, where the |
| 803 | +// hard-coded default range can cause convergence problems. |
| 804 | +// |
| 805 | +// These settings need to survive being persisted, so this test checks that the |
| 806 | +// value and range make it through: |
| 807 | +// 1. a ROOT file round trip (Measurement::writeToFile), |
| 808 | +// 2. an XML file round trip (Measurement::PrintXML / ConfigParser), and |
| 809 | +// 3. into the actual gamma parameters of the generated workspace. |
| 810 | +TEST(HistFactory, ShapeFactorValueAndRange) |
| 811 | +{ |
| 812 | + using namespace RooStats::HistFactory; |
| 813 | + RooHelpers::LocalChangeMsgLevel changeMsgLvl(RooFit::WARNING); |
| 814 | + |
| 815 | + // Deliberately use non-default values and a range that differs from the |
| 816 | + // hard-coded default of [0, 1000]. |
| 817 | + const double sfVal = 2.0; |
| 818 | + const double sfLow = 0.1; |
| 819 | + const double sfHigh = 12.0; |
| 820 | + |
| 821 | + const std::string inputFileName = "TestShapeFactorRange_input.root"; |
| 822 | + { |
| 823 | + TFile f(inputFileName.c_str(), "RECREATE"); |
| 824 | + auto *data = new TH1D("data", "data", 2, 1, 2); |
| 825 | + auto *signal = new TH1D("signal", "signal", 2, 1, 2); |
| 826 | + auto *bkg = new TH1D("background", "background", 2, 1, 2); |
| 827 | + data->SetBinContent(1, 220); |
| 828 | + data->SetBinContent(2, 230); |
| 829 | + signal->SetBinContent(1, 10); |
| 830 | + signal->SetBinContent(2, 20); |
| 831 | + bkg->SetBinContent(1, 200); |
| 832 | + bkg->SetBinContent(2, 200); |
| 833 | + for (auto *h : {data, signal, bkg}) |
| 834 | + f.WriteTObject(h); |
| 835 | + } |
| 836 | + |
| 837 | + auto makeMeasurement = [&]() { |
| 838 | + Measurement meas("meas", "meas"); |
| 839 | + meas.SetOutputFilePrefix("TestShapeFactorRange"); |
| 840 | + meas.SetPOI("SigXsecOverSM"); |
| 841 | + meas.AddConstantParam("Lumi"); |
| 842 | + meas.SetLumi(1.0); |
| 843 | + meas.SetLumiRelErr(0.10); |
| 844 | + |
| 845 | + Channel chan("channel1"); |
| 846 | + chan.SetData("data", inputFileName); |
| 847 | + |
| 848 | + Sample sig("signal", "signal", inputFileName); |
| 849 | + sig.AddNormFactor("SigXsecOverSM", 1, 0, 3); |
| 850 | + chan.AddSample(sig); |
| 851 | + |
| 852 | + // The new overload under test: ShapeFactor with custom value and range. |
| 853 | + Sample bkg("background", "background", inputFileName); |
| 854 | + bkg.AddShapeFactor("bkgShape", sfVal, sfLow, sfHigh); |
| 855 | + chan.AddSample(bkg); |
| 856 | + |
| 857 | + meas.AddChannel(chan); |
| 858 | + meas.CollectHistograms(); |
| 859 | + return meas; |
| 860 | + }; |
| 861 | + |
| 862 | + // Fetch the (single) ShapeFactor stored in a measurement. |
| 863 | + auto getShapeFactor = [](Measurement &meas) -> ShapeFactor & { |
| 864 | + Channel &chan = meas.GetChannel("channel1"); |
| 865 | + for (Sample &sample : chan.GetSamples()) { |
| 866 | + if (!sample.GetShapeFactorList().empty()) |
| 867 | + return sample.GetShapeFactorList().front(); |
| 868 | + } |
| 869 | + throw std::runtime_error("ShapeFactor not found in measurement"); |
| 870 | + }; |
| 871 | + |
| 872 | + auto checkShapeFactor = [&](Measurement &meas, const char *context) { |
| 873 | + ShapeFactor &sf = getShapeFactor(meas); |
| 874 | + EXPECT_DOUBLE_EQ(sf.GetVal(), sfVal) << context; |
| 875 | + EXPECT_DOUBLE_EQ(sf.GetLow(), sfLow) << context; |
| 876 | + EXPECT_DOUBLE_EQ(sf.GetHigh(), sfHigh) << context; |
| 877 | + }; |
| 878 | + |
| 879 | + // 0. Sanity check on the in-memory measurement. |
| 880 | + { |
| 881 | + Measurement meas = makeMeasurement(); |
| 882 | + checkShapeFactor(meas, "in-memory measurement"); |
| 883 | + } |
| 884 | + |
| 885 | + // 1. ROOT file round trip. |
| 886 | + { |
| 887 | + Measurement meas = makeMeasurement(); |
| 888 | + const std::string rootFileName = "TestShapeFactorRange_meas.root"; |
| 889 | + { |
| 890 | + TFile outFile(rootFileName.c_str(), "RECREATE"); |
| 891 | + meas.writeToFile(&outFile); |
| 892 | + } |
| 893 | + TFile inFile(rootFileName.c_str(), "READ"); |
| 894 | + std::unique_ptr<Measurement> measFromFile{inFile.Get<Measurement>("meas")}; |
| 895 | + ASSERT_NE(measFromFile, nullptr); |
| 896 | + checkShapeFactor(*measFromFile, "ROOT file round trip"); |
| 897 | + } |
| 898 | + |
| 899 | + // 2. XML file round trip. |
| 900 | + { |
| 901 | + Measurement meas = makeMeasurement(); |
| 902 | + const std::string xmlDir = "TestShapeFactorRangeXML"; |
| 903 | + meas.PrintXML(xmlDir); |
| 904 | + |
| 905 | + // The generated XML files refer to the DTD by relative path, so it has to |
| 906 | + // be available next to them for the validating parser to find it. |
| 907 | + gSystem->CopyFile(TString::Format("%s/HistFactorySchema.dtd", TROOT::GetEtcDir().Data()), |
| 908 | + TString::Format("%s/HistFactorySchema.dtd", xmlDir.c_str()), true); |
| 909 | + |
| 910 | + ConfigParser parser; |
| 911 | + std::vector<Measurement> measFromXML = parser.GetMeasurementsFromXML(xmlDir + "/meas.xml"); |
| 912 | + ASSERT_EQ(measFromXML.size(), 1u); |
| 913 | + checkShapeFactor(measFromXML.front(), "XML file round trip"); |
| 914 | + } |
| 915 | + |
| 916 | + // 3. End to end: the gamma parameters of the workspace pick up the requested |
| 917 | + // value and range. |
| 918 | + { |
| 919 | + Measurement meas = makeMeasurement(); |
| 920 | + std::unique_ptr<RooWorkspace> ws{MakeModelAndMeasurementFast(meas)}; |
| 921 | + ASSERT_NE(ws, nullptr); |
| 922 | + for (const char *name : {"gamma_bkgShape_bin_0", "gamma_bkgShape_bin_1"}) { |
| 923 | + auto *gamma = ws->var(name); |
| 924 | + ASSERT_NE(gamma, nullptr) << name; |
| 925 | + EXPECT_DOUBLE_EQ(gamma->getVal(), sfVal) << name; |
| 926 | + EXPECT_DOUBLE_EQ(gamma->getMin(), sfLow) << name; |
| 927 | + EXPECT_DOUBLE_EQ(gamma->getMax(), sfHigh) << name; |
| 928 | + } |
| 929 | + } |
| 930 | +} |
0 commit comments