// Step 1 in ROOT: yields out of ROOT, into a plain CSV histogram. The ROOT twin of extract_yields.py: // same config, same output (/yields_.csv), same format. Use whichever is handier. // // root -l -b -q 'extract_yields.C+("examples/27Al_ap_runA.toml")' // root -l -b -q 'extract_yields.C+("examples/27Al_ap_runA.toml", "out/elsewhere")' // another output folder // // (relative to where root runs) // THE CSV FORMAT (cross_section.py reads only this) // 1D: _lo, _hi, counts, variance // 2D: _lo, _hi, theta_cm_lo, theta_cm_hi, counts, variance (angular mode) // is z (mm), e_beam (MeV) or ecm (MeV). One row per bin; under/overflow are not written. // // CONFIG: the [extract] section, `dets`, `out_dir`, `title` and [det_vars.] of the study's TOML, exactly as for // extract_yields.py (keys listed there). Two differences: // - `cut` is handed to TTree::Draw. Write it with and / or (e.g. "det == 1 and e_si > 0.5"): they become && / || // here and stay valid pandas syntax for the Python version. // - A tree histogram puts an entry exactly on the last upper edge in the overflow (numpy puts it in the last bin). // The TOML reader below handles only what these configs use: "strings", numbers, one-line [lists] and // { start = .., stop = .., step = .. } tables. Anything else stops with an error. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xs { using Section = std::map; // key -> raw value text using Config = std::map; // section name ("" = top level) -> keys // ============================================================ minimal TOML reading std::string Trim(const std::string& s) { const auto a = s.find_first_not_of(" \t\r\n"); if (a == std::string::npos) return ""; const auto b = s.find_last_not_of(" \t\r\n"); return s.substr(a, b - a + 1); } std::string StripComment(const std::string& line) { bool inString = false; for (size_t i = 0; i < line.size(); ++i) { if (line[i] == '"') inString = !inString; if (line[i] == '#' && !inString) return line.substr(0, i); } return line; } Config ReadToml(const std::string& file) { std::ifstream in(file); if (!in) throw std::runtime_error("cannot open config " + file); Config config; std::string section, line; while (std::getline(in, line)) { line = Trim(StripComment(line)); if (line.empty()) continue; if (line.rfind("[[", 0) == 0) { section = line; continue; } // array tables (e.g. [[reference]]): not used here if (line[0] == '[') { section = Trim(line.substr(1, line.size() - 2)); continue; } const auto eq = line.find('='); if (eq == std::string::npos) throw std::runtime_error("cannot read config line: " + line); const std::string value = Trim(line.substr(eq + 1)); if ((value[0] == '[' && value.back() != ']') || (value[0] == '{' && value.back() != '}')) throw std::runtime_error("lists and tables must be on one line: " + line); config[section][Trim(line.substr(0, eq))] = value; } return config; } std::string Unquote(const std::string& v) { if (v.size() >= 2 && v.front() == '"' && v.back() == '"') return v.substr(1, v.size() - 2); return v; } // "[a, b]" or "{ k = v, ... }" -> its items, split on commas outside quotes std::vector SplitItems(const std::string& v) { std::vector items; std::string inner = Trim(v.substr(1, v.size() - 2)), item; bool inString = false; for (char c : inner) { if (c == '"') inString = !inString; if (c == ',' && !inString) { items.push_back(Trim(item)); item.clear(); } else item += c; } if (!Trim(item).empty()) items.push_back(Trim(item)); return items; } // Bin edges from [e0, e1, ...] or { start = .., stop = .., step = .. } (stop included), computed as numpy does std::vector Edges(const std::string& v) { std::vector edges; if (v.front() == '[') { for (const auto& item : SplitItems(v)) edges.push_back(std::stod(item)); return edges; } std::map table; for (const auto& item : SplitItems(v)) { const auto eq = item.find('='); table[Trim(item.substr(0, eq))] = std::stod(Trim(item.substr(eq + 1))); } const double start = table.at("start"), stop = table.at("stop"), step = table.at("step"); const long nBins = std::lround((stop - start) / step); for (long i = 0; i <= nBins; ++i) edges.push_back(start + step * i); return edges; } // The [extract] section of one detector: every string with {det} and the [det_vars.] keys replaced Section ExtractSection(const Config& config, const std::string& det) { std::map replacements = {{"det", det}}; const auto vars = config.find("det_vars." + det); if (vars != config.end()) for (const auto& kv : vars->second) replacements[kv.first] = Unquote(kv.second); const auto ex = config.find("extract"); if (ex == config.end()) throw std::runtime_error("the config has no [extract] section"); Section out; for (const auto& kv : ex->second) { TString value = kv.second.c_str(); for (const auto& r : replacements) value.ReplaceAll(("{" + r.first + "}").c_str(), r.second.c_str()); out[kv.first] = value.Data(); } return out; } std::string Get(const Section& s, const std::string& key, const std::string& fallback = "\x01") { const auto it = s.find(key); if (it != s.end()) return Unquote(it->second); if (fallback == "\x01") throw std::runtime_error("[extract] needs the key " + key); return fallback; } // ============================================================ the yields table struct Row { double lo, hi, thetaLo, thetaHi, counts, variance; }; // Sum of weights squared of a bin (global index); a histogram without Sumw2 counts each entry once double Variance(const TH1* h, int bin) { return h->GetSumw2N() > 0 ? h->GetSumw2()->At(bin) : h->GetBinContent(bin); } // The histogram at "folder/name". A name that itself contains '/' (e.g. "dE_(z/100)=1") is not reachable with Get(), // so each split into an existing folder + a key of that folder is also tried. Not found: the error names the // histograms whose path matches when case is ignored (e.g. "elastic+misc_qqq_a" for "elastic+misc_QQQ_a"). TH1* FindHistogram(TFile* file, const std::string& path) { if (auto* h = dynamic_cast(file->Get(path.c_str()))) return h; // every split into folder + name: at each '/', from the last one back, and finally no folder at all std::vector> splits; for (size_t i = path.size(); i-- > 0;) if (path[i] == '/') splits.emplace_back(path.substr(0, i), path.substr(i + 1)); splits.emplace_back("", path); for (const auto& [folder, name] : splits) { TDirectory* dir = folder.empty() ? static_cast(file) : file->GetDirectory(folder.c_str()); if (!dir) continue; if (TKey* key = dir->GetKey(name.c_str())) if (auto* h = dynamic_cast(key->ReadObj())) return h; } std::string close; TString lower = path.c_str(); lower.ToLower(); std::function walk = [&](TDirectory* dir, const std::string& prefix) { for (auto* obj : *dir->GetListOfKeys()) { auto* key = static_cast(obj); const std::string full = prefix + key->GetName(); if (TString(key->GetClassName()).BeginsWith("TDirectory")) walk(dir->GetDirectory(key->GetName()), full + "/"); else if (TString(full.c_str()).EqualTo(lower, TString::kIgnoreCase)) close += " '" + full + "'"; } }; walk(file, ""); throw std::runtime_error("no histogram " + path + " in " + file->GetName() + (close.empty() ? "" : "; same path ignoring case:" + close) + ". Paths are case-sensitive; for upper-case names per detector use [det_vars], e.g. DET = \"QQQ\""); } std::vector FromHistogram(const Section& ex, const std::string& path, bool& twoD) { TFile* file = TFile::Open(path.c_str()); if (!file || file->IsZombie()) throw std::runtime_error("cannot open " + path); const std::string name = Get(ex, "hist"); TH1* h = FindHistogram(file, name); std::vector rows; twoD = false; if (h->GetDimension() == 1) { for (int i = 1; i <= h->GetNbinsX(); ++i) rows.push_back({h->GetXaxis()->GetBinLowEdge(i), h->GetXaxis()->GetBinUpEdge(i), 0, 0, h->GetBinContent(i), Variance(h, i)}); file->Close(); return rows; } // TH2: `axis` holds the variable; the other axis is theta_cm (angle_axis given) or is summed const bool varOnX = Get(ex, "axis", "x") == "x"; TH2* h2 = static_cast(h); TAxis* varAxis = varOnX ? h2->GetXaxis() : h2->GetYaxis(); TAxis* otherAxis = varOnX ? h2->GetYaxis() : h2->GetXaxis(); const int nVar = varAxis->GetNbins(), nOther = otherAxis->GetNbins(); auto global = [&](int iVar, int iOther) { return varOnX ? h2->GetBin(iVar, iOther) : h2->GetBin(iOther, iVar); }; if (ex.count("angle_axis")) { // keep it 2D: one row per (variable bin, theta bin) twoD = true; for (int i = 1; i <= nVar; ++i) for (int j = 1; j <= nOther; ++j) rows.push_back({varAxis->GetBinLowEdge(i), varAxis->GetBinUpEdge(i), otherAxis->GetBinLowEdge(j), otherAxis->GetBinUpEdge(j), h2->GetBinContent(global(i, j)), Variance(h2, global(i, j))}); file->Close(); return rows; } // project on the variable: the other axis summed whole (with under/overflow), or the fraction inside other_range std::vector weight(nOther + 2, 1.0); if (ex.count("other_range")) { const auto range = Edges(ex.at("other_range")); weight.front() = weight.back() = 0.0; for (int j = 1; j <= nOther; ++j) { const double lo = otherAxis->GetBinLowEdge(j), hi = otherAxis->GetBinUpEdge(j); weight[j] = std::max(0.0, std::min(hi, range[1]) - std::max(lo, range[0])) / (hi - lo); } } for (int i = 1; i <= nVar; ++i) { double counts = 0, variance = 0; for (int j = 0; j <= nOther + 1; ++j) { counts += weight[j] * h2->GetBinContent(global(i, j)); variance += weight[j] * weight[j] * Variance(h2, global(i, j)); } rows.push_back({varAxis->GetBinLowEdge(i), varAxis->GetBinUpEdge(i), 0, 0, counts, variance}); } file->Close(); return rows; } std::vector FromTree(const Section& ex, const std::vector& files, bool& twoD) { TChain chain(Get(ex, "tree").c_str()); for (const auto& f : files) chain.Add(f.c_str()); // TChain::Add takes wildcards in the file name if (chain.GetEntries() <= 0) // a missing tree or file must not become a table of zeros throw std::runtime_error("no entries in tree " + Get(ex, "tree") + " of " + files.front() + (files.size() > 1 ? " ..." : "")); // The cut, with and / or as && / ||; a weight branch multiplies it TString cut = Get(ex, "cut", "").c_str(); cut.ReplaceAll(" and ", " && ").ReplaceAll(" or ", " || "); if (cut.IsNull()) cut = "1"; TString selection = ex.count("weight_branch") ? TString::Format("(%s)*(%s)", Get(ex, "weight_branch").c_str(), cut.Data()) : cut; const auto edges = Edges(ex.at("bins")); const TString branch = Get(ex, "branch").c_str(); gROOT->cd(); std::vector rows; twoD = ex.count("angle_branch") > 0; if (!twoD) { TH1D h("h_extract_yields", "", edges.size() - 1, edges.data()); h.Sumw2(); const Long64_t n = chain.Draw(branch + ">>h_extract_yields", selection, "goff"); printf(" %d file(s); %lld entries after the cut\n", chain.GetListOfFiles()->GetEntries(), n); for (int i = 1; i <= h.GetNbinsX(); ++i) rows.push_back({edges[i - 1], edges[i], 0, 0, h.GetBinContent(i), Variance(&h, i)}); return rows; } const auto angleEdges = Edges(ex.at("angle_bins")); TH2D h("h_extract_yields2", "", edges.size() - 1, edges.data(), angleEdges.size() - 1, angleEdges.data()); h.Sumw2(); const Long64_t n = chain.Draw(TString(Get(ex, "angle_branch").c_str()) + ":" + branch + ">>h_extract_yields2", selection, "goff"); // "y:x" in TTree::Draw printf(" %d file(s); %lld entries after the cut\n", chain.GetListOfFiles()->GetEntries(), n); for (size_t i = 1; i < edges.size(); ++i) for (size_t j = 1; j < angleEdges.size(); ++j) rows.push_back({edges[i - 1], edges[i], angleEdges[j - 1], angleEdges[j], h.GetBinContent(h.GetBin(i, j)), Variance(&h, h.GetBin(i, j))}); return rows; } void WriteCsv(const std::vector& rows, bool twoD, const std::string& var, const std::vector& header, const std::string& outFile) { FILE* f = fopen(outFile.c_str(), "w"); if (!f) throw std::runtime_error("cannot write " + outFile); for (const auto& line : header) fprintf(f, "# %s\n", line.c_str()); fprintf(f, twoD ? "%s_lo,%s_hi,theta_cm_lo,theta_cm_hi,counts,variance\n" : "%s_lo,%s_hi,counts,variance\n", var.c_str(), var.c_str()); for (const auto& r : rows) { if (twoD) fprintf(f, "%.6g,%.6g,%.6g,%.6g,%.6g,%.6g\n", r.lo, r.hi, r.thetaLo, r.thetaHi, r.counts, r.variance); else fprintf(f, "%.6g,%.6g,%.6g,%.6g\n", r.lo, r.hi, r.counts, r.variance); } fclose(f); } } // namespace xs void extract_yields(const char* configFile, const char* outDirOverride = "") { using namespace xs; try { const Config config = ReadToml(configFile); const std::string configDir = gSystem->GetDirName(configFile).Data(); auto fullPath = [&](const std::string& p) -> std::string { if (p.rfind("~/", 0) == 0 && getenv("HOME")) return std::string(getenv("HOME")) + p.substr(1); // "~/..." = home return p.empty() || p[0] == '/' ? p : configDir + "/" + p; }; const Section& top = config.count("") ? config.at("") : Section(); std::vector dets = {""}; if (top.count("dets")) { dets.clear(); for (const auto& d : SplitItems(top.at("dets"))) dets.push_back(Unquote(d)); } const std::string outDir = *outDirOverride ? std::string(outDirOverride) : fullPath(top.count("out_dir") ? Unquote(top.at("out_dir")) : "out"); gSystem->mkdir(outDir.c_str(), kTRUE); const std::string title = top.count("title") ? Unquote(top.at("title")) : ""; for (const auto& det : dets) { const Section ex = ExtractSection(config, det); const std::string source = Get(ex, "source"), var = Get(ex, "variable"); if (source != "hist" && source != "tree") throw std::runtime_error("[extract] source must be \"hist\" or \"tree\", not " + source + (source == "csv" ? " (event-list CSVs: use extract_yields.py)" : "")); std::vector files; // `file` may be one path or a list if (ex.at("file").front() == '[') for (const auto& f : SplitItems(ex.at("file"))) files.push_back(fullPath(Unquote(f))); else files.push_back(fullPath(Get(ex, "file"))); const std::string fileText = ex.at("file").front() == '[' ? ex.at("file") : Get(ex, "file"); printf("--- %s: %s : %s\n", det.empty() ? "data" : det.c_str(), fileText.c_str(), Get(ex, source == "hist" ? "hist" : "tree").c_str()); bool twoD = false; std::vector rows; std::string from; if (source == "hist") { rows = FromHistogram(ex, files.front(), twoD); from = "histogram " + Get(ex, "hist"); } else if (source == "tree") { rows = FromTree(ex, files, twoD); from = "tree " + Get(ex, "tree") + ", branch " + Get(ex, "branch"); if (!Get(ex, "cut", "").empty()) from += ", cut: " + Get(ex, "cut"); } else { throw std::runtime_error("[extract] source must be \"hist\" or \"tree\", not " + source + (source == "csv" ? " (event-list CSVs: use extract_yields.py)" : "")); } TString detUpper = det.c_str(); detUpper.ToUpper(); const std::string outFile = outDir + "/" + (det.empty() ? "yields.csv" : "yields_" + det + ".csv"); WriteCsv(rows, twoD, var, {title + " " + detUpper.Data() + ": yields, binned in " + var, "from " + fileText + " : " + from, "written by extract_yields.C"}, outFile); double total = 0; for (const auto& r : rows) total += r.counts; printf(" %.0f counts in %zu bins -> %s\n", total, rows.size(), outFile.c_str()); } } catch (const std::exception& e) { fprintf(stderr, "extract_yields.C: %s\n", e.what()); gSystem->Exit(1); // a failed extraction must not look like a finished one } }