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
339// The fallback for modifiers without interpolation in documents that predate HS3 v0.3, see interpolationCode(). Before
340// that, RooFit did not write the interpolation of modifiers that used its implicit default.
341struct LegacyInterpolationDefault {
342 bool allowed = false;
343 bool warned = false;
344};
345
346int interpolationCode(const JSONNode &modifier, const std::optional<Interpolation> &defaultInterpolation,
347 InterpolationClass interpolationClass, const std::string &context,
348 LegacyInterpolationDefault &legacyDefault)
349{
350 const auto toCode = [&](const Interpolation &interpolation) {
351 return codeFromInterpolation(interpolation, interpolationClass, context);
352 };
353
354 if (const auto *interpolationNode = modifier.find("interpolation")) {
355 if (interpolationNode->is_map()) {
356 return toCode(readInterpolation(*interpolationNode, context));
357 }
358 const int legacyCode = readLegacyInterpolationCode(*interpolationNode, context);
359 const Interpolation interpolation = interpolationFromCode(legacyCode, interpolationClass, context);
360 return toCode(interpolation);
361 }
362 if (defaultInterpolation) {
363 return toCode(*defaultInterpolation);
364 }
365
366 // HS3 forbids implementation-specific default interpolations.
367 if (!legacyDefault.allowed) {
368 RooJSONFactoryWSTool::error(context + " is not given, and the channel has no \"default_interpolation\"");
369 }
370
371 // Before structured interpolation was introduced, both modifier classes
372 // used the integer code 4 as their implicit default. The meaning of code 4
373 // is class-dependent. Files written by these ROOT versions are still read.
374 if (!legacyDefault.warned) {
375 RooJSONFactoryWSTool::warning("RooFitHS3: " + context +
376 " is not given, and the channel has no \"default_interpolation\". Using the "
377 "implicit default of older RooFit versions (code 4), which is deprecated.");
378 legacyDefault.warned = true;
379 }
380 return 4;
381}
382
383void erasePrefix(std::string &str, std::string_view prefix)
384{
385 if (startsWith(str, prefix)) {
386 str.erase(0, prefix.size());
387 }
388}
389
390bool eraseSuffix(std::string &str, std::string_view suffix)
391{
392 if (endsWith(str, suffix)) {
393 str.erase(str.size() - suffix.size());
394 return true;
395 } else {
396 return false;
397 }
398}
399
400template <class Coll>
401void sortByName(Coll &coll)
402{
403 std::sort(coll.begin(), coll.end(), [](auto &l, auto &r) { return l.name < r.name; });
404}
405
406template <class T>
407T *findClient(RooAbsArg *gamma)
408{
409 for (const auto &client : gamma->clients()) {
410 if (auto casted = dynamic_cast<T *>(client)) {
411 return casted;
412 } else {
413 T *c = findClient<T>(client);
414 if (c)
415 return c;
416 }
417 }
418 return nullptr;
419}
420
421RooAbsPdf *findConstraint(RooAbsArg *g)
422{
423 if (!g)
424 return nullptr;
425 if (auto *constraint = findClient<RooPoisson>(g))
426 return constraint;
427 if (auto *constraint = findClient<RooGaussian>(g))
428 return constraint;
429 return findClient<RooLognormal>(g);
430}
431
432inline std::string defaultGammaName(std::string const &sysname, std::size_t i)
433{
434 return "gamma_" + sysname + "_bin_" + std::to_string(i);
435}
436
437/// Export the names of the gamma parameters to the modifier struct
438void exportGammaParameters(JSONNode &mod, std::vector<RooAbsReal *> const &params)
439{
440 std::vector<std::string> paramNames;
441 for (RooAbsReal *param : params) {
442 paramNames.emplace_back(param->GetName());
443 }
444 mod["parameters"].fill_seq(paramNames);
445}
446
447RooRealVar &createNominal(RooWorkspace &ws, std::string const &parname, double val, double min, double max)
448{
449 RooRealVar &nom = getOrCreate<RooRealVar>(ws, "nom_" + parname, val, min, max);
450 nom.setConstant(true);
451 return nom;
452}
453
454/// Get the conventional name of the constraint pdf for a constrained
455/// parameter.
456std::string constraintName(std::string const &paramName)
457{
458 return paramName + "Constraint";
459}
460
461bool isLegacyConstraintType(std::string const &value)
462{
463 // HS3 spells "LogNormal", older RooFit versions used "Lognormal".
464 return value == "Gauss" || value == "Poisson" || value == "Const" || value == "LogNormal" || value == "Lognormal";
465}
466
467RooAbsPdf *findNamedConstraint(RooJSONFactoryWSTool &tool, std::string const &constraintName, std::string const &sample)
468{
469 if (auto *constraint = tool.workspace()->pdf(constraintName)) {
470 return constraint;
471 }
472
473 try {
474 return tool.request<RooAbsPdf>(constraintName, sample);
476 if (err.child() != constraintName) {
477 throw;
478 }
479 }
480
481 return nullptr;
482}
483
484RooAbsPdf &createLegacyConstraint(RooJSONFactoryWSTool &tool, const JSONNode &mod, RooRealVar &param,
485 std::string const &constraintType)
486{
487 if (constraintType == "Gauss") {
488 param.setError(1.0);
489 return getOrCreate<RooGaussian>(*tool.workspace(), constraintName(param.GetName()), param,
490 *tool.workspace()->var(std::string("nom_") + param.GetName()), 1.);
491 }
492
493 RooJSONFactoryWSTool::error("legacy constraint value '" + constraintType + "' for modifier '" +
495 "' is a known constraint type, but it cannot be resolved in this context");
496}
497
498ParamHistFunc &createPHF(const std::string &phfname, std::string const &sysname,
499 const std::vector<std::string> &parnames, const std::vector<double> &vals,
500 RooJSONFactoryWSTool &tool, RooAbsCollection &constraints, const RooArgSet &observables,
501 const std::string &constraintType, double gammaMin, double gammaMax, double minSigma,
502 bool createConstraints = true)
503{
504 RooWorkspace &ws = *tool.workspace();
505
506 size_t n = std::max(vals.size(), parnames.size());
507 RooArgList gammas;
508 for (std::size_t i = 0; i < n; ++i) {
509 const std::string name = parnames.empty() ? defaultGammaName(sysname, i) : parnames[i];
510 auto *e = dynamic_cast<RooAbsReal *>(ws.obj(name.c_str()));
511 if (e)
512 gammas.add(*e);
513 else
514 gammas.add(getOrCreate<RooRealVar>(ws, name, 1., gammaMin, gammaMax));
515 }
516
517 auto &phf = tool.wsEmplace<ParamHistFunc>(phfname, observables, gammas);
518
519 if (vals.size() > 0) {
520 if (!createConstraints) {
521 configureConstrainedGammas(gammas, vals, minSigma);
522 } else if (constraintType != "Const") {
523 if (constraintType == "LogNormal" || constraintType == "Lognormal") {
524 RooJSONFactoryWSTool::error("unsupported constraint '" + constraintType +
525 "' for the gamma parameters of '" + sysname +
526 "', supported are \"Poisson\" and \"Gauss\"");
527 }
528 auto constraintsInfo = createGammaConstraints(
529 gammas, vals, minSigma, constraintType == "Poisson" ? Constraint::Poisson : Constraint::Gaussian);
530 for (auto const &term : constraintsInfo.constraints) {
532 constraints.add(*ws.pdf(term->GetName()));
533 }
534 } else {
535 for (auto *gamma : static_range_cast<RooRealVar *>(gammas)) {
536 gamma->setConstant(true);
537 }
538 }
539 }
540
541 return phf;
542}
543
544/// Find the staterror modifier of a sample, or return nullptr if there is none.
545const JSONNode *findStaterror(const JSONNode &comp)
546{
547 if (comp.has_child("modifiers")) {
548 for (const auto &mod : comp["modifiers"].children()) {
549 if (mod["type"].val() == ::Literals::staterror)
550 return &mod;
551 }
552 }
553 return nullptr;
554}
555
556RooAbsPdf &
557getOrCreateConstraint(RooJSONFactoryWSTool &tool, const JSONNode &mod, RooRealVar &param, const std::string &sample)
558{
559 JSONNode const *constrName = mod.find("constraint_name");
560 if (constrName) {
561 auto constraint_name = constrName->val();
562 auto constraint = findNamedConstraint(tool, constraint_name, sample);
563 if (!constraint) {
564 RooJSONFactoryWSTool::error("unable to find definition of of constraint '" + constraint_name +
565 "' for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
566 }
567 if (auto gauss = dynamic_cast<RooGaussian *const>(constraint)) {
568 param.setError(gauss->getSigma().getVal());
569 }
570 return *constraint;
571 }
572
573 if (auto constr = mod.find("constraint")) {
574 std::string constraintValue = constr->val();
575 if (auto *constraint = findNamedConstraint(tool, constraintValue, sample)) {
576 if (auto gauss = dynamic_cast<RooGaussian *const>(constraint)) {
577 param.setError(gauss->getSigma().getVal());
578 }
579 return *constraint;
580 }
581
582 if (isLegacyConstraintType(constraintValue)) {
583 return createLegacyConstraint(tool, mod, param, constraintValue);
584 }
585
586 RooJSONFactoryWSTool::error("unable to resolve constraint value '" + constraintValue + "' for modifier '" +
588 "': this looks like a legacy workspace where the 'constraint' field is neither a "
589 "constraint pdf name nor a supported legacy constraint type");
590 }
591
592 std::string constraint_type = "Gauss";
593 if (auto constrType = mod.find("constraint_type")) {
594 constraint_type = constrType->val();
595 }
596 if (isLegacyConstraintType(constraint_type)) {
597 return createLegacyConstraint(tool, mod, param, constraint_type);
598 }
599 RooJSONFactoryWSTool::error("unknown or invalid constraint for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
600}
601bool hasConstraint(const JSONNode &mod)
602{
603 return mod.has_child("constraint") || mod.has_child("constraint_name") || mod.has_child("constraint_type");
604}
605
606// Create the constraint of a normsys or histosys modifier. Without a constraint, the parameter is unconstrained. Only
607// documents that predate HS3 v0.2 imply a unit Gaussian, like the ones written by the former HistFactory::JSONTool.
608void addSysConstraint(RooJSONFactoryWSTool &tool, const JSONNode &mod, RooRealVar &param, std::string const &sysname,
609 std::string const &sample, bool &warnedUnconstrained, RooArgSet &constraints)
610{
611 if (!hasConstraint(mod) && !isHS3VersionBefore(tool.hs3VersionInput(), 0, 2)) {
612 if (!warnedUnconstrained) {
613 RooJSONFactoryWSTool::warning("RooFitHS3: modifier '" + sysname + "' of sample '" + sample +
614 "' has no constraint, so its parameter '" + param.GetName() +
615 "' is unconstrained. Older RooFit versions implied a unit Gaussian. Other "
616 "modifiers of this channel are not reported.");
617 warnedUnconstrained = true;
618 }
619 return;
620 }
621 createNominal(*tool.workspace(), param.GetName(), 0.0, -10, 10);
622 constraints.add(getOrCreateConstraint(tool, mod, param, sample));
623}
624
625double poissonTau(RooPoisson const &constraint, RooAbsArg const &gamma)
626{
627 auto const *mean = dynamic_cast<RooProduct const *>(&constraint.getMean());
628 if (!mean) {
629 RooJSONFactoryWSTool::error("Poisson gamma constraint mean is not a RooProduct: " +
630 std::string(constraint.GetName()));
631 }
632
633 for (RooAbsArg *arg : mean->servers()) {
634 if (arg == &gamma) {
635 continue;
636 }
637
638 if (auto const *tau = dynamic_cast<RooConstVar const *>(arg)) {
639 return tau->getVal();
640 }
641
642 // Imported workspaces can sometimes represent
643 // constants as constant RooRealVars.
644 if (auto const *real = dynamic_cast<RooAbsReal const *>(arg)) {
645 if (real->isConstant() || endsWith(std::string(real->GetName()), "_tau")) {
646 return real->getVal();
647 }
648 }
649 }
650
651 RooJSONFactoryWSTool::error("Could not find tau component in Poisson gamma constraint mean: " +
652 std::string(constraint.GetName()));
653 return std::numeric_limits<double>::quiet_NaN();
654}
655
656// Returns the relative uncertainty encoded by a gamma constraint pdf. Only RooPoisson (via its tau) and RooGaussian
657// (via sigma/mean) are supported; anything else raises an error.
658double constraintRelError(RooAbsPdf const &constraint, RooAbsArg const &gamma)
659{
660 if (auto constraintP = dynamic_cast<RooPoisson const *>(&constraint)) {
661 return 1. / std::sqrt(poissonTau(*constraintP, gamma));
662 }
663 if (auto constraintG = dynamic_cast<RooGaussian const *>(&constraint)) {
664 return constraintG->getSigma().getVal() / constraintG->getMean().getVal();
665 }
666 RooJSONFactoryWSTool::error("currently, only RooPoisson and RooGaussian are supported as constraint types");
667 return std::numeric_limits<double>::quiet_NaN();
668}
669
670bool importHistSample(RooJSONFactoryWSTool &tool, RooDataHist &dh, RooArgSet const &varlist,
671 RooAbsArg const *mcStatObject, const std::string &fprefix, const JSONNode &p,
672 const std::optional<Interpolation> &defaultInterpolation,
673 LegacyInterpolationDefault &legacyInterpolationDefault, bool &warnedUnconstrained,
674 RooArgSet &constraints)
675{
676 RooWorkspace &ws = *tool.workspace();
677
678 std::string sampleName = RooJSONFactoryWSTool::name(p);
679 std::string prefixedName = fprefix + "_" + sampleName;
680
681 std::string channelName = fprefix;
682 erasePrefix(channelName, "model_");
683
684 if (!p.has_child("data")) {
685 RooJSONFactoryWSTool::error("sample '" + sampleName + "' does not define a 'data' key");
686 }
687
688 auto &hf = tool.wsEmplace<RooHistFunc>("hist_" + prefixedName, varlist, dh);
690
691 RooArgList shapeElems;
692 RooArgList normElems;
693
694 shapeElems.add(tool.wsEmplace<RooBinWidthFunction>(prefixedName + "_binWidth", hf, true));
695
696 if (findStaterror(p)) {
697 shapeElems.add(*mcStatObject);
698 }
699
700 if (p.has_child("modifiers")) {
701 RooArgList overall_nps;
702 std::vector<double> overall_low;
703 std::vector<double> overall_high;
704 std::vector<int> overall_interp;
705
706 RooArgList histNps;
707 RooArgList histoLo;
708 RooArgList histoHi;
709 std::vector<int> histoInterp;
710
711 int idx = 0;
712 for (const auto &mod : p["modifiers"].children()) {
713 std::string const &modtype = mod["type"].val();
714 std::string const &sysname =
715 mod.has_child("name")
716 ? mod["name"].val()
717 : (mod.has_child("parameter") ? mod["parameter"].val() : "syst_" + std::to_string(idx));
718 ++idx;
719 if (modtype == "staterror") {
720 // this is dealt with at a different place, ignore it for now
721 } else if (modtype == "normfactor") {
722 // The "name" is only a label of the modifier, the parameter is given by "parameter" if present.
723 auto *parameter = mod.find("parameter");
724 RooRealVar &constrParam = getOrCreate<RooRealVar>(ws, parameter ? parameter->val() : sysname, 1., -3, 5);
725 constrParam.setError(0.0);
726 normElems.add(constrParam);
727 if (hasConstraint(mod)) {
728 // for norm factors, constraints are optional
729 constraints.add(getOrCreateConstraint(tool, mod, constrParam, sampleName));
730 }
731 } else if (modtype == "normsys") {
732 auto *parameter = mod.find("parameter");
733 std::string parname(parameter ? parameter->val() : "alpha_" + sysname);
734 auto &par = getOrCreate<RooRealVar>(ws, parname, 0., -5, 5);
735 overall_nps.add(par);
736 auto &data = mod["data"];
737 const std::string context = "interpolation for normsys modifier '" + sysname + "' in sample '" +
738 sampleName + "' of channel '" + channelName + "'";
739 const int interp = interpolationCode(mod, defaultInterpolation, InterpolationClass::Flexible, context,
740 legacyInterpolationDefault);
741 double low = data["lo"].val_double();
742 double high = data["hi"].val_double();
743
744 // the below contains a a hack to cut off variations that go below 0
745 // This is needed because FlexibleInterpVar code 4 interpolates in log-space. Hence, values <= 0 result in
746 // NaN, which propagates throughout the model and causes evaluations to fail. If you know a nicer way to
747 // solve this, please go ahead and fix the lines below.
748 if (interp == 4 && low <= 0)
749 low = std::numeric_limits<double>::epsilon();
750 if (interp == 4 && high <= 0)
751 high = std::numeric_limits<double>::epsilon();
752
753 overall_low.push_back(low);
754 overall_high.push_back(high);
755 overall_interp.push_back(interp);
756
757 addSysConstraint(tool, mod, par, sysname, sampleName, warnedUnconstrained, constraints);
758 } else if (modtype == "histosys") {
759 auto *parameter = mod.find("parameter");
760 std::string parname(parameter ? parameter->val() : "alpha_" + sysname);
761 auto &par = getOrCreate<RooRealVar>(ws, parname, 0., -5, 5);
762 histNps.add(par);
763 auto &data = mod["data"];
764 histoLo.add(tool.wsEmplace<RooHistFunc>(
765 sysname + "Low_" + prefixedName, varlist,
766 RooJSONFactoryWSTool::readBinnedData(data["lo"], sysname + "Low_" + prefixedName, varlist)));
767 histoHi.add(tool.wsEmplace<RooHistFunc>(
768 sysname + "High_" + prefixedName, varlist,
769 RooJSONFactoryWSTool::readBinnedData(data["hi"], sysname + "High_" + prefixedName, varlist)));
770 const std::string context = "interpolation for histosys modifier '" + sysname + "' in sample '" +
771 sampleName + "' of channel '" + channelName + "'";
772 histoInterp.push_back(interpolationCode(mod, defaultInterpolation, InterpolationClass::Piecewise, context,
773 legacyInterpolationDefault));
774 addSysConstraint(tool, mod, par, sysname, sampleName, warnedUnconstrained, constraints);
775 } else if (modtype == "shapesys" || modtype == "shapefactor") {
776 std::string funcName = channelName + "_" + sysname + "_ShapeSys";
777 // funcName should be "<channel_name>_<sysname>_ShapeSys"
778 std::vector<double> vals;
779 if (mod["data"].has_child("vals")) {
780 for (const auto &v : mod["data"]["vals"].children()) {
781 vals.push_back(v.val_double());
782 }
783 }
784 std::vector<std::string> parnames;
785 for (const auto &v : mod["parameters"].children()) {
786 parnames.push_back(v.val());
787 }
788 if (vals.empty() && parnames.empty()) {
789 RooJSONFactoryWSTool::error("unable to instantiate shapesys '" + sysname +
790 "' with neither values nor parameters!");
791 }
792 std::string constraint = "unknown";
793 std::vector<RooAbsPdf *> constraintPdfs;
794 bool const hasConstraintList = mod.has_child("constraints");
795 if (hasConstraintList) {
796 for (const auto &v : mod["constraints"].children()) {
797 if (v.is_null()) {
798 constraintPdfs.push_back(nullptr);
799 } else {
800 std::string constraintName = v.val();
801 auto *constraintPdf = findNamedConstraint(tool, constraintName, sampleName);
802 if (!constraintPdf) {
803 RooJSONFactoryWSTool::error("unable to find definition of constraint '" + constraintName +
804 "' for modifier '" + RooJSONFactoryWSTool::name(mod) + "'");
805 }
806 constraintPdfs.push_back(constraintPdf);
807 }
808 }
809 std::size_t const nGammas = std::max(vals.size(), parnames.size());
810 if (constraintPdfs.size() != nGammas) {
811 std::stringstream ss;
812 ss << "modifier '" << RooJSONFactoryWSTool::name(mod) << "' has " << constraintPdfs.size()
813 << " constraints, but " << nGammas << " parameters";
815 }
816 } else if (mod.has_child("constraint_type")) {
817 constraint = mod["constraint_type"].val();
818 } else if (mod.has_child("constraint")) {
819 std::string constraintValue = mod["constraint"].val();
820 if (isLegacyConstraintType(constraintValue)) {
821 constraint = constraintValue;
822 } else {
823 RooJSONFactoryWSTool::error("unable to resolve constraint value '" + constraintValue +
824 "' for modifier '" + RooJSONFactoryWSTool::name(mod) +
825 "': this looks like a legacy workspace where the 'constraint' field is "
826 "not a supported legacy constraint type");
827 }
828 }
829 shapeElems.add(createPHF(funcName, sysname, parnames, vals, tool, constraints, varlist, constraint,
831 /*createConstraints=*/!hasConstraintList));
832 for (auto *constraintPdf : constraintPdfs) {
833 if (constraintPdf) {
834 constraints.add(*constraintPdf);
835 }
836 }
837 } else if (modtype == "custom") {
838 RooAbsReal *obj = ws.function(sysname);
839 if (!obj) {
840 RooJSONFactoryWSTool::error("unable to find custom modifier '" + sysname + "'");
841 }
842 if (obj->dependsOn(varlist)) {
843 shapeElems.add(*obj);
844 } else {
845 normElems.add(*obj);
846 }
847 } else {
848 RooJSONFactoryWSTool::error("modifier '" + sysname + "' of unknown type '" + modtype + "'");
849 }
850 }
851
852 std::string interpName = sampleName + "_" + channelName + "_epsilon";
853 if (!overall_nps.empty()) {
854 auto &v = tool.wsEmplace<RooStats::HistFactory::FlexibleInterpVar>(interpName, overall_nps, 1., overall_low,
855 overall_high, overall_interp);
856 normElems.add(v);
857 }
858 if (!histNps.empty()) {
859 auto &v = tool.wsEmplace<PiecewiseInterpolation>("histoSys_" + prefixedName, hf, histoLo, histoHi, histNps,
860 histoInterp);
862 shapeElems.add(v);
863 } else {
864 shapeElems.add(hf);
865 }
866 }
867
868 tool.wsEmplace<RooProduct>(prefixedName + "_shapes", shapeElems);
869 if (!normElems.empty()) {
870 tool.wsEmplace<RooProduct>(prefixedName + "_scaleFactors", normElems);
871 } else {
872 ws.factory("RooConstVar::" + prefixedName + "_scaleFactors(1.)");
873 }
874
875 return true;
876}
877
878class HistFactoryImporter : public RooFit::JSONIO::Importer {
879public:
880 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
881 {
882 std::string name = RooJSONFactoryWSTool::name(p);
883 if (!p.has_child("samples")) {
884 RooJSONFactoryWSTool::error("no samples in '" + name + "', skipping.");
885 }
886 double statErrThresh = 0;
887 std::string statErrType = "Poisson";
888 std::optional<Interpolation> defaultInterpolation;
889 if (p.has_child("default_interpolation")) {
890 defaultInterpolation =
891 readInterpolation(p["default_interpolation"], "default_interpolation of channel '" + name + "'");
892 }
893 if (p.has_child(::Literals::staterror)) {
894 auto &staterr = p[::Literals::staterror];
895 if (staterr.has_child("relThreshold"))
896 statErrThresh = staterr["relThreshold"].val_double();
897 if (staterr.has_child("constraint_type"))
898 statErrType = staterr["constraint_type"].val();
899 }
900 std::vector<double> sumW;
901 std::vector<double> sumW2;
902 std::vector<std::string> gammaParnames;
903 std::string modifierStatErrType;
905
906 std::string fprefix = name;
907
908 std::vector<std::unique_ptr<RooDataHist>> data;
909 for (const auto &comp : p["samples"].children()) {
910 std::unique_ptr<RooDataHist> dh = RooJSONFactoryWSTool::readBinnedData(
911 comp["data"], fprefix + "_" + RooJSONFactoryWSTool::name(comp) + "_dataHist", observables);
912 size_t nbins = dh->numEntries();
913
914 if (const JSONNode *staterror = findStaterror(comp)) {
915 if (sumW.empty()) {
916 sumW.resize(nbins);
917 sumW2.resize(nbins);
918 }
919 for (size_t i = 0; i < nbins; ++i) {
920 sumW[i] += dh->weight(i);
921 sumW2[i] += dh->weightSquared(i);
922 }
923 if (gammaParnames.empty()) {
924 if (auto staterrorParams = staterror->find("parameters")) {
925 for (const auto &v : staterrorParams->children()) {
926 gammaParnames.push_back(v.val());
927 }
928 }
929 }
930 // The constraint type given on the staterror modifier takes precedence over the channel-level setting.
931 if (modifierStatErrType.empty()) {
932 // ROOT 6.38 and 6.40.00 wrote "constraint_type".
933 auto constraint = staterror->find("constraint");
934 if (!constraint) {
935 constraint = staterror->find("constraint_type");
936 }
937 if (constraint) {
938 modifierStatErrType = constraint->val();
939 if (modifierStatErrType != "Poisson" && modifierStatErrType != "Gauss") {
940 RooJSONFactoryWSTool::error("unsupported constraint '" + modifierStatErrType +
941 "' of the staterror modifier in channel '" + name +
942 "', supported are \"Poisson\" and \"Gauss\"");
943 }
944 }
945 }
946 }
947 data.emplace_back(std::move(dh));
948 }
949
950 if (!modifierStatErrType.empty()) {
951 statErrType = modifierStatErrType;
952 }
953
954 RooAbsArg *mcStatObject = nullptr;
955 RooArgSet constraints;
956 if (!sumW.empty()) {
957 std::string channelName = name;
958 erasePrefix(channelName, "model_");
959
960 std::vector<double> errs(sumW.size());
961 for (size_t i = 0; i < sumW.size(); ++i) {
962 if (sumW[i] == 0.) {
963 errs[i] = 0.;
964 continue;
965 }
966 errs[i] = std::sqrt(sumW2[i]) / sumW[i];
967 // avoid negative sigma. This NP will be set constant anyway later
968 errs[i] = std::max(errs[i], 0.);
969 }
970
971 mcStatObject =
972 &createPHF("mc_stat_" + channelName, "stat_" + channelName, gammaParnames, errs, *tool, constraints,
973 observables, statErrType, defaultGammaMin, defaultStatErrorGammaMax, statErrThresh);
974 }
975
976 LegacyInterpolationDefault legacyInterpolationDefault;
977 legacyInterpolationDefault.allowed = isHS3VersionBefore(tool->hs3VersionInput(), 0, 3);
978 bool warnedUnconstrained = false;
979
980 int idx = 0;
982 RooArgList coefs;
983 for (const auto &comp : p["samples"].children()) {
984 importHistSample(*tool, *data[idx], observables, mcStatObject, fprefix, comp, defaultInterpolation,
985 legacyInterpolationDefault, warnedUnconstrained, constraints);
986 ++idx;
987
988 std::string const &compName = RooJSONFactoryWSTool::name(comp);
989 funcs.add(*tool->request<RooAbsReal>(fprefix + "_" + compName + "_shapes", name));
990 coefs.add(*tool->request<RooAbsReal>(fprefix + "_" + compName + "_scaleFactors", name));
991 }
992
993 if (constraints.empty()) {
994 tool->wsEmplace<RooRealSumPdf>(name, funcs, coefs, true);
995 } else {
996 std::string sumName = name + "_model";
997 erasePrefix(sumName, "model_");
998 auto &sum = tool->wsEmplace<RooRealSumPdf>(sumName, funcs, coefs, true);
999 sum.SetTitle(name.c_str());
1000 tool->wsEmplace<RooProdPdf>(name, constraints, RooFit::Conditional(sum, observables));
1001 }
1002 return true;
1003 }
1004};
1005
1006class FlexibleInterpVarStreamer : public RooFit::JSONIO::Exporter {
1007public:
1008 std::string const &key() const override
1009 {
1010 static const std::string keystring = "interpolation0d";
1011 return keystring;
1012 }
1013 bool exportObject(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem) const override
1014 {
1015 auto fip = static_cast<const RooStats::HistFactory::FlexibleInterpVar *>(func);
1016 const std::size_t nParameters = fip->variables().size();
1017 if (fip->low().size() != nParameters || fip->high().size() != nParameters ||
1018 fip->interpolationCodes().size() != nParameters) {
1019 RooJSONFactoryWSTool::error("FlexibleInterpVar '" + std::string{fip->GetName()} +
1020 "' has non-matching parameter, variation, and interpolation lengths");
1021 }
1022 elem["type"] << key();
1023 writeInterpolations(elem["interpolations"], fip->interpolationCodes(), InterpolationClass::Flexible,
1024 "FlexibleInterpVar '" + std::string{fip->GetName()} + "'");
1025 RooJSONFactoryWSTool::fillSeq(elem["vars"], fip->variables());
1026 elem["nom"] << fip->nominal();
1027 elem["high"].fill_seq(fip->high(), fip->variables().size());
1028 elem["low"].fill_seq(fip->low(), fip->variables().size());
1029 return true;
1030 }
1031};
1032
1033class PiecewiseInterpolationStreamer : public RooFit::JSONIO::Exporter {
1034public:
1035 std::string const &key() const override
1036 {
1037 static const std::string keystring = "interpolation";
1038 return keystring;
1039 }
1040 bool exportObject(RooJSONFactoryWSTool *, const RooAbsArg *func, JSONNode &elem) const override
1041 {
1042 const PiecewiseInterpolation *pip = static_cast<const PiecewiseInterpolation *>(func);
1043 const std::size_t nParameters = pip->paramList().size();
1044 if (pip->lowList().size() != nParameters || pip->highList().size() != nParameters ||
1045 pip->interpolationCodes().size() != nParameters) {
1046 RooJSONFactoryWSTool::error("PiecewiseInterpolation '" + std::string{pip->GetName()} +
1047 "' has non-matching parameter, variation, and interpolation lengths");
1048 }
1049 elem["type"] << key();
1050 writeInterpolations(elem["interpolations"], pip->interpolationCodes(), InterpolationClass::Piecewise,
1051 "PiecewiseInterpolation '" + std::string{pip->GetName()} + "'");
1052 elem["positiveDefinite"] << pip->positiveDefinite();
1053 RooJSONFactoryWSTool::fillSeq(elem["vars"], pip->paramList());
1054 elem["nom"] << pip->nominalHist()->GetName();
1055 RooJSONFactoryWSTool::fillSeq(elem["high"], pip->highList(), pip->paramList().size());
1056 RooJSONFactoryWSTool::fillSeq(elem["low"], pip->lowList(), pip->paramList().size());
1057 return true;
1058 }
1059};
1060
1061class PiecewiseInterpolationFactory : public RooFit::JSONIO::Importer {
1062public:
1063 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
1064 {
1065 std::string name(RooJSONFactoryWSTool::name(p));
1066
1067 RooArgList vars{tool->requestArgList<RooAbsReal>(p, "vars")};
1068 RooArgList low{tool->requestArgList<RooAbsReal>(p, "low")};
1069 RooArgList high{tool->requestArgList<RooAbsReal>(p, "high")};
1070 if (vars.size() != low.size() || vars.size() != high.size()) {
1071 RooJSONFactoryWSTool::error("PiecewiseInterpolation '" + name +
1072 "' has non-matching lengths of 'vars', 'high' and 'low'");
1073 }
1074 const std::vector<int> codes =
1075 readInterpolations(p, vars.size(), InterpolationClass::Piecewise, "PiecewiseInterpolation '" + name + "'");
1076
1077 auto &pip =
1078 tool->wsEmplace<PiecewiseInterpolation>(name, *tool->requestArg<RooAbsReal>(p, "nom"), low, high, vars, codes);
1079
1080 pip.setPositiveDefinite(p["positiveDefinite"].val_bool());
1081
1082 return true;
1083 }
1084};
1085
1086class FlexibleInterpVarFactory : public RooFit::JSONIO::Importer {
1087public:
1088 bool importArg(RooJSONFactoryWSTool *tool, const JSONNode &p) const override
1089 {
1090 std::string name(RooJSONFactoryWSTool::name(p));
1091 if (!p.has_child("high")) {
1092 RooJSONFactoryWSTool::error("no high variations of '" + name + "'");
1093 }
1094 if (!p.has_child("low")) {
1095 RooJSONFactoryWSTool::error("no low variations of '" + name + "'");
1096 }
1097 if (!p.has_child("nom")) {
1098 RooJSONFactoryWSTool::error("no nominal variation of '" + name + "'");
1099 }
1100
1101 double nom(p["nom"].val_double());
1102
1103 RooArgList vars{tool->requestArgList<RooRealVar>(p, "vars")};
1104
1105 std::vector<double> high;
1106 high << p["high"];
1107
1108 std::vector<double> low;
1109 low << p["low"];
1110
1111 if (vars.size() != low.size() || vars.size() != high.size()) {
1112 RooJSONFactoryWSTool::error("FlexibleInterpVar '" + name +
1113 "' has non-matching lengths of 'vars', 'high' and 'low'!");
1114 }
1115 const std::vector<int> codes =
1116 readInterpolations(p, vars.size(), InterpolationClass::Flexible, "FlexibleInterpVar '" + name + "'");
1117
1118 tool->wsEmplace<RooStats::HistFactory::FlexibleInterpVar>(name, vars, nom, low, high, codes);
1119
1120 return true;
1121 }
1122};
1123
1124struct NormFactor {
1125 std::string name;
1126 RooAbsReal const *param = nullptr;
1127 RooAbsPdf const *constraint = nullptr;
1128 NormFactor(RooAbsReal const &par, const RooAbsPdf *constr = nullptr)
1129 : name{par.GetName()}, param{&par}, constraint{constr}
1130 {
1131 }
1132};
1133
1134struct NormSys {
1135 std::string name = "";
1136 RooAbsReal const *param = nullptr;
1137 double low = 1.;
1138 double high = 1.;
1139 Interpolation interpolation = multiplicativePolynomialExponential;
1140 RooAbsPdf const *constraint = nullptr;
1141 NormSys() {};
1142 NormSys(const std::string &n, RooAbsReal *const p, double h, double l, Interpolation i, const RooAbsPdf *c)
1143 : name(n), param(p), low(l), high(h), interpolation(std::move(i)), constraint(c)
1144 {
1145 }
1146};
1147
1148struct HistoSys {
1149 std::string name;
1150 RooAbsReal const *param = nullptr;
1151 std::vector<double> low;
1152 std::vector<double> high;
1153 Interpolation interpolation = additivePolynomialLinear;
1154 RooAbsPdf const *constraint = nullptr;
1155 HistoSys(const std::string &n, RooAbsReal *const p, RooHistFunc *l, RooHistFunc *h, Interpolation i,
1156 const RooAbsPdf *c)
1157 : name(n), param(p), interpolation(std::move(i)), constraint(c)
1158 {
1159 low.assign(l->dataHist().weightArray(), l->dataHist().weightArray() + l->dataHist().numEntries());
1160 high.assign(h->dataHist().weightArray(), h->dataHist().weightArray() + h->dataHist().numEntries());
1161 }
1162};
1163struct ShapeSys {
1164 std::string name;
1165 std::vector<double> constraints;
1166 std::vector<RooAbsPdf const *> constraintPdfs;
1167 std::vector<RooAbsReal *> parameters;
1168 ShapeSys(const std::string &n) : name{n} {}
1169};
1170
1171struct GenericElement {
1172 std::string name;
1173 RooAbsReal *function = nullptr;
1174 GenericElement(RooAbsReal *e) : name(e->GetName()), function(e) {};
1175};
1176
1177std::string stripOuterParens(const std::string &s)
1178{
1179 size_t start = 0;
1180 size_t end = s.size();
1181
1182 while (start < end && s[start] == '(' && s[end - 1] == ')') {
1183 int depth = 0;
1184 bool balanced = true;
1185 for (size_t i = start; i < end - 1; ++i) {
1186 if (s[i] == '(')
1187 ++depth;
1188 else if (s[i] == ')')
1189 --depth;
1190 if (depth == 0 && i < end - 1) {
1191 balanced = false;
1192 break;
1193 }
1194 }
1195 if (balanced) {
1196 ++start;
1197 --end;
1198 } else {
1199 break;
1200 }
1201 }
1202 return s.substr(start, end - start);
1203}
1204
1205std::vector<std::string> splitTopLevelProduct(const std::string &expr)
1206{
1207 std::vector<std::string> parts;
1208 int depth = 0;
1209 size_t start = 0;
1210 bool foundTopLevelStar = false;
1211
1212 for (size_t i = 0; i < expr.size(); ++i) {
1213 char c = expr[i];
1214 if (c == '(') {
1215 ++depth;
1216 } else if (c == ')') {
1217 --depth;
1218 } else if (c == '*' && depth == 0) {
1219 foundTopLevelStar = true;
1220 std::string sub = expr.substr(start, i - start);
1221 parts.push_back(stripOuterParens(sub));
1222 start = i + 1;
1223 }
1224 }
1225
1226 if (!foundTopLevelStar) {
1227 return {}; // Not a top-level product
1228 }
1229
1230 std::string sub = expr.substr(start);
1231 parts.push_back(stripOuterParens(sub));
1232 return parts;
1233}
1234
1235NormSys parseOverallModifierFormula(const std::string &s, RooFormulaVar *formula)
1236{
1237 static const std::regex pattern(
1238 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*$)");
1239
1240 NormSys sys;
1241 double sign = 1.0;
1242
1243 std::smatch match;
1244 if (std::regex_match(s, match, pattern)) {
1245 if (match[1].str() == "-") {
1246 sign = -1.0;
1247 }
1248
1249 std::string token2 = match[2].str();
1250 std::string token3 = match[4].str();
1251
1252 RooAbsReal *p2 = static_cast<RooAbsReal *>(formula->getParameter(token2.c_str()));
1253 RooAbsReal *p3 = static_cast<RooAbsReal *>(formula->getParameter(token3.c_str()));
1254 RooRealVar *v2 = dynamic_cast<RooRealVar *>(p2);
1255 RooRealVar *v3 = dynamic_cast<RooRealVar *>(p3);
1256
1257 auto *constr2 = findConstraint(v2);
1258 auto *constr3 = findConstraint(v3);
1259
1260 if (constr2 && !p3) {
1261 sys.name = p2->GetName();
1262 sys.param = p2;
1263 sys.high = 1.0 + sign * toDouble(token3);
1264 sys.low = 1.0 - sign * toDouble(token3);
1265 } else if (!p2 && constr3) {
1266 sys.name = p3->GetName();
1267 sys.param = p3;
1268 sys.high = 1.0 + sign * toDouble(token2);
1269 sys.low = 1.0 - sign * toDouble(token2);
1270 } else if (constr2 && p3 && !constr3) {
1271 sys.name = v2->GetName();
1272 sys.param = v2;
1273 sys.high = 1.0 + sign * p3->getVal();
1274 sys.low = 1.0 - sign * p3->getVal();
1275 } else if (p2 && !constr2 && constr3) {
1276 sys.name = v3->GetName();
1277 sys.param = v3;
1278 sys.high = 1.0 + sign * p2->getVal();
1279 sys.low = 1.0 - sign * p2->getVal();
1280 }
1281
1282 // A recognized formula has exactly one constrained parameter.
1283 sys.constraint = constr2 ? constr2 : constr3;
1284
1285 // Preserve the legacy export behaviour for recognized explicit formulae.
1286 sys.interpolation = multiplicativePiecewiseExponential;
1287
1288 erasePrefix(sys.name, "alpha_");
1289 }
1290 return sys;
1291}
1292
1293void collectElements(RooArgList &elems, RooAbsArg *arg)
1294{
1295 if (auto prod = dynamic_cast<RooProduct *>(arg)) {
1296 for (const auto &e : prod->components()) {
1297 collectElements(elems, e);
1298 }
1299 } else {
1300 elems.add(*arg);
1301 }
1302}
1303
1304bool allRooRealVar(const RooAbsCollection &list)
1305{
1306 for (auto *var : list) {
1307 if (!dynamic_cast<RooRealVar *>(var)) {
1308 return false;
1309 }
1310 }
1311 return true;
1312}
1313
1314struct Sample {
1315 std::string name;
1316 std::vector<double> hist;
1317 std::vector<double> histError;
1318 std::vector<NormFactor> normfactors;
1319 std::vector<NormSys> normsys;
1320 std::vector<HistoSys> histosys;
1321 std::vector<ShapeSys> shapesys;
1322 std::vector<GenericElement> tmpElements;
1323 std::vector<GenericElement> otherElements;
1324 bool useBarlowBeestonLight = false;
1325 std::vector<RooAbsReal *> staterrorParameters;
1326 Sample(const std::string &n) : name{n} {}
1327};
1328
1329void addNormFactor(RooRealVar const *par, Sample &sample, RooWorkspace *ws)
1330{
1331 std::string parname = par->GetName();
1332 bool isConstrained = false;
1333 for (RooAbsArg const *pdf : ws->allPdfs()) {
1334 if (auto gauss = dynamic_cast<RooGaussian const *>(pdf)) {
1335 if (parname == gauss->getX().GetName()) {
1336 sample.normfactors.emplace_back(*par, gauss);
1337 isConstrained = true;
1338 }
1339 }
1340 }
1341 if (!isConstrained)
1342 sample.normfactors.emplace_back(*par);
1343}
1344
1345struct Channel {
1346 std::string name;
1347 std::vector<Sample> samples;
1348 std::map<int, double> tot_yield;
1349 std::map<int, double> tot_yield2;
1350 std::map<int, double> rel_errors;
1351 std::string statErrorConstraintType;
1352 RooArgSet const *varSet = nullptr;
1353 long unsigned int nBins = 0;
1354};
1355
1356Channel readChannel(RooJSONFactoryWSTool *tool, const std::string &pdfname, const RooRealSumPdf *sumpdf)
1357{
1358 Channel channel;
1359
1360 RooWorkspace *ws = tool->workspace();
1361
1362 channel.name = pdfname;
1363 erasePrefix(channel.name, "model_");
1364 eraseSuffix(channel.name, "_model");
1365
1366 for (size_t sampleidx = 0; sampleidx < sumpdf->funcList().size(); ++sampleidx) {
1367 PiecewiseInterpolation *pip = nullptr;
1368 std::vector<ParamHistFunc *> phfs;
1369
1370 const auto func = sumpdf->funcList().at(sampleidx);
1371 Sample sample(func->GetName());
1372 erasePrefix(sample.name, "L_x_");
1373 eraseSuffix(sample.name, "_shapes");
1374 eraseSuffix(sample.name, "_" + channel.name);
1375 erasePrefix(sample.name, pdfname + "_");
1376
1377 auto updateObservables = [&](RooDataHist const &dataHist) {
1378 if (channel.varSet == nullptr) {
1379 channel.varSet = dataHist.get();
1380 channel.nBins = dataHist.numEntries();
1381 }
1382 if (sample.hist.empty()) {
1383 auto *w = dataHist.weightArray();
1384 sample.hist.assign(w, w + dataHist.numEntries());
1385 }
1386 };
1387 auto processElements = [&](const auto &elements, auto &&self) -> void {
1388 for (RooAbsArg *e : elements) {
1389 if (TString(e->GetName()).Contains("binWidth")) {
1390 // The bin width modifiers are handled separately. We can't just
1391 // check for the RooBinWidthFunction type here, because prior to
1392 // ROOT 6.26, the multiplication with the inverse bin width was
1393 // done in a different way (like a normfactor with a RooRealVar,
1394 // but it was stored in the dataset).
1395 // Fortunately, the name was similar, so we can match the modifier
1396 // name.
1397 } else if (auto constVar = dynamic_cast<RooConstVar *>(e)) {
1398 if (constVar->getVal() != 1.) {
1399 sample.normfactors.emplace_back(*constVar);
1400 }
1401 } else if (auto par = dynamic_cast<RooRealVar *>(e)) {
1402 addNormFactor(par, sample, ws);
1403 } else if (auto hf = dynamic_cast<const RooHistFunc *>(e)) {
1404 updateObservables(hf->dataHist());
1405 } else if (ParamHistFunc *phf = dynamic_cast<ParamHistFunc *>(e); phf && allRooRealVar(phf->paramList())) {
1406 phfs.push_back(phf);
1407 } else if (auto fip = dynamic_cast<RooStats::HistFactory::FlexibleInterpVar *>(e)) {
1408 // some (modified) histfactory models have several instances of FlexibleInterpVar
1409 // we collect and merge them
1410 for (size_t i = 0; i < fip->variables().size(); ++i) {
1411 RooAbsReal *var = static_cast<RooAbsReal *>(fip->variables().at(i));
1412 std::string sysname(var->GetName());
1413 erasePrefix(sysname, "alpha_");
1414 // A parameter without constraint is unconstrained, see checkUnconstrainedModifiers().
1415 const auto *constraint = findConstraint(var);
1416 const std::string context = "normsys modifier '" + sysname + "' in sample '" + sample.name +
1417 "' of channel '" + channel.name + "'";
1418 sample.normsys.emplace_back(
1419 sysname, var, fip->high()[i], fip->low()[i],
1420 interpolationFromCode(fip->interpolationCodes()[i], InterpolationClass::Flexible, context),
1421 constraint);
1422 }
1423 } else if (!pip && (pip = dynamic_cast<PiecewiseInterpolation *>(e))) {
1424 // nothing to do here, already assigned
1425 } else if (RooFormulaVar *formula = dynamic_cast<RooFormulaVar *>(e)) {
1426 // people do a lot of fancy stuff with RooFormulaVar, like including NormSys via explicit formulae.
1427 // let's try to decompose it into building blocks
1428 TString expression(formula->expression());
1429 for (size_t i = formula->nParameters(); i--;) {
1430 const RooAbsArg *p = formula->getParameter(i);
1431 expression.ReplaceAll(("x[" + std::to_string(i) + "]").c_str(), p->GetName());
1432 expression.ReplaceAll(("@" + std::to_string(i)).c_str(), p->GetName());
1433 }
1434 auto components = splitTopLevelProduct(expression.Data());
1435 if (components.size() == 0) {
1436 // it's not a product, let's just treat it as an unknown element
1437 sample.otherElements.push_back(formula);
1438 } else {
1439 // it is a prododuct, we can try to handle the elements separately
1440 std::vector<RooAbsArg *> realComponents;
1441 int idx = 0;
1442 for (auto &comp : components) {
1443 // check if this is a trivial element of a product, we can treat it as its own modifier
1444 auto *part = formula->getParameter(comp.c_str());
1445 if (part) {
1446 realComponents.push_back(part);
1447 continue;
1448 }
1449 // check if this is an attempt at explicitly encoding an overallSys
1450 auto normsys = parseOverallModifierFormula(comp, formula);
1451 if (normsys.param) {
1452 sample.normsys.emplace_back(std::move(normsys));
1453 continue;
1454 }
1455
1456 // this is something non-trivial, let's deal with it separately
1457 std::string name = std::string(formula->GetName()) + "_part" + std::to_string(idx);
1458 ++idx;
1459 auto *var = new RooFormulaVar(name.c_str(), name.c_str(), comp.c_str(), formula->dependents());
1460 sample.tmpElements.push_back({var});
1461 }
1462 self(realComponents, self);
1463 }
1464 } else if (auto real = dynamic_cast<RooAbsReal *>(e)) {
1465 sample.otherElements.push_back(real);
1466 }
1467 }
1468 };
1469
1470 RooArgList elems;
1471 collectElements(elems, func);
1472 collectElements(elems, sumpdf->coefList().at(sampleidx));
1473 processElements(elems, processElements);
1474
1475 // see if we can get the observables
1476 if (pip) {
1477 if (auto nh = dynamic_cast<RooHistFunc const *>(pip->nominalHist())) {
1478 updateObservables(nh->dataHist());
1479 }
1480 }
1481
1482 // sort and configure norms
1483 sortByName(sample.normfactors);
1484 sortByName(sample.normsys);
1485
1486 // sort and configure the histosys
1487 if (pip) {
1488 for (size_t i = 0; i < pip->paramList().size(); ++i) {
1489 RooAbsReal *var = static_cast<RooAbsReal *>(pip->paramList().at(i));
1490 std::string sysname(var->GetName());
1491 erasePrefix(sysname, "alpha_");
1492 if (auto lo = dynamic_cast<RooHistFunc *>(pip->lowList().at(i))) {
1493 if (auto hi = dynamic_cast<RooHistFunc *>(pip->highList().at(i))) {
1494 // A parameter without constraint is unconstrained, see checkUnconstrainedModifiers().
1495 const auto *constraint = findConstraint(var);
1496 const std::string context = "histosys modifier '" + sysname + "' in sample '" + sample.name +
1497 "' of channel '" + channel.name + "'";
1498 sample.histosys.emplace_back(
1499 sysname, var, lo, hi,
1500 interpolationFromCode(pip->interpolationCodes()[i], InterpolationClass::Piecewise, context),
1501 constraint);
1502 }
1503 }
1504 }
1505 sortByName(sample.histosys);
1506 }
1507
1508 for (ParamHistFunc *phf : phfs) {
1509 if (startsWith(std::string(phf->GetName()), "mc_stat_")) { // MC stat uncertainty
1510 int idx = 0;
1511 for (const auto &g : phf->paramList()) {
1512 sample.staterrorParameters.push_back(static_cast<RooRealVar *>(g));
1513 ++idx;
1514 RooAbsPdf *constraint = findConstraint(g);
1515 channel.tot_yield[idx] += sample.hist[idx - 1];
1516 channel.tot_yield2[idx] += (sample.hist[idx - 1] * sample.hist[idx - 1]);
1517 if (constraint) {
1518 channel.rel_errors[idx] = constraintRelError(*constraint, *g);
1519 channel.statErrorConstraintType = dynamic_cast<RooPoisson *>(constraint) ? "Poisson" : "Gauss";
1520 }
1521 }
1522 sample.useBarlowBeestonLight = true;
1523 } else { // other ShapeSys
1524 ShapeSys sys(phf->GetName());
1525 erasePrefix(sys.name, channel.name + "_");
1526 bool isshapesys = eraseSuffix(sys.name, "_ShapeSys") || eraseSuffix(sys.name, "_shapeSys");
1527 bool isshapefactor = eraseSuffix(sys.name, "_ShapeFactor") || eraseSuffix(sys.name, "_shapeFactor");
1528
1529 for (const auto &g : phf->paramList()) {
1530 sys.parameters.push_back(static_cast<RooRealVar *>(g));
1531 RooAbsPdf *constraint = nullptr;
1532 if (isshapesys) {
1533 constraint = findConstraint(g);
1534 if (!constraint)
1535 constraint = ws->pdf(constraintName(g->GetName()));
1536 if (!constraint && !g->isConstant()) {
1537 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(g->GetName()));
1538 }
1539 } else if (!isshapefactor) {
1540 RooJSONFactoryWSTool::error("unknown type of shapesys " + std::string(phf->GetName()));
1541 }
1542 if (!constraint) {
1543 sys.constraints.push_back(0.0);
1544 sys.constraintPdfs.push_back(nullptr);
1545 } else {
1546 sys.constraints.push_back(constraintRelError(*constraint, *g));
1547 sys.constraintPdfs.push_back(constraint);
1548 }
1549 }
1550 sample.shapesys.emplace_back(std::move(sys));
1551 }
1552 }
1553 sortByName(sample.shapesys);
1554
1555 // add the sample
1556 channel.samples.emplace_back(std::move(sample));
1557 }
1558
1559 sortByName(channel.samples);
1560 return channel;
1561}
1562
1563bool hasSameMetadata(const RooAbsArg *lhs, const RooAbsArg *rhs)
1564{
1565 if (!lhs || !rhs) {
1566 return lhs == rhs;
1567 }
1568 return std::string{lhs->GetName()} == rhs->GetName() && lhs->IsA() == rhs->IsA();
1569}
1570
1571[[noreturn]] void duplicateModifierError(const Channel &channel, const Sample &sample, std::string_view type,
1572 std::string_view name, std::string_view reason)
1573{
1574 std::stringstream ss;
1575 ss << "cannot combine duplicate modifier '" << name << "' of type '" << type << "' in sample '" << sample.name
1576 << "' of channel '" << channel.name << "': " << reason;
1577 RooJSONFactoryWSTool::error(ss.str().c_str());
1578}
1579
1580void warnDuplicateModifiersCombined(const Channel &channel, const Sample &sample, std::string_view type,
1581 std::string_view name, std::size_t count)
1582{
1583 std::stringstream ss;
1584 ss << "combined " << count << " duplicate modifiers named '" << name << "' of type '" << type << "' in sample '"
1585 << sample.name << "' of channel '" << channel.name << "'";
1587}
1588
1589// Multiplicatively combining two normsys is only faithful when the interpolation is done in log-space, so that
1590// f1(alpha) * f2(alpha) is again representable by a single normsys with the multiplied lo/hi factors. This holds for
1591// the piecewise-exponential code 1 (exact everywhere) and for the default code 4 (exact at the +-1 sigma anchors and in
1592// the exponential extrapolation region). The linear-space codes (e.g. 0 and 2) would turn the product into a shape that
1593// cannot be represented by a single normsys, so those must not be merged.
1594bool normSysSupportsMultiplicativeMerge(const Interpolation &interpolation)
1595{
1596 return interpolation == multiplicativePiecewiseExponential || interpolation == multiplicativePolynomialExponential;
1597}
1598
1599// Combines runs of adjacent modifiers that share the same name (the container is sorted by name beforehand) into a
1600// single modifier. The shared metadata (constraint, parameter and interpolation behaviour) must be identical across the
1601// duplicates; the type-specific `combine` callable performs the actual merge and any additional validation.
1602template <class Modifiers, class CombineFn>
1603void mergeDuplicateModifiers(const Channel &channel, const Sample &sample, Modifiers &modifiers, std::string_view type,
1604 CombineFn combine)
1605{
1606 Modifiers mergedModifiers;
1607 mergedModifiers.reserve(modifiers.size());
1608
1609 for (std::size_t begin = 0; begin < modifiers.size();) {
1610 std::size_t end = begin + 1;
1611 while (end < modifiers.size() && modifiers[end].name == modifiers[begin].name) {
1612 ++end;
1613 }
1614
1615 auto merged = modifiers[begin];
1616 for (std::size_t i = begin + 1; i < end; ++i) {
1617 const auto &modifier = modifiers[i];
1618 if (!hasSameMetadata(merged.constraint, modifier.constraint)) {
1619 duplicateModifierError(channel, sample, type, merged.name, "constraint metadata differs");
1620 }
1621 if (!hasSameMetadata(merged.param, modifier.param)) {
1622 duplicateModifierError(channel, sample, type, merged.name, "parameter metadata differs");
1623 }
1624 if (merged.interpolation != modifier.interpolation) {
1625 duplicateModifierError(channel, sample, type, merged.name, "interpolation behaviours differ");
1626 }
1627 combine(merged, modifier);
1628 }
1629
1630 if (end - begin > 1) {
1631 warnDuplicateModifiersCombined(channel, sample, type, merged.name, end - begin);
1632 }
1633 mergedModifiers.emplace_back(std::move(merged));
1634 begin = end;
1635 }
1636
1637 modifiers = std::move(mergedModifiers);
1638}
1639
1640void mergeDuplicateNormSys(const Channel &channel, Sample &sample)
1641{
1642 mergeDuplicateModifiers(channel, sample, sample.normsys, "normsys", [&](NormSys &merged, const NormSys &modifier) {
1643 if (!normSysSupportsMultiplicativeMerge(merged.interpolation)) {
1644 duplicateModifierError(channel, sample, "normsys", merged.name,
1645 "multiplicative combination is only valid for log-space interpolation");
1646 }
1647 merged.low *= modifier.low;
1648 merged.high *= modifier.high;
1649 });
1650}
1651
1652void mergeDuplicateHistoSys(const Channel &channel, Sample &sample)
1653{
1654 const std::size_t nBins = sample.hist.size();
1655 mergeDuplicateModifiers(
1656 channel, sample, sample.histosys, "histosys", [&](HistoSys &merged, const HistoSys &modifier) {
1657 if (merged.interpolation != additivePolynomialLinear) {
1658 duplicateModifierError(channel, sample, "histosys", merged.name,
1659 "this interpolation cannot currently be combined for duplicate histosys "
1660 "modifiers");
1661 }
1662 if (merged.low.size() != nBins || merged.high.size() != nBins || modifier.low.size() != nBins ||
1663 modifier.high.size() != nBins) {
1664 duplicateModifierError(channel, sample, "histosys", merged.name, "histogram binning differs");
1665 }
1666 for (std::size_t bin = 0; bin < nBins; ++bin) {
1667 merged.low[bin] += modifier.low[bin] - sample.hist[bin];
1668 merged.high[bin] += modifier.high[bin] - sample.hist[bin];
1669 }
1670 });
1671}
1672
1673void ensureUniqueModifiers(const Channel &channel, const Sample &sample)
1674{
1675 std::set<std::pair<std::string, std::string>> seen;
1676 auto add = [&](std::string type, const std::string &name) {
1677 if (!seen.emplace(type, name).second) {
1678 duplicateModifierError(channel, sample, type, name,
1679 "this modifier type cannot be combined without changing its meaning");
1680 }
1681 };
1682
1683 for (const auto &modifier : sample.normfactors)
1684 add("normfactor", modifier.name);
1685 for (const auto &modifier : sample.normsys)
1686 add("normsys", modifier.name);
1687 for (const auto &modifier : sample.histosys)
1688 add("histosys", modifier.name);
1689 for (const auto &modifier : sample.shapesys)
1690 add("shapesys", modifier.name);
1691 for (const auto &modifier : sample.otherElements)
1692 add("custom", modifier.name);
1693 for (const auto &modifier : sample.tmpElements)
1694 add("custom", modifier.name);
1695 if (sample.useBarlowBeestonLight)
1696 add(::Literals::staterror, ::Literals::staterror);
1697}
1698
1699void canonicalizeModifiers(Channel &channel)
1700{
1701 for (auto &sample : channel.samples) {
1702 mergeDuplicateNormSys(channel, sample);
1703 mergeDuplicateHistoSys(channel, sample);
1704 ensureUniqueModifiers(channel, sample);
1705 }
1706}
1707
1708void configureStatError(Channel &channel)
1709{
1710 for (auto &sample : channel.samples) {
1711 if (sample.useBarlowBeestonLight) {
1712 sample.histError.resize(sample.hist.size());
1713 for (auto bin : channel.rel_errors) {
1714 // reverse engineering the correct partial error
1715 // the (arbitrary) convention used here is that all samples should have the same relative error
1716 const int i = bin.first;
1717 const double relerr_tot = bin.second;
1718 const double count = sample.hist[i - 1];
1719 // this reconstruction is inherently imprecise, so we truncate it at some decimal places to make sure that
1720 // we don't carry around too many useless digits
1721 sample.histError[i - 1] =
1722 round_prec(relerr_tot * channel.tot_yield[i] / std::sqrt(channel.tot_yield2[i]) * count, 7);
1723 }
1724 }
1725 }
1726}
1727
1728std::optional<Interpolation> defaultInterpolation(const Channel &channel)
1729{
1730 std::map<Interpolation, std::size_t> counts;
1731 for (const auto &sample : channel.samples) {
1732 for (const auto &modifier : sample.normsys) {
1733 ++counts[modifier.interpolation];
1734 }
1735 for (const auto &modifier : sample.histosys) {
1736 ++counts[modifier.interpolation];
1737 }
1738 }
1739 if (counts.empty()) {
1740 return std::nullopt;
1741 }
1742
1743 auto best = counts.begin();
1744 for (auto current = std::next(counts.begin()); current != counts.end(); ++current) {
1745 if (current->second > best->second ||
1746 (current->second == best->second && current->first == multiplicativePolynomialExponential &&
1747 best->first != multiplicativePolynomialExponential)) {
1748 best = current;
1749 }
1750 }
1751 return best->first;
1752}
1753
1754bool exportChannel(RooJSONFactoryWSTool *tool, const Channel &channel, JSONNode &elem)
1755{
1756 // Write the constraint reference for any modifier that supports an
1757 // external Gaussian/Poisson/etc. constraint.
1758 auto writeConstraint = [](JSONNode &mod, auto const &sys) {
1759 if (sys.constraint) {
1760 mod["constraint"] << sys.constraint->GetName();
1761 }
1762 };
1763 auto addModifier = [](JSONNode &modifiers, std::string const &name, const char *type) -> JSONNode & {
1764 auto &mod = modifiers.append_child();
1765 mod.set_map();
1766 mod["name"] << name;
1767 mod["type"] << type;
1768 return mod;
1769 };
1770
1771 elem["type"] << "histfactory_dist";
1772 // The constraint type of the staterror is also written on the channel level, where older RooFit versions read it.
1773 if (!channel.statErrorConstraintType.empty()) {
1774 elem[::Literals::staterror].set_map()["constraint_type"] << channel.statErrorConstraintType;
1775 }
1776 const auto channelDefaultInterpolation = defaultInterpolation(channel);
1777 if (channelDefaultInterpolation) {
1778 writeInterpolation(elem["default_interpolation"], *channelDefaultInterpolation);
1779 }
1780
1781 bool observablesWritten = false;
1782 for (const auto &sample : channel.samples) {
1783
1784 auto &s = RooJSONFactoryWSTool::appendNamedChild(elem["samples"], sample.name);
1785
1786 auto &modifiers = s["modifiers"];
1787 modifiers.set_seq();
1788
1789 for (const auto &nf : sample.normfactors) {
1790 auto &mod = modifiers.append_child();
1791 mod.set_map();
1792 mod["name"] << nf.name;
1793 mod["parameter"] << nf.param->GetName();
1794 mod["type"] << "normfactor";
1795 if (nf.constraint) {
1796 mod["constraint"] << nf.constraint->GetName();
1797 tool->queueExport(*nf.constraint);
1798 }
1799 }
1800
1801 for (const auto &sys : sample.normsys) {
1802 auto &mod = addModifier(modifiers, sys.name, "normsys");
1803 mod["parameter"] << sys.param->GetName();
1804 if (!channelDefaultInterpolation || sys.interpolation != *channelDefaultInterpolation) {
1805 writeInterpolation(mod["interpolation"], sys.interpolation);
1806 }
1807 writeConstraint(mod, sys);
1808 auto &data = mod["data"].set_map();
1809 data["lo"] << sys.low;
1810 data["hi"] << sys.high;
1811 }
1812
1813 for (const auto &sys : sample.histosys) {
1814 auto &mod = addModifier(modifiers, sys.name, "histosys");
1815 mod["parameter"] << sys.param->GetName();
1816 if (!channelDefaultInterpolation || sys.interpolation != *channelDefaultInterpolation) {
1817 writeInterpolation(mod["interpolation"], sys.interpolation);
1818 }
1819 writeConstraint(mod, sys);
1820 auto &data = mod["data"].set_map();
1821 if (channel.nBins != sys.low.size() || channel.nBins != sys.high.size()) {
1822 RooJSONFactoryWSTool::error("inconsistent binning: " + std::to_string(channel.nBins) +
1823 " bins expected, but " + std::to_string(sys.low.size()) + "/" +
1824 std::to_string(sys.high.size()) + " found in nominal histogram errors!");
1825 }
1826 RooJSONFactoryWSTool::exportArray(channel.nBins, sys.low.data(), data["lo"].set_map()["contents"]);
1827 RooJSONFactoryWSTool::exportArray(channel.nBins, sys.high.data(), data["hi"].set_map()["contents"]);
1828 }
1829
1830 for (const auto &sys : sample.shapesys) {
1831 auto &mod = addModifier(modifiers, sys.name, "shapesys");
1832 exportGammaParameters(mod, sys.parameters);
1833 if (std::any_of(sys.constraintPdfs.begin(), sys.constraintPdfs.end(),
1834 [](auto *pdf) { return pdf != nullptr; })) {
1835 auto &constraintNames = mod["constraints"].set_seq();
1836 for (auto *constraint : sys.constraintPdfs) {
1837 if (constraint) {
1838 constraintNames.append_child() << constraint->GetName();
1839 } else {
1840 constraintNames.append_child().set_null();
1841 }
1842 }
1843 }
1844 mod["data"].set_map()["vals"].fill_seq(sys.constraints);
1845 }
1846
1847 for (const auto &other : sample.otherElements) {
1848 addModifier(modifiers, other.name, "custom");
1849 }
1850 for (const auto &other : sample.tmpElements) {
1851 addModifier(modifiers, other.name, "custom");
1852 }
1853
1854 if (sample.useBarlowBeestonLight) {
1855 auto &mod = addModifier(modifiers, ::Literals::staterror, ::Literals::staterror);
1856 exportGammaParameters(mod, sample.staterrorParameters);
1857 if (!channel.statErrorConstraintType.empty()) {
1858 mod["constraint"] << channel.statErrorConstraintType;
1859 }
1860 }
1861
1862 if (!observablesWritten) {
1863 auto &output = elem["axes"].set_seq();
1864 for (auto *obs : static_range_cast<RooRealVar *>(*channel.varSet)) {
1865 RooJSONFactoryWSTool::exportAxis(output.append_child().set_map(), *obs);
1866 }
1867 observablesWritten = true;
1868 }
1869 auto &dataNode = s["data"].set_map();
1870 if (channel.nBins != sample.hist.size()) {
1871 RooJSONFactoryWSTool::error("inconsistent binning: " + std::to_string(channel.nBins) + " bins expected, but " +
1872 std::to_string(sample.hist.size()) + " found in nominal histogram!");
1873 }
1874 RooJSONFactoryWSTool::exportArray(channel.nBins, sample.hist.data(), dataNode["contents"]);
1875 if (!sample.histError.empty()) {
1876 if (channel.nBins != sample.histError.size()) {
1877 RooJSONFactoryWSTool::error("inconsistent binning: " + std::to_string(channel.nBins) +
1878 " bins expected, but " + std::to_string(sample.histError.size()) +
1879 " found in nominal histogram errors!");
1880 }
1881 RooJSONFactoryWSTool::exportArray(channel.nBins, sample.histError.data(), dataNode["errors"]);
1882 }
1883 }
1884
1885 return true;
1886}
1887
1888std::vector<RooAbsPdf *> findLostConstraints(const Channel &channel, const std::vector<RooAbsPdf *> &constraints)
1889{
1890 // collect all the vars that are used by the model
1891 std::set<const RooAbsReal *> vars;
1892 for (const auto &sample : channel.samples) {
1893 for (const auto &nf : sample.normfactors) {
1894 vars.insert(nf.param);
1895 }
1896 for (const auto &sys : sample.normsys) {
1897 vars.insert(sys.param);
1898 }
1899
1900 for (const auto &sys : sample.histosys) {
1901 vars.insert(sys.param);
1902 }
1903 for (const auto &sys : sample.shapesys) {
1904 for (const auto &par : sys.parameters) {
1905 vars.insert(par);
1906 }
1907 }
1908 if (sample.useBarlowBeestonLight) {
1909 for (const auto &par : sample.staterrorParameters) {
1910 vars.insert(par);
1911 }
1912 }
1913 }
1914
1915 // check if there is any constraint present that is unrelated to these vars
1916 std::vector<RooAbsPdf *> lostConstraints;
1917 for (auto *pdf : constraints) {
1918 bool related = false;
1919 for (const auto *var : vars) {
1920 if (pdf->dependsOn(*var)) {
1921 related = true;
1922 }
1923 }
1924 if (!related) {
1925 lostConstraints.push_back(pdf);
1926 }
1927 }
1928 // return the constraints that would be "lost" when exporting the model
1929 return lostConstraints;
1930}
1931
1932// A normsys or histosys modifier without a recognized constraint is exported as unconstrained. That is only correct if
1933// its parameter is a variable, and none of the other constraint terms of the channel depends on it.
1934void checkUnconstrainedModifiers(const Channel &channel, const std::vector<RooAbsPdf *> &constraints)
1935{
1936 auto check = [&](auto const &modifiers) {
1937 for (auto const &sys : modifiers) {
1938 if (sys.constraint || sys.param->isConstant()) {
1939 continue;
1940 }
1941 if (!dynamic_cast<RooRealVar const *>(sys.param)) {
1942 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(sys.param->GetName()) +
1943 ": functions are not supported as parameters of this modifier");
1944 }
1945 for (RooAbsPdf const *pdf : constraints) {
1946 if (pdf->dependsOn(*sys.param)) {
1947 RooJSONFactoryWSTool::error("cannot find constraint for " + std::string(sys.param->GetName()) +
1948 ": the constraint term '" + pdf->GetName() + "' is not supported");
1949 }
1950 }
1951 }
1952 };
1953 for (const auto &sample : channel.samples) {
1954 check(sample.normsys);
1955 check(sample.histosys);
1956 }
1957}
1958
1959bool tryExportHistFactory(RooJSONFactoryWSTool *tool, const std::string &pdfname, const RooRealSumPdf *sumpdf,
1960 std::vector<RooAbsPdf *> constraints, JSONNode &elem)
1961{
1962 // some preliminary checks
1963 if (!sumpdf) {
1964 return false;
1965 }
1966
1967 for (RooAbsArg *sample : sumpdf->funcList()) {
1968 if (!dynamic_cast<RooProduct *>(sample) && !dynamic_cast<RooRealSumPdf *>(sample)) {
1969 return false;
1970 }
1971 }
1972
1973 auto channel = readChannel(tool, pdfname, sumpdf);
1974
1975 // sanity checks
1976 if (channel.samples.size() == 0)
1977 return false;
1978 for (auto &sample : channel.samples) {
1979 if (sample.hist.empty()) {
1980 return false;
1981 }
1982 }
1983
1984 canonicalizeModifiers(channel);
1985 checkUnconstrainedModifiers(channel, constraints);
1986
1987 // stat error handling
1988 configureStatError(channel);
1989
1990 auto lostConstraints = findLostConstraints(channel, constraints);
1991 // Export all the lost constraints
1992 for (const auto *constraint : lostConstraints) {
1994 "losing constraint term '" + std::string(constraint->GetName()) +
1995 "', implicit constraints are not supported by HS3 yet! The term will appear in the HS3 file, but will not be "
1996 "picked up when creating a likelihood from it! You will have to add it manually as an external constraint.");
1997 tool->queueExport(*constraint);
1998 }
1999
2000 // Export all the regular modifiers
2001 auto queueConstraints = [&](auto const &modifiers) {
2002 for (auto &modifier : modifiers) {
2003 if (modifier.constraint) {
2004 tool->queueExport(*modifier.constraint);
2005 }
2006 }
2007 };
2008 for (const auto &sample : channel.samples) {
2009 queueConstraints(sample.normfactors);
2010 queueConstraints(sample.normsys);
2011 queueConstraints(sample.histosys);
2012 for (auto &modifier : sample.shapesys) {
2013 for (auto *constraint : modifier.constraintPdfs) {
2014 if (constraint) {
2015 tool->queueExport(*constraint);
2016 }
2017 }
2018 }
2019 }
2020
2021 // Export all the custom modifiers
2022 for (const auto &sample : channel.samples) {
2023 for (auto &modifier : sample.otherElements) {
2024 tool->queueExport(*modifier.function);
2025 }
2026 for (auto &modifier : sample.tmpElements) {
2027 tool->queueExportTemporary(modifier.function);
2028 }
2029 }
2030
2031 // Export all model parameters
2032 RooArgSet parameters;
2033 sumpdf->getParameters(channel.varSet, parameters);
2034 for (RooAbsArg *param : parameters) {
2035 // This should exclude the global observables
2036 if (!startsWith(std::string{param->GetName()}, "nom_")) {
2037 tool->queueExport(*param);
2038 }
2039 }
2040
2041 return exportChannel(tool, channel, elem);
2042}
2043
2044class HistFactoryStreamer_ProdPdf : public RooFit::JSONIO::Exporter {
2045public:
2046 bool autoExportDependants() const override { return false; }
2047 bool tryExport(RooJSONFactoryWSTool *tool, const RooProdPdf *prodpdf, JSONNode &elem) const
2048 {
2049 std::vector<RooAbsPdf *> constraints;
2050 RooRealSumPdf *sumpdf = nullptr;
2051 for (auto *pdf : static_range_cast<RooAbsPdf *>(prodpdf->pdfList())) {
2052 auto thispdf = dynamic_cast<RooRealSumPdf *>(pdf);
2053 if (thispdf) {
2054 if (!sumpdf)
2055 sumpdf = thispdf;
2056 else
2057 return false;
2058 } else {
2059 constraints.push_back(pdf);
2060 }
2061 }
2062 if (!sumpdf)
2063 return false;
2064
2065 bool ok = tryExportHistFactory(tool, prodpdf->GetName(), sumpdf, constraints, elem);
2066 return ok;
2067 }
2068 std::string const &key() const override
2069 {
2070 static const std::string keystring = "histfactory_dist";
2071 return keystring;
2072 }
2073 bool exportObject(RooJSONFactoryWSTool *tool, const RooAbsArg *p, JSONNode &elem) const override
2074 {
2075 return tryExport(tool, static_cast<const RooProdPdf *>(p), elem);
2076 }
2077};
2078
2079class HistFactoryStreamer_SumPdf : public RooFit::JSONIO::Exporter {
2080public:
2081 bool autoExportDependants() const override { return false; }
2082 bool tryExport(RooJSONFactoryWSTool *tool, const RooRealSumPdf *sumpdf, JSONNode &elem) const
2083 {
2084 std::vector<RooAbsPdf *> constraints;
2085 return tryExportHistFactory(tool, sumpdf->GetName(), sumpdf, constraints, elem);
2086 }
2087 std::string const &key() const override
2088 {
2089 static const std::string keystring = "histfactory_dist";
2090 return keystring;
2091 }
2092 bool exportObject(RooJSONFactoryWSTool *tool, const RooAbsArg *p, JSONNode &elem) const override
2093 {
2094 return tryExport(tool, static_cast<const RooRealSumPdf *>(p), elem);
2095 }
2096};
2097
2098STATIC_EXECUTE([]() {
2099 using namespace RooFit::JSONIO;
2100
2101 registerImporter<HistFactoryImporter>("histfactory_dist", true);
2102 registerImporter<PiecewiseInterpolationFactory>("interpolation", true);
2103 registerImporter<FlexibleInterpVarFactory>("interpolation0d", true);
2104 registerExporter<FlexibleInterpVarStreamer>(RooStats::HistFactory::FlexibleInterpVar::Class(), true);
2105 registerExporter<PiecewiseInterpolationStreamer>(PiecewiseInterpolation::Class(), true);
2106 registerExporter<HistFactoryStreamer_ProdPdf>(RooProdPdf::Class(), true);
2107 registerExporter<HistFactoryStreamer_SumPdf>(RooRealSumPdf::Class(), true);
2108});
2109
2110} // namespace
bool isHS3VersionBefore(std::string const &version, int major, int minor)
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:77
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:662
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)
std::string const & hs3VersionInput() const
The "hs3_version" of the document that is currently being imported, or an empty string outside of an ...
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