Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
HS3Upgrade.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// Everything that older versions of RooFit wrote in a way that is not part of the HS3 standard is translated here into
14// the form that the parser understands. The rewrites go by the shape of the JSON: the same version number was written
15// by ROOT releases with different conventions. Only what a missing component means depends on the version.
16
17#include "HS3Schema.h"
18
19#include <functional>
20#include <set>
21#include <string>
22
24
25namespace RooFit {
26namespace HS3 {
27namespace {
28
29// The version that the standard requires the interpolation of modifiers from on.
30constexpr Version explicitInterpolationVersion{0, 3, 0};
31// Before this version, normsys and histosys modifiers without constraint had an implicit Gaussian constraint.
32constexpr Version explicitConstraintVersion{0, 2, 0};
33// From this version on, the gamma parameters of modifiers are free unless "constraints" names their constraint terms,
34// "shapesys" is deprecated, and a sample without "errors" has no statistical uncertainty.
35constexpr Version namedConstraintVersion{0, 3, 0};
36
37using IsDistribution = std::function<bool(std::string const &)>;
38
39bool isScalar(JSONNode const &node)
40{
41 return !node.is_container() && !node.is_null();
42}
43
44bool isConstraintType(std::string const &value)
45{
46 return value == "Gauss" || value == "Poisson" || value == "LogNormal";
47}
48
49// Older versions of RooFit spelled "LogNormal" differently.
50std::string canonicalConstraintType(std::string const &value)
51{
52 return value == "Lognormal" ? "LogNormal" : value;
53}
54
55// The "constraints" of the standard take precedence over the keys that older documents used.
56bool eraseLegacyConstraints(JSONNode &mod)
57{
58 if (!mod.has_child("constraints")) {
59 return false;
60 }
61 mod.erase("constraint");
62 mod.erase("constraint_name");
63 mod.erase("constraint_type");
64 return true;
65}
66
67// Write a legacy constraint of a normsys, histosys or normfactor modifier as "constraint" if it is a type, or
68// "constraint_name" if it refers to a distribution.
69void upgradeConstraint(JSONNode &mod, IsDistribution const &isDistribution)
70{
71 if (eraseLegacyConstraints(mod)) {
72 return;
73 }
74 if (mod.has_child("constraint_name")) {
75 mod.erase("constraint");
76 mod.erase("constraint_type");
77 return;
78 }
79 if (mod.has_child("constraint")) {
80 mod.erase("constraint_type");
81 JSONNode &constraint = mod["constraint"];
82 if (!isScalar(constraint)) {
83 return; // reported by the parser
84 }
85 const std::string value = constraint.val();
86 // In older documents, the "constraint" can be the name of a distribution, which has priority.
87 if (isDistribution(value)) {
88 mod["constraint_name"] << value;
89 mod.erase("constraint");
90 } else {
91 constraint << canonicalConstraintType(value);
92 }
93 return;
94 }
95 if (mod.has_child("constraint_type")) {
96 JSONNode &constraintType = mod["constraint_type"];
97 if (isScalar(constraintType)) {
98 mod["constraint"] << canonicalConstraintType(constraintType.val());
99 }
100 mod.erase("constraint_type");
101 }
102}
103
104// The constraint of the gamma parameters of a shapesys modifier. In contrast to the other modifiers, it is not possible
105// to name a distribution in this way, and "constraint_type" has priority.
106void upgradeGammaConstraint(JSONNode &mod)
107{
108 if (eraseLegacyConstraints(mod)) {
109 return;
110 }
111 if (mod.has_child("constraint_type")) {
112 JSONNode &constraintType = mod["constraint_type"];
113 if (isScalar(constraintType)) {
114 mod["constraint"] << canonicalConstraintType(constraintType.val());
115 }
116 mod.erase("constraint_type");
117 } else if (mod.has_child("constraint") && isScalar(mod["constraint"])) {
118 mod["constraint"] << canonicalConstraintType(mod["constraint"].val());
119 }
120}
121
122// An integer code that was used before the structured interpolations.
123void upgradeInterpolationCode(JSONNode &mod, InterpolationClass interpolationClass, Cursor const &c)
124{
125 if (!mod.has_child("interpolation") || mod["interpolation"].is_map()) {
126 return;
127 }
128 JSONNode &node = mod["interpolation"];
129 if (auto interpolation = readInterpolationCode(Cursor{node, c, "interpolation"}, interpolationClass)) {
130 node.set_null();
131 writeInterpolation(node, *interpolation);
132 }
133}
134
135struct ChannelState {
136 std::string channelName;
139 bool warnedShapeSys = false;
140};
141
142// Before structured interpolations were introduced, both modifier classes used the integer code 4 as their implicit
143// default. The meaning of the code is class-dependent. HS3 forbids implementation-specific defaults.
144void upgradeMissingInterpolation(JSONNode &mod, InterpolationClass interpolationClass, Version const &version,
145 bool hasDefault, std::string const &context, ChannelState &state, Cursor const &c)
146{
147 if (mod.has_child("interpolation") || hasDefault || !(version < explicitInterpolationVersion)) {
148 return;
149 }
150 if (!state.warnedInterpolation) {
151 c.warning(context + " is not given, and the channel has no \"default_interpolation\". Using the implicit default "
152 "of older RooFit versions (code 4), which is deprecated.");
153 state.warnedInterpolation = true;
154 }
155 mod["interpolation"] << 4;
156 upgradeInterpolationCode(mod, interpolationClass, c);
157}
158
159// Whether "constraints" leaves the single parameter of a modifier unconstrained.
160bool isExplicitlyUnconstrained(JSONNode const &constraints)
161{
162 return constraints.is_seq() && constraints.num_children() == 1 && constraints.child(0).is_null();
163}
164
165void upgradeModifier(JSONNode &mod, std::size_t index, std::string const &sampleName, Version const &version,
166 bool hasDefaultInterpolation, IsDistribution const &isDistribution, ChannelState &state,
167 Cursor const &c)
168{
169 if (!mod.has_child("type") || !isScalar(mod["type"])) {
170 return; // reported by the parser
171 }
172 const std::string type = mod["type"].val();
173 const bool hasSingleParameter = type == "normfactor" || type == "normsys" || type == "histosys";
174
175 // The "parameters" of the standard take precedence over the "parameter" of older documents.
176 std::string parameter;
177 if (hasSingleParameter && mod.has_child("parameters")) {
178 mod.erase("parameter");
179 JSONNode const &parameters = mod["parameters"];
180 if (parameters.is_seq() && parameters.num_children() == 1 && isScalar(parameters.child(0))) {
181 parameter = parameters.child(0).val();
182 }
183 } else if (mod.has_child("parameter") && isScalar(mod["parameter"])) {
184 parameter = mod["parameter"].val();
185 }
186
187 // The name of a modifier is not part of the standard. RooFit names objects after it, so a generated name must be
188 // unique, unlike the name of a parameter, which is shared on purpose.
189 if (!mod.has_child("name")) {
190 mod["name"] << (!parameter.empty() ? parameter
191 : state.channelName + "_" + sampleName + "_syst_" + std::to_string(index));
192 }
193 const std::string name = isScalar(mod["name"]) ? mod["name"].val() : "";
194
195 if (hasSingleParameter && !mod.has_child("parameter") && !mod.has_child("parameters")) {
196 parameter = type == "normfactor" ? name : "alpha_" + name;
197 mod["parameter"] << parameter;
198 }
199
200 if (type == "normfactor") {
201 upgradeConstraint(mod, isDistribution);
202 } else if (type == "normsys" || type == "histosys") {
203 upgradeConstraint(mod, isDistribution);
204 const bool constrained = mod.has_child("constraint") || mod.has_child("constraint_name") ||
205 (mod.has_child("constraints") && !isExplicitlyUnconstrained(mod["constraints"]));
206 if (!constrained) {
207 if (version < explicitConstraintVersion) {
208 mod.erase("constraints");
209 mod["constraint"] << "Gauss";
210 } else if (!state.warnedUnconstrained) {
211 c.warning("modifier '" + name + "' of sample '" + sampleName + "' has no constraint, so its parameter '" +
212 parameter +
213 "' is unconstrained. Older RooFit versions implied a unit Gaussian. Other modifiers of this "
214 "channel are not reported.");
215 state.warnedUnconstrained = true;
216 }
217 }
218 const auto interpolationClass = type == "normsys" ? InterpolationClass::Flexible : InterpolationClass::Piecewise;
219 const std::string context = "interpolation for " + type + " modifier '" + name + "' in sample '" + sampleName +
220 "' of channel '" + state.channelName + "'";
221 upgradeInterpolationCode(mod, interpolationClass, c);
222 upgradeMissingInterpolation(mod, interpolationClass, version, hasDefaultInterpolation, context, state, c);
223 } else if (type == "shapesys" || type == "shapefactor") {
224 if (type == "shapesys" && !(version < namedConstraintVersion) && !state.warnedShapeSys) {
225 c.warning("the modifier type \"shapesys\" is deprecated, please use \"shapefactor\" instead.");
226 state.warnedShapeSys = true;
227 }
228 upgradeGammaConstraint(mod);
229 if (!mod.has_child("constraint") && !mod.has_child("constraints")) {
230 if (version < namedConstraintVersion) {
231 // Without any information about the constraint, older versions created Gaussian constraints.
232 mod["constraint"] << "Gauss";
233 } else if (mod.has_child("data") && mod["data"].is_map() && mod["data"].has_child("vals")) {
234 if (mod["data"]["vals"].is_seq() && mod["data"]["vals"].num_children() > 0) {
235 c.warning("the \"data.vals\" of modifier '" + name + "' of sample '" + sampleName +
236 "' are ignored, because it names no constraints, so its parameters are free");
237 }
238 mod["data"].erase("vals");
239 }
240 }
241 } else if (type == "staterror") {
242 if (eraseLegacyConstraints(mod)) {
243 return;
244 }
245 // ROOT 6.38 and 6.40.00 wrote "constraint_type".
246 if (mod.has_child("constraint_type")) {
247 if (!mod.has_child("constraint") && isScalar(mod["constraint_type"])) {
248 mod["constraint"] << mod["constraint_type"].val();
249 }
250 mod.erase("constraint_type");
251 }
252 }
253}
254
255void upgradeHistFactory(JSONNode &dist, Version const &version, IsDistribution const &isDistribution, Cursor const &c)
256{
257 ChannelState state;
258 if (dist.has_child("name") && isScalar(dist["name"])) {
259 state.channelName = dist["name"].val();
260 if (state.channelName.rfind("model_", 0) == 0) {
261 state.channelName.erase(0, 6);
262 }
263 }
264
265 // Before HS3 0.3, the gamma parameters of the staterror modifiers had Poisson constraints unless the document said
266 // otherwise, which RooFit also creates if the document has the channel-level settings of RooFit. Samples without
267 // "errors" had Poisson uncertainties.
268 const bool legacyStatError = version < namedConstraintVersion;
269 if (legacyStatError && !dist.has_child("staterror")) {
270 dist["staterror"].set_map();
271 }
272 if (dist.has_child("staterror") && dist["staterror"].is_map()) {
273 JSONNode &config = dist["staterror"];
274 upgradeGammaConstraint(config);
275 if (!config.has_child("constraint") && !config.has_child("constraints")) {
276 config["constraint"] << "Poisson";
277 }
278 if (legacyStatError) {
279 config["implied_poisson_errors"] << true;
280 }
281 }
282
283 if (!dist.has_child("samples") || !dist["samples"].is_seq()) {
284 return;
285 }
286 const bool hasDefaultInterpolation = dist.has_child("default_interpolation");
287
288 Cursor samples{dist["samples"], c, "samples"};
289 std::size_t iSample = 0;
290 for (JSONNode &sample : dist["samples"].children()) {
291 Cursor sampleCursor{sample, samples, iSample++};
292 if (!sample.is_map() || !sample.has_child("modifiers") || !sample["modifiers"].is_seq()) {
293 continue;
294 }
295 const std::string sampleName = sample.has_child("name") && isScalar(sample["name"]) ? sample["name"].val() : "";
296 Cursor modifiers{sample["modifiers"], sampleCursor, "modifiers"};
297 std::size_t iModifier = 0;
298 for (JSONNode &mod : sample["modifiers"].children()) {
299 Cursor modCursor{mod, modifiers, iModifier};
300 if (mod.is_map()) {
301 upgradeModifier(mod, iModifier, sampleName, version, hasDefaultInterpolation, isDistribution, state,
302 modCursor);
303 }
304 ++iModifier;
305 }
306 }
307}
308
309// Older versions of RooFit wrote the binning of the observables also for unbinned data, which the standard forbids.
310// The edges were written without "min" and "max".
311void upgradeUnbinnedAxes(JSONNode &data)
312{
313 if (!data.has_child("axes") || !data["axes"].is_seq()) {
314 return;
315 }
316 for (JSONNode &axis : data["axes"].children()) {
317 if (!axis.is_map()) {
318 continue;
319 }
320 if (axis.has_child("edges") && axis["edges"].is_seq() && axis["edges"].num_children() > 0) {
321 JSONNode &edges = axis["edges"];
322 JSONNode const &first = edges.child(0);
323 JSONNode const &last = edges.child(edges.num_children() - 1);
324 if (!axis.has_child("min") && first.is_number()) {
325 axis["min"] << first.val_double();
326 }
327 if (!axis.has_child("max") && last.is_number()) {
328 axis["max"] << last.val_double();
329 }
330 }
331 axis.erase("edges");
332 axis.erase("nbins");
333 }
334}
335
336void upgradeData(JSONNode &root, Version const &version)
337{
338 if (!root.has_child("data") || !root["data"].is_seq() || !(version < explicitInterpolationVersion)) {
339 return;
340 }
341 for (JSONNode &data : root["data"].children()) {
342 if (data.is_map() && data.has_child("type") && isScalar(data["type"]) && data["type"].val() == "unbinned") {
343 upgradeUnbinnedAxes(data);
344 }
345 }
346}
347
348Version documentVersion(JSONNode const &root)
349{
350 Version version;
351 if (JSONNode const *versionNode = root.find("metadata", "hs3_version")) {
352 if (isScalar(*versionNode)) {
353 parseVersion(versionNode->val(), version);
354 }
355 }
356 return version;
357}
358
359} // namespace
360
361void upgradeDocument(JSONNode &root, Diagnostics &diagnostics, ParseOptions const &options)
362{
363 if (!root.is_map()) {
364 return;
365 }
366 const Version version = documentVersion(root);
367 upgradeData(root, version);
368
369 if (!root.has_child("distributions") || !root["distributions"].is_seq() || !options.typedHistFactory) {
370 return;
371 }
372
373 std::set<std::string> distributionNames;
374 for (JSONNode const &dist : root["distributions"].children()) {
375 if (dist.is_map() && dist.has_child("name") && isScalar(dist["name"])) {
376 distributionNames.insert(dist["name"].val());
377 }
378 }
379 // The version from which on "constraint" is only a type of the standard.
380 const bool constraintCanBeName = version < explicitInterpolationVersion;
381 IsDistribution isDistribution = [&](std::string const &name) {
382 return constraintCanBeName && distributionNames.count(name) > 0;
383 };
384
385 Cursor rootCursor{root, diagnostics};
386 Cursor distributions{root["distributions"], rootCursor, "distributions"};
387 std::size_t i = 0;
388 for (JSONNode &dist : root["distributions"].children()) {
389 Cursor distCursor{dist, distributions, i++};
390 if (dist.is_map() && dist.has_child("type") && isScalar(dist["type"]) &&
391 dist["type"].val() == "histfactory_dist") {
392 upgradeHistFactory(dist, version, isDistribution, distCursor);
393 }
394 }
395}
396
397void upgradeHistFactoryDist(JSONNode &distribution, Version const &version, Diagnostics &diagnostics)
398{
399 IsDistribution isDistribution = [](std::string const &name) {
400 return !isConstraintType(name) && name != "Lognormal" && name != "Const";
401 };
402 upgradeHistFactory(distribution, version, isDistribution, Cursor{distribution, diagnostics});
403}
404
405} // namespace HS3
406} // namespace RooFit
bool warnedInterpolation
bool warnedShapeSys
std::string channelName
bool warnedUnconstrained
#define c(i)
Definition RSha256.hxx:101
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 index
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 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
XID Cursor
Definition TGX11.h:36
void erase(std::string const &key)
Remove a child of a map, if it exists.
std::string val() const
JSONNode const * find(std::string const &key) const
bool has_child(std::string const &) const
JSONNode & child(size_t pos)
A position in the JSON tree.
Definition HS3Schema.h:32
double dist(Rotation3D const &r1, Rotation3D const &r2)
Types that represent an HS3 document.
void upgradeHistFactoryDist(Detail::JSONNode &distribution, Version const &version, Diagnostics &diagnostics)
Same as upgradeDocument(), but for a single HistFactory distribution that doesn't come with a documen...
std::optional< Interpolation > readInterpolationCode(Cursor const &c, InterpolationClass interpolationClass)
Reads an integer interpolation code, as older versions of RooFit wrote them.
std::vector< Diagnostic > Diagnostics
void writeInterpolation(Detail::JSONNode &node, Interpolation const &interpolation)
void upgradeDocument(Detail::JSONNode &root, Diagnostics &diagnostics, ParseOptions const &options={})
Rewrites everything that older versions of RooFit wrote in a way that is not part of the standard int...
bool parseVersion(std::string const &version, Version &out)
Reads the leading "MAJOR.MINOR" and the optional ".PATCH" of an HS3 version string.
The namespace RooFit contains mostly switches that change the behaviour of functions of PDFs (or othe...
Definition CodegenImpl.h:73
Options for upgradeDocument() and parseDocument().
bool typedHistFactory
If false, HistFactory distributions stay generic objects, because a user registered their own importe...