Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
JSONFactories_HistFactory.cxx
Go to the documentation of this file.
1/*
2 * Project: RooFit
3 * Authors:
4 * Carsten D. Burgard, DESY/ATLAS, Dec 2021
5 *
6 * Copyright (c) 2022, CERN
7 *
8 * Redistribution and use in source and binary forms,
9 * with or without modification, are permitted according to the terms
10 * listed in LICENSE (http://roofit.sourceforge.net/license.txt)
11 */
12
14#include <RooFitHS3/JSONIO.h>
16
21#include <RooConstVar.h>
22#include <RooRealVar.h>
23#include <RooDataHist.h>
24#include <RooHistFunc.h>
25#include <RooRealSumPdf.h>
26#include <RooBinWidthFunction.h>
27#include <RooProdPdf.h>
28#include <RooPoisson.h>
29#include <RooFormulaVar.h>
30#include <RooLognormal.h>
31#include <RooGaussian.h>
32#include <RooProduct.h>
33#include <RooWorkspace.h>
34#include <RooFitImplHelpers.h>
35
36#include <regex>
37
38#include "static_execute.h"
39#include "JSONIOUtils.h"
40
42
43using namespace RooStats::HistFactory;
44using namespace RooStats::HistFactory::Detail;
46
47namespace {
48
49double round_prec(double d, int nSig)
50{
51 if (d == 0.0)
52 return 0.0;
53 int ndigits = std::floor(std::log10(std::abs(d))) + 1 - nSig;
54 double sf = std::pow(10, ndigits);
55 if (std::abs(d / sf) < 2)
56 ndigits--;
57 return sf * std::round(d / sf);
58}
59
60// To avoid repeating the same string literals that can potentially get out of
61// sync.
62namespace Literals {
63constexpr auto staterror = "staterror";
64}
65
66void erasePrefix(std::string &str, std::string_view prefix)
67{
68 if (startsWith(str, prefix)) {
69 str.erase(0, prefix.size());
70 }
71}
72
73bool eraseSuffix(std::string &str, std::string_view suffix)
74{
75 if (endsWith(str, suffix)) {
76 str.erase(str.size() - suffix.size());
77 return true;
78 } else {
79 return false;
80 }
81}
82
83template <class Coll>
84void sortByName(Coll &coll)
85{
86 std::sort(coll.begin(), coll.end(), [](auto &l, auto &r) { return l.name < r.name; });
87}
88
89template <class T>
90T *findClient(RooAbsArg *gamma)
91{
92 for (const auto &client : gamma->clients()) {
93 if (auto casted = dynamic_cast<T *>(client)) {
94 return casted;
95 } else {
96 T *c = findClient<T>(client);
97 if (c)
98 return c;
99 }
100 }
101 return nullptr;
102}
103
105{
106 if (!g)
107 return nullptr;
109 if (constraint_p)
110 return constraint_p;
112 if (constraint_g)
113 return constraint_g;
115 if (constraint_l)
116 return constraint_l;
117 return nullptr;
118}
119
120inline std::string defaultGammaName(std::string const &sysname, std::size_t i)
121{
122 return "gamma_" + sysname + "_bin_" + std::to_string(i);
123}
124
125/// Export the names of the gamma parameters to the modifier struct if the
126/// names don't match the default gamma parameter names, which is gamma_<sysname>_bin_<i>
127void optionallyExportGammaParameters(JSONNode &mod, std::string const &sysname, std::vector<RooAbsReal *> const &params,
128 bool forceExport = true)
129{
130 std::vector<std::string> paramNames;
131 bool needExport = forceExport;
132 for (std::size_t i = 0; i < params.size(); ++i) {
133 std::string name(params[i]->GetName());
134 paramNames.push_back(name);
135 if (name != defaultGammaName(sysname, i)) {
136 needExport = true;
137 }
138 }
139 if (needExport) {
140 mod["parameters"].fill_seq(paramNames);
141 }
142}
143
144RooRealVar &createNominal(RooWorkspace &ws, std::string const &parname, double val, double min, double max)
145{
146 RooRealVar &nom = getOrCreate<RooRealVar>(ws, "nom_" + parname, val, min, max);
147 nom.setConstant(true);
148 return nom;
149}
150
151/// Get the conventional name of the constraint pdf for a constrained
152/// parameter.
153std::string constraintName(std::string const &paramName)
154{
155 return paramName + "Constraint";
156}
157
158bool isLegacyConstraintType(std::string const &value)
159{
160 return value == "Gauss" || value == "Poisson" || value == "Const" || value == "Lognormal";
161}
162
163RooAbsPdf *findNamedConstraint(RooJSONFactoryWSTool &tool, std::string const &constraintName, std::string const &sample)
164{
165 if (auto *constraint = tool.workspace()->pdf(constraintName)) {
166 return constraint;
167 }
168
169 try {
170 return tool.request<RooAbsPdf>(constraintName, sample);
172 if (err.child() != constraintName) {
173 throw;
174 }
175 }
176
177 return nullptr;
178}
179
181 std::string const &constraintType)
182{
183 if (constraintType == "Gauss") {
184 param.setError(1.0);
185 return getOrCreate<RooGaussian>(*tool.workspace(), constraintName(param.GetName()), param,
186 *tool.workspace()->var(std::string("nom_") + param.GetName()), 1.);
187 }
188
189 RooJSONFactoryWSTool::error("legacy constraint value '" + constraintType + "' for modifier '" +
191 "' is a known constraint type, but it cannot be resolved in this context");
192}
193
194ParamHistFunc &createPHF(const std::string &phfname, std::string const &sysname,
195 const std::vector<std::string> &parnames, const std::vector<double> &vals,
196 RooJSONFactoryWSTool &tool, RooAbsCollection &constraints, const RooArgSet &observables,
197 const std::string &constraintType, double gammaMin, double gammaMax, double minSigma,
198 bool createConstraints = true)
199{
200 RooWorkspace &ws = *tool.workspace();
201
202 size_t n = std::max(vals.size(), parnames.size());
204 for (std::size_t i = 0; i < n; ++i) {
205 const std::string name = parnames.empty() ? defaultGammaName(sysname, i) : parnames[i];
206 auto *e = dynamic_cast<RooAbsReal *>(ws.obj(name.c_str()));
207 if (e)
208 gammas.add(*e);
209 else
211 }
212
213 auto &phf = tool.wsEmplace<ParamHistFunc>(phfname, observables, gammas);
214
215 if (vals.size() > 0) {
216 if (!createConstraints) {
218 } else if (constraintType != "Const") {
220 gammas, vals, minSigma, constraintType == "Poisson" ? Constraint::Poisson : Constraint::Gaussian);
221 for (auto const &term : constraintsInfo.constraints) {
223 constraints.add(*ws.pdf(term->GetName()));
224 }
225 } else {
226 for (auto *gamma : static_range_cast<RooRealVar *>(gammas)) {
227 gamma->setConstant(true);
228 }
229 }
230 }
231
232 return phf;
233}
234
235bool hasStaterror(const JSONNode &comp)
236{
237 if (!comp.has_child("modifiers"))
238 return false;
239 for (const auto &mod : comp["modifiers"].children()) {
240 if (mod["type"].val() == ::Literals::staterror)
241 return true;
242 }
243 return false;
244}
245
246const JSONNode &findStaterror(const JSONNode &comp)
247{
248 if (comp.has_child("modifiers")) {
249 for (const auto &mod : comp["modifiers"].children()) {
250 if (mod["type"].val() == ::Literals::staterror)
251 return mod;
252 }
253 }
254 RooJSONFactoryWSTool::error("sample '" + RooJSONFactoryWSTool::name(comp) + "' does not have a " +
255 ::Literals::staterror + " modifier!");
256}
257
258RooAbsPdf &
259getOrCreateConstraint(RooJSONFactoryWSTool &tool, const JSONNode &mod, RooRealVar &param, const std::string &sample)
260{
261 JSONNode const *constrName = mod.find("constraint_name");
262 if (constrName) {
263 auto constraint_name = constrName->val();
264 auto constraint = findNamedConstraint(tool, constraint_name, sample);
265 if (!constraint) {
266 RooJSONFactoryWSTool::error("unable to find definition of of constraint '" + constraint_name +
267 "' for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
268 }
269 if (auto gauss = dynamic_cast<RooGaussian *const>(constraint)) {
270 param.setError(gauss->getSigma().getVal());
271 }
272 return *constraint;
273 }
274
275 if (auto constr = mod.find("constraint")) {
276 std::string constraintValue = constr->val();
277 if (auto *constraint = findNamedConstraint(tool, constraintValue, sample)) {
278 if (auto gauss = dynamic_cast<RooGaussian *const>(constraint)) {
279 param.setError(gauss->getSigma().getVal());
280 }
281 return *constraint;
282 }
283
286 }
287
288 RooJSONFactoryWSTool::error("unable to resolve constraint value '" + constraintValue + "' for modifier '" +
290 "': this looks like a legacy workspace where the 'constraint' field is neither a "
291 "constraint pdf name nor a supported legacy constraint type");
292 }
293
294 std::string constraint_type = "Gauss";
295 if (auto constrType = mod.find("constraint_type")) {
297 }
300 }
301 RooJSONFactoryWSTool::error("unknown or invalid constraint for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
302}
303double poissonTau(RooPoisson const &constraint, RooAbsArg const &gamma)
304{
305 auto const *mean = dynamic_cast<RooProduct const *>(&constraint.getMean());
306 if (!mean) {
307 RooJSONFactoryWSTool::error("Poisson gamma constraint mean is not a RooProduct: " +
308 std::string(constraint.GetName()));
309 }
310
311 for (RooAbsArg *arg : mean->servers()) {
312 if (arg == &gamma) {
313 continue;
314 }
315
316 if (auto const *tau = dynamic_cast<RooConstVar const *>(arg)) {
317 return tau->getVal();
318 }
319
320 // Imported workspaces can sometimes represent
321 // constants as constant RooRealVars.
322 if (auto const *real = dynamic_cast<RooAbsReal const *>(arg)) {
323 if (real->isConstant() || endsWith(std::string(real->GetName()), "_tau")) {
324 return real->getVal();
325 }
326 }
327 }
328
329 RooJSONFactoryWSTool::error("Could not find tau component in Poisson gamma constraint mean: " +
330 std::string(constraint.GetName()));
331 return std::numeric_limits<double>::quiet_NaN();
332}
333
334// Returns the relative uncertainty encoded by a gamma constraint pdf. Only RooPoisson (via its tau) and RooGaussian
335// (via sigma/mean) are supported; anything else raises an error.
336double constraintRelError(RooAbsPdf const &constraint, RooAbsArg const &gamma)
337{
338 if (auto constraintP = dynamic_cast<RooPoisson const *>(&constraint)) {
339 return 1. / std::sqrt(poissonTau(*constraintP, gamma));
340 }
341 if (auto constraintG = dynamic_cast<RooGaussian const *>(&constraint)) {
342 return constraintG->getSigma().getVal() / constraintG->getMean().getVal();
343 }
344 RooJSONFactoryWSTool::error("currently, only RooPoisson and RooGaussian are supported as constraint types");
345 return std::numeric_limits<double>::quiet_NaN();
346}
347
349 RooAbsArg const *mcStatObject, const std::string &fprefix, const JSONNode &p,
350 RooArgSet &constraints)
351{
352 RooWorkspace &ws = *tool.workspace();
353
355 std::string prefixedName = fprefix + "_" + sampleName;
356
357 std::string channelName = fprefix;
358 erasePrefix(channelName, "model_");
359
360 if (!p.has_child("data")) {
361 RooJSONFactoryWSTool::error("sample '" + sampleName + "' does not define a 'data' key");
362 }
363
364 auto &hf = tool.wsEmplace<RooHistFunc>("hist_" + prefixedName, varlist, dh);
365 hf.SetTitle(RooJSONFactoryWSTool::name(p).c_str());
366
369
370 shapeElems.add(tool.wsEmplace<RooBinWidthFunction>(prefixedName + "_binWidth", hf, true));
371
372 if (hasStaterror(p)) {
374 }
375
376 if (p.has_child("modifiers")) {
378 std::vector<double> overall_low;
379 std::vector<double> overall_high;
380 std::vector<int> overall_interp;
381
385
386 int idx = 0;
387 for (const auto &mod : p["modifiers"].children()) {
388 std::string const &modtype = mod["type"].val();
389 std::string const &sysname =
390 mod.has_child("name")
391 ? mod["name"].val()
392 : (mod.has_child("parameter") ? mod["parameter"].val() : "syst_" + std::to_string(idx));
393 ++idx;
394 if (modtype == "staterror") {
395 // this is dealt with at a different place, ignore it for now
396 } else if (modtype == "normfactor") {
398 constrParam.setError(0.0);
400 if (mod.has_child("constraint") || mod.has_child("constraint_name") || mod.has_child("constraint_type")) {
401 // for norm factors, constraints are optional
403 }
404 } else if (modtype == "normsys") {
405 auto *parameter = mod.find("parameter");
406 std::string parname(parameter ? parameter->val() : "alpha_" + sysname);
407 createNominal(ws, parname, 0.0, -10, 10);
408 auto &par = getOrCreate<RooRealVar>(ws, parname, 0., -5, 5);
409 overall_nps.add(par);
410 auto &data = mod["data"];
411 int interp = 4;
412 if (mod.has_child("interpolation")) {
413 interp = mod["interpolation"].val_int();
414 }
415 double low = data["lo"].val_double();
416 double high = data["hi"].val_double();
417
418 // the below contains a a hack to cut off variations that go below 0
419 // this is needed because with interpolation code 4, which is the default, interpolation is done in
420 // log-space. hence, values <= 0 result in NaN which propagate throughout the model and cause evaluations to
421 // fail if you know a nicer way to solve this, please go ahead and fix the lines below
422 if (interp == 4 && low <= 0)
423 low = std::numeric_limits<double>::epsilon();
424 if (interp == 4 && high <= 0)
425 high = std::numeric_limits<double>::epsilon();
426
427 overall_low.push_back(low);
428 overall_high.push_back(high);
429 overall_interp.push_back(interp);
430
431 constraints.add(getOrCreateConstraint(tool, mod, par, sampleName));
432 } else if (modtype == "histosys") {
433 auto *parameter = mod.find("parameter");
434 std::string parname(parameter ? parameter->val() : "alpha_" + sysname);
435 createNominal(ws, parname, 0.0, -10, 10);
436 auto &par = getOrCreate<RooRealVar>(ws, parname, 0., -5, 5);
437 histNps.add(par);
438 auto &data = mod["data"];
439 histoLo.add(tool.wsEmplace<RooHistFunc>(
440 sysname + "Low_" + prefixedName, varlist,
442 histoHi.add(tool.wsEmplace<RooHistFunc>(
443 sysname + "High_" + prefixedName, varlist,
444 RooJSONFactoryWSTool::readBinnedData(data["hi"], sysname + "High_" + prefixedName, varlist)));
445 constraints.add(getOrCreateConstraint(tool, mod, par, sampleName));
446 } else if (modtype == "shapesys" || modtype == "shapefactor") {
447 std::string funcName = channelName + "_" + sysname + "_ShapeSys";
448 // funcName should be "<channel_name>_<sysname>_ShapeSys"
449 std::vector<double> vals;
450 if (mod["data"].has_child("vals")) {
451 for (const auto &v : mod["data"]["vals"].children()) {
452 vals.push_back(v.val_double());
453 }
454 }
455 std::vector<std::string> parnames;
456 for (const auto &v : mod["parameters"].children()) {
457 parnames.push_back(v.val());
458 }
459 if (vals.empty() && parnames.empty()) {
460 RooJSONFactoryWSTool::error("unable to instantiate shapesys '" + sysname +
461 "' with neither values nor parameters!");
462 }
463 std::string constraint = "unknown";
464 std::vector<RooAbsPdf *> constraintPdfs;
465 bool const hasConstraintList = mod.has_child("constraints");
466 if (hasConstraintList) {
467 for (const auto &v : mod["constraints"].children()) {
468 if (v.is_null()) {
469 constraintPdfs.push_back(nullptr);
470 } else {
471 std::string constraintName = v.val();
473 if (!constraintPdf) {
474 RooJSONFactoryWSTool::error("unable to find definition of constraint '" + constraintName +
475 "' for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
476 }
477 constraintPdfs.push_back(constraintPdf);
478 }
479 }
480 std::size_t const nGammas = std::max(vals.size(), parnames.size());
481 if (constraintPdfs.size() != nGammas) {
482 std::stringstream ss;
483 ss << "modifier '" << RooJSONFactoryWSTool::name(mod) << "' has " << constraintPdfs.size()
484 << " constraints, but " << nGammas << " parameters";
486 }
487 } else if (mod.has_child("constraint_type")) {
488 constraint = mod["constraint_type"].val();
489 } else if (mod.has_child("constraint")) {
490 std::string constraintValue = mod["constraint"].val();
492 constraint = constraintValue;
493 } else {
494 RooJSONFactoryWSTool::error("unable to resolve constraint value '" + constraintValue +
495 "' for modifier '" + RooJSONFactoryWSTool::name(mod) +
496 "': this looks like a legacy workspace where the 'constraint' field is "
497 "not a supported legacy constraint type");
498 }
499 }
500 shapeElems.add(createPHF(funcName, sysname, parnames, vals, tool, constraints, varlist, constraint,
502 /*createConstraints=*/!hasConstraintList));
503 for (auto *constraintPdf : constraintPdfs) {
504 if (constraintPdf) {
505 constraints.add(*constraintPdf);
506 }
507 }
508 } else if (modtype == "custom") {
509 RooAbsReal *obj = ws.function(sysname);
510 if (!obj) {
511 RooJSONFactoryWSTool::error("unable to find custom modifier '" + sysname + "'");
512 }
513 if (obj->dependsOn(varlist)) {
514 shapeElems.add(*obj);
515 } else {
516 normElems.add(*obj);
517 }
518 } else {
519 RooJSONFactoryWSTool::error("modifier '" + sysname + "' of unknown type '" + modtype + "'");
520 }
521 }
522
523 std::string interpName = sampleName + "_" + channelName + "_epsilon";
524 if (!overall_nps.empty()) {
527 normElems.add(v);
528 }
529 if (!histNps.empty()) {
530 auto &v = tool.wsEmplace<PiecewiseInterpolation>("histoSys_" + prefixedName, hf, histoLo, histoHi, histNps);
532 v.setAllInterpCodes(4); // default interpCode for HistFactory
533 shapeElems.add(v);
534 } else {
535 shapeElems.add(hf);
536 }
537 }
538
539 tool.wsEmplace<RooProduct>(prefixedName + "_shapes", shapeElems);
540 if (!normElems.empty()) {
541 tool.wsEmplace<RooProduct>(prefixedName + "_scaleFactors", normElems);
542 } else {
543 ws.factory("RooConstVar::" + prefixedName + "_scaleFactors(1.)");
544 }
545
546 return true;
547}
548
549class HistFactoryImporter : public RooFit::JSONIO::Importer {
550public:
551 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
552 {
553 std::string name = RooJSONFactoryWSTool::name(p);
554 if (!p.has_child("samples")) {
555 RooJSONFactoryWSTool::error("no samples in '" + name + "', skipping.");
556 }
557 double statErrThresh = 0;
558 std::string statErrType = "Poisson";
559 if (p.has_child(::Literals::staterror)) {
560 auto &staterr = p[::Literals::staterror];
561 if (staterr.has_child("relThreshold"))
562 statErrThresh = staterr["relThreshold"].val_double();
563 if (staterr.has_child("constraint_type"))
564 statErrType = staterr["constraint_type"].val();
565 }
566 std::vector<double> sumW;
567 std::vector<double> sumW2;
568 std::vector<std::string> gammaParnames;
570
571 std::string fprefix = name;
572
573 std::vector<std::unique_ptr<RooDataHist>> data;
574 for (const auto &comp : p["samples"].children()) {
575 std::unique_ptr<RooDataHist> dh = RooJSONFactoryWSTool::readBinnedData(
576 comp["data"], fprefix + "_" + RooJSONFactoryWSTool::name(comp) + "_dataHist", observables);
577 size_t nbins = dh->numEntries();
578
579 if (hasStaterror(comp)) {
580 if (sumW.empty()) {
581 sumW.resize(nbins);
582 sumW2.resize(nbins);
583 }
584 for (size_t i = 0; i < nbins; ++i) {
585 sumW[i] += dh->weight(i);
586 sumW2[i] += dh->weightSquared(i);
587 }
588 if (gammaParnames.empty()) {
589 if (auto staterrorParams = findStaterror(comp).find("parameters")) {
590 for (const auto &v : staterrorParams->children()) {
591 gammaParnames.push_back(v.val());
592 }
593 }
594 }
595 }
596 data.emplace_back(std::move(dh));
597 }
598
599 RooAbsArg *mcStatObject = nullptr;
600 RooArgSet constraints;
601 if (!sumW.empty()) {
602 std::string channelName = name;
603 erasePrefix(channelName, "model_");
604
605 std::vector<double> errs(sumW.size());
606 for (size_t i = 0; i < sumW.size(); ++i) {
607 if (sumW[i] == 0.) {
608 errs[i] = 0.;
609 continue;
610 }
611 errs[i] = std::sqrt(sumW2[i]) / sumW[i];
612 // avoid negative sigma. This NP will be set constant anyway later
613 errs[i] = std::max(errs[i], 0.);
614 }
615
617 &createPHF("mc_stat_" + channelName, "stat_" + channelName, gammaParnames, errs, *tool, constraints,
619 }
620
621 int idx = 0;
623 RooArgList coefs;
624 for (const auto &comp : p["samples"].children()) {
625 importHistSample(*tool, *data[idx], observables, mcStatObject, fprefix, comp, constraints);
626 ++idx;
627
628 std::string const &compName = RooJSONFactoryWSTool::name(comp);
629 funcs.add(*tool->request<RooAbsReal>(fprefix + "_" + compName + "_shapes", name));
630 coefs.add(*tool->request<RooAbsReal>(fprefix + "_" + compName + "_scaleFactors", name));
631 }
632
633 if (constraints.empty()) {
634 tool->wsEmplace<RooRealSumPdf>(name, funcs, coefs, true);
635 } else {
636 std::string sumName = name + "_model";
637 erasePrefix(sumName, "model_");
638 auto &sum = tool->wsEmplace<RooRealSumPdf>(sumName, funcs, coefs, true);
639 sum.SetTitle(name.c_str());
640 tool->wsEmplace<RooProdPdf>(name, constraints, RooFit::Conditional(sum, observables));
641 }
642 return true;
643 }
644};
645
646class FlexibleInterpVarStreamer : public RooFit::JSONIO::Exporter {
647public:
648 std::string const &key() const override
649 {
650 static const std::string keystring = "interpolation0d";
651 return keystring;
652 }
653 bool exportObject(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem) const override
654 {
655 auto fip = static_cast<const RooStats::HistFactory::FlexibleInterpVar *>(func);
656 elem["type"] << key();
657 elem["interpolationCodes"].fill_seq(fip->interpolationCodes());
658 RooJSONFactoryWSTool::fillSeq(elem["vars"], fip->variables());
659 elem["nom"] << fip->nominal();
660 elem["high"].fill_seq(fip->high(), fip->variables().size());
661 elem["low"].fill_seq(fip->low(), fip->variables().size());
662 return true;
663 }
664};
665
666class PiecewiseInterpolationStreamer : public RooFit::JSONIO::Exporter {
667public:
668 std::string const &key() const override
669 {
670 static const std::string keystring = "interpolation";
671 return keystring;
672 }
673 bool exportObject(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem) const override
674 {
675 const PiecewiseInterpolation *pip = static_cast<const PiecewiseInterpolation *>(func);
676 elem["type"] << key();
677 elem["interpolationCodes"].fill_seq(pip->interpolationCodes());
678 elem["positiveDefinite"] << pip->positiveDefinite();
679 RooJSONFactoryWSTool::fillSeq(elem["vars"], pip->paramList());
680 elem["nom"] << pip->nominalHist()->GetName();
681 RooJSONFactoryWSTool::fillSeq(elem["high"], pip->highList(), pip->paramList().size());
682 RooJSONFactoryWSTool::fillSeq(elem["low"], pip->lowList(), pip->paramList().size());
683 return true;
684 }
685};
686
687class PiecewiseInterpolationFactory : public RooFit::JSONIO::Importer {
688public:
689 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
690 {
691 std::string name(RooJSONFactoryWSTool::name(p));
692
693 RooArgList vars{tool->requestArgList<RooAbsReal>(p, "vars")};
694
695 auto &pip = tool->wsEmplace<PiecewiseInterpolation>(name, *tool->requestArg<RooAbsReal>(p, "nom"),
696 tool->requestArgList<RooAbsReal>(p, "low"),
697 tool->requestArgList<RooAbsReal>(p, "high"), vars);
698
699 pip.setPositiveDefinite(p["positiveDefinite"].val_bool());
700
701 if (p.has_child("interpolationCodes")) {
702 std::size_t i = 0;
703 for (auto const &node : p["interpolationCodes"].children()) {
704 pip.setInterpCode(*static_cast<RooAbsReal *>(vars.at(i)), node.val_int(), true);
705 ++i;
706 }
707 }
708
709 return true;
710 }
711};
712
713class FlexibleInterpVarFactory : public RooFit::JSONIO::Importer {
714public:
715 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
716 {
717 std::string name(RooJSONFactoryWSTool::name(p));
718 if (!p.has_child("high")) {
719 RooJSONFactoryWSTool::error("no high variations of '" + name + "'");
720 }
721 if (!p.has_child("low")) {
722 RooJSONFactoryWSTool::error("no low variations of '" + name + "'");
723 }
724 if (!p.has_child("nom")) {
725 RooJSONFactoryWSTool::error("no nominal variation of '" + name + "'");
726 }
727
728 double nom(p["nom"].val_double());
729
730 RooArgList vars{tool->requestArgList<RooRealVar>(p, "vars")};
731
732 std::vector<double> high;
733 high << p["high"];
734
735 std::vector<double> low;
736 low << p["low"];
737
738 if (vars.size() != low.size() || vars.size() != high.size()) {
739 RooJSONFactoryWSTool::error("FlexibleInterpVar '" + name +
740 "' has non-matching lengths of 'vars', 'high' and 'low'!");
741 }
742
743 auto &fip = tool->wsEmplace<RooStats::HistFactory::FlexibleInterpVar>(name, vars, nom, low, high);
744
745 if (p.has_child("interpolationCodes")) {
746 size_t i = 0;
747 for (auto const &node : p["interpolationCodes"].children()) {
748 fip.setInterpCode(*static_cast<RooAbsReal *>(vars.at(i)), node.val_int());
749 ++i;
750 }
751 }
752
753 return true;
754 }
755};
756
757struct NormFactor {
758 std::string name;
759 RooAbsReal const *param = nullptr;
760 RooAbsPdf const *constraint = nullptr;
761 NormFactor(RooAbsReal const &par, const RooAbsPdf *constr = nullptr)
762 : name{par.GetName()}, param{&par}, constraint{constr}
763 {
764 }
765};
766
767struct NormSys {
768 std::string name = "";
769 RooAbsReal const *param = nullptr;
770 double low = 1.;
771 double high = 1.;
772 int interpolationCode = 4;
773 RooAbsPdf const *constraint = nullptr;
774 NormSys() {};
775 NormSys(const std::string &n, RooAbsReal *const p, double h, double l, int i, const RooAbsPdf *c)
776 : name(n), param(p), low(l), high(h), interpolationCode(i), constraint(c)
777 {
778 }
779};
780
781struct HistoSys {
782 std::string name;
783 RooAbsReal const *param = nullptr;
784 std::vector<double> low;
785 std::vector<double> high;
786 // Used to validate duplicate RooFit modifiers. This is intentionally not serialized until HS3 defines the
787 // structured histosys interpolation representation.
788 int interpolationCode = 4;
789 RooAbsPdf const *constraint = nullptr;
790 HistoSys(const std::string &n, RooAbsReal *const p, RooHistFunc *l, RooHistFunc *h, int i, const RooAbsPdf *c)
791 : name(n), param(p), interpolationCode(i), constraint(c)
792 {
793 low.assign(l->dataHist().weightArray(), l->dataHist().weightArray() + l->dataHist().numEntries());
794 high.assign(h->dataHist().weightArray(), h->dataHist().weightArray() + h->dataHist().numEntries());
795 }
796};
797struct ShapeSys {
798 std::string name;
799 std::vector<double> constraints;
800 std::vector<RooAbsPdf const *> constraintPdfs;
801 std::vector<RooAbsReal *> parameters;
802 ShapeSys(const std::string &n) : name{n} {}
803};
804
805struct GenericElement {
806 std::string name;
807 RooAbsReal *function = nullptr;
808 GenericElement(RooAbsReal *e) : name(e->GetName()), function(e) {};
809};
810
811std::string stripOuterParens(const std::string &s)
812{
813 size_t start = 0;
814 size_t end = s.size();
815
816 while (start < end && s[start] == '(' && s[end - 1] == ')') {
817 int depth = 0;
818 bool balanced = true;
819 for (size_t i = start; i < end - 1; ++i) {
820 if (s[i] == '(')
821 ++depth;
822 else if (s[i] == ')')
823 --depth;
824 if (depth == 0 && i < end - 1) {
825 balanced = false;
826 break;
827 }
828 }
829 if (balanced) {
830 ++start;
831 --end;
832 } else {
833 break;
834 }
835 }
836 return s.substr(start, end - start);
837}
838
839std::vector<std::string> splitTopLevelProduct(const std::string &expr)
840{
841 std::vector<std::string> parts;
842 int depth = 0;
843 size_t start = 0;
844 bool foundTopLevelStar = false;
845
846 for (size_t i = 0; i < expr.size(); ++i) {
847 char c = expr[i];
848 if (c == '(') {
849 ++depth;
850 } else if (c == ')') {
851 --depth;
852 } else if (c == '*' && depth == 0) {
853 foundTopLevelStar = true;
854 std::string sub = expr.substr(start, i - start);
855 parts.push_back(stripOuterParens(sub));
856 start = i + 1;
857 }
858 }
859
860 if (!foundTopLevelStar) {
861 return {}; // Not a top-level product
862 }
863
864 std::string sub = expr.substr(start);
865 parts.push_back(stripOuterParens(sub));
866 return parts;
867}
868
869NormSys parseOverallModifierFormula(const std::string &s, RooFormulaVar *formula)
870{
871 static const std::regex pattern(
872 R"(^\s*1(?:\.0)?\s*([\+\-])\s*([a-zA-Z_][a-zA-Z0-9_]*|[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*\*\s*([a-zA-Z_][a-zA-Z0-9_]*|[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*$)");
873
874 NormSys sys;
875 double sign = 1.0;
876
877 std::smatch match;
878 if (std::regex_match(s, match, pattern)) {
879 if (match[1].str() == "-") {
880 sign = -1.0;
881 }
882
883 std::string token2 = match[2].str();
884 std::string token3 = match[4].str();
885
886 RooAbsReal *p2 = static_cast<RooAbsReal *>(formula->getParameter(token2.c_str()));
887 RooAbsReal *p3 = static_cast<RooAbsReal *>(formula->getParameter(token3.c_str()));
888 RooRealVar *v2 = dynamic_cast<RooRealVar *>(p2);
889 RooRealVar *v3 = dynamic_cast<RooRealVar *>(p3);
890
891 auto *constr2 = findConstraint(v2);
892 auto *constr3 = findConstraint(v3);
893
894 if (constr2 && !p3) {
895 sys.name = p2->GetName();
896 sys.param = p2;
897 sys.high = sign * toDouble(token3);
898 sys.low = -sign * toDouble(token3);
899 } else if (!p2 && constr3) {
900 sys.name = p3->GetName();
901 sys.param = p3;
902 sys.high = sign * toDouble(token2);
903 sys.low = -sign * toDouble(token2);
904 } else if (constr2 && p3 && !constr3) {
905 sys.name = v2->GetName();
906 sys.param = v2;
907 sys.high = sign * p3->getVal();
908 sys.low = -sign * p3->getVal();
909 } else if (p2 && !constr2 && constr3) {
910 sys.name = v3->GetName();
911 sys.param = v3;
912 sys.high = sign * p2->getVal();
913 sys.low = -sign * p2->getVal();
914 }
915
916 // interpolation code 1 means linear, which is what we have here
917 sys.interpolationCode = 1;
918
919 erasePrefix(sys.name, "alpha_");
920 }
921 return sys;
922}
923
924void collectElements(RooArgList &elems, RooAbsArg *arg)
925{
926 if (auto prod = dynamic_cast<RooProduct *>(arg)) {
927 for (const auto &e : prod->components()) {
928 collectElements(elems, e);
929 }
930 } else {
931 elems.add(*arg);
932 }
933}
934
935bool allRooRealVar(const RooAbsCollection &list)
936{
937 for (auto *var : list) {
938 if (!dynamic_cast<RooRealVar *>(var)) {
939 return false;
940 }
941 }
942 return true;
943}
944
945struct Sample {
946 std::string name;
947 std::vector<double> hist;
948 std::vector<double> histError;
949 std::vector<NormFactor> normfactors;
950 std::vector<NormSys> normsys;
951 std::vector<HistoSys> histosys;
952 std::vector<ShapeSys> shapesys;
953 std::vector<GenericElement> tmpElements;
954 std::vector<GenericElement> otherElements;
955 bool useBarlowBeestonLight = false;
956 std::vector<RooAbsReal *> staterrorParameters;
957 Sample(const std::string &n) : name{n} {}
958};
959
960void addNormFactor(RooRealVar const *par, Sample &sample, RooWorkspace *ws)
961{
962 std::string parname = par->GetName();
963 bool isConstrained = false;
964 for (RooAbsArg const *pdf : ws->allPdfs()) {
965 if (auto gauss = dynamic_cast<RooGaussian const *>(pdf)) {
966 if (parname == gauss->getX().GetName()) {
967 sample.normfactors.emplace_back(*par, gauss);
968 isConstrained = true;
969 }
970 }
971 }
972 if (!isConstrained)
973 sample.normfactors.emplace_back(*par);
974}
975
976struct Channel {
977 std::string name;
978 std::vector<Sample> samples;
979 std::map<int, double> tot_yield;
980 std::map<int, double> tot_yield2;
981 std::map<int, double> rel_errors;
982 RooArgSet const *varSet = nullptr;
983 long unsigned int nBins = 0;
984};
985
987{
988 Channel channel;
989
990 RooWorkspace *ws = tool->workspace();
991
992 channel.name = pdfname;
993 erasePrefix(channel.name, "model_");
994 eraseSuffix(channel.name, "_model");
995
996 for (size_t sampleidx = 0; sampleidx < sumpdf->funcList().size(); ++sampleidx) {
997 PiecewiseInterpolation *pip = nullptr;
998 std::vector<ParamHistFunc *> phfs;
999
1000 const auto func = sumpdf->funcList().at(sampleidx);
1001 Sample sample(func->GetName());
1002 erasePrefix(sample.name, "L_x_");
1003 eraseSuffix(sample.name, "_shapes");
1004 eraseSuffix(sample.name, "_" + channel.name);
1005 erasePrefix(sample.name, pdfname + "_");
1006
1007 auto updateObservables = [&](RooDataHist const &dataHist) {
1008 if (channel.varSet == nullptr) {
1009 channel.varSet = dataHist.get();
1010 channel.nBins = dataHist.numEntries();
1011 }
1012 if (sample.hist.empty()) {
1013 auto *w = dataHist.weightArray();
1014 sample.hist.assign(w, w + dataHist.numEntries());
1015 }
1016 };
1017 auto processElements = [&](const auto &elements, auto &&self) -> void {
1018 for (RooAbsArg *e : elements) {
1019 if (TString(e->GetName()).Contains("binWidth")) {
1020 // The bin width modifiers are handled separately. We can't just
1021 // check for the RooBinWidthFunction type here, because prior to
1022 // ROOT 6.26, the multiplication with the inverse bin width was
1023 // done in a different way (like a normfactor with a RooRealVar,
1024 // but it was stored in the dataset).
1025 // Fortunately, the name was similar, so we can match the modifier
1026 // name.
1027 } else if (auto constVar = dynamic_cast<RooConstVar *>(e)) {
1028 if (constVar->getVal() != 1.) {
1029 sample.normfactors.emplace_back(*constVar);
1030 }
1031 } else if (auto par = dynamic_cast<RooRealVar *>(e)) {
1032 addNormFactor(par, sample, ws);
1033 } else if (auto hf = dynamic_cast<const RooHistFunc *>(e)) {
1034 updateObservables(hf->dataHist());
1035 } else if (ParamHistFunc *phf = dynamic_cast<ParamHistFunc *>(e); phf && allRooRealVar(phf->paramList())) {
1036 phfs.push_back(phf);
1037 } else if (auto fip = dynamic_cast<RooStats::HistFactory::FlexibleInterpVar *>(e)) {
1038 // some (modified) histfactory models have several instances of FlexibleInterpVar
1039 // we collect and merge them
1040 for (size_t i = 0; i < fip->variables().size(); ++i) {
1041 RooAbsReal *var = static_cast<RooAbsReal *>(fip->variables().at(i));
1042 std::string sysname(var->GetName());
1043 erasePrefix(sysname, "alpha_");
1044 const auto *constraint = findConstraint(var);
1045 if (!constraint && !var->isConstant()) {
1046 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(var->GetName()));
1047 } else {
1048 sample.normsys.emplace_back(sysname, var, fip->high()[i], fip->low()[i],
1049 fip->interpolationCodes()[i], constraint);
1050 }
1051 }
1052 } else if (!pip && (pip = dynamic_cast<PiecewiseInterpolation *>(e))) {
1053 // nothing to do here, already assigned
1054 } else if (RooFormulaVar *formula = dynamic_cast<RooFormulaVar *>(e)) {
1055 // people do a lot of fancy stuff with RooFormulaVar, like including NormSys via explicit formulae.
1056 // let's try to decompose it into building blocks
1057 TString expression(formula->expression());
1058 for (size_t i = formula->nParameters(); i--;) {
1059 const RooAbsArg *p = formula->getParameter(i);
1060 expression.ReplaceAll(("x[" + std::to_string(i) + "]").c_str(), p->GetName());
1061 expression.ReplaceAll(("@" + std::to_string(i)).c_str(), p->GetName());
1062 }
1063 auto components = splitTopLevelProduct(expression.Data());
1064 if (components.size() == 0) {
1065 // it's not a product, let's just treat it as an unknown element
1066 sample.otherElements.push_back(formula);
1067 } else {
1068 // it is a prododuct, we can try to handle the elements separately
1069 std::vector<RooAbsArg *> realComponents;
1070 int idx = 0;
1071 for (auto &comp : components) {
1072 // check if this is a trivial element of a product, we can treat it as its own modifier
1073 auto *part = formula->getParameter(comp.c_str());
1074 if (part) {
1075 realComponents.push_back(part);
1076 continue;
1077 }
1078 // check if this is an attempt at explicitly encoding an overallSys
1079 auto normsys = parseOverallModifierFormula(comp, formula);
1080 if (normsys.param) {
1081 sample.normsys.emplace_back(std::move(normsys));
1082 continue;
1083 }
1084
1085 // this is something non-trivial, let's deal with it separately
1086 std::string name = std::string(formula->GetName()) + "_part" + std::to_string(idx);
1087 ++idx;
1088 auto *var = new RooFormulaVar(name.c_str(), name.c_str(), comp.c_str(), formula->dependents());
1089 sample.tmpElements.push_back({var});
1090 }
1091 self(realComponents, self);
1092 }
1093 } else if (auto real = dynamic_cast<RooAbsReal *>(e)) {
1094 sample.otherElements.push_back(real);
1095 }
1096 }
1097 };
1098
1099 RooArgList elems;
1100 collectElements(elems, func);
1101 collectElements(elems, sumpdf->coefList().at(sampleidx));
1103
1104 // see if we can get the observables
1105 if (pip) {
1106 if (auto nh = dynamic_cast<RooHistFunc const *>(pip->nominalHist())) {
1107 updateObservables(nh->dataHist());
1108 }
1109 }
1110
1111 // sort and configure norms
1112 sortByName(sample.normfactors);
1113 sortByName(sample.normsys);
1114
1115 // sort and configure the histosys
1116 if (pip) {
1117 for (size_t i = 0; i < pip->paramList().size(); ++i) {
1118 RooAbsReal *var = static_cast<RooAbsReal *>(pip->paramList().at(i));
1119 std::string sysname(var->GetName());
1120 erasePrefix(sysname, "alpha_");
1121 if (auto lo = dynamic_cast<RooHistFunc *>(pip->lowList().at(i))) {
1122 if (auto hi = dynamic_cast<RooHistFunc *>(pip->highList().at(i))) {
1123 const auto *constraint = findConstraint(var);
1124 if (!constraint && !var->isConstant()) {
1125 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(var->GetName()));
1126 } else {
1127 sample.histosys.emplace_back(sysname, var, lo, hi, pip->interpolationCodes()[i], constraint);
1128 }
1129 }
1130 }
1131 }
1132 sortByName(sample.histosys);
1133 }
1134
1135 for (ParamHistFunc *phf : phfs) {
1136 if (startsWith(std::string(phf->GetName()), "mc_stat_")) { // MC stat uncertainty
1137 int idx = 0;
1138 for (const auto &g : phf->paramList()) {
1139 sample.staterrorParameters.push_back(static_cast<RooRealVar *>(g));
1140 ++idx;
1141 RooAbsPdf *constraint = findConstraint(g);
1142 if (channel.tot_yield.find(idx) == channel.tot_yield.end()) {
1143 channel.tot_yield[idx] = 0;
1144 channel.tot_yield2[idx] = 0;
1145 }
1146 channel.tot_yield[idx] += sample.hist[idx - 1];
1147 channel.tot_yield2[idx] += (sample.hist[idx - 1] * sample.hist[idx - 1]);
1148 if (constraint) {
1149 channel.rel_errors[idx] = constraintRelError(*constraint, *g);
1150 }
1151 }
1152 sample.useBarlowBeestonLight = true;
1153 } else { // other ShapeSys
1154 ShapeSys sys(phf->GetName());
1155 erasePrefix(sys.name, channel.name + "_");
1156 bool isshapesys = eraseSuffix(sys.name, "_ShapeSys") || eraseSuffix(sys.name, "_shapeSys");
1157 bool isshapefactor = eraseSuffix(sys.name, "_ShapeFactor") || eraseSuffix(sys.name, "_shapeFactor");
1158
1159 for (const auto &g : phf->paramList()) {
1160 sys.parameters.push_back(static_cast<RooRealVar *>(g));
1161 RooAbsPdf *constraint = nullptr;
1162 if (isshapesys) {
1163 constraint = findConstraint(g);
1164 if (!constraint)
1165 constraint = ws->pdf(constraintName(g->GetName()));
1166 if (!constraint && !g->isConstant()) {
1167 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(g->GetName()));
1168 }
1169 } else if (!isshapefactor) {
1170 RooJSONFactoryWSTool::error("unknown type of shapesys " + std::string(phf->GetName()));
1171 }
1172 if (!constraint) {
1173 sys.constraints.push_back(0.0);
1174 sys.constraintPdfs.push_back(nullptr);
1175 } else {
1176 sys.constraints.push_back(constraintRelError(*constraint, *g));
1177 sys.constraintPdfs.push_back(constraint);
1178 }
1179 }
1180 sample.shapesys.emplace_back(std::move(sys));
1181 }
1182 }
1183 sortByName(sample.shapesys);
1184
1185 // add the sample
1186 channel.samples.emplace_back(std::move(sample));
1187 }
1188
1189 sortByName(channel.samples);
1190 return channel;
1191}
1192
1193bool hasSameMetadata(const RooAbsArg *lhs, const RooAbsArg *rhs)
1194{
1195 if (!lhs || !rhs) {
1196 return lhs == rhs;
1197 }
1198 return std::string{lhs->GetName()} == rhs->GetName() && lhs->IsA() == rhs->IsA();
1199}
1200
1201[[noreturn]] void duplicateModifierError(const Channel &channel, const Sample &sample, std::string_view type,
1202 std::string_view name, std::string_view reason)
1203{
1204 std::stringstream ss;
1205 ss << "cannot combine duplicate modifier '" << name << "' of type '" << type << "' in sample '" << sample.name
1206 << "' of channel '" << channel.name << "': " << reason;
1207 RooJSONFactoryWSTool::error(ss.str().c_str());
1208}
1209
1210void warnDuplicateModifiersCombined(const Channel &channel, const Sample &sample, std::string_view type,
1211 std::string_view name, std::size_t count)
1212{
1213 std::stringstream ss;
1214 ss << "combined " << count << " duplicate modifiers named '" << name << "' of type '" << type << "' in sample '"
1215 << sample.name << "' of channel '" << channel.name << "'";
1217}
1218
1219// Multiplicatively combining two normsys is only faithful when the interpolation is done in log-space, so that
1220// f1(alpha) * f2(alpha) is again representable by a single normsys with the multiplied lo/hi factors. This holds for
1221// the piecewise-exponential code 1 (exact everywhere) and for the default code 4 (exact at the +-1 sigma anchors and in
1222// the exponential extrapolation region). The linear-space codes (e.g. 0 and 2) would turn the product into a shape that
1223// cannot be represented by a single normsys, so those must not be merged.
1224bool normSysSupportsMultiplicativeMerge(int interpolationCode)
1225{
1226 return interpolationCode == 1 || interpolationCode == 4;
1227}
1228
1229// Combines runs of adjacent modifiers that share the same name (the container is sorted by name beforehand) into a
1230// single modifier. The shared metadata (constraint, parameter and interpolation code) must be identical across the
1231// duplicates; the type-specific `combine` callable performs the actual merge and any additional validation.
1232template <class Modifiers, class CombineFn>
1233void mergeDuplicateModifiers(const Channel &channel, const Sample &sample, Modifiers &modifiers,
1234 std::string_view type, CombineFn combine)
1235{
1237 mergedModifiers.reserve(modifiers.size());
1238
1239 for (std::size_t begin = 0; begin < modifiers.size();) {
1240 std::size_t end = begin + 1;
1241 while (end < modifiers.size() && modifiers[end].name == modifiers[begin].name) {
1242 ++end;
1243 }
1244
1245 auto merged = modifiers[begin];
1246 for (std::size_t i = begin + 1; i < end; ++i) {
1247 const auto &modifier = modifiers[i];
1248 if (!hasSameMetadata(merged.constraint, modifier.constraint)) {
1249 duplicateModifierError(channel, sample, type, merged.name, "constraint metadata differs");
1250 }
1251 if (!hasSameMetadata(merged.param, modifier.param)) {
1252 duplicateModifierError(channel, sample, type, merged.name, "parameter metadata differs");
1253 }
1254 if (merged.interpolationCode != modifier.interpolationCode) {
1255 duplicateModifierError(channel, sample, type, merged.name, "interpolation codes differ");
1256 }
1258 }
1259
1260 if (end - begin > 1) {
1261 warnDuplicateModifiersCombined(channel, sample, type, merged.name, end - begin);
1262 }
1263 mergedModifiers.emplace_back(std::move(merged));
1264 begin = end;
1265 }
1266
1267 modifiers = std::move(mergedModifiers);
1268}
1269
1270void mergeDuplicateNormSys(const Channel &channel, Sample &sample)
1271{
1272 mergeDuplicateModifiers(channel, sample, sample.normsys, "normsys",
1273 [&](NormSys &merged, const NormSys &modifier) {
1274 if (!normSysSupportsMultiplicativeMerge(merged.interpolationCode)) {
1275 duplicateModifierError(
1276 channel, sample, "normsys", merged.name,
1277 "multiplicative combination is only valid for log-space interpolation codes");
1278 }
1279 merged.low *= modifier.low;
1280 merged.high *= modifier.high;
1281 });
1282}
1283
1284void mergeDuplicateHistoSys(const Channel &channel, Sample &sample)
1285{
1286 const std::size_t nBins = sample.hist.size();
1287 mergeDuplicateModifiers(channel, sample, sample.histosys, "histosys",
1288 [&](HistoSys &merged, const HistoSys &modifier) {
1289 if (merged.interpolationCode != 4) {
1290 duplicateModifierError(
1291 channel, sample, "histosys", merged.name,
1292 "non-default interpolation cannot currently be represented by the HS3 exporter");
1293 }
1294 if (merged.low.size() != nBins || merged.high.size() != nBins ||
1295 modifier.low.size() != nBins || modifier.high.size() != nBins) {
1296 duplicateModifierError(channel, sample, "histosys", merged.name,
1297 "histogram binning differs");
1298 }
1299 for (std::size_t bin = 0; bin < nBins; ++bin) {
1300 merged.low[bin] += modifier.low[bin] - sample.hist[bin];
1301 merged.high[bin] += modifier.high[bin] - sample.hist[bin];
1302 }
1303 });
1304}
1305
1306void ensureUniqueModifiers(const Channel &channel, const Sample &sample)
1307{
1308 std::set<std::pair<std::string, std::string>> seen;
1309 auto add = [&](std::string type, const std::string &name) {
1310 if (!seen.emplace(type, name).second) {
1312 "this modifier type cannot be combined without changing its meaning");
1313 }
1314 };
1315
1316 for (const auto &modifier : sample.normfactors)
1317 add("normfactor", modifier.name);
1318 for (const auto &modifier : sample.normsys)
1319 add("normsys", modifier.name);
1320 for (const auto &modifier : sample.histosys)
1321 add("histosys", modifier.name);
1322 for (const auto &modifier : sample.shapesys)
1323 add("shapesys", modifier.name);
1324 for (const auto &modifier : sample.otherElements)
1325 add("custom", modifier.name);
1326 for (const auto &modifier : sample.tmpElements)
1327 add("custom", modifier.name);
1328 if (sample.useBarlowBeestonLight)
1329 add(::Literals::staterror, ::Literals::staterror);
1330}
1331
1332void canonicalizeModifiers(Channel &channel)
1333{
1334 for (auto &sample : channel.samples) {
1335 mergeDuplicateNormSys(channel, sample);
1337 ensureUniqueModifiers(channel, sample);
1338 }
1339}
1340
1341void configureStatError(Channel &channel)
1342{
1343 for (auto &sample : channel.samples) {
1344 if (sample.useBarlowBeestonLight) {
1345 sample.histError.resize(sample.hist.size());
1346 for (auto bin : channel.rel_errors) {
1347 // reverse engineering the correct partial error
1348 // the (arbitrary) convention used here is that all samples should have the same relative error
1349 const int i = bin.first;
1350 const double relerr_tot = bin.second;
1351 const double count = sample.hist[i - 1];
1352 // this reconstruction is inherently imprecise, so we truncate it at some decimal places to make sure that
1353 // we don't carry around too many useless digits
1354 sample.histError[i - 1] =
1355 round_prec(relerr_tot * channel.tot_yield[i] / std::sqrt(channel.tot_yield2[i]) * count, 7);
1356 }
1357 }
1358 }
1359}
1360
1362{
1363 // Write the constraint reference for any modifier that supports an
1364 // external Gaussian/Poisson/etc. constraint.
1365 auto writeConstraint = [](JSONNode &mod, auto const &sys) {
1366 if (sys.constraint) {
1367 mod["constraint"] << sys.constraint->GetName();
1368 }
1369 };
1370
1371 bool observablesWritten = false;
1372 for (const auto &sample : channel.samples) {
1373
1374 elem["type"] << "histfactory_dist";
1375
1376 auto &s = RooJSONFactoryWSTool::appendNamedChild(elem["samples"], sample.name);
1377
1378 auto &modifiers = s["modifiers"];
1379 modifiers.set_seq();
1380
1381 for (const auto &nf : sample.normfactors) {
1382 auto &mod = modifiers.append_child();
1383 mod.set_map();
1384 mod["name"] << nf.name;
1385 mod["parameter"] << nf.param->GetName();
1386 mod["type"] << "normfactor";
1387 if (nf.constraint) {
1388 mod["constraint"] << nf.constraint->GetName();
1389 tool->queueExport(*nf.constraint);
1390 }
1391 }
1392
1393 for (const auto &sys : sample.normsys) {
1394 auto &mod = modifiers.append_child();
1395 mod.set_map();
1396 mod["name"] << sys.name;
1397 mod["type"] << "normsys";
1398 mod["parameter"] << sys.param->GetName();
1399 if (sys.interpolationCode != 4) {
1400 mod["interpolation"] << sys.interpolationCode;
1401 }
1402 writeConstraint(mod, sys);
1403 auto &data = mod["data"].set_map();
1404 data["lo"] << sys.low;
1405 data["hi"] << sys.high;
1406 }
1407
1408 for (const auto &sys : sample.histosys) {
1409 auto &mod = modifiers.append_child();
1410 mod.set_map();
1411 mod["name"] << sys.name;
1412 mod["type"] << "histosys";
1413 mod["parameter"] << sys.param->GetName();
1414 writeConstraint(mod, sys);
1415 auto &data = mod["data"].set_map();
1416 if (channel.nBins != sys.low.size() || channel.nBins != sys.high.size()) {
1417 std::stringstream ss;
1418 ss << "inconsistent binning: " << channel.nBins << " bins expected, but " << sys.low.size() << "/"
1419 << sys.high.size() << " found in nominal histogram errors!";
1420 RooJSONFactoryWSTool::error(ss.str().c_str());
1421 }
1422 RooJSONFactoryWSTool::exportArray(channel.nBins, sys.low.data(), data["lo"].set_map()["contents"]);
1423 RooJSONFactoryWSTool::exportArray(channel.nBins, sys.high.data(), data["hi"].set_map()["contents"]);
1424 }
1425
1426 for (const auto &sys : sample.shapesys) {
1427 auto &mod = modifiers.append_child();
1428 mod.set_map();
1429 mod["name"] << sys.name;
1430 mod["type"] << "shapesys";
1431 optionallyExportGammaParameters(mod, sys.name, sys.parameters);
1432 if (std::any_of(sys.constraintPdfs.begin(), sys.constraintPdfs.end(),
1433 [](auto *pdf) { return pdf != nullptr; })) {
1434 auto &constraintNames = mod["constraints"].set_seq();
1435 for (auto *constraint : sys.constraintPdfs) {
1436 if (constraint) {
1437 constraintNames.append_child() << constraint->GetName();
1438 } else {
1439 constraintNames.append_child().set_null();
1440 }
1441 }
1442 }
1443 mod["data"].set_map()["vals"].fill_seq(sys.constraints);
1444 }
1445
1446 for (const auto &other : sample.otherElements) {
1447 auto &mod = modifiers.append_child();
1448 mod.set_map();
1449 mod["name"] << other.name;
1450 mod["type"] << "custom";
1451 }
1452 for (const auto &other : sample.tmpElements) {
1453 auto &mod = modifiers.append_child();
1454 mod.set_map();
1455 mod["name"] << other.name;
1456 mod["type"] << "custom";
1457 }
1458
1459 if (sample.useBarlowBeestonLight) {
1460 auto &mod = modifiers.append_child();
1461 mod.set_map();
1462 mod["name"] << ::Literals::staterror;
1463 mod["type"] << ::Literals::staterror;
1464 optionallyExportGammaParameters(mod, "stat_" + channel.name, sample.staterrorParameters);
1465 }
1466
1467 if (!observablesWritten) {
1468 auto &output = elem["axes"].set_seq();
1469 for (auto *obs : static_range_cast<RooRealVar *>(*channel.varSet)) {
1470 RooJSONFactoryWSTool::exportAxis(output.append_child().set_map(), *obs);
1471 }
1472 observablesWritten = true;
1473 }
1474 auto &dataNode = s["data"].set_map();
1475 if (channel.nBins != sample.hist.size()) {
1476 std::stringstream ss;
1477 ss << "inconsistent binning: " << channel.nBins << " bins expected, but " << sample.hist.size()
1478 << " found in nominal histogram!";
1479 RooJSONFactoryWSTool::error(ss.str().c_str());
1480 }
1481 RooJSONFactoryWSTool::exportArray(channel.nBins, sample.hist.data(), dataNode["contents"]);
1482 if (!sample.histError.empty()) {
1483 if (channel.nBins != sample.histError.size()) {
1484 std::stringstream ss;
1485 ss << "inconsistent binning: " << channel.nBins << " bins expected, but " << sample.histError.size()
1486 << " found in nominal histogram errors!";
1487 RooJSONFactoryWSTool::error(ss.str().c_str());
1488 }
1489 RooJSONFactoryWSTool::exportArray(channel.nBins, sample.histError.data(), dataNode["errors"]);
1490 }
1491 }
1492
1493 return true;
1494}
1495
1496std::vector<RooAbsPdf *> findLostConstraints(const Channel &channel, const std::vector<RooAbsPdf *> &constraints)
1497{
1498 // collect all the vars that are used by the model
1499 std::set<const RooAbsReal *> vars;
1500 for (const auto &sample : channel.samples) {
1501 for (const auto &nf : sample.normfactors) {
1502 vars.insert(nf.param);
1503 }
1504 for (const auto &sys : sample.normsys) {
1505 vars.insert(sys.param);
1506 }
1507
1508 for (const auto &sys : sample.histosys) {
1509 vars.insert(sys.param);
1510 }
1511 for (const auto &sys : sample.shapesys) {
1512 for (const auto &par : sys.parameters) {
1513 vars.insert(par);
1514 }
1515 }
1516 if (sample.useBarlowBeestonLight) {
1517 for (const auto &par : sample.staterrorParameters) {
1518 vars.insert(par);
1519 }
1520 }
1521 }
1522
1523 // check if there is any constraint present that is unrelated to these vars
1524 std::vector<RooAbsPdf *> lostConstraints;
1525 for (auto *pdf : constraints) {
1526 bool related = false;
1527 for (const auto *var : vars) {
1528 if (pdf->dependsOn(*var)) {
1529 related = true;
1530 }
1531 }
1532 if (!related) {
1533 lostConstraints.push_back(pdf);
1534 }
1535 }
1536 // return the constraints that would be "lost" when exporting the model
1537 return lostConstraints;
1538}
1539
1541 std::vector<RooAbsPdf *> constraints, JSONNode &elem)
1542{
1543 // some preliminary checks
1544 if (!sumpdf) {
1545 return false;
1546 }
1547
1548 for (RooAbsArg *sample : sumpdf->funcList()) {
1549 if (!dynamic_cast<RooProduct *>(sample) && !dynamic_cast<RooRealSumPdf *>(sample)) {
1550 return false;
1551 }
1552 }
1553
1554 auto channel = readChannel(tool, pdfname, sumpdf);
1555
1556 // sanity checks
1557 if (channel.samples.size() == 0)
1558 return false;
1559 for (auto &sample : channel.samples) {
1560 if (sample.hist.empty()) {
1561 return false;
1562 }
1563 }
1564
1565 canonicalizeModifiers(channel);
1566
1567 // stat error handling
1568 configureStatError(channel);
1569
1570 auto lostConstraints = findLostConstraints(channel, constraints);
1571 // Export all the lost constraints
1572 for (const auto *constraint : lostConstraints) {
1574 "losing constraint term '" + std::string(constraint->GetName()) +
1575 "', implicit constraints are not supported by HS3 yet! The term will appear in the HS3 file, but will not be "
1576 "picked up when creating a likelihood from it! You will have to add it manually as an external constraint.");
1577 tool->queueExport(*constraint);
1578 }
1579
1580 // Export all the regular modifiers
1581 for (const auto &sample : channel.samples) {
1582 for (auto &modifier : sample.normfactors) {
1583 if (modifier.constraint) {
1584 tool->queueExport(*modifier.constraint);
1585 }
1586 }
1587 for (auto &modifier : sample.normsys) {
1588 if (modifier.constraint) {
1589 tool->queueExport(*modifier.constraint);
1590 }
1591 }
1592 for (auto &modifier : sample.histosys) {
1593 if (modifier.constraint) {
1594 tool->queueExport(*modifier.constraint);
1595 }
1596 }
1597 for (auto &modifier : sample.shapesys) {
1598 for (auto *constraint : modifier.constraintPdfs) {
1599 if (constraint) {
1600 tool->queueExport(*constraint);
1601 }
1602 }
1603 }
1604 }
1605
1606 // Export all the custom modifiers
1607 for (const auto &sample : channel.samples) {
1608 for (auto &modifier : sample.otherElements) {
1609 tool->queueExport(*modifier.function);
1610 }
1611 for (auto &modifier : sample.tmpElements) {
1612 tool->queueExportTemporary(modifier.function);
1613 }
1614 }
1615
1616 // Export all model parameters
1617 RooArgSet parameters;
1618 sumpdf->getParameters(channel.varSet, parameters);
1619 for (RooAbsArg *param : parameters) {
1620 // This should exclude the global observables
1621 if (!startsWith(std::string{param->GetName()}, "nom_")) {
1622 tool->queueExport(*param);
1623 }
1624 }
1625
1626 return exportChannel(tool, channel, elem);
1627}
1628
1629class HistFactoryStreamer_ProdPdf : public RooFit::JSONIO::Exporter {
1630public:
1631 bool autoExportDependants() const override { return false; }
1633 {
1634 std::vector<RooAbsPdf *> constraints;
1635 RooRealSumPdf *sumpdf = nullptr;
1636 for (auto *pdf : static_range_cast<RooAbsPdf *>(prodpdf->pdfList())) {
1637 auto thispdf = dynamic_cast<RooRealSumPdf *>(pdf);
1638 if (thispdf) {
1639 if (!sumpdf)
1640 sumpdf = thispdf;
1641 else
1642 return false;
1643 } else {
1644 constraints.push_back(pdf);
1645 }
1646 }
1647 if (!sumpdf)
1648 return false;
1649
1650 bool ok = tryExportHistFactory(tool, prodpdf->GetName(), sumpdf, constraints, elem);
1651 return ok;
1652 }
1653 std::string const &key() const override
1654 {
1655 static const std::string keystring = "histfactory_dist";
1656 return keystring;
1657 }
1658 bool exportObject(RooJSONFactoryWSTool *tool, const RooAbsArg *p, JSONNode &elem) const override
1659 {
1660 return tryExport(tool, static_cast<const RooProdPdf *>(p), elem);
1661 }
1662};
1663
1664class HistFactoryStreamer_SumPdf : public RooFit::JSONIO::Exporter {
1665public:
1666 bool autoExportDependants() const override { return false; }
1668 {
1669 std::vector<RooAbsPdf *> constraints;
1670 return tryExportHistFactory(tool, sumpdf->GetName(), sumpdf, constraints, elem);
1671 }
1672 std::string const &key() const override
1673 {
1674 static const std::string keystring = "histfactory_dist";
1675 return keystring;
1676 }
1677 bool exportObject(RooJSONFactoryWSTool *tool, const RooAbsArg *p, JSONNode &elem) const override
1678 {
1679 return tryExport(tool, static_cast<const RooRealSumPdf *>(p), elem);
1680 }
1681};
1682
1683STATIC_EXECUTE([]() {
1684 using namespace RooFit::JSONIO;
1685
1686 registerImporter<HistFactoryImporter>("histfactory_dist", true);
1688 registerImporter<FlexibleInterpVarFactory>("interpolation0d", true);
1693});
1694
1695} // namespace
bool startsWith(std::string_view str, std::string_view prefix)
bool endsWith(std::string_view str, std::string_view suffix)
#define d(i)
Definition RSha256.hxx:102
#define c(i)
Definition RSha256.hxx:101
#define g(i)
Definition RSha256.hxx:105
#define h(i)
Definition RSha256.hxx:106
#define e(i)
Definition RSha256.hxx:103
ROOT::RRangeCast< T, false, Range_t > static_range_cast(Range_t &&coll)
double toDouble(const char *s)
size_t size(const MatrixT &matrix)
retrieve the size of a square matrix
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
winID h TVirtualViewer3D TVirtualGLPainter p
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void data
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t r
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void value
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void funcs
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t modifier
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t Int_t Int_t Window_t TString Int_t GCValues_t GetPrimarySelectionOwner GetDisplay GetScreen GetColormap GetNativeEvent const char const char dpyName wid window const char font_name cursor keysym reg const char only_if_exist regb h Point_t winding char text const char depth char const char Int_t count const char ColorStruct_t color const char Pixmap_t Pixmap_t PictureAttributes_t attr const char char ret_data h unsigned char height h Atom_t Int_t ULong_t ULong_t unsigned char prop_list Atom_t Atom_t Atom_t Time_t type
char name[80]
Definition TGX11.cxx:145
#define hi
A class which maps the current values of a RooRealVar (or a set of RooRealVars) to one of a number of...
The PiecewiseInterpolation is a class that can morph distributions into each other,...
static TClass * Class()
void setPositiveDefinite(bool flag=true)
const_iterator begin() const
const_iterator end() const
Common abstract base class for objects that represent a value and a "shape" in RooFit.
Definition RooAbsArg.h:76
bool dependsOn(const RooAbsCollection &serverList, const RooAbsArg *ignoreArg=nullptr, bool valueOnly=false) const
Test whether we depend on (ie, are served by) any object in the specified collection.
bool isConstant() const
Check if the "Constant" attribute is set.
Definition RooAbsArg.h:283
Abstract container object that can hold multiple RooAbsArg objects.
virtual bool add(const RooAbsArg &var, bool silent=false)
Add the specified argument to list.
Abstract interface for all probability density functions.
Definition RooAbsPdf.h:32
Abstract base class for objects that represent a real value and implements functionality common to al...
Definition RooAbsReal.h:63
RooArgList is a container object that can hold multiple RooAbsArg objects.
Definition RooArgList.h:22
RooArgSet is a container object that can hold multiple RooAbsArg objects.
Definition RooArgSet.h:24
Returns the bin width (or volume) given a RooHistFunc.
Represents a constant real-valued object.
Definition RooConstVar.h:23
Container class to hold N-dimensional binned data.
Definition RooDataHist.h:40
A RooFormulaVar is a generic implementation of a real-valued object, which takes a RooArgList of serv...
RooAbsArg * getParameter(const char *name) const
Return pointer to parameter with given name.
const char * expression() const
const RooArgList & dependents() const
size_t nParameters() const
Return the number of parameters.
Plain Gaussian p.d.f.
Definition RooGaussian.h:24
A real-valued function sampled from a multidimensional histogram.
Definition RooHistFunc.h:31
When using RooFit, statistical models can be conveniently handled and stored as a RooWorkspace.
static void fillSeq(RooFit::Detail::JSONNode &node, RooAbsCollection const &coll, size_t nMax=-1)
static std::unique_ptr< RooDataHist > readBinnedData(const RooFit::Detail::JSONNode &n, const std::string &namecomp, RooArgSet const &vars)
Read binned data from the JSONNode and create a RooDataHist object.
static RooFit::Detail::JSONNode & appendNamedChild(RooFit::Detail::JSONNode &node, std::string const &name)
static void exportArray(std::size_t n, double const *contents, RooFit::Detail::JSONNode &output)
Export an array of doubles to a JSONNode.
static void exportAxis(RooFit::Detail::JSONNode &obsNode, RooRealVar const &var)
Export the name and binning of a RooRealVar to a JSONNode.
static void error(const char *s)
Writes an error message to the RooFit message service and throws a runtime_error.
static std::string name(const RooFit::Detail::JSONNode &n)
static std::ostream & warning(const std::string &s)
Writes a warning message to the RooFit message service.
static RooArgSet readAxes(const RooFit::Detail::JSONNode &node)
Read axes from the JSONNode and create a RooArgSet representing them.
RooFit Lognormal PDF.
Poisson pdf.
Definition RooPoisson.h:19
RooAbsReal const & getMean() const
Get the mean parameter.
Definition RooPoisson.h:48
Efficient implementation of a product of PDFs of the form.
Definition RooProdPdf.h:36
static TClass * Class()
Represents the product of a given set of RooAbsReal objects.
Definition RooProduct.h:29
Implements a PDF constructed from a sum of functions:
static TClass * Class()
Variable that can be changed from the outside.
Definition RooRealVar.h:37
void setError(double value)
Definition RooRealVar.h:61
This class encapsulates all information for the statistical interpretation of one experiment.
Configuration for a constrained, coherent shape variation of affected samples.
Configuration for an un- constrained overall systematic to scale sample normalisations.
Definition Measurement.h:60
Constrained bin-by-bin variation of affected histogram.
Persistable container for RooFit projects.
TObject * obj(RooStringView name) const
Return any type of object (RooAbsArg, RooAbsData or generic object) with given name)
RooAbsPdf * pdf(RooStringView name) const
Retrieve p.d.f (RooAbsPdf) with given name. A null pointer is returned if not found.
RooAbsReal * function(RooStringView name) const
Retrieve function (RooAbsReal) with given name. Note that all RooAbsPdfs are also RooAbsReals....
RooFactoryWSTool & factory()
Return instance to factory tool.
bool import(const RooAbsArg &arg, const RooCmdArg &arg1={}, const RooCmdArg &arg2={}, const RooCmdArg &arg3={}, const RooCmdArg &arg4={}, const RooCmdArg &arg5={}, const RooCmdArg &arg6={}, const RooCmdArg &arg7={}, const RooCmdArg &arg8={}, const RooCmdArg &arg9={})
Import a RooAbsArg object, e.g.
const char * GetName() const override
Returns name of object.
Definition TNamed.h:49
Basic string class.
Definition TString.h:138
Bool_t Contains(const char *pat, ECaseCompare cmp=kExact) const
Definition TString.h:641
RooCmdArg RecycleConflictNodes(bool flag=true)
RooCmdArg Conditional(const RooArgSet &pdfSet, const RooArgSet &depSet, bool depsAreCond=false)
const Int_t n
Definition legend1.C:16
double gamma(double x)
void function(const Char_t *name_, T fun, const Char_t *docstring=0)
Definition RExports.h:168
void configureConstrainedGammas(RooArgList const &gammas, std::span< const double > relSigmas, double minSigma)
Configure constrained gamma parameters for fitting.
CreateGammaConstraintsOutput createGammaConstraints(RooArgList const &paramList, std::span< const double > relSigmas, double minSigma, Constraint::Type type)
#define STATIC_EXECUTE(MY_FUNC)
TLine l
Definition textangle.C:4
static uint64_t sum(uint64_t i)
Definition Factory.cxx:2338
static void output()