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