diff --git a/roofit/hs3/src/JSONFactories_RooFitCore.cxx b/roofit/hs3/src/JSONFactories_RooFitCore.cxx index d2b27f0a03389..24673f9084d69 100644 --- a/roofit/hs3/src/JSONFactories_RooFitCore.cxx +++ b/roofit/hs3/src/JSONFactories_RooFitCore.cxx @@ -1146,7 +1146,11 @@ bool importWrapperPdf(RooJSONFactoryWSTool *tool, const JSONNode &node) bool selfNormalized = false; - if (auto sn = node.find("selfNormalized")) + auto sn = node.find("self_normalized"); + // ROOT previously exported this key without an underscore. + if (!sn) + sn = node.find("selfnormalized"); + if (sn) selfNormalized = sn->val_bool(); tool->wsEmplace(name, *func, selfNormalized); @@ -1169,7 +1173,7 @@ bool exportWrapperPdf(RooJSONFactoryWSTool *, const RooAbsArg *arg, JSONNode &no node["function"] << funcProxy->absArg()->GetName(); if (pdf->selfNormalized()) - node["selfnormalized"] << true; + node["self_normalized"] << true; return true; } diff --git a/roofit/hs3/test/testRooFitHS3.cxx b/roofit/hs3/test/testRooFitHS3.cxx index 010528d3b238f..4abb6b1e43e61 100644 --- a/roofit/hs3/test/testRooFitHS3.cxx +++ b/roofit/hs3/test/testRooFitHS3.cxx @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -231,6 +232,17 @@ std::size_t countOccurrences(std::string_view haystack, std::string_view needle) return result; } +std::string makeWrapperPdfJson(bool selfNormalized) +{ + RooRealVar x{"x", "x", 0.5, 0.0, 1.0}; + RooFormulaVar function{"function", "1.0 + 0.0 * x", RooArgList{x}}; + RooWrapperPdf pdf{"pdf", "pdf", function, selfNormalized}; + + RooWorkspace workspace; + workspace.import(pdf, RooFit::Silence()); + return RooJSONFactoryWSTool{workspace}.exportJSONtoString(); +} + // Asserts that exporting `ws` to HS3 throws and logs an error message containing `expectedReason`. void expectExportThrowsWithError(RooWorkspace &ws, std::string const &expectedReason) { @@ -821,6 +833,74 @@ TEST(RooFitHS3, RooGenericPdf) EXPECT_EQ(status, 0); } +TEST(RooFitHS3, RooWrapperPdfSelfNormalizedRoundTrip) +{ + const std::string json = makeWrapperPdfJson(true); + EXPECT_NE(json.find("\"self_normalized\":true"), std::string::npos) << json; + EXPECT_EQ(json.find("\"selfnormalized\""), std::string::npos) << json; + EXPECT_EQ(json.find("\"selfNormalized\""), std::string::npos) << json; + + RooWorkspace imported; + ASSERT_TRUE(RooJSONFactoryWSTool{imported}.importJSONfromString(json)); + auto *pdf = dynamic_cast(imported.pdf("pdf")); + ASSERT_NE(pdf, nullptr); + EXPECT_TRUE(pdf->selfNormalized()); + + const std::string defaultJson = makeWrapperPdfJson(false); + EXPECT_EQ(defaultJson.find("self_normalized"), std::string::npos) << defaultJson; + + RooWorkspace importedDefault; + ASSERT_TRUE(RooJSONFactoryWSTool{importedDefault}.importJSONfromString(defaultJson)); + auto *defaultPdf = dynamic_cast(importedDefault.pdf("pdf")); + ASSERT_NE(defaultPdf, nullptr); + EXPECT_FALSE(defaultPdf->selfNormalized()); +} + +TEST(RooFitHS3, RooWrapperPdfSelfNormalizedLegacyCompatibility) +{ + std::string legacyJson = makeWrapperPdfJson(true); + const auto canonicalPos = legacyJson.find("self_normalized"); + ASSERT_NE(canonicalPos, std::string::npos) << legacyJson; + legacyJson.replace(canonicalPos, std::string{"self_normalized"}.size(), "selfnormalized"); + + RooWorkspace imported; + ASSERT_TRUE(RooJSONFactoryWSTool{imported}.importJSONfromString(legacyJson)); + auto *pdf = dynamic_cast(imported.pdf("pdf")); + ASSERT_NE(pdf, nullptr); + EXPECT_TRUE(pdf->selfNormalized()); + + const std::string canonicalJson = RooJSONFactoryWSTool{imported}.exportJSONtoString(); + EXPECT_NE(canonicalJson.find("\"self_normalized\":true"), std::string::npos) << canonicalJson; + EXPECT_EQ(canonicalJson.find("\"selfnormalized\""), std::string::npos) << canonicalJson; +} + +TEST(RooFitHS3, RooWrapperPdfSelfNormalizedCanonicalKeyTakesPrecedence) +{ + std::string json = makeWrapperPdfJson(true); + const std::string canonicalField = "\"self_normalized\":true"; + const auto canonicalPos = json.find(canonicalField); + ASSERT_NE(canonicalPos, std::string::npos) << json; + json.replace(canonicalPos, canonicalField.size(), "\"self_normalized\":false,\"selfnormalized\":true"); + + RooWorkspace imported; + ASSERT_TRUE(RooJSONFactoryWSTool{imported}.importJSONfromString(json)); + auto *pdf = dynamic_cast(imported.pdf("pdf")); + ASSERT_NE(pdf, nullptr); + EXPECT_FALSE(pdf->selfNormalized()); +} + +TEST(RooFitHS3, RooWrapperPdfSelfNormalizedCamelCaseKeyIsIgnored) +{ + std::string json = makeWrapperPdfJson(true); + const auto canonicalPos = json.find("self_normalized"); + ASSERT_NE(canonicalPos, std::string::npos) << json; + json.replace(canonicalPos, std::string{"self_normalized"}.size(), "selfNormalized"); + + RooWorkspace imported; + ASSERT_TRUE(RooJSONFactoryWSTool{imported}.importJSONfromString(json)); + auto *pdf = dynamic_cast(imported.pdf("pdf")); + ASSERT_NE(pdf, nullptr); + EXPECT_FALSE(pdf->selfNormalized()); TEST(RooFitHS3, BinnedGenericPdfUniformRoundTrip) { RooRealVar x{"x", "x", 0.0, 10.0};