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 <charconv>
37#include <iterator>
38#include <map>
39#include <optional>
40#include <regex>
41#include <tuple>
42
43#include "static_execute.h"
44#include "JSONIOUtils.h"
45
47
48using namespace RooStats::HistFactory;
49using namespace RooStats::HistFactory::Detail;
51
52namespace {
53
54double round_prec(double d, int nSig)
55{
56 if (d == 0.0)
57 return 0.0;
58 int ndigits = std::floor(std::log10(std::abs(d))) + 1 - nSig;
59 double sf = std::pow(10, ndigits);
60 if (std::abs(d / sf) < 2)
61 ndigits--;
62 return sf * std::round(d / sf);
63}
64
65// To avoid repeating the same string literals that can potentially get out of
66// sync.
67namespace Literals {
68constexpr auto staterror = "staterror";
69}
70
71struct Interpolation {
72 std::string type;
73 std::string in;
74 std::optional<std::string> out;
75
76 bool operator==(const Interpolation &other) const
77 {
78 return std::tie(type, in, out) == std::tie(other.type, other.in, other.out);
79 }
80
81 bool operator!=(const Interpolation &other) const { return !(*this == other); }
82
83 bool operator<(const Interpolation &other) const
84 {
85 return std::tie(type, in, out) < std::tie(other.type, other.in, other.out);
86 }
87};
88
89const Interpolation additivePiecewiseLinear{"add", "poly1", std::nullopt};
90const Interpolation multiplicativePiecewiseExponential{"mult", "exp", std::nullopt};
91const Interpolation additiveQuadraticLinear{"add", "poly2", "poly1"};
92const Interpolation additivePolynomialLinear{"add", "poly6", "poly1"};
93const Interpolation multiplicativePolynomialExponential{"mult", "poly6", "exp"};
94const Interpolation multiplicativePolynomialLinear{"mult", "poly6", "poly1"};
95
96std::string interpolationString(const Interpolation &interpolation)
97{
98 std::stringstream ss;
99 ss << R"({"type":")" << interpolation.type << R"(","in":")" << interpolation.in << R"(","out":)";
100 if (interpolation.out) {
101 ss << '"' << *interpolation.out << '"';
102 } else {
103 ss << "null";
104 }
105 ss << '}';
106 return ss.str();
107}
108
109bool isInterpolationFunction(std::string_view function)
110{
111 return function == "poly1" || function == "poly2" || function == "poly6" || function == "exp";
112}
113
114Interpolation readInterpolation(const JSONNode &node, const std::string &context)
115{
116 if (!node.is_map()) {
117 RooJSONFactoryWSTool::error(context + " must be a struct with components 'type', 'in', and 'out'");
118 }
119 for (const char *component : {"type", "in", "out"}) {
120 if (!node.has_child(component)) {
121 RooJSONFactoryWSTool::error(context + " does not define the required '" + component + "' component");
122 }
123 }
124
125 const auto &typeNode = node["type"];
126 const auto &inNode = node["in"];
127 const auto &outNode = node["out"];
128 if (typeNode.is_container() || typeNode.is_null() || inNode.is_container() || inNode.is_null()) {
129 RooJSONFactoryWSTool::error(context + " components 'type' and 'in' must be strings");
130 }
131
132 Interpolation interpolation{typeNode.val(), inNode.val(), std::nullopt};
133 if (interpolation.type != "add" && interpolation.type != "mult") {
134 RooJSONFactoryWSTool::error(context + " has unknown interpolation type '" + interpolation.type + "'");
135 }
136 if (!isInterpolationFunction(interpolation.in)) {
137 RooJSONFactoryWSTool::error(context + " has unknown interpolation function '" + interpolation.in + "'");
138 }
139
140 if (!outNode.is_null()) {
141 if (outNode.is_container()) {
142 RooJSONFactoryWSTool::error(context + " component 'out' must be a string or null");
143 }
144 interpolation.out = outNode.val();
145 if (!isInterpolationFunction(*interpolation.out)) {
146 RooJSONFactoryWSTool::error(context + " has unknown extrapolation function '" + *interpolation.out + "'");
147 }
148 }
149
150 return interpolation;
151}
152
153void writeInterpolation(JSONNode &node, const Interpolation &interpolation)
154{
155 node.set_map();
156 node["type"] << interpolation.type;
157 node["in"] << interpolation.in;
158 if (interpolation.out) {
159 node["out"] << *interpolation.out;
160 } else {
161 node["out"].set_null();
162 }
163}
164
165int readLegacyInterpolationCode(const JSONNode &node, const std::string &context)
166{
167 if (node.is_container() || node.is_null() || !node.has_val()) {
168 RooJSONFactoryWSTool::error(context + " must be a structured interpolation or a legacy integer code");
169 }
170
171 const std::string value = node.val();
172 int code = 0;
173 const auto result = std::from_chars(value.data(), value.data() + value.size(), code);
174 if (result.ec != std::errc{} || result.ptr != value.data() + value.size()) {
175 RooJSONFactoryWSTool::error(context + " has invalid legacy interpolation code '" + value + "'");
176 }
177 return code;
178}
179
180enum class InterpolationClass {
181 Piecewise,
182 Flexible
183};
184
185// Single source of truth for the mapping between the structured HS3 interpolation
186// descriptors and the RooFit integer codes of PiecewiseInterpolation and
187// FlexibleInterpVar. A code of `kUnrepresentable` means the descriptor cannot be
188// expressed by that class: FlexibleInterpVar internally remaps code 4 to code 5,
189// so it has no additive-linear or multiplicative-linear poly6 variant.
190struct InterpolationCodes {
191 const Interpolation &descriptor;
192 int piecewise;
193 int flexible;
194};
195
196constexpr int kUnrepresentable = -1;
197
198// clang-format off
199const std::vector<InterpolationCodes> interpolationTable{
200 {additivePiecewiseLinear, 0, 0},
201 {multiplicativePiecewiseExponential, 1, 1},
202 {additiveQuadraticLinear, 2, 2},
203 {additivePolynomialLinear, 4, kUnrepresentable},
204 {multiplicativePolynomialExponential, 5, 4},
205 {multiplicativePolynomialLinear, 6, kUnrepresentable},
206};
207// clang-format on
208
209int codeForClass(const InterpolationCodes &row, InterpolationClass interpolationClass)
210{
211 return interpolationClass == InterpolationClass::Piecewise ? row.piecewise : row.flexible;
212}
213
214const char *interpolationClassName(InterpolationClass interpolationClass)
215{
216 return interpolationClass == InterpolationClass::Piecewise ? "PiecewiseInterpolation" : "FlexibleInterpVar";
217}
218
219Interpolation interpolationFromCode(int code, InterpolationClass interpolationClass, const std::string &context)
220{
221 // Code 3 was historically an unimplemented alias of code 2 for both classes,
222 // and FlexibleInterpVar treats code 5 identically to its canonical code 4.
223 if (code == 3) {
224 code = 2;
225 }
226 if (interpolationClass == InterpolationClass::Flexible && code == 5) {
227 code = 4;
228 }
229 for (const auto &row : interpolationTable) {
230 const int rowCode = codeForClass(row, interpolationClass);
231 if (rowCode != kUnrepresentable && rowCode == code) {
232 return row.descriptor;
233 }
234 }
235 RooJSONFactoryWSTool::error(context + " has unsupported " + interpolationClassName(interpolationClass) + " code " +
236 std::to_string(code));
237}
238
239int codeFromInterpolation(const Interpolation &interpolation, InterpolationClass interpolationClass,
240 const std::string &context)
241{
242 for (const auto &row : interpolationTable) {
243 const int rowCode = codeForClass(row, interpolationClass);
244 if (rowCode != kUnrepresentable && row.descriptor == interpolation) {
245 return rowCode;
246 }
247 }
248 RooJSONFactoryWSTool::error(context + " " + interpolationString(interpolation) + " cannot be represented by " +
249 interpolationClassName(interpolationClass));
250}
251
252void writeInterpolations(JSONNode &node, const std::vector<int> &codes, InterpolationClass interpolationClass,
253 const std::string &context)
254{
255 auto &interpolations = node.set_seq();
256 if (codes.empty()) {
257 return;
258 }
259
260 std::vector<Interpolation> descriptors;
261 descriptors.reserve(codes.size());
262 for (std::size_t i = 0; i < codes.size(); ++i) {
263 descriptors.push_back(
264 interpolationFromCode(codes[i], interpolationClass, context + " at parameter index " + std::to_string(i)));
265 }
266
267 bool allEqual = true;
268 for (std::size_t i = 1; i < descriptors.size(); ++i) {
269 if (descriptors[i] != descriptors.front()) {
270 allEqual = false;
271 break;
272 }
273 }
274
275 const std::size_t outputSize = allEqual ? 1 : descriptors.size();
276 for (std::size_t i = 0; i < outputSize; ++i) {
277 writeInterpolation(interpolations.append_child(), descriptors[i]);
278 }
279}
280
281std::vector<int> readInterpolations(const JSONNode &object, std::size_t nParameters,
282 InterpolationClass interpolationClass, const std::string &context)
283{
284 if (const auto *interpolations = object.find("interpolations")) {
285 if (!interpolations->is_seq()) {
286 RooJSONFactoryWSTool::error(context + " component 'interpolations' must be an array");
287 }
288
289 const std::size_t size = interpolations->num_children();
290 const bool validSize = nParameters == 0 ? size == 0 : size == 1 || size == nParameters;
291 if (!validSize) {
293 " component 'interpolations' must contain either one descriptor or one "
294 "descriptor per parameter (got " +
295 std::to_string(size) + " for " + std::to_string(nParameters) + " parameters)");
296 }
297
298 std::vector<int> codes;
299 codes.reserve(size);
300 std::size_t i = 0;
301 for (const auto &node : interpolations->children()) {
302 const std::string entryContext = context + " component 'interpolations' at index " + std::to_string(i);
303 codes.push_back(
304 codeFromInterpolation(readInterpolation(node, entryContext), interpolationClass, entryContext));
305 ++i;
306 }
307 if (size == 1) {
308 codes.resize(nParameters, codes.front());
309 }
310 return codes;
311 }
312
313 std::vector<int> codes(nParameters, 0);
314 if (const auto *legacyCodes = object.find("interpolationCodes")) {
315 if (!legacyCodes->is_seq()) {
316 RooJSONFactoryWSTool::error(context + " legacy component 'interpolationCodes' must be an array");
317 }
318 if (legacyCodes->num_children() != nParameters) {
320 " legacy component 'interpolationCodes' must contain one code per "
321 "parameter (got " +
322 std::to_string(legacyCodes->num_children()) + " for " +
323 std::to_string(nParameters) + " parameters)");
324 }
325
326 std::size_t i = 0;
327 for (const auto &node : legacyCodes->children()) {
328 const std::string entryContext =
329 context + " legacy component 'interpolationCodes' at index " + std::to_string(i);
330 const Interpolation interpolation =
331 interpolationFromCode(readLegacyInterpolationCode(node, entryContext), interpolationClass, entryContext);
332 codes[i] = codeFromInterpolation(interpolation, interpolationClass, entryContext);
333 ++i;
334 }
335 }
336 return codes;
337}
338
339int interpolationCode(const JSONNode &modifier, const std::optional<Interpolation> &defaultInterpolation,
340 InterpolationClass interpolationClass, const std::string &context)
341{
342 const auto toCode = [&](const Interpolation &interpolation) {
343 return codeFromInterpolation(interpolation, interpolationClass, context);
344 };
345
346 if (const auto *interpolationNode = modifier.find("interpolation")) {
347 if (interpolationNode->is_map()) {
348 return toCode(readInterpolation(*interpolationNode, context));
349 }
350 const int legacyCode = readLegacyInterpolationCode(*interpolationNode, context);
351 const Interpolation interpolation = interpolationFromCode(legacyCode, interpolationClass, context);
352 return toCode(interpolation);
353 }
354 if (defaultInterpolation) {
355 return toCode(*defaultInterpolation);
356 }
357
358 // Before structured interpolation was introduced, both modifier classes
359 // used the integer code 4 as their implicit default. The meaning of code 4
360 // is class-dependent.
361 return 4;
362}
363
364void erasePrefix(std::string &str, std::string_view prefix)
365{
366 if (startsWith(str, prefix)) {
367 str.erase(0, prefix.size());
368 }
369}
370
371bool eraseSuffix(std::string &str, std::string_view suffix)
372{
373 if (endsWith(str, suffix)) {
374 str.erase(str.size() - suffix.size());
375 return true;
376 } else {
377 return false;
378 }
379}
380
381template <class Coll>
382void sortByName(Coll &coll)
383{
384 std::sort(coll.begin(), coll.end(), [](auto &l, auto &r) { return l.name < r.name; });
385}
386
387template <class T>
388T *findClient(RooAbsArg *gamma)
389{
390 for (const auto &client : gamma->clients()) {
391 if (auto casted = dynamic_cast<T *>(client)) {
392 return casted;
393 } else {
394 T *c = findClient<T>(client);
395 if (c)
396 return c;
397 }
398 }
399 return nullptr;
400}
401
402RooAbsPdf *findConstraint(RooAbsArg *g)
403{
404 if (!g)
405 return nullptr;
406 if (auto *constraint = findClient<RooPoisson>(g))
407 return constraint;
408 if (auto *constraint = findClient<RooGaussian>(g))
409 return constraint;
410 return findClient<RooLognormal>(g);
411}
412
413inline std::string defaultGammaName(std::string const &sysname, std::size_t i)
414{
415 return "gamma_" + sysname + "_bin_" + std::to_string(i);
416}
417
418/// Export the names of the gamma parameters to the modifier struct
419void exportGammaParameters(JSONNode &mod, std::vector<RooAbsReal *> const &params)
420{
421 std::vector<std::string> paramNames;
422 for (RooAbsReal *param : params) {
423 paramNames.emplace_back(param->GetName());
424 }
425 mod["parameters"].fill_seq(paramNames);
426}
427
428RooRealVar &createNominal(RooWorkspace &ws, std::string const &parname, double val, double min, double max)
429{
430 RooRealVar &nom = getOrCreate<RooRealVar>(ws, "nom_" + parname, val, min, max);
431 nom.setConstant(true);
432 return nom;
433}
434
435/// Get the conventional name of the constraint pdf for a constrained
436/// parameter.
437std::string constraintName(std::string const &paramName)
438{
439 return paramName + "Constraint";
440}
441
442bool isLegacyConstraintType(std::string const &value)
443{
444 return value == "Gauss" || value == "Poisson" || value == "Const" || value == "Lognormal";
445}
446
447RooAbsPdf *findNamedConstraint(RooJSONFactoryWSTool &tool, std::string const &constraintName, std::string const &sample)
448{
449 if (auto *constraint = tool.workspace()->pdf(constraintName)) {
450 return constraint;
451 }
452
453 try {
454 return tool.request<RooAbsPdf>(constraintName, sample);
456 if (err.child() != constraintName) {
457 throw;
458 }
459 }
460
461 return nullptr;
462}
463
464RooAbsPdf &createLegacyConstraint(RooJSONFactoryWSTool &tool, const JSONNode &mod, RooRealVar &param,
465 std::string const &constraintType)
466{
467 if (constraintType == "Gauss") {
468 param.setError(1.0);
469 return getOrCreate<RooGaussian>(*tool.workspace(), constraintName(param.GetName()), param,
470 *tool.workspace()->var(std::string("nom_") + param.GetName()), 1.);
471 }
472
473 RooJSONFactoryWSTool::error("legacy constraint value '" + constraintType + "' for modifier '" +
475 "' is a known constraint type, but it cannot be resolved in this context");
476}
477
478ParamHistFunc &createPHF(const std::string &phfname, std::string const &sysname,
479 const std::vector<std::string> &parnames, const std::vector<double> &vals,
480 RooJSONFactoryWSTool &tool, RooAbsCollection &constraints, const RooArgSet &observables,
481 const std::string &constraintType, double gammaMin, double gammaMax, double minSigma,
482 bool createConstraints = true)
483{
484 RooWorkspace &ws = *tool.workspace();
485
486 size_t n = std::max(vals.size(), parnames.size());
487 RooArgList gammas;
488 for (std::size_t i = 0; i < n; ++i) {
489 const std::string name = parnames.empty() ? defaultGammaName(sysname, i) : parnames[i];
490 auto *e = dynamic_cast<RooAbsReal *>(ws.obj(name.c_str()));
491 if (e)
492 gammas.add(*e);
493 else
494 gammas.add(getOrCreate<RooRealVar>(ws, name, 1., gammaMin, gammaMax));
495 }
496
497 auto &phf = tool.wsEmplace<ParamHistFunc>(phfname, observables, gammas);
498
499 if (vals.size() > 0) {
500 if (!createConstraints) {
501 configureConstrainedGammas(gammas, vals, minSigma);
502 } else if (constraintType != "Const") {
503 auto constraintsInfo = createGammaConstraints(
504 gammas, vals, minSigma, constraintType == "Poisson" ? Constraint::Poisson : Constraint::Gaussian);
505 for (auto const &term : constraintsInfo.constraints) {
507 constraints.add(*ws.pdf(term->GetName()));
508 }
509 } else {
510 for (auto *gamma : static_range_cast<RooRealVar *>(gammas)) {
511 gamma->setConstant(true);
512 }
513 }
514 }
515
516 return phf;
517}
518
519/// Find the staterror modifier of a sample, or return nullptr if there is none.
520const JSONNode *findStaterror(const JSONNode &comp)
521{
522 if (comp.has_child("modifiers")) {
523 for (const auto &mod : comp["modifiers"].children()) {
524 if (mod["type"].val() == ::Literals::staterror)
525 return &mod;
526 }
527 }
528 return nullptr;
529}
530
531RooAbsPdf &
532getOrCreateConstraint(RooJSONFactoryWSTool &tool, const JSONNode &mod, RooRealVar &param, const std::string &sample)
533{
534 JSONNode const *constrName = mod.find("constraint_name");
535 if (constrName) {
536 auto constraint_name = constrName->val();
537 auto constraint = findNamedConstraint(tool, constraint_name, sample);
538 if (!constraint) {
539 RooJSONFactoryWSTool::error("unable to find definition of of constraint '" + constraint_name +
540 "' for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
541 }
542 if (auto gauss = dynamic_cast<RooGaussian *const>(constraint)) {
543 param.setError(gauss->getSigma().getVal());
544 }
545 return *constraint;
546 }
547
548 if (auto constr = mod.find("constraint")) {
549 std::string constraintValue = constr->val();
550 if (auto *constraint = findNamedConstraint(tool, constraintValue, sample)) {
551 if (auto gauss = dynamic_cast<RooGaussian *const>(constraint)) {
552 param.setError(gauss->getSigma().getVal());
553 }
554 return *constraint;
555 }
556
557 if (isLegacyConstraintType(constraintValue)) {
558 return createLegacyConstraint(tool, mod, param, constraintValue);
559 }
560
561 RooJSONFactoryWSTool::error("unable to resolve constraint value '" + constraintValue + "' for modifier '" +
563 "': this looks like a legacy workspace where the 'constraint' field is neither a "
564 "constraint pdf name nor a supported legacy constraint type");
565 }
566
567 std::string constraint_type = "Gauss";
568 if (auto constrType = mod.find("constraint_type")) {
569 constraint_type = constrType->val();
570 }
571 if (isLegacyConstraintType(constraint_type)) {
572 return createLegacyConstraint(tool, mod, param, constraint_type);
573 }
574 RooJSONFactoryWSTool::error("unknown or invalid constraint for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
575}
576double poissonTau(RooPoisson const &constraint, RooAbsArg const &gamma)
577{
578 auto const *mean = dynamic_cast<RooProduct const *>(&constraint.getMean());
579 if (!mean) {
580 RooJSONFactoryWSTool::error("Poisson gamma constraint mean is not a RooProduct: " +
581 std::string(constraint.GetName()));
582 }
583
584 for (RooAbsArg *arg : mean->servers()) {
585 if (arg == &gamma) {
586 continue;
587 }
588
589 if (auto const *tau = dynamic_cast<RooConstVar const *>(arg)) {
590 return tau->getVal();
591 }
592
593 // Imported workspaces can sometimes represent
594 // constants as constant RooRealVars.
595 if (auto const *real = dynamic_cast<RooAbsReal const *>(arg)) {
596 if (real->isConstant() || endsWith(std::string(real->GetName()), "_tau")) {
597 return real->getVal();
598 }
599 }
600 }
601
602 RooJSONFactoryWSTool::error("Could not find tau component in Poisson gamma constraint mean: " +
603 std::string(constraint.GetName()));
604 return std::numeric_limits<double>::quiet_NaN();
605}
606
607// Returns the relative uncertainty encoded by a gamma constraint pdf. Only RooPoisson (via its tau) and RooGaussian
608// (via sigma/mean) are supported; anything else raises an error.
609double constraintRelError(RooAbsPdf const &constraint, RooAbsArg const &gamma)
610{
611 if (auto constraintP = dynamic_cast<RooPoisson const *>(&constraint)) {
612 return 1. / std::sqrt(poissonTau(*constraintP, gamma));
613 }
614 if (auto constraintG = dynamic_cast<RooGaussian const *>(&constraint)) {
615 return constraintG->getSigma().getVal() / constraintG->getMean().getVal();
616 }
617 RooJSONFactoryWSTool::error("currently, only RooPoisson and RooGaussian are supported as constraint types");
618 return std::numeric_limits<double>::quiet_NaN();
619}
620
621bool importHistSample(RooJSONFactoryWSTool &tool, RooDataHist &dh, RooArgSet const &varlist,
622 RooAbsArg const *mcStatObject, const std::string &fprefix, const JSONNode &p,
623 const std::optional<Interpolation> &defaultInterpolation, RooArgSet &constraints)
624{
625 RooWorkspace &ws = *tool.workspace();
626
627 std::string sampleName = RooJSONFactoryWSTool::name(p);
628 std::string prefixedName = fprefix + "_" + sampleName;
629
630 std::string channelName = fprefix;
631 erasePrefix(channelName, "model_");
632
633 if (!p.has_child("data")) {
634 RooJSONFactoryWSTool::error("sample '" + sampleName + "' does not define a 'data' key");
635 }
636
637 auto &hf = tool.wsEmplace<RooHistFunc>("hist_" + prefixedName, varlist, dh);
639
640 RooArgList shapeElems;
641 RooArgList normElems;
642
643 shapeElems.add(tool.wsEmplace<RooBinWidthFunction>(prefixedName + "_binWidth", hf, true));
644
645 if (findStaterror(p)) {
646 shapeElems.add(*mcStatObject);
647 }
648
649 if (p.has_child("modifiers")) {
650 RooArgList overall_nps;
651 std::vector<double> overall_low;
652 std::vector<double> overall_high;
653 std::vector<int> overall_interp;
654
655 RooArgList histNps;
656 RooArgList histoLo;
657 RooArgList histoHi;
658 std::vector<int> histoInterp;
659
660 int idx = 0;
661 for (const auto &mod : p["modifiers"].children()) {
662 std::string const &modtype = mod["type"].val();
663 std::string const &sysname =
664 mod.has_child("name")
665 ? mod["name"].val()
666 : (mod.has_child("parameter") ? mod["parameter"].val() : "syst_" + std::to_string(idx));
667 ++idx;
668 if (modtype == "staterror") {
669 // this is dealt with at a different place, ignore it for now
670 } else if (modtype == "normfactor") {
671 RooRealVar &constrParam = getOrCreate<RooRealVar>(ws, sysname, 1., -3, 5);
672 constrParam.setError(0.0);
673 normElems.add(constrParam);
674 if (mod.has_child("constraint") || mod.has_child("constraint_name") || mod.has_child("constraint_type")) {
675 // for norm factors, constraints are optional
676 constraints.add(getOrCreateConstraint(tool, mod, constrParam, sampleName));
677 }
678 } else if (modtype == "normsys") {
679 auto *parameter = mod.find("parameter");
680 std::string parname(parameter ? parameter->val() : "alpha_" + sysname);
681 createNominal(ws, parname, 0.0, -10, 10);
682 auto &par = getOrCreate<RooRealVar>(ws, parname, 0., -5, 5);
683 overall_nps.add(par);
684 auto &data = mod["data"];
685 const std::string context = "interpolation for normsys modifier '" + sysname + "' in sample '" +
686 sampleName + "' of channel '" + channelName + "'";
687 const int interp = interpolationCode(mod, defaultInterpolation, InterpolationClass::Flexible, context);
688 double low = data["lo"].val_double();
689 double high = data["hi"].val_double();
690
691 // the below contains a a hack to cut off variations that go below 0
692 // This is needed because FlexibleInterpVar code 4 interpolates in log-space. Hence, values <= 0 result in
693 // NaN, which propagates throughout the model and causes evaluations to fail. If you know a nicer way to
694 // solve this, please go ahead and fix the lines below.
695 if (interp == 4 && low <= 0)
696 low = std::numeric_limits<double>::epsilon();
697 if (interp == 4 && high <= 0)
698 high = std::numeric_limits<double>::epsilon();
699
700 overall_low.push_back(low);
701 overall_high.push_back(high);
702 overall_interp.push_back(interp);
703
704 constraints.add(getOrCreateConstraint(tool, mod, par, sampleName));
705 } else if (modtype == "histosys") {
706 auto *parameter = mod.find("parameter");
707 std::string parname(parameter ? parameter->val() : "alpha_" + sysname);
708 createNominal(ws, parname, 0.0, -10, 10);
709 auto &par = getOrCreate<RooRealVar>(ws, parname, 0., -5, 5);
710 histNps.add(par);
711 auto &data = mod["data"];
712 histoLo.add(tool.wsEmplace<RooHistFunc>(
713 sysname + "Low_" + prefixedName, varlist,
714 RooJSONFactoryWSTool::readBinnedData(data["lo"], sysname + "Low_" + prefixedName, varlist)));
715 histoHi.add(tool.wsEmplace<RooHistFunc>(
716 sysname + "High_" + prefixedName, varlist,
717 RooJSONFactoryWSTool::readBinnedData(data["hi"], sysname + "High_" + prefixedName, varlist)));
718 const std::string context = "interpolation for histosys modifier '" + sysname + "' in sample '" +
719 sampleName + "' of channel '" + channelName + "'";
720 histoInterp.push_back(interpolationCode(mod, defaultInterpolation, InterpolationClass::Piecewise, context));
721 constraints.add(getOrCreateConstraint(tool, mod, par, sampleName));
722 } else if (modtype == "shapesys" || modtype == "shapefactor") {
723 std::string funcName = channelName + "_" + sysname + "_ShapeSys";
724 // funcName should be "<channel_name>_<sysname>_ShapeSys"
725 std::vector<double> vals;
726 if (mod["data"].has_child("vals")) {
727 for (const auto &v : mod["data"]["vals"].children()) {
728 vals.push_back(v.val_double());
729 }
730 }
731 std::vector<std::string> parnames;
732 for (const auto &v : mod["parameters"].children()) {
733 parnames.push_back(v.val());
734 }
735 if (vals.empty() && parnames.empty()) {
736 RooJSONFactoryWSTool::error("unable to instantiate shapesys '" + sysname +
737 "' with neither values nor parameters!");
738 }
739 std::string constraint = "unknown";
740 std::vector<RooAbsPdf *> constraintPdfs;
741 bool const hasConstraintList = mod.has_child("constraints");
742 if (hasConstraintList) {
743 for (const auto &v : mod["constraints"].children()) {
744 if (v.is_null()) {
745 constraintPdfs.push_back(nullptr);
746 } else {
747 std::string constraintName = v.val();
748 auto *constraintPdf = findNamedConstraint(tool, constraintName, sampleName);
749 if (!constraintPdf) {
750 RooJSONFactoryWSTool::error("unable to find definition of constraint '" + constraintName +
751 "' for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
752 }
753 constraintPdfs.push_back(constraintPdf);
754 }
755 }
756 std::size_t const nGammas = std::max(vals.size(), parnames.size());
757 if (constraintPdfs.size() != nGammas) {
758 std::stringstream ss;
759 ss << "modifier '" << RooJSONFactoryWSTool::name(mod) << "' has " << constraintPdfs.size()
760 << " constraints, but " << nGammas << " parameters";
762 }
763 } else if (mod.has_child("constraint_type")) {
764 constraint = mod["constraint_type"].val();
765 } else if (mod.has_child("constraint")) {
766 std::string constraintValue = mod["constraint"].val();
767 if (isLegacyConstraintType(constraintValue)) {
768 constraint = constraintValue;
769 } else {
770 RooJSONFactoryWSTool::error("unable to resolve constraint value '" + constraintValue +
771 "' for modifier '" + RooJSONFactoryWSTool::name(mod) +
772 "': this looks like a legacy workspace where the 'constraint' field is "
773 "not a supported legacy constraint type");
774 }
775 }
776 shapeElems.add(createPHF(funcName, sysname, parnames, vals, tool, constraints, varlist, constraint,
778 /*createConstraints=*/!hasConstraintList));
779 for (auto *constraintPdf : constraintPdfs) {
780 if (constraintPdf) {
781 constraints.add(*constraintPdf);
782 }
783 }
784 } else if (modtype == "custom") {
785 RooAbsReal *obj = ws.function(sysname);
786 if (!obj) {
787 RooJSONFactoryWSTool::error("unable to find custom modifier '" + sysname + "'");
788 }
789 if (obj->dependsOn(varlist)) {
790 shapeElems.add(*obj);
791 } else {
792 normElems.add(*obj);
793 }
794 } else {
795 RooJSONFactoryWSTool::error("modifier '" + sysname + "' of unknown type '" + modtype + "'");
796 }
797 }
798
799 std::string interpName = sampleName + "_" + channelName + "_epsilon";
800 if (!overall_nps.empty()) {
801 auto &v = tool.wsEmplace<RooStats::HistFactory::FlexibleInterpVar>(interpName, overall_nps, 1., overall_low,
802 overall_high, overall_interp);
803 normElems.add(v);
804 }
805 if (!histNps.empty()) {
806 auto &v = tool.wsEmplace<PiecewiseInterpolation>("histoSys_" + prefixedName, hf, histoLo, histoHi, histNps,
807 histoInterp);
809 shapeElems.add(v);
810 } else {
811 shapeElems.add(hf);
812 }
813 }
814
815 tool.wsEmplace<RooProduct>(prefixedName + "_shapes", shapeElems);
816 if (!normElems.empty()) {
817 tool.wsEmplace<RooProduct>(prefixedName + "_scaleFactors", normElems);
818 } else {
819 ws.factory("RooConstVar::" + prefixedName + "_scaleFactors(1.)");
820 }
821
822 return true;
823}
824
825class HistFactoryImporter : public RooFit::JSONIO::Importer {
826public:
827 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
828 {
829 std::string name = RooJSONFactoryWSTool::name(p);
830 if (!p.has_child("samples")) {
831 RooJSONFactoryWSTool::error("no samples in '" + name + "', skipping.");
832 }
833 double statErrThresh = 0;
834 std::string statErrType = "Poisson";
835 std::optional<Interpolation> defaultInterpolation;
836 if (p.has_child("default_interpolation")) {
837 defaultInterpolation =
838 readInterpolation(p["default_interpolation"], "default_interpolation of channel '" + name + "'");
839 }
840 if (p.has_child(::Literals::staterror)) {
841 auto &staterr = p[::Literals::staterror];
842 if (staterr.has_child("relThreshold"))
843 statErrThresh = staterr["relThreshold"].val_double();
844 if (staterr.has_child("constraint_type"))
845 statErrType = staterr["constraint_type"].val();
846 }
847 std::vector<double> sumW;
848 std::vector<double> sumW2;
849 std::vector<std::string> gammaParnames;
851
852 std::string fprefix = name;
853
854 std::vector<std::unique_ptr<RooDataHist>> data;
855 for (const auto &comp : p["samples"].children()) {
856 std::unique_ptr<RooDataHist> dh = RooJSONFactoryWSTool::readBinnedData(
857 comp["data"], fprefix + "_" + RooJSONFactoryWSTool::name(comp) + "_dataHist", observables);
858 size_t nbins = dh->numEntries();
859
860 if (const JSONNode *staterror = findStaterror(comp)) {
861 if (sumW.empty()) {
862 sumW.resize(nbins);
863 sumW2.resize(nbins);
864 }
865 for (size_t i = 0; i < nbins; ++i) {
866 sumW[i] += dh->weight(i);
867 sumW2[i] += dh->weightSquared(i);
868 }
869 if (gammaParnames.empty()) {
870 if (auto staterrorParams = staterror->find("parameters")) {
871 for (const auto &v : staterrorParams->children()) {
872 gammaParnames.push_back(v.val());
873 }
874 }
875 }
876 }
877 data.emplace_back(std::move(dh));
878 }
879
880 RooAbsArg *mcStatObject = nullptr;
881 RooArgSet constraints;
882 if (!sumW.empty()) {
883 std::string channelName = name;
884 erasePrefix(channelName, "model_");
885
886 std::vector<double> errs(sumW.size());
887 for (size_t i = 0; i < sumW.size(); ++i) {
888 if (sumW[i] == 0.) {
889 errs[i] = 0.;
890 continue;
891 }
892 errs[i] = std::sqrt(sumW2[i]) / sumW[i];
893 // avoid negative sigma. This NP will be set constant anyway later
894 errs[i] = std::max(errs[i], 0.);
895 }
896
897 mcStatObject =
898 &createPHF("mc_stat_" + channelName, "stat_" + channelName, gammaParnames, errs, *tool, constraints,
899 observables, statErrType, defaultGammaMin, defaultStatErrorGammaMax, statErrThresh);
900 }
901
902 int idx = 0;
904 RooArgList coefs;
905 for (const auto &comp : p["samples"].children()) {
906 importHistSample(*tool, *data[idx], observables, mcStatObject, fprefix, comp, defaultInterpolation,
907 constraints);
908 ++idx;
909
910 std::string const &compName = RooJSONFactoryWSTool::name(comp);
911 funcs.add(*tool->request<RooAbsReal>(fprefix + "_" + compName + "_shapes", name));
912 coefs.add(*tool->request<RooAbsReal>(fprefix + "_" + compName + "_scaleFactors", name));
913 }
914
915 if (constraints.empty()) {
916 tool->wsEmplace<RooRealSumPdf>(name, funcs, coefs, true);
917 } else {
918 std::string sumName = name + "_model";
919 erasePrefix(sumName, "model_");
920 auto &sum = tool->wsEmplace<RooRealSumPdf>(sumName, funcs, coefs, true);
921 sum.SetTitle(name.c_str());
922 tool->wsEmplace<RooProdPdf>(name, constraints, RooFit::Conditional(sum, observables));
923 }
924 return true;
925 }
926};
927
928class FlexibleInterpVarStreamer : public RooFit::JSONIO::Exporter {
929public:
930 std::string const &key() const override
931 {
932 static const std::string keystring = "interpolation0d";
933 return keystring;
934 }
935 bool exportObject(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem) const override
936 {
937 auto fip = static_cast<const RooStats::HistFactory::FlexibleInterpVar *>(func);
938 const std::size_t nParameters = fip->variables().size();
939 if (fip->low().size() != nParameters || fip->high().size() != nParameters ||
940 fip->interpolationCodes().size() != nParameters) {
941 RooJSONFactoryWSTool::error("FlexibleInterpVar '" + std::string{fip->GetName()} +
942 "' has non-matching parameter, variation, and interpolation lengths");
943 }
944 elem["type"] << key();
945 writeInterpolations(elem["interpolations"], fip->interpolationCodes(), InterpolationClass::Flexible,
946 "FlexibleInterpVar '" + std::string{fip->GetName()} + "'");
947 RooJSONFactoryWSTool::fillSeq(elem["vars"], fip->variables());
948 elem["nom"] << fip->nominal();
949 elem["high"].fill_seq(fip->high(), fip->variables().size());
950 elem["low"].fill_seq(fip->low(), fip->variables().size());
951 return true;
952 }
953};
954
955class PiecewiseInterpolationStreamer : public RooFit::JSONIO::Exporter {
956public:
957 std::string const &key() const override
958 {
959 static const std::string keystring = "interpolation";
960 return keystring;
961 }
962 bool exportObject(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem) const override
963 {
964 const PiecewiseInterpolation *pip = static_cast<const PiecewiseInterpolation *>(func);
965 const std::size_t nParameters = pip->paramList().size();
966 if (pip->lowList().size() != nParameters || pip->highList().size() != nParameters ||
967 pip->interpolationCodes().size() != nParameters) {
968 RooJSONFactoryWSTool::error("PiecewiseInterpolation '" + std::string{pip->GetName()} +
969 "' has non-matching parameter, variation, and interpolation lengths");
970 }
971 elem["type"] << key();
972 writeInterpolations(elem["interpolations"], pip->interpolationCodes(), InterpolationClass::Piecewise,
973 "PiecewiseInterpolation '" + std::string{pip->GetName()} + "'");
974 elem["positiveDefinite"] << pip->positiveDefinite();
975 RooJSONFactoryWSTool::fillSeq(elem["vars"], pip->paramList());
976 elem["nom"] << pip->nominalHist()->GetName();
977 RooJSONFactoryWSTool::fillSeq(elem["high"], pip->highList(), pip->paramList().size());
978 RooJSONFactoryWSTool::fillSeq(elem["low"], pip->lowList(), pip->paramList().size());
979 return true;
980 }
981};
982
983class PiecewiseInterpolationFactory : public RooFit::JSONIO::Importer {
984public:
985 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
986 {
987 std::string name(RooJSONFactoryWSTool::name(p));
988
989 RooArgList vars{tool->requestArgList<RooAbsReal>(p, "vars")};
990 RooArgList low{tool->requestArgList<RooAbsReal>(p, "low")};
991 RooArgList high{tool->requestArgList<RooAbsReal>(p, "high")};
992 if (vars.size() != low.size() || vars.size() != high.size()) {
993 RooJSONFactoryWSTool::error("PiecewiseInterpolation '" + name +
994 "' has non-matching lengths of 'vars', 'high' and 'low'");
995 }
996 const std::vector<int> codes =
997 readInterpolations(p, vars.size(), InterpolationClass::Piecewise, "PiecewiseInterpolation '" + name + "'");
998
999 auto &pip =
1000 tool->wsEmplace<PiecewiseInterpolation>(name, *tool->requestArg<RooAbsReal>(p, "nom"), low, high, vars, codes);
1001
1002 pip.setPositiveDefinite(p["positiveDefinite"].val_bool());
1003
1004 return true;
1005 }
1006};
1007
1008class FlexibleInterpVarFactory : public RooFit::JSONIO::Importer {
1009public:
1010 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
1011 {
1012 std::string name(RooJSONFactoryWSTool::name(p));
1013 if (!p.has_child("high")) {
1014 RooJSONFactoryWSTool::error("no high variations of '" + name + "'");
1015 }
1016 if (!p.has_child("low")) {
1017 RooJSONFactoryWSTool::error("no low variations of '" + name + "'");
1018 }
1019 if (!p.has_child("nom")) {
1020 RooJSONFactoryWSTool::error("no nominal variation of '" + name + "'");
1021 }
1022
1023 double nom(p["nom"].val_double());
1024
1025 RooArgList vars{tool->requestArgList<RooRealVar>(p, "vars")};
1026
1027 std::vector<double> high;
1028 high << p["high"];
1029
1030 std::vector<double> low;
1031 low << p["low"];
1032
1033 if (vars.size() != low.size() || vars.size() != high.size()) {
1034 RooJSONFactoryWSTool::error("FlexibleInterpVar '" + name +
1035 "' has non-matching lengths of 'vars', 'high' and 'low'!");
1036 }
1037 const std::vector<int> codes =
1038 readInterpolations(p, vars.size(), InterpolationClass::Flexible, "FlexibleInterpVar '" + name + "'");
1039
1040 tool->wsEmplace<RooStats::HistFactory::FlexibleInterpVar>(name, vars, nom, low, high, codes);
1041
1042 return true;
1043 }
1044};
1045
1046struct NormFactor {
1047 std::string name;
1048 RooAbsReal const *param = nullptr;
1049 RooAbsPdf const *constraint = nullptr;
1050 NormFactor(RooAbsReal const &par, const RooAbsPdf *constr = nullptr)
1051 : name{par.GetName()}, param{&par}, constraint{constr}
1052 {
1053 }
1054};
1055
1056struct NormSys {
1057 std::string name = "";
1058 RooAbsReal const *param = nullptr;
1059 double low = 1.;
1060 double high = 1.;
1061 Interpolation interpolation = multiplicativePolynomialExponential;
1062 RooAbsPdf const *constraint = nullptr;
1063 NormSys() {};
1064 NormSys(const std::string &n, RooAbsReal *const p, double h, double l, Interpolation i, const RooAbsPdf *c)
1065 : name(n), param(p), low(l), high(h), interpolation(std::move(i)), constraint(c)
1066 {
1067 }
1068};
1069
1070struct HistoSys {
1071 std::string name;
1072 RooAbsReal const *param = nullptr;
1073 std::vector<double> low;
1074 std::vector<double> high;
1075 Interpolation interpolation = additivePolynomialLinear;
1076 RooAbsPdf const *constraint = nullptr;
1077 HistoSys(const std::string &n, RooAbsReal *const p, RooHistFunc *l, RooHistFunc *h, Interpolation i,
1078 const RooAbsPdf *c)
1079 : name(n), param(p), interpolation(std::move(i)), constraint(c)
1080 {
1081 low.assign(l->dataHist().weightArray(), l->dataHist().weightArray() + l->dataHist().numEntries());
1082 high.assign(h->dataHist().weightArray(), h->dataHist().weightArray() + h->dataHist().numEntries());
1083 }
1084};
1085struct ShapeSys {
1086 std::string name;
1087 std::vector<double> constraints;
1088 std::vector<RooAbsPdf const *> constraintPdfs;
1089 std::vector<RooAbsReal *> parameters;
1090 ShapeSys(const std::string &n) : name{n} {}
1091};
1092
1093struct GenericElement {
1094 std::string name;
1095 RooAbsReal *function = nullptr;
1096 GenericElement(RooAbsReal *e) : name(e->GetName()), function(e) {};
1097};
1098
1099std::string stripOuterParens(const std::string &s)
1100{
1101 size_t start = 0;
1102 size_t end = s.size();
1103
1104 while (start < end && s[start] == '(' && s[end - 1] == ')') {
1105 int depth = 0;
1106 bool balanced = true;
1107 for (size_t i = start; i < end - 1; ++i) {
1108 if (s[i] == '(')
1109 ++depth;
1110 else if (s[i] == ')')
1111 --depth;
1112 if (depth == 0 && i < end - 1) {
1113 balanced = false;
1114 break;
1115 }
1116 }
1117 if (balanced) {
1118 ++start;
1119 --end;
1120 } else {
1121 break;
1122 }
1123 }
1124 return s.substr(start, end - start);
1125}
1126
1127std::vector<std::string> splitTopLevelProduct(const std::string &expr)
1128{
1129 std::vector<std::string> parts;
1130 int depth = 0;
1131 size_t start = 0;
1132 bool foundTopLevelStar = false;
1133
1134 for (size_t i = 0; i < expr.size(); ++i) {
1135 char c = expr[i];
1136 if (c == '(') {
1137 ++depth;
1138 } else if (c == ')') {
1139 --depth;
1140 } else if (c == '*' && depth == 0) {
1141 foundTopLevelStar = true;
1142 std::string sub = expr.substr(start, i - start);
1143 parts.push_back(stripOuterParens(sub));
1144 start = i + 1;
1145 }
1146 }
1147
1148 if (!foundTopLevelStar) {
1149 return {}; // Not a top-level product
1150 }
1151
1152 std::string sub = expr.substr(start);
1153 parts.push_back(stripOuterParens(sub));
1154 return parts;
1155}
1156
1157NormSys parseOverallModifierFormula(const std::string &s, RooFormulaVar *formula)
1158{
1159 static const std::regex pattern(
1160 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*$)");
1161
1162 NormSys sys;
1163 double sign = 1.0;
1164
1165 std::smatch match;
1166 if (std::regex_match(s, match, pattern)) {
1167 if (match[1].str() == "-") {
1168 sign = -1.0;
1169 }
1170
1171 std::string token2 = match[2].str();
1172 std::string token3 = match[4].str();
1173
1174 RooAbsReal *p2 = static_cast<RooAbsReal *>(formula->getParameter(token2.c_str()));
1175 RooAbsReal *p3 = static_cast<RooAbsReal *>(formula->getParameter(token3.c_str()));
1176 RooRealVar *v2 = dynamic_cast<RooRealVar *>(p2);
1177 RooRealVar *v3 = dynamic_cast<RooRealVar *>(p3);
1178
1179 auto *constr2 = findConstraint(v2);
1180 auto *constr3 = findConstraint(v3);
1181
1182 if (constr2 && !p3) {
1183 sys.name = p2->GetName();
1184 sys.param = p2;
1185 sys.high = sign * toDouble(token3);
1186 sys.low = -sign * toDouble(token3);
1187 } else if (!p2 && constr3) {
1188 sys.name = p3->GetName();
1189 sys.param = p3;
1190 sys.high = sign * toDouble(token2);
1191 sys.low = -sign * toDouble(token2);
1192 } else if (constr2 && p3 && !constr3) {
1193 sys.name = v2->GetName();
1194 sys.param = v2;
1195 sys.high = sign * p3->getVal();
1196 sys.low = -sign * p3->getVal();
1197 } else if (p2 && !constr2 && constr3) {
1198 sys.name = v3->GetName();
1199 sys.param = v3;
1200 sys.high = sign * p2->getVal();
1201 sys.low = -sign * p2->getVal();
1202 }
1203
1204 // Preserve the legacy export behaviour for recognized explicit formulae.
1205 sys.interpolation = multiplicativePiecewiseExponential;
1206
1207 erasePrefix(sys.name, "alpha_");
1208 }
1209 return sys;
1210}
1211
1212void collectElements(RooArgList &elems, RooAbsArg *arg)
1213{
1214 if (auto prod = dynamic_cast<RooProduct *>(arg)) {
1215 for (const auto &e : prod->components()) {
1216 collectElements(elems, e);
1217 }
1218 } else {
1219 elems.add(*arg);
1220 }
1221}
1222
1223bool allRooRealVar(const RooAbsCollection &list)
1224{
1225 for (auto *var : list) {
1226 if (!dynamic_cast<RooRealVar *>(var)) {
1227 return false;
1228 }
1229 }
1230 return true;
1231}
1232
1233struct Sample {
1234 std::string name;
1235 std::vector<double> hist;
1236 std::vector<double> histError;
1237 std::vector<NormFactor> normfactors;
1238 std::vector<NormSys> normsys;
1239 std::vector<HistoSys> histosys;
1240 std::vector<ShapeSys> shapesys;
1241 std::vector<GenericElement> tmpElements;
1242 std::vector<GenericElement> otherElements;
1243 bool useBarlowBeestonLight = false;
1244 std::vector<RooAbsReal *> staterrorParameters;
1245 Sample(const std::string &n) : name{n} {}
1246};
1247
1248void addNormFactor(RooRealVar const *par, Sample &sample, RooWorkspace *ws)
1249{
1250 std::string parname = par->GetName();
1251 bool isConstrained = false;
1252 for (RooAbsArg const *pdf : ws->allPdfs()) {
1253 if (auto gauss = dynamic_cast<RooGaussian const *>(pdf)) {
1254 if (parname == gauss->getX().GetName()) {
1255 sample.normfactors.emplace_back(*par, gauss);
1256 isConstrained = true;
1257 }
1258 }
1259 }
1260 if (!isConstrained)
1261 sample.normfactors.emplace_back(*par);
1262}
1263
1264struct Channel {
1265 std::string name;
1266 std::vector<Sample> samples;
1267 std::map<int, double> tot_yield;
1268 std::map<int, double> tot_yield2;
1269 std::map<int, double> rel_errors;
1270 RooArgSet const *varSet = nullptr;
1271 long unsigned int nBins = 0;
1272};
1273
1274Channel readChannel(RooJSONFactoryWSTool *tool, const std::string &pdfname, const RooRealSumPdf *sumpdf)
1275{
1276 Channel channel;
1277
1278 RooWorkspace *ws = tool->workspace();
1279
1280 channel.name = pdfname;
1281 erasePrefix(channel.name, "model_");
1282 eraseSuffix(channel.name, "_model");
1283
1284 for (size_t sampleidx = 0; sampleidx < sumpdf->funcList().size(); ++sampleidx) {
1285 PiecewiseInterpolation *pip = nullptr;
1286 std::vector<ParamHistFunc *> phfs;
1287
1288 const auto func = sumpdf->funcList().at(sampleidx);
1289 Sample sample(func->GetName());
1290 erasePrefix(sample.name, "L_x_");
1291 eraseSuffix(sample.name, "_shapes");
1292 eraseSuffix(sample.name, "_" + channel.name);
1293 erasePrefix(sample.name, pdfname + "_");
1294
1295 auto updateObservables = [&](RooDataHist const &dataHist) {
1296 if (channel.varSet == nullptr) {
1297 channel.varSet = dataHist.get();
1298 channel.nBins = dataHist.numEntries();
1299 }
1300 if (sample.hist.empty()) {
1301 auto *w = dataHist.weightArray();
1302 sample.hist.assign(w, w + dataHist.numEntries());
1303 }
1304 };
1305 auto processElements = [&](const auto &elements, auto &&self) -> void {
1306 for (RooAbsArg *e : elements) {
1307 if (TString(e->GetName()).Contains("binWidth")) {
1308 // The bin width modifiers are handled separately. We can't just
1309 // check for the RooBinWidthFunction type here, because prior to
1310 // ROOT 6.26, the multiplication with the inverse bin width was
1311 // done in a different way (like a normfactor with a RooRealVar,
1312 // but it was stored in the dataset).
1313 // Fortunately, the name was similar, so we can match the modifier
1314 // name.
1315 } else if (auto constVar = dynamic_cast<RooConstVar *>(e)) {
1316 if (constVar->getVal() != 1.) {
1317 sample.normfactors.emplace_back(*constVar);
1318 }
1319 } else if (auto par = dynamic_cast<RooRealVar *>(e)) {
1320 addNormFactor(par, sample, ws);
1321 } else if (auto hf = dynamic_cast<const RooHistFunc *>(e)) {
1322 updateObservables(hf->dataHist());
1323 } else if (ParamHistFunc *phf = dynamic_cast<ParamHistFunc *>(e); phf && allRooRealVar(phf->paramList())) {
1324 phfs.push_back(phf);
1325 } else if (auto fip = dynamic_cast<RooStats::HistFactory::FlexibleInterpVar *>(e)) {
1326 // some (modified) histfactory models have several instances of FlexibleInterpVar
1327 // we collect and merge them
1328 for (size_t i = 0; i < fip->variables().size(); ++i) {
1329 RooAbsReal *var = static_cast<RooAbsReal *>(fip->variables().at(i));
1330 std::string sysname(var->GetName());
1331 erasePrefix(sysname, "alpha_");
1332 const auto *constraint = findConstraint(var);
1333 if (!constraint && !var->isConstant()) {
1334 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(var->GetName()));
1335 } else {
1336 const std::string context = "normsys modifier '" + sysname + "' in sample '" + sample.name +
1337 "' of channel '" + channel.name + "'";
1338 sample.normsys.emplace_back(
1339 sysname, var, fip->high()[i], fip->low()[i],
1340 interpolationFromCode(fip->interpolationCodes()[i], InterpolationClass::Flexible, context),
1341 constraint);
1342 }
1343 }
1344 } else if (!pip && (pip = dynamic_cast<PiecewiseInterpolation *>(e))) {
1345 // nothing to do here, already assigned
1346 } else if (RooFormulaVar *formula = dynamic_cast<RooFormulaVar *>(e)) {
1347 // people do a lot of fancy stuff with RooFormulaVar, like including NormSys via explicit formulae.
1348 // let's try to decompose it into building blocks
1349 TString expression(formula->expression());
1350 for (size_t i = formula->nParameters(); i--;) {
1351 const RooAbsArg *p = formula->getParameter(i);
1352 expression.ReplaceAll(("x[" + std::to_string(i) + "]").c_str(), p->GetName());
1353 expression.ReplaceAll(("@" + std::to_string(i)).c_str(), p->GetName());
1354 }
1355 auto components = splitTopLevelProduct(expression.Data());
1356 if (components.size() == 0) {
1357 // it's not a product, let's just treat it as an unknown element
1358 sample.otherElements.push_back(formula);
1359 } else {
1360 // it is a prododuct, we can try to handle the elements separately
1361 std::vector<RooAbsArg *> realComponents;
1362 int idx = 0;
1363 for (auto &comp : components) {
1364 // check if this is a trivial element of a product, we can treat it as its own modifier
1365 auto *part = formula->getParameter(comp.c_str());
1366 if (part) {
1367 realComponents.push_back(part);
1368 continue;
1369 }
1370 // check if this is an attempt at explicitly encoding an overallSys
1371 auto normsys = parseOverallModifierFormula(comp, formula);
1372 if (normsys.param) {
1373 sample.normsys.emplace_back(std::move(normsys));
1374 continue;
1375 }
1376
1377 // this is something non-trivial, let's deal with it separately
1378 std::string name = std::string(formula->GetName()) + "_part" + std::to_string(idx);
1379 ++idx;
1380 auto *var = new RooFormulaVar(name.c_str(), name.c_str(), comp.c_str(), formula->dependents());
1381 sample.tmpElements.push_back({var});
1382 }
1383 self(realComponents, self);
1384 }
1385 } else if (auto real = dynamic_cast<RooAbsReal *>(e)) {
1386 sample.otherElements.push_back(real);
1387 }
1388 }
1389 };
1390
1391 RooArgList elems;
1392 collectElements(elems, func);
1393 collectElements(elems, sumpdf->coefList().at(sampleidx));
1394 processElements(elems, processElements);
1395
1396 // see if we can get the observables
1397 if (pip) {
1398 if (auto nh = dynamic_cast<RooHistFunc const *>(pip->nominalHist())) {
1399 updateObservables(nh->dataHist());
1400 }
1401 }
1402
1403 // sort and configure norms
1404 sortByName(sample.normfactors);
1405 sortByName(sample.normsys);
1406
1407 // sort and configure the histosys
1408 if (pip) {
1409 for (size_t i = 0; i < pip->paramList().size(); ++i) {
1410 RooAbsReal *var = static_cast<RooAbsReal *>(pip->paramList().at(i));
1411 std::string sysname(var->GetName());
1412 erasePrefix(sysname, "alpha_");
1413 if (auto lo = dynamic_cast<RooHistFunc *>(pip->lowList().at(i))) {
1414 if (auto hi = dynamic_cast<RooHistFunc *>(pip->highList().at(i))) {
1415 const auto *constraint = findConstraint(var);
1416 if (!constraint && !var->isConstant()) {
1417 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(var->GetName()));
1418 } else {
1419 const std::string context = "histosys modifier '" + sysname + "' in sample '" + sample.name +
1420 "' of channel '" + channel.name + "'";
1421 sample.histosys.emplace_back(
1422 sysname, var, lo, hi,
1423 interpolationFromCode(pip->interpolationCodes()[i], InterpolationClass::Piecewise, context),
1424 constraint);
1425 }
1426 }
1427 }
1428 }
1429 sortByName(sample.histosys);
1430 }
1431
1432 for (ParamHistFunc *phf : phfs) {
1433 if (startsWith(std::string(phf->GetName()), "mc_stat_")) { // MC stat uncertainty
1434 int idx = 0;
1435 for (const auto &g : phf->paramList()) {
1436 sample.staterrorParameters.push_back(static_cast<RooRealVar *>(g));
1437 ++idx;
1438 RooAbsPdf *constraint = findConstraint(g);
1439 channel.tot_yield[idx] += sample.hist[idx - 1];
1440 channel.tot_yield2[idx] += (sample.hist[idx - 1] * sample.hist[idx - 1]);
1441 if (constraint) {
1442 channel.rel_errors[idx] = constraintRelError(*constraint, *g);
1443 }
1444 }
1445 sample.useBarlowBeestonLight = true;
1446 } else { // other ShapeSys
1447 ShapeSys sys(phf->GetName());
1448 erasePrefix(sys.name, channel.name + "_");
1449 bool isshapesys = eraseSuffix(sys.name, "_ShapeSys") || eraseSuffix(sys.name, "_shapeSys");
1450 bool isshapefactor = eraseSuffix(sys.name, "_ShapeFactor") || eraseSuffix(sys.name, "_shapeFactor");
1451
1452 for (const auto &g : phf->paramList()) {
1453 sys.parameters.push_back(static_cast<RooRealVar *>(g));
1454 RooAbsPdf *constraint = nullptr;
1455 if (isshapesys) {
1456 constraint = findConstraint(g);
1457 if (!constraint)
1458 constraint = ws->pdf(constraintName(g->GetName()));
1459 if (!constraint && !g->isConstant()) {
1460 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(g->GetName()));
1461 }
1462 } else if (!isshapefactor) {
1463 RooJSONFactoryWSTool::error("unknown type of shapesys " + std::string(phf->GetName()));
1464 }
1465 if (!constraint) {
1466 sys.constraints.push_back(0.0);
1467 sys.constraintPdfs.push_back(nullptr);
1468 } else {
1469 sys.constraints.push_back(constraintRelError(*constraint, *g));
1470 sys.constraintPdfs.push_back(constraint);
1471 }
1472 }
1473 sample.shapesys.emplace_back(std::move(sys));
1474 }
1475 }
1476 sortByName(sample.shapesys);
1477
1478 // add the sample
1479 channel.samples.emplace_back(std::move(sample));
1480 }
1481
1482 sortByName(channel.samples);
1483 return channel;
1484}
1485
1486bool hasSameMetadata(const RooAbsArg *lhs, const RooAbsArg *rhs)
1487{
1488 if (!lhs || !rhs) {
1489 return lhs == rhs;
1490 }
1491 return std::string{lhs->GetName()} == rhs->GetName() && lhs->IsA() == rhs->IsA();
1492}
1493
1494[[noreturn]] void duplicateModifierError(const Channel &channel, const Sample &sample, std::string_view type,
1495 std::string_view name, std::string_view reason)
1496{
1497 std::stringstream ss;
1498 ss << "cannot combine duplicate modifier '" << name << "' of type '" << type << "' in sample '" << sample.name
1499 << "' of channel '" << channel.name << "': " << reason;
1500 RooJSONFactoryWSTool::error(ss.str().c_str());
1501}
1502
1503void warnDuplicateModifiersCombined(const Channel &channel, const Sample &sample, std::string_view type,
1504 std::string_view name, std::size_t count)
1505{
1506 std::stringstream ss;
1507 ss << "combined " << count << " duplicate modifiers named '" << name << "' of type '" << type << "' in sample '"
1508 << sample.name << "' of channel '" << channel.name << "'";
1510}
1511
1512// Multiplicatively combining two normsys is only faithful when the interpolation is done in log-space, so that
1513// f1(alpha) * f2(alpha) is again representable by a single normsys with the multiplied lo/hi factors. This holds for
1514// the piecewise-exponential code 1 (exact everywhere) and for the default code 4 (exact at the +-1 sigma anchors and in
1515// the exponential extrapolation region). The linear-space codes (e.g. 0 and 2) would turn the product into a shape that
1516// cannot be represented by a single normsys, so those must not be merged.
1517bool normSysSupportsMultiplicativeMerge(const Interpolation &interpolation)
1518{
1519 return interpolation == multiplicativePiecewiseExponential || interpolation == multiplicativePolynomialExponential;
1520}
1521
1522// Combines runs of adjacent modifiers that share the same name (the container is sorted by name beforehand) into a
1523// single modifier. The shared metadata (constraint, parameter and interpolation behaviour) must be identical across the
1524// duplicates; the type-specific `combine` callable performs the actual merge and any additional validation.
1525template <class Modifiers, class CombineFn>
1526void mergeDuplicateModifiers(const Channel &channel, const Sample &sample, Modifiers &modifiers, std::string_view type,
1527 CombineFn combine)
1528{
1529 Modifiers mergedModifiers;
1530 mergedModifiers.reserve(modifiers.size());
1531
1532 for (std::size_t begin = 0; begin < modifiers.size();) {
1533 std::size_t end = begin + 1;
1534 while (end < modifiers.size() && modifiers[end].name == modifiers[begin].name) {
1535 ++end;
1536 }
1537
1538 auto merged = modifiers[begin];
1539 for (std::size_t i = begin + 1; i < end; ++i) {
1540 const auto &modifier = modifiers[i];
1541 if (!hasSameMetadata(merged.constraint, modifier.constraint)) {
1542 duplicateModifierError(channel, sample, type, merged.name, "constraint metadata differs");
1543 }
1544 if (!hasSameMetadata(merged.param, modifier.param)) {
1545 duplicateModifierError(channel, sample, type, merged.name, "parameter metadata differs");
1546 }
1547 if (merged.interpolation != modifier.interpolation) {
1548 duplicateModifierError(channel, sample, type, merged.name, "interpolation behaviours differ");
1549 }
1550 combine(merged, modifier);
1551 }
1552
1553 if (end - begin > 1) {
1554 warnDuplicateModifiersCombined(channel, sample, type, merged.name, end - begin);
1555 }
1556 mergedModifiers.emplace_back(std::move(merged));
1557 begin = end;
1558 }
1559
1560 modifiers = std::move(mergedModifiers);
1561}
1562
1563void mergeDuplicateNormSys(const Channel &channel, Sample &sample)
1564{
1565 mergeDuplicateModifiers(channel, sample, sample.normsys, "normsys", [&](NormSys &merged, const NormSys &modifier) {
1566 if (!normSysSupportsMultiplicativeMerge(merged.interpolation)) {
1567 duplicateModifierError(channel, sample, "normsys", merged.name,
1568 "multiplicative combination is only valid for log-space interpolation");
1569 }
1570 merged.low *= modifier.low;
1571 merged.high *= modifier.high;
1572 });
1573}
1574
1575void mergeDuplicateHistoSys(const Channel &channel, Sample &sample)
1576{
1577 const std::size_t nBins = sample.hist.size();
1578 mergeDuplicateModifiers(
1579 channel, sample, sample.histosys, "histosys", [&](HistoSys &merged, const HistoSys &modifier) {
1580 if (merged.interpolation != additivePolynomialLinear) {
1581 duplicateModifierError(channel, sample, "histosys", merged.name,
1582 "this interpolation cannot currently be combined for duplicate histosys "
1583 "modifiers");
1584 }
1585 if (merged.low.size() != nBins || merged.high.size() != nBins || modifier.low.size() != nBins ||
1586 modifier.high.size() != nBins) {
1587 duplicateModifierError(channel, sample, "histosys", merged.name, "histogram binning differs");
1588 }
1589 for (std::size_t bin = 0; bin < nBins; ++bin) {
1590 merged.low[bin] += modifier.low[bin] - sample.hist[bin];
1591 merged.high[bin] += modifier.high[bin] - sample.hist[bin];
1592 }
1593 });
1594}
1595
1596void ensureUniqueModifiers(const Channel &channel, const Sample &sample)
1597{
1598 std::set<std::pair<std::string, std::string>> seen;
1599 auto add = [&](std::string type, const std::string &name) {
1600 if (!seen.emplace(type, name).second) {
1601 duplicateModifierError(channel, sample, type, name,
1602 "this modifier type cannot be combined without changing its meaning");
1603 }
1604 };
1605
1606 for (const auto &modifier : sample.normfactors)
1607 add("normfactor", modifier.name);
1608 for (const auto &modifier : sample.normsys)
1609 add("normsys", modifier.name);
1610 for (const auto &modifier : sample.histosys)
1611 add("histosys", modifier.name);
1612 for (const auto &modifier : sample.shapesys)
1613 add("shapesys", modifier.name);
1614 for (const auto &modifier : sample.otherElements)
1615 add("custom", modifier.name);
1616 for (const auto &modifier : sample.tmpElements)
1617 add("custom", modifier.name);
1618 if (sample.useBarlowBeestonLight)
1619 add(::Literals::staterror, ::Literals::staterror);
1620}
1621
1622void canonicalizeModifiers(Channel &channel)
1623{
1624 for (auto &sample : channel.samples) {
1625 mergeDuplicateNormSys(channel, sample);
1626 mergeDuplicateHistoSys(channel, sample);
1627 ensureUniqueModifiers(channel, sample);
1628 }
1629}
1630
1631void configureStatError(Channel &channel)
1632{
1633 for (auto &sample : channel.samples) {
1634 if (sample.useBarlowBeestonLight) {
1635 sample.histError.resize(sample.hist.size());
1636 for (auto bin : channel.rel_errors) {
1637 // reverse engineering the correct partial error
1638 // the (arbitrary) convention used here is that all samples should have the same relative error
1639 const int i = bin.first;
1640 const double relerr_tot = bin.second;
1641 const double count = sample.hist[i - 1];
1642 // this reconstruction is inherently imprecise, so we truncate it at some decimal places to make sure that
1643 // we don't carry around too many useless digits
1644 sample.histError[i - 1] =
1645 round_prec(relerr_tot * channel.tot_yield[i] / std::sqrt(channel.tot_yield2[i]) * count, 7);
1646 }
1647 }
1648 }
1649}
1650
1651std::optional<Interpolation> defaultInterpolation(const Channel &channel)
1652{
1653 std::map<Interpolation, std::size_t> counts;
1654 for (const auto &sample : channel.samples) {
1655 for (const auto &modifier : sample.normsys) {
1656 ++counts[modifier.interpolation];
1657 }
1658 for (const auto &modifier : sample.histosys) {
1659 ++counts[modifier.interpolation];
1660 }
1661 }
1662 if (counts.empty()) {
1663 return std::nullopt;
1664 }
1665
1666 auto best = counts.begin();
1667 for (auto current = std::next(counts.begin()); current != counts.end(); ++current) {
1668 if (current->second > best->second ||
1669 (current->second == best->second && current->first == multiplicativePolynomialExponential &&
1670 best->first != multiplicativePolynomialExponential)) {
1671 best = current;
1672 }
1673 }
1674 return best->first;
1675}
1676
1677bool exportChannel(RooJSONFactoryWSTool *tool, const Channel &channel, JSONNode &elem)
1678{
1679 // Write the constraint reference for any modifier that supports an
1680 // external Gaussian/Poisson/etc. constraint.
1681 auto writeConstraint = [](JSONNode &mod, auto const &sys) {
1682 if (sys.constraint) {
1683 mod["constraint"] << sys.constraint->GetName();
1684 }
1685 };
1686 auto addModifier = [](JSONNode &modifiers, std::string const &name, const char *type) -> JSONNode & {
1687 auto &mod = modifiers.append_child();
1688 mod.set_map();
1689 mod["name"] << name;
1690 mod["type"] << type;
1691 return mod;
1692 };
1693
1694 elem["type"] << "histfactory_dist";
1695 const auto channelDefaultInterpolation = defaultInterpolation(channel);
1696 if (channelDefaultInterpolation) {
1697 writeInterpolation(elem["default_interpolation"], *channelDefaultInterpolation);
1698 }
1699
1700 bool observablesWritten = false;
1701 for (const auto &sample : channel.samples) {
1702
1703 auto &s = RooJSONFactoryWSTool::appendNamedChild(elem["samples"], sample.name);
1704
1705 auto &modifiers = s["modifiers"];
1706 modifiers.set_seq();
1707
1708 for (const auto &nf : sample.normfactors) {
1709 auto &mod = modifiers.append_child();
1710 mod.set_map();
1711 mod["name"] << nf.name;
1712 mod["parameter"] << nf.param->GetName();
1713 mod["type"] << "normfactor";
1714 if (nf.constraint) {
1715 mod["constraint"] << nf.constraint->GetName();
1716 tool->queueExport(*nf.constraint);
1717 }
1718 }
1719
1720 for (const auto &sys : sample.normsys) {
1721 auto &mod = addModifier(modifiers, sys.name, "normsys");
1722 mod["parameter"] << sys.param->GetName();
1723 if (!channelDefaultInterpolation || sys.interpolation != *channelDefaultInterpolation) {
1724 writeInterpolation(mod["interpolation"], sys.interpolation);
1725 }
1726 writeConstraint(mod, sys);
1727 auto &data = mod["data"].set_map();
1728 data["lo"] << sys.low;
1729 data["hi"] << sys.high;
1730 }
1731
1732 for (const auto &sys : sample.histosys) {
1733 auto &mod = addModifier(modifiers, sys.name, "histosys");
1734 mod["parameter"] << sys.param->GetName();
1735 if (!channelDefaultInterpolation || sys.interpolation != *channelDefaultInterpolation) {
1736 writeInterpolation(mod["interpolation"], sys.interpolation);
1737 }
1738 writeConstraint(mod, sys);
1739 auto &data = mod["data"].set_map();
1740 if (channel.nBins != sys.low.size() || channel.nBins != sys.high.size()) {
1741 RooJSONFactoryWSTool::error("inconsistent binning: " + std::to_string(channel.nBins) +
1742 " bins expected, but " + std::to_string(sys.low.size()) + "/" +
1743 std::to_string(sys.high.size()) + " found in nominal histogram errors!");
1744 }
1745 RooJSONFactoryWSTool::exportArray(channel.nBins, sys.low.data(), data["lo"].set_map()["contents"]);
1746 RooJSONFactoryWSTool::exportArray(channel.nBins, sys.high.data(), data["hi"].set_map()["contents"]);
1747 }
1748
1749 for (const auto &sys : sample.shapesys) {
1750 auto &mod = addModifier(modifiers, sys.name, "shapesys");
1751 exportGammaParameters(mod, sys.parameters);
1752 if (std::any_of(sys.constraintPdfs.begin(), sys.constraintPdfs.end(),
1753 [](auto *pdf) { return pdf != nullptr; })) {
1754 auto &constraintNames = mod["constraints"].set_seq();
1755 for (auto *constraint : sys.constraintPdfs) {
1756 if (constraint) {
1757 constraintNames.append_child() << constraint->GetName();
1758 } else {
1759 constraintNames.append_child().set_null();
1760 }
1761 }
1762 }
1763 mod["data"].set_map()["vals"].fill_seq(sys.constraints);
1764 }
1765
1766 for (const auto &other : sample.otherElements) {
1767 addModifier(modifiers, other.name, "custom");
1768 }
1769 for (const auto &other : sample.tmpElements) {
1770 addModifier(modifiers, other.name, "custom");
1771 }
1772
1773 if (sample.useBarlowBeestonLight) {
1774 auto &mod = addModifier(modifiers, ::Literals::staterror, ::Literals::staterror);
1775 exportGammaParameters(mod, sample.staterrorParameters);
1776 }
1777
1778 if (!observablesWritten) {
1779 auto &output = elem["axes"].set_seq();
1780 for (auto *obs : static_range_cast<RooRealVar *>(*channel.varSet)) {
1781 RooJSONFactoryWSTool::exportAxis(output.append_child().set_map(), *obs);
1782 }
1783 observablesWritten = true;
1784 }
1785 auto &dataNode = s["data"].set_map();
1786 if (channel.nBins != sample.hist.size()) {
1787 RooJSONFactoryWSTool::error("inconsistent binning: " + std::to_string(channel.nBins) + " bins expected, but " +
1788 std::to_string(sample.hist.size()) + " found in nominal histogram!");
1789 }
1790 RooJSONFactoryWSTool::exportArray(channel.nBins, sample.hist.data(), dataNode["contents"]);
1791 if (!sample.histError.empty()) {
1792 if (channel.nBins != sample.histError.size()) {
1793 RooJSONFactoryWSTool::error("inconsistent binning: " + std::to_string(channel.nBins) +
1794 " bins expected, but " + std::to_string(sample.histError.size()) +
1795 " found in nominal histogram errors!");
1796 }
1797 RooJSONFactoryWSTool::exportArray(channel.nBins, sample.histError.data(), dataNode["errors"]);
1798 }
1799 }
1800
1801 return true;
1802}
1803
1804std::vector<RooAbsPdf *> findLostConstraints(const Channel &channel, const std::vector<RooAbsPdf *> &constraints)
1805{
1806 // collect all the vars that are used by the model
1807 std::set<const RooAbsReal *> vars;
1808 for (const auto &sample : channel.samples) {
1809 for (const auto &nf : sample.normfactors) {
1810 vars.insert(nf.param);
1811 }
1812 for (const auto &sys : sample.normsys) {
1813 vars.insert(sys.param);
1814 }
1815
1816 for (const auto &sys : sample.histosys) {
1817 vars.insert(sys.param);
1818 }
1819 for (const auto &sys : sample.shapesys) {
1820 for (const auto &par : sys.parameters) {
1821 vars.insert(par);
1822 }
1823 }
1824 if (sample.useBarlowBeestonLight) {
1825 for (const auto &par : sample.staterrorParameters) {
1826 vars.insert(par);
1827 }
1828 }
1829 }
1830
1831 // check if there is any constraint present that is unrelated to these vars
1832 std::vector<RooAbsPdf *> lostConstraints;
1833 for (auto *pdf : constraints) {
1834 bool related = false;
1835 for (const auto *var : vars) {
1836 if (pdf->dependsOn(*var)) {
1837 related = true;
1838 }
1839 }
1840 if (!related) {
1841 lostConstraints.push_back(pdf);
1842 }
1843 }
1844 // return the constraints that would be "lost" when exporting the model
1845 return lostConstraints;
1846}
1847
1848bool tryExportHistFactory(RooJSONFactoryWSTool *tool, const std::string &pdfname, const RooRealSumPdf *sumpdf,
1849 std::vector<RooAbsPdf *> constraints, JSONNode &elem)
1850{
1851 // some preliminary checks
1852 if (!sumpdf) {
1853 return false;
1854 }
1855
1856 for (RooAbsArg *sample : sumpdf->funcList()) {
1857 if (!dynamic_cast<RooProduct *>(sample) && !dynamic_cast<RooRealSumPdf *>(sample)) {
1858 return false;
1859 }
1860 }
1861
1862 auto channel = readChannel(tool, pdfname, sumpdf);
1863
1864 // sanity checks
1865 if (channel.samples.size() == 0)
1866 return false;
1867 for (auto &sample : channel.samples) {
1868 if (sample.hist.empty()) {
1869 return false;
1870 }
1871 }
1872
1873 canonicalizeModifiers(channel);
1874
1875 // stat error handling
1876 configureStatError(channel);
1877
1878 auto lostConstraints = findLostConstraints(channel, constraints);
1879 // Export all the lost constraints
1880 for (const auto *constraint : lostConstraints) {
1882 "losing constraint term '" + std::string(constraint->GetName()) +
1883 "', implicit constraints are not supported by HS3 yet! The term will appear in the HS3 file, but will not be "
1884 "picked up when creating a likelihood from it! You will have to add it manually as an external constraint.");
1885 tool->queueExport(*constraint);
1886 }
1887
1888 // Export all the regular modifiers
1889 auto queueConstraints = [&](auto const &modifiers) {
1890 for (auto &modifier : modifiers) {
1891 if (modifier.constraint) {
1892 tool->queueExport(*modifier.constraint);
1893 }
1894 }
1895 };
1896 for (const auto &sample : channel.samples) {
1897 queueConstraints(sample.normfactors);
1898 queueConstraints(sample.normsys);
1899 queueConstraints(sample.histosys);
1900 for (auto &modifier : sample.shapesys) {
1901 for (auto *constraint : modifier.constraintPdfs) {
1902 if (constraint) {
1903 tool->queueExport(*constraint);
1904 }
1905 }
1906 }
1907 }
1908
1909 // Export all the custom modifiers
1910 for (const auto &sample : channel.samples) {
1911 for (auto &modifier : sample.otherElements) {
1912 tool->queueExport(*modifier.function);
1913 }
1914 for (auto &modifier : sample.tmpElements) {
1915 tool->queueExportTemporary(modifier.function);
1916 }
1917 }
1918
1919 // Export all model parameters
1920 RooArgSet parameters;
1921 sumpdf->getParameters(channel.varSet, parameters);
1922 for (RooAbsArg *param : parameters) {
1923 // This should exclude the global observables
1924 if (!startsWith(std::string{param->GetName()}, "nom_")) {
1925 tool->queueExport(*param);
1926 }
1927 }
1928
1929 return exportChannel(tool, channel, elem);
1930}
1931
1932class HistFactoryStreamer_ProdPdf : public RooFit::JSONIO::Exporter {
1933public:
1934 bool autoExportDependants() const override { return false; }
1935 bool tryExport(RooJSONFactoryWSTool *tool, const RooProdPdf *prodpdf, JSONNode &elem) const
1936 {
1937 std::vector<RooAbsPdf *> constraints;
1938 RooRealSumPdf *sumpdf = nullptr;
1939 for (auto *pdf : static_range_cast<RooAbsPdf *>(prodpdf->pdfList())) {
1940 auto thispdf = dynamic_cast<RooRealSumPdf *>(pdf);
1941 if (thispdf) {
1942 if (!sumpdf)
1943 sumpdf = thispdf;
1944 else
1945 return false;
1946 } else {
1947 constraints.push_back(pdf);
1948 }
1949 }
1950 if (!sumpdf)
1951 return false;
1952
1953 bool ok = tryExportHistFactory(tool, prodpdf->GetName(), sumpdf, constraints, elem);
1954 return ok;
1955 }
1956 std::string const &key() const override
1957 {
1958 static const std::string keystring = "histfactory_dist";
1959 return keystring;
1960 }
1961 bool exportObject(RooJSONFactoryWSTool *tool, const RooAbsArg *p, JSONNode &elem) const override
1962 {
1963 return tryExport(tool, static_cast<const RooProdPdf *>(p), elem);
1964 }
1965};
1966
1967class HistFactoryStreamer_SumPdf : public RooFit::JSONIO::Exporter {
1968public:
1969 bool autoExportDependants() const override { return false; }
1970 bool tryExport(RooJSONFactoryWSTool *tool, const RooRealSumPdf *sumpdf, JSONNode &elem) const
1971 {
1972 std::vector<RooAbsPdf *> constraints;
1973 return tryExportHistFactory(tool, sumpdf->GetName(), sumpdf, constraints, elem);
1974 }
1975 std::string const &key() const override
1976 {
1977 static const std::string keystring = "histfactory_dist";
1978 return keystring;
1979 }
1980 bool exportObject(RooJSONFactoryWSTool *tool, const RooAbsArg *p, JSONNode &elem) const override
1981 {
1982 return tryExport(tool, static_cast<const RooRealSumPdf *>(p), elem);
1983 }
1984};
1985
1986STATIC_EXECUTE([]() {
1987 using namespace RooFit::JSONIO;
1988
1989 registerImporter<HistFactoryImporter>("histfactory_dist", true);
1990 registerImporter<PiecewiseInterpolationFactory>("interpolation", true);
1991 registerImporter<FlexibleInterpVarFactory>("interpolation0d", true);
1992 registerExporter<FlexibleInterpVarStreamer>(RooStats::HistFactory::FlexibleInterpVar::Class(), true);
1993 registerExporter<PiecewiseInterpolationStreamer>(PiecewiseInterpolation::Class(), true);
1994 registerExporter<HistFactoryStreamer_ProdPdf>(RooProdPdf::Class(), true);
1995 registerExporter<HistFactoryStreamer_SumPdf>(RooRealSumPdf::Class(), true);
1996});
1997
1998} // 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
Bool_t operator!=(const TDatime &d1, const TDatime &d2)
Definition TDatime.h:104
Bool_t operator<(const TDatime &d1, const TDatime &d2)
Definition TDatime.h:106
Bool_t operator==(const TDatime &d1, const TDatime &d2)
Definition TDatime.h:102
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 char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t result
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:142
#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,...
const RooArgList & highList() const
const RooAbsReal * nominalHist() const
Return pointer to the nominal hist function.
static TClass * Class()
const RooArgList & lowList() const
void setPositiveDefinite(bool flag=true)
const RooArgList & paramList() const
const std::vector< int > & interpolationCodes() 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.
TClass * IsA() const override
Definition RooAbsArg.h:656
bool isConstant() const
Check if the "Constant" attribute is set.
Definition RooAbsArg.h:283
RooFit::OwningPtr< RooArgSet > getParameters(const RooAbsData *data, bool stripDisconnected=true) const
Create a list of leaf nodes in the arg tree starting with ourself as top node that don't match any of...
Abstract container object that can hold multiple RooAbsArg objects.
virtual bool add(const RooAbsArg &var, bool silent=false)
Add the specified argument to list.
Storage_t::size_type size() const
Abstract interface for all probability density functions.
Definition RooAbsPdf.h:32
void setConstant(bool value=true)
Abstract base class for objects that represent a real value and implements functionality common to al...
Definition RooAbsReal.h:63
double getVal(const RooArgSet *normalisationSet=nullptr) const
Evaluate object.
Definition RooAbsReal.h:107
RooArgList is a container object that can hold multiple RooAbsArg objects.
Definition RooArgList.h:22
RooAbsArg * at(Int_t idx) const
Return object at given index, or nullptr if index is out of range.
Definition RooArgList.h:110
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
virtual std::string val() const =0
void fill_seq(Collection const &coll)
virtual JSONNode & set_map()=0
virtual JSONNode & append_child()=0
virtual JSONNode & set_null()=0
virtual JSONNode & set_seq()=0
virtual bool is_container() const =0
virtual bool is_map() const =0
virtual bool has_child(std::string const &) const =0
JSONNode const * find(std::string const &key) const
virtual bool is_null() const =0
virtual bool has_val() const =0
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:29
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)
T * requestArg(const RooFit::Detail::JSONNode &node, const std::string &key)
T * request(const std::string &objname, const std::string &requestAuthor)
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.
void queueExportTemporary(RooAbsArg *arg)
void queueExport(RooAbsArg const &arg)
RooArgList requestArgList(const RooFit::Detail::JSONNode &node, const std::string &seqName)
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.
Obj_t & wsEmplace(RooStringView name, Args_t &&...args)
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.
Poisson pdf.
Definition RooPoisson.h:18
RooAbsReal const & getMean() const
Get the mean parameter.
Definition RooPoisson.h:47
Efficient implementation of a product of PDFs of the form.
Definition RooProdPdf.h:35
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:
const RooArgList & funcList() const
static TClass * Class()
const RooArgList & coefList() const
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.
const RooListProxy & variables() const
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.
RooRealVar * var(RooStringView name) const
Retrieve real-valued variable (RooRealVar) with given name. A null pointer is returned if not found.
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.
virtual void SetTitle(const char *title="")
Set the title of the TNamed.
Definition TNamed.cxx:173
const char * GetName() const override
Returns name of object.
Definition TNamed.h:49
virtual const char * GetName() const
Returns name of object.
Definition TObject.cxx:461
Basic string class.
Definition TString.h:137
Bool_t Contains(const char *pat, ECaseCompare cmp=kExact) const
Definition TString.h:642
RooCmdArg RecycleConflictNodes(bool flag=true)
RooCmdArg Conditional(const RooArgSet &pdfSet, const RooArgSet &depSet, bool depsAreCond=false)
const Int_t n
Definition legend1.C:16
for(Int_t i=0;i< n;i++)
Definition legend1.C:18
double gamma(double x)
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)
if(fPos !=fText.size()) Fail("unexpected trailing content")
std::string GetName(TCppScope_t)
#define STATIC_EXECUTE(MY_FUNC)
TLine l
Definition textangle.C:4
static uint64_t sum(uint64_t i)
Definition Factory.cxx:2335