Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
543 changes: 517 additions & 26 deletions roofit/histfactory/test/testHistFactory.cxx

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions roofit/hs3/inc/RooFitHS3/RooJSONFactoryWSTool.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class RooJSONFactoryWSTool {
bool allowExportInvalidNames = true;
bool allowSanitizeNames = true;
bool importNoDomainParametersAsRooConstVars = true;
/// Identify constant factors of HistFactory samples that are used like the inverse bin width of HistFactory
/// before ROOT 6.26 by their structure, and don't write them, since `histfactory_dist` implies them. Turn this
/// off if a workspace has such factors that are not meant as bin widths: they are then written as normfactors.
bool identifyLegacyBinWidthFactors = true;
};

static Config &config();
Expand Down
21 changes: 13 additions & 8 deletions roofit/hs3/src/Domains.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ constexpr static auto defaultDomainName = "default_domain";

namespace {

void writeBound(RooFit::Detail::JSONNode &node, double value)
// JSON can't represent infinity, so an infinite bound is expressed by omitting it. An absent bound is read as
// infinite by readBound() in JSONIOUtils.
void writeBound(RooFit::Detail::JSONNode &node, const char *key, double value)
{
if (RooNumber::isInfinite(value)) {
node.set_null();
} else {
node << value;
if (!RooNumber::isInfinite(value)) {
node[key] << value;
}
}

Expand Down Expand Up @@ -259,9 +259,14 @@ void Domains::ProductDomain::writeJSON(RooFit::Detail::JSONNode &node) const
for (auto const &item : _map) {
auto const &elem = item.second;
RooFit::Detail::JSONNode &varnode = RooJSONFactoryWSTool::appendNamedChild(variablesNode, item.first);
writeBound(varnode["min"], elem.hasMin ? elem.min : -RooNumber::infinity());
writeBound(varnode["max"], elem.hasMax ? elem.max : RooNumber::infinity());
writeBinning(varnode, elem);
const double min = elem.hasMin ? elem.min : -RooNumber::infinity();
const double max = elem.hasMax ? elem.max : RooNumber::infinity();
writeBound(varnode, "min", min);
writeBound(varnode, "max", max);
// A binning requires a bounded range.
if (!RooNumber::isInfinite(min) && !RooNumber::isInfinite(max)) {
writeBinning(varnode, elem);
}
}
}
void Domains::ProductDomain::populate(RooWorkspace &ws) const
Expand Down
406 changes: 314 additions & 92 deletions roofit/hs3/src/JSONFactories_HistFactory.cxx

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions roofit/hs3/src/JSONFactories_RooFitCore.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include <RooBinning.h>
#include <RooBinSamplingPdf.h>
#include <RooBinWidthFunction.h>
#include <RooConstVar.h>
#include <RooCrystalBall.h>
#include <RooDataHist.h>
#include <RooDecay.h>
#include <RooDerivative.h>
Expand Down Expand Up @@ -66,7 +68,9 @@
#include <cmath>
#include <limits>
#include <memory>
#include <initializer_list>
#include <set>
#include <sstream>
#include <string_view>
#include <vector>

Expand Down Expand Up @@ -869,9 +873,152 @@ class DeprecatedKeyImporter : public RooFit::JSONIO::Importer {
mutable bool _warned = false;
};

namespace CrystalBall {

constexpr const char *tailKeys[] = {"alpha_L", "n_L", "alpha_R", "n_R"};

bool hasAny(const JSONNode &p, std::initializer_list<const char *> keys)
{
for (const char *key : keys) {
if (p.has_child(key)) {
return true;
}
}
return false;
}

bool hasDoubleSidedKeys(const JSONNode &p)
{
return hasAny(p, {"alpha_L", "n_L", "alpha_R", "n_R", "sigma_L", "sigma_R"});
}

// Create a RooCrystalBall with two tails. With `symmetricShorthand`, the tails may also be given as "alpha" and "n"
// for both sides, as in the "crystalball_dist" of HS3 versions before 0.3.
void importDoubleSided(RooJSONFactoryWSTool *tool, const JSONNode &p, std::string const &type, bool symmetricShorthand)
{
const std::string name(RooJSONFactoryWSTool::name(p));
const std::string context = type + " '" + name + "'";

const bool asymmetricTails = hasAny(p, {"alpha_L", "n_L", "alpha_R", "n_R"});
const bool symmetricTails = hasAny(p, {"alpha", "n"});
if (symmetricTails && !symmetricShorthand) {
RooJSONFactoryWSTool::error(context + " must not have \"alpha\" or \"n\", the tails are given by \"alpha_L\", " +
"\"n_L\", \"alpha_R\" and \"n_R\"");
}
if (symmetricTails && asymmetricTails) {
RooJSONFactoryWSTool::error(context +
" must not combine \"alpha\"/\"n\" with \"alpha_L\"/\"n_L\"/\"alpha_R\"/\"n_R\"");
}
if (asymmetricTails || !symmetricTails) {
for (const char *key : tailKeys) {
if (!p.has_child(key)) {
RooJSONFactoryWSTool::error(context + " is missing \"" + key +
"\": \"alpha_L\", \"n_L\", \"alpha_R\" and \"n_R\" are all required");
}
}
} else if (!p.has_child("alpha") || !p.has_child("n")) {
RooJSONFactoryWSTool::error(context + " needs both \"alpha\" and \"n\"");
}

const bool asymmetricWidths = hasAny(p, {"sigma_L", "sigma_R"});
if (asymmetricWidths && p.has_child("sigma")) {
RooJSONFactoryWSTool::error(context + " must not combine \"sigma\" with \"sigma_L\"/\"sigma_R\"");
}
if (asymmetricWidths && !(p.has_child("sigma_L") && p.has_child("sigma_R"))) {
RooJSONFactoryWSTool::error(context + " needs both \"sigma_L\" and \"sigma_R\"");
}
if (!asymmetricWidths && !p.has_child("sigma")) {
RooJSONFactoryWSTool::error(context + " needs either \"sigma\" or both \"sigma_L\" and \"sigma_R\"");
}

RooAbsReal *m = tool->requestArg<RooAbsReal>(p, "m");
RooAbsReal *m0 = tool->requestArg<RooAbsReal>(p, "m0");
RooAbsReal *sigmaL = tool->requestArg<RooAbsReal>(p, asymmetricWidths ? "sigma_L" : "sigma");
RooAbsReal *sigmaR = tool->requestArg<RooAbsReal>(p, asymmetricWidths ? "sigma_R" : "sigma");
RooAbsReal *alphaL = tool->requestArg<RooAbsReal>(p, symmetricTails ? "alpha" : "alpha_L");
RooAbsReal *nL = tool->requestArg<RooAbsReal>(p, symmetricTails ? "n" : "n_L");
RooAbsReal *alphaR = tool->requestArg<RooAbsReal>(p, symmetricTails ? "alpha" : "alpha_R");
RooAbsReal *nR = tool->requestArg<RooAbsReal>(p, symmetricTails ? "n" : "n_R");
tool->wsEmplace<RooCrystalBall>(name, *m, *m0, *sigmaL, *sigmaR, *alphaL, *nL, *alphaR, *nR);
}

} // namespace CrystalBall

// "crystalball_dist" is the single-sided Crystal Ball, which is left to the factory expression (RooCBShape). Before
// HS3 0.3, it was also the double-sided Crystal Ball, recognizable by the "_L"/"_R" fields. Such documents are still
// read, with a deprecation warning.
class CrystalBallImporter : public RooFit::JSONIO::Importer {
public:
bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
{
if (!CrystalBall::hasDoubleSidedKeys(p)) {
return false;
}
if (!_warned) {
RooJSONFactoryWSTool::warning("RooFitHS3: \"crystalball_dist\" with the fields \"alpha_L\", \"n_L\", "
"\"alpha_R\", \"n_R\", \"sigma_L\" or \"sigma_R\" is deprecated, please use "
"\"crystalball_doublesided_dist\" for the double-sided Crystal Ball.");
_warned = true;
}
CrystalBall::importDoubleSided(tool, p, "crystalball_dist", /*symmetricShorthand=*/true);
return true;
}

private:
mutable bool _warned = false;
};

bool importCrystalBallDoubleSided(RooJSONFactoryWSTool *tool, const JSONNode &p)
{
CrystalBall::importDoubleSided(tool, p, "crystalball_doublesided_dist", /*symmetricShorthand=*/false);
return true;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////
// specialized exporter implementations
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Write a reference to `arg`: the value if it is a literal constant, and the name otherwise.
void writeReference(JSONNode &node, RooAbsArg const &arg)
{
auto const *constVar = dynamic_cast<RooConstVar const *>(&arg);
std::istringstream stream(arg.GetName());
double value = 0.0;
if (constVar && (stream >> value) && stream.eof()) {
node << constVar->getVal();
} else {
node << arg.GetName();
}
}

// A RooCrystalBall with one tail is the single-sided "crystalball_dist" (read back as RooCBShape, which follows the
// same convention for the sign of alpha). With two tails, it is a "crystalball_doublesided_dist", whose tails are
// always written as "alpha_L"/"n_L"/"alpha_R"/"n_R": there is no shorthand for symmetric tails.
bool exportCrystalBall(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem, std::string const &key)
{
auto const *pdf = static_cast<const RooCrystalBall *>(func);
writeReference(elem["m"], pdf->x());
writeReference(elem["m0"], pdf->x0());
if (!pdf->alphaR() || !pdf->nR()) {
elem["type"] << "crystalball_dist";
writeReference(elem["sigma"], pdf->sigmaL());
writeReference(elem["alpha"], pdf->alphaL());
writeReference(elem["n"], pdf->nL());
return true;
}
elem["type"] << key;
if (&pdf->sigmaL() == &pdf->sigmaR()) {
writeReference(elem["sigma"], pdf->sigmaL());
} else {
writeReference(elem["sigma_L"], pdf->sigmaL());
writeReference(elem["sigma_R"], pdf->sigmaR());
}
writeReference(elem["alpha_L"], pdf->alphaL());
writeReference(elem["n_L"], pdf->nL());
writeReference(elem["alpha_R"], *pdf->alphaR());
writeReference(elem["n_R"], *pdf->nR());
return true;
}

template <class RooArg_t>
bool exportAddPdf(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem, std::string const &key)
{
Expand Down Expand Up @@ -1459,6 +1606,8 @@ void registerExporter(TClass const *cl, std::string key, bool topPriority = true
STATIC_EXECUTE([]() {
registerImporter<importWrapperPdf>("density_function_dist");
registerImporter<importExtendPdf>("rate_extended_dist");
RooFit::JSONIO::registerImporter("crystalball_dist", std::make_unique<CrystalBallImporter>());
registerImporter<importCrystalBallDoubleSided>("crystalball_doublesided_dist");
RooFit::JSONIO::registerImporter("ARGUS_dist", std::make_unique<DeprecatedKeyImporter>("ARGUS_dist", "argus_dist"));
RooFit::JSONIO::registerImporter("binsampling",
std::make_unique<DeprecatedKeyImporter>("binsampling", "binsampling_dist"));
Expand Down Expand Up @@ -1500,6 +1649,7 @@ STATIC_EXECUTE([]() {
registerExporter<exportAddPdf<RooAddPdf>>(RooAddPdf::Class(), "mixture_dist", false);
registerExporter<exportAddPdf<RooAddModel>>(RooAddModel::Class(), "mixture_resolution_model", false);
registerExporter<exportBinSamplingPdf>(RooBinSamplingPdf::Class(), "binsampling_dist", false);
registerExporter<exportCrystalBall>(RooCrystalBall::Class(), "crystalball_doublesided_dist", false);
registerExporter<exportBinWidthFunction>(RooBinWidthFunction::Class(), "binvolume", false);
registerExporter<exportPolynomial<RooLegacyExpPoly>>(RooLegacyExpPoly::Class(), "legacy_exp_poly_dist", false);
registerExporter<exportExponential>(RooExponential::Class(), "exponential_dist", false);
Expand Down
13 changes: 0 additions & 13 deletions roofit/hs3/src/RooFitHS3_wsexportkeys.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,6 @@ auto RooFitHS3_wsexportkeys = R"({
"coefList": "coefficients"
}
},
"RooCrystalBall": {
"type": "crystalball_doublesided_dist",
"proxies": {
"alphaL": "alpha_L",
"alphaR": "alpha_R",
"nL": "n_L",
"nR": "n_R",
"x": "m",
"x0": "m0",
"sigmaL": "sigma_L",
"sigmaR": "sigma_R"
}
},
"RooEffProd": {
"type": "efficiency_product_pdf_dist",
"proxies": {
Expand Down
6 changes: 0 additions & 6 deletions roofit/hs3/src/RooFitHS3_wsfactoryexpressions.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,6 @@ auto RooFitHS3_wsfactoryexpressions = R"({
"arguments": [
"x"
]
},
"crystalball_doublesided_dist": {
"class": "RooCrystalBall",
"arguments": [
"m", "m0", "sigma_L", "sigma_R", "alpha_L", "n_L", "alpha_R", "n_R"
]
}
})";

Expand Down
55 changes: 43 additions & 12 deletions roofit/hs3/src/RooJSONFactoryWSTool.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ tool.writedoc("hs3.tex")
~~~
*/

constexpr auto hs3VersionTag = "0.2";
constexpr auto hs3VersionTag = "0.3";

using RooFit::Detail::JSONNode;
using RooFit::Detail::JSONTree;
Expand Down Expand Up @@ -609,15 +609,35 @@ void importAnalysis(const JSONNode &rootnode, const JSONNode &analysisNode, cons
if (!extConstraints.empty())
mc->SetExternalConstraints(extConstraints);

auto readArgSet = [&](std::string const &name) {
RooArgSet out;
for (auto const &child : analysisNode[name].children()) {
out.add(*workspace.arg(child.val()));
// HS3 v0.2.9 used the singular keys "parameter_of_interest" (a list) and "domain" (a single name). They are still
// accepted, but the plural keys take precedence.
auto findAnalysisKey = [&](const char *key, const char *legacyKey) -> JSONNode const * {
if (auto node = analysisNode.find(key)) {
return node;
}
return out;
if (auto node = analysisNode.find(legacyKey)) {
RooJSONFactoryWSTool::warning(std::string("RooFitHS3: the key \"") + legacyKey + "\" of analysis '" +
analysisName + "' is deprecated, please use \"" + key + "\" instead.");
return node;
}
return nullptr;
};
JSONNode const *poiNode = findAnalysisKey("parameters_of_interest", "parameter_of_interest");
JSONNode const *domainsNodeOfAnalysis = findAnalysisKey("domains", "domain");
std::vector<std::string> analysisDomains;
if (domainsNodeOfAnalysis && domainsNodeOfAnalysis->is_seq()) {
analysisDomains = valsToStringVec(*domainsNodeOfAnalysis);
} else if (domainsNodeOfAnalysis) {
analysisDomains.push_back(domainsNodeOfAnalysis->val());
}

mc->SetParametersOfInterest(readArgSet("parameters_of_interest"));
RooArgSet pois;
if (poiNode) {
for (auto const &child : poiNode->children()) {
pois.add(*workspace.arg(child.val()));
}
}
mc->SetParametersOfInterest(pois);
mc->SetObservables(observables);
RooArgSet pars;
pdf->getParameters(&observables, pars);
Expand All @@ -629,8 +649,8 @@ void importAnalysis(const JSONNode &rootnode, const JSONNode &analysisNode, cons
pdf->getAllConstraints(observables, mainPars, /*stripDisconnected*/ true);

RooArgSet domainPars;
for (auto &domain : analysisNode["domains"].children()) {
const auto &thisDomain = RooJSONFactoryWSTool::findNamedChild(domainsNode, domain.val());
for (std::string const &domain : analysisDomains) {
const auto &thisDomain = RooJSONFactoryWSTool::findNamedChild(domainsNode, domain);
if (!thisDomain || !thisDomain->has_child("axes"))
continue;
for (auto &var : (*thisDomain)["axes"].children()) {
Expand Down Expand Up @@ -825,8 +845,10 @@ void checkHS3Version(std::string const &version)
}
}

/// HS3 requires the names of all top-level objects to be unique within a document. Throw if a name is used twice, or
/// only warn if not `strict`: RooFit wrote such documents before HS3 v0.3, e.g. for a range and a pdf of one name.
/// HS3 requires the names of all top-level objects to be unique within a document. If `strict`, throw if a name is
/// used twice within one section or by a distribution and a function. Other clashes, and all clashes if not
/// `strict`, only warn: RooFit's own export still produces them, e.g. for a range and a pdf of one name, and it
/// must be able to read its own output.
void checkUniqueNames(JSONNode const &rootnode, bool strict)
{
std::map<std::string, std::string> sectionOfName;
Expand All @@ -850,7 +872,11 @@ void checkUniqueNames(JSONNode const &rootnode, bool strict)
// compatibility: analyses are not referenced by name, and likelihoods only from analyses, so there is no
// ambiguity.
auto isLegacyNamed = [](std::string const &s) { return s == "likelihoods" || s == "analyses"; };
if (!strict || isLegacyNamed(firstSection) || isLegacyNamed(section)) {
// Within one section, and between distributions and functions (both can be referenced by name), a clash is
// ambiguous. Other clashes are produced by RooFit itself (a range, snapshot or data set named like a pdf).
auto isReferable = [](std::string const &s) { return s == "distributions" || s == "functions"; };
const bool ambiguous = firstSection == section || (isReferable(firstSection) && isReferable(section));
if (!strict || !ambiguous || isLegacyNamed(firstSection) || isLegacyNamed(section)) {
RooJSONFactoryWSTool::warning("RooFitHS3: the name '" + name + "' is used in \"" + firstSection +
"\" and in \"" + section + "\", which is not allowed by the HS3 standard.");
continue;
Expand Down Expand Up @@ -1354,6 +1380,11 @@ void RooJSONFactoryWSTool::exportAxis(JSONNode &obsNode, RooRealVar const &var)
obsNode["name"] << name;

writeAxisBinning(obsNode, var.getBinning());
// Binned axes always carry their range, also if it is redundant with the "edges" of an irregular binning.
if (!obsNode.has_child("min")) {
obsNode["min"] << var.getBinning().lowBound();
obsNode["max"] << var.getBinning().highBound();
}
}

/**
Expand Down
Loading