|
| 1 | +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. |
| 2 | +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. |
| 3 | +// All rights not expressly granted are reserved. |
| 4 | +// |
| 5 | +// This software is distributed under the terms of the GNU General Public |
| 6 | +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". |
| 7 | +// |
| 8 | +// In applying this license CERN does not waive the privileges and immunities |
| 9 | +// granted to it by virtue of its status as an Intergovernmental Organization |
| 10 | +// or submit itself to any jurisdiction. |
| 11 | + |
| 12 | +/// \file particleCompositionCorrection.cxx |
| 13 | +/// \brief Task to generate a table of dNdEta, pt and PID dependent weigths for MC particles to reflect the measured particle abundances |
| 14 | +/// \author Mario Krüger <mario.kruger@cern.ch> |
| 15 | + |
| 16 | +#include <PWGLF/DataModel/particleCompositionCorrectionTable.h> |
| 17 | + |
| 18 | +#include "Tools/ML/model.h" |
| 19 | + |
| 20 | +#include <Framework/AnalysisTask.h> |
| 21 | +#include <Framework/HistogramRegistry.h> |
| 22 | +#include <Framework/O2DatabasePDGPlugin.h> |
| 23 | +#include <Framework/runDataProcessing.h> |
| 24 | + |
| 25 | +#include <TMCProcess.h> |
| 26 | +#include <TPDGCode.h> |
| 27 | + |
| 28 | +#include <map> |
| 29 | +#include <string> |
| 30 | +#include <vector> |
| 31 | + |
| 32 | +using namespace o2; |
| 33 | +using namespace o2::framework; |
| 34 | +using namespace o2::ml; |
| 35 | + |
| 36 | +struct ParticleCompositionCorrection { |
| 37 | + // produces table of (dNdEta, pt, PID)-dependent weights for adjusting the particle composition in MC to better match the measured particle chemistry |
| 38 | + // applying these weights in the analysis allows mitigating on event-by-event basis the corresponding biases in efficiency and contamination of unidentified particles |
| 39 | + // weights are determined using measured pi,K,p,labda(as basis for sigma) spectra togehter with their counterpart from pythia simulations |
| 40 | + // they are interpolated in dNdEta and pt dimension using DNNs, which then provide particle fractions in data and MC and are stored as .onnx in the CCDB |
| 41 | + // weigths are assigned to the primary generated particle as well as its daughter particles (both from decay and material interactions) |
| 42 | + // weights are calculated only for particles within the configured kineamatic range and only for a distinct set of mother particles (see code) |
| 43 | + // assumes neutral particles require the same scaling as their charged counterparts (e.g. pi0 is scaled the same as pi+) |
| 44 | + // multi-strange baryons are assigned scaling factors of the sigma, which should be better than no scaling at all |
| 45 | + |
| 46 | + /* |
| 47 | + backlog: |
| 48 | + - store final neural networks in ccdb and implement accessing them |
| 49 | + - support collision systems beyond pp |
| 50 | + - add QA task illustrating improved mc/data matching of DCA distributions after scaling of secondaries |
| 51 | + - extend PCC weight table by columns with systematic variations (up/down) |
| 52 | + - investigate why particles can be their own mothers |
| 53 | + - investigate why histogram registry writing does not respect subfolder structure |
| 54 | + */ |
| 55 | + |
| 56 | + Service<o2::framework::O2DatabasePDG> pdg; |
| 57 | + Configurable<float> etaCut{"etaCut", 0.8f, "eta cut"}; |
| 58 | + Configurable<float> ptMinCut{"ptMinCut", 0.15f, "pt min cut"}; |
| 59 | + Configurable<float> ptMaxCut{"ptMaxCut", 10.f, "pt max cut"}; |
| 60 | + Configurable<bool> skipSec{"skipSec", false, "dont calculate weights for secondaries"}; |
| 61 | + Configurable<bool> enableQAHistos{"enableQAHistos", true, "enable qa histograms showing the effect of the PCC"}; |
| 62 | + |
| 63 | + Configurable<std::string> modelNameData{"modelNameData", "ParticleFractions_pp_data.onnx", "Path to the .onnx file containing the particle fractions in data"}; |
| 64 | + Configurable<std::string> modelNameMC{"modelNameMC", "ParticleFractions_pp_pythia.onnx", "Path to the .onnx file containing the particle fractions in MC"}; |
| 65 | + |
| 66 | + OnnxModel particleFractionsData; |
| 67 | + OnnxModel particleFractionsMC; |
| 68 | + |
| 69 | + Produces<o2::aod::ParticleCompositionCorrection> pccTable; |
| 70 | + void init(InitContext const& cfgc); |
| 71 | + void process(aod::McCollisions::iterator const& mcCollision, aod::McParticles const& particles); |
| 72 | + |
| 73 | + HistogramRegistry histos; |
| 74 | +}; |
| 75 | + |
| 76 | +WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) |
| 77 | +{ |
| 78 | + return WorkflowSpec{adaptAnalysisTask<ParticleCompositionCorrection>(cfgc)}; |
| 79 | +} |
| 80 | + |
| 81 | +void ParticleCompositionCorrection::init(InitContext const&) |
| 82 | +{ |
| 83 | + particleFractionsData.initModel(modelNameData.value, true); |
| 84 | + particleFractionsMC.initModel(modelNameMC.value, true); |
| 85 | + |
| 86 | + if (enableQAHistos) { |
| 87 | + std::vector<double> ptBinEdges = {0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, |
| 88 | + 0.8, 0.85, 0.9, 0.95, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, |
| 89 | + 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.5, 5.0, 5.5, |
| 90 | + 6.0, 6.5, 7.0, 8.0, 9.0, 10.0}; |
| 91 | + const AxisSpec ptAxis{ptBinEdges, "#it{p}_{T} (GeV/#it{c})", "pt"}; |
| 92 | + |
| 93 | + histos.add("frac/data/pion", "", kTProfile, {ptAxis}); |
| 94 | + histos.add("frac/data/kaon", "", kTProfile, {ptAxis}); |
| 95 | + histos.add("frac/data/proton", "", kTProfile, {ptAxis}); |
| 96 | + histos.add("frac/data/sigma", "", kTProfile, {ptAxis}); |
| 97 | + histos.addClone("frac/data/", "frac/mc/"); // FIXME: bug in writer/registry moves one histogram out of subfolder... |
| 98 | + |
| 99 | + histos.add("weight/pion", "", kTProfile, {ptAxis}); |
| 100 | + histos.add("weight/kaon", "", kTProfile, {ptAxis}); |
| 101 | + histos.add("weight/proton", "", kTProfile, {ptAxis}); |
| 102 | + histos.add("weight/sigma", "", kTProfile, {ptAxis}); |
| 103 | + |
| 104 | + histos.add("weight/secDec", "", kTProfile, {ptAxis}); |
| 105 | + histos.add("weight/secMat", "", kTProfile, {ptAxis}); |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +void ParticleCompositionCorrection::process(aod::McCollisions::iterator const&, aod::McParticles const& particles) |
| 110 | +{ |
| 111 | + // determine dNdEta of the collision |
| 112 | + float dNdEta = 0.f; |
| 113 | + for (const auto& particle : particles) { |
| 114 | + if (!particle.isPhysicalPrimary()) { |
| 115 | + continue; |
| 116 | + } |
| 117 | + auto pdgParticle = pdg->GetParticle(particle.pdgCode()); |
| 118 | + if (!pdgParticle || pdgParticle->Charge() == 0.) { |
| 119 | + continue; |
| 120 | + } |
| 121 | + if (std::abs(particle.eta()) >= 0.5) { // o2-linter: disable=magic-number (particle density at mid-rapidity) |
| 122 | + continue; |
| 123 | + } |
| 124 | + ++dNdEta; |
| 125 | + } |
| 126 | + |
| 127 | + // fill particle composition table with corresponding weights |
| 128 | + for (const auto& particle : particles) { |
| 129 | + float weight = 1.f; |
| 130 | + |
| 131 | + // calculate weights only inside the configured kinematic range |
| 132 | + if (std::abs(particle.eta()) < etaCut && particle.pt() > ptMinCut && particle.pt() < ptMaxCut) { |
| 133 | + auto refParticleID = particle.index(); |
| 134 | + |
| 135 | + // find initial particle of secondaries from decays or interactions with material |
| 136 | + while (!particles.iteratorAt(refParticleID).producedByGenerator() && particles.iteratorAt(refParticleID).has_mothers()) { |
| 137 | + if (skipSec) { |
| 138 | + break; |
| 139 | + } |
| 140 | + const auto& mother = particle.mothers_first_as<aod::McParticles>(); |
| 141 | + auto motherID = mother.globalIndex() - particles.offset(); |
| 142 | + if (motherID == static_cast<uint64_t>(refParticleID)) { |
| 143 | + // FIXME: this needs to be investigated! |
| 144 | + // LOGP(error, " - !!! particle is its own mother: {} (pdg: {}) status: {} ; process: {}", motherID, mother.pdgCode(), mother.getGenStatusCode(), mother.getProcess()); |
| 145 | + break; |
| 146 | + } |
| 147 | + refParticleID = motherID; |
| 148 | + } |
| 149 | + |
| 150 | + if (const auto& refParticle = particles.iteratorAt(refParticleID); refParticle.producedByGenerator()) { |
| 151 | + |
| 152 | + auto absPDGCode = std::abs(refParticle.pdgCode()); |
| 153 | + // translate abs PDG code to PID variable of neural networks (0: pion, 1: kaon, 2: proton, 3: sigma) |
| 154 | + static const std::map<int, float> mapPID = { |
| 155 | + {PDG_t::kPiPlus, 0.f}, |
| 156 | + {PDG_t::kPi0, 0.f}, |
| 157 | + {PDG_t::kKPlus, 1.f}, |
| 158 | + {PDG_t::kK0Short, 1.f}, |
| 159 | + {PDG_t::kK0Long, 1.f}, |
| 160 | + {PDG_t::kProton, 2.f}, |
| 161 | + {PDG_t::kNeutron, 2.f}, |
| 162 | + {PDG_t::kSigmaPlus, 3.f}, |
| 163 | + {PDG_t::kSigmaMinus, 3.f}, |
| 164 | + {PDG_t::kLambda0, 3.f}, |
| 165 | + {PDG_t::kSigma0, 3.f}, |
| 166 | + {PDG_t::kXiMinus, 3.f}, |
| 167 | + // TODO: potentially extend by xi0/eta/omega/rho/phi/Delta... |
| 168 | + }; |
| 169 | + |
| 170 | + if (auto iterMapPID = mapPID.find(absPDGCode); iterMapPID != mapPID.end()) { |
| 171 | + // LOGP(info, "scaling a {} with status code {} from process {}", particle.pdgCode(), particle.getGenStatusCode(), particle.getProcess()); |
| 172 | + float pt = refParticle.pt(); |
| 173 | + |
| 174 | + // calculate particle fractions and corresponding weight for given reference particle |
| 175 | + std::vector<std::vector<float>> input = {{dNdEta}, {pt}, {iterMapPID->second}}; |
| 176 | + float fracData = particleFractionsData.evalModel(input)[0]; |
| 177 | + float fracMC = particleFractionsMC.evalModel(input)[0]; |
| 178 | + if (fracMC) { |
| 179 | + weight = fracData / fracMC; |
| 180 | + } |
| 181 | + |
| 182 | + if (enableQAHistos && std::abs(particle.eta()) < 0.8) { // o2-linter: disable=magic-number (usual range of charged-partilce measurements) |
| 183 | + if (particle.isPhysicalPrimary()) { |
| 184 | + if (iterMapPID->first == PDG_t::kPiPlus) { |
| 185 | + histos.fill(HIST("frac/data/pion"), pt, fracData); |
| 186 | + histos.fill(HIST("frac/mc/pion"), pt, fracMC); |
| 187 | + histos.fill(HIST("weight/pion"), pt, weight); |
| 188 | + } |
| 189 | + if (iterMapPID->first == PDG_t::kKPlus) { |
| 190 | + histos.fill(HIST("frac/data/kaon"), pt, fracData); |
| 191 | + histos.fill(HIST("frac/mc/kaon"), pt, fracMC); |
| 192 | + histos.fill(HIST("weight/kaon"), pt, weight); |
| 193 | + } |
| 194 | + if (iterMapPID->first == PDG_t::kProton) { |
| 195 | + histos.fill(HIST("frac/data/proton"), pt, fracData); |
| 196 | + histos.fill(HIST("frac/mc/proton"), pt, fracMC); |
| 197 | + histos.fill(HIST("weight/proton"), pt, weight); |
| 198 | + } |
| 199 | + if (iterMapPID->first == PDG_t::kSigmaPlus || iterMapPID->first == PDG_t::kSigmaMinus) { |
| 200 | + histos.fill(HIST("frac/data/sigma"), pt, fracData); |
| 201 | + histos.fill(HIST("frac/mc/sigma"), pt, fracMC); |
| 202 | + histos.fill(HIST("weight/sigma"), pt, weight); |
| 203 | + } |
| 204 | + } else { |
| 205 | + auto pdgParticle = pdg->GetParticle(particle.pdgCode()); |
| 206 | + if (pdgParticle && pdgParticle->Charge() != 0.) { |
| 207 | + // fill the decay daughter info only (usually there is max. one parent generation so no relevant double counting here) |
| 208 | + if (particle.getProcess() == TMCProcess::kPDecay) { |
| 209 | + histos.fill(HIST("weight/secDec"), pt, weight); |
| 210 | + } else if (particle.getProcess() == TMCProcess::kPHInhelastic) { |
| 211 | + histos.fill(HIST("weight/secMat"), pt, weight); |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + pccTable(weight, weight, weight); // TODO: put here systematic variations of the weights |
| 220 | + } |
| 221 | +} |
0 commit comments