Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
HS3Document.cxx
Go to the documentation of this file.
1/*
2 * Project: RooFit
3 * Authors:
4 * Jonas Rembser, CERN, Nov 2026
5 *
6 * Copyright (c) 2026, 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
13// Operations on documents that RooFit needs besides reading, upgrading, validating and writing them.
14
15#include "HS3Document.h"
16
17#include <algorithm>
18
19namespace RooFit {
20namespace HS3 {
21
22DomainRanges::DomainRanges(std::vector<Domain> const &domains)
23{
24 std::vector<Domain const *> sorted;
25 for (Domain const &domain : domains) {
26 sorted.push_back(&domain);
27 }
28 std::stable_sort(sorted.begin(), sorted.end(), [](Domain const *l, Domain const *r) { return l->name < r->name; });
29 const bool hasDefault =
30 std::any_of(domains.begin(), domains.end(), [](Domain const &d) { return d.name == defaultDomainName; });
31
32 for (Domain const *domain : sorted) {
33 // Of several axes of a variable in one domain, the last one counts.
34 std::map<std::string, DomainAxis const *> axes;
35 for (auto const &axis : domain->axes) {
36 if (auto const *continuous = std::get_if<DomainAxis>(&axis)) {
37 axes[continuous->name] = continuous;
38 _names.insert(continuous->name);
39 }
40 }
41 if (!hasDefault || domain->name == defaultDomainName) {
42 _ranges.insert(axes.begin(), axes.end());
43 }
44 }
45}
46
47DomainAxis const *DomainRanges::find(std::string const &name) const
48{
49 auto found = _ranges.find(name);
50 return found != _ranges.end() ? found->second : nullptr;
51}
52
53namespace {
54
55std::string const &nameOf(Modifier const &modifier)
56{
57 return std::visit([](auto const &m) -> std::string const & { return m.name; }, modifier);
58}
59
60char const *typeOf(Modifier const &modifier)
61{
62 return std::visit(
63 overloaded{[](NormFactor const &) { return "normfactor"; }, [](NormSys const &) { return "normsys"; },
64 [](HistoSys const &) { return "histosys"; }, [](ShapeSys const &) { return "shapefactor"; },
65 [](StatError const &) { return "staterror"; }, [](CustomModifier const &) { return "custom"; }},
66 modifier);
67}
68
69// What duplicates must have in common to be combined.
70template <class Modifier_t>
71char const *differences(Modifier_t const &l, Modifier_t const &r)
72{
73 if (!(l.constraint == r.constraint)) {
74 return "constraint metadata differs";
75 }
76 if (l.parameter.name != r.parameter.name) {
77 return "parameter metadata differs";
78 }
79 if (l.interpolation != r.interpolation) {
80 return "interpolation behaviours differ";
81 }
82 return nullptr;
83}
84
85// Combines `modifier` into `merged`, or returns why that's not possible.
86char const *combine(NormSys &merged, NormSys const &modifier, std::vector<double> const &)
87{
88 if (char const *reason = differences(merged, modifier)) {
89 return reason;
90 }
91 // Multiplicatively combining two normsys is only faithful when the interpolation is done in log-space, so that
92 // f1(alpha) * f2(alpha) is again representable by a single normsys with the multiplied lo/hi factors. This holds
93 // for the piecewise exponential (exact everywhere) and for the polynomial exponential (exact at the +-1 sigma
94 // anchors and in the exponential extrapolation region). The linear-space interpolations would turn the product into
95 // a shape that cannot be represented by a single normsys.
96 if (merged.interpolation != multiplicativePiecewiseExponential &&
97 merged.interpolation != multiplicativePolynomialExponential) {
98 return "multiplicative combination is only valid for log-space interpolation";
99 }
100 merged.lo *= modifier.lo;
101 merged.hi *= modifier.hi;
102 return nullptr;
103}
104
105char const *combine(HistoSys &merged, HistoSys const &modifier, std::vector<double> const &nominal)
106{
107 if (char const *reason = differences(merged, modifier)) {
108 return reason;
109 }
110 if (merged.interpolation != additivePolynomialLinear) {
111 return "this interpolation cannot currently be combined for duplicate histosys modifiers";
112 }
113 const std::size_t nBins = nominal.size();
114 auto fits = [&](HistoSysVariation const &variation) { return variation.contents.size() == nBins; };
115 if (!fits(merged.lo) || !fits(merged.hi) || !fits(modifier.lo) || !fits(modifier.hi)) {
116 return "histogram binning differs";
117 }
118 for (std::size_t bin = 0; bin < nBins; ++bin) {
119 merged.lo.contents[bin] += modifier.lo.contents[bin] - nominal[bin];
120 merged.hi.contents[bin] += modifier.hi.contents[bin] - nominal[bin];
121 }
122 return nullptr;
123}
124
125} // namespace
126
127void mergeDuplicateModifiers(HistFactoryDist &dist, std::string const &channel, Diagnostics &diagnostics)
128{
129 for (Sample &sample : dist.samples) {
130 auto error = [&](Modifier const &modifier, std::string const &reason) {
131 diagnostics.push_back({Diagnostic::Severity::Error, "",
132 "cannot combine duplicate modifier '" + nameOf(modifier) + "' of type '" +
133 typeOf(modifier) + "' in sample '" + sample.name + "' of channel '" + channel +
134 "': " + reason});
135 };
136
137 // The duplicates of a type are next to each other, because the modifiers of a type are sorted by name.
138 std::vector<Modifier> merged;
139 std::size_t count = 0; // of the modifiers that the last merged one combines
140 bool failed = false;
141 auto warnIfCombined = [&]() {
142 if (count > 1 && !failed) {
143 diagnostics.push_back({Diagnostic::Severity::Warning, "",
144 "combined " + std::to_string(count) + " duplicate modifiers named '" +
145 nameOf(merged.back()) + "' of type '" + typeOf(merged.back()) + "' in sample '" +
146 sample.name + "' of channel '" + channel + "'"});
147 }
148 };
149 for (Modifier &modifier : sample.modifiers) {
150 Modifier *last = merged.empty() ? nullptr : &merged.back();
151 const bool combinable =
152 std::holds_alternative<NormSys>(modifier) || std::holds_alternative<HistoSys>(modifier);
153 if (combinable && last && last->index() == modifier.index() && nameOf(*last) == nameOf(modifier)) {
154 char const *reason = nullptr;
155 if (auto *normSys = std::get_if<NormSys>(last)) {
156 reason = combine(*normSys, std::get<NormSys>(modifier), sample.contents);
157 } else {
158 reason = combine(std::get<HistoSys>(*last), std::get<HistoSys>(modifier), sample.contents);
159 }
160 if (reason) {
161 error(modifier, reason);
162 failed = true;
163 }
164 ++count;
165 continue;
166 }
167 warnIfCombined();
168 merged.push_back(std::move(modifier));
169 count = 1;
170 failed = false;
171 }
172 warnIfCombined();
173 sample.modifiers = std::move(merged);
174
175 std::set<std::pair<std::string, std::string>> seen;
176 for (Modifier const &modifier : sample.modifiers) {
177 if (!seen.emplace(typeOf(modifier), nameOf(modifier)).second) {
178 error(modifier, "this modifier type cannot be combined without changing its meaning");
179 }
180 }
181 }
182}
183
185{
186 auto forEachInterpolation = [&](auto &&fn) {
187 for (Sample &sample : dist.samples) {
188 for (Modifier &modifier : sample.modifiers) {
189 if (auto *normSys = std::get_if<NormSys>(&modifier)) {
190 fn(normSys->interpolation);
191 } else if (auto *histoSys = std::get_if<HistoSys>(&modifier)) {
192 fn(histoSys->interpolation);
193 }
194 }
195 }
196 };
197
198 std::map<Interpolation, std::size_t> counts;
199 forEachInterpolation([&](std::optional<Interpolation> &interpolation) {
200 if (interpolation) {
201 ++counts[*interpolation];
202 }
203 });
204 if (counts.empty()) {
205 return;
206 }
207 // A tie is decided in favor of the default of HistFactory.
208 auto best = counts.begin();
209 for (auto current = std::next(counts.begin()); current != counts.end(); ++current) {
210 if (current->second > best->second ||
211 (current->second == best->second && current->first == multiplicativePolynomialExponential &&
212 best->first != multiplicativePolynomialExponential)) {
213 best = current;
214 }
215 }
216 dist.defaultInterpolation = best->first;
217 forEachInterpolation([&](std::optional<Interpolation> &interpolation) {
218 if (interpolation == dist.defaultInterpolation) {
219 interpolation.reset();
220 }
221 });
222}
223
224} // namespace HS3
225} // namespace RooFit
#define d(i)
Definition RSha256.hxx:102
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 modifier
char name[80]
Definition TGX11.cxx:142
DomainAxis const * find(std::string const &name) const
The axis that defines the range of a variable, or null.
DomainRanges(std::vector< Domain > const &domains)
std::map< std::string, DomainAxis const * > _ranges
std::set< std::string > _names
Types that represent an HS3 document.
std::variant< NormFactor, NormSys, HistoSys, ShapeSys, StatError, CustomModifier > Modifier
constexpr char const * defaultDomainName
The domain in which RooFit writes the ranges of all variables. It is a convention of RooFit,...
void mergeDuplicateModifiers(HistFactoryDist &dist, std::string const &channel, Diagnostics &diagnostics)
Combines the modifiers of a sample that have the same type and name, which HistFactory models can hav...
void factorOutDefaultInterpolation(HistFactoryDist &dist)
Makes the most common interpolation of the modifiers the "default_interpolation", which they then don...
std::vector< Diagnostic > Diagnostics
overloaded(Ts...) -> overloaded< Ts... >
The namespace RooFit contains mostly switches that change the behaviour of functions of PDFs (or othe...
Definition CodegenImpl.h:73
The axis of a domain.
Definition HS3Document.h:98
TMarker m
Definition textangle.C:8
TLine l
Definition textangle.C:4