From e03096e36dbfab3c1c08c6de7a48a0830a1839a6 Mon Sep 17 00:00:00 2001 From: ddobrigk Date: Wed, 1 Jul 2026 14:28:29 +0200 Subject: [PATCH 001/144] Add diffraction-tuned pythia8 inel generator (#2393) * Add diffraction-tuned pythia8 inel cfg * Adjust default energy to 13.6 --- .../common/ini/pythia8_inel_difftuned.ini | 2 ++ .../common/ini/tests/pythia8_inel_difftuned.C | 24 ++++++++++++++++++ .../generator/pythia8_inel_difftuned.cfg | 25 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 MC/config/common/ini/pythia8_inel_difftuned.ini create mode 100644 MC/config/common/ini/tests/pythia8_inel_difftuned.C create mode 100644 MC/config/common/pythia8/generator/pythia8_inel_difftuned.cfg diff --git a/MC/config/common/ini/pythia8_inel_difftuned.ini b/MC/config/common/ini/pythia8_inel_difftuned.ini new file mode 100644 index 000000000..65ff9b972 --- /dev/null +++ b/MC/config/common/ini/pythia8_inel_difftuned.ini @@ -0,0 +1,2 @@ +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_inel_difftuned.cfg diff --git a/MC/config/common/ini/tests/pythia8_inel_difftuned.C b/MC/config/common/ini/tests/pythia8_inel_difftuned.C new file mode 100644 index 000000000..7ee7ea397 --- /dev/null +++ b/MC/config/common/ini/tests/pythia8_inel_difftuned.C @@ -0,0 +1,24 @@ +int pythia8() { + std::string path{"o2sim_Kine.root"}; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + auto nEvents = tree->GetEntries(); + if (nEvents == 0) { + std::cerr << "No event of interest\n"; + return 1; + } + return 0; +} diff --git a/MC/config/common/pythia8/generator/pythia8_inel_difftuned.cfg b/MC/config/common/pythia8/generator/pythia8_inel_difftuned.cfg new file mode 100644 index 000000000..22dcfbb7e --- /dev/null +++ b/MC/config/common/pythia8/generator/pythia8_inel_difftuned.cfg @@ -0,0 +1,25 @@ +### beams +Beams:idA 2212 # proton +Beams:idB 2212 # proton +Beams:eCM 13600. # GeV + +### processes +SoftQCD:inelastic on # all inelastic processes + +### decays +ParticleDecays:limitTau0 on +ParticleDecays:tau0Max 10. + +### adjust diffraction +### note that PYTHIA calculates the ND sigma via a +### 'remaining from total' formula: no need to put +### that in by hand + +SigmaTotal:mode 0 +SigmaDiffractive:mode 0 +SigmaTotal:sigmaTot 78.6 +SigmaTotal:sigmaEl 0.0 +SigmaTotal:sigmaXB 10.625 +SigmaTotal:sigmaAX 10.625 +SigmaTotal:sigmaXX 9.78 +SigmaTotal:sigmaAXB 0.0 From d70a8d124765e683efc4260b59078d07a0e2a569 Mon Sep 17 00:00:00 2001 From: Rrantu <156880782+Rrantu@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:21:47 +0200 Subject: [PATCH 002/144] Add triggerLc function call to makeStarlightConfig.py (#2398) * Add triggerLc function for Lambda_c particle selection * Refactor process argument choices in makeStarlightConfig.py * Add triggerLc function call to makeStarlightConfig.py --- MC/config/PWGUD/ini/makeStarlightConfig.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MC/config/PWGUD/ini/makeStarlightConfig.py b/MC/config/PWGUD/ini/makeStarlightConfig.py index 46dc4823c..1655624c7 100755 --- a/MC/config/PWGUD/ini/makeStarlightConfig.py +++ b/MC/config/PWGUD/ini/makeStarlightConfig.py @@ -127,6 +127,8 @@ fout.write('funcName = triggerPhi(-0.9,0.9) \n') if 'Kstar' in args.process: fout.write('funcName = triggerKstar(-0.9,0.9) \n') + if 'Lc' in args.process: + fout.write('funcName = triggerLc(-0.9,0.9) \n') ### close outout file fout.close() From f0f978d88d99738f16371a8bcccbb6602ffe36c4 Mon Sep 17 00:00:00 2001 From: Amit Kumar Pradhan <124381334+vetex007@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:05:00 +0530 Subject: [PATCH 003/144] =?UTF-8?q?PWGDQ:=20Add=20Ne=E2=80=93Ne=20prompt?= =?UTF-8?q?=20charmonia=20generator=20configuration=20for=20anchored=20MC?= =?UTF-8?q?=20(#2394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * PWGDQ: add Ne-Ne prompt charmonia generator configuration * PWGDQ: add Ne-Ne validation test * PWGDQ: reduce Ne-Ne trigger gap from 5 to 2 --- ...romptCharmoniaFwdy_TriggerGap_NeNe5TeV.ini | 7 ++ ...dPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.C | 85 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.ini create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.C diff --git a/MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.ini new file mode 100644 index 000000000..3668ce8df --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.ini @@ -0,0 +1,7 @@ +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedPromptSignals_gaptriggered_dq.C +funcName=GeneratorPythia8InjectedPromptCharmoniaGapTriggered(2,11) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_NeNe_536.cfg diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.C new file mode 100644 index 000000000..5dd26f49d --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_NeNe5TeV.C @@ -0,0 +1,85 @@ +int External() +{ + int checkPdgSignal[] = {443,100443}; + int checkPdgDecay = 13; + double rapiditymin = -4.3; double rapiditymax = -2.3; + std::string path{"o2sim_Kine.root"}; + std::cout << "Check for\nsignal PDG " << checkPdgSignal << "\ndecay PDG " << checkPdgDecay << "\n"; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree*)file.Get("o2sim"); + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + auto nEvents = tree->GetEntries(); + o2::steer::MCKinematicsReader mcreader("o2sim", o2::steer::MCKinematicsReader::Mode::kMCKine); + Bool_t isInjected = kFALSE; + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + } else if(pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + } else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) { + if(idMoth < 0) { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if(rapidity > rapiditymin && rapidity < rapiditymax) { pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++;} + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#lepton pairs: " << nLeptonPairs << "\n" + << "#lepton pairs to be done: " << nLeptonPairs << "\n"; + + + if (nLeptonPairs == 0 || nLeptons == 0 || nAntileptons == 0) { + std::cerr << "Number of leptons, number of anti-leptons as well as number of lepton pairs should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) { + std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; + return 1; + } + + return 0; +} From 5f8f8184bc2053f011fa9bb10565ec0541a98398 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 7 Jul 2026 12:40:38 +0200 Subject: [PATCH 004/144] External to Hybrid example (#2401) --- MC/config/examples/hybrid/example.json | 30 +++++++++++++++ MC/config/examples/hybrid/examplemanifest.dat | 10 +++++ .../examples/ini/GeneratorExtToHybrid.ini | 3 ++ .../examples/ini/tests/GeneratorExtToHybrid.C | 38 +++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 MC/config/examples/hybrid/example.json create mode 100755 MC/config/examples/hybrid/examplemanifest.dat create mode 100644 MC/config/examples/ini/GeneratorExtToHybrid.ini create mode 100644 MC/config/examples/ini/tests/GeneratorExtToHybrid.C diff --git a/MC/config/examples/hybrid/example.json b/MC/config/examples/hybrid/example.json new file mode 100644 index 000000000..a43766ab7 --- /dev/null +++ b/MC/config/examples/hybrid/example.json @@ -0,0 +1,30 @@ +{ + "generators": [ + { + "name": "pythia8", + "config": { + "config": "${O2_ROOT}/share/Generators/egconfig/pythia8_inel.cfg", + "hooksFileName": "", + "hooksFuncName": "", + "includePartonEvent": false, + "particleFilter": "", + "verbose": 0 + } + }, + { + "name": "evtpool", + "config": { + "eventPoolPath" : "${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/hybrid/examplemanifest.dat", + "skipNonTrackable" : true, + "roundRobin" : false, + "randomize" : true, + "rngseed" : 0, + "randomphi" : false + } + } + ], + "fractions": [ + 1, + 1 + ] +} diff --git a/MC/config/examples/hybrid/examplemanifest.dat b/MC/config/examples/hybrid/examplemanifest.dat new file mode 100755 index 000000000..5cbfda065 --- /dev/null +++ b/MC/config/examples/hybrid/examplemanifest.dat @@ -0,0 +1,10 @@ +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/001/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/002/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/003/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/004/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/005/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/006/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/007/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/008/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/009/evtpool.root +alien:///alice/sim/2025/EP25i1_GeneratorHF_D2H_bbbar_Mode2_XiC_NoDecay/1/010/evtpool.root \ No newline at end of file diff --git a/MC/config/examples/ini/GeneratorExtToHybrid.ini b/MC/config/examples/ini/GeneratorExtToHybrid.ini new file mode 100644 index 000000000..62e0986b7 --- /dev/null +++ b/MC/config/examples/ini/GeneratorExtToHybrid.ini @@ -0,0 +1,3 @@ +[GeneratorHybrid] +configFile = ${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/hybrid/example.json +switchExtToHybrid = true \ No newline at end of file diff --git a/MC/config/examples/ini/tests/GeneratorExtToHybrid.C b/MC/config/examples/ini/tests/GeneratorExtToHybrid.C new file mode 100644 index 000000000..2694d57e4 --- /dev/null +++ b/MC/config/examples/ini/tests/GeneratorExtToHybrid.C @@ -0,0 +1,38 @@ +int Hybrid() +{ + std::string path{"o2sim_Kine.root"}; + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + // Check if there are 100 events, as simulated in the o2dpg-test + if (nEvents != 100) + { + std::cerr << "Expected 100 events, got " << nEvents << "\n"; + return 1; + } + return 0; +} From c53f5cd07020a69d7dc61c8774573481d00aeea2 Mon Sep 17 00:00:00 2001 From: donglovo Date: Wed, 8 Jul 2026 00:19:45 +0800 Subject: [PATCH 005/144] Make the non-prompt injection ratio configurable and fix EvtGen decay definitions (#2400) * Set the non-prompt injection ratio to 2 Change the default injection ratio from 5 to 2 for the non-prompt gap-triggered generators, corresponding to one injected signal event every two generated events. * Allow input trigger ratio configuration via environment variable Read INPUT_TRIGGER_RATIO from the environment and use it to override the default input trigger ratio. Fall back to the constructor default when the variable is unset or invalid. * Fix EvtGen decay definitions in BTOPSIJPSITODIELECTRON.DEC Align several inconsistent decay definitions with the standard DECAY.DEC. This fixes invalid decay models, missing SVV_HELAMP arguments, and incorrect EvtGen particle names. The updated decay table was tested successfully with local event generation. --- .../BTOPSIJPSITODIELECTRON.DEC | 56 +++++++++---------- ...hia8_NonPromptSignals_OO_gaptriggered_dq.C | 26 ++++++++- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC index 2a45f28c6..4c3e5a129 100644 --- a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC +++ b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC @@ -13,7 +13,7 @@ Decay B0 0.0001 psi(2S) K+ pi- pi0 PHSP; 0.0004 psi(2S) K_10 PHSP; ### -0.000620000 psi(2S) K0 PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000620000 psi(2S) K0 SVS; #[New mode added] #[Reconstructed PDG2011] 0.000435500 J/psi K_S0 SVS; #[Reconstructed PDG2011] 0.000435500 J/psi K_L0 SVS; #[Reconstructed PDG2011] @@ -30,7 +30,7 @@ Decay B0 0.0005 J/psi K_2*0 PHSP; 0.000094000 J/psi phi K0 PHSP; #[Reconstructed PDG2011] #### -0.000871000 J/psi K0 PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000871000 J/psi K0 SVS; #[New mode added] #[Reconstructed PDG2011] 0.000310000 J/psi omega K0 PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000009500 J/psi eta PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000019000 J/psi pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] @@ -40,15 +40,15 @@ Decay B0 0.000660000 J/psi K*0 pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000011200 chi_c1 pi0 SVS; + 0.000011200 chi_c1 pi0 PHSP; 0.000395000 chi_c1 K0 SVS; 0.000497000 chi_c1 K+ pi- PHSP; 0.000320000 chi_c1 K0 pi+ pi- PHSP; 0.000350000 chi_c1 K+ pi0 pi- PHSP; # --- chi_c2 --- - 0.000015000 chi_c2 K0 SVS; - 0.000049000 chi_c2 K*0 SVV_HELAMP 1.0 0.0 0.0; + 0.000015000 chi_c2 K0 STS; + 0.000049000 chi_c2 K*0 PHSP; 0.000072000 chi_c2 K+ pi- PHSP; 0.000174000 chi_c2 pi+ pi- K0 PHSP; 0.000074000 chi_c2 pi- pi0 K+ PHSP; @@ -67,7 +67,7 @@ Decay anti-B0 0.0001 psi(2S) K- pi+ pi0 PHSP; 0.0004 psi(2S) anti-K_10 PHSP; ### -0.000620000 psi(2S) anti-K0 PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000620000 psi(2S) anti-K0 SVS; #[New mode added] #[Reconstructed PDG2011] 0.000435500 J/psi K_S0 SVS; #[Reconstructed PDG2011] 0.000435500 J/psi K_L0 SVS; #[Reconstructed PDG2011] @@ -84,7 +84,7 @@ Decay anti-B0 0.0005 J/psi anti-K_2*0 PHSP; 0.000094000 J/psi phi anti-K0 PHSP; #[Reconstructed PDG2011] ### -0.000871000 J/psi anti-K0 PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000871000 J/psi anti-K0 SVS; #[New mode added] #[Reconstructed PDG2011] 0.000310000 J/psi omega anti-K0 PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000009500 J/psi eta PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000019000 J/psi pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] @@ -94,15 +94,15 @@ Decay anti-B0 0.000660000 J/psi anti-K*0 pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000011200 chi_c1 pi0 SVS; + 0.000011200 chi_c1 pi0 PHSP; 0.000395000 chi_c1 anti-K0 SVS; 0.000497000 chi_c1 K- pi+ PHSP; 0.000320000 chi_c1 anti-K0 pi- pi+ PHSP; 0.000350000 chi_c1 K- pi0 pi+ PHSP; # --- chi_c2 --- - 0.000015000 chi_c2 anti-K0 SVS; - 0.000049000 chi_c2 anti-K*0 SVV_HELAMP 1.0 0.0 0.0; + 0.000015000 chi_c2 anti-K0 STS; + 0.000049000 chi_c2 anti-K*0 PHSP; 0.000072000 chi_c2 K- pi+ PHSP; 0.000174000 chi_c2 pi- pi+ anti-K0 PHSP; 0.000074000 chi_c2 pi+ pi0 K- PHSP; @@ -139,16 +139,16 @@ Decay B+ 0.000011800 J/psi p+ anti-Lambda0 PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000022000 chi_c1 pi+ SVS; + 0.000022000 chi_c1 pi+ PHSP; 0.000474000 chi_c1 K+ SVS; - 0.000300000 chi_c1 K*+ SVV_HELAMP 1.0 0.0 0.0; + 0.000300000 chi_c1 K*+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; 0.000329000 chi_c1 K+ pi0 PHSP; 0.000580000 chi_c1 K0 pi+ PHSP; 0.000374000 chi_c1 K+ pi+ pi- PHSP; # --- chi_c2 --- - 0.000110000 chi_c2 K+ SVS; - 0.000120000 chi_c2 K*+ SVV_HELAMP 1.0 0.0 0.0; + 0.000110000 chi_c2 K+ STS; + 0.000120000 chi_c2 K*+ PHSP; 0.000062000 chi_c2 K+ pi0 PHSP; 0.000124000 chi_c2 K0 pi+ PHSP; 0.000134000 chi_c2 K+ pi+ pi- PHSP; @@ -184,16 +184,16 @@ Decay B- 0.000011800 J/psi anti-p- Lambda0 PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000022000 chi_c1 pi- SVS; + 0.000022000 chi_c1 pi- PHSP; 0.000474000 chi_c1 K- SVS; - 0.000300000 chi_c1 K*- SVV_HELAMP 1.0 0.0 0.0; + 0.000300000 chi_c1 K*- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; 0.000329000 chi_c1 K- pi0 PHSP; 0.000580000 chi_c1 anti-K0 pi- PHSP; 0.000374000 chi_c1 K- pi- pi+ PHSP; # --- chi_c2 --- - 0.000110000 chi_c2 K- SVS; - 0.000120000 chi_c2 K*- SVV_HELAMP 1.0 0.0 0.0; + 0.000110000 chi_c2 K- STS; + 0.000120000 chi_c2 K*- PHSP; 0.000062000 chi_c2 K- pi0 PHSP; 0.000124000 chi_c2 anti-K0 pi- PHSP; 0.000134000 chi_c2 K- pi- pi+ PHSP; @@ -242,7 +242,7 @@ Decay B_s0 0.0000023 phi e+ e- BTOSLLALI; # --- chi_c1 --- - 0.0001970 chi_c1 phi SVV_HELAMP 1.0 0.0 0.0; + 0.0001970 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; Enddecay Decay anti-B_s0 @@ -287,27 +287,27 @@ Decay anti-B_s0 0.0000023 phi e- e+ BTOSLLALI; # --- chi_c1 --- - 0.0001970 chi_c1 phi SVV_HELAMP 1.0 0.0 0.0; + 0.0001970 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; Enddecay Decay Lambda_b0 ### 0.00038 Lambda0 psi(2S) PHSP; 0.00047 Lambda0 J/psi PHSP; - 0.0000760 chi_c1 p K- PHSP; - 0.0000050 chi_c1 p pi- PHSP; - 0.0000770 chi_c2 p K- PHSP; - 0.0000048 chi_c2 p pi- PHSP; + 0.0000760 chi_c1 p+ K- PHSP; + 0.0000050 chi_c1 p+ pi- PHSP; + 0.0000770 chi_c2 p+ K- PHSP; + 0.0000048 chi_c2 p+ pi- PHSP; Enddecay Decay anti-Lambda_b0 ### 0.00038 anti-Lambda0 psi(2S) PHSP; 0.00047 anti-Lambda0 J/psi PHSP; - 0.0000760 chi_c1 anti-p K+ PHSP; - 0.0000050 chi_c1 anti-p pi+ PHSP; - 0.0000770 chi_c2 anti-p K+ PHSP; - 0.0000048 chi_c2 anti-p pi+ PHSP; + 0.0000760 chi_c1 anti-p- K+ PHSP; + 0.0000050 chi_c1 anti-p- pi+ PHSP; + 0.0000770 chi_c2 anti-p- K+ PHSP; + 0.0000048 chi_c2 anti-p- pi+ PHSP; Enddecay Decay Xi_b- diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_OO_gaptriggered_dq.C b/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_OO_gaptriggered_dq.C index bacf31747..0673ca60f 100644 --- a/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_OO_gaptriggered_dq.C +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_OO_gaptriggered_dq.C @@ -19,12 +19,36 @@ class GeneratorPythia8NonPromptInjectedOOGapTriggeredDQ : public o2::eventgen::G public: /// constructor - GeneratorPythia8NonPromptInjectedOOGapTriggeredDQ(int inputTriggerRatio = 5) { + GeneratorPythia8NonPromptInjectedOOGapTriggeredDQ(int inputTriggerRatio = 2) { mGeneratedEvents = 0; mInverseTriggerRatio = inputTriggerRatio; + + // Read INPUT_TRIGGER_RATIO from the shell environment. + // If it is not defined, keep the default constructor value. + const char* envTriggerRatio = gSystem->Getenv("INPUT_TRIGGER_RATIO"); + + if (envTriggerRatio && envTriggerRatio[0] != '\0') { + const int valueFromEnv = TString(envTriggerRatio).Atoi(); + + if (valueFromEnv > 0) { + inputTriggerRatio = valueFromEnv; + } else { + printf( + "WARNING: invalid INPUT_TRIGGER_RATIO=%s, using default value %d\n", + envTriggerRatio, + inputTriggerRatio); + } + } + + mGeneratedEvents = 0; + mInverseTriggerRatio = inputTriggerRatio; + + printf("Input trigger ratio: %d\n", mInverseTriggerRatio); + // define minimum bias event generator auto seed = (gRandom->TRandom::GetSeed() % 900000000); + TString pathconfigMB = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_OO_536.cfg"); pythiaMBgen.readFile(pathconfigMB.Data()); pythiaMBgen.readString("Random:setSeed on"); From e4a7c3d30998bab4e31292d797d627f990bbcf3c Mon Sep 17 00:00:00 2001 From: aimeric-landou <46970521+aimeric-landou@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:03:24 +0100 Subject: [PATCH 006/144] [PWGGAJE] jet parametrised model: add bkg choice in ini (#2399) --- ...rametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini | 2 +- ...metrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini | 2 +- ...rametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini | 2 +- ...metrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini index bdd527890..1fdc88fb9 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json") +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", false) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini index bdd527890..57227ffe1 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json") +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini index 9b4974c1a..b51c7ded5 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json") +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", false) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini index 9b4974c1a..654469b4d 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json") +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file From c6adb1b602db7fe90be1028e70b1e2101c765ad2 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Thu, 9 Jul 2026 00:23:38 +0200 Subject: [PATCH 007/144] ExtToHybrid parallel example (#2403) --- .../examples/hybrid/exampleparallel.json | 47 +++++++++++++++++++ .../ini/GeneratorExtToHybridParallel.ini | 4 ++ .../ini/tests/GeneratorExtToHybridParallel.C | 38 +++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 MC/config/examples/hybrid/exampleparallel.json create mode 100644 MC/config/examples/ini/GeneratorExtToHybridParallel.ini create mode 100644 MC/config/examples/ini/tests/GeneratorExtToHybridParallel.C diff --git a/MC/config/examples/hybrid/exampleparallel.json b/MC/config/examples/hybrid/exampleparallel.json new file mode 100644 index 000000000..77416d23d --- /dev/null +++ b/MC/config/examples/hybrid/exampleparallel.json @@ -0,0 +1,47 @@ +{ + "mode": "parallel", + "generators": [ + { + "name": "pythia8pp", + "config": "" + }, + { + "name": "pythia8pp", + "config": "" + }, + { + "name": "pythia8pp", + "config": "" + }, + { + "name": "pythia8pp", + "config": "" + }, + { + "name": "pythia8pp", + "config": "" + }, + { + "name": "pythia8pp", + "config": "" + }, + { + "name": "pythia8pp", + "config": "" + }, + { + "name": "pythia8pp", + "config": "" + } + ], + "fractions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] +} \ No newline at end of file diff --git a/MC/config/examples/ini/GeneratorExtToHybridParallel.ini b/MC/config/examples/ini/GeneratorExtToHybridParallel.ini new file mode 100644 index 000000000..7745d9095 --- /dev/null +++ b/MC/config/examples/ini/GeneratorExtToHybridParallel.ini @@ -0,0 +1,4 @@ +[GeneratorHybrid] +configFile = ${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/hybrid/exampleparallel.json +switchExtToHybrid = true +num_workers = 8 diff --git a/MC/config/examples/ini/tests/GeneratorExtToHybridParallel.C b/MC/config/examples/ini/tests/GeneratorExtToHybridParallel.C new file mode 100644 index 000000000..2694d57e4 --- /dev/null +++ b/MC/config/examples/ini/tests/GeneratorExtToHybridParallel.C @@ -0,0 +1,38 @@ +int Hybrid() +{ + std::string path{"o2sim_Kine.root"}; + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + // Check if there are 100 events, as simulated in the o2dpg-test + if (nEvents != 100) + { + std::cerr << "Expected 100 events, got " << nEvents << "\n"; + return 1; + } + return 0; +} From 8bf336598ce7f807c326f74e8df47cb46ebc49dd Mon Sep 17 00:00:00 2001 From: shahoian Date: Thu, 9 Jul 2026 12:06:58 +0200 Subject: [PATCH 008/144] Extra settings for Pb26 apass At the moment impose tuned TRD error and MeanVertex bias related to v10n geometry only for Pb26 processing (runNumber > 572013) --- DATA/production/configurations/asyncReco/setenv_extra.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index 307c88d71..c414dc716 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -646,6 +646,14 @@ fi # ad-hoc settings for TRD matching export CONFIG_EXTRA_PROCESS_o2_trd_global_tracking+=";$ITSEXTRAERR;$TRACKTUNETPC;$VDRIFTPARAMOPTION;GPU_rec_trd.minTrackPt=0.3;" +# Pb26 reconstruction uses improved alignment with shifted ITS (mean vertex) +if [[ $RUNNUMBER -ge 572013 ]] ; then + export CONFIG_EXTRA_PROCESS_o2_trd_global_tracking+=";GPU_rec_trd.trkltResRPhiIdeal=0.2;GPU_rec_trd.trkltResVsTanPhiMisalign=1.;" + if [[ -z $ALIEN_JDL_MVBIAS ]]; then + export O2_DPL_MVBIAS=";mvbias.xyz[0]=0.0332;mvbias.xyz[1]=-0.1825;mvbias.xyz[2]=-0.2846;mvbias.slopeX=-4.27e-05;mvbias.slopeY=-6.96e-05;"; + fi +fi + # ad-hoc settings for FT0 export ARGS_EXTRA_PROCESS_o2_ft0_reco_workflow+=" --ft0-reconstructor" if [[ $BEAMTYPE == "PbPb" ]]; then From 18f55b02f45896e70ea85fcbe15dabda7efad6d7 Mon Sep 17 00:00:00 2001 From: "Paul Veen (paveen)" <80593165+ppoava@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:28:10 +0300 Subject: [PATCH 009/144] Make the non-prompt injection ratio configurable also for p-O (#2402) Following the commit for O-O (see https://github.com/AliceO2Group/O2DPG/commit/c53f5cd07020a69d7dc61c8774573481d00aeea2) from donglovo, the non-prompt injection ratio has now also made configurable for the p-O case. I copy here below the description from the commit above, as this one is identical: * Set the non-prompt injection ratio to 2 Change the default injection ratio from 5 to 2 for the non-prompt gap-triggered generators, corresponding to one injected signal event every two generated events. * Allow input trigger ratio configuration via environment variable Read INPUT_TRIGGER_RATIO from the environment and use it to override the default input trigger ratio. Fall back to the constructor default when the variable is unset or invalid. --- ...hia8_NonPromptSignals_pO_gaptriggered_dq.C | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_pO_gaptriggered_dq.C b/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_pO_gaptriggered_dq.C index f4aab2fea..ed028b373 100644 --- a/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_pO_gaptriggered_dq.C +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_pO_gaptriggered_dq.C @@ -19,10 +19,33 @@ class GeneratorPythia8NonPromptInjectedpOGapTriggeredDQ : public o2::eventgen::G public: /// constructor - GeneratorPythia8NonPromptInjectedpOGapTriggeredDQ(int inputTriggerRatio = 5) { + GeneratorPythia8NonPromptInjectedpOGapTriggeredDQ(int inputTriggerRatio = 2) { mGeneratedEvents = 0; - mInverseTriggerRatio = inputTriggerRatio; + mInverseTriggerRatio = inputTriggerRatio; + + // Read INPUT_TRIGGER_RATIO from the shell environment. + // If it is not defined, keep the default constructor value. + const char* envTriggerRatio = gSystem->Getenv("INPUT_TRIGGER_RATIO"); + + if (envTriggerRatio && envTriggerRatio[0] != '\0') { + const int valueFromEnv = TString(envTriggerRatio).Atoi(); + + if (valueFromEnv > 0) { + inputTriggerRatio = valueFromEnv; + } else { + printf( + "WARNING: invalid INPUT_TRIGGER_RATIO=%s, using default value %d\n", + envTriggerRatio, + inputTriggerRatio); + } + } + + mGeneratedEvents = 0; + mInverseTriggerRatio = inputTriggerRatio; + + printf("Input trigger ratio: %d\n", mInverseTriggerRatio); + // define minimum bias event generator auto seed = (gRandom->TRandom::GetSeed() % 900000000); TString pathconfigMB = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_pO_961.cfg"); From a6836eb281e886789da36477b5e1fd47af102fbb Mon Sep 17 00:00:00 2001 From: smaff92 <33285879+smaff92@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:33:44 +0900 Subject: [PATCH 010/144] Bugfix for phi angle in PWGGAJE hooks (#2408) --- MC/config/PWGGAJE/hooks/jets_hook.C | 9 +++++++-- MC/config/PWGGAJE/hooks/prompt_gamma_hook.C | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/MC/config/PWGGAJE/hooks/jets_hook.C b/MC/config/PWGGAJE/hooks/jets_hook.C index 02ccb4dd9..b7e8cb4b3 100644 --- a/MC/config/PWGGAJE/hooks/jets_hook.C +++ b/MC/config/PWGGAJE/hooks/jets_hook.C @@ -40,7 +40,10 @@ class UserHooks_jets : public Pythia8::UserHooks // Check the first jet // - bool acc1 = detector_acceptance(mAcceptance, event[id1].phi(), event[id1].eta()); + Float_t phi_j1 = event[id1].phi(); + if (phi_j1 < 0) phi_j1 += 2 * TMath::Pi(); + + bool acc1 = detector_acceptance(mAcceptance, phi_j1, event[id1].eta()); bool okpdg1 = true; if (mOutPartonPDG > 0 && TMath::Abs(event[id1].id()) != mOutPartonPDG) @@ -48,7 +51,9 @@ class UserHooks_jets : public Pythia8::UserHooks // Check the second jet // - bool acc2 = detector_acceptance(mAcceptance, event[id2].phi(), event[id2].eta()); + Float_t phi_j2 = event[id2].phi(); + if (phi_j2 < 0) phi_j2 += 2 * TMath::Pi(); + bool acc2 = detector_acceptance(mAcceptance, phi_j2, event[id2].eta()); bool okpdg2 = true; if (mOutPartonPDG > 0 && TMath::Abs(event[id2].id()) != mOutPartonPDG) diff --git a/MC/config/PWGGAJE/hooks/prompt_gamma_hook.C b/MC/config/PWGGAJE/hooks/prompt_gamma_hook.C index 678b67c18..9ab6836dc 100644 --- a/MC/config/PWGGAJE/hooks/prompt_gamma_hook.C +++ b/MC/config/PWGGAJE/hooks/prompt_gamma_hook.C @@ -62,16 +62,17 @@ class UserHooks_promptgamma : public Pythia8::UserHooks return true; } } - // Select photons within acceptance // - if (detector_acceptance(mAcceptance, event[idGam].phi(), event[idGam].eta())) { + Float_t phiGam = event[idGam].phi(); + if (phiGam < 0) phiGam += 2 * TMath::Pi(); + + if (detector_acceptance(mAcceptance, phiGam, event[idGam].eta())) { // printf("+++ Accepted event +++ \n"); printf("Selected gamma, id %d, PDG %d, status %d, mother %d, E %2.2f, pT %2.2f, eta %2.2f, phi %2.2f\n", idGam, event[idGam].id(), event[idGam].status(), event[idGam].mother1(), event[idGam].e(), event[idGam].pT(), - event[idGam].eta(), event[idGam].phi() * TMath::RadToDeg()); - + event[idGam].eta(), phiGam * TMath::RadToDeg()); // printf("Back-to-back parton, id %d, PDG %d, status %d, mother %d, E %2.2f, pT %2.2f, eta %2.2f, phi %2.2f\n", idPar, // event[idPar].id() , event[idPar].status(), event[idPar].mother1(), // event[idPar].e() , event[idPar].pT(), @@ -83,7 +84,6 @@ class UserHooks_promptgamma : public Pythia8::UserHooks // event[idPar].pT() - event[idGam].pT(), // event[idPar].eta()- event[idGam].eta(), // event[idPar].phi()*TMath::RadToDeg()-event[idGam].phi()*TMath::RadToDeg()); - return false; } else { // printf("--- Rejected event ---\n"); From 015dba5e16ad482c416afae93925162f6aa3b752 Mon Sep 17 00:00:00 2001 From: mj525 <99000728+mjkim525@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:53:49 +0900 Subject: [PATCH 011/144] [PWGLF] Add finite-width resonance injection and particle definitions (#2405) * Fix resonance injeciton codes and add width information from PDG2025 * Fix the list of injected resonances with minor updates * Update generator documentation comments --- .../generator/resonancelistgun_width_inj.json | 211 +++++ .../pythia8/generator/resonances_width.cfg | 82 ++ .../generator_pythia8_LF_rapidity_width.C | 791 ++++++++++++++++++ 3 files changed, 1084 insertions(+) create mode 100644 MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonances_width.cfg create mode 100644 MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C diff --git a/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json new file mode 100644 index 000000000..f6ea92df8 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json @@ -0,0 +1,211 @@ +{ + "f_0(980)" : { + "pdg": 9010221, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "K1(1270)+": { + "pdg": 10323, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "K1(1270)-": { + "pdg": -10323, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "K1(1270)0": { + "pdg": 10313, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "anti-K1(1270)0": { + "pdg": -10313, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Sigma(1385)-" : { + "pdg": 3114, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "anti-Sigma(1385)+" : { + "pdg": -3114, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Sigma(1385)+" : { + "pdg": 3224, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "anti-Sigma(1385)-" : { + "pdg": -3224, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Lambda(1520)0" : { + "pdg": 102134, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "anti-Lambda(1520)0" : { + "pdg": -102134, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Xi(1530)0" : { + "pdg": 3324, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "anti-Xi(1530)0" : { + "pdg": -3324, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Xi(1820)0" : { + "pdg": 123324, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Anti-Xi(1820)0" : { + "pdg": -123324, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Xi(1820)-" : { + "pdg": 123314, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Xi(1820)+" : { + "pdg": -123314, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Omega(2012)-" : { + "pdg": 123334, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, + "Omega(2012)+" : { + "pdg": -123334, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonances_width.cfg b/MC/config/PWGLF/pythia8/generator/resonances_width.cfg new file mode 100644 index 000000000..4845f45b8 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonances_width.cfg @@ -0,0 +1,82 @@ +# id::all = name antiName spinType chargeType colType m0 mWidth mMin mMax tau0 +# mass interval: +# mMin ~= sum of daughter masses + 1--2 MeV +# mMax = m0 + 10 * mWidth +# particle properties based on PDG 2025 + +### f0(980) +# 9010221:all = f_0(980) void 1 0 0 0.9900 0.0500 0.2791 1.6900 0 +9010221:onMode = off +# 9010221:oneChannel = 1 1.000 0 211 -211 +9010221:onIfMatch = 211 -211 + +# ### Delta(1232)++ +# 2224:all = DeltaPlusPlus DeltaPlusPlusbar 4 6 0 1.2320 0.1170 1.0778 2.4020 0 +2224:onMode = off +# 2224:oneChannel = 1 1.000 0 2212 211 +2224:onIfMatch = 2212 211 + +# ### Delta(1232)0 +# 2114:all = DeltaZero DeltaZerobar 4 0 0 1.2320 0.1170 1.0778 2.4020 0 +2114:onMode = off +# 2114:oneChannel = 1 1.000 0 2212 -211 +2114:onIfMatch = 2212 -211 + + +# ### K1(1270)+ / K1(1270)+ bar +# 10323:all = K_1(1270)Plus K_1(1270)Plusbar 3 3 0 1.2530 0.09000 0.7728 2.1530 0 +# ### K1(1270)+ -> K+ rho0 +# 10323:oneChannel = 1 0.500 0 321 113 +# ### K1(1270)+ -> K*(892)0 pi+ +# 10323:addChannel = 1 0.500 0 313 211 +10323:onMode = off +10323:onIfMatch = 321 113 +10323:onIfMatch = 313 211 + + +# ### K1(1270)0 / anti-K1(1270)0 +# 10313:all = K_1(1270)Zero K_1(1270)Zerobar 3 0 0 1.2530 0.0900 0.7682 2.1530 0 +# ### K1(1270)0 -> K0 rho0 +# 10313:oneChannel = 1 0.500 0 311 113 +# ### K1(1270)0 -> K*(892)+ pi- +# 10313:addChannel = 1 0.500 0 323 -211 +10313:onMode = off +10313:onIfMatch = 311 113 +10313:onIfMatch = 323 -211 + +# ### Sigma(1385)+ +# 3224:all = Sigma1385Plus Sigma1385Plusbar 4 3 0 1.3828 0.0362 1.2552 1.6900 0 +# 3224:oneChannel = 1 1 0 3122 211 +3224:onMode = off +3224:onIfMatch = 3122 211 + +# ### Sigma(1385)- +# 3114:all = Sigma1385Minus Sigma1385Minusbar 4 -3 0 1.3872 0.0394 1.2552 1.7812 0 +# 3114:oneChannel = 1 1 0 3122 -211 +3114:onMode = off +3114:onIfMatch = 3122 -211 + +# ### Lambda1520 +# 102134:all = Lambda1520 Lambda1520bar 4 0 0 1.5190 0.01600 1.4319 1.6790 0 +# 102134:oneChannel = 1 1.000 0 2212 -321 +102134:onMode = off +102134:onIfMatch = 2212 -321 + +# ### Xi1530 +# # id::all = name antiName spinType chargeType colType m0 mWidth mMin mMax tau0 +# 3324:all = Xi1530 Xi1530bar 4 0 0 1.5318 0.0091 1.4612 1.6228 0 +# 3324:oneChannel = 1 1.000 0 3312 211 +3324:onMode = off +3324:onIfMatch = 3312 211 + +### Xi1820 +123324:all = Xi1820Zero Xi1820Zerobar 4 0 0 1.8230 0.0240 1.6150 2.0630 0 +123324:oneChannel = 1 1.000 0 3122 310 + +123314:all = Xi1820Minus Xi1820Minusbar 4 -3 0 1.8230 0.0240 1.6110 2.0630 0 +123314:oneChannel = 1 1.000 0 3122 -321 + +### Omega2012 +123334:all = Omega2012Minus Omega2012Minusbar 4 -3 0 2.0125 0.0064 1.8210 2.0765 0 +### Omega(2012)- -> Xi- K0S +123334:oneChannel = 1 1.000 0 3312 310 diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C b/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C new file mode 100644 index 000000000..6d71a8db5 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C @@ -0,0 +1,791 @@ +/// +/// \file generator_pythia8_LF_rapidity_width.C +/// \author Hirak Kumar Koley hirak.koley@cern.ch +/// \author Bong-Hwi Lim bong-hwi.lim@cern.ch +/// \author Minjae Kim minjae.kim@cern.ch +/// \author Based on generator_pythia8_LF.C by Nicolò Jacazio +/// \since 2025/08/18 +/// \brief Implementation of a gun generator for multiple particles using rapidity or pseudorapidity (default) instead of eta, built on generator_pythia8_longlived.C +/// Needs PDG, Number of injected, minimum and maximum pT, minimum and maximum y/eta. These can be provided in three ways, bundeling variables, particles or from input file +/// usage: +/// `o2-sim -g external --configKeyValues 'GeneratorExternal.fileName=generator_pythia8_LF_rapidity_width.C;GeneratorExternal.funcName=generateLFRapidity({1000010020, 1000010030}, {10, 10}, {0.5, 0.5}, {10, 10}, {-1.0, -1.0}, {1.0, 1.0})'` +/// Here PDG, Number injected, pT limits, y/eta limits are separated and matched by index +/// or: +/// `o2-sim -g external --configKeyValues 'GeneratorExternal.fileName=generator_pythia8_LF_rapidity_width.C;GeneratorExternal.funcName=generateLFRapidity({{1000010020, 10, 0.5, 10, -1.0, 1.0}, {1000010030, 10, 0.5, 10, -1.0, 1.0}})'` +/// Here PDG, Number injected, pT limits, y/eta limits are divided per particle +/// or: +/// `o2-sim -g external --configKeyValues 'GeneratorExternal.fileName=generator_pythia8_LF_rapidity_width.C;GeneratorExternal.funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/exotic_nuclei_pp.gun")'` +/// Here PDG, Number injected, pT limits, y/eta limits are provided via an intermediate configuration file +/// + +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "FairGenerator.h" + +#include "TDatabasePDG.h" +#include "TMath.h" +#include "TParticlePDG.h" +#include "TRandom3.h" + +#include "Generators/GeneratorPythia8.h" +#include "Pythia8/Pythia.h" +#if __has_include("SimulationDataFormat/MCGenStatus.h") +#include "SimulationDataFormat/MCGenStatus.h" +#else +#include "SimulationDataFormat/MCGenProperties.h" +#endif +#if __has_include("SimulationDataFormat/MCUtils.h") +#include "SimulationDataFormat/MCUtils.h" +#endif +#include "TSystem.h" + +#include "Generators/GeneratorPythia8Param.h" +#include "fairlogger/Logger.h" + +#include +#include +#endif +// DecayerPythia8Param needs to be included after the #endif to work with Cling +#include "Generators/DecayerPythia8Param.h" +#if defined(__CLING__) && !defined(__ROOTCLING__) +#if __has_include("SimulationDataFormat/MCGenStatus.h") +#include "SimulationDataFormat/MCGenStatus.h" +#elif __has_include("SimulationDataFormat/MCGenProperties.h") +#include "SimulationDataFormat/MCGenProperties.h" +#endif +#if __has_include("SimulationDataFormat/MCUtils.h") +#include "SimulationDataFormat/MCUtils.h" +#endif +#pragma cling load("libO2Generators") +#endif +// #include "Generators/GeneratorPythia8.h" +#include "generator_pythia8_longlived.C" + +#include + +using namespace Pythia8; +using namespace o2::mcgenstatus; + +class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 +{ + public: + /// Parametric constructor + GeneratorPythia8LFRapidity(bool injOnePerEvent = true, + int gapBetweenInjection = 0, + bool useTrigger = false, + bool useRapidity = false, + std::string pythiaCfgMb = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", + std::string pythiaCfgSignal = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg") : GeneratorPythia8{}, + mOneInjectionPerEvent{injOnePerEvent}, + mGapBetweenInjection{gapBetweenInjection}, + mUseTriggering{useTrigger}, + mUseRapidity{useRapidity} + { + LOG(info) << "GeneratorPythia8LFRapidity constructor"; + LOG(info) << "++ mOneInjectionPerEvent: " << mOneInjectionPerEvent; + LOG(info) << "++ mGapBetweenInjection: " << mGapBetweenInjection; + LOG(info) << "++ mUseTriggering: " << mUseTriggering; + LOG(info) << "++ mUseRapidity: " << mUseRapidity; + LOG(info) << "++ pythiaCfgMb: " << pythiaCfgMb; + LOG(info) << "++ pythiaCfgSignal: " << pythiaCfgSignal; + gRandom->SetSeed(0); + if (useTrigger) { + mPythia.readString("ProcessLevel:all off"); + if (pythiaCfgMb == "") { // If no configuration file is provided, use the one from the Pythia8Param + auto& param = o2::eventgen::GeneratorPythia8Param::Instance(); + LOG(info) << "Instance LFRapidity \'Pythia8\' generator with following parameters for MB event"; + LOG(info) << param; + pythiaCfgMb = param.config; + } + if (pythiaCfgSignal == "") { // If no configuration file is provided, use the one from the Pythia8Param + auto& param = o2::eventgen::GeneratorPythia8Param::Instance(); + LOG(info) << "Instance LFRapidity \'Pythia8\' generator with following parameters for signal event"; + LOG(info) << param; + pythiaCfgSignal = param.config; + } + pythiaCfgMb = gSystem->ExpandPathName(pythiaCfgMb.c_str()); + pythiaCfgSignal = gSystem->ExpandPathName(pythiaCfgSignal.c_str()); + LOG(info) << " ++ Using trigger, initializing Pythia8 for trigger"; + if (!pythiaObjectMinimumBias.readFile(pythiaCfgMb)) { + LOG(fatal) << "Could not pythiaObjectMinimumBias.readFile(\"" << pythiaCfgMb << "\")"; + } + pythiaObjectMinimumBias.readString("Random:setSeed = on"); + pythiaObjectMinimumBias.readString("Random:seed =" + std::to_string(gRandom->Integer(900000000 - 2) + 1)); + + // FIX: Init signal pythia object + if (!pythiaObjectSignal.readFile(pythiaCfgSignal)) { + LOG(fatal) << "Could not pythiaObjectSignal.readFile(\"" << pythiaCfgSignal << "\")"; + } + pythiaObjectSignal.readString("Random:setSeed = on"); + pythiaObjectSignal.readString("Random:seed =" + std::to_string(gRandom->Integer(900000000 - 2) + 1)); + pythiaObjectMinimumBias.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + mPythiaGun.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + if (!pythiaObjectMinimumBias.init()) { + LOG(fatal) << "Could not pythiaObjectMinimumBias.init() from " << pythiaCfgMb; + } + if (!pythiaObjectSignal.init()) { + LOG(fatal) << "Could not pythiaObjectSignal.init() from " << pythiaCfgSignal; + } + } else { // Using simple injection with internal decay (if needed). Fetching the parameters from the configuration file of the PythiaDecayer + + if (pythiaCfgMb == "") { // If no configuration file is provided, use the one from the Pythia8Param + auto& param = o2::eventgen::GeneratorPythia8Param::Instance(); + LOG(info) << "Instance LFRapidity \'Pythia8\' generator with following parameters for MB event"; + LOG(info) << param; + pythiaCfgMb = param.config; + } + // FIX: Fallback if still empty to default minbias + if (pythiaCfgMb == "") { + pythiaCfgMb = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg"; + } + pythiaCfgMb = gSystem->ExpandPathName(pythiaCfgMb.c_str()); + if (!pythiaObjectMinimumBias.readFile(pythiaCfgMb)) { + LOG(fatal) << "Could not pythiaObjectMinimumBias.readFile(\"" << pythiaCfgMb << "\")"; + } + pythiaObjectMinimumBias.readString("Random:setSeed = on"); + pythiaObjectMinimumBias.readString("Random:seed =" + std::to_string(gRandom->Integer(900000000 - 2) + 1)); + + pythiaObjectMinimumBias.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + mPythiaGun.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + if (!pythiaObjectMinimumBias.init()) { + LOG(fatal) << "Could not pythiaObjectMinimumBias.init() from " << pythiaCfgMb; + } + + /** switch off process level **/ + mPythiaGun.readString("ProcessLevel:all off"); + + auto& param = o2::eventgen::DecayerPythia8Param::Instance(); + LOG(info) << "Init \'GeneratorPythia8LFRapidity\' with following parameters"; + LOG(info) << param; + for (int i = 0; i < 8; ++i) { + if (param.config[i].empty()) { + continue; + } + std::string config = gSystem->ExpandPathName(param.config[i].c_str()); + readParticleDataLines(mPythia, config, "import Pythia"); + LOG(info) << "GeneratorPythia8LFRapidity Reading configuration from file: " << config; + if (!mPythiaGun.readFile(config, true)) { + LOG(fatal) << "Failed to init \'DecayerPythia8\': problems with configuration file " + << config; + return; + } + } + + /** show changed particle data **/ + if (param.showChanged) { + mPythiaGun.readString(std::string("Init:showChangedParticleData on")); + } else { + mPythiaGun.readString(std::string("Init:showChangedParticleData off")); + } + + /** initialise **/ + if (!mPythiaGun.init()) { + LOG(fatal) << "Failed to init \'DecayerPythia8\': init returned with error"; + return; + } + if (pythiaCfgSignal != "") { + LOG(fatal) << "Cannot use simple injection and have a configuration file. pythiaCfgSignal= `" << pythiaCfgSignal << "` must be empty"; + } + } + } + + /// Destructor + ~GeneratorPythia8LFRapidity() = default; + + //__________________________________________________________________ + Bool_t generateEvent() override + { + if (!mUseTriggering) { // Injected mode: Embedding into MB + // 1. Generate Background (MB) + // LOG(info) << "Generating background event " << mEventCounter; + + bool lGenerationOK = false; + while (!lGenerationOK) { + lGenerationOK = pythiaObjectMinimumBias.next(); + } + copyMinimumBiasEventForInjection(); + + // 2. Determine if we inject specific particles (Gap logic) + bool doInjection = true; + if (mGapBetweenInjection > 0) { + if (mGapBetweenInjection == 1 && mEventCounter % 2 == 0) { + doInjection = false; + } else if (mEventCounter % mGapBetweenInjection != 0) { + doInjection = false; + } + } + + if (!doInjection) { + LOG(info) << "Skipping injection for event " << mEventCounter; + return true; + } + } + + LOG(info) << "generateEvent (Injection) " << mEventCounter; + + // For Triggered mode, we start clean. For Injected mode, we have MB in mPythia.event + if (mUseTriggering) { + mPythia.event.reset(); + } + + mConfigToUse = mOneInjectionPerEvent ? static_cast(gRandom->Uniform(0.f, getNGuns())) : -1; + LOG(info) << "Using configuration " << mConfigToUse << " out of " << getNGuns() << ", of which " << mGunConfigs.size() << " are transport decayed and " << mGunConfigsGenDecayed.size() << " are generator decayed"; + + bool injectedForThisEvent = false; + int nConfig = mGunConfigs.size(); // We start counting from the configurations of the transport decayed particles + for (const ConfigContainer& cfg : mGunConfigsGenDecayed) { + nConfig++; + if (mConfigToUse >= 0 && (nConfig - 1) != mConfigToUse) { + continue; + } + LOG(info) << "Using config container "; + cfg.print(); + if (mUseTriggering) { // Do the triggering + bool doSignal{mEventCounter % (mGapBetweenInjection + 1) == 0}; // Do signal or gap + + if (doSignal) { + LOG(info) << "Generating triggered signal event for particle"; + cfg.print(); + bool satisfiesTrigger = false; + int nTries = 0; + while (!satisfiesTrigger) { + if (!pythiaObjectSignal.next()) { + continue; + } + // Check if triggered condition satisfied + for (int j = 0; j < pythiaObjectSignal.event.size(); j++) { + const int& pypid = pythiaObjectSignal.event[j].id(); + const float& pyeta = mUseRapidity ? pythiaObjectSignal.event[j].y() : pythiaObjectSignal.event[j].eta(); + const float& pypt = pythiaObjectSignal.event[j].pT(); + if (pypid == cfg.mPdg && cfg.mMin < pyeta && pyeta < cfg.mMax && pypt > cfg.mPtMin && pypt < cfg.mPtMax) { + LOG(info) << "Found particle " << j << " " << pypid << " with " << (mUseRapidity ? "rapidity" : "eta") << " " << pyeta << " and pT " << pypt << " in event " << mEventCounter << " after " << nTries << " tries"; + satisfiesTrigger = true; + break; + } + } + nTries++; + } + mPythia.event = pythiaObjectSignal.event; + } else { + LOG(info) << "Generating background event " << mEventCounter; + // Generate minimum-bias event + bool lGenerationOK = false; + while (!lGenerationOK) { + lGenerationOK = pythiaObjectMinimumBias.next(); + } + mPythia.event = pythiaObjectMinimumBias.event; + } + continue; + } + + // Do the injection + // Use mPythiaGun for separate generation and decay + mPythiaGun.event.reset(); + for (int i{0}; i < cfg.mNInject; ++i) { + const double pt = gRandom->Uniform(cfg.mPtMin, cfg.mPtMax); + const double eta = gRandom->Uniform(cfg.mMin, cfg.mMax); + const double phi = gRandom->Uniform(0, TMath::TwoPi()); + const double px{pt * std::cos(phi)}; + const double py{pt * std::sin(phi)}; + const double mass = sampleMass(cfg); + + double pz = 0; + double et = 0; + + if (mUseRapidity) { + // Rapidty Case + const double mT = std::sqrt(mass * mass + pt * pt); + pz = mT * std::sinh(eta); + et = mT * std::cosh(eta); + } else { + // Eta Case + pz = pt * std::sinh(eta); + const double p = pt * std::cosh(eta); + et = std::sqrt(p * p + mass * mass); + } + + Particle particle; + particle.id(cfg.mPdg); + particle.status(11); + particle.m(mass); + particle.px(px); + particle.py(py); + particle.pz(pz); + particle.e(et); + particle.xProd(0.f); + particle.yProd(0.f); + particle.zProd(0.f); + mPythiaGun.particleData.mayDecay(cfg.mPdg, true); // force decay + mPythiaGun.event.append(particle); + } + // Decay the gun particles + mPythiaGun.moreDecays(); + mPythiaGun.next(); + + // Merge mPythiaGun event into mPythia.event (MB) + int offset = mPythia.event.size(); + LOG(info) << "Merging " << mPythiaGun.event.size() - 1 << " injected particles into MB event of size " << offset; + + for (int i = 1; i < mPythiaGun.event.size(); ++i) { // Skip system particle 0 + Particle& p = mPythiaGun.event[i]; + // Adjust history indices + int mother1 = p.mother1(); + int mother2 = p.mother2(); + int daughter1 = p.daughter1(); + int daughter2 = p.daughter2(); + + if (mother1 > 0) + mother1 += offset - 1; + if (mother2 > 0) + mother2 += offset - 1; + if (daughter1 > 0) + daughter1 += offset - 1; + if (daughter2 > 0) + daughter2 += offset - 1; + + p.mothers(mother1, mother2); + p.daughters(daughter1, daughter2); + + mPythia.event.append(p); + } + + injectedForThisEvent = true; + } + + // For purely trivial injection (no generator decay needed in loop or just transport decay), we still might have injection flag + // But above loop covers generator decayed. + // What if we only have Transport Decay particles? (mGunConfigs) + // We treat them in importParticles usually. + // But if we are in embedding mode, we just need to ensure the MB event is there. + // The mGunConfigs are handled in importParticles. + + if (mVerbose) { + LOG(info) << "Eventlisting"; + mPythia.event.list(1); + mPythia.stat(); + } + return true; + } + + //__________________________________________________________________ + Bool_t importParticles() override + { + if (!mUseTriggering) { // If the triggering is used we handle the the gap when generating the signal + if (mGapBetweenInjection > 0) { + if (mGapBetweenInjection == 1 && mEventCounter % 2 == 0) { + LOG(info) << "Skipping importParticles event " << mEventCounter++; + return true; + } else if (mEventCounter % mGapBetweenInjection != 0) { + LOG(info) << "Skipping importParticles event " << mEventCounter++; + return true; + } + } + } + LOG(info) << "importParticles " << mEventCounter++; + GeneratorPythia8::importParticles(); + int nConfig = 0; + for (const ConfigContainer& cfg : mGunConfigs) { + nConfig++; + if (mConfigToUse >= 0 && (nConfig - 1) != mConfigToUse) { + continue; + } + LOGF(info, "Injecting %i particles with PDG %i, pT in [%f, %f], %s in [%f, %f]", cfg.mNInject, cfg.mPdg, cfg.mPtMin, cfg.mPtMax, (mUseRapidity ? "rapidity" : "eta"), cfg.mMin, cfg.mMax); + + for (int i{0}; i < cfg.mNInject; ++i) { + const double pt = gRandom->Uniform(cfg.mPtMin, cfg.mPtMax); + const double eta = gRandom->Uniform(cfg.mMin, cfg.mMax); + const double phi = gRandom->Uniform(0, TMath::TwoPi()); + const double px{pt * std::cos(phi)}; + const double py{pt * std::sin(phi)}; + const double mass = sampleMass(cfg); + double pz = 0; + double et = 0; + + if (mUseRapidity) { + const double mT = std::sqrt(mass * mass + pt * pt); + pz = mT * std::sinh(eta); + et = mT * std::cosh(eta); + } else { + pz = pt * std::sinh(eta); + const double p = pt * std::cosh(eta); + et = std::sqrt(p * p + mass * mass); + } + + // TParticle::TParticle(Int_t pdg, + // Int_t status, + // Int_t mother1, Int_t mother2, + // Int_t daughter1, Int_t daughter2, + // Double_t px, Double_t py, Double_t pz, Double_t etot, + // Double_t vx, Double_t vy, Double_t vz, Double_t time) + + mParticles.push_back(TParticle(cfg.mPdg, + MCGenStatusEncoding(1, 1).fullEncoding, + -1, -1, + -1, -1, + px, py, pz, et, + 0., 0., 0., 0.)); + // make sure status code is encoded properly. Transport flag will be set by default and we have nothing + // to do since all pushed particles should be tracked. + o2::mcutils::MCGenHelper::encodeParticleStatusAndTracking(mParticles.back()); + } + nConfig++; + } + if (mVerbose) { + LOG(info) << "Printing particles that are appended"; + int n = 0; + for (const auto& p : mParticles) { + LOG(info) << "Particle " << n++ << " is a " << p.GetPdgCode() << " with status " << p.GetStatusCode() << " and px = " << p.Py() << " py = " << p.Py() << " pz = " << p.Pz(); + } + } + return true; + } + + struct ConfigContainer { + ConfigContainer(int input_pdg = 0, int n = 1, + float ptMin = 1, float ptMax = 10, + float min = -1, float max = 1) : mPdg{input_pdg}, + mNInject{n}, + mPtMin{ptMin}, + mPtMax{ptMax}, + mMin{min}, + mMax{max} + { + // mMass = getMassFromPDG(mPdg); + mMass = GeneratorPythia8LongLivedGun::getMass(mPdg); + if (mMass <= 0) { + LOG(warning) << "Could not find mass for mPdg " << mPdg << " in TDatabasePDG; will try Pythia particle data at injection time"; + } + LOGF(info, "ConfigContainer: mPdg = %i, mNInject = %i, mPtMin = %f, mPtMax = %f, mMin = %f, mMax = %f, mMass = %f", + mPdg, mNInject, mPtMin, mPtMax, mMin, mMax, mMass); + }; + + ConfigContainer(TObjArray* arr) : ConfigContainer(atoi(arr->At(0)->GetName()), + atoi(arr->At(1)->GetName()), + atof(arr->At(2)->GetName()), + atof(arr->At(3)->GetName()), + atof(arr->At(4)->GetName()), + atof(arr->At(5)->GetName())) + { + bool hasGenDecayed = false; + for (int i = 0; i < arr->GetEntries(); i++) { + const TString n = arr->At(i)->GetName(); + std::cout << n << std::endl; + if (n == "genDecayed") { + hasGenDecayed = true; + break; + } + } + if (hasGenDecayed) { + if (arr->GetEntries() != 7) { + LOG(fatal) << "Wrong number of entries in the configuration array, should be 7, is " << arr->GetEntries(); + } + } else { + if (arr->GetEntries() != 6) { + LOG(fatal) << "Wrong number of entries in the configuration array, should be 6, is " << arr->GetEntries(); + } + } + }; + ConfigContainer(TString line) : ConfigContainer(line.Tokenize(" ")) {}; + ConfigContainer(const nlohmann::json& jsonParams, bool useRapidity = false) : ConfigContainer(jsonParams["pdg"], + jsonParams["n"], + jsonParams["ptMin"], + jsonParams["ptMax"], + (useRapidity && jsonParams.contains("rapidityMin")) ? jsonParams["rapidityMin"] : (jsonParams.contains("min") ? jsonParams["min"] : jsonParams["etaMin"]), + (useRapidity && jsonParams.contains("rapidityMax")) ? jsonParams["rapidityMax"] : (jsonParams.contains("max") ? jsonParams["max"] : jsonParams["etaMax"])) {}; + + // Data Members + const int mPdg = 0; + const int mNInject = 1; + const float mPtMin = 1; + const float mPtMax = 10; + const float mMin = -1.f; + const float mMax = 1.f; + double mMass = 0.f; + + void print() const + { + LOGF(info, "int mPdg = %i", mPdg); + LOGF(info, "int mNInject = %i", mNInject); + LOGF(info, "float mPtMin = %f", mPtMin); + LOGF(info, "float mPtMax = %f", mPtMax); + LOGF(info, "float mMin = %f", mMin); + LOGF(info, "float mMax = %f", mMax); + LOGF(info, "double mMass = %f", mMass); + } + }; + + double sampleMass(const ConfigContainer& cfg) + { + const double pythiaMass = mPythiaGun.particleData.m0(cfg.mPdg); + const double mass = mPythiaGun.particleData.mWidth(cfg.mPdg) > 0. + ? mPythiaGun.particleData.mSel(cfg.mPdg) + : pythiaMass > 0. + ? pythiaMass + : cfg.mMass; + if (mass <= 0.) { + LOG(fatal) << "Could not find mass for mPdg " << cfg.mPdg; + } + return mass; + } + + //__________________________________________________________________ + ConfigContainer addGun(int input_pdg, int nInject = 1, float ptMin = 1, float ptMax = 10, float min = 1, float max = 10) + { + if (mUseTriggering) { // If in trigger mode, every particle needs to be generated from pythia + return addGunGenDecayed(input_pdg, nInject, ptMin, ptMax, min, max); + } + ConfigContainer cfg{input_pdg, nInject, ptMin, ptMax, min, max}; + mGunConfigs.push_back(cfg); + return cfg; + } + + //__________________________________________________________________ + ConfigContainer addGun(ConfigContainer cfg) { return addGun(cfg.mPdg, cfg.mNInject, cfg.mPtMin, cfg.mPtMax, cfg.mMin, cfg.mMax); } + + //__________________________________________________________________ + ConfigContainer addGunGenDecayed(int input_pdg, int nInject = 1, float ptMin = 1, float ptMax = 10, float min = 1, float max = 10) + { + ConfigContainer cfg{input_pdg, nInject, ptMin, ptMax, min, max}; + mGunConfigsGenDecayed.push_back(cfg); + return cfg; + } + + //__________________________________________________________________ + ConfigContainer addGunGenDecayed(ConfigContainer cfg) { return addGunGenDecayed(cfg.mPdg, cfg.mNInject, cfg.mPtMin, cfg.mPtMax, cfg.mMin, cfg.mMax); } + + //__________________________________________________________________ + long int getNGuns() const { return mGunConfigs.size() + mGunConfigsGenDecayed.size(); } + + //__________________________________________________________________ + void print() + { + LOG(info) << "GeneratorPythia8LFRapidity configuration with " << getNGuns() << " guns:"; + LOG(info) << "Particles decayed by the transport:"; + int n = 0; + for (const auto& cfg : mGunConfigs) { + LOG(info) << n++ << "/" << mGunConfigs.size() << ":"; + cfg.print(); + } + n = 0; + LOG(info) << "Particles decayed by the generator:"; + for (const auto& cfg : mGunConfigsGenDecayed) { + LOG(info) << n++ << "/" << mGunConfigs.size() << ":"; + cfg.print(); + } + } + + void setVerbose(bool verbose = true) { mVerbose = verbose; } + + private: + void copyMinimumBiasEventForInjection() + { + mPythia.event = pythiaObjectMinimumBias.event; + // Event assignment adopts the source ParticleData pointer; use the import table for injected custom PDGs. + mPythia.event.init("Minimum-bias event with injected particles", &mPythia.particleData); + mPythia.event.restorePtrs(); + } + + static bool isParticleDataLine(const std::string& line) + { + size_t pos = line.find_first_not_of(" \t"); + if (pos == std::string::npos) { + return false; + } + if (line[pos] == '+' || line[pos] == '-') { + ++pos; + } + if (pos >= line.size() || line[pos] < '0' || line[pos] > '9') { + return false; + } + while (pos < line.size() && line[pos] >= '0' && line[pos] <= '9') { + ++pos; + } + return pos < line.size() && line[pos] == ':'; + } + + static bool readParticleDataLines(Pythia8::Pythia& pythia, const std::string& config, const char* target) + { + std::ifstream input(config.c_str(), ios::in); + if (!input.is_open()) { + LOG(fatal) << "Could not open particle data configuration " << config << " for " << target; + return false; + } + std::string line; + while (std::getline(input, line)) { + TString stripped = line; + stripped.Strip(TString::kBoth, ' '); + if (stripped.IsNull() || stripped.IsWhitespace() || stripped.BeginsWith("#")) { + continue; + } + std::string command = stripped.Data(); + if (!isParticleDataLine(command)) { + continue; + } + LOG(info) << "Reading particle data for " << target << ": " << command; + if (!pythia.readString(command)) { + LOG(fatal) << "Failed to read particle data line for " << target << ": " << command; + return false; + } + } + return true; + } + // Configuration + const bool mOneInjectionPerEvent = true; // if true, only one injection per event is performed, i.e. if multiple PDG (including antiparticles) are requested to be injected only one will be done per event + const bool mUseTriggering = false; // if true, use triggering instead of injection + const int mGapBetweenInjection = 0; // Gap between two signal events. 0 means injection at every event + const bool mUseRapidity = false; // if true, use rapidity instead of eta + + // Running variables + int mConfigToUse = -1; // Index of the configuration to use + int mEventCounter = 0; // Event counter + bool mVerbose = true; // Verbosity flag + + std::vector mGunConfigs; // List of gun configurations to use + std::vector mGunConfigsGenDecayed; // List of gun configurations to use that will be decayed by the generator + Pythia8::Pythia pythiaObjectSignal; // Signal collision generator + Pythia8::Pythia pythiaObjectMinimumBias; // Minimum bias collision generator + Pythia8::Pythia mPythiaGun; // Gun generator with decay support +}; + +///___________________________________________________________ +/// Create generator via arrays of entries. By default injecting in every event and all particles +FairGenerator* generateLFRapidity(std::vector PDGs, std::vector nInject, std::vector ptMin, std::vector ptMax, std::vector min, std::vector max, bool useRapidity = false) +{ + const std::vector entries = {PDGs.size(), nInject.size(), ptMin.size(), ptMax.size(), min.size(), max.size()}; + if (!std::equal(entries.begin() + 1, entries.end(), entries.begin())) { + LOGF(fatal, "Not equal number of entries, check configuration"); + return nullptr; + } + GeneratorPythia8LFRapidity* multiGun = new GeneratorPythia8LFRapidity(false, 0, false, useRapidity, "", ""); + for (unsigned long i = 0; i < entries[0]; i++) { + multiGun->addGun(PDGs[i], nInject[i], ptMin[i], ptMax[i], min[i], max[i]); + } + return multiGun; +} + +///___________________________________________________________ +/// Create generator via an array of configurations +FairGenerator* generateLFRapidity(std::vector cfg, + std::vector cfgGenDecayed, + bool injectOnePDGPerEvent = true, + int gapBetweenInjection = 0, + bool useTrigger = false, + bool useRapidity = false, + std::string pythiaCfgMb = "", + std::string pythiaCfgSignal = "") +{ + GeneratorPythia8LFRapidity* multiGun = new GeneratorPythia8LFRapidity(injectOnePDGPerEvent, gapBetweenInjection, useTrigger, useRapidity, pythiaCfgMb, pythiaCfgSignal); + for (const auto& c : cfg) { + LOGF(info, "Adding gun %i", multiGun->getNGuns()); + c.print(); + multiGun->addGun(c); + } + for (const auto& c : cfgGenDecayed) { + LOGF(info, "Adding gun %i, particle will be decayed by the generator", multiGun->getNGuns()); + c.print(); + multiGun->addGunGenDecayed(c); + } + multiGun->print(); + return multiGun; +} + +///___________________________________________________________ +/// Create generator via input file +FairGenerator* generateLFRapidity(std::string configuration = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/exotic_nuclei_pp.gun", + bool injectOnePDGPerEvent = true, + int gapBetweenInjection = 0, + bool useTrigger = false, + bool useRapidity = false, + std::string pythiaCfgMb = "", + std::string pythiaCfgSignal = "") +{ + configuration = gSystem->ExpandPathName(configuration.c_str()); + LOGF(info, "Using configuration file '%s'", configuration.c_str()); + std::ifstream inputFile(configuration.c_str(), ios::in); + std::vector cfgVec; + std::vector cfgVecGenDecayed; + if (!inputFile.is_open()) { + LOGF(fatal, "Can't open '%s' !", configuration.c_str()); + return nullptr; + } + if (TString(configuration.c_str()).EndsWith(".json")) { // read from JSON file + nlohmann::json paramfile = nlohmann::json::parse(inputFile); + std::cout << "paramfile " << paramfile << std::endl; + for (const auto& param : paramfile) { + std::cout << param << std::endl; + // cfgVecGenDecayed.push_back(GeneratorPythia8LFRapidity::ConfigContainer{paramfile[n].template get(), param}); + if (param["genDecayed"]) { + cfgVecGenDecayed.push_back(GeneratorPythia8LFRapidity::ConfigContainer{param, useRapidity}); + } else { + cfgVec.push_back(GeneratorPythia8LFRapidity::ConfigContainer{param, useRapidity}); + } + } + } else { + std::string l; + int n = 0; + while (getline(inputFile, l)) { + TString line = l; + line.Strip(TString::kBoth, ' '); + std::cout << n++ << " '" << line << "'" << endl; + if (line.IsNull() || line.IsWhitespace()) { + continue; + } + if (line.BeginsWith("#")) { + std::cout << "Skipping\n"; + continue; + } + if (line.Contains("genDecayed")) { + cfgVecGenDecayed.push_back(GeneratorPythia8LFRapidity::ConfigContainer{line}); + } else { + cfgVec.push_back(GeneratorPythia8LFRapidity::ConfigContainer{line}); + } + } + } + return generateLFRapidity(cfgVec, cfgVecGenDecayed, injectOnePDGPerEvent, gapBetweenInjection, useTrigger, useRapidity, pythiaCfgMb, pythiaCfgSignal); +} + +///___________________________________________________________ +/// Create generator via input file for the triggered mode +FairGenerator* generateLFRapidityTriggered(std::string configuration = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/exotic_nuclei_pp.gun", + int gapBetweenInjection = 0, + bool useRapidity = false, + std::string pythiaCfgMb = "", + std::string pythiaCfgSignal = "") +{ + return generateLFRapidity(configuration, /*injectOnePDGPerEvent=*/true, gapBetweenInjection, /*useTrigger=*/true, useRapidity, pythiaCfgMb, pythiaCfgSignal); +} + +///___________________________________________________________ +void generator_pythia8_LF_rapidity(bool testInj = true, bool testTrg = false, bool useRapidity = false, const char* particleListFile = "cfg_rapidity.json") +{ + LOG(info) << "Compiled correctly!"; + if (!testInj && !testTrg) { + return; + } + // Injected mode + if (testInj) { + LOG(info) << "Testing the injected mode"; + auto* gen = static_cast(generateLFRapidity(particleListFile, true, 0, false, useRapidity)); + gen->setVerbose(); + gen->Print(); + gen->print(); + gen->Init(); + gen->generateEvent(); + gen->importParticles(); + } + + // Triggered mode + if (testTrg) { + LOG(info) << "Testing the triggered mode"; + GeneratorPythia8LFRapidity* gen = static_cast(generateLFRapidityTriggered(particleListFile, + /*gapBetweenInjection=*/0, + useRapidity, + /*pythiaCfgMb=*/"inel136tev.cfg", + /*pythiaCfgSignal=*/"inel136tev.cfg")); + gen->setVerbose(); + gen->Print(); + gen->print(); + gen->Init(); + gen->generateEvent(); + gen->importParticles(); + } +} From 8879050e733eebb4c1f13513d02b442d53f26a09 Mon Sep 17 00:00:00 2001 From: rebeccacerri <116571396+rebeccacerri@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:28:16 +0200 Subject: [PATCH 012/144] Adding OO input shape (#2411) --- .../generator/GeneratorPromptCharmonia.C | 172 ++++++++++++++++++ ...ithInjectedPromptSignals_gaptriggered_dq.C | 3 + ...dPromptCharmoniaFwdy_TriggerGap_OO5TeV.ini | 2 +- 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/MC/config/PWGDQ/external/generator/GeneratorPromptCharmonia.C b/MC/config/PWGDQ/external/generator/GeneratorPromptCharmonia.C index 86a9fd47b..ad603c1e8 100644 --- a/MC/config/PWGDQ/external/generator/GeneratorPromptCharmonia.C +++ b/MC/config/PWGDQ/external/generator/GeneratorPromptCharmonia.C @@ -932,6 +932,152 @@ class O2_GeneratorParamPsipp5TeV : public GeneratorTGenerator GeneratorParam* paramPsi = nullptr; }; +class O2_GeneratorParamJpsiOO5TeV : public GeneratorTGenerator +{ + + public: + O2_GeneratorParamJpsiOO5TeV() : GeneratorTGenerator("ParamJpsi") + { + paramJpsi = new GeneratorParam(1, -1, PtJPsiOO5TeV, YJPsiOO5TeV, V2JPsiOO5TeV, IpJPsiOO5TeV); + paramJpsi->SetMomentumRange(0., 1.e6); + paramJpsi->SetPtRange(0, 999.); + paramJpsi->SetYRange(-4.2, -2.3); + paramJpsi->SetPhiRange(0., 360.); + paramJpsi->SetDecayer(new TPythia6Decayer()); + paramJpsi->SetForceDecay(kNoDecay); // particle left undecayed + // - - paramJpsi->SetTrackingFlag(1); // (from AliGenParam) -> check this + setTGenerator(paramJpsi); + }; + + ~O2_GeneratorParamJpsiOO5TeV() + { + delete paramJpsi; + }; + + Bool_t Init() override + { + GeneratorTGenerator::Init(); + paramJpsi->Init(); + return true; + } + + void SetNSignalPerEvent(Int_t nsig) { paramJpsi->SetNumberParticles(nsig); } + + //-------------------------------------------------------------------------// + static Double_t PtJPsiOO5TeV(const Double_t* px, const Double_t* /*dummy*/) + { + // jpsi pT in OO at 5.36 TeV, tuned on Data LHC25ae_pass2 + Double_t x = *px; + Float_t p0, p1, p2, p3; + p0 = 1; + p1 = 4.11443; + p2 = 1.92874; + p3 = 3.79588; + return p0 * x / TMath::Power(1. + TMath::Power(x / p1, p2), p3); + } + + //-------------------------------------------------------------------------// + static Double_t YJPsiOO5TeV(const Double_t* py, const Double_t* /*dummy*/) + { + // jpsi y in OO at 5.36 TeV, tuned on Data LHC25ae_pass2 + Double_t y = *py; + Float_t p0, p1, p2; + p0 = 1; + p1 = 2.12653e-05; + p2 = 3.28228; + return p0 * TMath::Exp(-(1. / 2.) * TMath::Power(((y - p1) / p2), 2)); + } + + //-------------------------------------------------------------------------// + static Double_t V2JPsiOO5TeV(const Double_t* /*dummy*/, const Double_t* /*dummy*/) + { + // jpsi v2 + return 0.; + } + + //-------------------------------------------------------------------------// + static Int_t IpJPsiOO5TeV(TRandom*) + { + return 443; + } + + private: + GeneratorParam* paramJpsi = nullptr; +}; + +class O2_GeneratorParamPsiOO5TeV : public GeneratorTGenerator +{ + + public: + O2_GeneratorParamPsiOO5TeV() : GeneratorTGenerator("ParamPsi") + { + paramPsi = new GeneratorParam(1, -1, PtPsiOO5TeV, YPsiOO5TeV, V2PsiOO5TeV, IpPsiOO5TeV); + paramPsi->SetMomentumRange(0., 1.e6); + paramPsi->SetPtRange(0, 999.); + paramPsi->SetYRange(-4.2, -2.3); + paramPsi->SetPhiRange(0., 360.); + paramPsi->SetDecayer(new TPythia6Decayer()); + paramPsi->SetForceDecay(kNoDecay); // particle left undecayed + // - - paramJpsi->SetTrackingFlag(1); // check this + setTGenerator(paramPsi); + }; + + ~O2_GeneratorParamPsiOO5TeV() + { + delete paramPsi; + }; + + Bool_t Init() override + { + GeneratorTGenerator::Init(); + paramPsi->Init(); + return true; + } + + void SetNSignalPerEvent(Int_t nsig) { paramPsi->SetNumberParticles(nsig); } + + //-------------------------------------------------------------------------// + static Double_t PtPsiOO5TeV(const Double_t* px, const Double_t* /*dummy*/) + { + // jpsi pT in OO at 5.36 TeV, tuned on Data LHC25ae_pass2 + Double_t x = *px; + Float_t p0, p1, p2, p3; + p0 = 1; + p1 = 4.11443; + p2 = 1.92874; + p3 = 3.79588; + return p0 * x / TMath::Power(1. + TMath::Power(x / p1, p2), p3); + } + + //-------------------------------------------------------------------------// + static Double_t YPsiOO5TeV(const Double_t* py, const Double_t* /*dummy*/) + { + // jpsi y in OO at 5.36 TeV, tuned on Data LHC25ae_pass2 + Double_t y = *py; // DA AGGIUNGERE + Float_t p0, p1, p2; + p0 = 1; + p1 = 2.12653e-05; + p2 = 3.28228; + return p0 * TMath::Exp(-(1. / 2.) * TMath::Power(((y - p1) / p2), 2)); + } + + //-------------------------------------------------------------------------// + static Double_t V2PsiOO5TeV(const Double_t* /*dummy*/, const Double_t* /*dummy*/) + { + // jpsi v2 + return 0.; + } + + //-------------------------------------------------------------------------// + static Int_t IpPsiOO5TeV(TRandom*) + { + return 100443; + } + + private: + GeneratorParam* paramPsi = nullptr; +}; + class O2_GeneratorParamJpsipp96TeV : public GeneratorTGenerator { @@ -1427,6 +1573,32 @@ FairGenerator* GeneratorCocktailPromptCharmoniaToMuonEvtGen_pp5TeV() return genCocktailEvtGen; } +FairGenerator* GeneratorCocktailPromptCharmoniaToMuonEvtGen_OO5TeV() +{ + + auto genCocktailEvtGen = new o2::eventgen::GeneratorEvtGen(); + + auto genJpsi = new o2::eventgen::O2_GeneratorParamJpsiOO5TeV; + genJpsi->SetNSignalPerEvent(1); // 1 J/psi generated per event by GeneratorParam + auto genPsi = new o2::eventgen::O2_GeneratorParamPsiOO5TeV; + genPsi->SetNSignalPerEvent(1); // 1 Psi(2S) generated per event by GeneratorParam + genCocktailEvtGen->AddGenerator(genJpsi, 1); // add J/psi generator + genCocktailEvtGen->AddGenerator(genPsi, 1); // add Psi(2S) generator + + TString pdgs = "443;100443"; + std::string spdg; + TObjArray* obj = pdgs.Tokenize(";"); + genCocktailEvtGen->SetSizePdg(obj->GetEntriesFast()); + for (int i = 0; i < obj->GetEntriesFast(); i++) { + spdg = obj->At(i)->GetName(); + genCocktailEvtGen->AddPdg(std::stoi(spdg), i); + printf("PDG %d \n", std::stoi(spdg)); + } + genCocktailEvtGen->SetForceDecay(kEvtDiMuon); + + return genCocktailEvtGen; +} + FairGenerator* GeneratorCocktailPromptCharmoniaToMuonEvtGen_pp96TeV() { diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedPromptSignals_gaptriggered_dq.C b/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedPromptSignals_gaptriggered_dq.C index 896bb7ed5..03281b391 100644 --- a/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedPromptSignals_gaptriggered_dq.C +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedPromptSignals_gaptriggered_dq.C @@ -66,6 +66,9 @@ public: case 14: // generate prompt charmonia cocktail at mid rapidity at 9.6TeV mGeneratorParam = (Generator*)GeneratorCocktailPromptCharmoniaToElectronEvtGen_pp96TeV(); break; + case 15: // generate prompt charmonium at forward rapidity at 5TeV for OO + mGeneratorParam = (Generator*)GeneratorCocktailPromptCharmoniaToMuonEvtGen_OO5TeV(); + break; } mGeneratorParam->Init(); } diff --git a/MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_OO5TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_OO5TeV.ini index 112685838..6f9e8fce4 100755 --- a/MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_OO5TeV.ini +++ b/MC/config/PWGDQ/ini/Generator_InjectedPromptCharmoniaFwdy_TriggerGap_OO5TeV.ini @@ -1,7 +1,7 @@ ### The external generator derives from GeneratorPythia8. [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedPromptSignals_gaptriggered_dq.C -funcName=GeneratorPythia8InjectedPromptCharmoniaGapTriggered(5,11) +funcName=GeneratorPythia8InjectedPromptCharmoniaGapTriggered(5,15) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_OO_536.cfg From c929fac0e1865cf927323bb0ebe65d2143a3495d Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Wed, 15 Jul 2026 11:09:50 +0200 Subject: [PATCH 013/144] Disable CTP lumi if not active --- MC/bin/o2dpg_sim_workflow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 89827daea..6aa16d1f3 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -1341,7 +1341,10 @@ def getDigiTaskName(det): anchor_corrmaplumi_mode = '--corrmap-lumi-mode ' + anchor_corrmaplumi_mode tpc_corr_scaling_options = anchor_lumi_type + ' ' + anchor_corrmaplumi_mode - + if not isActive('CTP'): + # CTP digits won't be produced for this timeframe (CTP not in the readout detector list) + tpc_corr_scaling_options += ' --disable-ctp-lumi-request' + # why not simply? # tpc_corr_scaling_options = ('--lumi-type 1', '')[tpcDistortionType != 0] From c7ca6a5b475e28ca4479f436e516a9b6d3fe8964 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 16 Jul 2026 20:01:18 +0200 Subject: [PATCH 014/144] anchorMC: Fail hard if modulecmd not found for 2-tag operations --- MC/run/ANCHOR/anchorMC.sh | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/MC/run/ANCHOR/anchorMC.sh b/MC/run/ANCHOR/anchorMC.sh index 9425f8dfb..8b814f05e 100755 --- a/MC/run/ANCHOR/anchorMC.sh +++ b/MC/run/ANCHOR/anchorMC.sh @@ -178,9 +178,38 @@ fi export > env_base.env +# The 2-tag / alternative-reco setup below uses 'module save/purge/restore' to stash +# and swap the software environment. If the shell provides no 'module' function, build +# one on a 'modulecmd' binary. Its location varies by version, so resolve it flexibly +# (was hardwired to /usr/bin/modulecmd) and verify it supports saved collections. Fail +# hard -- but only when an alternative-reco tag is set, since single-tag jobs never call +# 'module' -- instead of silently no-op'ing every 'module' call (cf. O2-7070). if ! declare -F module > /dev/null; then + MODULECMD_BIN="" + for _cand in "${MODULECMD}" "/usr/bin/modulecmd" "$(command -v modulecmd 2>/dev/null)" "${MODULESHOME:+${MODULESHOME}/libexec/modulecmd}"; do + if [ -n "${_cand}" ] && [ -x "${_cand}" ] && "${_cand}" bash savelist > /dev/null 2>&1; then + MODULECMD_BIN="${_cand}" + break + fi + done + if [ -n "${MODULECMD_BIN}" ]; then + echo_info "Using modulecmd at ${MODULECMD_BIN}" + elif [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then + echo_error "Alternative-reco (2-tag) requested (ALIEN_JDL_O2DPG_ASYNC_RECO_TAG=${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG})" + echo_error "but no 'modulecmd' supporting saved collections (save/restore) was found." + echo_error " Looked at: \$MODULECMD='${MODULECMD}', /usr/bin/modulecmd, PATH, \$MODULESHOME/libexec/modulecmd." + echo_error " Diagnostics: MODULESHOME='${MODULESHOME}' PATH='${PATH}'" + exit 1 + else + echo_info "No 'modulecmd' found, but no alternative-reco tag is set, so 'module' is not needed; continuing." + fi + export MODULECMD_BIN module() { - eval "$(/usr/bin/modulecmd bash "$@")"; + if [ -z "${MODULECMD_BIN}" ]; then + echo_error "'module ${*}' invoked but no usable 'modulecmd' was found (see earlier diagnostics)." + return 127 + fi + eval "$("${MODULECMD_BIN}" bash "$@")"; } export -f module fi From 6e8202b9232254330252cc172192a6d1e14d30c3 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 16 Jul 2026 20:45:28 +0200 Subject: [PATCH 015/144] anchorMC: resolve modulecmd via readlink -f + /usr/share/Modules fallback Extend the modulecmd candidate search with 'readlink -f /usr/bin/modulecmd' and the canonical environment-modules install (/usr/share/Modules/{,*/}libexec/ modulecmd*). On some HPC/container setups /usr/bin/modulecmd is a *dangling* symlink (-> /etc/alternatives -> the real .tcl) that exec's as "No such file or directory"; resolving the link / probing the real install recovers when the binary is still reachable, and otherwise still fails hard (a Modules tree missing from the job's mount namespace is a site-side problem). cf. O2-7070 (Perlmutter). Co-Authored-By: Claude Opus 4.8 --- MC/run/ANCHOR/anchorMC.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/MC/run/ANCHOR/anchorMC.sh b/MC/run/ANCHOR/anchorMC.sh index 8b814f05e..087b4870b 100755 --- a/MC/run/ANCHOR/anchorMC.sh +++ b/MC/run/ANCHOR/anchorMC.sh @@ -184,9 +184,18 @@ export > env_base.env # (was hardwired to /usr/bin/modulecmd) and verify it supports saved collections. Fail # hard -- but only when an alternative-reco tag is set, since single-tag jobs never call # 'module' -- instead of silently no-op'ing every 'module' call (cf. O2-7070). +# The candidate list also resolves symlinks ('readlink -f') and probes the canonical +# environment-modules install (/usr/share/Modules): on some HPC/container setups +# /usr/bin/modulecmd is a *dangling* symlink (-> /etc/alternatives -> the real .tcl), +# which exec's as "No such file or directory"; this recovers when the real binary is +# still reachable, and otherwise still fails hard (a genuinely missing Modules tree in +# the job's mount namespace is a site-side problem). if ! declare -F module > /dev/null; then MODULECMD_BIN="" - for _cand in "${MODULECMD}" "/usr/bin/modulecmd" "$(command -v modulecmd 2>/dev/null)" "${MODULESHOME:+${MODULESHOME}/libexec/modulecmd}"; do + for _cand in "${MODULECMD}" "/usr/bin/modulecmd" "$(command -v modulecmd 2>/dev/null)" \ + "${MODULESHOME:+${MODULESHOME}/libexec/modulecmd}" \ + "$(readlink -f /usr/bin/modulecmd 2>/dev/null)" \ + /usr/share/Modules/libexec/modulecmd* /usr/share/Modules/*/libexec/modulecmd*; do if [ -n "${_cand}" ] && [ -x "${_cand}" ] && "${_cand}" bash savelist > /dev/null 2>&1; then MODULECMD_BIN="${_cand}" break @@ -197,8 +206,9 @@ if ! declare -F module > /dev/null; then elif [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then echo_error "Alternative-reco (2-tag) requested (ALIEN_JDL_O2DPG_ASYNC_RECO_TAG=${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG})" echo_error "but no 'modulecmd' supporting saved collections (save/restore) was found." - echo_error " Looked at: \$MODULECMD='${MODULECMD}', /usr/bin/modulecmd, PATH, \$MODULESHOME/libexec/modulecmd." - echo_error " Diagnostics: MODULESHOME='${MODULESHOME}' PATH='${PATH}'" + echo_error " Looked at: \$MODULECMD='${MODULECMD}', /usr/bin/modulecmd (+readlink -f), PATH," + echo_error " \$MODULESHOME/libexec/modulecmd, /usr/share/Modules/{,*/}libexec/modulecmd*." + echo_error " Diagnostics: MODULESHOME='${MODULESHOME}' /usr/bin/modulecmd -> '$(readlink -f /usr/bin/modulecmd 2>/dev/null)' PATH='${PATH}'" exit 1 else echo_info "No 'modulecmd' found, but no alternative-reco tag is set, so 'module' is not needed; continuing." From 7e41835dd1576c987fd916584406dd70e167b3f7 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 25 Jun 2026 18:56:57 +0200 Subject: [PATCH 016/144] Add --no-qed to reliably disable QED background QED background is forced on for same-species (heavy-ion) collisions and had no way to turn off, which can be a problem in some expert/debugging scenarios. Add a --no-qed flag, mutually exclusive with --with-qed, that wins over the species default. Also drop the dead (doembedding and QED_enabled) term. Default behaviour is unchanged. --- MC/bin/o2dpg_sim_workflow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 6aa16d1f3..17a921cdd 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -83,7 +83,9 @@ parser.add_argument('-eB',help='Beam B energy', default=-1) parser.add_argument('-col',help='collision system: pp, PbPb, pPb, Pbp, ..., in case of embedding collision system of signal', default='pp') parser.add_argument('-field',help='L3 field rounded to kGauss, allowed values: +-2,+-5 and 0; +-5U for uniform field; or "ccdb" to take from conditions database', default='ccdb') -parser.add_argument('--with-qed',action='store_true', help='Enable QED background contribution (for PbPb always included)') +qed_group = parser.add_mutually_exclusive_group() +qed_group.add_argument('--with-qed', dest='with_qed', action='store_true', default=False, help='Force-enable QED background contribution (on by default for same-species heavy-ion collisions such as Pb-Pb)') +qed_group.add_argument('--no-qed', dest='no_qed', action='store_true', default=False, help='Force-disable QED background contribution, even for same-species heavy-ion collisions where it is enabled by default') parser.add_argument('-ptHatMin',help='pT hard minimum when no bin requested', default=0) parser.add_argument('-ptHatMax',help='pT hard maximum when no bin requested', default=-1) @@ -612,9 +614,11 @@ def getDPL_global_options(bigshm=False, ccdbbackend=True, runcommand=True): workflow['stages'].append(GRP_TASK) -# QED is enabled only for same beam species for now +# QED background is enabled by default only for same-species (heavy-ion) collisions. +# --with-qed forces it on; --no-qed forces it off and wins over both the default and --with-qed +# (the two flags are mutually exclusive at the argparse level, so they can never both be set). QED_enabled = True if (PDGA==PDGB and PDGA!=2212) else False -includeQED = (QED_enabled or (doembedding and QED_enabled)) or (args.with_qed == True) +includeQED = (QED_enabled or args.with_qed) and not args.no_qed signalprefix='sgn' # No vertexing for event pool generation; otherwise the vertex comes from CCDB and later from CollContext From 9d2776d2c0ece96eec18d961b392f80c113e7a5f Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 14 Jul 2026 09:52:59 +0200 Subject: [PATCH 017/144] Add specific EPOS4HQ parameters --- MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns | 4 ++++ MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns b/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns index 294a581ac..a8659654a 100644 --- a/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns +++ b/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns @@ -20,6 +20,10 @@ nodecays 110 20 2130 -2130 2230 -2230 1130 -1130 1330 -1330 2330 -2330 3331 -3331 end +!---EPOS4HQ specific changes--- +set hqnbcol 1 +set hqnbcolsaveEPOS 1 +------------------------------- set ninicon 1 !number of initial conditions used for hydro evolution core full !core/corona activated hydro hlle !hydro activated diff --git a/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns b/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns index ac4dacb34..b3d37d7e1 100644 --- a/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns +++ b/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns @@ -20,6 +20,10 @@ nodecays 110 20 2130 -2130 2230 -2230 1130 -1130 1330 -1330 2330 -2330 3331 -3331 end +!---EPOS4HQ specific changes--- +set hqnbcol 1 +set hqnbcolsaveEPOS 1 +------------------------------- set ninicon 1 !number of initial conditions used for hydro evolution core full !core/corona activated hydro hlle !hydro activated From fe572f3238ef69f705f08c0e7f4044cf1789be98 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Sat, 18 Jul 2026 14:48:58 +0400 Subject: [PATCH 018/144] By default tag invalidated tracks and/or residuals instead of rejecting them (#2410) Unless JDL variable ALIEN_JDL_TPCRESIDUAL_WRITEUNFILTERED != 1 and ALIEN_JDL_TPCRESIDUAL_KEEPREJECTEDRES != 1 respectively are defined --- DATA/production/configurations/asyncReco/setenv_extra.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index c414dc716..eb1f1bcd1 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -728,9 +728,14 @@ if [[ $ADD_CALIB == "1" ]]; then : ${ALIEN_JDL_TPCRESIDUALTRKSOURCESMAPEXTRACTION:="ITS-TPC"} : ${ALIEN_JDL_TPCRESIDUALMAXTRACKSPERSLOT:=-1} : ${ALIEN_JDL_TPCRESIDUALADDTRACKSMAP:=35000000} + : ${ALIEN_JDL_TPCRESIDUAL_WRITEUNFILTERED:=1} + : ${ALIEN_JDL_TPCRESIDUAL_KEEPREJECTEDRES:=1} export CALIB_TPC_SCDCALIB=1 export CALIB_TPC_SCDCALIB_SENDTRKDATA=1 - export CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=";scdcalib.maxTracksPerCalibSlot=$ALIEN_JDL_TPCRESIDUALMAXTRACKSPERSLOT;scdcalib.additionalTracksMap=$ALIEN_JDL_TPCRESIDUALADDTRACKSMAP;scdcalib.minPtNoOuterPoint=0.2;scdcalib.maxQ2Pt=5;scdcalib.minITSNClsNoOuterPoint=6;scdcalib.minITSNCls=4;scdcalib.minTPCNClsNoOuterPoint=50;scdcalib.minTOFTRDPVContributors=2" + export CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=";scdcalib.maxTracksPerCalibSlot=$ALIEN_JDL_TPCRESIDUALMAXTRACKSPERSLOT;scdcalib.additionalTracksMap=$ALIEN_JDL_TPCRESIDUALADDTRACKSMAP;scdcalib.minPtNoOuterPoint=0.2;scdcalib.maxQ2Pt=5;scdcalib.minITSNClsNoOuterPoint=6;scdcalib.minITSNCls=4;scdcalib.minTPCNClsNoOuterPoint=50;scdcalib.minTOFTRDPVContributors=2;" + CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=$([[ "$ALIEN_JDL_TPCRESIDUAL_WRITEUNFILTERED" == "1" ]] && printf "scdcalib.writeUnfiltered=true;" || printf "scdcalib.writeUnfiltered=false;") + CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=$([[ "$ALIEN_JDL_TPCRESIDUAL_KEEPREJECTEDRES" == "1" ]] && printf "scdcalib.keepRejectedResiduals=true;" || printf "scdcalib.keepRejectedResiduals=false;") + export ARGS_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=" --tracking-sources-map-extraction $ALIEN_JDL_TPCRESIDUALTRKSOURCESMAPEXTRACTION" if [[ ! $DO_TPC_RESIDUAL_EXTRACTION =~ "NOEXTCLROAD" ]]; then clusterErrors=";GPU_rec_tpc.clusterError2AdditionalY=0.3;GPU_rec_tpc.clusterError2AdditionalZ=0.3" From a92d70bc978bb88bb78868ac4c4ccd1ede34617e Mon Sep 17 00:00:00 2001 From: Daiki Sekihata Date: Wed, 22 Jul 2026 17:41:34 +0200 Subject: [PATCH 019/144] [MC/PWGEM] add HF and DY->dimuon MC in pp at 13.6 TeV (#2414) --- .../Generator_pythia8_GapTriggered_HFMuon.C | 224 ++++++++++++++++++ ...torDYmumu_GapTriggered_Gap1_pp13600GeV.ini | 7 + ...autyNoForcedDecay_Muon_pp13600GeV_Gap1.ini | 7 + ...apTriggered_Charm_Muon_pp13600GeV_Gap1.ini | 7 + ...ratorDYmumu_GapTriggered_Gap1_pp13600GeV.C | 51 ++++ ...BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.C | 113 +++++++++ ...FGapTriggered_Charm_Muon_pp13600GeV_Gap1.C | 117 +++++++++ ...bar_forceddecayscharmbeauty_semimuonic.cfg | 105 ++++++++ ...a8_pp_cr2_forceddecayscharm_semimuonic.cfg | 67 ++++++ .../PWGEM/runAnchoredDYmumu_pp_13600_Gap1.sh | 50 ++++ .../PWGEM/runAnchoredHFmumu_pp_13600_Gap1.sh | 57 +++++ 11 files changed, 805 insertions(+) create mode 100644 MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_HFMuon.C create mode 100644 MC/config/PWGEM/ini/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.ini create mode 100644 MC/config/PWGEM/ini/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.ini create mode 100644 MC/config/PWGEM/ini/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.ini create mode 100644 MC/config/PWGEM/ini/tests/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.C create mode 100644 MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.C create mode 100644 MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.C create mode 100644 MC/config/PWGEM/pythia8/generator/pythia8_bbbar_forceddecayscharmbeauty_semimuonic.cfg create mode 100644 MC/config/PWGEM/pythia8/generator/pythia8_pp_cr2_forceddecayscharm_semimuonic.cfg create mode 100644 MC/run/PWGEM/runAnchoredDYmumu_pp_13600_Gap1.sh create mode 100644 MC/run/PWGEM/runAnchoredHFmumu_pp_13600_Gap1.sh diff --git a/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_HFMuon.C b/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_HFMuon.C new file mode 100644 index 000000000..b84608346 --- /dev/null +++ b/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_HFMuon.C @@ -0,0 +1,224 @@ +#include "Pythia8/Pythia.h" +#include "Pythia8/HeavyIons.h" +#include "FairGenerator.h" +#include "FairPrimaryGenerator.h" +#include "Generators/GeneratorPythia8.h" +#include "TRandom3.h" +#include "TParticlePDG.h" +#include "TDatabasePDG.h" + +#include +#include +//#include // for std::pair + +using namespace Pythia8; + +class GeneratorPythia8GapTriggeredHFLepton : public o2::eventgen::GeneratorPythia8 +{ +public: + /// default constructor + GeneratorPythia8GapTriggeredHFLepton() = default; + + /// constructor + GeneratorPythia8GapTriggeredHFLepton(TString configsignal, int quarkPdg = 4, int lInputTriggerRatio = 5, int lInputExternalID = 0) + { + + lGeneratedEvents = 0; + lInverseTriggerRatio = lInputTriggerRatio; + lExternalID = lInputExternalID; + mQuarkPdg = quarkPdg; + + auto seed = (gRandom->TRandom::GetSeed() % 900000000); + + int offset = (int)(gRandom->Uniform(lInverseTriggerRatio)); // create offset to mitigate edge effects due to small number of events per job + lGeneratedEvents += offset; + + cout << "Initalizing extra PYTHIA object used to generate min-bias events..." << endl; + TString pathconfigMB = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg"); + pythiaObjectMinimumBias.readFile(pathconfigMB.Data()); + pythiaObjectMinimumBias.readString("Random:setSeed on"); + pythiaObjectMinimumBias.readString("Random:seed " + std::to_string(seed)); + pythiaObjectMinimumBias.init(); + cout << "Initalization complete" << endl; + cout << "Initalizing extra PYTHIA object used to generate signal events..." << endl; + TString pathconfigSignal = gSystem->ExpandPathName(configsignal.Data()); + pythiaObjectSignal.readFile(pathconfigSignal.Data()); + pythiaObjectSignal.readString("Random:setSeed on"); + pythiaObjectSignal.readString("Random:seed " + std::to_string(seed)); + pythiaObjectSignal.init(); + cout << "Initalization complete" << endl; + // flag the generators using type + // addCocktailConstituent(type, "interesting"); + // addCocktailConstitent(0, "minbias"); + // Add Sub generators + addSubGenerator(0, "default generator"); + addSubGenerator(1, "charm lepton"); + addSubGenerator(2, "beauty forced decay"); + addSubGenerator(3, "beauty no foced decay"); + } + + /// Destructor + ~GeneratorPythia8GapTriggeredHFLepton() = default; + + void addTriggerOnDaughter(int nb, int pdg) + { + mNbDaughter = nb; + mPdgDaughter = pdg; + }; + void setQuarkRapidity(float yMin, float yMax) + { + mQuarkRapidityMin = yMin; + mQuarkRapidityMax = yMax; + }; + void setDaughterRapidity(float yMin, float yMax) + { + mDaughterRapidityMin = yMin; + mDaughterRapidityMax = yMax; + }; + +protected: + //__________________________________________________________________ + Bool_t generateEvent() override + { + /// reset event + mPythia.event.reset(); + + // Simple straightforward check to alternate generators + if (lGeneratedEvents % lInverseTriggerRatio == 0) { + // Generate event of interest + Bool_t lGenerationOK = kFALSE; + while (!lGenerationOK) { + if (pythiaObjectSignal.next()) { + lGenerationOK = selectEvent(pythiaObjectSignal.event); + } + } + mPythia.event = pythiaObjectSignal.event; + notifySubGenerator(lExternalID); + } else { + // Generate minimum-bias event + Bool_t lGenerationOK = kFALSE; + while (!lGenerationOK) { + lGenerationOK = pythiaObjectMinimumBias.next(); + } + mPythia.event = pythiaObjectMinimumBias.event; + notifySubGenerator(0); + } + + lGeneratedEvents++; + // mPythia.next(); + + return true; + } + + bool selectEvent(const Pythia8::Event& event) + { + bool isGoodAtPartonLevel = false, isGoodAtDaughterLevel = (mPdgDaughter != 0) ? false : true; + int nbDaughter = 0; + for (auto iPart{0}; iPart < event.size(); ++iPart) { + // search for Q-Qbar mother with at least one Q in rapidity window + if (!isGoodAtPartonLevel) { + auto daughterList = event[iPart].daughterList(); + bool hasQ = false, hasQbar = false, atSelectedY = false; + for (auto iDau : daughterList) { + if (event[iDau].id() == mQuarkPdg) { + hasQ = true; + } + if (event[iDau].id() == -mQuarkPdg) { + hasQbar = true; + } + if ((std::abs(event[iDau].id()) == mQuarkPdg) && (event[iDau].y() > mQuarkRapidityMin) && (event[iDau].y() < mQuarkRapidityMax)) + atSelectedY = true; + } + if (hasQ && hasQbar && atSelectedY) { + isGoodAtPartonLevel = true; + } + } + // search for mNbDaughter daughters of type mPdgDaughter in rapidity window + if (!isGoodAtDaughterLevel) { + int id = std::abs(event[iPart].id()); + float rap = event[iPart].y(); + if (id == mPdgDaughter) { + int motherindexa = event[iPart].mother1(); + if (motherindexa > 0) { + int idmother = std::abs(event[motherindexa].id()); + if (int(std::abs(idmother) / 100.) == 4 || int(std::abs(idmother) / 1000.) == 4 || int(std::abs(idmother) / 100.) == 5 || int(std::abs(idmother) / 1000.) == 5) { + if (rap > mDaughterRapidityMin && rap < mDaughterRapidityMax) { + nbDaughter++; + if (nbDaughter >= mNbDaughter) isGoodAtDaughterLevel = true; + } + } + } + } + } + // we send the trigger + if (isGoodAtPartonLevel && isGoodAtDaughterLevel) { + return true; + } + } + return false; + }; + +private: + // Interface to override import particles + Pythia8::Event mOutputEvent; + + // Properties of selection + int mQuarkPdg; + float mQuarkRapidityMin; + float mQuarkRapidityMax; + int mPdgDaughter; + int mNbDaughter; + float mDaughterRapidityMin; + float mDaughterRapidityMax; + + // Control gap-triggering + Long64_t lGeneratedEvents; + int lInverseTriggerRatio; + // ID for different generators + int lExternalID; + + // Base event generators + Pythia8::Pythia pythiaObjectMinimumBias; ///Minimum bias collision generator + Pythia8::Pythia pythiaObjectSignal; ///Signal collision generator +}; + +// Predefined generators: + +// Charm-enriched forced decay +FairGenerator* GeneratorPythia8GapTriggeredCharmLepton(int inputTriggerRatio, int inputExternalID, float yMin = -1.5, float yMax = 1.5) +{ + auto myGen = new GeneratorPythia8GapTriggeredHFLepton("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_pp_cr2_forceddecayscharm_semimuonic.cfg", 4, inputTriggerRatio, inputExternalID); + auto seed = (gRandom->TRandom::GetSeed() % 900000000); + myGen->readString("Random:setSeed on"); + myGen->readString("Random:seed " + std::to_string(seed)); + myGen->setQuarkRapidity(yMin, yMax); + myGen->addTriggerOnDaughter(2, 13); + myGen->setDaughterRapidity(-6., -1.); + return myGen; +} + +// Beauty-enriched forced decay +FairGenerator* GeneratorPythia8GapTriggeredBeautyForcedDecays(int inputTriggerRatio, int inputExternalID, float yMin = -1.5, float yMax = 1.5) +{ + auto myGen = new GeneratorPythia8GapTriggeredHFLepton("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_bbbar_forceddecayscharmbeauty_semimuonic.cfg", 5, inputTriggerRatio, inputExternalID); + auto seed = (gRandom->TRandom::GetSeed() % 900000000); + myGen->readString("Random:setSeed on"); + myGen->readString("Random:seed " + std::to_string(seed)); + myGen->setQuarkRapidity(yMin, yMax); + myGen->addTriggerOnDaughter(2, 13); + myGen->setDaughterRapidity(-6., -1.); + return myGen; +} + +// Beauty-enriched no forced decay +FairGenerator* GeneratorPythia8GapTriggeredBeautyNoForcedDecays(int inputTriggerRatio, int inputExternalID, float yMin = -1.5, float yMax = 1.5) +{ + auto myGen = new GeneratorPythia8GapTriggeredHFLepton("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_bbbar.cfg", 5, inputTriggerRatio, inputExternalID); + auto seed = (gRandom->TRandom::GetSeed() % 900000000); + myGen->readString("Random:setSeed on"); + myGen->readString("Random:seed " + std::to_string(seed)); + myGen->setQuarkRapidity(yMin, yMax); + myGen->addTriggerOnDaughter(2, 13); + myGen->setDaughterRapidity(-6., -1.); + return myGen; +} diff --git a/MC/config/PWGEM/ini/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.ini b/MC/config/PWGEM/ini/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.ini new file mode 100644 index 000000000..101e8ce12 --- /dev/null +++ b/MC/config/PWGEM/ini/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_DYll.C +funcName = GeneratorPythia8GapTriggeredDYll(1, 1, 13, -6, -1, 2212, 2212, 13600.0) + +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/configPythiaEmpty.cfg +includePartonEvent = true diff --git a/MC/config/PWGEM/ini/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.ini b/MC/config/PWGEM/ini/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.ini new file mode 100644 index 000000000..0641b5724 --- /dev/null +++ b/MC/config/PWGEM/ini/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_HFMuon.C +funcName = GeneratorPythia8GapTriggeredBeautyNoForcedDecays(1, 3, -6, -1) + +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/configPythiaEmpty.cfg +includePartonEvent = true diff --git a/MC/config/PWGEM/ini/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.ini b/MC/config/PWGEM/ini/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.ini new file mode 100644 index 000000000..302abe087 --- /dev/null +++ b/MC/config/PWGEM/ini/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_HFMuon.C +funcName = GeneratorPythia8GapTriggeredCharmLepton(1, 1, -6, -1) + +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/configPythiaEmpty.cfg +includePartonEvent = true diff --git a/MC/config/PWGEM/ini/tests/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.C b/MC/config/PWGEM/ini/tests/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.C new file mode 100644 index 000000000..0332e54fc --- /dev/null +++ b/MC/config/PWGEM/ini/tests/GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.C @@ -0,0 +1,51 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + // check if each event has at least two oxygen ions + for (int i = 0; i < nEvents; i++) + { + auto check = tree->GetEntry(i); + int count = 0; + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) + { + auto track = tracks->at(idxMCTrack); + if (track.GetPdgCode() == 2212) + { + count++; + } + } + if (count < 2) + { + std::cerr << "Event " << i << " has less than 2 oxygen ions\n"; + return 1; + } + } + + return 0; +} diff --git a/MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.C b/MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.C new file mode 100644 index 000000000..99e697a46 --- /dev/null +++ b/MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.C @@ -0,0 +1,113 @@ +int External() +{ + + int checkPdgDecay = -13; + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + float ratioTrigger = 1./1; // one event triggered out of 1 + auto tree = (TTree*)file.Get("o2sim"); + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptonsInAcceptance{}; + int nLeptons{}; + int nAntileptons{}; + int nLeptonsToBeDone{}; + int nAntileptonsToBeDone{}; + int nSignalPairs{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + int nElectrons = 0; + int nPositrons = 0; + int nElectronsToBeDone = 0; + int nPositronsToBeDone = 0; + int nOpenBeautyPos = 0; + int nOpenBeautyNeg = 0; + int nPositronsElectronsInAcceptance = 0; + for (auto& track : *tracks) { + auto pdg = track.GetPdgCode(); + auto y = track.GetRapidity(); + if (pdg == checkPdgDecay) { + int igmother = track.getMotherTrackId(); + if (igmother > 0) { + auto gmTrack = (*tracks)[igmother]; + int gmpdg = gmTrack.GetPdgCode(); + if (int(std::abs(gmpdg)/100.) == 5 || int(std::abs(gmpdg)/1000.) == 5 || int(std::abs(gmpdg)/100.) == 4 || int(std::abs(gmpdg)/1000.) == 4) { + nLeptons++; + nElectrons++; + if (-1 < y && y < 1) nPositronsElectronsInAcceptance++; + if (track.getToBeDone()) { + nLeptonsToBeDone++; + nElectronsToBeDone++; + } + } + } + } else if (pdg == -checkPdgDecay) { + int igmother = track.getMotherTrackId(); + if (igmother > 0) { + auto gmTrack = (*tracks)[igmother]; + int gmpdg = gmTrack.GetPdgCode(); + if (int(TMath::Abs(gmpdg)/100.) == 4 || int(TMath::Abs(gmpdg)/1000.) == 4 || int(std::abs(gmpdg)/100.) == 5 || int(std::abs(gmpdg)/1000.) == 5) { + nAntileptons++; + nPositrons++; + if (-1 < y && y < 1) nPositronsElectronsInAcceptance++; + if (track.getToBeDone()) { + nAntileptonsToBeDone++; + nPositronsToBeDone++; + } + } + } + } else if (pdg == 511 || pdg == 521 || pdg == 531 || pdg == 5122 || pdg == 5132 || pdg == 5232 || pdg == 5332) { + nOpenBeautyPos++; + } else if (pdg == -511 || pdg == -521 || pdg == -531 || pdg == -5122 || pdg == -5132 || pdg == -5232 || pdg == -5332) { + nOpenBeautyNeg++; + } + } + if (nOpenBeautyPos > 0 && nOpenBeautyNeg > 0) { + nSignalPairs++; + } + if (nPositronsElectronsInAcceptance > 1) { + nLeptonsInAcceptance++; + } + if (nElectrons > 0 && nPositrons > 0) { + nLeptonPairs++; + } + if (nElectronsToBeDone > 0 && nPositronsToBeDone > 0) nLeptonPairsToBeDone++; + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#leptons to be done: " << nLeptonsToBeDone << "\n" + << "#antileptons to be done: " << nAntileptonsToBeDone << "\n" + << "#Open-beauty hadron pairs: " << nSignalPairs << "\n" + << "#leptons in acceptance: " << nLeptonsInAcceptance << "\n" + << "#Electron-positron pairs: " << nLeptonPairs << "\n" + << "#Electron-positron pairs to be done: " << nLeptonPairsToBeDone << "\n"; + if (nLeptons == 0 && nAntileptons == 0) { + std::cerr << "Number of leptons, number of anti-leptons should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) { + std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; + return 1; + } + if (nLeptons != nLeptonsToBeDone) { + std::cerr << "The number of leptons should be the same as the number of leptons which should be transported.\n"; + return 1; + } + if (nLeptonsInAcceptance == (nEvents/ratioTrigger)) { + std::cerr << "The number of leptons in acceptance should be at least equaled to the number of events.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.C b/MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.C new file mode 100644 index 000000000..41ee27f54 --- /dev/null +++ b/MC/config/PWGEM/ini/tests/GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.C @@ -0,0 +1,117 @@ +int External() +{ + + int checkPdgDecay = -13; + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + float ratioTrigger = 1./1.; // one event triggered out of 1 + auto tree = (TTree*)file.Get("o2sim"); + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptonsInAcceptance{}; + int nLeptons{}; + int nAntileptons{}; + int nLeptonsToBeDone{}; + int nAntileptonsToBeDone{}; + int nSignalPairs{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + int nElectrons = 0; + int nPositrons = 0; + int nElectronsToBeDone = 0; + int nPositronsToBeDone = 0; + int nOpenCharmPos = 0; + int nOpenCharmNeg = 0; + int nPositronsElectronsInAcceptance = 0; + for (auto& track : *tracks) { + auto pdg = track.GetPdgCode(); + auto y = track.GetRapidity(); + if (pdg == checkPdgDecay) { + int igmother = track.getMotherTrackId(); + if (igmother > 0) { + auto gmTrack = (*tracks)[igmother]; + int gmpdg = gmTrack.GetPdgCode(); + if (int(std::abs(gmpdg)/100.) == 4 || int(std::abs(gmpdg)/1000.) == 4) { + nLeptons++; + nElectrons++; + if (-1 < y && y < 1) nPositronsElectronsInAcceptance++; + if (track.getToBeDone()) { + nLeptonsToBeDone++; + nElectronsToBeDone++; + } + } + } + } else if (pdg == -checkPdgDecay) { + int igmother = track.getMotherTrackId(); + if (igmother > 0) { + auto gmTrack = (*tracks)[igmother]; + int gmpdg = gmTrack.GetPdgCode(); + if (int(TMath::Abs(gmpdg)/100.) == 4 || int(TMath::Abs(gmpdg)/1000.) == 4) { + nAntileptons++; + nPositrons++; + if (-1 < y && y < 1) nPositronsElectronsInAcceptance++; + if (track.getToBeDone()) { + nAntileptonsToBeDone++; + nPositronsToBeDone++; + } + } + } + } else if (pdg == 411 || pdg == 421 || pdg == 431 || pdg == 4122 || pdg == 4132 || pdg == 4232 || pdg == 4332) { + nOpenCharmPos++; + } else if (pdg == -411 || pdg == -421 || pdg == -431 || pdg == -4122 || pdg == -4132 || pdg == -4232 || pdg == -4332) { + nOpenCharmNeg++; + } + } + if (nOpenCharmPos > 0 && nOpenCharmNeg > 0) { + nSignalPairs++; + } + if (nPositronsElectronsInAcceptance > 1) { + nLeptonsInAcceptance++; + } + if (nElectrons > 0 && nPositrons > 0) { + nLeptonPairs++; + } + if (nElectronsToBeDone > 0 && nPositronsToBeDone > 0) nLeptonPairsToBeDone++; + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#leptons to be done: " << nLeptonsToBeDone << "\n" + << "#antileptons to be done: " << nAntileptonsToBeDone << "\n" + << "#open-charm hadron pairs: " << nSignalPairs << "\n" + << "#leptons in acceptance: " << nLeptonsInAcceptance << "\n" + << "#Electron-positron pairs: " << nLeptonPairs << "\n" + << "#Electron-positron pairs to be done: " << nLeptonPairsToBeDone << "\n"; + if (nLeptons == 0 && nAntileptons == 0) { + std::cerr << "Number of leptons, number of anti-leptons should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) { + std::cerr << "The number of electron-positron pairs should be the same as the number of electron-positron pairs which should be transported.\n"; + return 1; + } + if (nLeptons != nLeptonsToBeDone) { + std::cerr << "The number of leptons should be the same as the number of leptons which should be transported.\n"; + return 1; + } + if (nLeptonsInAcceptance == (nEvents/ratioTrigger)) { + std::cerr << "The number of leptons in acceptance should be at least equaled to the number of events.\n"; + return 1; + } + if (nLeptonPairs < nLeptonsInAcceptance) { + std::cerr << "The number of positron-electron pairs should be at least equaled to the number of leptons in acceptance.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGEM/pythia8/generator/pythia8_bbbar_forceddecayscharmbeauty_semimuonic.cfg b/MC/config/PWGEM/pythia8/generator/pythia8_bbbar_forceddecayscharmbeauty_semimuonic.cfg new file mode 100644 index 000000000..7dd200525 --- /dev/null +++ b/MC/config/PWGEM/pythia8/generator/pythia8_bbbar_forceddecayscharmbeauty_semimuonic.cfg @@ -0,0 +1,105 @@ +### beams +Beams:idA 2212 # proton +Beams:idB 2212 # proton +Beams:eCM 13600. # GeV + +### processes +#HardQCD:hardccbar on # scatterings g-g / q-qbar -> c-cbar +HardQCD:hardbbbar on # scatterings g-g / q-qbar -> b-bbar + +### decays +ParticleDecays:limitTau0 on +ParticleDecays:tau0Max 10. + +### switch on color reconnection in mode 2 (https://arxiv.org/pdf/1505.01681.pdf) +Tune:pp = 14 +ColourReconnection:mode = 1 +ColourReconnection:allowDoubleJunRem = off +ColourReconnection:m0 = 0.3 +ColourReconnection:allowJunctions = on +ColourReconnection:junctionCorrection = 1.20 +ColourReconnection:timeDilationMode = 2 +ColourReconnection:timeDilationPar = 0.18 +StringPT:sigma = 0.335 +StringZ:aLund = 0.36 +StringZ:bLund = 0.56 +StringFlav:probQQtoQ = 0.078 +StringFlav:ProbStoUD = 0.2 +StringFlav:probQQ1toQQ0join = 0.0275,0.0275,0.0275,0.0275 +MultiPartonInteractions:pT0Ref = 2.15 +BeamRemnants:remnantMode = 1 +BeamRemnants:saturation =5 + +### only semileptonic decays for charm +### D+ +411:oneChannel = 1 0.0868 0 -311 -13 14 +411:addChannel = 1 0.0527 0 -313 -13 14 +411:addChannel = 1 0.0019 0 -321 211 -13 14 +### D0 +421:oneChannel = 1 0.03418 0 -321 -13 14 +421:addChannel = 1 0.02060 0 -323 -13 14 +421:addChannel = 1 0.000421 0 -321 111 -13 14 +421:addChannel = 1 0.00267 0 -211 -13 14 +421:addChannel = 1 0.00145 0 -211 111 -13 14 +### Ds +431:oneChannel = 1 0.0224 0 333 -13 14 +431:addChannel = 1 0.0224 0 221 -13 14 +431:addChannel = 1 0.0080 0 331 -13 14 + +### Lambdac +4122:oneChannel = 1 0.036 0 3122 -13 14 +### Xi_{c}^{+} +4232:oneChannel = 1 0.07 0 3322 -13 14 +### Xi_{c}^{0} +4132:oneChannel = 1 0.014 0 3312 -13 14 +### Omega_{c} +4332:oneChannel = 1 0.01224 0 3334 -13 14 + + +### only semileptonic decays for beauty +### B0 +511:oneChannel = 1 0.0212000 0 -411 -13 14 +511:addChannel = 1 0.0490000 0 -413 -13 14 +511:addChannel = 1 0.0028500 0 -10413 -13 14 +511:addChannel = 1 0.0025000 0 -20413 -13 14 +511:addChannel = 1 0.0014100 0 -415 -13 14 +511:addChannel = 1 0.0001500 0 -211 -13 14 +511:addChannel = 1 0.0002940 0 -213 -13 14 + +### B+ +521:oneChannel = 1 0.022100 0 -421 -13 14 +521:addChannel = 1 0.055300 0 -423 -13 14 +521:addChannel = 1 0.000900 0 -10421 -13 14 +521:addChannel = 1 0.002840 0 -10423 -13 14 +521:addChannel = 1 0.001700 0 -20423 -13 14 +521:addChannel = 1 0.001590 0 -425 -13 14 +521:addChannel = 1 0.000300 0 -431 321 -13 14 +521:addChannel = 1 0.000290 0 -433 321 -13 14 +521:addChannel = 1 0.000078 0 111 -13 14 +521:addChannel = 1 0.000035 0 221 -13 14 +521:addChannel = 1 0.000024 0 331 -13 14 +521:addChannel = 1 0.000119 0 223 -13 14 +521:addChannel = 1 0.000158 0 113 -13 14 + +### Bs +531:oneChannel = 1 0.023100 0 -431 -13 14 +531:addChannel = 1 0.052000 0 -433 -13 14 +531:addChannel = 1 0.000106 0 -321 -13 14 + +### Lambdab +5122:oneChannel = 1 0.06200 0 4122 13 -14 +5122:addChannel = 1 0.00790 0 14122 13 -14 +5122:addChannel = 1 0.01300 0 4124 13 -14 + +# Forced exclusive decay; BR=1 is not a physical branching fraction +### Xi_{b}^{-} +5132:oneChannel = 1 1.0 0 4132 13 -14 +### Xi_{b}^{0} +5232:oneChannel = 1 1.0 0 4232 13 -14 +### Omega_{b}^{-} +5332:oneChannel = 1 1.0 0 4332 13 -14 + +# Correct OmegaC decay length (wrong in PYTHIA8 decay table) (mm/c) +4332:tau0 = 0.08000000000 +# Correct Lb decay length (wrong in PYTHIA8 decay table) +5122:tau0 = 4.41000e-01 diff --git a/MC/config/PWGEM/pythia8/generator/pythia8_pp_cr2_forceddecayscharm_semimuonic.cfg b/MC/config/PWGEM/pythia8/generator/pythia8_pp_cr2_forceddecayscharm_semimuonic.cfg new file mode 100644 index 000000000..330b31a23 --- /dev/null +++ b/MC/config/PWGEM/pythia8/generator/pythia8_pp_cr2_forceddecayscharm_semimuonic.cfg @@ -0,0 +1,67 @@ +### main + +Main:timesAllowErrors 2000 +#allow more errors in the pythia. + + +### beams +Beams:idA 2212 # proton +Beams:idB 2212 # proton +Beams:eCM 13600 # GeV + +### processes +# HardQCD:hardccbar on # ccbar production +SoftQCD:inelastic = on + +### decays +ParticleDecays:limitTau0 on +ParticleDecays:tau0Max 10. + +### switch on color reconnection in mode 2 (https://arxiv.org/pdf/1505.01681.pdf) +Tune:pp = 14 +ColourReconnection:mode = 1 +ColourReconnection:allowDoubleJunRem = off +ColourReconnection:m0 = 0.3 +ColourReconnection:allowJunctions = on +ColourReconnection:junctionCorrection = 1.20 +ColourReconnection:timeDilationMode = 2 +ColourReconnection:timeDilationPar = 0.18 +StringPT:sigma = 0.335 +StringZ:aLund = 0.36 +StringZ:bLund = 0.56 +StringFlav:probQQtoQ = 0.078 +StringFlav:ProbStoUD = 0.2 +StringFlav:probQQ1toQQ0join = 0.0275,0.0275,0.0275,0.0275 +MultiPartonInteractions:pT0Ref = 2.15 +BeamRemnants:remnantMode = 1 +BeamRemnants:saturation =5 + +### only semileptonic decays +### D+ +411:oneChannel = 1 0.0868 0 -311 -13 14 +411:addChannel = 1 0.0527 0 -313 -13 14 +411:addChannel = 1 0.0019 0 -321 211 -13 14 +### D0 +421:oneChannel = 1 0.03418 0 -321 -13 14 +421:addChannel = 1 0.02060 0 -323 -13 14 +421:addChannel = 1 0.000421 0 -321 111 -13 14 +421:addChannel = 1 0.00267 0 -211 -13 14 +421:addChannel = 1 0.00145 0 -211 111 -13 14 +### Ds +431:oneChannel = 1 0.0224 0 333 -13 14 +431:addChannel = 1 0.0224 0 221 -13 14 +431:addChannel = 1 0.0080 0 331 -13 14 + +### Lambdac +4122:oneChannel = 1 0.036 0 3122 -13 14 +### chi_{c}^{+} +4232:oneChannel = 1 0.07 0 3322 -13 14 +### chi_{c}^{0} +4132:oneChannel = 1 0.014 0 3312 -13 14 +### Omega_{c} +4332:oneChannel = 1 0.01224 0 3334 -13 14 + +# Correct OmegaC decay length (wrong in PYTHIA8 decay table) (mm/c) +4332:tau0 = 0.08000000000 +# Correct Lb decay length (wrong in PYTHIA8 decay table) +5122:tau0 = 4.41000e-01 diff --git a/MC/run/PWGEM/runAnchoredDYmumu_pp_13600_Gap1.sh b/MC/run/PWGEM/runAnchoredDYmumu_pp_13600_Gap1.sh new file mode 100644 index 000000000..86e411252 --- /dev/null +++ b/MC/run/PWGEM/runAnchoredDYmumu_pp_13600_Gap1.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# +# Steering script for DY->dimuon MC anchored to LHC26 pp apass1 +# + +# example anchoring +# taken from https://its.cern.ch/jira/browse/O2-5670 +export ALIEN_JDL_LPMANCHORPASSNAME=apass1 +export ALIEN_JDL_MCANCHOR=apass1 +export ALIEN_JDL_CPULIMIT=8 +export ALIEN_JDL_LPMRUNNUMBER=559348 +export ALIEN_JDL_LPMPRODUCTIONTYPE=MC +export ALIEN_JDL_LPMINTERACTIONTYPE=pp +export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24a6 +export ALIEN_JDL_LPMANCHORRUN=559348 +export ALIEN_JDL_LPMANCHORPRODUCTION=LHC26ac +export ALIEN_JDL_LPMANCHORYEAR=2026 +export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 + +export NTIMEFRAMES=1 +export NSIGEVENTS=20 +export SPLITID=100 +export PRODSPLIT=153 +export CYCLE=0 + +# on the GRID, this is set and used as seed; when set, it takes precedence over SEED +#export ALIEN_PROC_ID=2963436952 +export SEED=0 + +# for pp and 50 events per TF, we launch only 4 workers. +export NWORKERS=2 + +# define the generator via ini file +# use 20/40/40 sampling for different generators +# generate random number +#RNDSIG=$(($RANDOM % 100)) + +CONFIGNAME="GeneratorDYmumu_GapTriggered_Gap1_pp13600GeV.ini" + +export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" + +# run the central anchor steering script; this includes +# * derive timestamp +# * derive interaction rate +# * extract and prepare configurations (which detectors are contained in the run etc.) +# * run the simulation (and QC) +# To disable QC, uncomment the following line +#export DISABLE_QC=1 +${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runAnchoredHFmumu_pp_13600_Gap1.sh b/MC/run/PWGEM/runAnchoredHFmumu_pp_13600_Gap1.sh new file mode 100644 index 000000000..6e60d1815 --- /dev/null +++ b/MC/run/PWGEM/runAnchoredHFmumu_pp_13600_Gap1.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# +# Steering script for HF enhanced dielectron MC anchored to LHC26 pp apass1 +# + +# example anchoring +# taken from https://its.cern.ch/jira/browse/O2-5670 +export ALIEN_JDL_LPMANCHORPASSNAME=apass1 +export ALIEN_JDL_MCANCHOR=apass1 +export ALIEN_JDL_CPULIMIT=8 +export ALIEN_JDL_LPMRUNNUMBER=559348 +export ALIEN_JDL_LPMPRODUCTIONTYPE=MC +export ALIEN_JDL_LPMINTERACTIONTYPE=pp +export ALIEN_JDL_LPMPRODUCTIONTAG=LHC26e9 +export ALIEN_JDL_LPMANCHORRUN=559348 +export ALIEN_JDL_LPMANCHORPRODUCTION=LHC26ac +export ALIEN_JDL_LPMANCHORYEAR=2024 +export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 + +export NTIMEFRAMES=1 +export NSIGEVENTS=20 +export SPLITID=100 +export PRODSPLIT=153 +export CYCLE=0 + +# on the GRID, this is set and used as seed; when set, it takes precedence over SEED +#export ALIEN_PROC_ID=2963436952 +export SEED=0 + +# for pp and 50 events per TF, we launch only 4 workers. +export NWORKERS=2 + +# define the generator via ini file +# use 20/40/40 sampling for different generators +# generate random number +RNDSIG=$(($RANDOM % 100)) + + +if [[ $RNDSIG -ge 0 && $RNDSIG -lt 20 ]]; +then + CONFIGNAME="GeneratorHFGapTriggered_Charm_Muon_pp13600GeV_Gap1.ini" +elif [[ $RNDSIG -ge 20 && $RNDSIG -lt 100 ]]; +then + CONFIGNAME="GeneratorHFGapTriggered_BeautyNoForcedDecay_Muon_pp13600GeV_Gap1.ini" +fi + +export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" + +# run the central anchor steering script; this includes +# * derive timestamp +# * derive interaction rate +# * extract and prepare configurations (which detectors are contained in the run etc.) +# * run the simulation (and QC) +# To disable QC, uncomment the following line +#export DISABLE_QC=1 +${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh From bd6ef72939efd4bb52bf4a46a1eeb02128c5e67f Mon Sep 17 00:00:00 2001 From: shahor02 Date: Thu, 23 Jul 2026 17:12:48 +0400 Subject: [PATCH 020/144] Disable V0-finder in ITSTracks QC (#2413) Its results are not used and it may explode processing time if mean vertex position is wrong --- DATA/production/qc-async/its.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DATA/production/qc-async/its.json b/DATA/production/qc-async/its.json index be552977f..4e1280af2 100644 --- a/DATA/production/qc-async/its.json +++ b/DATA/production/qc-async/its.json @@ -84,7 +84,7 @@ "nBCbins" : "103", "dicttimestamp" : 0, "doNorm": "1", - "InvMasses" : "1", + "InvMasses" : "0", "publishMore": "0" } } From f3990a8238b6e6e4befb406b09c782e31596ca40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Jacazio?= Date: Thu, 23 Jul 2026 18:44:18 +0200 Subject: [PATCH 021/144] Add pp 13.6 cfg with tuned diffractive events (#2415) * Add pp 13.6 cfg with tuned diffractive events for OTF sim * Add test for pp 13.6 cfg with tuned diffractive events for OTF sim --- .../ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini | 9 +++++++++ .../ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C | 1 + 2 files changed, 10 insertions(+) create mode 100644 MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini create mode 120000 MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C diff --git a/MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini b/MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini new file mode 100644 index 000000000..f1b57ecf1 --- /dev/null +++ b/MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini @@ -0,0 +1,9 @@ +[Diamond] +width[2]=6.0 + +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_pythia8_ALICE3.C +funcName=generator_pythia8_ALICE3() + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_inel_difftuned.cfg diff --git a/MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C b/MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C new file mode 120000 index 000000000..860755581 --- /dev/null +++ b/MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C @@ -0,0 +1 @@ +pythia8_pp_136tev.C \ No newline at end of file From 1e7bf822a7a11a628c5c091eee719dfae9da44d1 Mon Sep 17 00:00:00 2001 From: Francesco Mazzaschi <43742195+fmazzasc@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:33:30 +0200 Subject: [PATCH 022/144] [SigmaProton generator] Add minPt for protons and sigmas separately (#2417) Co-authored-by: Francesco Mazzaschi --- .../ini/GeneratorSigmaProtonTriggered.ini | 2 +- .../pythia8/generator_pythia8_sigma_hadron.C | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/MC/config/PWGLF/ini/GeneratorSigmaProtonTriggered.ini b/MC/config/PWGLF/ini/GeneratorSigmaProtonTriggered.ini index 923a5947d..4443d2070 100644 --- a/MC/config/PWGLF/ini/GeneratorSigmaProtonTriggered.ini +++ b/MC/config/PWGLF/ini/GeneratorSigmaProtonTriggered.ini @@ -1,5 +1,5 @@ [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_sigma_hadron.C -funcName=generateSigmaHadron(2212, 2, 1, 10, 0.8) +funcName=generateSigmaHadron(2212, 2, 1, 0.5, 10, 0.5) [GeneratorPythia8] config=${O2_ROOT}/share/Generators/egconfig/pythia8_inel.cfg \ No newline at end of file diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_sigma_hadron.C b/MC/config/PWGLF/pythia8/generator_pythia8_sigma_hadron.C index 296dd78fc..c006a66d3 100644 --- a/MC/config/PWGLF/pythia8/generator_pythia8_sigma_hadron.C +++ b/MC/config/PWGLF/pythia8/generator_pythia8_sigma_hadron.C @@ -26,19 +26,20 @@ class GeneratorPythia8SigmaHadron : public o2::eventgen::GeneratorPythia8 { public: /// Constructor - GeneratorPythia8SigmaHadron(int hadronPdg, int gapSize = 4, double minPt = 0.2, + GeneratorPythia8SigmaHadron(int hadronPdg, int gapSize = 4, double minSigmaPt = 0.2, double minHadronPt = 0.2, double maxPt = 10, double maxEta = 0.8, double kStarMax = 1.0) : o2::eventgen::GeneratorPythia8(), mHadronPdg(hadronPdg), mGapSize(gapSize), - mMinPt(minPt), + mMinSigmaPt(minSigmaPt), + mMinHadronPt(minHadronPt), mMaxPt(maxPt), mMaxEta(maxEta), mKStarMax(kStarMax) { fmt::printf( - ">> Pythia8 generator: Sigma± + hadron(PDG=%d), gap = %d, minPt = %f, maxPt = %f, |eta| < %f, k* < %f\n", - hadronPdg, gapSize, minPt, maxPt, maxEta, kStarMax); + ">> Pythia8 generator: Sigma± + hadron(PDG=%d), gap = %d, minSigmaPt = %f, minHadronPt = %f, maxPt = %f, |eta| < %f, k* < %f\n", + hadronPdg, gapSize, minSigmaPt, minHadronPt, maxPt, maxEta, kStarMax); } ~GeneratorPythia8SigmaHadron() = default; @@ -135,7 +136,7 @@ protected: for (int i = 0; i < event.size(); i++) { const auto& p = event[i]; - if (std::abs(p.eta()) > mMaxEta || p.pT() < mMinPt || p.pT() > mMaxPt) { + if (std::abs(p.eta()) > mMaxEta || p.pT() > mMaxPt) { continue; } @@ -143,11 +144,11 @@ protected: const int absPdg = std::abs(pdg); // Sigma- or Sigma+ - if (absPdg == 3112 || absPdg == 3222) { + if ((absPdg == 3112 || absPdg == 3222) && p.pT() >= mMinSigmaPt) { sigmaIndices.push_back(i); } - if (std::abs(pdg) == mHadronPdg && !isFromSigmaDecay(p, event)) { + if (std::abs(pdg) == mHadronPdg && p.pT() >= mMinHadronPt && !isFromSigmaDecay(p, event)) { hadronIndices.push_back(i); } } @@ -178,7 +179,8 @@ protected: private: int mHadronPdg{211}; int mGapSize{4}; - double mMinPt{0.2}; + double mMinSigmaPt{0.2}; + double mMinHadronPt{0.2}; double mMaxPt{10.0}; double mMaxEta{0.8}; double mKStarMax{1.0}; @@ -186,10 +188,10 @@ private: }; ///___________________________________________________________ -FairGenerator* generateSigmaHadron(int hadronPdg, int gap = 4, double minPt = 0.2, +FairGenerator* generateSigmaHadron(int hadronPdg, int gap = 4, double minSigmaPt = 0.2, double minHadronPt = 0.2, double maxPt = 10, double maxEta = 0.8, double kStarMax = 1.0) { - auto myGenerator = new GeneratorPythia8SigmaHadron(hadronPdg, gap, minPt, maxPt, maxEta, kStarMax); + auto myGenerator = new GeneratorPythia8SigmaHadron(hadronPdg, gap, minSigmaPt, minHadronPt, maxPt, maxEta, kStarMax); auto seed = (gRandom->TRandom::GetSeed() % 900000000); myGenerator->readString("Random:setSeed on"); myGenerator->readString("Random:seed " + std::to_string(seed)); From de4c4abeb65bd9d6cc9c0ee07d89401db350f34b Mon Sep 17 00:00:00 2001 From: Koushik Barai Date: Sat, 25 Jul 2026 10:20:42 +0530 Subject: [PATCH 023/144] PWGDQ: add PbPb 5.36 TeV Bottomonia generator parameters (#2397) * PWGDQ: add PbPb 5.36 TeV Bottomonia generator parameters * PWGDQ: add PbPb Bottomonia gap-trigger generator case * PWGDQ: fix PbPb bottomonia generator parameterization --------- Co-authored-by: Koushik Barai --- .../external/generator/GeneratorBottomonia.C | 261 +++++++++++++++++- ...njectedBottomoniaSignals_gaptriggered_dq.C | 3 + 2 files changed, 262 insertions(+), 2 deletions(-) diff --git a/MC/config/PWGDQ/external/generator/GeneratorBottomonia.C b/MC/config/PWGDQ/external/generator/GeneratorBottomonia.C index 2f9897cd2..2da2edf9b 100644 --- a/MC/config/PWGDQ/external/generator/GeneratorBottomonia.C +++ b/MC/config/PWGDQ/external/generator/GeneratorBottomonia.C @@ -6,7 +6,9 @@ R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/EvtGen) R__LOAD_LIBRARY(libpythia6) #include "GeneratorCocktail.C" #include "GeneratorEvtGen.C" - +// ========================== +// === pp 13 TeV classes ==== +// ========================== namespace o2 { namespace eventgen @@ -277,4 +279,259 @@ FairGenerator* GeneratorCocktailBottomoniaToMuonEvtGen_pp13TeV() genCocktailEvtGen->SetForceDecay(kEvtDiMuon); return genCocktailEvtGen; -} \ No newline at end of file +} +// === END OF pp13tev CLASSES ===== + +// ================================ +// ==== PbPb 5.36 TeV classes ===== +// ================================ +namespace o2 +{ +namespace eventgen +{ +///////////////////////////////////////////////////////////////////////////// +class O2_GeneratorParamUpsilon1SFwdY_PbPb5TeV : public GeneratorTGenerator +{ + public: + O2_GeneratorParamUpsilon1SFwdY_PbPb5TeV() : GeneratorTGenerator("ParamUpsilon1S_PbPb5TeV") + { + paramUpsilon1S = new GeneratorParam(1, -1, PtUpsilon1SPbPb5TeV, YUpsilon1SPbPb5TeV, V2Upsilon1SPbPb5TeV, IpUpsilon1SPbPb5TeV); + paramUpsilon1S->SetMomentumRange(0., 1.e6); + paramUpsilon1S->SetPtRange(0, 999.); + paramUpsilon1S->SetYRange(-4.2, -2.3); // ALICE muon spectrometer + paramUpsilon1S->SetPhiRange(0., 360.); + paramUpsilon1S->SetDecayer(new TPythia6Decayer()); + paramUpsilon1S->SetForceDecay(kNoDecay); // left undecayed for EvtGen + setTGenerator(paramUpsilon1S); + }; + + ~O2_GeneratorParamUpsilon1SFwdY_PbPb5TeV() { delete paramUpsilon1S; }; + + Bool_t Init() override + { + GeneratorTGenerator::Init(); + paramUpsilon1S->Init(); + return true; + } + void SetNSignalPerEvent(Int_t nsig) { paramUpsilon1S->SetNumberParticles(nsig); } + + static Double_t PtUpsilon1SPbPb5TeV(const Double_t* px, const Double_t* /*dummy*/) + { + // Upsilon(1S) pT in PbPb at 5.36 TeV + // Parameterized from pp LHCb 13 TeV (arXiv:1804.09214) scaled to PbPb 5.36 TeV + // NOTE: should be updated once dedicated ALICE PbPb 5.36 TeV tuning is available + Double_t x = *px; + Float_t p0, p1, p2, p3; + p0 = 3.68558e+02; + p1 = 9.15000e+00; // softer than pp 13TeV (p1=10.31) reflecting PbPb medium + p2 = 1.62309e+00; + p3 = 4.84709e+00; + return (p0 * x / TMath::Power(1. + TMath::Power(x / p1, p2), p3)); + } + + + //-------------------------------------------------------------------------// + static Double_t YUpsilon1SPbPb5TeV(const Double_t* py, const Double_t* /*dummy*/) + { + // Upsilon(1S) y in PbPb at 5.36 TeV + // Based on LHCb pp 13 TeV shape (arXiv:1804.09214), reasonable for PbPb forward y + Double_t x = *py; + Float_t p0, p1; + p0 = 3.07931e+03; + p1 = -3.53102e-02; + return (p0 * (1. + p1 * x * x)); + } + + //-------------------------------------------------------------------------// + static Double_t V2Upsilon1SPbPb5TeV(const Double_t* /*dummy*/, const Double_t* /*dummy*/) + { + // Upsilon(1S) v2 - set to 0, can be updated with measurement + return 0.; + } + //-------------------------------------------------------------------------// + static Int_t IpUpsilon1SPbPb5TeV(TRandom*) + { + return 553; // PDG code Upsilon(1S) + } + + private: + GeneratorParam* paramUpsilon1S = nullptr; +}; +///////////////////////////////////////////////////////////////////////////// +class O2_GeneratorParamUpsilon2SFwdY_PbPb5TeV : public GeneratorTGenerator +{ + + public: + O2_GeneratorParamUpsilon2SFwdY_PbPb5TeV() + : GeneratorTGenerator("ParamUpsilon2S_PbPb5TeV") + { + paramUpsilon2S = new GeneratorParam(1, -1, PtUpsilon2SPbPb5TeV, YUpsilon2SPbPb5TeV, V2Upsilon2SPbPb5TeV, IpUpsilon2SPbPb5TeV); + paramUpsilon2S->SetMomentumRange(0., 1.e6); + paramUpsilon2S->SetPtRange(0, 999.); + paramUpsilon2S->SetYRange(-4.2, -2.3); + paramUpsilon2S->SetPhiRange(0., 360.); + paramUpsilon2S->SetDecayer(new TPythia6Decayer()); + paramUpsilon2S->SetForceDecay(kNoDecay); + setTGenerator(paramUpsilon2S); + }; + + ~O2_GeneratorParamUpsilon2SFwdY_PbPb5TeV() { delete paramUpsilon2S; }; + + Bool_t Init() override + { + GeneratorTGenerator::Init(); + paramUpsilon2S->Init(); + return true; + } + + void SetNSignalPerEvent(Int_t nsig) { paramUpsilon2S->SetNumberParticles(nsig); } + + //-------------------------------------------------------------------------// + static Double_t PtUpsilon2SPbPb5TeV(const Double_t* px, const Double_t* /*dummy*/) + { + // Upsilon(2S) pT in PbPb at 5.36 TeV + // Scaled from LHCb pp 13 TeV (arXiv:1804.09214) + // NOTE: should be updated once dedicated ALICE PbPb 5.36 TeV tuning is available + Double_t x = *px; + Float_t p0, p1, p2, p3; + p0 = 7.29541e+01; + p1 = 1.30000e+01; // scaled down from pp 13TeV (p1=14.81) for PbPb + p2 = 1.50018e+00; + p3 = 6.34208e+00; + return (p0 * x / TMath::Power(1. + TMath::Power(x / p1, p2), p3)); + } + + //-------------------------------------------------------------------------// + static Double_t YUpsilon2SPbPb5TeV(const Double_t* py, const Double_t* /*dummy*/) + { + // Upsilon(2S) y in PbPb at 5.36 TeV + Double_t x = *py; + Float_t p0, p1; + p0 = 7.50409e+02; + p1 = -3.57039e-02; + return (p0 * (1. + p1 * x * x)); + } + + //-------------------------------------------------------------------------// + static Double_t V2Upsilon2SPbPb5TeV(const Double_t* /*dummy*/, const Double_t* /*dummy*/) + { + return 0.; + } + + //-------------------------------------------------------------------------// + static Int_t IpUpsilon2SPbPb5TeV(TRandom*) + { + return 100553; // PDG code Upsilon(2S) + } + + private: + GeneratorParam* paramUpsilon2S = nullptr; +}; + +///////////////////////////////////////////////////////////////////////////// +class O2_GeneratorParamUpsilon3SFwdY_PbPb5TeV : public GeneratorTGenerator +{ + public: + O2_GeneratorParamUpsilon3SFwdY_PbPb5TeV() : GeneratorTGenerator("ParamUpsilon3S_PbPb5TeV") + { + paramUpsilon3S = new GeneratorParam(1, -1, PtUpsilon3SPbPb5TeV, YUpsilon3SPbPb5TeV, V2Upsilon3SPbPb5TeV, IpUpsilon3SPbPb5TeV); + paramUpsilon3S->SetMomentumRange(0., 1.e6); + paramUpsilon3S->SetPtRange(0, 999.); + paramUpsilon3S->SetYRange(-4.2, -2.3); + paramUpsilon3S->SetPhiRange(0., 360.); + paramUpsilon3S->SetDecayer(new TPythia6Decayer()); + paramUpsilon3S->SetForceDecay(kNoDecay); + setTGenerator(paramUpsilon3S); + }; + + ~O2_GeneratorParamUpsilon3SFwdY_PbPb5TeV() { delete paramUpsilon3S; }; + + Bool_t Init() override + { + GeneratorTGenerator::Init(); + paramUpsilon3S->Init(); + return true; + } + + void SetNSignalPerEvent(Int_t nsig) { paramUpsilon3S->SetNumberParticles(nsig); } + + //-------------------------------------------------------------------------// + static Double_t PtUpsilon3SPbPb5TeV(const Double_t* px, const Double_t* /*dummy*/) + { + // Upsilon(3S) pT in PbPb at 5.36 TeV + // Scaled from LHCb pp 13 TeV (arXiv:1804.09214) + // NOTE: should be updated once dedicated ALICE PbPb 5.36 TeV tuning is available + Double_t x = *px; + Float_t p0, p1, p2, p3; + p0 = 3.14590e+01; + p1 = 2.05000e+01; // scaled from pp 13TeV (p1=23.08) for PbPb + p2 = 1.40822e+00; + p3 = 9.38026e+00; + return (p0 * x / TMath::Power(1. + TMath::Power(x / p1, p2), p3)); + } + + //-------------------------------------------------------------------------// + static Double_t YUpsilon3SPbPb5TeV(const Double_t* py, const Double_t* /*dummy*/) + { + // Upsilon(3S) y in PbPb at 5.36 TeV + Double_t x = *py; + Float_t p0, p1; + p0 = 3.69961e+02; + p1 = -3.54650e-02; + return (p0 * (1. + p1 * x * x)); + } + + //-------------------------------------------------------------------------// + static Double_t V2Upsilon3SPbPb5TeV(const Double_t* /*dummy*/, const Double_t* /*dummy*/) + { + return 0.; + } + + //-------------------------------------------------------------------------// + static Int_t IpUpsilon3SPbPb5TeV(TRandom*) + { + return 200553; // PDG code Upsilon(3S) + } + + private: + GeneratorParam* paramUpsilon3S = nullptr; +}; + +} // namespace eventgen +} // namespace o2 + +// ================================== +// NEW COCKTAIL FUCNTION FOR PbPb === +// ================================== + +FairGenerator* GeneratorCocktailBottomoniaToMuonEvtGen_PbPb5TeV() +{ + auto genCocktailEvtGen = new o2::eventgen::GeneratorEvtGen(); + + auto genUpsilon1S = new o2::eventgen::O2_GeneratorParamUpsilon1SFwdY_PbPb5TeV; + genUpsilon1S->SetNSignalPerEvent(1); + + auto genUpsilon2S = new o2::eventgen::O2_GeneratorParamUpsilon2SFwdY_PbPb5TeV; + genUpsilon2S->SetNSignalPerEvent(1); + + auto genUpsilon3S = new o2::eventgen::O2_GeneratorParamUpsilon3SFwdY_PbPb5TeV; + genUpsilon3S->SetNSignalPerEvent(1); + + genCocktailEvtGen->AddGenerator(genUpsilon1S, 1); + genCocktailEvtGen->AddGenerator(genUpsilon2S, 1); + genCocktailEvtGen->AddGenerator(genUpsilon3S, 1); + + TString pdgs = "553;100553;200553"; + std::string spdg; + TObjArray* obj = pdgs.Tokenize(";"); + genCocktailEvtGen->SetSizePdg(obj->GetEntriesFast()); + for (int i = 0; i < obj->GetEntriesFast(); i++) { + spdg = obj->At(i)->GetName(); + genCocktailEvtGen->AddPdg(std::stoi(spdg), i); + printf("PDG %d \n", std::stoi(spdg)); + } + genCocktailEvtGen->SetForceDecay(kEvtDiMuon); // Υ → μ⁺μ⁻ via EvtGen + + return genCocktailEvtGen; +} +// ==== END of cocktail function ====== diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedBottomoniaSignals_gaptriggered_dq.C b/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedBottomoniaSignals_gaptriggered_dq.C index de4ab2b7a..7f2ced70e 100644 --- a/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedBottomoniaSignals_gaptriggered_dq.C +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedBottomoniaSignals_gaptriggered_dq.C @@ -24,6 +24,9 @@ public: case 0: // generate bottomonia cocktail at forward rapidity mGeneratorParam = (Generator*)GeneratorCocktailBottomoniaToMuonEvtGen_pp13TeV(); break; + case 1: // generate bottomonia cocktail at FwdY for PbPb 5p36TeV + mGeneratorParam = (Generator*)GeneratorCocktailBottomoniaToMuonEvtGen_PbPb5TeV(); + break; } mGeneratorParam->Init(); } From 9d7983ace79181908325e1ed13c17b9e4756bd6c Mon Sep 17 00:00:00 2001 From: Koushik Barai Date: Mon, 27 Jul 2026 12:23:20 +0530 Subject: [PATCH 024/144] PWGDQ: add PbPb 5.36 TeV Bottomonia gap trigger generator config (#2395) * PWGDQ: add PbPb 5.36 TeV Bottomonia gap trigger generator config * Add PbPb gap-trigger injected bottomonia generator test config * Update trigger ratio from 5 to 2 * Rename generator test macro to match INI filename --------- Co-authored-by: Koushik Barai --- ...dBottomoniaFwdY_TriggerGap_PbPb_536TeV.ini | 8 ++ ...tedBottomoniaFwdY_TriggerGap_PbPb_536TeV.C | 103 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 MC/config/PWGDQ/ini/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.ini create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.C diff --git a/MC/config/PWGDQ/ini/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.ini new file mode 100644 index 000000000..ec91423b7 --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.ini @@ -0,0 +1,8 @@ +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_withInjectedBottomoniaSignals_gaptriggered_dq.C + +funcName=GeneratorPythia8InjectedBottomoniaGapTriggered(2,1) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_536tev.cfg diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.C new file mode 100644 index 000000000..bdafa97b1 --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedBottomoniaFwdY_TriggerGap_PbPb_536TeV.C @@ -0,0 +1,103 @@ +int External() +{ + int checkPdgSignal[] = {553,100553,200553}; + int checkPdgDecay = 13; + double rapiditymin = -4.3; double rapiditymax = -2.3; + std::string path{"o2sim_Kine.root"}; + std::cout << "Check for\nsignal PDG " << checkPdgSignal << "\ndecay PDG " << checkPdgDecay << "\n"; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree*)file.Get("o2sim"); + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + int nSignalUpsilon1S{}; + int nSignalUpsilon2S{}; + int nSignalUpsilon3S{}; + int nSignalUpsilon1SWithinAcc{}; + int nSignalUpsilon2SWithinAcc{}; + int nSignalUpsilon3SWithinAcc{}; + auto nEvents = tree->GetEntries(); + o2::steer::MCKinematicsReader mcreader("o2sim", o2::steer::MCKinematicsReader::Mode::kMCKine); + Bool_t isInjected = kFALSE; + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + } else if(pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + } else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1] || pdg == checkPdgSignal[2]) { + if(idMoth < 0){ + // count signal PDG + if (pdg == checkPdgSignal[0]) { + nSignalUpsilon1S++; + } else if (pdg == checkPdgSignal[1]) { + nSignalUpsilon2S++; + } else { + nSignalUpsilon3S++; + } + + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) { + if (pdg == checkPdgSignal[0]) { + nSignalUpsilon1SWithinAcc++; + } else if (pdg == checkPdgSignal[1]) { + nSignalUpsilon2SWithinAcc++; + } else { + nSignalUpsilon3SWithinAcc++; + } + } + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#signal (Upsilon(1S)): " << nSignalUpsilon1S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalUpsilon1SWithinAcc << "\n" + << "#signal (Upsilon(2S)): " << nSignalUpsilon2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalUpsilon2SWithinAcc << "\n" + << "#signal (Upsilon(2S)): " << nSignalUpsilon3S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalUpsilon3SWithinAcc << "\n" + << "#lepton pairs: " << nLeptonPairs << "\n" + << "#lepton pairs to be done: " << nLeptonPairs << "\n"; + + + if (nLeptonPairs == 0 || nLeptons == 0 || nAntileptons == 0) { + std::cerr << "Number of leptons, number of anti-leptons as well as number of lepton pairs should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) { + std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; + return 1; + } + + return 0; +} From ba8a6b207ff5589d96db680cf8c32b2e5307396a Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Tue, 28 Jul 2026 13:03:50 +0200 Subject: [PATCH 025/144] AODBcRewriter: remap every index column an output table carries (O2-7098) Stage 1b (added in #2370) reorders O2track_iu/O2mfttrack/O2fwdtrack to keep the "-1" ambiguous group contiguous, but wrote each table as soon as it was planned. O2fwdtrack references O2mfttrack (reordered later in the same loop) and itself (fIndexFwdTracks_MatchMCHTrack); neither permutation existed yet at write time, so both columns kept pre-reorder row numbers -- in range, so AODBcRewriterValidate passed, while every global muon got a foreign MFT/MCH leg (Maurice Coquet, sameParticle=0 on an anchored pO sample; patch proposal #2418). This fires whenever Stage 1b's regroup runs, independent of any BC/collision reorder, so every merged MC AO2D since 4 Jun is affected. Fix: * kIndexRefs: one registry of "which fIndex* column points at which table", used by the rewriter, the validator, and a new drift guard (an unregistered fIndex* column now fails the test instead of silently mis-linking data). * buildRemaps() derives a table's full remap set from that registry; the old "primary index + optional extras" split in rewriteTable is gone, so a stage can no longer forget a column the way Stage 1b forgot O2fwdtrack's. * Planning and writing are separate phases: every stage publishes its row permutation before anything is written, so forward/self references resolve correctly. New AODBcRewriterCheckLinks(in, out): fingerprints each row's non-index payload and checks, per table, that (row, referenced row) pairs survive the rewrite -- the only check that can see this bug class, since the corrupted indices were structurally valid. Needs only the input file, so it also runs on real production AO2Ds. MC/utils/tests/ adds the executable test this tool never had: a synthetic AO2D covering every pathology (duplicate/non-monotonic BCs, duplicate MC collisions, split -1 groups, fwd<->MFT and fwd->fwd references, ...), driven by run_aodbcrewriter_tests.sh -- ROOT only, seconds to run, nothing binary committed. Verified against the pre-fix rewriter and a real anchored pp production (run 553185, LHC24al apass1): sameParticle collapses to 0 with the shipped code and is fully restored with this fix, in every scenario tested, including one engineered to contain genuine duplicate BCs. Diagnosis and original patch: Maurice Coquet (#2418). Co-Authored-By: Claude Opus 5 --- MC/utils/AODBcRewriter.C | 1026 ++++++++++++++------- MC/utils/CLAUDE.md | 330 +++++-- MC/utils/tests/makeTestAOD.C | 336 +++++++ MC/utils/tests/run_aodbcrewriter_tests.sh | 105 +++ MC/utils/tests/testAODBcRewriter.C | 294 ++++++ 5 files changed, 1682 insertions(+), 409 deletions(-) create mode 100644 MC/utils/tests/makeTestAOD.C create mode 100755 MC/utils/tests/run_aodbcrewriter_tests.sh create mode 100644 MC/utils/tests/testAODBcRewriter.C diff --git a/MC/utils/AODBcRewriter.C b/MC/utils/AODBcRewriter.C index 16c7451ef..f03d013a1 100644 --- a/MC/utils/AODBcRewriter.C +++ b/MC/utils/AODBcRewriter.C @@ -39,6 +39,25 @@ // Unrelated tables — tables with no dependency on BCs or MCCollisions are // copied verbatim. // +// PLAN, THEN WRITE — every stage above only decides row orders and publishes +// the resulting permutations; processDF writes all tables +// afterwards. References are not a DAG: O2fwdtrack points at +// O2mfttrack (reordered in the same stage) and at itself, so a +// stage that wrote as it went could not remap them. It did not, +// and every global muon in every merged MC AO2D between June and +// July 2026 got a foreign MFT and MCH leg (O2-7098). +// +// ONE INDEX REGISTRY — kIndexRefs lists every fIndex* column and the table it +// points at. buildRemaps() turns it into a table's remap set, the +// validator range-checks against it, and any fIndex* column NOT in +// it is reported loudly. Do not reintroduce per-stage lists of +// "indices this stage knows how to remap". +// +// TESTING — MC/utils/tests/run_aodbcrewriter_tests.sh (ROOT only, seconds). +// AODBcRewriterCheckLinks(in, out) is the check that sees a +// mis-remapped index; the output-only checks cannot, because a +// wrong row number is still a valid one. +// // ----------------------------------------------------------------------------- // DATA MODEL DEPENDENCY GRAPH (relevant subset) // ----------------------------------------------------------------------------- @@ -75,6 +94,7 @@ #include "TTree.h" #include "TGrid.h" #include +#include #include #include #include @@ -172,6 +192,89 @@ static bool isPasteJoinChild(const std::string &tname) { return false; } +// ---------------------------------------------------------------------------- +// INDEX-REFERENCE REGISTRY — the single source of truth for "which fIndex* +// column points into which table". +// +// This ONE list drives three things: +// +// * the rewriter — buildRemaps() turns it into the complete set of value +// remaps for a table, so a reference can no longer be +// forgotten. Forgetting one is the O2-7098 bug class: +// Stage 1b reordered O2fwdtrack but nobody remapped its +// fIndexMFTTracks / fIndexFwdTracks_MatchMCHTrack, so every +// global muon silently got the wrong MFT and MCH leg. The +// values stayed in range, so the range check below passed. +// * the validator — the generic in-range check. +// * the drift guard — any fIndex* branch NOT listed here is reported, so a +// schema addition breaks the test instead of quietly +// producing mis-linked data. +// +// The value is a list of candidate table-name prefixes; the first one that +// resolves in this DF wins (that is how fIndexTracks* prefers O2track_iu over +// O2track). +// +// Referent resolution (isTableNamed): a tree named K is the table named by +// prefix P when K == P or K == P + "_" (the AO2D schema-version +// suffix). That deliberately separates O2mfttrack_001 from O2mfttrackcov, +// O2bc_001 from O2bcflag, and O2mccollision_001 from O2mccollisionlabel — a +// plain BeginsWith() would confuse them. +static const std::vector>> kIndexRefs = { + { "fIndexBCs", { "O2bc" } }, + { "fIndexBC", { "O2bc" } }, + { "fIndexSliceBCs", { "O2bc" } }, + { "fIndexCollisions", { "O2collision" } }, + { "fIndexCollision", { "O2collision" } }, + { "fIndexMcCollisions", { "O2mccollision" } }, + { "fIndexMcParticles", { "O2mcparticle" } }, + { "fIndexArrayMcParticles", { "O2mcparticle" } }, + // O2mcparticle intra-table links (mother list + [first,last] daughter slice). + { "fIndexArray_Mothers", { "O2mcparticle" } }, + { "fIndexSlice_Daughters", { "O2mcparticle" } }, + { "fIndexTracks", { "O2track_iu", "O2track" } }, + { "fIndexTracks_0", { "O2track_iu", "O2track" } }, + { "fIndexTracks_1", { "O2track_iu", "O2track" } }, + { "fIndexTracks_2", { "O2track_iu", "O2track" } }, + { "fIndexTracks_Pos", { "O2track_iu", "O2track" } }, + { "fIndexTracks_Neg", { "O2track_iu", "O2track" } }, + { "fIndexTracks_Bach", { "O2track_iu", "O2track" } }, + { "fIndexTracks_ITS", { "O2track_iu", "O2track" } }, + { "fIndexMFTTracks", { "O2mfttrack" } }, + { "fIndexFwdTracks", { "O2fwdtrack" } }, + { "fIndexFwdTracks_MatchMCHTrack", { "O2fwdtrack" } }, + { "fIndexV0s", { "O2v0" } }, + { "fIndexCascades", { "O2cascade" } }, + { "fIndexDecay3Bodys", { "O2decay3body" } }, +}; + +// K names the table with prefix P? See the comment on kIndexRefs. +static bool isTableNamed(const std::string &key, const std::string &prefix) { + if (key == prefix) return true; + if (key.size() <= prefix.size() + 1) return false; + if (key.compare(0, prefix.size(), prefix) != 0) return false; + if (key[prefix.size()] != '_') return false; + for (size_t i = prefix.size() + 1; i < key.size(); ++i) + if (!std::isdigit(static_cast(key[i]))) return false; + return true; +} + +// Every fIndex* branch of t that is NOT covered by kIndexRefs. "*_size" count +// branches of VLA index arrays are not references and are excluded. A non-empty +// result means the schema grew a link the tool does not know how to follow. +static std::vector unregisteredIndexBranches(TTree *t) { + std::vector out; + if (!t) return out; + for (auto *obj : *t->GetListOfBranches()) { + std::string bname = static_cast(obj)->GetName(); + if (bname.rfind("fIndex", 0) != 0) continue; + if (bname.size() > 5 && bname.compare(bname.size() - 5, 5, "_size") == 0) continue; + bool known = false; + for (auto &kv : kIndexRefs) if (kv.first == bname) { known = true; break; } + if (!known) out.push_back(bname); + } + return out; +} + // ============================================================================ // SECTION 2 — Generic ROOT branch I/O helpers // ============================================================================ @@ -248,13 +351,6 @@ static void writeAsInt(void *buf, ScalarTag tag, Long64_t val) { } } -// Remap a single Int_t index value through a PermMap. Returns -1 for any -// out-of-range or already-invalid (negative) value. -static Int_t remapIdx(Int_t val, const PermMap &perm) { - if (val < 0 || (size_t)val >= perm.size()) return -1; - return perm[(size_t)val]; -} - // A description of one branch in a tree: its name, scalar type tag, byte // size, and whether it is a VLA (variable-length array). For VLAs we also // keep the name of the count branch and the maximum observed element count @@ -341,40 +437,59 @@ static std::vector describeBranches(TTree *tree) { // SECTION 3 — Table rewriting engine // ============================================================================ // -// rewriteTable() is the single generic function that handles any table. +// writeTable() is the single generic function that writes any table. // It takes: -// - src : the source TTree -// - dirOut : directory to write the output TTree into -// - rowOrder : which source rows to include and in what order -// (a vector of source row indices, possibly a subset) -// - indexBranch : name of the index branch to remap, or "" if none -// - parentPerm : PermMap for remapping that index (may be empty) -// - extraRemaps : additional index columns to remap in-place via their own -// PermMaps (used for intra-table and cross-table indices -// that are not the primary sort key, e.g. mother/daughter -// indices in O2mcparticle or fIndexMcParticles in labels) +// - src : the source TTree +// - dirOut : directory to write the output TTree into +// - rowOrder : which source rows to include and in what order +// (a vector of source row indices, possibly a subset) +// - remaps : the index columns to remap in-place, each with its own PermMap // -// It returns the permutation of source rows implied by rowOrder, expressed -// as a PermMap: perm[srcRow] = outputRow, -1 if the row was dropped. - -// Describes one extra index column to remap independently of the sort key. -struct ExtraRemap { - std::string branchName; // branch whose integer values to remap - const PermMap *perm; // remapping table: newVal = (*perm)[oldVal] +// There is deliberately NO "primary index" special case. Every index column — +// the one the table was sorted by as much as an intra-table mother/daughter +// link — goes through the same `remaps` list, which callers obtain from +// buildRemaps() and therefore from the kIndexRefs registry. The previous +// design had one privileged index plus an optional list of "extra" ones, and +// every stage had to remember to populate the extras; Stage 1b did not, which +// is precisely how O2fwdtrack's match indices were left dangling (O2-7098). + +// Describes one index column to remap. +struct IndexRemap { + std::string branchName; // branch whose integer values to remap + const PermMap *perm; // remapping table: newVal = (*perm)[oldVal] }; -static PermMap rewriteTable(TTree *src, TDirectory *dirOut, - const std::vector &rowOrder, - const std::string &indexBranch, - const PermMap &parentPerm, - const std::vector &extraRemaps = {}) { +// The row permutation implied by a rowOrder: perm[srcRow] = outRow, -1 if the +// row is not emitted. +static PermMap permFromRowOrder(Long64_t nSrc, + const std::vector &rowOrder) { + PermMap perm(nSrc, -1); + for (Long64_t outRow = 0; outRow < (Long64_t)rowOrder.size(); ++outRow) + perm[rowOrder[outRow]] = (Int_t)outRow; + return perm; +} - Long64_t nSrc = src->GetEntries(); +// Remap n consecutive integers of the given type held in buf. Out-of-range and +// already-invalid values become -1 (the AO2D "no link" sentinel). +// Returns how many values actually changed. +static Long64_t remapBuffer(void *buf, ScalarTag tag, size_t elemSize, int n, + const PermMap &perm) { + Long64_t changed = 0; + auto *bytes = static_cast(buf); + for (int j = 0; j < n; ++j) { + void *slot = bytes + (size_t)j * elemSize; + Long64_t v = readAsInt(slot, tag); + Long64_t nv = (v < 0 || (size_t)v >= perm.size()) ? -1 : perm[(size_t)v]; + if (nv != v) { writeAsInt(slot, tag, nv); ++changed; } + } + return changed; +} - // Build the inverse permutation (srcRow → outRow) from rowOrder - PermMap srcToOut(nSrc, -1); - for (Long64_t outRow = 0; outRow < (Long64_t)rowOrder.size(); ++outRow) - srcToOut[rowOrder[outRow]] = (Int_t)outRow; +static void writeTable(TTree *src, TDirectory *dirOut, + const std::vector &rowOrder, + const std::vector &remaps = {}) { + + Long64_t nSrc = src->GetEntries(); // Describe all branches auto descs = describeBranches(src); @@ -422,24 +537,9 @@ static PermMap rewriteTable(TTree *src, TDirectory *dirOut, dirOut->cd(); TTree *out = src->CloneTree(0, "fast"); - // Find the index branch (if any) — we will update its value on the fly - ScalarTag idxTag = ScalarTag::kUnknown; - std::vector newIdxBuf; - TBranch *outIdxBr = nullptr; - if (!indexBranch.empty()) { - TBranch *inIdxBr = src->GetBranch(indexBranch.c_str()); - TLeaf *idxLeaf = inIdxBr ? static_cast(inIdxBr->GetListOfLeaves()->At(0)) : nullptr; - idxTag = idxLeaf ? tagOf(idxLeaf) : ScalarTag::kUnknown; - if (idxTag != ScalarTag::kUnknown) { - newIdxBuf.assign(byteSize(idxTag), 0); - outIdxBr = out->GetBranch(indexBranch.c_str()); - if (outIdxBr) outIdxBr->SetAddress(newIdxBuf.data()); - } - } - - // Set all other output branch addresses to the same data buffers as input + // Output branches share the input buffers, so an in-place remap below is + // what out->Fill() sees. for (auto &io : ios) { - if (io.desc.name == indexBranch) continue; // handled separately above TBranch *outBr = out->GetBranch(io.desc.name.c_str()); if (!outBr) { std::cerr << " [warn] no output branch for " << io.desc.name << "\n"; continue; } outBr->SetAddress(io.dataBuf.data()); @@ -449,46 +549,36 @@ static PermMap rewriteTable(TTree *src, TDirectory *dirOut, } } + // Resolve each remap to its BranchIO once, up front, instead of searching the + // branch list per row (O2mcparticle has millions of rows). + struct ResolvedRemap { BranchIO *io; const PermMap *perm; }; + std::vector resolved; + for (auto &r : remaps) { + BranchIO *found = nullptr; + for (auto &io : ios) if (io.desc.name == r.branchName) { found = &io; break; } + if (!found) continue; // branch absent (caller probed, so this is unusual) + // Index columns are signed 32-bit in the AO2D schema. Refuse to touch + // anything else rather than write -1 into an unsigned field. + if (found->desc.tag != ScalarTag::kInt && found->desc.tag != ScalarTag::kShort && + found->desc.tag != ScalarTag::kLong64) { + std::cerr << " [warn] index branch " << r.branchName + << " has non-signed-integer type — NOT remapped\n"; + continue; + } + resolved.push_back({found, r.perm}); + } + // Fill the output tree row by row in the requested order Long64_t nRemapped = 0; for (Long64_t srcRow : rowOrder) { src->GetEntry(srcRow); - // Remap the index branch if required - if (outIdxBr && idxTag != ScalarTag::kUnknown && !parentPerm.empty()) { - // Read old index from the input branch's buffer (one of the ios entries) - Long64_t oldIdx = -1; - for (auto &io : ios) { - if (io.desc.name == indexBranch) { oldIdx = readAsInt(io.dataBuf.data(), idxTag); break; } - } - Long64_t newIdx = -1; - if (oldIdx >= 0 && oldIdx < (Long64_t)parentPerm.size()) - newIdx = parentPerm[oldIdx]; - writeAsInt(newIdxBuf.data(), idxTag, newIdx >= 0 ? newIdx : -1); - if (newIdx != oldIdx) ++nRemapped; - } - - // Apply extra in-place index remaps (e.g. intra-table mother/daughter - // indices in O2mcparticle, or fIndexMcParticles in label tables). - // The output branch shares the same buffer, so modifying dataBuf here - // is read by out->Fill() below. - for (auto &er : extraRemaps) { - for (auto &io : ios) { - if (io.desc.name != er.branchName) continue; - if (io.desc.isVLA) { - // VLA: remap each element according to count - Long64_t cnt = readAsInt(io.countBuf.data(), io.countTag); - auto *p = reinterpret_cast(io.dataBuf.data()); - for (Long64_t j = 0; j < cnt; ++j) - p[j] = remapIdx(p[j], *er.perm); - } else { - // Scalar or fixed-size array: remap all nElems integers - auto *p = reinterpret_cast(io.dataBuf.data()); - for (int j = 0; j < io.desc.nElems; ++j) - p[j] = remapIdx(p[j], *er.perm); - } - break; - } + for (auto &r : resolved) { + BranchIO &io = *r.io; + int n = io.desc.isVLA ? (int)readAsInt(io.countBuf.data(), io.countTag) + : io.desc.nElems; + nRemapped += remapBuffer(io.dataBuf.data(), io.desc.tag, io.desc.elemSize, + n, *r.perm); } out->Fill(); @@ -497,9 +587,62 @@ static PermMap rewriteTable(TTree *src, TDirectory *dirOut, std::cout << " wrote " << out->GetEntries() << " / " << nSrc << " rows; " << nRemapped << " index values remapped\n"; out->Write(); - return srcToOut; } +// Look up the permutation of the table named by the first prefix that resolves. +// Ambiguity (several schema versions of the same table in one DF) is resolved +// deterministically by taking the lexicographically smallest name. +static const PermMap *findPermFor( + const std::unordered_map &allPerms, + const std::vector &prefixes, + std::string *foundName = nullptr) { + for (auto &prefix : prefixes) { + const PermMap *best = nullptr; + std::string bestName; + for (auto &[name, perm] : allPerms) { + if (!isTableNamed(name, prefix)) continue; + if (bestName.empty() || name < bestName) { best = &perm; bestName = name; } + } + if (best) { if (foundName) *foundName = bestName; return best; } + } + return nullptr; +} + +// THE central safety net: derive the complete remap list for a table from the +// kIndexRefs registry. Every stage writes through this, so an index column can +// only be missed by being absent from kIndexRefs — which +// unregisteredIndexBranches() reports. +static std::vector buildRemaps( + TTree *src, const std::unordered_map &allPerms) { + std::vector remaps; + if (!src) return remaps; + for (auto &[branchName, prefixes] : kIndexRefs) { + if (!src->GetBranch(branchName.c_str())) continue; + const PermMap *perm = findPermFor(allPerms, prefixes); + if (!perm) continue; // referent table not in this DF — nothing to remap + remaps.push_back({branchName, perm}); + } + return remaps; +} + +// ============================================================================ +// SECTION 3b — Deferred write plan +// ============================================================================ +// +// Writing is deferred until every table's row order (and hence its permutation) +// is known, because references can point forwards and sideways: O2fwdtrack +// references O2mfttrack — written later in the same stage — and also itself, via +// fIndexFwdTracks_MatchMCHTrack. The old code wrote each table as soon as it +// had planned it, so those two permutations simply did not exist yet at write +// time. That is the mechanism behind O2-7098. +// +// So: every stage now only PLANS (appends a TablePlan, publishes its perm into +// allPerms), and processDF writes all plans afterwards in one pass. +struct TablePlan { + std::string name; // tree name; re-fetched from dirIn at write time + std::vector rowOrder; // source rows to emit, in output order +}; + // ============================================================================ // SECTION 4 — Stage 0: BC table sort + deduplication // ============================================================================ @@ -527,7 +670,7 @@ struct BCStage0Result { Long64_t nUnique = 0; }; -static BCStage0Result stage0_sortBCs(TTree *treeBCs, TDirectory *dirOut) { +static BCStage0Result stage0_sortBCs(TTree *treeBCs, std::vector &plans) { BCStage0Result res; Long64_t n = treeBCs->GetEntries(); if (n == 0) return res; @@ -566,8 +709,8 @@ static BCStage0Result stage0_sortBCs(TTree *treeBCs, TDirectory *dirOut) { std::cout << " BC stage: " << n << " rows -> " << res.nUnique << " unique (sorted)\n"; - // Write the BC table (no index remapping needed for the table itself) - rewriteTable(treeBCs, dirOut, rowOrder, /*indexBranch=*/"", /*parentPerm=*/{}); + // The BC table itself carries no index columns. + plans.push_back({treeBCs->GetName(), std::move(rowOrder)}); return res; } @@ -576,10 +719,9 @@ static BCStage0Result stage0_sortBCs(TTree *treeBCs, TDirectory *dirOut) { // SECTION 5 — Stage 0b: BC flags table (follows BC row order exactly) // ============================================================================ -static void stage0_copyBCFlags(TTree *treeFlags, TDirectory *dirOut, +static void stage0_copyBCFlags(TTree *treeFlags, std::vector &plans, const PermMap &bcPerm) { if (!treeFlags) return; - Long64_t nSrc = treeFlags->GetEntries(); // Build rowOrder: for each unique output BC row, pick the first source row // that mapped to it @@ -590,7 +732,7 @@ static void stage0_copyBCFlags(TTree *treeFlags, TDirectory *dirOut, rowOrder.reserve(first.size()); for (auto &kv : first) rowOrder.push_back(kv.second); - rewriteTable(treeFlags, dirOut, rowOrder, /*indexBranch=*/"", /*parentPerm=*/{}); + plans.push_back({treeFlags->GetName(), std::move(rowOrder)}); } // ============================================================================ @@ -632,10 +774,10 @@ struct MCCollKeyHash { } }; -static std::unordered_map -stage1_BCindexedTables(TDirectory *dirIn, TDirectory *dirOut, +static void +stage1_BCindexedTables(TDirectory *dirIn, std::vector &plans, + std::unordered_map &allPerms, const PermMap &bcPerm) { - std::unordered_map tablePerms; TIter it(dirIn->GetListOfKeys()); while (TKey *key = static_cast(it())) { @@ -719,20 +861,19 @@ stage1_BCindexedTables(TDirectory *dirIn, TDirectory *dirOut, << " duplicate rows (" << rowOrder.size() << " kept)\n"; } - PermMap perm = rewriteTable(src, dirOut, rowOrder, idxBr, bcPerm); - tablePerms[tname] = std::move(perm); + allPerms[tname] = permFromRowOrder(nSrc, rowOrder); + plans.push_back({tname, std::move(rowOrder)}); } - return tablePerms; } // ============================================================================ // SECTION 7 — Stage 2: Tables indexed by MCCollisions // ============================================================================ -static std::unordered_map -stage2_MCCollIndexedTables(TDirectory *dirIn, TDirectory *dirOut, +static void +stage2_MCCollIndexedTables(TDirectory *dirIn, std::vector &plans, + std::unordered_map &allPerms, const PermMap &mcCollPerm) { - std::unordered_map tablePerms; TIter it(dirIn->GetListOfKeys()); while (TKey *key = static_cast(it())) { @@ -800,27 +941,16 @@ stage2_MCCollIndexedTables(TDirectory *dirIn, TDirectory *dirOut, std::cout << " dropped " << dropped << " rows whose MCCollision parent was deduplicated\n"; - // For O2mcparticle: compute the self-permutation (old row -> new row) from - // the row order BEFORE calling rewriteTable, then pass it as extra remaps - // so that intra-table mother/daughter indices are updated in the same pass. - // The stable sort above preserves within-collision particle order, which - // keeps fIndexSlice_Daughters contiguous — so remapping [first,last] via - // selfPerm is correct. - std::vector extraRemaps; - PermMap selfPerm; - if (TString(tname.c_str()).BeginsWith("O2mcparticle")) { - selfPerm.assign(nSrc, -1); - for (Long64_t outRow = 0; outRow < (Long64_t)rowOrder.size(); ++outRow) - selfPerm[rowOrder[outRow]] = (Int_t)outRow; - extraRemaps.push_back({"fIndexArray_Mothers", &selfPerm}); - extraRemaps.push_back({"fIndexSlice_Daughters", &selfPerm}); - std::cout << " O2mcparticle: will remap intra-table mother/daughter indices\n"; - } - - PermMap perm = rewriteTable(src, dirOut, rowOrder, idxBr, mcCollPerm, extraRemaps); - tablePerms[tname] = std::move(perm); + // O2mcparticle's intra-table mother/daughter links are remapped through this + // table's OWN permutation. No special case is needed any more: the perm is + // published here and buildRemaps() picks it up for fIndexArray_Mothers / + // fIndexSlice_Daughters at write time, because writing happens after all + // planning. The stable sort above preserves within-collision particle + // order, which keeps fIndexSlice_Daughters contiguous — so remapping + // [first,last] elementwise is correct. + allPerms[tname] = permFromRowOrder(nSrc, rowOrder); + plans.push_back({tname, std::move(rowOrder)}); } - return tablePerms; } // ============================================================================ @@ -835,46 +965,15 @@ stage2_MCCollIndexedTables(TDirectory *dirIn, TDirectory *dirOut, // // The list of paste-join pairs is in kPasteJoins (Section 1). // -// Index columns we know how to remap in a child: -// fIndexMcCollisions -> via mcCollPerm -// fIndexCollisions -> via collPerm -// fIndexMcParticles -> via mcParticlePerm -// fIndexArrayMcParticles -> via mcParticlePerm (VLA) +// This stage no longer enumerates which index columns it knows how to remap: +// buildRemaps() derives them from kIndexRefs at write time, for every table +// alike. The old hand-maintained enumeration here is what let O2fwdtrack slip +// through — the table was never routed to this function in the first place. // // When the named parent is not in allPerms (e.g. tracks aren't reordered in // this build), the child is processed with identity row order so the // value-wise remaps still apply but the row order is unchanged. -// Build the row order from a PermMap (srcRow -> outRow), inverted. -static std::vector rowOrderFromPerm(const PermMap &perm) { - // perm[srcRow] = outRow (or -1 if dropped) - // We need: outRow -> srcRow, i.e. a sorted list of (outRow, srcRow) pairs - std::vector> pairs; - pairs.reserve(perm.size()); - for (Long64_t srcRow = 0; srcRow < (Long64_t)perm.size(); ++srcRow) - if (perm[srcRow] >= 0) pairs.push_back({perm[srcRow], srcRow}); - std::sort(pairs.begin(), pairs.end()); - std::vector order; - order.reserve(pairs.size()); - for (auto &p : pairs) order.push_back(p.second); - return order; -} - -// Locate a permutation in allPerms whose key begins with a given prefix. -// Returns nullptr if none found. -static const PermMap *findPermByPrefix( - const std::unordered_map &allPerms, - const char *prefix, - std::string *foundName = nullptr) { - for (auto &[name, perm] : allPerms) { - if (TString(name.c_str()).BeginsWith(prefix)) { - if (foundName) *foundName = name; - return &perm; - } - } - return nullptr; -} - // ============================================================================ // SECTION 9b — Stage 1b: Collision-grouped track tables // ============================================================================ @@ -899,7 +998,14 @@ static const PermMap *findPermByPrefix( // * paste-join children (O2trackextra, O2trackcov_iu, O2mctracklabel, ...) // follow the published parent permutation; // * every fIndexTracks* / fIndexMFTTracks / fIndexFwdTracks reference is -// remapped through it in processPasteJoinTables. +// remapped through it — including the ones the track tables hold on EACH +// OTHER. O2fwdtrack points at O2mfttrack (fIndexMFTTracks) and at itself +// (fIndexFwdTracks_MatchMCHTrack), so its remap needs permutations that are +// only established later in this very stage. That is why this function +// PLANS all tables first and lets processDF write them afterwards; writing +// inline (as it used to) meant those two columns kept pre-reorder row +// numbers — in range, so no validator complained, but every global muon +// ended up with a foreign MFT and MCH leg. See O2-7098. static bool isCollGroupedTrackTable(const std::string &tname) { static const char *kPrefixes[] = {"O2track_iu", "O2track", "O2mfttrack", "O2fwdtrack"}; @@ -909,11 +1015,11 @@ static bool isCollGroupedTrackTable(const std::string &tname) { } static void stage1b_reorderTrackTables( - TDirectory *dirIn, TDirectory *dirOut, + TDirectory *dirIn, std::vector &plans, std::unordered_map &allPerms, - std::unordered_set &written) { + std::unordered_set &planned) { - const PermMap *collPermP = findPermByPrefix(allPerms, "O2collision_"); + const PermMap *collPermP = findPermFor(allPerms, {"O2collision"}); if (!collPermP) return; // no collisions present — nothing to regroup against TIter it(dirIn->GetListOfKeys()); @@ -924,7 +1030,7 @@ static void stage1b_reorderTrackTables( if (!src) continue; std::string tname = src->GetName(); - if (written.count(tname)) continue; // BC-indexed tracks etc. already done + if (planned.count(tname)) continue; // BC-indexed tracks etc. already done if (!isCollGroupedTrackTable(tname)) continue; if (isPasteJoinChild(tname)) continue; // children follow their parent below if (!src->GetBranch("fIndexCollisions")) continue; @@ -960,33 +1066,23 @@ static void stage1b_reorderTrackTables( rowOrder.reserve(nSrc); for (auto &e : entries) rowOrder.push_back(e.srcRow); - // Reorder rows and remap fIndexCollisions values through collPerm. - PermMap perm = rewriteTable(src, dirOut, rowOrder, "fIndexCollisions", *collPermP); - allPerms[tname] = std::move(perm); - written.insert(tname); + // Publish the permutation now; the actual write (with the full set of + // remaps, this table's own included) happens in processDF once every + // permutation in the DF is known. + allPerms[tname] = permFromRowOrder(nSrc, rowOrder); + plans.push_back({tname, std::move(rowOrder)}); + planned.insert(tname); } } -static void processPasteJoinTables( - TDirectory *dirIn, TDirectory *dirOut, - const std::unordered_map &allPerms, - const std::unordered_set &alreadyWritten, - const PermMap *bcPermP = nullptr) { - - // Pre-locate the parent permutations that paste-join children may want to - // apply to their own index columns. Any of these may legitimately be null - // (e.g. mcParticlePerm absent if there is no O2mcparticle in this DF). - const PermMap *mcParticlePerm = findPermByPrefix(allPerms, "O2mcparticle"); - const PermMap *mcCollPermP = findPermByPrefix(allPerms, "O2mccollision_"); - const PermMap *collPermP = findPermByPrefix(allPerms, "O2collision_"); - // Track tables reordered in Stage 1b: every reference into them must be - // remapped through their permutation (null if the table is absent / wasn't - // reordered, in which case no remap is needed). - const PermMap *trkPerm = findPermByPrefix(allPerms, "O2track_iu"); - const PermMap *mftPerm = findPermByPrefix(allPerms, "O2mfttrack"); - const PermMap *fwdPerm = findPermByPrefix(allPerms, "O2fwdtrack"); - // bcPermP is passed in from processDF (the BC table is the only stage - // whose permutation isn't already published in allPerms). +// Plan every table not yet claimed by an earlier stage: paste-join children +// follow their parent's row order, everything else keeps its own. The index +// columns they carry are NOT enumerated here any more — buildRemaps() derives +// them from kIndexRefs when processDF writes. +static void planRemainingTables( + TDirectory *dirIn, std::vector &plans, + std::unordered_map &allPerms, + std::unordered_set &planned) { TIter it(dirIn->GetListOfKeys()); while (TKey *key = static_cast(it())) { @@ -996,100 +1092,56 @@ static void processPasteJoinTables( if (!src) continue; std::string tname = src->GetName(); - if (alreadyWritten.count(tname)) continue; + if (planned.count(tname)) continue; if (isBCTable(tname.c_str())) continue; // Stage-1 BC-indexed and Stage-2 MCColl-indexed non-paste-join tables are - // already in alreadyWritten. A paste-join child carrying its own MCColl - // index (e.g. O2mccollisionlabel) was deferred from stage2 and lands here. + // already planned. A paste-join child carrying its own MCColl index + // (e.g. O2mccollisionlabel) was deferred from stage2 and lands here. if (bcIndexBranch(src)) continue; if (mcCollIndexBranch(src) && !isPasteJoinChild(tname)) continue; - // Build value-wise extra remaps for any index column this table carries - // that points into a table whose row order may have changed. - std::vector extraRemaps; - if (mcParticlePerm) { - if (src->GetBranch("fIndexMcParticles")) - extraRemaps.push_back({"fIndexMcParticles", mcParticlePerm}); - if (src->GetBranch("fIndexArrayMcParticles")) - extraRemaps.push_back({"fIndexArrayMcParticles", mcParticlePerm}); - } - if (mcCollPermP && src->GetBranch("fIndexMcCollisions")) - extraRemaps.push_back({"fIndexMcCollisions", mcCollPermP}); - if (collPermP && src->GetBranch("fIndexCollisions")) - extraRemaps.push_back({"fIndexCollisions", collPermP}); - if (bcPermP) { - // BC-pointing indices that weren't already remapped in Stage 1. - // Stage-1 BC-indexed tables (with fIndexBCs / fIndexBC) are in - // alreadyWritten by now, so this only fires for tables that escaped - // Stage 1 — chiefly the O2ambiguous* family, which carries the SOA - // SLICE_INDEX_COLUMN(BC, bc) stored on disk as fIndexSliceBCs[2]/I. - // After BC dedup the slice endpoints would otherwise point past the - // compacted BC table; remapping through bcPerm fixes this. - if (src->GetBranch("fIndexSliceBCs")) - extraRemaps.push_back({"fIndexSliceBCs", bcPermP}); - if (src->GetBranch("fIndexBCs")) - extraRemaps.push_back({"fIndexBCs", bcPermP}); - if (src->GetBranch("fIndexBC")) - extraRemaps.push_back({"fIndexBC", bcPermP}); - } - - // Track-pointing indices: the track tables may have been reordered in - // Stage 1b, so every reference into them must be remapped through the - // corresponding permutation. (No-op when the perm is null / absent.) - auto addTrkRemap = [&](const char *br, const PermMap *pm) { - if (pm && src->GetBranch(br)) extraRemaps.push_back({br, pm}); - }; - addTrkRemap("fIndexTracks", trkPerm); - addTrkRemap("fIndexTracks_0", trkPerm); - addTrkRemap("fIndexTracks_1", trkPerm); - addTrkRemap("fIndexTracks_2", trkPerm); - addTrkRemap("fIndexTracks_Pos", trkPerm); - addTrkRemap("fIndexTracks_Neg", trkPerm); - addTrkRemap("fIndexTracks_ITS", trkPerm); - addTrkRemap("fIndexMFTTracks", mftPerm); - addTrkRemap("fIndexFwdTracks", fwdPerm); - addTrkRemap("fIndexFwdTracks_MatchMCHTrack", fwdPerm); + Long64_t nSrc = src->GetEntries(); // Find a paste-join parent for this table (kPasteJoins lookup). const PermMap *parentPerm = nullptr; std::string parentName; for (auto &[pastePrefix, parentPrefix] : kPasteJoins) { if (!TString(tname.c_str()).BeginsWith(pastePrefix.c_str())) continue; - parentPerm = findPermByPrefix(allPerms, parentPrefix.c_str(), &parentName); + parentPerm = findPermFor(allPerms, {parentPrefix}, &parentName); if (parentPerm) break; } + std::vector rowOrder; if (parentPerm) { + // perm[srcRow] = outRow (-1 = dropped); invert it into an output order. + std::vector> pairs; + pairs.reserve(parentPerm->size()); + for (Long64_t srcRow = 0; srcRow < (Long64_t)parentPerm->size(); ++srcRow) + if ((*parentPerm)[srcRow] >= 0) pairs.push_back({(*parentPerm)[srcRow], srcRow}); + std::sort(pairs.begin(), pairs.end()); + rowOrder.reserve(pairs.size()); + for (auto &p : pairs) rowOrder.push_back(p.second); + } + + if (parentPerm && (Long64_t)rowOrder.size() == nSrc) { std::cout << " Paste-join: " << tname << " follows " << parentName << "\n"; - auto rowOrder = rowOrderFromPerm(*parentPerm); - if ((Long64_t)rowOrder.size() != src->GetEntries()) { + } else { + if (parentPerm) { + // Schema drift: the child cannot be aligned to its parent. Keep its own + // row order (the index remaps still get applied at write time, so at + // least no value is left dangling) and shout — the validator's + // paste-join parity check will fail on the output. std::cerr << " [warn] paste-join size mismatch: " << tname - << " has " << src->GetEntries() << " rows but parent perm covers " - << rowOrder.size() << " — cloning as-is\n"; - dirOut->cd(); - TTree *c = src->CloneTree(-1, "fast"); - c->SetDirectory(dirOut); - c->Write(); - } else { - rewriteTable(src, dirOut, rowOrder, "", {}, extraRemaps); + << " has " << nSrc << " rows but parent perm covers " + << rowOrder.size() << " — keeping own row order\n"; } - } else if (!extraRemaps.empty() || isPasteJoinChild(tname)) { - // Parent wasn't reordered (or not present in this DF) — keep row order - // identical but still apply value-wise index remaps and follow the - // paste-join 1:1 invariant by going through the identity row order. - std::cout << " Identity-order remap: " << tname << "\n"; - Long64_t n = src->GetEntries(); - std::vector identity(n); - std::iota(identity.begin(), identity.end(), 0LL); - rewriteTable(src, dirOut, identity, "", {}, extraRemaps); - } else { - // No paste-join and no index remapping needed — fast clone - std::cout << " Copy (no dependency): " << tname << "\n"; - dirOut->cd(); - TTree *c = src->CloneTree(-1, "fast"); - c->SetDirectory(dirOut); - c->Write(); + rowOrder.resize(nSrc); + std::iota(rowOrder.begin(), rowOrder.end(), 0LL); } + + allPerms[tname] = permFromRowOrder(nSrc, rowOrder); + plans.push_back({tname, std::move(rowOrder)}); + planned.insert(tname); } } @@ -1152,52 +1204,86 @@ static void processDF(TDirectory *dirIn, TDirectory *dirOut) { return; } + // ========================================================================== + // PLAN PHASE — every stage only decides row orders and publishes the + // resulting permutations. Nothing is written yet, because a table's index + // columns may point at tables planned later in the same or a later stage + // (O2fwdtrack -> O2mfttrack, and O2fwdtrack -> itself). + // ========================================================================== + std::vector plans; + std::unordered_map allPerms; + std::unordered_set planned; + // ---- Stage 0: sort & deduplicate BCs ---- std::cout << "-- Stage 0: BCs --\n"; - dirOut->cd(); - BCStage0Result s0 = stage0_sortBCs(treeBCs, dirOut); - if (treeFlags) stage0_copyBCFlags(treeFlags, dirOut, s0.bcPerm); - - // Track which tree names have been written so we don't double-write - std::unordered_set written; - written.insert(treeBCs->GetName()); - if (treeFlags) written.insert(treeFlags->GetName()); + BCStage0Result s0 = stage0_sortBCs(treeBCs, plans); + if (treeFlags) stage0_copyBCFlags(treeFlags, plans, s0.bcPerm); + planned.insert(treeBCs->GetName()); + if (treeFlags) planned.insert(treeFlags->GetName()); + // bcPerm is a dedup map (several old rows collapse onto one new row), not a + // plain row permutation — which is exactly the mapping references into the BC + // table need. Publishing it under the BC tree's real name lets buildRemaps() + // resolve fIndexBCs / fIndexBC / fIndexSliceBCs like any other reference. + allPerms[treeBCs->GetName()] = s0.bcPerm; // ---- Stage 1: BC-indexed tables (including MCCollisions dedup) ---- std::cout << "-- Stage 1: BC-indexed tables --\n"; - auto stage1Perms = stage1_BCindexedTables(dirIn, dirOut, s0.bcPerm); - for (auto &kv : stage1Perms) written.insert(kv.first); + stage1_BCindexedTables(dirIn, plans, allPerms, s0.bcPerm); + for (auto &kv : allPerms) planned.insert(kv.first); // ---- Stage 2: MCCollision-indexed tables ---- - // Find the MCCollision permutation from stage 1 std::cout << "-- Stage 2: MCCollision-indexed tables --\n"; - PermMap mcCollPerm; - for (auto &[tname, perm] : stage1Perms) { - if (TString(tname.c_str()).BeginsWith("O2mccollision")) { - mcCollPerm = perm; - break; - } - } - if (!mcCollPerm.empty()) { - auto stage2Perms = stage2_MCCollIndexedTables(dirIn, dirOut, mcCollPerm); - for (auto &kv : stage2Perms) { - written.insert(kv.first); - stage1Perms[kv.first] = kv.second; // merge into allPerms for paste-join lookup - } + const PermMap *mcCollPermP = findPermFor(allPerms, {"O2mccollision"}); + if (mcCollPermP) { + PermMap mcCollPerm = *mcCollPermP; // copy: allPerms grows below + stage2_MCCollIndexedTables(dirIn, plans, allPerms, mcCollPerm); + for (auto &kv : allPerms) planned.insert(kv.first); } else { std::cout << " (no MCCollision table found — skipping stage 2)\n"; } // ---- Stage 1b: regroup collision-grouped track tables ---- - // Must run after Stage 1 (needs the collision permutation) and before the - // paste-join stage (so children follow the new track order and fIndexTracks* - // references are remapped). Publishes track permutations into stage1Perms. + // Must run after Stage 1 (needs the collision permutation). std::cout << "-- Stage 1b: collision-grouped track tables --\n"; - stage1b_reorderTrackTables(dirIn, dirOut, stage1Perms, written); + stage1b_reorderTrackTables(dirIn, plans, allPerms, planned); // ---- Paste-join tables + unrelated tables ---- std::cout << "-- Paste-join and unrelated tables --\n"; - processPasteJoinTables(dirIn, dirOut, stage1Perms, written, &s0.bcPerm); + planRemainingTables(dirIn, plans, allPerms, planned); + + // ========================================================================== + // WRITE PHASE — all permutations are known, so every table can now have + // ALL of its index columns remapped, whichever table they point at. + // ========================================================================== + std::cout << "-- Writing " << plans.size() << " tables --\n"; + for (auto &plan : plans) { + TTree *src = dynamic_cast(dirIn->Get(plan.name.c_str())); + if (!src) { std::cerr << " [warn] lost tree " << plan.name << " before write\n"; continue; } + + // Drift guard: a link the registry does not know about cannot be remapped, + // so say so loudly here as well as in the validator. + for (auto &b : unregisteredIndexBranches(src)) + std::cerr << " [warn] " << plan.name << "." << b + << " is not in kIndexRefs — it will NOT be remapped\n"; + + auto remaps = buildRemaps(src, allPerms); + + bool identity = ((Long64_t)plan.rowOrder.size() == src->GetEntries()); + for (Long64_t i = 0; identity && i < (Long64_t)plan.rowOrder.size(); ++i) + if (plan.rowOrder[i] != i) identity = false; + + if (identity && remaps.empty()) { + std::cout << " Copy (no dependency): " << plan.name << "\n"; + dirOut->cd(); + TTree *c = src->CloneTree(-1, "fast"); + c->SetDirectory(dirOut); + c->Write(); + continue; + } + + std::cout << " Write: " << plan.name << " (" << remaps.size() << " index column(s))\n"; + writeTable(src, dirOut, plan.rowOrder, remaps); + } // ---- Non-tree objects (TMap metadata) ---- copyNonTreeObjects(dirIn, dirOut); @@ -1217,46 +1303,23 @@ static void processDF(TDirectory *dirIn, TDirectory *dirOut) { // (e.g. O2mccollisionlabel matches O2collision). // 4. Every fIndex* value across the DF is in range w.r.t. its referent // table (value -1 is always permitted as the "no link" sentinel). +// 5. Every fIndex* branch present is covered by kIndexRefs — an unknown link +// is a link nothing remapped. // // Returns true if all checks pass. Prints [FAIL] lines for each violation. +// +// NOTE on what these checks can and cannot see: they are all *structural*. The +// O2-7098 corruption was in-range and structurally perfect — the row numbers +// were simply the wrong ones. Catching that needs the input file too; see +// AODBcRewriterCheckLinks() at the end of this section. -// Map from fIndex* branch name to the table-name prefix it refers to. The -// match on the referent side uses TString::BeginsWith so versioned suffixes -// (O2collision_001, O2bc_001, ...) are handled. Branches not in this list -// are skipped by the range check (this includes O2mcparticle's intra-table -// fIndexArray_Mothers / fIndexSlice_Daughters, which are checked separately -// with stricter semantics in the MC-particle block). -static const std::vector> kIndexBranchToTable = { - { "fIndexBCs", "O2bc_" }, - { "fIndexBC", "O2bc_" }, - { "fIndexSliceBCs", "O2bc_" }, - { "fIndexCollisions", "O2collision_" }, - { "fIndexCollision", "O2collision_" }, - { "fIndexMcCollisions", "O2mccollision_" }, - { "fIndexMcParticles", "O2mcparticle" }, - { "fIndexArrayMcParticles", "O2mcparticle" }, - { "fIndexTracks", "O2track_iu" }, - { "fIndexTracks_0", "O2track_iu" }, - { "fIndexTracks_1", "O2track_iu" }, - { "fIndexTracks_2", "O2track_iu" }, - { "fIndexTracks_Pos", "O2track_iu" }, - { "fIndexTracks_Neg", "O2track_iu" }, - { "fIndexTracks_ITS", "O2track_iu" }, - { "fIndexFwdTracks", "O2fwdtrack" }, - { "fIndexFwdTracks_MatchMCHTrack", "O2fwdtrack" }, - { "fIndexMFTTracks", "O2mfttrack" }, - { "fIndexV0s", "O2v0_" }, - { "fIndexCascades", "O2cascade_" }, - { "fIndexDecay3Bodys", "O2decay3body" }, -}; - -// Find a tree in d whose name begins with the given prefix. Returns the -// number of entries, or -1 if not found. +// Find a tree in d whose name matches the given table prefix (see isTableNamed). +// Returns the number of entries, or -1 if not found. static Long64_t treeEntriesByPrefix(TDirectory *d, const char *prefix) { TIter it(d->GetListOfKeys()); TKey *k; while ((k = (TKey*)it())) { - if (!TString(k->GetName()).BeginsWith(prefix)) continue; + if (!isTableNamed(k->GetName(), prefix)) continue; TObject *obj = d->Get(k->GetName()); if (!obj || !obj->InheritsFrom(TTree::Class())) continue; return ((TTree*)obj)->GetEntries(); @@ -1264,6 +1327,17 @@ static Long64_t treeEntriesByPrefix(TDirectory *d, const char *prefix) { return -1; } +// Entries of the table a given index branch refers to (first prefix that +// resolves wins), or -1 if the referent is not in this DF. +static Long64_t referentEntries(TDirectory *d, + const std::vector &prefixes) { + for (auto &p : prefixes) { + Long64_t n = treeEntriesByPrefix(d, p.c_str()); + if (n >= 0) return n; + } + return -1; +} + // Generic in-range check for every fIndex* branch listed above. Reads the // branch's leaf (scalar, fixed-array, or VLA), iterates all entries, and // counts how many values are outside [-1, nReferent). @@ -1439,19 +1513,29 @@ static bool validateDF(TDirectory *d) { } } - // ---- Generic fIndex* range check ---- + // ---- Generic fIndex* range check + registry drift guard ---- // For each table in the DF, scan all fIndex* branches and confirm every // value lies in [-1, nReferent). This catches stale pointers across // tables (cross-table index drift) which a per-DF-tree-only check misses. + // + // Any fIndex* branch that is NOT in kIndexRefs is a hard failure: the + // rewriter cannot have remapped it, so if the referent table was reordered + // the column is now silently wrong. Adding the entry to kIndexRefs is the + // fix — that is the whole point of having one registry. TIter it2(d->GetListOfKeys()); TKey *k2; while ((k2 = (TKey*)it2())) { TObject *obj = d->Get(k2->GetName()); if (!obj || !obj->InheritsFrom(TTree::Class())) continue; TTree *t = (TTree*)obj; - for (auto &[branchName, referentPrefix] : kIndexBranchToTable) { + for (auto &b : unregisteredIndexBranches(t)) { + std::cerr << " [FAIL] " << t->GetName() << "." << b + << " is not registered in kIndexRefs — nothing remaps it\n"; + ok = false; + } + for (auto &[branchName, referentPrefixes] : kIndexRefs) { if (!t->GetBranch(branchName.c_str())) continue; - Long64_t nRef = treeEntriesByPrefix(d, referentPrefix.c_str()); + Long64_t nRef = referentEntries(d, referentPrefixes); if (nRef < 0) continue; // referent not in this DF; skip silently Long64_t bad = checkIndexRange(t, branchName.c_str(), nRef); if (bad > 0) { @@ -1513,6 +1597,294 @@ bool AODBcRewriterValidate(const char *fname = "AO2D_rewritten.root") { return allOk; } +// ============================================================================ +// SECTION 11b — Link preservation check (input vs output) +// ============================================================================ +// +// WHY THIS EXISTS +// +// Everything in Section 11 is a *structural* check of the output alone: are the +// BCs monotonic, are the paste-join row counts equal, is every index value in +// range. O2-7098 passed all of them. Stage 1b reordered O2fwdtrack without +// remapping fIndexMFTTracks / fIndexFwdTracks_MatchMCHTrack, so every global +// muon pointed at a perfectly valid, perfectly in-range, completely unrelated +// MFT track. No structural check can see that — you have to compare against +// the input. +// +// THE INVARIANT +// +// The rewriter permutes rows and drops duplicates. It must never change what a +// row *is* or what it *points at*. So for every table, the multiset of tuples +// +// ( payload fingerprint of the row, +// payload fingerprint of the row each index column points at, ... ) +// +// must be the same before and after — up to rows that were legitimately +// dropped. Row identity comes from the payload fingerprint (a hash of every +// non-fIndex* branch), so no permutation map, no synthetic tagging and no +// assumption about ordering is needed. It runs on real production AO2Ds. +// +// Dedup is handled by canonicalising the INPUT side: if a referenced input row +// has no counterpart in the output (it was dropped), the link is rewritten to +// the "no link" sentinel first, which is exactly what the rewriter does. A +// tuple that exists in the output but in no input row is then always a bug. + +static const ULong64_t kFnvOffset = 14695981039346656037ULL; +static const ULong64_t kFnvPrime = 1099511628211ULL; +static const ULong64_t kNullFp = 0ULL; // reserved: "points at nothing" + +static ULong64_t fnv1a(ULong64_t h, const void *data, size_t n) { + const unsigned char *p = static_cast(data); + for (size_t i = 0; i < n; ++i) { h ^= p[i]; h *= kFnvPrime; } + return h; +} + +// Per-row hash of every non-index branch. Two rows with the same fingerprint +// are interchangeable as far as this check is concerned, which is the right +// semantics: identical rows may be freely permuted among themselves. +// +// `onlyBranch`, when given, restricts the hash to that single branch. It is +// used for the BC table, and only for it: Stage 0 deduplicates BCs by +// fGlobalBC and *redirects* references onto the surviving row rather than +// nulling them, so two BC rows that share a fGlobalBC have to be treated as one +// identity here. Every other dedup in this tool drops rows and nulls the +// references, which the survivor canonicalisation below handles. +static std::vector payloadFingerprints(TTree *t, + const char *onlyBranch = nullptr) { + std::vector fps; + if (!t) return fps; + Long64_t n = t->GetEntries(); + fps.resize(n, kFnvOffset); + + auto descs = describeBranches(t); + struct IO { + BranchDesc desc; + std::vector dataBuf, countBuf; + ScalarTag countTag = ScalarTag::kUnknown; + TBranch *br = nullptr, *cntBr = nullptr; + }; + std::vector ios; + for (auto &d : descs) { + if (onlyBranch) { if (d.name != onlyBranch) continue; } + else if (d.name.rfind("fIndex", 0) == 0) continue; // index columns are not payload + IO io; io.desc = d; + io.dataBuf.assign((d.isVLA ? d.maxElems : d.nElems) * d.elemSize, 0); + io.br = t->GetBranch(d.name.c_str()); + if (d.isVLA) { + io.cntBr = t->GetBranch(d.countBranchName.c_str()); + TLeaf *cl = io.cntBr ? static_cast(io.cntBr->GetListOfLeaves()->At(0)) : nullptr; + io.countTag = cl ? tagOf(cl) : ScalarTag::kUnknown; + io.countBuf.assign(byteSize(io.countTag), 0); + } + ios.push_back(std::move(io)); + } + for (auto &io : ios) { + if (io.br) io.br->SetAddress(io.dataBuf.data()); + if (io.cntBr) io.cntBr->SetAddress(io.countBuf.data()); + } + + for (Long64_t i = 0; i < n; ++i) { + ULong64_t h = kFnvOffset; + for (auto &io : ios) { + if (io.br) io.br->GetEntry(i); + if (io.cntBr) io.cntBr->GetEntry(i); + if (io.desc.isVLA) { + Long64_t cnt = readAsInt(io.countBuf.data(), io.countTag); + h = fnv1a(h, &cnt, sizeof(cnt)); + h = fnv1a(h, io.dataBuf.data(), (size_t)std::max(0, cnt) * io.desc.elemSize); + } else { + h = fnv1a(h, io.dataBuf.data(), (size_t)io.desc.nElems * io.desc.elemSize); + } + } + fps[i] = (h == kNullFp) ? 1ULL : h; // never collide with the NULL sentinel + } + t->ResetBranchAddresses(); + return fps; +} + +// One index column of a table, resolved to the referent's fingerprint table. +struct LinkColumn { + std::string branchName; + const std::vector *refFp = nullptr; // referent fingerprints, same file + const std::unordered_set *survivors = nullptr; // input side only +}; + +// Multiset (as a count map) of the per-row tuples described at the top of this +// section, hashed down to a single 64-bit key. +static std::map linkTupleCounts( + TTree *t, const std::vector &ownFp, + const std::vector &links) { + + std::map counts; + Long64_t n = t->GetEntries(); + + struct IO { + BranchDesc desc; + std::vector dataBuf, countBuf; + ScalarTag countTag = ScalarTag::kUnknown; + TBranch *br = nullptr, *cntBr = nullptr; + const LinkColumn *link = nullptr; + }; + auto descs = describeBranches(t); + std::vector ios; + for (auto &lk : links) { + for (auto &d : descs) { + if (d.name != lk.branchName) continue; + IO io; io.desc = d; io.link = &lk; + io.dataBuf.assign((d.isVLA ? d.maxElems : d.nElems) * d.elemSize, 0); + io.br = t->GetBranch(d.name.c_str()); + if (d.isVLA) { + io.cntBr = t->GetBranch(d.countBranchName.c_str()); + TLeaf *cl = io.cntBr ? static_cast(io.cntBr->GetListOfLeaves()->At(0)) : nullptr; + io.countTag = cl ? tagOf(cl) : ScalarTag::kUnknown; + io.countBuf.assign(byteSize(io.countTag), 0); + } + ios.push_back(std::move(io)); + break; + } + } + for (auto &io : ios) { + if (io.br) io.br->SetAddress(io.dataBuf.data()); + if (io.cntBr) io.cntBr->SetAddress(io.countBuf.data()); + } + + for (Long64_t i = 0; i < n; ++i) { + ULong64_t h = fnv1a(kFnvOffset, &ownFp[i], sizeof(ULong64_t)); + for (auto &io : ios) { + if (io.br) io.br->GetEntry(i); + if (io.cntBr) io.cntBr->GetEntry(i); + int cnt = io.desc.isVLA ? (int)readAsInt(io.countBuf.data(), io.countTag) + : io.desc.nElems; + for (int j = 0; j < cnt; ++j) { + Long64_t v = readAsInt(io.dataBuf.data() + (size_t)j * io.desc.elemSize, + io.desc.tag); + ULong64_t fp = kNullFp; + if (v >= 0 && v < (Long64_t)io.link->refFp->size()) { + fp = (*io.link->refFp)[v]; + // Input side: a referent row that did not survive into the output is + // expected to become a null link, so canonicalise it to one here. + if (io.link->survivors && !io.link->survivors->count(fp)) fp = kNullFp; + } + h = fnv1a(h, &fp, sizeof(fp)); + } + } + ++counts[h]; + } + t->ResetBranchAddresses(); + return counts; +} + +static bool checkLinksDF(TDirectory *din, TDirectory *dout, const char *dfName) { + bool ok = true; + + // Collect the tables present in both files. + std::vector tables; + TIter it(din->GetListOfKeys()); + while (TKey *k = static_cast(it())) { + if (TString(k->GetClassName()) != "TTree") continue; + std::string tn = k->GetName(); + if (!dout->Get(tn.c_str())) { + std::cerr << " [FAIL] " << dfName << ": table " << tn << " missing from output\n"; + ok = false; + continue; + } + tables.push_back(tn); + } + + // Fingerprint every table in both files once — referents are looked up by name. + std::unordered_map> fpIn, fpOut; + std::unordered_map> survivors; + for (auto &tn : tables) { + const char *only = isTableNamed(tn, "O2bc") ? "fGlobalBC" : nullptr; + fpIn[tn] = payloadFingerprints(dynamic_cast(din->Get(tn.c_str())), only); + fpOut[tn] = payloadFingerprints(dynamic_cast(dout->Get(tn.c_str())), only); + survivors[tn].insert(fpOut[tn].begin(), fpOut[tn].end()); + } + + for (auto &tn : tables) { + TTree *tIn = dynamic_cast(din->Get(tn.c_str())); + TTree *tOut = dynamic_cast(dout->Get(tn.c_str())); + if (!tIn || !tOut) continue; + + if (tOut->GetEntries() > tIn->GetEntries()) { + std::cerr << " [FAIL] " << dfName << ": " << tn << " grew from " + << tIn->GetEntries() << " to " << tOut->GetEntries() << " rows\n"; + ok = false; + } + + // Resolve this table's index columns to their referent fingerprint tables. + std::vector linksIn, linksOut; + for (auto &[branchName, prefixes] : kIndexRefs) { + if (!tIn->GetBranch(branchName.c_str())) continue; + std::string refName; + for (auto &p : prefixes) { + for (auto &cand : tables) if (isTableNamed(cand, p)) { refName = cand; break; } + if (!refName.empty()) break; + } + if (refName.empty()) continue; // referent not in this DF + linksIn.push_back({branchName, &fpIn[refName], &survivors[refName]}); + linksOut.push_back({branchName, &fpOut[refName], nullptr}); + } + + auto cIn = linkTupleCounts(tIn, fpIn[tn], linksIn); + auto cOut = linkTupleCounts(tOut, fpOut[tn], linksOut); + + // Every output tuple must be accounted for by an input tuple. The reverse + // is not required: dedup legitimately removes rows. + Long64_t unexplained = 0; + for (auto &[key, nOut] : cOut) { + auto found = cIn.find(key); + Long64_t nIn = (found == cIn.end()) ? 0 : found->second; + if (nOut > nIn) unexplained += nOut - nIn; + } + if (unexplained > 0) { + std::cerr << " [FAIL] " << dfName << ": " << tn << " — " << unexplained + << " of " << tOut->GetEntries() + << " output row(s) have a payload/link combination that no input" + " row had (rows or references were mis-permuted)\n"; + ok = false; + } + } + return ok; +} + +// Compare a rewritten AO2D against the file it was produced from and verify +// that no row changed what it points at. This is the check that catches the +// O2-7098 bug class; run it whenever AODBcRewriter.C is touched. +bool AODBcRewriterCheckLinks(const char *inFileName = "AO2D_pre.root", + const char *outFileName = "AO2D_rewritten.root") { + std::cout << "Checking link preservation: " << inFileName + << " -> " << outFileName << "\n"; + if (TString(inFileName).BeginsWith("alien:") || + TString(outFileName).BeginsWith("alien:")) TGrid::Connect("alien"); + std::unique_ptr fin(TFile::Open(inFileName, "READ")); + std::unique_ptr fout(TFile::Open(outFileName, "READ")); + if (!fin || fin->IsZombie()) { std::cerr << "Cannot open " << inFileName << "\n"; return false; } + if (!fout || fout->IsZombie()) { std::cerr << "Cannot open " << outFileName << "\n"; return false; } + + bool allOk = true; + int nDF = 0; + TIter top(fin->GetListOfKeys()); + while (TKey *k = static_cast(top())) { + if (!isDF(k->GetName())) continue; + TDirectory *din = dynamic_cast(fin->Get(k->GetName())); + TDirectory *dout = dynamic_cast(fout->Get(k->GetName())); + if (!din) continue; + if (!dout) { + std::cerr << " [FAIL] " << k->GetName() << " missing from output\n"; + allOk = false; + continue; + } + allOk = checkLinksDF(din, dout, k->GetName()) && allOk; + ++nDF; + } + fin->Close(); + fout->Close(); + if (allOk) std::cout << "LINK CHECK PASSED (" << nDF << " DFs checked)\n"; + else std::cout << "LINK CHECK FAILED — see [FAIL] lines above\n"; + return allOk; +} + // ============================================================================ // SECTION 12 — Top-level entry point // ============================================================================ diff --git a/MC/utils/CLAUDE.md b/MC/utils/CLAUDE.md index 56968ed16..52bc83901 100644 --- a/MC/utils/CLAUDE.md +++ b/MC/utils/CLAUDE.md @@ -1,11 +1,59 @@ # CLAUDE.md — AODBcRewriter Development Handoff -## Current work state (handoff — 2026-06-09) - -**Branch:** `fix/aodbcrewriter-track-regroup` · **Last commit:** `8bb9d30b3c` -(committed, not yet pushed/validated). - -### The bug being fixed: tracks' `-1` collision group split +## Current work state (2026-07-28) — O2-7098 + +**Branch:** `swenzel/O2-7098-aodbcrewriter-index-remap`. + +### The bug: O2fwdtrack's match indices were reordered but not remapped + +Reported by Maurice Coquet (JIRA **O2-7098**, patch proposal +[O2DPG#2418](https://github.com/AliceO2Group/O2DPG/pull/2418)): in anchored MC +AO2Ds produced since early June 2026, `O2fwdtrack.fIndexMFTTracks` and +`O2fwdtrack.fIndexFwdTracks_MatchMCHTrack` point at the wrong rows, so the MFT +leg and the MCH leg of every global muon belong to different MC particles +(`sameParticle=0`). + +**Cause.** Stage 1b (added by #2370, merged 4 Jun 2026) re-sorts `O2track_iu`, +`O2mfttrack_*` and `O2fwdtrack`, but wrote each table the moment it had planned +it. `O2fwdtrack` points at `O2mfttrack` — reordered later in the same loop — and +at *itself*; neither permutation existed yet at write time, so the two columns +kept pre-reorder row numbers. They stayed in range, so every structural check +passed. + +**Scope.** The corruption does *not* need the BC→collision reorder cascade. +Stage 1b re-sorts unconditionally to make the split `-1` group contiguous, so in +practice **every merged MC AO2D since 4 Jun 2026 is affected**. + +**Reproduced** on `example_AOD/AO2D_pre.root`: 171 fwd tracks with an MFT match, +`sameParticle` 30 → **0** with the pre-fix code, 30 → **30** with the fix. + +### What the fix does + +1. **One index registry, `kIndexRefs` (Section 1).** Every `fIndex*` column and + the table it points at, in one place, used by the rewriter, the validator and + the drift guard alike. Referent resolution is `isTableNamed()` (`K == P` or + `K == P_`), which — unlike the old `BeginsWith` — keeps + `O2mfttrack_001` apart from `O2mfttrackcov`, `O2bc_001` from `O2bcflag` and + `O2calo` from `O2calotrigger`. +2. **`buildRemaps()` (Section 3)** derives a table's complete remap list from + that registry. The privileged "primary index + optional extras" split in + `rewriteTable` is gone — there is one `writeTable(src, dirOut, rowOrder, + remaps)` and every column goes through the same path. Forgetting a column is + no longer possible; the old design *required* each stage to remember. +3. **Plan, then write (Sections 3b + 10).** Every stage now only appends a + `TablePlan` and publishes its permutation; `processDF` writes all plans + afterwards, when every permutation in the DF is known. This is what makes the + forward reference (fwd→mft) and the self reference (fwd→fwd) work. +4. **Drift guard.** Any `fIndex*` branch not in `kIndexRefs` is a `[warn]` at + rewrite time and a `[FAIL]` in the validator. Schema growth now breaks the + test instead of silently mis-linking data. +5. **`AODBcRewriterCheckLinks()` (Section 11b)** — the check that can actually + see this bug class; see "Testing" below. + +Maurice's #2418 has the same shape (defer `O2fwdtrack`, remap via the MFT/Fwd +perms) and is correct; this generalises it from one table to all of them. + +### Previous work state (2026-06-09): tracks' `-1` collision group split Downstream O2 analysis (`o2-analysis-event-selection`) was crashing with: @@ -51,24 +99,71 @@ and forces a reorder cascade **BC (Stage 0) → collisions (Stage 1) → tracks **not** re-introduce an "assert already sorted / order-preserving" Stage 0 — it is a known dead end (see the history note in the Section 4 code comment). -### Not done yet / next steps for whoever picks this up - -- **UNVALIDATED on real data.** Parses cleanly in cling (`.L AODBcRewriter.C`), - but has *not* been run on a real merged AO2D, nor through the analysis task - that crashed. The macro is interpreter-only; `.L AODBcRewriter.C+` (ACLiC) - fails on missing std includes — pre-existing, not a regression. - Test sequence: `AODBcRewriter("AO2D.root","out.root")` → - `AODBcRewriterValidate("out.root")` (expect no `[FAIL]`) → then the **real** - `o2-analysis-event-selection` on `out.root` (ground truth). -- **Known fragility (whack-a-mole):** the `fIndexTracks*` reference remap is a - hardcoded enumeration in `processPasteJoinTables` and the validator's - `kIndexBranchToTable`. A missed reference into a reordered track table = silent - corruption. Longer term, *derive* index→referent relationships from the AO2D - column-name conventions instead of enumerating. -- **Biggest gap:** there is no executable analysis-level CI for this tool, so - regressions are only found in production with delay. Building a reproducer - (merged AO2D that triggers the split/abort) + a CI check that runs the real - task is the agreed top priority after this fix lands. +### Status of the 2026-06-09 items + +- ~~**UNVALIDATED on real data.**~~ Now run on `example_AOD/AO2D_pre.root`, + `bigger/` and `bigger2/` (3–4 DFs each, up to 6M MC particles): rewrite clean, + `AODBcRewriterValidate` and `AODBcRewriterCheckLinks` both pass. The macro is + still interpreter-only; `.L AODBcRewriter.C+` (ACLiC) fails on missing std + includes — pre-existing, not a regression. +- ~~**Known fragility (whack-a-mole).**~~ Resolved by `kIndexRefs` + + `buildRemaps()` + the drift guard (see above). There is now exactly one list, + and an entry missing from it fails the test rather than corrupting output. +- ~~**Biggest gap: no executable CI.**~~ `MC/utils/tests/` now provides one that + needs nothing but ROOT and runs in seconds. Still open: wiring it into the + ALICE CI job list — `.github/workflows/` has no ROOT runner, so it needs a + line wherever `test/run_*_tests.sh` are invoked. **Confirm with the O2DPG CI + owners rather than guessing.** + +--- + +## Testing + +```bash +${O2DPG_ROOT}/MC/utils/tests/run_aodbcrewriter_tests.sh +``` + +Needs only ROOT — no simulation, no O2Physics, no GRID, no committed binary +fixture. `makeTestAOD.C` builds a few-kB synthetic AO2D carrying every pathology +the tool exists to repair (non-monotonic + duplicate BCs, duplicate MC +collisions, a split `-1` track group from two sub-timeframe blocks, fwd↔MFT and +fwd→fwd matches, V0/cascade links, `fIndexSliceBCs`, mcparticle mother/daughter +links, and a link-free table for the fast-clone path). + +Three independent layers, in increasing specificity: + +| layer | what it sees | +|---|---| +| `AODBcRewriterValidate(out)` | structural invariants of the output alone | +| `AODBcRewriterCheckLinks(in, out)` | input vs output: no row changed what it points at | +| `testAODBcRewriter(in, out)` | named, physics-readable assertions | + +**The middle layer is the one that matters for this bug class.** Everything in +`AODBcRewriterValidate` is structural, and O2-7098 was structurally perfect — +the row numbers were simply the wrong ones, and in range. Catching that requires +comparing against the input. `AODBcRewriterCheckLinks` does it without +permutation dumps or synthetic tagging: it fingerprints each row from its +non-`fIndex*` branches and compares, per table, the multiset of + +``` +( payload fingerprint of the row, payload fingerprint of each row it points at ) +``` + +before and after. Dedup is handled by canonicalising the *input* side — a +referenced row that did not survive becomes the null link, which is what the +rewriter does — so a tuple present in the output but in no input row is always a +bug. The BC table is fingerprinted on `fGlobalBC` alone, because Stage 0 +*redirects* references onto the surviving row rather than nulling them. + +Because it needs no synthetic data, run it on real production files too: + +```cpp +root -l -b -q -e '.L AODBcRewriter.C' \ + -e 'AODBcRewriterCheckLinks("AO2D_pre.root","AO2D.root")' +``` + +Regression-locked: against the pre-fix `master` rewriter the suite reports +`O2fwdtrack: links not preserved` and `MFT-MCH match: sameParticle=1/6`. --- @@ -152,35 +247,44 @@ order strictly follow its paste-join parent. | Section | Function(s) | Purpose | |---------|-------------|---------| -| 1 | `PermMap`, `isBCTable`, `bcIndexBranch`, `mcCollIndexBranch`, `collIndexBranch`, `kPasteJoins`, `isPasteJoinChild` | Core types, name-probe helpers, and the authoritative paste-join list | +| 1 | `PermMap`, `isBCTable`, `bcIndexBranch`, `mcCollIndexBranch`, `collIndexBranch`, `kPasteJoins`, `isPasteJoinChild`, **`kIndexRefs`**, **`isTableNamed`**, **`unregisteredIndexBranches`** | Core types, name-probe helpers, the paste-join list, and the authoritative index-reference registry | | 2 | `ScalarTag`, `tagOf`, `byteSize`, `readAsInt`, `writeAsInt`, `BranchDesc`, `describeBranches` | Generic ROOT branch I/O over raw byte buffers | -| 3 | `rewriteTable` | **Central engine**: writes any table in a given row order, remapping one nominated index column via a PermMap | +| 3 | **`writeTable`**, `permFromRowOrder`, `remapBuffer`, `findPermFor`, **`buildRemaps`** | **Central engine**: writes any table in a given row order, remapping *every* index column it carries | +| 3b | `TablePlan` | Deferred write plan — why planning and writing are separate phases | | 4 | `BCStage0Result`, `stage0_sortBCs` | Sort + deduplicate the BC table; produce `bcPerm` | -| 5 | `stage0_copyBCFlags` | Copy BC flags table following BC row selection | -| 6 | `MCCollKey`, `MCCollKeyHash`, `stage1_BCindexedTables` | Process all BC-indexed tables; deduplicate MCCollisions | -| 7 | `stage2_MCCollIndexedTables` | Process all MCCollision-indexed tables; drop rows whose parent was deduped | +| 5 | `stage0_copyBCFlags` | Plan the BC flags table following BC row selection | +| 6 | `MCCollKey`, `MCCollKeyHash`, `stage1_BCindexedTables` | Plan all BC-indexed tables; deduplicate MCCollisions | +| 7 | `stage2_MCCollIndexedTables` | Plan all MCCollision-indexed tables; drop rows whose parent was deduped | | 9b | `isCollGroupedTrackTable`, `stage1b_reorderTrackTables` | **Stage 1b**: regroup collision-grouped track tables (`O2track_iu`, `O2mfttrack`, `O2fwdtrack`) by remapped `fIndexCollisions` (`-1` sinks to a contiguous tail); publish track perms so children/references follow. Restores the O2 slicing invariant after the BC→collision reorder cascade | -| 8 | `rowOrderFromPerm`, `findPermByPrefix`, `processPasteJoinTables` | Reorder paste-joined tables to follow their parent (1:1 row count guaranteed); remap any of their own index columns value-wise (incl. `fIndexTracks*` via the Stage 1b track perms); copy unrelated tables verbatim | +| 8 | `planRemainingTables` | Paste-joined tables follow their parent's row order; everything else keeps its own | | 9 | `copyNonTreeObjects` | Copy TMap metadata and other non-TTree objects | -| 10 | `processDF` | Orchestrates all stages for one `DF_*` directory | -| 11 | `AODBcRewriter` | Top-level entry: opens files, iterates `DF_*` dirs, preserves compression | +| 10 | `processDF` | Orchestrates the **plan phase** then the **write phase** for one `DF_*` directory | +| 11 | `AODBcRewriterValidate` and helpers | Structural validation of an output file | +| 11b | `payloadFingerprints`, `linkTupleCounts`, **`AODBcRewriterCheckLinks`** | Input-vs-output link preservation — the check that sees the O2-7098 bug class | +| 12 | `AODBcRewriter` | Top-level entry: opens files, iterates `DF_*` dirs, preserves compression | -### `rewriteTable` — the central engine +### `writeTable` — the central engine ```cpp -PermMap rewriteTable(TTree *src, TDirectory *dirOut, - const vector &rowOrder, - const string &indexBranch, - const PermMap &parentPerm); +void writeTable(TTree *src, TDirectory *dirOut, + const vector &rowOrder, + const vector &remaps); ``` - `rowOrder`: which source rows to emit and in what sequence (may be a subset for deduplication, or reordered for sorting) -- `indexBranch`: name of the one index column to remap (e.g. `"fIndexBCs"`), - or `""` for none -- `parentPerm`: the PermMap from the parent stage used to translate the old - index value to a new one -- Returns `srcToOut` PermMap: `srcToOut[srcRow] = outRow`, -1 if dropped +- `remaps`: **every** index column to remap, each with its own PermMap — + obtained from `buildRemaps(src, allPerms)` and therefore from `kIndexRefs` + +There is deliberately **no privileged "primary index"** parameter any more. The +old signature took one nominated index column plus an optional list of "extras", +which meant every stage had to remember to populate the extras. Stage 1b did +not, and that is O2-7098. Do not reintroduce the distinction. + +The row permutation a `rowOrder` implies is `permFromRowOrder(nSrc, rowOrder)` +(`perm[srcRow] = outRow`, -1 if dropped). Stages publish it into `allPerms` +*before* anything is written, which is what lets a table's remaps refer to +tables written later — or to itself. The function handles both scalar branches and VLA (variable-length array) branches generically. For VLAs it pre-scans the count branch to find the @@ -188,6 +292,21 @@ maximum array length and allocates buffers accordingly. Input and output branches share the same raw byte buffers; ROOT handles the VLA count implicitly through the shared count buffer. +### The two phases in `processDF` + +``` +PLAN stage0 -> stage1 -> stage2 -> stage1b -> planRemainingTables + each appends a TablePlan and publishes its PermMap into allPerms +WRITE for every plan: buildRemaps(src, allPerms) then writeTable(...) + (identity row order and no remaps -> fast CloneTree instead) +``` + +Note `allPerms` holds **reference-remapping** semantics, not plain row +permutations: for the BC table it is `bcPerm`, where several old rows collapse +onto the surviving one, because that is what references into a deduplicated BC +table need. For `O2mccollision` a dropped duplicate maps to -1 instead — that is +the existing, deliberate behaviour (see resolved gap 7), not an oversight. + --- ## MCCollision deduplication @@ -212,18 +331,41 @@ row whose `fIndexMcCollisions` pointed to a dropped row is also dropped. These were identified during the refactor but not yet implemented: -### 1. `fIndexCollisions` inside `O2mccollision` is not remapped +### 1. Tables SORTED BY a reference into a reordered table are not re-sorted -`O2mccollision` has both `fIndexBCs` (handled) and `fIndexCollisions` (linking -back to the reconstructed `O2collision` row). After Stage 1 reorders -`O2collision`, this second index in `O2mccollision` becomes stale. +**This is the remaining member of the O2-7098 family — open, and the natural +follow-up PR.** Stage 1b reorders `O2track_iu` / `O2mfttrack` / `O2fwdtrack`. +Several other tables are *stored sorted by* a reference into one of those (or +into `O2collision`). Their values are now correctly remapped, but their rows are +left where they were, so the ordering is destroyed: -**Fix**: After `stage1_BCindexedTables` runs, find `collPerm` (the PermMap for -`O2collision_*`) in `stage1Perms`, then apply a second `rewriteTable` pass on -`O2mccollision_*` to remap `fIndexCollisions` via `collPerm`. The -`ExtraRemap` mechanism in `rewriteTable` already supports this pattern. +| table | key | sorted in input? | +|---|---|---| +| `O2fwdtrkcl` | `fIndexFwdTracks` | yes | +| `O2ambiguoustrack` | `fIndexTracks` | yes | +| `O2trackqa_003` | `fIndexTracks` | yes | +| `O2v0_002`, `O2cascade_001`, `O2decay3body` | `fIndexCollisions` | yes | +| `O2mfttrackcov` | `fIndexMFTTracks` | **no** — so not an invariant there | -### 2. Deduplication key could be strengthened +(measured on `example_AOD/AO2D_pre.root`.) This is the same failure mode as the +split `-1` group that produced the `ArrowTableSlicingCache::validateOrder` +FATAL — not yet observed in the wild, but structurally identical. + +**Suggested fix** — data-derived rather than another hardcoded list: *if `T.B` +was sorted in the input and `B`'s referent was reordered, re-sort `T` by the +remapped `B`.* That rule reproduces Stage 1b's behaviour and correctly leaves +`O2mfttrackcov` alone. It is deliberately **not** in the O2-7098 fix: it changes +the row order of tables that are currently untouched, so it deserves its own +review. `AODBcRewriterCheckLinks` is already in place to police it. + +### ~~2. `fIndexCollisions` inside `O2mccollision` is not remapped~~ (MOOT) + +Checked against three real AO2Ds (`example_AOD/`, `bigger/`, `bigger2/`): +`O2mccollision_001` carries **only** `fIndexBCs` in the current schema, so there +is nothing to remap. Should the column reappear, `kIndexRefs` now handles it +with no code change. + +### 3. Deduplication key could be strengthened The current `(newBCrow, fEventWeight)` key is a good heuristic. A more robust key would additionally include `fImpactParameter` and/or `fGeneratorsID` if @@ -231,20 +373,21 @@ those branches are present. Consider making the key construction a small helper function that probes which fields are available and builds the strongest possible key. -### 3. `O2mccollision` has two potential parents for paste-join lookup +### ~~4. `O2mccollision` has two potential parents for paste-join lookup~~ (PARTLY RESOLVED) -In `processDF`, the MCColl PermMap is extracted by scanning `stage1Perms` for -a name beginning with `"O2mccollision"`. If the DF contains both -`O2mccollision_000` and `O2mccollision_001` (schema version coexistence), -only the first found is used. Add a warning and handle this explicitly if it -becomes relevant. +Referent resolution now goes through `findPermFor` / `isTableNamed`, which +matches `P` or `P_` and, when several schema versions coexist, picks the +lexicographically smallest name **deterministically** instead of whatever the +`unordered_map` happened to yield first. Still no warning for the ambiguous +case — add one if schema-version coexistence ever becomes real. -### 4. Paste-join size-mismatch fallback is silent-ish +### 5. Paste-join size-mismatch fallback is silent-ish When a paste-joined table has a different row count from its parent (schema -drift), the tool falls back to `CloneTree(-1, "fast")` and prints a warning. -This produces a structurally inconsistent output. Consider making this a hard -error, or implement a best-effort row-count reconciliation. +drift), the tool now keeps the child's own row order — index remaps still get +applied, so nothing is left dangling — and prints a `[warn]`. The output is +still structurally inconsistent and the validator's paste-join parity check will +`[FAIL]` on it. Consider making it a hard error, or reconciling row counts. ### ~~5. No validation pass~~ (RESOLVED) @@ -258,11 +401,14 @@ to confirm output correctness. This was the root cause of the O2Physics FATAL `MC particle N has daughter with index M > MC particle table size`. After Stage 2 reorders `O2mcparticle`, the intra-table mother/daughter indices -now get remapped via `ExtraRemap` in the same pass (Section 7). +get remapped through the table's own permutation. `fIndexMcParticles` in label tables (`O2mctracklabel`, `O2mcfwdtracklabel`, -`O2mcmfttracklabel`, `O2mccalolabel`) is also now remapped via the MC-particle -permutation in `processPasteJoinTables` (Section 8). +`O2mcmfttracklabel`, `O2mccalolabel`) is remapped via the MC-particle +permutation. + +*(Since O2-7098 both are plain `kIndexRefs` entries and need no special +casing — `buildRemaps` picks them up like any other column.)* ### ~~8. fIndexSliceBCs in O2ambiguous* not remapped after BC dedup~~ (RESOLVED) @@ -273,12 +419,12 @@ table. It appears in `O2ambiguoustrack`, `O2ambiguousmfttr`, were processed by Stage 1. After BC dedup the slice endpoints would then point past the compacted table. -**Fix**: `processPasteJoinTables` now also accepts the BC permutation -(passed explicitly from `processDF`) and applies it value-wise to any -`fIndexSliceBCs` / `fIndexBCs` / `fIndexBC` column it finds. Validated -against `example_AOD/AO2D_pre.root`: pre-fix the rewritten output had 7 -and 19 out-of-range slice endpoints in DF_3594457012003; post-fix the -validator reports zero. +**Fix**: `bcPerm` is published into `allPerms` under the BC tree's name, so +`buildRemaps` applies it to any `fIndexSliceBCs` / `fIndexBCs` / `fIndexBC` +column it finds. Validated against `example_AOD/AO2D_pre.root`: pre-fix the +rewritten output had 7 and 19 out-of-range slice endpoints in +DF_3594457012003; post-fix the validator reports zero. `testAODBcRewriter` +also asserts the endpoints still name the same bunch crossings. ### ~~7. Paste-join row-count drift on MC-collision dedup~~ (RESOLVED) @@ -291,7 +437,7 @@ to downstream "O2collision_001 is one larger than O2mccollisionlabel" crashes. **Fix**: `kPasteJoins` was extended to cover every joined pair from `AnalysisDataModel.h`. Paste-join children are now *deferred* from Stage 2 -to `processPasteJoinTables`, where they take the parent's row order and have +to `planRemainingTables`, where they take the parent's row order and have their own index columns remapped value-wise. Rows that lose their MC label on dedup now correctly produce `fIndexMcCollisions == -1`, and the row count matches the parent collision table. @@ -309,28 +455,48 @@ group — including the `-1` ambiguous group — stays one contiguous run, as O2 `ArrowTableSlicingCache::validateOrder` requires. **Fix:** Stage 1b (`stage1b_reorderTrackTables`, Section 9b) stable-sorts each -track table by remapped `fIndexCollisions` (`-1` to a contiguous tail), publishes -the track perm, and `processPasteJoinTables` follows it for paste-join children -and remaps every `fIndexTracks*` reference. New validator check +track table by remapped `fIndexCollisions` (`-1` to a contiguous tail) and +publishes the track perm; paste-join children follow it and every +`fIndexTracks*` reference is remapped through it. Validator check `checkCollisionGroupContiguity` flags split groups as `[FAIL] ... fIndexCollisions has N group(s) split into non-contiguous runs`. -**Status:** committed on `fix/aodbcrewriter-track-regroup` (`8bb9d30b3c`), parses -in cling, **not yet run on a real merged AO2D or the failing analysis task.** +**Status:** on `master` since `5597f516`, now run on real merged AO2Ds. Note +this fix is what *introduced* gap 10 below. + +### ~~10. O2fwdtrack match indices reordered but not remapped (O2-7098)~~ (RESOLVED) + +See the **Current work state** section at the top. Stage 1b wrote each table as +soon as it had planned it, so `O2fwdtrack`'s `fIndexMFTTracks` (pointing at a +table reordered later in the same loop) and `fIndexFwdTracks_MatchMCHTrack` +(pointing at itself) kept pre-reorder row numbers. + +**Fix:** planning and writing are now separate phases, and every index column is +derived from the single `kIndexRefs` registry by `buildRemaps` instead of being +enumerated per stage. --- ## Testing checklist -When testing a new AO2D: +Automated: `${O2DPG_ROOT}/MC/utils/tests/run_aodbcrewriter_tests.sh` (see the +**Testing** section above). Run it for any change to this tool. + +When testing a real AO2D by hand: 1. Run `AODBcRewriterValidate("AO2D_rewritten.root")` (Section 11). It checks BC monotonicity, MC-particle intra-table integrity, paste-join - row-count parity for every pair in `kPasteJoins`, and `fIndex*` value - ranges against the referent table. Failures appear as `[FAIL] ...` lines. -2. Check stdout from the rewrite run itself for any `[warn]` lines — these - indicate branches or tables that fell through to a fallback path. -3. If deduplication ran, verify the dropped count is as expected by comparing + row-count parity for every pair in `kPasteJoins`, `fIndex*` value + ranges against the referent table, and that no `fIndex*` column is missing + from `kIndexRefs`. Failures appear as `[FAIL] ...` lines. +2. Run `AODBcRewriterCheckLinks("AO2D_pre.root","AO2D.root")` (Section 11b). + **Do not skip this one** — it is the only check that compares against the + input, and therefore the only one that can see a mis-remapped index. + Roughly 30 s for a 3-DF file with 6M MC particles. +3. Check stdout from the rewrite run itself for any `[warn]` lines — these + indicate branches or tables that fell through to a fallback path, or an + index column the registry does not know about. +4. If deduplication ran, verify the dropped count is as expected by comparing the input DF MCCollision count vs. output. A standalone minimal validation script (kept here for reference; in practice diff --git a/MC/utils/tests/makeTestAOD.C b/MC/utils/tests/makeTestAOD.C new file mode 100644 index 000000000..a437328f8 --- /dev/null +++ b/MC/utils/tests/makeTestAOD.C @@ -0,0 +1,336 @@ +// makeTestAOD.C +// +// Builds a tiny synthetic AO2D that reproduces, in a few kB, every pathology +// AODBcRewriter.C exists to repair — so the tool can be tested in seconds +// without a simulation, a GRID job or a committed binary fixture. +// +// root -l -b -q 'makeTestAOD.C("AO2D_test.root")' +// +// ----------------------------------------------------------------------------- +// WHAT THE FIXTURE CONTAINS +// ----------------------------------------------------------------------------- +// One DF holding two "sub-timeframe" blocks, as o2-aod-merger produces when MC +// timeframes are merged into the parent data file's DF (data-embedding +// anchoring). That layout is what makes all of the following true at once: +// +// * fGlobalBC is non-monotonic — block B's BCs are earlier than block A's +// * fGlobalBC has a duplicate — BC 201 appears in both blocks +// * MCCollisions have a duplicate — same (BC, fEventWeight) in both blocks +// * the tracks' "-1" ambiguous group is SPLIT into two runs, one per block +// +// so the BC sort cascades into a collision reorder, which cascades into a track +// regroup. Every table then has to follow, and every index column has to be +// remapped — which is the whole job. +// +// Row payloads are unique by construction (fX = 10+i for tracks, 1000+i for MFT +// tracks, 2000+i for fwd tracks, ...) so a test can state ground truth as +// "fwd track 2000+i must still point at MFT track 1000+i" and check it after +// the rows have moved. +// +// Cross-table links present here, and why each one matters: +// +// O2fwdtrack.fIndexMFTTracks -> another Stage-1b table +// O2fwdtrack.fIndexFwdTracks_MatchMCHTrack -> ITSELF +// These two are O2-7098. Both referents are reordered in the same stage +// as the referrer, so they only work if writing is deferred until every +// permutation is known. +// O2v0/O2cascade fIndexTracks_Pos/_Neg/fIndexTracks, fIndexV0s +// references into reordered tables from tables that are not reordered. +// O2ambiguoustrack.fIndexSliceBCs -> a [2] slice into a DEDUPLICATED +// table; endpoints must follow the compaction. +// O2mcparticle fIndexArray_Mothers (VLA) / fIndexSlice_Daughters ([2]) +// intra-table links that must follow the table's own reordering. +// paste-join children (O2trackextra_002, O2fwdtrackcov, O2mc*label) +// no index column of their own for the row order — they must follow +// their parent row-for-row. +// O2origin +// no links at all — exercises the fast-clone path. + +#ifndef __CLING__ +#include "TFile.h" +#include "TTree.h" +#include +#include +#include +#endif + +namespace +{ + +// --------------------------------------------------------------------------- +// The fixture layout, spelled out once so tests can refer to it. +// +// BCs (input row -> fGlobalBC): block A: 0:200 1:201 2:202 +// block B: 3:100 4:101 5:201 (5 duplicates 1) +// -> after Stage 0: [100,101,200,201,202], bcPerm = [2,3,4,0,1,3] +// +// Collisions: 0->bc0 1->bc2 | 2->bc3 3->bc4 +// -> remapped to 2,4,0,1 -> reordered to [2,3,0,1] +// +// MCCollisions: 0->bc0 w1.5 1->bc1 w2.5 | 2->bc3 w3.5 3->bc4 w4.5 +// 4->bc5 w2.5 <- same (newBC,weight) as 1: dropped as duplicate +// +// Tracks: A: 0,1 -> coll0 2 -> coll1 3 -> -1 +// B: 4 -> coll2 5 -> coll3 6 -> -1 (-1 group is split) +// MFT tracks: A: 0 -> coll0 1 -> coll1 2 -> -1 +// B: 3 -> coll2 4 -> coll3 5 -> -1 +// Fwd tracks: A: 0 -> coll0 1 -> coll1 2 -> -1 +// B: 3 -> coll2 4 -> coll3 5 -> -1 +// fwd i matches MFT i (all six). fwd 1 -> fwd 0 and fwd 4 -> fwd 3 as +// MCH matches; the rest have none. +// --------------------------------------------------------------------------- + +const int kNBC = 6; +const uint64_t kGlobalBC[] = {200, 201, 202, 100, 101, 201}; + +const int kNColl = 4; +const int kCollBC[] = {0, 2, 3, 4}; + +const int kNMcColl = 5; +const int kMcCollBC[] = {0, 1, 3, 4, 5}; +const float kMcCollWeight[]= {1.5f, 2.5f, 3.5f, 4.5f, 2.5f}; + +const int kNPartPerMcColl = 3; // mother + two daughters +const int kNPart = kNMcColl * kNPartPerMcColl; + +const int kNTrack = 7; +const int kTrackColl[] = {0, 0, 1, -1, 2, 3, -1}; +const int kTrackMcPart[] = {0, 1, 3, -1, 6, 9, -1}; + +const int kNMft = 6; +const int kMftColl[] = {0, 1, -1, 2, 3, -1}; +const int kMftMcPart[] = {0, 3, -1, 6, 9, -1}; + +const int kNFwd = 6; +const int kFwdColl[] = {0, 1, -1, 2, 3, -1}; +const int kFwdMft[] = {0, 1, 2, 3, 4, 5}; // fwd i <-> MFT i +const int kFwdMatch[] = {-1, 0, -1, -1, 3, -1};// fwd -> fwd self-reference +// The MC particle of a fwd track is the one of its matched MFT track: a correct +// rewrite keeps "same particle" at 100%, a broken one collapses it to ~0 — +// exactly the metric O2-7098 was reported with. +const int kFwdMcPart[] = {0, 3, -1, 6, 9, -1}; + +const int kNV0 = 2; +const int kV0Coll[] = {0, 2}; +const int kV0Pos[] = {0, 4}; +const int kV0Neg[] = {1, 5}; + +const int kNCasc = 1; +const int kCascColl[] = {0}; +const int kCascV0[] = {0}; +const int kCascBach[] = {2}; + +const int kNAmb = 2; +const int kAmbTrack[] = {3, 6}; +const int kAmbBCFirst[] = {0, 3}; +const int kAmbBCLast[] = {2, 4}; + +} // namespace + +void makeTestAOD(const char *outFileName = "AO2D_test.root") +{ + TFile f(outFileName, "RECREATE"); + TDirectory *df = f.mkdir("DF_1000000000001"); + df->cd(); + + // ---- O2bc_001 ----------------------------------------------------------- + { + TTree t("O2bc_001", "bcs"); + uint64_t globalBC; int runNumber; uint64_t triggerMask; + t.Branch("fRunNumber", &runNumber, "fRunNumber/I"); + t.Branch("fGlobalBC", &globalBC, "fGlobalBC/l"); + t.Branch("fTriggerMask", &triggerMask, "fTriggerMask/l"); + for (int i = 0; i < kNBC; ++i) { + runNumber = 300000; globalBC = kGlobalBC[i]; triggerMask = 1000 + i; + t.Fill(); + } + t.Write(); + } + + // ---- O2bcflag (paste-joined to O2bc) ------------------------------------ + { + TTree t("O2bcflag", "bc flags"); + uint8_t flags; + t.Branch("fBCFlags", &flags, "fBCFlags/b"); + for (int i = 0; i < kNBC; ++i) { flags = (uint8_t)(i + 1); t.Fill(); } + t.Write(); + } + + // ---- O2collision_001 ---------------------------------------------------- + { + TTree t("O2collision_001", "collisions"); + int bc; float posX; + t.Branch("fIndexBCs", &bc, "fIndexBCs/I"); + t.Branch("fPosX", &posX, "fPosX/F"); + for (int i = 0; i < kNColl; ++i) { bc = kCollBC[i]; posX = 500.f + i; t.Fill(); } + t.Write(); + } + + // ---- O2mccollision_001 -------------------------------------------------- + { + TTree t("O2mccollision_001", "mc collisions"); + int bc; float weight, posX; + t.Branch("fIndexBCs", &bc, "fIndexBCs/I"); + t.Branch("fPosX", &posX, "fPosX/F"); + t.Branch("fEventWeight",&weight, "fEventWeight/F"); + for (int i = 0; i < kNMcColl; ++i) { + bc = kMcCollBC[i]; weight = kMcCollWeight[i]; + // The duplicate must be byte-identical to the row it duplicates, as it + // would be in a real merged file. + posX = (i == 4) ? 600.f + 1 : 600.f + i; + t.Fill(); + } + t.Write(); + } + + // ---- O2mcparticle_001 --------------------------------------------------- + { + TTree t("O2mcparticle_001", "mc particles"); + int mcColl, pdg, motherSize, mothers[4], daughters[2]; + t.Branch("fIndexMcCollisions", &mcColl, "fIndexMcCollisions/I"); + t.Branch("fPdgCode", &pdg, "fPdgCode/I"); + t.Branch("fIndexArray_Mothers_size",&motherSize, "fIndexArray_Mothers_size/I"); + t.Branch("fIndexArray_Mothers", mothers, "fIndexArray_Mothers[fIndexArray_Mothers_size]/I"); + t.Branch("fIndexSlice_Daughters", daughters, "fIndexSlice_Daughters[2]/I"); + for (int c = 0; c < kNMcColl; ++c) { + int base = c * kNPartPerMcColl; + for (int j = 0; j < kNPartPerMcColl; ++j) { + mcColl = c; + pdg = 100 + base + j; // unique per particle + if (j == 0) { // the mother: daughters are the next two rows + motherSize = 0; + daughters[0] = base + 1; + daughters[1] = base + 2; + } else { // a daughter: its mother is the first row + motherSize = 1; + mothers[0] = base; + daughters[0] = -1; + daughters[1] = -1; + } + t.Fill(); + } + } + t.Write(); + } + + // ---- O2track_iu + paste-join children ----------------------------------- + { + TTree t("O2track_iu", "tracks"); + int coll; float x; + t.Branch("fIndexCollisions", &coll, "fIndexCollisions/I"); + t.Branch("fX", &x, "fX/F"); + for (int i = 0; i < kNTrack; ++i) { coll = kTrackColl[i]; x = 10.f + i; t.Fill(); } + t.Write(); + } + { + TTree t("O2trackextra_002", "track extras"); + uint8_t nClsFindable; + t.Branch("fTPCNClsFindable", &nClsFindable, "fTPCNClsFindable/b"); + for (int i = 0; i < kNTrack; ++i) { nClsFindable = (uint8_t)(50 + i); t.Fill(); } + t.Write(); + } + { + TTree t("O2mctracklabel", "track mc labels"); + int mcPart; uint16_t mask; + t.Branch("fIndexMcParticles", &mcPart, "fIndexMcParticles/I"); + t.Branch("fMcMask", &mask, "fMcMask/s"); + for (int i = 0; i < kNTrack; ++i) { mcPart = kTrackMcPart[i]; mask = (uint16_t)i; t.Fill(); } + t.Write(); + } + + // ---- O2mfttrack_001 + label --------------------------------------------- + { + TTree t("O2mfttrack_001", "mft tracks"); + int coll; float x; + t.Branch("fIndexCollisions", &coll, "fIndexCollisions/I"); + t.Branch("fX", &x, "fX/F"); + for (int i = 0; i < kNMft; ++i) { coll = kMftColl[i]; x = 1000.f + i; t.Fill(); } + t.Write(); + } + { + TTree t("O2mcmfttracklabel", "mft mc labels"); + int mcPart; uint16_t mask; + t.Branch("fIndexMcParticles", &mcPart, "fIndexMcParticles/I"); + t.Branch("fMcMask", &mask, "fMcMask/s"); + for (int i = 0; i < kNMft; ++i) { mcPart = kMftMcPart[i]; mask = (uint16_t)(100 + i); t.Fill(); } + t.Write(); + } + + // ---- O2fwdtrack + cov + label ------------------------------------------- + // The table O2-7098 is about: two index columns pointing into tables that are + // reordered in the same stage as this one, one of them itself. + { + TTree t("O2fwdtrack", "fwd tracks"); + int coll, mft, match; float x; + t.Branch("fIndexCollisions", &coll, "fIndexCollisions/I"); + t.Branch("fX", &x, "fX/F"); + t.Branch("fIndexMFTTracks", &mft, "fIndexMFTTracks/I"); + t.Branch("fIndexFwdTracks_MatchMCHTrack", &match, "fIndexFwdTracks_MatchMCHTrack/I"); + for (int i = 0; i < kNFwd; ++i) { + coll = kFwdColl[i]; mft = kFwdMft[i]; match = kFwdMatch[i]; x = 2000.f + i; + t.Fill(); + } + t.Write(); + } + { + TTree t("O2fwdtrackcov", "fwd track cov"); + float sigmaX; + t.Branch("fSigmaX", &sigmaX, "fSigmaX/F"); + for (int i = 0; i < kNFwd; ++i) { sigmaX = 0.5f + i; t.Fill(); } + t.Write(); + } + { + TTree t("O2mcfwdtracklabel", "fwd mc labels"); + int mcPart; uint16_t mask; + t.Branch("fIndexMcParticles", &mcPart, "fIndexMcParticles/I"); + t.Branch("fMcMask", &mask, "fMcMask/s"); + for (int i = 0; i < kNFwd; ++i) { mcPart = kFwdMcPart[i]; mask = (uint16_t)(200 + i); t.Fill(); } + t.Write(); + } + + // ---- O2v0_002 / O2cascade_001 ------------------------------------------- + { + TTree t("O2v0_002", "v0s"); + int coll, pos, neg; + t.Branch("fIndexCollisions", &coll, "fIndexCollisions/I"); + t.Branch("fIndexTracks_Pos", &pos, "fIndexTracks_Pos/I"); + t.Branch("fIndexTracks_Neg", &neg, "fIndexTracks_Neg/I"); + for (int i = 0; i < kNV0; ++i) { coll = kV0Coll[i]; pos = kV0Pos[i]; neg = kV0Neg[i]; t.Fill(); } + t.Write(); + } + { + TTree t("O2cascade_001", "cascades"); + int coll, v0, bach; + t.Branch("fIndexCollisions", &coll, "fIndexCollisions/I"); + t.Branch("fIndexV0s", &v0, "fIndexV0s/I"); + t.Branch("fIndexTracks", &bach, "fIndexTracks/I"); + for (int i = 0; i < kNCasc; ++i) { coll = kCascColl[i]; v0 = kCascV0[i]; bach = kCascBach[i]; t.Fill(); } + t.Write(); + } + + // ---- O2ambiguoustrack (slice into the deduplicated BC table) ------------ + { + TTree t("O2ambiguoustrack", "ambiguous tracks"); + int track, sliceBCs[2]; + t.Branch("fIndexTracks", &track, "fIndexTracks/I"); + t.Branch("fIndexSliceBCs", sliceBCs,"fIndexSliceBCs[2]/I"); + for (int i = 0; i < kNAmb; ++i) { + track = kAmbTrack[i]; sliceBCs[0] = kAmbBCFirst[i]; sliceBCs[1] = kAmbBCLast[i]; + t.Fill(); + } + t.Write(); + } + + // ---- O2origin (no links at all — exercises the fast-clone path) --------- + { + TTree t("O2origin", "origins"); + int dataframeID; + t.Branch("fDataframeID", &dataframeID, "fDataframeID/I"); + dataframeID = 1; t.Fill(); + t.Write(); + } + + f.Write(); + f.Close(); + std::cout << "makeTestAOD: wrote " << outFileName << "\n"; +} diff --git a/MC/utils/tests/run_aodbcrewriter_tests.sh b/MC/utils/tests/run_aodbcrewriter_tests.sh new file mode 100755 index 000000000..f8bda9432 --- /dev/null +++ b/MC/utils/tests/run_aodbcrewriter_tests.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# +# Self-contained regression test for MC/utils/AODBcRewriter.C. +# +# ${O2DPG_ROOT}/MC/utils/tests/run_aodbcrewriter_tests.sh +# +# Needs nothing but ROOT — no simulation, no O2Physics, no GRID, no committed +# binary fixture. Runs in a couple of seconds. +# +# It builds a synthetic AO2D carrying every pathology the rewriter exists to +# repair (see makeTestAOD.C), rewrites it, and then applies three independent +# layers of checking: +# +# 1. AODBcRewriterValidate() — structural invariants of the output alone +# (BC monotonicity, paste-join row parity, +# index ranges, collision-group contiguity, +# and that no fIndex* column is unregistered) +# 2. AODBcRewriterCheckLinks() — input vs output: no row changed what it +# points at. This is the layer that catches +# O2-7098; the structural checks cannot, since +# a mis-remapped index is still perfectly in +# range. +# 3. testAODBcRewriter() — named, physics-readable assertions, headed by +# the MFT/MCH same-particle check the bug was +# reported with. +# +# Exit code 0 = pass. + +set -o pipefail + +MY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UTILS_DIR="$(dirname "${MY_DIR}")" +REWRITER="${UTILS_DIR}/AODBcRewriter.C" + +if ! command -v root > /dev/null 2>&1; then + echo "ERROR: no ROOT in PATH — load an O2/O2Physics/ROOT environment first" >&2 + exit 1 +fi +if [ ! -f "${REWRITER}" ]; then + echo "ERROR: cannot find ${REWRITER}" >&2 + exit 1 +fi + +WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/aodbcrewriter-test-XXXXXX") +trap 'rm -rf "${WORKDIR}"' EXIT +cd "${WORKDIR}" || exit 1 + +IN="AO2D_test.root" +OUT="AO2D_test_rewritten.root" +RC=0 + +run_step() +{ + local name="$1"; shift + echo "=== ${name} ===" + if ! "$@" > "${name}.log" 2>&1; then + echo "--- ${name} FAILED, log follows ---" >&2 + cat "${name}.log" >&2 + RC=1 + return 1 + fi + # ROOT macros signal failure through their return value, which the shell + # does not see; look for the markers as well. + if grep -q '\[FAIL\]' "${name}.log"; then + echo "--- ${name} reported [FAIL] lines ---" >&2 + grep -E '\[FAIL\]|FAILED' "${name}.log" >&2 + RC=1 + return 1 + fi + return 0 +} + +echo "Working in ${WORKDIR}" + +run_step "01-make-fixture" \ + root -l -b -q "${MY_DIR}/makeTestAOD.C(\"${IN}\")" + +run_step "02-rewrite" \ + root -l -b -q "${REWRITER}(\"${IN}\",\"${OUT}\")" + +# From here on every step needs the rewritten file. +if [ ! -f "${OUT}" ]; then + echo "ERROR: rewriter produced no output" >&2 + exit 1 +fi + +run_step "03-validate" \ + root -l -b -q -e ".L ${REWRITER}" -e "if (!AODBcRewriterValidate(\"${OUT}\")) gSystem->Exit(1);" + +run_step "04-check-links" \ + root -l -b -q -e ".L ${REWRITER}" -e "if (!AODBcRewriterCheckLinks(\"${IN}\",\"${OUT}\")) gSystem->Exit(1);" + +run_step "05-assertions" \ + root -l -b -q -e ".L ${REWRITER}" -e ".L ${MY_DIR}/testAODBcRewriter.C" \ + -e "if (testAODBcRewriter(\"${IN}\",\"${OUT}\") != 0) gSystem->Exit(1);" + +# Surface the readable assertion results even on success. +[ -f 05-assertions.log ] && grep -E '\[ ok \]|\[FAIL\]|PASSED|FAILED' 05-assertions.log + +if [ "${RC}" -eq 0 ]; then + echo "AODBcRewriter tests: PASSED" +else + echo "AODBcRewriter tests: FAILED" +fi +exit "${RC}" diff --git a/MC/utils/tests/testAODBcRewriter.C b/MC/utils/tests/testAODBcRewriter.C new file mode 100644 index 000000000..a0357aa82 --- /dev/null +++ b/MC/utils/tests/testAODBcRewriter.C @@ -0,0 +1,294 @@ +// testAODBcRewriter.C +// +// Physics-readable assertions on a rewritten AO2D, on top of the generic +// AODBcRewriterValidate() / AODBcRewriterCheckLinks() checks. +// +// root -l -b -q 'testAODBcRewriter.C("AO2D_test.root","AO2D_test_rewritten.root")' +// +// Returns 0 on success, the number of failed assertions otherwise. +// +// Expectations are derived from the INPUT file rather than hard-coded, so the +// fixture in makeTestAOD.C can grow without this file having to be kept in +// sync. What is hard-coded is only the *invariant*: a rewrite permutes rows, +// it never changes which row points at which. +// +// The headline assertion is the one from the O2-7098 report: for a fwd track +// with an MFT match, the MFT leg and the MCH leg must belong to the same MC +// particle. In this fixture the matching is perfect by construction, so the +// correct answer is 100% and a mis-remapped fIndexMFTTracks gives ~0% — +// the same signature seen in the anchored pO production. + +#ifndef __CLING__ +#include "TFile.h" +#include "TTree.h" +#include +#include +#include +#include +#include +#include +#endif + +namespace +{ + +int gFailures = 0; + +void fail(const std::string &what) +{ + std::cerr << " [FAIL] " << what << "\n"; + ++gFailures; +} + +void pass(const std::string &what) +{ + std::cout << " [ ok ] " << what << "\n"; +} + +TTree *get(TDirectory *d, const char *name) +{ + TTree *t = dynamic_cast(d->Get(name)); + if (!t) fail(std::string("table ") + name + " missing"); + return t; +} + +std::vector readInt(TTree *t, const char *branch) +{ + std::vector v; + if (!t || !t->GetBranch(branch)) return v; + Int_t x = 0; + t->ResetBranchAddresses(); + t->SetBranchAddress(branch, &x); + v.reserve(t->GetEntries()); + for (Long64_t i = 0; i < t->GetEntries(); ++i) { t->GetEntry(i); v.push_back(x); } + t->ResetBranchAddresses(); + return v; +} + +std::vector readFloat(TTree *t, const char *branch) +{ + std::vector v; + if (!t || !t->GetBranch(branch)) return v; + Float_t x = 0; + t->ResetBranchAddresses(); + t->SetBranchAddress(branch, &x); + v.reserve(t->GetEntries()); + for (Long64_t i = 0; i < t->GetEntries(); ++i) { t->GetEntry(i); v.push_back(x); } + t->ResetBranchAddresses(); + return v; +} + +std::vector readULong(TTree *t, const char *branch) +{ + std::vector v; + if (!t || !t->GetBranch(branch)) return v; + ULong64_t x = 0; + t->ResetBranchAddresses(); + t->SetBranchAddress(branch, &x); + v.reserve(t->GetEntries()); + for (Long64_t i = 0; i < t->GetEntries(); ++i) { t->GetEntry(i); v.push_back(x); } + t->ResetBranchAddresses(); + return v; +} + +// The multiset of (referrer payload, referent payload) pairs implied by an +// index column. Rows with no link contribute (payload, sentinel). +std::multimap linkPairs(const std::vector &idx, + const std::vector &ownPayload, + const std::vector &refPayload) +{ + std::multimap pairs; + for (size_t i = 0; i < idx.size() && i < ownPayload.size(); ++i) { + float ref = -1.f; + if (idx[i] >= 0 && (size_t)idx[i] < refPayload.size()) ref = refPayload[idx[i]]; + pairs.emplace(ownPayload[i], ref); + } + return pairs; +} + +void expectSameLinks(const char *what, + const std::multimap &in, + const std::multimap &out) +{ + if (in.size() != out.size()) { + fail(std::string(what) + ": row count changed (" + + std::to_string(in.size()) + " -> " + std::to_string(out.size()) + ")"); + return; + } + std::vector> a(in.begin(), in.end()), b(out.begin(), out.end()); + std::sort(a.begin(), a.end()); + std::sort(b.begin(), b.end()); + if (a != b) { + size_t shown = 0; + for (size_t i = 0; i < a.size() && shown < 5; ++i) { + if (a[i] != b[i]) { + std::cerr << " row " << a[i].first << " pointed at " << a[i].second + << " before, at " << b[i].second << " after\n"; + ++shown; + } + } + fail(std::string(what) + ": links not preserved"); + return; + } + pass(std::string(what) + ": links preserved (" + std::to_string(a.size()) + " rows)"); +} + +} // namespace + +int testAODBcRewriter(const char *inFileName = "AO2D_test.root", + const char *outFileName = "AO2D_test_rewritten.root") +{ + gFailures = 0; + std::cout << "testAODBcRewriter: " << inFileName << " -> " << outFileName << "\n"; + + TFile fin(inFileName, "READ"); + TFile fout(outFileName, "READ"); + if (fin.IsZombie()) { std::cerr << "cannot open " << inFileName << "\n"; return 1; } + if (fout.IsZombie()) { std::cerr << "cannot open " << outFileName << "\n"; return 1; } + + const char *dfName = "DF_1000000000001"; + TDirectory *din = dynamic_cast(fin.Get(dfName)); + TDirectory *dout = dynamic_cast(fout.Get(dfName)); + if (!din || !dout) { std::cerr << "DF " << dfName << " missing\n"; return 1; } + + // ---- Stage 0 did its job ------------------------------------------------- + { + auto bc = readULong(get(dout, "O2bc_001"), "fGlobalBC"); + bool mono = true; + for (size_t i = 1; i < bc.size(); ++i) if (bc[i] <= bc[i - 1]) mono = false; + if (!mono) fail("O2bc_001: fGlobalBC not strictly monotonic after rewrite"); + else pass("O2bc_001: fGlobalBC strictly increasing"); + if (bc.size() != 5) fail("O2bc_001: expected 5 unique BCs, got " + std::to_string(bc.size())); + else pass("O2bc_001: duplicate BC removed (6 -> 5)"); + } + + // ---- MCCollision deduplication ------------------------------------------ + { + Long64_t n = get(dout, "O2mccollision_001")->GetEntries(); + if (n != 4) fail("O2mccollision_001: expected 4 rows after dedup, got " + std::to_string(n)); + else pass("O2mccollision_001: duplicate MC collision removed (5 -> 4)"); + } + + // ---- The O2-7098 links --------------------------------------------------- + { + auto fwdXIn = readFloat(get(din, "O2fwdtrack"), "fX"); + auto fwdXOut = readFloat(get(dout, "O2fwdtrack"), "fX"); + auto mftXIn = readFloat(get(din, "O2mfttrack_001"), "fX"); + auto mftXOut = readFloat(get(dout, "O2mfttrack_001"), "fX"); + + expectSameLinks("O2fwdtrack.fIndexMFTTracks", + linkPairs(readInt(get(din, "O2fwdtrack"), "fIndexMFTTracks"), fwdXIn, mftXIn), + linkPairs(readInt(get(dout, "O2fwdtrack"), "fIndexMFTTracks"), fwdXOut, mftXOut)); + + expectSameLinks("O2fwdtrack.fIndexFwdTracks_MatchMCHTrack (self-reference)", + linkPairs(readInt(get(din, "O2fwdtrack"), "fIndexFwdTracks_MatchMCHTrack"), fwdXIn, fwdXIn), + linkPairs(readInt(get(dout, "O2fwdtrack"), "fIndexFwdTracks_MatchMCHTrack"), fwdXOut, fwdXOut)); + } + + // ---- The reported symptom: MFT leg and MCH leg of the same global muon --- + // must carry the same MC particle. This is Maurice Coquet's tableMakerMC + // check, reduced to the label tables. Perfect matching by construction here. + { + auto mftIdx = readInt(get(dout, "O2fwdtrack"), "fIndexMFTTracks"); + auto fwdLbl = readInt(get(dout, "O2mcfwdtracklabel"), "fIndexMcParticles"); + auto mftLbl = readInt(get(dout, "O2mcmfttracklabel"), "fIndexMcParticles"); + int checked = 0, same = 0; + for (size_t i = 0; i < mftIdx.size() && i < fwdLbl.size(); ++i) { + if (mftIdx[i] < 0 || (size_t)mftIdx[i] >= mftLbl.size()) continue; + ++checked; + if (fwdLbl[i] == mftLbl[mftIdx[i]]) ++same; + } + if (checked == 0) fail("MFT-MCH match check: nothing to check"); + else if (same != checked) fail("MFT-MCH match: sameParticle=" + std::to_string(same) + + "/" + std::to_string(checked) + ", expected all (this is O2-7098)"); + else pass("MFT-MCH match: sameParticle=" + std::to_string(same) + + "/" + std::to_string(checked) + " (100%)"); + } + + // ---- References into the reordered track tables from elsewhere ----------- + { + auto trkXIn = readFloat(get(din, "O2track_iu"), "fX"); + auto trkXOut = readFloat(get(dout, "O2track_iu"), "fX"); + + // V0s have no float payload of their own; identify them by their positive + // leg, which is unique in this fixture. + auto v0PosIn = readInt(get(din, "O2v0_002"), "fIndexTracks_Pos"); + auto v0PosOut = readInt(get(dout, "O2v0_002"), "fIndexTracks_Pos"); + auto v0NegIn = readInt(get(din, "O2v0_002"), "fIndexTracks_Neg"); + auto v0NegOut = readInt(get(dout, "O2v0_002"), "fIndexTracks_Neg"); + std::vector v0IdIn, v0IdOut; + for (auto p : v0PosIn) v0IdIn.push_back(p >= 0 && (size_t)p < trkXIn.size() ? trkXIn[p] : -1.f); + for (auto p : v0PosOut) v0IdOut.push_back(p >= 0 && (size_t)p < trkXOut.size() ? trkXOut[p] : -1.f); + expectSameLinks("O2v0_002.fIndexTracks_Neg", + linkPairs(v0NegIn, v0IdIn, trkXIn), + linkPairs(v0NegOut, v0IdOut, trkXOut)); + } + + // ---- Paste-join children still line up with their parent ---------------- + { + auto fwdXOut = readFloat(get(dout, "O2fwdtrack"), "fX"); + auto sigmaIn = readFloat(get(din, "O2fwdtrackcov"), "fSigmaX"); + auto sigmaOut= readFloat(get(dout, "O2fwdtrackcov"), "fSigmaX"); + auto fwdXIn = readFloat(get(din, "O2fwdtrack"), "fX"); + std::map expect; // fX -> fSigmaX, from the input + for (size_t i = 0; i < fwdXIn.size() && i < sigmaIn.size(); ++i) expect[fwdXIn[i]] = sigmaIn[i]; + bool aligned = (fwdXOut.size() == sigmaOut.size()); + for (size_t i = 0; aligned && i < fwdXOut.size(); ++i) + if (expect.count(fwdXOut[i]) == 0 || expect[fwdXOut[i]] != sigmaOut[i]) aligned = false; + if (!aligned) fail("O2fwdtrackcov: paste-join alignment with O2fwdtrack broken"); + else pass("O2fwdtrackcov: follows O2fwdtrack row-for-row"); + } + + // ---- Intra-table MC particle links -------------------------------------- + { + TTree *mcp = get(dout, "O2mcparticle_001"); + auto pdg = readInt(mcp, "fPdgCode"); + Int_t daughters[2] = {-1, -1}; + mcp->ResetBranchAddresses(); + mcp->SetBranchAddress("fIndexSlice_Daughters", daughters); + int checked = 0, bad = 0; + for (Long64_t i = 0; i < mcp->GetEntries(); ++i) { + mcp->GetEntry(i); + if (daughters[0] < 0) continue; + ++checked; + // In the fixture a mother with fPdgCode P has daughters P+1 and P+2. + if (daughters[1] - daughters[0] != 1) { ++bad; continue; } + if (pdg[daughters[0]] != pdg[i] + 1 || pdg[daughters[1]] != pdg[i] + 2) ++bad; + } + mcp->ResetBranchAddresses(); + if (checked == 0) fail("O2mcparticle_001: no daughter slices found"); + else if (bad) fail("O2mcparticle_001: " + std::to_string(bad) + "/" + + std::to_string(checked) + " daughter slices point at the wrong particles"); + else pass("O2mcparticle_001: all " + std::to_string(checked) + + " daughter slices still point at the right particles"); + } + + // ---- Slice into the deduplicated BC table ------------------------------- + { + auto bcIn = readULong(get(din, "O2bc_001"), "fGlobalBC"); + auto bcOut = readULong(get(dout, "O2bc_001"), "fGlobalBC"); + TTree *aIn = get(din, "O2ambiguoustrack"); + TTree *aOut = get(dout, "O2ambiguoustrack"); + Int_t sIn[2] = {-1, -1}, sOut[2] = {-1, -1}; + aIn->ResetBranchAddresses(); aIn->SetBranchAddress("fIndexSliceBCs", sIn); + aOut->ResetBranchAddresses(); aOut->SetBranchAddress("fIndexSliceBCs", sOut); + int bad = 0; + Long64_t n = std::min(aIn->GetEntries(), aOut->GetEntries()); + for (Long64_t i = 0; i < n; ++i) { + aIn->GetEntry(i); aOut->GetEntry(i); + for (int j = 0; j < 2; ++j) { + if (sIn[j] < 0 || (size_t)sIn[j] >= bcIn.size()) continue; + if (sOut[j] < 0 || (size_t)sOut[j] >= bcOut.size()) { ++bad; continue; } + if (bcIn[sIn[j]] != bcOut[sOut[j]]) ++bad; + } + } + aIn->ResetBranchAddresses(); aOut->ResetBranchAddresses(); + if (bad) fail("O2ambiguoustrack.fIndexSliceBCs: " + std::to_string(bad) + + " endpoint(s) no longer point at the same bunch crossing"); + else pass("O2ambiguoustrack.fIndexSliceBCs: endpoints follow the BC compaction"); + } + + if (gFailures == 0) std::cout << "testAODBcRewriter: ALL CHECKS PASSED\n"; + else std::cout << "testAODBcRewriter: " << gFailures << " CHECK(S) FAILED\n"; + return gFailures; +} From 367e25b56aa2b13cc5aa4623c74ad88e33c04f50 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Tue, 28 Jul 2026 23:19:39 +0200 Subject: [PATCH 026/144] AODBcRewriter: re-sort tables stored sorted by a reordered reference Second member of the O2-7098 family. Stage 1b reorders O2track_iu/ O2mfttrack/O2fwdtrack; several other tables (O2fwdtrkcl, O2ambiguoustrack, O2trackqa_003, O2v0_002, O2cascade_001, O2decay3body) are stored SORTED BY a reference into one of those (or into O2collision). Their index values were remapped correctly, but their rows stayed put, so a sorted column came out unsorted -- the same defect as the split "-1" group that once caused [FATAL] Table Tracks_IU index fIndexCollisions has a group with index -1 that is split by 776 Confirmed on real files: O2fwdtrkcl and O2trackqa_003 came out unsorted in every DF of two of three anchored-MC samples tested. Fix: findGroupingColumn() + resortByGroupingColumn(), derived from the data rather than another hardcoded table list -- "if T.B is non-decreasing on input and B's referent moved, re-sort T by the remapped B". Leaves O2mfttrackcov alone (not sorted on input). Iterated to a fixed point, since O2cascade_001 is sorted by fIndexV0s and O2v0_002 may itself have just moved. AODBcRewriterCheckLinks() now also asserts sortedness is preserved. Fixture extended with O2fwdtrkcl and a second cascade to exercise the O2v0->O2cascade dependency; against the previous commit the suite fails 3 assertions and 6 link checks, and passes with this one. Co-Authored-By: Claude Opus 5 --- MC/utils/AODBcRewriter.C | 179 ++++++++++++++++++++++++++++- MC/utils/CLAUDE.md | 42 ++++--- MC/utils/tests/makeTestAOD.C | 28 ++++- MC/utils/tests/testAODBcRewriter.C | 34 ++++++ 4 files changed, 260 insertions(+), 23 deletions(-) diff --git a/MC/utils/AODBcRewriter.C b/MC/utils/AODBcRewriter.C index f03d013a1..4080c9044 100644 --- a/MC/utils/AODBcRewriter.C +++ b/MC/utils/AODBcRewriter.C @@ -1075,15 +1075,155 @@ static void stage1b_reorderTrackTables( } } +// ---------------------------------------------------------------------------- +// Re-sorting tables that are STORED SORTED BY a reference +// +// Stage 1b exists because a track table grouped by fIndexCollisions stops being +// sliceable once the collision table is reordered. Exactly the same is true of +// every other table stored sorted by a reference into a table this tool +// reorders — O2v0_002 and O2cascade_001 by fIndexCollisions, O2fwdtrkcl by +// fIndexFwdTracks, O2ambiguoustrack and O2trackqa_003 by fIndexTracks. +// Remapping their values while leaving their rows in place turns a sorted +// column into an unsorted one, which is the same defect that produced +// "Table ... index fIndexCollisions has a group with index -1 that is split". +// +// Rather than hardcode which tables are grouped by what — the enumeration habit +// that caused O2-7098 — the decision is derived from the data: IF a column is +// non-decreasing in the input, THEN it is an ordering the file carries and the +// output must preserve it. That is self-maintaining across schema changes, and +// it correctly leaves O2mfttrackcov alone: its fIndexMFTTracks is not sorted in +// the input, so there is no ordering to preserve. + +struct ResortCandidate { + size_t planIdx = 0; + std::string tableName; + std::string keyBranch; + std::vector keyValues; // raw input values of keyBranch + std::vector refPrefixes; // referent of keyBranch, from kIndexRefs +}; + +// Read a scalar Int_t-like index column. Returns false for VLA / fixed-array +// columns, which are not row orderings. +static bool readScalarIndexColumn(TTree *t, const char *branch, + std::vector &out) { + TBranch *br = t->GetBranch(branch); + if (!br) return false; + TLeaf *leaf = static_cast(br->GetListOfLeaves()->At(0)); + if (!leaf || leaf->GetLeafCount() || leaf->GetLen() != 1) return false; + ScalarTag tag = tagOf(leaf); + size_t sz = byteSize(tag); + if (sz == 0) return false; + std::vector buf(sz, 0); + br->SetAddress(buf.data()); + out.clear(); + out.reserve(t->GetEntries()); + for (Long64_t i = 0; i < t->GetEntries(); ++i) { + br->GetEntry(i); + out.push_back(readAsInt(buf.data(), tag)); + } + br->ResetAddress(); + return true; +} + +// The convention this tool writes (and Stage 1b establishes): valid values +// ascending, the -1 "ambiguous" group as one contiguous run at the end. +static bool isOrderedWithNullsLast(const std::vector &v) { + Long64_t prev = -1; + bool seenNull = false; + for (auto x : v) { + if (x < 0) { seenNull = true; continue; } + if (seenNull) return false; // a valid value after a null: not this convention + if (x < prev) return false; + prev = x; + } + return true; +} + +// Pick the column a table is stored sorted by, if any. fIndexCollisions wins +// when several qualify, since that is the grouping O2's slicing cache checks. +static bool findGroupingColumn(TTree *src, std::string &keyBranch, + std::vector &keyValues, + std::vector &refPrefixes) { + if (!src || src->GetEntries() < 2) return false; + bool found = false; + for (auto &[branchName, prefixes] : kIndexRefs) { + std::vector vals; + if (!readScalarIndexColumn(src, branchName.c_str(), vals)) continue; + if (!isOrderedWithNullsLast(vals)) continue; + bool preferred = (branchName == "fIndexCollisions"); + if (found && !preferred) continue; + keyBranch = branchName; + keyValues = std::move(vals); + refPrefixes = prefixes; + found = true; + if (preferred) break; + } + return found; +} + +// Re-sort each candidate by its remapped grouping column. Iterated to a fixed +// point because these tables reference each other: O2cascade_001 is sorted by +// fIndexV0s, and O2v0_002 may itself have just been re-sorted. +static void resortByGroupingColumn( + std::vector &plans, + std::unordered_map &allPerms, + const std::vector &candidates) { + + const int kMaxPasses = 8; + int pass = 0; + for (; pass < kMaxPasses; ++pass) { + bool changed = false; + for (auto &cand : candidates) { + const PermMap *refPerm = findPermFor(allPerms, cand.refPrefixes); + if (!refPerm) continue; // referent absent from this DF + + Long64_t n = (Long64_t)cand.keyValues.size(); + struct SortEntry { Long64_t key; Long64_t srcRow; }; + std::vector entries; + entries.reserve(n); + for (Long64_t i = 0; i < n; ++i) { + Long64_t old = cand.keyValues[i]; + Long64_t nw = (old >= 0 && old < (Long64_t)refPerm->size()) ? (*refPerm)[old] : -1; + entries.push_back({nw, i}); + } + // Same ordering convention as Stage 1: -1 sinks to a contiguous tail. + std::stable_sort(entries.begin(), entries.end(), + [](const SortEntry &a, const SortEntry &b) { + if (a.key < 0 && b.key >= 0) return false; + if (a.key >= 0 && b.key < 0) return true; + return a.key < b.key; + }); + std::vector rowOrder; + rowOrder.reserve(n); + for (auto &e : entries) rowOrder.push_back(e.srcRow); + + if (rowOrder == plans[cand.planIdx].rowOrder) continue; + plans[cand.planIdx].rowOrder = rowOrder; + allPerms[cand.tableName] = permFromRowOrder(n, rowOrder); + changed = true; + std::cout << " Re-sort: " << cand.tableName << " by remapped " + << cand.keyBranch << " (was sorted by it on input)\n"; + } + if (!changed) break; + } + if (pass == kMaxPasses) + std::cerr << " [warn] re-sort did not reach a fixed point after " + << kMaxPasses << " passes — check for a reference cycle\n"; +} + // Plan every table not yet claimed by an earlier stage: paste-join children -// follow their parent's row order, everything else keeps its own. The index -// columns they carry are NOT enumerated here any more — buildRemaps() derives -// them from kIndexRefs when processDF writes. +// follow their parent's row order, everything else keeps its own — unless it is +// stored sorted by a reference, in which case resortByGroupingColumn() below +// re-establishes that ordering. The index columns these tables carry are NOT +// enumerated here — buildRemaps() derives them from kIndexRefs when processDF +// writes. static void planRemainingTables( TDirectory *dirIn, std::vector &plans, std::unordered_map &allPerms, std::unordered_set &planned) { + std::vector resortCandidates; + TIter it(dirIn->GetListOfKeys()); while (TKey *key = static_cast(it())) { if (TString(key->GetClassName()) != "TTree") continue; @@ -1139,10 +1279,26 @@ static void planRemainingTables( std::iota(rowOrder.begin(), rowOrder.end(), 0LL); } + // A table that arrives here with its own row order may still be STORED + // SORTED BY one of its index columns; if that column's referent gets + // reordered, the sortedness has to be re-established. Record what is + // needed for that; the decision is made below, once every table in this + // stage has a permutation. + if (!parentPerm) { + ResortCandidate cand; + if (findGroupingColumn(src, cand.keyBranch, cand.keyValues, cand.refPrefixes)) { + cand.planIdx = plans.size(); + cand.tableName = tname; + resortCandidates.push_back(std::move(cand)); + } + } + allPerms[tname] = permFromRowOrder(nSrc, rowOrder); plans.push_back({tname, std::move(rowOrder)}); planned.insert(tname); } + + resortByGroupingColumn(plans, allPerms, resortCandidates); } // ============================================================================ @@ -1826,6 +1982,23 @@ static bool checkLinksDF(TDirectory *din, TDirectory *dout, const char *dfName) linksOut.push_back({branchName, &fpOut[refName], nullptr}); } + // Ordering preservation: a column that is sorted on input describes a + // grouping the file carries, and O2's slicing cache relies on it. Remapping + // the values while leaving the rows in place silently destroys it — the same + // defect as the split "-1" group, just in a different table. + for (auto &[branchName, prefixes] : kIndexRefs) { + std::vector vIn, vOut; + if (!readScalarIndexColumn(tIn, branchName.c_str(), vIn)) continue; + if (!isOrderedWithNullsLast(vIn)) continue; // no ordering to preserve + if (!readScalarIndexColumn(tOut, branchName.c_str(), vOut)) continue; + if (!isOrderedWithNullsLast(vOut)) { + std::cerr << " [FAIL] " << dfName << ": " << tn << "." << branchName + << " was sorted on input but is not on output" + " (grouping destroyed — slicing will misbehave)\n"; + ok = false; + } + } + auto cIn = linkTupleCounts(tIn, fpIn[tn], linksIn); auto cOut = linkTupleCounts(tOut, fpOut[tn], linksOut); diff --git a/MC/utils/CLAUDE.md b/MC/utils/CLAUDE.md index 52bc83901..13d799382 100644 --- a/MC/utils/CLAUDE.md +++ b/MC/utils/CLAUDE.md @@ -49,6 +49,11 @@ practice **every merged MC AO2D since 4 Jun 2026 is affected**. test instead of silently mis-linking data. 5. **`AODBcRewriterCheckLinks()` (Section 11b)** — the check that can actually see this bug class; see "Testing" below. +6. **Re-sort tables stored sorted by a reference** (`findGroupingColumn` / + `resortByGroupingColumn`, Section 8). The same family: `O2fwdtrkcl` and + `O2trackqa_003` were coming out unsorted in every DF of two of the three + sample files, which breaks O2's slicing the same way the split `-1` group + did. Derived from the data, not from a list — see resolved gap 1. Maurice's #2418 has the same shape (defer `O2fwdtrack`, remap via the MFT/Fwd perms) and is correct; this generalises it from one table to all of them. @@ -331,13 +336,12 @@ row whose `fIndexMcCollisions` pointed to a dropped row is also dropped. These were identified during the refactor but not yet implemented: -### 1. Tables SORTED BY a reference into a reordered table are not re-sorted +### ~~1. Tables SORTED BY a reference into a reordered table are not re-sorted~~ (RESOLVED) -**This is the remaining member of the O2-7098 family — open, and the natural -follow-up PR.** Stage 1b reorders `O2track_iu` / `O2mfttrack` / `O2fwdtrack`. -Several other tables are *stored sorted by* a reference into one of those (or -into `O2collision`). Their values are now correctly remapped, but their rows are -left where they were, so the ordering is destroyed: +Stage 1b reorders `O2track_iu` / `O2mfttrack` / `O2fwdtrack`. Several other +tables are *stored sorted by* a reference into one of those (or into +`O2collision`). Their values were remapped but their rows left where they were, +so the ordering was destroyed: | table | key | sorted in input? | |---|---|---| @@ -347,16 +351,22 @@ left where they were, so the ordering is destroyed: | `O2v0_002`, `O2cascade_001`, `O2decay3body` | `fIndexCollisions` | yes | | `O2mfttrackcov` | `fIndexMFTTracks` | **no** — so not an invariant there | -(measured on `example_AOD/AO2D_pre.root`.) This is the same failure mode as the -split `-1` group that produced the `ArrowTableSlicingCache::validateOrder` -FATAL — not yet observed in the wild, but structurally identical. - -**Suggested fix** — data-derived rather than another hardcoded list: *if `T.B` -was sorted in the input and `B`'s referent was reordered, re-sort `T` by the -remapped `B`.* That rule reproduces Stage 1b's behaviour and correctly leaves -`O2mfttrackcov` alone. It is deliberately **not** in the O2-7098 fix: it changes -the row order of tables that are currently untouched, so it deserves its own -review. `AODBcRewriterCheckLinks` is already in place to police it. +(measured on `example_AOD/AO2D_pre.root`.) Same failure mode as the split `-1` +group that produced the `ArrowTableSlicingCache::validateOrder` FATAL. **Not +theoretical**: on `example_AOD/AO2D_pre.root` and `bigger2/`, `O2fwdtrkcl` and +`O2trackqa_003` came out unsorted in *every* DF. + +**Fix**: `findGroupingColumn` + `resortByGroupingColumn` (Section 8), driven by +the data rather than by another hardcoded list — *if `T.B` is non-decreasing in +the input and `B`'s referent gets reordered, re-sort `T` by the remapped `B`*. +Self-maintaining across schema changes, and it correctly leaves `O2mfttrackcov` +alone (its `fIndexMFTTracks` is not sorted on input, so there is no ordering to +preserve). Iterated to a fixed point, because these tables reference each other: +`O2cascade_001` is sorted by `fIndexV0s` and `O2v0_002` may itself have just +been re-sorted. + +Policed by a new check in `AODBcRewriterCheckLinks`: a column sorted on input +must be sorted on output. ### ~~2. `fIndexCollisions` inside `O2mccollision` is not remapped~~ (MOOT) diff --git a/MC/utils/tests/makeTestAOD.C b/MC/utils/tests/makeTestAOD.C index a437328f8..e87cf02dc 100644 --- a/MC/utils/tests/makeTestAOD.C +++ b/MC/utils/tests/makeTestAOD.C @@ -110,21 +110,31 @@ const int kFwdMatch[] = {-1, 0, -1, -1, 3, -1};// fwd -> fwd self-reference // exactly the metric O2-7098 was reported with. const int kFwdMcPart[] = {0, 3, -1, 6, 9, -1}; +// V0s and cascades are stored SORTED BY fIndexCollisions, as reconstruction +// writes them. The collision table gets reordered, so these have to be +// re-sorted or the grouping is destroyed. The cascades are additionally sorted +// by fIndexV0s, whose referent is itself one of these re-sorted tables — a +// two-level dependency that a single pass would not resolve. const int kNV0 = 2; const int kV0Coll[] = {0, 2}; const int kV0Pos[] = {0, 4}; const int kV0Neg[] = {1, 5}; -const int kNCasc = 1; -const int kCascColl[] = {0}; -const int kCascV0[] = {0}; -const int kCascBach[] = {2}; +const int kNCasc = 2; +const int kCascColl[] = {0, 2}; +const int kCascV0[] = {0, 1}; +const int kCascBach[] = {2, 4}; +// Sorted by fIndexTracks / fIndexFwdTracks respectively — both referents are +// reordered by Stage 1b. const int kNAmb = 2; const int kAmbTrack[] = {3, 6}; const int kAmbBCFirst[] = {0, 3}; const int kAmbBCLast[] = {2, 4}; +const int kNFwdCl = 4; +const int kFwdClTrack[] = {0, 1, 3, 4}; + } // namespace void makeTestAOD(const char *outFileName = "AO2D_test.root") @@ -308,6 +318,16 @@ void makeTestAOD(const char *outFileName = "AO2D_test.root") t.Write(); } + // ---- O2fwdtrkcl (grouped by fIndexFwdTracks, a Stage-1b table) --------- + { + TTree t("O2fwdtrkcl", "fwd track clusters"); + int fwd; float x; + t.Branch("fIndexFwdTracks", &fwd, "fIndexFwdTracks/I"); + t.Branch("fX", &x, "fX/F"); + for (int i = 0; i < kNFwdCl; ++i) { fwd = kFwdClTrack[i]; x = 3000.f + i; t.Fill(); } + t.Write(); + } + // ---- O2ambiguoustrack (slice into the deduplicated BC table) ------------ { TTree t("O2ambiguoustrack", "ambiguous tracks"); diff --git a/MC/utils/tests/testAODBcRewriter.C b/MC/utils/tests/testAODBcRewriter.C index a0357aa82..aa9e524ee 100644 --- a/MC/utils/tests/testAODBcRewriter.C +++ b/MC/utils/tests/testAODBcRewriter.C @@ -263,6 +263,40 @@ int testAODBcRewriter(const char *inFileName = "AO2D_test.root", " daughter slices still point at the right particles"); } + // ---- Grouping preserved for tables stored sorted by a reference --------- + // These are written sorted by an index into a table the rewriter reorders. + // Remapping the values without re-sorting the rows leaves the column + // unsorted, which breaks O2's slicing cache the same way a split "-1" group + // does. The referenced tables here are all genuinely reordered, so a + // no-op implementation cannot pass this by accident. + { + const char *cases[][2] = { + {"O2v0_002", "fIndexCollisions"}, + {"O2cascade_001", "fIndexCollisions"}, + {"O2cascade_001", "fIndexV0s"}, // referent is itself re-sorted + {"O2fwdtrkcl", "fIndexFwdTracks"}, + {"O2ambiguoustrack", "fIndexTracks"}, + }; + for (auto &c : cases) { + auto vIn = readInt(get(din, c[0]), c[1]); + auto vOut = readInt(get(dout, c[0]), c[1]); + auto ordered = [](const std::vector &v) { + Int_t prev = -1; bool seenNull = false; + for (auto x : v) { + if (x < 0) { seenNull = true; continue; } + if (seenNull || x < prev) return false; + prev = x; + } + return true; + }; + std::string what = std::string(c[0]) + "." + c[1]; + if (vIn.empty()) { fail(what + ": column missing from input"); continue; } + if (!ordered(vIn)) { fail(what + ": fixture is not sorted on input — fix makeTestAOD.C"); continue; } + if (!ordered(vOut)) fail(what + ": sorted on input, unsorted on output (grouping destroyed)"); + else pass(what + ": still sorted after the rewrite"); + } + } + // ---- Slice into the deduplicated BC table ------------------------------- { auto bcIn = readULong(get(din, "O2bc_001"), "fGlobalBC"); From 3ccf70fd5a6e230523cfd40be8470b419eb8bc7b Mon Sep 17 00:00:00 2001 From: Marian Ivanov Date: Wed, 29 Jul 2026 12:30:36 +0200 Subject: [PATCH 027/144] TPC TimeSeries: propagate track selection cuts from ALIEN JDL (#2423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ALIEN_JDL variables for o2-tpc-time-series-workflow configuration: ALIEN_JDL_TPCTIMESERIESMINMOMENTUM → TPCTIMESERIES_MIN_MOMENTUM ALIEN_JDL_TPCTIMESERIESMINCLUSTER → TPCTIMESERIES_MIN_CLUSTER ALIEN_JDL_TPCTIMESERIESMAXTGL → TPCTIMESERIES_MAX_TGL ALIEN_JDL_TPCTIMESERIESMULTMAX → TPCTIMESERIES_MULT_MAX Companion to AliceO2 calib-workflow.sh change. Co-authored-by: miranov25 --- .../configurations/asyncReco/setenv_extra.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index eb1f1bcd1..10c595455 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -785,6 +785,18 @@ if [[ $ADD_CALIB == "1" ]]; then if [[ -n $ALIEN_JDL_SAMPLINGFACTORTIMESERIES ]]; then export CALIB_ASYNC_SAMPLINGFACTORTIMESERIES=$ALIEN_JDL_SAMPLINGFACTORTIMESERIES fi + if [[ -n $ALIEN_JDL_TPCTIMESERIESMINMOMENTUM ]]; then + export TPCTIMESERIES_MIN_MOMENTUM=$ALIEN_JDL_TPCTIMESERIESMINMOMENTUM + fi + if [[ -n $ALIEN_JDL_TPCTIMESERIESMINCLUSTER ]]; then + export TPCTIMESERIES_MIN_CLUSTER=$ALIEN_JDL_TPCTIMESERIESMINCLUSTER + fi + if [[ -n $ALIEN_JDL_TPCTIMESERIESMAXTGL ]]; then + export TPCTIMESERIES_MAX_TGL=$ALIEN_JDL_TPCTIMESERIESMAXTGL + fi + if [[ -n $ALIEN_JDL_TPCTIMESERIESMULTMAX ]]; then + export TPCTIMESERIES_MULT_MAX=$ALIEN_JDL_TPCTIMESERIESMULTMAX + fi fi if [[ $ALIEN_JDL_DOUPLOADSLOCALLY == 1 ]]; then export CCDB_POPULATOR_UPLOAD_PATH="file://$PWD" From f4702907ef28b0c4e16ec8d9a0bc69101729ffc6 Mon Sep 17 00:00:00 2001 From: Matthias Kleiner <48915672+matthias-kleiner@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:01:36 +0200 Subject: [PATCH 028/144] Add TPC space-charge map creation GRID pipeline (#2422) Two-stage grid_submit.sh-based pipeline (map creation, then smoothing + FastTransform), one time slot per GRID job. Runs staticMapCreatorCPM.C / SmoothingExtrapolate.C / voxResQA.C / TPCFastTransformInitCPM.C, which live in the main O2 repository and install to $O2_ROOT/share/macro/. Macro source defaults to that official install path (MACRO_SOURCE=official), with an alien-staging fallback (MACRO_SOURCE=alien) for use before a packagespec with the merged macros is published. --- .../configurations/mapCreationGrid/README.md | 402 ++++++++++++++++++ .../mapCreationGrid/discoverResiduals.sh | 201 +++++++++ .../mapCreationGrid/makeResidualLists.sh | 280 ++++++++++++ .../mapCreationGrid/mapCreationJob.sh | 296 +++++++++++++ .../mapCreationGrid/postProcessJob.sh | 281 ++++++++++++ .../mapCreationGrid/stageSlotsForGrid.py | 340 +++++++++++++++ .../mapCreationGrid/submitMapCreation.sh | 222 ++++++++++ .../mapCreationGrid/submitPostProcess.sh | 145 +++++++ 8 files changed, 2167 insertions(+) create mode 100644 DATA/production/configurations/mapCreationGrid/README.md create mode 100755 DATA/production/configurations/mapCreationGrid/discoverResiduals.sh create mode 100755 DATA/production/configurations/mapCreationGrid/makeResidualLists.sh create mode 100755 DATA/production/configurations/mapCreationGrid/mapCreationJob.sh create mode 100755 DATA/production/configurations/mapCreationGrid/postProcessJob.sh create mode 100755 DATA/production/configurations/mapCreationGrid/stageSlotsForGrid.py create mode 100755 DATA/production/configurations/mapCreationGrid/submitMapCreation.sh create mode 100755 DATA/production/configurations/mapCreationGrid/submitPostProcess.sh diff --git a/DATA/production/configurations/mapCreationGrid/README.md b/DATA/production/configurations/mapCreationGrid/README.md new file mode 100644 index 000000000..d022fe8f5 --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/README.md @@ -0,0 +1,402 @@ +# TPC Map Creation on the GRID + +Runs the ALICE TPC space-charge distortion map creation chain on the ALICE GRID, as a two-stage +pipeline: a heavy map-creation stage (hours per slot) followed by a fast post-processing stage +(minutes per slot). Ported from the original SLURM/Lustre-based production. + +The four ROOT macros this pipeline runs (`staticMapCreatorCPM.C`, `SmoothingExtrapolate.C`, +`voxResQA.C`, `TPCFastTransformInitCPM.C`) live in the main O2 repository, under +`Detectors/TPC/calibration/SpacePoints/macro/` and `GPU/TPCFastTransformation/macro/` respectively, and +install to `$O2_ROOT/share/macro/` as part of a normal O2 build -- this directory only holds the GRID +submission/orchestration layer (list discovery, manifest staging, `grid_submit.sh` wrappers, per-slot +job scripts). + +## Quickstart + +For a normal production on the official O2 build, with defaults left alone, only two settings usually +need changing: the bad-time-ranges list and how long each map should span. Everything else below can be +left as-is. + +1. **Build the slot lists** (local, once per batch of runs): + + ```bash + ./discoverResiduals.sh + ``` + + `` lists one run per line: ` `, e.g. `LHC26ak 572557`. + +2. **Submit stage 1 (map creation).** In `submitMapCreation.sh`, set: + - `BAD_RANGES_NAME` — the bad-time-ranges file for this data period (keep the default if it still + applies). + - `SLOT_LENGTH_MIN` — how many minutes of data go into one map, e.g. `5`. + - `JOBNAME` — any name that's unique to this submission; you'll need it again in step 3. + + Then: + + ```bash + ./submitMapCreation.sh [path/to/lists] + ``` + + `path/to/lists` is the `./lists` directory step 1 wrote (default: `./lists` in the current directory) + — only pass it explicitly if you're running this from a different directory than step 1. + + This takes hours per map — watch progress on alimonitor and wait for it to finish (or mostly finish) + before continuing. + +3. **Submit stage 2 (smoothing + FastTransform).** In `submitPostProcess.sh`, set `STAGE1_JOBNAME_TAG` + to the exact `JOBNAME` used in step 2, then: + + ```bash + ./submitPostProcess.sh + ``` + + This takes minutes per map and produces the final smoothed map, the `FT_*.root` FastTransform file, + and QA plots. + +Everything else in this document explains what the other settings do and how to debug things when they +don't go as expected — skip ahead to it only if you need to change something beyond the three above. + +## Prerequisites + +- A valid AliEn/JAliEn token (`alien-token-init `) and `alien.py` on your `PATH`. +- `O2DPG_ROOT` set (provides `grid_submit.sh`). +- **bash ≥ 4** for the locally-run scripts (`discoverResiduals.sh`, `makeResidualLists.sh`) — they use + `mapfile`. macOS still ships bash 3.2, so on a Mac install a newer one (`brew install bash`) and make + sure it precedes `/bin` on your `PATH`; `#!/usr/bin/env bash` then picks it up. Note this also makes + `set -e` stricter (an arithmetic `(( ))` evaluating to 0 aborts), which is the behaviour lxplus has. +- GNU `parallel` (used by `discoverResiduals.sh`). +- Write access to your AliEn home directory. The wrappers derive + `alien:///alice/cern.ch/user///MapCreation` automatically from `ALIEN_USER` (falling + back to your local username), the same way `grid_submit.sh` derives `MY_HOMEDIR` — **no per-user edit + needed**. Both wrappers print the resolved directory before doing anything, so check that line on your + first run: if your laptop's local username differs from your AliEn account, `export ALIEN_USER=`, or set `MAPCREATION_ALIEN_DIR` to override the whole path. This directory only holds + the slot manifest, the bad-time-ranges list, and the `stage1Workdir` pointer files -- not the actual + map data (see Step 3) -- and, if `MACRO_SOURCE=alien` (see below), a staged copy of the macros. +- A `--packagespec` tag whose O2 build includes the four macros above, **or** `MACRO_SOURCE=alien` (see + "Where the macros come from" below) if no such tag is published yet. + +## Where the macros come from (`MACRO_SOURCE`) + +Every payload script (`mapCreationJob.sh`, `postProcessJob.sh`) reads a +`MACRO_SOURCE` variable, substituted by its wrapper the same way as `JOBNAME_TAG`: + +- **`official` (default)**: runs the macros straight from the O2 package this job's `--packagespec` + loads (`$O2_ROOT/share/macro/.C`) — no staging or download at all. This is the same idiom + `DATA/production/configurations/asyncCalib/createCorrectionMap.sh` already uses for the older + `staticMapCreator.C`/`TPCFastTransformInit.C` pair. Requires `PACKAGESPEC` to resolve to an O2 build new + enough to include `staticMapCreatorCPM.C` et al. If it doesn't, the job fails fast with a clear error + rather than silently falling back. +- **`alien`**: stages a macro from a local path (`MACRO_LOCAL_PATH` in `submitMapCreation.sh`, + `MACRO_LOCAL_DIR` in `submitPostProcess.sh`) to AliEn, and jobs download it + from there instead. Use this until a `--packagespec` tag with the macros is published — point + `MACRO_LOCAL_PATH`/`MACRO_LOCAL_DIR` at your own O2 checkout's copy of the macro(s). + +## The chain, in order + +``` +discoverResiduals.sh (local) find residual files on AliEn, build per-run slot lists + │ + ▼ +submitMapCreation.sh (submits) STAGE 1: map creation (hours/slot) + │ └─ per-slot payload: mapCreationJob.sh + ▼ (wait for jobs to finish on alimonitor) + │ +submitPostProcess.sh (submits) STAGE 2: smoothing + FastTransform (minutes/slot) + └─ per-slot payload: postProcessJob.sh +``` + +Stage 1 and stage 2 are **independent GRID submissions**, not one script. If some stage-1 slots fail +or are still running, stage 2 simply skips those (see "Partial failure" below) — no need to wait for +100% of stage 1 before submitting stage 2, and a failed stage-2 subjob can be resubmitted on its own +without redoing the expensive stage-1 work for that slot. + +### Step 1 — discover residual files and build slot lists (local, once per batch of runs) + +```bash +./discoverResiduals.sh [--calib-pass cpass2_residuals] +``` + +`` is one run per line: ` `, e.g.: + +``` +LHC26ak 572557 +LHC26ak 572558 +``` + +This queries AliEn (`/alice/data/////`) for `o2tpc_residuals_*.root` +files and writes per-run slot lists under `./lists//`. Runs in parallel (`N_PARALLEL=8` in the +script). Safe to re-run; each run is independent. The period is resolved **per line**, so a batch file +may legitimately span periods; malformed rows are rejected up front rather than as one failed run +mid-batch. + +**One list per run — granularity comes from `SLOT_LENGTH_MIN`, not from the catalog.** Each residual +filename carries the aggregation slot label `___` its reco job +assigned, and those labels are **not** a uniform grid — same-length slots are not guaranteed, and a +run's catalog can hold overlapping/nested labels at different granularities. Example (run 572266): + +``` +0_105425 (22 files) 105426_210851 (6) 0_210851 (35) +210852_316277 (2) 210852_421703 (45) +``` + +Slicing per label would give overlapping slots with wildly uneven statistics — the 2- and 6-file ones +trip `MIN_TRACKS_PER_SLICE` and come back `badCalib_` while the data that would have fixed them sits in +a sibling list. So `makeResidualLists.sh` writes **one list per run** holding every file, and you choose +the map length with `SLOT_LENGTH_MIN` at submission time. It is then uniform across runs and independent +of whatever granularity the upstream catalog happens to have. + +`stageSlotsForGrid.py` cuts each run into windows of `SLOT_LENGTH_MIN` and writes **one manifest line +per window**, carrying that window's bounds and only the files whose label overlaps it. A file's +contents are always a subset of its label's range, so this selection never discards data that could +contribute, and it costs nothing — no file is opened. That matters: the macro's own per-file skip has to +*open* each out-of-window file to read its time range (~7 s each over `alien://`, at ~zero CPU), and +AliEn kills a payload whose CPU stays low for 15 min. For run 572266: + +| `SLOT_LENGTH_MIN` | windows × files per subjob (of 110) | +|---|---| +| `0` (one map per run) | 1 × 110 | +| `10` | 2 × (63, 47) | +| `5` | 4 × (57, 41, 47, 45) | + +A coarse-labelled file spanning several windows is deliberately kept by each of them; the macro's per-TF +`firstTFTime`/`lastTFTime` cut then decides which TFs each map actually uses, so nothing is +double-counted. That is why the per-subjob counts can sum to more than the total. + +A pure overlap test is too generous at window edges, though: the aggregator's coarse labels routinely +begin 1–2 ms before a window ends, and that was enough to drag all 45 of their files into a window whose +map they contribute nothing to (run 572266 at 5 min: window 1 took 88 files to use 41). So a file is +only assigned to a window when the overlap **exceeds `OVERLAP_MARGIN_MS`** (default 1000). The cost is +bounded exactly: if a label overlaps by X ms then at most X ms of that file's data belongs here, so +skipping at X ≤ margin loses at most `margin` ms of coverage per window edge — 1 s out of 300 s. The +margin is clamped to a tenth of the window, and `stageSlotsForGrid.py` verifies that every file still +lands in at least one window, warning loudly if the margin is ever set high enough to orphan one. +With it, run 572266 at 5 min gives 57 / 41 / 47 / 45 instead of 63 / 88 / 47 / 45. + +`mapCreationJob.sh` additionally *orders* each window's files by orbit distance from the window centre, +so whatever still comes along is opened last and `MAX_TRACKS_PER_SLICE` usually stops the job first. + +Neither job script does any time arithmetic: both read the run and the window straight off the same +manifest line, so they cannot disagree about a slot's identity. + +**Previewing the split.** The windows are decided at submission time, not at discovery, so `./lists/` +holds one file per *run*, not per window. To see how a given `SLOT_LENGTH_MIN` actually slices your +runs, run the manifest builder in preview mode — it skips the AliEn upload, so it needs no token and +re-queries nothing; add `--window-lists` to also write one plain file list per window: + +```bash +./stageSlotsForGrid.py --lists-dir ./lists --manifest-name mapSlots.preview.tsv \ + --slot-length-min 5 --preview --window-lists +ls mapSlots.preview.windows/ # one .TDof._.txt per window +``` + +`--preview` skips the upload, so it needs no token and re-queries nothing; `--window-lists` additionally +writes the per-window lists. See `./stageSlotsForGrid.py --help` for the full option list. + +The window boundaries and file counts are printed as it goes, which is all you usually need. To see +*which* files a window got, add `--window-lists`. That is off by default because on a parallel +filesystem each list is a metadata round trip: on Lustre, 120 of them measured **32.8 s**, against +0.03 s for all the actual work. Pair it with `--window-dir /tmp/windows` to keep them on local disk. +`--timing` prints the read/compute/write breakdown. + +The manifest itself is always written locally — one file, and the complete record of the split (one +line per window, sixth column being the comma-separated file list). + +Cheap enough to compare several lengths back to back. For run 572266 (20 min): + +| `SLOT_LENGTH_MIN` | windows | files per window | +|---|---|---| +| `10` | 2 | 63, 47 | +| `5` | 4 | 57, 41, 47, 45 | +| `2` | 10 | 57, 57, 63, 41, 41, 47, 47, 47, 45, 45 | + +Note the counts stop falling below ~5 min: a coarse aggregation label spanning 10 minutes overlaps +*every* 2-minute window inside it, so slicing finer than the aggregator's own slot length buys time +resolution but not less I/O per job. Add `--window-lists` to a normal (non-preview) run to write the +same directory alongside the uploaded manifest, so you can check afterwards what a submission actually +used. + +**`./lists/` accumulates.** A second batch discovered into the same tree does not replace the first — +it adds to it, and Step 2 by default puts *everything* under `./lists/` into the manifest. That +silently recalibrates the earlier batch from scratch at hours per slot. Either use a fresh `listsDir` +per batch, or set `RUNS_FILTER` in the Step-2 wrapper (see below). + +### Step 2 — configure and submit stage 1 (map creation) + +Edit the config block at the top of **`submitMapCreation.sh`**: + +| Variable | Meaning | +|---|---| +| `JOBNAME` | Identifies this production. Tags the AliEn manifest (`mapSlots..tsv`) and the small `stage1Workdir..txt` pointer file (see Step 3) — **must be unique per submission** so concurrent/repeated productions don't clobber each other's manifest or pointer. Stage 2 needs to know this value (see Step 4). | +| `TOPWORKDIR` | Static top-level bucket under your GRID home for this job's working directory (replaces `grid_submit.sh`'s generic `selfjobs` default with something identifiable). | +| `SLOT_LENGTH_MIN` | Target length of **one map**, in minutes. Each run is cut into whole steps of this length; a trailing remainder shorter than half a step is absorbed into the last window rather than left as a runt that would fail `MIN_TRACKS_PER_SLICE`. `0` = one map per run. A 20 min run at `5` gives 4 maps, a 60 min run gives 12 — the count follows the run length instead of being fixed. | +| `PACKAGESPEC` | O2/O2DPG software tag to run under — set this to the async-reco tag matching your data. Also determines whether `MACRO_SOURCE=official` can find the macros (see "Where the macros come from" above). | +| `ASUSER` | AliEn account this job runs/is charged as, e.g. `pwg_pp` (your account must already be a member of that group) instead of your own personal account/quota. Determines both which quota pays for the job and where its `${ALIEN_JDL_OUTPUTDIR}` lands (see Step 3). Verify on alimonitor that the first job is really attributed correctly before relying on it for a big batch. | +| `MACRO_SOURCE` / `MACRO_LOCAL_PATH` | See "Where the macros come from" above. | +| `RUNS_FILTER` | Optional comma-separated run whitelist, e.g. `"572557,572558"`. Empty means every run found under `listsDir` — including runs left over from an earlier batch (see Step 1). Naming a run that has no lists is a hard error, not a silently smaller production. | +| `BAD_RANGES_NAME` | Filename of the bad-time-ranges list under `${MACRO_ALIEN_DIR}/lists/`. Period- and system-specific (the default is the PbPb26 list), so change it when the data changes. It is both the upload destination and the name each job downloads, so the two cannot drift apart. Set to `""` to disable bad-ranges filtering entirely for this submission — a deliberate opt-out (`staticMapCreatorCPM.C` treats an empty filename that way), not the same as forgetting to set it. | +| `BAD_RANGES_LOCAL` | Local path to the bad-time-ranges list, only needed if you want to (re-)upload a new one this submission. Leave empty to reuse whatever is already staged on AliEn under `BAD_RANGES_NAME` — the wrapper checks that it actually exists there before submitting anything, so a wrong `BAD_RANGES_NAME`/`ALIEN_USER` or a never-uploaded file fails fast, locally, once, instead of inside every GRID subjob. | +| `MACRO_ALIEN_DIR_BARE` | AliEn directory (no `alien://` prefix) used for staging the slot manifest and the small `stage1Workdir` pointer file (see Step 3) — and the macro too, when `MACRO_SOURCE=alien`. **Not** the actual map data, which goes to `${ALIEN_JDL_OUTPUTDIR}` instead. Only edit it here — the wrapper substitutes it into the payload script (`mapCreationJob.sh`) automatically before every submission, same mechanism as `JOBNAME_TAG`. Don't hand-edit `MACRO_ALIEN_DIR_BARE`/`MACRO_ALIEN_DIR` inside the payload scripts themselves — it's a placeholder line there, not a real per-user setting. | + +**Physics/processing parameters live in a different file** — edit the config block in +**`mapCreationJob.sh`** itself (not the wrapper; these are not part of the wrapper's +placeholder-substitution mechanism, so a hand-edit here is a normal, permanent change, not something +that gets overwritten per submission): + +| Variable | Meaning | +|---|---| +| `Z2XBINNING`, `Y2XBINNING` | Voxel binning along z/x and y/x. | +| `USE_SMOOTHED` | Passed through to the macro's `useSmoothed`. | +| `CREATE_SPLINE` | Leave `false` — `TPCFastTransformInitCPM.C` is invoked separately in stage 2, not from this macro. | +| `MAX_TRACKS_PER_SLICE` / `MIN_TRACKS_PER_SLICE` | Per-voxel-slice track count cap / floor. Falling short of `MIN_TRACKS_PER_SLICE` is what triggers the `badCalib_` marking (see Step 3). | +| `MAX_DEDX`, `MAX_DEDX_EXP`, `MAX_DEV_DEDX_OVER_EXP` | dE/dx-based track rejection cuts (`-1` = disabled). | +| `SKIP_EDGE_PADS` | Skip edge pads in the calibration. | +| `BAD_RANGE_SELECTION` | Which bad-time-range comment tag(s) to apply (`"ALL"` = every entry in the bad-ranges list). | +| `MAX_Z2X_CUT` | Track `tgl` cut (`SpacePointsCalibConfParam scdcalib.maxZ2X`) — real production value is `1.4`. | +| `MAX_TRACK_WORKERS` | **Set this to match your GRID job's actual allocated CPU core count** (i.e. whatever `--packagespec`/JDL gives you, commonly matching a `multicore_N` tag). Auto-detection via `hardware_concurrency()` does *not* reliably reflect the real GRID allocation — confirmed auto-detecting 32 threads on an 8-core job (4x oversubscription) once. Getting this wrong doesn't fail the job, just wastes/starves CPU. | + +Note: the macro no longer reads IDC scalers from CCDB directly (`useCTPLumi`/`nScalerRanges`/`useScalerRange` +are gone from its signature) — luminosity values are joined back offline by `SmoothingExtrapolate.C` in +stage 2 instead (its own `USE_CTP_LUMI`, see Step 4). + +Then: + +```bash +./submitMapCreation.sh [listsDir] # listsDir defaults to ./lists +``` + +This builds + uploads the slot manifest (and, if `MACRO_SOURCE=alien`, re-stages the macro) and submits +a SINGLE `grid_submit.sh --prodsplit N` production, where N is the total number of windows across all +selected runs. There is no longer one submission per time division: a subjob index identifies a +(run, window) pair on its own, because the windows are baked into the manifest. + +**Local single-slot test before submitting to the GRID** (recommended — much faster feedback loop): + +```bash +SUBJOBID=1 ./mapCreationJob.sh +``` + +Runs subjob 1's slot locally using whatever manifest/macro is already staged on AliEn. + +### Step 3 — monitor and wait + +Watch progress on alimonitor (job status, logs, quota). + +**Where the output actually goes** (per ALICE Grid support): each slot's map is staged +directly to *that job's own* JDL-assigned output directory (`${ALIEN_JDL_OUTPUTDIR}`), **not** a fixed +path under your own AliEn home — writing multi-GB production data into personal space would need its +own separate disk-quota request on top of the CPU one, so this pipeline deliberately doesn't do that. +Concretely, for subjob N: + +``` +/-//voxRes.__.TDof.it0.root +``` + +That parent directory is only known once the job actually runs and is otherwise unrecoverable later +(it includes the real submission timestamp) — so `submitMapCreation.sh` captures it the +moment it submits and uploads it as a small pointer file, `stage1Workdir..txt`, to +your own AliEn space (negligible size — a single line of text, not the actual data). Stage 2 downloads +this pointer and reconstructs each slot's exact output location from it (see Step 4) — you shouldn't +need to look at it directly, but it's what makes stage 2 able to find stage 1's output at all. + +If a slot didn't reach the minimum track-count threshold, its map is staged with a `badCalib_` prefix +instead of the normal name — that's the real production's own convention, replicated here so stage 2 +knows to skip it (see below). This is expected behavior for low-statistics slots, not a failure. + +You do **not** need to wait for every stage-1 subjob to finish before moving on to stage 2 — see +"Partial failure" below. + +**Note**: there's no clean, human-browsable `/...`-style directory tree here (unlike the real +input data's own `/alice/data////...` catalog convention) — that catalog is +populated by a centrally-run, formally-registered production system with dedicated write access, not +something reachable via a generic `grid_submit.sh` submission like this one. If you want a tidy, +reorganized view of a run's output later, that needs a separate finalize/merge step (copying out of +these per-job locations into wherever makes sense) — deliberately not built as part of this pipeline. + +### Step 4 — configure and submit stage 2 (smoothing + FastTransform) + +Edit the config block at the top of **`submitPostProcess.sh`**: + +| Variable | Meaning | +|---|---| +| `JOBNAME` | This stage's *own* job name (just for GRID job monitoring) — **not** the same thing as `STAGE1_JOBNAME_TAG` below. | +| `STAGE1_JOBNAME_TAG` | **Must equal the `JOBNAME` stage 1 was actually submitted with** (Step 2). This is what stage 2 uses to find both the manifest and the `stage1Workdir..txt` pointer file that tells it where stage 1's own jobs staged their maps. A mismatch here doesn't corrupt anything — the wrapper fails loudly up front if the pointer file can't be fetched (see Step 4's submission output), rather than silently producing zero output. | +| `TOPWORKDIR` | Same meaning as stage 1's. Independent value — stage 2 is its own GRID submission with its own working directory, not nested under stage 1's. | +| `PACKAGESPEC` | Same meaning as stage 1; keep in sync. Also determines whether `MACRO_SOURCE=official` can find the macros. | +| `ASUSER` | Same meaning as stage 1's — independent of what stage 1 used (stage 2's own submission). | +| `MACRO_SOURCE` / `MACRO_LOCAL_DIR` | See "Where the macros come from" above (this stage uploads all three macros at once when `MACRO_SOURCE=alien`, via `MACRO_LOCAL_DIR` rather than a single-file path). | + +**Physics/processing parameters**, same as stage 1, live in the payload script +(**`postProcessJob.sh`**), not the wrapper: + +| Variable | Meaning | +|---|---| +| `USE_SMOOTHED` | Should match stage 1's `USE_SMOOTHED` — selects `DO_GAUS_SMOOTH` below. | +| `DO_GAUS_SMOOTH` | Whether `SmoothingExtrapolate.C` applies Gaussian smoothing. | +| `DO_EXTRAPOLATION` | `2` = extrapolate both smoothed and raw values. | +| `A11_MAX_Z2X` | `-1` = disabled. | +| `MASK_IA11` | `0` = disabled. | +| `USE_CTP_LUMI` | Selects `TPCFastTransformInitCPM.C`'s luminosity source. `2` = CTP (matches stage 1's macro not accessing IDC scalers directly at all -- see the note at the end of Step 2). | +| `QA_DRAW_ERRORS` | `voxResQA.C` option — draw error bars. | +| `QA_USE_SMOOTHED` | `voxResQA.C` option — `true` draws the smoothed dXS/dYS/dZS (needs `SmoothingExtrapolate.C`'s output, which is exactly what it runs on); `false` draws raw dX/dY/dZ instead. | +| `QA_Z2X_BIN_SEL` | `voxResQA.C` option — `-1` = all z2x bins. | + +**QA plots**: stage 2 also runs `voxResQA.C` on each slot's smoothed map, producing +`voxResQA_1D.root`/`_2D.root` (canvases) plus individual `.png` quick-look images. QA failure +is non-fatal — it won't fail an otherwise-good slot. + +Then: + +```bash +./submitPostProcess.sh +``` + +This downloads the *existing* manifest stage 1 already uploaded (does not rebuild it — see the comment in +the script for why that matters), re-stages the three macros if `MACRO_SOURCE=alien`, and submits one +production, same slot granularity as stage 1. + +**Local single-slot test:** + +```bash +SUBJOBID=1 ./postProcessJob.sh +``` + +Stage 2's final output (the smoothed map, `FT_*.root` FastTransform file, and the two `voxResQA*.root` +QA files) is staged the same way as stage 1's — directly to *this job's own* `${ALIEN_JDL_OUTPUTDIR}`, +not stage 1's. Since stage 2 is a completely separate GRID submission (its own `JOBNAME`, its own +submission timestamp), this is a **different directory tree** from stage 1's, even for the exact same +logical slot: + +``` +/-// + voxRes.__.TDof.it0.GausSmooth.Extrapolation.root + FT_voxRes.__.TDof.it0.GausSmooth.Extrapolation.root + voxResQA.voxRes.__.TDof.it0.GausSmooth.Extrapolation_1D.root + voxResQA.voxRes.__.TDof.it0.GausSmooth.Extrapolation_2D.root +``` + +Stage 1's and stage 2's output for the same slot are *not* grouped under one tidy parent directory — +that's the direct cost of letting the JDL own output placement instead of managing it ourselves (see +the note at the end of Step 3). Same disk-quota reasoning as stage 1: explicit, synchronous `alien.py cp` +straight there, not the passive `#JDL_OUTPUT@disk=N` auto-upload (which only happens during the job's +asynchronous SAVING phase, after this script has already exited). + +## Partial failure — this is by design, not a workaround + +- **Some stage-1 slots fail or haven't finished**: stage 2, for that slot, finds nothing at the + reconstructed `${ALIEN_JDL_OUTPUTDIR}` location, logs a warning, and exits 0 (success/no-op) — it does + not fail the whole production. Resubmit just the missing stage-1 slot(s) later, then resubmit the + corresponding stage-2 subjob(s). +- **A stage-1 slot is marked `badCalib_`** (too few tracks — see Step 3): stage 2 recognizes the + `badCalib_` prefix specifically and logs that the calibration was intentionally marked bad, rather + than the generic "not found" message. Also a no-op, not a failure. +- **Some stage-2 subjobs fail** (e.g. a transient GRID/network issue): only those need resubmitting — + stage 1's expensive output is untouched and doesn't need to be redone. + +## Known gotchas + +- **`STAGE1_JOBNAME_TAG` is the only thing stage 2 has to get right.** It selects both the manifest and the `stage1Workdir` pointer. There is no time-window arithmetic to keep in sync any more — both stages read each slot's window straight off the same manifest line. A wrong tag fails loudly at the pointer fetch; a *stale* pointer under a reused tag would still no-op silently, so keep `JOBNAME` unique per submission. +- **Don't hand-edit `STAGE1_OUTPUTDIR_BASE`/`COUNTERWIDTH`/`MACRO_ALIEN_DIR_BARE`/`BAD_RANGES_NAME` inside the payload scripts** — like `JOBNAME_TAG`, these are placeholder lines the wrapper substitutes automatically per submission, not real per-user settings. +- **A window split can, rarely, produce a half with zero real data** if the split point lands past the run's actual end-of-run (a residual file's nominal timestamp range can pad past the true last data). That slot correctly gets marked `badCalib_` — it's an expected edge case, not something to debug further; the macro now also fails fast on it instead of wasting the CPU-time quota downloading data it will discard. +- **AliEn `wc -l`-derived counts can come back whitespace-padded**, which `grid_submit.sh --prodsplit` rejects outright (`Production split must be a positive integer (got ' 70')`) — already fixed in the checked-in scripts, just noting it in case you copy this pattern elsewhere. +- **Job success rate around 80-90% is normal** for a first production pass — check individual failed-job logs on alimonitor before assuming something is systemically broken. diff --git a/DATA/production/configurations/mapCreationGrid/discoverResiduals.sh b/DATA/production/configurations/mapCreationGrid/discoverResiduals.sh new file mode 100755 index 000000000..b2d714c4b --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/discoverResiduals.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# discoverResiduals.sh +# +# Runs locally. For each run in a batch file, calls makeResidualLists.sh +# with --alien to query AliEn for o2tpc_residuals*.root files and build +# per-period residual lists under lists//. +# +# Runs are processed in parallel using GNU parallel. +# +# Usage: +# ./discoverResiduals.sh [--calib-pass ] +# +# Batch file format (one run per line, whitespace-separated): +# +# e.g.: LHC26ak 572557 +# +# PERIOD is derived from the first field by stripping from the first '.' onward +# (e.g. LHC26ab.b5p → LHC26ab — a trailing '.xyz' suffix is tolerated but not +# required). +# YEAR is derived from the two-digit year in the period token +# (e.g. LHC26ab → 2026). +# +# Dependencies: alien.py, parallel, makeResidualLists.sh + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +BATCH_FILE="" +CALIB_PASS="cpass2_residuals" + +while [[ $# -gt 0 ]]; do + case "$1" in + --calib-pass) + [[ $# -lt 2 ]] && { echo "ERROR: --calib-pass requires an argument"; exit 1; } + CALIB_PASS="$2"; shift 2 ;; + -*) + echo "ERROR: Unknown option: $1"; exit 1 ;; + *) + [[ -n "${BATCH_FILE}" ]] && { echo "ERROR: unexpected extra argument: $1"; exit 1; } + BATCH_FILE="$1"; shift ;; + esac +done + +[[ -z "${BATCH_FILE}" ]] && { echo "Usage: $0 [--calib-pass ]"; exit 1; } +[[ -f "${BATCH_FILE}" ]] || { echo "ERROR: batch file not found: ${BATCH_FILE}"; exit 1; } + +# --------------------------------------------------------------------------- +# Parse the batch file into one " " entry per line +# --------------------------------------------------------------------------- +# The period is resolved PER LINE, not once from the first line: the batch format carries a period +# token on every row, so a batch legitimately spanning two periods must not silently inherit the +# first row's period for all of its runs (that produced a valid-looking /alice/data//// query, which just finds nothing and degrades into a per-run warning). +mapfile -t ENTRIES < <(awk 'NF > 0 && !/^[[:space:]]*#/ {print $1, $2}' "${BATCH_FILE}") +[[ ${#ENTRIES[@]} -gt 0 ]] || { echo "ERROR: no runs found in ${BATCH_FILE}"; exit 1; } + +# Validate every entry up front -- a malformed row should stop the batch here, not surface as one +# failed run out of fifty after the parallel fan-out has already started. +RUNS=() +PERIODS_SEEN=() +for entry in "${ENTRIES[@]}"; do + read -r _tok _run <<< "${entry}" + [[ -n "${_tok}" && -n "${_run}" ]] \ + || { echo "ERROR: malformed line in ${BATCH_FILE} (need ' '): '${entry}'"; exit 1; } + [[ "${_run}" =~ ^[0-9]+$ ]] \ + || { echo "ERROR: run number is not numeric in ${BATCH_FILE}: '${entry}'"; exit 1; } + # Field 1 example: LHC26ab.b5p → PERIOD=LHC26ab, YEAR=2026 + [[ "${_tok%%.*}" =~ LHC([0-9]{2}) ]] \ + || { echo "ERROR: cannot extract year from period token '${_tok}' (line: '${entry}')"; exit 1; } + RUNS+=("${_run}") + PERIODS_SEEN+=("${_tok%%.*}") +done + +mapfile -t PERIODS_UNIQ < <(printf '%s\n' "${PERIODS_SEEN[@]}" | sort -u) + +# --------------------------------------------------------------------------- +# Fixed configuration +# --------------------------------------------------------------------------- +# Maximum number of runs to process in parallel (0 = one per CPU core) +N_PARALLEL=8 + +# Root of the output tree (relative or absolute) +WORK_DIR="./lists" + +# Directory containing this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Path to makeResidualLists.sh (co-located here) +MAKE_LISTS_SCRIPT="${SCRIPT_DIR}/makeResidualLists.sh" + +# --------------------------------------------------------------------------- + +echo "Batch file : ${BATCH_FILE}" +echo "Period(s) : ${PERIODS_UNIQ[*]}" +echo "Calib pass : ${CALIB_PASS}" +echo "Runs (${#RUNS[@]}): ${RUNS[*]}" +echo + +mkdir -p "${WORK_DIR}" + +# --------------------------------------------------------------------------- +# per_run — all work for a single run, called by parallel +# +# On success writes /run_completed.txt containing the absolute +# path of RUN_DIR so the parent shell can collect completed runs after +# parallel finishes. +# --------------------------------------------------------------------------- +per_run() { + local PERIOD_TOKEN="$1" # field 1 of this batch-file row + local RUN="$2" # field 2 of this batch-file row + local CALIB_PASS="$3" + local WORK_DIR="$4" + local MAKE_LISTS_SCRIPT="$5" + + # Period/year come from THIS row, so a multi-period batch resolves each run against its own period. + # Already validated in the caller's pre-flight loop, so a parse failure here would be a bug. + local PERIOD YEAR + PERIOD="${PERIOD_TOKEN%%.*}" # strip suffix after first '.' + [[ "${PERIOD}" =~ LHC([0-9]{2}) ]] + YEAR="20${BASH_REMATCH[1]}" + + local RUN_DIR="${WORK_DIR}/${RUN}" + mkdir -p "${RUN_DIR}" + + local ALIEN_PATH="/alice/data/${YEAR}/${PERIOD}/${RUN}/${CALIB_PASS}/" + + echo "========================================" + echo "Processing run ${RUN} (period ${PERIOD}, year ${YEAR})" + echo "========================================" + + echo " [${RUN}] Calling makeResidualLists.sh (alien path: ${ALIEN_PATH})..." + ( + cd "${RUN_DIR}" + run="${RUN}" "${MAKE_LISTS_SCRIPT}" --subdir "${CALIB_PASS}" --alien --alien-path "${ALIEN_PATH}" + ) || { + echo " [${RUN}] WARNING: makeResidualLists.sh failed, skipping." + return 0 + } + + echo " [${RUN}] Done." + + # Signal success to the parent by writing the absolute run dir path + realpath "${RUN_DIR}" > "${RUN_DIR}/run_completed.txt" +} + +# Export everything parallel needs to call per_run in a subshell +export -f per_run + +# --------------------------------------------------------------------------- +# Run all runs in parallel +# --------------------------------------------------------------------------- +echo "========================================" +echo "Launching ${#RUNS[@]} runs with N_PARALLEL=${N_PARALLEL}..." +echo "========================================" + +# Fan out over the full " " entries, not bare run numbers, so each job carries its +# own period. --colsep splits each entry into {1}=period token and {2}=run; note that under --colsep +# a bare {} means column 1 ONLY, so both columns must be passed explicitly. --tagstring uses {2} to +# keep the log prefix to just the run number. +# shellcheck disable=SC1083 # {1}/{2} are GNU parallel replacement strings, not brace expansions +parallel \ + --jobs "${N_PARALLEL}" \ + --line-buffer \ + --colsep ' ' \ + --tagstring "[{2}]" \ + per_run {1} {2} "${CALIB_PASS}" "${WORK_DIR}" "${MAKE_LISTS_SCRIPT}" \ + ::: "${ENTRIES[@]}" + +# --------------------------------------------------------------------------- +# Collect completed runs (sentinel files written by per_run on success) +# --------------------------------------------------------------------------- +COMPLETED_RUNS=() +for RUN in "${RUNS[@]}"; do + SENTINEL="${WORK_DIR}/${RUN}/run_completed.txt" + if [[ -f "${SENTINEL}" ]]; then + COMPLETED_RUNS+=("$(cat "${SENTINEL}")") + rm -f "${SENTINEL}" + fi +done + +echo "========================================" +echo "All runs processed. ${#COMPLETED_RUNS[@]}/${#RUNS[@]} succeeded." +echo "========================================" + +if (( ${#COMPLETED_RUNS[@]} == 0 )); then + echo "No runs completed successfully." + exit 0 +fi + +# Sort for deterministic output order. Must come AFTER the empty check: expanding "${arr[@]}" on an +# empty array is an "unbound variable" error under `set -u` in bash <= 4.3. +IFS=$'\n' COMPLETED_RUNS=($(printf '%s\n' "${COMPLETED_RUNS[@]}" | sort)) +unset IFS + +echo "Per-run slot-file indexes (input for stageSlotsForGrid.py):" +for RUN_DIR in "${COMPLETED_RUNS[@]}"; do + RUN="$(basename "${RUN_DIR}")" + echo " ${RUN_DIR}/${RUN}.residuals_lists.txt" +done diff --git a/DATA/production/configurations/mapCreationGrid/makeResidualLists.sh b/DATA/production/configurations/mapCreationGrid/makeResidualLists.sh new file mode 100755 index 000000000..cbdb6fd51 --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/makeResidualLists.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +# makeResidualLists.sh +# Find o2tpc_residuals*.root files (local or alien) and split them into +# per-period lists plus a combined all-runs list. +# +# Single-run usage: +# run= ./makeResidualLists.sh [--subdir ] [--alien] [--dry-run] +# +# Multi-run usage (loops over all run directories automatically): +# ./makeResidualLists.sh --all [--subdir ] [--alien] [--dry-run] +# +# Arguments: +# --all loop over all NNN*/ subdirs in the current directory +# --subdir subdirectory within each run dir to search in (default: cpass1_residuals) +# --alien use alien.py instead of local find +# --alien-path

explicit AliEn directory to search (overrides --year/--period/--subdir) +# --year year component of the AliEn path (required with --alien unless --alien-path) +# --period period component of the AliEn path (required with --alien unless --alien-path) +# --dry-run print what would be done without writing files +# In the normal chain you never pass --year/--period by hand: discoverResiduals.sh derives them per +# run from the batch file and passes a fully-formed --alien-path instead. +# +# Environment (single-run mode): +# run : run number (optional; used in output filenames) + +set -euo pipefail + +# ── colours ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m' +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' + + +# Safe exit: use return when sourced, exit when run directly +_finish() { local code=${1:-0}; [[ "${BASH_SOURCE[0]}" != "${0}" ]] && return "$code" || exit "$code"; } +error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; _finish 1; } +warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } +info() { echo -e "${CYAN}[INFO]${RESET} $*"; } +ok() { echo -e "${GREEN}[OK]${RESET} $*"; } + +# ── settings ────────────────────────────────────────────────────────────────── +: ${run:=""} +USE_ALIEN=0 +DRY_RUN=0 +ALL_MODE=0 +SUBDIR="cpass1_residuals" +ALIEN_PATH_OVERRIDE="" +YEAR="" +PERIOD="" + +# ── parse arguments ─────────────────────────────────────────────────────────── +while [[ $# -gt 0 ]]; do + case "$1" in + --alien) USE_ALIEN=1 ; shift ;; + --dry-run) DRY_RUN=1 ; shift ;; + --all) ALL_MODE=1 ; shift ;; + --subdir) + [[ $# -lt 2 ]] && error "--subdir requires an argument" + SUBDIR="$2" ; shift 2 ;; + --alien-path) + [[ $# -lt 2 ]] && error "--alien-path requires an argument" + ALIEN_PATH_OVERRIDE="$2" ; shift 2 ;; + --year) + [[ $# -lt 2 ]] && error "--year requires an argument" + YEAR="$2" ; shift 2 ;; + --period) + [[ $# -lt 2 ]] && error "--period requires an argument" + PERIOD="$2" ; shift 2 ;; + *) error "Unknown argument: $1" ;; + esac +done + +# AliEn search path, e.g. /alice/data/2024/LHC24as/560334/cpass4_residuals/ +# +# There is deliberately NO default year/period here any more. There used to be a hardcoded +# YEAR=2026 / PERIOD=LHC26ab pair, which was harmless in the normal chain (discoverResiduals.sh always +# passes --alien-path, which overrides it) but became live in --all mode, where it silently queried +# LHC26ab for every run regardless of what the caller actually asked for. Requiring the caller to be +# explicit turns that into an up-front error instead of an empty result set. +ALIEN_PATH="" +if [[ -n "${ALIEN_PATH_OVERRIDE}" ]]; then + ALIEN_PATH="${ALIEN_PATH_OVERRIDE}" +elif [[ $USE_ALIEN -eq 1 ]]; then + [[ -n "$YEAR" && -n "$PERIOD" ]] \ + || error "--alien needs either --alien-path , or both --year and --period " + ALIEN_PATH="/alice/data/${YEAR}/${PERIOD}/${run}/${SUBDIR}/" +fi + +# ── multi-run mode ──────────────────────────────────────────────────────────── +# Re-invokes this script once per run directory, then collects all_list files. +if [[ $ALL_MODE -eq 1 ]]; then + SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" + TOP="$PWD" + + echo + echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${RESET}" + echo -e "${BOLD}║ TPC Residual List Builder [multi-run mode] ║${RESET}" + echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${RESET}" + echo + info "Base directory : $TOP" + info "Subdir : $SUBDIR" + [[ $DRY_RUN -eq 1 ]] && warn "DRY-RUN mode — no files will be written" + echo + + RUNS=( $(ls -d [0-9]*/ 2>/dev/null | sed 's|/$||') ) + [[ ${#RUNS[@]} -eq 0 ]] && error "No run directories (NNN*/) found in $TOP" + info "Found ${#RUNS[@]} run director(ies): ${RUNS[*]}" + echo + + FAILED=() + for r in "${RUNS[@]}"; do + WORK_DIR="$TOP/$r/$SUBDIR" + if [[ ! -d "$WORK_DIR" ]]; then + warn "Skipping run $r — subdir not found: $WORK_DIR" + FAILED+=("$r") + continue + fi + echo -e "${BOLD}════ Run $r ════${RESET}" + ( + cd "$WORK_DIR" + # Propagate every option that affects WHERE we look, not just the two mode flags. --subdir + # was previously dropped here: the parent used it to build WORK_DIR while the child fell + # back to the "cpass1_residuals" default, so the two disagreed about which calib pass was + # being listed. Array, not a string, so a value containing spaces cannot re-split. + EXTRA_FLAGS=("--subdir" "$SUBDIR") + [[ $USE_ALIEN -eq 1 ]] && EXTRA_FLAGS+=("--alien") + [[ $DRY_RUN -eq 1 ]] && EXTRA_FLAGS+=("--dry-run") + [[ -n "$YEAR" ]] && EXTRA_FLAGS+=("--year" "$YEAR") + [[ -n "$PERIOD" ]] && EXTRA_FLAGS+=("--period" "$PERIOD") + run="$r" bash "$SCRIPT" "${EXTRA_FLAGS[@]}" + ) || { warn "Run $r failed — continuing"; FAILED+=("$r"); } + echo + done + + # Collect the per-run index files back to the top directory + if [[ $DRY_RUN -eq 0 ]]; then + info "Collecting *.residuals_lists.txt → $TOP" + find "$TOP" -mindepth 3 -maxdepth 3 -name "*.residuals_lists.txt" \ + -exec cp {} "$TOP/" \; + else + info "[DRY-RUN] Would copy *.residuals_lists.txt to $TOP" + fi + + echo + if [[ ${#FAILED[@]} -gt 0 ]]; then + warn "Finished with ${#FAILED[@]} skipped/failed run(s): ${FAILED[*]}" + else + ok "All ${#RUNS[@]} run(s) processed successfully." + fi + _finish 0 +fi + +# ── single-run mode ─────────────────────────────────────────────────────────── +# Safety check: refuse to run in single-run mode from a directory that contains +# numeric run subdirectories — that almost certainly means --all was forgotten. +if [[ -z "$run" ]] && ls -d [0-9][0-9][0-9][0-9][0-9][0-9]*/ &>/dev/null; then + error "Current directory contains run subdirectories but no run number is set.\n" \ + " Did you mean: $0 --all\n" \ + " Or: run= $0" +fi + +FILE_ALL="residuals.all.txt" +RUN_PREFIX="${run:+${run}_}" + +echo +echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${RESET}" +echo -e "${BOLD}║ TPC Residual List Builder ║${RESET}" +echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${RESET}" +echo +[[ -n "$run" ]] && info "Run number : ${run}" || warn "No run number set — output filenames will omit run" +info "Working dir: $PWD" +info "Source : $([ $USE_ALIEN -eq 1 ] && echo "alien ($ALIEN_PATH)" || echo "local")" +[[ $DRY_RUN -eq 1 ]] && warn "DRY-RUN mode — no files will be written" +echo + +# ── clean up stale output files from previous run ───────────────────────────── +mapfile -t STALE < <(ls "$FILE_ALL" "residuals.${RUN_PREFIX}"*.txt \ + "${run:-NOMATCH}.residuals_lists.txt" \ + "${run:-NOMATCH}.residuals_all_list.txt" 2>/dev/null || true) + +if [[ ${#STALE[@]} -gt 0 ]]; then + warn "Removing ${#STALE[@]} stale file(s) from previous run:" + for f in "${STALE[@]}"; do + echo " rm $f" + [[ $DRY_RUN -eq 0 ]] && rm -f "$f" + done + echo +fi + +# ── collect residual file list ──────────────────────────────────────────────── +info "Searching for o2tpc_residuals*.root files..." + +if [[ $DRY_RUN -eq 1 ]]; then + if [[ $USE_ALIEN -eq 1 ]]; then + info "[DRY-RUN] Would run: alien.py find $ALIEN_PATH o2tpc_residuals*.root | sed 's|^|alien://|' > $FILE_ALL" + else + COUNT=$(find "$PWD" -name "o2tpc_residuals*.root" | wc -l) + info "[DRY-RUN] Would write $COUNT file path(s) to: $FILE_ALL" + find "$PWD" -name "o2tpc_residuals*.root" \ + | awk -F'/' '{print $NF";"$0}' | sort | awk -F';' '{print $NF}' \ + | head -5 | sed 's/^/ /' || true # `|| true`: head -5 SIGPIPEs the upstream find, + # which under pipefail would abort this dry run + [[ $COUNT -gt 5 ]] && echo " ... ($COUNT total)" + fi + echo + info "[DRY-RUN] Would rename to residuals.all.${RUN_PREFIX}___.txt" + info "[DRY-RUN] Would split into per-period lists residuals.${RUN_PREFIX}.txt" + info "[DRY-RUN] Would write index file ${run:-norunnumber}.residuals_lists.txt" + echo + ok "Dry-run complete — no files written." + _finish 0 +fi + +# ── real mode from here ─────────────────────────────────────────────────────── +if [[ $USE_ALIEN -eq 1 ]]; then + alien.py find "$ALIEN_PATH" o2tpc_residuals*.root \ + | sed 's|^|alien://|' > "$FILE_ALL" +else + find "$PWD" -name "o2tpc_residuals*.root" \ + | awk -F'/' '{print $NF";"$0}' \ + | sort \ + | awk -F';' '{print $NF}' \ + > "$FILE_ALL" +fi + +if [[ ! -s "$FILE_ALL" ]]; then + warn "No o2tpc_residuals*.root files found — all output lists will be empty." + _finish 0 +fi +ok "Found $(wc -l < "$FILE_ALL" | tr -d '[:space:]') file(s)." + +# ── extract timestamp range ─────────────────────────────────────────────────── +read -r firstTS1 lastTS1 firstTF1 lastTF1 <<< \ + "$(head -1 "$FILE_ALL" | sed -rn 's|.*_([0-9]{13})_([0-9]{13})_([0-9]+)_([0-9]+).*|\1 \2 \3 \4|p')" +read -r firstTS2 lastTS2 firstTF2 lastTF2 <<< \ + "$(tail -1 "$FILE_ALL" | sed -rn 's|.*_([0-9]{13})_([0-9]{13})_([0-9]+)_([0-9]+).*|\1 \2 \3 \4|p')" + +# ── MC fallback (no timestamps) ─────────────────────────────────────────────── +if [[ -z "${firstTS1:-}" || -z "${lastTS2:-}" ]]; then + warn "No timestamp info found — assuming MC input." + FILE_ALL_RANGE="residuals.all.${run}.txt" + mv "$FILE_ALL" "$FILE_ALL_RANGE" + ls "$PWD/${FILE_ALL_RANGE}" > "${run}.residuals_lists.txt" + ok "Done (MC mode)." + _finish 0 +fi + +# ── rename with timestamp range ─────────────────────────────────────────────── +FILE_ALL_RANGE="residuals.all.${RUN_PREFIX}${firstTS1}_${lastTS2}_${firstTF1}_${lastTF2}.txt" +info "Renaming: $FILE_ALL → $FILE_ALL_RANGE" +mv "$FILE_ALL" "$FILE_ALL_RANGE" + +# ── write the run's slot list ───────────────────────────────────────────────── +# ONE list per run, holding every residual file found. Slot granularity is deliberately NOT taken from +# the aggregation labels in the filenames -- it is chosen at submission time via SLOT_LENGTH_MIN, and +# mapCreationJob.sh prefilters each subjob's copy of this list down to the files that can actually +# contribute to its own time window. +# +# Why not group by label: each residual filename carries a slot label +# ___, and those labels are NOT a uniform grid -- same-length slots +# are not guaranteed, and a run's catalog can hold overlapping/nested labels at different granularities +# (why doesn't matter here, just that it happens), e.g. run 572266: +# 0_105425 (22 files) 105426_210851 (6) 0_210851 (35) +# 210852_316277 (2) 210852_421703 (45) +# Slicing per distinct label would give overlapping slots with wildly uneven statistics -- the 2- and +# 6-file ones trip minTracksPerSlice and come back badCalib_ while the data that would have fixed them +# sits in a sibling list. Keeping one list per run makes granularity a deliberate choice instead of an +# artefact of the upstream catalog's granularity, and keeps it uniform across runs. +# The run's slot list IS the all-files list -- there is exactly one per run, so it is not copied to a +# second name. Both index files below therefore point at this one file. +info "Slot list: $FILE_ALL_RANGE ($(wc -l < "$FILE_ALL_RANGE" | tr -d '[:space:]') file(s))" +info "Windowing happens at submission time (SLOT_LENGTH_MIN), not here -- this is the whole run." + +# ── write index files ───────────────────────────────────────────────────────── +# One index per run, holding the single path to that run's slot list. +LISTS_INDEX="${run:-norunnumber}.residuals_lists.txt" +ls "$PWD/${FILE_ALL_RANGE}" > "$LISTS_INDEX" + +echo +ok "Done. 1 slot list written." +ok "Slot list index : $LISTS_INDEX" \ No newline at end of file diff --git a/DATA/production/configurations/mapCreationGrid/mapCreationJob.sh b/DATA/production/configurations/mapCreationGrid/mapCreationJob.sh new file mode 100755 index 000000000..0e0d2c65c --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/mapCreationJob.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +# mapCreationJob.sh — TPC space-charge map creation, one time slot per GRID job. +# +# This is STAGE 1 of a two-stage pipeline: runs staticMapCreatorCPM.C directly to produce the raw +# per-slot voxRes.*.it0.root -- the heavy step, hours per slot. Stages its output to this job's own +# JDL-assigned ${ALIEN_JDL_OUTPUTDIR} (see below) so STAGE 2 (postProcessJob.sh: +# SmoothingExtrapolate.C + TPCFastTransformInitCPM.C, minutes per slot) can find it independently and +# be resubmitted on its own if it fails, without re-running this stage. Everything further downstream +# (iteration chains beyond it0, finalize/QA) stays local for now -- deliberately out of scope here. +# +# Submitted via: +# ${O2DPG_ROOT}/GRID/utils/grid_submit.sh --script mapCreationJob.sh \ +# --prodsplit --packagespec O2PDPSuite:: ... +# +# --prodsplit gives every subjob nothing but a plain integer index +# (ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID, exported by grid_submit.sh -- real GRID only). This script +# downloads the mapSlots..tsv manifest that stageSlotsForGrid.py uploaded (one line +# per slot: "\t") -- tagged by JOBNAME_TAG +# so a different-named submission's manifest doesn't overwrite this one's (see JOBNAME_TAG below) -- +# picks its own line by index, and reconstructs a local slot-list file. --prodsplit is used instead of +# --split-on-collection: the latter needs one already-registered AliEn catalog file per split unit +# (N slow alien.py round trips, N tiny objects cluttering one AliEn directory), while a single manifest +# upload avoids both. +# +# Slot windowing: this script does NO time arithmetic. stageSlotsForGrid.py decides the windows at +# submission time (from SLOT_LENGTH_MIN) and writes one manifest line per window, carrying the window's +# bounds and exactly the files that can contribute to it. Stage 2 reads the same line, so the two +# stages cannot disagree about a slot's identity -- they used to each recompute it from +# TIME_DIVISION/TIME_DIVISION_INDEX, and any mismatch silently produced zero output. +# +# JOBNAME_TAG: tags the slot manifest name (mapSlots..tsv) so a different-named submission's +# manifest doesn't silently overwrite this one's. Substituted from the wrapper's own JOBNAME. +# +# MACRO_SOURCE: "official" (default) runs the macro straight from the O2 package already loaded by this +# job's --packagespec ($O2_ROOT/share/macro/staticMapCreatorCPM.C -- the same idiom +# DATA/production/configurations/asyncCalib/createCorrectionMap.sh already uses for staticMapCreator.C), +# no staging/download needed. Requires a packagespec built from an O2 tag that includes +# staticMapCreatorCPM.C (Detectors/TPC/calibration/SpacePoints/macro/) -- until such a tag is published, +# use MACRO_SOURCE=alien instead, which downloads a copy staged ahead of time under MACRO_ALIEN_DIR (see +# submitMapCreation.sh's MACRO_LOCAL_PATH). +# +# MACRO_ALIEN_DIR_BARE: same placeholder mechanism, substituted from the wrapper's own +# MACRO_ALIEN_DIR_BARE -- if you change that in submitMapCreation.sh (e.g. to point at a +# different AliEn user/directory), it now propagates here automatically instead of needing a second, +# easy-to-forget manual edit of this file. Used for the bad-time-ranges list and the slot manifest +# regardless of MACRO_SOURCE; only used for the macro itself when MACRO_SOURCE=alien. +JOBNAME_TAG=default +MACRO_SOURCE=official +MACRO_ALIEN_DIR_BARE=${MACRO_ALIEN_DIR_BARE:-} +# +# BAD_RANGES_NAME: filename of the bad-time-ranges list to pull from ${MACRO_ALIEN_DIR}/lists/. Same +# placeholder mechanism -- substituted from the wrapper's own BAD_RANGES_NAME, which is also what the +# wrapper uploads to when BAD_RANGES_LOCAL is set, so the uploaded name and the downloaded name are the +# same value by construction. The list is period/system-specific (the default below is PbPb26), so +# expect to change it in the wrapper when the data changes -- not here. Empty ("") is a supported value +# meaning "skip bad-ranges filtering entirely for this submission" -- see BAD_RANGES_ARG below. +BAD_RANGES_NAME=${BAD_RANGES_NAME:-Merge_BadIntervals_LHC26_PbPb_DCA-0P300.txt} +#JDL_OUTPUT=*.log@disk=1,*.txt@disk=1,*.info@disk=1 +#JDL_REQUIRE={member(other.GridPartitions,"multicore_8") && (other.CE != "ALICE::NIHAM::PBS64") && (other.CE != "ALICE::KISTI_GSDC::LCG") && (other.CE != "ALICE::SaoPaulo::LCG")}; +# Site blacklist: these three CEs excluded from this pipeline's jobs. Setting #JDL_REQUIRE overrides +# grid_submit.sh's own default Requirements entirely (it's read verbatim, not merged -- see +# grid_submit.sh's REQUIRESPEC handling), so the multicore_8 GridPartitions clause is repeated here +# rather than dropped. +# voxRes.*.root deliberately NOT in this pattern -- it's staged explicitly, once, via the synchronous +# alien.py cp below (to a fixed, predictable location stage 2 can find). Having it ALSO match here would +# make the automatic per-job upload duplicate the same (100+ MB) file a second time, to the ephemeral +# per-job dir, for no benefit now that the fixed-location copy is the reliable one. See "Two-stage +# pipeline" in README.md. + +set -e + +# xrootd client tuning -- retry/timeout behavior for alien://xrootd reads. Set before any AliEn +# transfer below (both our own alien.py cp calls and the macro's own residual-file reads go through +# this same client). Without it, a stalled read can hang until AliEn's own watchdog kills the payload +# for staying below its CPU-usage threshold too long (TJAlienFile "Operation expired"). +# +# CONNECTIONRETRY=2, CONNECTIONWINDOW=30: patiently retrying a sick storage server for many minutes is +# fine on a machine you own, but fatal on the GRID -- AliEn's job agent kills any payload whose CPU +# stays below threshold for 15 minutes, and a stalled bulk read burns that budget at ~0% CPU. A higher +# CONNECTIONRETRY would allow correspondingly longer silent reconnection per request before an error is +# ever raised; at 2 x 30s the worst case is 60 s, so the macro's own "could not load entry -> skip file" +# path (which handles a FAILED read perfectly well -- the problem is only ever that xrootd does not +# fail, it waits) gets a chance to run before the watchdog does. +# +# Note this is a mitigation, not a guarantee: none of these knobs bound a transfer that is trickling +# rather than dead, since each sub-request completes before its own timeout while the overall read +# crawls. That case is caught earlier instead, by the unhealthy-replica open-time gate in +# staticMapCreatorCPM.C (SCDCALIB_MAX_FILE_OPEN_SEC). +export XRD_TIMEOUTRESOLUTION=15 +export XRD_CONNECTIONWINDOW=30 +export XRD_CONNECTIONRETRY=2 +export XRD_REQUESTTIMEOUT=120 +export XRD_STREAMTIMEOUT=120 +export XRD_STREAMERRORWINDOW=0 + +# --- AliEn path holding the bad-time-ranges list / mapSlots.tsv (and staticMapCreatorCPM.C too, when +# MACRO_SOURCE=alien). grid_submit.sh only ships this one script; everything else is staged here once +# and pulled down per job. +# +# Normally substituted above by submitMapCreation.sh (which derives it from your AliEn +# account), so it is empty only when this script is run by hand -- in which case say so explicitly +# rather than building "alien:///mapSlots...tsv" and failing later with a confusing catalog error. +: "${MACRO_ALIEN_DIR_BARE:?MACRO_ALIEN_DIR_BARE is empty -- the submission wrapper normally substitutes it. For a standalone/local run, export it first, e.g. MACRO_ALIEN_DIR_BARE=/alice/cern.ch/user///MapCreation}" +MACRO_ALIEN_DIR="alien://${MACRO_ALIEN_DIR_BARE}" + +if [[ "${MACRO_SOURCE}" == "official" ]]; then + : "${O2_ROOT:?O2_ROOT is not set -- MACRO_SOURCE=official needs the O2 package loaded via --packagespec}" + MACRO_PATH="${O2_ROOT}/share/macro/staticMapCreatorCPM.C" + if [[ ! -f "${MACRO_PATH}" ]]; then + echo "ERROR: MACRO_SOURCE=official but ${MACRO_PATH} does not exist -- this job's --packagespec does not include staticMapCreatorCPM.C yet. Use MACRO_SOURCE=alien until a packagespec with it is published." >&2 + exit 1 + fi +else + alien.py cp -f "${MACRO_ALIEN_DIR}/staticMapCreatorCPM.C" file:./staticMapCreatorCPM.C + MACRO_PATH="./staticMapCreatorCPM.C" +fi +# BAD_RANGES_NAME="" is a deliberate, supported value -- see the wrapper's own comment on it -- meaning +# "skip bad-ranges filtering entirely" (staticMapCreatorCPM.C treats an empty badRangeList argument that +# way, see its `if (badRangeList.length() > 0)` guard). BAD_RANGES_ARG is what actually gets passed to +# the macro below; empty there, not "badRanges.dat", is what selects that mode. +BAD_RANGES_ARG="" +if [[ -n "${BAD_RANGES_NAME}" ]]; then + alien.py cp -f "${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME}" file:./badRanges.dat + BAD_RANGES_ARG="badRanges.dat" +fi +alien.py cp -f "${MACRO_ALIEN_DIR}/mapSlots.${JOBNAME_TAG}.tsv" file:./mapSlots.tsv + +# --- Determine this subjob's index into mapSlots.tsv --- +# ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID is set by grid_submit.sh on the real GRID (--prodsplit mode) before +# it runs this script. For --local testing without it, pass SUBJOBID explicitly, e.g.: +# SUBJOBID=3 grid_submit.sh --local ... +SUBJOBID="${ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID:-${SUBJOBID:-}}" +: "${SUBJOBID:?SUBJOBID must be set (via ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID on GRID, or the SUBJOBID env var for --local testing)}" + +LINE=$(sed -n "${SUBJOBID}p" mapSlots.tsv) +if [[ -z "${LINE}" ]]; then + echo "ERROR: mapSlots.tsv has no line ${SUBJOBID}" >&2 + exit 1 +fi + +# Manifest line: \t \t \t \t \t +# The window bounds and the file selection are decided once, at submission time, by +# stageSlotsForGrid.py -- this script does no time arithmetic of its own. That is deliberate: stage 2 +# reads the SAME manifest line, so the two stages cannot disagree about a slot's identity (they +# previously each recomputed it and a mismatch silently produced zero output). +IFS=$'\t' read -r RUN WIN_IDX WIN_COUNT firstTSIter lastTSIter PATHS <<< "${LINE}" +for v in RUN WIN_IDX WIN_COUNT firstTSIter lastTSIter; do + if [[ ! "${!v}" =~ ^[0-9]+$ ]]; then + echo "ERROR: manifest line ${SUBJOBID} field ${v}='${!v}' is not numeric -- expected 6 tab-separated fields " >&2 + exit 1 + fi +done +[[ -n "${PATHS}" ]] || { echo "ERROR: manifest line ${SUBJOBID} carries no file paths" >&2; exit 1; } + +SLOT_LIST="slotlist_${SUBJOBID}.txt" +tr ',' '\n' <<< "${PATHS}" > "${SLOT_LIST}" +nSlotFiles=$(wc -l < "${SLOT_LIST}" | tr -d '[:space:]') # tr: wc right-justifies its count +echo "Processing slot ${SUBJOBID}: run ${RUN}, window ${WIN_IDX}/${WIN_COUNT} [${firstTSIter},${lastTSIter}] ($(( (lastTSIter - firstTSIter) / 1000 ))s), ${nSlotFiles} file(s)" + +# --- Process the files in the order that reaches this window's data soonest --- +# WHY: the macro's per-file skip still has to OPEN every out-of-window file to read its records tree -- +# ~7 s each over alien://, at ~zero CPU -- and AliEn's job agent kills any payload whose CPU stays +# below threshold for 15 minutes (observed for real, run 572246: 47 files skipped in 6 min, killed +# before reaching in-window data). +# +# stageSlotsForGrid.py already dropped every file whose aggregation label cannot overlap this window, +# but that filter can only be as sharp as the labels are. A coarse label produced by the aggregator +# merging an underpopulated slot forward can span several windows, so overlapping this window by as +# little as 1 ms pulls in all of its files. Ordering fixes what filtering cannot: each file's CTF +# directory carries an _orbit field, which IS that file's true position in time, so we sort by +# distance from this window's centre. The files that can actually contribute are opened first and +# maxTracksPerSlice usually stops the job before it ever reaches the far ones. +# +# Only the ORDER depends on this, never which files are considered, so the orbit->time estimate below +# is allowed to be approximate: it is interpolated from the list's own extremes. +if awk -F'_orbit' 'NF < 2 {exit 1}' "${SLOT_LIST}"; then + awk -F'_orbit' -v lo="${firstTSIter}" -v hi="${lastTSIter}" ' + { + line[NR] = $0 + # orbit from the CTF directory name + split($2, o, "_"); orb[NR] = o[1] + 0 + # Time anchor: the EXTREMES of the aggregation labels present, not their midpoints. In nearly + # every window all files carry a single label (one aggregator slot spans several windows), and + # identical midpoints collapse the interpolation to frac=0 -- which sorts ascending no matter + # where the window actually sits, the exact order that gets a late window killed by the CPU + # watchdog. Extremes stay meaningful with one label: [labelFirst,labelLast] maps onto + # [oMin,oMax]. + if (match($0, /o2tpc_residuals_[0-9]{13}_[0-9]{13}_/)) { + split(substr($0, RSTART + 16, 27), t, "_") + if (!seen || (t[1] + 0) < mMin) mMin = t[1] + 0 + if (!seen || (t[2] + 0) > mMax) mMax = t[2] + 0 + seen = 1 + } + if (n == 0 || orb[NR] < oMin) oMin = orb[NR] + if (n == 0 || orb[NR] > oMax) oMax = orb[NR] + n = NR + } + END { + centre = (lo + hi) / 2 + # map the window centre onto the orbit axis using this list own extremes + frac = (seen && mMax > mMin) ? (centre - mMin) / (mMax - mMin) : 0 + if (frac < 0) frac = 0 + if (frac > 1) frac = 1 + target = oMin + frac * (oMax - oMin) + for (i = 1; i <= n; i++) { + d = orb[i] - target; if (d < 0) d = -d + printf "%d\t%s\n", d, line[i] + } + }' "${SLOT_LIST}" \ + | sort -n -k1,1 | cut -f2- > "${SLOT_LIST}.ord" + if [[ -s "${SLOT_LIST}.ord" ]]; then + mv "${SLOT_LIST}.ord" "${SLOT_LIST}" + echo "File order: sorted by orbit distance from the window centre (closest first)" + else + rm -f "${SLOT_LIST}.ord" + echo "WARNING: could not order ${SLOT_LIST} by orbit -- leaving the file order untouched" >&2 + fi +else + # No _orbit field in at least one path -- cannot establish a time order, so leave the list alone + # rather than reorder it on a key that is not there. Costs the optimization, never correctness. + echo "WARNING: not every path in ${SLOT_LIST} carries an _orbit field -- leaving the file order untouched (no time-ordering possible)" >&2 +fi + +# --- Config (production values; createSpline is the one deliberate exception, see CREATE_SPLINE +# below: TPCFastTransformInitCPM.C runs separately in stage 2 here instead) --- +# NOTE: nScalerRanges/useScalerRange/useCTPLumi are gone from the macro's signature. It no longer reads +# IDC scalers from CCDB at all; the values are joined in offline by SmoothingExtrapolate.C in stage 2. +Z2XBINNING="28" +Y2XBINNING="20" +USE_SMOOTHED=2 +CREATE_SPLINE=false # TPCFastTransformInitCPM.C runs in stage 2 instead; leave false here +MAX_TRACKS_PER_SLICE=10000 +MIN_TRACKS_PER_SLICE=3000 +MAX_DEDX=-1 +MAX_DEDX_EXP=-1 +MAX_DEV_DEDX_OVER_EXP=-1 +SKIP_EDGE_PADS=1 +BAD_RANGE_SELECTION="ALL" +MAX_Z2X_CUT=1.4 # SpacePointsCalibConfParam scdcalib.maxZ2X override -- real + # production value, vs. the struct's own compiled-in default of 1.0 +MAX_TRACK_WORKERS=8 # matches the JDL's CPUCores/multicore_8 allocation (grid_submit.sh's + # default) -- hardware_concurrency() does NOT reliably reflect the + # real GRID allocation (confirmed 2026-07-25: auto-detected 32 on an + # 8-core job, 4x oversubscription), so pass the known real value. + +OUTPUT_STEM="voxRes.${RUN}_${firstTSIter}_${lastTSIter}.TD${WIN_IDX}of${WIN_COUNT}.it0" +OUTPUT_FILE="${OUTPUT_STEM}.root" + +export alien_CLOSE_SE="ALICE::CERN::EOS" +cmd="(time root.exe -b -q -l -x '${MACRO_PATH}+O(\"${SLOT_LIST}\", ${RUN}, \"${OUTPUT_FILE}\", \"\", \"ITS-TPC\", \"${Z2XBINNING}\", \"${Y2XBINNING}\", ${USE_SMOOTHED}, ${CREATE_SPLINE}, ${MAX_TRACKS_PER_SLICE}, ${MIN_TRACKS_PER_SLICE}, \"${BAD_RANGES_ARG}\", ${firstTSIter}, ${lastTSIter}, ${MAX_DEDX}, ${MAX_DEDX_EXP}, ${MAX_DEV_DEDX_OVER_EXP}, ${SKIP_EDGE_PADS}, \"${BAD_RANGE_SELECTION}\", ${MAX_Z2X_CUT}, ${MAX_TRACK_WORKERS})')" +echo "$(date) ${cmd}" +eval "${cmd}" + +if [[ ! -s "${OUTPUT_FILE}" ]]; then + echo "ERROR: ${OUTPUT_FILE} was not produced -- this slot's map creation failed" >&2 + exit 1 +fi + +# Bad-calib handling: the macro creates an extensionless badCalib. marker file when +# minTracksPerSlice wasn't reached. Stage under a badCalib_-prefixed +# name instead of the normal one -- stage 2, looking for the normal (unprefixed) name, simply won't +# find it and will hit the same graceful-skip path already used for "stage 1 hasn't produced this yet", +# with no extra logic needed there. The marker file itself has no extension and matches none of our +# JDL output patterns, so it doesn't survive on its own -- this rename is what actually carries the +# bad-calib status forward. +STAGE_NAME="${OUTPUT_FILE}" +if [[ -f "badCalib.${OUTPUT_STEM}" ]]; then + STAGE_NAME="badCalib_${OUTPUT_FILE}" + echo "Calibration marked bad for this slot (found badCalib.${OUTPUT_STEM}) -- staging as ${STAGE_NAME} instead of ${OUTPUT_FILE}" +fi + +# --- Two-stage pipeline: stage the produced map to THIS job's own JDL-assigned output directory +# (per ALICE Grid support -- do not write production data to a personal-space path; that needs its own +# disk-quota request on top of the CPU one, unlike ${ALIEN_JDL_OUTPUTDIR}, which is already covered by +# however this job's quota/ownership is set up). ALIEN_JDL_OUTPUTDIR is JAliEn's own per-JDL-field env +# var (case-sensitive, same convention as ALIEN_JDL_CPUCORES etc.); fall back to ALIEN_JOB_OUTPUTDIR +# (grid_submit.sh's own equivalent, computed the same way from the same JDL field -- see that script) in +# case the former isn't exported in some execution context. Explicit, synchronous cp rather than relying +# on the passive #JDL_OUTPUT@disk=N auto-upload -- that only happens during the job's asynchronous +# SAVING phase after this script exits, which can take minutes and would race against a later reader +# (stage 2 doesn't try to read this immediately, but no reason to introduce that fragility when an +# explicit cp is just as easy and finishes before this script does). +# +# NOTE: this directory is only known once the job actually runs -- see +# submitMapCreation.sh for how the post-processing stage (postProcessJob.sh, +# SmoothingExtrapolate.C + TPCFastTransformInitCPM.C) finds it later without needing an in-job breadcrumb. +ALIEN_OUTPUT_DIR="alien://${ALIEN_JDL_OUTPUTDIR:-${ALIEN_JOB_OUTPUTDIR:-}}" +if [[ "${ALIEN_OUTPUT_DIR}" == "alien://" ]]; then + echo "ERROR: neither ALIEN_JDL_OUTPUTDIR nor ALIEN_JOB_OUTPUTDIR is set -- don't know where to stage ${OUTPUT_FILE}" >&2 + exit 1 +fi +if ! alien.py cp -f "file:${OUTPUT_FILE}" "${ALIEN_OUTPUT_DIR}/${STAGE_NAME}"; then + echo "ERROR: failed to stage ${OUTPUT_FILE} to ${ALIEN_OUTPUT_DIR} -- the post-processing stage won't be able to find this slot's map" >&2 + exit 1 +fi +echo "Staged ${OUTPUT_FILE} to ${ALIEN_OUTPUT_DIR}/${STAGE_NAME}" diff --git a/DATA/production/configurations/mapCreationGrid/postProcessJob.sh b/DATA/production/configurations/mapCreationGrid/postProcessJob.sh new file mode 100755 index 000000000..676bcf459 --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/postProcessJob.sh @@ -0,0 +1,281 @@ +#!/usr/bin/env bash +# postProcessJob.sh — STAGE 2 of the two-stage pipeline. +# +# Runs the fast (minutes, not hours) part of the chain on an already-produced map: +# SmoothingExtrapolate.C -> TPCFastTransformInitCPM.C +# +# Independent of stage 1 (mapCreationJob.sh): does NOT re-run map creation, does NOT even download the +# (multi-GB) residual files. It only needs the small manifest (mapSlots.tsv, reused as-is from stage 1, +# NOT rebuilt) to work out which map stage 1 should have produced for this same subjob index. Both +# stages read the run/window straight off the SAME manifest line, so they cannot disagree about a +# slot's identity. +# +# If stage 1 hasn't produced (or failed to produce) this slot's map yet, this script logs a warning +# and exits 0 (success/no-op) rather than failing -- so a partial stage-1 completion (some slots ok, +# some failed) doesn't turn every stage-2 subjob into a failure. Once stage 1 is resubmitted for the +# missing slots, just resubmit the corresponding stage-2 subjob(s). +# +# Slot windowing: none happens here. stageSlotsForGrid.py fixed the windows at submission time and +# wrote them into the manifest; this script just reads its line. +# +# STAGE1_JOBNAME_TAG: which stage-1 production's manifest/output to look for -- NOT this stage's own job +# name, but stage 1's JOBNAME_TAG (mapCreationJob.sh), since that's what determined the manifest +# name AND (via the STAGE1_OUTPUTDIR_BASE pointer file it uploaded) where stage 1's own jobs staged their +# maps. Must match exactly, or this will always hit the graceful-skip path even though stage 1 succeeded. +# +# STAGE1_OUTPUTDIR_BASE / COUNTERWIDTH (per ALICE Grid support): stage 1 stages its map to its own +# ${ALIEN_JDL_OUTPUTDIR} rather than a fixed personal-space path (avoids a separate disk-quota request), +# so there's no static location to just look up. STAGE1_OUTPUTDIR_BASE is stage 1's MY_JOBWORKDIR (that +# submission's timestamped parent directory, captured by submitMapCreation.sh right when it +# ran grid_submit.sh -- otherwise unrecoverable later) and COUNTERWIDTH is grid_submit.sh's own +# alien_counter zero-padding width -- together they let this script reconstruct stage 1's exact per-slot +# ${ALIEN_JDL_OUTPUTDIR} the same way grid_submit.sh itself derives OutputDir (see below), without any +# in-job breadcrumb from stage 1 or an extra AliEn round trip. +# +# MACRO_SOURCE: same meaning and default ("official") as stage 1 -- see mapCreationJob.sh. Applies +# to all three macros used here (SmoothingExtrapolate.C, TPCFastTransformInitCPM.C, voxResQA.C), all +# installed to the same $O2_ROOT/share/macro/ once the O2 PR is merged into this job's packagespec. +# +# MACRO_ALIEN_DIR_BARE: same placeholder mechanism, substituted from the wrapper's own +# MACRO_ALIEN_DIR_BARE -- must match stage 1's. Used for the manifest/stage1Workdir pointer regardless of +# MACRO_SOURCE; only used for the macros themselves when MACRO_SOURCE=alien. +STAGE1_JOBNAME_TAG=default +STAGE1_OUTPUTDIR_BASE= +COUNTERWIDTH=3 +MACRO_SOURCE=official +MACRO_ALIEN_DIR_BARE=${MACRO_ALIEN_DIR_BARE:-} +#JDL_OUTPUT=*.log@disk=1,*.txt@disk=1,*.png@disk=1 +#JDL_REQUIRE={member(other.GridPartitions,"multicore_8") && (other.CE != "ALICE::NIHAM::PBS64") && (other.CE != "ALICE::KISTI_GSDC::LCG") && (other.CE != "ALICE::SaoPaulo::LCG")}; +# Site blacklist -- same three CEs excluded as stage 1, see that script's comment for why the +# GridPartitions clause has to be repeated here (grid_submit.sh replaces its whole default Requirements, +# doesn't merge with it). +# voxRes.*.root/FT_*.root/voxResQA*.root deliberately NOT in this pattern -- same reasoning as stage 1 +# (mapCreationJob.sh): they're staged explicitly, once, to this job's own ${ALIEN_JDL_OUTPUTDIR} +# below, so having them ALSO match here would duplicate the upload to the same place for no benefit. +# The QA step's individual *.png canvases ARE left in this pattern -- they're small, convenience-only +# quick-look images, not worth a second explicit-staging code path for. + +set -e + +# xrootd client tuning -- see mapCreationJob.sh for the reasoning; same values, same rationale, +# applies equally here since this script does just as much alien://xrootd transfer (macros, manifest, +# the fixed-location map download) even though it doesn't touch the multi-GB residual files. +export XRD_TIMEOUTRESOLUTION=15 +export XRD_CONNECTIONWINDOW=30 +export XRD_CONNECTIONRETRY=4 +export XRD_REQUESTTIMEOUT=120 +export XRD_STREAMTIMEOUT=120 +export XRD_STREAMERRORWINDOW=0 + +# --- AliEn path holding mapSlots.tsv / the stage1Workdir pointer file (and the three macros too, when +# MACRO_SOURCE=alien). Same root dir as stage 1's MACRO_ALIEN_DIR. +# +# Normally substituted above by submitPostProcess.sh; empty only on a standalone/local run -- see the +# same guard in mapCreationJob.sh. +: "${MACRO_ALIEN_DIR_BARE:?MACRO_ALIEN_DIR_BARE is empty -- the submission wrapper normally substitutes it. For a standalone/local run, export it first, e.g. MACRO_ALIEN_DIR_BARE=/alice/cern.ch/user///MapCreation}" +MACRO_ALIEN_DIR="alien://${MACRO_ALIEN_DIR_BARE}" + +if [[ "${MACRO_SOURCE}" == "official" ]]; then + : "${O2_ROOT:?O2_ROOT is not set -- MACRO_SOURCE=official needs the O2 package loaded via --packagespec}" + SMOOTHING_MACRO_PATH="${O2_ROOT}/share/macro/SmoothingExtrapolate.C" + FASTTRANSFORM_MACRO_PATH="${O2_ROOT}/share/macro/TPCFastTransformInitCPM.C" + QA_MACRO_PATH="${O2_ROOT}/share/macro/voxResQA.C" + for f in "${SMOOTHING_MACRO_PATH}" "${FASTTRANSFORM_MACRO_PATH}" "${QA_MACRO_PATH}"; do + if [[ ! -f "${f}" ]]; then + echo "ERROR: MACRO_SOURCE=official but ${f} does not exist -- this job's --packagespec does not include it yet. Use MACRO_SOURCE=alien until a packagespec with it is published." >&2 + exit 1 + fi + done +else + alien.py cp -f "${MACRO_ALIEN_DIR}/SmoothingExtrapolate.C" file:./SmoothingExtrapolate.C + alien.py cp -f "${MACRO_ALIEN_DIR}/TPCFastTransformInitCPM.C" file:./TPCFastTransformInitCPM.C + alien.py cp -f "${MACRO_ALIEN_DIR}/voxResQA.C" file:./voxResQA.C + SMOOTHING_MACRO_PATH="./SmoothingExtrapolate.C" + FASTTRANSFORM_MACRO_PATH="./TPCFastTransformInitCPM.C" + QA_MACRO_PATH="./voxResQA.C" +fi +# Tagged manifest name -- must match whatever stage 1 actually uploaded (mapSlots..tsv). +alien.py cp -f "${MACRO_ALIEN_DIR}/mapSlots.${STAGE1_JOBNAME_TAG}.tsv" file:./mapSlots.tsv + +# --- Determine this subjob's index into mapSlots.tsv (same convention as stage 1) --- +SUBJOBID="${ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID:-${SUBJOBID:-}}" +: "${SUBJOBID:?SUBJOBID must be set (via ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID on GRID, or the SUBJOBID env var for --local testing)}" + +LINE=$(sed -n "${SUBJOBID}p" mapSlots.tsv) +if [[ -z "${LINE}" ]]; then + echo "ERROR: mapSlots.tsv has no line ${SUBJOBID}" >&2 + exit 1 +fi + +# Manifest line: \t \t \t \t \t +# Stage 2 does NO time arithmetic: it reads the window straight off the same manifest line stage 1 +# used, so OUTPUT_STEM below is identical to stage 1's by construction. Previously both stages +# recomputed the window from TIME_DIVISION/TIME_DIVISION_INDEX and any disagreement meant stage 2 +# silently found nothing and no-opped for every slot. +# The 6th field (the file list) is deliberately discarded: stage 2 never touches the residual +# files, it only needs the run and the window. It still has to be READ, or lastTSIter would +# swallow the rest of the line. +IFS=$'\t' read -r RUN WIN_IDX WIN_COUNT firstTSIter lastTSIter _ <<< "${LINE}" +for v in RUN WIN_IDX WIN_COUNT firstTSIter lastTSIter; do + if [[ ! "${!v}" =~ ^[0-9]+$ ]]; then + echo "ERROR: manifest line ${SUBJOBID} field ${v}='${!v}' is not numeric -- expected 6 tab-separated fields " >&2 + exit 1 + fi +done +echo "Slot ${SUBJOBID}: run ${RUN}, window ${WIN_IDX}/${WIN_COUNT} [${firstTSIter},${lastTSIter}]" + +# --- Config (production defaults) --- +USE_SMOOTHED=2 # matches stage 1's USE_SMOOTHED -- selects doGausSmooth=true below +DO_GAUS_SMOOTH=true +DO_EXTRAPOLATION=2 # smoothed AND raw values extrapolated +A11_MAX_Z2X=-1 # disabled +MASK_IA11=0 # disabled +USE_CTP_LUMI=2 # matches stage 1's USE_CTP_LUMI + +# voxResQA.C config -- needs SmoothingExtrapolate.C's DS[] (smoothed dX/dY/dZ) already filled in, +# hence run from here, on OUTPUT_MAP_SMOOTH, not the raw stage-1 map. +QA_DRAW_ERRORS=false +QA_USE_SMOOTHED=true # true: draw dXS/dYS/dZS (needs SmoothingExtrapolate.C's output); false: raw dX/dY/dZ +QA_Z2X_BIN_SEL=-1 # -1: all z2x bins + +# --- Expected stage-1 output filename (must match mapCreationJob.sh's OUTPUT_STEM exactly) --- +OUTPUT_STEM="voxRes.${RUN}_${firstTSIter}_${lastTSIter}.TD${WIN_IDX}of${WIN_COUNT}.it0" +STAGE1_OUTPUT_FILE="${OUTPUT_STEM}.root" + +# Reconstruct stage 1's exact per-slot ${ALIEN_JDL_OUTPUTDIR} -- same formula grid_submit.sh itself uses +# to derive OutputDir (MY_JOBWORKDIR/#alien_counter_0Wi#): STAGE1_OUTPUTDIR_BASE is stage 1's +# MY_JOBWORKDIR, and this slot's own subjob index (SUBJOBID, same manifest line/counter convention as +# stage 1) zero-padded to COUNTERWIDTH gives the exact subdirectory stage 1's own job for this slot wrote +# its output to. +SUBJOB_COUNTER_DIR=$(printf "%0${COUNTERWIDTH}d" "${SUBJOBID}") +STAGE1_OUTPUT_LOCATION="alien://${STAGE1_OUTPUTDIR_BASE}/${SUBJOB_COUNTER_DIR}" + +# Local name deliberately does NOT look like our own output (voxRes.*.root/FT_*.root) -- both are now +# staged explicitly below rather than picked up via a JDL_OUTPUT glob, but naming this distinctly still +# avoids any confusion between "the map we downloaded as input" and "the map we produced" (the exact bug +# found+fixed earlier for badRanges.txt/badRanges.dat). +INPUT_MAP="input_map.root" + +echo "Looking for stage-1 output at ${STAGE1_OUTPUT_LOCATION}/${STAGE1_OUTPUT_FILE}" +if ! alien.py cp -f "${STAGE1_OUTPUT_LOCATION}/${STAGE1_OUTPUT_FILE}" "file:${INPUT_MAP}" || [[ ! -s "${INPUT_MAP}" ]]; then + rm -f "${INPUT_MAP}" + # Not there under the normal name -- check whether stage 1 actually ran but renamed it away because + # the calibration was marked bad (same convention as the real production, see mapCreationJob.sh), + # rather than assuming it just hasn't run yet. + if alien.py cp -f "${STAGE1_OUTPUT_LOCATION}/badCalib_${STAGE1_OUTPUT_FILE}" "file:${INPUT_MAP}" 2>/dev/null && [[ -s "${INPUT_MAP}" ]]; then + echo "Stage 1 marked this slot's calibration as bad (found badCalib_${STAGE1_OUTPUT_FILE} instead of ${STAGE1_OUTPUT_FILE}) -- skipping post-processing on purpose, not an error." >&2 + else + echo "WARNING: stage-1 output '${STAGE1_OUTPUT_FILE}' not found at ${STAGE1_OUTPUT_LOCATION} -- this slot's map creation likely hasn't run yet or failed. Skipping (not an error) -- resubmit this subjob once stage 1 has produced it." >&2 + fi + rm -f "${INPUT_MAP}" + exit 0 +fi + +# --- [1/2] Gaussian smoothing + extrapolation --- +GAUSSMOOTH_ADD="" +[[ "${DO_GAUS_SMOOTH}" == "true" ]] && GAUSSMOOTH_ADD+=".GausSmooth" +[[ "${DO_EXTRAPOLATION}" -gt 0 ]] && GAUSSMOOTH_ADD+=".Extrapolation" +OUTPUT_MAP_SMOOTH="${OUTPUT_STEM}${GAUSSMOOTH_ADD}.root" + +cmd="(time root.exe -b -q -l -x '${SMOOTHING_MACRO_PATH}+Og(\"${INPUT_MAP}\", \"${OUTPUT_MAP_SMOOTH}\", ${DO_GAUS_SMOOTH}, ${DO_EXTRAPOLATION}, ${A11_MAX_Z2X}, ${MASK_IA11})')" +echo "$(date) ${cmd}" +eval "${cmd}" + +if [[ ! -s "${OUTPUT_MAP_SMOOTH}" ]]; then + echo "ERROR: ${OUTPUT_MAP_SMOOTH} was not produced -- SmoothingExtrapolate.C failed" >&2 + exit 1 +fi + +# --- Resolve this job's own JDL-assigned output directory (per ALICE Grid support -- see +# mapCreationJob.sh for the full reasoning: production data belongs in ${ALIEN_JDL_OUTPUTDIR}, +# not a personal-space path). ALIEN_JDL_OUTPUTDIR is JAliEn's own per-JDL-field env var; fall back to +# ALIEN_JOB_OUTPUTDIR (grid_submit.sh's equivalent, computed from the same JDL field). +ALIEN_OUTPUT_DIR="alien://${ALIEN_JDL_OUTPUTDIR:-${ALIEN_JOB_OUTPUTDIR:-}}" +if [[ "${ALIEN_OUTPUT_DIR}" == "alien://" ]]; then + echo "ERROR: neither ALIEN_JDL_OUTPUTDIR nor ALIEN_JOB_OUTPUTDIR is set -- don't know where to stage this slot's output" >&2 + exit 1 +fi + +# Stage the smoothed map NOW, before the QA and FastTransform steps rather than after them. Those steps +# can fail hard on input this script cannot control -- notably TPCFastTransformInitCPM.C's +# LOGP(fatal, "meanCTP ... not set!"), which fires for any slot whose stage-1 map carries meanCTP=0 in +# its UserInfo (useCTPLumi=2 exempts meanIDC from that check, but NOT meanCTP). Staging last meant such +# a slot lost its perfectly good smoothed map as well, forcing a resubmit to redo the smoothing. +if ! alien.py cp -f "file:${OUTPUT_MAP_SMOOTH}" "${ALIEN_OUTPUT_DIR}/${OUTPUT_MAP_SMOOTH}"; then + echo "ERROR: failed to stage ${OUTPUT_MAP_SMOOTH} to ${ALIEN_OUTPUT_DIR}" >&2 + exit 1 +fi +echo "Staged ${OUTPUT_MAP_SMOOTH} to ${ALIEN_OUTPUT_DIR}" + +# --- Provenance pointer: unlike stage 1 -> stage 2 (which has the stage1Workdir..txt pointer), +# stage 2's own output directory otherwise carries no trace of which exact stage-1 output it was built +# from. Record it as a one-line pointer, same idea, staged right alongside the smoothed map. +STAGE1_SOURCE_FILE="stage1Source.txt" +echo "${STAGE1_OUTPUT_LOCATION}/${STAGE1_OUTPUT_FILE}" > "${STAGE1_SOURCE_FILE}" +if ! alien.py cp -f "file:${STAGE1_SOURCE_FILE}" "${ALIEN_OUTPUT_DIR}/${STAGE1_SOURCE_FILE}"; then + echo "WARNING: failed to stage ${STAGE1_SOURCE_FILE} -- provenance pointer only, not fatal" >&2 +fi + +# --- QA plots (voxResQA.C) -- runs on just this one slot's smoothed map, same per-slot granularity as +# everything else in this pipeline. Non-fatal on failure: QA is a diagnostic nice-to-have, not a blocking +# deliverable like the smoothed map/FastTransform output -- a QA hiccup shouldn't fail an otherwise-good +# slot. +QA_SUFFIX=".${OUTPUT_STEM}${GAUSSMOOTH_ADD}" +cmd="(time root.exe -b -q -l -x '${QA_MACRO_PATH}+Og(\"ls ${OUTPUT_MAP_SMOOTH}\", \"${QA_SUFFIX}\", ${QA_DRAW_ERRORS}, ${QA_USE_SMOOTHED}, ${QA_Z2X_BIN_SEL})')" +echo "$(date) ${cmd}" +eval "${cmd}" || echo "WARNING: voxResQA.C failed for this slot -- QA plots will be missing, but this doesn't affect ${OUTPUT_MAP_SMOOTH} itself" >&2 + +QA_FILE_1D="voxResQA${QA_SUFFIX}_1D.root" +QA_FILE_2D="voxResQA${QA_SUFFIX}_2D.root" + +# --- [2/2] Final O2 FastTransform --- +OUTPUT_FT="FT_${OUTPUT_MAP_SMOOTH}" + +cmd="(time root.exe -b -q -l -x '${FASTTRANSFORM_MACRO_PATH}+Og(\"${OUTPUT_MAP_SMOOTH}\", \"${OUTPUT_FT}\", ${USE_SMOOTHED}, ${USE_CTP_LUMI})')" +echo "$(date) ${cmd}" +eval "${cmd}" + +if [[ ! -s "${OUTPUT_FT}" ]]; then + echo "ERROR: ${OUTPUT_FT} was not produced -- TPCFastTransformInitCPM.C failed" >&2 + exit 1 +fi + +# --- Stage the FastTransform output to this job's own JDL-assigned output directory (the smoothed map +# was already staged above, right after it was produced). Explicit, synchronous cp rather than relying on +# the passive #JDL_OUTPUT@disk=N auto-upload, which only happens during the job's asynchronous SAVING +# phase after this script exits. +if ! alien.py cp -f "file:${OUTPUT_FT}" "${ALIEN_OUTPUT_DIR}/${OUTPUT_FT}"; then + echo "ERROR: failed to stage ${OUTPUT_FT} to ${ALIEN_OUTPUT_DIR}" >&2 + exit 1 +fi +echo "Staged ${OUTPUT_FT} to ${ALIEN_OUTPUT_DIR}" + +# QA plots -- non-fatal staging, same reasoning as the non-fatal QA run itself above: if voxResQA.C +# failed or produced nothing, there's nothing to stage, and that shouldn't fail this otherwise-good slot. +for qaFile in "${QA_FILE_1D}" "${QA_FILE_2D}"; do + if [[ -s "${qaFile}" ]]; then + alien.py cp -f "file:${qaFile}" "${ALIEN_OUTPUT_DIR}/${qaFile}" || \ + echo "WARNING: failed to stage ${qaFile} -- non-fatal, just loses this slot's QA plots" >&2 + else + echo "WARNING: ${qaFile} was not produced -- voxResQA.C likely failed above, skipping its staging" >&2 + fi +done + +# TPCFastTransformInitCPM.C's own debug/summary trees (produced unconditionally alongside OUTPUT_FT, +# named after its stem -- see createFastTransform() in that macro) were never staged anywhere: the +# #JDL_OUTPUT pattern above only matches *.log/*.txt/*.png (no *.root), and the explicit staging block +# above only names OUTPUT_MAP_SMOOTH/OUTPUT_FT -- so these were produced on the worker and then silently +# discarded when the job finished. Non-fatal staging, same reasoning as the QA plots: diagnostic, not a +# blocking deliverable. +OUTPUT_FT_DEBUG="${OUTPUT_FT%.root}.debug.root" +OUTPUT_FT_SUMMARY="${OUTPUT_FT%.root}.summary.root" +for ftDebugFile in "${OUTPUT_FT_DEBUG}" "${OUTPUT_FT_SUMMARY}"; do + if [[ -s "${ftDebugFile}" ]]; then + alien.py cp -f "file:${ftDebugFile}" "${ALIEN_OUTPUT_DIR}/${ftDebugFile}" || \ + echo "WARNING: failed to stage ${ftDebugFile} -- non-fatal, just loses this slot's FT debug/summary tree" >&2 + else + echo "WARNING: ${ftDebugFile} was not produced -- skipping its staging" >&2 + fi +done + +echo "Stage 2 complete: ${OUTPUT_MAP_SMOOTH}, ${OUTPUT_FT}" diff --git a/DATA/production/configurations/mapCreationGrid/stageSlotsForGrid.py b/DATA/production/configurations/mapCreationGrid/stageSlotsForGrid.py new file mode 100755 index 000000000..eae34838d --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/stageSlotsForGrid.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +"""Build and upload the slot manifest for the TPC map-creation GRID pipeline. + +Bridge between the local Step-1 list discovery (discoverResiduals.sh) and GRID submission. + +Cuts each run into time windows of ~--slot-length-min and writes ONE manifest line per window: + + \t \t \t \t \t + +paired with `grid_submit.sh --prodsplit `. Every subjob gets nothing but a plain integer +index (ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID), downloads this one manifest and picks its own line, so a +subjob index identifies a (run, window) pair on its own. Both job scripts read the window straight off +that line, which is why they cannot disagree about a slot's identity. + +Why the windows are not simply the aggregation labels +----------------------------------------------------- +Each residual filename carries the slot label ___ that its reco +job assigned, and those labels are NOT a uniform grid -- same-length slots are not guaranteed, and a +run's catalog can hold overlapping/nested labels at different granularities (why doesn't matter here, +just that it happens). Slicing per label gives overlapping slots with wildly uneven statistics. +Choosing the window length here instead makes granularity a deliberate decision, uniform across runs. + +A file is assigned to a window when its label overlaps it by MORE than --overlap-margin-ms. A bare +overlap test is too generous at the edges: coarse labels routinely begin 1-2 ms before a window ends, +which would drag all of their files into a window whose map they contribute nothing to. The bound is +exact -- if a label overlaps by X ms then at most X ms of that file's data can belong to the window, +and the macro's own per-TF cut discards the rest regardless -- so skipping at X <= margin costs at +most `margin` ms of coverage per window edge. + +This was a bash+awk script. It moved to Python because the work is arithmetic over 13-digit +millisecond timestamps and every bug it produced was a property of the shell rather than of the +problem: `(( x += 0 ))` returning 1 under `set -e`, `sed | sort | head` aborting the script by SIGPIPE +under `pipefail`, `<<<` herestrings writing temp files onto Lustre, awk's %d truncating a millisecond +timestamp through a 32-bit int on mawk, and awk holding one output file open per window. Python has +arbitrary-precision ints and none of those failure modes. + +Prints the number of manifest lines (windows) to stdout on its own -- pass that as --prodsplit N to +grid_submit.sh. All other output goes to stderr, so `N=$(./stageSlotsForGrid.py ...)` captures just the +count. +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +# o2tpc_residuals____.root, anchored: the timestamps are always +# exactly 13 digits (ResidualAggregator.cxx formats them from the slot's start/end in ms). +SIG_RE = re.compile(r"o2tpc_residuals_(\d{13})_(\d{13})_\d+_\d+\.root$") + + +def log(msg): + print(msg, file=sys.stderr) + + +def parse_args(argv): + p = argparse.ArgumentParser( + description="Build and upload the TPC map-creation slot manifest.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p.add_argument("--lists-dir", default="./lists", + help="root lists dir (discoverResiduals.sh's WORK_DIR), containing one / " + "subdir per run, each with a .residuals_lists.txt index") + p.add_argument("--alien-dir", default="", + help="AliEn directory to upload the manifest into. Ignored with --preview.") + p.add_argument("--manifest-name", default="mapSlots.tsv", + help="manifest filename. Tag it per submission (mapSlots..tsv) so " + "concurrent productions do not overwrite each other's.") + p.add_argument("--runs", default="", + help="comma-separated run whitelist. Empty means EVERY run under --lists-dir, " + "including ones left from an earlier batch, which would be recalibrated from " + "scratch at hours per slot. Naming a run that has no lists is an error.") + p.add_argument("--slot-length-min", type=float, default=0, + help="target length of ONE map, in minutes. 0 = one window per run.") + p.add_argument("--overlap-margin-ms", type=int, default=1000, + help="a file whose label overlaps a window by <= this is not assigned to it") + p.add_argument("--window-lists", action="store_true", + help="also write one plain file list per window, for inspecting which files a " + "window got. OFF by default: on Lustre each is a metadata round trip and 120 " + "of them cost ~33 s, against ~0.03 s for all the actual work. The manifest " + "already holds every window in full, one line each.") + p.add_argument("--window-dir", default="", + help="where --window-lists writes. Default: .windows. Point it at " + "local disk (e.g. /tmp/windows) on a parallel filesystem.") + p.add_argument("--timing", action="store_true", default=bool(os.environ.get("STAGE_TIMING")), + help="print a read/compute/write breakdown to stderr (also via STAGE_TIMING=1)") + p.add_argument("--preview", action="store_true", + default=bool(os.environ.get("PREVIEW_ONLY")), + help="write the per-window lists but do not upload (also via PREVIEW_ONLY=1)") + return p.parse_args(argv) + + +def read_slot_files(index_path): + """The per-run index lists the file(s) holding that run's residual paths (normally exactly one).""" + out = [] + for line in index_path.read_text().splitlines(): + line = line.strip() + if line: + out.append(Path(line)) + return out + + +def plan_windows(run_first, run_last, slot_ms, margin_ms): + """Whole steps of slot_ms across [run_first, run_last]. + + A trailing remainder shorter than half a step is absorbed into the last window rather than left as + a runt that would fail minTracksPerSlice. Returns [(winFirst, winLast, effectiveMargin)]. + """ + span = run_last - run_first + if slot_ms <= 0 or span <= 0: + n_win = 1 + else: + n_win = max(1, span // slot_ms) + if (span - n_win * slot_ms) * 2 >= slot_ms: + n_win += 1 + + wins = [] + for w in range(n_win): + if n_win == 1: + ws, we = run_first, run_last + else: + ws = run_first + w * slot_ms + we = run_last if w + 1 == n_win else run_first + (w + 1) * slot_ms - 1 + # Clamp the margin to a tenth of the window so a short window can never be emptied by it. + wins.append((ws, we, min(margin_ms, (we - ws) // 10))) + return wins + + +def assign(paths, wins, run_first, slot_ms): + """Fan every file out to the windows its label overlaps by more than that window's margin. + + Only the windows a label can possibly touch are examined, derived directly from the label bounds, + rather than scanning all of them per file: with 10-minute labels and 5-minute windows that is 2-3 + candidates instead of the whole run's worth. Returns (buckets, grazed, orphans). + """ + n_win = len(wins) + buckets = [[] for _ in range(n_win)] + grazed = [0] * n_win + orphans = [] + + for lo, hi, line in paths: + if slot_ms > 0 and n_win > 1: + # Windows are contiguous steps of slot_ms from run_first, so a label maps to an index + # range. +/-1 guards the boundaries and the last window, which extends to run_last. + first = max(0, (lo - run_first) // slot_ms - 1) + last = min(n_win - 1, (hi - run_first) // slot_ms + 1) + else: + first, last = 0, n_win - 1 + + hit = False + for i in range(first, last + 1): + ws, we, margin = wins[i] + if hi < ws or lo > we: + continue + if min(hi, we) - max(lo, ws) > margin: + buckets[i].append(line) + hit = True + else: + grazed[i] += 1 + if not hit: + orphans.append(line) + + return buckets, grazed, orphans + + +def main(argv): + args = parse_args(argv) + + if not args.preview and not args.alien_dir: + log("ERROR: --alien-dir is required unless --preview is given") + return 1 + + lists_dir = Path(args.lists_dir) + index_files = sorted(lists_dir.glob("*/*.residuals_lists.txt")) + if not index_files: + log(f"ERROR: no *.residuals_lists.txt files found under {lists_dir}") + return 1 + + slot_ms = int(round(args.slot_length_min * 60_000)) + wanted = [r for r in args.runs.split(",") if r] if args.runs else [] + + # Rebuilt from scratch each time so it always reflects THIS invocation's slot length and margin + # rather than a stale earlier split. + window_dir = Path(args.window_dir) if args.window_dir else \ + Path(Path(args.manifest_name).with_suffix("").name + ".windows") + t_rm = time.monotonic() + if args.window_lists: + if window_dir.exists(): + shutil.rmtree(window_dir) + window_dir.mkdir(parents=True) + t_rm = time.monotonic() - t_rm + t_read = t_calc = t_write = 0.0 + + manifest_lines = [] + runs_included = [] + empty_runs = [] + n_missing = 0 + n_orphan_total = 0 + + for idx in index_files: + run = idx.name[: -len(".residuals_lists.txt")] + if wanted and run not in wanted: + log(f"Run {run}: not in runs filter, skipping") + continue + runs_included.append(run) + log(f"Run {run}: adding slots from {idx.name}") + + n_slots_run = 0 + for slot_file in read_slot_files(idx): + if not slot_file.is_file(): + log(f" WARNING: missing local file {slot_file}, skipping") + n_missing += 1 + continue + + _t = time.monotonic() + _text = slot_file.read_text() + t_read += time.monotonic() - _t + _t = time.monotonic() + parsed = [] + for line in _text.splitlines(): + m = SIG_RE.search(line) + if m: + parsed.append((int(m.group(1)), int(m.group(2)), line)) + if not parsed: + log(f" WARNING: no parseable residual signature in {slot_file}, skipping") + continue + + t_calc += time.monotonic() - _t + _t = time.monotonic() + run_first = min(p[0] for p in parsed) + run_last = max(p[1] for p in parsed) + wins = plan_windows(run_first, run_last, slot_ms, args.overlap_margin_ms) + buckets, grazed, orphans = assign(parsed, wins, run_first, slot_ms) + n_win = len(wins) + t_calc += time.monotonic() - _t + + for w, ((ws, we, _), files) in enumerate(zip(wins, buckets)): + if grazed[w]: + log(f" (skipped {grazed[w]} file(s) grazing this window)") + if not files: + log(f" WARNING: run {run} window {w}/{n_win} [{ws},{we}] has no files, skipping") + continue + if args.window_lists: + _t = time.monotonic() + out = window_dir / f"{run}.TD{w}of{n_win}.{ws}_{we}.txt" + out.write_text("\n".join(files) + "\n") + t_write += time.monotonic() - _t + manifest_lines.append(f"{run}\t{w}\t{n_win}\t{ws}\t{we}\t{','.join(files)}") + log(f" window {w}/{n_win} [{ws},{we}] {(we - ws) // 1000}s {len(files)} file(s)") + n_slots_run += 1 + + # Safety net for the overlap margin: every file must still land in at least one window. It + # always should -- an aggregation label is minutes long, so it overlaps some window by far + # more than the margin -- but a file in no window at all is silent data loss. + if orphans: + log(f" WARNING: {len(orphans)} file(s) of run {run} landed in NO window -- " + f"their data is not in any map.") + log(f" --overlap-margin-ms ({args.overlap_margin_ms}) is too large for a " + f"slot length of {args.slot_length_min} min.") + for o in orphans[:3]: + log(f" {o}") + n_orphan_total += len(orphans) + + log(f" {n_slots_run} slot(s) added for run {run}") + if n_slots_run == 0: + empty_runs.append(run) + + # A run named in --runs but absent from --lists-dir is a typo or a missed discovery step, not a + # request for a smaller production. + missing_runs = [r for r in wanted if r not in runs_included] + if missing_runs: + log(f"ERROR: run(s) requested in --runs have no *.residuals_lists.txt under {lists_dir}: " + f"{' '.join(missing_runs)}") + log(" Run discoverResiduals.sh for them first, or drop them from --runs.") + return 1 + + if n_missing or empty_runs or n_orphan_total: + log("") + log(" ****************************************************************") + log(" ** INCOMPLETE INPUT -- this manifest is smaller than expected **") + if n_missing: + log(f" ** {n_missing} slot file(s) referenced by an index but missing on disk") + if empty_runs: + log(f" ** run(s) contributing 0 slots: {' '.join(empty_runs)}") + if n_orphan_total: + log(f" ** {n_orphan_total} file(s) landed in no window -- lower --overlap-margin-ms") + log(" ** -> re-run discoverResiduals.sh for those runs, or accept") + log(" ** that they are simply absent from this production.") + log(" ****************************************************************") + log("") + + manifest_text = "".join(l + "\n" for l in manifest_lines) + # The manifest is always written locally, as a single file -- it is the complete record of the + # split and costs one create. + _t = time.monotonic() + Path(args.manifest_name).write_text(manifest_text) + t_write += time.monotonic() - _t + n_total = len(manifest_lines) + + if args.timing: + log(f"timing: clear={t_rm:.2f}s read={t_read:.2f}s compute={t_calc:.2f}s " + f"write={t_write:.2f}s ({n_total} window file(s))") + + if args.preview: + log("PREVIEW_ONLY set -- manifest NOT uploaded.") + log(f"Manifest written to: {args.manifest_name} ({n_total} window(s))") + if args.window_lists: + log(f"Per-window file lists in: {window_dir}/") + else: + log("Pass --window-lists to also dump one file list per window " + "(slow on Lustre; add --window-dir /tmp/... )") + print(n_total) + return 0 + + with tempfile.NamedTemporaryFile("w", suffix=".tsv", delete=False) as fh: + fh.write(manifest_text) + tmp_manifest = fh.name + try: + dest = f"alien://{args.alien_dir.rstrip('/')}/{args.manifest_name}" + subprocess.run(["alien.py", "cp", "-f", f"file:{tmp_manifest}", dest], + check=True, stdout=sys.stderr.fileno()) + log(f"Manifest uploaded: {dest} ({n_total} slot(s) across {len(runs_included)} run(s))") + finally: + os.unlink(tmp_manifest) + + log(f"Manifest also written locally to: {args.manifest_name}") + if args.window_lists: + log(f"Per-window file lists in: {window_dir}/") + log(f"Runs in this manifest: {' '.join(runs_included)}") + print(n_total) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/DATA/production/configurations/mapCreationGrid/submitMapCreation.sh b/DATA/production/configurations/mapCreationGrid/submitMapCreation.sh new file mode 100755 index 000000000..72d80cc6b --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/submitMapCreation.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +# submitMapCreation.sh +# +# Submits one grid_submit.sh production-split job, one subjob per time-window slot found in +# (built beforehand by discoverResiduals.sh -- run that first). +# +# Before each submission, this re-stages the slot manifest (mapSlots.tsv, built here via +# stageSlotsForGrid.py) -- and, when MACRO_SOURCE=alien, the macro (staticMapCreatorCPM.C) too -- to +# the same AliEn dir mapCreationJob.sh downloads from. Both must be fresh: a stale macro, or +# stageSlotsForGrid.py pointed at a different destination than the job script expects, would silently +# desync what a running job downloads from what this wrapper thinks it uploaded. The bad-time-ranges list +# is treated differently -- see BAD_RANGES_LOCAL below -- it's occasionally-updated reference data, not +# something edited every session like the macro, so forcing a fresh re-upload on every submission is +# unnecessary friction. +# +# Slot windowing: stageSlotsForGrid.py cuts each run into windows of ~SLOT_LENGTH_MIN and writes ONE +# manifest line per window, carrying the window bounds and exactly the files that can contribute to it. +# A subjob index therefore identifies a (run, window) pair on its own, so this is a SINGLE submission -- +# no loop over TimeDivisionIndex, no TIME_DIVISION placeholders substituted into the payload, and one +# stage1Workdir pointer instead of one per index. (With a fixed window length, runs of different length +# produce different window counts, so the old "one production per index x prodsplit runs" rectangle +# does not exist any more.) Both stages read the window off the same manifest line, which removes the +# old failure mode where stage 2 recomputed it and any mismatch silently produced zero output. +# +# Output location (2026-07-26, per ALICE Grid support): the actual map data (voxRes.*.root) is staged by +# mapCreationJob.sh directly to ${ALIEN_JDL_OUTPUTDIR} (the JDL-assigned per-subjob output dir -- +# see that script), NOT to a fixed personal-space path -- avoids a separate disk-quota request on top of +# the CPU one, and is the standard mechanism support asked us to use. That directory is only known once +# the job actually runs, and its parent (MY_JOBWORKDIR, timestamped at THIS submission) is otherwise +# unrecoverable later -- grid_submit.sh prints it once, right here, before any GRID execution starts: +# pok "Your job's working directory will be $MY_JOBWORKDIR" +# So this wrapper captures that one line, once per submission, and uploads it as a small pointer file +# (stage1Workdir..txt, negligible size, fine to keep in personal space) -- submitPostProcess.sh +# downloads it and reconstructs each slot's exact +# ${ALIEN_JDL_OUTPUTDIR} from MY_JOBWORKDIR + the same alien_counter formula grid_submit.sh itself uses +# (see that script), without needing any in-job breadcrumb or querying AliEn after the fact. +# +# ASUSER / quota: setting ASUSER="pwg_pp" charges this job's CPU usage to that working group instead of +# the personal account, per ALICE Grid support's instruction (the account must already be a member of +# that group). grid_submit.sh's --asuser sets MY_USER=${ASUSER}, which determines BOTH the role active +# when its own internal transaction issues `submit` (so JAliEn attributes the job to that role) AND +# MY_HOMEDIR="/alice/cern.ch/user/${MY_USER:0:1}/${MY_USER}" -- so OutputDir automatically lands under +# that group's own writable space, with zero extra redirection needed. MACRO_ALIEN_DIR_BARE (the small +# manifest/macro staging area this wrapper itself uploads to, read-only from the job's point of view) +# stays fixed under the submitting account regardless of ASUSER -- read access there is unaffected by +# role. +# +# Usage: +# ./submitMapCreation.sh [listsDir] +# (listsDir defaults to ./lists -- discoverResiduals.sh's WORK_DIR, run from wherever you ran that +# script) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LISTS_DIR="${1:-./lists}" + +# --------------------------------------------------------------------------- +# Config -- edit per submission +# --------------------------------------------------------------------------- +JOBNAME="mapcreation-v1" # must be unique per submission -- bump the suffix for each new production +TOPWORKDIR="MapCreation" # replaces grid_submit.sh's generic default "selfjobs" bucket (on top of $HOME) + # with something identifiable -- matches MACRO_ALIEN_DIR_BARE's own naming + # below. Still just a static string per submission, not a per-run/period path + # like the real input data's own convention (.../2026/LHC26ak/572244/...) -- + # that needs a real post-processing/organize step, deliberately deferred to a + # later iteration of this pipeline. +PACKAGESPEC="O2::daily-20260729-0000-1" +ASUSER="pwg_pp" # your own account = personal quota. "pwg_pp" = charged to that working group's quota AND + # moves MY_JOBWORKDIR/OutputDir to pwg_pp's own space automatically -- see the + # long comment above for why this one value now does everything. NOTE support + # hedged on whether pwg_pp behaves like the other major accounts -- confirm on + # alimonitor that usage is really charged to the group before a large batch. +MACRO_SOURCE="official" # "official" (default): GRID jobs run staticMapCreatorCPM.C straight from the + # O2 package loaded via PACKAGESPEC ($O2_ROOT/share/macro/), no macro upload + # needed here at all -- requires PACKAGESPEC to resolve to a tag built from an O2 + # version that includes it (Detectors/TPC/calibration/SpacePoints/macro/). "alien": + # stage MACRO_LOCAL_PATH to AliEn and have jobs download it instead -- use this + # until such a tag is published. +MACRO_LOCAL_PATH="" # only used when MACRO_SOURCE=alien: local path to staticMapCreatorCPM.C to + # upload, e.g. your O2 checkout's + # Detectors/TPC/calibration/SpacePoints/macro/staticMapCreatorCPM.C. + # Staging area for the manifest + stage1Workdir pointer (and the macro, when + # MACRO_SOURCE=alien). No alien:// prefix. Derived from your own AliEn account the + # same way grid_submit.sh derives MY_HOMEDIR (ALIEN_USER, else the local + # username), so this file needs no per-user edit. Stays under the SUBMITTING + # account regardless of ASUSER -- read access there is unaffected by role, see the + # comment above. If your laptop's local username differs from your AliEn account + # (common), export ALIEN_USER, or just set MAPCREATION_ALIEN_DIR outright. +ALIEN_USER_NAME="${ALIEN_USER:-$(whoami)}" +MACRO_ALIEN_DIR_BARE="${MAPCREATION_ALIEN_DIR:-/alice/cern.ch/user/${ALIEN_USER_NAME:0:1}/${ALIEN_USER_NAME}/MapCreation}" +BAD_RANGES_NAME="Merge_BadIntervals_LHC26_PbPb_DCA-0P300.txt" + # Filename of the bad-time-ranges list, under ${MACRO_ALIEN_DIR}/lists/. Period- + # and system-specific (this one is PbPb26), so it changes when the data does -- + # hence a variable rather than a constant baked into three places. Substituted + # into mapCreationJob.sh the same way as JOBNAME_TAG, so + # set it only here. Used both as the upload destination (when BAD_RANGES_LOCAL is + # set) and as the name each job downloads -- the two cannot drift apart. + # Set to "" to disable bad-ranges filtering entirely for this submission + # (staticMapCreatorCPM.C treats an empty badRangeList argument that way) -- a + # deliberate opt-out, not the same as forgetting to set it. +BAD_RANGES_LOCAL="" # optional: local path to the bad-time-ranges file to (re-)upload this run (real + # source for the PbPb26 list above: + # /lustre/alice/tpcdata/Run3/SCDprodTests/lists/badRange/PbPb26/). Leave empty to + # skip the upload and just use the copy already at + # ${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME}. +RUNS_FILTER="" # optional comma-separated run whitelist, e.g. "572557,572558". Empty = every run + # found under listsDir. discoverResiduals.sh appends to the same ./lists tree + # across batches, so leaving this empty after a second discovery round silently + # folds the EARLIER batch's runs into this submission too -- recalibrating them + # from scratch at hours per slot. Set it when ./lists holds more than you want. + # A run named here with no lists under listsDir is a hard error. +OVERLAP_MARGIN_MS=1000 # a file is assigned to a window only if its aggregation label overlaps that + # window by MORE than this. Coarse labels routinely begin 1-2 ms before a window + # ends, and a bare overlap test drags all of their files into a window whose map + # they contribute nothing to. Bounded cost: at most this many ms of coverage per + # window edge, since the macro's per-TF cut discards the rest anyway. +SLOT_LENGTH_MIN=5 # target length of ONE map, in minutes. Each run is cut into whole steps of this + # length; a trailing remainder shorter than half a step is absorbed into the last + # window rather than left as a runt that would fail MIN_TRACKS_PER_SLICE. 0 means + # one map per run (no splitting). This replaces the old TIME_DIVISION count: with + # a fixed length, a 20 min run gives 4 maps and a 60 min run gives 12, instead of + # the same count regardless of run length. Stage 2 does not need this value -- it + # reuses stage 1's manifest, which already carries the windows. +# --------------------------------------------------------------------------- + +MACRO_ALIEN_DIR="alien://${MACRO_ALIEN_DIR_BARE}" +echo "== Staging area (manifest + stage1Workdir pointer): ${MACRO_ALIEN_DIR_BARE} ==" +echo " (derived from AliEn user '${ALIEN_USER_NAME}' -- export ALIEN_USER or MAPCREATION_ALIEN_DIR if that is wrong)" + +if [[ "${MACRO_SOURCE}" == "alien" ]]; then + [[ -n "${MACRO_LOCAL_PATH}" && -f "${MACRO_LOCAL_PATH}" ]] || { echo "ERROR: MACRO_SOURCE=alien needs MACRO_LOCAL_PATH set to a real staticMapCreatorCPM.C (got '${MACRO_LOCAL_PATH}')" >&2; exit 1; } + echo "== Re-staging macro from ${MACRO_LOCAL_PATH} to ${MACRO_ALIEN_DIR} ==" + alien.py cp -f "file:${MACRO_LOCAL_PATH}" "${MACRO_ALIEN_DIR}/staticMapCreatorCPM.C" +else + echo "== MACRO_SOURCE=official -- skipping macro upload, jobs will use \${O2_ROOT}/share/macro/staticMapCreatorCPM.C from PACKAGESPEC ==" +fi + +if [[ -z "${BAD_RANGES_NAME}" ]]; then + # Deliberate opt-out, not a mistake: an empty BAD_RANGES_NAME propagates through to + # mapCreationJob.sh as BAD_RANGES_ARG="" -- staticMapCreatorCPM.C's own + # `if (badRangeList.length() > 0)` guard treats that as "skip bad-ranges filtering entirely" for + # this submission. Nothing to upload or verify. + echo "== BAD_RANGES_NAME is empty -- bad-time-ranges filtering disabled for this submission ==" +elif [[ -n "${BAD_RANGES_LOCAL}" ]]; then + [[ -f "${BAD_RANGES_LOCAL}" ]] || { echo "ERROR: BAD_RANGES_LOCAL not found: ${BAD_RANGES_LOCAL}" >&2; exit 1; } + echo "== Re-staging bad-ranges list from ${BAD_RANGES_LOCAL} to ${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME} ==" + alien.py cp -f "file:${BAD_RANGES_LOCAL}" "${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME}" +else + # No local file to (re-)upload -- the job will download whatever is already staged under + # BAD_RANGES_NAME, so check now, once, locally, that it actually exists. Left unchecked, a wrong + # BAD_RANGES_NAME/ALIEN_USER or a never-uploaded file would only surface inside + # mapCreationJob.sh's own alien.py cp, in every single subjob, with a raw/confusing AliEn error + # instead of one clear message before anything is submitted. + echo "== BAD_RANGES_LOCAL not set -- checking ${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME} already exists on AliEn ==" + if ! alien.py stat "${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME}" &>/dev/null; then + echo "ERROR: ${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME} not found on AliEn, and BAD_RANGES_LOCAL is not set to (re-)upload one." >&2 + echo " Either set BAD_RANGES_LOCAL to a local file to upload, set BAD_RANGES_NAME=\"\" to disable bad-ranges filtering, or upload the file to that path by hand." >&2 + exit 1 + fi + echo "== Found existing ${MACRO_ALIEN_DIR}/lists/${BAD_RANGES_NAME} ==" +fi + +# Tagged by JOBNAME: a shared, untagged manifest name would let a second submission (different +# lists/config) silently overwrite the first's manifest, breaking any already-running job that re-reads +# it and desyncing stage 2's later subjob-index -> slot lookup. +MANIFEST_NAME="mapSlots.${JOBNAME}.tsv" +echo "== Building + uploading slot manifest from ${LISTS_DIR} (as ${MANIFEST_NAME}) ==" +N=$("${SCRIPT_DIR}/stageSlotsForGrid.py" \ + --lists-dir "${LISTS_DIR}" \ + --alien-dir "${MACRO_ALIEN_DIR_BARE}" \ + --manifest-name "${MANIFEST_NAME}" \ + --runs "${RUNS_FILTER}" \ + --slot-length-min "${SLOT_LENGTH_MIN}" \ + --overlap-margin-ms "${OVERLAP_MARGIN_MS}") +echo "N=${N} slot(s) == map(s) to be produced" +[[ "${N}" -gt 0 ]] || { echo "ERROR: no slots found under ${LISTS_DIR}" >&2; exit 1; } + +TMP_SCRIPT="$(mktemp)" +trap 'rm -f "${TMP_SCRIPT}"' EXIT + +# ONE submission, one subjob per manifest line. There is no longer a loop over TimeDivisionIndex: the +# windows are baked into the manifest by stageSlotsForGrid.py, so a subjob index alone identifies a +# (run, window) pair. That also means one stage1Workdir pointer instead of one per index, and no +# TIME_DIVISION/TIME_DIVISION_INDEX placeholders to substitute -- with a fixed SLOT_LENGTH_MIN, +# different-length runs yield different window counts, so the old rectangular +# "one production per index x prodsplit runs" grid does not exist any more. +sed \ + -e "s/^JOBNAME_TAG=.*/JOBNAME_TAG=${JOBNAME}/" \ + -e "s/^MACRO_SOURCE=.*/MACRO_SOURCE=${MACRO_SOURCE}/" \ + -e "s|^MACRO_ALIEN_DIR_BARE=.*|MACRO_ALIEN_DIR_BARE=${MACRO_ALIEN_DIR_BARE}|" \ + -e "s|^BAD_RANGES_NAME=.*|BAD_RANGES_NAME=${BAD_RANGES_NAME}|" \ + "${SCRIPT_DIR}/mapCreationJob.sh" > "${TMP_SCRIPT}" + +GRID_SUBMIT_ARGS=( + --script "${TMP_SCRIPT}" + --jobname "${JOBNAME}" + --topworkdir "${TOPWORKDIR}" + --prodsplit "${N}" + --packagespec "${PACKAGESPEC}" + --asuser "${ASUSER}" +) + +echo "== Submitting ${JOBNAME} (prodsplit=${N}, asuser=${ASUSER}) ==" +# Capture grid_submit.sh's own stderr (its "pok" status messages go there) while still showing it live, +# so we can pull out the one line that tells us where this submission's data will actually land. +SUBMIT_LOG=$("${O2DPG_ROOT}/GRID/utils/grid_submit.sh" "${GRID_SUBMIT_ARGS[@]}" 2>&1 | tee /dev/stderr) + +# Strip ANSI color codes (pok() wraps its output in \033[32m...\033[m) before parsing. +MY_JOBWORKDIR=$(echo "${SUBMIT_LOG}" | sed -E 's/\x1b\[[0-9;]*m//g' | sed -n "s/^Your job's working directory will be //p" | tail -1) +if [[ -z "${MY_JOBWORKDIR}" ]]; then + echo "ERROR: could not find \"Your job's working directory will be ...\" in grid_submit.sh's output -- stage 2 won't be able to find this submission's output. Aborting." >&2 + exit 1 +fi +echo "== MY_JOBWORKDIR = ${MY_JOBWORKDIR} ==" + +WORKDIR_POINTER_NAME="stage1Workdir.${JOBNAME}.txt" +WORKDIR_POINTER_TMP="$(mktemp)" +echo "${MY_JOBWORKDIR}" > "${WORKDIR_POINTER_TMP}" +alien.py cp -f "file:${WORKDIR_POINTER_TMP}" "${MACRO_ALIEN_DIR}/${WORKDIR_POINTER_NAME}" +rm -f "${WORKDIR_POINTER_TMP}" +echo "== Uploaded ${WORKDIR_POINTER_NAME} ==" diff --git a/DATA/production/configurations/mapCreationGrid/submitPostProcess.sh b/DATA/production/configurations/mapCreationGrid/submitPostProcess.sh new file mode 100755 index 000000000..dce0500b7 --- /dev/null +++ b/DATA/production/configurations/mapCreationGrid/submitPostProcess.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# submitPostProcess.sh +# +# STAGE 2 submission wrapper: submits one grid_submit.sh production-split job, one subjob per slot -- +# same granularity as stage 1 (submitMapCreation.sh), so a failed +# stage-2 subjob can be resubmitted on its own without touching stage 1's (hours-long) map creation. +# +# Unlike stage 1, this does NOT rebuild the slot manifest (mapSlots.tsv) -- it reuses the exact one +# stage 1 already uploaded (downloaded here only to count lines for --prodsplit N). Rebuilding it here +# would risk a mismatch if the local lists/ dir changed between the two submissions, which would +# silently desync stage 2's subjob-index -> slot mapping from stage 1's. +# +# IMPORTANT: STAGE1_JOBNAME_TAG must match whatever the stage-1 submission this is post-processing +# actually used -- it selects both the manifest and the stage1Workdir pointer. There is nothing else to +# keep in sync: the slot windows come from stage 1's own manifest, which this stage reuses verbatim, so +# the two stages cannot disagree about a slot's identity the way they could when each recomputed it. +# +# Output location (per ALICE Grid support -- see submitMapCreation.sh): stage 1 stages its map to its +# own ${ALIEN_JDL_OUTPUTDIR}, not a fixed personal-space path, so there's no static location +# to just look up. Stage 1's wrapper captured MY_JOBWORKDIR (that submission's timestamped parent +# directory) at submission time and uploaded it as a small pointer file -- +# downloaded below and substituted into the payload script (STAGE1_OUTPUTDIR_BASE) alongside COUNTERWIDTH +# (grid_submit.sh's own alien_counter zero-padding width -- deterministic from N, which is shared between +# both stages since this wrapper reuses stage 1's manifest, so it's recomputed here rather than needing +# its own pointer file). The payload script reconstructs each slot's exact ${ALIEN_JDL_OUTPUTDIR} from +# those two values the same way grid_submit.sh itself derives OutputDir. +# +# ASUSER / quota: see submitMapCreation.sh's own comment for the full story -- ASUSER +# determines both the role JAliEn attributes the job to and where MY_HOMEDIR (and therefore OutputDir) +# lands. Must match whatever STAGE1_JOBNAME_TAG's submission actually used, since STAGE1_OUTPUTDIR_BASE +# below (from stage 1's pointer file) already reflects wherever stage 1's own ASUSER put it -- this +# script doesn't need its own OutputDir logic, it just needs to read/write consistently with whatever +# stage 1 did. +# +# Usage: +# ./submitPostProcess.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# --------------------------------------------------------------------------- +# Config -- edit per submission. STAGE1_JOBNAME_TAG must name the stage-1 submission this +# post-processes; everything else about the slot layout comes from that submission's manifest. +# --------------------------------------------------------------------------- +JOBNAME="mapcreation-postprocess-v1" # this stage's OWN job name (GRID job monitoring identity) -- + # NOT the same thing as STAGE1_JOBNAME_TAG below. +STAGE1_JOBNAME_TAG="mapcreation-v1" # must equal the JOBNAME stage 1 (submitMapCreation.sh) was + # actually submitted with -- that's what determined where + # stage 1 staged its map. +TOPWORKDIR="MapCreation" +PACKAGESPEC="O2::daily-20260729-0000-1" +ASUSER="pwg_pp" # see the long comment above -- your own account for personal quota, "pwg_pp" for group + # quota (this stage's own submission; independent of what stage 1 used). +MACRO_SOURCE="official" # same meaning/default as stage 1 (submitMapCreation.sh) -- applies + # to all three macros used here. +MACRO_LOCAL_DIR="" # only used when MACRO_SOURCE=alien: local directory containing + # SmoothingExtrapolate.C, TPCFastTransformInitCPM.C, voxResQA.C to upload, e.g. + # your O2 checkout's Detectors/TPC/calibration/SpacePoints/macro/ + + # GPU/TPCFastTransformation/macro/ (voxResQA.C and SmoothingExtrapolate.C live in + # the former, TPCFastTransformInitCPM.C in the latter -- symlink/copy them into one + # local dir first if they're not already colocated). + # Same staging root as stage 1, derived the same way (see that script's comment): + # ALIEN_USER, else the local username; override with MAPCREATION_ALIEN_DIR. Must + # resolve to whatever stage 1 actually used, since that's where its manifest and + # stage1Workdir pointer live. +ALIEN_USER_NAME="${ALIEN_USER:-$(whoami)}" +MACRO_ALIEN_DIR_BARE="${MAPCREATION_ALIEN_DIR:-/alice/cern.ch/user/${ALIEN_USER_NAME:0:1}/${ALIEN_USER_NAME}/MapCreation}" +# --------------------------------------------------------------------------- + +MACRO_ALIEN_DIR="alien://${MACRO_ALIEN_DIR_BARE}" +echo "== Staging area (must match stage 1's): ${MACRO_ALIEN_DIR_BARE} ==" +echo " (derived from AliEn user '${ALIEN_USER_NAME}' -- export ALIEN_USER or MAPCREATION_ALIEN_DIR if that is wrong)" + +if [[ "${MACRO_SOURCE}" == "alien" ]]; then + [[ -n "${MACRO_LOCAL_DIR}" && -f "${MACRO_LOCAL_DIR}/SmoothingExtrapolate.C" && -f "${MACRO_LOCAL_DIR}/TPCFastTransformInitCPM.C" && -f "${MACRO_LOCAL_DIR}/voxResQA.C" ]] || { echo "ERROR: MACRO_SOURCE=alien needs MACRO_LOCAL_DIR set to a directory containing all three macros (got '${MACRO_LOCAL_DIR}')" >&2; exit 1; } + echo "== Re-staging SmoothingExtrapolate.C / TPCFastTransformInitCPM.C / voxResQA.C from ${MACRO_LOCAL_DIR} to ${MACRO_ALIEN_DIR} ==" + alien.py cp -f "file:${MACRO_LOCAL_DIR}/SmoothingExtrapolate.C" "${MACRO_ALIEN_DIR}/SmoothingExtrapolate.C" + alien.py cp -f "file:${MACRO_LOCAL_DIR}/TPCFastTransformInitCPM.C" "${MACRO_ALIEN_DIR}/TPCFastTransformInitCPM.C" + alien.py cp -f "file:${MACRO_LOCAL_DIR}/voxResQA.C" "${MACRO_ALIEN_DIR}/voxResQA.C" +else + echo "== MACRO_SOURCE=official -- skipping macro upload, jobs will use \${O2_ROOT}/share/macro/ from PACKAGESPEC ==" +fi + +# Tagged manifest name -- must match whatever stage 1 actually uploaded (mapSlots..tsv), +# hence STAGE1_JOBNAME_TAG rather than this stage's own JOBNAME. +MANIFEST_NAME="mapSlots.${STAGE1_JOBNAME_TAG}.tsv" +echo "== Fetching existing slot manifest from ${MACRO_ALIEN_DIR}/${MANIFEST_NAME} (NOT rebuilt) ==" +TMP_MANIFEST="$(mktemp)" +alien.py cp -f "${MACRO_ALIEN_DIR}/${MANIFEST_NAME}" "file:${TMP_MANIFEST}" +N=$(wc -l < "${TMP_MANIFEST}" | tr -d '[:space:]') # wc -l right-justifies its count with leading + # whitespace even without a filename arg; grid_submit.sh's + # --prodsplit does a strict positive-integer check that + # rejects that -- confirmed real, 2026-07-26 ("Production + # split must be a positive integer (got ' 70')"). +echo "N=${N} slot(s)" +[[ "${N}" -gt 0 ]] || { echo "ERROR: ${MANIFEST_NAME} has no lines -- was stage 1 ever submitted with this JOBNAME?" >&2; exit 1; } + +# Same formula grid_submit.sh itself uses to derive its alien_counter zero-padding width -- deterministic +# from N, which is identical between stage 1 and stage 2 (same manifest, same line count), so this will +# match whatever stage 1's own submission actually used without needing to record it separately. +COUNTERWIDTH=${#N} +[[ "${COUNTERWIDTH}" -lt 3 ]] && COUNTERWIDTH=3 + +TMP_SCRIPT="$(mktemp)" +TMP_WORKDIR_POINTER="$(mktemp)" +trap 'rm -f "${TMP_MANIFEST}" "${TMP_SCRIPT:-}" "${TMP_WORKDIR_POINTER}"' EXIT + +# ONE submission, mirroring stage 1: the windows live in the manifest, so a subjob index identifies a +# (run, window) pair by itself and there is nothing to loop over. +WORKDIR_POINTER_NAME="stage1Workdir.${STAGE1_JOBNAME_TAG}.txt" +echo "== Fetching ${WORKDIR_POINTER_NAME} ==" +# Truncate first: an alien.py cp that returns 0 without writing would otherwise leave stale content +# here and point this whole submission at the wrong stage-1 workdir, silently no-opping every slot. +: > "${TMP_WORKDIR_POINTER}" +if ! alien.py cp -f "${MACRO_ALIEN_DIR}/${WORKDIR_POINTER_NAME}" "file:${TMP_WORKDIR_POINTER}" || [[ ! -s "${TMP_WORKDIR_POINTER}" ]]; then + echo "ERROR: could not fetch ${WORKDIR_POINTER_NAME} -- was stage 1 actually submitted under STAGE1_JOBNAME_TAG=${STAGE1_JOBNAME_TAG}?" >&2 + exit 1 +fi +STAGE1_OUTPUTDIR_BASE=$(head -1 "${TMP_WORKDIR_POINTER}") +if [[ "${STAGE1_OUTPUTDIR_BASE}" != /alice/* ]]; then + echo "ERROR: ${WORKDIR_POINTER_NAME} does not contain an absolute /alice/... path: '${STAGE1_OUTPUTDIR_BASE}'" >&2 + exit 1 +fi +echo "== STAGE1_OUTPUTDIR_BASE = ${STAGE1_OUTPUTDIR_BASE} ==" + +sed \ + -e "s/^STAGE1_JOBNAME_TAG=.*/STAGE1_JOBNAME_TAG=${STAGE1_JOBNAME_TAG}/" \ + -e "s/^MACRO_SOURCE=.*/MACRO_SOURCE=${MACRO_SOURCE}/" \ + -e "s|^MACRO_ALIEN_DIR_BARE=.*|MACRO_ALIEN_DIR_BARE=${MACRO_ALIEN_DIR_BARE}|" \ + -e "s|^STAGE1_OUTPUTDIR_BASE=.*|STAGE1_OUTPUTDIR_BASE=${STAGE1_OUTPUTDIR_BASE}|" \ + -e "s/^COUNTERWIDTH=.*/COUNTERWIDTH=${COUNTERWIDTH}/" \ + "${SCRIPT_DIR}/postProcessJob.sh" > "${TMP_SCRIPT}" + +GRID_SUBMIT_ARGS=( + --script "${TMP_SCRIPT}" + --jobname "${JOBNAME}" + --topworkdir "${TOPWORKDIR}" + --prodsplit "${N}" + --packagespec "${PACKAGESPEC}" + --asuser "${ASUSER}" +) + +echo "== Submitting ${JOBNAME} (prodsplit=${N}, asuser=${ASUSER}) ==" +"${O2DPG_ROOT}/GRID/utils/grid_submit.sh" "${GRID_SUBMIT_ARGS[@]}" From 479787dacf6002fd4b535b57765e2356c04a4c3b Mon Sep 17 00:00:00 2001 From: shahor02 Date: Thu, 30 Jul 2026 02:16:43 +0400 Subject: [PATCH 029/144] Disable minTrackdEdxMax2Tot cut for >= 2025 data (#2425) --- DATA/production/configurations/asyncReco/setenv_extra.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index 10c595455..57c9ae688 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -862,6 +862,10 @@ if [[ -n $ALIEN_JDL_MVBIAS ]]; then export O2_DPL_MVBIAS=$ALIEN_JDL_MVBIAS fi +if [[ $ALIEN_JDL_LPMANCHORYEAR -ge 2025 ]] ; then + export CONFIG_EXTRA_PROCESS_o2_gpu_reco_workflow+=";GPU_rec_tpc.minTrackdEdxMax2Tot=-1.;" +fi + # Enabling QC if [[ $ALIEN_JDL_QCOFF != "1" ]]; then export WORKFLOW_PARAMETERS="QC,${WORKFLOW_PARAMETERS}" From fe3dca57989f9c3198419aa46a5f41e7b9725a53 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Thu, 30 Jul 2026 12:33:00 +0400 Subject: [PATCH 030/144] Patches to allow storing ConfiParam jsons in the AOD metadata (#2421) * Disable ConfigParam outputs only in EPNSYNC mode * JDL option to force AODProducer to collect ConfigParam jsons --- DATA/common/getCommonArgs.sh | 6 ++++-- DATA/production/configurations/asyncReco/setenv_extra.sh | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/DATA/common/getCommonArgs.sh b/DATA/common/getCommonArgs.sh index c585409ee..bcecea974 100755 --- a/DATA/common/getCommonArgs.sh +++ b/DATA/common/getCommonArgs.sh @@ -10,16 +10,18 @@ if [[ -z $SEVERITY || -z $NUMAID || -z $SHMSIZE || -z $FILEWORKDIR || -z $EPNSYN fi ARGS_ALL="--session ${OVERRIDE_SESSION:-default} --severity $SEVERITY --shm-segment-id ${O2JOBSHMID:-$NUMAID} --shm-segment-size $SHMSIZE ${ARGS_ALL_EXTRA:-} --early-forward-policy noraw" -ARGS_ALL_CONFIG="keyval.input_dir=$FILEWORKDIR;keyval.output_dir=/dev/null;${ALL_EXTRA_CONFIG:-}" +ARGS_ALL_CONFIG="keyval.input_dir=$FILEWORKDIR;" if [[ $EPNSYNCMODE == 1 ]]; then ARGS_ALL+=" --infologger-severity $INFOLOGGER_SEVERITY" ARGS_ALL+=" --monitoring-backend influxdb-unix:///tmp/telegraf.sock --resources-monitoring ${GEN_TOPO_RESOURCE_MONITORING:-15} ${GEN_TOPO_RESOURCE_MONITORING:+--dpl-stats-min-online-publishing-interval $GEN_TOPO_RESOURCE_MONITORING}" - ARGS_ALL_CONFIG+="NameConf.mCCDBServer=$GEN_TOPO_EPN_CCDB_SERVER;" + ARGS_ALL_CONFIG+="keyval.output_dir=/dev/null;NameConf.mCCDBServer=$GEN_TOPO_EPN_CCDB_SERVER;" export DPL_CONDITION_BACKEND=$GEN_TOPO_EPN_CCDB_SERVER [[ -z ${O2_DPL_DEPLOYMENT_MODE:-} ]] && O2_DPL_DEPLOYMENT_MODE=OnlineECS elif [[ "${ENABLE_METRICS:-}" != "1" ]]; then ARGS_ALL+=" --monitoring-backend no-op://" fi +ARGS_ALL_CONFIG+="${ALL_EXTRA_CONFIG:-}" + [[ $SHMTHROW == 0 ]] && ARGS_ALL+=" --bad-alloc-max-attempts 60 --bad-alloc-attempt-interval 1000" [[ ! -z ${SHM_MANAGER_SHMID:-} && ${GEN_TOPO_CALIB_WORKFLOW:-} != 1 ]] && ARGS_ALL+=" --no-cleanup --shm-no-cleanup on --shmid $SHM_MANAGER_SHMID" [[ $NORATELOG == 1 ]] && ARGS_ALL+=" --fairmq-rate-logging 0" diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index 57c9ae688..5f6d70e7d 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -540,7 +540,6 @@ elif [[ $ALIGNLEVEL == 1 ]]; then TRACKTUNETPCINNER="trackTuneParams.tpcCovInnerType=2;trackTuneParams.tpcCovInner[0]=0.05;trackTuneParams.tpcCovInner[1]=0.2;trackTuneParams.tpcCovInner[2]=0.0003;trackTuneParams.tpcCovInner[3]=0.0013;trackTuneParams.tpcCovInner[4]=0.0059300284;trackTuneParams.tpcCovInnerSlope[0]=1.4794650254467986e-08;trackTuneParams.tpcCovInnerSlope[1]=5.9178601017871944e-08;trackTuneParams.tpcCovInnerSlope[2]=8.87679015268079e-11;trackTuneParams.tpcCovInnerSlope[3]=3.846609066161676e-10;trackTuneParams.tpcCovInnerSlope[4]=1.7546539235412473e-09;" TRACKTUNETPCOUTER="trackTuneParams.tpcCovOuterType=2;trackTuneParams.tpcCovOuter[0]=0.05;trackTuneParams.tpcCovOuter[1]=0.2;trackTuneParams.tpcCovOuter[2]=0.0003;trackTuneParams.tpcCovOuter[3]=0.0013;trackTuneParams.tpcCovOuter[4]=0.0059300284;trackTuneParams.tpcCovOuterSlope[0]=1.4794650254467986e-08;trackTuneParams.tpcCovOuterSlope[1]=5.9178601017871944e-08;trackTuneParams.tpcCovOuterSlope[2]=8.87679015268079e-11;trackTuneParams.tpcCovOuterSlope[3]=3.846609066161676e-10;trackTuneParams.tpcCovOuterSlope[4]=1.7546539235412473e-09;" - fi TRACKTUNETPC=${TPCEXTRAERR-} @@ -857,6 +856,11 @@ if [[ $ALIEN_JDL_PREPROPAGATE == "1" ]] ; then export ARGS_EXTRA_PROCESS_o2_aod_producer_workflow+=" --propagate-tracks --propagate-tracks-max-xiu 5" fi +if [[ $ALIEN_JDL_CONFIG2AODMETA == "1" ]] ; then + export ARGS_EXTRA_PROCESS_o2_aod_producer_workflow+=" --collect-config-files" +fi + + # possibility to bias the MeanVertex from the CCDB, see https://github.com/AliceO2Group/AliceO2/pull/15549 if [[ -n $ALIEN_JDL_MVBIAS ]]; then export O2_DPL_MVBIAS=$ALIEN_JDL_MVBIAS From 3e9018acfbbc1d38aac841e86e4f905424176929 Mon Sep 17 00:00:00 2001 From: Francesco Mazzaschi Date: Thu, 30 Jul 2026 10:02:34 +0200 Subject: [PATCH 031/144] Fix lambda1405 generator --- MC/config/PWGLF/ini/GeneratorLFSigmaPiResonances.ini | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MC/config/PWGLF/ini/GeneratorLFSigmaPiResonances.ini b/MC/config/PWGLF/ini/GeneratorLFSigmaPiResonances.ini index e58e173c7..046e95765 100644 --- a/MC/config/PWGLF/ini/GeneratorLFSigmaPiResonances.ini +++ b/MC/config/PWGLF/ini/GeneratorLFSigmaPiResonances.ini @@ -1,6 +1,9 @@ [GeneratorExternal] -fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C -funcName=generateLF("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/sigmapi_resonances_gun.json", true, 4) +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/sigmapi_resonances_gun.json", true, 4, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg [DecayerPythia8] # after for transport code! config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg From 1845629c908816c09d17fe40fe4bd93f17c70a20 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Fri, 31 Jul 2026 15:19:38 +0200 Subject: [PATCH 032/144] Add CODEOWNERS for GRID and update existing entries --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 5cc844348..20e4e4eee 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -18,6 +18,7 @@ /DATA/testing/detectors/ZDC /MC @sawenzel @jackal1-66 +/GRID @sawenzel # PWG experts /MC/*/ALICE3 @njacazio From b4f03d472b1c6674f00f231d53f743699cae8109 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Fri, 31 Jul 2026 14:54:27 +0200 Subject: [PATCH 033/144] Pin reproducer CCDB queries to the original job's run time Reproducers resolved CCDB against the present, so any object re-uploaded since the job ran silently changed the outcome -- e.g. FT0/Calib/EventsPerBc, whose late arrival flips o2-steer-colcontexttool to the non-uniform sampler and gives a different collision context at the same fixed seed. Inject ALICEO2_CCDB_CONDITION_NOT_AFTER, which BasicCCDBManager picks up, so it covers the anchoring stage as well as the workflow tasks. Only pin when the moment is actually known: $2 if given, else "now" when run inside the job itself (the anchorMC.sh path). Otherwise skip and say so -- a wrong pin is worse than none. Skipped too if the JDL already sets it. Co-Authored-By: Claude Opus 5 --- GRID/utils/getReproducerScript.sh | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/GRID/utils/getReproducerScript.sh b/GRID/utils/getReproducerScript.sh index 6d3ab119b..d95204b38 100755 --- a/GRID/utils/getReproducerScript.sh +++ b/GRID/utils/getReproducerScript.sh @@ -1,6 +1,30 @@ #!/bin/bash ALIEN_PID=$1 +# Epoch-ms the generated reproducer will pin its CCDB queries to (see the +# INJECTION block below for why). This MUST be the moment the *original* job +# ran; "now" is only the same thing when this script runs inside that very job. +# +# $2 given -> use it verbatim (the escape hatch for +# regenerating an old job's reproducer) +# running inside the job -> now == the job's start time, correct +# otherwise -> unknown; do NOT guess, skip the pinning +# +# The last case matters: silently pinning to the wrong moment is worse than not +# pinning at all, because it looks authoritative while reconstructing a CCDB +# view that never existed for this job. +NOT_AFTER=$2 +if [ -z "${NOT_AFTER}" ]; then + if [ -n "${ALIEN_PROC_ID}" ] && [ "${ALIEN_PROC_ID}" = "${ALIEN_PID}" ]; then + NOT_AFTER=$(date +%s%3N) + else + echo "[getReproducerScript] Not running inside job ${ALIEN_PID} and no start" + echo "[getReproducerScript] time given, so CCDB will NOT be pinned: the" + echo "[getReproducerScript] reproducer will use present-day conditions." + echo "[getReproducerScript] Re-run as: $0 ${ALIEN_PID} " + fi +fi + if [ ${JALIEN_TOKEN_CERT} ]; then TOKENCERT=${JALIEN_TOKEN_CERT} TOKENKEY=${JALIEN_TOKEN_KEY} @@ -24,10 +48,33 @@ curl 'https://alimonitor.cern.ch/users/jobenv.jsp?pid='${ALIEN_PID} -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36' \ --insecure --cert ${TOKENCERT} --key ${TOKENKEY} -o ${SCRIPT} +# If the JDL already declares a CCDB time-machine timestamp, the job pinned +# itself and that value is restored further down by the MonaLisa env block (and +# consumed by anchorMC.sh). Injecting our own would fight it, and the JDL's is +# the authoritative one, so stand down. +CCDB_PIN_BLOCK="" +if grep -q "ALIEN_JDL_CCDB_CONDITION_NOT_AFTER" "${SCRIPT}"; then + echo "[getReproducerScript] JDL already sets CCDB_CONDITION_NOT_AFTER; keeping it." +elif [ -n "${NOT_AFTER}" ]; then + # Pin every CCDB query to the time the ORIGINAL job ran ("time machine"). + # Without this the reproducer resolves each object to whatever is newest in + # CCDB *today*, so any re-upload since the job ran silently changes the + # result -- including the collision context, which is otherwise fully + # deterministic given the fixed seed (cf. O2-7093). This reconstructs the + # job's CCDB view rather than sharing a declared one, because production jobs + # are themselves normally unpinned (o2dpg_sim_workflow.py defaults + # --condition-not-after to year 2077). + # Unset ALICEO2_CCDB_CONDITION_NOT_AFTER before running to get present-day + # conditions instead. + CCDB_PIN_BLOCK="export ALICEO2_CCDB_CONDITION_NOT_AFTER=\${ALICEO2_CCDB_CONDITION_NOT_AFTER:-${NOT_AFTER}}" + echo "[getReproducerScript] Pinning reproducer CCDB queries to ${NOT_AFTER}." +fi + # Define the Apptainer injection block which makes sure # that the job script is automatically executed in apptainer INJECTION=' export ALIEN_PID=#ALIEN_PID# +#CCDB_PIN_BLOCK# # Check if the script is running inside an Apptainer (Singularity) container if [ -z "$APPTAINER_NAME" ] && [ -z "$SINGULARITY_NAME" ]; then # Relaunch this script inside the container @@ -77,4 +124,9 @@ sed -i "/cd alien-job-$ALIEN_PID/d" "$SCRIPT" # replace the PID sed -i "s/#ALIEN_PID#/${ALIEN_PID}/g" "$SCRIPT" +# substitute the CCDB pinning placeholder (dropping the line entirely when we +# decided not to pin -- see above) +awk -v blk="$CCDB_PIN_BLOCK" '$0=="#CCDB_PIN_BLOCK#"{ if (blk!="") print blk; next } 1' \ + "$SCRIPT" > tmpfile && mv tmpfile "$SCRIPT" + chmod +x "${SCRIPT}" From 9e88429fed42cb3240749d5aab4b162f1f507fe6 Mon Sep 17 00:00:00 2001 From: alcaliva <32872606+alcaliva@users.noreply.github.com> Date: Fri, 31 Jul 2026 20:24:11 +0200 Subject: [PATCH 034/144] Fix seg fault caused by stale particle indices after coalescence from HF decays (#2416) --- .../external/generator/CoalescencePythia8.h | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/MC/config/common/external/generator/CoalescencePythia8.h b/MC/config/common/external/generator/CoalescencePythia8.h index 7e4abc53e..4daacfe64 100644 --- a/MC/config/common/external/generator/CoalescencePythia8.h +++ b/MC/config/common/external/generator/CoalescencePythia8.h @@ -107,6 +107,7 @@ bool doCoal(Pythia8::Event& event, int charge, int pdgCode, float mass, bool tri bool CoalescencePythia8(Pythia8::Event& event, std::vector inputPdgList = {}, bool trivialCoal = false, double coalMomentum = 0.4, int firstDauID = -1, int lastDauId = -1, float maxRapidity = 1.) { const double coalescenceRadius{0.5 * 1.122462 * coalMomentum}; + // if coalescence from a heavy hadron, loop only between firstDauID and lastDauID int loopStart = firstDauID > -1 ? firstDauID : 0; int loopEnd = lastDauId > -1 ? lastDauId : event.size() - 1; @@ -143,7 +144,7 @@ bool CoalescencePythia8(Pythia8::Event& event, std::vector inputPd lambdas[event[iPart].id() > 0].push_back(iPart); } } - // run coalescence + // Run coalescence bool nuclFromDecay = firstDauID > -1; bool coalHappened = false; @@ -152,26 +153,41 @@ bool CoalescencePythia8(Pythia8::Event& event, std::vector inputPd for (int iN{0}; iN < neutrons[iC].size(); ++iN) { if (nuclearMask & (1 << kDeuteron)) { coalHappened |= doCoal(event, iC, pdgList[kDeuteron], massList[kDeuteron], trivialCoal, coalescenceRadius, nuclFromDecay, protons[iC][iP], neutrons[iC][iN]); + if (nuclFromDecay && coalHappened) { + return true; + } } if (nuclearMask & (1 << kTriton)) { for (int iN2{iN + 1}; iN2 < neutrons[iC].size(); ++iN2) { coalHappened |= doCoal(event, iC, pdgList[kTriton], massList[kTriton], trivialCoal, coalescenceRadius, nuclFromDecay, protons[iC][iP], neutrons[iC][iN], neutrons[iC][iN2]); + if (nuclFromDecay && coalHappened) { + return true; + } } } if (nuclearMask & (1 << kHe3)) { for (int iP2{iP + 1}; iP2 < protons[iC].size(); ++iP2) { coalHappened |= doCoal(event, iC, pdgList[kHe3], massList[kHe3], trivialCoal, coalescenceRadius, nuclFromDecay, protons[iC][iP], protons[iC][iP2], neutrons[iC][iN]); + if (nuclFromDecay && coalHappened) { + return true; + } } } if (nuclearMask & (1 << kHyperTriton)) { for (int iL{0}; iL < lambdas[iC].size(); ++iL) { coalHappened |= doCoal(event, iC, pdgList[kHyperTriton], massList[kHyperTriton], trivialCoal, coalescenceRadius, nuclFromDecay, protons[iC][iP], neutrons[iC][iN], lambdas[iC][iL]); + if (nuclFromDecay && coalHappened) { + return true; + } } } if (nuclearMask & (1 << kHe4)) { for (int iP2{iP + 1}; iP2 < protons[iC].size(); ++iP2) { for (int iN2{iN + 1}; iN2 < neutrons[iC].size(); ++iN2) { coalHappened |= doCoal(event, iC, pdgList[kHe4], massList[kHe4], trivialCoal, coalescenceRadius, nuclFromDecay, protons[iC][iP], protons[iC][iP2], neutrons[iC][iN], neutrons[iC][iN2]); + if (nuclFromDecay && coalHappened) { + return true; + } } } } From a1f47da1328025dbd47950d21c79eae177c05f05 Mon Sep 17 00:00:00 2001 From: mj525 <99000728+mjkim525@users.noreply.github.com> Date: Sat, 1 Aug 2026 06:36:52 +0900 Subject: [PATCH 035/144] Fix LF resonance gap injection and add phi channel (#2419) --- .../generator/resonancelistgun_width_inj.json | 11 ++ .../pythia8/generator/resonances_width.cfg | 5 + .../generator_pythia8_LF_rapidity_width.C | 164 ++++++++++-------- 3 files changed, 104 insertions(+), 76 deletions(-) diff --git a/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json index f6ea92df8..80918f6a0 100644 --- a/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json +++ b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json @@ -10,6 +10,17 @@ "rapidityMax": 1.2, "genDecayed": true }, + "phi(1020)" : { + "pdg": 333, + "n": 10, + "ptMin": 0.0, + "ptMax": 20, + "etaMin": -1.2, + "etaMax": 1.2, + "rapidityMin": -1.2, + "rapidityMax": 1.2, + "genDecayed": true + }, "K1(1270)+": { "pdg": 10323, "n": 10, diff --git a/MC/config/PWGLF/pythia8/generator/resonances_width.cfg b/MC/config/PWGLF/pythia8/generator/resonances_width.cfg index 4845f45b8..1c5a80f0a 100644 --- a/MC/config/PWGLF/pythia8/generator/resonances_width.cfg +++ b/MC/config/PWGLF/pythia8/generator/resonances_width.cfg @@ -10,6 +10,11 @@ # 9010221:oneChannel = 1 1.000 0 211 -211 9010221:onIfMatch = 211 -211 +### phi(1020) +# 333:all = phi phi 3 0 0 1.0195 0.0042 1.0091 1.0599 0 +333:onMode = off +333:onIfMatch = 11 -11 + # ### Delta(1232)++ # 2224:all = DeltaPlusPlus DeltaPlusPlusbar 4 6 0 1.2320 0.1170 1.0778 2.4020 0 2224:onMode = off diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C b/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C index 6d71a8db5..7caf0fdd3 100644 --- a/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +++ b/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C @@ -191,31 +191,38 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 /// Destructor ~GeneratorPythia8LFRapidity() = default; + bool Init() override + { + // Follow the PWGHF gap-trigger convention and persist the event class in + // MCEventHeader::subGeneratorId. The AOD producer exposes this through + // mcCollision.getSubGeneratorId(). + addSubGenerator(kSubGeneratorMinimumBias, "Minimum bias"); + addSubGenerator(kSubGeneratorLFSignal, "LF signal"); + return o2::eventgen::GeneratorPythia8::Init(); + } + //__________________________________________________________________ Bool_t generateEvent() override { + // Follow the gap-trigger convention used by PWGHF/PWGDQ: a ratio N + // produces signal events at 0, N, 2N, ... and minimum-bias events in + // between. Ratios 0 and 1 both mean signal in every event. + mDoSignalThisEvent = isSignalEvent(); + notifySubGenerator(mDoSignalThisEvent ? kSubGeneratorLFSignal : kSubGeneratorMinimumBias); + if (!mUseTriggering) { // Injected mode: Embedding into MB // 1. Generate Background (MB) - // LOG(info) << "Generating background event " << mEventCounter; - bool lGenerationOK = false; while (!lGenerationOK) { lGenerationOK = pythiaObjectMinimumBias.next(); } copyMinimumBiasEventForInjection(); - // 2. Determine if we inject specific particles (Gap logic) - bool doInjection = true; - if (mGapBetweenInjection > 0) { - if (mGapBetweenInjection == 1 && mEventCounter % 2 == 0) { - doInjection = false; - } else if (mEventCounter % mGapBetweenInjection != 0) { - doInjection = false; - } - } - - if (!doInjection) { - LOG(info) << "Skipping injection for event " << mEventCounter; + // Keep the generated minimum-bias event on gap events. It must still be + // imported in importParticles(), otherwise o2-sim sees an empty event + // and retries until it obtains another injected event. + if (!mDoSignalThisEvent) { + LOG(info) << "Generating minimum-bias gap event " << mEventCounter; return true; } } @@ -230,7 +237,6 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 mConfigToUse = mOneInjectionPerEvent ? static_cast(gRandom->Uniform(0.f, getNGuns())) : -1; LOG(info) << "Using configuration " << mConfigToUse << " out of " << getNGuns() << ", of which " << mGunConfigs.size() << " are transport decayed and " << mGunConfigsGenDecayed.size() << " are generator decayed"; - bool injectedForThisEvent = false; int nConfig = mGunConfigs.size(); // We start counting from the configurations of the transport decayed particles for (const ConfigContainer& cfg : mGunConfigsGenDecayed) { nConfig++; @@ -239,10 +245,8 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 } LOG(info) << "Using config container "; cfg.print(); - if (mUseTriggering) { // Do the triggering - bool doSignal{mEventCounter % (mGapBetweenInjection + 1) == 0}; // Do signal or gap - - if (doSignal) { + if (mUseTriggering) { // Do the triggering + if (mDoSignalThisEvent) { LOG(info) << "Generating triggered signal event for particle"; cfg.print(); bool satisfiesTrigger = false; @@ -347,8 +351,6 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 mPythia.event.append(p); } - - injectedForThisEvent = true; } // For purely trivial injection (no generator decay needed in loop or just transport decay), we still might have injection flag @@ -369,66 +371,67 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 //__________________________________________________________________ Bool_t importParticles() override { - if (!mUseTriggering) { // If the triggering is used we handle the the gap when generating the signal - if (mGapBetweenInjection > 0) { - if (mGapBetweenInjection == 1 && mEventCounter % 2 == 0) { - LOG(info) << "Skipping importParticles event " << mEventCounter++; - return true; - } else if (mEventCounter % mGapBetweenInjection != 0) { - LOG(info) << "Skipping importParticles event " << mEventCounter++; - return true; - } - } - } - LOG(info) << "importParticles " << mEventCounter++; - GeneratorPythia8::importParticles(); - int nConfig = 0; - for (const ConfigContainer& cfg : mGunConfigs) { - nConfig++; - if (mConfigToUse >= 0 && (nConfig - 1) != mConfigToUse) { - continue; - } - LOGF(info, "Injecting %i particles with PDG %i, pT in [%f, %f], %s in [%f, %f]", cfg.mNInject, cfg.mPdg, cfg.mPtMin, cfg.mPtMax, (mUseRapidity ? "rapidity" : "eta"), cfg.mMin, cfg.mMax); + LOG(info) << "importParticles " << mEventCounter << " (" << (mDoSignalThisEvent ? "signal" : "minimum bias") << ")"; - for (int i{0}; i < cfg.mNInject; ++i) { - const double pt = gRandom->Uniform(cfg.mPtMin, cfg.mPtMax); - const double eta = gRandom->Uniform(cfg.mMin, cfg.mMax); - const double phi = gRandom->Uniform(0, TMath::TwoPi()); - const double px{pt * std::cos(phi)}; - const double py{pt * std::sin(phi)}; - const double mass = sampleMass(cfg); - double pz = 0; - double et = 0; + // Always import the Pythia event. On gap events this is the minimum-bias + // event prepared in generateEvent(). + if (!GeneratorPythia8::importParticles()) { + return false; + } - if (mUseRapidity) { - const double mT = std::sqrt(mass * mass + pt * pt); - pz = mT * std::sinh(eta); - et = mT * std::cosh(eta); - } else { - pz = pt * std::sinh(eta); - const double p = pt * std::cosh(eta); - et = std::sqrt(p * p + mass * mass); + // Generator-decayed particles were already merged into mPythia.event in + // generateEvent(). Append transport-decayed gun particles only on signal + // events in injection mode. + if (!mUseTriggering && mDoSignalThisEvent) { + int nConfig = 0; + for (const ConfigContainer& cfg : mGunConfigs) { + nConfig++; + if (mConfigToUse >= 0 && (nConfig - 1) != mConfigToUse) { + continue; } + LOGF(info, "Injecting %i particles with PDG %i, pT in [%f, %f], %s in [%f, %f]", cfg.mNInject, cfg.mPdg, cfg.mPtMin, cfg.mPtMax, (mUseRapidity ? "rapidity" : "eta"), cfg.mMin, cfg.mMax); + + for (int i{0}; i < cfg.mNInject; ++i) { + const double pt = gRandom->Uniform(cfg.mPtMin, cfg.mPtMax); + const double eta = gRandom->Uniform(cfg.mMin, cfg.mMax); + const double phi = gRandom->Uniform(0, TMath::TwoPi()); + const double px{pt * std::cos(phi)}; + const double py{pt * std::sin(phi)}; + const double mass = sampleMass(cfg); + double pz = 0; + double et = 0; + + if (mUseRapidity) { + const double mT = std::sqrt(mass * mass + pt * pt); + pz = mT * std::sinh(eta); + et = mT * std::cosh(eta); + } else { + pz = pt * std::sinh(eta); + const double p = pt * std::cosh(eta); + et = std::sqrt(p * p + mass * mass); + } - // TParticle::TParticle(Int_t pdg, - // Int_t status, - // Int_t mother1, Int_t mother2, - // Int_t daughter1, Int_t daughter2, - // Double_t px, Double_t py, Double_t pz, Double_t etot, - // Double_t vx, Double_t vy, Double_t vz, Double_t time) - - mParticles.push_back(TParticle(cfg.mPdg, - MCGenStatusEncoding(1, 1).fullEncoding, - -1, -1, - -1, -1, - px, py, pz, et, - 0., 0., 0., 0.)); - // make sure status code is encoded properly. Transport flag will be set by default and we have nothing - // to do since all pushed particles should be tracked. - o2::mcutils::MCGenHelper::encodeParticleStatusAndTracking(mParticles.back()); + // TParticle::TParticle(Int_t pdg, + // Int_t status, + // Int_t mother1, Int_t mother2, + // Int_t daughter1, Int_t daughter2, + // Double_t px, Double_t py, Double_t pz, Double_t etot, + // Double_t vx, Double_t vy, Double_t vz, Double_t time) + + mParticles.push_back(TParticle(cfg.mPdg, + MCGenStatusEncoding(1, 1).fullEncoding, + -1, -1, + -1, -1, + px, py, pz, et, + 0., 0., 0., 0.)); + // make sure status code is encoded properly. Transport flag will be set by default and we have nothing + // to do since all pushed particles should be tracked. + o2::mcutils::MCGenHelper::encodeParticleStatusAndTracking(mParticles.back()); + } } - nConfig++; } + + mEventCounter++; if (mVerbose) { LOG(info) << "Printing particles that are appended"; int n = 0; @@ -576,6 +579,14 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 void setVerbose(bool verbose = true) { mVerbose = verbose; } private: + static constexpr int kSubGeneratorMinimumBias = 0; + static constexpr int kSubGeneratorLFSignal = 1; + + bool isSignalEvent() const + { + return mGapBetweenInjection <= 1 || mEventCounter % mGapBetweenInjection == 0; + } + void copyMinimumBiasEventForInjection() { mPythia.event = pythiaObjectMinimumBias.event; @@ -631,12 +642,13 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 // Configuration const bool mOneInjectionPerEvent = true; // if true, only one injection per event is performed, i.e. if multiple PDG (including antiparticles) are requested to be injected only one will be done per event const bool mUseTriggering = false; // if true, use triggering instead of injection - const int mGapBetweenInjection = 0; // Gap between two signal events. 0 means injection at every event + const int mGapBetweenInjection = 0; // 0/1: signal every event; N>1: signal every Nth event const bool mUseRapidity = false; // if true, use rapidity instead of eta // Running variables int mConfigToUse = -1; // Index of the configuration to use int mEventCounter = 0; // Event counter + bool mDoSignalThisEvent = true; bool mVerbose = true; // Verbosity flag std::vector mGunConfigs; // List of gun configurations to use From a0289312a007ec8e28ea2509bb8f746de4e22ca9 Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Fri, 31 Jul 2026 22:47:04 +0100 Subject: [PATCH 036/144] Add configs to force decays of Xic for MC productions for triggered data (#2424) --- .../decayer/force_hadronic_Xic_trigger.cfg | 33 +++++++++ .../decayer/force_hadronic_charmbaryon.cfg | 26 ++++--- .../force_hadronic_charmbaryon_trigger.cfg | 72 +++++++++++++++++++ .../geant4_externaldecayer_Xic_trigger.in | 68 ++++++++++++++++++ 4 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg create mode 100644 MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg create mode 100644 MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in diff --git a/MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg b/MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg new file mode 100644 index 000000000..404f675bc --- /dev/null +++ b/MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg @@ -0,0 +1,33 @@ +# Decay configuration taken from MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_pp_ref.cfg +Init:showChangedParticleData = on +4132:tau0 = 0.0455 # Xic0 + +# switch off all decay channels +4232:onMode = off +4132:onMode = off + +## Xic decays +### Ξc+ -> Ξ- π+ π+ (70%) +4232:oneChannel = 1 0.35000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% +### Ξc+ -> Ξ*0 π+ (30%) +4232:addChannel = 1 0.12500 0 3324 211 + +### Ξc+ -> Ξ*0 π+, Ξ*0 -> Ξ- π+ +4232:onIfMatch = 3324 211 +### Ξc+ -> Ξ- π+ π+ +4232:onIfMatch = 3312 211 211 + +## Xic0 decays +### add Xic0 decays absent in PYTHIA8 decay table +4132:oneChannel = 1 0.7 0 3312 211 ### Xi_c()0 --> Xi- pi+ +4132:addChannel = 1 0.3 0 3334 321 ### Xi_c()0 --> Omega- K+ + +### Xic0 -> Xi- pi+ +4132:onIfMatch = 3312 211 +# Matching Exclusive Decay Channels: +# ------------------------------------------------------- +# Xi_c()0 --> Xi- pi+ 0.01432524810113947 +# -> PDG Codes: 3312, 211 +# Xi_c()0 --> Xi- pi+ pi+ pi- 0.04775082700379833 +# -> PDG Codes: 3312, 211, 211, -211 +4132:onIfMatch = 3334 321 diff --git a/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg index 9e5e79e45..a6128cb23 100644 --- a/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg +++ b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg @@ -9,17 +9,17 @@ Init:showChangedParticleData = on 4132:onMode = off ## Xic decays -### Ξc+ -> p K- π+ (35%) -4232:oneChannel = 1 0.17500 0 2212 -321 211 ### Ξc+ -> p K- π+ 6.18e-3 -4232:addChannel = 1 0.17500 0 2212 -313 ### Ξc+ -> p antiK*0(892) -### Ξc+ -> Ξ- π+ π+ (35%) (set the same as Ξc+ -> p K- π+) -4232:addChannel = 1 0.35000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% +### Ξc+ -> p K- π+ (30%) +4232:oneChannel = 1 0.1500 0 2212 -321 211 ### Ξc+ -> p K- π+ 6.18e-3 +4232:addChannel = 1 0.1500 0 2212 -313 ### Ξc+ -> p antiK*0(892) +### Ξc+ -> Ξ- π+ π+ (40%) (set the same as Ξc+ -> p K- π+) +4232:addChannel = 1 0.4000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% ### Ξc+ -> p φ (10%) 4232:addChannel = 1 0.10000 0 2212 333 ### Ξc+ -> p φ ### Ξc+ -> sigma+ π+ π- (10%) -4232:addChannel = 1 0.12500 0 3222 -211 211 ### Ξc+ -> sigma+ π- π+ 1.37% +4232:addChannel = 1 0.10000 0 3222 -211 211 ### Ξc+ -> sigma+ π- π+ 1.37% ### Ξc+ -> Ξ*0 π+ (10%) -4232:addChannel = 1 0.12500 0 3324 211 +4232:addChannel = 1 0.10000 0 3324 211 ### Ξc+ -> p K- π+ 4232:onIfMatch = 2212 321 211 @@ -36,7 +36,8 @@ Init:showChangedParticleData = on ## Xic0 decays ### add Xic0 decays absent in PYTHIA8 decay table -4132:oneChannel = 1 0.0143 0 3312 211 ### Xi_c()0 --> Xi- pi+ 0.01432524810113947 +4132:oneChannel = 1 0.7 0 3312 211 ### Xi_c()0 --> Xi- pi+ +4132:addChannel = 1 0.3 0 3334 321 ### Xi_c()0 --> Omega- K+ ### Xic0 -> Xi- pi+ 4132:onIfMatch = 3312 211 @@ -46,11 +47,13 @@ Init:showChangedParticleData = on # -> PDG Codes: 3312, 211 # Xi_c()0 --> Xi- pi+ pi+ pi- 0.04775082700379833 # -> PDG Codes: 3312, 211, 211, -211 +4132:onIfMatch = 3334 321 ## OmegaC decays ### add custom OmegaC decays absent in PYTHIA8 decay table -4332:oneChannel = 1 0.5 0 3334 211 -4332:addChannel = 1 0.5 0 3312 211 +4332:oneChannel = 1 0.8 0 3334 211 +4332:addChannel = 1 0.1 0 3312 211 +4332:addChannel = 1 0.1 0 3334 321 ### Omega_c -> Omega pi 4332:onIfMatch = 3334 211 @@ -72,6 +75,9 @@ Init:showChangedParticleData = on # Omega_c()0 --> Xi- K- 2 pi+ 0.625932203389831 # -> PDG Codes: 3312, -321, [2], 211 +### Omega_c -> Omega Kaon +4332:onIfMatch = 3334 321 + ## Allow the decay of resonances in the decay chain ### K*0(892) -> K- π+ 313:onMode = off diff --git a/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg new file mode 100644 index 000000000..5983a65f1 --- /dev/null +++ b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg @@ -0,0 +1,72 @@ +# Decay configuration taken from MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_pp_ref.cfg +Init:showChangedParticleData = on +4332:tau0 = 0.0803 # OmegaC +4132:tau0 = 0.0455 # Xic0 + +# switch off all decay channels +4232:onMode = off +4332:onMode = off +4132:onMode = off + +## Xic decays +### Ξc+ -> Ξ- π+ π+ (40%) (set the same as Ξc+ -> p K- π+) +4232:oneChannel = 1 0.8000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% +### Ξc+ -> Ξ*0 π+ (10%) +4232:addChannel = 1 0.20000 0 3324 211 + +### Ξc+ -> Ξ*0 π+, Ξ*0 -> Ξ- π+ +4232:onIfMatch = 3324 211 +### Ξc+ -> Ξ- π+ π+ +4232:onIfMatch = 3312 211 211 + +## Xic0 decays +### add Xic0 decays absent in PYTHIA8 decay table +4132:oneChannel = 1 0.7 0 3312 211 ### Xi_c()0 --> Xi- pi+ +4132:addChannel = 1 0.3 0 3334 321 ### Xi_c()0 --> Omega- K+ + +### Xic0 -> Xi- pi+ +4132:onIfMatch = 3312 211 +# Matching Exclusive Decay Channels: +# ------------------------------------------------------- +# Xi_c()0 --> Xi- pi+ 0.01432524810113947 +# -> PDG Codes: 3312, 211 +# Xi_c()0 --> Xi- pi+ pi+ pi- 0.04775082700379833 +# -> PDG Codes: 3312, 211, 211, -211 +4132:onIfMatch = 3334 321 + +## OmegaC decays +### add custom OmegaC decays absent in PYTHIA8 decay table +4332:oneChannel = 1 0.8 0 3334 211 +4332:addChannel = 1 0.1 0 3312 211 +4332:addChannel = 1 0.1 0 3334 321 + +### Omega_c -> Omega pi +4332:onIfMatch = 3334 211 +# Matching Exclusive Decay Channels: +# ------------------------------------------------------- +# Omega_c()0 --> Omega- pi+ None +# -> PDG Codes: 3334, 211 +# Omega_c()0 --> Omega- pi+ pi0 1.79939678284182 +# -> PDG Codes: 3334, 211, 111 +# Omega_c()0 --> Omega- pi- 2 pi+ 0.30954954954955 +# -> PDG Codes: 3334, -211, [2], 211 + +### Omega_c -> Xi pi +4332:onIfMatch = 3312 211 +# Matching Exclusive Decay Channels: +# ------------------------------------------------------- +# Omega_c()0 --> Xi- Kbar0 pi+ 2.12 +# -> PDG Codes: 3312, -311, 211 +# Omega_c()0 --> Xi- K- 2 pi+ 0.625932203389831 +# -> PDG Codes: 3312, -321, [2], 211 + +### Omega_c -> Omega Kaon +4332:onIfMatch = 3334 321 + +## Allow the decay of resonances in the decay chain +### and Omega_c -> pi Xi -> pi pi Lambda -> pi pi pi p +3312:onMode = off +3312:onIfAll = 3122 -211 +### for Omega_c -> pi Omega -> pi K Lambda -> pi K pi p +3334:onMode = off +3334:onIfAll = 3122 -321 diff --git a/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in b/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in new file mode 100644 index 000000000..0ecc07cee --- /dev/null +++ b/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in @@ -0,0 +1,68 @@ + +/control/verbose 2 +/mcVerbose/all 1 +/mcVerbose/geometryManager 1 +/mcVerbose/opGeometryManager 1 +/mcTracking/loopVerbose 1 +/mcVerbose/composedPhysicsList 2 +/mcVerbose/runAction 2 # For looping thresholds control +#/tracking/verbose 1 +#//control/cout/ignoreThreadsExcept 0 + +/mcPhysics/rangeCuts 0.001 mm +/mcRegions/setRangePrecision 5 +/mcTracking/skipNeutrino true +/mcDet/setIsMaxStepInLowDensityMaterials true +/mcDet/setMaxStepInLowDensityMaterials 10 m +/mcMagField/setConstDistance 1 mm +/mcDet/setIsZeroMagField true +/mcControl/useRootRandom true # couple G4 random seed to gRandom + +# optical + +/process/optical/verbose 0 +/process/optical/processActivation Scintillation 0 +/process/optical/processActivation OpWLS 0 +/process/optical/processActivation OpMieHG 0 +/process/optical/cerenkov/setTrackSecondariesFirst false +/mcMagField/stepperType NystromRK4 + +# PAI for TRD +# Geant4 VMC >= v3.2 +/mcPhysics/emModel/setEmModel PAI +/mcPhysics/emModel/setRegions TRD_Gas-mix +/mcPhysics/emModel/setParticles all +/mcPrimaryGenerator/skipUnknownParticles true # don't crash when seeing unknown ion etc. (issue warning) + +# +# Precise Msc for EMCAL +# +# Geant4 VMC >= v3.2 +/mcPhysics/emModel/setEmModel SpecialUrbanMsc +/mcPhysics/emModel/setRegions EMC_Lead$ EMC_Scintillator$ +/mcPhysics/emModel/setParticles e- e+ + +# combined transportation + Msc mode is currently broken for ALICE (Geant 10.2.0) +/process/em/transportationWithMsc Disabled + +# +# Adding extra lines for fixing tracking bias +# +/mcMagField/setDeltaIntersection 1.0e-05 mm +/mcMagField/setMinimumEpsilonStep 0.5e-05 +/mcMagField/setMaximumEpsilonStep 1.0e-05 +/mcMagField/printParameters + +# Change default parameters for killing looping particles +# +/mcPhysics/useHighLooperThresholds +/mcRun/setLooperThresholdImportantEnergy 100. MeV + +# Define media with the INCLXX physics list; here basically in all ITS media +#/mcVerbose/biasingConfigurationManager 3 +/mcPhysics/biasing/setModel inclxx +/mcPhysics/biasing/setRegions ITS_AIR$ ITS_WATER$ ITS_COPPER$ ITS_KAPTON(POLYCH2)$ ITS_GLUE_IBFPC$ ITS_CERAMIC$ ITS_K13D2U2k$ ITS_K13D2U120$ ITS_F6151B05M$ ITS_M60J3K$ ITS_M55J6K$ ITS_FGS003$ ITS_CarbonFleece$ ITS_PEEKCF30$ ITS_GLUE$ ITS_ALUMINUM$ ITS_INOX304$ ALPIDE_METALSTACK$ ALPIDE_SI$ +/mcPhysics/biasing/setParticles proton neutron pi+ pi- + +# external decayer +/mcPhysics/setExtDecayerSelection xi_c0 anti_xi_c0 xi_c+ xi_c- From ec32708eb9296699997d63ba09d0bfd6fc901917 Mon Sep 17 00:00:00 2001 From: Francesco Mazzaschi <43742195+fmazzasc@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:23:23 +0200 Subject: [PATCH 037/144] Add generator for Double-Omega searches (#2429) * Add double-omega generator * Add tests --------- Co-authored-by: Francesco Mazzaschi --- .../PWGLF/ini/GeneratorLFDoubleOmegaPbPb.ini | 6 +++ .../ini/tests/GeneratorLFDoubleOmegaPbPb.C | 53 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLFDoubleOmegaPbPb.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLFDoubleOmegaPbPb.C diff --git a/MC/config/PWGLF/ini/GeneratorLFDoubleOmegaPbPb.ini b/MC/config/PWGLF/ini/GeneratorLFDoubleOmegaPbPb.ini new file mode 100644 index 000000000..445dbf7c5 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFDoubleOmegaPbPb.ini @@ -0,0 +1,6 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_longlived_gaptriggered.C +funcName=generateLongLivedGapTriggered({1060020020}, 1, 20, 0.2, 10, -1, 1, true, false) + +[GeneratorPythia8] +config=${O2_ROOT}/share/Generators/egconfig/pythia8_hi.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLFDoubleOmegaPbPb.C b/MC/config/PWGLF/ini/tests/GeneratorLFDoubleOmegaPbPb.C new file mode 100644 index 000000000..4b9f6f898 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLFDoubleOmegaPbPb.C @@ -0,0 +1,53 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + std::vector possiblePDGs = {1060020020, -1060020020}; + + int nPossiblePDGs = possiblePDGs.size(); + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + std::vector injectedPDGs; + + auto nEvents = tree->GetEntries(); + for (int i = 0; i < nEvents; i++) + { + auto check = tree->GetEntry(i); + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) + { + auto track = tracks->at(idxMCTrack); + auto pdg = track.GetPdgCode(); + auto it = std::find(possiblePDGs.begin(), possiblePDGs.end(), pdg); + if (it != possiblePDGs.end() && track.isPrimary()) // found + { + injectedPDGs.push_back(pdg); + } + } + } + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + if(injectedPDGs.empty()){ + std::cerr << "No injected particles\n"; + return 1; // At least one of the injected particles should be generated + } + for (int i = 0; i < nPossiblePDGs; i++) + { + std::cout << "# Injected double omegas \n"; + std::cout << possiblePDGs[i] << ": " << std::count(injectedPDGs.begin(), injectedPDGs.end(), possiblePDGs[i]) << "\n"; + } + return 0; +} From 411377cc2f0538121f963bc431057f897ed7c0d4 Mon Sep 17 00:00:00 2001 From: Fabrizio Chinu <91954233+fchinu@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:38:21 +0200 Subject: [PATCH 038/144] Fix Xic- name used in geant4 configuration (#2431) --- .../PWGHF/pythia8/decayer/geant4_externaldecayer_charmbaryon.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_charmbaryon.in b/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_charmbaryon.in index 46b4def18..d82da4a38 100644 --- a/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_charmbaryon.in +++ b/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_charmbaryon.in @@ -65,4 +65,4 @@ /mcPhysics/biasing/setParticles proton neutron pi+ pi- # external decayer -/mcPhysics/setExtDecayerSelection omega_c0 anti_omega_c0 xi_c0 anti_xi_c0 xi_c+ xi_c- +/mcPhysics/setExtDecayerSelection omega_c0 anti_omega_c0 xi_c0 anti_xi_c0 xi_c+ anti_xi_c+ From faac260b713f35040d6a0fb31121017140d66f11 Mon Sep 17 00:00:00 2001 From: Fabrizio Chinu <91954233+fchinu@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:50:03 +0200 Subject: [PATCH 039/144] Fix Xic- name in geant4_externaldecayer_Xic_trigger.in (#2432) --- .../PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in b/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in index 0ecc07cee..3ba380d5f 100644 --- a/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in +++ b/MC/config/PWGHF/pythia8/decayer/geant4_externaldecayer_Xic_trigger.in @@ -65,4 +65,4 @@ /mcPhysics/biasing/setParticles proton neutron pi+ pi- # external decayer -/mcPhysics/setExtDecayerSelection xi_c0 anti_xi_c0 xi_c+ xi_c- +/mcPhysics/setExtDecayerSelection xi_c0 anti_xi_c0 xi_c+ anti_xi_c+ From e175a5a6ab28c540b3b2a394261e26cc9ede878e Mon Sep 17 00:00:00 2001 From: sawan <124118453+sawankumawat@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:06:28 +0530 Subject: [PATCH 040/144] [PWGLF] Added code for flat phase-space MC generator for PWA study (#2433) * Added code for flat phase-space MC generator for PWA study * Added ini and test file * Correct path for json file --- .../ini/generatorLF_Resonances_flat_mass.ini | 10 + .../tests/generatorLF_Resonances_flat_mass.C | 118 ++++ .../PWGLF/pythia8/generator/flatMCgun.json | 13 + .../pythia8/generator_pythia8_LF_flat_mass.C | 600 ++++++++++++++++++ 4 files changed, 741 insertions(+) create mode 100644 MC/config/PWGLF/ini/generatorLF_Resonances_flat_mass.ini create mode 100644 MC/config/PWGLF/ini/tests/generatorLF_Resonances_flat_mass.C create mode 100644 MC/config/PWGLF/pythia8/generator/flatMCgun.json create mode 100644 MC/config/PWGLF/pythia8/generator_pythia8_LF_flat_mass.C diff --git a/MC/config/PWGLF/ini/generatorLF_Resonances_flat_mass.ini b/MC/config/PWGLF/ini/generatorLF_Resonances_flat_mass.ini new file mode 100644 index 000000000..18d62cb10 --- /dev/null +++ b/MC/config/PWGLF/ini/generatorLF_Resonances_flat_mass.ini @@ -0,0 +1,10 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_flat_mass.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/flatMCgun.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # Background event config +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # Transport decayer config +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonances.cfg \ No newline at end of file diff --git a/MC/config/PWGLF/ini/tests/generatorLF_Resonances_flat_mass.C b/MC/config/PWGLF/ini/tests/generatorLF_Resonances_flat_mass.C new file mode 100644 index 000000000..7854e829a --- /dev/null +++ b/MC/config/PWGLF/ini/tests/generatorLF_Resonances_flat_mass.C @@ -0,0 +1,118 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + + //Kept only target single resonance (e.g. PDG 10331 -> K0S K0S) + std::vector injectedPDGs = {10331}; + std::vector> decayDaughters = {{310, 310}}; + + int numberOfInjectedSignalsPerEvent = 5; + int numberOfEventsProcessed = 0; + int numberOfEventsProcessedWithoutInjection = 0; + + auto nInjection = injectedPDGs.size(); + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + std::vector nSignal(nInjection, 0); + std::vector> nDecays; + std::vector nNotDecayed(nInjection, 0); + + for (size_t i = 0; i < nInjection; i++) + { + std::vector nDecay(decayDaughters[i].size(), 0); + nDecays.push_back(nDecay); + } + + auto nEvents = tree->GetEntries(); + bool hasInjection = false; + + for (int i = 0; i < nEvents; i++) + { + hasInjection = false; + numberOfEventsProcessed++; + tree->GetEntry(i); + + for (size_t idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) + { + auto track = tracks->at(idxMCTrack); + auto pdg = track.GetPdgCode(); + auto it = std::find(injectedPDGs.begin(), injectedPDGs.end(), pdg); + + if (it != injectedPDGs.end()) // Found injected mother particle + { + int index = std::distance(injectedPDGs.begin(), it); + nSignal[index]++; + + if (track.getFirstDaughterTrackId() < 0) + { + nNotDecayed[index]++; + continue; + } + + for (int j{track.getFirstDaughterTrackId()}; j <= track.getLastDaughterTrackId(); ++j) + { + auto pdgDau = tracks->at(j).GetPdgCode(); + bool foundDau = false; + + for (size_t idxDaughter = 0; idxDaughter < decayDaughters[index].size(); ++idxDaughter) + { + if (pdgDau == decayDaughters[index][idxDaughter]) + { + nDecays[index][idxDaughter]++; + foundDau = true; + hasInjection = true; + break; + } + } + if (!foundDau) + { + std::cerr << "Decay daughter not found: " << pdg << " -> " << pdgDau << "\n"; + } + } + } + } + if (!hasInjection) + { + numberOfEventsProcessedWithoutInjection++; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + for (size_t i = 0; i < nInjection; i++) + { + std::cout << "# Mother PDG: " << injectedPDGs[i] << "\n"; + std::cout << " Generated: " << nSignal[i] << ", " << nNotDecayed[i] << " did not decay\n"; + + if (nSignal[i] == 0) + { + std::cerr << "No generated signal found for PDG: " << injectedPDGs[i] << "\n"; + } + for (size_t j = 0; j < decayDaughters[i].size(); j++) + { + std::cout << " Daughter PDG " << decayDaughters[i][j] << ": " << nDecays[i][j] << "\n"; + } + } + std::cout << "--------------------------------\n"; + std::cout << "Number of events processed: " << numberOfEventsProcessed << "\n"; + std::cout << "Number of events without injection: " << numberOfEventsProcessedWithoutInjection << "\n"; + + return 0; +} + +void generatorLF_Resonances_flat_mass() { External(); } \ No newline at end of file diff --git a/MC/config/PWGLF/pythia8/generator/flatMCgun.json b/MC/config/PWGLF/pythia8/generator/flatMCgun.json new file mode 100644 index 000000000..4e0d12d2c --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/flatMCgun.json @@ -0,0 +1,13 @@ +{ + "f0(1710)" : { + "pdg": 10331, + "n": 100, + "ptMin": 0.0, + "ptMax": 30, + "rapidityMin": -1.0, + "rapidityMax": 1.0, + "massMin": 1.0, + "massMax": 3.0, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_LF_flat_mass.C b/MC/config/PWGLF/pythia8/generator_pythia8_LF_flat_mass.C new file mode 100644 index 000000000..38f144144 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator_pythia8_LF_flat_mass.C @@ -0,0 +1,600 @@ +/// \file generator_pythia8_LF_flat_mass.C +/// \author Sawan sawan.sawan@cern.ch +/// \brief Modified LF Gun Generator for Flat Mass PHSP PWA Acceptance/Efficiency MC. +/// Adapted from generator_pythia8_LF_rapidity_width.C + +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "FairGenerator.h" + +#include "TDatabasePDG.h" +#include "TMath.h" +#include "TParticlePDG.h" +#include "TRandom3.h" + +#include "Generators/GeneratorPythia8.h" +#include "Pythia8/Pythia.h" +#if __has_include("SimulationDataFormat/MCGenStatus.h") +#include "SimulationDataFormat/MCGenStatus.h" +#else +#include "SimulationDataFormat/MCGenProperties.h" +#endif +#if __has_include("SimulationDataFormat/MCUtils.h") +#include "SimulationDataFormat/MCUtils.h" +#endif +#include "TSystem.h" + +#include "Generators/GeneratorPythia8Param.h" +#include "fairlogger/Logger.h" + +#include +#include +#endif +// DecayerPythia8Param needs to be included after the #endif to work with Cling +#include "Generators/DecayerPythia8Param.h" +#if defined(__CLING__) && !defined(__ROOTCLING__) +#if __has_include("SimulationDataFormat/MCGenStatus.h") +#include "SimulationDataFormat/MCGenStatus.h" +#elif __has_include("SimulationDataFormat/MCGenProperties.h") +#include "SimulationDataFormat/MCGenProperties.h" +#endif +#if __has_include("SimulationDataFormat/MCUtils.h") +#include "SimulationDataFormat/MCUtils.h" +#endif +#pragma cling load("libO2Generators") +#endif +#include "generator_pythia8_longlived.C" + +#include + +using namespace Pythia8; +using namespace o2::mcgenstatus; + +class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 +{ +public: + /// Parametric constructor + GeneratorPythia8LFRapidity(bool injOnePerEvent = true, + int gapBetweenInjection = 0, + bool useTrigger = false, + bool useRapidity = false, + std::string pythiaCfgMb = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", + std::string pythiaCfgSignal = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg") : GeneratorPythia8{}, + mOneInjectionPerEvent{injOnePerEvent}, + mGapBetweenInjection{gapBetweenInjection}, + mUseTriggering{useTrigger}, + mUseRapidity{useRapidity} + { + LOG(info) << "GeneratorPythia8LFRapidity constructor (Flat Mass PHSP mode)"; + LOG(info) << "++ mOneInjectionPerEvent: " << mOneInjectionPerEvent; + LOG(info) << "++ mGapBetweenInjection: " << mGapBetweenInjection; + LOG(info) << "++ mUseTriggering: " << mUseTriggering; + LOG(info) << "++ mUseRapidity: " << mUseRapidity; + LOG(info) << "++ pythiaCfgMb: " << pythiaCfgMb; + LOG(info) << "++ pythiaCfgSignal: " << pythiaCfgSignal; + gRandom->SetSeed(0); + + // Register PDG 10331 f0(1710) in ROOT TDatabasePDG if missing to prevent Geant4 / TDatabase crashes + if (!TDatabasePDG::Instance()->GetParticle(10331)) + { + TDatabasePDG::Instance()->AddParticle("f0_1710", "f0_1710", 1.710, true, 0.150, 0, "meson", 10331); + } + + if (useTrigger) + { + mPythia.readString("ProcessLevel:all off"); + if (pythiaCfgMb == "") + { + auto ¶m = o2::eventgen::GeneratorPythia8Param::Instance(); + pythiaCfgMb = param.config; + } + if (pythiaCfgSignal == "") + { + auto ¶m = o2::eventgen::GeneratorPythia8Param::Instance(); + pythiaCfgSignal = param.config; + } + pythiaCfgMb = gSystem->ExpandPathName(pythiaCfgMb.c_str()); + pythiaCfgSignal = gSystem->ExpandPathName(pythiaCfgSignal.c_str()); + if (!pythiaObjectMinimumBias.readFile(pythiaCfgMb)) + { + LOG(fatal) << "Could not pythiaObjectMinimumBias.readFile(\"" << pythiaCfgMb << "\")"; + } + pythiaObjectMinimumBias.readString("Random:setSeed = on"); + pythiaObjectMinimumBias.readString("Random:seed =" + std::to_string(gRandom->Integer(900000000 - 2) + 1)); + + if (!pythiaObjectSignal.readFile(pythiaCfgSignal)) + { + LOG(fatal) << "Could not pythiaObjectSignal.readFile(\"" << pythiaCfgSignal << "\")"; + } + pythiaObjectSignal.readString("Random:setSeed = on"); + pythiaObjectSignal.readString("Random:seed =" + std::to_string(gRandom->Integer(900000000 - 2) + 1)); + pythiaObjectMinimumBias.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + mPythiaGun.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + if (!pythiaObjectMinimumBias.init()) + { + LOG(fatal) << "Could not pythiaObjectMinimumBias.init() from " << pythiaCfgMb; + } + if (!pythiaObjectSignal.init()) + { + LOG(fatal) << "Could not pythiaObjectSignal.init() from " << pythiaCfgSignal; + } + } + else + { + if (pythiaCfgMb == "") + { + auto ¶m = o2::eventgen::GeneratorPythia8Param::Instance(); + pythiaCfgMb = param.config; + } + if (pythiaCfgMb == "") + { + pythiaCfgMb = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg"; + } + pythiaCfgMb = gSystem->ExpandPathName(pythiaCfgMb.c_str()); + if (!pythiaObjectMinimumBias.readFile(pythiaCfgMb)) + { + LOG(fatal) << "Could not pythiaObjectMinimumBias.readFile(\"" << pythiaCfgMb << "\")"; + } + pythiaObjectMinimumBias.readString("Random:setSeed = on"); + pythiaObjectMinimumBias.readString("Random:seed =" + std::to_string(gRandom->Integer(900000000 - 2) + 1)); + + pythiaObjectMinimumBias.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + mPythiaGun.particleData.addParticle(1000100200, "20Ne", 6, 30, 0, 19.992440); + if (!pythiaObjectMinimumBias.init()) + { + LOG(fatal) << "Could not pythiaObjectMinimumBias.init() from " << pythiaCfgMb; + } + + mPythiaGun.readString("ProcessLevel:all off"); + + auto ¶m = o2::eventgen::DecayerPythia8Param::Instance(); + for (int i = 0; i < 8; ++i) + { + if (param.config[i].empty()) + { + continue; + } + std::string config = gSystem->ExpandPathName(param.config[i].c_str()); + readParticleDataLines(mPythia, config, "import Pythia"); + if (!mPythiaGun.readFile(config, true)) + { + LOG(fatal) << "Failed to init \'DecayerPythia8\': problems with configuration file " << config; + return; + } + } + + if (param.showChanged) + { + mPythiaGun.readString(std::string("Init:showChangedParticleData on")); + } + else + { + mPythiaGun.readString(std::string("Init:showChangedParticleData off")); + } + + if (!mPythiaGun.init()) + { + LOG(fatal) << "Failed to init \'DecayerPythia8\': init returned with error"; + return; + } + } + } + + ~GeneratorPythia8LFRapidity() = default; + + bool Init() override + { + addSubGenerator(kSubGeneratorMinimumBias, "Minimum bias"); + addSubGenerator(kSubGeneratorLFSignal, "LF signal"); + return o2::eventgen::GeneratorPythia8::Init(); + } + + Bool_t generateEvent() override + { + mDoSignalThisEvent = isSignalEvent(); + notifySubGenerator(mDoSignalThisEvent ? kSubGeneratorLFSignal : kSubGeneratorMinimumBias); + + if (!mUseTriggering) + { + bool lGenerationOK = false; + while (!lGenerationOK) + { + lGenerationOK = pythiaObjectMinimumBias.next(); + } + copyMinimumBiasEventForInjection(); + + if (!mDoSignalThisEvent) + { + return true; + } + } + + if (mUseTriggering) + { + mPythia.event.reset(); + } + + mConfigToUse = mOneInjectionPerEvent ? static_cast(gRandom->Uniform(0.f, getNGuns())) : -1; + + int nConfig = mGunConfigs.size(); + for (const ConfigContainer &cfg : mGunConfigsGenDecayed) + { + nConfig++; + if (mConfigToUse >= 0 && (nConfig - 1) != mConfigToUse) + { + continue; + } + + if (mUseTriggering) + { + if (mDoSignalThisEvent) + { + bool satisfiesTrigger = false; + int nTries = 0; + while (!satisfiesTrigger) + { + if (!pythiaObjectSignal.next()) + { + continue; + } + for (int j = 0; j < pythiaObjectSignal.event.size(); j++) + { + const int &pypid = pythiaObjectSignal.event[j].id(); + const float &pyeta = mUseRapidity ? pythiaObjectSignal.event[j].y() : pythiaObjectSignal.event[j].eta(); + const float &pypt = pythiaObjectSignal.event[j].pT(); + if (pypid == cfg.mPdg && cfg.mMin < pyeta && pyeta < cfg.mMax && pypt > cfg.mPtMin && pypt < cfg.mPtMax) + { + satisfiesTrigger = true; + break; + } + } + nTries++; + } + mPythia.event = pythiaObjectSignal.event; + } + else + { + bool lGenerationOK = false; + while (!lGenerationOK) + { + lGenerationOK = pythiaObjectMinimumBias.next(); + } + mPythia.event = pythiaObjectMinimumBias.event; + } + continue; + } + + mPythiaGun.event.reset(); + for (int i{0}; i < cfg.mNInject; ++i) + { + const double pt = gRandom->Uniform(cfg.mPtMin, cfg.mPtMax); + const double eta = gRandom->Uniform(cfg.mMin, cfg.mMax); + const double phi = gRandom->Uniform(0, TMath::TwoPi()); + const double px{pt * std::cos(phi)}; + const double py{pt * std::sin(phi)}; + const double mass = sampleMass(cfg); // Uses Flat Mass Sampling + + double pz = 0; + double et = 0; + + if (mUseRapidity) + { + const double mT = std::sqrt(mass * mass + pt * pt); + pz = mT * std::sinh(eta); + et = mT * std::cosh(eta); + } + else + { + pz = pt * std::sinh(eta); + const double p = pt * std::cosh(eta); + et = std::sqrt(p * p + mass * mass); + } + + Particle particle; + particle.id(cfg.mPdg); + particle.status(11); + particle.m(mass); + particle.px(px); + particle.py(py); + particle.pz(pz); + particle.e(et); + particle.xProd(0.f); + particle.yProd(0.f); + particle.zProd(0.f); + particle.tProd(0.f); + mPythiaGun.particleData.mayDecay(cfg.mPdg, true); + mPythiaGun.event.append(particle); + } + + mPythiaGun.moreDecays(); + mPythiaGun.next(); + + int offset = mPythia.event.size(); + for (int i = 1; i < mPythiaGun.event.size(); ++i) + { + Particle &p = mPythiaGun.event[i]; + int mother1 = p.mother1() > 0 ? p.mother1() + offset - 1 : p.mother1(); + int mother2 = p.mother2() > 0 ? p.mother2() + offset - 1 : p.mother2(); + int daughter1 = p.daughter1() > 0 ? p.daughter1() + offset - 1 : p.daughter1(); + int daughter2 = p.daughter2() > 0 ? p.daughter2() + offset - 1 : p.daughter2(); + + p.mothers(mother1, mother2); + p.daughters(daughter1, daughter2); + + mPythia.event.append(p); + } + } + return true; + } + + Bool_t importParticles() override + { + if (!GeneratorPythia8::importParticles()) + { + return false; + } + + if (!mUseTriggering && mDoSignalThisEvent) + { + int nConfig = 0; + for (const ConfigContainer &cfg : mGunConfigs) + { + nConfig++; + if (mConfigToUse >= 0 && (nConfig - 1) != mConfigToUse) + { + continue; + } + + for (int i{0}; i < cfg.mNInject; ++i) + { + const double pt = gRandom->Uniform(cfg.mPtMin, cfg.mPtMax); + const double eta = gRandom->Uniform(cfg.mMin, cfg.mMax); + const double phi = gRandom->Uniform(0, TMath::TwoPi()); + const double px{pt * std::cos(phi)}; + const double py{pt * std::sin(phi)}; + const double mass = sampleMass(cfg); + double pz = 0; + double et = 0; + + if (mUseRapidity) + { + const double mT = std::sqrt(mass * mass + pt * pt); + pz = mT * std::sinh(eta); + et = mT * std::cosh(eta); + } + else + { + pz = pt * std::sinh(eta); + const double p = pt * std::cosh(eta); + et = std::sqrt(p * p + mass * mass); + } + + mParticles.push_back(TParticle(cfg.mPdg, + MCGenStatusEncoding(1, 1).fullEncoding, + -1, -1, + -1, -1, + px, py, pz, et, + 0., 0., 0., 0.)); + o2::mcutils::MCGenHelper::encodeParticleStatusAndTracking(mParticles.back()); + } + } + } + + mEventCounter++; + return true; + } + + struct ConfigContainer + { + // 1. Set default constructor arguments to 1.0 and 3.0 + ConfigContainer(int input_pdg = 10331, int n = 1, + float ptMin = 0.0, float ptMax = 30.0, + float min = -1.0, float max = 1.0, + double massMin = 1.0, double massMax = 3.0) : mPdg{input_pdg}, + mNInject{n}, + mPtMin{ptMin}, + mPtMax{ptMax}, + mMin{min}, + mMax{max}, + mMassMin{massMin}, + mMassMax{massMax} + { + mMass = GeneratorPythia8LongLivedGun::getMass(mPdg); + if (mMass <= 0) + { + mMass = 1.710; + } + LOGF(info, "ConfigContainer: mPdg = %i, mNInject = %i, mPtMin = %f, mPtMax = %f, mMin = %f, mMax = %f, mMassMin = %f, mMassMax = %f", + mPdg, mNInject, mPtMin, mPtMax, mMin, mMax, mMassMin, mMassMax); + }; + + ConfigContainer(TObjArray *arr) : ConfigContainer( + arr->GetEntries() > 0 ? atoi(arr->At(0)->GetName()) : 10331, + arr->GetEntries() > 1 ? atoi(arr->At(1)->GetName()) : 1, + arr->GetEntries() > 2 ? atof(arr->At(2)->GetName()) : 0.0, + arr->GetEntries() > 3 ? atof(arr->At(3)->GetName()) : 30.0, + arr->GetEntries() > 4 ? atof(arr->At(4)->GetName()) : -1.0, + arr->GetEntries() > 5 ? atof(arr->At(5)->GetName()) : 1.0, + arr->GetEntries() > 6 ? atof(arr->At(6)->GetName()) : 1.0, + arr->GetEntries() > 7 ? atof(arr->At(7)->GetName()) : 3.0) {}; + + ConfigContainer(TString line) : ConfigContainer(line.Tokenize(" ")) {}; + + // 2. Set jsonParams.value fallback defaults to 1.0 and 3.0 + ConfigContainer(const nlohmann::json &jsonParams, bool useRapidity = false) + : ConfigContainer(jsonParams.value("pdg", 10331), + jsonParams.value("n", 1), + jsonParams.value("ptMin", 0.0f), + jsonParams.value("ptMax", 30.0f), + (useRapidity && jsonParams.contains("rapidityMin")) ? jsonParams["rapidityMin"].get() : (jsonParams.contains("min") ? jsonParams["min"].get() : jsonParams.value("etaMin", -1.0f)), + (useRapidity && jsonParams.contains("rapidityMax")) ? jsonParams["rapidityMax"].get() : (jsonParams.contains("max") ? jsonParams["max"].get() : jsonParams.value("etaMax", 1.0f)), + jsonParams.value("massMin", 1.0), + jsonParams.value("massMax", 3.0)) {}; + + const int mPdg = 10331; + const int mNInject = 1; + const float mPtMin = 0.0; + const float mPtMax = 30.0; + const float mMin = -1.f; + const float mMax = 1.f; + double mMass = 0.f; + double mMassMin = 1.0; + double mMassMax = 3.0; + + void print() const + { + LOGF(info, "int mPdg = %i, mNInject = %i, mPtMin = %f, mPtMax = %f, mMin = %f, mMax = %f, mMassMin = %f, mMassMax = %f", + mPdg, mNInject, mPtMin, mPtMax, mMin, mMax, mMassMin, mMassMax); + } + }; + + // FLAT MASS SAMPLING LOGIC + // FLAT MASS SAMPLING LOGIC + double sampleMass(const ConfigContainer &cfg) + { + if (cfg.mMassMax > cfg.mMassMin && cfg.mMassMin > 0.0) + { + return gRandom->Uniform(cfg.mMassMin, cfg.mMassMax); + } + // Fallback if range is invalid or missing: flat uniform in [1.0, 3.0] GeV + return gRandom->Uniform(1.0, 3.0); + } + + ConfigContainer addGun(int input_pdg, int nInject = 1, float ptMin = 0, float ptMax = 30, float min = -1, float max = 1, double massMin = -1.0, double massMax = -1.0) + { + if (mUseTriggering) + { + return addGunGenDecayed(input_pdg, nInject, ptMin, ptMax, min, max, massMin, massMax); + } + ConfigContainer cfg{input_pdg, nInject, ptMin, ptMax, min, max, massMin, massMax}; + mGunConfigs.push_back(cfg); + return cfg; + } + + ConfigContainer addGun(ConfigContainer cfg) { return addGun(cfg.mPdg, cfg.mNInject, cfg.mPtMin, cfg.mPtMax, cfg.mMin, cfg.mMax, cfg.mMassMin, cfg.mMassMax); } + + ConfigContainer addGunGenDecayed(int input_pdg, int nInject = 1, float ptMin = 0, float ptMax = 30, float min = -1, float max = 1, double massMin = -1.0, double massMax = -1.0) + { + ConfigContainer cfg{input_pdg, nInject, ptMin, ptMax, min, max, massMin, massMax}; + mGunConfigsGenDecayed.push_back(cfg); + return cfg; + } + + ConfigContainer addGunGenDecayed(ConfigContainer cfg) { return addGunGenDecayed(cfg.mPdg, cfg.mNInject, cfg.mPtMin, cfg.mPtMax, cfg.mMin, cfg.mMax, cfg.mMassMin, cfg.mMassMax); } + + long int getNGuns() const { return mGunConfigs.size() + mGunConfigsGenDecayed.size(); } + +private: + static constexpr int kSubGeneratorMinimumBias = 0; + static constexpr int kSubGeneratorLFSignal = 1; + + bool isSignalEvent() const { return mGapBetweenInjection <= 1 || mEventCounter % mGapBetweenInjection == 0; } + + void copyMinimumBiasEventForInjection() + { + mPythia.event = pythiaObjectMinimumBias.event; + mPythia.event.init("Minimum-bias event with injected particles", &mPythia.particleData); + mPythia.event.restorePtrs(); + } + + static bool isParticleDataLine(const std::string &line) + { + size_t pos = line.find_first_not_of(" \t"); + if (pos == std::string::npos) + return false; + if (line[pos] == '+' || line[pos] == '-') + ++pos; + if (pos >= line.size() || line[pos] < '0' || line[pos] > '9') + return false; + while (pos < line.size() && line[pos] >= '0' && line[pos] <= '9') + ++pos; + return pos < line.size() && line[pos] == ':'; + } + + static bool readParticleDataLines(Pythia8::Pythia &pythia, const std::string &config, const char *target) + { + std::ifstream input(config.c_str(), ios::in); + if (!input.is_open()) + return false; + std::string line; + while (std::getline(input, line)) + { + TString stripped = line; + stripped.Strip(TString::kBoth, ' '); + if (stripped.IsNull() || stripped.IsWhitespace() || stripped.BeginsWith("#")) + continue; + std::string command = stripped.Data(); + if (!isParticleDataLine(command)) + continue; + if (!pythia.readString(command)) + return false; + } + return true; + } + + const bool mOneInjectionPerEvent = true; + const bool mUseTriggering = false; + const int mGapBetweenInjection = 0; + const bool mUseRapidity = false; + + int mConfigToUse = -1; + int mEventCounter = 0; + bool mDoSignalThisEvent = true; + + std::vector mGunConfigs; + std::vector mGunConfigsGenDecayed; + Pythia8::Pythia pythiaObjectSignal; + Pythia8::Pythia pythiaObjectMinimumBias; + Pythia8::Pythia mPythiaGun; +}; + +FairGenerator *generateLFRapidity(std::string configuration = "", + bool injectOnePDGPerEvent = true, + int gapBetweenInjection = 0, + bool useTrigger = false, + bool useRapidity = false, + std::string pythiaCfgMb = "", + std::string pythiaCfgSignal = "") +{ + std::vector cfgVec; + std::vector cfgVecGenDecayed; + + if (!configuration.empty()) + { + configuration = gSystem->ExpandPathName(configuration.c_str()); + std::ifstream inputFile(configuration.c_str(), ios::in); + if (inputFile.is_open() && TString(configuration.c_str()).EndsWith(".json")) + { + nlohmann::json paramfile = nlohmann::json::parse(inputFile); + for (const auto ¶m : paramfile) + { + if (param.value("genDecayed", true)) + { + cfgVecGenDecayed.push_back(GeneratorPythia8LFRapidity::ConfigContainer{param, useRapidity}); + } + else + { + cfgVec.push_back(GeneratorPythia8LFRapidity::ConfigContainer{param, useRapidity}); + } + } + } + } + + // DEFAULT FALLBACK: If no configuration file passed or empty, default to PDG 10331 Flat Mass PHSP + if (cfgVec.empty() && cfgVecGenDecayed.empty()) + { + LOG(info) << "No particle list provided or empty. Falling back to default PDG 10331 (f0(1710)) Flat Mass setup."; + GeneratorPythia8LFRapidity::ConfigContainer defaultCfg(10331, 1, 0.0, 30.0, -1.0, 1.0, 1.0, 3.0); + cfgVecGenDecayed.push_back(defaultCfg); + } + + GeneratorPythia8LFRapidity *multiGun = new GeneratorPythia8LFRapidity(injectOnePDGPerEvent, gapBetweenInjection, useTrigger, useRapidity, pythiaCfgMb, pythiaCfgSignal); + for (const auto &c : cfgVec) + { + multiGun->addGun(c); + } + for (const auto &c : cfgVecGenDecayed) + { + multiGun->addGunGenDecayed(c); + } + return multiGun; +} \ No newline at end of file From 5da6f9912d67a2907b8faf40ae106ce585317e66 Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Mon, 17 Aug 2026 15:51:48 +0200 Subject: [PATCH 041/144] o2_dpg_workflow_runner.py: fix dry-run crash --- MC/bin/o2_dpg_workflow_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MC/bin/o2_dpg_workflow_runner.py b/MC/bin/o2_dpg_workflow_runner.py index 229125c3c..eebde6501 100755 --- a/MC/bin/o2_dpg_workflow_runner.py +++ b/MC/bin/o2_dpg_workflow_runner.py @@ -1244,7 +1244,7 @@ def submit(self, tid, nice): self.procstatus[tid]='Running' if args.dry_run: drycommand="echo \' " + str(self.scheduling_iteration) + " : would do " + str(self.workflowspec['stages'][tid]['name']) + "\'" - return subprocess.Popen(['/bin/bash','-c',drycommand], cwd=workdir) + return psutil.Popen(['/bin/bash','-c',drycommand], cwd=workdir) taskenv = os.environ.copy() # apply specific (non-default) software version, if any From 0cc4061e2b0ee5c85c3f237b6dc80b5a89a9aabf Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 05:20:06 +0200 Subject: [PATCH 042/144] Prepare o2dpg_sim_metrics.py for the new workflow runner This is the first step of a refactoring of MC/bin/o2_dpg_workflow_runner.py into a modular package, presented at CHEP 2026. It updates the metrics tool to read what that runner writes, and fixes two numbers it was already getting wrong. - cgroup_cpu and cgroup_mem are read when the runner ran under --systemd-run, and slice-level CPU is preferred over the sum of per-task psutil readings. - __cgroup_global__ and __global_init_task__ rows are internal markers and no longer enter the per-task statistics. - lifetime was an iteration count and is now seconds, which is what the new schedule simulator reads it as. update_resource_estimates() only reads pss.max and cpu.mean, so the current runner is unaffected. - json-stat reads the *.log_time files written by GNU time, and falls back to user time where a task was too short for its 10 ms wall resolution. - The runtime estimate assumed every monitor iteration takes 5 seconds. On a 12.2 s run it reported 65 s. Walltime now comes from the log timestamps, which gives 10.9 s for the same run. - min_lifetime was computed with .max(). It now uses .min(), and a std is reported alongside. https://indico.cern.ch/event/1471803/contributions/6967072/ Co-Authored-By: Claude Opus 5 --- MC/utils/o2dpg_sim_metrics.py | 463 +++++++++++++++++++++++++++++----- 1 file changed, 405 insertions(+), 58 deletions(-) diff --git a/MC/utils/o2dpg_sim_metrics.py b/MC/utils/o2dpg_sim_metrics.py index cb90fec09..c9367b7be 100755 --- a/MC/utils/o2dpg_sim_metrics.py +++ b/MC/utils/o2dpg_sim_metrics.py @@ -3,6 +3,7 @@ import sys from os.path import join, exists, basename from os import makedirs +import os from copy import deepcopy import argparse import re @@ -63,9 +64,16 @@ METRIC_NAME_USS = "uss" METRIC_NAME_PSS = "pss" METRIC_NAME_TIME = "time" +METRIC_NAME_CGROUP_CPU = "cgroup_cpu" +METRIC_NAME_CGROUP_MEM = "cgroup_mem" # metrics that are extracted by the o2_dpg_workflow_runner and put in pipeline_metric*.log METRICS = [METRIC_NAME_CPU, METRIC_NAME_USS, METRIC_NAME_PSS, METRIC_NAME_TIME] +# additional cgroup-based metrics produced when runner uses --systemd-run +CGROUP_METRICS = [METRIC_NAME_CGROUP_CPU, METRIC_NAME_CGROUP_MEM] + +# task names matching this pattern are internal runner markers, not real tasks +_INTERNAL_NAME_PREFIX = "__" # some features of MC runs, these can be extracted from the meta information # use these when multiple pipelines are given and we want to extract comparison plots for those based on these features @@ -149,6 +157,12 @@ def __init__(self, pipeline_path=None): self.name = None # use this as an id in the dataframe later self.timestamp = int(time_ns() / 1000) + # actual elapsed walltime derived from log timestamps (more accurate than + # iter_count × monitor_interval, which ignores per-pass overhead) + self.wall_time_s = None + # cgroup global entries (iter → {cpu_pct, mem_mb}); populated when the + # metric log contains __cgroup_global__ rows from the systemd-run backend + self.cgroup_global_rows = [] if pipeline_path: self.extract_from_pipeline(pipeline_path) @@ -202,15 +216,19 @@ def convert_columns_to_float_if_possible(self): def clean_cpu(self): """ - Sometimes we have negative CPU values, set them to 0 + Sometimes we have negative CPU values, set them to 0. + Also normalise cgroup_cpu by the same factor (100) so it is in #cores. """ - if METRIC_NAME_CPU not in self.dict_for_df: - return - - cpu_list = self.dict_for_df[METRIC_NAME_CPU] - for i, value in enumerate(cpu_list): - # if negative, set to 0; in addition, divide by 100 since we are counting number of CPUs while psutil is doing in % - cpu_list[i] = max(0, value) / 100 + for col in (METRIC_NAME_CPU, METRIC_NAME_CGROUP_CPU): + cpu_list = self.dict_for_df.get(col) + if cpu_list is None: + continue + for i, value in enumerate(cpu_list): + # if negative, set to 0; divide by 100 to convert % → #cores + if value is None: + cpu_list[i] = None + else: + cpu_list[i] = max(0, value) / 100 def compute_time_delta(self): """ @@ -256,6 +274,10 @@ def add_iteration(self, iteration): and derive the timeframe and parent category as well """ + # Skip internal runner markers (e.g. __cgroup_global__, __global_init_task__) + if str(iteration.get("name", "")).startswith(_INTERNAL_NAME_PREFIX): + return + for key, value in iteration.items(): if key == "name": try: @@ -294,8 +316,16 @@ def extract_from_pipeline(self, pipeline_path): continue if "iter" in d: - # That is an iteration, add it to the dictionary - self.add_iteration(d) + # Intercept __cgroup_global__ rows before they reach add_iteration. + # These carry slice-level CPU (% units) and memory.current (MB). + if str(d.get("name", "")) == "__cgroup_global__": + self.cgroup_global_rows.append({ + 'iter': d.get('iter'), + 'cpu_pct': d.get('cpu'), # % (100 = 1 core) + 'mem_mb': d.get('pss'), # memory.current in MB + }) + else: + self.add_iteration(d) continue if not self.meta: # at this point, the only other line in the pipeline_metric is the meta info, so when we end up here, we know that it is meta info @@ -308,6 +338,13 @@ def extract_from_pipeline(self, pipeline_path): if not self.check(): return False + # Capture actual elapsed walltime from absolute log timestamps before + # compute_time_delta overwrites them with per-task deltas. + raw_times = [t for t in self.dict_for_df.get(METRIC_NAME_TIME, []) + if t is not None and isinstance(t, (int, float))] + if len(raw_times) >= 2: + self.wall_time_s = max(raw_times) - min(raw_times) + self.add_meta() self.convert_columns_to_float_if_possible() self.clean_cpu() @@ -856,11 +893,20 @@ def print_statistics(resource_object): dframe = resource_object.df meta = resource_object.meta - # estimate runtime from iteration count max_iter = dframe['iter'].max() - print ("Iterations: ", max_iter) - # each iteration takes 5 seconds in the pipeline runner --> should be made dynamic and adaptive - print ("Estimated runtime (s): ", max_iter * 5) + print("Iterations: ", max_iter) + # Prefer actual walltime from log timestamps (last ts - first ts); fall + # back to iter_count × monitor_interval when timestamps are unavailable. + if getattr(resource_object, 'wall_time_s', None) is not None: + print("Actual walltime (s): ", round(resource_object.wall_time_s, 1)) + else: + monitor_interval = 5 + if meta and 'monitor_interval_cpu' in meta: + try: + monitor_interval = float(meta['monitor_interval_cpu']) + except (TypeError, ValueError): + pass + print("Estimated runtime (s): ", max_iter * monitor_interval) #(a) PSS memory summed_pss_per_iter=dframe.groupby("iter")['pss'].sum() @@ -869,13 +915,30 @@ def print_statistics(resource_object): print ("Mean-PSS (MB): ", mean_pss) print ("Max-PSS (MB): ", max_pss) - #(b) CPU consumption - summed_cpu_per_iter=dframe.groupby("iter")['cpu'].sum() - mean_cpu = summed_cpu_per_iter.mean() - max_cpu = summed_cpu_per_iter.max() - print ("Mean-CPU (cores): ", mean_cpu) - print ("Max-CPU (cores): ", max_cpu) - print ("CPU-efficiency: ", mean_cpu / meta["cpu_limit"]) + #(b) CPU consumption — prefer cgroup global (slice-level, immune to + # per-task psutil sampling artefacts) over the psutil per-task sum. + cg_rows = resource_object.cgroup_global_rows + if cg_rows: + cg_df = pd.DataFrame(cg_rows).dropna(subset=['cpu_pct']) + cg_by_iter = cg_df.groupby('iter')['cpu_pct'].mean() + mean_cpu = cg_by_iter.mean() / 100.0 # % → cores + max_cpu = cg_by_iter.max() / 100.0 + print ("Mean-CPU (cores, cgroup): ", mean_cpu) + print ("Max-CPU (cores, cgroup): ", max_cpu) + print ("CPU-efficiency (cgroup): ", mean_cpu / meta["cpu_limit"]) + # also print cgroup memory if available + cg_mem_df = pd.DataFrame(cg_rows).dropna(subset=['mem_mb']) + if not cg_mem_df.empty: + cg_mem = cg_mem_df.groupby('iter')['mem_mb'].mean() + print ("Mean-mem (MB, cgroup): ", cg_mem.mean()) + print ("Max-mem (MB, cgroup): ", cg_mem.max()) + else: + summed_cpu_per_iter=dframe.groupby("iter")['cpu'].sum() + mean_cpu = summed_cpu_per_iter.mean() + max_cpu = summed_cpu_per_iter.max() + print ("Mean-CPU (cores): ", mean_cpu) + print ("Max-CPU (cores): ", max_cpu) + print ("CPU-efficiency: ", mean_cpu / meta["cpu_limit"]) #(c) Top N memory consumers by name top_n = 5 @@ -916,57 +979,49 @@ def produce_json_stat(resource_object): # to adjust the resource estimates in o2dpg_workflow_runner.py # resource_json = {} - # group by 'name' and compute all needed stats for each metric - stats = ( - dframe - .groupby('name') - .agg({ - 'pss': ['min', 'max', 'mean'], - 'uss': ['min', 'max', 'mean'], - 'cpu': ['min', 'max', 'mean'] - }) - ) + # group by 'name' and compute all needed stats for each metric. + # Include cgroup metrics when present (runner with --systemd-run). + agg_spec = { + 'pss': ['min', 'max', 'mean'], + 'uss': ['min', 'max', 'mean'], + 'cpu': ['min', 'max', 'mean'], + } + for cg_col in CGROUP_METRICS: + if cg_col in dframe.columns and dframe[cg_col].notna().any(): + agg_spec[cg_col] = ['min', 'max', 'mean'] + + stats = dframe.groupby('name').agg(agg_spec) # turn the multi-level columns into flat names stats.columns = [f"{col[0]}_{col[1]}" for col in stats.columns] stats = stats.reset_index() - # ----- compute lifetime ~ walltime per (timeframe, name) ----- - # ------------------------------------------------ - # Filter out unrealistic timeframes (nice == 19) because it's not the realistic runtime + # ----- compute lifetime (walltime in seconds) per (timeframe, name) ----- + # Use the 'time' column which compute_time_delta() already converted to + # seconds elapsed since the first observation of each (task, timeframe). + # max(time) for a group therefore equals the observed walltime in seconds. + # Filter out nice==19 tasks because they run at background priority and + # would inflate walltime estimates for the foreground scheduling budget. df_nice_filtered = dframe[dframe['nice'] != 19].copy() - # the calculates of mean runtime should be averaged over timeframes lifetime_per_tf = ( df_nice_filtered - .groupby(['timeframe', 'name'])['iter'] - .agg(lambda x: x.max() - x.min() + 1) # +1 to include both ends + .groupby(['timeframe', 'name'])['time'] + .max() # time is delta from task start → max = observed walltime [s] .reset_index(name='lifetime') ) - # now average over timeframes for each name - mean_lifetime = ( - lifetime_per_tf - .groupby('name')['lifetime'] - .mean() - ) - max_lifetime = ( - lifetime_per_tf - .groupby('name')['lifetime'] - .max() - ) - min_lifetime = ( - lifetime_per_tf - .groupby('name')['lifetime'] - .max() - ) + mean_lifetime = lifetime_per_tf.groupby('name')['lifetime'].mean() + max_lifetime = lifetime_per_tf.groupby('name')['lifetime'].max() + min_lifetime = lifetime_per_tf.groupby('name')['lifetime'].min() + std_lifetime = lifetime_per_tf.groupby('name')['lifetime'].std().fillna(0.0) resource_json["count"] = 1 # basic sample size # convert to nested dictionary for _, row in stats.iterrows(): name = row['name'] - resource_json[name] = { + entry = { 'pss': { 'min': r3(row['pss_min']), 'max': r3(row['pss_max']), @@ -985,9 +1040,20 @@ def produce_json_stat(resource_object): 'lifetime': { 'min' : r3(float(min_lifetime.get(name, np.nan))), 'max' : r3(float(max_lifetime.get(name, np.nan))), - 'mean' : r3(float(mean_lifetime.get(name, np.nan))) + 'mean': r3(float(mean_lifetime.get(name, np.nan))), + 'std' : r3(float(std_lifetime.get(name, 0.0))), } } + # include cgroup metrics when available + for cg_col in CGROUP_METRICS: + min_col = f"{cg_col}_min" + if min_col in row.index: + entry[cg_col] = { + 'min': r3(row[f"{cg_col}_min"]), + 'max': r3(row[f"{cg_col}_max"]), + 'mean': r3(row[f"{cg_col}_mean"]), + } + resource_json[name] = entry return resource_json def stat(args): @@ -1029,15 +1095,281 @@ def build_meta_header(arg): print ("Unsupported Meta input type") return meta -def json_stat_impl(pipelines, output, header_data): +def _read_log_time_file(fpath): + """Parse a *.log_time file written by GNU time via taskwrapper. + + Returns a dict with 'walltime' [s], 'cpu_cores', 'maxmem_mb', or None + if the file cannot be parsed. + + GNU time has ~10ms wall-clock resolution, so sub-10ms tasks report + walltime=0.00. When that happens we fall back to usertime (CPU time in + user space) which sometimes captures the real duration better. The + effective walltime is max(walltime, usertime) so we never under-count. + """ + result = {} + try: + with open(fpath) as f: + for line in f: + line = line.strip().lstrip('#') + parts = line.split() + if not parts: + continue + key = parts[0] + if key == 'walltime' and len(parts) > 1: + result['walltime'] = float(parts[1]) + elif key == 'usertime' and len(parts) > 1: + result['usertime'] = float(parts[1]) + elif key == 'CPU' and len(parts) > 1: + result['cpu_cores'] = float(parts[1].rstrip('%')) / 100.0 + elif key == 'maxmem' and len(parts) > 1: + result['maxmem_mb'] = float(parts[1]) / 1024.0 # KB -> MB + except (OSError, ValueError, IndexError): + return None + if 'walltime' not in result: + return None + # Only fall back to usertime when walltime rounded to 0 (GNU time has ~10ms + # resolution). Do NOT use max(walltime, usertime): for multithreaded tasks + # usertime = sum of CPU time across all workers >> walltime, so max() would + # replace a correct 45s wall time with an incorrect 116s usertime. + if result['walltime'] == 0.0: + result['walltime'] = result.get('usertime', 0.0) + return result + + +def _collect_log_times(search_path): + """Collect *.log_time files from search_path and one level of subdirectories. + + Returns {base_task_name: [measurement_dict, ...]} where each measurement + dict has the keys returned by _read_log_time_file(). + """ + import glob + collected = {} + patterns = [ + os.path.join(search_path, '*.log_time'), + os.path.join(search_path, '*', '*.log_time'), + ] + for pattern in patterns: + for fpath in sorted(glob.glob(pattern)): + fname = os.path.basename(fpath).replace('.log_time', '') + # Strip timeframe suffix _N to get base task name + parts = fname.split('_') + base = '_'.join(parts[:-1]) if (parts and parts[-1].isdigit() and len(parts) > 1) else fname + data = _read_log_time_file(fpath) + if data is not None: + collected.setdefault(base, []).append(data) + return collected + + +def _stats_dict(values): + """Build a min/max/mean/std/count dict from a list of floats. + + std is None when n=1 (sample std is undefined, not zero). + """ + n = len(values) + if n == 0: + return None + mean = sum(values) / n + std = round((sum((x - mean) ** 2 for x in values) / (n - 1)) ** 0.5, 3) if n > 1 else None + return { + 'min': round(min(values), 3), + 'max': round(max(values), 3), + 'mean': round(mean, 3), + 'std': std, + 'M2': 0.0, 'count': n, + } + + +def incorporate_log_times(json_path, search_path): + """Update a json-stat file with walltime (and cpu/mem) from *.log_time files. + + For tasks already in the stat: overwrite lifetime with log_time walltime + (GNU time is more accurate than 1 Hz psutil sampling, especially for + short-lived tasks that only appear in one or two monitoring windows). + + For tasks absent from the stat (finished before the first monitor sample): + add a new entry with walltime, approximate cpu, and approximate pss from + maxmem (RSS ≈ PSS for short-lived tasks). + """ + log_times = _collect_log_times(search_path) + if not log_times: + print(f' No *.log_time files found under {search_path}', file=sys.stderr) + return + + with open(json_path) as f: + stat = json.load(f) + + n_updated = n_added = 0 + for base_name, measurements in sorted(log_times.items()): + walltimes = [m['walltime'] for m in measurements] + cpu_cores = [m['cpu_cores'] for m in measurements if 'cpu_cores' in m] + maxmem_mbs = [m['maxmem_mb'] for m in measurements if 'maxmem_mb' in m] + + lifetime_entry = _stats_dict(walltimes) + + if base_name in stat: + # Task is in the stat: replace lifetime with ground-truth log_time values. + stat[base_name]['lifetime'] = lifetime_entry + n_updated += 1 + else: + # Task is absent (too short for monitor sampling): create a minimal entry. + entry = {'lifetime': lifetime_entry} + cpu_entry = _stats_dict(cpu_cores) + if cpu_entry: + entry['cpu'] = cpu_entry + mem_entry = _stats_dict(maxmem_mbs) + if mem_entry: + # maxmem (RSS) is a reasonable PSS proxy for short-lived tasks + entry['pss'] = mem_entry + entry['uss'] = mem_entry # same approximation + stat[base_name] = entry + n_added += 1 + + with open(json_path, 'w') as f: + json.dump(stat, f, indent=2) + + print(f' log_time: updated lifetime for {n_updated} tasks, ' + f'added {n_added} new tasks from {search_path}') + + +_NWORKER_CMD_PATTERNS = [ + re.compile(r'\$\{O2DPG_DYNAMIC_NWORKER_OVERWRITE:-(\d+)\}'), # ${VAR:-N} — default value + re.compile(r'(? n_ref: + return None + t_serial = walltime_ref * (n_ref - cpu_mean_ref) / (n_ref - 1) + t_parallel_tot = max(0.0, walltime_ref * n_ref * (cpu_mean_ref - 1) / (n_ref - 1)) + return { + 't_serial': round(t_serial, 3), + 't_parallel_tot': round(t_parallel_tot, 3), + 'n_ref': n_ref, + 'cpu_mean_ref': round(cpu_mean_ref, 3), + 'min_workers': int(min_workers), + 'max_workers': int(max_workers if max_workers is not None else n_ref), + } + + +def incorporate_amdahl_models(json_path, workflow_path): + """Build Amdahl scaling models and add them to the json-stat file. + + Reads the original workflow.json to obtain n_ref (the worker count used + during the measurement run, i.e. the pre-update resources.cpu value for + tasks that carry O2DPG_DYNAMIC_NWORKER_OVERWRITE or a 'scaling' block). + Combines with cpu.mean and lifetime.mean from the json-stat to derive + t_serial and t_parallel_tot for each scalable task. + """ + try: + with open(workflow_path) as f: + wf_raw = json.load(f) + except (OSError, json.JSONDecodeError) as e: + print(f' WARNING: could not read workflow {workflow_path}: {e}', file=sys.stderr) + return + + stages = wf_raw.get('stages', []) + + # Collect n_ref and bounds per base task name from the workflow. + scalable = {} + for task in stages: + cmd = task.get('cmd', '') + res = task.get('resources', {}) or {} + scaling = res.get('scaling') + is_scalable = ( + res.get('amdahl_scalable', False) or + 'O2DPG_DYNAMIC_NWORKER_OVERWRITE' in cmd or + scaling is not None + ) + if not is_scalable: + continue + name = task['name'] + tf = task.get('timeframe', -1) + base = '_'.join(name.split('_')[:-1]) if (tf >= 1 and name.split('_')[-1].isdigit()) else name + if base in scalable: + continue # use the first TF instance as representative + + # n_ref = actual worker count used during the measurement run. + # Read from the cmd string (the ground truth) rather than resources.cpu + # which is a scheduler booking estimate and may be fractional/rounded. + n_ref_cmd = _extract_n_workers_from_cmd(cmd) + n_ref = n_ref_cmd if n_ref_cmd else max(1, int(round(float(res.get('cpu', 1))))) + + min_w, max_w = 1, n_ref + if scaling: + min_w = int(scaling.get('min_workers', 1)) + max_w = int(scaling.get('max_workers', n_ref)) + scalable[base] = {'n_ref': n_ref, 'min_workers': min_w, 'max_workers': max_w} + + with open(json_path) as f: + stat = json.load(f) + + n_built = 0 + for base_name, info in scalable.items(): + if base_name not in stat: + continue + entry = stat[base_name] + cpu_mean = (entry.get('cpu') or {}).get('mean') + walltime_ref = (entry.get('lifetime') or {}).get('mean') + if cpu_mean is None or walltime_ref is None or walltime_ref <= 0: + continue + model = _build_amdahl_model( + walltime_ref, cpu_mean, info['n_ref'], + info['min_workers'], info['max_workers'], + ) + if model: + stat[base_name]['amdahl'] = model + n_built += 1 + + with open(json_path, 'w') as f: + json.dump(stat, f, indent=2) + + print(f' Amdahl models built for {n_built}/{len(scalable)} scalable task(s).') + + +def json_stat_impl(pipelines, output, header_data, log_time_path=None, workflow_path=None): resources = extract_resources(pipelines) all_stats = [produce_json_stat(res) for res in resources] - merge_stats_into(all_stats, output, build_meta_header(header_data)) + if log_time_path: + incorporate_log_times(output, log_time_path) + + if workflow_path: + incorporate_amdahl_models(output, workflow_path) + def json_stat(args): - json_stat_impl(args.pipelines, args.output, args.header_data) + log_time_path = getattr(args, 'log_time_path', None) + workflow_path = getattr(args, 'workflow_path', None) + json_stat_impl(args.pipelines, args.output, args.header_data, + log_time_path=log_time_path, workflow_path=workflow_path) def merge_json_stats(args): all_stats = [] @@ -1300,6 +1632,21 @@ def main(): json_stat_parser.add_argument("-p", "--pipelines", nargs="*", help="Pipeline_metric files from o2_dpg_workflow_runner; Merges information", required=True) json_stat_parser.add_argument("-o", "--output", type=str, help="Output json filename", required=True) json_stat_parser.add_argument("-hd", "--header-data", type=str, default='', help="Some meta-data headers to be included in the JSON") + json_stat_parser.add_argument("-w", "--workflow", dest="workflow_path", default=None, + metavar="WORKFLOW_JSON", + help="Original workflow.json used for the measurement run. " + "When provided, Amdahl scaling models are computed for " + "tasks that carry O2DPG_DYNAMIC_NWORKER_OVERWRITE or a " + "'scaling' block, using the original resources.cpu as " + "n_ref combined with learned cpu.mean and lifetime.mean.") + json_stat_parser.add_argument("--log-time-path", dest="log_time_path", default=None, + metavar="DIR", + help="Directory to search for *.log_time files written by " + "taskwrapper (GNU time). Searched recursively one level " + "deep (covers tf1/, tf2/, ... subdirs). When provided, " + "lifetime values are replaced with the more accurate " + "GNU-time walltime, and tasks too short to appear in the " + "1 Hz monitor log are added from the log_time data.") merge_stat_parser = sub_parsers.add_parser("merge-json-stats", help="Merge information from json-stats into an aggregated stat") merge_stat_parser.set_defaults(func=merge_json_stats) From d4f14f0da7d694353643da17e435a5373f139ae4 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 05:16:07 +0200 Subject: [PATCH 043/144] Add a modular workflow runner next to the existing one This is the main step of a refactoring of MC/bin/o2_dpg_workflow_runner.py, presented at CHEP 2026. The 2000-line script becomes a package, resource monitoring moves off the scheduling loop and costs about a tenth of what it did, and the scheduling policy becomes selectable, with two alternatives to the original one. Both runners are installed side by side and a dispatcher picks between them, so nothing changes for a caller that does not ask for the new one. - MC/bin/o2_dpg_workflow_runner.py becomes that dispatcher. It reads ALIEN_O2DPG_WORKFLOW_RUNNER and defaults to "legacy". The original runner moves unchanged to MC/bin/o2dpg_workflow_runner_legacy.py, and the o2dpg_workflow_runner.py symlink beside it is untouched, so all ten call sites in the repository keep working under either runner. - MC/workflow_runner/ holds the new package: workflow, graph, resources, monitoring, scheduler, executor, cleanup and cache modules. - Monitoring moves to a background thread with separate CPU and memory cadences. Polling psutil synchronously in the scheduling loop cost 10-20 % of a core on realistic workflows; this costs 1-2 %. - --scheduler-policy selects timeframe, which is the default and reproduces the original ordering, critical-path, or best-fit. - --systemd-run supersedes --cgroup for confining a workflow to a CPU and memory budget. - --cache-policy writes a fingerprint of command, environment, software tag and dependencies next to the _done marker, so a task whose command changed re-runs instead of being skipped. _done remains the skip marker. - Every flag the original parser accepts, the new one accepts. --cgroup, --webhook and --checkpoint-on-failure are accepted, ignored and warned about, so a JDL passing them through ALIEN_O2DPG_ADDITIONAL_WORKFLOW_RUNNER_ARGS still runs. - 68 unit tests come with it, run by a new CI job in .github/workflows/syntax-checks.yml. No Python test ran in CI before. - MC/workflow_runner/o2dpg_runner/README.md documents the layout, the behavioural differences and the pitfalls found while building it. https://indico.cern.ch/event/1471803/contributions/6967072/ Co-Authored-By: Claude Opus 5 --- .github/workflows/syntax-checks.yml | 15 + .gitignore | 1 + MC/bin/o2_dpg_workflow_runner.py | 2013 +---------------- MC/bin/o2dpg_workflow_runner_legacy.py | 2004 ++++++++++++++++ MC/workflow_runner/o2dpg_runner/README.md | 304 +++ MC/workflow_runner/o2dpg_runner/__init__.py | 11 + MC/workflow_runner/o2dpg_runner/alienv.py | 66 + MC/workflow_runner/o2dpg_runner/cache.py | 192 ++ MC/workflow_runner/o2dpg_runner/cleanup.py | 221 ++ MC/workflow_runner/o2dpg_runner/cli.py | 468 ++++ MC/workflow_runner/o2dpg_runner/config.py | 62 + MC/workflow_runner/o2dpg_runner/executor.py | 763 +++++++ MC/workflow_runner/o2dpg_runner/graph.py | 159 ++ MC/workflow_runner/o2dpg_runner/monitoring.py | 505 +++++ MC/workflow_runner/o2dpg_runner/resources.py | 362 +++ .../o2dpg_runner/scheduler/__init__.py | 22 + .../o2dpg_runner/scheduler/base.py | 62 + .../o2dpg_runner/scheduler/best_fit.py | 104 + .../o2dpg_runner/scheduler/critical_path.py | 60 + .../o2dpg_runner/scheduler/timeframe.py | 81 + .../o2dpg_runner/tests/__init__.py | 0 .../tests/fixtures/tiny_workflow.json | 104 + .../o2dpg_runner/tests/test_cache.py | 119 + .../o2dpg_runner/tests/test_executor_e2e.py | 216 ++ .../o2dpg_runner/tests/test_graph.py | 90 + .../o2dpg_runner/tests/test_resources.py | 137 ++ .../o2dpg_runner/tests/test_scheduler.py | 179 ++ .../o2dpg_runner/tests/test_simulator.py | 131 ++ .../o2dpg_runner/tests/test_workflow.py | 117 + MC/workflow_runner/o2dpg_runner/workflow.py | 411 ++++ .../o2dpg_schedule_simulator.py | 1268 +++++++++++ MC/workflow_runner/o2dpg_workflow_runner.py | 18 + 32 files changed, 8273 insertions(+), 1992 deletions(-) create mode 100755 MC/bin/o2dpg_workflow_runner_legacy.py create mode 100644 MC/workflow_runner/o2dpg_runner/README.md create mode 100644 MC/workflow_runner/o2dpg_runner/__init__.py create mode 100644 MC/workflow_runner/o2dpg_runner/alienv.py create mode 100644 MC/workflow_runner/o2dpg_runner/cache.py create mode 100644 MC/workflow_runner/o2dpg_runner/cleanup.py create mode 100644 MC/workflow_runner/o2dpg_runner/cli.py create mode 100644 MC/workflow_runner/o2dpg_runner/config.py create mode 100644 MC/workflow_runner/o2dpg_runner/executor.py create mode 100644 MC/workflow_runner/o2dpg_runner/graph.py create mode 100644 MC/workflow_runner/o2dpg_runner/monitoring.py create mode 100644 MC/workflow_runner/o2dpg_runner/resources.py create mode 100644 MC/workflow_runner/o2dpg_runner/scheduler/__init__.py create mode 100644 MC/workflow_runner/o2dpg_runner/scheduler/base.py create mode 100644 MC/workflow_runner/o2dpg_runner/scheduler/best_fit.py create mode 100644 MC/workflow_runner/o2dpg_runner/scheduler/critical_path.py create mode 100644 MC/workflow_runner/o2dpg_runner/scheduler/timeframe.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/__init__.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/fixtures/tiny_workflow.json create mode 100644 MC/workflow_runner/o2dpg_runner/tests/test_cache.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/test_executor_e2e.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/test_graph.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/test_resources.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/test_scheduler.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/test_simulator.py create mode 100644 MC/workflow_runner/o2dpg_runner/tests/test_workflow.py create mode 100644 MC/workflow_runner/o2dpg_runner/workflow.py create mode 100755 MC/workflow_runner/o2dpg_schedule_simulator.py create mode 100755 MC/workflow_runner/o2dpg_workflow_runner.py diff --git a/.github/workflows/syntax-checks.yml b/.github/workflows/syntax-checks.yml index 44bd0306f..942d7b1dd 100644 --- a/.github/workflows/syntax-checks.yml +++ b/.github/workflows/syntax-checks.yml @@ -105,6 +105,21 @@ jobs: done exit "$error" + runner-tests: + name: Workflow-runner unit tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install prerequisites + run: pip install pytest psutil + + - name: Run the o2dpg_runner test suite + working-directory: MC/workflow_runner + run: pytest o2dpg_runner/tests -q + pylint: name: Pylint runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 27281eba7..8271e2f98 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .vscode *.pyc o2dpg_tests/** +__pycache__/ diff --git a/MC/bin/o2_dpg_workflow_runner.py b/MC/bin/o2_dpg_workflow_runner.py index eebde6501..fda79a19f 100755 --- a/MC/bin/o2_dpg_workflow_runner.py +++ b/MC/bin/o2_dpg_workflow_runner.py @@ -1,2004 +1,33 @@ #!/usr/bin/env python3 +"""Run the workflow runner selected by ALIEN_O2DPG_WORKFLOW_RUNNER. -# started February 2021, sandro.wenzel@cern.ch +'legacy' (the default) is the original single-file runner, 'new' is the +o2dpg_runner package under MC/workflow_runner. All arguments are passed on +unchanged. +""" -import re -import subprocess -import time -import json -import logging import os -import signal -import socket import sys -import traceback -import platform -import tarfile -from copy import deepcopy -try: - from graphviz import Digraph - havegraphviz=True -except ImportError: - havegraphviz=False - -formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - -sys.setrecursionlimit(100000) - -import argparse -import psutil -max_system_mem=psutil.virtual_memory().total - -sys.path.append(os.path.join(os.path.dirname(__file__), '.', 'o2dpg_workflow_utils')) -from o2dpg_workflow_utils import read_workflow - -# defining command line options -parser = argparse.ArgumentParser(description='Parallel execution of a (O2-DPG) DAG data/job pipeline under resource contraints.', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - -parser.add_argument('-f','--workflowfile', help='Input workflow file name', required=True) -parser.add_argument('-jmax','--maxjobs', type=int, help='Number of maximal parallel tasks.', default=100) -parser.add_argument('-k','--keep-going', action='store_true', help='Keep executing the pipeline as far possibe (not stopping on first failure)') -parser.add_argument('--dry-run', action='store_true', help='Show what you would do.') -parser.add_argument('--visualize-workflow', action='store_true', help='Saves a graph visualization of workflow.') -parser.add_argument('--target-labels', nargs='+', help='Runs the pipeline by target labels (example "TPC" or "DIGI").\ - This condition is used as logical AND together with --target-tasks.', default=[]) -parser.add_argument('-tt','--target-tasks', nargs='+', help='Runs the pipeline by target tasks (example "tpcdigi"). By default everything in the graph is run. Regular expressions supported.', default=["*"]) -parser.add_argument('--produce-script', help='Produces a shell script that runs the workflow in serialized manner and quits.') -parser.add_argument('--rerun-from', help='Reruns the workflow starting from given task (or pattern). All dependent jobs will be rerun.') -parser.add_argument('--list-tasks', help='Simply list all tasks by name and quit.', action='store_true') - -# Resources -parser.add_argument('--update-resources', dest="update_resources", help='Read resource estimates from a JSON and apply where possible.') -parser.add_argument("--dynamic-resources", dest="dynamic_resources", action="store_true", help="Update reources estimates of task based on finished related tasks") # derive resources dynamically -parser.add_argument('--optimistic-resources', dest="optimistic_resources", action="store_true", help="Try to run workflow even though resource limits might underestimate resource needs of some tasks") -parser.add_argument("--n-backfill", dest="n_backfill", type=int, default=1) -parser.add_argument('--mem-limit', help='Set memory limit as scheduling constraint (in MB)', default=0.9*max_system_mem/1024./1024, type=float) -parser.add_argument('--cpu-limit', help='Set CPU limit (core count)', default=8, type=float) -parser.add_argument('--cgroup', help='Execute pipeline under a given cgroup (e.g., 8coregrid) emulating resource constraints. This m\ -ust exist and the tasks file must be writable to with the current user.') - -# run control, webhooks -parser.add_argument('--stdout-on-failure', action='store_true', help='Print log files of failing tasks to stdout,') -parser.add_argument('--webhook', help=argparse.SUPPRESS) # log some infos to this webhook channel -parser.add_argument('--checkpoint-on-failure', help=argparse.SUPPRESS) # debug option making a debug-tarball and sending to specified address - # argument is alien-path -parser.add_argument('--retry-on-failure', help=argparse.SUPPRESS, default=0) # number of times a failing task is retried -parser.add_argument('--no-rootinit-speedup', help=argparse.SUPPRESS, action='store_true') # disable init of ROOT environment vars to speedup init/startup - -parser.add_argument('--remove-files-early', type=str, default="", help="Delete intermediate files early (using the file graph information in the given file)") - - -# Logging -parser.add_argument('--action-logfile', help='Logfilename for action logs. If none given, pipeline_action_#PID.log will be used') -parser.add_argument('--metric-logfile', help='Logfilename for metric logs. If none given, pipeline_metric_#PID.log will be used') -parser.add_argument('--production-mode', action='store_true', help='Production mode') -# will trigger special features good for non-interactive/production processing (automatic cleanup of files etc). -args = parser.parse_args() - -def setup_logger(name, log_file, level=logging.INFO): - """To setup as many loggers as you want""" - - handler = logging.FileHandler(log_file, mode='w') - handler.setFormatter(formatter) - - logger = logging.getLogger(name) - logger.setLevel(level) - logger.addHandler(handler) - - return logger - -# first file logger -actionlogger_file = ('pipeline_action_' + str(os.getpid()) + '.log', args.action_logfile)[args.action_logfile!=None] -actionlogger = setup_logger('pipeline_action_logger', actionlogger_file, level=logging.DEBUG) - -# second file logger -metriclogger = setup_logger('pipeline_metric_logger', ('pipeline_metric_' + str(os.getpid()) + '.log', args.action_logfile)[args.action_logfile!=None]) - -# Immediately log imposed memory and CPU limit as well as further useful meta info -_ , meta = read_workflow(args.workflowfile) -meta["cpu_limit"] = args.cpu_limit -meta["mem_limit"] = args.mem_limit -meta["workflow_file"] = os.path.abspath(args.workflowfile) -args.target_tasks = [f.strip('"').strip("'") for f in args.target_tasks] # strip quotes from the shell -meta["target_task"] = args.target_tasks -meta["rerun_from"] = args.rerun_from -meta["target_labels"] = args.target_labels -metriclogger.info(meta) - -# for debugging without terminal access -# TODO: integrate into standard logger -def send_webhook(hook, t): - if hook!=None: - command="curl -X POST -H 'Content-type: application/json' --data '{\"text\":\" " + str(t) + "\"}' " + str(hook) + " &> /dev/null" - os.system(command) - -# A fallback solution to getting all child procs -# in case psutil has problems (PermissionError). -# It returns the same list as psutil.children(recursive=True). -def getChildProcs(basepid): - cmd=''' - childprocs() { - local parent=$1 - if [ ! "$2" ]; then - child_pid_list="" - fi - if [ "$parent" ] ; then - child_pid_list="$child_pid_list $parent" - for childpid in $(pgrep -P ${parent}); do - childprocs $childpid "nottoplevel" - done; - fi - # return via a string list (only if toplevel) - if [ ! "$2" ]; then - echo "${child_pid_list}" - fi - } - ''' - cmd = cmd + '\n' + 'childprocs ' + str(basepid) - output = subprocess.check_output(cmd, shell=True) - plist = [] - for p in output.strip().split(): - try: - proc=psutil.Process(int(p)) - except psutil.NoSuchProcess: - continue - - plist.append(proc) - return plist - -# -# Code section to find all topological orderings -# of a DAG. This is used to know when we can schedule -# things in parallel. -# Taken from https://www.geeksforgeeks.org/all-topological-sorts-of-a-directed-acyclic-graph/ - -# class to represent a graph object -class Graph: - - # Constructor - def __init__(self, edges, N): - - # A List of Lists to represent an adjacency list - self.adjList = [[] for _ in range(N)] - - # stores in-degree of a vertex - # initialize in-degree of each vertex by 0 - self.indegree = [0] * N - - # add edges to the undirected graph - for (src, dest) in edges: - - # add an edge from source to destination - self.adjList[src].append(dest) - - # increment in-degree of destination vertex by 1 - self.indegree[dest] = self.indegree[dest] + 1 - -# Recursive function to find all topological orderings of a given DAG -def findAllTopologicalOrders(graph, path, discovered, N, allpaths, maxnumber=1): - if len(allpaths) >= maxnumber: - return - - # do for every vertex - for v in range(N): - - # proceed only if in-degree of current node is 0 and - # current node is not processed yet - if graph.indegree[v] == 0 and not discovered[v]: - - # for every adjacent vertex u of v, reduce in-degree of u by 1 - for u in graph.adjList[v]: - graph.indegree[u] = graph.indegree[u] - 1 - - # include current node in the path and mark it as discovered - path.append(v) - discovered[v] = True - - # recur - findAllTopologicalOrders(graph, path, discovered, N, allpaths) - - # backtrack: reset in-degree information for the current node - for u in graph.adjList[v]: - graph.indegree[u] = graph.indegree[u] + 1 - - # backtrack: remove current node from the path and - # mark it as undiscovered - path.pop() - discovered[v] = False - - # record valid ordering - if len(path) == N: - allpaths.append(path.copy()) - - -# get all topological orderings of a given DAG as a list -def printAllTopologicalOrders(graph, maxnumber=1): - # get number of nodes in the graph - N = len(graph.adjList) - - # create an auxiliary space to keep track of whether vertex is discovered - discovered = [False] * N - - # list to store the topological order - path = [] - allpaths = [] - # find all topological ordering and print them - findAllTopologicalOrders(graph, path, discovered, N, allpaths, maxnumber=maxnumber) - return allpaths - -# <--- end code section for topological sorts - -# find all tasks that depend on a given task (id); when a cache -# dict is given we can fill for the whole graph in one pass... -def find_all_dependent_tasks(possiblenexttask, tid, cache=None): - c=cache.get(tid) if cache else None - if c!=None: - return c - - daughterlist=[tid] - # possibly recurse - for n in possiblenexttask[tid]: - c = cache.get(n) if cache else None - if c == None: - c = find_all_dependent_tasks(possiblenexttask, n, cache) - daughterlist = daughterlist + c - if cache is not None: - cache[n]=c - - if cache is not None: - cache[tid]=daughterlist - return list(set(daughterlist)) - - -# wrapper taking some edges, constructing the graph, -# obtain all topological orderings and some other helper data structures -def analyseGraph(edges, nodes): - # Number of nodes in the graph - N = len(nodes) - - # candidate list trivial - nextjobtrivial = { n:[] for n in nodes } - # startnodes - nextjobtrivial[-1] = nodes - for e in edges: - nextjobtrivial[e[0]].append(e[1]) - if nextjobtrivial[-1].count(e[1]): - nextjobtrivial[-1].remove(e[1]) - - # find topological orderings of the graph - # create a graph from edges - graph = Graph(edges, N) - orderings = printAllTopologicalOrders(graph) - - return (orderings, nextjobtrivial) - - -def draw_workflow(workflowspec): - if not havegraphviz: - print('graphviz not installed, cannot draw workflow') - return - - dot = Digraph(comment='MC workflow') - nametoindex={} - index=0 - # nodes - for node in workflowspec['stages']: - name=node['name'] - nametoindex[name]=index - dot.node(str(index), name) - index=index+1 - - # edges - for node in workflowspec['stages']: - toindex = nametoindex[node['name']] - for req in node['needs']: - fromindex = nametoindex[req] - dot.edge(str(fromindex), str(toindex)) - - dot.render('workflow.gv') - -# builds the graph given a "taskuniverse" list -# builds accompagnying structures tasktoid and idtotask -def build_graph(taskuniverse, workflowspec): - tasktoid={ t[0]['name']:i for i, t in enumerate(taskuniverse, 0) } - # print (tasktoid) - - nodes = [] - edges = [] - for t in taskuniverse: - nodes.append(tasktoid[t[0]['name']]) - for n in t[0]['needs']: - edges.append((tasktoid[n], tasktoid[t[0]['name']])) - - return (edges, nodes) - - -# loads json into dict, e.g. for workflow specification -def load_json(workflowfile): - fp=open(workflowfile) - workflowspec=json.load(fp) - return workflowspec - - -# filters the original workflowspec according to wanted targets or labels -# returns a new workflowspec and the list of "final" workflowtargets -def filter_workflow(workflowspec, targets=[], targetlabels=[]): - if len(targets)==0: - return workflowspec, [] - if len(targetlabels)==0 and len(targets)==1 and targets[0]=="*": - return workflowspec, [] - - transformedworkflowspec = workflowspec - - def task_matches(t): - for filt in targets: - if filt=="*": - return True - if re.match(filt, t) != None: - return True - return False - - def task_matches_labels(t): - # when no labels are given at all it's ok - if len(targetlabels)==0: - return True - - for l in t['labels']: - if targetlabels.count(l)!=0: - return True - return False - - # The following sequence of operations works and is somewhat structured. - # However, it builds lookups used elsewhere as well, so some CPU might be saved by reusing - # some structures across functions or by doing less passes on the data. - - # helper lookup - tasknametoid = { t['name']:i for i, t in enumerate(workflowspec['stages'],0) } - - # check if a task can be run at all - # or not due to missing requirements - def canBeDone(t,cache={}): - ok = True - c = cache.get(t['name']) - if c != None: - return c - for r in t['needs']: - taskid = tasknametoid.get(r) - if taskid != None: - if not canBeDone(workflowspec['stages'][taskid], cache): - ok = False - break - else: - ok = False - break - cache[t['name']] = ok - if ok == False: - print (f"Disabling target {t['name']} due to unsatisfied requirements") - return ok - - okcache = {} - # build full target list - full_target_list = [ t for t in workflowspec['stages'] if task_matches(t['name']) and task_matches_labels(t) and canBeDone(t,okcache) ] - full_target_name_list = [ t['name'] for t in full_target_list ] - - # build full dependency list for a task t - def getallrequirements(t): - _l=[] - for r in t['needs']: - fulltask = workflowspec['stages'][tasknametoid[r]] - _l.append(fulltask) - _l=_l+getallrequirements(fulltask) - return _l - - full_requirements_list = [ getallrequirements(t) for t in full_target_list ] - - # make flat and fetch names only - full_requirements_name_list = list(set([ item['name'] for sublist in full_requirements_list for item in sublist ])) - - # inner "lambda" helper answering if a task "name" is needed by given targets - def needed_by_targets(name): - if full_target_name_list.count(name)!=0: - return True - if full_requirements_name_list.count(name)!=0: - return True - return False - - # we finaly copy everything matching the targets as well - # as all their requirements - transformedworkflowspec['stages']=[ l for l in workflowspec['stages'] if needed_by_targets(l['name']) ] - return transformedworkflowspec, full_target_name_list - - -# builds topological orderings (for each timeframe) -def build_dag_properties(workflowspec): - globaltaskuniverse = [ (l, i) for i, l in enumerate(workflowspec['stages'], 1) ] - timeframeset = set( l['timeframe'] for l in workflowspec['stages'] ) - - edges, nodes = build_graph(globaltaskuniverse, workflowspec) - tup = analyseGraph(edges, nodes.copy()) - # - global_next_tasks = tup[1] - - - dependency_cache = {} - # weight influences scheduling order can be anything user defined ... for the moment we just prefer to stay within a timeframe - # then take the number of tasks that depend on a task as further weight - # TODO: bring in resource estimates from runtime, CPU, MEM - # TODO: make this a policy of the runner to study different strategies - def getweight(tid): - return (globaltaskuniverse[tid][0]['timeframe'], len(find_all_dependent_tasks(global_next_tasks, tid, dependency_cache))) - - task_weights = [ getweight(tid) for tid in range(len(globaltaskuniverse)) ] - - for tid in range(len(globaltaskuniverse)): - actionlogger.info("Score for " + str(globaltaskuniverse[tid][0]['name']) + " is " + str(task_weights[tid])) - - # print (global_next_tasks) - return { 'nexttasks' : global_next_tasks, 'weights' : task_weights, 'topological_ordering' : tup[0] } - - -# update the resource estimates of a workflow based on resources given via JSON -def update_resource_estimates(workflow, resource_json): - # the resource_dict here is generated by tool o2dpg_sim_metrics.py json-stat - resource_dict = load_json(resource_json) - stages = workflow["stages"] - - for task in stages: - if task["timeframe"] >= 1: - name = "_".join(task["name"].split("_")[:-1]) - else: - name = task["name"] - - if name not in resource_dict: - continue - - new_resources = resource_dict[name] - - # memory - newmem = new_resources.get("pss", {}).get("max", None) - if newmem is not None: - oldmem = task["resources"]["mem"] - actionlogger.info("Updating mem estimate for " + task["name"] + " from " + str(oldmem) + " to " + str(newmem)) - task["resources"]["mem"] = newmem - - # cpu - newcpu = new_resources.get("cpu", {}).get("mean", None) - if newcpu is not None: - oldcpu = task["resources"]["cpu"] - rel_cpu = task["resources"]["relative_cpu"] - # TODO: No longer sure about this since we inject numbers from actually measured workloads - if rel_cpu is not None: - # respect the relative CPU settings - # By default, the CPU value in the workflow is already scaled if relative_cpu is given. - # The new estimate on the other hand is not yet scaled so it needs to be done here. - newcpu *= rel_cpu - actionlogger.info("Updating cpu estimate for " + task["name"] + " from " + str(oldcpu) + " to " + str(newcpu)) - task["resources"]["cpu"] = newcpu - -# a function to read a software environment determined by alienv into -# a python dictionary -def get_alienv_software_environment(packagestring): - """ - packagestring is something like O2::v202298081-1,O2Physics::xxx representing packages - published on CVMFS ... or ... a file containing directly the software environment to apply - """ - - # the trivial cases do nothing - if packagestring == None or packagestring == "" or packagestring == "None": - return {} - - def load_env_file(env_file): - """Transform an environment file generated with 'export > env.txt' into a python dictionary.""" - env_vars = {} - with open(env_file, "r") as f: - for line in f: - line = line.strip() - - # Ignore empty lines or comments - if not line or line.startswith("#"): - continue - - # Remove 'declare -x ' if present - if line.startswith("declare -x "): - line = line.replace("declare -x ", "", 1) - - # Handle case: "FOO" without "=" (assign empty string) - if "=" not in line: - key, value = line.strip(), "" - else: - key, value = line.split("=", 1) - value = value.strip('"') # Remove surrounding quotes if present - - env_vars[key.strip()] = value - return env_vars - - # see if this is a file - if os.path.exists(packagestring) and os.path.isfile(packagestring): - actionlogger.info("Taking software environment from file " + packagestring) - return load_env_file(packagestring) - - # alienv printenv packagestring --> dictionary - # for the moment this works with CVMFS only - cmd="/cvmfs/alice.cern.ch/bin/alienv printenv " + packagestring - proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - - envstring, err = proc.communicate() - # see if the printenv command was successful - if len(err.decode()) > 0: - print (err.decode()) - raise Exception - - # the software environment is now in the evnstring - # split it on semicolon - envstring=envstring.decode() - tokens=envstring.split(";") - # build envmap - envmap = {} - for t in tokens: - # check if assignment - if t.count("=") > 0: - assignment = t.rstrip().split("=") - envmap[assignment[0]] = assignment[1] - elif t.count("export") > 0: - # the case when we export or a simple variable - # need to consider the case when this has not been previously assigned - variable = t.split()[1] - if not variable in envmap: - envmap[variable]="" - - return envmap - -# -# functions for execution; encapsulated in a WorkflowExecutor class -# - -class Semaphore: - """ - Object that can be used as semaphore - """ - def __init__(self): - self.locked = False - def lock(self): - self.locked = True - def unlock(self): - self.locked = False - - -class ResourceBoundaries: - """ - Container holding global resource properties - """ - def __init__(self, cpu_limit, mem_limit, dynamic_resources=False, optimistic_resources=False): - self.cpu_limit = cpu_limit - self.mem_limit = mem_limit - self.dynamic_resources = dynamic_resources - # if this is set, tasks that would normally go beyond the resource limits will tried to be run in any case - self.optimistic_resources = optimistic_resources - - -class TaskResources: - """ - Container holding resources of a single task - """ - def __init__(self, tid, name, cpu, cpu_relative, mem, resource_boundaries): - # the task ID belonging to these resources - self.tid = tid - self.name = name - # original CPUs/MEM assigned (persistent) - self.cpu_assigned_original = cpu - self.mem_assigned_original = mem - # relative CPU, to be multiplied with sampled CPU; set by the user, e.g. to allow to backfill tasks - # only takes effect when sampling resources; persistent - self.cpu_relative = cpu_relative if cpu_relative else 1 - # CPUs/MEM assigned (transient) - self.cpu_assigned = cpu - self.mem_assigned = mem - # global resource settings - self.resource_boundaries = resource_boundaries - # sampled resources of this - self.cpu_sampled = None - self.mem_sampled = None - # Set these after a task has finished to compute new estimates for related tasks - self.walltime = None - self.cpu_taken = None - self.mem_taken = None - # collected during monitoring - self.time_collect = [] - self.cpu_collect = [] - self.mem_collect = [] - # linked to other resources of task that are of the same type as this one - self.related_tasks = None - # can assign a semaphore - self.semaphore = None - # the task's nice value - self.nice_value = None - # whether or not the task's resources are currently booked - self.booked = False - - @property - def is_done(self): - return self.time_collect and not self.booked - - def is_within_limits(self): - """ - Check if assigned resources respect limits - """ - cpu_within_limits = True - mem_within_limits = True - if self.cpu_assigned > self.resource_boundaries.cpu_limit: - cpu_within_limits = False - actionlogger.warning("CPU of task %s exceeds limits %d > %d", self.name, self.cpu_assigned, self.resource_boundaries.cpu_limit) - if self.cpu_assigned > self.resource_boundaries.mem_limit: - mem_within_limits = False - actionlogger.warning("MEM of task %s exceeds limits %d > %d", self.name, self.cpu_assigned, self.resource_boundaries.cpu_limit) - return cpu_within_limits and mem_within_limits - - def limit_resources(self, cpu_limit=None, mem_limit=None): - """ - Limit resources of this specific task - """ - if not cpu_limit: - cpu_limit = self.resource_boundaries.cpu_limit - if not mem_limit: - mem_limit = self.resource_boundaries.mem_limit - self.cpu_assigned = min(self.cpu_assigned, cpu_limit) - self.mem_assigned = min(self.mem_assigned, mem_limit) - - def add(self, time_passed, cpu, mem): - """ - Brief interface to add resources that were measured after time_passed - """ - self.time_collect.append(time_passed) - self.cpu_collect.append(cpu) - self.mem_collect.append(mem) - - def sample_resources(self): - """ - If this task is done, sample CPU and MEM for all related tasks that have not started yet - """ - if not self.is_done: - return - - if len(self.time_collect) < 3: - # Consider at least 3 points to sample from - self.cpu_sampled = self.cpu_assigned - self.mem_sampled = self.mem_assigned - actionlogger.debug("Task %s has not enough points (< 3) to sample resources, setting to previosuly assigned values.", self.name) - else: - # take the time deltas and leave out the very first CPU measurent which is not meaningful, - # at least when it domes from psutil.Proc.cpu_percent(interval=None) - time_deltas = [self.time_collect[i+1] - self.time_collect[i] for i in range(len(self.time_collect) - 1)] - cpu = sum([cpu * time_delta for cpu, time_delta in zip(self.cpu_collect[1:], time_deltas) if cpu >= 0]) - self.cpu_sampled = cpu / sum(time_deltas) - self.mem_sampled = max(self.mem_collect) - - mem_sampled = 0 - cpu_sampled = [] - for res in self.related_tasks: - if res.is_done: - mem_sampled = max(mem_sampled, res.mem_sampled) - cpu_sampled.append(res.cpu_sampled) - cpu_sampled = sum(cpu_sampled) / len(cpu_sampled) - - # This task ran already with the assigned resources, so let's set it to the limit - if cpu_sampled > self.resource_boundaries.cpu_limit: - actionlogger.warning("Sampled CPU (%.2f) exceeds assigned CPU limit (%.2f)", cpu_sampled, self.resource_boundaries.cpu_limit) - elif cpu_sampled < 0: - actionlogger.debug("Sampled CPU for %s is %.2f < 0, setting to previously assigned value %.2f", self.name, cpu_sampled, self.cpu_assigned) - cpu_sampled = self.cpu_assigned - - if mem_sampled > self.resource_boundaries.mem_limit: - actionlogger.warning("Sampled MEM (%.2f) exceeds assigned MEM limit (%.2f)", mem_sampled, self.resource_boundaries.mem_limit) - elif mem_sampled <= 0: - actionlogger.debug("Sampled memory for %s is %.2f <= 0, setting to previously assigned value %.2f", self.name, mem_sampled, self.mem_assigned) - mem_sampled = self.mem_assigned - - for res in self.related_tasks: - if res.is_done or res.booked: - continue - res.cpu_assigned = cpu_sampled * res.cpu_relative - res.mem_assigned = mem_sampled - # This task has been run before, stay optimistic and limit the resources in case the sampled ones exceed limits - res.limit_resources() - - -class ResourceManager: - """ - Central class to manage resources - - - CPU limits - - MEM limits - - Semaphores - - Entrypoint to set and to query for resources to be updated. - - Can be asked whether a certain task can be run under current resource usage. - Book and unbook resources. - """ - def __init__(self, cpu_limit, mem_limit, procs_parallel_max=100, dynamic_resources=False, optimistic_resources=False): - """ - Initialise members with defaults - """ - # hold TaskResources of all tasks - self.resources = [] - - # helper dictionaries holding common objects which will be distributed to single TaskResources objects - # to avoid further lookup and at the same time to share the same common objects - self.resources_related_tasks_dict = {} - self.semaphore_dict = {} - - # one common object that holds global resource settings such as CPU and MEM limits - self.resource_boundaries = ResourceBoundaries(cpu_limit, mem_limit, dynamic_resources, optimistic_resources) - - # register resources that are booked under default nice value - self.cpu_booked = 0 - self.mem_booked = 0 - # number of tasks currently booked - self.n_procs = 0 - - # register resources that are booked under high nice value - self.cpu_booked_backfill = 0 - self.mem_booked_backfill = 0 - # number of tasks currently booked under high nice value - self.n_procs_backfill = 0 - - # the maximum number of tasks that run at the same time - self.procs_parallel_max = procs_parallel_max - - # get the default nice value of this python script - self.nice_default = os.nice(0) - # add 19 to get nice value of low-priority tasks - self.nice_backfill = self.nice_default + 19 - - def add_task_resources(self, name, related_tasks_name, cpu, cpu_relative, mem, semaphore_string=None): - """ - Construct and Add a new TaskResources object - """ - resources = TaskResources(len(self.resources), name, cpu, cpu_relative, mem, self.resource_boundaries) - if not resources.is_within_limits() and not self.resource_boundaries.optimistic_resources: - # exit if we don't dare to try - print(f"Resources of task {name} are exceeding the boundaries.\nCPU: {cpu} (estimate) vs. {self.resource_boundaries.cpu_limit} (boundary)\nMEM: {mem} (estimated) vs. {self.resource_boundaries.mem_limit} (boundary).") - print("Pass --optimistic-resources to the runner to attempt the run anyway.") - exit(1) - # if we get here, either all is good or the user decided to be optimistic and we limit the resources, by default to the given CPU and mem limits. - resources.limit_resources() - - self.resources.append(resources) - # do the following to have the same Semaphore object for all corresponding TaskResources so that we do not need a lookup - if semaphore_string: - if semaphore_string not in self.semaphore_dict: - self.semaphore_dict[semaphore_string] = Semaphore() - resources.semaphore = self.semaphore_dict[semaphore_string] - - # do the following to give each TaskResources a list of the related tasks so we do not need an additional lookup - if related_tasks_name: - if related_tasks_name not in self.resources_related_tasks_dict: - # assigned list is [valid top be used, list of CPU, list of MEM, list of walltimes of each related task, list of processes that ran in parallel on average, list of taken CPUs, list of assigned CPUs, list of tasks finished in the meantime] - self.resources_related_tasks_dict[related_tasks_name] = [] - self.resources_related_tasks_dict[related_tasks_name].append(resources) - resources.related_tasks = self.resources_related_tasks_dict[related_tasks_name] - - def add_monitored_resources(self, tid, time_delta_since_start, cpu, mem): - self.resources[tid].add(time_delta_since_start, cpu, mem) - - def book(self, tid, nice_value): - """ - Book the resources of this task with given nice value - - The final nice value is determined by the final submission and could be different. - This can happen if the nice value should have been changed while that is not allowed by the system. - """ - res = self.resources[tid] - # take the nice value that was previously assigned when resources where checked last time - previous_nice_value = res.nice_value - - if previous_nice_value is None: - # this has not been checked ever if it was ok to be submitted - actionlogger.warning("Task ID %d has never been checked for resources. Treating as backfill", tid) - nice_value = self.nice_backfill - elif res.nice_value != nice_value: - actionlogger.warning("Task ID %d has was last time checked for a different nice value (%d) but is now submitted with (%d).", tid, res.nice_value, nice_value) - - res.nice_value = nice_value - res.booked = True - if res.semaphore is not None: - res.semaphore.lock() - if nice_value != self.nice_default: - self.n_procs_backfill += 1 - self.cpu_booked_backfill += res.cpu_assigned - self.mem_booked_backfill += res.mem_assigned - return - self.n_procs += 1 - self.cpu_booked += res.cpu_assigned - self.mem_booked += res.mem_assigned - - def unbook(self, tid): - """ - Unbook the reources of this task - """ - res = self.resources[tid] - res.booked = False - if self.resource_boundaries.dynamic_resources: - res.sample_resources() - if res.semaphore is not None: - res.semaphore.unlock() - if res.nice_value != self.nice_default: - self.cpu_booked_backfill -= res.cpu_assigned - self.mem_booked_backfill -= res.mem_assigned - self.n_procs_backfill -= 1 - if self.n_procs_backfill <= 0: - self.cpu_booked_backfill = 0 - self.mem_booked_backfill = 0 - return - self.n_procs -= 1 - self.cpu_booked -= res.cpu_assigned - self.mem_booked -= res.mem_assigned - if self.n_procs <= 0: - self.cpu_booked = 0 - self.mem_booked = 0 - - def ok_to_submit(self, tids): - """ - This generator yields the tid and nice value tuple from the list of task ids that should be checked - """ - tids_copy = tids.copy() - - def ok_to_submit_default(res): - """ - Return default nice value if conditions are met, None otherwise - """ - # analyse CPU - okcpu = (self.cpu_booked + res.cpu_assigned <= self.resource_boundaries.cpu_limit) - # analyse MEM - okmem = (self.mem_booked + res.mem_assigned <= self.resource_boundaries.mem_limit) - actionlogger.debug ('Condition check --normal-- for ' + str(res.tid) + ':' + res.name + ' CPU ' + str(okcpu) + ' MEM ' + str(okmem)) - return self.nice_default if (okcpu and okmem) else None - - def ok_to_submit_backfill(res, backfill_cpu_factor=1.5, backfill_mem_factor=1.5): - """ - Return backfill nice value if conditions are met, None otherwise - """ - if self.n_procs_backfill >= args.n_backfill: - return None - - if res.cpu_assigned > 0.9 * self.resource_boundaries.cpu_limit or res.mem_assigned / self.resource_boundaries.cpu_limit >= 1900: - return None - - # analyse CPU - okcpu = (self.cpu_booked_backfill + res.cpu_assigned <= self.resource_boundaries.cpu_limit) - okcpu = okcpu and (self.cpu_booked + self.cpu_booked_backfill + res.cpu_assigned <= backfill_cpu_factor * self.resource_boundaries.cpu_limit) - # analyse MEM - okmem = (self.mem_booked + self.mem_booked_backfill + res.mem_assigned <= backfill_mem_factor * self.resource_boundaries.mem_limit) - actionlogger.debug ('Condition check --backfill-- for ' + str(res.tid) + ':' + res.name + ' CPU ' + str(okcpu) + ' MEM ' + str(okmem)) - - return self.nice_backfill if (okcpu and okmem) else None - - if self.n_procs + self.n_procs_backfill >= self.procs_parallel_max: - # in this case, nothing can be done - return - - for ok_to_submit_impl, should_break in ((ok_to_submit_default, True), (ok_to_submit_backfill, False)): - tid_index = 0 - while tid_index < len(tids_copy): - - tid = tids_copy[tid_index] - res = self.resources[tid] - - actionlogger.info("Setup resources for task %s, cpu: %f, mem: %f", res.name, res.cpu_assigned, res.mem_assigned) - tid_index += 1 - - if (res.semaphore is not None and res.semaphore.locked) or res.booked: - continue - - nice_value = ok_to_submit_impl(res) - if nice_value is not None: - # if we get a non-None nice value, it means that this task is good to go - res.nice_value = nice_value - # yield the tid and its assigned nice value - yield tid, nice_value - - elif should_break: - # break here if resources of the next task do not fit - break - - -def filegraph_expand_timeframes(data: dict, timeframes: set, target_namelist) -> dict: - """ - A utility function for the fileaccess logic. Takes a template and duplicates - for the multi-timeframe structure. - """ - tf_entries = [ - entry for entry in data.get("file_report", []) - if re.match(r"^\./tf\d+/", entry["file"]) - ] - - result = {} - for i in timeframes: - if i == -1: - continue - # Deepcopy to avoid modifying original - new_entries = deepcopy(tf_entries) - for entry in new_entries: - # Fix filepath - entry["file"] = re.sub(r"^\./tf\d+/", f"./tf{i}/", entry["file"]) - # Fix written_by and read_by (preserve prefix, change numeric suffix) - entry["written_by"] = [ - re.sub(r"_\d+$", f"_{i}", w) for w in entry["written_by"] - ] - # for now we mark some files as keep if they are written - # by a target in the runner targetlist. TODO: Add other mechanisms - # to ask for file keeping (such as via regex or the like) - for e in entry["written_by"]: - if e in target_namelist: - entry["keep"] = True - entry["read_by"] = [ - re.sub(r"_\d+$", f"_{i}", r) for r in entry["read_by"] - ] - result[f"timeframe-{i}"] = new_entries - - return result - - - -class WorkflowExecutor: - # Constructor - def __init__(self, workflowfile, args, jmax=100): - self.args=args - self.is_productionmode = args.production_mode == True # os.getenv("ALIEN_PROC_ID") != None - self.workflowfile = workflowfile - self.workflowspec = load_json(workflowfile) - self.globalinit = self.extract_global_environment(self.workflowspec) # initialize global environment settings - for e in self.globalinit['env']: - if os.environ.get(e, None) == None: - value = self.globalinit['env'][e] - actionlogger.info("Applying global environment from init section " + str(e) + " : " + str(value)) - os.environ[e] = str(value) - - # only keep those tasks that are necessary to be executed based on user's filters - self.full_target_namelist = [] - self.workflowspec, self.full_target_namelist = filter_workflow(self.workflowspec, args.target_tasks, args.target_labels) - - if not self.workflowspec['stages']: - if args.target_tasks: - print ('Apparently some of the chosen target tasks are not in the workflow') - exit (0) - print ('Workflow is empty. Nothing to do') - exit (0) - - # construct the DAG, compute task weights - workflow = build_dag_properties(self.workflowspec) - if args.visualize_workflow: - draw_workflow(self.workflowspec) - self.possiblenexttask = workflow['nexttasks'] - self.taskweights = workflow['weights'] - self.topological_orderings = workflow['topological_ordering'] - self.taskuniverse = [ l['name'] for l in self.workflowspec['stages'] ] - # construct task ID <-> task name lookup - self.idtotask = [ 0 for _ in self.taskuniverse ] - self.tasktoid = {} - self.idtotf = [ l['timeframe'] for l in self.workflowspec['stages'] ] - for i, name in enumerate(self.taskuniverse): - self.tasktoid[name]=i - self.idtotask[i]=name - - if args.update_resources: - update_resource_estimates(self.workflowspec, args.update_resources) - - # construct the object that is in charge of resource management... - self.resource_manager = ResourceManager(args.cpu_limit, args.mem_limit, args.maxjobs, args.dynamic_resources, args.optimistic_resources) - for task in self.workflowspec['stages']: - # ...and add all initial resource estimates - global_task_name = self.get_global_task_name(task["name"]) - try: - cpu_relative = float(task["resources"]["relative_cpu"]) - except TypeError: - cpu_relative = 1 - self.resource_manager.add_task_resources(task["name"], global_task_name, float(task["resources"]["cpu"]), cpu_relative, float(task["resources"]["mem"]), task.get("semaphore")) - - self.procstatus = { tid:'ToDo' for tid in range(len(self.workflowspec['stages'])) } - self.taskneeds= { t:set(self.getallrequirements(t)) for t in self.taskuniverse } - self.stoponfailure = not args.keep_going - print ("Stop on failure ",self.stoponfailure) - - self.scheduling_iteration = 0 # count how often it was tried to schedule new tasks - self.process_list = [] # list of currently scheduled tasks with normal priority - self.backfill_process_list = [] # list of curently scheduled tasks with low backfill priority (not sure this is needed) - self.pid_to_psutilsproc = {} # cache of putilsproc for resource monitoring - self.pid_to_files = {} # we can auto-detect what files are produced by which task (at least to some extent) - self.pid_to_connections = {} # we can auto-detect what connections are opened by which task (at least to some extent) - signal.signal(signal.SIGINT, self.SIGHandler) - signal.siginterrupt(signal.SIGINT, False) - self.internalmonitorcounter = 0 # internal use - self.internalmonitorid = 0 # internal use - self.tids_marked_toretry = [] # sometimes we might want to retry a failed task (simply because it was "unlucky") and we put them here - self.retry_counter = [ 0 for tid in range(len(self.taskuniverse)) ] # we keep track of many times retried already - self.task_retries = [ self.workflowspec['stages'][tid].get('retry_count',0) for tid in range(len(self.taskuniverse)) ] # the per task specific "retry" number -> needs to be parsed from the JSON - - self.alternative_envs = {} # mapping of taskid to alternative software envs (to be applied on a per-task level) - # init alternative software environments - self.init_alternative_software_environments() - - # initialize container to keep track of file-task relationsships - self.file_removal_candidates = {} - self.do_early_file_removal = False - self.timeframeset = set([ task["timeframe"] for task in self.workflowspec['stages'] ]) - if args.remove_files_early != "": - with open(args.remove_files_early) as f: - filegraph_data = json.load(f) - self.do_early_file_removal = True - self.file_removal_candidates = filegraph_expand_timeframes(filegraph_data, self.timeframeset, self.full_target_namelist) - - def apply_global_env(self, environ_dict): - for e in self.globalinit['env']: - if environ_dict.get(e, None) == None: - value = self.globalinit['env'][e] - actionlogger.info("Applying global environment from init section " + str(e) + " : " + str(value)) - environ_dict[e] = str(value) - - def perform_early_file_removal(self, taskids): - """ - This function checks which files can be deleted upon completion of task - and optionally does so. - """ - - def remove_if_exists(filepath: str) -> None: - """ - Check if a file exists, and remove it if found. - """ - if os.path.exists(filepath): - fsize = os.path.getsize(filepath) - os.remove(filepath) - actionlogger.info(f"Removing {filepath} since no longer needed. Freeing {fsize/1024./1024.} MB.") - return True - - return False - - def remove_for_task_id(taskname, file_dict, timeframe_id, listofalltimeframes): - marked_for_removal = [] - - timeframestoscan = [ timeframe_id ] - if timeframe_id == -1: - timeframestoscan = [ i for i in listofalltimeframes if i != -1 ] - - # TODO: Note that this traversal of files is not certainly not optimal - # We should (and will) keep an mapping of tasks->potential files and just - # scan these. This is already provided by the FileIOGraph analysis tool. - for tid in timeframestoscan: - for i,file_entry in enumerate(file_dict[f"timeframe-{tid}"]): - filename = file_entry['file'] - read_by = file_entry['read_by'] - written_by = file_entry['written_by'] - if taskname in read_by: - file_entry['read_by'].remove(taskname) - if taskname in written_by: - file_entry['written_by'].remove(taskname) - - # TODO: in principle the written_by criterion might not be needed - if len(file_entry['read_by']) == 0 and len(file_entry['written_by']) == 0 and file_entry.get('keep', False) == False: - # the filename mentioned here is no longer needed and we can remove it - # make sure it is there and then delete it - if remove_if_exists(filename): - # also take out the file entry from the dict altogether - marked_for_removal.append(file_entry) - - #for k in marked_for_removal: - # file_dict[f"timeframe-{tid}"].remove(k) - - for tid in taskids: - taskname = self.idtotask[tid] - timeframe_id = self.idtotf[tid] - remove_for_task_id(taskname, self.file_removal_candidates, timeframe_id, self.timeframeset) - - - def SIGHandler(self, signum, frame): - """ - basically forcing shut down of all child processes - """ - actionlogger.info("Signal " + str(signum) + " caught") - try: - procs = psutil.Process().children(recursive=True) - except (psutil.NoSuchProcess): - pass - except (psutil.AccessDenied, PermissionError): - procs = getChildProcs(os.getpid()) - - for p in procs: - actionlogger.info("Terminating " + str(p)) - try: - p.terminate() - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - - _, alive = psutil.wait_procs(procs, timeout=3) - for p in alive: - try: - actionlogger.info("Killing " + str(p)) - p.kill() - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - - exit (1) - - def extract_global_environment(self, workflowspec): - """ - Checks if the workflow contains a dedicated init task - defining a global environment. Extract information and remove from workflowspec. - """ - init_index = 0 # this has to be the first task in the workflow - globalenv = {} - initcmd = None - if workflowspec['stages'][init_index]['name'] == '__global_init_task__': - env = workflowspec['stages'][init_index].get('env', None) - if env != None: - globalenv = { e : env[e] for e in env } - cmd = workflowspec['stages'][init_index].get('cmd', None) - if cmd != 'NO-COMMAND': - initcmd = cmd - - del workflowspec['stages'][init_index] - - return {"env" : globalenv, "cmd" : initcmd } - - def execute_globalinit_cmd(self, cmd): - actionlogger.info("Executing global setup cmd " + str(cmd)) - # perform the global init command (think of cleanup/setup things to be done in any case) - p = subprocess.Popen(['/bin/bash','-c', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = p.communicate() - - # Check if the command was successful (return code 0) - if p.returncode == 0: - actionlogger.info(stdout.decode()) - else: - # this should be an error - actionlogger.error("Error executing global init function") - return False - return True - - def get_global_task_name(self, name): - """ - Get the global task name - - Tasks are related if only the suffix _ is different - """ - tokens = name.split("_") - try: - int(tokens[-1]) - return "_".join(tokens[:-1]) - except ValueError: - pass - return name - - def getallrequirements(self, task_name): - """ - get all requirement of a task by its name - """ - l=[] - for required_task_name in self.workflowspec['stages'][self.tasktoid[task_name]]['needs']: - l.append(required_task_name) - l=l+self.getallrequirements(required_task_name) - return l - - def get_logfile(self, tid): - """ - O2 taskwrapper logs task stdout and stderr to logfile .log - Get its exact path based on task ID - """ - # determines the logfile name for this task - name = self.workflowspec['stages'][tid]['name'] - workdir = self.workflowspec['stages'][tid]['cwd'] - return os.path.join(workdir, f"{name}.log") - - def get_done_filename(self, tid): - """ - O2 taskwrapper leaves .log_done after a task has successfully finished - Get its exact path based on task ID - """ - return f"{self.get_logfile(tid)}_done" - - def get_resources_filename(self, tid): - """ - O2 taskwrapper leaves .log_time after a task is done - Get its exact path based on task ID - """ - return f"{self.get_logfile(tid)}_time" - - # removes the done flag from tasks that need to be run again - def remove_done_flag(self, listoftaskids): - """ - Remove .log_done files to given task IDs - """ - for tid in listoftaskids: - done_filename = self.get_done_filename(tid) - name=self.workflowspec['stages'][tid]['name'] - if args.dry_run: - print ("Would mark task " + name + " as to be done again") - else: - print ("Marking task " + name + " as to be done again") - if os.path.exists(done_filename) and os.path.isfile(done_filename): - os.remove(done_filename) - - # submits a task as subprocess and records Popen instance - def submit(self, tid, nice): - """ - Submit a task - - 1. if needed, construct working directory if it does not yet exist - 2. update lookup structures flagging the task as being run - 3. set specific environment if requested for task - 4. construct psutil.Process from command line - 4.1 adjust the niceness of that process if requested - 5. return psutil.Process object - """ - actionlogger.debug("Submitting task " + str(self.idtotask[tid]) + " with nice value " + str(nice)) - c = self.workflowspec['stages'][tid]['cmd'] - workdir = self.workflowspec['stages'][tid]['cwd'] - if workdir: - if os.path.exists(workdir) and not os.path.isdir(workdir): - actionlogger.error('Cannot create working dir ... some other resource exists already') - return None - - if not os.path.isdir(workdir): - os.makedirs(workdir) - - self.procstatus[tid]='Running' - if args.dry_run: - drycommand="echo \' " + str(self.scheduling_iteration) + " : would do " + str(self.workflowspec['stages'][tid]['name']) + "\'" - return psutil.Popen(['/bin/bash','-c',drycommand], cwd=workdir) - - taskenv = os.environ.copy() - # apply specific (non-default) software version, if any - # (this was setup earlier) - alternative_env = self.alternative_envs.get(tid, None) - if alternative_env != None and len(alternative_env) > 0: - actionlogger.info('Applying alternative software environment to task ' + self.idtotask[tid]) - if alternative_env.get('TERM') != None: - # the environment is a complete environment - taskenv = {} - taskenv = alternative_env - else: - for entry in alternative_env: - # overwrite what is present in default - taskenv[entry] = alternative_env[entry] - - # add task specific environment - if self.workflowspec['stages'][tid].get('env')!=None: - taskenv.update(self.workflowspec['stages'][tid]['env']) - - # add global workflow environment - self.apply_global_env(taskenv) - - if os.environ.get('PIPELINE_RUNNER_DUMP_TASKENVS') != None: - envfilename = "taskenv_" + str(tid) + ".log" - with open(envfilename, "w") as file: - json.dump(taskenv, file, indent=2) - - p = psutil.Popen(['/bin/bash','-c',c], cwd=workdir, env=taskenv) - try: - p.nice(nice) - except (psutil.NoSuchProcess, psutil.AccessDenied): - actionlogger.error('Couldn\'t set nice value of ' + str(p.pid) + ' to ' + str(nice)) - - return p - - def ok_to_skip(self, tid): - """ - Decide if task can be skipped based on existence of .log_done - """ - done_filename = self.get_done_filename(tid) - if os.path.exists(done_filename) and os.path.isfile(done_filename): - return True - return False - - def try_job_from_candidates(self, taskcandidates, finished): - """ - Try to schedule next tasks - - Args: - taskcandidates: list - list of possible tasks that can be submitted - finished: list - empty list that will be filled with IDs of tasks that were finished in the meantime - """ - self.scheduling_iteration = self.scheduling_iteration + 1 - - # remove "done / skippable" tasks immediately - for tid in taskcandidates.copy(): # <--- the copy is important !! otherwise this loop is not doing what you think - if self.ok_to_skip(tid): - finished.append(tid) - taskcandidates.remove(tid) - actionlogger.info("Skipping task " + str(self.idtotask[tid])) - - # if tasks_skipped: - # return # ---> we return early in order to preserve some ordering (the next candidate tried should be daughters of skipped jobs) - # get task ID and proposed niceness from generator - for (tid, nice_value) in self.resource_manager.ok_to_submit(taskcandidates): - actionlogger.debug ("trying to submit " + str(tid) + ':' + str(self.idtotask[tid])) - if p := self.submit(tid, nice_value): - # explicitly set the nice value here from the process again because it might happen that submit could not change the niceness - # so we let the ResourceManager know what the final niceness is - self.resource_manager.book(tid, p.nice()) - self.process_list.append((tid,p)) - taskcandidates.remove(tid) - # minimal delay - time.sleep(0.1) - - def stop_pipeline_and_exit(self, process_list): - # kill all remaining jobs - for p in process_list: - p[1].kill() - - exit(1) - - - def monitor(self, process_list): - """ - Go through all running tasks and get their current resources - - Resources are summed up for tasks and all their children - - Pass CPU, PSS, USS, niceness, current time to metriclogger - - Warn if overall PSS exceeds assigned memory limit - """ - self.internalmonitorcounter+=1 - if self.internalmonitorcounter % 5 != 0: - return - - self.internalmonitorid+=1 - - globalCPU=0. - globalPSS=0. - resources_per_task = {} - - # On a global level, we are interested in total disc space used (not differential in tasks) - # We can call system "du" as the fastest impl - def disk_usage_du(path: str) -> int: - """Use system du to get total size in bytes.""" - out = subprocess.check_output(['du', '-sb', path], text=True) - return int(out.split()[0]) - - disc_usage = -1 - if os.getenv("MONITOR_DISC_USAGE"): - disc_usage = disk_usage_du(os.getcwd()) / 1024. / 1024 # in MB - - for tid, proc in process_list: - - # proc is Popen object - pid=proc.pid - if self.pid_to_files.get(pid)==None: - self.pid_to_files[pid]=set() - self.pid_to_connections[pid]=set() - try: - psutilProcs = [ proc ] - # use psutil for CPU measurement - psutilProcs = psutilProcs + proc.children(recursive=True) - except (psutil.NoSuchProcess): - continue - - except (psutil.AccessDenied, PermissionError): - psutilProcs = psutilProcs + getChildProcs(pid) - - # accumulate total metrics (CPU, memory) - totalCPU = 0. - totalPSS = 0. - totalSWAP = 0. - totalUSS = 0. - for p in psutilProcs: - """ - try: - for f in p.open_files(): - self.pid_to_files[pid].add(str(f.path)+'_'+str(f.mode)) - for f in p.connections(kind="all"): - remote=f.raddr - if remote==None: - remote='none' - self.pid_to_connections[pid].add(str(f.type)+"_"+str(f.laddr)+"_"+str(remote)) - except Exception: - pass - """ - thispss=0 - thisuss=0 - # MEMORY part - try: - fullmem=p.memory_full_info() - thispss=getattr(fullmem,'pss',0) #<-- pss not available on MacOS - totalPSS=totalPSS + thispss - totalSWAP=totalSWAP + fullmem.swap - thisuss=fullmem.uss - totalUSS=totalUSS + thisuss - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - - # CPU part - # fetch existing proc or insert - cachedproc = self.pid_to_psutilsproc.get(p.pid) - if cachedproc!=None: - try: - thiscpu = cachedproc.cpu_percent(interval=None) - except (psutil.NoSuchProcess, psutil.AccessDenied): - thiscpu = 0. - totalCPU = totalCPU + thiscpu - # thisresource = {'iter':self.internalmonitorid, 'pid': p.pid, 'cpu':thiscpu, 'uss':thisuss/1024./1024., 'pss':thispss/1024./1024.} - # metriclogger.info(thisresource) - else: - self.pid_to_psutilsproc[p.pid] = p - try: - self.pid_to_psutilsproc[p.pid].cpu_percent() - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - - time_delta = int((time.perf_counter() - self.start_time) * 1000) - totalUSS = totalUSS / 1024 / 1024 - totalPSS = totalPSS / 1024 / 1024 - nice_value = proc.nice() - resources_per_task[tid]={'iter':self.internalmonitorid, - 'name':self.idtotask[tid], - 'cpu':totalCPU, - 'uss':totalUSS, - 'pss':totalPSS, - 'nice':nice_value, - 'swap':totalSWAP, - 'label':self.workflowspec['stages'][tid]['labels'], - 'disc': disc_usage} - self.resource_manager.add_monitored_resources(tid, time_delta, totalCPU / 100, totalPSS) - if nice_value == self.resource_manager.nice_default: - globalCPU += totalCPU - globalPSS += totalPSS - - metriclogger.info(resources_per_task[tid]) - send_webhook(self.args.webhook, resources_per_task) - - if globalPSS > self.resource_manager.resource_boundaries.mem_limit: - metriclogger.info('*** MEMORY LIMIT PASSED !! ***') - # --> We could use this for corrective actions such as killing jobs currently back-filling - # (or better hibernating) - - def waitforany(self, process_list, finished, failingtasks): - """ - Loop through all submitted tasks and check if they are finished - - 1. If process is still running, do nothing - 2. If process is finished, get its return value, update finished and failingtasks lists - 2.1 unbook resources - 2.2 add taken resources and pass the to ResourceManager - """ - failuredetected = False - failingpids = [] - if len(process_list)==0: - return False - - for p in list(process_list): - pid = p[1].pid - tid = p[0] # the task id of this process - returncode = 0 - if not self.args.dry_run: - returncode = p[1].poll() - if returncode!=None: - actionlogger.info ('Task ' + str(pid) + ' ' + str(tid)+':'+str(self.idtotask[tid]) + ' finished with status ' + str(returncode)) - # account for cleared resources - self.resource_manager.unbook(tid) - self.procstatus[tid]='Done' - finished.append(tid) - #self.validate_resources_running(tid) - process_list.remove(p) - if returncode != 0: - print (str(self.idtotask[tid]) + ' failed ... checking retry') - # we inspect if this is something "unlucky" which could be resolved by a simple resubmit - if self.is_worth_retrying(tid) and ((self.retry_counter[tid] < int(args.retry_on_failure)) or (self.retry_counter[tid] < int(self.task_retries[tid]))): - print (str(self.idtotask[tid]) + ' to be retried') - actionlogger.info ('Task ' + str(self.idtotask[tid]) + ' failed but marked to be retried ') - self.tids_marked_toretry.append(tid) - self.retry_counter[tid] += 1 - - else: - failuredetected = True - failingpids.append(pid) - failingtasks.append(tid) - - if failuredetected and self.stoponfailure: - actionlogger.info('Stoping pipeline due to failure in stages with PID ' + str(failingpids)) - # self.analyse_files_and_connections() - if self.args.stdout_on_failure: - self.cat_logfiles_tostdout(failingtasks) - self.send_checkpoint(failingtasks, self.args.checkpoint_on_failure) - self.stop_pipeline_and_exit(process_list) - - # empty finished means we have to wait more - return len(finished)==0 - - def is_worth_retrying(self, tid): - # This checks for some signatures in logfiles that indicate that a retry of this task - # might have a chance. - # Ideally, this should be made user configurable. Either the user could inject a lambda - # or a regular expression to use. For now we just put a hard coded list - logfile = self.get_logfile(tid) - - return True #! --> for now we just retry tasks a few times - - # 1) ZMQ_EVENT + interrupted system calls (DPL bug during shutdown) - # Not sure if grep is faster than native Python text search ... - # status = os.system('grep "failed setting ZMQ_EVENTS" ' + logfile + ' &> /dev/null') - # if os.WEXITSTATUS(status) == 0: - # return True - - # return False - - - def cat_logfiles_tostdout(self, taskids): - # In case of errors we can cat the logfiles for this taskname - # to stdout. Assuming convention that "taskname" translates to "taskname.log" logfile. - for tid in taskids: - logfile = self.get_logfile(tid) - if os.path.exists(logfile): - print (' ----> START OF LOGFILE ', logfile, ' -----') - os.system('cat ' + logfile) - print (' <---- END OF LOGFILE ', logfile, ' -----') - - def send_checkpoint(self, taskids, location): - # Makes a tarball containing all files in the base dir - # (timeframe independent) and the dir with corrupted timeframes - # and copies it to a specific ALIEN location. Not a core function - # just some tool get hold on error conditions appearing on the GRID. - - def get_tar_command(dir='./', flags='cf', findtype='f', filename='checkpoint.tar'): - return 'find ' + str(dir) + ' -maxdepth 1 -type ' + str(findtype) + ' -print0 | xargs -0 tar ' + str(flags) + ' ' + str(filename) - - if location != None: - print ('Making a failure checkpoint') - # let's determine a filename from ALIEN_PROC_ID - hostname - and PID - - aliprocid=os.environ.get('ALIEN_PROC_ID') - if aliprocid == None: - aliprocid = 0 - - fn='pipeline_checkpoint_ALIENPROC' + str(aliprocid) + '_PID' + str(os.getpid()) + '_HOST' + socket.gethostname() + '.tar' - actionlogger.info("Checkpointing to file " + fn) - tarcommand = get_tar_command(filename=fn) - actionlogger.info("Taring " + tarcommand) - - # create a README file with instruction on how to use checkpoint - readmefile=open('README_CHECKPOINT_PID' + str(os.getpid()) + '.txt','w') - - for tid in taskids: - taskspec = self.workflowspec['stages'][tid] - name = taskspec['name'] - readmefile.write('Checkpoint created because of failure in task ' + name + '\n') - readmefile.write('In order to reproduce with this checkpoint, do the following steps:\n') - readmefile.write('a) setup the appropriate O2sim environment using alienv\n') - readmefile.write('b) run: $O2DPG_ROOT/MC/bin/o2_dpg_workflow_runner.py -f workflow.json -tt ' + name + '$ --retry-on-failure 0\n') - readmefile.close() - - # first of all the base directory - os.system(tarcommand) - - # then we add stuff for the specific timeframes ids if any - for tid in taskids: - taskspec = self.workflowspec['stages'][tid] - directory = taskspec['cwd'] - if directory != "./": - tarcommand = get_tar_command(dir=directory, flags='rf', filename=fn) - actionlogger.info("Tar command is " + tarcommand) - os.system(tarcommand) - # same for soft links - tarcommand = get_tar_command(dir=directory, flags='rf', findtype='l', filename=fn) - actionlogger.info("Tar command is " + tarcommand) - os.system(tarcommand) - - # prepend file:/// to denote local file - fn = "file://" + fn - actionlogger.info("Local checkpoint file is " + fn) - - # location needs to be an alien path of the form alien:///foo/bar/ - copycommand='alien.py cp ' + fn + ' ' + str(location) + '@disk:1' - actionlogger.info("Copying to alien " + copycommand) - os.system(copycommand) - - def init_alternative_software_environments(self): - """ - Initialises alternative software environments for specific tasks, if there - is an annotation in the workflow specificiation. - """ - - environment_cache = {} - # go through all the tasks once and setup environment - for taskid in range(len(self.workflowspec['stages'])): - packagestr = self.workflowspec['stages'][taskid].get("alternative_alienv_package") - if packagestr == None: - continue - - if environment_cache.get(packagestr) == None: - environment_cache[packagestr] = get_alienv_software_environment(packagestr) - - self.alternative_envs[taskid] = environment_cache[packagestr] - - - def analyse_files_and_connections(self): - for p,s in self.pid_to_files.items(): - for f in s: - print("F" + str(f) + " : " + str(p)) - for p,s in self.pid_to_connections.items(): - for c in s: - print("C" + str(c) + " : " + str(p)) - #print(str(p) + " CONS " + str(c)) - try: - # check for intersections - for p1, s1 in self.pid_to_files.items(): - for p2, s2 in self.pid_to_files.items(): - if p1!=p2: - if type(s1) is set and type(s2) is set: - if len(s1)>0 and len(s2)>0: - try: - inters = s1.intersection(s2) - except Exception: - print ('Exception during intersect inner') - pass - if (len(inters)>0): - print ('FILE Intersection ' + str(p1) + ' ' + str(p2) + ' ' + str(inters)) - # check for intersections - for p1, s1 in self.pid_to_connections.items(): - for p2, s2 in self.pid_to_connections.items(): - if p1!=p2: - if type(s1) is set and type(s2) is set: - if len(s1)>0 and len(s2)>0: - try: - inters = s1.intersection(s2) - except Exception: - print ('Exception during intersect inner') - pass - if (len(inters)>0): - print ('CON Intersection ' + str(p1) + ' ' + str(p2) + ' ' + str(inters)) - - # check for intersections - #for p1, s1 in slf.pid_to_files.items(): - # for p2, s2 in self.pid_to_files.items(): - # if p1!=p2 and len(s1.intersection(s2))!=0: - # print ('Intersection found files ' + str(p1) + ' ' + str(p2) + ' ' + s1.intersection(s2)) - except Exception as e: - exc_type, exc_obj, exc_tb = sys.exc_info() - fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] - print(exc_type, fname, exc_tb.tb_lineno) - print('Exception during intersect outer') - pass - - def is_good_candidate(self, candid, finishedtasks): - if self.procstatus[candid] != 'ToDo': - return False - needs = set([self.tasktoid[t] for t in self.taskneeds[self.idtotask[candid]]]) - if set(finishedtasks).intersection(needs) == needs: - return True - return False - - def emit_code_for_task(self, tid, lines): - actionlogger.debug("Submitting task " + str(self.idtotask[tid])) - taskspec = self.workflowspec['stages'][tid] - c = taskspec['cmd'] - workdir = taskspec['cwd'] - env = taskspec.get('env') - # in general: - # try to make folder - lines.append('[ ! -d ' + workdir + ' ] && mkdir ' + workdir + '\n') - # cd folder - lines.append('cd ' + workdir + '\n') - # set local environment - if env!=None: - for e in env.items(): - lines.append('export ' + e[0] + '=' + str(e[1]) + '\n') - # do command - lines.append(c + '\n') - # unset local environment - if env!=None: - for e in env.items(): - lines.append('unset ' + e[0] + '\n') - - # cd back - lines.append('cd $OLDPWD\n') - - - # produce a bash script that runs workflow standalone - def produce_script(self, filename): - # pick one of the correct task orderings - taskorder = self.topological_orderings[0] - outF = open(filename, "w") - - lines=[] - # header - lines.append('#!/usr/bin/env bash\n') - lines.append('#THIS FILE IS AUTOGENERATED\n') - lines.append('export JOBUTILS_SKIPDONE=ON\n') - - # we record the global environment setting - # in particular to capture global workflow initialization - lines.append('#-- GLOBAL INIT SECTION FROM WORKFLOW --\n') - for e in self.globalinit['env']: - lines.append('export ' + str(e) + '=' + str(self.globalinit['env'][e]) + '\n') - lines.append('#-- TASKS FROM WORKFLOW --\n') - for tid in taskorder: - print ('Doing task ' + self.idtotask[tid]) - self.emit_code_for_task(tid, lines) - - outF.writelines(lines) - outF.close() - - def production_endoftask_hook(self, tid): - # Executes a hook at end of a successful task, meant to be used in GRID productions. - # For the moment, archiving away log files, done + time files from jobutils. - # TODO: In future this may be much more generic tasks such as dynamic cleanup of intermediate - # files (when they are no longer needed). - # TODO: Care must be taken with the continue feature as `_done` files are stored elsewhere now - actionlogger.info("Cleaning up log files for task " + str(tid)) - logf = self.get_logfile(tid) - donef = self.get_done_filename(tid) - timef = logf + "_time" - - # add to tar file archive - tf = tarfile.open(name="pipeline_log_archive.log.tar", mode='a') - if tf != None: - tf.add(logf) - tf.add(donef) - tf.add(timef) - tf.close() - - # remove original file - os.remove(logf) - os.remove(donef) - os.remove(timef) - - # print error message when no progress can be made - def noprogress_errormsg(self): - # TODO: rather than writing this out here; refer to the documentation discussion this? - msg = """Scheduler runtime error: The scheduler is not able to make progress although we have a non-zero candidate set. - -Explanation: This is typically the case because the **ESTIMATED** resource requirements for some tasks -in the workflow exceed the available number of CPU cores or the memory (as explicitely or implicitely determined from the ---cpu-limit and --mem-limit options). Often, this might be the case on laptops with <=16GB of RAM if one of the tasks -is demanding ~16GB. In this case, one could try to tell the scheduler to use a slightly higher memory limit -with an explicit --mem-limit option (for instance `--mem-limit 20000` to set to 20GB). This might work whenever the -**ACTUAL** resource usage of the tasks is smaller than anticipated (because only small test cases are run). - -In addition it might be worthwile running the workflow without this resource aware, dynamic scheduler. -This is possible by converting the json workflow into a linearized shell script and by directly executing the shell script. -Use the `--produce-script myscript.sh` option for this. -""" - print (msg, file=sys.stderr) - - def execute(self): - self.start_time = time.perf_counter() - psutil.cpu_percent(interval=None) - os.environ['JOBUTILS_SKIPDONE'] = "ON" - errorencountered = False - - def speedup_ROOT_Init(): - """initialize some env variables that speed up ROOT init - and prevent ROOT from spawning many short-lived child - processes""" - - # only do it on Linux - if platform.system() != 'Linux': - return - - if os.environ.get('ROOT_LDSYSPATH')!=None and os.environ.get('ROOT_CPPSYSINCL')!=None: - # do nothing if already defined - return - - # a) the PATH for system libraries - # search taken from ROOT TUnixSystem - cmd='LD_DEBUG=libs LD_PRELOAD=DOESNOTEXIST ls /tmp/DOESNOTEXIST 2>&1 | grep -m 1 "system search path" | sed \'s/.*=//g\' | awk \'//{print $1}\'' - proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - libpath, err = proc.communicate() - if not (args.no_rootinit_speedup == True): - print ("setting up ROOT system") - os.environ['ROOT_LDSYSPATH'] = libpath.decode() - os.environ['CLING_LDSYSPATH'] = libpath.decode() - - # b) the PATH for compiler includes needed by Cling - cmd = "LC_ALL=C c++ -xc++ -E -v /dev/null 2>&1 | sed -n '/^#include/,${/^ \\/.*++/{p}}'" - proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - incpath, err = proc.communicate() - incpaths = [ line.lstrip() for line in incpath.decode().splitlines() ] - joined = ':'.join(incpaths) - if not (args.no_rootinit_speedup == True): - actionlogger.info("Determined ROOT_CPPSYSINCL=" + joined) - os.environ['ROOT_CPPSYSINCL'] = joined - os.environ['CLING_CPPSYSINCL'] = joined - - speedup_ROOT_Init() - - # we make our own "tmp" folder - # where we can put stuff such as tmp socket files etc (for instance DPL FAIR-MQ sockets) - # (In case of running within docker/singularity, this may not be so important) - if not os.path.isdir("./.tmp"): - os.mkdir("./.tmp") - if os.environ.get('FAIRMQ_IPC_PREFIX')==None: - socketpath = os.getcwd() + "/.tmp" - actionlogger.info("Setting FAIRMQ socket path to " + socketpath) - os.environ['FAIRMQ_IPC_PREFIX'] = socketpath - - # some maintenance / init work - if args.list_tasks: - print ('List of tasks in this workflow:') - for i,t in enumerate(self.workflowspec['stages'],0): - print (t['name'] + ' (' + str(t['labels']) + ')' + ' ToDo: ' + str(not self.ok_to_skip(i))) - exit (0) - - if args.produce_script != None: - self.produce_script(args.produce_script) - exit (0) - - # execute the user-given global init cmd for this workflow - globalinitcmd = self.globalinit.get("cmd", None) - if globalinitcmd != None: - if not self.execute_globalinit_cmd(globalinitcmd): - exit (1) - - if args.rerun_from: - reruntaskfound=False - for task in self.workflowspec['stages']: - taskname=task['name'] - if re.match(args.rerun_from, taskname): - reruntaskfound=True - taskid=self.tasktoid[taskname] - self.remove_done_flag(find_all_dependent_tasks(self.possiblenexttask, taskid)) - if not reruntaskfound: - print('No task matching ' + args.rerun_from + ' found; cowardly refusing to do anything ') - exit (1) - - # ***************** - # main control loop - # ***************** - candidates = [ tid for tid in self.possiblenexttask[-1] ] - - self.process_list=[] # list of tuples of nodes ids and Popen subprocess instances - - finishedtasks=[] # global list of finished tasks - - try: - - while True: - # sort candidate list according to task weights - candidates = [ (tid, self.taskweights[tid]) for tid in candidates ] - candidates.sort(key=lambda tup: (tup[1][0],-tup[1][1])) # prefer small and same timeframes first then prefer important tasks within frameframe - # remove weights - candidates = [ tid for tid,_ in candidates ] - - finished = [] # --> to account for finished because already done or skipped - actionlogger.debug('Sorted current candidates: ' + str([(c,self.idtotask[c]) for c in candidates])) - self.try_job_from_candidates(candidates, finished) - if len(candidates) > 0 and len(self.process_list) == 0: - self.noprogress_errormsg() - send_webhook(self.args.webhook,"Unable to make further progress: Quitting") - errorencountered = True - break - - finished_from_started = [] # to account for finished when actually started - failing = [] - while self.waitforany(self.process_list, finished_from_started, failing): - if not args.dry_run: - self.monitor(self.process_list) # ---> make async to normal operation? - time.sleep(1) # <--- make this incremental (small wait at beginning) - else: - time.sleep(0.001) - - finished = finished + finished_from_started - actionlogger.debug("finished now :" + str(finished_from_started)) - finishedtasks = finishedtasks + finished - - # perform file cleanup - if self.do_early_file_removal: - self.perform_early_file_removal(finished_from_started) - - if self.is_productionmode: - # we can do some generic cleanup of finished tasks in non-interactive/GRID mode - # TODO: this can run asynchronously - for _t in finished_from_started: - self.production_endoftask_hook(_t) - - # if a task was marked "failed" and we come here (because - # we use --keep-going) ... we need to take out the pid from finished - if len(failing) > 0: - # remove these from those marked finished in order - # not to continue with their children - errorencountered = True - for t in failing: - finished = [ x for x in finished if x != t ] - finishedtasks = [ x for x in finishedtasks if x != t ] - - # if a task was marked as "retry" we simply put it back into the candidate list - if len(self.tids_marked_toretry) > 0: - # we need to remove these first of all from those marked finished - for t in self.tids_marked_toretry: - finished = [ x for x in finished if x != t ] - finishedtasks = [ x for x in finishedtasks if x != t ] - - candidates = candidates + self.tids_marked_toretry - self.tids_marked_toretry = [] - - - # new candidates - for tid in finished: - if self.possiblenexttask.get(tid)!=None: - potential_candidates=list(self.possiblenexttask[tid]) - for candid in potential_candidates: - # try to see if this is really a candidate: - if self.is_good_candidate(candid, finishedtasks) and candidates.count(candid)==0: - candidates.append(candid) - - actionlogger.debug("New candidates " + str( candidates)) - send_webhook(self.args.webhook, "New candidates " + str(candidates)) - - if len(candidates)==0 and len(self.process_list)==0: - break - except Exception as e: - exc_type, exc_obj, exc_tb = sys.exc_info() - fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] - print(exc_type, fname, exc_tb.tb_lineno) - traceback.print_exc() - print ('Cleaning up ') - - self.SIGHandler(0,0) - - endtime = time.perf_counter() - statusmsg = "success" - if errorencountered: - statusmsg = "with failures" - - print ('\n**** Pipeline done ' + statusmsg + ' (global_runtime : {:.3f}s) *****\n'.format(endtime-self.start_time)) - actionlogger.debug("global_runtime : {:.3f}s".format(endtime-self.start_time)) - return errorencountered +_HERE = os.path.dirname(os.path.realpath(__file__)) -if args.cgroup!=None: - myPID=os.getpid() - # cgroups such as /sys/fs/cgroup/cpuset//tasks - # or /sys/fs/cgroup/cpu//tasks - command="echo " + str(myPID) + f" > {args.cgroup}" - actionlogger.info(f"Try running in cgroup {args.cgroup}") - waitstatus = os.system(command) - if code := os.waitstatus_to_exitcode(waitstatus): - actionlogger.error(f"Could not apply cgroup") - exit(code) - actionlogger.info("Running in cgroup") +RUNNERS = { + "legacy": os.path.join(_HERE, "o2dpg_workflow_runner_legacy.py"), + "new": os.path.join(_HERE, os.pardir, "workflow_runner", + "o2dpg_workflow_runner.py"), +} +DEFAULT_RUNNER = "legacy" -# This starts the fanotify fileaccess monitoring process -# if asked for -o2dpg_filegraph_exec = os.getenv("O2DPG_PRODUCE_FILEGRAPH") # switches filegraph monitoring on and contains the executable name -if o2dpg_filegraph_exec: - env = os.environ.copy() - env["FILEACCESS_MON_ROOTPATH"] = os.getcwd() - env["MAXMOTHERPID"] = f"{os.getpid()}" - fileaccess_log_file_name = f"pipeline_fileaccess_{os.getpid()}.log" - fileaccess_log_file = open(fileaccess_log_file_name, "w") - fileaccess_monitor_proc = subprocess.Popen( - [o2dpg_filegraph_exec], - stdout=fileaccess_log_file, - stderr=subprocess.STDOUT, - env=env) -else: - fileaccess_monitor_proc = None +def main(): + which = os.getenv("ALIEN_O2DPG_WORKFLOW_RUNNER", DEFAULT_RUNNER).strip() + script = RUNNERS.get(which) + if script is None: + sys.exit(f"ALIEN_O2DPG_WORKFLOW_RUNNER={which!r} is not one of " + f"{', '.join(sorted(RUNNERS))}") + os.execv(sys.executable, [sys.executable, script] + sys.argv[1:]) -try: - # This is core workflow runner invocation - executor=WorkflowExecutor(args.workflowfile,jmax=int(args.maxjobs),args=args) - rc = executor.execute() -finally: - if fileaccess_monitor_proc: - fileaccess_monitor_proc.terminate() # sends SIGTERM - try: - fileaccess_monitor_proc.wait(timeout=5) - except subprocess.TimeoutExpired: - fileaccess_monitor_proc.kill() # force kill if not stopping - # now produce the final filegraph output - o2dpg_root = os.getenv("O2DPG_ROOT") - analyse_cmd = [ - sys.executable, # runs with same Python interpreter - f"{o2dpg_root}/UTILS/FileIOGraph/analyse_FileIO.py", - "--actionFile", actionlogger_file, - "--monitorFile", fileaccess_log_file_name, - "-o", f"pipeline_fileaccess_report_{os.getpid()}.json", - "--basedir", os.getcwd() ] - print (f"Producing FileIOGraph with command {analyse_cmd}") - subprocess.run(analyse_cmd, check=True) -sys.exit(rc) \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/MC/bin/o2dpg_workflow_runner_legacy.py b/MC/bin/o2dpg_workflow_runner_legacy.py new file mode 100755 index 000000000..eebde6501 --- /dev/null +++ b/MC/bin/o2dpg_workflow_runner_legacy.py @@ -0,0 +1,2004 @@ +#!/usr/bin/env python3 + +# started February 2021, sandro.wenzel@cern.ch + +import re +import subprocess +import time +import json +import logging +import os +import signal +import socket +import sys +import traceback +import platform +import tarfile +from copy import deepcopy +try: + from graphviz import Digraph + havegraphviz=True +except ImportError: + havegraphviz=False + +formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + +sys.setrecursionlimit(100000) + +import argparse +import psutil +max_system_mem=psutil.virtual_memory().total + +sys.path.append(os.path.join(os.path.dirname(__file__), '.', 'o2dpg_workflow_utils')) +from o2dpg_workflow_utils import read_workflow + +# defining command line options +parser = argparse.ArgumentParser(description='Parallel execution of a (O2-DPG) DAG data/job pipeline under resource contraints.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + +parser.add_argument('-f','--workflowfile', help='Input workflow file name', required=True) +parser.add_argument('-jmax','--maxjobs', type=int, help='Number of maximal parallel tasks.', default=100) +parser.add_argument('-k','--keep-going', action='store_true', help='Keep executing the pipeline as far possibe (not stopping on first failure)') +parser.add_argument('--dry-run', action='store_true', help='Show what you would do.') +parser.add_argument('--visualize-workflow', action='store_true', help='Saves a graph visualization of workflow.') +parser.add_argument('--target-labels', nargs='+', help='Runs the pipeline by target labels (example "TPC" or "DIGI").\ + This condition is used as logical AND together with --target-tasks.', default=[]) +parser.add_argument('-tt','--target-tasks', nargs='+', help='Runs the pipeline by target tasks (example "tpcdigi"). By default everything in the graph is run. Regular expressions supported.', default=["*"]) +parser.add_argument('--produce-script', help='Produces a shell script that runs the workflow in serialized manner and quits.') +parser.add_argument('--rerun-from', help='Reruns the workflow starting from given task (or pattern). All dependent jobs will be rerun.') +parser.add_argument('--list-tasks', help='Simply list all tasks by name and quit.', action='store_true') + +# Resources +parser.add_argument('--update-resources', dest="update_resources", help='Read resource estimates from a JSON and apply where possible.') +parser.add_argument("--dynamic-resources", dest="dynamic_resources", action="store_true", help="Update reources estimates of task based on finished related tasks") # derive resources dynamically +parser.add_argument('--optimistic-resources', dest="optimistic_resources", action="store_true", help="Try to run workflow even though resource limits might underestimate resource needs of some tasks") +parser.add_argument("--n-backfill", dest="n_backfill", type=int, default=1) +parser.add_argument('--mem-limit', help='Set memory limit as scheduling constraint (in MB)', default=0.9*max_system_mem/1024./1024, type=float) +parser.add_argument('--cpu-limit', help='Set CPU limit (core count)', default=8, type=float) +parser.add_argument('--cgroup', help='Execute pipeline under a given cgroup (e.g., 8coregrid) emulating resource constraints. This m\ +ust exist and the tasks file must be writable to with the current user.') + +# run control, webhooks +parser.add_argument('--stdout-on-failure', action='store_true', help='Print log files of failing tasks to stdout,') +parser.add_argument('--webhook', help=argparse.SUPPRESS) # log some infos to this webhook channel +parser.add_argument('--checkpoint-on-failure', help=argparse.SUPPRESS) # debug option making a debug-tarball and sending to specified address + # argument is alien-path +parser.add_argument('--retry-on-failure', help=argparse.SUPPRESS, default=0) # number of times a failing task is retried +parser.add_argument('--no-rootinit-speedup', help=argparse.SUPPRESS, action='store_true') # disable init of ROOT environment vars to speedup init/startup + +parser.add_argument('--remove-files-early', type=str, default="", help="Delete intermediate files early (using the file graph information in the given file)") + + +# Logging +parser.add_argument('--action-logfile', help='Logfilename for action logs. If none given, pipeline_action_#PID.log will be used') +parser.add_argument('--metric-logfile', help='Logfilename for metric logs. If none given, pipeline_metric_#PID.log will be used') +parser.add_argument('--production-mode', action='store_true', help='Production mode') +# will trigger special features good for non-interactive/production processing (automatic cleanup of files etc). +args = parser.parse_args() + +def setup_logger(name, log_file, level=logging.INFO): + """To setup as many loggers as you want""" + + handler = logging.FileHandler(log_file, mode='w') + handler.setFormatter(formatter) + + logger = logging.getLogger(name) + logger.setLevel(level) + logger.addHandler(handler) + + return logger + +# first file logger +actionlogger_file = ('pipeline_action_' + str(os.getpid()) + '.log', args.action_logfile)[args.action_logfile!=None] +actionlogger = setup_logger('pipeline_action_logger', actionlogger_file, level=logging.DEBUG) + +# second file logger +metriclogger = setup_logger('pipeline_metric_logger', ('pipeline_metric_' + str(os.getpid()) + '.log', args.action_logfile)[args.action_logfile!=None]) + +# Immediately log imposed memory and CPU limit as well as further useful meta info +_ , meta = read_workflow(args.workflowfile) +meta["cpu_limit"] = args.cpu_limit +meta["mem_limit"] = args.mem_limit +meta["workflow_file"] = os.path.abspath(args.workflowfile) +args.target_tasks = [f.strip('"').strip("'") for f in args.target_tasks] # strip quotes from the shell +meta["target_task"] = args.target_tasks +meta["rerun_from"] = args.rerun_from +meta["target_labels"] = args.target_labels +metriclogger.info(meta) + +# for debugging without terminal access +# TODO: integrate into standard logger +def send_webhook(hook, t): + if hook!=None: + command="curl -X POST -H 'Content-type: application/json' --data '{\"text\":\" " + str(t) + "\"}' " + str(hook) + " &> /dev/null" + os.system(command) + +# A fallback solution to getting all child procs +# in case psutil has problems (PermissionError). +# It returns the same list as psutil.children(recursive=True). +def getChildProcs(basepid): + cmd=''' + childprocs() { + local parent=$1 + if [ ! "$2" ]; then + child_pid_list="" + fi + if [ "$parent" ] ; then + child_pid_list="$child_pid_list $parent" + for childpid in $(pgrep -P ${parent}); do + childprocs $childpid "nottoplevel" + done; + fi + # return via a string list (only if toplevel) + if [ ! "$2" ]; then + echo "${child_pid_list}" + fi + } + ''' + cmd = cmd + '\n' + 'childprocs ' + str(basepid) + output = subprocess.check_output(cmd, shell=True) + plist = [] + for p in output.strip().split(): + try: + proc=psutil.Process(int(p)) + except psutil.NoSuchProcess: + continue + + plist.append(proc) + return plist + +# +# Code section to find all topological orderings +# of a DAG. This is used to know when we can schedule +# things in parallel. +# Taken from https://www.geeksforgeeks.org/all-topological-sorts-of-a-directed-acyclic-graph/ + +# class to represent a graph object +class Graph: + + # Constructor + def __init__(self, edges, N): + + # A List of Lists to represent an adjacency list + self.adjList = [[] for _ in range(N)] + + # stores in-degree of a vertex + # initialize in-degree of each vertex by 0 + self.indegree = [0] * N + + # add edges to the undirected graph + for (src, dest) in edges: + + # add an edge from source to destination + self.adjList[src].append(dest) + + # increment in-degree of destination vertex by 1 + self.indegree[dest] = self.indegree[dest] + 1 + +# Recursive function to find all topological orderings of a given DAG +def findAllTopologicalOrders(graph, path, discovered, N, allpaths, maxnumber=1): + if len(allpaths) >= maxnumber: + return + + # do for every vertex + for v in range(N): + + # proceed only if in-degree of current node is 0 and + # current node is not processed yet + if graph.indegree[v] == 0 and not discovered[v]: + + # for every adjacent vertex u of v, reduce in-degree of u by 1 + for u in graph.adjList[v]: + graph.indegree[u] = graph.indegree[u] - 1 + + # include current node in the path and mark it as discovered + path.append(v) + discovered[v] = True + + # recur + findAllTopologicalOrders(graph, path, discovered, N, allpaths) + + # backtrack: reset in-degree information for the current node + for u in graph.adjList[v]: + graph.indegree[u] = graph.indegree[u] + 1 + + # backtrack: remove current node from the path and + # mark it as undiscovered + path.pop() + discovered[v] = False + + # record valid ordering + if len(path) == N: + allpaths.append(path.copy()) + + +# get all topological orderings of a given DAG as a list +def printAllTopologicalOrders(graph, maxnumber=1): + # get number of nodes in the graph + N = len(graph.adjList) + + # create an auxiliary space to keep track of whether vertex is discovered + discovered = [False] * N + + # list to store the topological order + path = [] + allpaths = [] + # find all topological ordering and print them + findAllTopologicalOrders(graph, path, discovered, N, allpaths, maxnumber=maxnumber) + return allpaths + +# <--- end code section for topological sorts + +# find all tasks that depend on a given task (id); when a cache +# dict is given we can fill for the whole graph in one pass... +def find_all_dependent_tasks(possiblenexttask, tid, cache=None): + c=cache.get(tid) if cache else None + if c!=None: + return c + + daughterlist=[tid] + # possibly recurse + for n in possiblenexttask[tid]: + c = cache.get(n) if cache else None + if c == None: + c = find_all_dependent_tasks(possiblenexttask, n, cache) + daughterlist = daughterlist + c + if cache is not None: + cache[n]=c + + if cache is not None: + cache[tid]=daughterlist + return list(set(daughterlist)) + + +# wrapper taking some edges, constructing the graph, +# obtain all topological orderings and some other helper data structures +def analyseGraph(edges, nodes): + # Number of nodes in the graph + N = len(nodes) + + # candidate list trivial + nextjobtrivial = { n:[] for n in nodes } + # startnodes + nextjobtrivial[-1] = nodes + for e in edges: + nextjobtrivial[e[0]].append(e[1]) + if nextjobtrivial[-1].count(e[1]): + nextjobtrivial[-1].remove(e[1]) + + # find topological orderings of the graph + # create a graph from edges + graph = Graph(edges, N) + orderings = printAllTopologicalOrders(graph) + + return (orderings, nextjobtrivial) + + +def draw_workflow(workflowspec): + if not havegraphviz: + print('graphviz not installed, cannot draw workflow') + return + + dot = Digraph(comment='MC workflow') + nametoindex={} + index=0 + # nodes + for node in workflowspec['stages']: + name=node['name'] + nametoindex[name]=index + dot.node(str(index), name) + index=index+1 + + # edges + for node in workflowspec['stages']: + toindex = nametoindex[node['name']] + for req in node['needs']: + fromindex = nametoindex[req] + dot.edge(str(fromindex), str(toindex)) + + dot.render('workflow.gv') + +# builds the graph given a "taskuniverse" list +# builds accompagnying structures tasktoid and idtotask +def build_graph(taskuniverse, workflowspec): + tasktoid={ t[0]['name']:i for i, t in enumerate(taskuniverse, 0) } + # print (tasktoid) + + nodes = [] + edges = [] + for t in taskuniverse: + nodes.append(tasktoid[t[0]['name']]) + for n in t[0]['needs']: + edges.append((tasktoid[n], tasktoid[t[0]['name']])) + + return (edges, nodes) + + +# loads json into dict, e.g. for workflow specification +def load_json(workflowfile): + fp=open(workflowfile) + workflowspec=json.load(fp) + return workflowspec + + +# filters the original workflowspec according to wanted targets or labels +# returns a new workflowspec and the list of "final" workflowtargets +def filter_workflow(workflowspec, targets=[], targetlabels=[]): + if len(targets)==0: + return workflowspec, [] + if len(targetlabels)==0 and len(targets)==1 and targets[0]=="*": + return workflowspec, [] + + transformedworkflowspec = workflowspec + + def task_matches(t): + for filt in targets: + if filt=="*": + return True + if re.match(filt, t) != None: + return True + return False + + def task_matches_labels(t): + # when no labels are given at all it's ok + if len(targetlabels)==0: + return True + + for l in t['labels']: + if targetlabels.count(l)!=0: + return True + return False + + # The following sequence of operations works and is somewhat structured. + # However, it builds lookups used elsewhere as well, so some CPU might be saved by reusing + # some structures across functions or by doing less passes on the data. + + # helper lookup + tasknametoid = { t['name']:i for i, t in enumerate(workflowspec['stages'],0) } + + # check if a task can be run at all + # or not due to missing requirements + def canBeDone(t,cache={}): + ok = True + c = cache.get(t['name']) + if c != None: + return c + for r in t['needs']: + taskid = tasknametoid.get(r) + if taskid != None: + if not canBeDone(workflowspec['stages'][taskid], cache): + ok = False + break + else: + ok = False + break + cache[t['name']] = ok + if ok == False: + print (f"Disabling target {t['name']} due to unsatisfied requirements") + return ok + + okcache = {} + # build full target list + full_target_list = [ t for t in workflowspec['stages'] if task_matches(t['name']) and task_matches_labels(t) and canBeDone(t,okcache) ] + full_target_name_list = [ t['name'] for t in full_target_list ] + + # build full dependency list for a task t + def getallrequirements(t): + _l=[] + for r in t['needs']: + fulltask = workflowspec['stages'][tasknametoid[r]] + _l.append(fulltask) + _l=_l+getallrequirements(fulltask) + return _l + + full_requirements_list = [ getallrequirements(t) for t in full_target_list ] + + # make flat and fetch names only + full_requirements_name_list = list(set([ item['name'] for sublist in full_requirements_list for item in sublist ])) + + # inner "lambda" helper answering if a task "name" is needed by given targets + def needed_by_targets(name): + if full_target_name_list.count(name)!=0: + return True + if full_requirements_name_list.count(name)!=0: + return True + return False + + # we finaly copy everything matching the targets as well + # as all their requirements + transformedworkflowspec['stages']=[ l for l in workflowspec['stages'] if needed_by_targets(l['name']) ] + return transformedworkflowspec, full_target_name_list + + +# builds topological orderings (for each timeframe) +def build_dag_properties(workflowspec): + globaltaskuniverse = [ (l, i) for i, l in enumerate(workflowspec['stages'], 1) ] + timeframeset = set( l['timeframe'] for l in workflowspec['stages'] ) + + edges, nodes = build_graph(globaltaskuniverse, workflowspec) + tup = analyseGraph(edges, nodes.copy()) + # + global_next_tasks = tup[1] + + + dependency_cache = {} + # weight influences scheduling order can be anything user defined ... for the moment we just prefer to stay within a timeframe + # then take the number of tasks that depend on a task as further weight + # TODO: bring in resource estimates from runtime, CPU, MEM + # TODO: make this a policy of the runner to study different strategies + def getweight(tid): + return (globaltaskuniverse[tid][0]['timeframe'], len(find_all_dependent_tasks(global_next_tasks, tid, dependency_cache))) + + task_weights = [ getweight(tid) for tid in range(len(globaltaskuniverse)) ] + + for tid in range(len(globaltaskuniverse)): + actionlogger.info("Score for " + str(globaltaskuniverse[tid][0]['name']) + " is " + str(task_weights[tid])) + + # print (global_next_tasks) + return { 'nexttasks' : global_next_tasks, 'weights' : task_weights, 'topological_ordering' : tup[0] } + + +# update the resource estimates of a workflow based on resources given via JSON +def update_resource_estimates(workflow, resource_json): + # the resource_dict here is generated by tool o2dpg_sim_metrics.py json-stat + resource_dict = load_json(resource_json) + stages = workflow["stages"] + + for task in stages: + if task["timeframe"] >= 1: + name = "_".join(task["name"].split("_")[:-1]) + else: + name = task["name"] + + if name not in resource_dict: + continue + + new_resources = resource_dict[name] + + # memory + newmem = new_resources.get("pss", {}).get("max", None) + if newmem is not None: + oldmem = task["resources"]["mem"] + actionlogger.info("Updating mem estimate for " + task["name"] + " from " + str(oldmem) + " to " + str(newmem)) + task["resources"]["mem"] = newmem + + # cpu + newcpu = new_resources.get("cpu", {}).get("mean", None) + if newcpu is not None: + oldcpu = task["resources"]["cpu"] + rel_cpu = task["resources"]["relative_cpu"] + # TODO: No longer sure about this since we inject numbers from actually measured workloads + if rel_cpu is not None: + # respect the relative CPU settings + # By default, the CPU value in the workflow is already scaled if relative_cpu is given. + # The new estimate on the other hand is not yet scaled so it needs to be done here. + newcpu *= rel_cpu + actionlogger.info("Updating cpu estimate for " + task["name"] + " from " + str(oldcpu) + " to " + str(newcpu)) + task["resources"]["cpu"] = newcpu + +# a function to read a software environment determined by alienv into +# a python dictionary +def get_alienv_software_environment(packagestring): + """ + packagestring is something like O2::v202298081-1,O2Physics::xxx representing packages + published on CVMFS ... or ... a file containing directly the software environment to apply + """ + + # the trivial cases do nothing + if packagestring == None or packagestring == "" or packagestring == "None": + return {} + + def load_env_file(env_file): + """Transform an environment file generated with 'export > env.txt' into a python dictionary.""" + env_vars = {} + with open(env_file, "r") as f: + for line in f: + line = line.strip() + + # Ignore empty lines or comments + if not line or line.startswith("#"): + continue + + # Remove 'declare -x ' if present + if line.startswith("declare -x "): + line = line.replace("declare -x ", "", 1) + + # Handle case: "FOO" without "=" (assign empty string) + if "=" not in line: + key, value = line.strip(), "" + else: + key, value = line.split("=", 1) + value = value.strip('"') # Remove surrounding quotes if present + + env_vars[key.strip()] = value + return env_vars + + # see if this is a file + if os.path.exists(packagestring) and os.path.isfile(packagestring): + actionlogger.info("Taking software environment from file " + packagestring) + return load_env_file(packagestring) + + # alienv printenv packagestring --> dictionary + # for the moment this works with CVMFS only + cmd="/cvmfs/alice.cern.ch/bin/alienv printenv " + packagestring + proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + + envstring, err = proc.communicate() + # see if the printenv command was successful + if len(err.decode()) > 0: + print (err.decode()) + raise Exception + + # the software environment is now in the evnstring + # split it on semicolon + envstring=envstring.decode() + tokens=envstring.split(";") + # build envmap + envmap = {} + for t in tokens: + # check if assignment + if t.count("=") > 0: + assignment = t.rstrip().split("=") + envmap[assignment[0]] = assignment[1] + elif t.count("export") > 0: + # the case when we export or a simple variable + # need to consider the case when this has not been previously assigned + variable = t.split()[1] + if not variable in envmap: + envmap[variable]="" + + return envmap + +# +# functions for execution; encapsulated in a WorkflowExecutor class +# + +class Semaphore: + """ + Object that can be used as semaphore + """ + def __init__(self): + self.locked = False + def lock(self): + self.locked = True + def unlock(self): + self.locked = False + + +class ResourceBoundaries: + """ + Container holding global resource properties + """ + def __init__(self, cpu_limit, mem_limit, dynamic_resources=False, optimistic_resources=False): + self.cpu_limit = cpu_limit + self.mem_limit = mem_limit + self.dynamic_resources = dynamic_resources + # if this is set, tasks that would normally go beyond the resource limits will tried to be run in any case + self.optimistic_resources = optimistic_resources + + +class TaskResources: + """ + Container holding resources of a single task + """ + def __init__(self, tid, name, cpu, cpu_relative, mem, resource_boundaries): + # the task ID belonging to these resources + self.tid = tid + self.name = name + # original CPUs/MEM assigned (persistent) + self.cpu_assigned_original = cpu + self.mem_assigned_original = mem + # relative CPU, to be multiplied with sampled CPU; set by the user, e.g. to allow to backfill tasks + # only takes effect when sampling resources; persistent + self.cpu_relative = cpu_relative if cpu_relative else 1 + # CPUs/MEM assigned (transient) + self.cpu_assigned = cpu + self.mem_assigned = mem + # global resource settings + self.resource_boundaries = resource_boundaries + # sampled resources of this + self.cpu_sampled = None + self.mem_sampled = None + # Set these after a task has finished to compute new estimates for related tasks + self.walltime = None + self.cpu_taken = None + self.mem_taken = None + # collected during monitoring + self.time_collect = [] + self.cpu_collect = [] + self.mem_collect = [] + # linked to other resources of task that are of the same type as this one + self.related_tasks = None + # can assign a semaphore + self.semaphore = None + # the task's nice value + self.nice_value = None + # whether or not the task's resources are currently booked + self.booked = False + + @property + def is_done(self): + return self.time_collect and not self.booked + + def is_within_limits(self): + """ + Check if assigned resources respect limits + """ + cpu_within_limits = True + mem_within_limits = True + if self.cpu_assigned > self.resource_boundaries.cpu_limit: + cpu_within_limits = False + actionlogger.warning("CPU of task %s exceeds limits %d > %d", self.name, self.cpu_assigned, self.resource_boundaries.cpu_limit) + if self.cpu_assigned > self.resource_boundaries.mem_limit: + mem_within_limits = False + actionlogger.warning("MEM of task %s exceeds limits %d > %d", self.name, self.cpu_assigned, self.resource_boundaries.cpu_limit) + return cpu_within_limits and mem_within_limits + + def limit_resources(self, cpu_limit=None, mem_limit=None): + """ + Limit resources of this specific task + """ + if not cpu_limit: + cpu_limit = self.resource_boundaries.cpu_limit + if not mem_limit: + mem_limit = self.resource_boundaries.mem_limit + self.cpu_assigned = min(self.cpu_assigned, cpu_limit) + self.mem_assigned = min(self.mem_assigned, mem_limit) + + def add(self, time_passed, cpu, mem): + """ + Brief interface to add resources that were measured after time_passed + """ + self.time_collect.append(time_passed) + self.cpu_collect.append(cpu) + self.mem_collect.append(mem) + + def sample_resources(self): + """ + If this task is done, sample CPU and MEM for all related tasks that have not started yet + """ + if not self.is_done: + return + + if len(self.time_collect) < 3: + # Consider at least 3 points to sample from + self.cpu_sampled = self.cpu_assigned + self.mem_sampled = self.mem_assigned + actionlogger.debug("Task %s has not enough points (< 3) to sample resources, setting to previosuly assigned values.", self.name) + else: + # take the time deltas and leave out the very first CPU measurent which is not meaningful, + # at least when it domes from psutil.Proc.cpu_percent(interval=None) + time_deltas = [self.time_collect[i+1] - self.time_collect[i] for i in range(len(self.time_collect) - 1)] + cpu = sum([cpu * time_delta for cpu, time_delta in zip(self.cpu_collect[1:], time_deltas) if cpu >= 0]) + self.cpu_sampled = cpu / sum(time_deltas) + self.mem_sampled = max(self.mem_collect) + + mem_sampled = 0 + cpu_sampled = [] + for res in self.related_tasks: + if res.is_done: + mem_sampled = max(mem_sampled, res.mem_sampled) + cpu_sampled.append(res.cpu_sampled) + cpu_sampled = sum(cpu_sampled) / len(cpu_sampled) + + # This task ran already with the assigned resources, so let's set it to the limit + if cpu_sampled > self.resource_boundaries.cpu_limit: + actionlogger.warning("Sampled CPU (%.2f) exceeds assigned CPU limit (%.2f)", cpu_sampled, self.resource_boundaries.cpu_limit) + elif cpu_sampled < 0: + actionlogger.debug("Sampled CPU for %s is %.2f < 0, setting to previously assigned value %.2f", self.name, cpu_sampled, self.cpu_assigned) + cpu_sampled = self.cpu_assigned + + if mem_sampled > self.resource_boundaries.mem_limit: + actionlogger.warning("Sampled MEM (%.2f) exceeds assigned MEM limit (%.2f)", mem_sampled, self.resource_boundaries.mem_limit) + elif mem_sampled <= 0: + actionlogger.debug("Sampled memory for %s is %.2f <= 0, setting to previously assigned value %.2f", self.name, mem_sampled, self.mem_assigned) + mem_sampled = self.mem_assigned + + for res in self.related_tasks: + if res.is_done or res.booked: + continue + res.cpu_assigned = cpu_sampled * res.cpu_relative + res.mem_assigned = mem_sampled + # This task has been run before, stay optimistic and limit the resources in case the sampled ones exceed limits + res.limit_resources() + + +class ResourceManager: + """ + Central class to manage resources + + - CPU limits + - MEM limits + - Semaphores + + Entrypoint to set and to query for resources to be updated. + + Can be asked whether a certain task can be run under current resource usage. + Book and unbook resources. + """ + def __init__(self, cpu_limit, mem_limit, procs_parallel_max=100, dynamic_resources=False, optimistic_resources=False): + """ + Initialise members with defaults + """ + # hold TaskResources of all tasks + self.resources = [] + + # helper dictionaries holding common objects which will be distributed to single TaskResources objects + # to avoid further lookup and at the same time to share the same common objects + self.resources_related_tasks_dict = {} + self.semaphore_dict = {} + + # one common object that holds global resource settings such as CPU and MEM limits + self.resource_boundaries = ResourceBoundaries(cpu_limit, mem_limit, dynamic_resources, optimistic_resources) + + # register resources that are booked under default nice value + self.cpu_booked = 0 + self.mem_booked = 0 + # number of tasks currently booked + self.n_procs = 0 + + # register resources that are booked under high nice value + self.cpu_booked_backfill = 0 + self.mem_booked_backfill = 0 + # number of tasks currently booked under high nice value + self.n_procs_backfill = 0 + + # the maximum number of tasks that run at the same time + self.procs_parallel_max = procs_parallel_max + + # get the default nice value of this python script + self.nice_default = os.nice(0) + # add 19 to get nice value of low-priority tasks + self.nice_backfill = self.nice_default + 19 + + def add_task_resources(self, name, related_tasks_name, cpu, cpu_relative, mem, semaphore_string=None): + """ + Construct and Add a new TaskResources object + """ + resources = TaskResources(len(self.resources), name, cpu, cpu_relative, mem, self.resource_boundaries) + if not resources.is_within_limits() and not self.resource_boundaries.optimistic_resources: + # exit if we don't dare to try + print(f"Resources of task {name} are exceeding the boundaries.\nCPU: {cpu} (estimate) vs. {self.resource_boundaries.cpu_limit} (boundary)\nMEM: {mem} (estimated) vs. {self.resource_boundaries.mem_limit} (boundary).") + print("Pass --optimistic-resources to the runner to attempt the run anyway.") + exit(1) + # if we get here, either all is good or the user decided to be optimistic and we limit the resources, by default to the given CPU and mem limits. + resources.limit_resources() + + self.resources.append(resources) + # do the following to have the same Semaphore object for all corresponding TaskResources so that we do not need a lookup + if semaphore_string: + if semaphore_string not in self.semaphore_dict: + self.semaphore_dict[semaphore_string] = Semaphore() + resources.semaphore = self.semaphore_dict[semaphore_string] + + # do the following to give each TaskResources a list of the related tasks so we do not need an additional lookup + if related_tasks_name: + if related_tasks_name not in self.resources_related_tasks_dict: + # assigned list is [valid top be used, list of CPU, list of MEM, list of walltimes of each related task, list of processes that ran in parallel on average, list of taken CPUs, list of assigned CPUs, list of tasks finished in the meantime] + self.resources_related_tasks_dict[related_tasks_name] = [] + self.resources_related_tasks_dict[related_tasks_name].append(resources) + resources.related_tasks = self.resources_related_tasks_dict[related_tasks_name] + + def add_monitored_resources(self, tid, time_delta_since_start, cpu, mem): + self.resources[tid].add(time_delta_since_start, cpu, mem) + + def book(self, tid, nice_value): + """ + Book the resources of this task with given nice value + + The final nice value is determined by the final submission and could be different. + This can happen if the nice value should have been changed while that is not allowed by the system. + """ + res = self.resources[tid] + # take the nice value that was previously assigned when resources where checked last time + previous_nice_value = res.nice_value + + if previous_nice_value is None: + # this has not been checked ever if it was ok to be submitted + actionlogger.warning("Task ID %d has never been checked for resources. Treating as backfill", tid) + nice_value = self.nice_backfill + elif res.nice_value != nice_value: + actionlogger.warning("Task ID %d has was last time checked for a different nice value (%d) but is now submitted with (%d).", tid, res.nice_value, nice_value) + + res.nice_value = nice_value + res.booked = True + if res.semaphore is not None: + res.semaphore.lock() + if nice_value != self.nice_default: + self.n_procs_backfill += 1 + self.cpu_booked_backfill += res.cpu_assigned + self.mem_booked_backfill += res.mem_assigned + return + self.n_procs += 1 + self.cpu_booked += res.cpu_assigned + self.mem_booked += res.mem_assigned + + def unbook(self, tid): + """ + Unbook the reources of this task + """ + res = self.resources[tid] + res.booked = False + if self.resource_boundaries.dynamic_resources: + res.sample_resources() + if res.semaphore is not None: + res.semaphore.unlock() + if res.nice_value != self.nice_default: + self.cpu_booked_backfill -= res.cpu_assigned + self.mem_booked_backfill -= res.mem_assigned + self.n_procs_backfill -= 1 + if self.n_procs_backfill <= 0: + self.cpu_booked_backfill = 0 + self.mem_booked_backfill = 0 + return + self.n_procs -= 1 + self.cpu_booked -= res.cpu_assigned + self.mem_booked -= res.mem_assigned + if self.n_procs <= 0: + self.cpu_booked = 0 + self.mem_booked = 0 + + def ok_to_submit(self, tids): + """ + This generator yields the tid and nice value tuple from the list of task ids that should be checked + """ + tids_copy = tids.copy() + + def ok_to_submit_default(res): + """ + Return default nice value if conditions are met, None otherwise + """ + # analyse CPU + okcpu = (self.cpu_booked + res.cpu_assigned <= self.resource_boundaries.cpu_limit) + # analyse MEM + okmem = (self.mem_booked + res.mem_assigned <= self.resource_boundaries.mem_limit) + actionlogger.debug ('Condition check --normal-- for ' + str(res.tid) + ':' + res.name + ' CPU ' + str(okcpu) + ' MEM ' + str(okmem)) + return self.nice_default if (okcpu and okmem) else None + + def ok_to_submit_backfill(res, backfill_cpu_factor=1.5, backfill_mem_factor=1.5): + """ + Return backfill nice value if conditions are met, None otherwise + """ + if self.n_procs_backfill >= args.n_backfill: + return None + + if res.cpu_assigned > 0.9 * self.resource_boundaries.cpu_limit or res.mem_assigned / self.resource_boundaries.cpu_limit >= 1900: + return None + + # analyse CPU + okcpu = (self.cpu_booked_backfill + res.cpu_assigned <= self.resource_boundaries.cpu_limit) + okcpu = okcpu and (self.cpu_booked + self.cpu_booked_backfill + res.cpu_assigned <= backfill_cpu_factor * self.resource_boundaries.cpu_limit) + # analyse MEM + okmem = (self.mem_booked + self.mem_booked_backfill + res.mem_assigned <= backfill_mem_factor * self.resource_boundaries.mem_limit) + actionlogger.debug ('Condition check --backfill-- for ' + str(res.tid) + ':' + res.name + ' CPU ' + str(okcpu) + ' MEM ' + str(okmem)) + + return self.nice_backfill if (okcpu and okmem) else None + + if self.n_procs + self.n_procs_backfill >= self.procs_parallel_max: + # in this case, nothing can be done + return + + for ok_to_submit_impl, should_break in ((ok_to_submit_default, True), (ok_to_submit_backfill, False)): + tid_index = 0 + while tid_index < len(tids_copy): + + tid = tids_copy[tid_index] + res = self.resources[tid] + + actionlogger.info("Setup resources for task %s, cpu: %f, mem: %f", res.name, res.cpu_assigned, res.mem_assigned) + tid_index += 1 + + if (res.semaphore is not None and res.semaphore.locked) or res.booked: + continue + + nice_value = ok_to_submit_impl(res) + if nice_value is not None: + # if we get a non-None nice value, it means that this task is good to go + res.nice_value = nice_value + # yield the tid and its assigned nice value + yield tid, nice_value + + elif should_break: + # break here if resources of the next task do not fit + break + + +def filegraph_expand_timeframes(data: dict, timeframes: set, target_namelist) -> dict: + """ + A utility function for the fileaccess logic. Takes a template and duplicates + for the multi-timeframe structure. + """ + tf_entries = [ + entry for entry in data.get("file_report", []) + if re.match(r"^\./tf\d+/", entry["file"]) + ] + + result = {} + for i in timeframes: + if i == -1: + continue + # Deepcopy to avoid modifying original + new_entries = deepcopy(tf_entries) + for entry in new_entries: + # Fix filepath + entry["file"] = re.sub(r"^\./tf\d+/", f"./tf{i}/", entry["file"]) + # Fix written_by and read_by (preserve prefix, change numeric suffix) + entry["written_by"] = [ + re.sub(r"_\d+$", f"_{i}", w) for w in entry["written_by"] + ] + # for now we mark some files as keep if they are written + # by a target in the runner targetlist. TODO: Add other mechanisms + # to ask for file keeping (such as via regex or the like) + for e in entry["written_by"]: + if e in target_namelist: + entry["keep"] = True + entry["read_by"] = [ + re.sub(r"_\d+$", f"_{i}", r) for r in entry["read_by"] + ] + result[f"timeframe-{i}"] = new_entries + + return result + + + +class WorkflowExecutor: + # Constructor + def __init__(self, workflowfile, args, jmax=100): + self.args=args + self.is_productionmode = args.production_mode == True # os.getenv("ALIEN_PROC_ID") != None + self.workflowfile = workflowfile + self.workflowspec = load_json(workflowfile) + self.globalinit = self.extract_global_environment(self.workflowspec) # initialize global environment settings + for e in self.globalinit['env']: + if os.environ.get(e, None) == None: + value = self.globalinit['env'][e] + actionlogger.info("Applying global environment from init section " + str(e) + " : " + str(value)) + os.environ[e] = str(value) + + # only keep those tasks that are necessary to be executed based on user's filters + self.full_target_namelist = [] + self.workflowspec, self.full_target_namelist = filter_workflow(self.workflowspec, args.target_tasks, args.target_labels) + + if not self.workflowspec['stages']: + if args.target_tasks: + print ('Apparently some of the chosen target tasks are not in the workflow') + exit (0) + print ('Workflow is empty. Nothing to do') + exit (0) + + # construct the DAG, compute task weights + workflow = build_dag_properties(self.workflowspec) + if args.visualize_workflow: + draw_workflow(self.workflowspec) + self.possiblenexttask = workflow['nexttasks'] + self.taskweights = workflow['weights'] + self.topological_orderings = workflow['topological_ordering'] + self.taskuniverse = [ l['name'] for l in self.workflowspec['stages'] ] + # construct task ID <-> task name lookup + self.idtotask = [ 0 for _ in self.taskuniverse ] + self.tasktoid = {} + self.idtotf = [ l['timeframe'] for l in self.workflowspec['stages'] ] + for i, name in enumerate(self.taskuniverse): + self.tasktoid[name]=i + self.idtotask[i]=name + + if args.update_resources: + update_resource_estimates(self.workflowspec, args.update_resources) + + # construct the object that is in charge of resource management... + self.resource_manager = ResourceManager(args.cpu_limit, args.mem_limit, args.maxjobs, args.dynamic_resources, args.optimistic_resources) + for task in self.workflowspec['stages']: + # ...and add all initial resource estimates + global_task_name = self.get_global_task_name(task["name"]) + try: + cpu_relative = float(task["resources"]["relative_cpu"]) + except TypeError: + cpu_relative = 1 + self.resource_manager.add_task_resources(task["name"], global_task_name, float(task["resources"]["cpu"]), cpu_relative, float(task["resources"]["mem"]), task.get("semaphore")) + + self.procstatus = { tid:'ToDo' for tid in range(len(self.workflowspec['stages'])) } + self.taskneeds= { t:set(self.getallrequirements(t)) for t in self.taskuniverse } + self.stoponfailure = not args.keep_going + print ("Stop on failure ",self.stoponfailure) + + self.scheduling_iteration = 0 # count how often it was tried to schedule new tasks + self.process_list = [] # list of currently scheduled tasks with normal priority + self.backfill_process_list = [] # list of curently scheduled tasks with low backfill priority (not sure this is needed) + self.pid_to_psutilsproc = {} # cache of putilsproc for resource monitoring + self.pid_to_files = {} # we can auto-detect what files are produced by which task (at least to some extent) + self.pid_to_connections = {} # we can auto-detect what connections are opened by which task (at least to some extent) + signal.signal(signal.SIGINT, self.SIGHandler) + signal.siginterrupt(signal.SIGINT, False) + self.internalmonitorcounter = 0 # internal use + self.internalmonitorid = 0 # internal use + self.tids_marked_toretry = [] # sometimes we might want to retry a failed task (simply because it was "unlucky") and we put them here + self.retry_counter = [ 0 for tid in range(len(self.taskuniverse)) ] # we keep track of many times retried already + self.task_retries = [ self.workflowspec['stages'][tid].get('retry_count',0) for tid in range(len(self.taskuniverse)) ] # the per task specific "retry" number -> needs to be parsed from the JSON + + self.alternative_envs = {} # mapping of taskid to alternative software envs (to be applied on a per-task level) + # init alternative software environments + self.init_alternative_software_environments() + + # initialize container to keep track of file-task relationsships + self.file_removal_candidates = {} + self.do_early_file_removal = False + self.timeframeset = set([ task["timeframe"] for task in self.workflowspec['stages'] ]) + if args.remove_files_early != "": + with open(args.remove_files_early) as f: + filegraph_data = json.load(f) + self.do_early_file_removal = True + self.file_removal_candidates = filegraph_expand_timeframes(filegraph_data, self.timeframeset, self.full_target_namelist) + + def apply_global_env(self, environ_dict): + for e in self.globalinit['env']: + if environ_dict.get(e, None) == None: + value = self.globalinit['env'][e] + actionlogger.info("Applying global environment from init section " + str(e) + " : " + str(value)) + environ_dict[e] = str(value) + + def perform_early_file_removal(self, taskids): + """ + This function checks which files can be deleted upon completion of task + and optionally does so. + """ + + def remove_if_exists(filepath: str) -> None: + """ + Check if a file exists, and remove it if found. + """ + if os.path.exists(filepath): + fsize = os.path.getsize(filepath) + os.remove(filepath) + actionlogger.info(f"Removing {filepath} since no longer needed. Freeing {fsize/1024./1024.} MB.") + return True + + return False + + def remove_for_task_id(taskname, file_dict, timeframe_id, listofalltimeframes): + marked_for_removal = [] + + timeframestoscan = [ timeframe_id ] + if timeframe_id == -1: + timeframestoscan = [ i for i in listofalltimeframes if i != -1 ] + + # TODO: Note that this traversal of files is not certainly not optimal + # We should (and will) keep an mapping of tasks->potential files and just + # scan these. This is already provided by the FileIOGraph analysis tool. + for tid in timeframestoscan: + for i,file_entry in enumerate(file_dict[f"timeframe-{tid}"]): + filename = file_entry['file'] + read_by = file_entry['read_by'] + written_by = file_entry['written_by'] + if taskname in read_by: + file_entry['read_by'].remove(taskname) + if taskname in written_by: + file_entry['written_by'].remove(taskname) + + # TODO: in principle the written_by criterion might not be needed + if len(file_entry['read_by']) == 0 and len(file_entry['written_by']) == 0 and file_entry.get('keep', False) == False: + # the filename mentioned here is no longer needed and we can remove it + # make sure it is there and then delete it + if remove_if_exists(filename): + # also take out the file entry from the dict altogether + marked_for_removal.append(file_entry) + + #for k in marked_for_removal: + # file_dict[f"timeframe-{tid}"].remove(k) + + for tid in taskids: + taskname = self.idtotask[tid] + timeframe_id = self.idtotf[tid] + remove_for_task_id(taskname, self.file_removal_candidates, timeframe_id, self.timeframeset) + + + def SIGHandler(self, signum, frame): + """ + basically forcing shut down of all child processes + """ + actionlogger.info("Signal " + str(signum) + " caught") + try: + procs = psutil.Process().children(recursive=True) + except (psutil.NoSuchProcess): + pass + except (psutil.AccessDenied, PermissionError): + procs = getChildProcs(os.getpid()) + + for p in procs: + actionlogger.info("Terminating " + str(p)) + try: + p.terminate() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + _, alive = psutil.wait_procs(procs, timeout=3) + for p in alive: + try: + actionlogger.info("Killing " + str(p)) + p.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + exit (1) + + def extract_global_environment(self, workflowspec): + """ + Checks if the workflow contains a dedicated init task + defining a global environment. Extract information and remove from workflowspec. + """ + init_index = 0 # this has to be the first task in the workflow + globalenv = {} + initcmd = None + if workflowspec['stages'][init_index]['name'] == '__global_init_task__': + env = workflowspec['stages'][init_index].get('env', None) + if env != None: + globalenv = { e : env[e] for e in env } + cmd = workflowspec['stages'][init_index].get('cmd', None) + if cmd != 'NO-COMMAND': + initcmd = cmd + + del workflowspec['stages'][init_index] + + return {"env" : globalenv, "cmd" : initcmd } + + def execute_globalinit_cmd(self, cmd): + actionlogger.info("Executing global setup cmd " + str(cmd)) + # perform the global init command (think of cleanup/setup things to be done in any case) + p = subprocess.Popen(['/bin/bash','-c', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + + # Check if the command was successful (return code 0) + if p.returncode == 0: + actionlogger.info(stdout.decode()) + else: + # this should be an error + actionlogger.error("Error executing global init function") + return False + return True + + def get_global_task_name(self, name): + """ + Get the global task name + + Tasks are related if only the suffix _ is different + """ + tokens = name.split("_") + try: + int(tokens[-1]) + return "_".join(tokens[:-1]) + except ValueError: + pass + return name + + def getallrequirements(self, task_name): + """ + get all requirement of a task by its name + """ + l=[] + for required_task_name in self.workflowspec['stages'][self.tasktoid[task_name]]['needs']: + l.append(required_task_name) + l=l+self.getallrequirements(required_task_name) + return l + + def get_logfile(self, tid): + """ + O2 taskwrapper logs task stdout and stderr to logfile .log + Get its exact path based on task ID + """ + # determines the logfile name for this task + name = self.workflowspec['stages'][tid]['name'] + workdir = self.workflowspec['stages'][tid]['cwd'] + return os.path.join(workdir, f"{name}.log") + + def get_done_filename(self, tid): + """ + O2 taskwrapper leaves .log_done after a task has successfully finished + Get its exact path based on task ID + """ + return f"{self.get_logfile(tid)}_done" + + def get_resources_filename(self, tid): + """ + O2 taskwrapper leaves .log_time after a task is done + Get its exact path based on task ID + """ + return f"{self.get_logfile(tid)}_time" + + # removes the done flag from tasks that need to be run again + def remove_done_flag(self, listoftaskids): + """ + Remove .log_done files to given task IDs + """ + for tid in listoftaskids: + done_filename = self.get_done_filename(tid) + name=self.workflowspec['stages'][tid]['name'] + if args.dry_run: + print ("Would mark task " + name + " as to be done again") + else: + print ("Marking task " + name + " as to be done again") + if os.path.exists(done_filename) and os.path.isfile(done_filename): + os.remove(done_filename) + + # submits a task as subprocess and records Popen instance + def submit(self, tid, nice): + """ + Submit a task + + 1. if needed, construct working directory if it does not yet exist + 2. update lookup structures flagging the task as being run + 3. set specific environment if requested for task + 4. construct psutil.Process from command line + 4.1 adjust the niceness of that process if requested + 5. return psutil.Process object + """ + actionlogger.debug("Submitting task " + str(self.idtotask[tid]) + " with nice value " + str(nice)) + c = self.workflowspec['stages'][tid]['cmd'] + workdir = self.workflowspec['stages'][tid]['cwd'] + if workdir: + if os.path.exists(workdir) and not os.path.isdir(workdir): + actionlogger.error('Cannot create working dir ... some other resource exists already') + return None + + if not os.path.isdir(workdir): + os.makedirs(workdir) + + self.procstatus[tid]='Running' + if args.dry_run: + drycommand="echo \' " + str(self.scheduling_iteration) + " : would do " + str(self.workflowspec['stages'][tid]['name']) + "\'" + return psutil.Popen(['/bin/bash','-c',drycommand], cwd=workdir) + + taskenv = os.environ.copy() + # apply specific (non-default) software version, if any + # (this was setup earlier) + alternative_env = self.alternative_envs.get(tid, None) + if alternative_env != None and len(alternative_env) > 0: + actionlogger.info('Applying alternative software environment to task ' + self.idtotask[tid]) + if alternative_env.get('TERM') != None: + # the environment is a complete environment + taskenv = {} + taskenv = alternative_env + else: + for entry in alternative_env: + # overwrite what is present in default + taskenv[entry] = alternative_env[entry] + + # add task specific environment + if self.workflowspec['stages'][tid].get('env')!=None: + taskenv.update(self.workflowspec['stages'][tid]['env']) + + # add global workflow environment + self.apply_global_env(taskenv) + + if os.environ.get('PIPELINE_RUNNER_DUMP_TASKENVS') != None: + envfilename = "taskenv_" + str(tid) + ".log" + with open(envfilename, "w") as file: + json.dump(taskenv, file, indent=2) + + p = psutil.Popen(['/bin/bash','-c',c], cwd=workdir, env=taskenv) + try: + p.nice(nice) + except (psutil.NoSuchProcess, psutil.AccessDenied): + actionlogger.error('Couldn\'t set nice value of ' + str(p.pid) + ' to ' + str(nice)) + + return p + + def ok_to_skip(self, tid): + """ + Decide if task can be skipped based on existence of .log_done + """ + done_filename = self.get_done_filename(tid) + if os.path.exists(done_filename) and os.path.isfile(done_filename): + return True + return False + + def try_job_from_candidates(self, taskcandidates, finished): + """ + Try to schedule next tasks + + Args: + taskcandidates: list + list of possible tasks that can be submitted + finished: list + empty list that will be filled with IDs of tasks that were finished in the meantime + """ + self.scheduling_iteration = self.scheduling_iteration + 1 + + # remove "done / skippable" tasks immediately + for tid in taskcandidates.copy(): # <--- the copy is important !! otherwise this loop is not doing what you think + if self.ok_to_skip(tid): + finished.append(tid) + taskcandidates.remove(tid) + actionlogger.info("Skipping task " + str(self.idtotask[tid])) + + # if tasks_skipped: + # return # ---> we return early in order to preserve some ordering (the next candidate tried should be daughters of skipped jobs) + # get task ID and proposed niceness from generator + for (tid, nice_value) in self.resource_manager.ok_to_submit(taskcandidates): + actionlogger.debug ("trying to submit " + str(tid) + ':' + str(self.idtotask[tid])) + if p := self.submit(tid, nice_value): + # explicitly set the nice value here from the process again because it might happen that submit could not change the niceness + # so we let the ResourceManager know what the final niceness is + self.resource_manager.book(tid, p.nice()) + self.process_list.append((tid,p)) + taskcandidates.remove(tid) + # minimal delay + time.sleep(0.1) + + def stop_pipeline_and_exit(self, process_list): + # kill all remaining jobs + for p in process_list: + p[1].kill() + + exit(1) + + + def monitor(self, process_list): + """ + Go through all running tasks and get their current resources + + Resources are summed up for tasks and all their children + + Pass CPU, PSS, USS, niceness, current time to metriclogger + + Warn if overall PSS exceeds assigned memory limit + """ + self.internalmonitorcounter+=1 + if self.internalmonitorcounter % 5 != 0: + return + + self.internalmonitorid+=1 + + globalCPU=0. + globalPSS=0. + resources_per_task = {} + + # On a global level, we are interested in total disc space used (not differential in tasks) + # We can call system "du" as the fastest impl + def disk_usage_du(path: str) -> int: + """Use system du to get total size in bytes.""" + out = subprocess.check_output(['du', '-sb', path], text=True) + return int(out.split()[0]) + + disc_usage = -1 + if os.getenv("MONITOR_DISC_USAGE"): + disc_usage = disk_usage_du(os.getcwd()) / 1024. / 1024 # in MB + + for tid, proc in process_list: + + # proc is Popen object + pid=proc.pid + if self.pid_to_files.get(pid)==None: + self.pid_to_files[pid]=set() + self.pid_to_connections[pid]=set() + try: + psutilProcs = [ proc ] + # use psutil for CPU measurement + psutilProcs = psutilProcs + proc.children(recursive=True) + except (psutil.NoSuchProcess): + continue + + except (psutil.AccessDenied, PermissionError): + psutilProcs = psutilProcs + getChildProcs(pid) + + # accumulate total metrics (CPU, memory) + totalCPU = 0. + totalPSS = 0. + totalSWAP = 0. + totalUSS = 0. + for p in psutilProcs: + """ + try: + for f in p.open_files(): + self.pid_to_files[pid].add(str(f.path)+'_'+str(f.mode)) + for f in p.connections(kind="all"): + remote=f.raddr + if remote==None: + remote='none' + self.pid_to_connections[pid].add(str(f.type)+"_"+str(f.laddr)+"_"+str(remote)) + except Exception: + pass + """ + thispss=0 + thisuss=0 + # MEMORY part + try: + fullmem=p.memory_full_info() + thispss=getattr(fullmem,'pss',0) #<-- pss not available on MacOS + totalPSS=totalPSS + thispss + totalSWAP=totalSWAP + fullmem.swap + thisuss=fullmem.uss + totalUSS=totalUSS + thisuss + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + # CPU part + # fetch existing proc or insert + cachedproc = self.pid_to_psutilsproc.get(p.pid) + if cachedproc!=None: + try: + thiscpu = cachedproc.cpu_percent(interval=None) + except (psutil.NoSuchProcess, psutil.AccessDenied): + thiscpu = 0. + totalCPU = totalCPU + thiscpu + # thisresource = {'iter':self.internalmonitorid, 'pid': p.pid, 'cpu':thiscpu, 'uss':thisuss/1024./1024., 'pss':thispss/1024./1024.} + # metriclogger.info(thisresource) + else: + self.pid_to_psutilsproc[p.pid] = p + try: + self.pid_to_psutilsproc[p.pid].cpu_percent() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + time_delta = int((time.perf_counter() - self.start_time) * 1000) + totalUSS = totalUSS / 1024 / 1024 + totalPSS = totalPSS / 1024 / 1024 + nice_value = proc.nice() + resources_per_task[tid]={'iter':self.internalmonitorid, + 'name':self.idtotask[tid], + 'cpu':totalCPU, + 'uss':totalUSS, + 'pss':totalPSS, + 'nice':nice_value, + 'swap':totalSWAP, + 'label':self.workflowspec['stages'][tid]['labels'], + 'disc': disc_usage} + self.resource_manager.add_monitored_resources(tid, time_delta, totalCPU / 100, totalPSS) + if nice_value == self.resource_manager.nice_default: + globalCPU += totalCPU + globalPSS += totalPSS + + metriclogger.info(resources_per_task[tid]) + send_webhook(self.args.webhook, resources_per_task) + + if globalPSS > self.resource_manager.resource_boundaries.mem_limit: + metriclogger.info('*** MEMORY LIMIT PASSED !! ***') + # --> We could use this for corrective actions such as killing jobs currently back-filling + # (or better hibernating) + + def waitforany(self, process_list, finished, failingtasks): + """ + Loop through all submitted tasks and check if they are finished + + 1. If process is still running, do nothing + 2. If process is finished, get its return value, update finished and failingtasks lists + 2.1 unbook resources + 2.2 add taken resources and pass the to ResourceManager + """ + failuredetected = False + failingpids = [] + if len(process_list)==0: + return False + + for p in list(process_list): + pid = p[1].pid + tid = p[0] # the task id of this process + returncode = 0 + if not self.args.dry_run: + returncode = p[1].poll() + if returncode!=None: + actionlogger.info ('Task ' + str(pid) + ' ' + str(tid)+':'+str(self.idtotask[tid]) + ' finished with status ' + str(returncode)) + # account for cleared resources + self.resource_manager.unbook(tid) + self.procstatus[tid]='Done' + finished.append(tid) + #self.validate_resources_running(tid) + process_list.remove(p) + if returncode != 0: + print (str(self.idtotask[tid]) + ' failed ... checking retry') + # we inspect if this is something "unlucky" which could be resolved by a simple resubmit + if self.is_worth_retrying(tid) and ((self.retry_counter[tid] < int(args.retry_on_failure)) or (self.retry_counter[tid] < int(self.task_retries[tid]))): + print (str(self.idtotask[tid]) + ' to be retried') + actionlogger.info ('Task ' + str(self.idtotask[tid]) + ' failed but marked to be retried ') + self.tids_marked_toretry.append(tid) + self.retry_counter[tid] += 1 + + else: + failuredetected = True + failingpids.append(pid) + failingtasks.append(tid) + + if failuredetected and self.stoponfailure: + actionlogger.info('Stoping pipeline due to failure in stages with PID ' + str(failingpids)) + # self.analyse_files_and_connections() + if self.args.stdout_on_failure: + self.cat_logfiles_tostdout(failingtasks) + self.send_checkpoint(failingtasks, self.args.checkpoint_on_failure) + self.stop_pipeline_and_exit(process_list) + + # empty finished means we have to wait more + return len(finished)==0 + + def is_worth_retrying(self, tid): + # This checks for some signatures in logfiles that indicate that a retry of this task + # might have a chance. + # Ideally, this should be made user configurable. Either the user could inject a lambda + # or a regular expression to use. For now we just put a hard coded list + logfile = self.get_logfile(tid) + + return True #! --> for now we just retry tasks a few times + + # 1) ZMQ_EVENT + interrupted system calls (DPL bug during shutdown) + # Not sure if grep is faster than native Python text search ... + # status = os.system('grep "failed setting ZMQ_EVENTS" ' + logfile + ' &> /dev/null') + # if os.WEXITSTATUS(status) == 0: + # return True + + # return False + + + def cat_logfiles_tostdout(self, taskids): + # In case of errors we can cat the logfiles for this taskname + # to stdout. Assuming convention that "taskname" translates to "taskname.log" logfile. + for tid in taskids: + logfile = self.get_logfile(tid) + if os.path.exists(logfile): + print (' ----> START OF LOGFILE ', logfile, ' -----') + os.system('cat ' + logfile) + print (' <---- END OF LOGFILE ', logfile, ' -----') + + def send_checkpoint(self, taskids, location): + # Makes a tarball containing all files in the base dir + # (timeframe independent) and the dir with corrupted timeframes + # and copies it to a specific ALIEN location. Not a core function + # just some tool get hold on error conditions appearing on the GRID. + + def get_tar_command(dir='./', flags='cf', findtype='f', filename='checkpoint.tar'): + return 'find ' + str(dir) + ' -maxdepth 1 -type ' + str(findtype) + ' -print0 | xargs -0 tar ' + str(flags) + ' ' + str(filename) + + if location != None: + print ('Making a failure checkpoint') + # let's determine a filename from ALIEN_PROC_ID - hostname - and PID + + aliprocid=os.environ.get('ALIEN_PROC_ID') + if aliprocid == None: + aliprocid = 0 + + fn='pipeline_checkpoint_ALIENPROC' + str(aliprocid) + '_PID' + str(os.getpid()) + '_HOST' + socket.gethostname() + '.tar' + actionlogger.info("Checkpointing to file " + fn) + tarcommand = get_tar_command(filename=fn) + actionlogger.info("Taring " + tarcommand) + + # create a README file with instruction on how to use checkpoint + readmefile=open('README_CHECKPOINT_PID' + str(os.getpid()) + '.txt','w') + + for tid in taskids: + taskspec = self.workflowspec['stages'][tid] + name = taskspec['name'] + readmefile.write('Checkpoint created because of failure in task ' + name + '\n') + readmefile.write('In order to reproduce with this checkpoint, do the following steps:\n') + readmefile.write('a) setup the appropriate O2sim environment using alienv\n') + readmefile.write('b) run: $O2DPG_ROOT/MC/bin/o2_dpg_workflow_runner.py -f workflow.json -tt ' + name + '$ --retry-on-failure 0\n') + readmefile.close() + + # first of all the base directory + os.system(tarcommand) + + # then we add stuff for the specific timeframes ids if any + for tid in taskids: + taskspec = self.workflowspec['stages'][tid] + directory = taskspec['cwd'] + if directory != "./": + tarcommand = get_tar_command(dir=directory, flags='rf', filename=fn) + actionlogger.info("Tar command is " + tarcommand) + os.system(tarcommand) + # same for soft links + tarcommand = get_tar_command(dir=directory, flags='rf', findtype='l', filename=fn) + actionlogger.info("Tar command is " + tarcommand) + os.system(tarcommand) + + # prepend file:/// to denote local file + fn = "file://" + fn + actionlogger.info("Local checkpoint file is " + fn) + + # location needs to be an alien path of the form alien:///foo/bar/ + copycommand='alien.py cp ' + fn + ' ' + str(location) + '@disk:1' + actionlogger.info("Copying to alien " + copycommand) + os.system(copycommand) + + def init_alternative_software_environments(self): + """ + Initialises alternative software environments for specific tasks, if there + is an annotation in the workflow specificiation. + """ + + environment_cache = {} + # go through all the tasks once and setup environment + for taskid in range(len(self.workflowspec['stages'])): + packagestr = self.workflowspec['stages'][taskid].get("alternative_alienv_package") + if packagestr == None: + continue + + if environment_cache.get(packagestr) == None: + environment_cache[packagestr] = get_alienv_software_environment(packagestr) + + self.alternative_envs[taskid] = environment_cache[packagestr] + + + def analyse_files_and_connections(self): + for p,s in self.pid_to_files.items(): + for f in s: + print("F" + str(f) + " : " + str(p)) + for p,s in self.pid_to_connections.items(): + for c in s: + print("C" + str(c) + " : " + str(p)) + #print(str(p) + " CONS " + str(c)) + try: + # check for intersections + for p1, s1 in self.pid_to_files.items(): + for p2, s2 in self.pid_to_files.items(): + if p1!=p2: + if type(s1) is set and type(s2) is set: + if len(s1)>0 and len(s2)>0: + try: + inters = s1.intersection(s2) + except Exception: + print ('Exception during intersect inner') + pass + if (len(inters)>0): + print ('FILE Intersection ' + str(p1) + ' ' + str(p2) + ' ' + str(inters)) + # check for intersections + for p1, s1 in self.pid_to_connections.items(): + for p2, s2 in self.pid_to_connections.items(): + if p1!=p2: + if type(s1) is set and type(s2) is set: + if len(s1)>0 and len(s2)>0: + try: + inters = s1.intersection(s2) + except Exception: + print ('Exception during intersect inner') + pass + if (len(inters)>0): + print ('CON Intersection ' + str(p1) + ' ' + str(p2) + ' ' + str(inters)) + + # check for intersections + #for p1, s1 in slf.pid_to_files.items(): + # for p2, s2 in self.pid_to_files.items(): + # if p1!=p2 and len(s1.intersection(s2))!=0: + # print ('Intersection found files ' + str(p1) + ' ' + str(p2) + ' ' + s1.intersection(s2)) + except Exception as e: + exc_type, exc_obj, exc_tb = sys.exc_info() + fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] + print(exc_type, fname, exc_tb.tb_lineno) + print('Exception during intersect outer') + pass + + def is_good_candidate(self, candid, finishedtasks): + if self.procstatus[candid] != 'ToDo': + return False + needs = set([self.tasktoid[t] for t in self.taskneeds[self.idtotask[candid]]]) + if set(finishedtasks).intersection(needs) == needs: + return True + return False + + def emit_code_for_task(self, tid, lines): + actionlogger.debug("Submitting task " + str(self.idtotask[tid])) + taskspec = self.workflowspec['stages'][tid] + c = taskspec['cmd'] + workdir = taskspec['cwd'] + env = taskspec.get('env') + # in general: + # try to make folder + lines.append('[ ! -d ' + workdir + ' ] && mkdir ' + workdir + '\n') + # cd folder + lines.append('cd ' + workdir + '\n') + # set local environment + if env!=None: + for e in env.items(): + lines.append('export ' + e[0] + '=' + str(e[1]) + '\n') + # do command + lines.append(c + '\n') + # unset local environment + if env!=None: + for e in env.items(): + lines.append('unset ' + e[0] + '\n') + + # cd back + lines.append('cd $OLDPWD\n') + + + # produce a bash script that runs workflow standalone + def produce_script(self, filename): + # pick one of the correct task orderings + taskorder = self.topological_orderings[0] + outF = open(filename, "w") + + lines=[] + # header + lines.append('#!/usr/bin/env bash\n') + lines.append('#THIS FILE IS AUTOGENERATED\n') + lines.append('export JOBUTILS_SKIPDONE=ON\n') + + # we record the global environment setting + # in particular to capture global workflow initialization + lines.append('#-- GLOBAL INIT SECTION FROM WORKFLOW --\n') + for e in self.globalinit['env']: + lines.append('export ' + str(e) + '=' + str(self.globalinit['env'][e]) + '\n') + lines.append('#-- TASKS FROM WORKFLOW --\n') + for tid in taskorder: + print ('Doing task ' + self.idtotask[tid]) + self.emit_code_for_task(tid, lines) + + outF.writelines(lines) + outF.close() + + def production_endoftask_hook(self, tid): + # Executes a hook at end of a successful task, meant to be used in GRID productions. + # For the moment, archiving away log files, done + time files from jobutils. + # TODO: In future this may be much more generic tasks such as dynamic cleanup of intermediate + # files (when they are no longer needed). + # TODO: Care must be taken with the continue feature as `_done` files are stored elsewhere now + actionlogger.info("Cleaning up log files for task " + str(tid)) + logf = self.get_logfile(tid) + donef = self.get_done_filename(tid) + timef = logf + "_time" + + # add to tar file archive + tf = tarfile.open(name="pipeline_log_archive.log.tar", mode='a') + if tf != None: + tf.add(logf) + tf.add(donef) + tf.add(timef) + tf.close() + + # remove original file + os.remove(logf) + os.remove(donef) + os.remove(timef) + + # print error message when no progress can be made + def noprogress_errormsg(self): + # TODO: rather than writing this out here; refer to the documentation discussion this? + msg = """Scheduler runtime error: The scheduler is not able to make progress although we have a non-zero candidate set. + +Explanation: This is typically the case because the **ESTIMATED** resource requirements for some tasks +in the workflow exceed the available number of CPU cores or the memory (as explicitely or implicitely determined from the +--cpu-limit and --mem-limit options). Often, this might be the case on laptops with <=16GB of RAM if one of the tasks +is demanding ~16GB. In this case, one could try to tell the scheduler to use a slightly higher memory limit +with an explicit --mem-limit option (for instance `--mem-limit 20000` to set to 20GB). This might work whenever the +**ACTUAL** resource usage of the tasks is smaller than anticipated (because only small test cases are run). + +In addition it might be worthwile running the workflow without this resource aware, dynamic scheduler. +This is possible by converting the json workflow into a linearized shell script and by directly executing the shell script. +Use the `--produce-script myscript.sh` option for this. +""" + print (msg, file=sys.stderr) + + def execute(self): + self.start_time = time.perf_counter() + psutil.cpu_percent(interval=None) + os.environ['JOBUTILS_SKIPDONE'] = "ON" + errorencountered = False + + def speedup_ROOT_Init(): + """initialize some env variables that speed up ROOT init + and prevent ROOT from spawning many short-lived child + processes""" + + # only do it on Linux + if platform.system() != 'Linux': + return + + if os.environ.get('ROOT_LDSYSPATH')!=None and os.environ.get('ROOT_CPPSYSINCL')!=None: + # do nothing if already defined + return + + # a) the PATH for system libraries + # search taken from ROOT TUnixSystem + cmd='LD_DEBUG=libs LD_PRELOAD=DOESNOTEXIST ls /tmp/DOESNOTEXIST 2>&1 | grep -m 1 "system search path" | sed \'s/.*=//g\' | awk \'//{print $1}\'' + proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + libpath, err = proc.communicate() + if not (args.no_rootinit_speedup == True): + print ("setting up ROOT system") + os.environ['ROOT_LDSYSPATH'] = libpath.decode() + os.environ['CLING_LDSYSPATH'] = libpath.decode() + + # b) the PATH for compiler includes needed by Cling + cmd = "LC_ALL=C c++ -xc++ -E -v /dev/null 2>&1 | sed -n '/^#include/,${/^ \\/.*++/{p}}'" + proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + incpath, err = proc.communicate() + incpaths = [ line.lstrip() for line in incpath.decode().splitlines() ] + joined = ':'.join(incpaths) + if not (args.no_rootinit_speedup == True): + actionlogger.info("Determined ROOT_CPPSYSINCL=" + joined) + os.environ['ROOT_CPPSYSINCL'] = joined + os.environ['CLING_CPPSYSINCL'] = joined + + speedup_ROOT_Init() + + # we make our own "tmp" folder + # where we can put stuff such as tmp socket files etc (for instance DPL FAIR-MQ sockets) + # (In case of running within docker/singularity, this may not be so important) + if not os.path.isdir("./.tmp"): + os.mkdir("./.tmp") + if os.environ.get('FAIRMQ_IPC_PREFIX')==None: + socketpath = os.getcwd() + "/.tmp" + actionlogger.info("Setting FAIRMQ socket path to " + socketpath) + os.environ['FAIRMQ_IPC_PREFIX'] = socketpath + + # some maintenance / init work + if args.list_tasks: + print ('List of tasks in this workflow:') + for i,t in enumerate(self.workflowspec['stages'],0): + print (t['name'] + ' (' + str(t['labels']) + ')' + ' ToDo: ' + str(not self.ok_to_skip(i))) + exit (0) + + if args.produce_script != None: + self.produce_script(args.produce_script) + exit (0) + + # execute the user-given global init cmd for this workflow + globalinitcmd = self.globalinit.get("cmd", None) + if globalinitcmd != None: + if not self.execute_globalinit_cmd(globalinitcmd): + exit (1) + + if args.rerun_from: + reruntaskfound=False + for task in self.workflowspec['stages']: + taskname=task['name'] + if re.match(args.rerun_from, taskname): + reruntaskfound=True + taskid=self.tasktoid[taskname] + self.remove_done_flag(find_all_dependent_tasks(self.possiblenexttask, taskid)) + if not reruntaskfound: + print('No task matching ' + args.rerun_from + ' found; cowardly refusing to do anything ') + exit (1) + + # ***************** + # main control loop + # ***************** + candidates = [ tid for tid in self.possiblenexttask[-1] ] + + self.process_list=[] # list of tuples of nodes ids and Popen subprocess instances + + finishedtasks=[] # global list of finished tasks + + try: + + while True: + # sort candidate list according to task weights + candidates = [ (tid, self.taskweights[tid]) for tid in candidates ] + candidates.sort(key=lambda tup: (tup[1][0],-tup[1][1])) # prefer small and same timeframes first then prefer important tasks within frameframe + # remove weights + candidates = [ tid for tid,_ in candidates ] + + finished = [] # --> to account for finished because already done or skipped + actionlogger.debug('Sorted current candidates: ' + str([(c,self.idtotask[c]) for c in candidates])) + self.try_job_from_candidates(candidates, finished) + if len(candidates) > 0 and len(self.process_list) == 0: + self.noprogress_errormsg() + send_webhook(self.args.webhook,"Unable to make further progress: Quitting") + errorencountered = True + break + + finished_from_started = [] # to account for finished when actually started + failing = [] + while self.waitforany(self.process_list, finished_from_started, failing): + if not args.dry_run: + self.monitor(self.process_list) # ---> make async to normal operation? + time.sleep(1) # <--- make this incremental (small wait at beginning) + else: + time.sleep(0.001) + + finished = finished + finished_from_started + actionlogger.debug("finished now :" + str(finished_from_started)) + finishedtasks = finishedtasks + finished + + # perform file cleanup + if self.do_early_file_removal: + self.perform_early_file_removal(finished_from_started) + + if self.is_productionmode: + # we can do some generic cleanup of finished tasks in non-interactive/GRID mode + # TODO: this can run asynchronously + for _t in finished_from_started: + self.production_endoftask_hook(_t) + + # if a task was marked "failed" and we come here (because + # we use --keep-going) ... we need to take out the pid from finished + if len(failing) > 0: + # remove these from those marked finished in order + # not to continue with their children + errorencountered = True + for t in failing: + finished = [ x for x in finished if x != t ] + finishedtasks = [ x for x in finishedtasks if x != t ] + + # if a task was marked as "retry" we simply put it back into the candidate list + if len(self.tids_marked_toretry) > 0: + # we need to remove these first of all from those marked finished + for t in self.tids_marked_toretry: + finished = [ x for x in finished if x != t ] + finishedtasks = [ x for x in finishedtasks if x != t ] + + candidates = candidates + self.tids_marked_toretry + self.tids_marked_toretry = [] + + + # new candidates + for tid in finished: + if self.possiblenexttask.get(tid)!=None: + potential_candidates=list(self.possiblenexttask[tid]) + for candid in potential_candidates: + # try to see if this is really a candidate: + if self.is_good_candidate(candid, finishedtasks) and candidates.count(candid)==0: + candidates.append(candid) + + actionlogger.debug("New candidates " + str( candidates)) + send_webhook(self.args.webhook, "New candidates " + str(candidates)) + + if len(candidates)==0 and len(self.process_list)==0: + break + except Exception as e: + exc_type, exc_obj, exc_tb = sys.exc_info() + fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] + print(exc_type, fname, exc_tb.tb_lineno) + traceback.print_exc() + print ('Cleaning up ') + + self.SIGHandler(0,0) + + endtime = time.perf_counter() + statusmsg = "success" + if errorencountered: + statusmsg = "with failures" + + print ('\n**** Pipeline done ' + statusmsg + ' (global_runtime : {:.3f}s) *****\n'.format(endtime-self.start_time)) + actionlogger.debug("global_runtime : {:.3f}s".format(endtime-self.start_time)) + return errorencountered + + +if args.cgroup!=None: + myPID=os.getpid() + # cgroups such as /sys/fs/cgroup/cpuset//tasks + # or /sys/fs/cgroup/cpu//tasks + command="echo " + str(myPID) + f" > {args.cgroup}" + actionlogger.info(f"Try running in cgroup {args.cgroup}") + waitstatus = os.system(command) + if code := os.waitstatus_to_exitcode(waitstatus): + actionlogger.error(f"Could not apply cgroup") + exit(code) + actionlogger.info("Running in cgroup") + + +# This starts the fanotify fileaccess monitoring process +# if asked for +o2dpg_filegraph_exec = os.getenv("O2DPG_PRODUCE_FILEGRAPH") # switches filegraph monitoring on and contains the executable name +if o2dpg_filegraph_exec: + env = os.environ.copy() + env["FILEACCESS_MON_ROOTPATH"] = os.getcwd() + env["MAXMOTHERPID"] = f"{os.getpid()}" + + fileaccess_log_file_name = f"pipeline_fileaccess_{os.getpid()}.log" + fileaccess_log_file = open(fileaccess_log_file_name, "w") + fileaccess_monitor_proc = subprocess.Popen( + [o2dpg_filegraph_exec], + stdout=fileaccess_log_file, + stderr=subprocess.STDOUT, + env=env) +else: + fileaccess_monitor_proc = None + +try: + # This is core workflow runner invocation + executor=WorkflowExecutor(args.workflowfile,jmax=int(args.maxjobs),args=args) + rc = executor.execute() +finally: + if fileaccess_monitor_proc: + fileaccess_monitor_proc.terminate() # sends SIGTERM + try: + fileaccess_monitor_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + fileaccess_monitor_proc.kill() # force kill if not stopping + # now produce the final filegraph output + o2dpg_root = os.getenv("O2DPG_ROOT") + analyse_cmd = [ + sys.executable, # runs with same Python interpreter + f"{o2dpg_root}/UTILS/FileIOGraph/analyse_FileIO.py", + "--actionFile", actionlogger_file, + "--monitorFile", fileaccess_log_file_name, + "-o", f"pipeline_fileaccess_report_{os.getpid()}.json", + "--basedir", os.getcwd() ] + print (f"Producing FileIOGraph with command {analyse_cmd}") + subprocess.run(analyse_cmd, check=True) + +sys.exit(rc) \ No newline at end of file diff --git a/MC/workflow_runner/o2dpg_runner/README.md b/MC/workflow_runner/o2dpg_runner/README.md new file mode 100644 index 000000000..768ba592b --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/README.md @@ -0,0 +1,304 @@ +# o2dpg_runner — modular rewrite of the O2DPG workflow runner + +This package is a modular rewrite of the single-file runner +`MC/bin/o2_dpg_workflow_runner.py` (~2000 lines, module-global state). +Both are installed; which one runs is chosen at run time, so the two can be +compared on the same job. Every flag the original parser accepts, this one +accepts, with the same semantics. + +## Where it lives, and how it is selected + +The package and its entry point live in `MC/workflow_runner/`: + +``` +MC/workflow_runner/ + o2dpg_workflow_runner.py # entry point + o2dpg_schedule_simulator.py + o2dpg_runner/ + cli.py # argparse -> RunnerConfig -> Executor + config.py # RunnerConfig dataclass + workflow.py # load / filter / build DAG + graph.py # Kahn topo sort, memoized descendants + resources.py # TaskResources, ResourceManager + monitoring.py # threaded psutil monitor + scheduler/ # timeframe (default), critical_path, best_fit + executor.py # main control loop + cleanup.py # early file removal, log archival + alienv.py # alienv env resolution + cache.py # _done + _done.json fingerprint cache + tests/ +``` + +Nothing calls it directly. `MC/bin/o2_dpg_workflow_runner.py` is a +dispatcher that execs either this runner or the original one, and every +existing call site goes through it unchanged: + +```bash +ALIEN_O2DPG_WORKFLOW_RUNNER=new $O2DPG_ROOT/MC/bin/o2_dpg_workflow_runner.py -f workflow.json +``` + +`legacy` is the default and runs `MC/bin/o2dpg_workflow_runner_legacy.py`. +An unknown value is an error, not a silent fallback. + +No new required dependencies. `psutil` and optionally `graphviz` as before. + +## What changed behaviorally + +The default invocation should reproduce prototype behavior bit-for-bit +(same scheduling decisions, same output files, same log formats). +Everything new is opt-in. + +### New CLI flags (all default to current behavior) + +| Flag | Default | Effect | +| ----------------------------- | ------------- | ------------------------------------------------------------------------------ | +| `--scheduler-policy` | `timeframe` | Choose: `timeframe` (legacy), `critical-path`, or `best-fit`. | +| `--drop-should-break` | off | In `timeframe`, let light tasks slip past a non-fitting heavy task. | +| `--monitor-interval-cpu` | `1.0` (s) | CPU polling cadence for the background monitor thread. | +| `--monitor-interval-mem` | `5.0` (s) | PSS polling cadence (much cheaper to read less often). | +| `--monitor-backend` | `psutil` | Reserved for a future cgroup-v2 backend. | +| `--cache-policy` | `off` | Task-completion cache: `off` (legacy), `lenient`, `strict`. See below. | + +### Removed flags + +- `--webhook` — the debug Mattermost channel integration is gone. The + flag is still accepted (for compatibility) but ignored. +- `--checkpoint-on-failure` — the tarball + `alien.py cp` failure + checkpoint was only used for Grid debugging. Same: accepted, ignored. +- `--cgroup` — superseded by `--systemd-run`. Same: accepted, ignored. + All three log a warning when passed, and none of them aborts, so a JDL + passing one through `ALIEN_O2DPG_ADDITIONAL_WORKFLOW_RUNNER_ARGS` still + runs. + +### Monitoring + +The old runner polled psutil synchronously in the main scheduling loop, +costing 10–20 % of one core on realistic workflows. The new monitor runs +in a background thread with two independent cadences — CPU (cheap, +~1 Hz) and PSS (expensive, ~0.2 Hz). The scheduler reads the latest +snapshot non-blockingly. Result: the runner's self-CPU drops to ~1-2 %. + +The scheduler's dynamic-resource sampling (`--dynamic-resources`) still +triggers in `ResourceManager.unbook()`, preserving ordering with +respect to task completions. + +### Scheduler policies + +Three policies ship, switchable via `--scheduler-policy`: + +- **`timeframe`** (default) — exact prototype behavior. Sort by + `(timeframe, -num_descendants)`; first non-fitting task in the default + pass breaks the pass (this is the legacy quirk that blocks light tasks + behind heavy ones). Set `--drop-should-break` to disable that quirk. +- **`critical-path`** — sort by longest-remaining CPU-weighted path to a + leaf. Standard HEFT-style heuristic; tends to win when resource + estimates are accurate (after `--update-resources`). +- **`best-fit`** — iterative best-fit bin-packing over the candidate set + against the remaining CPU/MEM budget. Maximizes parallelism at the cost + of slightly less predictable task ordering. + +Comparing these three on the same workflow with the same +estimates gives a direct A/B measurement of scheduling strategy impact. + +### Cache policy + +`_done` files remain the primary skip marker (O2 taskwrapper compatibility +is preserved). With `--cache-policy lenient`, a sidecar `_done.json` is +written containing a fingerprint of (command, env subset, software tag, +needs). On rerun, if the command or `needs` list changed, the `_done` +file is removed and the task re-runs. `strict` additionally invalidates +on env/software changes. + +No behavior change unless you pass the flag. + +## What to know + +Things that only came out of running this, and that cost time to find again. + +**A task's first CPU reading is always 0.** `psutil.cpu_percent(interval=None)` +has no baseline on its first call and returns `0.0` by construction. That is +why `sample_resources()` refuses to learn from fewer than three samples, and +why a sampled CPU of `0` is treated as missing information rather than as a +task that needs no CPU. Propagating a zero would make every sibling look free +and admit them all at once. + +**A monitor tick is not a poll.** The wait loop polls at 0.1 s ramping to 1 s +while the monitor thread fires at `--monitor-interval-cpu`. Samples must be +taken once per tick; recording per poll grows the sample lists without bound +over a long job and, worse, satisfies the three-sample guard with copies of +one reading. + +**A tracer or a monitor has to sit inside the systemd scope.** With +`--systemd-run`, wrapping the outside of `systemd-run` means observing +`systemd-run` and nothing else. The wrapper goes on the inner command. + +**`--maxjobs 1` serialises a workflow; `--cpu-limit 1` does not.** The latter +makes every task that declares more than one core unschedulable, and the run +stops with `ResourceLimitExceeded`. + +**The prototype and this runner disagree on short tasks, in this runner's +favour.** The prototype samples from its scheduling loop at roughly 5 s, so a +task shorter than ~15 s never reaches three samples and `--dynamic-resources` +learns nothing from it. The monitor thread here runs at 1 s. + +## Bug fixes incorporated + +Silently fixed relative to the prototype: + +1. `TaskResources.is_within_limits()` compared CPU to mem_limit instead + of MEM to mem_limit; the memory safety net was effectively disabled. +2. `find_all_dependent_tasks()` cached duplicates but returned + deduped; cache hits returned different values than cache misses. +3. `filter_workflow()` aliased the caller's dict and mutated it in place. +4. `getallrequirements()` recursed without memoization — exponential on + diamond DAGs; `sys.setrecursionlimit(100000)` was a workaround. +5. `send_webhook()` shell-interpolated task names into `os.system` + (command injection). Removed along with the webhook feature. +6. `SIGHandler` only caught SIGINT; Grid preemption via SIGTERM was + ignored. Now both are handled. +7. The emitted `produce_script` output used `cd $OLDPWD` which broke if + a task `cd`s internally. Now uses subshells: `( cd "$workdir" && ... )`. +8. `candidates` list used `.count()` for membership checks (O(n)); + replaced by set-based lookups. +9. A sampled CPU of `0` was propagated to the un-started sibling tasks, + which then all looked free and were admitted at once. It is now treated + as missing information, as a sampled memory of `0` already was. The + prototype carries this too, masked by its slower monitoring. +10. The dry-run path returned a plain `subprocess.Popen`, which has no + `.nice()`. Fixed here the same way it was later fixed in the + prototype: by returning a `psutil.Popen`. + +## Running the tests + +```bash +cd MC/workflow_runner +python -m pytest o2dpg_runner/tests/ -q +``` + +`pytest` is required. The tests run in CI, in the `Workflow-runner unit +tests` job of `.github/workflows/syntax-checks.yml`. + +The tests cover: +- `test_graph.py` — Kahn topological sort, memoized descendants/ancestors, + longest path, diamond + deep-chain cases. +- `test_workflow.py` — load, global-init extraction, filtering by target + and by label, regex, resource-estimate update. +- `test_resources.py` — booking/unbooking, semaphores, related-task + grouping, dynamic sampling, limit enforcement. +- `test_scheduler.py` — all three policies, the `should_break` quirk + and its removal, `n_backfill_max` cap, semaphore blocking. +- `test_cache.py` — cache policies (off/lenient/strict), fingerprint + sensitivity, sidecar round-trip. +- `test_executor_e2e.py` — the tiny fixture workflow driven end-to-end + with real subprocesses, exercising each policy, `--dry-run`, + `--produce-script`, rerun-from-cache behavior. +- `test_simulator.py` — simulator-only coverage for Amdahl-derived + critical-path weights, unschedulable-task handling, and simulated + backfill behaviour (`slowdown` and `holefill`). + +Integration test (from the prototype, still valid): +```bash +NSIGEVENTS=5 NTIMEFRAMES=2 bash MC/bin/tests/wf_test_pp.sh +``` + +## A/B measurement + +The Python entry point accepts the same `workflow.json` under all +scheduling policies. A typical comparison: + +```bash +# baseline (prototype-equivalent) +./o2dpg_workflow_runner.py -f wf.json \ + --metric-logfile metric_timeframe.log + +# drop the should_break quirk +./o2dpg_workflow_runner.py -f wf.json --drop-should-break \ + --metric-logfile metric_timeframe_nobrk.log + +# critical path +./o2dpg_workflow_runner.py -f wf.json --scheduler-policy critical-path \ + --metric-logfile metric_cp.log + +# best-fit bin-packing +./o2dpg_workflow_runner.py -f wf.json --scheduler-policy best-fit \ + --metric-logfile metric_bf.log +``` + +The metric logs have the same schema as before (`o2dpg_sim_metrics.py` +post-processing is unaffected), plus the run's meta line now records +`scheduler_policy`, `drop_should_break`, and `cache_policy` for easy +downstream grouping. + +## Simulator notes + +`MC/bin/o2dpg_schedule_simulator.py` is an offline discrete-event model +of the runner. It exists to compare policies and tune worker-count / +resource parameters quickly, not to emulate Linux scheduling perfectly. + +- The simulator now uses the same walltime-weighted critical-path input + as the runner when learned `resources.walltime` data is available. +- Amdahl worker overrides are applied before simulator scheduler-state + construction, so optimization runs evaluate policies against the same + task costs they simulate. +- Tasks that exceed the hard simulated CPU/MEM limits are kept in the + workflow model and reported as unschedulable, rather than being + dropped from resource bookkeeping. + +### Simulated backfill + +The real runner has a two-lane admission model: default tasks stay +within the hard budget, while backfill tasks may use a bounded amount of +overcommit and run at lower priority. The simulator now offers a +"sweet-spot" family of approximations for that behaviour: + +- `--backfill-model off` — no backfill, one hard budget only. +- `--backfill-model structural` — replay the runner's second admission + lane (`n_backfill`, CPU factor, MEM factor), but do not change task + duration. +- `--backfill-model slowdown` — same structural backfill admission, plus + a single fitted slowdown factor applied to backfill task walltimes. +- `--backfill-model holefill` — preferred realistic mode. Foreground + tasks keep their nominal duration; backfill tasks consume only the CPU + left idle by currently running foreground tasks. When the foreground + hole is smaller than the task's nominal CPU demand, the task slows + down proportionally. When the hole is large enough, it runs at nominal + speed. + +Relevant knobs: + +- `--n-backfill` +- `--backfill-cpu-factor` +- `--backfill-mem-factor` +- `--backfill-slowdown-factor` + +`holefill` is the recommended mode for scheduling studies: it +keeps simulated CPU efficiency physically bounded by 100%, tracks +observed runtime improvements from backfilling much better than the +single-factor slowdown model, and still stays simple enough to explain. + +Two modelling choices are worth keeping in mind: + +- foreground tasks are assumed not to slow down due to backfill; +- hole allocation is online and greedy, so enabling backfill may still + change the order in which later tasks become runnable. + +This is intentionally a scheduler-level approximation, not a kernel CPU +sharing model. It is accurate enough for comparative studies while still +remaining easy to reason about and calibrate against real runs. + +### Suggested simulator usage + +For realistic policy comparison, use learned resources and the holefill +backfill model: + +```bash +./o2dpg_schedule_simulator.py \ + --timeframes 1 2 4 5 8 12 20 \ + --update-resources learned.json \ + -f workflow.json \ + --backfill-model holefill +``` + +Use `off` as the baseline and `holefill` as the realistic backfill +comparison. The older `slowdown` mode remains useful as a coarse control +study, but it is no longer the preferred setting for reporting numbers. diff --git a/MC/workflow_runner/o2dpg_runner/__init__.py b/MC/workflow_runner/o2dpg_runner/__init__.py new file mode 100644 index 000000000..0c8a58525 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/__init__.py @@ -0,0 +1,11 @@ +""" +o2dpg_runner: modular rewrite of the O2DPG workflow runner. + +Originally started February 2021 by sandro.wenzel@cern.ch as a single-file +prototype (MC/bin/o2dpg_workflow_runner.py). This package is the modular +refactor, keeping full CLI and behavioral compatibility by default while +exposing pluggable scheduling policies, a threaded resource monitor, and +a cleaner testable structure. +""" + +__version__ = "2.0.0" diff --git a/MC/workflow_runner/o2dpg_runner/alienv.py b/MC/workflow_runner/o2dpg_runner/alienv.py new file mode 100644 index 000000000..692e0f077 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/alienv.py @@ -0,0 +1,66 @@ +"""Resolving alternative software environments via alienv.""" + +from __future__ import annotations + +import logging +import os +import subprocess +from typing import Dict, Optional + +log = logging.getLogger(__name__) + + +def _load_env_file(path: str) -> Dict[str, str]: + env: Dict[str, str] = {} + with open(path, "r") as f: + for raw in f: + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith("declare -x "): + line = line.replace("declare -x ", "", 1) + if "=" not in line: + env[line.strip()] = "" + else: + k, v = line.split("=", 1) + env[k.strip()] = v.strip('"') + return env + + +def get_alienv_software_environment(packagestring: Optional[str]) -> Dict[str, str]: + """Resolve ``packagestring`` to an env dict. + + Accepts: + - None / '' / 'None' -> empty dict + - a path to a file -> 'export > env.txt' format, parsed + - an alienv spec -> calls /cvmfs/alice.cern.ch/bin/alienv printenv + """ + if not packagestring or packagestring == "None": + return {} + + if os.path.exists(packagestring) and os.path.isfile(packagestring): + log.info("Taking software environment from file %s", packagestring) + return _load_env_file(packagestring) + + cmd = "/cvmfs/alice.cern.ch/bin/alienv printenv " + packagestring + proc = subprocess.Popen( + [cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + envstring, err = proc.communicate() + if err: + err_s = err.decode() + if err_s.strip(): + print(err_s) + raise RuntimeError(f"alienv printenv failed for {packagestring}") + + envmap: Dict[str, str] = {} + for t in envstring.decode().split(";"): + if "=" in t: + k, v = t.rstrip().split("=", 1) + envmap[k] = v + elif "export" in t: + tokens = t.split() + if len(tokens) >= 2: + variable = tokens[1] + envmap.setdefault(variable, "") + return envmap diff --git a/MC/workflow_runner/o2dpg_runner/cache.py b/MC/workflow_runner/o2dpg_runner/cache.py new file mode 100644 index 000000000..b7e937f0b --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/cache.py @@ -0,0 +1,192 @@ +"""Task-completion cache. + +Preserves the O2-taskwrapper-compatible `.log_done` marker file as +the primary source of truth (so that downstream tools and the wrapper +itself continue to work unchanged), and optionally writes a sidecar +`.log_done.json` containing a fingerprint of the inputs that +determine the task's output. + +Cache policies: + off - current behavior: a _done file means "skip" + lenient - read _done.json when present; invalidate only if the + command string changed. Warn on env / software changes. + strict - invalidate on any fingerprint change. + +Fingerprint components: + cmd_hash hash of the task command string + env_hash hash of the allow-listed subset of the task env + software the alienv package string (or '' if default) + needs list of upstream task names + +The sidecar is written best-effort after the _done file exists (so +torn writes don't leave stale fingerprints around). +""" + +from __future__ import annotations + +import hashlib +import json +import logging +import os +from typing import Dict, List, Optional, Tuple + +log = logging.getLogger(__name__) + +# Env vars that actually affect task semantics. Keep this conservative: +# everything else is considered noise (TMPDIR, PWD, terminal colors, etc.). +# Users can override via the workflow by not relying on env and by expressing +# variation through the cmd itself. +SEMANTIC_ENV_KEYS = ( + "ALICE_O2_VERSION", "O2_ROOT", "O2DPG_ROOT", "O2PHYSICS_ROOT", + "NEVENTS", "NWORKERS", "SEED", "INTERACTIONRATE", + "SIMENGINE", "NSIGEVENTS", "NTIMEFRAMES", "GENERATOR", + "ALIDIST_TAG", +) + + +def _hash(*parts: str) -> str: + h = hashlib.sha256() + for p in parts: + h.update(p.encode("utf-8", "replace")) + h.update(b"\x1f") # separator + return h.hexdigest()[:16] + + +def compute_fingerprint( + task: Dict, + alienv_package: str = "", + allow_env_keys: Tuple[str, ...] = SEMANTIC_ENV_KEYS, +) -> Dict[str, str]: + """Compute a fingerprint for a task spec. + + Does NOT depend on upstream task fingerprints -- upstream changes + propagate via the _done/_done.json of upstream tasks (if upstream + was re-run, its _done file was removed and the current task will + see a missing dependency marker and re-run). + """ + cmd = task.get("cmd", "") or "" + env_subset = {} + task_env = task.get("env") or {} + for k in allow_env_keys: + if k in task_env: + env_subset[k] = str(task_env[k]) + env_str = json.dumps(env_subset, sort_keys=True) + needs = sorted(task.get("needs", []) or []) + needs_str = json.dumps(needs) + + return { + "cmd_hash": _hash(cmd), + "env_hash": _hash(env_str), + "software": alienv_package or "", + "needs": needs, + "cmd_preview": cmd[:120], # for human debugging + } + + +def done_path(logfile: str) -> str: + return logfile + "_done" + + +def fingerprint_path(logfile: str) -> str: + return logfile + "_done.json" + + +class TaskCache: + """Policy-aware interface for checking/recording task completion.""" + + def __init__(self, policy: str = "off"): + if policy not in ("off", "lenient", "strict"): + raise ValueError(f"unknown cache policy: {policy}") + self.policy = policy + + def is_done(self, logfile: str, current_fp: Dict[str, str]) -> bool: + """Return True iff the task can be skipped. + + logfile: path like /cwd/taskname.log (we append _done / _done.json) + current_fp: fingerprint dict from compute_fingerprint(task) + """ + dp = done_path(logfile) + if not (os.path.exists(dp) and os.path.isfile(dp)): + return False + + if self.policy == "off": + return True + + fp_path = fingerprint_path(logfile) + if not os.path.exists(fp_path): + # Old run; nothing to compare against. + if self.policy == "strict": + log.info("%s: strict cache policy but no fingerprint -> invalidating", logfile) + self._invalidate(logfile) + return False + log.debug("%s: no fingerprint sidecar; keeping _done (lenient)", logfile) + return True + + try: + with open(fp_path) as f: + prev = json.load(f) + except (OSError, json.JSONDecodeError) as e: + log.warning("%s: fingerprint unreadable (%s); invalidating", fp_path, e) + self._invalidate(logfile) + return False + + cmd_changed = prev.get("cmd_hash") != current_fp["cmd_hash"] + env_changed = prev.get("env_hash") != current_fp["env_hash"] + sw_changed = prev.get("software") != current_fp["software"] + needs_changed = prev.get("needs") != current_fp["needs"] + + if self.policy == "lenient": + if cmd_changed or needs_changed: + log.info("%s: cmd/needs changed -> invalidating (lenient)", logfile) + self._invalidate(logfile) + return False + if env_changed: + log.warning("%s: env fingerprint changed but keeping cache (lenient)", logfile) + if sw_changed: + log.warning("%s: software fingerprint changed but keeping cache (lenient)", + logfile) + return True + + # strict + if cmd_changed or env_changed or sw_changed or needs_changed: + reasons = [] + if cmd_changed: reasons.append("cmd") + if env_changed: reasons.append("env") + if sw_changed: reasons.append("software") + if needs_changed: reasons.append("needs") + log.info("%s: changed (%s) -> invalidating (strict)", logfile, ",".join(reasons)) + self._invalidate(logfile) + return False + return True + + def record(self, logfile: str, current_fp: Dict[str, str]) -> None: + """Write the fingerprint sidecar after the task's _done file exists. + + Best effort: errors are logged but not raised. + """ + if self.policy == "off": + return + dp = done_path(logfile) + if not os.path.exists(dp): + # _done doesn't exist (e.g. skipped in dry-run); nothing to record. + return + try: + with open(fingerprint_path(logfile), "w") as f: + json.dump(current_fp, f, indent=2) + except OSError as e: + log.warning("Could not write fingerprint for %s: %s", logfile, e) + + def _invalidate(self, logfile: str) -> None: + for p in (done_path(logfile), fingerprint_path(logfile)): + try: + if os.path.exists(p): + os.remove(p) + except OSError as e: + log.warning("Could not remove %s: %s", p, e) + + +def remove_done_flag(logfile: str) -> None: + """Explicit invalidation used by --rerun-from.""" + for p in (done_path(logfile), fingerprint_path(logfile)): + if os.path.exists(p) and os.path.isfile(p): + os.remove(p) diff --git a/MC/workflow_runner/o2dpg_runner/cleanup.py b/MC/workflow_runner/o2dpg_runner/cleanup.py new file mode 100644 index 000000000..a5e526ea5 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/cleanup.py @@ -0,0 +1,221 @@ +"""Cleanup utilities invoked after a task completes. + +Two concerns: + 1. Early file removal based on a FileIOGraph-derived file dependency map + (--remove-files-early). Files that no task will read/write again are + deleted to keep disc pressure low during large productions. + + 2. Production-mode log archival: .log / .log_done / .log_time files are + appended to a tar archive and removed. Mirrors the prototype's + production_endoftask_hook(). + +Both are side-effecting; errors are logged but don't abort the run. +""" + +from __future__ import annotations + +import json +import logging +import os +import re +import tarfile +from typing import Dict, List, Optional, Set + +log = logging.getLogger(__name__) + +_TF_PATH_RE = re.compile(r"^\./tf(?P\d+)/") + + +def _task_template_for_timeframe(task_name: str, source_tf: int) -> str: + """Turn task_7 from a tf7 observation into task_{tf}. + + Global tasks such as aodmerge, and any explicit cross-timeframe task + references, are left untouched. + """ + suffix = f"_{source_tf}" + if task_name.endswith(suffix): + return f"{task_name[:-len(suffix)]}_{{tf}}" + return task_name + + +def _task_template_from_placeholder(task_name: str) -> str: + return re.sub(r"_X$", "_{tf}", task_name) + + +def _filegraph_expand_timeframes( + data: Dict, + timeframes: Set[int], + target_namelist: List[str], + logger: Optional[logging.Logger] = None, +) -> Dict[str, List[Dict]]: + """Build canonical per-timeframe file templates and replicate per TF. + + The FileIOGraph may have been recorded with one or many timeframes. We + merge all observed timeframe-local entries by normalising ``./tfX/...`` to + ``./tf{tf}/...`` and task suffixes ``_X`` to ``_{tf}``, then instantiate the + merged template for the timeframes of the current workflow. + """ + logger = logger if logger is not None else log + templates: Dict[str, Dict] = {} + source_tfs: Set[int] = set() + + template_report = data.get("file_template_report") or [] + if template_report: + for entry in template_report: + filename = entry.get("file", "") + if filename.startswith("./tfX/"): + file_template = filename.replace("./tfX/", "./tf{tf}/", 1) + elif "./tf{tf}/" in filename: + file_template = filename + else: + continue + + source_tfs.update(int(tf) for tf in entry.get("source_timeframes", [])) + merged = templates.setdefault( + file_template, + { + "file": file_template, + "written_by": set(), + "read_by": set(), + "keep": bool(entry.get("keep", False)), + }, + ) + merged["keep"] = merged["keep"] or bool(entry.get("keep", False)) + for task in entry.get("written_by", []): + merged["written_by"].add(_task_template_from_placeholder(task)) + for task in entry.get("read_by", []): + merged["read_by"].add(_task_template_from_placeholder(task)) + else: + for entry in data.get("file_report", []): + filename = entry.get("file", "") + match = _TF_PATH_RE.match(filename) + if match is None: + continue + + source_tf = int(match.group("tf")) + source_tfs.add(source_tf) + file_template = _TF_PATH_RE.sub("./tf{tf}/", filename, count=1) + merged = templates.setdefault( + file_template, + { + "file": file_template, + "written_by": set(), + "read_by": set(), + "keep": bool(entry.get("keep", False)), + }, + ) + merged["keep"] = merged["keep"] or bool(entry.get("keep", False)) + for task in entry.get("written_by", []): + merged["written_by"].add(_task_template_for_timeframe(task, source_tf)) + for task in entry.get("read_by", []): + merged["read_by"].add(_task_template_for_timeframe(task, source_tf)) + + if not templates: + logger.warning("FileIOGraph contains no ./tfN/ file entries; early removal disabled") + return {} + + logger.info( + "FileIOGraph timeframe template built from observed timeframe(s) %s: %d file pattern(s)", + sorted(source_tfs), + len(templates), + ) + + result: Dict[str, List[Dict]] = {} + for i in timeframes: + if i == -1: + continue + new_entries: List[Dict] = [] + for template in templates.values(): + written_by = sorted(t.format(tf=i) for t in template["written_by"]) + read_by = sorted(t.format(tf=i) for t in template["read_by"]) + expanded = { + "file": template["file"].format(tf=i), + "written_by": written_by, + "read_by": read_by, + } + if template["keep"] or any(w in target_namelist for w in written_by): + expanded["keep"] = True + new_entries.append(expanded) + result[f"timeframe-{i}"] = new_entries + return result + + +class EarlyFileRemover: + """Owns the timeframe-expanded file dependency dict and performs + per-task-completion file deletion.""" + + def __init__( + self, + filegraph_path: str, + timeframes: Set[int], + target_namelist: List[str], + logger: Optional[logging.Logger] = None, + ): + self.log = logger if logger is not None else log + with open(filegraph_path) as f: + data = json.load(f) + self.file_dict = _filegraph_expand_timeframes( + data, timeframes, target_namelist, logger=self.log, + ) + self.timeframes = timeframes + # Pre-build a reverse index: task_name -> list[file_entry dict] (all TFs) + # so completions don't re-scan the whole file map. + self._by_task: Dict[str, List[Dict]] = {} + for entries in self.file_dict.values(): + for e in entries: + for t in e.get("written_by", []): + self._by_task.setdefault(t, []).append(e) + for t in e.get("read_by", []): + self._by_task.setdefault(t, []).append(e) + + def on_task_done(self, taskname: str) -> None: + entries = self._by_task.get(taskname, []) + for entry in entries: + if taskname in entry.get("read_by", []): + entry["read_by"].remove(taskname) + if taskname in entry.get("written_by", []): + entry["written_by"].remove(taskname) + if (not entry.get("read_by") and not entry.get("written_by") + and not entry.get("keep", False)): + self._remove_if_exists(entry["file"]) + + def _remove_if_exists(self, path: str) -> bool: + if os.path.exists(path): + try: + sz = os.path.getsize(path) + os.remove(path) + self.log.info("Removing %s (no longer needed); freed %.2f MB", + path, sz / 1024.0 / 1024.0) + return True + except OSError as e: + self.log.warning("Could not remove %s: %s", path, e) + return False + + +def archive_task_logs(logfile: str, logger: Optional[logging.Logger] = None) -> None: + """Append , _done, _time to a tar archive + and delete the originals. Used in production mode.""" + logger = logger if logger is not None else log + done = logfile + "_done" + timef = logfile + "_time" + try: + tf = tarfile.open(name="pipeline_log_archive.log.tar", mode="a") + except Exception as e: + logger.warning("Could not open log archive: %s", e) + return + try: + for path in (logfile, done, timef): + if os.path.exists(path): + try: + tf.add(path) + except Exception as e: + logger.warning("tar add %s failed: %s", path, e) + finally: + tf.close() + + for path in (logfile, done, timef): + if os.path.exists(path): + try: + os.remove(path) + except OSError as e: + logger.warning("Could not remove %s: %s", path, e) diff --git a/MC/workflow_runner/o2dpg_runner/cli.py b/MC/workflow_runner/o2dpg_runner/cli.py new file mode 100644 index 000000000..defab9ad2 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/cli.py @@ -0,0 +1,468 @@ +"""Command-line entry point. + +Translates argparse -> RunnerConfig, builds loggers, creates the +WorkflowExecutor, and invokes it. All semantics of the original script +are preserved; new flags are additive and default-compatible. +""" + +from __future__ import annotations + +import argparse +import json +import logging +import os +import shutil +import subprocess +import sys +from typing import Optional, Tuple + +import psutil + +from .config import RunnerConfig +from .workflow import build_workflow, load_json +from .executor import WorkflowExecutor + +_FORMATTER = logging.Formatter("%(asctime)s %(levelname)s %(message)s") +_IN_SLICE_ENV = "O2DPG_RUNNER_IN_SLICE" + + +def _setup_logger(name: str, logfile: str, level: int = logging.INFO) -> logging.Logger: + handler = logging.FileHandler(logfile, mode="w") + handler.setFormatter(_FORMATTER) + logger = logging.getLogger(name) + logger.setLevel(level) + logger.handlers.clear() + logger.addHandler(handler) + logger.propagate = False + return logger + + +def build_parser() -> argparse.ArgumentParser: + max_system_mem = psutil.virtual_memory().total + default_mem = 0.9 * max_system_mem / 1024.0 / 1024.0 + + p = argparse.ArgumentParser( + description="Parallel execution of an O2-DPG data/job DAG under resource constraints.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p.add_argument("-f", "--workflowfile", required=True) + p.add_argument("-jmax", "--maxjobs", type=int, default=100) + p.add_argument("-k", "--keep-going", action="store_true") + p.add_argument("--dry-run", action="store_true") + p.add_argument("--visualize-workflow", action="store_true") + p.add_argument("--target-labels", nargs="+", default=[]) + p.add_argument("-tt", "--target-tasks", nargs="+", default=["*"]) + p.add_argument("--produce-script", default=None) + p.add_argument("--rerun-from", default=None) + p.add_argument("--list-tasks", action="store_true") + + # Resources + p.add_argument("--update-resources", dest="update_resources", default=None) + p.add_argument("--dynamic-resources", dest="dynamic_resources", action="store_true") + p.add_argument("--optimistic-resources", dest="optimistic_resources", action="store_true") + p.add_argument("--n-backfill", dest="n_backfill", type=int, default=1) + p.add_argument("--mem-limit", type=float, default=default_mem, help="in MB") + p.add_argument("--cpu-limit", type=float, default=8) + + # systemd-run slice confinement (replaces the old --cgroup option) + p.add_argument( + "--systemd-run", + dest="systemd_run_spec", + default=None, + metavar="SPEC", + help=( + "Relaunch the whole runner (and all child processes) inside a transient " + "systemd scope unit with resource limits. SPEC is a slash-separated list " + "of key:value pairs. Supported keys: ncpus (number of CPU cores, e.g. 8), " + "mem (memory limit in systemd format, e.g. 16G or 16384M). " + "Either key may be omitted. Examples: \"ncpus:8/mem:16G\", \"ncpus:4\", \"mem:32G\"." + ), + ) + + # Scheduling (new) + p.add_argument("--scheduler-policy", default="timeframe", + choices=["timeframe", "critical-path", "best-fit"]) + p.add_argument("--drop-should-break", action="store_true", + help="In timeframe policy, don't stop scanning on the first " + "non-fitting task (lets light tasks slip past heavy ones).") + + # Monitoring (new) + p.add_argument("--monitor-interval-cpu", type=float, default=1.0) + p.add_argument("--monitor-interval-mem", type=float, default=1.0) + p.add_argument("--monitor-backend", default="psutil", choices=["psutil"]) + + # Cache (new) + p.add_argument("--cache-policy", default="off", + choices=["off", "lenient", "strict"]) + + # Control + p.add_argument("--stdout-on-failure", action="store_true") + p.add_argument("--retry-on-failure", type=int, default=0) + p.add_argument("--no-rootinit-speedup", action="store_true") + p.add_argument("--remove-files-early", type=str, default="") + + # Accept-and-ignore for backward compatibility of call sites + # that still pass these flags. They have no effect. + p.add_argument("--webhook", default=None, help=argparse.SUPPRESS) + p.add_argument("--checkpoint-on-failure", default=None, help=argparse.SUPPRESS) + # superseded by --systemd-run; a JDL still passing it must not abort here + p.add_argument("--cgroup", default=None, help=argparse.SUPPRESS) + + # Logging + p.add_argument("--action-logfile", default=None) + p.add_argument("--metric-logfile", default=None) + p.add_argument("--production-mode", action="store_true") + + return p + + +def _args_to_config(ns: argparse.Namespace) -> RunnerConfig: + target_tasks = [f.strip('"').strip("'") for f in ns.target_tasks] + # Extract slice name from spec so the executor can name child scopes. + slice_name: Optional[str] = None + if ns.systemd_run_spec: + try: + _, _, slice_name = _parse_systemd_run_spec(ns.systemd_run_spec) + except ValueError: + pass # error already caught at re-exec time + return RunnerConfig( + workflowfile=ns.workflowfile, + maxjobs=ns.maxjobs, + mem_limit=ns.mem_limit, + cpu_limit=ns.cpu_limit, + n_backfill=ns.n_backfill, + update_resources=ns.update_resources, + dynamic_resources=ns.dynamic_resources, + optimistic_resources=ns.optimistic_resources, + in_systemd_slice=bool(os.environ.get(_IN_SLICE_ENV)), + systemd_run_spec=ns.systemd_run_spec, + systemd_slice_name=slice_name, + scheduler_policy=ns.scheduler_policy, + drop_should_break=ns.drop_should_break, + monitor_interval_cpu=ns.monitor_interval_cpu, + monitor_interval_mem=ns.monitor_interval_mem, + monitor_backend=ns.monitor_backend, + cache_policy=ns.cache_policy, + target_tasks=target_tasks, + target_labels=list(ns.target_labels), + keep_going=ns.keep_going, + dry_run=ns.dry_run, + visualize_workflow=ns.visualize_workflow, + produce_script=ns.produce_script, + rerun_from=ns.rerun_from, + list_tasks=ns.list_tasks, + retry_on_failure=ns.retry_on_failure, + no_rootinit_speedup=ns.no_rootinit_speedup, + remove_files_early=ns.remove_files_early, + stdout_on_failure=ns.stdout_on_failure, + production_mode=ns.production_mode, + action_logfile=ns.action_logfile, + metric_logfile=ns.metric_logfile, + ) + + +def _parse_systemd_run_spec(spec: str) -> Tuple[Optional[str], Optional[str], str]: + """Parse "ncpus:N/mem:M/name:S" into (cpu_quota_str, mem_str, slice_name). + + ncpus is given as a number of cores and converted to systemd CPUQuota + format (e.g. 8 cores → "800%"). mem is passed through as-is. + name sets the systemd slice name (default "o2dpg"). Any part may be absent. + """ + cpu_quota: Optional[str] = None + mem: Optional[str] = None + slice_name: str = "o2dpg" + for part in spec.split("/"): + part = part.strip() + if not part: + continue + if ":" not in part: + raise ValueError(f"Expected key:value in --systemd-run spec, got: {part!r}") + key, _, val = part.partition(":") + key = key.strip().lower() + val = val.strip() + if key == "ncpus": + try: + cores = float(val) + except ValueError: + raise ValueError(f"ncpus must be a number, got: {val!r}") + cpu_quota = f"{int(cores * 100)}%" + elif key == "mem": + mem = val + elif key == "name": + slice_name = val + else: + raise ValueError(f"Unknown key in --systemd-run spec: {key!r}. " + f"Supported: ncpus, mem, name") + return cpu_quota, mem, slice_name + + +def _parse_mem_to_bytes(mem_str: str) -> Optional[int]: + """Convert a memory size string (e.g. "16G", "512M") to bytes.""" + suffixes = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4} + s = mem_str.strip() + if s and s[-1].upper() in suffixes: + try: + return int(float(s[:-1]) * suffixes[s[-1].upper()]) + except ValueError: + pass + try: + return int(s) + except ValueError: + return None + + +def _apply_slice_cgroup_limits( + cpu_quota: Optional[str], + mem_str: Optional[str], + logger: logging.Logger, +) -> None: + """Write resource limits to the parent slice's cgroup directory. + + Called after re-exec inside the slice. The runner's own scope lives at + .../kaz1.slice/o2dpg-runner-.scope/; one dirname() up is the slice + that covers both the runner and all sibling per-task scopes. + + This is the portable fallback for systemd < 246 which lacks + --slice-property. Writing directly to cpu.max / memory.max works on + any cgroup v2 system where the user owns the cgroup. + """ + cgroup_rel: Optional[str] = None + try: + with open("/proc/self/cgroup") as fh: + for line in fh: + parts = line.strip().split(":", 2) + if len(parts) == 3 and parts[0] == "0": + cgroup_rel = parts[2].lstrip("/") + break + except OSError: + pass + + if not cgroup_rel: + logger.warning("Cannot determine cgroup path; slice limits not applied.") + return + + scope_dir = f"/sys/fs/cgroup/{cgroup_rel}" + slice_dir = os.path.dirname(scope_dir) + + if cpu_quota: + pct = float(cpu_quota.rstrip("%")) + # cgroup cpu.max format: " " + # 100% = 1 core = 100000 usec per 100000 usec period + quota_usec = int(pct * 1000) + cpu_max = os.path.join(slice_dir, "cpu.max") + try: + with open(cpu_max, "w") as fh: + fh.write(f"{quota_usec} 100000\n") + logger.info("Slice CPUQuota=%s applied → %s", cpu_quota, cpu_max) + except OSError as e: + logger.warning("Could not set CPUQuota on slice (%s): %s", cpu_max, e) + + if mem_str: + mem_bytes = _parse_mem_to_bytes(mem_str) + if mem_bytes is not None: + mem_max = os.path.join(slice_dir, "memory.max") + try: + with open(mem_max, "w") as fh: + fh.write(f"{mem_bytes}\n") + logger.info("Slice MemoryMax=%s (%d bytes) applied → %s", + mem_str, mem_bytes, mem_max) + except OSError as e: + logger.warning("Could not set MemoryMax on slice (%s): %s", mem_max, e) + + +def _maybe_reexec_in_slice(ns: argparse.Namespace) -> None: + """If --systemd-run is set and we are not already in the slice, re-exec. + + Uses os.execvp so the current process image is replaced by systemd-run, + which creates a transient scope cgroup and then exec's the runner again. + The child runner sees O2DPG_RUNNER_IN_SLICE=1 and skips this function. + """ + spec = getattr(ns, "systemd_run_spec", None) + if not spec: + return + if os.environ.get(_IN_SLICE_ENV): + return # already inside the slice + + if not shutil.which("systemd-run"): + print( + "Warning: --systemd-run requested but systemd-run not found on PATH; " + "continuing without slice confinement.", + file=sys.stderr, + ) + return + + try: + _, _, slice_name = _parse_systemd_run_spec(spec) + except ValueError as e: + print(f"Error in --systemd-run spec: {e}", file=sys.stderr) + sys.exit(1) + + # Ensure the slice name has the .slice suffix expected by systemd. + systemd_slice = slice_name if slice_name.endswith(".slice") else f"{slice_name}.slice" + unit_name = f"o2dpg-runner-{os.getpid()}.scope" + cmd = ["systemd-run", "--user", "--scope", "--collect", + f"--unit={unit_name}", f"--slice={systemd_slice}"] + # Resource limits are NOT passed here; they are written directly to the + # slice cgroup after re-exec via _apply_slice_cgroup_limits(). + # --property=CPUQuota applies only to the scope (runner), not to the + # sibling task scopes. --slice-property would be correct but requires + # systemd ≥ 246. Direct cgroup writes work on all versions. + cmd += ["--", sys.executable] + sys.argv + + os.environ[_IN_SLICE_ENV] = "1" + try: + os.execvp(cmd[0], cmd) + except OSError as e: + # execvp only returns on failure + del os.environ[_IN_SLICE_ENV] + print( + f"Warning: could not exec systemd-run ({e}); " + "continuing without slice confinement.", + file=sys.stderr, + ) + + +def _maybe_draw_workflow(raw_spec): + try: + from graphviz import Digraph + except ImportError: + print("graphviz not installed; cannot draw workflow") + return + dot = Digraph(comment="MC workflow") + name_to_idx = {} + for i, node in enumerate(raw_spec["stages"]): + name_to_idx[node["name"]] = i + dot.node(str(i), node["name"]) + for node in raw_spec["stages"]: + to_i = name_to_idx[node["name"]] + for r in node.get("needs", []): + if r in name_to_idx: + dot.edge(str(name_to_idx[r]), str(to_i)) + dot.render("workflow.gv") + + +def _launch_fileaccess_sidecar(actionlogger_file: str): + """Start the fanotify-based file-IO graph sidecar if requested.""" + exe = os.getenv("O2DPG_PRODUCE_FILEGRAPH") + if not exe: + return None, None, None + env = os.environ.copy() + env["FILEACCESS_MON_ROOTPATH"] = os.getcwd() + env["MAXMOTHERPID"] = f"{os.getpid()}" + log_file = f"pipeline_fileaccess_{os.getpid()}.log" + fh = open(log_file, "w") + proc = subprocess.Popen( + [exe], stdout=fh, stderr=subprocess.STDOUT, env=env, + ) + return proc, fh, log_file + + +def main(argv=None) -> int: + ns = build_parser().parse_args(argv) + _maybe_reexec_in_slice(ns) # may replace this process; returns only if not re-execing + cfg = _args_to_config(ns) + + # loggers + action_log = cfg.action_logfile or f"pipeline_action_{os.getpid()}.log" + metric_log = cfg.metric_logfile or f"pipeline_metric_{os.getpid()}.log" + action_logger = _setup_logger("pipeline_action_logger", action_log, level=logging.DEBUG) + metric_logger = _setup_logger("pipeline_metric_logger", metric_log) + + for flag in ("webhook", "checkpoint_on_failure", "cgroup"): + if getattr(ns, flag, None): + action_logger.warning("--%s is accepted but has no effect", + flag.replace("_", "-")) + + # also route the package-level log records to the action log + pkg_log = logging.getLogger("o2dpg_runner") + pkg_log.setLevel(logging.INFO) + for h in list(pkg_log.handlers): + pkg_log.removeHandler(h) + pkg_log.propagate = False + for h in action_logger.handlers: + pkg_log.addHandler(h) + + # Apply slice-level cgroup resource limits now that we are inside the + # slice and the action logger is ready to record the outcome. + if cfg.in_systemd_slice and cfg.systemd_run_spec: + try: + cpu_quota, mem_str, _ = _parse_systemd_run_spec(cfg.systemd_run_spec) + _apply_slice_cgroup_limits(cpu_quota, mem_str, action_logger) + except Exception as e: + action_logger.warning("Could not apply slice cgroup limits: %s", e) + + # record meta to the metric log (mirrors prototype) + raw = load_json(cfg.workflowfile) + meta = raw.get("meta", {}) if isinstance(raw, dict) else {} + if not isinstance(meta, dict): + meta = {} + meta.update({ + "cpu_limit": cfg.cpu_limit, + "mem_limit": cfg.mem_limit, + "workflow_file": os.path.abspath(cfg.workflowfile), + "target_task": cfg.target_tasks, + "rerun_from": cfg.rerun_from, + "target_labels": cfg.target_labels, + "scheduler_policy": cfg.scheduler_policy, + "drop_should_break": cfg.drop_should_break, + "cache_policy": cfg.cache_policy, + "systemd_run_spec": cfg.systemd_run_spec, + "in_systemd_slice": cfg.in_systemd_slice, + "monitor_interval_cpu": cfg.monitor_interval_cpu, + }) + metric_logger.info(meta) + + # visualize if asked (uses raw spec before filtering) + if cfg.visualize_workflow: + _maybe_draw_workflow(raw) + + # build workflow (filters, strips global init, builds DAG) + wf = build_workflow(raw, cfg.target_tasks, cfg.target_labels) + if not wf.stages: + if cfg.target_tasks: + print("Apparently some of the chosen target tasks are not in the workflow") + else: + print("Workflow is empty. Nothing to do") + return 0 + + # Apply global env (as the prototype did at construction time) + for k, v in wf.global_env.items(): + os.environ.setdefault(k, str(v)) + + # Optional file-access sidecar + fileaccess_proc, fileaccess_fh, fileaccess_log_file = _launch_fileaccess_sidecar(action_log) + + rc = 0 + try: + execer = WorkflowExecutor(cfg, wf, action_logger, metric_logger) + rc = int(execer.execute()) + finally: + if fileaccess_proc is not None: + fileaccess_proc.terminate() + try: + fileaccess_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + fileaccess_proc.kill() + if fileaccess_fh is not None: + fileaccess_fh.close() + o2dpg_root = os.getenv("O2DPG_ROOT") + if o2dpg_root and fileaccess_log_file: + analyse_cmd = [ + sys.executable, + f"{o2dpg_root}/UTILS/FileIOGraph/analyse_FileIO_v2.py", + "--actionFile", action_log, + "--monitorFile", fileaccess_log_file, + "-o", f"pipeline_fileaccess_report_{os.getpid()}.json", + "--basedir", os.getcwd(), + ] + print(f"Producing FileIOGraph with command {analyse_cmd}") + try: + subprocess.run(analyse_cmd, check=True) + except subprocess.CalledProcessError as e: + print(f"FileIOGraph analysis failed: {e}", file=sys.stderr) + + return rc + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/MC/workflow_runner/o2dpg_runner/config.py b/MC/workflow_runner/o2dpg_runner/config.py new file mode 100644 index 000000000..d12bf73c8 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/config.py @@ -0,0 +1,62 @@ +"""Runtime configuration for the runner. + +A dataclass that holds everything the old module-level ``args`` exposed, +so nothing in the code needs to import argparse. Constructed from the +argparse Namespace in cli.py. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional + + +@dataclass +class RunnerConfig: + # --- required --- + workflowfile: str + + # --- scheduling / resources --- + maxjobs: int = 100 + mem_limit: float = 0.0 # MB; 0 means "auto from psutil" + cpu_limit: float = 8.0 + n_backfill: int = 1 + update_resources: Optional[str] = None + dynamic_resources: bool = False + optimistic_resources: bool = False + in_systemd_slice: bool = False # True when runner was re-exec'd under systemd-run --scope + + # --- new scheduler knobs --- + scheduler_policy: str = "timeframe" # timeframe | critical-path | best-fit + drop_should_break: bool = False # let timeframe policy scan past non-fitting + + # --- systemd-run slice --- + systemd_run_spec: Optional[str] = None # raw "ncpus:N/mem:M/name:S" spec, kept for metric meta + systemd_slice_name: Optional[str] = None # parsed "name:" value; used for child scope names + + # --- new monitor knobs --- + monitor_interval_cpu: float = 1.0 + monitor_interval_mem: float = 1.0 # match prototype cadence; raise for cheaper monitor + monitor_backend: str = "psutil" # psutil | auto (auto reserved for future cgroup backend) + + # --- cache policy (v1 _done.json) --- + cache_policy: str = "off" # off | lenient | strict + + # --- selection / control --- + target_tasks: List[str] = field(default_factory=lambda: ["*"]) + target_labels: List[str] = field(default_factory=list) + keep_going: bool = False + dry_run: bool = False + visualize_workflow: bool = False + produce_script: Optional[str] = None + rerun_from: Optional[str] = None + list_tasks: bool = False + retry_on_failure: int = 0 + no_rootinit_speedup: bool = False + remove_files_early: str = "" + stdout_on_failure: bool = False + production_mode: bool = False + + # --- logging --- + action_logfile: Optional[str] = None + metric_logfile: Optional[str] = None diff --git a/MC/workflow_runner/o2dpg_runner/executor.py b/MC/workflow_runner/o2dpg_runner/executor.py new file mode 100644 index 000000000..10dc73b45 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/executor.py @@ -0,0 +1,763 @@ +"""Main control loop. + +Responsibilities: + - boot sequence (global init, ROOT speedup, FAIRMQ socket setup) + - candidate management + - submit / wait / monitor-feedback interplay + - retry, failure handling, rerun-from + - end-of-task hooks (file removal, production archival) + +Split from the prototype's monolithic WorkflowExecutor and freed from +module-level args. +""" + +from __future__ import annotations + +import json +import logging +import os +import platform +import re +import signal +import subprocess +import sys +import threading +import time +import traceback +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Set, Tuple + +import psutil + +from .config import RunnerConfig +from .workflow import Workflow, update_resource_estimates +from .graph import descendants, longest_path_length, kahn_topological_order +from .resources import ResourceManager, ResourceLimitExceeded +from .monitoring import MonitorThread, PsutilBackend, _read_cgroup_v2_dir +from .scheduler import get_policy +from .scheduler.base import SchedulerState +from .scheduler.timeframe import TimeframeFirstPolicy +from .cache import TaskCache, compute_fingerprint, remove_done_flag, done_path +from .alienv import get_alienv_software_environment +from .cleanup import EarlyFileRemover, archive_task_logs + +log = logging.getLogger(__name__) + +_UNIT_NAME_RE = re.compile(r"[^a-zA-Z0-9_\-.]") + + +def _unit_name(task_name: str, tid: int) -> str: + """Build a valid systemd unit name for a per-task scope.""" + safe = _UNIT_NAME_RE.sub("-", task_name) + return f"task-{safe}-{tid}.scope" + + +def _start_stderr_drainer(pipe, logger: logging.Logger, tag: str) -> threading.Thread: + """Drain *pipe* line-by-line in a daemon thread, forwarding to *logger*. + + Used to capture systemd-run's own informational messages (e.g. "Running + as unit: ...") and route them to the action log instead of the terminal. + The thread exits naturally when the pipe reaches EOF (process finished). + """ + def _run() -> None: + try: + for raw in pipe: + line = raw.rstrip() if isinstance(raw, str) else raw.rstrip().decode(errors="replace") + if line: + logger.info("[systemd] %s: %s", tag, line) + except Exception: + pass + t = threading.Thread(target=_run, daemon=True, name=f"stderr-{tag}") + t.start() + return t + + +@dataclass +class _TaskRuntime: + """Everything we learn about a task at runtime and need to feed back.""" + logfile: str + fingerprint: Dict[str, str] = field(default_factory=dict) + start_time: float = 0.0 + pid: int = 0 + + +class WorkflowExecutor: + """Construct from a config + workflow; call execute().""" + + def __init__( + self, + config: RunnerConfig, + workflow: Workflow, + action_logger: logging.Logger, + metric_logger: logging.Logger, + ): + self.cfg = config + self.wf = workflow + self.actionlog = action_logger + self.metriclog = metric_logger + + # apply update-resources (before building resource manager) + if config.update_resources: + update_resource_estimates( + workflow, config.update_resources, + logger=action_logger, + ) + + # resource manager + self.rm = ResourceManager( + cpu_limit=config.cpu_limit, + mem_limit=config.mem_limit, + procs_parallel_max=config.maxjobs, + n_backfill_max=config.n_backfill, + dynamic_resources=config.dynamic_resources, + optimistic_resources=config.optimistic_resources, + ) + for task in workflow.stages: + try: + rel = float(task["resources"].get("relative_cpu") or 1) + except (TypeError, ValueError): + rel = 1.0 + try: + self.rm.add_task( + name=task["name"], + related_name=self._global_name(task["name"]), + cpu=float(task["resources"]["cpu"]), + cpu_relative=rel, + mem=float(task["resources"]["mem"]), + semaphore_string=task.get("semaphore"), + ) + except ResourceLimitExceeded as e: + print(e, file=sys.stderr) + print("Pass --optimistic-resources to the runner to attempt the run anyway.", + file=sys.stderr) + raise + + # scheduler + if config.scheduler_policy == "timeframe": + self.policy = TimeframeFirstPolicy(drop_should_break=config.drop_should_break) + else: + self.policy = get_policy(config.scheduler_policy) + + # scheduler state: precompute weights once + self.state = self._build_scheduler_state() + + # task cache (covers _done + optional _done.json) + self.cache = TaskCache(policy=config.cache_policy) + # precompute per-task fingerprint so we don't recompute on each check + self._fingerprint_by_tid: Dict[int, Dict[str, str]] = {} + for tid, task in enumerate(workflow.stages): + alienv = task.get("alternative_alienv_package") or "" + self._fingerprint_by_tid[tid] = compute_fingerprint(task, alienv) + + # alternative alienv envs + self.alternative_envs: Dict[int, Dict[str, str]] = {} + self._init_alternative_envs() + + # Compute the global cgroup directory for the aggregate monitor. + # Cgroup monitoring is only meaningful when the runner was launched + # under --systemd-run: the runner's own scope is a leaf node + # (e.g. o2dpg.slice/o2dpg-runner-.scope/) and task scopes are + # siblings. The *parent* slice directory's cpu.stat / memory.current + # then cover the runner + all task scopes. Without --systemd-run the + # runner shares a generic user-session cgroup with unrelated + # processes, so cgroup readings would be misleading; fall back to + # plain psutil monitoring instead. + _global_cgroup: Optional[str] = None + if config.in_systemd_slice: + _runner_cgroup = _read_cgroup_v2_dir(os.getpid()) + if _runner_cgroup: + _global_cgroup = os.path.dirname(_runner_cgroup) + if not os.path.isdir(_global_cgroup): + _global_cgroup = _runner_cgroup # safety fallback + + # monitor + self.monitor = MonitorThread( + cpu_interval=config.monitor_interval_cpu, + mem_interval=config.monitor_interval_mem, + backend=PsutilBackend(), + monitor_disc=bool(os.getenv("MONITOR_DISC_USAGE")), + disc_path=os.getcwd(), + global_cgroup_dir=_global_cgroup, + ) + + # process tracking + self.proc_status: Dict[int, str] = {tid: "ToDo" for tid in range(workflow.n_tasks())} + self.task_runtime: Dict[int, _TaskRuntime] = {} + self.process_list: List[Tuple[int, psutil.Popen]] = [] + self.tids_marked_retry: List[int] = [] + self.retry_counter: List[int] = [0] * workflow.n_tasks() + self.task_retries: List[int] = [ + int(t.get("retry_count", 0)) for t in workflow.stages + ] + + # early file removal + self.file_remover: Optional[EarlyFileRemover] = None + if config.remove_files_early: + try: + self.file_remover = EarlyFileRemover( + config.remove_files_early, + workflow.timeframes, + workflow.full_target_names, + logger=action_logger, + ) + except Exception as e: + log.warning("Could not set up early file removal: %s", e) + + self.start_time: float = 0.0 + self.scheduling_iteration = 0 + self._last_metric_tick: int = -1 # prevents duplicate metric rows per tick + + # signals + signal.signal(signal.SIGINT, self._sighandler) + signal.signal(signal.SIGTERM, self._sighandler) + signal.siginterrupt(signal.SIGINT, False) + signal.siginterrupt(signal.SIGTERM, False) + + # ----- small helpers ----- + @staticmethod + def _global_name(name: str) -> str: + """Strip _ suffix to find sibling group for resource sampling.""" + toks = name.split("_") + if toks and toks[-1].isdigit() and len(toks) > 1: + return "_".join(toks[:-1]) + return name + + def _build_scheduler_state(self) -> SchedulerState: + n = self.wf.n_tasks() + # descendants via memoized iterative DFS (no recursion limit concerns) + desc_cache: Dict[int, Set[int]] = {} + desc_counts = [0] * n + for tid in range(n): + desc_counts[tid] = len(descendants(self.wf.forward_adj, tid, desc_cache)) + + timeframe_of = [t.get("timeframe", -1) for t in self.wf.stages] + tf_weight = [(timeframe_of[t], desc_counts[t]) for t in range(n)] + + cpu = [float(t.get("resources", {}).get("cpu", 1.0)) for t in self.wf.stages] + mem = [float(t.get("resources", {}).get("mem", 0.0)) for t in self.wf.stages] + + # Per-task walltime [s] from learned resources (resources.walltime set + # by update_resource_estimates when --update-resources is given). + # Fall back to cpu as a proxy so behaviour is unchanged without + # learned data. + walltime = [ + float(t.get("resources", {}).get("walltime") or cpu[i]) + for i, t in enumerate(self.wf.stages) + ] + has_walltime = any( + t.get("resources", {}).get("walltime") for t in self.wf.stages + ) + + # Critical path: longest remaining *wall time* to any leaf. + # Using walltime as the node weight gives a true makespan estimate; + # using cpu (the fallback) preserves the original heuristic. + cp_weight = walltime if has_walltime else cpu + topo = kahn_topological_order(n, self.wf.forward_adj, self.wf.indegree) + cp = longest_path_length(self.wf.forward_adj, topo, cp_weight) + + if has_walltime: + self.actionlog.info("Critical path weighted by learned walltime [s]") + + # self-log weights (matches prototype's informational logging) + for tid in range(n): + self.actionlog.info("Score for %s is %s", self.wf.id_to_name[tid], tf_weight[tid]) + + return SchedulerState( + timeframe_of=timeframe_of, + descendants_count=desc_counts, + critical_path=cp, + task_cpu=cpu, + task_mem=mem, + task_walltime=walltime, + timeframe_weight=tf_weight, + ) + + def _init_alternative_envs(self) -> None: + cache: Dict[str, Dict[str, str]] = {} + for tid, task in enumerate(self.wf.stages): + pkg = task.get("alternative_alienv_package") + if not pkg: + continue + if pkg not in cache: + cache[pkg] = get_alienv_software_environment(pkg) + self.alternative_envs[tid] = cache[pkg] + + # ----- task-level helpers ----- + def logfile(self, tid: int) -> str: + task = self.wf.stages[tid] + return os.path.join(task.get("cwd", "."), f"{task['name']}.log") + + def apply_global_env(self, env: Dict[str, str]) -> None: + for k, v in self.wf.global_env.items(): + env.setdefault(k, str(v)) + + # ----- signal handling ----- + def _sighandler(self, signum, frame): + self.actionlog.info("Signal %s caught; terminating children", signum) + try: + self.monitor.stop() + except Exception: + pass + try: + procs = psutil.Process().children(recursive=True) + except psutil.NoSuchProcess: + procs = [] + except (psutil.AccessDenied, PermissionError): + procs = [] + + for p in procs: + try: + p.terminate() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + _, alive = psutil.wait_procs(procs, timeout=3) + for p in alive: + try: + p.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + sys.exit(1) + + # ----- task submission ----- + def submit(self, tid: int, nice: int) -> Optional[psutil.Popen]: + task = self.wf.stages[tid] + self.actionlog.debug("Submitting %s with nice=%d", task["name"], nice) + cmd = task["cmd"] + workdir = task.get("cwd", ".") + if workdir: + if os.path.exists(workdir) and not os.path.isdir(workdir): + self.actionlog.error("cwd %s exists and is not a directory", workdir) + return None + if not os.path.isdir(workdir): + os.makedirs(workdir, exist_ok=True) + + self.proc_status[tid] = "Running" + + if self.cfg.dry_run: + dry = f"echo ' {self.scheduling_iteration} : would do {task['name']}'" + # psutil.Popen so that the dry-run path also answers .nice() + return psutil.Popen(["/bin/bash", "-c", dry], cwd=workdir) + + env = os.environ.copy() + alt = self.alternative_envs.get(tid) + if alt: + self.actionlog.info("Applying alternative environment to %s", task["name"]) + if alt.get("TERM") is not None: + env = dict(alt) + else: + env.update(alt) + if task.get("env"): + env.update({k: str(v) for k, v in task["env"].items()}) + self.apply_global_env(env) + + if os.environ.get("PIPELINE_RUNNER_DUMP_TASKENVS") is not None: + try: + with open(f"taskenv_{tid}.log", "w") as f: + json.dump(env, f, indent=2) + except OSError as e: + log.warning("could not dump taskenv: %s", e) + + # When the runner is inside a systemd slice, wrap each task in its own + # child scope so per-task cgroup metrics are available alongside psutil. + slice_name = self.cfg.systemd_slice_name + use_scope = self.cfg.in_systemd_slice and bool(slice_name) + if use_scope: + systemd_slice = ( + slice_name if slice_name.endswith(".slice") else f"{slice_name}.slice" + ) + unit = _unit_name(task["name"], tid) + launch_argv = [ + "systemd-run", "--user", "--scope", "--collect", + "--expand-environment=no", # suppress the $VAR warning; bash handles expansion + f"--unit={unit}", f"--slice={systemd_slice}", + "--", "/bin/bash", "-c", cmd, + ] + p = psutil.Popen(launch_argv, cwd=workdir, env=env, stderr=subprocess.PIPE) + _start_stderr_drainer(p.stderr, self.actionlog, task["name"]) + else: + launch_argv = ["/bin/bash", "-c", cmd] + p = psutil.Popen(launch_argv, cwd=workdir, env=env) + try: + p.nice(nice) + except (psutil.NoSuchProcess, psutil.AccessDenied): + self.actionlog.error("Could not renice %d to %d", p.pid, nice) + + rt = _TaskRuntime( + logfile=self.logfile(tid), + fingerprint=self._fingerprint_by_tid[tid], + start_time=time.perf_counter(), + pid=p.pid, + ) + self.task_runtime[tid] = rt + self.monitor.register( + tid, p.pid, task["name"], task.get("labels", []) or [], rt.start_time, + resolve_cgroup=use_scope, + ) + return p + + # ----- skip logic ----- + def ok_to_skip(self, tid: int) -> bool: + return self.cache.is_done(self.logfile(tid), self._fingerprint_by_tid[tid]) + + # ----- candidate scheduling pass ----- + def try_submit_from_candidates( + self, + candidates: List[int], + finished_out: List[int], + ) -> None: + self.scheduling_iteration += 1 + + # skip already-done tasks first (in place) + remaining = [] + for tid in candidates: + if self.ok_to_skip(tid): + finished_out.append(tid) + self.actionlog.info("Skipping %s", self.wf.id_to_name[tid]) + if self.file_remover is not None: + self.file_remover.on_task_done(self.wf.id_to_name[tid]) + else: + remaining.append(tid) + # mutate the list the caller passed in + candidates[:] = remaining + + ordered = self.policy.order(candidates, self.state) + for tid, nice in self.policy.pick_submittable(ordered, self.rm): + self.actionlog.debug("Submitting tid=%d %s (nice=%d)", + tid, self.wf.id_to_name[tid], nice) + p = self.submit(tid, nice) + if p is None: + continue + # pin the nice value the OS actually granted + try: + actual_nice = p.nice() + except (psutil.NoSuchProcess, psutil.AccessDenied): + actual_nice = nice + self.rm.book(tid, actual_nice) + self.process_list.append((tid, p)) + if tid in candidates: + candidates.remove(tid) + + # ----- wait / complete / retry ----- + def wait_for_any( + self, + finished_out: List[int], + failing_out: List[int], + ) -> bool: + """Return True if we should keep waiting (no completion this pass). + + Polls each process via poll(); pulls latest monitor snapshot and + feeds it into the ResourceManager for the dynamic-resources path. + """ + if not self.process_list: + return False + + # Take each monitor tick exactly once. This loop polls faster than the + # monitor fires, and a snapshot recorded twice both inflates the sample + # lists and defeats the "too few samples" guard in sample_resources(). + snapshots = self.monitor.latest() + tick = self.monitor.tick + + if tick != self._last_metric_tick: + self._last_metric_tick = tick + for tid, snap in snapshots.items(): + self.rm.add_monitored(tid, snap.t_delta_ms, + snap.cpu_pct / 100.0, snap.pss_mb) + self.metriclog.info({ + "iter": tick, "name": snap.name, + "cpu": snap.cpu_pct, "uss": snap.uss_mb, "pss": snap.pss_mb, + "nice": snap.nice, "swap": snap.swap_mb, + "label": snap.labels, "disc": snap.disc_mb, + # cgroup-based readings for comparison with psutil (None when + # no per-task scope is active) + "cgroup_cpu": snap.cgroup_cpu_pct, "cgroup_mem": snap.cgroup_mem_mb, + }) + + # cgroup-aggregate slice totals + g_cpu = self.monitor.global_cpu_pct + g_mem = self.monitor.global_mem_mb + if g_cpu is not None or g_mem is not None: + self.metriclog.info({ + "iter": tick, "name": "__cgroup_global__", + "cpu": g_cpu, "uss": None, "pss": g_mem, + "nice": 0, "swap": None, "label": [], "disc": -1, + }) + + # check for completions + newly_done: List[Tuple[int, psutil.Popen, int]] = [] + for tid, p in list(self.process_list): + rc = 0 if self.cfg.dry_run else p.poll() + if rc is None: + continue + newly_done.append((tid, p, rc)) + + failure_detected = False + for tid, p, rc in newly_done: + name = self.wf.id_to_name[tid] + self.actionlog.info("Task pid=%d tid=%d %s finished rc=%d", + p.pid, tid, name, rc) + self.rm.unbook(tid) + self.proc_status[tid] = "Done" + self.monitor.deregister(tid) + self.process_list.remove((tid, p)) + + if rc == 0: + finished_out.append(tid) + # record fingerprint sidecar (best effort) + rt = self.task_runtime.get(tid) + if rt is not None: + self.cache.record(rt.logfile, rt.fingerprint) + if self.file_remover is not None: + self.file_remover.on_task_done(name) + if self.cfg.production_mode: + archive_task_logs(self.logfile(tid), logger=self.actionlog) + else: + print(f"{name} failed ... checking retry") + max_retries = max(self.cfg.retry_on_failure, self.task_retries[tid]) + if self._is_worth_retrying(tid) and self.retry_counter[tid] < max_retries: + self.actionlog.info("Task %s marked for retry", name) + self.tids_marked_retry.append(tid) + self.retry_counter[tid] += 1 + else: + failure_detected = True + failing_out.append(tid) + + if failure_detected and not self.cfg.keep_going: + self.actionlog.info("Stopping due to failure in tids %s", failing_out) + if self.cfg.stdout_on_failure: + self._cat_logfiles(failing_out) + self.stop_and_exit() + + return not finished_out + + def _is_worth_retrying(self, tid: int) -> bool: + """Hook for future log-inspection; currently always True (match prototype).""" + return True + + def _cat_logfiles(self, tids: List[int]) -> None: + for tid in tids: + logf = self.logfile(tid) + if os.path.exists(logf): + print(f" ----> START OF LOGFILE {logf} -----") + try: + with open(logf) as f: + sys.stdout.write(f.read()) + except OSError: + pass + print(f" <---- END OF LOGFILE {logf} -----") + + def stop_and_exit(self) -> None: + for _, p in self.process_list: + try: + p.kill() + except Exception: + pass + self.monitor.stop() + sys.exit(1) + + # ----- boot helpers ----- + def _speedup_root_init(self) -> None: + if platform.system() != "Linux": + return + if os.environ.get("ROOT_LDSYSPATH") and os.environ.get("ROOT_CPPSYSINCL"): + return + if self.cfg.no_rootinit_speedup: + return + try: + cmd = ('LD_DEBUG=libs LD_PRELOAD=DOESNOTEXIST ls /tmp/DOESNOTEXIST 2>&1 | ' + 'grep -m 1 "system search path" | sed \'s/.*=//g\' | ' + 'awk \'//{print $1}\'') + libpath = subprocess.check_output(cmd, shell=True).decode().strip() + if libpath: + os.environ["ROOT_LDSYSPATH"] = libpath + os.environ["CLING_LDSYSPATH"] = libpath + cmd2 = ("LC_ALL=C c++ -xc++ -E -v /dev/null 2>&1 | " + "sed -n '/^#include/,${/^ \\/.*++/{p}}'") + incpath = subprocess.check_output(cmd2, shell=True).decode() + joined = ":".join(line.lstrip() for line in incpath.splitlines()) + if joined: + os.environ["ROOT_CPPSYSINCL"] = joined + os.environ["CLING_CPPSYSINCL"] = joined + except Exception as e: + log.warning("ROOT init speedup failed: %s", e) + + def _execute_global_init_cmd(self) -> bool: + cmd = self.wf.global_init_cmd + if not cmd: + return True + self.actionlog.info("Executing global init cmd: %s", cmd) + p = subprocess.Popen(["/bin/bash", "-c", cmd], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if p.returncode == 0: + self.actionlog.info(stdout.decode()) + return True + self.actionlog.error("global init failed: %s", stderr.decode()) + return False + + def _handle_rerun_from(self) -> None: + if not self.cfg.rerun_from: + return + import re + matched = False + for task in self.wf.stages: + if re.match(self.cfg.rerun_from, task["name"]): + matched = True + tid = self.wf.tid(task["name"]) + # remove done flags for tid and all its descendants (iterative) + for d in descendants(self.wf.forward_adj, tid) | {tid}: + name = self.wf.id_to_name[d] + self.actionlog.info("Marking %s for rerun", name) + if not self.cfg.dry_run: + remove_done_flag(self.logfile(d)) + else: + print(f"Would mark {name} as to be done again") + if not matched: + print(f"No task matching {self.cfg.rerun_from} found; refusing to proceed") + sys.exit(1) + + # ----- bash-script emission ----- + def produce_script(self, filename: str) -> None: + topo = kahn_topological_order(self.wf.n_tasks(), + self.wf.forward_adj, + self.wf.indegree) + lines = [ + "#!/usr/bin/env bash\n", + "#THIS FILE IS AUTOGENERATED\n", + "export JOBUTILS_SKIPDONE=ON\n", + "#-- GLOBAL INIT SECTION FROM WORKFLOW --\n", + ] + for k, v in self.wf.global_env.items(): + lines.append(f"export {k}={v}\n") + lines.append("#-- TASKS FROM WORKFLOW --\n") + for tid in topo: + t = self.wf.stages[tid] + workdir = t.get("cwd", ".") + env_pairs = t.get("env") or {} + env_prefix = " ".join(f"{k}={v}" for k, v in env_pairs.items()) + # Subshell so inner `cd` doesn't leak, and local env doesn't pollute. + inner = t["cmd"] + if env_prefix: + inner = f"{env_prefix} {inner}" + lines.append(f"( [ -d {workdir} ] || mkdir -p {workdir}; cd {workdir} && {inner} )\n") + with open(filename, "w") as f: + f.writelines(lines) + + # ----- main loop ----- + def execute(self) -> bool: + self.start_time = time.perf_counter() + psutil.cpu_percent(interval=None) + os.environ["JOBUTILS_SKIPDONE"] = "ON" + self._speedup_root_init() + + if not os.path.isdir("./.tmp"): + os.mkdir("./.tmp") + if os.environ.get("FAIRMQ_IPC_PREFIX") is None: + sp = os.path.join(os.getcwd(), ".tmp") + self.actionlog.info("Setting FAIRMQ_IPC_PREFIX=%s", sp) + os.environ["FAIRMQ_IPC_PREFIX"] = sp + + if self.cfg.list_tasks: + print("List of tasks in this workflow:") + for i, t in enumerate(self.wf.stages): + label_part = t.get("labels", []) + print(f"{t['name']} ({label_part}) ToDo: {not self.ok_to_skip(i)}") + return False + + if self.cfg.produce_script is not None: + self.produce_script(self.cfg.produce_script) + return False + + if not self._execute_global_init_cmd(): + sys.exit(1) + + self._handle_rerun_from() + + # start monitor + self.monitor.start() + + # initial candidates: tasks with no predecessors + candidates = [i for i, d in enumerate(self.wf.indegree) if d == 0] + finishedtasks_set: Set[int] = set() + error_encountered = False + + try: + while True: + finished: List[int] = [] + self.actionlog.debug("candidates: %s", + [(c, self.wf.id_to_name[c]) for c in candidates]) + self.try_submit_from_candidates(candidates, finished) + + if candidates and not self.process_list: + self._noprogress_error() + error_encountered = True + break + + # wait loop + finished_running: List[int] = [] + failing: List[int] = [] + poll_delay = 0.1 # adaptive; grows up to 1s + while self.wait_for_any(finished_running, failing): + if not self.cfg.dry_run: + time.sleep(poll_delay) + poll_delay = min(1.0, poll_delay * 1.5) + else: + time.sleep(0.001) + + finished.extend(finished_running) + finishedtasks_set.update(finished) + + # take failed tasks out of the "finished" accounting + if failing: + error_encountered = True + fs = set(failing) + finished = [x for x in finished if x not in fs] + finishedtasks_set.difference_update(fs) + + # retries go back onto the candidate list + if self.tids_marked_retry: + rs = set(self.tids_marked_retry) + finished = [x for x in finished if x not in rs] + finishedtasks_set.difference_update(rs) + for t in self.tids_marked_retry: + if t not in candidates: + candidates.append(t) + self.tids_marked_retry.clear() + + # new candidates: successors whose all needs are done + for tid in finished: + for succ in self.wf.forward_adj[tid]: + if succ in candidates: + continue + if self.proc_status[succ] != "ToDo": + continue + preds = self.wf.reverse_adj[succ] + if all(p in finishedtasks_set for p in preds): + candidates.append(succ) + + self.actionlog.debug("new candidates %s", candidates) + + if not candidates and not self.process_list: + break + except Exception: + traceback.print_exc() + self._sighandler(0, None) + + self.monitor.stop() + self.monitor.join(timeout=2) + end = time.perf_counter() + msg = "with failures" if error_encountered else "success" + print(f"\n**** Pipeline done {msg} (global_runtime : {end - self.start_time:.3f}s) *****\n") + self.actionlog.debug("global_runtime : %.3fs", end - self.start_time) + return error_encountered + + # ----- error message ----- + def _noprogress_error(self) -> None: + msg = ( + "Scheduler runtime error: cannot make progress although candidates exist.\n\n" + "This typically means a task's estimated resources exceed the configured\n" + "--cpu-limit or --mem-limit. On a 16 GB node, try --mem-limit 20000 (MB);\n" + "the ACTUAL use might be lower than the estimate. Alternatively convert\n" + "the workflow to a linear shell script via --produce-script .sh and\n" + "run that directly.\n" + ) + print(msg, file=sys.stderr) diff --git a/MC/workflow_runner/o2dpg_runner/graph.py b/MC/workflow_runner/o2dpg_runner/graph.py new file mode 100644 index 000000000..d04184f47 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/graph.py @@ -0,0 +1,159 @@ +"""Graph utilities on the task DAG. + +Nothing here knows about workflows, tasks, or resources -- it operates on +integer-indexed adjacency lists only. Pure, testable, no side effects. + +Supersedes the recursive ``findAllTopologicalOrders`` and +``find_all_dependent_tasks`` in the original prototype, both of which had +bugs and needed sys.setrecursionlimit(100000) to survive diamond DAGs. +""" + +from __future__ import annotations + +from collections import defaultdict, deque +from typing import Dict, Iterable, List, Set, Tuple + + +def build_adjacency( + n_nodes: int, edges: Iterable[Tuple[int, int]] +) -> Tuple[List[List[int]], List[List[int]], List[int]]: + """Return (forward_adj, reverse_adj, indegree) for ``n_nodes`` nodes. + + forward_adj[u] lists successors; reverse_adj[v] lists predecessors. + """ + forward: List[List[int]] = [[] for _ in range(n_nodes)] + reverse: List[List[int]] = [[] for _ in range(n_nodes)] + indeg = [0] * n_nodes + for u, v in edges: + forward[u].append(v) + reverse[v].append(u) + indeg[v] += 1 + return forward, reverse, indeg + + +def kahn_topological_order( + n_nodes: int, + forward_adj: List[List[int]], + indegree: List[int], + tiebreak: List[int] = None, +) -> List[int]: + """Deterministic topological order via Kahn's algorithm. + + ``tiebreak`` is an optional per-node integer used to break ties + among ready nodes. Smaller tiebreak value first. If None, the + node index is used (which is also deterministic). + """ + if tiebreak is None: + tiebreak = list(range(n_nodes)) + indeg = list(indegree) + # Use a sorted list as a tiny priority queue; for small DAGs this is + # fine, and ties are broken deterministically. + ready = sorted([n for n in range(n_nodes) if indeg[n] == 0], + key=lambda n: (tiebreak[n], n)) + out: List[int] = [] + # Simple loop; no heap because re-sorting on small ready sets is cheap + # and we want full determinism. + while ready: + u = ready.pop(0) + out.append(u) + for v in forward_adj[u]: + indeg[v] -= 1 + if indeg[v] == 0: + # insert keeping sorted-by-tiebreak + lo, hi = 0, len(ready) + key = (tiebreak[v], v) + while lo < hi: + mid = (lo + hi) // 2 + if (tiebreak[ready[mid]], ready[mid]) < key: + lo = mid + 1 + else: + hi = mid + ready.insert(lo, v) + if len(out) != n_nodes: + raise ValueError("Graph has at least one cycle; topological sort impossible") + return out + + +def descendants( + forward_adj: List[List[int]], source: int, cache: Dict[int, Set[int]] = None +) -> Set[int]: + """All nodes reachable from ``source`` (excluding ``source`` itself), memoized. + + Uses iterative DFS with post-order memoization. Safe on DAGs with + diamonds and deep chains; no recursion limit concerns. + """ + if cache is None: + cache = {} + if source in cache: + return cache[source] + + # iterative post-order: process children first, then combine + order: List[int] = [] + seen: Set[int] = set() + stack: List[Tuple[int, int]] = [(source, 0)] + while stack: + node, child_idx = stack[-1] + kids = forward_adj[node] + if child_idx < len(kids): + stack[-1] = (node, child_idx + 1) + child = kids[child_idx] + if child not in seen and child not in cache: + seen.add(child) + stack.append((child, 0)) + else: + stack.pop() + order.append(node) + + # Now assign cache entries in post-order (children before parents). + for node in order: + if node in cache: + continue + s: Set[int] = set() + for child in forward_adj[node]: + s.add(child) + s |= cache.get(child, set()) + cache[node] = s + return cache[source] + + +def ancestors( + reverse_adj: List[List[int]], sink: int, cache: Dict[int, Set[int]] = None +) -> Set[int]: + """All nodes that can reach ``sink`` (excluding sink itself), memoized.""" + # ancestors in forward graph == descendants in reverse graph + return descendants(reverse_adj, sink, cache) + + +def longest_path_length( + forward_adj: List[List[int]], + topo_order: List[int], + node_weight: List[float], +) -> List[float]: + """Longest-path weight from each node to any leaf (inclusive of node). + + ``topo_order`` must be a valid topological ordering. Uses reverse + traversal and DP. Used by CriticalPathPolicy. + """ + n = len(node_weight) + lp = list(node_weight) # include own weight + for u in reversed(topo_order): + best_child = 0.0 + for v in forward_adj[u]: + if lp[v] > best_child: + best_child = lp[v] + lp[u] = node_weight[u] + best_child + return lp + + +def invert_adj(forward_adj: List[List[int]]) -> List[List[int]]: + """Compute reverse adjacency from forward adjacency.""" + n = len(forward_adj) + rev: List[List[int]] = [[] for _ in range(n)] + for u in range(n): + for v in forward_adj[u]: + rev[v].append(u) + return rev + + +def root_nodes(indegree: List[int]) -> List[int]: + return [i for i, d in enumerate(indegree) if d == 0] diff --git a/MC/workflow_runner/o2dpg_runner/monitoring.py b/MC/workflow_runner/o2dpg_runner/monitoring.py new file mode 100644 index 000000000..f156c6655 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/monitoring.py @@ -0,0 +1,505 @@ +"""Resource monitor running in a background thread. + +The main scheduler loop used to poll psutil synchronously at 1 Hz, which +cost 10-20% of one core on realistic workflows. Now the monitor is a +separate thread with independent CPU (cheap, 1 Hz) and MEM (expensive, +0.2 Hz) cadences. The scheduler reads the latest snapshot lock-free via +an atomic dict reference. + +Backend interface: a callable + sample(task_pid_list) -> {pid: {"cpu_pct": float, "pss_mb": float, + "uss_mb": float, "swap_mb": float, + "children": [pid, ...]}} +""" + +from __future__ import annotations + +import logging +import os +import subprocess +import threading +import time +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Tuple + +try: + import psutil + HAVE_PSUTIL = True +except ImportError: + HAVE_PSUTIL = False + +log = logging.getLogger(__name__) + + +@dataclass +class TaskSnapshot: + """Per-task roll-up of the most recent monitor pass.""" + tid: int + name: str + t_delta_ms: int = 0 + cpu_pct: float = 0.0 # 0..100 * n_cores (psutil) + uss_mb: float = 0.0 + pss_mb: float = 0.0 + swap_mb: float = 0.0 + nice: int = 0 + labels: List[str] = field(default_factory=list) + disc_mb: float = -1.0 # global disc usage (same value on all tasks, or -1) + mem_fresh: bool = False # did this snapshot's mem numbers come from a fresh read? + # cgroup-based metrics (None when not in a systemd slice / no per-task scope) + cgroup_cpu_pct: Optional[float] = None # aggregate CPU % from cgroup cpu.stat + cgroup_mem_mb: Optional[float] = None # aggregate memory from cgroup memory.current + + +def _get_child_procs_fallback(base_pid: int) -> List[int]: + """Pure-bash fallback when psutil.Process.children raises AccessDenied.""" + script = r''' + childprocs() { + local parent=$1 + if [ ! "$2" ]; then child_pid_list=""; fi + if [ "$parent" ]; then + child_pid_list="$child_pid_list $parent" + for childpid in $(pgrep -P ${parent}); do + childprocs $childpid "nottoplevel" + done + fi + if [ ! "$2" ]; then echo "${child_pid_list}"; fi + } + ''' + full = script + f"\nchildprocs {base_pid}\n" + out = subprocess.check_output(full, shell=True) + pids: List[int] = [] + for tok in out.decode().split(): + try: + pids.append(int(tok)) + except ValueError: + continue + return pids + + +class PsutilBackend: + """psutil-based monitor backend. Caches per-pid Process objects so that + cpu_percent(interval=None) has a stable 'previous reading' baseline.""" + + def __init__(self): + if not HAVE_PSUTIL: + raise RuntimeError("psutil not available") + self._proc_cache: Dict[int, "psutil.Process"] = {} + # Prime baseline on the manager process so first delta is sensible. + try: + psutil.cpu_percent(interval=None) + except Exception: + pass + + def _get_or_add(self, pid: int) -> Optional["psutil.Process"]: + p = self._proc_cache.get(pid) + if p is not None: + return p + try: + p = psutil.Process(pid) + # Deliberately DO NOT call p.cpu_percent(interval=None) here to prime. + # psutil's rule: the first call after Process() construction returns + # a CPU % relative to process creation -- useful as a "since start" + # figure, not "since last poll". On multi-process tasks that spawn + # children between monitor ticks, priming right before the read + # yields 0 because no time has elapsed. Letting the monitor thread's + # regular cadence handle both calls produces correct deltas from + # tick 2 onward (tick 1 returns the since-creation value, which is + # a reasonable initial estimate anyway). + self._proc_cache[pid] = p + return p + except Exception: + return None + + def forget(self, pid: int) -> None: + self._proc_cache.pop(pid, None) + + def sweep_dead(self) -> int: + """Evict cached Process objects whose underlying PID is gone. + + Called once per monitor pass. Keeps the cache size bounded over + long runs where short-lived DPL/FAIRMQ children come and go. + """ + dead = 0 + for pid in list(self._proc_cache.keys()): + try: + if not self._proc_cache[pid].is_running(): + del self._proc_cache[pid] + dead += 1 + except Exception: + self._proc_cache.pop(pid, None) + dead += 1 + return dead + + def sample( + self, + root_pid: int, + want_mem: bool, + ) -> Tuple[float, float, float, float, int]: + """Return (cpu_pct_sum, pss_mb, uss_mb, swap_mb, nice_of_root). + + Sums over root and all descendants. If want_mem is False the + memory figures are returned as 0 (caller should interpret -> use + last known). + """ + root = self._get_or_add(root_pid) + if root is None: + return 0.0, 0.0, 0.0, 0.0, 0 + + # Enumerate the PIDs of the whole process tree, then look each one + # up in the cache. CRITICAL: psutil.Process.children() returns NEW + # Process objects on every call, which means their cpu_percent() + # baseline is reset each tick -> we'd read 0.0 forever. Always go + # through _get_or_add so baselines persist between ticks. + try: + child_pids = [c.pid for c in root.children(recursive=True)] + except (psutil.NoSuchProcess,): + return 0.0, 0.0, 0.0, 0.0, 0 + except (psutil.AccessDenied, PermissionError): + try: + child_pids = _get_child_procs_fallback(root_pid) + if root_pid in child_pids: + child_pids.remove(root_pid) + except Exception: + child_pids = [] + + procs = [root] + for pid in child_pids: + p = self._get_or_add(pid) + if p is not None: + procs.append(p) + + cpu_sum = 0.0 + pss_sum = 0.0 + uss_sum = 0.0 + swap_sum = 0.0 + for p in procs: + # CPU: cheap + try: + cpu_sum += p.cpu_percent(interval=None) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + if want_mem: + try: + mi = p.memory_full_info() + pss_sum += getattr(mi, "pss", 0) or 0 + uss_sum += getattr(mi, "uss", 0) or 0 + swap_sum += getattr(mi, "swap", 0) or 0 + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + # bytes -> MB + pss_mb = pss_sum / 1024.0 / 1024.0 + uss_mb = uss_sum / 1024.0 / 1024.0 + swap_mb = swap_sum / 1024.0 / 1024.0 + + try: + nice = root.nice() + except Exception: + nice = 0 + + return cpu_sum, pss_mb, uss_mb, swap_mb, nice + + +def _read_cgroup_v2_dir(pid: int) -> Optional[str]: + """Return the cgroup v2 directory for *pid*, or None if not on cgroup v2.""" + try: + with open(f"/proc/{pid}/cgroup") as fh: + for line in fh: + parts = line.strip().split(":", 2) + # cgroup v2 unified hierarchy: single entry "0::" + if len(parts) == 3 and parts[0] == "0": + rel = parts[2].lstrip("/") + candidate = f"/sys/fs/cgroup/{rel}" if rel else "/sys/fs/cgroup" + if os.path.isdir(candidate): + return candidate + except OSError: + pass + return None + + +class CgroupV2Monitor: + """Reads aggregate CPU time and memory for one cgroup v2 directory. + + Works with cgroup v2 (unified hierarchy) only — the format used by + systemd on modern Linux. Instantiating this class is always safe; call + ``available`` to test whether a valid cgroup path was found/given before + using ``sample()``. + + If *cgroup_dir* is provided the directory is used directly (for per-task + monitoring of a known scope). Otherwise the caller's own cgroup is + detected from ``/proc/self/cgroup`` (for the global runner-level monitor). + + CPU is computed by differentiating the ``usage_usec`` counter in + ``cpu.stat``. The first ``sample()`` call primes the counter and returns + ``None`` for cpu_pct; subsequent calls give the average utilisation + (as a percentage of one core, same units as psutil's cpu_percent). + + Memory is read from ``memory.current`` (bytes → MB). It counts all + memory mapped by processes in the cgroup including file-backed pages; + shared pages are counted once per cgroup, not per process. For an + apples-to-apples comparison with psutil's PSS metric use the + ``memory.stat`` anon field (not implemented here — ``memory.current`` + is the right proxy for a hard MemoryMax enforcement budget). + """ + + def __init__(self, cgroup_dir: Optional[str] = None) -> None: + if cgroup_dir is not None: + self._cgroup_dir: Optional[str] = cgroup_dir if os.path.isdir(cgroup_dir) else None + else: + self._cgroup_dir = _read_cgroup_v2_dir(os.getpid()) + self._last_usage_usec: Optional[int] = None + self._last_ts: Optional[float] = None + + @property + def available(self) -> bool: + return self._cgroup_dir is not None + + def sample(self) -> Tuple[Optional[float], Optional[float]]: + """Return (cpu_pct, mem_mb). cpu_pct is None on the first call.""" + if not self._cgroup_dir: + return None, None + + cpu_pct: Optional[float] = None + mem_mb: Optional[float] = None + + # --- CPU --- + try: + with open(os.path.join(self._cgroup_dir, "cpu.stat")) as fh: + for line in fh: + if line.startswith("usage_usec"): + usage_usec = int(line.split()[1]) + now = time.monotonic() + if self._last_usage_usec is not None and self._last_ts is not None: + dt = now - self._last_ts + if dt > 0: + d_usec = usage_usec - self._last_usage_usec + # d_usec / (dt * 1e6) is CPU fraction relative to 1 core; + # multiply by 100 to match psutil's cpu_percent scale. + cpu_pct = (d_usec / (dt * 1e6)) * 100.0 + self._last_usage_usec = usage_usec + self._last_ts = now + break + except OSError: + pass + + # --- Memory --- + try: + with open(os.path.join(self._cgroup_dir, "memory.current")) as fh: + mem_mb = int(fh.read().strip()) / 1024.0 / 1024.0 + except OSError: + pass + + return cpu_pct, mem_mb + + +class MonitorThread(threading.Thread): + """Background thread polling a set of (tid, pid) pairs. + + The scheduler registers tasks via register()/deregister(). On each + pass the thread computes the latest snapshot and stores it in + self.snapshots; the scheduler reads atomically. + + CPU and MEM have independent cadences because PSS reads via + /proc//smaps_rollup are ~10x more expensive than cpu_percent. + """ + + def __init__( + self, + cpu_interval: float = 1.0, + mem_interval: float = 5.0, + backend: Optional[PsutilBackend] = None, + monitor_disc: bool = False, + disc_path: str = ".", + global_cgroup_dir: Optional[str] = None, + ): + super().__init__(daemon=True, name="o2dpg-monitor") + self.cpu_interval = cpu_interval + self.mem_interval = mem_interval + self.backend = backend if backend is not None else PsutilBackend() + self.monitor_disc = monitor_disc + self.disc_path = disc_path + + self._lock = threading.Lock() + # registered[tid] = {"pid": int, "name": str, "labels": list, + # "start_time": float} + self._registered: Dict[int, Dict] = {} + self._snapshots: Dict[int, TaskSnapshot] = {} + self._stop_event = threading.Event() + self._last_mem_ts: float = 0.0 + self._last_disc_mb: float = -1.0 + self._last_disc_ts: float = 0.0 + # Tick counter: incremented once per monitor pass (roughly once per + # cpu_interval). Read by the executor when writing metric lines so + # the "iter" field reflects wall-clock ticks like the prototype did. + self.tick: int = 0 + + # Opportunistic cgroup v2 global monitor. Only enabled when the + # caller passes an explicit *slice* directory (i.e. the runner was + # launched under --systemd-run). Without an explicit directory we + # skip cgroup monitoring entirely — the runner's own cgroup outside a + # dedicated slice covers unrelated user-session processes and gives + # misleading aggregates. + # Written only from the monitor thread; read from the executor thread — + # GIL makes bare float/None assignment atomic. + self._cgroup: Optional[CgroupV2Monitor] = ( + CgroupV2Monitor(cgroup_dir=global_cgroup_dir) if global_cgroup_dir else None + ) + self.global_cpu_pct: Optional[float] = None # cgroup-aggregate CPU % + self.global_mem_mb: Optional[float] = None # cgroup-aggregate memory MB + if self._cgroup is not None and self._cgroup.available: + log.info("CgroupV2Monitor global active at %s", self._cgroup._cgroup_dir) + + # ----- registration ----- + def register( + self, + tid: int, + pid: int, + name: str, + labels: List[str], + start_time: float, + resolve_cgroup: bool = False, + ) -> None: + """Register a task for monitoring. + + If *resolve_cgroup* is True the monitor thread will lazily locate the + task's cgroup v2 directory by inspecting the first child process of + *pid* (which is the systemd-run wrapper when per-task scopes are used). + Once resolved a per-task CgroupV2Monitor is created and its readings + are stored in the TaskSnapshot alongside the psutil figures. + """ + with self._lock: + self._registered[tid] = { + "pid": pid, + "name": name, + "labels": labels, + "start_time": start_time, + "resolve_cgroup": resolve_cgroup, + "cgroup_monitor": None, # filled lazily by _one_pass + } + + def deregister(self, tid: int) -> None: + with self._lock: + entry = self._registered.pop(tid, None) + if entry is not None: + self.backend.forget(entry["pid"]) + + # ----- snapshot access ----- + def latest(self) -> Dict[int, TaskSnapshot]: + """Return a shallow copy of current snapshots.""" + with self._lock: + return dict(self._snapshots) + + def latest_for(self, tid: int) -> Optional[TaskSnapshot]: + with self._lock: + return self._snapshots.get(tid) + + # ----- control ----- + def stop(self) -> None: + self._stop_event.set() + + # ----- loop ----- + def _disc_usage_mb(self) -> float: + try: + out = subprocess.check_output(["du", "-sb", self.disc_path], text=True) + return int(out.split()[0]) / 1024.0 / 1024.0 + except Exception: + return -1.0 + + def _one_pass(self, now: float) -> None: + self.tick += 1 + + # cgroup global totals (cheap reads, always done every pass) + if self._cgroup is not None and self._cgroup.available: + g_cpu, g_mem = self._cgroup.sample() + if g_cpu is not None: + self.global_cpu_pct = g_cpu + if g_mem is not None: + self.global_mem_mb = g_mem + + want_mem = (now - self._last_mem_ts) >= self.mem_interval + if want_mem: + self._last_mem_ts = now + + if self.monitor_disc and (now - self._last_disc_ts) >= self.mem_interval: + self._last_disc_mb = self._disc_usage_mb() + self._last_disc_ts = now + + # snapshot registrations under lock, release for the slow work + with self._lock: + registered_copy = dict(self._registered) + + new_snaps: Dict[int, TaskSnapshot] = {} + for tid, info in registered_copy.items(): + pid = info["pid"] + cpu_pct, pss_mb, uss_mb, swap_mb, nice = self.backend.sample(pid, want_mem) + # fall back to previous mem reading if not a mem-interval tick + prev = self._snapshots.get(tid) + if not want_mem and prev is not None: + pss_mb = prev.pss_mb + uss_mb = prev.uss_mb + swap_mb = prev.swap_mb + + # --- per-task cgroup monitoring --- + # Lazy resolution: when the task runs inside a systemd scope, its + # direct child of the systemd-run wrapper PID lands in that scope's + # cgroup. We probe once per pass until the child appears. + if info.get("resolve_cgroup") and info.get("cgroup_monitor") is None: + try: + p_obj = self.backend._get_or_add(pid) + if p_obj is not None: + children = p_obj.children() + if children: + cgroup_dir = _read_cgroup_v2_dir(children[0].pid) + if cgroup_dir: + info["cgroup_monitor"] = CgroupV2Monitor(cgroup_dir=cgroup_dir) + log.info("Per-task cgroup resolved: tid=%d %s → %s", + tid, info["name"], cgroup_dir) + except Exception: + pass + + cgroup_cpu: Optional[float] = None + cgroup_mem: Optional[float] = None + cm: Optional[CgroupV2Monitor] = info.get("cgroup_monitor") + if cm is not None and cm.available: + cgroup_cpu, cgroup_mem = cm.sample() + # cgroup_cpu is None on the very first sample() call (no prior + # baseline yet) and whenever the scope's cpu.stat is unreadable + # (e.g. after --collect removes the finished scope). In both + # cases we leave it as None rather than carrying forward a stale + # value — one tick with None is preferable to a wrong number. + # ------------------------------------------------------- + + t_delta_ms = int((now - info["start_time"]) * 1000) + new_snaps[tid] = TaskSnapshot( + tid=tid, name=info["name"], + t_delta_ms=t_delta_ms, cpu_pct=cpu_pct, + uss_mb=uss_mb, pss_mb=pss_mb, swap_mb=swap_mb, + nice=nice, labels=info["labels"], + disc_mb=self._last_disc_mb, + mem_fresh=want_mem, + cgroup_cpu_pct=cgroup_cpu, + cgroup_mem_mb=cgroup_mem, + ) + + with self._lock: + self._snapshots = new_snaps + + # housekeeping: evict dead cached Process objects once per pass + try: + self.backend.sweep_dead() + except Exception: + pass + + def run(self) -> None: + log.debug("Monitor thread started (cpu=%.2fs, mem=%.2fs)", + self.cpu_interval, self.mem_interval) + while not self._stop_event.is_set(): + now = time.perf_counter() + try: + self._one_pass(now) + except Exception: + log.exception("Monitor pass failed (continuing)") + # Sleep at the CPU cadence; MEM is gated by its own interval. + self._stop_event.wait(self.cpu_interval) + log.debug("Monitor thread exiting") diff --git a/MC/workflow_runner/o2dpg_runner/resources.py b/MC/workflow_runner/o2dpg_runner/resources.py new file mode 100644 index 000000000..4b187d90c --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/resources.py @@ -0,0 +1,362 @@ +"""Resource management: per-task estimates, global budget, semaphores. + +Structurally equivalent to the prototype's TaskResources / ResourceManager, +with three changes: + 1. is_within_limits() actually checks MEM against mem_limit (not CPU). + 2. No module-level args dependency; n_backfill is passed in. + 3. Booking/unbooking is explicit about which bucket (default / backfill). + +Sibling-sampling for --dynamic-resources still runs inside unbook(), same +ordering as before. +""" + +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass, field +from typing import Dict, Iterator, List, Optional, Tuple + +log = logging.getLogger(__name__) + + +class Semaphore: + """A named mutual-exclusion flag shared by a group of tasks. + + Deliberately not threading.Semaphore -- this is a logical gate checked + by the scheduler, not a concurrency primitive. + """ + __slots__ = ("locked",) + + def __init__(self): + self.locked = False + + def lock(self): + self.locked = True + + def unlock(self): + self.locked = False + + +@dataclass +class ResourceBoundaries: + cpu_limit: float + mem_limit: float + dynamic_resources: bool = False + optimistic_resources: bool = False + + +class TaskResources: + """Resource accounting for a single task.""" + + def __init__( + self, + tid: int, + name: str, + cpu: float, + cpu_relative: Optional[float], + mem: float, + boundaries: ResourceBoundaries, + ): + self.tid = tid + self.name = name + # originals (never mutated) + self.cpu_assigned_original = cpu + self.mem_assigned_original = mem + self.cpu_relative = cpu_relative if cpu_relative else 1.0 + # transient assignments (may be updated by sampling) + self.cpu_assigned = cpu + self.mem_assigned = mem + self.boundaries = boundaries + # sampled (after a sibling finished) + self.cpu_sampled: Optional[float] = None + self.mem_sampled: Optional[float] = None + # live monitor feed + self.time_collect: List[float] = [] + self.cpu_collect: List[float] = [] + self.mem_collect: List[float] = [] + # siblings (same "global" task name) + self.related_tasks: Optional[List["TaskResources"]] = None + self.semaphore: Optional[Semaphore] = None + self.nice_value: Optional[int] = None + self.booked = False + + # ----- helpers ----- + @property + def is_done(self) -> bool: + return bool(self.time_collect) and not self.booked + + def is_within_limits(self) -> bool: + """Check the current assignment against global boundaries.""" + ok_cpu = self.cpu_assigned <= self.boundaries.cpu_limit + ok_mem = self.mem_assigned <= self.boundaries.mem_limit + if not ok_cpu: + log.warning("CPU of %s exceeds limit: %.2f > %.2f", + self.name, self.cpu_assigned, self.boundaries.cpu_limit) + if not ok_mem: + log.warning("MEM of %s exceeds limit: %.2f > %.2f", + self.name, self.mem_assigned, self.boundaries.mem_limit) + return ok_cpu and ok_mem + + def limit_resources(self, cpu_limit: float = None, mem_limit: float = None) -> None: + if cpu_limit is None: + cpu_limit = self.boundaries.cpu_limit + if mem_limit is None: + mem_limit = self.boundaries.mem_limit + self.cpu_assigned = min(self.cpu_assigned, cpu_limit) + self.mem_assigned = min(self.mem_assigned, mem_limit) + + def add_sample(self, time_passed: float, cpu_fraction: float, mem_mb: float) -> None: + """Record a monitor sample.""" + self.time_collect.append(time_passed) + self.cpu_collect.append(cpu_fraction) + self.mem_collect.append(mem_mb) + + def sample_resources(self) -> None: + """Compute CPU/MEM sample and propagate to un-started siblings.""" + if not self.is_done: + return + + if len(self.time_collect) < 3: + self.cpu_sampled = self.cpu_assigned + self.mem_sampled = self.mem_assigned + log.debug("Task %s: not enough samples (<3); using assigned as sampled", + self.name) + else: + # Weighted mean of CPU over sample intervals, skipping the first + # (cpu_percent(interval=None) first reading is meaningless). + deltas = [self.time_collect[i + 1] - self.time_collect[i] + for i in range(len(self.time_collect) - 1)] + tot = sum(deltas) or 1.0 + cpu_integral = sum( + c * dt for c, dt in zip(self.cpu_collect[1:], deltas) if c >= 0 + ) + self.cpu_sampled = cpu_integral / tot + self.mem_sampled = max(self.mem_collect) + + if self.related_tasks is None: + return + + # aggregate over finished siblings + mem_agg = 0.0 + cpu_list: List[float] = [] + for sib in self.related_tasks: + if sib.is_done and sib.cpu_sampled is not None and sib.mem_sampled is not None: + mem_agg = max(mem_agg, sib.mem_sampled) + cpu_list.append(sib.cpu_sampled) + if not cpu_list: + return + cpu_agg = sum(cpu_list) / len(cpu_list) + + if cpu_agg > self.boundaries.cpu_limit: + log.warning("Sampled CPU (%.2f) exceeds limit (%.2f)", + cpu_agg, self.boundaries.cpu_limit) + elif cpu_agg <= 0: + # a zero reading is missing information, not a task that needs no + # CPU; handing it on would let every sibling be admitted at once + log.debug("Sampled CPU<=0 for %s; reverting to assigned", self.name) + cpu_agg = self.cpu_assigned + + if mem_agg > self.boundaries.mem_limit: + log.warning("Sampled MEM (%.2f) exceeds limit (%.2f)", + mem_agg, self.boundaries.mem_limit) + elif mem_agg <= 0: + log.debug("Sampled MEM<=0 for %s; reverting to assigned", self.name) + mem_agg = self.mem_assigned + + for sib in self.related_tasks: + if sib.is_done or sib.booked: + continue + sib.cpu_assigned = cpu_agg * sib.cpu_relative + sib.mem_assigned = mem_agg + sib.limit_resources() + + +class ResourceManager: + """Central accounting: who is booked, which bucket, what's left.""" + + def __init__( + self, + cpu_limit: float, + mem_limit: float, + procs_parallel_max: int = 100, + n_backfill_max: int = 1, + backfill_cpu_factor: float = 1.5, + backfill_mem_factor: float = 1.5, + dynamic_resources: bool = False, + optimistic_resources: bool = False, + ): + self.boundaries = ResourceBoundaries( + cpu_limit, mem_limit, dynamic_resources, optimistic_resources + ) + self.resources: List[TaskResources] = [] + self._related_by_name: Dict[str, List[TaskResources]] = {} + self._semaphores: Dict[str, Semaphore] = {} + + # default-priority bucket + self.cpu_booked = 0.0 + self.mem_booked = 0.0 + self.n_procs = 0 + + # backfill (niced) bucket + self.cpu_booked_backfill = 0.0 + self.mem_booked_backfill = 0.0 + self.n_procs_backfill = 0 + + self.procs_parallel_max = procs_parallel_max + self.n_backfill_max = n_backfill_max + self.backfill_cpu_factor = backfill_cpu_factor + self.backfill_mem_factor = backfill_mem_factor + + try: + self.nice_default = os.nice(0) + except (AttributeError, OSError): + self.nice_default = 0 + self.nice_backfill = self.nice_default + 19 + + # ----- registration ----- + def add_task( + self, + name: str, + related_name: Optional[str], + cpu: float, + cpu_relative: Optional[float], + mem: float, + semaphore_string: Optional[str] = None, + ) -> TaskResources: + res = TaskResources( + len(self.resources), name, cpu, cpu_relative, mem, self.boundaries + ) + if not res.is_within_limits() and not self.boundaries.optimistic_resources: + raise ResourceLimitExceeded( + f"Task {name} exceeds resource boundaries " + f"(cpu={cpu}/{self.boundaries.cpu_limit}, " + f"mem={mem}/{self.boundaries.mem_limit}). " + f"Use --optimistic-resources to attempt anyway." + ) + res.limit_resources() + self.resources.append(res) + + if semaphore_string: + if semaphore_string not in self._semaphores: + self._semaphores[semaphore_string] = Semaphore() + res.semaphore = self._semaphores[semaphore_string] + + if related_name: + bucket = self._related_by_name.setdefault(related_name, []) + bucket.append(res) + res.related_tasks = bucket + + return res + + # ----- monitor hook ----- + def add_monitored(self, tid: int, t_delta: float, cpu_fraction: float, mem_mb: float) -> None: + self.resources[tid].add_sample(t_delta, cpu_fraction, mem_mb) + + # ----- booking ----- + def book(self, tid: int, nice_value: int) -> None: + res = self.resources[tid] + # Prior check is expected to have set nice_value; if not, force backfill. + if res.nice_value is None: + log.warning("Task %d booked without prior ok_to_submit check; forcing backfill", + tid) + nice_value = self.nice_backfill + + res.nice_value = nice_value + res.booked = True + if res.semaphore is not None: + res.semaphore.lock() + if nice_value != self.nice_default: + self.n_procs_backfill += 1 + self.cpu_booked_backfill += res.cpu_assigned + self.mem_booked_backfill += res.mem_assigned + else: + self.n_procs += 1 + self.cpu_booked += res.cpu_assigned + self.mem_booked += res.mem_assigned + + def unbook(self, tid: int) -> None: + res = self.resources[tid] + res.booked = False + if self.boundaries.dynamic_resources: + res.sample_resources() + if res.semaphore is not None: + res.semaphore.unlock() + if res.nice_value != self.nice_default: + self.cpu_booked_backfill -= res.cpu_assigned + self.mem_booked_backfill -= res.mem_assigned + self.n_procs_backfill -= 1 + if self.n_procs_backfill <= 0: + self.cpu_booked_backfill = 0.0 + self.mem_booked_backfill = 0.0 + else: + self.n_procs -= 1 + self.cpu_booked -= res.cpu_assigned + self.mem_booked -= res.mem_assigned + if self.n_procs <= 0: + self.cpu_booked = 0.0 + self.mem_booked = 0.0 + + # ----- queries ----- + def total_procs(self) -> int: + return self.n_procs + self.n_procs_backfill + + def at_proc_cap(self) -> bool: + return self.total_procs() >= self.procs_parallel_max + + def cpu_free_default(self) -> float: + return self.boundaries.cpu_limit - self.cpu_booked + + def mem_free_default(self) -> float: + return self.boundaries.mem_limit - self.mem_booked + + def fits_default(self, res: TaskResources) -> bool: + return ( + self.cpu_booked + res.cpu_assigned <= self.boundaries.cpu_limit + and self.mem_booked + res.mem_assigned <= self.boundaries.mem_limit + ) + + def fits_backfill( + self, + res: TaskResources, + cpu_factor: float = None, + mem_factor: float = None, + ) -> bool: + if cpu_factor is None: + cpu_factor = self.backfill_cpu_factor + if mem_factor is None: + mem_factor = self.backfill_mem_factor + if self.n_procs_backfill >= self.n_backfill_max: + return False + # don't backfill with huge tasks (originals: avoid tasks too close to limit) + if res.cpu_assigned > 0.9 * self.boundaries.cpu_limit: + return False + # mem per core sanity: don't launch something whose mem is huge relative to + # the CPU budget (original heuristic: mem/cpu_limit >= 1900). + if self.boundaries.cpu_limit > 0 and res.mem_assigned / self.boundaries.cpu_limit >= 1900: + return False + + ok_cpu = (self.cpu_booked_backfill + res.cpu_assigned + <= self.boundaries.cpu_limit) + ok_cpu = ok_cpu and ( + self.cpu_booked + self.cpu_booked_backfill + res.cpu_assigned + <= cpu_factor * self.boundaries.cpu_limit + ) + ok_mem = ( + self.mem_booked + self.mem_booked_backfill + res.mem_assigned + <= mem_factor * self.boundaries.mem_limit + ) + return ok_cpu and ok_mem + + def can_be_submitted_at_all(self, res: TaskResources) -> bool: + """True if a task is not blocked by its semaphore and is not already booked.""" + if res.booked: + return False + if res.semaphore is not None and res.semaphore.locked: + return False + return True + + +class ResourceLimitExceeded(Exception): + """Raised when a task's declared resources exceed the global boundaries + and --optimistic-resources was not given.""" diff --git a/MC/workflow_runner/o2dpg_runner/scheduler/__init__.py b/MC/workflow_runner/o2dpg_runner/scheduler/__init__.py new file mode 100644 index 000000000..0386a93ea --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/scheduler/__init__.py @@ -0,0 +1,22 @@ +from .base import SchedulerPolicy, SchedulerState +from .timeframe import TimeframeFirstPolicy +from .critical_path import CriticalPathPolicy +from .best_fit import BestFitBackfillPolicy + + +def get_policy(name: str) -> SchedulerPolicy: + name = name.lower().strip() + if name in ("timeframe", "tf", "legacy"): + return TimeframeFirstPolicy() + if name in ("critical-path", "cp"): + return CriticalPathPolicy() + if name in ("best-fit", "bf"): + return BestFitBackfillPolicy() + raise ValueError(f"unknown scheduler policy: {name}") + + +__all__ = [ + "SchedulerPolicy", "SchedulerState", + "TimeframeFirstPolicy", "CriticalPathPolicy", "BestFitBackfillPolicy", + "get_policy", +] diff --git a/MC/workflow_runner/o2dpg_runner/scheduler/base.py b/MC/workflow_runner/o2dpg_runner/scheduler/base.py new file mode 100644 index 000000000..9ea3fea36 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/scheduler/base.py @@ -0,0 +1,62 @@ +"""Pluggable scheduler policy interface. + +A SchedulerPolicy has two responsibilities: + 1. order(candidates, state) -> ordered list of tids + (how to prioritize among runnable tasks) + 2. pick_submittable(ordered, resource_manager) -> iterator of (tid, nice) + (which of the ordered tasks actually fit in the remaining budget, + at what nice level, subject to policy-specific packing rules) + +Resources-aware bookkeeping (what's currently booked, what the limits +are) lives in ResourceManager. Policies only read it; they don't mutate +it. The executor calls rm.book() after picking a task. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Iterator, List, Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from ..resources import ResourceManager + + +@dataclass +class SchedulerState: + """Everything a policy might want to know about the current run. + + Kept lightweight: policies that don't need a field just ignore it. + """ + # Static data + timeframe_of: List[int] = field(default_factory=list) # tid -> timeframe + descendants_count: List[int] = field(default_factory=list) # |desc(tid)| + critical_path: List[float] = field(default_factory=list) # longest_path_length weighted by walltime (or cpu fallback) + task_cpu: List[float] = field(default_factory=list) + task_mem: List[float] = field(default_factory=list) + task_walltime: List[float] = field(default_factory=list) # per-task walltime [s]; 0 if unknown + # Derived weight tuples (static, cached by executor) + # timeframe_weight[tid] = (timeframe, -num_descendants) + timeframe_weight: List[Tuple[int, int]] = field(default_factory=list) + + +class SchedulerPolicy: + """Base class. Subclasses override order() and/or pick_submittable().""" + + name = "base" + + def order(self, candidates: List[int], state: SchedulerState) -> List[int]: + """Return candidates ordered by policy preference (best first).""" + raise NotImplementedError + + def pick_submittable( + self, + ordered: List[int], + rm: "ResourceManager", + ) -> Iterator[Tuple[int, int]]: + """Yield (tid, nice_value) for tasks that fit now. + + Removes nothing; the executor is responsible for calling rm.book() + and rebuilding candidate lists. pick_submittable is a pure + read-only view onto rm's current state. + """ + raise NotImplementedError diff --git a/MC/workflow_runner/o2dpg_runner/scheduler/best_fit.py b/MC/workflow_runner/o2dpg_runner/scheduler/best_fit.py new file mode 100644 index 000000000..a6546ac9d --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/scheduler/best_fit.py @@ -0,0 +1,104 @@ +"""Best-fit bin-packing scheduler. + +Instead of following the ordered list linearly, it picks the candidate +that maximizes a fitness score given the current remaining budget. +Iterates until nothing fits. The backfill pass uses the same idea. + +Fitness = critical_path_weight * packing_tightness, where: + - critical_path_weight is state.critical_path[tid] — remaining walltime + on the longest path (walltime-weighted when learned data is available, + cpu-weighted otherwise). This is the "hybrid CP + packing" heuristic: + prefer tasks on the longest critical path, but among those that fit + similarly, pick the one that uses capacity most tightly. + - packing_tightness = max(c/cpu_free, m/mem_free) — dominant-resource + utilisation fraction. A task scores high when it fills at least one + resource bin well; unlike 1/max(ratio), this is not dominated by a + resource with extreme slack (e.g. 60 GB mem_limit with tasks using 1 GB). + +Default ordering is still critical-path (good baseline); pick_submittable +re-ranks on the fly within the fitting set. +""" + +from __future__ import annotations + +from typing import Iterator, List, Optional, Tuple + +from .base import SchedulerPolicy, SchedulerState +from .critical_path import CriticalPathPolicy +from ..resources import ResourceManager + + +class BestFitBackfillPolicy(SchedulerPolicy): + name = "best-fit" + + def __init__(self): + self._ordering = CriticalPathPolicy() + self._state: Optional[SchedulerState] = None + + def order(self, candidates: List[int], state: SchedulerState) -> List[int]: + self._state = state + return self._ordering.order(candidates, state) + + @staticmethod + def _fitness(res, state: SchedulerState, cpu_free: float, mem_free: float) -> float: + """Higher is better; negative if it doesn't fit.""" + c = max(res.cpu_assigned, 0.01) + m = max(res.mem_assigned, 1.0) + cpu_ratio = cpu_free / c + mem_ratio = mem_free / m + if cpu_ratio < 1 or mem_ratio < 1: + return -1.0 + # Dominant-resource utilisation: fraction of the most-used resource. + # max(c/cpu_free, m/mem_free) = max(1/cpu_ratio, 1/mem_ratio). + # Higher means the task fills at least one resource bin well. + # This is correct in both constrained and resource-ample (serial) modes: + # unlike 1/max(ratio), it is not dominated by a resource with extreme slack. + tightness = max(c / cpu_free, m / mem_free) + # CP weight: remaining walltime on the longest path from this task. + # Rewards placing tasks that unblock the most remaining work first. + # Falls back to descendants_count+1 when no critical_path available. + cp = state.critical_path[res.tid] if state.critical_path else 0.0 + cp_weight = cp if cp > 0 else (state.descendants_count[res.tid] + 1 if state.descendants_count else 1) + return cp_weight * tightness + + def pick_submittable( + self, ordered: List[int], rm: ResourceManager + ) -> Iterator[Tuple[int, int]]: + if rm.at_proc_cap(): + return + + state = self._state + assert state is not None, "order() must be called before pick_submittable()" + + # --- default-nice: iterative best-fit --- + pool = list(ordered) + while pool and not rm.at_proc_cap(): + cpu_free = rm.cpu_free_default() + mem_free = rm.mem_free_default() + best_tid = -1 + best_score = -1.0 + for tid in pool: + res = rm.resources[tid] + if not rm.can_be_submitted_at_all(res): + continue + s = self._fitness(res, state, cpu_free, mem_free) + if s > best_score: + best_score = s + best_tid = tid + if best_tid < 0 or best_score < 0: + break + pool.remove(best_tid) + res = rm.resources[best_tid] + res.nice_value = rm.nice_default + yield best_tid, rm.nice_default + + # --- backfill pass --- + if rm.at_proc_cap(): + return + for tid in pool: + res = rm.resources[tid] + if not rm.can_be_submitted_at_all(res): + continue + if rm.fits_backfill(res): + res.nice_value = rm.nice_backfill + yield tid, rm.nice_backfill diff --git a/MC/workflow_runner/o2dpg_runner/scheduler/critical_path.py b/MC/workflow_runner/o2dpg_runner/scheduler/critical_path.py new file mode 100644 index 000000000..d07a6b748 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/scheduler/critical_path.py @@ -0,0 +1,60 @@ +"""Critical-path-first scheduler. + +Sorts candidates by state.critical_path[tid] — the longest remaining +path weight to any leaf. The weight is walltime [s] when --update-resources +has been used to inject learned lifetime data (resources.walltime per task); +it falls back to cpu cores otherwise. Both give a valid makespan proxy; +the walltime variant is strictly more accurate for multithreaded tasks. + +Submit discipline: scan ordered list, submit everything that fits at default +nice, then do a second backfill pass at elevated nice. No should_break — +there is no reason to stop scanning once we have committed to CP ordering. +""" + +from __future__ import annotations + +from typing import Iterator, List, Tuple + +from .base import SchedulerPolicy, SchedulerState +from ..resources import ResourceManager + + +class CriticalPathPolicy(SchedulerPolicy): + name = "critical-path" + + def order(self, candidates: List[int], state: SchedulerState) -> List[int]: + cp = state.critical_path + tfw = state.timeframe_weight + # primary: longest path (largest first); tie-break: timeframe, tid + return sorted( + candidates, + key=lambda t: (-cp[t] if cp else 0, tfw[t][0], t), + ) + + def pick_submittable( + self, ordered: List[int], rm: ResourceManager + ) -> Iterator[Tuple[int, int]]: + if rm.at_proc_cap(): + return + skipped: List[int] = [] + for tid in ordered: + if rm.at_proc_cap(): + return + res = rm.resources[tid] + if not rm.can_be_submitted_at_all(res): + continue + if rm.fits_default(res): + res.nice_value = rm.nice_default + yield tid, rm.nice_default + else: + skipped.append(tid) + + if rm.at_proc_cap(): + return + for tid in skipped: + res = rm.resources[tid] + if not rm.can_be_submitted_at_all(res): + continue + if rm.fits_backfill(res): + res.nice_value = rm.nice_backfill + yield tid, rm.nice_backfill diff --git a/MC/workflow_runner/o2dpg_runner/scheduler/timeframe.py b/MC/workflow_runner/o2dpg_runner/scheduler/timeframe.py new file mode 100644 index 000000000..22df6acd2 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/scheduler/timeframe.py @@ -0,0 +1,81 @@ +"""Timeframe-first scheduler: exact behavior of the prototype. + +Order: + (timeframe, -num_descendants) # small TF first, then most-connected tasks + +Submit: + - First pass: default nice. Scan in order; on first non-fitting task, BREAK + (this is the legacy behavior; it can block light tasks behind a heavy one). + - Second pass: backfill nice. Scan remaining in order; no break on miss. + +This is the default policy so a bare-minimum invocation reproduces the +prototype's scheduling decisions. +""" + +from __future__ import annotations + +import logging +from typing import Iterator, List, Tuple + +from .base import SchedulerPolicy, SchedulerState +from ..resources import ResourceManager + +log = logging.getLogger(__name__) + + +class TimeframeFirstPolicy(SchedulerPolicy): + name = "timeframe" + + def __init__(self, drop_should_break: bool = False): + # When True, the default pass does not break on a non-fitting task + # but keeps scanning, so a light task can slip past a heavy one. + self.drop_should_break = drop_should_break + + def order(self, candidates: List[int], state: SchedulerState) -> List[int]: + # sort prefers small timeframe, then more descendants + return sorted( + candidates, + key=lambda t: (state.timeframe_weight[t][0], -state.timeframe_weight[t][1]), + ) + + def pick_submittable( + self, ordered: List[int], rm: ResourceManager + ) -> Iterator[Tuple[int, int]]: + if rm.at_proc_cap(): + return + + # --- default-nice pass --- + skipped_for_backfill: List[int] = [] + for tid in ordered: + if rm.at_proc_cap(): + return + res = rm.resources[tid] + if not rm.can_be_submitted_at_all(res): + continue + if rm.fits_default(res): + res.nice_value = rm.nice_default + yield tid, rm.nice_default + else: + if self.drop_should_break: + skipped_for_backfill.append(tid) + continue + # legacy behavior: the first non-fit breaks the default pass + skipped_for_backfill.extend( + ordered[ordered.index(tid):] + ) + break + + # --- backfill pass --- + if rm.at_proc_cap(): + return + seen = set() + for tid in skipped_for_backfill: + if tid in seen: + continue + seen.add(tid) + res = rm.resources[tid] + if not rm.can_be_submitted_at_all(res): + continue + if rm.fits_backfill(res): + res.nice_value = rm.nice_backfill + yield tid, rm.nice_backfill diff --git a/MC/workflow_runner/o2dpg_runner/tests/__init__.py b/MC/workflow_runner/o2dpg_runner/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/MC/workflow_runner/o2dpg_runner/tests/fixtures/tiny_workflow.json b/MC/workflow_runner/o2dpg_runner/tests/fixtures/tiny_workflow.json new file mode 100644 index 000000000..251cd5e04 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/fixtures/tiny_workflow.json @@ -0,0 +1,104 @@ +{ + "stages": [ + { + "name": "__global_init_task__", + "cmd": "NO-COMMAND", + "needs": [], + "cwd": "./", + "timeframe": -1, + "labels": [], + "resources": {"cpu": 1, "mem": 100, "relative_cpu": null}, + "env": {"FOO": "bar"} + }, + { + "name": "bkg", + "cmd": "echo bkg > out.dat", + "needs": [], + "cwd": "./", + "timeframe": -1, + "labels": ["SIM"], + "resources": {"cpu": 2, "mem": 500, "relative_cpu": null} + }, + { + "name": "sgnsim_1", + "cmd": "echo sgnsim_1", + "needs": ["bkg"], + "cwd": "./tf1", + "timeframe": 1, + "labels": ["SIM"], + "resources": {"cpu": 4, "mem": 2000, "relative_cpu": 1.0} + }, + { + "name": "digi_1", + "cmd": "echo digi_1", + "needs": ["sgnsim_1"], + "cwd": "./tf1", + "timeframe": 1, + "labels": ["DIGI"], + "resources": {"cpu": 2, "mem": 1000, "relative_cpu": 1.0} + }, + { + "name": "reco_1", + "cmd": "echo reco_1", + "needs": ["digi_1"], + "cwd": "./tf1", + "timeframe": 1, + "labels": ["RECO"], + "resources": {"cpu": 4, "mem": 1500, "relative_cpu": 1.0} + }, + { + "name": "qc_1", + "cmd": "echo qc_1", + "needs": ["reco_1"], + "cwd": "./tf1", + "timeframe": 1, + "labels": ["QC"], + "resources": {"cpu": 1, "mem": 500, "relative_cpu": 1.0} + }, + { + "name": "sgnsim_2", + "cmd": "echo sgnsim_2", + "needs": ["bkg"], + "cwd": "./tf2", + "timeframe": 2, + "labels": ["SIM"], + "resources": {"cpu": 4, "mem": 2000, "relative_cpu": 1.0} + }, + { + "name": "digi_2", + "cmd": "echo digi_2", + "needs": ["sgnsim_2"], + "cwd": "./tf2", + "timeframe": 2, + "labels": ["DIGI"], + "resources": {"cpu": 2, "mem": 1000, "relative_cpu": 1.0} + }, + { + "name": "reco_2", + "cmd": "echo reco_2", + "needs": ["digi_2"], + "cwd": "./tf2", + "timeframe": 2, + "labels": ["RECO"], + "resources": {"cpu": 4, "mem": 1500, "relative_cpu": 1.0} + }, + { + "name": "qc_2", + "cmd": "echo qc_2", + "needs": ["reco_2"], + "cwd": "./tf2", + "timeframe": 2, + "labels": ["QC"], + "resources": {"cpu": 1, "mem": 500, "relative_cpu": 1.0} + }, + { + "name": "aod", + "cmd": "echo aod", + "needs": ["reco_1", "reco_2"], + "cwd": "./", + "timeframe": -1, + "labels": ["AOD"], + "resources": {"cpu": 2, "mem": 800, "relative_cpu": 1.0} + } + ] +} diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_cache.py b/MC/workflow_runner/o2dpg_runner/tests/test_cache.py new file mode 100644 index 000000000..3dd68cfb7 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/test_cache.py @@ -0,0 +1,119 @@ +import json +import os + +import pytest + +from o2dpg_runner.cache import ( + TaskCache, compute_fingerprint, done_path, fingerprint_path, + remove_done_flag, +) + + +def _make_task(cmd="echo hi", needs=None, env=None): + return { + "name": "t", + "cmd": cmd, + "needs": needs or [], + "env": env or {}, + } + + +def _mkdone(tmpdir, logname="t.log"): + logfile = os.path.join(str(tmpdir), logname) + open(done_path(logfile), "w").close() + return logfile + + +def test_fingerprint_stable(): + t = _make_task("cmd1", needs=["a", "b"]) + f1 = compute_fingerprint(t) + f2 = compute_fingerprint(t) + assert f1 == f2 + + +def test_fingerprint_changes_on_cmd(): + f1 = compute_fingerprint(_make_task("cmd1")) + f2 = compute_fingerprint(_make_task("cmd2")) + assert f1["cmd_hash"] != f2["cmd_hash"] + + +def test_fingerprint_needs_order_insensitive(): + f1 = compute_fingerprint(_make_task(needs=["a", "b"])) + f2 = compute_fingerprint(_make_task(needs=["b", "a"])) + assert f1["needs"] == f2["needs"] + + +def test_cache_off_skips_if_done(tmp_path): + logfile = _mkdone(tmp_path) + c = TaskCache("off") + fp = compute_fingerprint(_make_task()) + assert c.is_done(logfile, fp) is True + + +def test_cache_off_does_not_skip_without_done(tmp_path): + logfile = os.path.join(str(tmp_path), "t.log") + c = TaskCache("off") + fp = compute_fingerprint(_make_task()) + assert c.is_done(logfile, fp) is False + + +def test_cache_lenient_keeps_when_no_sidecar(tmp_path): + logfile = _mkdone(tmp_path) + c = TaskCache("lenient") + fp = compute_fingerprint(_make_task()) + assert c.is_done(logfile, fp) is True + + +def test_cache_strict_invalidates_without_sidecar(tmp_path): + logfile = _mkdone(tmp_path) + c = TaskCache("strict") + fp = compute_fingerprint(_make_task()) + assert c.is_done(logfile, fp) is False + # also cleared the _done file + assert not os.path.exists(done_path(logfile)) + + +def test_cache_lenient_invalidates_on_cmd_change(tmp_path): + logfile = _mkdone(tmp_path) + c = TaskCache("lenient") + old_fp = compute_fingerprint(_make_task("old_cmd")) + c.record(logfile, old_fp) + new_fp = compute_fingerprint(_make_task("new_cmd")) + assert c.is_done(logfile, new_fp) is False + assert not os.path.exists(done_path(logfile)) + + +def test_cache_lenient_tolerates_env_change(tmp_path, caplog): + logfile = _mkdone(tmp_path) + c = TaskCache("lenient") + old_fp = compute_fingerprint(_make_task(env={"ALICE_O2_VERSION": "v1"})) + c.record(logfile, old_fp) + new_fp = compute_fingerprint(_make_task(env={"ALICE_O2_VERSION": "v2"})) + assert c.is_done(logfile, new_fp) is True + + +def test_cache_strict_invalidates_on_env_change(tmp_path): + logfile = _mkdone(tmp_path) + c = TaskCache("strict") + old_fp = compute_fingerprint(_make_task(env={"ALICE_O2_VERSION": "v1"})) + c.record(logfile, old_fp) + new_fp = compute_fingerprint(_make_task(env={"ALICE_O2_VERSION": "v2"})) + assert c.is_done(logfile, new_fp) is False + + +def test_cache_record_off_does_nothing(tmp_path): + logfile = _mkdone(tmp_path) + c = TaskCache("off") + fp = compute_fingerprint(_make_task()) + c.record(logfile, fp) + assert not os.path.exists(fingerprint_path(logfile)) + + +def test_remove_done_flag_removes_both(tmp_path): + logfile = _mkdone(tmp_path) + fp_path = fingerprint_path(logfile) + with open(fp_path, "w") as f: + f.write("{}") + remove_done_flag(logfile) + assert not os.path.exists(done_path(logfile)) + assert not os.path.exists(fp_path) diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_executor_e2e.py b/MC/workflow_runner/o2dpg_runner/tests/test_executor_e2e.py new file mode 100644 index 000000000..fc081a685 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/test_executor_e2e.py @@ -0,0 +1,216 @@ +"""End-to-end executor tests using the tiny fixture workflow. + +The commands are plain `echo` calls, so no CVMFS / O2 software needed. +We invoke the CLI's main() the same way the shell entry point would. +""" + +import json +import logging +import os +import shutil +import sys +from types import SimpleNamespace + +import pytest + +from o2dpg_runner.config import RunnerConfig +from o2dpg_runner.workflow import build_workflow, load_json +from o2dpg_runner.executor import WorkflowExecutor + +FIXTURE = os.path.join(os.path.dirname(__file__), "fixtures", "tiny_workflow.json") + + +def _make_logger(name, path): + lg = logging.getLogger(name) + lg.handlers.clear() + lg.addHandler(logging.FileHandler(path)) + lg.setLevel(logging.INFO) + lg.propagate = False + return lg + + +def _prep_workflow_in_tmp(tmp_path): + """Copy the fixture into tmp_path and wrap every cmd with a _done marker, + since the stock O2 taskwrapper isn't available in tests.""" + raw = load_json(FIXTURE) + # Patch each command so that it writes the expected _done file after success. + for t in raw["stages"]: + if t["name"] == "__global_init_task__": + continue + cwd = t.get("cwd", "./") + name = t["name"] + # emulate the taskwrapper: run the cmd, then write /.log_done + t["cmd"] = f'({t["cmd"]}) > {name}.log 2>&1 && touch {name}.log_done' + fixture_path = tmp_path / "wf.json" + fixture_path.write_text(json.dumps(raw)) + return str(fixture_path) + + +def _make_executor(tmp_path, cfg_overrides=None): + os.chdir(str(tmp_path)) + wf_path = _prep_workflow_in_tmp(tmp_path) + cfg = RunnerConfig( + workflowfile=wf_path, + cpu_limit=8, + mem_limit=16000, + maxjobs=100, + monitor_interval_cpu=0.2, + monitor_interval_mem=0.5, + ) + if cfg_overrides: + for k, v in cfg_overrides.items(): + setattr(cfg, k, v) + raw = load_json(wf_path) + wf = build_workflow(raw, cfg.target_tasks, cfg.target_labels) + action_logger = _make_logger("a", str(tmp_path / "act.log")) + metric_logger = _make_logger("m", str(tmp_path / "met.log")) + return WorkflowExecutor(cfg, wf, action_logger, metric_logger) + + +def _run(tmp_path, cfg_overrides=None): + exe = _make_executor(tmp_path, cfg_overrides) + return exe.execute(), exe.wf, tmp_path + + +class _FakeProc: + pid = 4242 + + def poll(self): + return None + + def kill(self): + pass + + +class _FakeMonitor: + """One task, one snapshot, and a tick the test advances by hand.""" + + def __init__(self): + self.tick = 1 + self.global_cpu_pct = None + self.global_mem_mb = None + self.snap = SimpleNamespace( + tid=0, name="t", t_delta_ms=1000, cpu_pct=100.0, uss_mb=1.0, + pss_mb=2.0, swap_mb=0.0, nice=0, labels=[], disc_mb=-1, + cgroup_cpu_pct=None, cgroup_mem_mb=None) + + def latest(self): + return {0: self.snap} + + +def test_a_monitor_tick_is_recorded_once_however_often_it_is_polled(tmp_path): + """The wait loop polls faster than the monitor fires. Recording the same + snapshot twice defeats the 'too few samples' guard in sample_resources().""" + exe = _make_executor(tmp_path) + exe.monitor = _FakeMonitor() + exe.process_list = [(0, _FakeProc())] + + for _ in range(5): + exe.wait_for_any([], []) + assert len(exe.rm.resources[0].time_collect) == 1 + + exe.monitor.tick = 2 + exe.monitor.snap.t_delta_ms = 2000 + exe.wait_for_any([], []) + assert exe.rm.resources[0].time_collect == [1000, 2000] + + +def test_executor_runs_all_tasks(tmp_path): + rc, wf, path = _run(tmp_path) + assert rc is False # no errors + # every task should have produced a _done file + for t in wf.stages: + done_file = path / (t.get("cwd", ".") or ".") / f"{t['name']}.log_done" + assert done_file.exists(), f"missing _done for {t['name']}" + + +def test_executor_respects_target_filter(tmp_path): + rc, wf, path = _run(tmp_path, {"target_tasks": ["qc_1"]}) + assert rc is False + # only bkg, sgnsim_1, digi_1, reco_1, qc_1 should have run + expected = {"bkg", "sgnsim_1", "digi_1", "reco_1", "qc_1"} + got = {t["name"] for t in wf.stages} + assert got == expected + + +def test_executor_skips_done_tasks_on_rerun(tmp_path): + # first run + rc1, _, _ = _run(tmp_path) + assert rc1 is False + + # second run: stages unchanged -> every task should be skipped via _done. + os.chdir(str(tmp_path)) + wf_path = str(tmp_path / "wf.json") + cfg = RunnerConfig( + workflowfile=wf_path, + cpu_limit=8, mem_limit=16000, + ) + raw = load_json(wf_path) + wf = build_workflow(raw, cfg.target_tasks, cfg.target_labels) + act = _make_logger("a2", str(tmp_path / "act2.log")) + met = _make_logger("m2", str(tmp_path / "met2.log")) + exe = WorkflowExecutor(cfg, wf, act, met) + # All tasks should appear skippable up front. + for tid in range(wf.n_tasks()): + assert exe.ok_to_skip(tid), f"{wf.id_to_name[tid]} should be skippable" + + +def test_executor_critical_path_policy(tmp_path): + rc, wf, path = _run(tmp_path, {"scheduler_policy": "critical-path"}) + assert rc is False + assert all((path / (t.get("cwd", ".") or ".") / f"{t['name']}.log_done").exists() + for t in wf.stages) + + +def test_executor_best_fit_policy(tmp_path): + rc, wf, path = _run(tmp_path, {"scheduler_policy": "best-fit"}) + assert rc is False + assert all((path / (t.get("cwd", ".") or ".") / f"{t['name']}.log_done").exists() + for t in wf.stages) + + +def test_executor_drop_should_break(tmp_path): + rc, wf, path = _run(tmp_path, {"drop_should_break": True}) + assert rc is False + + +def test_executor_produce_script(tmp_path): + os.chdir(str(tmp_path)) + wf_path = _prep_workflow_in_tmp(tmp_path) + cfg = RunnerConfig( + workflowfile=wf_path, + cpu_limit=8, mem_limit=16000, + produce_script=str(tmp_path / "run.sh"), + ) + raw = load_json(wf_path) + wf = build_workflow(raw, cfg.target_tasks, cfg.target_labels) + act = _make_logger("a3", str(tmp_path / "act3.log")) + met = _make_logger("m3", str(tmp_path / "met3.log")) + exe = WorkflowExecutor(cfg, wf, act, met) + exe.execute() + text = open(str(tmp_path / "run.sh")).read() + assert "bkg" in text + assert "aod" in text + assert "#!/usr/bin/env bash" in text + + +def test_executor_dry_run(tmp_path): + rc, wf, path = _run(tmp_path, {"dry_run": True}) + assert rc is False + # dry run should NOT create _done files + any_done = any((path / (t.get("cwd", ".") or ".") / f"{t['name']}.log_done").exists() + for t in wf.stages) + assert not any_done + + +def test_executor_cache_lenient_end_to_end(tmp_path): + # first run with lenient cache + rc1, _, _ = _run(tmp_path, {"cache_policy": "lenient"}) + assert rc1 is False + # fingerprint sidecars should exist + for t in ["bkg", "qc_1", "aod"]: + # find cwd + cwd = next(s["cwd"] for s in load_json(str(tmp_path / "wf.json"))["stages"] + if s["name"] == t) + assert os.path.exists(str(tmp_path / cwd / f"{t}.log_done")) + assert os.path.exists(str(tmp_path / cwd / f"{t}.log_done.json")) diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_graph.py b/MC/workflow_runner/o2dpg_runner/tests/test_graph.py new file mode 100644 index 000000000..f0e285d51 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/test_graph.py @@ -0,0 +1,90 @@ +import pytest + +from o2dpg_runner.graph import ( + build_adjacency, kahn_topological_order, descendants, ancestors, + longest_path_length, invert_adj, root_nodes, +) + + +def test_adjacency_basic(): + # 0 -> 1 -> 2 + # \-> 3 + fwd, rev, ind = build_adjacency(4, [(0, 1), (1, 2), (1, 3)]) + assert fwd == [[1], [2, 3], [], []] + assert rev == [[], [0], [1], [1]] + assert ind == [0, 1, 1, 1] + + +def test_kahn_simple_chain(): + fwd, _, ind = build_adjacency(3, [(0, 1), (1, 2)]) + assert kahn_topological_order(3, fwd, ind) == [0, 1, 2] + + +def test_kahn_diamond(): + # 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3 + fwd, _, ind = build_adjacency(4, [(0, 1), (0, 2), (1, 3), (2, 3)]) + order = kahn_topological_order(4, fwd, ind) + assert order[0] == 0 + assert order[-1] == 3 + assert set(order[1:3]) == {1, 2} + + +def test_kahn_detects_cycle(): + fwd, _, ind = build_adjacency(3, [(0, 1), (1, 2), (2, 0)]) + with pytest.raises(ValueError): + kahn_topological_order(3, fwd, ind) + + +def test_descendants_simple(): + # 0 -> 1 -> 2 + fwd, _, _ = build_adjacency(3, [(0, 1), (1, 2)]) + assert descendants(fwd, 0) == {1, 2} + assert descendants(fwd, 1) == {2} + assert descendants(fwd, 2) == set() + + +def test_descendants_diamond_memoized(): + fwd, _, _ = build_adjacency(4, [(0, 1), (0, 2), (1, 3), (2, 3)]) + cache = {} + assert descendants(fwd, 0, cache) == {1, 2, 3} + # call again - cache hit should return same result + assert descendants(fwd, 0, cache) == {1, 2, 3} + assert descendants(fwd, 1, cache) == {3} + + +def test_ancestors(): + fwd, rev, _ = build_adjacency(4, [(0, 1), (0, 2), (1, 3), (2, 3)]) + assert ancestors(rev, 3) == {0, 1, 2} + assert ancestors(rev, 0) == set() + + +def test_invert_adj(): + fwd, _, _ = build_adjacency(4, [(0, 1), (0, 2), (1, 3)]) + rev = invert_adj(fwd) + assert rev == [[], [0], [0], [1]] + + +def test_root_nodes(): + _, _, ind = build_adjacency(4, [(0, 2), (1, 2), (2, 3)]) + assert root_nodes(ind) == [0, 1] + + +def test_longest_path(): + # 0(w=1) -> 1(w=2) -> 3(w=4) + # 0(w=1) -> 2(w=10) -> 3(w=4) + fwd, _, ind = build_adjacency(4, [(0, 1), (0, 2), (1, 3), (2, 3)]) + topo = kahn_topological_order(4, fwd, ind) + lp = longest_path_length(fwd, topo, [1.0, 2.0, 10.0, 4.0]) + # longest from 0: 0 -> 2 -> 3 = 1+10+4 = 15 + assert lp[0] == 15.0 + assert lp[3] == 4.0 + assert lp[2] == 14.0 + + +def test_descendants_deep_chain_no_recursion_limit(): + # Chain of 2000 nodes -- would blow prototype's recursion limit + N = 2000 + edges = [(i, i + 1) for i in range(N - 1)] + fwd, _, _ = build_adjacency(N, edges) + result = descendants(fwd, 0) + assert len(result) == N - 1 diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_resources.py b/MC/workflow_runner/o2dpg_runner/tests/test_resources.py new file mode 100644 index 000000000..2a723fc87 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/test_resources.py @@ -0,0 +1,137 @@ +import pytest + +from o2dpg_runner.resources import ( + ResourceManager, ResourceLimitExceeded, Semaphore, TaskResources, + ResourceBoundaries, +) + + +def _make_rm(cpu=8.0, mem=16000.0, **kw): + return ResourceManager(cpu_limit=cpu, mem_limit=mem, **kw) + + +def test_add_task_within_limits(): + rm = _make_rm() + rm.add_task("a", None, cpu=2, cpu_relative=1, mem=1000) + assert len(rm.resources) == 1 + assert rm.resources[0].name == "a" + + +def test_add_task_exceeds_limits_raises(): + rm = _make_rm(cpu=4) + with pytest.raises(ResourceLimitExceeded): + rm.add_task("big", None, cpu=8, cpu_relative=1, mem=100) + + +def test_is_within_limits_checks_mem_correctly(): + """Regression: prototype compared CPU to mem_limit by mistake.""" + rm = _make_rm(cpu=100, mem=1000, optimistic_resources=True) + # CPU under limit, MEM over limit -> should be caught + res = rm.add_task("t", None, cpu=1, cpu_relative=1, mem=5000) + # After add_task's limit_resources, mem is capped + assert res.mem_assigned == 1000 + + +def test_book_and_unbook_default(): + rm = _make_rm() + rm.add_task("a", None, 2, 1, 1000) + res = rm.resources[0] + res.nice_value = rm.nice_default + rm.book(0, rm.nice_default) + assert rm.n_procs == 1 + assert rm.cpu_booked == 2 + assert rm.mem_booked == 1000 + rm.unbook(0) + assert rm.n_procs == 0 + assert rm.cpu_booked == 0 + assert rm.mem_booked == 0 + + +def test_book_and_unbook_backfill(): + rm = _make_rm() + rm.add_task("a", None, 2, 1, 1000) + res = rm.resources[0] + res.nice_value = rm.nice_backfill + rm.book(0, rm.nice_backfill) + assert rm.n_procs_backfill == 1 + assert rm.n_procs == 0 + rm.unbook(0) + assert rm.n_procs_backfill == 0 + + +def test_fits_default(): + rm = _make_rm(cpu=4, mem=4000) + rm.add_task("a", None, 2, 1, 1000) + rm.add_task("b", None, 3, 1, 1000) + rm.resources[0].nice_value = rm.nice_default + rm.book(0, rm.nice_default) + # now 2 cpu / 1000 mem booked; b (3 cpu) doesn't fit + assert not rm.fits_default(rm.resources[1]) + + +def test_fits_backfill_rejects_too_big(): + rm = _make_rm(cpu=4, mem=4000) + rm.add_task("big", None, 4, 1, 1000) # equals cpu_limit -> 100% > 90% + rm.resources[0].limit_resources() # ensure assigned=cpu_limit + rm.resources[0].cpu_assigned = 4 # at 90% threshold + assert not rm.fits_backfill(rm.resources[0]) + + +def test_semaphore_blocks_duplicate(): + rm = _make_rm() + rm.add_task("a", None, 1, 1, 100, semaphore_string="S") + rm.add_task("b", None, 1, 1, 100, semaphore_string="S") + assert rm.resources[0].semaphore is rm.resources[1].semaphore + rm.resources[0].nice_value = rm.nice_default + rm.book(0, rm.nice_default) + assert not rm.can_be_submitted_at_all(rm.resources[1]) + rm.unbook(0) + assert rm.can_be_submitted_at_all(rm.resources[1]) + + +def test_related_tasks_share_bucket(): + rm = _make_rm() + rm.add_task("sgnsim_1", "sgnsim", 2, 1, 1000) + rm.add_task("sgnsim_2", "sgnsim", 2, 1, 1000) + assert rm.resources[0].related_tasks is rm.resources[1].related_tasks + + +def test_dynamic_sampling_propagates(): + rm = ResourceManager(cpu_limit=8, mem_limit=16000, dynamic_resources=True) + rm.add_task("t_1", "t", 2, 1, 1000) + rm.add_task("t_2", "t", 2, 1, 1000) + # Feed monitor samples for t_1 + for i in range(5): + rm.add_monitored(0, i * 1.0, cpu_fraction=1.5, mem_mb=800.0) + rm.resources[0].nice_value = rm.nice_default + rm.book(0, rm.nice_default) + rm.unbook(0) # this triggers sampling + propagation + # t_2 should now have an adjusted assignment based on observed sample + assert rm.resources[1].cpu_assigned > 0 + # The sampled CPU was ~1.5; t_2's cpu_assigned should reflect that ballpark + assert rm.resources[1].mem_assigned == pytest.approx(800.0, abs=1) + + +def test_a_zero_cpu_sample_leaves_siblings_alone(): + """A task seen only through its psutil baseline reads 0.0 cores. Passing + that on would make every sibling look free and admit them all at once.""" + manager = ResourceManager(cpu_limit=8, mem_limit=16000, dynamic_resources=True) + manager.add_task("t_1", "t", 2, 1, 1000) + manager.add_task("t_2", "t", 2, 1, 1000) + for i in range(5): + manager.add_monitored(0, i * 1.0, cpu_fraction=0.0, mem_mb=800.0) + manager.resources[0].nice_value = manager.nice_default + manager.book(0, manager.nice_default) + manager.unbook(0) + assert manager.resources[1].cpu_assigned == pytest.approx(2.0) + + +def test_at_proc_cap(): + rm = _make_rm() + rm.procs_parallel_max = 2 + assert not rm.at_proc_cap() + for i in range(2): + rm.add_task(f"t{i}", None, 1, 1, 100) + rm.resources[i].nice_value = rm.nice_default + rm.book(i, rm.nice_default) + assert rm.at_proc_cap() diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_scheduler.py b/MC/workflow_runner/o2dpg_runner/tests/test_scheduler.py new file mode 100644 index 000000000..720f87ae4 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/test_scheduler.py @@ -0,0 +1,179 @@ +import pytest + +from o2dpg_runner.resources import ResourceManager +from o2dpg_runner.scheduler import ( + TimeframeFirstPolicy, CriticalPathPolicy, BestFitBackfillPolicy, get_policy, +) +from o2dpg_runner.scheduler.base import SchedulerState + + +def _setup(n=4, cpu_limit=8.0, mem_limit=16000.0): + """Create an RM with n tasks: all 2 cpu / 1000 mem.""" + rm = ResourceManager(cpu_limit=cpu_limit, mem_limit=mem_limit, n_backfill_max=1) + for i in range(n): + rm.add_task(f"t{i}", None, cpu=2, cpu_relative=1, mem=1000) + return rm + + +def _make_state(n, tf=None, desc=None, cp=None): + tf = tf if tf is not None else [0] * n + desc = desc if desc is not None else [0] * n + cp = cp if cp is not None else [1.0] * n + return SchedulerState( + timeframe_of=tf, + descendants_count=desc, + critical_path=cp, + task_cpu=[2.0] * n, + task_mem=[1000.0] * n, + timeframe_weight=[(tf[i], desc[i]) for i in range(n)], + ) + + +def test_timeframe_policy_order(): + s = _make_state(4, tf=[1, 0, 1, 0], desc=[5, 2, 10, 1]) + p = TimeframeFirstPolicy() + # timeframe 0 first, then within tf more descendants first + order = p.order([0, 1, 2, 3], s) + # tf 0: tasks 1 (desc=2) and 3 (desc=1) -> 1 before 3 + # tf 1: tasks 0 (desc=5) and 2 (desc=10) -> 2 before 0 + assert order == [1, 3, 2, 0] + + +def test_timeframe_policy_submit_default_fits_all(): + rm = _setup(n=4) # 4 * 2cpu = 8 = cpu_limit + s = _make_state(4) + p = TimeframeFirstPolicy() + picked = _drain(p.pick_submittable([0, 1, 2, 3], rm), rm) + assert len(picked) == 4 + assert all(nice == rm.nice_default for _, nice in picked) + + +def _drain(picks, rm): + """Drain a pick_submittable generator, booking each pick as the + executor would (so subsequent fits_default checks see it).""" + out = [] + for tid, nice in picks: + rm.book(tid, nice) + out.append((tid, nice)) + return out + + +def test_timeframe_policy_should_break_legacy(): + """Prototype behavior: the first non-fitting task breaks the default + pass entirely; following tasks only get a shot in the backfill pass + (which itself may be blocked once an earlier task consumed its budget).""" + rm = ResourceManager(cpu_limit=4, mem_limit=16000, n_backfill_max=4) + rm.add_task("small1", None, cpu=2, cpu_relative=1, mem=100) + rm.add_task("big", None, cpu=3, cpu_relative=1, mem=100) + rm.add_task("small2", None, cpu=2, cpu_relative=1, mem=100) + p = TimeframeFirstPolicy(drop_should_break=False) + picks = _drain(p.pick_submittable([0, 1, 2], rm), rm) + nice_for = {tid: n for tid, n in picks} + # small1 fits default + assert nice_for[0] == rm.nice_default + # small2, which WOULD have fit default (2+2=4), did not go default -- + # that's the legacy bug/feature. It's either backfill or not scheduled. + assert nice_for.get(2) != rm.nice_default + + +def test_timeframe_policy_drop_unblocks_small2_at_default(tmp_path=None): + """Contrast: with drop_should_break=True, small2 gets default priority.""" + rm = ResourceManager(cpu_limit=4, mem_limit=16000, n_backfill_max=4) + rm.add_task("small1", None, cpu=2, cpu_relative=1, mem=100) + rm.add_task("big", None, cpu=3, cpu_relative=1, mem=100) + rm.add_task("small2", None, cpu=2, cpu_relative=1, mem=100) + p = TimeframeFirstPolicy(drop_should_break=True) + picks = _drain(p.pick_submittable([0, 1, 2], rm), rm) + nice_for = {tid: n for tid, n in picks} + # small1 and small2 both at default -- the poster's headline result + assert nice_for[0] == rm.nice_default + assert nice_for[2] == rm.nice_default + + +def test_timeframe_policy_drop_should_break(): + """With drop_should_break=True, light tasks can slip past heavy ones + in the default pass.""" + rm = ResourceManager(cpu_limit=4, mem_limit=16000, n_backfill_max=4) + rm.add_task("small1", None, cpu=2, cpu_relative=1, mem=100) + rm.add_task("big", None, cpu=3, cpu_relative=1, mem=100) + rm.add_task("small2", None, cpu=2, cpu_relative=1, mem=100) + s = _make_state(3) + p = TimeframeFirstPolicy(drop_should_break=True) + picks = _drain(p.pick_submittable([0, 1, 2], rm), rm) + nice_for = {tid: n for tid, n in picks} + # small1 fits default; big doesn't (2+3=5>4); small2 fits default (2+2=4) + assert nice_for.get(0) == rm.nice_default + assert nice_for.get(2) == rm.nice_default + + +def test_critical_path_policy_order(): + # node 2 has the highest critical path -> should come first + s = _make_state(3, cp=[5.0, 3.0, 10.0]) + p = CriticalPathPolicy() + assert p.order([0, 1, 2], s) == [2, 0, 1] + + +def test_best_fit_policy_fills_budget(): + # cpu_limit=10; tasks: 3,3,4 -> best-fit should pack all three + rm = ResourceManager(cpu_limit=10, mem_limit=16000, n_backfill_max=1) + rm.add_task("a", None, cpu=3, cpu_relative=1, mem=100) + rm.add_task("b", None, cpu=3, cpu_relative=1, mem=100) + rm.add_task("c", None, cpu=4, cpu_relative=1, mem=100) + s = _make_state(3, cp=[1.0, 1.0, 1.0], desc=[0, 0, 0]) + p = BestFitBackfillPolicy() + ordered = p.order([0, 1, 2], s) + picks = _drain(p.pick_submittable(ordered, rm), rm) + assert len(picks) == 3 + assert all(n == rm.nice_default for _, n in picks) + # verify that with overfilled budget, best-fit DOES stop + rm2 = ResourceManager(cpu_limit=5, mem_limit=16000, n_backfill_max=0) + rm2.add_task("a", None, cpu=3, cpu_relative=1, mem=100) + rm2.add_task("b", None, cpu=3, cpu_relative=1, mem=100) + ordered2 = p.order([0, 1], _make_state(2)) + picks2 = _drain(p.pick_submittable(ordered2, rm2), rm2) + assert len(picks2) == 1 # only one fits the cpu=5 budget + + +def test_get_policy_names(): + assert isinstance(get_policy("timeframe"), TimeframeFirstPolicy) + assert isinstance(get_policy("critical-path"), CriticalPathPolicy) + assert isinstance(get_policy("best-fit"), BestFitBackfillPolicy) + with pytest.raises(ValueError): + get_policy("nonsense") + + +def test_semaphore_prevents_concurrent_submit(): + rm = ResourceManager(cpu_limit=8, mem_limit=16000, n_backfill_max=1) + rm.add_task("a", None, 1, 1, 100, semaphore_string="S") + rm.add_task("b", None, 1, 1, 100, semaphore_string="S") + s = _make_state(2) + # book a directly + rm.resources[0].nice_value = rm.nice_default + rm.book(0, rm.nice_default) + p = TimeframeFirstPolicy() + picks = list(p.pick_submittable([1], rm)) + assert picks == [] + + +def test_n_backfill_cap(): + rm = ResourceManager(cpu_limit=2, mem_limit=16000, n_backfill_max=1) + rm.add_task("a", None, 1, 1, 100) + rm.add_task("b", None, 1, 1, 100) + rm.add_task("c", None, 1, 1, 100) + # fill default with a+b + for i in (0, 1): + rm.resources[i].nice_value = rm.nice_default + rm.book(i, rm.nice_default) + assert rm.cpu_booked == 2 + # c doesn't fit default; backfill allowed? n_backfill_max=1 + p = TimeframeFirstPolicy() + picks = list(p.pick_submittable([2], rm)) + # one backfill slot, c gets it + assert len(picks) == 1 + assert picks[0][1] == rm.nice_backfill + rm.resources[2].nice_value = rm.nice_backfill + rm.book(2, rm.nice_backfill) + # next candidate tries to backfill but cap reached + rm.add_task("d", None, 1, 1, 100) + picks2 = list(p.pick_submittable([3], rm)) + assert picks2 == [] diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_simulator.py b/MC/workflow_runner/o2dpg_runner/tests/test_simulator.py new file mode 100644 index 000000000..727bb394f --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/test_simulator.py @@ -0,0 +1,131 @@ +import os +import sys + +import pytest + +from o2dpg_runner.workflow import build_workflow + +BIN_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if BIN_DIR not in sys.path: + sys.path.insert(0, BIN_DIR) + +import o2dpg_schedule_simulator as sim + + +def _wf(stages): + return build_workflow({"stages": stages}, ["*"], []) + + +def _task(name, cpu, mem=100.0, walltime=1.0, needs=None, timeframe=1): + return { + "name": name, + "needs": list(needs or []), + "cmd": "true", + "cwd": ".", + "timeframe": timeframe, + "labels": [], + "resources": {"cpu": cpu, "mem": mem, "walltime": walltime}, + } + + +def test_simulator_uses_amdahl_override_in_critical_path_state(): + wf = _wf([ + _task("a", cpu=1, walltime=1.0), + _task("b", cpu=1, walltime=5.0), + ]) + model = sim.AmdahlModel( + t_serial=20.0, + t_parallel_tot=0.0, + n_ref=1, + cpu_mean_ref=1.0, + min_workers=1, + max_workers=1, + ) + result = sim.simulate( + wf, + "critical-path", + cpu_limit=8.0, + mem_limit=1000.0, + amdahl_models={"a": model}, + worker_assignment={"a": 1}, + maxjobs=1, + ) + assert result.tasks[0].name == "a" + + +def test_simulator_keeps_tid_mapping_when_task_exceeds_limits(): + wf = _wf([ + _task("too_big", cpu=20, walltime=3.0), + _task("ok", cpu=1, walltime=2.0), + ]) + result = sim.simulate( + wf, + "timeframe", + cpu_limit=4.0, + mem_limit=1000.0, + maxjobs=1, + ) + assert [t.name for t in result.tasks] == ["ok"] + assert result.deadlocked_tids == [0] + + +def test_simulator_backfill_slowdown_marks_and_slows_backfill_tasks(): + wf = _wf([ + _task("small1", cpu=2, walltime=3.0), + _task("big", cpu=3, walltime=8.0), + _task("small2", cpu=2, walltime=3.0), + ]) + result = sim.simulate( + wf, + "timeframe", + cpu_limit=4.0, + mem_limit=1000.0, + backfill_model="slowdown", + n_backfill=1, + backfill_slowdown_factor=1.25, + ) + by_name = {t.name: t for t in result.tasks} + assert "big" in by_name + assert by_name["big"].start == pytest.approx(0.0) + compute_wt = 8.0 * 1.25 + assert by_name["big"].walltime == pytest.approx(compute_wt + 0.1) + # cpu is the average over the whole slot, so the idle overhead dilutes it; + # cpu * walltime is the CPU-seconds actually spent + assert by_name["big"].cpu == pytest.approx( + 3.0 / 1.25 * compute_wt / (compute_wt + 0.1)) + assert by_name["big"].cpu_booked == pytest.approx(3.0) + assert result.cpu_utilization(4.0) <= 1.0 + + +def test_simulator_holefill_uses_only_foreground_hole(): + wf = _wf([ + _task("fg", cpu=2, walltime=4.0), + _task("bf", cpu=3, walltime=2.0), + ]) + result = sim.simulate( + wf, + "timeframe", + cpu_limit=4.0, + mem_limit=1000.0, + backfill_model="holefill", + n_backfill=1, + task_overhead=0.0, + ) + by_name = {t.name: t for t in result.tasks} + assert by_name["fg"].walltime == pytest.approx(4.0) + assert by_name["bf"].walltime == pytest.approx(3.0) + assert by_name["bf"].cpu == pytest.approx(2.0) + assert by_name["bf"].cpu_booked == pytest.approx(3.0) + assert result.cpu_utilization(4.0) == pytest.approx(0.875) + + +def test_amdahl_model_rejects_negative_components(): + with pytest.raises(ValueError): + sim.AmdahlModel.from_dict( + { + "t_serial": -1.0, + "t_parallel_tot": 5.0, + "n_ref": 4, + "cpu_mean_ref": 4.0, + } + ) diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_workflow.py b/MC/workflow_runner/o2dpg_runner/tests/test_workflow.py new file mode 100644 index 000000000..db912673b --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/tests/test_workflow.py @@ -0,0 +1,117 @@ +import json +import os + +import pytest + +from o2dpg_runner.workflow import ( + load_json, extract_global_init, filter_workflow, build_workflow, + update_resource_estimates, +) + +FIXTURE = os.path.join(os.path.dirname(__file__), "fixtures", "tiny_workflow.json") + + +def _load(): + return load_json(FIXTURE) + + +def test_extract_global_init(): + spec = _load() + env, cmd = extract_global_init(spec) + assert env == {"FOO": "bar"} + assert cmd is None # cmd was "NO-COMMAND" + # __global_init_task__ removed from stages + assert spec["stages"][0]["name"] != "__global_init_task__" + assert spec["stages"][0]["name"] == "bkg" + + +def test_filter_workflow_all(): + spec = _load() + extract_global_init(spec) + filtered, targets = filter_workflow(spec, ["*"], []) + assert targets == [] # "*" means no target list + assert len(filtered["stages"]) == len(spec["stages"]) + + +def test_filter_workflow_by_target(): + spec = _load() + extract_global_init(spec) + filtered, targets = filter_workflow(spec, ["digi_1"], []) + names = [t["name"] for t in filtered["stages"]] + # digi_1 + its needs bkg, sgnsim_1 + assert "digi_1" in names + assert "sgnsim_1" in names + assert "bkg" in names + assert "reco_1" not in names + assert "aod" not in names + assert targets == ["digi_1"] + + +def test_filter_workflow_by_label(): + spec = _load() + extract_global_init(spec) + filtered, targets = filter_workflow(spec, ["*"], ["QC"]) + # "*" with label filter narrows, and pulls in all deps + names = [t["name"] for t in filtered["stages"]] + assert "qc_1" in names + assert "qc_2" in names + # deps pulled in + for need in ("reco_1", "reco_2", "digi_1", "digi_2", "sgnsim_1", "sgnsim_2", "bkg"): + assert need in names + + +def test_filter_workflow_regex_target(): + spec = _load() + extract_global_init(spec) + filtered, targets = filter_workflow(spec, ["^qc_.*"], []) + names = [t["name"] for t in filtered["stages"]] + assert set(targets) == {"qc_1", "qc_2"} + assert "aod" not in names + + +def test_filter_does_not_alias_input(): + """Regression: the prototype did `transformedworkflowspec = workflowspec` + and then mutated .stages, aliasing the caller's dict. Ensure we don't.""" + spec = _load() + extract_global_init(spec) + original_len = len(spec["stages"]) + filtered, _ = filter_workflow(spec, ["qc_1"], []) + # filtered should be smaller, spec unchanged + assert len(filtered["stages"]) < original_len + assert len(spec["stages"]) == original_len + + +def test_build_workflow_end_to_end(): + raw = _load() + wf = build_workflow(raw, ["*"], []) + # 10 tasks after removing global init + assert wf.n_tasks() == 10 + # indegrees: bkg has 0; aod has 2 (reco_1, reco_2) + bkg_tid = wf.tid("bkg") + aod_tid = wf.tid("aod") + assert wf.indegree[bkg_tid] == 0 + assert wf.indegree[aod_tid] == 2 + # timeframes: -1, 1, 2 + assert wf.timeframes == {-1, 1, 2} + + +def test_update_resource_estimates(tmp_path): + raw = _load() + wf = build_workflow(raw, ["*"], []) + # synthesize a learned-estimates JSON keyed by "global" task name + est = { + "sgnsim": {"pss": {"max": 3000}, "cpu": {"mean": 3.5}}, + "digi": {"pss": {"max": 1200}, "cpu": {"mean": 1.8}}, + } + p = tmp_path / "res.json" + p.write_text(json.dumps(est)) + update_resource_estimates(wf, str(p)) + + for name in ("sgnsim_1", "sgnsim_2"): + t = wf.stages[wf.tid(name)] + assert t["resources"]["mem"] == 3000 + assert t["resources"]["cpu"] == 3.5 + for name in ("digi_1", "digi_2"): + t = wf.stages[wf.tid(name)] + assert t["resources"]["mem"] == 1200 + assert t["resources"]["cpu"] == 1.8 diff --git a/MC/workflow_runner/o2dpg_runner/workflow.py b/MC/workflow_runner/o2dpg_runner/workflow.py new file mode 100644 index 000000000..7c2322f8d --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/workflow.py @@ -0,0 +1,411 @@ +"""Workflow loading, filtering, and DAG construction. + +The workflow JSON schema is unchanged from the original. Key fields per +stage: + - name: str + - needs: list[str] (names of upstream stages) + - cmd: str + - cwd: str + - timeframe: int (-1 for global stages) + - labels: list[str] + - resources: {cpu, mem, relative_cpu} + - semaphore: str (optional) + - retry_count: int (optional) + - alternative_alienv_package: str (optional) + - env: dict (optional) + +One stage may be the synthetic ``__global_init_task__`` at index 0, +holding global env and an optional init cmd; it is stripped from the +DAG during loading. +""" + +from __future__ import annotations + +import copy +import json +import logging +import math +import re +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Set, Tuple + +from .graph import build_adjacency + +log = logging.getLogger(__name__) + + +@dataclass +class Workflow: + """In-memory representation of a workflow after filtering. + + Tasks are stored as a list of dicts (the raw JSON objects) and indexed + by integer ``tid``. The forward/reverse adjacency and derived quantities + are computed once at construction time. + """ + stages: List[Dict[str, Any]] + global_env: Dict[str, str] = field(default_factory=dict) + global_init_cmd: Optional[str] = None + full_target_names: List[str] = field(default_factory=list) + + # Derived + name_to_id: Dict[str, int] = field(default_factory=dict) + id_to_name: List[str] = field(default_factory=list) + forward_adj: List[List[int]] = field(default_factory=list) + reverse_adj: List[List[int]] = field(default_factory=list) + indegree: List[int] = field(default_factory=list) + timeframes: Set[int] = field(default_factory=set) + + def __post_init__(self): + self._rebuild_indices() + + def _rebuild_indices(self): + self.name_to_id = {s["name"]: i for i, s in enumerate(self.stages)} + self.id_to_name = [s["name"] for s in self.stages] + edges: List[Tuple[int, int]] = [] + for i, s in enumerate(self.stages): + for n in s.get("needs", []): + if n in self.name_to_id: + edges.append((self.name_to_id[n], i)) + self.forward_adj, self.reverse_adj, self.indegree = build_adjacency( + len(self.stages), edges + ) + self.timeframes = {s.get("timeframe", -1) for s in self.stages} + + def tid(self, name: str) -> int: + return self.name_to_id[name] + + def name(self, tid: int) -> str: + return self.id_to_name[tid] + + def n_tasks(self) -> int: + return len(self.stages) + + +def load_json(path: str) -> Dict[str, Any]: + with open(path) as fp: + return json.load(fp) + + +def extract_global_init(raw_spec: Dict[str, Any]) -> Tuple[Dict[str, str], Optional[str]]: + """Pull out the synthetic __global_init_task__ if present. + + Mutates raw_spec['stages'] in place (removes the init stage). + """ + env: Dict[str, str] = {} + init_cmd: Optional[str] = None + stages = raw_spec.get("stages", []) + if stages and stages[0].get("name") == "__global_init_task__": + init = stages[0] + env_in = init.get("env") + if env_in: + env = {k: str(v) for k, v in env_in.items()} + cmd = init.get("cmd") + if cmd and cmd != "NO-COMMAND": + init_cmd = cmd + del stages[0] + return env, init_cmd + + +def filter_workflow( + raw_spec: Dict[str, Any], + targets: List[str], + target_labels: List[str], +) -> Tuple[Dict[str, Any], List[str]]: + """Filter the raw spec down to tasks matching target selectors. + + Returns (new_spec, full_target_names). When no filter is requested, + returns (raw_spec, []). The returned spec is always a fresh top-level + dict (no aliasing bug like in the prototype), but the per-stage dicts + are shared. + """ + stages = raw_spec.get("stages", []) + if not targets: + return {**raw_spec, "stages": list(stages)}, [] + if not target_labels and len(targets) == 1 and targets[0] == "*": + return {**raw_spec, "stages": list(stages)}, [] + + name_to_idx = {t["name"]: i for i, t in enumerate(stages)} + + def task_matches(name: str) -> bool: + for f in targets: + if f == "*": + return True + if re.match(f, name) is not None: + return True + return False + + def task_matches_labels(t: Dict[str, Any]) -> bool: + if not target_labels: + return True + for lbl in t.get("labels", []): + if lbl in target_labels: + return True + return False + + # Memoized canBeDone using iterative traversal. + ok_cache: Dict[str, bool] = {} + + def can_be_done(name: str) -> bool: + if name in ok_cache: + return ok_cache[name] + idx = name_to_idx.get(name) + if idx is None: + ok_cache[name] = False + return False + # iterative post-order DFS + order: List[str] = [] + seen: Set[str] = {name} + stack: List[Tuple[str, int]] = [(name, 0)] + while stack: + cur, ci = stack[-1] + needs = stages[name_to_idx[cur]].get("needs", []) if cur in name_to_idx else [] + if ci < len(needs): + stack[-1] = (cur, ci + 1) + child = needs[ci] + if child not in seen and child not in ok_cache: + if child not in name_to_idx: + ok_cache[child] = False + else: + seen.add(child) + stack.append((child, 0)) + else: + stack.pop() + order.append(cur) + for cur in order: + if cur in ok_cache: + continue + idx2 = name_to_idx.get(cur) + if idx2 is None: + ok_cache[cur] = False + continue + ok = all(ok_cache.get(r, False) for r in stages[idx2].get("needs", [])) + ok_cache[cur] = ok + if not ok: + log.info("Disabling target %s due to unsatisfied requirements", cur) + return ok_cache[name] + + full_target_list = [ + t for t in stages + if task_matches(t["name"]) and task_matches_labels(t) and can_be_done(t["name"]) + ] + full_target_names = [t["name"] for t in full_target_list] + + # Collect all upstream requirements (iterative, deduped). + needed: Set[str] = set(full_target_names) + stack2 = list(full_target_names) + while stack2: + cur = stack2.pop() + idx = name_to_idx.get(cur) + if idx is None: + continue + for r in stages[idx].get("needs", []): + if r not in needed: + needed.add(r) + stack2.append(r) + + new_stages = [t for t in stages if t["name"] in needed] + new_spec = {**raw_spec, "stages": new_stages} + return new_spec, full_target_names + + +def build_workflow( + raw_spec: Dict[str, Any], + targets: List[str], + target_labels: List[str], +) -> Workflow: + """End-to-end: strip global-init, filter, build DAG.""" + # Operate on a shallow copy at top level so we don't mutate caller's dict. + spec = {**raw_spec, "stages": list(raw_spec.get("stages", []))} + # extract_global_init mutates spec['stages'] + env, init_cmd = extract_global_init(spec) + filtered, target_names = filter_workflow(spec, targets, target_labels) + wf = Workflow( + stages=filtered["stages"], + global_env=env, + global_init_cmd=init_cmd, + full_target_names=target_names, + ) + return wf + + +def update_resource_estimates( + workflow: Workflow, + resource_json_path: str, + logger=None, +) -> None: + """Apply learned resource estimates from a JSON file. + + The JSON is produced by o2dpg_sim_metrics.py json-stat and is keyed on + the "global" task name (i.e. with the _ suffix stripped). + + MEM is taken from pss.max (peak proportional set size). + CPU is taken from cpu.mean (average cores used during the task). + + Note on relative_cpu: the workflow JSON carries a relative_cpu field + that historically scaled a "max" CPU estimate down to an "expected" + usage. When injecting *measured* cpu.mean values that scaling must NOT + be applied again — the measurement already reflects actual usage. + relative_cpu remains untouched and continues to be used by the dynamic- + resources sampler (resources.py) for sibling reassignment, which is + correct behaviour: the sampler scales a freshly-observed aggregate back + to an expected per-task assignment. + """ + _log = logger if logger is not None else log + _log.info("Applying learned resource estimates from: %s", resource_json_path) + + with open(resource_json_path) as fp: + resource_dict = json.load(fp) + + # Remove the metadata key so task lookup doesn't match it. + resource_dict.pop("count", None) + + n_stages = len(workflow.stages) + n_updated = 0 + missing_base_names: set = set() + + for task in workflow.stages: + tf = task.get("timeframe", -1) + name = task["name"] + global_name = "_".join(name.split("_")[:-1]) if tf >= 1 else name + + if global_name not in resource_dict: + missing_base_names.add(global_name) + continue + + new_res = resource_dict[global_name] + task_updated = False + + walltime = new_res.get("lifetime", {}).get("mean") + if walltime is not None: + # Store even when walltime=0 (sub-10ms tasks that GNU time rounds + # to zero). The simulator clamps to a 1ms minimum so zero is + # handled gracefully; the fallback (cpu * factor) is always worse. + task["resources"]["walltime"] = float(walltime) + _log.info(" WALLTIME %-40s %.3f s", name, float(walltime)) + task_updated = True + + new_mem = new_res.get("pss", {}).get("max") + if new_mem is not None: + old_mem = task["resources"]["mem"] + task["resources"]["mem"] = new_mem + _log.info(" MEM %-40s %.1f MB -> %.1f MB", name, float(old_mem), new_mem) + task_updated = True + + new_cpu = new_res.get("cpu", {}).get("mean") + if new_cpu is not None: + old_cpu = task["resources"]["cpu"] + uses_dynamic_workers = "O2DPG_DYNAMIC_NWORKER_OVERWRITE" in task.get("cmd", "") + + if uses_dynamic_workers: + # Round cpu.mean to the nearest integer worker count and use + # that value for BOTH the scheduler's cpu booking and the + # actual NWORKERS setting. This keeps them consistent: the + # task will run with n_workers processes each using ~1 core, + # so total cpu ≈ n_workers = what we book. + n_workers = max(1, round(new_cpu)) + task["resources"]["cpu"] = float(n_workers) + if not isinstance(task.get("env"), dict): + task["env"] = {} + task["env"]["O2DPG_DYNAMIC_NWORKER_OVERWRITE"] = str(n_workers) + _log.info( + " CPU+NWORKERS %-36s cpu.mean=%.2f -> %d workers, %.0f cores booked", + name, new_cpu, n_workers, float(n_workers), + ) + else: + # No dynamic worker override: book cpu.mean directly. + # Do NOT apply relative_cpu scaling — the measurement already + # reflects actual usage. + task["resources"]["cpu"] = new_cpu + _log.info(" CPU %-40s %.3f cores -> %.3f cores", + name, float(old_cpu), new_cpu) + task_updated = True + + if task_updated: + n_updated += 1 + + if missing_base_names: + _log.info(" No learned data for: %s", ", ".join(sorted(missing_base_names))) + _log.info( + "Resource update done: %d/%d task stages updated (%d base name(s) not in learned data).", + n_updated, n_stages, len(missing_base_names), + ) + + +def replicate_workflow_for_timeframes(raw_spec: Dict[str, Any], M: int) -> Dict[str, Any]: + """Return a synthetic M-timeframe workflow derived from *raw_spec*. + + The original workflow may have N timeframes. This function: + 1. Detects per-TF stages (``timeframe >= 1``) and uses the + lowest-numbered timeframe as the canonical template. + 2. Instantiates the template for TF=1..M. + 3. Updates global stage dependencies so they reference exactly TF=1..M. + + Works for both M < N (shrink) and M > N (expand). + The returned dict shares no mutable state with *raw_spec*. + """ + stages = raw_spec.get("stages", []) + per_tf = [s for s in stages if s.get("timeframe", -1) >= 1] + global_stgs = [s for s in stages if s.get("timeframe", -1) < 1] + + if not per_tf: + return raw_spec # no per-TF template structure detected + + original_tf_set = {s["timeframe"] for s in per_tf} + min_tf = min(original_tf_set) + template_stages = [s for s in per_tf if s.get("timeframe") == min_tf] + template_names = {s["name"] for s in template_stages} + all_per_tf_names = {s["name"] for s in per_tf} + + def _base(name: str, tf: int) -> str: + sfx = f"_{tf}" + return name[:-len(sfx)] if name.endswith(sfx) else name + + # Replicate per-TF tasks for i = 1 .. M. + new_per_tf: List[Dict[str, Any]] = [] + for i in range(1, M + 1): + for tmpl in template_stages: + s = copy.deepcopy(tmpl) + base = _base(s["name"], min_tf) + s["name"] = f"{base}_{i}" + s["timeframe"] = i + new_needs: List[str] = [] + for need in tmpl.get("needs", []): + if need in template_names: + new_needs.append(f"{_base(need, min_tf)}_{i}") + else: + new_needs.append(need) + s["needs"] = new_needs + new_per_tf.append(s) + + # Update global stages: replace all per-TF deps with the full 1..M set, + # expanding each unique base name exactly once (deduplicates cross-TF refs). + new_global: List[Dict[str, Any]] = [] + for gstage in global_stgs: + s = copy.deepcopy(gstage) + new_needs_g: List[str] = [] + seen: Set[str] = set() + expanded_bases: Set[str] = set() + for need in gstage.get("needs", []): + if need not in all_per_tf_names: + if need not in seen: + new_needs_g.append(need) + seen.add(need) + continue + # Identify the base name (strip whichever TF suffix this entry has). + base = need + for otf in original_tf_set: + if need.endswith(f"_{otf}"): + base = need[:-len(f"_{otf}")] + break + if base in expanded_bases: + continue + expanded_bases.add(base) + for j in range(1, M + 1): + n = f"{base}_{j}" + if n not in seen: + new_needs_g.append(n) + seen.add(n) + s["needs"] = new_needs_g + new_global.append(s) + + return {**raw_spec, "stages": new_per_tf + new_global} diff --git a/MC/workflow_runner/o2dpg_schedule_simulator.py b/MC/workflow_runner/o2dpg_schedule_simulator.py new file mode 100755 index 000000000..bb9e80819 --- /dev/null +++ b/MC/workflow_runner/o2dpg_schedule_simulator.py @@ -0,0 +1,1268 @@ +#!/usr/bin/env python3 +"""Discrete-event simulator for the O2DPG workflow scheduler. + +Loads a workflow (and optionally applies learned resources), then +simulates scheduling under one or more policies in microseconds — +no processes are spawned. + +Assumptions (matching a kernel-enforced hard CPU limit): + - No nice / backfill tier: all tasks are submitted at the default + nice level against a single hard CPU budget. + - Memory is also a hard limit. + - Tasks run for exactly their `resources.walltime` seconds (set by + --update-resources). If walltime is absent, cpu * --walltime-per-core + is used as a proxy. + - Task parallelism is limited only by cpu_limit and mem_limit, not + by --maxjobs (the simulator sets the process cap to infinity). + +Usage examples +-------------- +Compare all three policies with learned resources: + + o2dpg_schedule_simulator.py \\ + -f workflow.json \\ + --update-resources learned.json \\ + --cpu-limit 8 --mem-limit 16384 \\ + --policies timeframe critical-path best-fit + +Single policy, verbose per-task schedule: + + o2dpg_schedule_simulator.py \\ + -f workflow.json --update-resources learned.json \\ + --cpu-limit 8 --mem-limit 16384 \\ + --policies critical-path --verbose + +JSON output for downstream analysis: + + o2dpg_schedule_simulator.py ... --output sim.json +""" + +from __future__ import annotations + +import argparse +import copy +import json +import math +import os +import random +import statistics +import sys +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Set, Tuple + +_here = os.path.dirname(os.path.abspath(__file__)) +if _here not in sys.path: + sys.path.insert(0, _here) + +from o2dpg_runner.workflow import ( + build_workflow, + load_json, + replicate_workflow_for_timeframes, + update_resource_estimates, +) +from o2dpg_runner.resources import ResourceManager, ResourceLimitExceeded +from o2dpg_runner.scheduler import get_policy +from o2dpg_runner.scheduler.base import SchedulerState +from o2dpg_runner.scheduler.timeframe import TimeframeFirstPolicy +from o2dpg_runner.graph import descendants, longest_path_length, kahn_topological_order + + +# --------------------------------------------------------------------------- +# Data classes +# --------------------------------------------------------------------------- + +@dataclass +class SimTask: + tid: int + name: str + start: float # wall seconds from t=0 + finish: float + cpu: float # effective average cores consumed over [start, finish] + cpu_booked: float # cores booked for scheduler admission + mem: float # MB booked + walltime: float # finish - start + + +@dataclass +class _RunningBackfill: + task: SimTask + nominal_work: float + remaining_work: float + launch_seq: int + overhead_until: Optional[float] = None + + +@dataclass +class SimResult: + policy: str + makespan: float # total wall seconds + tasks: List[SimTask] = field(default_factory=list) + deadlocked_tids: List[int] = field(default_factory=list) + + def cpu_utilization(self, cpu_limit: float) -> float: + """Mean CPU utilisation as a fraction of cpu_limit.""" + if self.makespan <= 0 or cpu_limit <= 0: + return 0.0 + total = sum(t.cpu * t.walltime for t in self.tasks) + return total / (self.makespan * cpu_limit) + + def peak_mem_mb(self) -> float: + """Peak concurrent memory usage in MB (sweep-line).""" + events: List[Tuple[float, float]] = [] + for t in self.tasks: + events.append((t.start, +t.mem)) + events.append((t.finish, -t.mem)) + events.sort() + peak = cur = 0.0 + for _, delta in events: + cur += delta + peak = max(peak, cur) + return peak + + def to_dict(self) -> dict: + return { + "policy": self.policy, + "makespan_s": round(self.makespan, 3), + "tasks": [ + { + "tid": t.tid, "name": t.name, + "start": round(t.start, 3), "finish": round(t.finish, 3), + "cpu": round(t.cpu, 3), "cpu_booked": round(t.cpu_booked, 3), + "mem": round(t.mem, 1), + "walltime": round(t.walltime, 3), + } + for t in sorted(self.tasks, key=lambda x: x.start) + ], + "deadlocked_tids": self.deadlocked_tids, + } + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _global_name(name: str) -> str: + toks = name.split("_") + if toks and toks[-1].isdigit() and len(toks) > 1: + return "_".join(toks[:-1]) + return name + + +def _task_walltime(task: dict, cpu_fallback_factor: float) -> float: + """Return walltime [s] for a task, falling back to cpu * factor.""" + wt = task.get("resources", {}).get("walltime") + if wt is not None: + try: + return max(1e-3, float(wt)) + except (TypeError, ValueError): + pass + cpu = float(task.get("resources", {}).get("cpu", 1.0)) + return max(1e-3, cpu * cpu_fallback_factor) + + +_LEARN_ALL: Set[str] = frozenset({"cpu", "mem", "lifetime"}) + + +def _apply_learned_fields(workflow, learned: Dict, fields: Set[str]) -> int: + """Patch workflow stages in-place with a subset of learned resource fields. + + *fields* is a subset of ``{"cpu", "mem", "lifetime"}``. Only the named + dimensions are written; the rest keep the values from workflow.json. + Returns the count of stages that received at least one update. + """ + n_updated = 0 + for task in workflow.stages: + tf = task.get("timeframe", -1) + name = task["name"] + gname = "_".join(name.split("_")[:-1]) if tf >= 1 else name + data = learned.get(gname) + if not isinstance(data, dict): + continue + updated = False + if "lifetime" in fields: + wt = data.get("lifetime", {}).get("mean") + if wt is not None: + task["resources"]["walltime"] = float(wt) + updated = True + if "mem" in fields: + mem = data.get("pss", {}).get("max") + if mem is not None: + task["resources"]["mem"] = float(mem) + updated = True + if "cpu" in fields: + cpu = data.get("cpu", {}).get("mean") + if cpu is not None: + task["resources"]["cpu"] = float(cpu) + updated = True + if updated: + n_updated += 1 + return n_updated + + +@dataclass +class AmdahlModel: + """Amdahl scaling model derived from a single measurement point. + + walltime(n) = t_serial + t_parallel_tot / n + + t_serial and t_parallel_tot are solved from: + walltime_ref = t_serial + t_parallel_tot / n_ref + cpu_mean_ref = (t_serial + t_parallel_tot) / walltime_ref + """ + t_serial: float + t_parallel_tot: float + n_ref: int + cpu_mean_ref: float + min_workers: int = 1 + max_workers: int = 1 + + def walltime(self, n: int) -> float: + return max(1e-3, self.t_serial + self.t_parallel_tot / max(1, n)) + + @property + def worker_range(self) -> List[int]: + return list(range(self.min_workers, self.max_workers + 1)) + + @classmethod + def from_dict(cls, d: dict) -> "AmdahlModel": + model = cls( + t_serial=float(d["t_serial"]), + t_parallel_tot=float(d["t_parallel_tot"]), + n_ref=int(d["n_ref"]), + cpu_mean_ref=float(d["cpu_mean_ref"]), + min_workers=int(d.get("min_workers", 1)), + max_workers=int(d.get("max_workers", d["n_ref"])), + ) + if model.t_serial < 0 or model.t_parallel_tot < 0: + raise ValueError("Amdahl model has negative serial/parallel component") + if model.n_ref < 1 or model.min_workers < 1 or model.max_workers < model.min_workers: + raise ValueError("Amdahl model has invalid worker bounds") + return model + + +def _sample_walltime(mean: float, std: float, rng: random.Random) -> float: + """Draw a walltime sample from a log-normal distribution. + + Log-normal is a natural model for walltime: always positive, right-skewed + (occasional slow outliers). When std=0 the mean is returned unchanged. + """ + if std <= 1e-9 or mean <= 1e-9: + return max(1e-3, mean) + cv = std / mean + sigma2 = math.log(1.0 + cv * cv) + mu = math.log(mean) - 0.5 * sigma2 + return max(1e-3, rng.lognormvariate(mu, math.sqrt(sigma2))) + + +def _build_rm( + workflow, + cpu_limit: float, + mem_limit: float, + cpu_overrides: Optional[Dict[int, float]] = None, + n_backfill_max: int = 0, + backfill_cpu_factor: float = 1.5, + backfill_mem_factor: float = 1.5, + maxjobs: int = 10_000, +) -> Tuple[ResourceManager, Set[int]]: + """Fresh ResourceManager with no backfill tier and unlimited job slots. + + *cpu_overrides* maps tid → cpu to override resources.cpu for specific + tasks (used by the worker-count optimizer). + """ + rm = ResourceManager( + cpu_limit=cpu_limit, + mem_limit=mem_limit, + procs_parallel_max=maxjobs, + n_backfill_max=n_backfill_max, + backfill_cpu_factor=backfill_cpu_factor, + backfill_mem_factor=backfill_mem_factor, + dynamic_resources=False, + optimistic_resources=True, + ) + impossible_tids: Set[int] = set() + for i, task in enumerate(workflow.stages): + rel = None + try: + rv = task["resources"].get("relative_cpu") + rel = float(rv) if rv is not None else None + except (TypeError, ValueError): + pass + cpu = float(task["resources"]["cpu"]) + if cpu_overrides and i in cpu_overrides: + cpu = cpu_overrides[i] + mem = float(task["resources"]["mem"]) + if cpu > cpu_limit or mem > mem_limit: + impossible_tids.add(i) + try: + rm.add_task( + name=task["name"], + related_name=_global_name(task["name"]), + cpu=cpu, + cpu_relative=rel, + mem=mem, + semaphore_string=task.get("semaphore"), + ) + except ResourceLimitExceeded as e: + print(f" WARNING: task {task['name']} exceeds limits and will never run: {e}", + file=sys.stderr) + if impossible_tids: + names = [workflow.stages[i]["name"] for i in sorted(impossible_tids)] + print( + " WARNING: tasks exceed the hard simulator limits and will remain unscheduled: " + f"{names[:5]}{'...' if len(names) > 5 else ''}", + file=sys.stderr, + ) + return rm, impossible_tids + + +def _build_state( + workflow, + cpu_fallback_factor: float, + cpu_overrides: Optional[Dict[int, float]] = None, + walltime_overrides: Optional[Dict[int, float]] = None, +) -> SchedulerState: + n = workflow.n_tasks() + desc_cache: Dict = {} + desc_counts = [len(descendants(workflow.forward_adj, tid, desc_cache)) + for tid in range(n)] + + timeframe_of = [t.get("timeframe", -1) for t in workflow.stages] + tf_weight = [(timeframe_of[i], desc_counts[i]) for i in range(n)] + + cpu = [float(t.get("resources", {}).get("cpu", 1.0)) for t in workflow.stages] + if cpu_overrides: + for tid, value in cpu_overrides.items(): + cpu[tid] = float(value) + mem = [float(t.get("resources", {}).get("mem", 0.0)) for t in workflow.stages] + walltime = [_task_walltime(t, cpu_fallback_factor) for t in workflow.stages] + if walltime_overrides: + for tid, value in walltime_overrides.items(): + walltime[tid] = float(value) + + has_walltime = any(t.get("resources", {}).get("walltime") for t in workflow.stages) + cp_weight = walltime if has_walltime else cpu + topo = kahn_topological_order(n, workflow.forward_adj, workflow.indegree) + cp = longest_path_length(workflow.forward_adj, topo, cp_weight) + + return SchedulerState( + timeframe_of=timeframe_of, + descendants_count=desc_counts, + critical_path=cp, + task_cpu=cpu, + task_mem=mem, + task_walltime=walltime, + timeframe_weight=tf_weight, + ) + + +# --------------------------------------------------------------------------- +# Core simulation +# --------------------------------------------------------------------------- + +def simulate( + workflow, + policy_name: str, + cpu_limit: float, + mem_limit: float, + cpu_fallback_factor: float = 10.0, + task_overhead: float = 0.1, + walltime_stds: Optional[Dict[str, float]] = None, + cv_fallback: float = 0.15, + rng: Optional[random.Random] = None, + amdahl_models: Optional[Dict[str, "AmdahlModel"]] = None, + worker_assignment: Optional[Dict[str, int]] = None, + backfill_model: str = "off", + n_backfill: int = 1, + backfill_cpu_factor: float = 1.5, + backfill_mem_factor: float = 1.5, + backfill_slowdown_factor: float = 1.15, + maxjobs: int = 10_000, +) -> SimResult: + """Run one discrete-event simulation; return SimResult. + + When *rng* is provided, each task's walltime is sampled from + log-normal(mean, std). The std is taken from *walltime_stds* when + available; otherwise *cv_fallback* × mean is used as a noise floor. + + When *amdahl_models* and *worker_assignment* are both provided, walltime + and cpu booking for scalable tasks are derived from the Amdahl model at + the assigned worker count rather than from the workflow resources. + """ + mean_walltimes = [_task_walltime(t, cpu_fallback_factor) for t in workflow.stages] + + # Apply Amdahl model overrides for scalable tasks. + cpu_overrides: Dict[int, float] = {} + walltime_overrides: Dict[int, float] = {} + if amdahl_models and worker_assignment: + for i, task in enumerate(workflow.stages): + base = _global_name(task["name"]) + model = amdahl_models.get(base) + n = worker_assignment.get(base) + if model is not None and n is not None: + mean_walltimes[i] = model.walltime(n) + walltime_overrides[i] = mean_walltimes[i] + cpu_overrides[i] = float(n) + + state = _build_state( + workflow, + cpu_fallback_factor, + cpu_overrides=cpu_overrides or None, + walltime_overrides=walltime_overrides or None, + ) + + # Sample walltimes for this simulation run. + if rng is not None: + walltimes = [] + for i, task in enumerate(workflow.stages): + mean_wt = mean_walltimes[i] + base = _global_name(task["name"]) + std_wt = (walltime_stds or {}).get(base, 0.0) + if std_wt <= 0 and cv_fallback > 0: + std_wt = mean_wt * cv_fallback + walltimes.append(_sample_walltime(mean_wt, std_wt, rng)) + else: + walltimes = mean_walltimes + + if policy_name == "timeframe": + policy = TimeframeFirstPolicy(drop_should_break=False) + else: + policy = get_policy(policy_name) + + use_backfill = backfill_model in ("structural", "slowdown", "holefill") + rm, impossible_tids = _build_rm( + workflow, + cpu_limit, + mem_limit, + cpu_overrides=cpu_overrides or None, + n_backfill_max=n_backfill if use_backfill else 0, + backfill_cpu_factor=backfill_cpu_factor, + backfill_mem_factor=backfill_mem_factor, + maxjobs=maxjobs, + ) + + n = workflow.n_tasks() + proc_status = ["ToDo"] * n + candidates: List[int] = [ + i for i in range(n) if workflow.indegree[i] == 0 and i not in impossible_tids + ] + finished: Set[int] = set() + running: List[Tuple[int, float]] = [] # (tid, finish_time) + result = SimResult(policy=policy_name, makespan=0.0) + t = 0.0 + + if backfill_model == "holefill": + running_fg: List[Tuple[int, float]] = [] + running_bf: Dict[int, _RunningBackfill] = {} + launch_seq = 0 + + for _guard in range(n * n + 1): + ordered = policy.order(candidates, state) + for tid, nice in policy.pick_submittable(ordered, rm): + if nice != rm.nice_default and rm.cpu_free_default() <= 1e-9: + continue + rm.book(tid, nice) + candidates.remove(tid) + proc_status[tid] = "Running" + res = rm.resources[tid] + if nice == rm.nice_default: + compute_wt = walltimes[tid] + wt = compute_wt + task_overhead + finish = t + wt + fg_cpu = res.cpu_assigned * (compute_wt / wt) if wt > 0 else 0.0 + task = SimTask( + tid=tid, + name=workflow.id_to_name[tid], + start=t, + finish=finish, + cpu=fg_cpu, + cpu_booked=res.cpu_assigned, + mem=res.mem_assigned, + walltime=wt, + ) + running_fg.append((tid, finish)) + result.tasks.append(task) + else: + launch_seq += 1 + task = SimTask( + tid=tid, + name=workflow.id_to_name[tid], + start=t, + finish=t, + cpu=0.0, + cpu_booked=res.cpu_assigned, + mem=res.mem_assigned, + walltime=0.0, + ) + nominal_work = res.cpu_assigned * walltimes[tid] + running_bf[tid] = _RunningBackfill( + task=task, + nominal_work=nominal_work, + remaining_work=nominal_work, + launch_seq=launch_seq, + ) + result.tasks.append(task) + + if not running_fg and not running_bf: + break + + next_fg = min((ft for _, ft in running_fg), default=float("inf")) + cpu_fg = sum( + next(task.cpu_booked for task in result.tasks if task.tid == tid) + for tid, _ in running_fg + ) + hole_cpu = max(0.0, cpu_limit - cpu_fg) + remaining_hole = hole_cpu + bf_alloc: Dict[int, float] = {} + for tid, rb in sorted(running_bf.items(), key=lambda item: item[1].launch_seq): + if rb.overhead_until is not None: + bf_alloc[tid] = 0.0 + continue + alloc = min(rb.task.cpu_booked, remaining_hole) + bf_alloc[tid] = alloc + remaining_hole -= alloc + + next_bf = float("inf") + for tid, rb in running_bf.items(): + if rb.overhead_until is not None: + next_bf = min(next_bf, rb.overhead_until) + else: + alloc = bf_alloc.get(tid, 0.0) + if alloc > 1e-12: + next_bf = min(next_bf, t + rb.remaining_work / alloc) + + next_t = min(next_fg, next_bf) + if not math.isfinite(next_t): + break + dt = max(0.0, next_t - t) + for tid, rb in running_bf.items(): + if rb.overhead_until is None: + alloc = bf_alloc.get(tid, 0.0) + if alloc > 0.0: + rb.remaining_work = max(0.0, rb.remaining_work - alloc * dt) + t = next_t + + new_running_fg: List[Tuple[int, float]] = [] + for tid, ft in running_fg: + if abs(ft - t) < 1e-9: + rm.unbook(tid) + proc_status[tid] = "Done" + finished.add(tid) + for succ in workflow.forward_adj[tid]: + if proc_status[succ] == "ToDo": + if succ in impossible_tids: + continue + if all(p in finished for p in workflow.reverse_adj[succ]): + candidates.append(succ) + else: + new_running_fg.append((tid, ft)) + running_fg = new_running_fg + + done_bf: List[int] = [] + for tid, rb in running_bf.items(): + if rb.overhead_until is not None: + if abs(rb.overhead_until - t) < 1e-9: + done_bf.append(tid) + continue + if rb.remaining_work <= 1e-9: + if task_overhead > 0: + rb.overhead_until = t + task_overhead + else: + done_bf.append(tid) + + for tid in done_bf: + rb = running_bf.pop(tid) + rb.task.finish = t + rb.task.walltime = max(1e-9, rb.task.finish - rb.task.start) + rb.task.cpu = rb.nominal_work / rb.task.walltime + rm.unbook(tid) + proc_status[tid] = "Done" + finished.add(tid) + for succ in workflow.forward_adj[tid]: + if proc_status[succ] == "ToDo": + if succ in impossible_tids: + continue + if all(p in finished for p in workflow.reverse_adj[succ]): + candidates.append(succ) + + if not candidates and not running_fg and not running_bf: + break + else: + print(f" WARNING [{policy_name}]: simulation hit guard limit — possible deadlock", + file=sys.stderr) + + result.makespan = t + result.deadlocked_tids = [ + i for i, s in enumerate(proc_status) if s == "ToDo" + ] + if result.deadlocked_tids: + names = [workflow.id_to_name[i] for i in result.deadlocked_tids] + print(f" WARNING [{policy_name}]: {len(names)} tasks never scheduled " + f"(resource limits too tight?): {names[:5]}{'...' if len(names) > 5 else ''}", + file=sys.stderr) + return result + + for _guard in range(n * n + 1): # at most n rounds to completion + # --- schedule everything that fits right now --- + ordered = policy.order(candidates, state) + for tid, nice in policy.pick_submittable(ordered, rm): + # book immediately so subsequent picks in this pass see the + # updated resource availability (mirrors executor behaviour) + rm.book(tid, nice) + slowdown = 1.0 + if backfill_model == "slowdown" and nice != rm.nice_default: + slowdown = backfill_slowdown_factor + compute_wt = walltimes[tid] * slowdown # time spent doing actual work + wt = compute_wt + task_overhead # total slot duration (incl. idle overhead) + finish = t + wt + running.append((tid, finish)) + candidates.remove(tid) + proc_status[tid] = "Running" + res = rm.resources[tid] + # Overhead is idle time (process start/stop, alienv load, I/O flush). + # Average CPU over the full slot = booked_cpu × compute_fraction only. + effective_cpu = res.cpu_assigned / slowdown * (compute_wt / wt) if wt > 0 else 0.0 + result.tasks.append(SimTask( + tid=tid, + name=workflow.id_to_name[tid], + start=t, + finish=finish, + cpu=effective_cpu, + cpu_booked=res.cpu_assigned, + mem=res.mem_assigned, + walltime=wt, + )) + + if not running: + break + + # --- advance to next task completion --- + next_t = min(ft for _, ft in running) + t = next_t + + # --- complete all tasks finishing at t (within float tolerance) --- + still_running: List[Tuple[int, float]] = [] + for tid, ft in running: + if abs(ft - t) < 1e-9: + rm.unbook(tid) + proc_status[tid] = "Done" + finished.add(tid) + for succ in workflow.forward_adj[tid]: + if proc_status[succ] == "ToDo": + if succ in impossible_tids: + continue + if all(p in finished for p in workflow.reverse_adj[succ]): + candidates.append(succ) + else: + still_running.append((tid, ft)) + running = still_running + + if not candidates and not running: + break + else: + print(f" WARNING [{policy_name}]: simulation hit guard limit — possible deadlock", + file=sys.stderr) + + result.makespan = t + result.deadlocked_tids = [ + i for i, s in enumerate(proc_status) if s == "ToDo" + ] + if result.deadlocked_tids: + names = [workflow.id_to_name[i] for i in result.deadlocked_tids] + print(f" WARNING [{policy_name}]: {len(names)} tasks never scheduled " + f"(resource limits too tight?): {names[:5]}{'...' if len(names) > 5 else ''}", + file=sys.stderr) + return result + + +# --------------------------------------------------------------------------- +# Presentation +# --------------------------------------------------------------------------- + +def _fmt_time(s: float) -> str: + return f"{s:.1f}s" + + +def print_summary( + results_by_policy: Dict[str, List[SimResult]], + cpu_limit: float, + n_samples: int, +) -> None: + w = 16 + stoch = n_samples > 1 + mk_hdr = f"{'Makespan (mean±std)':>22}" if stoch else f"{'Makespan':>10}" + cpu_hdr = f"{'CPU util (mean±std)':>20}" if stoch else f"{'CPU util':>9}" + mem_hdr = f"{'Peak mem (mean±std)':>22}" if stoch else f"{'Peak mem':>10}" + header = f"{'Policy':<{w}} {mk_hdr} {cpu_hdr} {mem_hdr} {'Tasks':>6}" + print() + print(header) + print("-" * len(header)) + for policy, runs in results_by_policy.items(): + makespans = [r.makespan for r in runs] + mean_mk = statistics.mean(makespans) + util_pct = statistics.mean(r.cpu_utilization(cpu_limit) * 100 for r in runs) + peak = statistics.mean(r.peak_mem_mb() for r in runs) + n_tasks = runs[0].tasks.__len__() if runs else 0 + if stoch: + std_mk = statistics.stdev(makespans) if len(makespans) > 1 else 0.0 + std_util = statistics.stdev(r.cpu_utilization(cpu_limit)*100 for r in runs) if len(runs)>1 else 0.0 + std_peak = statistics.stdev(r.peak_mem_mb() for r in runs) if len(runs)>1 else 0.0 + mk_str = f"{_fmt_time(mean_mk)} ± {_fmt_time(std_mk)}" + print(f"{policy:<{w}} {mk_str:>22} {util_pct:>7.1f}±{std_util:.1f}% {peak:>8.0f}±{std_peak:.0f}MB {n_tasks:>6}") + else: + print(f"{policy:<{w}} {_fmt_time(mean_mk):>10} {util_pct:>8.1f}% " + f"{peak:>9.0f}MB {n_tasks:>6}") + print() + + +def print_verbose(result: SimResult) -> None: + print(f"\n--- Schedule: {result.policy} ---") + prev_t = -1.0 + for task in sorted(result.tasks, key=lambda x: (x.start, x.name)): + if abs(task.start - prev_t) > 1e-9: + print(f" t={_fmt_time(task.start)}") + prev_t = task.start + print(f" START {task.name:<40} " + f"cpu={task.cpu:.1f}" + + (f" ({task.cpu_booked:.1f} booked)" if abs(task.cpu - task.cpu_booked) > 1e-9 else "") + + f" mem={task.mem:.0f}MB " + f"dur={_fmt_time(task.walltime)}") + print(f" Makespan: {_fmt_time(result.makespan)}") + + +def _print_sweep_table( + sweep_results: "List[Tuple]", # (M, results_by_policy, n_stages, worker_assignment|None) + cpu_limit: float, + n_samples: int, +) -> None: + """Print a compact table summarising a timeframe-sweep simulation run.""" + stoch = n_samples > 1 + + # Collect all scalable task names that appear in any worker assignment. + scalable_names: List[str] = [] + seen_names: "Set[str]" = set() + for _, _, _, wa in sweep_results: + if wa: + for name in sorted(wa): + if name not in seen_names: + scalable_names.append(name) + seen_names.add(name) + + # Build header with optional per-task worker columns. + worker_cols = " ".join(f"{n[:12]:>12}" for n in scalable_names) + cpu_col_hdr = f"{'CPU util(±std)':>14}" if stoch else f"{'CPU util':>9}" + hdr = (f" {'M':>4} {'Policy':<16} {'N tasks':>7} " + f"{'Makespan':>12} {cpu_col_hdr} {'Peak mem':>9}" + + (f" {worker_cols}" if scalable_names else "")) + cpu_col_w = 14 if stoch else 9 + print("\nTimeframe sweep results:") + print(hdr) + print(" " + "-" * (len(hdr) - 2)) + + for M, results_by_policy, n_stages, worker_assignment in sweep_results: + m_str = str(M) if M is not None else "orig" + for policy, runs in results_by_policy.items(): + makespans = [r.makespan for r in runs] + mean_mk = statistics.mean(makespans) + util_pct = statistics.mean(r.cpu_utilization(cpu_limit) * 100 for r in runs) + peak = statistics.mean(r.peak_mem_mb() for r in runs) + if stoch and len(runs) > 1: + std_mk = statistics.stdev(makespans) + mk_str = f"{_fmt_time(mean_mk)}±{_fmt_time(std_mk)}" + std_util = statistics.stdev(r.cpu_utilization(cpu_limit) * 100 for r in runs) + util_str = f"{util_pct:.1f}±{std_util:.1f}%" + else: + mk_str = _fmt_time(mean_mk) + util_str = f"{util_pct:.1f}%" + row = (f" {m_str:>4} {policy:<16} {n_stages:>7} " + f"{mk_str:>12} {util_str:>{cpu_col_w}} {peak:>7.0f}MB") + if scalable_names and worker_assignment: + wvals = " ".join( + f"{worker_assignment.get(n, '-'):>12}" + for n in scalable_names + ) + row += f" {wvals}" + print(row) + print() + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def optimize_workers( + workflow, + policy_name: str, + cpu_limit: float, + mem_limit: float, + amdahl_models: Dict[str, AmdahlModel], + cpu_fallback_factor: float = 10.0, + task_overhead: float = 0.1, + n_eval_samples: int = 3, + rng_seed: int = 0, + backfill_model: str = "off", + n_backfill: int = 1, + backfill_cpu_factor: float = 1.5, + backfill_mem_factor: float = 1.5, + backfill_slowdown_factor: float = 1.15, + maxjobs: int = 10_000, +) -> Tuple[Dict[str, int], float]: + """Coordinate-descent search for the best worker assignment. + + For each scalable task, iterates over its valid worker range (from the + Amdahl model) and picks the count that minimises mean makespan while + holding all other tasks fixed. Repeats until no improvement is found. + + Returns (best_assignment, best_makespan_s). + """ + # Start from the current assignment implied by each model's cpu_mean_ref + # (what update_resource_estimates already set via round(cpu_mean)). + assignment: Dict[str, int] = { + name: max(model.min_workers, + min(model.max_workers, max(1, round(model.cpu_mean_ref)))) + for name, model in amdahl_models.items() + } + + walltime_stds: Dict[str, float] = {} # no learned std for optimizer runs + + def _evaluate(asgn: Dict[str, int]) -> float: + makespans = [] + for s in range(n_eval_samples): + rng = random.Random(rng_seed + s) if n_eval_samples > 1 else None + r = simulate( + workflow, policy_name, cpu_limit, mem_limit, + cpu_fallback_factor=cpu_fallback_factor, + task_overhead=task_overhead, + walltime_stds=walltime_stds, + cv_fallback=0.1, + rng=rng, + amdahl_models=amdahl_models, + worker_assignment=asgn, + backfill_model=backfill_model, + n_backfill=n_backfill, + backfill_cpu_factor=backfill_cpu_factor, + backfill_mem_factor=backfill_mem_factor, + backfill_slowdown_factor=backfill_slowdown_factor, + maxjobs=maxjobs, + ) + makespans.append(r.makespan) + return statistics.mean(makespans) + + best_score = _evaluate(assignment) + improved = True + while improved: + improved = False + for name, model in amdahl_models.items(): + best_n = assignment[name] + for n in model.worker_range: + if n == best_n: + continue + trial = dict(assignment) + trial[name] = n + score = _evaluate(trial) + if score < best_score - 0.1: # 0.1 s improvement threshold + best_score = score + best_n = n + improved = True + assignment[name] = best_n + return assignment, best_score + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + description="Simulate O2DPG workflow scheduling without running tasks.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p.add_argument("-f", "--workflowfile", required=True) + p.add_argument("--update-resources", dest="update_resources", default=None, + metavar="JSON[:FIELDS]", + help="Apply learned resources from JSON (same file as " + "--update-resources in the runner). Enables walltime-based " + "critical path. Optionally restrict which dimensions are " + "patched with a colon-separated field list chosen from " + "{cpu,mem,lifetime}. Example: learned.json:lifetime applies " + "only walltimes, keeping cpu/mem from workflow.json.") + p.add_argument("--cpu-limit", type=float, default=8.0) + p.add_argument("--mem-limit", type=float, default=60000.0, help="in MB") + p.add_argument("--policies", nargs="+", + default=["timeframe", "critical-path", "best-fit"], + choices=["timeframe", "critical-path", "best-fit"]) + p.add_argument("-tt", "--target-tasks", nargs="+", default=["*"]) + p.add_argument("--target-labels", nargs="+", default=[]) + p.add_argument("--walltime-per-core", type=float, default=10.0, metavar="S", + help="Fallback walltime per CPU core [s] when no learned " + "walltime is available.") + p.add_argument("--task-overhead", type=float, default=0.1, metavar="S", + help="Per-task idle overhead [s] added to every task's slot " + "duration (models alienv load, process startup, I/O flush, " + "and scheduler reaction time). This time contributes zero " + "CPU to the utilisation numerator, so larger values lower " + "both predicted makespan and CPU efficiency. " + "Default 0.1 s is conservative; calibration against " + "measurements typically gives 5–7 s for production " + "ALICE MC workflows.") + p.add_argument("--backfill-model", default="off", + choices=["off", "structural", "slowdown", "holefill"], + help="Backfill approximation used by the simulator. " + "'structural' replays the runner's second admission lane; " + "'slowdown' adds a fitted walltime penalty to backfill tasks; " + "'holefill' lets backfill tasks consume only the CPU left idle " + "by foreground tasks, slowing them proportionally.") + p.add_argument("--n-backfill", type=int, default=1, metavar="N", + help="Maximum concurrent backfill tasks when backfill simulation is enabled.") + p.add_argument("--backfill-cpu-factor", type=float, default=1.5, metavar="X", + help="Total CPU oversubscription factor allowed for backfill admission.") + p.add_argument("--backfill-mem-factor", type=float, default=1.5, metavar="X", + help="Total memory oversubscription factor allowed for backfill admission.") + p.add_argument("--backfill-slowdown-factor", type=float, default=1.15, metavar="X", + help="Walltime multiplier applied to backfill tasks in " + "--backfill-model slowdown.") + p.add_argument("--samples", type=int, default=1, metavar="N", + help="Number of Monte Carlo samples for stochastic simulation. " + "When >1, walltime for each task is drawn from a log-normal " + "distribution parameterised by lifetime.mean and lifetime.std " + "from the learned JSON. Output shows mean ± std of makespan.") + p.add_argument("--cv-fallback", type=float, default=0.15, metavar="CV", + help="Coefficient of variation (std/mean) used as a noise floor " + "for tasks whose learned walltime std is zero or absent " + "(single-TF runs, old learned files, etc.). " + "0 disables fallback and makes those tasks deterministic.") + p.add_argument("-j", "--maxjobs", type=int, default=0, metavar="N", + help="Maximum concurrent tasks. -1 = serial mode (1 task at a time): " + "reproduces the learning-run conditions (-jmax 1) and makes the " + "optimizer prefer maximum workers per task. 0 = unlimited (default). " + "N > 0 = at most N concurrent tasks.") + p.add_argument("--optimize-workers", action="store_true", + help="Run coordinate-descent optimizer to find the best worker " + "assignment for scalable tasks. Requires --update-resources " + "with a learned.json that contains 'amdahl' blocks (produced " + "by json-stat --workflow workflow.json).") + p.add_argument("--opt-eval-samples", type=int, default=3, metavar="N", + help="Simulator evaluations per candidate during optimization " + "(more = less noise, slower).") + p.add_argument("--write-optimized", default=None, metavar="FILE", + help="After --optimize-workers, write a copy of the learned JSON " + "with updated lifetime.mean and cpu.mean for scalable tasks. " + "Pass this file to --update-resources in the runner to apply " + "optimized worker counts. When multiple --policies are given " + "the policy with the best (lowest) optimized makespan is used.") + p.add_argument("--verbose", action="store_true", + help="Print per-task schedule for each policy.") + p.add_argument("--output", default=None, metavar="FILE", + help="Write results as JSON to FILE.") + p.add_argument("--timeframes", type=int, nargs="+", default=None, metavar="M", + help="Simulate with M timeframes instead of the workflow's original count. " + "Detects the per-TF template structure automatically and replicates it. " + "Pass multiple values for a sweep (e.g. --timeframes 1 2 5 10 20) to " + "produce a table of makespan and CPU utilisation vs timeframe count.") + return p + + +def main(argv=None) -> int: + ns = build_parser().parse_args(argv) + + raw = load_json(ns.workflowfile) + target_tasks = [t.strip('"').strip("'") for t in ns.target_tasks] + + # ── Parse --update-resources path[:field,field,...] ─────────────────────── + ur_path: Optional[str] = None + ur_fields: Optional[Set[str]] = None # None = all three fields + if ns.update_resources: + _parts = ns.update_resources.split(":", 1) + ur_path = _parts[0] + if len(_parts) > 1: + _raw_fields = {f.strip().lower() for f in _parts[1].split(",")} + _bad = _raw_fields - _LEARN_ALL + if _bad: + print(f"ERROR: unknown field(s) in --update-resources specifier: " + f"{sorted(_bad)}. Valid: cpu, mem, lifetime", file=sys.stderr) + return 1 + ur_fields = _raw_fields + + # ── Load learned JSON once (independent of timeframe count) ────────────── + walltime_stds: Dict[str, float] = {} + learned_full: Dict = {} + amdahl_models: Dict[str, AmdahlModel] = {} + + if ur_path: + _active = ur_fields if ur_fields is not None else _LEARN_ALL + _fields_str = ", ".join(sorted(_active)) + print(f"Applying learned resources from {ur_path}" + + (f" (fields: {_fields_str})" if ur_fields is not None else "") + + " ...") + with open(ur_path) as fh: + learned_full = json.load(fh) + apply_lifetime = "lifetime" in _active + for name, data in learned_full.items(): + if name == "count": + continue + if ns.samples > 1 and apply_lifetime: + std = data.get("lifetime", {}).get("std", 0.0) or 0.0 + if std > 0: + walltime_stds[name] = float(std) + if isinstance(data, dict) and "amdahl" in data: + try: + amdahl_models[name] = AmdahlModel.from_dict(data["amdahl"]) + except (KeyError, ValueError): + pass + if amdahl_models: + print(f" Amdahl models loaded for {len(amdahl_models)} scalable task(s): " + f"{', '.join(sorted(amdahl_models))}") + else: + print(f"No learned resources; using cpu * {ns.walltime_per_core}s as walltime proxy.") + + stochastic = ns.samples > 1 + if stochastic: + n_with_std = len(walltime_stds) + print(f" Stochastic: {n_with_std} tasks use learned std, " + f"cv_fallback={ns.cv_fallback:.2f} for the rest.") + if ns.backfill_model != "off": + print( + "Backfill simulation: " + f"model={ns.backfill_model}, n_backfill={ns.n_backfill}, " + f"cpu_factor={ns.backfill_cpu_factor}, mem_factor={ns.backfill_mem_factor}, " + + ( + f"slowdown={ns.backfill_slowdown_factor:.2f}x\n" + if ns.backfill_model == "slowdown" + else "foreground-hole driven\n" + if ns.backfill_model == "holefill" + else "\n" + ) + ) + + # Verbose Amdahl model summary (shown once, outside the sweep loop). + if amdahl_models and ns.verbose: + print() + for name, model in sorted(amdahl_models.items()): + n_cur = max(model.min_workers, + min(model.max_workers, max(1, round(model.cpu_mean_ref)))) + print(f" Amdahl: {name} " + f"(n_ref={model.n_ref}, cpu_mean={model.cpu_mean_ref:.2f}, " + f"t_serial={model.t_serial:.1f}s, " + f"t_parallel_tot={model.t_parallel_tot:.1f}s)") + print(f" {'n':>4} {'walltime':>10} {'Δ vs current':>14}") + wt_cur = model.walltime(n_cur) + for n in model.worker_range: + wt = model.walltime(n) + marker = " ← current" if n == n_cur else "" + print(f" {n:>4} {_fmt_time(wt):>10} " + f"{wt - wt_cur:>+12.1f}s{marker}") + print() + + # Resolve maxjobs — must come before optimizer and policy loops. + # -j -1 → serial + n_ref workers -j 1 → serial + round(cpu_mean) workers + # -j 0 → unlimited (default) -j N → at most N concurrent tasks + serial_mode = ns.maxjobs == -1 + procs_limit = 1 if (serial_mode or ns.maxjobs == 1) else (10_000 if ns.maxjobs <= 0 else ns.maxjobs) + if serial_mode: + print("Serial mode (-j -1): reproducing learning-run conditions " + "(1 task at a time, n_ref workers per scalable task).") + + # Default worker assignment: Amdahl-based, same for all M values in a sweep. + default_worker_assignment: Optional[Dict[str, int]] = None + if amdahl_models: + if serial_mode: + default_worker_assignment = {name: m.n_ref for name, m in amdahl_models.items()} + parts = [f"{n}: {w}w (n_ref)" for n, w in sorted(default_worker_assignment.items())] + else: + default_worker_assignment = { + name: max(m.min_workers, min(m.max_workers, max(1, round(m.cpu_mean_ref)))) + for name, m in amdahl_models.items() + } + parts = [f"{n}: {w}w" for n, w in sorted(default_worker_assignment.items())] + print(f"Worker counts for simulation: {', '.join(parts)}\n") + + # ── Timeframe sweep ─────────────────────────────────────────────────────── + tf_list: List[Optional[int]] = sorted(set(ns.timeframes)) if ns.timeframes else [None] + sweep_mode = len(tf_list) > 1 + if sweep_mode: + print(f"Timeframe sweep: M={tf_list} policies={ns.policies} " + f"cpu_limit={ns.cpu_limit} mem_limit={ns.mem_limit} MB\n") + + all_sweep_results: List[Tuple] = [] # (M, results_by_policy, n_stages, worker_assignment) + last_opt_results: List[Tuple[str, Dict[str, int], float]] = [] + + for M in tf_list: + if sweep_mode: + print(f" M={M} ...", end=" ", flush=True) + + cur_raw = replicate_workflow_for_timeframes(raw, M) if M is not None else raw + wf = build_workflow(cur_raw, target_tasks, ns.target_labels) + if not wf.stages: + print(f"Workflow is empty after filtering (M={M}).") + continue + + if ur_path: + if ur_fields is None: + update_resource_estimates(wf, ur_path) + else: + _apply_learned_fields(wf, learned_full, ur_fields) + has_wt = sum(1 for t in wf.stages if t.get("resources", {}).get("walltime")) + if not sweep_mode: + print(f" {has_wt}/{len(wf.stages)} tasks have learned walltime.") + if stochastic: + n_fallback = len(wf.stages) - len(walltime_stds) + print(f" Stochastic: {len(walltime_stds)} tasks use learned std, " + f"{n_fallback} use cv_fallback={ns.cv_fallback:.2f}.") + if not sweep_mode: + print(f"Workflow: {len(wf.stages)} tasks, cpu_limit={ns.cpu_limit}, " + f"mem_limit={ns.mem_limit} MB, samples={ns.samples}" + + (" (stochastic)" if stochastic else " (deterministic)") + "\n") + + # Worker-count optimizer (--optimize-workers): runs per-M so that the + # simulated workflow matches the actual task count. + cur_worker_assignment = default_worker_assignment + opt_results: List[Tuple[str, Dict[str, int], float]] = [] + if ns.optimize_workers: + if not amdahl_models: + print("WARNING: --optimize-workers requires Amdahl models in learned.json. " + "Re-run json-stat with --workflow workflow.json first.", file=sys.stderr) + else: + if not sweep_mode: + print(f"\nOptimizing worker assignment over " + f"{len(amdahl_models)} scalable task(s)...") + for policy_name in ns.policies: + best_asgn, best_mk = optimize_workers( + wf, policy_name, ns.cpu_limit, ns.mem_limit, + amdahl_models=amdahl_models, + cpu_fallback_factor=ns.walltime_per_core, + task_overhead=ns.task_overhead, + n_eval_samples=ns.opt_eval_samples, + backfill_model=ns.backfill_model, + n_backfill=ns.n_backfill, + backfill_cpu_factor=ns.backfill_cpu_factor, + backfill_mem_factor=ns.backfill_mem_factor, + backfill_slowdown_factor=ns.backfill_slowdown_factor, + maxjobs=procs_limit, + ) + opt_results.append((policy_name, best_asgn, best_mk)) + if not sweep_mode: + print(f"\n [{policy_name}] best makespan: {_fmt_time(best_mk)}") + print(f" {'Task':<35} {'n_ref':>6} {'current':>8} {'→ opt':>6} " + f"{'wt(current)':>12} {'wt(opt)':>10} {'Δwt':>8}") + print(f" {'-'*85}") + for name, model in sorted(amdahl_models.items()): + n_cur = max(model.min_workers, + min(model.max_workers, max(1, round(model.cpu_mean_ref)))) + n_opt = best_asgn[name] + wt_cur = model.walltime(n_cur) + wt_opt = model.walltime(n_opt) + delta = wt_opt - wt_cur + changed = "←" if n_opt != n_cur else "" + print(f" {name:<35} {model.n_ref:>6} {n_cur:>8} {n_opt:>6} " + f"{_fmt_time(wt_cur):>12} {_fmt_time(wt_opt):>10} " + f"{delta:>+7.1f}s {changed}") + if not sweep_mode: + print() + _, best_asgn_opt, _ = min(opt_results, key=lambda x: x[2]) + cur_worker_assignment = best_asgn_opt + last_opt_results = opt_results + + # ── Run simulations ─────────────────────────────────────────────────── + results_by_policy: Dict[str, List[SimResult]] = {} + for policy_name in ns.policies: + runs: List[SimResult] = [] + for s in range(ns.samples): + rng = random.Random(s) if stochastic else None + r = simulate( + wf, policy_name, + cpu_limit=ns.cpu_limit, + mem_limit=ns.mem_limit, + cpu_fallback_factor=ns.walltime_per_core, + task_overhead=ns.task_overhead, + walltime_stds=walltime_stds, + cv_fallback=ns.cv_fallback, + rng=rng, + amdahl_models=amdahl_models if amdahl_models else None, + worker_assignment=cur_worker_assignment, + backfill_model=ns.backfill_model, + n_backfill=ns.n_backfill, + backfill_cpu_factor=ns.backfill_cpu_factor, + backfill_mem_factor=ns.backfill_mem_factor, + backfill_slowdown_factor=ns.backfill_slowdown_factor, + maxjobs=procs_limit, + ) + runs.append(r) + results_by_policy[policy_name] = runs + if not sweep_mode: + makespans = [r.makespan for r in runs] + summary = _fmt_time(statistics.mean(makespans)) + if stochastic: + summary += f" ± {_fmt_time(statistics.stdev(makespans))}" + print(f" {policy_name:<16} makespan={summary}") + + all_sweep_results.append((M, results_by_policy, len(wf.stages), cur_worker_assignment)) + + if sweep_mode: + best_mk = min( + statistics.mean(r.makespan for r in runs) + for runs in results_by_policy.values() + ) + print(f"{len(wf.stages)} tasks, best makespan={_fmt_time(best_mk)}") + + if not all_sweep_results: + return 1 + + # ── Summary ─────────────────────────────────────────────────────────────── + if sweep_mode: + _print_sweep_table(all_sweep_results, ns.cpu_limit, ns.samples) + else: + _, results_by_policy, _, _ = all_sweep_results[0] + print_summary(results_by_policy, ns.cpu_limit, ns.samples) + if ns.verbose: + for policy_name, runs in results_by_policy.items(): + print_verbose(runs[0]) + + # ── Write optimized learned.json ────────────────────────────────────────── + if ns.write_optimized and last_opt_results and learned_full: + if sweep_mode: + print(f"NOTE: --write-optimized in sweep mode writes the optimisation " + f"result from the last M={tf_list[-1]}.") + best_policy, best_asgn, best_mk = min(last_opt_results, key=lambda x: x[2]) + if len(ns.policies) > 1: + print(f"Writing optimized resources: policy '{best_policy}' " + f"selected (best makespan {_fmt_time(best_mk)}).") + else: + print(f"Writing optimized resources (makespan {_fmt_time(best_mk)}).") + out_learned = copy.deepcopy(learned_full) + n_changed = 0 + for name, model in amdahl_models.items(): + if name not in out_learned: + continue + n_opt = best_asgn[name] + n_cur = max(model.min_workers, + min(model.max_workers, max(1, round(model.cpu_mean_ref)))) + if "lifetime" not in out_learned[name]: + out_learned[name]["lifetime"] = {} + out_learned[name]["lifetime"]["mean"] = round(model.walltime(n_opt), 3) + if "cpu" not in out_learned[name]: + out_learned[name]["cpu"] = {} + out_learned[name]["cpu"]["mean"] = float(n_opt) + if n_opt != n_cur: + n_changed += 1 + with open(ns.write_optimized, "w") as fh: + json.dump(out_learned, fh, indent=2) + print(f" Wrote {ns.write_optimized} " + f"({len(amdahl_models)} scalable tasks updated, " + f"{n_changed} changed from current assignment).") + + # ── JSON output ─────────────────────────────────────────────────────────── + if ns.output: + out_list = [] + for M, results_by_policy, n_stages, _ in all_sweep_results: + for policy_name, runs in results_by_policy.items(): + for i, r in enumerate(runs): + d = r.to_dict() + d["sample"] = i + d["n_stages"] = n_stages + if M is not None: + d["timeframes"] = M + out_list.append(d) + with open(ns.output, "w") as fh: + json.dump(out_list, fh, indent=2) + print(f"Results written to {ns.output}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/MC/workflow_runner/o2dpg_workflow_runner.py b/MC/workflow_runner/o2dpg_workflow_runner.py new file mode 100755 index 000000000..70c56a4ba --- /dev/null +++ b/MC/workflow_runner/o2dpg_workflow_runner.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Entry point preserving the legacy filename MC/bin/o2dpg_workflow_runner.py. + +All behavior lives in the o2dpg_runner package next to this script. +""" + +import os +import sys + +# Make sure we can import the o2dpg_runner package that sits next to us. +_here = os.path.dirname(os.path.realpath(__file__)) +if _here not in sys.path: + sys.path.insert(0, _here) + +from o2dpg_runner.cli import main + +if __name__ == "__main__": + sys.exit(main()) From 5e0a865e9ddaaac9c51a2cf062fba36553fc160c Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 05:18:01 +0200 Subject: [PATCH 044/144] Learn the file-IO graph without root, using strace This makes the file-IO-graph learning pluggable and adds an strace backend, so a pilot run no longer needs a binary with CAP_SYS_ADMIN. The graph it produces is the one --remove-files-early already reads back. - The runner gains --filegraph-backends. Naming several at once runs them side by side, which is how they are compared. O2DPG_PRODUCE_FILEGRAPH still selects fanotify and names its monitor. - The strace backend wraps each task command, so a file access is attributed by the trace it lands in rather than by walking /proc after the event. --seccomp-bpf keeps the cost at about 59 us per traced open; the backend probes for it. - filegraph_report.py holds the exclusion rules, the ./tfN -> ./tfX templating, the JSON schema and the graphviz rendering, so the two analysers cannot drift. analyse_FileIO_v2.py reproduces its previous output byte for byte. - compare_reports.py grades one report against another. A missing edge deletes a file a later task still reads; an extra edge only delays the deletion. The verdicts EXACT, SAFE and UNSAFE follow that asymmetry. - tests/equivalence_test.py runs a workflow whose graph is known by construction and grades every backend against it, in seconds and with no ALICE software. strace comes out EXACT. - monitor_fileaccess_v2.cpp spun forever on a queue overflow, because the overflow branch skipped FAN_EVENT_NEXT and re-tested the same event. - 33 offline tests come with it, plain unittest so they also run on a worker node, and a CI job runs them. Part of the o2dpg_workflow_runner.py refactoring. --- .github/workflows/syntax-checks.yml | 14 + MC/workflow_runner/o2dpg_runner/cli.py | 60 ++--- MC/workflow_runner/o2dpg_runner/config.py | 1 + MC/workflow_runner/o2dpg_runner/executor.py | 20 +- MC/workflow_runner/o2dpg_runner/filegraph.py | 239 ++++++++++++++++++ UTILS/FileIOGraph/README.md | 97 +++++-- UTILS/FileIOGraph/analyse_FileIO_strace.py | 152 +++++++++++ UTILS/FileIOGraph/analyse_FileIO_v2.py | 138 ++++++++++ UTILS/FileIOGraph/compare_reports.py | 156 ++++++++++++ UTILS/FileIOGraph/filegraph_report.py | 173 +++++++++++++ UTILS/FileIOGraph/monitor_fileaccess_v2.cpp | 7 +- UTILS/FileIOGraph/tests/equivalence_test.py | 205 +++++++++++++++ .../tests/filegraph_test_support.py | 20 ++ .../FileIOGraph/tests/test_analyse_strace.py | 131 ++++++++++ UTILS/FileIOGraph/tests/test_filegraph.py | 237 +++++++++++++++++ 15 files changed, 1581 insertions(+), 69 deletions(-) create mode 100644 MC/workflow_runner/o2dpg_runner/filegraph.py create mode 100755 UTILS/FileIOGraph/analyse_FileIO_strace.py create mode 100644 UTILS/FileIOGraph/analyse_FileIO_v2.py create mode 100755 UTILS/FileIOGraph/compare_reports.py create mode 100644 UTILS/FileIOGraph/filegraph_report.py create mode 100755 UTILS/FileIOGraph/tests/equivalence_test.py create mode 100644 UTILS/FileIOGraph/tests/filegraph_test_support.py create mode 100644 UTILS/FileIOGraph/tests/test_analyse_strace.py create mode 100644 UTILS/FileIOGraph/tests/test_filegraph.py diff --git a/.github/workflows/syntax-checks.yml b/.github/workflows/syntax-checks.yml index 942d7b1dd..bd9b5de6e 100644 --- a/.github/workflows/syntax-checks.yml +++ b/.github/workflows/syntax-checks.yml @@ -120,6 +120,20 @@ jobs: working-directory: MC/workflow_runner run: pytest o2dpg_runner/tests -q + filegraph-tests: + name: File-IO-graph unit tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install prerequisites + run: pip install psutil + + - name: Run the FileIOGraph test suite + run: python3 -m unittest discover -s UTILS/FileIOGraph/tests -t UTILS/FileIOGraph/tests + pylint: name: Pylint runs-on: ubuntu-latest diff --git a/MC/workflow_runner/o2dpg_runner/cli.py b/MC/workflow_runner/o2dpg_runner/cli.py index defab9ad2..222780782 100644 --- a/MC/workflow_runner/o2dpg_runner/cli.py +++ b/MC/workflow_runner/o2dpg_runner/cli.py @@ -8,17 +8,16 @@ from __future__ import annotations import argparse -import json import logging import os import shutil -import subprocess import sys from typing import Optional, Tuple import psutil from .config import RunnerConfig +from .filegraph import BACKENDS as FILEGRAPH_BACKENDS, FileGraphManager from .workflow import build_workflow, load_json from .executor import WorkflowExecutor @@ -100,6 +99,11 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--retry-on-failure", type=int, default=0) p.add_argument("--no-rootinit-speedup", action="store_true") p.add_argument("--remove-files-early", type=str, default="") + p.add_argument("--filegraph-backends", type=str, + default=os.getenv("O2DPG_FILEGRAPH_BACKENDS", ""), + help="comma-separated file-IO-graph backends to learn the " + "file dependencies with: " + + ", ".join(sorted(FILEGRAPH_BACKENDS))) # Accept-and-ignore for backward compatibility of call sites # that still pass these flags. They have no effect. @@ -154,6 +158,7 @@ def _args_to_config(ns: argparse.Namespace) -> RunnerConfig: retry_on_failure=ns.retry_on_failure, no_rootinit_speedup=ns.no_rootinit_speedup, remove_files_early=ns.remove_files_early, + filegraph_backends=ns.filegraph_backends, stdout_on_failure=ns.stdout_on_failure, production_mode=ns.production_mode, action_logfile=ns.action_logfile, @@ -341,22 +346,6 @@ def _maybe_draw_workflow(raw_spec): dot.render("workflow.gv") -def _launch_fileaccess_sidecar(actionlogger_file: str): - """Start the fanotify-based file-IO graph sidecar if requested.""" - exe = os.getenv("O2DPG_PRODUCE_FILEGRAPH") - if not exe: - return None, None, None - env = os.environ.copy() - env["FILEACCESS_MON_ROOTPATH"] = os.getcwd() - env["MAXMOTHERPID"] = f"{os.getpid()}" - log_file = f"pipeline_fileaccess_{os.getpid()}.log" - fh = open(log_file, "w") - proc = subprocess.Popen( - [exe], stdout=fh, stderr=subprocess.STDOUT, env=env, - ) - return proc, fh, log_file - - def main(argv=None) -> int: ns = build_parser().parse_args(argv) _maybe_reexec_in_slice(ns) # may replace this process; returns only if not re-execing @@ -409,6 +398,7 @@ def main(argv=None) -> int: "systemd_run_spec": cfg.systemd_run_spec, "in_systemd_slice": cfg.in_systemd_slice, "monitor_interval_cpu": cfg.monitor_interval_cpu, + "filegraph_backends": cfg.filegraph_backends, }) metric_logger.info(meta) @@ -429,37 +419,19 @@ def main(argv=None) -> int: for k, v in wf.global_env.items(): os.environ.setdefault(k, str(v)) - # Optional file-access sidecar - fileaccess_proc, fileaccess_fh, fileaccess_log_file = _launch_fileaccess_sidecar(action_log) + filegraph = FileGraphManager.from_config( + cfg.filegraph_backends, os.getcwd(), os.getpid(), action_log, action_logger) + filegraph.start() rc = 0 try: - execer = WorkflowExecutor(cfg, wf, action_logger, metric_logger) + execer = WorkflowExecutor(cfg, wf, action_logger, metric_logger, + filegraph=filegraph) rc = int(execer.execute()) finally: - if fileaccess_proc is not None: - fileaccess_proc.terminate() - try: - fileaccess_proc.wait(timeout=5) - except subprocess.TimeoutExpired: - fileaccess_proc.kill() - if fileaccess_fh is not None: - fileaccess_fh.close() - o2dpg_root = os.getenv("O2DPG_ROOT") - if o2dpg_root and fileaccess_log_file: - analyse_cmd = [ - sys.executable, - f"{o2dpg_root}/UTILS/FileIOGraph/analyse_FileIO_v2.py", - "--actionFile", action_log, - "--monitorFile", fileaccess_log_file, - "-o", f"pipeline_fileaccess_report_{os.getpid()}.json", - "--basedir", os.getcwd(), - ] - print(f"Producing FileIOGraph with command {analyse_cmd}") - try: - subprocess.run(analyse_cmd, check=True) - except subprocess.CalledProcessError as e: - print(f"FileIOGraph analysis failed: {e}", file=sys.stderr) + filegraph.stop() + for backend, path in filegraph.analyse().items(): + print(f"FileIOGraph[{backend}] -> {path}") return rc diff --git a/MC/workflow_runner/o2dpg_runner/config.py b/MC/workflow_runner/o2dpg_runner/config.py index d12bf73c8..5769d0223 100644 --- a/MC/workflow_runner/o2dpg_runner/config.py +++ b/MC/workflow_runner/o2dpg_runner/config.py @@ -54,6 +54,7 @@ class RunnerConfig: retry_on_failure: int = 0 no_rootinit_speedup: bool = False remove_files_early: str = "" + filegraph_backends: str = "" stdout_on_failure: bool = False production_mode: bool = False diff --git a/MC/workflow_runner/o2dpg_runner/executor.py b/MC/workflow_runner/o2dpg_runner/executor.py index 10dc73b45..ca2d178ab 100644 --- a/MC/workflow_runner/o2dpg_runner/executor.py +++ b/MC/workflow_runner/o2dpg_runner/executor.py @@ -34,10 +34,11 @@ from .graph import descendants, longest_path_length, kahn_topological_order from .resources import ResourceManager, ResourceLimitExceeded from .monitoring import MonitorThread, PsutilBackend, _read_cgroup_v2_dir +from .filegraph import FileGraphManager from .scheduler import get_policy from .scheduler.base import SchedulerState from .scheduler.timeframe import TimeframeFirstPolicy -from .cache import TaskCache, compute_fingerprint, remove_done_flag, done_path +from .cache import TaskCache, compute_fingerprint, remove_done_flag from .alienv import get_alienv_software_environment from .cleanup import EarlyFileRemover, archive_task_logs @@ -90,8 +91,10 @@ def __init__( workflow: Workflow, action_logger: logging.Logger, metric_logger: logging.Logger, + filegraph=None, ): self.cfg = config + self.filegraph = filegraph or FileGraphManager([], os.getpid(), action_logger) self.wf = workflow self.actionlog = action_logger self.metriclog = metric_logger @@ -366,16 +369,23 @@ def submit(self, tid: int, nice: int) -> Optional[psutil.Popen]: slice_name if slice_name.endswith(".slice") else f"{slice_name}.slice" ) unit = _unit_name(task["name"], tid) - launch_argv = [ + prefix = [ "systemd-run", "--user", "--scope", "--collect", "--expand-environment=no", # suppress the $VAR warning; bash handles expansion - f"--unit={unit}", f"--slice={systemd_slice}", - "--", "/bin/bash", "-c", cmd, + f"--unit={unit}", f"--slice={systemd_slice}", "--", ] + else: + prefix = [] + + # a tracer has to sit inside any systemd scope, or it would only ever + # see systemd-run itself + inner_argv = self.filegraph.wrap(["/bin/bash", "-c", cmd], task["name"], tid) + launch_argv = prefix + inner_argv + + if use_scope: p = psutil.Popen(launch_argv, cwd=workdir, env=env, stderr=subprocess.PIPE) _start_stderr_drainer(p.stderr, self.actionlog, task["name"]) else: - launch_argv = ["/bin/bash", "-c", cmd] p = psutil.Popen(launch_argv, cwd=workdir, env=env) try: p.nice(nice) diff --git a/MC/workflow_runner/o2dpg_runner/filegraph.py b/MC/workflow_runner/o2dpg_runner/filegraph.py new file mode 100644 index 000000000..27da41bf1 --- /dev/null +++ b/MC/workflow_runner/o2dpg_runner/filegraph.py @@ -0,0 +1,239 @@ +"""Pluggable observation of task file IO during a pilot run. + +A backend records which task produces and which tasks consume each +intermediate file and writes ``filegraph__.json``, which a +later production run replays with ``--remove-files-early``. Several +backends can be active at once so they can be compared on one run; see +UTILS/FileIOGraph/README.md. +""" + +from __future__ import annotations + +import logging +import os +import shutil +import subprocess +import sys +from typing import Dict, List, Optional + + +def filegraph_dir() -> str: + """The UTILS/FileIOGraph directory holding the monitors and analysers.""" + root = os.getenv("O2DPG_ROOT") + if not root: + root = os.path.abspath(__file__) + while root != "/" and not os.path.isdir( + os.path.join(root, "UTILS", "FileIOGraph")): + root = os.path.dirname(root) + return os.path.join(root, "UTILS", "FileIOGraph") + + +class FileGraphBackend: + """One way of observing which task reads and writes which file.""" + + name = "none" + analyser = "" + #: backends that replace the original fanotify sidecar also write its + #: report under the name the runner has always used + legacy_report = False + + def __init__(self, workdir: str, runner_pid: int, action_log: str, + logger: logging.Logger): + self.workdir = workdir + self.runner_pid = runner_pid + self.action_log = action_log + self.log = logger + + def start(self) -> None: + pass + + def stop(self) -> None: + pass + + def wrap(self, argv: List[str], taskname: str, tid: int) -> List[str]: + """The argv actually used to launch a task.""" + return argv + + # -- result -------------------------------------------------------- + def recorded(self) -> bool: + return False + + def analyser_args(self) -> List[str]: + return [] + + def analyse(self, out_json: str) -> bool: + if not self.recorded(): + return False + argv = [sys.executable, os.path.join(filegraph_dir(), self.analyser), + "--basedir", self.workdir, "-o", out_json] + self.analyser_args() + self.log.info("FileIOGraph analysis: %s", " ".join(argv)) + try: + subprocess.run(argv, check=True) + return True + except (subprocess.CalledProcessError, OSError) as e: + self.log.error("FileIOGraph analysis failed: %s", e) + return False + + +class FanotifyBackend(FileGraphBackend): + """Mount-wide sidecar; needs CAP_SYS_ADMIN on the monitor binary.""" + + name = "fanotify" + analyser = "analyse_FileIO_v2.py" + legacy_report = True + + def __init__(self, workdir, runner_pid, action_log, logger, + exe: Optional[str] = None): + super().__init__(workdir, runner_pid, action_log, logger) + # O2DPG_PRODUCE_FILEGRAPH is the original spelling and still names + # the monitor to run + self.exe = (exe or os.getenv("O2DPG_PRODUCE_FILEGRAPH") + or os.path.join(filegraph_dir(), "monitor_fileaccess_v2.exe")) + self.logfile = f"pipeline_fileaccess_{self.name}_{runner_pid}.log" + self._proc: Optional[subprocess.Popen] = None + self._fh = None + + def start(self) -> None: + if not os.path.exists(self.exe): + self.log.error("filegraph fanotify: no such executable %s", self.exe) + return + env = dict(os.environ, FILEACCESS_MON_ROOTPATH=self.workdir, + MAXMOTHERPID=str(self.runner_pid)) + self._fh = open(self.logfile, "w") + try: + self._proc = subprocess.Popen([self.exe], stdout=self._fh, + stderr=subprocess.STDOUT, env=env) + except OSError as e: + self.log.error("filegraph fanotify: could not start %s: %s", self.exe, e) + self._fh.close() + self._fh = None + return + self.log.info("filegraph fanotify: %s pid=%d -> %s", + self.exe, self._proc.pid, self.logfile) + + def stop(self) -> None: + if self._proc is not None: + self._proc.terminate() + try: + self._proc.wait(timeout=10) + except subprocess.TimeoutExpired: + self._proc.kill() + self._proc = None + if self._fh is not None: + self._fh.close() + self._fh = None + + def recorded(self) -> bool: + return os.path.exists(self.logfile) + + def analyser_args(self) -> List[str]: + return ["--actionFile", self.action_log, "--monitorFile", self.logfile] + + +class StraceBackend(FileGraphBackend): + """Wraps every task in its own strace; attribution is by construction.""" + + name = "strace" + analyser = "analyse_FileIO_strace.py" + #: must stay in step with what analyse_FileIO_strace.py parses + SYSCALLS = "openat,openat2,open,creat,rename,renameat,renameat2" + + def __init__(self, workdir, runner_pid, action_log, logger, + exe: str = "strace"): + super().__init__(workdir, runner_pid, action_log, logger) + self.exe = exe + self.tracedir = os.path.join(workdir, f"strace_{runner_pid}") + self._argv: List[str] = [] + + def start(self) -> None: + if not shutil.which(self.exe): + self.log.error("filegraph strace: %s not found on PATH", self.exe) + return + argv = [self.exe, "-f", "-qq", "-y", + "-e", f"trace={self.SYSCALLS}", "-e", "status=successful"] + # without --seccomp-bpf every read() costs two ptrace stops + if self._seccomp_works(argv): + argv.insert(1, "--seccomp-bpf") + os.makedirs(self.tracedir, exist_ok=True) + self._argv = argv + self.log.info("filegraph strace: %s -> %s", " ".join(argv), self.tracedir) + + def _seccomp_works(self, argv: List[str]) -> bool: + try: + r = subprocess.run(argv[:1] + ["--seccomp-bpf"] + argv[1:] + + ["-o", os.devnull, "true"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=60) + return r.returncode == 0 + except (OSError, subprocess.SubprocessError): + return False + + def wrap(self, argv, taskname, tid): + if not self._argv: + return argv + trace = os.path.join(self.tracedir, f"trace_{tid}_{taskname}.log") + return self._argv + ["-o", trace, "--"] + list(argv) + + def recorded(self) -> bool: + return os.path.isdir(self.tracedir) + + def analyser_args(self) -> List[str]: + return ["--straceDir", self.tracedir] + + +BACKENDS = {b.name: b for b in (FanotifyBackend, StraceBackend)} + + +class FileGraphManager: + """Drives zero or more backends over the runner's lifecycle.""" + + def __init__(self, backends: List[FileGraphBackend], runner_pid: int, + logger: logging.Logger): + self.backends = backends + self.runner_pid = runner_pid + self.log = logger + + @classmethod + def from_config(cls, spec: str, workdir: str, runner_pid: int, + action_log: str, logger: logging.Logger) -> "FileGraphManager": + names = [n.strip() for n in spec.split(",") if n.strip()] + if not names and os.getenv("O2DPG_PRODUCE_FILEGRAPH"): + names = ["fanotify"] + backends = [] + for n in names: + factory = BACKENDS.get(n) + if factory is None: + logger.error("unknown filegraph backend %r (known: %s)", + n, ", ".join(sorted(BACKENDS))) + continue + backends.append(factory(workdir, runner_pid, action_log, logger)) + return cls(backends, runner_pid, logger) + + def start(self) -> None: + for b in self.backends: + b.start() + + def stop(self) -> None: + for b in self.backends: + b.stop() + + def wrap(self, argv: List[str], taskname: str, tid: int) -> List[str]: + for b in self.backends: + argv = b.wrap(argv, taskname, tid) + return argv + + def analyse(self) -> Dict[str, str]: + """Produce one report per backend; returns backend name -> path.""" + produced: Dict[str, str] = {} + for b in self.backends: + out = f"filegraph_{b.name}_{self.runner_pid}.json" + if not b.analyse(out): + continue + produced[b.name] = out + if b.legacy_report: + legacy = f"pipeline_fileaccess_report_{self.runner_pid}.json" + try: + shutil.copyfile(out, legacy) + except OSError as e: + self.log.warning("could not write %s: %s", legacy, e) + return produced diff --git a/UTILS/FileIOGraph/README.md b/UTILS/FileIOGraph/README.md index 48443b310..8450e09af 100644 --- a/UTILS/FileIOGraph/README.md +++ b/UTILS/FileIOGraph/README.md @@ -1,38 +1,97 @@ -This is a small custom tool to monitor file access -and to produce graphs of file production and file consumption -by O2DPG Monte Carlo tasks. Such information can be useful for +# FileIOGraph — learning which task produces and which tasks consume each file -(a) verification of data paths -(b) early removal of files as soon as they are not needed anymore +An O2DPG Monte Carlo workflow declares task dependencies, but not which +file each task reads or writes. The tools here observe a pilot run and +write that missing relation down, so a production run can +(a) verify the data paths, and +(b) delete an intermediate file the moment its last consumer is finished + (`o2dpg_workflow_runner.py --remove-files-early`). -In more detail, core elements of this directory are +## Backends -* monitor_fileaccess: +The observation is pluggable: -A tool, useable by root, providing reports about -read and write events to files and which process is involved. -The tool is based on the efficient fanotify kernel system and reporting -can be restricted to certain shells (by giving a mother PID). +```bash +ALIEN_O2DPG_WORKFLOW_RUNNER=new $O2DPG_ROOT/MC/bin/o2_dpg_workflow_runner.py \ + -f workflow.json --filegraph-backends fanotify +``` -The tool is standalone and can be compiled, if needed, by running +`--filegraph-backends` exists only in the runner under +`MC/workflow_runner`, hence the environment variable. -`g++ monitor_fileaccess.cpp -O2 -o monitor_fileaccess.exe` +Each named backend writes `filegraph__.json`; fanotify also +writes `pipeline_fileaccess_report_.json`, the name the runner has +always used. Naming several at once is how they are compared. -The tool can be run simply by +| Backend | How it observes | How it attributes | Privilege | +|---|---|---|---| +| `fanotify` | `monitor_fileaccess_v2.exe`, a mount-wide sidecar | walks `/proc/` up to the runner, after the event | `CAP_SYS_ADMIN` | +| `strace` | each task command wrapped in its own `strace` | by construction, from the trace file it lands in | none | -``` -sudo MAXMOTHERPID=689584 ./monitor.exe | tee /tmp/fileaccess -``` +`O2DPG_PRODUCE_FILEGRAPH=` still selects fanotify and names the +monitor to run. + +### fanotify + +The original. It needs `CAP_SYS_ADMIN`, granted per machine with +`setcap cap_sys_admin+ep monitor_fileaccess_v2.exe`, so a pilot run is +only possible where somebody with root has prepared the binary — and file +capabilities do not survive a `nosuid` mount, so the copy distributed +through CVMFS can never carry one. + +Unprivileged fanotify does not help: since Linux 5.13 `fanotify_init()` +needs no `CAP_SYS_ADMIN`, but such a group may not use `FAN_MARK_MOUNT` +and does not receive the pid, and the pid is the whole attribution +mechanism. -to monitor file events happening by child processes of shell 689584. +Two further properties are worth knowing. A mount-wide mark sees +everything, so in a two-timeframe pilot only 4 301 of 563 611 records were +inside the working directory. And because the process chain is resolved +from `/proc` after the event, an access made by a process that has already +exited yields the chain `;0` and is dropped. +### strace -* analyse_FileIO.py: +Wraps each task command, so attribution needs no inference. It sees the +`open` itself rather than the close, and `-y` gives the path the kernel +resolved the descriptor to, so relative paths, `chdir` and directory +descriptors all come out right. +`--seccomp-bpf` is what makes this affordable; the backend probes for it +and uses it when present. Without it every `read()` costs two ptrace +stops: on a read-heavy task, 22.5 s against a 0.95 s baseline, versus +1.00 s with it. The residual is about 59 µs per traced `open`. +## Checking one backend against another +```bash +python3 compare_reports.py --reference filegraph_fanotify_123.json \ + --candidate filegraph_strace_123.json +``` + +`EXACT` means edge for edge, `SAFE` that the candidate is a superset, +`UNSAFE` that it misses an edge the reference has. The asymmetry is the +point — an extra reader only keeps a file on disc longer, a missing one +deletes a file a later task still needs. + +`tests/equivalence_test.py` runs a synthetic workflow whose graph is known +by construction under several backends at once and grades all of them, in +seconds and without any ALICE software: +```bash +python3 tests/equivalence_test.py --backends strace --reference fanotify \ + --ntf 8 --cpu-limit 8 --sleep 0.05 +``` + +Drop `--reference fanotify` where no privileged monitor exists; the +synthetic workflow still carries its own analytic truth. +Offline tests: `python3 -m unittest discover -s tests -t tests`. +## Building +```bash +g++ monitor_fileaccess_v2.cpp -O2 -o monitor_fileaccess_v2.exe +sudo setcap cap_sys_admin+ep monitor_fileaccess_v2.exe # fanotify only +``` diff --git a/UTILS/FileIOGraph/analyse_FileIO_strace.py b/UTILS/FileIOGraph/analyse_FileIO_strace.py new file mode 100755 index 000000000..2ea49e99e --- /dev/null +++ b/UTILS/FileIOGraph/analyse_FileIO_strace.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Turn per-task strace logs into the file-task dependency report. + +The runner wraps each task in its own strace, so the trace file name says +which task made the call and nothing has to be attributed after the fact. +Traces are taken with -y, so the path on the *return* value is the one the +kernel resolved -- which is what makes relative paths, chdir and a +non-AT_FDCWD directory descriptor all come out right. +""" +from __future__ import annotations + +import argparse +import os +import re +import sys +from typing import Dict, List, Set, Tuple + +from filegraph_report import ( + add_common_arguments, basedir_prefix, emit, keep_file, relative_to_basedir, +) + +#: trace__.log, written by the runner's strace backend +TRACEFILE_RE = re.compile(r'^trace_(?P\d+)_(?P.+)\.log$') + +OPEN_CALLS = ("openat", "openat2", "open", "creat") +#: longest first, or 'openat' would match the head of 'openat2' +_OPEN_ALT = "|".join(sorted(OPEN_CALLS, key=len, reverse=True)) + +_OPEN_RE = re.compile( + rf'(?:^|\s)(?:{_OPEN_ALT})\((?P.*?)\)\s*=\s*\d+<(?P[^>]*)>') +_RENAME_RE = re.compile(r'(?:^|\s)rename(?:at2?)?\((?P.*?)\)\s*=\s*0') +_UNFINISHED_RE = re.compile(r'^(?P\d+)\s+(?P\w+)\((?P.*?)\d+)\s+<\.\.\.\s+(?P\w+)\s+resumed>(?P.*)$') +_RESULT_RE = re.compile(r'=\s*\d+<(?P[^>]*)>') +_QUOTED_RE = re.compile(r'"((?:[^"\\]|\\.)*)"') + +#: opendir reaches the kernel as an open with O_DIRECTORY; fanotify reports +#: directories only with FAN_ONDIR, so they are in neither graph +_DIRECTORY = "O_DIRECTORY" +_WRITE_FLAGS = ("O_WRONLY", "O_RDWR", "O_CREAT", "O_TRUNC", "O_APPEND") + + +def _kind(args: str) -> str: + return "write" if any(f in args for f in _WRITE_FLAGS) else "read" + + +def parse_trace(path: str) -> Tuple[Set[Tuple[str, str]], int]: + """Distinct (absolute path, read|write) pairs for one task, and the + number of calls they were distilled from.""" + seen: Set[Tuple[str, str]] = set() + calls = 0 + # strace splits a call around another process's entry; the flags are on + # the half that was interrupted + pending: Dict[Tuple[str, str], str] = {} + + with open(path, errors="replace") as fh: + for line in fh: + if "" in line: + m = _RESUMED_RE.match(line) + if m is None or m.group("call") not in OPEN_CALLS: + continue + res = _RESULT_RE.search(m.group("rest")) + if res is None or not res.group("path"): + continue + args = (pending.pop((m.group("pid"), m.group("call")), "") + + m.group("rest").split("=", 1)[0]) + calls += 1 + if _DIRECTORY not in args: + seen.add((res.group("path"), _kind(args))) + continue + + m = _OPEN_RE.search(line) + if m is not None: + calls += 1 + if m.group("path") and _DIRECTORY not in m.group("args"): + seen.add((m.group("path"), _kind(m.group("args")))) + continue + + if "rename" in line: + m = _RENAME_RE.search(line) + if m is None: + continue + names = _QUOTED_RE.findall(m.group("args")) + if names: + calls += 1 + seen.add((names[-1], "write")) # the destination is produced here + + return seen, calls + + +def collect(tracedir: str, basedir: str, file_filters): + written: Dict[str, Set[str]] = {} + read: Dict[str, Set[str]] = {} + tasks: List[str] = [] + prefix = basedir_prefix(basedir) + stats = {"traces": 0, "calls": 0, "kept": 0} + + for name in sorted(os.listdir(tracedir)): + m = TRACEFILE_RE.match(name) + if m is None: + continue + task = m.group("task") + tasks.append(task) + stats["traces"] += 1 + + accesses, calls = parse_trace(os.path.join(tracedir, name)) + stats["calls"] += calls + for target, kind in accesses: + rel = relative_to_basedir(target, prefix) + if rel is None or not keep_file(rel, file_filters): + continue + stats["kept"] += 1 + (written if kind == "write" else read).setdefault(rel, set()).add(task) + + return written, read, sorted(set(tasks)), stats + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--straceDir", required=True, + help="directory of trace__.log files") + add_common_arguments(p) + return p + + +def main(argv=None) -> int: + a = build_parser().parse_args(argv) + if not os.path.isdir(a.straceDir): + print(f"no such directory: {a.straceDir}", file=sys.stderr) + return 2 + + written, read, tasks, stats = collect( + a.straceDir, a.basedir, [re.compile(f) for f in a.file_filters]) + print(f"strace: {stats['traces']} task trace(s), {stats['calls']} call(s), " + f"{stats['kept']} access(es) kept under {a.basedir}") + if not stats["traces"]: + print(f"WARNING: no trace_*.log in {a.straceDir}", file=sys.stderr) + + emit(a, written, read, tasks) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/UTILS/FileIOGraph/analyse_FileIO_v2.py b/UTILS/FileIOGraph/analyse_FileIO_v2.py new file mode 100644 index 000000000..67951f183 --- /dev/null +++ b/UTILS/FileIOGraph/analyse_FileIO_v2.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Turn a fanotify file-access log plus the runner action log into the +file-task dependency report. + +Attribution comes from the process chain the monitor records: an access is +a task's if any pid in the chain is the pid the action log gives that task. +Both action-log formats are accepted: + + Old runner: "... INFO Task : finished with status 0" + New runner: "... INFO Task pid= tid= finished rc=0" +""" +from __future__ import annotations + +import argparse +import re +import sys +from typing import Dict, List, Set, Tuple + +from filegraph_report import ( + add_common_arguments, basedir_prefix, emit, keep_file, relative_to_basedir, +) + + +# ── action-log patterns ─────────────────────────────────────────────────────── + +# Old runner: "... INFO Task : finished with status 0" +_PAT_OLD = re.compile(r'.*INFO Task (\d+)[^:]*:(\w+) finished with status 0') +# New runner: "... INFO Task pid= tid= finished rc=0" +_PAT_NEW = re.compile(r'.*INFO Task pid=(\d+) tid=\d+ (\S+) finished rc=0') + + +def parse_action_log(path: str) -> Tuple[Dict[str, str], Dict[str, str]]: + """Parse task-PID associations from an action log. + + Returns (pid_to_task, task_to_pid). Only successfully completed tasks + are included (rc=0 / status 0) so failed retries don't pollute the map. + """ + pid_to_task: Dict[str, str] = {} + task_to_pid: Dict[str, str] = {} + with open(path) as fh: + for line in fh: + m = _PAT_OLD.match(line) or _PAT_NEW.match(line) + if m: + pid, name = m.group(1), m.group(2) + pid_to_task[pid] = name + task_to_pid[name] = pid + return pid_to_task, task_to_pid + + +# ── monitor-log parsing ─────────────────────────────────────────────────────── + +_RECORD_RE = re.compile(r'"?([^"]+)"?,(read|write),(.*)') + + +def parse_monitor_log( + path: str, + pid_to_task: Dict[str, str], + basedir: str, + file_filters: List[re.Pattern], +) -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]: + """Parse the fanotify raw log and map files to the tasks that touched them. + + Returns (file_written_by, file_read_by) where each value is a set of + task names. Only files inside *basedir* that pass *file_filters* and + are not excluded by the built-in exclude pattern are included. + + A file access is attributed to a task when any PID in the process-chain + column of the monitor log appears in *pid_to_task*. This works for both + direct children and children-of-children of the task process because the + fanotify monitor records the full ancestor chain up to the root PID. + """ + file_written: Dict[str, Set[str]] = {} + file_read: Dict[str, Set[str]] = {} + prefix = basedir_prefix(basedir) + + with open(path) as fh: + for line in fh: + m = _RECORD_RE.match(line) + if not m: + continue + fname, mode, chain = m.group(1), m.group(2), m.group(3) + + rel = relative_to_basedir(fname, prefix) + if rel is None or not keep_file(rel, file_filters): + continue + + for pid in chain.split(";"): + task = pid_to_task.get(pid) + if task is None: + continue + if mode == "write": + file_written.setdefault(rel, set()).add(task) + else: + file_read.setdefault(rel, set()).add(task) + + return file_written, file_read + + +# ── CLI ─────────────────────────────────────────────────────────────────────── + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument("--actionFile", required=True, + help="O2DPG pipeline runner action log") + p.add_argument("--monitorFile", required=True, + help="fanotify raw log from monitor_fileaccess_v2.exe") + add_common_arguments(p) + return p + + +def main(argv=None) -> None: + args = build_parser().parse_args(argv) + + file_filters = [re.compile(f) for f in args.file_filters] + + pid_to_task, task_to_pid = parse_action_log(args.actionFile) + if not pid_to_task: + print( + f"WARNING: no task completions found in {args.actionFile}.\n" + "Check that the action log is from a completed run and that\n" + "its format matches either the old or new O2DPG runner.", + file=sys.stderr, + ) + else: + print(f"Action log: {len(pid_to_task)} completed task(s) found") + + file_written, file_read = parse_monitor_log( + args.monitorFile, pid_to_task, args.basedir, file_filters, + ) + + emit(args, file_written, file_read, sorted(task_to_pid)) + + +if __name__ == "__main__": + main() diff --git a/UTILS/FileIOGraph/compare_reports.py b/UTILS/FileIOGraph/compare_reports.py new file mode 100755 index 000000000..588abae78 --- /dev/null +++ b/UTILS/FileIOGraph/compare_reports.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Grade one file-graph report against another. + + EXACT candidate and reference agree edge for edge + SAFE candidate is a superset: nothing missing, some extra + UNSAFE candidate misses an edge the reference has + +The asymmetry is the point: the runner deletes a file once every task +listed against it has finished, so a missing edge deletes a file a later +task still reads, while an extra one only keeps it on disc longer. +""" +from __future__ import annotations + +import argparse +import json +import sys +from typing import Dict, List, Set, Tuple + +SECTIONS = ("file_report", "file_template_report") +Edges = Dict[Tuple[str, str], Set[str]] + + +def _edges(report: Dict, section: str) -> Edges: + """(file, 'written_by'|'read_by') -> set of task names.""" + out: Edges = {} + for entry in report.get(section) or []: + f = entry.get("file") + if not f: + continue + for kind in ("written_by", "read_by"): + out[(f, kind)] = set(entry.get(kind) or []) + return out + + +def _diff(a: Edges, b: Edges) -> List[Tuple[str, str, List[str]]]: + """Edges present in a and not in b, as (file, kind, tasks).""" + out = [] + for (f, kind), tasks in a.items(): + extra = tasks - b.get((f, kind), set()) + if extra: + out.append((f, kind, sorted(extra))) + return sorted(out) + + +class SectionDiff: + def __init__(self, section: str, ref: Dict, cand: Dict): + self.section = section + re_, ce = _edges(ref, section), _edges(cand, section) + self.ref_files = {f for f, _ in re_} + self.cand_files = {f for f, _ in ce} + self.only_ref = sorted(self.ref_files - self.cand_files) + self.only_cand = sorted(self.cand_files - self.ref_files) + self.missing = _diff(re_, ce) + self.extra = _diff(ce, re_) + self.n_ref_edges = sum(len(v) for v in re_.values()) + self.n_cand_edges = sum(len(v) for v in ce.values()) + self.n_missing = sum(len(t) for _, _, t in self.missing) + self.n_extra = sum(len(t) for _, _, t in self.extra) + + @property + def verdict(self) -> str: + if self.n_missing: + return "UNSAFE" + return "SAFE" if self.n_extra else "EXACT" + + @property + def recall(self) -> float: + if not self.n_ref_edges: + return 1.0 + return (self.n_ref_edges - self.n_missing) / self.n_ref_edges + + def report(self, limit: int) -> str: + def block(header, items, fmt): + if not items: + return [] + out = [f" {header} ({len(items)}):"] + out += [f" {fmt(i)}" for i in items[:limit]] + if len(items) > limit: + out.append(f" ... {len(items) - limit} more") + return out + + lines = [ + f"[{self.section}]", + f" files reference={len(self.ref_files)} " + f"candidate={len(self.cand_files)} only-ref={len(self.only_ref)} " + f"only-cand={len(self.only_cand)}", + f" edges reference={self.n_ref_edges} " + f"candidate={self.n_cand_edges} missing={self.n_missing} " + f"extra={self.n_extra} recall={self.recall * 100:.2f}%", + f" verdict {self.verdict}", + ] + lines += block("files the candidate never saw", self.only_ref, str) + lines += block("MISSING edges", self.missing, + lambda e: f"- {e[0]} {e[1]}: {', '.join(e[2])}") + lines += block("extra edges, harmless", self.extra, + lambda e: f"+ {e[0]} {e[1]}: {', '.join(e[2])}") + return "\n".join(lines) + + +def compare(ref: Dict, cand: Dict, limit: int = 20) -> Tuple[str, Dict]: + diffs = [SectionDiff(s, ref, cand) for s in SECTIONS] + verdicts = [d.verdict for d in diffs] + overall = ("UNSAFE" if "UNSAFE" in verdicts + else "SAFE" if "SAFE" in verdicts else "EXACT") + summary = { + "verdict": overall, + "sections": { + d.section: { + "verdict": d.verdict, + "reference_edges": d.n_ref_edges, + "candidate_edges": d.n_cand_edges, + "missing_edges": d.n_missing, + "extra_edges": d.n_extra, + "recall": d.recall, + "files_only_in_reference": d.only_ref, + "files_only_in_candidate": d.only_cand, + "missing": [list(e) for e in d.missing], + "extra": [list(e) for e in d.extra], + } for d in diffs + }, + } + return "\n".join(d.report(limit) for d in diffs), summary + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--reference", required=True, help="the report to be reproduced") + ap.add_argument("--candidate", required=True, help="the report under test") + ap.add_argument("--limit", type=int, default=20, help="differences to print per class") + ap.add_argument("--json", default=None, help="also write the full diff here") + ap.add_argument("--allow", choices=["EXACT", "SAFE"], default="SAFE", + help="worst verdict that still exits 0 (default: SAFE)") + a = ap.parse_args(argv) + + with open(a.reference) as f: + ref = json.load(f) + with open(a.candidate) as f: + cand = json.load(f) + + text, summary = compare(ref, cand, a.limit) + print(f"reference: {a.reference}") + print(f"candidate: {a.candidate}") + print(text) + print(f"\nOVERALL: {summary['verdict']}") + if a.json: + with open(a.json, "w") as f: + json.dump(summary, f, indent=2) + print(f"wrote {a.json}") + + return 0 if summary["verdict"] in ("EXACT", a.allow) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/UTILS/FileIOGraph/filegraph_report.py b/UTILS/FileIOGraph/filegraph_report.py new file mode 100644 index 000000000..952110771 --- /dev/null +++ b/UTILS/FileIOGraph/filegraph_report.py @@ -0,0 +1,173 @@ +"""Build the file-task dependency report from two file->tasks maps. + +Holds the exclusion rules, the ./tfN -> ./tfX templating, the JSON schema +that --remove-files-early reads back, and the graphviz rendering. +""" +from __future__ import annotations + +import json +import re +import sys +from typing import Dict, Optional, Sequence, Set + +#: log files, the CCDB log and the DPL config are noise, not data flow +EXCLUDE_RE = re.compile(r'(.*\.log.*|ccdb/log|.*dpl-config\.json)') +TF_PATH_RE = re.compile(r'^\./tf(?P\d+)/') + + +def basedir_prefix(basedir: str) -> str: + return basedir.rstrip("/") + "/" + + +def relative_to_basedir(fname: str, prefix: str) -> Optional[str]: + """'./x/y' for a path under the prefix from basedir_prefix(), else None.""" + if not fname.startswith(prefix): + return None + return "./" + fname[len(prefix):] + + +def keep_file(rel: str, file_filters: Sequence) -> bool: + if EXCLUDE_RE.match(rel): + return False + return any(r.match(rel) for r in file_filters) + + +def task_template_for_timeframe(task_name: str, source_tf: int) -> str: + suffix = f"_{source_tf}" + if task_name.endswith(suffix): + return f"{task_name[:-len(suffix)]}_X" + return task_name + + +def build_report(file_written: Dict[str, Set[str]], + file_read: Dict[str, Set[str]], + tasks: Sequence[str]) -> Dict: + """Assemble the JSON document from the two file->tasks maps.""" + all_files = sorted(set(file_written) | set(file_read)) + file_report = [ + { + "file": f, + "written_by": sorted(file_written.get(f, set())), + "read_by": sorted(file_read.get(f, set())), + } + for f in all_files + ] + + templates: Dict[str, Dict] = {} + for entry in file_report: + match = TF_PATH_RE.match(entry["file"]) + if match is None: + continue + source_tf = int(match.group("tf")) + merged = templates.setdefault( + TF_PATH_RE.sub("./tfX/", entry["file"], count=1), + {"written_by": set(), "read_by": set(), "source_timeframes": set()}, + ) + merged["source_timeframes"].add(source_tf) + for kind in ("written_by", "read_by"): + for task in entry[kind]: + merged[kind].add(task_template_for_timeframe(task, source_tf)) + + file_template_report = [ + { + "file": f, + "written_by": sorted(v["written_by"]), + "read_by": sorted(v["read_by"]), + "source_timeframes": sorted(v["source_timeframes"]), + } + for f, v in sorted(templates.items()) + ] + + task_reads: Dict[str, Set[str]] = {} + task_writes: Dict[str, Set[str]] = {} + for f, ts in file_read.items(): + for t in ts: + task_reads.setdefault(t, set()).add(f) + for f, ts in file_written.items(): + for t in ts: + task_writes.setdefault(t, set()).add(f) + task_report = [ + { + "task": t, + "writes": sorted(task_writes.get(t, set())), + "reads": sorted(task_reads.get(t, set())), + } + for t in sorted(tasks) + ] + + return { + "file_report": file_report, + "file_template_report": file_template_report, + "task_report": task_report, + } + + +def draw_graph(filename: str, file_written: Dict[str, Set[str]], + file_read: Dict[str, Set[str]], tasks: Sequence[str]) -> None: + try: + from graphviz import Digraph + except ImportError: + print("graphviz not installed, skipping graph", file=sys.stderr) + return + + ccdb_re = re.compile(r"ccdb(.*)/snapshot\.root") + dot = Digraph(comment="O2DPG file-task network") + idx: Dict[str, int] = {} + + # CCDB snapshots are labelled by object path; a real workflow pulls + # dozens and the full names swamp the picture + ccdb, normal = [], [] + for f in set(file_written) | set(file_read): + m = ccdb_re.match(f) + (ccdb if m else normal).append((f, m.group(1) if m else f)) + + with dot.subgraph(name="CCDB") as sg: + sg.attr(color="blue") + for f, label in ccdb: + idx[f] = len(idx) + sg.node(str(idx[f]), label, color="blue") + + with dot.subgraph(name="normal") as sg: + sg.attr(color="black") + for f, label in normal: + idx[f] = len(idx) + sg.node(str(idx[f]), label, color="red") + for t in tasks: + idx[t] = len(idx) + sg.node(str(idx[t]), t, shape="box", color="green", style="filled") + + for f, ts in file_read.items(): + for t in ts: + dot.edge(str(idx[f]), str(idx[t])) + for f, ts in file_written.items(): + for t in ts: + dot.edge(str(idx[t]), str(idx[f])) + + dot.render(filename, format="pdf") + dot.render(filename, format="gv") + print(f"Wrote {filename}.pdf and {filename}.gv") + + +def add_common_arguments(parser) -> None: + parser.add_argument("--basedir", default="/", + help="Workflow working directory (default: /)") + parser.add_argument("--file-filters", nargs="+", default=[r".*"], + help="Regex filters to select file paths (default: all)") + parser.add_argument("--graphviz", default=None, + help="also render the file/task network with this " + "base filename") + parser.add_argument("-o", "--output", required=True, + help="Output JSON report path") + + +def emit(args, file_written: Dict[str, Set[str]], file_read: Dict[str, Set[str]], + tasks: Sequence[str]) -> None: + """Render and write what add_common_arguments() asked for.""" + if args.graphviz: + draw_graph(args.graphviz, file_written, file_read, tasks) + doc = build_report(file_written, file_read, tasks) + with open(args.output, "w") as fh: + json.dump(doc, fh, indent=2) + print(f"Wrote {args.output}: {len(doc['file_report'])} file(s) referenced, " + f"{len(doc['file_template_report'])} timeframe file template(s), " + f"{len(doc['task_report'])} task(s) mapped") diff --git a/UTILS/FileIOGraph/monitor_fileaccess_v2.cpp b/UTILS/FileIOGraph/monitor_fileaccess_v2.cpp index 2f3750bc6..b3d89ae8b 100644 --- a/UTILS/FileIOGraph/monitor_fileaccess_v2.cpp +++ b/UTILS/FileIOGraph/monitor_fileaccess_v2.cpp @@ -180,7 +180,12 @@ int main(int argc, char **argv) { if (metadata->mask & FAN_Q_OVERFLOW) { - fprintf(stderr, "Queue overflow!\n"); + // 'continue' here would re-test the same event forever, because it + // skips FAN_EVENT_NEXT; report and advance instead. + printf("#OVERFLOW\n"); + fflush(stdout); + fprintf(stderr, "Queue overflow! events were dropped\n"); + metadata = FAN_EVENT_NEXT(metadata, buflen); continue; } snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", metadata->fd); diff --git a/UTILS/FileIOGraph/tests/equivalence_test.py b/UTILS/FileIOGraph/tests/equivalence_test.py new file mode 100755 index 000000000..04541ee5b --- /dev/null +++ b/UTILS/FileIOGraph/tests/equivalence_test.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Build a workflow whose file-IO graph is known, run it under one or more +backends, and grade every report against that graph and against a reference. + + equivalence_test.py --backends strace --reference fanotify --ntf 8 +""" +from __future__ import annotations + +import argparse +import glob +import json +import os +import subprocess +import sys +import time + +from filegraph_test_support import REPO, filegraph + +from compare_reports import compare # noqa: E402 +from filegraph_report import build_report # noqa: E402 + +RESOURCES = {"cpu": 1, "mem": 100, "relative_cpu": 1.0} + + +def build_workflow(ntf: int, sleep: str): + """A workflow plus the file->tasks maps its commands imply. + + Covers what makes attribution hard: a global task consuming every + timeframe, a file with two readers, a subdirectory created at run time, + four different libc entry points, and tasks that overlap in time. + """ + stages, written, read = [], {}, {} + + def w(f, t): + written.setdefault(f, set()).add(t) + + def r(f, t): + read.setdefault(f, set()).add(t) + + def stage(name, cmd, needs, cwd, tf, label): + stages.append({"name": name, "cmd": f"sleep {sleep}; {cmd}", + "needs": needs, "cwd": cwd, "timeframe": tf, + "labels": [label], "resources": RESOURCES}) + + stage("bkg", "echo bkgdata > bkg.dat", [], "./", -1, "SIM") + w("./bkg.dat", "bkg") + + for i in range(1, ntf + 1): + tf = f"tf{i}" + # shell redirection and Python open() in one task + stage(f"sgnsim_{i}", + f"cat ../bkg.dat > sgn.dat; " + f"python3 -c \"open('kine.dat','w').write('kine{i}')\"", + ["bkg"], f"./{tf}", i, "SIM") + r("./bkg.dat", f"sgnsim_{i}") + w(f"./{tf}/sgn.dat", f"sgnsim_{i}") + w(f"./{tf}/kine.dat", f"sgnsim_{i}") + + # cp, and a subdirectory created while the workflow is running + stage(f"digi_{i}", "cp sgn.dat digi.dat; mkdir -p sub; " + "cp digi.dat sub/extra.dat", + [f"sgnsim_{i}"], f"./{tf}", i, "DIGI") + r(f"./{tf}/sgn.dat", f"digi_{i}") + w(f"./{tf}/digi.dat", f"digi_{i}") + r(f"./{tf}/digi.dat", f"digi_{i}") + w(f"./{tf}/sub/extra.dat", f"digi_{i}") + + # awk opens its output file itself, through fopen + stage(f"reco_{i}", "awk '{print > \"reco.dat\"}' digi.dat; " + "cat kine.dat >> reco.dat", + [f"digi_{i}"], f"./{tf}", i, "RECO") + r(f"./{tf}/digi.dat", f"reco_{i}") + r(f"./{tf}/kine.dat", f"reco_{i}") + w(f"./{tf}/reco.dat", f"reco_{i}") + + stage(f"aod_{i}", "cat reco.dat sub/extra.dat > AO2D.dat", + [f"reco_{i}"], f"./{tf}", i, "AOD") + r(f"./{tf}/reco.dat", f"aod_{i}") + r(f"./{tf}/sub/extra.dat", f"aod_{i}") + w(f"./{tf}/AO2D.dat", f"aod_{i}") + r(f"./tf{i}/AO2D.dat", "aodmerge") + + inputs = " ".join(f"tf{i}/AO2D.dat" for i in range(1, ntf + 1)) + stage("aodmerge", f"cat {inputs} > AO2D_merged.dat", + [f"aod_{i}" for i in range(1, ntf + 1)], "./", -1, "AOD") + w("./AO2D_merged.dat", "aodmerge") + + truth = build_report(written, read, [s["name"] for s in stages]) + return {"stages": stages}, truth + + +def run(argv, **kw): + print("+ " + " ".join(argv), flush=True) + return subprocess.run(argv, **kw) + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--workdir", default=None, + help="scratch directory (default: a fresh one under $TMPDIR)") + ap.add_argument("--backends", default=None, + help="comma-separated backends to test (default: all but " + f"the reference; known: {', '.join(sorted(filegraph.BACKENDS))})") + ap.add_argument("--reference", default="fanotify", + help="backend to compare the others against ('' to skip)") + ap.add_argument("--ntf", type=int, default=4) + ap.add_argument("--cpu-limit", type=int, default=4) + ap.add_argument("--sleep", default="0.4") + ap.add_argument("--python", default=sys.executable) + a = ap.parse_args(argv) + + runner = os.path.join(REPO, "MC", "workflow_runner", + "o2dpg_workflow_runner.py") + if not os.path.exists(runner): + print(f"no runner at {runner}", file=sys.stderr) + return 2 + + ref = a.reference.strip() + candidates = ([b.strip() for b in a.backends.split(",") if b.strip()] + if a.backends is not None + else [b for b in sorted(filegraph.BACKENDS) if b != ref]) + active = list(dict.fromkeys(([ref] if ref else []) + candidates)) + if not active: + print("nothing to run", file=sys.stderr) + return 2 + + workdir = a.workdir or os.path.join( + os.getenv("TMPDIR", "/tmp"), f"filegraph_equiv_{os.getpid()}") + os.makedirs(workdir, exist_ok=True) + print(f"workdir: {workdir}") + + wf, truth = build_workflow(a.ntf, a.sleep) + for name, doc in (("workflow.json", wf), ("truth.json", truth)): + with open(os.path.join(workdir, name), "w") as f: + json.dump(doc, f, indent=2) + print(f"{len(wf['stages'])} stages, {len(truth['file_report'])} files expected") + + t0 = time.time() + r = run([a.python, runner, "-f", "workflow.json", + "--cpu-limit", str(a.cpu_limit), "--mem-limit", "8000", + "--filegraph-backends", ",".join(active)], + cwd=workdir, env=dict(os.environ, O2DPG_ROOT=REPO)) + if r.returncode != 0: + print(f"runner failed with rc={r.returncode}", file=sys.stderr) + return 2 + print(f"workflow finished in {time.time() - t0:.1f}s") + + reports = {} + for b in active: + found = glob.glob(os.path.join(workdir, f"filegraph_{b}_*.json")) + if found: + with open(found[0]) as f: + reports[b] = json.load(f) + + failures = [] + + def grade(title, reference, candidate, backend, why): + print(f"\n{'=' * 72}\n== {title}\n{'=' * 72}") + text, summary = compare(reference, candidate, limit=10) + print(text) + print(f"OVERALL: {summary['verdict']}") + if summary["verdict"] == "UNSAFE": + failures.append((backend, why)) + return summary + + for backend in active: + if backend not in reports: + print(f"\nNO REPORT from {backend}", file=sys.stderr) + if backend != ref: + failures.append((backend, "no report")) + continue + + if backend == ref: + # fanotify resolves the process chain from /proc after the event, + # so on a fast machine it loses accesses by processes that exited + print(f"\n{'=' * 72}\n== {backend} (reference) vs truth\n{'=' * 72}") + text, summary = compare(truth, reports[backend], limit=10) + print(text) + print(f"OVERALL: {summary['verdict']}") + if summary["verdict"] == "UNSAFE": + print(f"NOTE: the reference {backend} itself misses edges") + continue + + grade(f"{backend} vs truth", truth, reports[backend], backend, + "misses edges the truth has") + if ref in reports: + summary = grade(f"{backend} vs {ref} (reference)", reports[ref], + reports[backend], backend, f"misses edges {ref} has") + with open(os.path.join(workdir, f"diff_{backend}_vs_{ref}.json"), "w") as f: + json.dump(summary, f, indent=2) + + print(f"\n{'=' * 72}") + for b, why in failures: + print(f"FAIL {b}: {why}") + if not failures: + print("PASS every backend is at least SAFE against truth" + + (f" and against {ref}" if ref else "")) + print(f"workdir kept at {workdir}") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/UTILS/FileIOGraph/tests/filegraph_test_support.py b/UTILS/FileIOGraph/tests/filegraph_test_support.py new file mode 100644 index 000000000..69ee796c8 --- /dev/null +++ b/UTILS/FileIOGraph/tests/filegraph_test_support.py @@ -0,0 +1,20 @@ +"""Put the FileIOGraph tools and the runner package on sys.path. + +Imported first by every test module here, so there is one bootstrap +instead of one per file. +""" +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +FILEGRAPH_DIR = os.path.dirname(_HERE) +REPO = os.path.dirname(os.path.dirname(FILEGRAPH_DIR)) +RUNNER_BIN = os.path.join(REPO, "MC", "workflow_runner") + +for _p in (FILEGRAPH_DIR, RUNNER_BIN): + if _p not in sys.path: + sys.path.insert(0, _p) + +from o2dpg_runner import filegraph # noqa: E402 + +__all__ = ["FILEGRAPH_DIR", "REPO", "RUNNER_BIN", "filegraph"] diff --git a/UTILS/FileIOGraph/tests/test_analyse_strace.py b/UTILS/FileIOGraph/tests/test_analyse_strace.py new file mode 100644 index 000000000..66a105e5b --- /dev/null +++ b/UTILS/FileIOGraph/tests/test_analyse_strace.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Offline tests for the strace analyser.""" +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import tempfile +import unittest + +from filegraph_test_support import FILEGRAPH_DIR # noqa: F401 + +import analyse_FileIO_strace as A # noqa: E402 + +ALL = [re.compile(r".*")] + +# What `strace -f -qq -y -e status=successful` really writes. The last two +# lines of the open block are a call split around another process's entry. +TRACE = '''\ +101 openat(AT_FDCWD, "sgn.dat", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 +101 openat(AT_FDCWD, "../bkg.dat", O_RDONLY) = 4 +102 openat(AT_FDCWD, "kine.dat", O_RDONLY|O_CLOEXEC) = 3 +102 openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 +102 openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 8 +101 openat(AT_FDCWD, "reco.dat", O_WRONLY|O_CREAT|O_APPEND +102 openat(AT_FDCWD, "digi.dat", O_RDONLY) = 5 +101 <... openat resumed>, 0666) = 6 +101 renameat2(AT_FDCWD, "tmp.dat", AT_FDCWD, "final.dat", 0) = 0 +''' + + +class TestParseTrace(unittest.TestCase): + @classmethod + def setUpClass(cls): + with tempfile.TemporaryDirectory() as d: + p = os.path.join(d, "trace_0_reco_1.log") + with open(p, "w") as f: + f.write(TRACE) + cls.seen, cls.calls = A.parse_trace(p) + + def test_resolved_path_wins_over_the_argument(self): + # the argument said '../bkg.dat'; -y resolved it + self.assertIn(("/w/bkg.dat", "read"), self.seen) + + def test_write_flags_classify_as_write(self): + self.assertIn(("/w/tf1/sgn.dat", "write"), self.seen) + self.assertIn(("/w/tf1/kine.dat", "read"), self.seen) + + def test_split_call_keeps_its_flags(self): + # O_WRONLY appeared on the '' half only + self.assertIn(("/w/tf1/reco.dat", "write"), self.seen) + + def test_interleaved_call_is_not_swallowed(self): + self.assertIn(("/w/tf1/digi.dat", "read"), self.seen) + + def test_rename_destination_counts_as_written(self): + self.assertIn(("final.dat", "write"), self.seen) + + def test_directory_open_is_not_part_of_the_graph(self): + self.assertNotIn("/w/tf1", [p for p, _ in self.seen]) + + def test_repeated_accesses_collapse(self): + with tempfile.TemporaryDirectory() as d: + p = os.path.join(d, "trace_0_t.log") + line = ('9 openat(AT_FDCWD, "a.dat", O_RDONLY) = 3\n') + with open(p, "w") as f: + f.write(line * 500) + seen, calls = A.parse_trace(p) + self.assertEqual(seen, {("/w/a.dat", "read")}) + self.assertEqual(calls, 500) + + +class TestCollect(unittest.TestCase): + def test_attribution_comes_from_the_file_name(self): + with tempfile.TemporaryDirectory() as d: + with open(os.path.join(d, "trace_3_reco_1.log"), "w") as f: + f.write(TRACE) + with open(os.path.join(d, "trace_4_aod_1.log"), "w") as f: + f.write('7 openat(AT_FDCWD, "reco.dat", O_RDONLY) ' + '= 3\n') + written, read, tasks, stats = A.collect(d, "/w", ALL) + + self.assertEqual(tasks, ["aod_1", "reco_1"]) + self.assertEqual(written["./tf1/sgn.dat"], {"reco_1"}) + self.assertEqual(read["./tf1/reco.dat"], {"aod_1"}) + # outside the working directory, so not part of the graph + self.assertNotIn("/usr/lib/libc.so.6", written) + self.assertNotIn("/usr/lib/libc.so.6", read) + + def test_non_trace_files_are_ignored(self): + with tempfile.TemporaryDirectory() as d: + with open(os.path.join(d, "notes.txt"), "w") as f: + f.write("nothing\n") + _, _, tasks, stats = A.collect(d, "/w", ALL) + self.assertEqual(tasks, []) + self.assertEqual(stats["traces"], 0) + + +class TestCli(unittest.TestCase): + def test_end_to_end(self): + with tempfile.TemporaryDirectory() as d: + td = os.path.join(d, "traces") + os.makedirs(td) + with open(os.path.join(td, "trace_1_sgnsim_1.log"), "w") as f: + f.write('9 openat(AT_FDCWD, "sgn.dat", O_WRONLY|O_CREAT, ' + '0666) = 3\n') + with open(os.path.join(td, "trace_2_digi_1.log"), "w") as f: + f.write('9 openat(AT_FDCWD, "sgn.dat", O_RDONLY) ' + '= 3\n') + out = os.path.join(d, "report.json") + subprocess.run( + [sys.executable, + os.path.join(FILEGRAPH_DIR, "analyse_FileIO_strace.py"), + "--straceDir", td, "--basedir", "/w", "-o", out], + check=True, stdout=subprocess.DEVNULL) + with open(out) as f: + doc = json.load(f) + + entry = doc["file_report"][0] + self.assertEqual(entry["file"], "./tf1/sgn.dat") + self.assertEqual(entry["written_by"], ["sgnsim_1"]) + self.assertEqual(entry["read_by"], ["digi_1"]) + tpl = doc["file_template_report"][0] + self.assertEqual(tpl["file"], "./tfX/sgn.dat") + self.assertEqual(tpl["written_by"], ["sgnsim_X"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/UTILS/FileIOGraph/tests/test_filegraph.py b/UTILS/FileIOGraph/tests/test_filegraph.py new file mode 100644 index 000000000..960c452f9 --- /dev/null +++ b/UTILS/FileIOGraph/tests/test_filegraph.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Offline tests for the report machinery, the comparison and the backends. + +Plain unittest: this has to run on the bare interpreter of a GRID worker +or a CVMFS O2 environment, neither of which has pytest. + + python3 -m unittest discover -s UTILS/FileIOGraph/tests -t UTILS/FileIOGraph/tests +""" +from __future__ import annotations + +import json +import logging +import os +import re +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +from filegraph_test_support import FILEGRAPH_DIR, filegraph + +import equivalence_test # noqa: E402 +from compare_reports import compare # noqa: E402 +from filegraph_report import ( # noqa: E402 + basedir_prefix, build_report, keep_file, relative_to_basedir, +) + +ALL = [re.compile(r".*")] +LOG = logging.getLogger("test") + + +class TestPathHelpers(unittest.TestCase): + def test_relative_to_basedir(self): + for base in ("/w", "/w/"): + prefix = basedir_prefix(base) + self.assertEqual(relative_to_basedir("/w/tf1/a.root", prefix), + "./tf1/a.root") + self.assertIsNone(relative_to_basedir("/usr/lib/libc.so", prefix)) + + def test_logs_and_dpl_config_are_noise(self): + self.assertFalse(keep_file("./tf1/sgnsim_1.log", ALL)) + self.assertFalse(keep_file("./tf1/sgnsim_1.log_done", ALL)) + self.assertFalse(keep_file("./dpl-config.json", ALL)) + self.assertTrue(keep_file("./tf1/AO2D.root", ALL)) + + def test_filters_apply(self): + self.assertTrue(keep_file("./tf1/a.root", [re.compile(r".*\.root")])) + self.assertFalse(keep_file("./tf1/a.dat", [re.compile(r".*\.root")])) + + +class TestBuildReport(unittest.TestCase): + def setUp(self): + self.doc = build_report( + {"./tf1/a.root": {"digi_1"}, "./tf2/a.root": {"digi_2"}, + "./AO2D.root": {"aodmerge"}}, + {"./tf1/a.root": {"reco_1", "aodmerge"}, + "./tf2/a.root": {"reco_2", "aodmerge"}}, + ["digi_1", "digi_2", "reco_1", "reco_2", "aodmerge"]) + self.tpl = {e["file"]: e for e in self.doc["file_template_report"]} + + def test_timeframe_tasks_become_templates(self): + self.assertEqual(self.tpl["./tfX/a.root"]["written_by"], ["digi_X"]) + self.assertEqual(self.tpl["./tfX/a.root"]["source_timeframes"], [1, 2]) + + def test_global_reader_is_not_templated(self): + # a naive "strip the trailing _N" would corrupt aodmerge + self.assertIn("aodmerge", self.tpl["./tfX/a.root"]["read_by"]) + self.assertIn("reco_X", self.tpl["./tfX/a.root"]["read_by"]) + + def test_non_timeframe_file_absent_from_templates(self): + self.assertNotIn("./AO2D.root", self.tpl) + + def test_task_report_covers_every_task(self): + self.assertEqual([t["task"] for t in self.doc["task_report"]], + ["aodmerge", "digi_1", "digi_2", "reco_1", "reco_2"]) + + +class TestCompare(unittest.TestCase): + def _verdict(self, cand_written, cand_read): + ref = build_report({"./tf1/a.root": {"digi_1"}}, + {"./tf1/a.root": {"reco_1"}}, ["digi_1", "reco_1"]) + cand = build_report(cand_written, cand_read, ["digi_1", "reco_1"]) + return compare(ref, cand)[1] + + def test_identical_is_exact(self): + s = self._verdict({"./tf1/a.root": {"digi_1"}}, {"./tf1/a.root": {"reco_1"}}) + self.assertEqual(s["verdict"], "EXACT") + + def test_extra_reader_is_safe(self): + s = self._verdict({"./tf1/a.root": {"digi_1"}}, + {"./tf1/a.root": {"reco_1", "qc_1"}}) + self.assertEqual(s["verdict"], "SAFE") + self.assertEqual(s["sections"]["file_report"]["missing_edges"], 0) + + def test_missing_reader_is_unsafe(self): + s = self._verdict({"./tf1/a.root": {"digi_1"}}, {}) + self.assertEqual(s["verdict"], "UNSAFE") + self.assertEqual(s["sections"]["file_report"]["missing_edges"], 1) + self.assertAlmostEqual(s["sections"]["file_report"]["recall"], 0.5) + + def test_missing_file_is_unsafe(self): + self.assertEqual(self._verdict({}, {})["verdict"], "UNSAFE") + + +class TestAnalyseFanotifyLog(unittest.TestCase): + """End-to-end on the fanotify analyser, so the shared module stays honest.""" + + ACTION = ( + "2026-05-06 16:49:18,267 INFO Task pid=101 tid=0 bkg finished rc=0\n" + "2026-05-06 16:49:19,267 INFO Task pid=102 tid=1 sgnsim_1 finished rc=0\n" + "2026-05-06 16:49:20,267 INFO Task pid=103 tid=2 reco_1 finished rc=0\n" + ) + + def test_report(self): + with tempfile.TemporaryDirectory() as d: + action = os.path.join(d, "action.log") + monitor = os.path.join(d, "monitor.log") + out = os.path.join(d, "report.json") + with open(action, "w") as f: + f.write(self.ACTION) + with open(monitor, "w") as f: + f.write(f'"{d}/bkg.dat",write,101\n' + f'"{d}/tf1/sgn.dat",write,201;102\n' + f'"{d}/bkg.dat",read,201;102\n' + f'"{d}/tf1/sgn.dat",read,103\n' + f'"{d}/tf1/reco_1.log",write,103\n' + f'"/usr/lib/libc.so.6",read,103\n') + subprocess.run( + [sys.executable, os.path.join(FILEGRAPH_DIR, "analyse_FileIO_v2.py"), + "--actionFile", action, "--monitorFile", monitor, + "--basedir", d, "-o", out], + check=True, stdout=subprocess.DEVNULL) + with open(out) as f: + doc = json.load(f) + + got = {e["file"]: (e["written_by"], e["read_by"]) for e in doc["file_report"]} + self.assertEqual(got["./bkg.dat"], (["bkg"], ["sgnsim_1"])) + # a grandchild's access is attributed through the parent chain + self.assertEqual(got["./tf1/sgn.dat"], (["sgnsim_1"], ["reco_1"])) + self.assertNotIn("./tf1/reco_1.log", got) + self.assertNotIn("/usr/lib/libc.so.6", got) + + +class StubBackend(filegraph.FileGraphBackend): + name = "stub" + + def wrap(self, argv, taskname, tid): + return ["stub", taskname, str(tid), "--"] + argv + + +def manager(spec, action_log="action.log"): + return filegraph.FileGraphManager.from_config(spec, "/w", 4242, action_log, LOG) + + +class TestBackendSelection(unittest.TestCase): + def setUp(self): + patch = mock.patch.dict(os.environ, {}, clear=False) + patch.start() + self.addCleanup(patch.stop) + for k in ("O2DPG_PRODUCE_FILEGRAPH", "O2DPG_FILEGRAPH_BACKENDS"): + os.environ.pop(k, None) + filegraph.BACKENDS["stub"] = StubBackend + self.addCleanup(filegraph.BACKENDS.pop, "stub", None) + + def test_nothing_requested_means_no_backend(self): + self.assertEqual(manager("").backends, []) + + def test_named_backends_are_built_in_order(self): + self.assertEqual([b.name for b in manager("stub,fanotify").backends], + ["stub", "fanotify"]) + + def test_legacy_variable_selects_fanotify_with_that_exe(self): + os.environ["O2DPG_PRODUCE_FILEGRAPH"] = "/opt/mon.exe" + m = manager("") + self.assertEqual([b.name for b in m.backends], ["fanotify"]) + self.assertEqual(m.backends[0].exe, "/opt/mon.exe") + + def test_unknown_backend_is_skipped_not_fatal(self): + self.assertEqual([b.name for b in manager("stub,nonsense").backends], ["stub"]) + + def test_the_action_log_reaches_the_backend_that_needs_it(self): + b = manager("fanotify", action_log="pipeline_action_7.log").backends[0] + self.assertIn("pipeline_action_7.log", b.analyser_args()) + + def test_only_fanotify_writes_the_legacy_report_name(self): + self.assertTrue(filegraph.FanotifyBackend.legacy_report) + self.assertFalse(StubBackend.legacy_report) + + +class TestBackendWrapping(unittest.TestCase): + ARGV = ["/bin/bash", "-c", "echo hi"] + + def setUp(self): + filegraph.BACKENDS["stub"] = StubBackend + self.addCleanup(filegraph.BACKENDS.pop, "stub", None) + + def test_a_backend_that_does_not_wrap_leaves_the_command_alone(self): + self.assertEqual(manager("fanotify").wrap(list(self.ARGV), "sgnsim_1", 3), + self.ARGV) + + def test_a_wrapping_backend_keeps_the_command_as_the_tail(self): + argv = manager("stub").wrap(list(self.ARGV), "sgnsim_1", 3) + self.assertEqual(argv[:4], ["stub", "sgnsim_1", "3", "--"]) + self.assertEqual(argv[4:], self.ARGV) + + def test_an_empty_manager_is_a_no_op(self): + m = manager("") + self.assertEqual(m.wrap(list(self.ARGV), "sgnsim_1", 3), self.ARGV) + m.start() + m.stop() + self.assertEqual(m.analyse(), {}) + + +class TestFilegraphDir(unittest.TestCase): + def test_resolves_without_o2dpg_root(self): + with mock.patch.dict(os.environ, {}, clear=False): + os.environ.pop("O2DPG_ROOT", None) + d = filegraph.filegraph_dir() + self.assertTrue(os.path.exists(os.path.join(d, "analyse_FileIO_v2.py")), d) + + +class TestSyntheticWorkflow(unittest.TestCase): + def test_the_truth_matches_the_commands(self): + wf, truth = equivalence_test.build_workflow(2, "0") + files = {e["file"]: e for e in truth["file_report"]} + self.assertEqual(files["./tf1/AO2D.dat"]["read_by"], ["aodmerge"]) + # a run-time subdirectory is part of the expected graph + self.assertEqual(files["./tf1/sub/extra.dat"]["written_by"], ["digi_1"]) + for s in wf["stages"]: + self.assertIn("cwd", s) + self.assertIn("resources", s) + self.assertIn("timeframe", s) + + +if __name__ == "__main__": + unittest.main() From ca79d62d20c2f09efb9b251b4ed8b053c3793001 Mon Sep 17 00:00:00 2001 From: Matthias Kleiner Date: Fri, 31 Jul 2026 08:14:56 +0200 Subject: [PATCH 045/144] MapCreation: avoid copying same file twice to the output folder - use 2 replicas instead of 1 - use proper O2 daily tag - store output alien path of stage 2 --- .../configurations/mapCreationGrid/README.md | 7 +++- .../mapCreationGrid/mapCreationJob.sh | 2 +- .../mapCreationGrid/postProcessJob.sh | 7 +++- .../mapCreationGrid/submitMapCreation.sh | 2 +- .../mapCreationGrid/submitPostProcess.sh | 32 +++++++++++++++++-- 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/DATA/production/configurations/mapCreationGrid/README.md b/DATA/production/configurations/mapCreationGrid/README.md index d022fe8f5..31dd71544 100644 --- a/DATA/production/configurations/mapCreationGrid/README.md +++ b/DATA/production/configurations/mapCreationGrid/README.md @@ -353,7 +353,12 @@ Then: This downloads the *existing* manifest stage 1 already uploaded (does not rebuild it — see the comment in the script for why that matters), re-stages the three macros if `MACRO_SOURCE=alien`, and submits one -production, same slot granularity as stage 1. +production, same slot granularity as stage 1. It also captures and uploads its own workdir pointer, +`stage2Workdir..txt`, to `MACRO_ALIEN_DIR` — the same mechanism stage 1 uses for its own output +(see Step 3), so an external consumer that needs to locate a specific slot's final `FT_*.root`/smoothed +map (e.g. a downstream pipeline using this stage's output as an input) can reconstruct the path the same +way stage 2 itself reconstructs stage 1's — read `stage2Workdir..txt` for the base, recompute +`COUNTERWIDTH` from this submission's manifest line count, join with the zero-padded subjob index. **Local single-slot test:** diff --git a/DATA/production/configurations/mapCreationGrid/mapCreationJob.sh b/DATA/production/configurations/mapCreationGrid/mapCreationJob.sh index 0e0d2c65c..e5bae7188 100755 --- a/DATA/production/configurations/mapCreationGrid/mapCreationJob.sh +++ b/DATA/production/configurations/mapCreationGrid/mapCreationJob.sh @@ -55,7 +55,7 @@ MACRO_ALIEN_DIR_BARE=${MACRO_ALIEN_DIR_BARE:-} # expect to change it in the wrapper when the data changes -- not here. Empty ("") is a supported value # meaning "skip bad-ranges filtering entirely for this submission" -- see BAD_RANGES_ARG below. BAD_RANGES_NAME=${BAD_RANGES_NAME:-Merge_BadIntervals_LHC26_PbPb_DCA-0P300.txt} -#JDL_OUTPUT=*.log@disk=1,*.txt@disk=1,*.info@disk=1 +#JDL_OUTPUT=*.log@disk=2,*.txt@disk=2,*.info@disk=2 #JDL_REQUIRE={member(other.GridPartitions,"multicore_8") && (other.CE != "ALICE::NIHAM::PBS64") && (other.CE != "ALICE::KISTI_GSDC::LCG") && (other.CE != "ALICE::SaoPaulo::LCG")}; # Site blacklist: these three CEs excluded from this pipeline's jobs. Setting #JDL_REQUIRE overrides # grid_submit.sh's own default Requirements entirely (it's read verbatim, not merged -- see diff --git a/DATA/production/configurations/mapCreationGrid/postProcessJob.sh b/DATA/production/configurations/mapCreationGrid/postProcessJob.sh index 676bcf459..e555d09cc 100755 --- a/DATA/production/configurations/mapCreationGrid/postProcessJob.sh +++ b/DATA/production/configurations/mapCreationGrid/postProcessJob.sh @@ -44,7 +44,7 @@ STAGE1_OUTPUTDIR_BASE= COUNTERWIDTH=3 MACRO_SOURCE=official MACRO_ALIEN_DIR_BARE=${MACRO_ALIEN_DIR_BARE:-} -#JDL_OUTPUT=*.log@disk=1,*.txt@disk=1,*.png@disk=1 +#JDL_OUTPUT=*.log@disk=2,*.txt@disk=2,*.png@disk=2 #JDL_REQUIRE={member(other.GridPartitions,"multicore_8") && (other.CE != "ALICE::NIHAM::PBS64") && (other.CE != "ALICE::KISTI_GSDC::LCG") && (other.CE != "ALICE::SaoPaulo::LCG")}; # Site blacklist -- same three CEs excluded as stage 1, see that script's comment for why the # GridPartitions clause has to be repeated here (grid_submit.sh replaces its whole default Requirements, @@ -215,6 +215,11 @@ echo "${STAGE1_OUTPUT_LOCATION}/${STAGE1_OUTPUT_FILE}" > "${STAGE1_SOURCE_FILE}" if ! alien.py cp -f "file:${STAGE1_SOURCE_FILE}" "${ALIEN_OUTPUT_DIR}/${STAGE1_SOURCE_FILE}"; then echo "WARNING: failed to stage ${STAGE1_SOURCE_FILE} -- provenance pointer only, not fatal" >&2 fi +# Remove the local copy now that it's staged: it matches the #JDL_OUTPUT *.txt glob above, and the +# JobAgent's own passive upload of that glob (run after this script exits) does a plain, non-forced +# cp -- it would otherwise collide with the LFN this explicit -f upload just created and put the +# whole subjob into ERROR_SV, even though the actual deliverable upload above succeeded. +rm -f "${STAGE1_SOURCE_FILE}" # --- QA plots (voxResQA.C) -- runs on just this one slot's smoothed map, same per-slot granularity as # everything else in this pipeline. Non-fatal on failure: QA is a diagnostic nice-to-have, not a blocking diff --git a/DATA/production/configurations/mapCreationGrid/submitMapCreation.sh b/DATA/production/configurations/mapCreationGrid/submitMapCreation.sh index 72d80cc6b..f63b5cd7b 100755 --- a/DATA/production/configurations/mapCreationGrid/submitMapCreation.sh +++ b/DATA/production/configurations/mapCreationGrid/submitMapCreation.sh @@ -65,7 +65,7 @@ TOPWORKDIR="MapCreation" # replaces grid_submit.sh's generic default "selfjobs # like the real input data's own convention (.../2026/LHC26ak/572244/...) -- # that needs a real post-processing/organize step, deliberately deferred to a # later iteration of this pipeline. -PACKAGESPEC="O2::daily-20260729-0000-1" +PACKAGESPEC="O2::daily-20260730-0000-1" ASUSER="pwg_pp" # your own account = personal quota. "pwg_pp" = charged to that working group's quota AND # moves MY_JOBWORKDIR/OutputDir to pwg_pp's own space automatically -- see the # long comment above for why this one value now does everything. NOTE support diff --git a/DATA/production/configurations/mapCreationGrid/submitPostProcess.sh b/DATA/production/configurations/mapCreationGrid/submitPostProcess.sh index dce0500b7..4f34df062 100755 --- a/DATA/production/configurations/mapCreationGrid/submitPostProcess.sh +++ b/DATA/production/configurations/mapCreationGrid/submitPostProcess.sh @@ -32,6 +32,13 @@ # script doesn't need its own OutputDir logic, it just needs to read/write consistently with whatever # stage 1 did. # +# This wrapper also captures and uploads ITS OWN workdir pointer (stage2Workdir..txt), the same +# way stage 1 does for its own output -- nothing inside this pipeline needed that before (stage 2 is the +# end of the line here), but an external consumer that needs to locate a specific slot's final +# FT_*.root/smoothed map (e.g. a downstream pipeline using it as an input) has no other way to find it, +# since it also lands in an ephemeral ${ALIEN_JDL_OUTPUTDIR}, not a fixed path. Purely additive: this +# stage's own control flow and outputs are unchanged, it just also records where they went. +# # Usage: # ./submitPostProcess.sh @@ -49,7 +56,7 @@ STAGE1_JOBNAME_TAG="mapcreation-v1" # must equal the JOBNAME stage 1 (sub # actually submitted with -- that's what determined where # stage 1 staged its map. TOPWORKDIR="MapCreation" -PACKAGESPEC="O2::daily-20260729-0000-1" +PACKAGESPEC="O2::daily-20260730-0000-1" ASUSER="pwg_pp" # see the long comment above -- your own account for personal quota, "pwg_pp" for group # quota (this stage's own submission; independent of what stage 1 used). MACRO_SOURCE="official" # same meaning/default as stage 1 (submitMapCreation.sh) -- applies @@ -104,7 +111,8 @@ COUNTERWIDTH=${#N} TMP_SCRIPT="$(mktemp)" TMP_WORKDIR_POINTER="$(mktemp)" -trap 'rm -f "${TMP_MANIFEST}" "${TMP_SCRIPT:-}" "${TMP_WORKDIR_POINTER}"' EXIT +TMP_STAGE2_WORKDIR_POINTER="$(mktemp)" +trap 'rm -f "${TMP_MANIFEST}" "${TMP_SCRIPT:-}" "${TMP_WORKDIR_POINTER}" "${TMP_STAGE2_WORKDIR_POINTER}"' EXIT # ONE submission, mirroring stage 1: the windows live in the manifest, so a subjob index identifies a # (run, window) pair by itself and there is nothing to loop over. @@ -142,4 +150,22 @@ GRID_SUBMIT_ARGS=( ) echo "== Submitting ${JOBNAME} (prodsplit=${N}, asuser=${ASUSER}) ==" -"${O2DPG_ROOT}/GRID/utils/grid_submit.sh" "${GRID_SUBMIT_ARGS[@]}" +# Capture grid_submit.sh's own stderr (its "pok" status messages go there) while still showing it live, +# so we can pull out the one line that tells us where this submission's data will actually land -- same +# mechanism submitMapCreation.sh uses for its own stage1Workdir pointer. Nothing downstream needed +# stage 2's own output location before now; this exists so a future consumer (e.g. a pipeline that reads +# stage 2's FT_*.root as an input) can find it without guessing. +SUBMIT_LOG=$("${O2DPG_ROOT}/GRID/utils/grid_submit.sh" "${GRID_SUBMIT_ARGS[@]}" 2>&1 | tee /dev/stderr) + +# Strip ANSI color codes (pok() wraps its output in \033[32m...\033[m) before parsing. +MY_JOBWORKDIR=$(echo "${SUBMIT_LOG}" | sed -E 's/\x1b\[[0-9;]*m//g' | sed -n "s/^Your job's working directory will be //p" | tail -1) +if [[ -z "${MY_JOBWORKDIR}" ]]; then + echo "ERROR: could not find \"Your job's working directory will be ...\" in grid_submit.sh's output -- a future consumer of this stage's output won't be able to find it. Aborting." >&2 + exit 1 +fi +echo "== MY_JOBWORKDIR (stage 2's own) = ${MY_JOBWORKDIR} ==" + +STAGE2_WORKDIR_POINTER_NAME="stage2Workdir.${JOBNAME}.txt" +echo "${MY_JOBWORKDIR}" > "${TMP_STAGE2_WORKDIR_POINTER}" +alien.py cp -f "file:${TMP_STAGE2_WORKDIR_POINTER}" "${MACRO_ALIEN_DIR}/${STAGE2_WORKDIR_POINTER_NAME}" +echo "== Uploaded ${STAGE2_WORKDIR_POINTER_NAME} ==" From a8c42f816cab16627b0155731d8ccc0b56712d00 Mon Sep 17 00:00:00 2001 From: mj525 <99000728+mjkim525@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:06:04 +0900 Subject: [PATCH 046/144] [PWGLF] Revise injection list and add QA codes for injected particles (#2430) * Fix LF resonance gap injection and add phi channel * Revise injection targets due to resource limitaion and add QA codes * Trigger CI rerun * Fix generator test macro naming --- .../ini/GeneratorLF_Resonances_pp_Rare.ini | 10 ++ .../tests/GeneratorLF_Resonances_pp_Rare.C | 111 ++++++++++++++ .../generator/resonancelistgun_width_inj.json | 139 ++---------------- 3 files changed, 130 insertions(+), 130 deletions(-) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_Resonances_pp_Rare.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp_Rare.C diff --git a/MC/config/PWGLF/ini/GeneratorLF_Resonances_pp_Rare.ini b/MC/config/PWGLF/ini/GeneratorLF_Resonances_pp_Rare.ini new file mode 100644 index 000000000..4c9fbc777 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_Resonances_pp_Rare.ini @@ -0,0 +1,10 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json", true, 5, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonances_width.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp_Rare.C b/MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp_Rare.C new file mode 100644 index 000000000..d19d72afc --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp_Rare.C @@ -0,0 +1,111 @@ + +int External() +{ + const std::string path{"o2sim_Kine.root"}; + const int numberOfGapEvents{4}; // generateLFRapidity(..., gap=5) + const std::vector injectedPDGs = { + 9010221, // f0(980) + 3324, -3324, // Xi(1530)0 and anti-Xi(1530)0 + 123324, -123324, // Xi(1820)0 and anti-Xi(1820)0 + 123314, -123314, // Xi(1820)- and Xi(1820)+ + 123334, -123334 // Omega(2012)- and Omega(2012)+ + }; + const std::vector> decayDaughters = { + {211, -211}, + {3312, 211}, {-3312, -211}, + {3122, 310}, {-3122, 310}, + {3122, -321}, {-3122, 321}, + {3312, 310}, {-3312, 310} + }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + std::vector nSignal(injectedPDGs.size(), 0); + std::vector nNotDecayed(injectedPDGs.size(), 0); + std::vector> nDecays; + for (const auto& daughters : decayDaughters) { + nDecays.emplace_back(daughters.size(), 0); + } + + int numberOfEventsProcessed{0}; + int numberOfEventsProcessedWithoutInjection{0}; + for (Long64_t i = 0; i < tree->GetEntries(); ++i) { + tree->GetEntry(i); + ++numberOfEventsProcessed; + bool hasInjection{false}; + for (const auto& track : *tracks) { + const auto pdg = track.GetPdgCode(); + const auto it = std::find(injectedPDGs.begin(), injectedPDGs.end(), pdg); + if (it == injectedPDGs.end()) { + continue; + } + const auto index = static_cast(std::distance(injectedPDGs.begin(), it)); + ++nSignal[index]; + if (track.getFirstDaughterTrackId() < 0) { + ++nNotDecayed[index]; + continue; + } + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { + const auto pdgDau = tracks->at(j).GetPdgCode(); + bool foundDau{false}; + for (size_t k = 0; k < decayDaughters[index].size(); ++k) { + if (pdgDau == decayDaughters[index][k]) { + ++nDecays[index][k]; + foundDau = true; + hasInjection = true; + break; + } + } + if (!foundDau) { + std::cerr << "Decay daughter not found: " << pdg << " -> " << pdgDau + << " (mother=" << track.getMotherTrackId() + << ", secondMother=" << track.getSecondMotherTrackId() << ")\n"; + } + } + } + if (!hasInjection) { + ++numberOfEventsProcessedWithoutInjection; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << tree->GetEntries() << "\n"; + for (size_t i = 0; i < injectedPDGs.size(); ++i) { + std::cout << "# Mother\n"; + std::cout << injectedPDGs[i] << " generated: " << nSignal[i] + << ", " << nNotDecayed[i] << " did not decay\n"; + for (size_t j = 0; j < decayDaughters[i].size(); ++j) { + std::cout << "# Daughter " << decayDaughters[i][j] << ": " << nDecays[i][j] << "\n"; + } + } + std::cout << "--------------------------------\n"; + std::cout << "Number of events processed: " << numberOfEventsProcessed << "\n"; + std::cout << "Number of input for the gap events: " << numberOfGapEvents << "\n"; + std::cout << "Number of events processed without injection: " + << numberOfEventsProcessedWithoutInjection << "\n"; + const double ratioOfNormalEvents = numberOfEventsProcessed + ? static_cast(numberOfEventsProcessedWithoutInjection) / + numberOfEventsProcessed + : 0.0; + std::cout << "Fraction without injection: " << ratioOfNormalEvents << "\n"; + const double expectedRatio = static_cast(numberOfGapEvents) / (numberOfGapEvents + 1); + std::cout << "Expected fraction for 1+" << numberOfGapEvents << " pattern: " << expectedRatio << "\n"; + + // Same basic gap sanity check as the referenced O2DPG test. + if (ratioOfNormalEvents > 0.90 || ratioOfNormalEvents < 0.70) { + std::cerr << "The number of injected events is too low or too high\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json index 80918f6a0..6aa349acb 100644 --- a/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json +++ b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json @@ -1,128 +1,7 @@ { "f_0(980)" : { "pdg": 9010221, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "phi(1020)" : { - "pdg": 333, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "K1(1270)+": { - "pdg": 10323, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "K1(1270)-": { - "pdg": -10323, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "K1(1270)0": { - "pdg": 10313, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "anti-K1(1270)0": { - "pdg": -10313, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "Sigma(1385)-" : { - "pdg": 3114, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "anti-Sigma(1385)+" : { - "pdg": -3114, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "Sigma(1385)+" : { - "pdg": 3224, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "anti-Sigma(1385)-" : { - "pdg": -3224, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "Lambda(1520)0" : { - "pdg": 102134, - "n": 10, - "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, - "genDecayed": true - }, - "anti-Lambda(1520)0" : { - "pdg": -102134, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -133,7 +12,7 @@ }, "Xi(1530)0" : { "pdg": 3324, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -144,7 +23,7 @@ }, "anti-Xi(1530)0" : { "pdg": -3324, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -155,7 +34,7 @@ }, "Xi(1820)0" : { "pdg": 123324, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -166,7 +45,7 @@ }, "Anti-Xi(1820)0" : { "pdg": -123324, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -177,7 +56,7 @@ }, "Xi(1820)-" : { "pdg": 123314, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -188,7 +67,7 @@ }, "Xi(1820)+" : { "pdg": -123314, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -199,7 +78,7 @@ }, "Omega(2012)-" : { "pdg": 123334, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, @@ -210,7 +89,7 @@ }, "Omega(2012)+" : { "pdg": -123334, - "n": 10, + "n": 1, "ptMin": 0.0, "ptMax": 20, "etaMin": -1.2, From a7854c2659e748e4ef3881f0563630279eb9e916 Mon Sep 17 00:00:00 2001 From: Chuntai <48704924+wuctlby@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:28:14 +0800 Subject: [PATCH 047/144] tunable ratio of ccbar and bbbar for embedding (#2435) * tunable ccbar and bbbar * Update MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C Co-authored-by: Mattia Faggin * Add test macro --------- Co-authored-by: Mattia Faggin --- .../generator/generator_pythia8_embed_hf.C | 87 ++++++++ .../GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini | 9 + .../GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C | 192 ++++++++++++++++++ ...adronic_with_decays_Mode2_hardQCD_5TeV.cfg | 4 +- 4 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini create mode 100644 MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C diff --git a/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C b/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C index 7a28d631a..7f325eb67 100644 --- a/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C +++ b/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C @@ -18,6 +18,7 @@ namespace hf_generators GapTriggeredBeauty, // --> GeneratorPythia8GapTriggeredBeauty: beauty enriched GapTriggeredCharmAndBeauty, // --> GeneratorPythia8GapTriggeredCharmAndBeauty: charm and beauty enriched (with same ratio) GapHF, // --> GeneratorPythia8GapHF + GapHFRatio, // --> GeneratorPythia8GapHF, quark list built from b/c ratio NGenType }; } @@ -83,6 +84,12 @@ public: LOG(info) << "********** Default number of HF signal events to be merged (updated by notifyEmbedding): " << mNumSigEvs; mGeneratorEvHF = dynamic_cast(GeneratorPythia8GapTriggeredBeauty(/*no gap trigger*/1, yQuarkMin, yQuarkMax, yHadronMin, yHadronMax, hadronPdgList, partPdgToReplaceList, freqReplaceList)); break; + + case hf_generators::GapHFRatio: + LOG(info) << "********** [GeneratorPythia8EmbedHF] configuring GapHFRatio (custom b/c ratio) **********"; + LOG(info) << "********** Default number of HF signal events to be merged (updated by notifyEmbedding): " << mNumSigEvs; + mGeneratorEvHF = dynamic_cast(GeneratorPythia8GapHF(/*no gap trigger*/1, yQuarkMin, yQuarkMax, yHadronMin, yHadronMax, quarkPdgList, hadronPdgList, partPdgToReplaceList, freqReplaceList)); + break; default: LOG(fatal) << "********** [GeneratorPythia8EmbedHF] bad configuration, fix it! **********"; break; @@ -387,6 +394,71 @@ private: }; +// Helper: build quarkPdgList from bOverCRatio (= N(beauty)/N(charm), default 1 = 1:1) +// Integer b/c: taken directly; fractional b/c: reduced to nearest nB:nC with nC+nB <= 20 +static std::vector BuildQuarkListFromBOverC(float bOverCRatio) +{ +const int iterNMax = 19; + if (bOverCRatio <= 0.f) { + LOG(fatal) << "bOverCRatio (b/c) must be > 0"; + } + if (bOverCRatio > iterNMax*1.f) { + bOverCRatio = iterNMax*1.f; + LOG(warn) << "bOverCRatio (b/c) too large, using 19:1"; + }else if (bOverCRatio < 1.f/iterNMax) { + bOverCRatio = 1.f/iterNMax; + LOG(warn) << "bOverCRatio (b/c) too small, using 1:19"; + } + + int nC = 1, nB = 1; + float bestErr = 1e9f; + for (int c = 1; c <= iterNMax; ++c) { + int b = static_cast(std::lround(bOverCRatio * c)); + if (b < 1 || c + b > 20) { + continue; + } + float err = std::fabs(static_cast(b) / c - bOverCRatio); + if (err < bestErr) { + /// This check here is needed in case bOverCRatio*c is not integer (it can happen with e.g. bOverCRatio=4./9.) + /// In this case, b is its truncation and the desired ratio is not obtained + /// It means that one needs to continue iterating until bOverCRatio*c is integer, i.e. b not truncated + /// + /// Possible cases: + /// 1. we want nB = nC*R, with R integer + /// -> we enter here in the first loop iteration + /// 2. we want more charm than beauty by an integer amount, i.e. nB = nC*R with R=1./N, with N integer + /// -> we enter here after N iterations, when c=N + /// 3. we want either more charm or beauty, but with a factor R that is not integer, as well as its inverse (e.g. bOverCRatio=4./9.) + /// -> In this case, bOverCRatio*c is not integer, namely b is its truncation and the desired ratio is not obtained + /// The code iterates at most until c becomes equal to the denominator of the fraction + /// 4. we want one of the previous cases, but we assign to bOverCRatio a value that is not rational + /// or such as we do not enter here within 19 iterations + /// -> nC and nB, are not touched, therefore we do not have the desired fraction. We'll need to throw a fatal (*) + /// + bestErr = err; + nC = c; + nB = b; + + /// If we are at this point, we reached already the desired ratio between b and c. + /// Let's break the loop + break; + } + } + + // (*) check if we have the desired fraction + bool isRatioUnity = std::fabs(bOverCRatio-1) < 1e-05; + if (!isRatioUnity && nC==1 && nB==1) { + LOG(fatal) << "nC=" << nC << ", nB=" << nB << " but you ask bOverCRatio to be " << bOverCRatio <<", which is different from nB/nC. It means either that bOverCRatio is not rational, or that you need more than " << iterNMax << " iterations. Change it!"; + } + + std::vector list; + // Bresenham interleaving + int len = nC + nB; + for (int k = 0; k < len; ++k) + list.push_back((((k + 1) * nC) / len > (k * nC) / len) ? 4 : 5); + return list; +} + // Charm enriched FairGenerator * GeneratorPythia8EmbedHFCharm(bool usePtHardBins = false, float yQuarkMin = -1.5, float yQuarkMax = 1.5, float yHadronMin = -1.5, float yHadronMax = 1.5, std::vector quarkPdgList = {}, std::vector hadronPdgList = {}, std::vector> partPdgToReplaceList = {}, std::vector freqReplaceList = {}) { @@ -420,3 +492,18 @@ FairGenerator * GeneratorPythia8EmbedHFCharmAndBeauty(bool usePtHardBins = false return myGen; } +// Charm and beauty enriched with tunable b/c ratio +FairGenerator * GeneratorPythia8EmbedHFRatio(float bOverCRatio = 1.f, bool usePtHardBins = false, float yQuarkMin = -1.5, float yQuarkMax = 1.5, float yHadronMin = -1.5, float yHadronMax = 1.5, std::vector hadronPdgList = {}, std::vector> partPdgToReplaceList = {}, std::vector freqReplaceList = {}) +{ + auto myGen = new GeneratorPythia8EmbedHF(); + + /// build the quark list from the b/c ratio + auto quarkPdgList = BuildQuarkListFromBOverC(bOverCRatio); + + /// setup the internal generator for HF events + myGen->setupGeneratorEvHF(hf_generators::GapHFRatio, + usePtHardBins, yQuarkMin, yQuarkMax, yHadronMin, yHadronMax, + quarkPdgList, hadronPdgList, partPdgToReplaceList, freqReplaceList); + + return myGen; +} diff --git a/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini new file mode 100644 index 000000000..418304b17 --- /dev/null +++ b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini @@ -0,0 +1,9 @@ +#NEV_TEST> 10 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C +funcName=GeneratorPythia8EmbedHFRatio(3.0) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_hardQCD_5TeV.cfg +includePartonEvent=true diff --git a/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C new file mode 100644 index 000000000..c73693d87 --- /dev/null +++ b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C @@ -0,0 +1,192 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + //std::string path{"tf1/sgn_1_Kine.root"}; + + int checkPdgQuarkOne{4}; + int checkPdgQuarkTwo{5}; + float ratioTrigger = 1.; // one event triggered out of 1 + + std::vector checkPdgHadron{411, 421, 431, 4122, 4132, 4232, 4332}; + std::map>> checkHadronDecays{ // sorted pdg of daughters + {421, { + {-321, 211}, // D0 -> K-, pi+ + {-321, 211, 111}, // D0 -> K-, pi+, pi0 + {213, -321}, // D0 -> rho(770)+, K- + {-313, 111}, // D0 -> Kbar^*(892)0, pi0 + {-323, 211}, // D0 -> K^*(892)-, pi+ + {-211, 211}, // D0 -> pi-, pi+ + {213, -211}, // D0 -> rho(770)+, pi- + {-211, 211, 111}, // D0 -> pi-, pi+, pi0 + {-321, 321}, // D0 -> K-, K+ + }}, + + {411, { + {-321, 211, 211}, // D+ -> K-, pi+, pi+ + {-10311, 211}, // D+ -> Kbar0^*(1430)0, pi+ + {-313, 211}, // D+ -> Kbar^*(892)0, pi+ + {-321, 211, 211, 111}, // D+ -> K-, pi+, pi+, pi0 + {333, 211}, // D+ -> phi(1020)0, pi+ + {-313, 321}, // D+ -> Kbar^*(892)0, K+ + {-10311, 321}, // D+ -> Kbar0^*(1430)0, K+ + {-321, 321, 211}, // D+ -> K-, K+, pi+ + {113, 211}, // D+ -> rho(770)0, pi+ + {225, 211}, // D+ -> f2(1270)0, pi+ + {-211, 211, 211}, // D+ -> pi-, pi+, pi+ + }}, + + {431, { + {333, 211}, // Ds+ -> phi(1020)0, pi+ + {-313, 321}, // Ds+ -> Kbar^*(892)0, K+ + {333, 213}, // Ds+ -> phi(1020)0, rho(770)+ + {113, 211}, // Ds+ -> rho(770)0, pi+ + {225, 211}, // Ds+ -> f2(1270)0, pi+ + {-211, 211, 211}, // Ds+ -> pi-, pi+, pi+ + {313, 211}, // Ds+ -> K^*(892)0, pi+ + {10221, 321}, // Ds+ -> f0(1370)0, K+ + {113, 321}, // Ds+ -> rho(770)0, K+ + {-211, 321, 211}, // Ds+ -> pi-, K+, pi+ + {221, 211}, // Ds+ -> eta, pi+ + }}, + + {4122, { + {2212, -321, 211}, // Lambdac+ -> p, K-, pi+ + {2212, -313}, // Lambdac+ -> p, Kbar^*(892)0 + {2224, -321}, // Lambdac+ -> Delta(1232)++, K- + {102134, 211}, // Lambdac+ -> 102134, pi+ + {2212, 310}, // Lambdac+ -> p, K0s + {2212, -321, 211, 111}, // Lambdac+ -> p, K-, pi+, pi0 + {2212, -211, 211}, // Lambdac+ -> p, pi-, pi+ + {2212, 333}, // Lambdac+ -> p, phi(1020)0 + }}, + + {4232, { + {2212, -321, 211}, // Xic+ -> p, K-, pi+ + {2212, -313}, // Xic+ -> p, Kbar^*(892)0 + {3312, 211, 211}, // Xic+ -> Xi-, pi+, pi+ + {2212, 333}, // Xic+ -> p, phi(1020)0 + {3222, -211, 211}, // Xic+ -> Sigma+, pi-, pi+ + {3324, 211}, // Xic+ -> Xi(1530)0, pi+ + }}, + + {4132, { + {3312, 211}, // Xic0 -> Xi-, pi+ + }}, + + {4332, { + {3334, 211}, // Omegac0 -> Omega-, pi+ + {3312, 211}, // Omegac0 -> Xi-, pi+ + }}, + }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsInjOne{}, nEventsInjTwo{}; + int nQuarksOne{}, nQuarksTwo{}, nSignals{}, nSignalGoodDecay{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information + //if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + // bool isValid = false; + // int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + // if (subGeneratorId == 0) { + // nEventsMB++; + // } else if (subGeneratorId == checkPdgQuarkOne) { + // nEventsInjOne++; + // } else if (subGeneratorId == checkPdgQuarkTwo) { + // nEventsInjTwo++; + // } + //} + + for (auto &track : *tracks) { + auto pdg = track.GetPdgCode(); + if (std::abs(pdg) == checkPdgQuarkOne) { + nQuarksOne++; + continue; + } + if (std::abs(pdg) == checkPdgQuarkTwo) { + nQuarksTwo++; + continue; + } + if (std::find(checkPdgHadron.begin(), checkPdgHadron.end(), std::abs(pdg)) != checkPdgHadron.end()) { // found signal + nSignals++; // count signal PDG + + std::vector pdgsDecay{}; + std::vector pdgsDecayAntiPart{}; + for (int j{track.getFirstDaughterTrackId()}; j <= track.getLastDaughterTrackId(); ++j) { + auto pdgDau = tracks->at(j).GetPdgCode(); + pdgsDecay.push_back(pdgDau); + if (pdgDau != 333 && pdgDau != 111 && pdgDau != 221 && pdgDau != 113 && pdgDau != 225) { // phi is antiparticle of itself + pdgsDecayAntiPart.push_back(-pdgDau); + } else { + pdgsDecayAntiPart.push_back(pdgDau); + } + } + + std::sort(pdgsDecay.begin(), pdgsDecay.end()); + std::sort(pdgsDecayAntiPart.begin(), pdgsDecayAntiPart.end()); + + for (auto &decay : checkHadronDecays[std::abs(pdg)]) { + std::sort(decay.begin(), decay.end()); + if (pdgsDecay == decay || pdgsDecayAntiPart == decay) { + nSignalGoodDecay++; + break; + } + } + } + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + //std::cout << "# MB events: " << nEventsMB << "\n"; + //std::cout << Form("# events injected with %d quark pair: ", checkPdgQuarkOne) << nEventsInjOne << "\n"; + //std::cout << Form("# events injected with %d quark pair: ", checkPdgQuarkTwo) << nEventsInjTwo << "\n"; + std::cout << Form("# %d (anti)quarks: ", checkPdgQuarkOne) << nQuarksOne << "\n"; + std::cout << Form("# %d (anti)quarks: ", checkPdgQuarkTwo) << nQuarksTwo << "\n"; + std::cout <<"# signal hadrons: " << nSignals << "\n"; + std::cout <<"# signal hadrons decaying in the correct channel: " << nSignalGoodDecay << "\n"; + + //if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + // std::cerr << "Number of generated MB events different than expected\n"; + // return 1; + //} + //if (nEventsInjOne < nEvents * ratioTrigger * 0.5 * 0.95 || nEventsInjOne > nEvents * ratioTrigger * 0.5 * 1.05) { + // std::cerr << "Number of generated events injected with " << checkPdgQuarkOne << " different than expected\n"; + // return 1; + //} + //if (nEventsInjTwo < nEvents * ratioTrigger * 0.5 * 0.95 || nEventsInjTwo > nEvents * ratioTrigger * 0.5 * 1.05) { + // std::cerr << "Number of generated events injected with " << checkPdgQuarkTwo << " different than expected\n"; + // return 1; + //} + + if (nQuarksOne < nEvents * ratioTrigger) { // we expect anyway more because the same quark is repeated several time, after each gluon radiation + std::cerr << "Number of generated (anti)quarks " << checkPdgQuarkOne << " lower than expected\n"; + return 1; + } + if (nQuarksTwo < nEvents * ratioTrigger) { // we expect anyway more because the same quark is repeated several time, after each gluon radiation + std::cerr << "Number of generated (anti)quarks " << checkPdgQuarkTwo << " lower than expected\n"; + return 1; + } + + float fracForcedDecays = nSignals ? float(nSignalGoodDecay) / nSignals : 0.0f; + float uncFracForcedDecays = nSignals ? std::sqrt(fracForcedDecays * (1 - fracForcedDecays) / nSignals) : 1.0f; + if (1 - fracForcedDecays > 0.15 + uncFracForcedDecays) { // we put some tolerance (e.g. due to oscillations which might change the final state) + std::cerr << "Fraction of signals decaying into the correct channel " << fracForcedDecays << " lower than expected\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_hardQCD_5TeV.cfg b/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_hardQCD_5TeV.cfg index 81da1a4ba..f407097af 100644 --- a/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_hardQCD_5TeV.cfg +++ b/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_hardQCD_5TeV.cfg @@ -107,7 +107,7 @@ BeamRemnants:saturation 5 4122:addChannel = 1 0.04500 100 2224 -321 ### Λc+ -> Delta++ K- 1.08% 4122:addChannel = 1 0.09000 100 102134 211 ### Λc+ -> Lambda(1520) K- 2.20e-3 ### Λc+ -> p K0S (36%) -4122:addChannel = 1 0.36000 0 2212 311 ### Λc+ -> p K0S 1.59% +4122:addChannel = 1 0.36000 0 2212 310 ### Λc+ -> p K0S 1.59% ### Λc+ -> p K- π+ π0 (small, 3%) 4122:addChannel = 1 0.03000 0 2212 -321 211 111 ### Λc+ -> p K- π+ π0 (non-resonant) 4.6% ### Λc+ -> p π- π+ (12.50%) @@ -256,7 +256,7 @@ BeamRemnants:saturation 5 431:onIfMatch = 221 211 ### Λc -> pK0s -4122:onIfMatch = 2212 311 +4122:onIfMatch = 2212 310 ### Λc -> p K- π+ π0 4122:onIfMatch = 2212 321 211 ### Λc -> p K* From dfaedea000f0b6c57eb8a980dfb57d2aa322d47c Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 22 Aug 2026 18:36:21 +0200 Subject: [PATCH 048/144] Let learned resource estimates replace dynamic sampling in anchorMC This adds an ALIEN_O2DPG_RESOURCES hook to anchorMC.sh so a production can run with the resource estimates a pilot job measured. - anchorMC.sh passed --dynamic-resources unconditionally, so learned estimates could only be added on top of runtime sampling, never instead of it. - When ALIEN_O2DPG_RESOURCES names a file, --update-resources is passed and --dynamic-resources is not; without it the invocation is unchanged. - The file is the json-stat output of MC/utils/o2dpg_sim_metrics.py. - ALIEN_O2DPG_FILEGRAPH is added to the usage text, which did not mention it. Co-Authored-By: Claude Opus 5 --- MC/run/ANCHOR/anchorMC.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/MC/run/ANCHOR/anchorMC.sh b/MC/run/ANCHOR/anchorMC.sh index 087b4870b..0aa4832f4 100755 --- a/MC/run/ANCHOR/anchorMC.sh +++ b/MC/run/ANCHOR/anchorMC.sh @@ -59,6 +59,8 @@ print_help() echo "ALIEN_JDL_RUN_TIME_SPAN_FILE=FILE, use a run-time-span file to exclude bad data-taking periods" echo "ALIEN_JDL_INVERT_IRFRAME_SELECTION, invertes the choice of ALIEN_JDL_RUN_TIME_SPAN_FILE" echo "ALIEN_JDL_CCDB_CONDITION_NOT_AFTER, sets the condition_not_after timestamp for CCDB queries" + echo "ALIEN_O2DPG_FILEGRAPH=FILE, delete intermediate files early, using a FileIOGraph report" + echo "ALIEN_O2DPG_RESOURCES=FILE, use resource estimates learned by a pilot run instead of sampling them" echo "DISABLE_QC, set this to disable QC, e.g. to 1" echo "CYCLE, to set a cycle number different than 0" echo "NSIGEVENTS, to enforce a specific upper limit of events in a timeframe (not counting orbit-early) events" @@ -430,8 +432,15 @@ else fi echo_info "Workflow will run with target specification ${targetString}" +# resource estimates learned by a pilot run (o2dpg_sim_metrics.py json-stat) +# replace the runtime sampling; without them we sample dynamically as before +RESOURCE_ARGS="--dynamic-resources" +if [ -n "${ALIEN_O2DPG_RESOURCES}" ]; then + RESOURCE_ARGS="--update-resources ${ALIEN_O2DPG_RESOURCES}" +fi + ${O2DPG_ROOT}/MC/bin/o2_dpg_workflow_runner.py -f workflow.json -tt ${targetString} \ - --cpu-limit ${ALIEN_JDL_CPULIMIT:-8} --dynamic-resources \ + --cpu-limit ${ALIEN_JDL_CPULIMIT:-8} ${RESOURCE_ARGS} \ ${ALIEN_O2DPG_FILEGRAPH:+--remove-files-early ${ALIEN_O2DPG_FILEGRAPH}} \ ${ALIEN_O2DPG_ADDITIONAL_WORKFLOW_RUNNER_ARGS} From c2a97f3167171dfe377fd728b7056b7cb1fe801e Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 24 Aug 2026 13:31:02 +0200 Subject: [PATCH 049/144] Give the QED interaction spec the size of the QED event pool This fixes a problem in the QED specification handed to the collision context tool. - The MC number string was 10000000:NEventsQED, so the round robin wrapped the QED event IDs at 10000000 instead of at the number of events the workflow simulates, and the MC labels named QED events which do not exist. - Both numbers are NEventsQED now, which also repairs the labels on the O2 versions already deployed. https://its.cern.ch/jira/browse/O2-7132 --- MC/bin/o2dpg_sim_workflow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 17a921cdd..5f66a9ff9 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -659,7 +659,11 @@ def getDPL_global_options(bigshm=False, ccdbbackend=True, runcommand=True): includeQED = False else: qedrate = INTRATE * QEDXSecExpected[COLTYPE] / XSecSys[COLTYPE] # hadronic interaction rate * cross_section_ratio - qedspec = 'qed' + ',' + str(qedrate) + ',10000000:' + str(NEventsQED) + # the QED events are reused in a round robin, so both numbers are the size of the QED pool. + # Older O2 versions wrap the QED event IDs at the first number instead of the second one and + # would otherwise put event IDs into the MC labels which are not in the QED kinematics. + # See https://its.cern.ch/jira/browse/O2-7132 + qedspec = 'qed' + ',' + str(qedrate) + ',' + str(NEventsQED) + ':' + str(NEventsQED) PreCollContextTask['cmd'] = task_finalizer([ '${O2_ROOT}/bin/o2-steer-colcontexttool', From 66bad3a1196c1b35757069167a7c8b86174c7239 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Tue, 25 Aug 2026 09:08:28 +0200 Subject: [PATCH 050/144] Do not anchor an MC to a part of the run that had no collisions This fixes the interaction rate of anchored jobs sampled near end of run. - Collisions stop before data taking does. In run 571043 the FT0 vertex input reads zero over the last 256 s. - That zero was forwarded as "-interactionRate 0", so workflow creation aborted and the job ended as ERROR_V. - The end of the run is now the last scaler interval that counted, mirroring the ITS ramp-up adjustment of the start. - A timestamp with no collisions is marked excluded, which anchorMC.sh handles. No rate is invented for it. - AggregatedRunInfo cannot give this; its end of run is the end of data taking. - The raw rate now comes from o2::ctp::CTPRateFetcher rather than a fixed scaler index, which is not the same class in every run. - The pile-up correction is guarded against zero colliding bunches and against a rate with no Poisson inverse. https://its.cern.ch/jira/browse/O2-7143 Co-Authored-By: Claude Opus 5 --- MC/bin/o2dpg_sim_workflow_anchored.py | 133 ++++++++++++++++++++------ 1 file changed, 102 insertions(+), 31 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow_anchored.py b/MC/bin/o2dpg_sim_workflow_anchored.py index e777b9acf..e6ad5a803 100755 --- a/MC/bin/o2dpg_sim_workflow_anchored.py +++ b/MC/bin/o2dpg_sim_workflow_anchored.py @@ -266,6 +266,45 @@ def retrieve_CTPScalers(ccdbreader, run_number, timestamp=None): return ctpscaler return None +def make_CTP_raw_rate_source(run_number, timestamp, ColSystem): + """ + A function giving the raw CTP counting rate in Hz at a timestamp in milliseconds, and the name + of the source it reads. + + Which class to count, at which scaler index and with which scaler type, and how that changed + over the years, is CTP's business: o2::ctp::CTPRateFetcher knows it, a fixed scaler index does + not. Only the raw rate is taken from it; the pile-up correction is done below. + """ + ccdb = o2.ccdb.BasicCCDBManager.instance() + fetcher = o2.ctp.CTPRateFetcher() + fetcher.setupRun(run_number, ccdb, int(timestamp), True) + # "ZNC" without "hadronic" so that the fetcher does not apply the factor 28 itself + source = "ZNC" if ColSystem == "PbPb" else "T0VTX" + + def raw_rate_at(timestamp_ms): + return fetcher.fetchNoPuCorr(ccdb, int(timestamp_ms), run_number, source) + + return raw_rate_at, source + +def determine_last_collision_time(raw_rate_at, ctpscaler, run_end): + """ + The end of the run as far as collisions are concerned, in milliseconds. + + Data taking goes on after the collisions stop and the time frames written in between hold + nothing to simulate: in run 571043 the CTP counts zero over the last 256 s. The run record only + knows the end of data taking, so ask the scalers. Mirrors the ITS ramp-up adjustment of the + start of the run. + """ + times = [float(r.epochTime) for r in ctpscaler.getScalerRecordO2()] + for i in range(len(times) - 1, 0, -1): + # interval i spans records i-1 and i; ask in the middle so that getRateGivenT brackets it + if raw_rate_at(1000. * 0.5 * (times[i - 1] + times[i])) > 0: + if i == len(times) - 1: + return run_end + # counting stopped inside interval i, so the last moment we know of is where it began + return min(int(1000. * times[i - 1]), run_end) + return run_end + def retrieve_ITS_RampDuration(ccdbreader, timestamp): """ Retrieves the ITS ramp-up duration for a given timestamp and @@ -281,12 +320,19 @@ def retrieve_ITS_RampDuration(ccdbreader, timestamp): print("WARNING: ITS ramp duration vector is empty, using 0") return 0 -def retrieve_MinBias_CTPScaler_Rate(ctpscaler, finaltime, trig_eff_arg, NBunches, ColSystem, eCM): +def retrieve_MinBias_CTPScaler_Rate(raw_rate_at, finaltime, trig_eff_arg, NBunches, ColSystem, eCM, run_number = -1): """ - retrieves the CTP scalers object for a given timestamp - and calculates the interation rate to be applied in Monte Carlo digitizers. - Uses trig_eff_arg when positive, otherwise calculates the effTrigger. + Turns the raw CTP counting rate at finaltime (in milliseconds) into the interaction rate for + the digitizers. Uses trig_eff_arg when positive, otherwise the efficiency of the collision + system. + + Returns (interaction rate, raw rate). No collisions at that timestamp gives (None, 0.0) so the + caller can drop it; anything else unusable gives (None, None), which leaves the externally + given -interactionRate in place. """ + # Fraction of inelastic collisions firing the class counted below: FT0 vertex for pp and the + # light systems, ZDC for PbPb, where the ratio is above one because the ZDC also sees + # electromagnetic dissociation. trigger_effs = { "pp": { "1000": 0.68, @@ -327,29 +373,38 @@ def retrieve_MinBias_CTPScaler_Rate(ctpscaler, finaltime, trig_eff_arg, NBunches else: effTrigger = 0.759 # The simulation will fail later if the collision system is not defined - # this is the default for pp - ctpclass = 0 # <---- we take the scaler for FT0 - ctptype = 1 - # this is the default for PbPb - if ColSystem == "PbPb": - ctpclass = 25 # <--- we take scalers for ZDC - ctptype = 7 - print("Fetching rate with time " + str(finaltime) + " class " + str(ctpclass) + " type " + str(ctptype)) - rate = ctpscaler.getRateGivenT(finaltime, ctpclass, ctptype) - - print("Global rate " + str(rate.first) + " local rate " + str(rate.second)) - ctp_local_rate_raw = None - if rate.second >= 0: - ctp_local_rate_raw = rate.second - if rate.first >= 0: - # calculate true rate (input from Chiara Zampolli) using number of bunches - coll_bunches = NBunches - mu = - math.log(1. - rate.second / 11245 / coll_bunches) / effTrigger - finalRate = coll_bunches * mu * 11245 - return finalRate, ctp_local_rate_raw - - print (f"[ERROR]: Could not determine interaction rate; Some (external) default used") - return None, None + raw_rate = raw_rate_at(finaltime) + print(f"CTP raw rate at timestamp {finaltime} is {raw_rate} Hz") + + if raw_rate == 0: + print(f"[WARNING]: The CTP counted no collisions in run {run_number} at timestamp " + f"{finaltime}; there is nothing to simulate at this moment") + return None, 0.0 + + if not raw_rate > 0: + print(f"[ERROR]: Could not determine interaction rate for run {run_number} at timestamp " + f"{finaltime} (raw rate {raw_rate}); Some (external) default used") + return None, None + + # calculate true rate (input from Chiara Zampolli) using number of bunches + coll_bunches = NBunches + if coll_bunches <= 0: + print(f"[ERROR]: Number of colliding bunches is {coll_bunches} for run {run_number}; " + f"cannot determine interaction rate; Some (external) default used") + return None, None + + # invert the Poisson zero-probability to get the mean number of triggering interactions per + # colliding crossing, then divide by the efficiency to get all of them + trigger_prob_per_bc = raw_rate / 11245 / coll_bunches + if trigger_prob_per_bc >= 1.: + print(f"[ERROR]: Raw scaler rate {raw_rate} Hz is not below the {coll_bunches} * 11245 Hz of " + f"colliding bunch crossings of run {run_number}; cannot determine interaction rate; " + f"Some (external) default used") + return None, None + + mu = - math.log(1. - trigger_prob_per_bc) / effTrigger + finalRate = coll_bunches * mu * 11245 + return finalRate, raw_rate def determine_timestamp(sor, eor, splitinfo, cycle, ntf, HBF_per_timeframe = 256): """ @@ -585,11 +640,21 @@ def main(): print ("Collision system ", ColSystem) + # how to read the raw CTP counting rate in this run, and where the collisions actually end + raw_rate_at, ctp_source = make_CTP_raw_rate_source(args.run_number, mid_run_timestamp, ColSystem) + print("Reading the CTP rate from source " + ctp_source) + collisions_end = determine_last_collision_time(raw_rate_at, ctp_scalers, run_end) + if collisions_end < run_end: + print(f"Adjusting end-of-run from {run_end} to {collisions_end}: the CTP counted no " + f"collisions over the last {(run_end - collisions_end)/1000.:.1f} s of this run") + run_end = collisions_end + mid_run_timestamp = (effective_run_start + run_end) // 2 + # possibly overwrite the orbitsPerTF with some external choices if args.orbitsPerTF!="": # we actually need the interaction rate for this calculation # let's use the one provided from IR.txt (async reco) as quick way to make the decision - run_rate, _ = retrieve_MinBias_CTPScaler_Rate(ctp_scalers, mid_run_timestamp/1000., args.trig_eff, grplhcif.getBunchFilling().getNBunches(), ColSystem, eCM) + run_rate, _ = retrieve_MinBias_CTPScaler_Rate(raw_rate_at, mid_run_timestamp, args.trig_eff, grplhcif.getBunchFilling().getNBunches(), ColSystem, eCM, args.run_number) determined_orbits = parse_orbits_per_tf(args.orbitsPerTF, run_rate) if determined_orbits != -1: print("Adjusting orbitsPerTF from " + str(GLOparams["OrbitsPerTF"]) + " to " + str(determined_orbits)) @@ -599,7 +664,9 @@ def main(): timestamp = 0 prod_offset = 0 if args.timeframeID != -1: - timestamp = determine_timestamp_from_timeframeID(effective_run_start, run_end, args.timeframeID, GLOparams["OrbitsPerTF"]) + # an explicit timeframe is a fixed position in the run, so bound it by the run as recorded; + # if it falls past the collisions the rate below is zero and the job is excluded + timestamp = determine_timestamp_from_timeframeID(effective_run_start, GLOparams["EOR"], args.timeframeID, GLOparams["OrbitsPerTF"]) prod_offset = args.timeframeID else: timestamp, prod_offset = determine_timestamp(effective_run_start, run_end, [args.split_id - 1, args.prod_split], args.cycle, args.tf, GLOparams["OrbitsPerTF"]) @@ -631,7 +698,11 @@ def main(): ctp_local_rate_raw = None if args.ccdb_IRate == True: - rate, ctp_local_rate_raw = retrieve_MinBias_CTPScaler_Rate(ctp_scalers, timestamp/1000., args.trig_eff, grplhcif.getBunchFilling().getNBunches(), ColSystem, eCM) + rate, ctp_local_rate_raw = retrieve_MinBias_CTPScaler_Rate(raw_rate_at, timestamp, args.trig_eff, grplhcif.getBunchFilling().getNBunches(), ColSystem, eCM, args.run_number) + + # no collisions here, so nothing to simulate: treat it like any other excluded timestamp + if rate == None and ctp_local_rate_raw == 0: + job_is_exluded = True if rate != None: # if the rate calculation was successful we will use it, otherwise we fall back to some rate given as part @@ -641,7 +712,7 @@ def main(): # Use re.sub() to replace the pattern with an empty string forwardargs = re.sub(pattern, " ", forwardargs) forwardargs += ' -interactionRate ' + str(int(rate)) - if ctp_local_rate_raw != None: + if ctp_local_rate_raw != None and ctp_local_rate_raw > 0: forwardargs += ' --ctp-scaler ' + str(ctp_local_rate_raw) # we finally pass forward to the unanchored MC workflow creation From 08431cd484be0a1c402a2e45f3ee3944548c1049 Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Mon, 31 Aug 2026 10:24:59 +0200 Subject: [PATCH 051/144] o2dpg_sim_workflow.py: clean up trailing white space --- MC/bin/o2dpg_sim_workflow.py | 180 +++++++++++++++++------------------ 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 5f66a9ff9..da406b45a 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -236,7 +236,7 @@ def load_external_config(configfile): if ("ConfigParams" in anchorConfig) != ("ConfigParams" in config_overwrite): print ("Error: overwrite config not following same format as base config; Cannot merge") exit (1) - + # merge the dictionaries into anchorConfig, the latter takes precedence merge_dicts(anchorConfig, config_overwrite) @@ -247,7 +247,7 @@ def load_external_config(configfile): tpcreco_mctimegain = option_if_available('o2-tpc-reco-workflow', '--tpc-mc-time-gain', envfile=async_envfile) if tpcreco_mctimegain == '': # TODO: Upload all MC time gain objects to TestReco and remove year dependence - year = environ.get('ALIEN_JDL_LPMANCHORYEAR') if environ.get('ALIEN_JDL_LPMANCHORYEAR') is not None else environ.get('ANCHORYEAR') + year = environ.get('ALIEN_JDL_LPMANCHORYEAR') if environ.get('ALIEN_JDL_LPMANCHORYEAR') is not None else environ.get('ANCHORYEAR') if year == '2023': print("TPC reco does not support --tpc-mc-time-gain. Remapping time gain objects for 2023 MC") # Year dependent workaround for MC anchored to 2023, solving the issue with 2tag mechanism @@ -374,7 +374,7 @@ def retrieve_sor(run_number): if match_object != None: SOR=int(match_object[2]) break - + return SOR @@ -846,7 +846,7 @@ def getDPL_global_options(bigshm=False, ccdbbackend=True, runcommand=True): print("Timeframe " + str(tf) + " seed: ", TFSEED) timeframeworkdir='tf'+str(tf) - # ---- transport task ------- + # ---- transport task ------- # produce QED background for PbPb collissions QEDdigiargs = "" @@ -917,7 +917,7 @@ def getDPL_global_options(bigshm=False, ccdbbackend=True, runcommand=True): # possible generator) workflow['stages'].append(SGN_CONFIG_task) - + # default flags for extkinO2 signal simulation (no transport) extkinO2Config = '' if GENERATOR == 'extkinO2': @@ -1004,7 +1004,7 @@ def getDPL_global_options(bigshm=False, ccdbbackend=True, runcommand=True): SGNtask['cmd'] = sgncmdbase + ' -g ' + str(GENERATOR) + ' ' + str(TRIGGER) + ' --vertexMode kCCDB ' if not isActive('all'): SGNtask['cmd'] += ' --readoutDetectors ' + " ".join(activeDetectors) - + SGNtask['cmd'] += ' --fromCollContext collisioncontext.root' workflow['stages'].append(SGNtask) @@ -1074,7 +1074,7 @@ def putConfigValues(listOfMainKeys=[], localCF = {}, globalTFConfig = True): listOfMainKeys : list of keys to be applied from the global configuration object localCF: a dictionary mapping key to param - possibly overrides settings taken from global config """ - returnstring = ' --configKeyValues "' + returnstring = ' --configKeyValues "' cf = globalTFConfigValues.copy() if globalTFConfig else {} isfirst=True @@ -1236,9 +1236,9 @@ def createRestDigiTask(name, det='ALLSMALLER'): tf=tf, cwd=timeframeworkdir, lab=["DIGI","SMALLDIGI"], cpu='1') FT0FV0EMCCTPDIGItask['cmd'] = ('','ln -nfs ../bkg_HitsFT0.root . ; ln -nfs ../bkg_HitsFV0.root . ; ln -nfs ../bkg_HitsEMC.root; ln -nfs ../bkg_Kine.root; ')[doembedding] FT0FV0EMCCTPDIGItask['cmd'] += task_finalizer([ - '${O2_ROOT}/bin/o2-sim-digitizer-workflow', - getDPL_global_options(), - f'-n {args.ns}', + '${O2_ROOT}/bin/o2-sim-digitizer-workflow', + getDPL_global_options(), + f'-n {args.ns}', simsoption, '--onlyDet ' + ','.join([det for det in ['FT0', 'FV0', 'EMC', 'CTP'] if isActive(det)]), f'--interactionRate {INTRATE}', @@ -1247,10 +1247,10 @@ def createRestDigiTask(name, det='ALLSMALLER'): '--disable-write-ini', putConfigValues(listOfMainKeys=['EMCSimParam','FV0DigParam','FT0DigParam'], localCF={"DigiParams.seed" : str(TFSEED)}), ('--combine-devices','')[args.no_combine_dpl_devices], - ('',' --disable-mc')[args.no_mc_labels], + ('',' --disable-mc')[args.no_mc_labels], QEDdigiargs, '--forceSelectedDets'], configname = 'ft0fv0emcctp_digi') - + workflow['stages'].append(FT0FV0EMCCTPDIGItask) det_to_digitask["FT0"]=FT0FV0EMCCTPDIGItask det_to_digitask["FV0"]=FT0FV0EMCCTPDIGItask @@ -1347,7 +1347,7 @@ def getDigiTaskName(det): anchor_corrmaplumi_mode = dpl_option_from_config(anchorConfig, 'o2-tpcits-match-workflow', '--corrmap-lumi-mode', section = 'full', default_value = '') if anchor_corrmaplumi_mode != '': anchor_corrmaplumi_mode = '--corrmap-lumi-mode ' + anchor_corrmaplumi_mode - + tpc_corr_scaling_options = anchor_lumi_type + ' ' + anchor_corrmaplumi_mode if not isActive('CTP'): # CTP digits won't be produced for this timeframe (CTP not in the readout detector list) @@ -1366,7 +1366,7 @@ def getDigiTaskName(det): TPCSyncRECOtask['cmd'] += ' | ${O2_ROOT}/bin/o2-gpu-reco-workflow' + getDPL_global_options(bigshm=True, ccdbbackend=True, runcommand=False) \ + '--input-type clusters --output-type compressed-clusters-flat,clusters,send-clusters-per-sector --filtered-output-specs ' \ + tpc_corr_scaling_options + ' ' + tpc_corr_options_mc \ - + putConfigValues(["TPCGasParam", "TPCCorrMap", "trackTuneParams"], + + putConfigValues(["TPCGasParam", "TPCCorrMap", "trackTuneParams"], localCF={"GPU_proc.ompThreads":NWORKERS_TF, \ "GPU_proc.tpcWriteClustersAfterRejection":1, \ "GPU_rec_tpc.compressionTypeMask":0, \ @@ -1385,18 +1385,18 @@ def getDigiTaskName(det): '--input-type clusters', '--output-type tracks,send-clusters-per-sector', putConfigValues(["GPU_global", - "TPCGasParam", - "TPCCorrMap", - "GPU_rec_tpc", - "trackTuneParams"], + "TPCGasParam", + "TPCCorrMap", + "GPU_rec_tpc", + "trackTuneParams"], {"GPU_proc.ompThreads":NWORKERS_TF} | tpcLocalCFreco), ('',' --disable-mc')[args.no_mc_labels], - tpc_corr_scaling_options, + tpc_corr_scaling_options, tpc_corr_options_mc, tpcreco_mctimegain]) workflow['stages'].append(TPCRECOtask) - #<--------- ITS reco task + #<--------- ITS reco task ITSMemEstimate = 12000 if havePbPb else 2000 # PbPb has much large mem requirement for now (in worst case) ITSRECOtask=createTask(name='itsreco_'+str(tf), needs=[getDigiTaskName("ITS" if args.detectorList == 'ALICE2' else "IT3")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu='1', mem=str(ITSMemEstimate)) @@ -1404,16 +1404,16 @@ def getDigiTaskName(det): "${O2_ROOT}/bin/o2-its-reco-workflow" if args.detectorList == 'ALICE2' else "${O2_ROOT}/bin/o2-its3-reco-workflow", getDPL_global_options(bigshm=havePbPb), '--tracking-mode async', - putConfigValues(["ITSVertexerParam", + putConfigValues(["ITSVertexerParam", "ITSAlpideParam", - "ITSClustererParam", - "ITSCATrackerParam"], + "ITSClustererParam", + "ITSCATrackerParam"], {"NameConf.mDirMatLUT" : ".."}), ('',' --disable-mc')[args.no_mc_labels] ]) workflow['stages'].append(ITSRECOtask) - #<--------- FT0 reco task + #<--------- FT0 reco task FT0RECOtask = createTask(name='ft0reco_'+str(tf), needs=[getDigiTaskName("FT0")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1000') FT0RECOtask["cmd"] = task_finalizer([ '${O2_ROOT}/bin/o2-ft0-reco-workflow', @@ -1424,7 +1424,7 @@ def getDigiTaskName(det): ]) workflow['stages'].append(FT0RECOtask) - #<--------- ITS-TPC track matching task + #<--------- ITS-TPC track matching task ITSTPCMATCHtask=createTask(name='itstpcMatch_'+str(tf), needs=[TPCRECOtask['name'], ITSRECOtask['name'], FT0RECOtask['name'] if isActive("FT0") else None], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='8000', relative_cpu=3/8) ITSTPCMATCHtask["cmd"] = task_finalizer([ '${O2_ROOT}/bin/o2-tpcits-match-workflow', @@ -1432,38 +1432,38 @@ def getDigiTaskName(det): ' --tpc-track-reader tpctracks.root', '--tpc-native-cluster-reader \"--infile tpc-native-clusters.root\"', '--use-ft0' if isActive("FT0") else None, - putConfigValues(['MFTClustererParam', - 'ITSCATrackerParam', - 'tpcitsMatch', - 'TPCGasParam', - 'TPCCorrMap', - 'ITSClustererParam', - 'GPU_rec_tpc', - 'trackTuneParams', + putConfigValues(['MFTClustererParam', + 'ITSCATrackerParam', + 'tpcitsMatch', + 'TPCGasParam', + 'TPCCorrMap', + 'ITSClustererParam', + 'GPU_rec_tpc', + 'trackTuneParams', 'GlobalParams', - 'ft0tag'], + 'ft0tag'], {"NameConf.mDirMatLUT" : ".."} | tpcLocalCFreco), tpc_corr_scaling_options, tpc_corr_options_mc ]) workflow['stages'].append(ITSTPCMATCHtask) - #<--------- ITS-TPC track matching task + #<--------- ITS-TPC track matching task TRDTRACKINGtask = createTask(name='trdreco_'+str(tf), needs=[TRDDigitask['name'], ITSTPCMATCHtask['name'], TPCRECOtask['name'], ITSRECOtask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu='1', mem='2000') TRDTRACKINGtask['cmd'] = task_finalizer(['${O2_ROOT}/bin/o2-trd-tracklet-transformer', - getDPL_global_options(), + getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) if isActive("TRD"): workflow['stages'].append(TRDTRACKINGtask) - #<--------- TRD global tracking + #<--------- TRD global tracking # FIXME This is so far a workaround to avoud a race condition for trdcalibratedtracklets.root TRDTRACKINGtask2 = createTask(name='trdreco2_'+str(tf), needs=[TRDTRACKINGtask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu='1', mem='2000') trd_track_sources = cleanDetectorInputList(dpl_option_from_config(anchorConfig, 'o2-trd-global-tracking', '--track-sources', default_value='TPC,ITS-TPC')) TRDTRACKINGtask2['cmd'] = task_finalizer([ '${O2_ROOT}/bin/o2-trd-global-tracking', - getDPL_global_options(bigshm=True), + getDPL_global_options(bigshm=True), ('',' --disable-mc')[args.no_mc_labels], putConfigValues(['ITSClustererParam', 'ITSCATrackerParam', @@ -1473,7 +1473,7 @@ def getDigiTaskName(det): 'GlobalParams', 'TPCCorrMap'], {"NameConf.mDirMatLUT" : ".."} | tpcLocalCFreco), '--track-sources ' + trd_track_sources, - tpc_corr_scaling_options, + tpc_corr_scaling_options, tpc_corr_options_mc]) if isActive("TRD"): workflow['stages'].append(TRDTRACKINGtask2) @@ -1528,10 +1528,10 @@ def getDigiTaskName(det): MFTRECOtask = createTask(name='mftreco_'+str(tf), needs=mftreconeeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') MFTRECOtask['cmd'] = ('','ln -nfs ../bkg_Kine.root . ;')[doembedding] MFTRECOtask['cmd'] += task_finalizer([ - '${O2_ROOT}/bin/o2-mft-reco-workflow', - getDPL_global_options(), - putConfigValues(['MFTTracking', - 'MFTAlpideParam', + '${O2_ROOT}/bin/o2-mft-reco-workflow', + getDPL_global_options(), + putConfigValues(['MFTTracking', + 'MFTAlpideParam', 'ITSClustererParam', 'MFTClustererParam']), ('','--disable-mc')[args.no_mc_labels], @@ -1548,9 +1548,9 @@ def getDigiTaskName(det): MCHRECOtask = createTask(name='mchreco_'+str(tf), needs=mchreconeeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') MCHRECOtask['cmd'] = ('','ln -nfs ../bkg_Kine.root . ;')[doembedding] MCHRECOtask['cmd'] += task_finalizer( - ['${O2_ROOT}/bin/o2-mch-reco-workflow', - getDPL_global_options(), - putConfigValues(), + ['${O2_ROOT}/bin/o2-mch-reco-workflow', + getDPL_global_options(), + putConfigValues(), ('',' --disable-mc')[args.no_mc_labels], '--enable-clusters-root-output']) workflow['stages'].append(MCHRECOtask) @@ -1561,24 +1561,24 @@ def getDigiTaskName(det): ['${O2_ROOT}/bin/o2-mid-digits-reader-workflow', ('',' --disable-mc')[args.no_mc_labels]]) MIDRECOtask['cmd'] += ' | ' - MIDRECOtask['cmd'] += task_finalizer(['${O2_ROOT}/bin/o2-mid-reco-workflow', - getDPL_global_options(), + MIDRECOtask['cmd'] += task_finalizer(['${O2_ROOT}/bin/o2-mid-reco-workflow', + getDPL_global_options(), putConfigValues(),('',' --disable-mc')[args.no_mc_labels]]) if isActive('MID'): workflow['stages'].append(MIDRECOtask) #<--------- FDD reco workflow FDDRECOtask = createTask(name='fddreco_'+str(tf), needs=[getDigiTaskName("FDD")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') - FDDRECOtask['cmd'] = task_finalizer(['${O2_ROOT}/bin/o2-fdd-reco-workflow', - getDPL_global_options(ccdbbackend=False), + FDDRECOtask['cmd'] = task_finalizer(['${O2_ROOT}/bin/o2-fdd-reco-workflow', + getDPL_global_options(ccdbbackend=False), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) workflow['stages'].append(FDDRECOtask) #<--------- FV0 reco workflow FV0RECOtask = createTask(name='fv0reco_'+str(tf), needs=[getDigiTaskName("FV0")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') - FV0RECOtask['cmd'] = task_finalizer(['${O2_ROOT}/bin/o2-fv0-reco-workflow', - getDPL_global_options(), + FV0RECOtask['cmd'] = task_finalizer(['${O2_ROOT}/bin/o2-fv0-reco-workflow', + getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) workflow['stages'].append(FV0RECOtask) @@ -1586,41 +1586,41 @@ def getDigiTaskName(det): # calorimeters #<--------- EMC reco workflow EMCRECOtask = createTask(name='emcalreco_'+str(tf), needs=[getDigiTaskName("EMC")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') - # first part + # first part EMCRECOtask['cmd'] = task_finalizer([ - '${O2_ROOT}/bin/o2-emcal-reco-workflow', + '${O2_ROOT}/bin/o2-emcal-reco-workflow', putConfigValues(), '--input-type digits', - '--output-type cells', + '--output-type cells', '--infile emcaldigits.root', - '--disable-root-output', + '--disable-root-output', '--subspecificationOut 1', ('',' --disable-mc')[args.no_mc_labels]]) # second part - EMCRECOtask['cmd'] += ' | ' + EMCRECOtask['cmd'] += ' | ' EMCRECOtask['cmd'] += task_finalizer([ - '${O2_ROOT}/bin/o2-emcal-cell-recalibrator-workflow', + '${O2_ROOT}/bin/o2-emcal-cell-recalibrator-workflow', putConfigValues(), - '--input-subspec 1', + '--input-subspec 1', '--output-subspec 0', - '--no-timecalib', + '--no-timecalib', '--no-gaincalib', (' --isMC','')[args.no_mc_labels]]) # third part - EMCRECOtask['cmd'] += ' | ' + EMCRECOtask['cmd'] += ' | ' EMCRECOtask['cmd'] += task_finalizer([ '${O2_ROOT}/bin/o2-emcal-cell-writer-workflow', getDPL_global_options(), - '--subspec 0', + '--subspec 0', ('',' --disable-mc')[args.no_mc_labels]]) workflow['stages'].append(EMCRECOtask) #<--------- PHS reco workflow PHSRECOtask = createTask(name='phsreco_'+str(tf), needs=[getDigiTaskName("PHS")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') PHSRECOtask['cmd'] = task_finalizer([ - '${O2_ROOT}/bin/o2-phos-reco-workflow', - getDPL_global_options(), - putConfigValues(), + '${O2_ROOT}/bin/o2-phos-reco-workflow', + getDPL_global_options(), + putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) if isActive("PHS"): workflow['stages'].append(PHSRECOtask) @@ -1628,9 +1628,9 @@ def getDigiTaskName(det): #<--------- CPV reco workflow CPVRECOtask = createTask(name='cpvreco_'+str(tf), needs=[getDigiTaskName("CPV")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') CPVRECOtask['cmd'] = task_finalizer( - ['${O2_ROOT}/bin/o2-cpv-reco-workflow', - getDPL_global_options(), - putConfigValues(), + ['${O2_ROOT}/bin/o2-cpv-reco-workflow', + getDPL_global_options(), + putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) if isActive("CPV"): workflow['stages'].append(CPVRECOtask) @@ -1638,9 +1638,9 @@ def getDigiTaskName(det): #<--------- ZDC reco workflow ZDCRECOtask = createTask(name='zdcreco_'+str(tf), needs=[getDigiTaskName("ZDC")], tf=tf, cwd=timeframeworkdir, lab=["RECO", "ZDC"]) ZDCRECOtask['cmd'] = task_finalizer( - ['${O2_ROOT}/bin/o2-zdc-digits-reco', - getDPL_global_options(), - putConfigValues(), + ['${O2_ROOT}/bin/o2-zdc-digits-reco', + getDPL_global_options(), + putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) if isActive("ZDC"): workflow['stages'].append(ZDCRECOtask) @@ -1649,7 +1649,7 @@ def getDigiTaskName(det): #<--------- MCH-MID forward matching MCHMIDMATCHtask = createTask(name='mchmidMatch_'+str(tf), needs=[MCHRECOtask['name'], MIDRECOtask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') MCHMIDMATCHtask['cmd'] = task_finalizer( - ['${O2_ROOT}/bin/o2-muon-tracks-matcher-workflow', + ['${O2_ROOT}/bin/o2-muon-tracks-matcher-workflow', getDPL_global_options(ccdbbackend=False), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) @@ -1686,15 +1686,15 @@ def getDigiTaskName(det): #<--------- HMP forward matching HMPRECOtask = createTask(name='hmpreco_'+str(tf), needs=[getDigiTaskName('HMP')], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1000') HMPRECOtask['cmd'] = task_finalizer( - ['${O2_ROOT}/bin/o2-hmpid-digits-to-clusters-workflow', - getDPL_global_options(ccdbbackend=False), + ['${O2_ROOT}/bin/o2-hmpid-digits-to-clusters-workflow', + getDPL_global_options(ccdbbackend=False), putConfigValues()]) workflow['stages'].append(HMPRECOtask) #<--------- HMP forward matching - hmpmatchneeds = [HMPRECOtask['name'], - ITSTPCMATCHtask['name'], - TOFTPCMATCHERtask['name'] if isActive("TOF") else None, + hmpmatchneeds = [HMPRECOtask['name'], + ITSTPCMATCHtask['name'], + TOFTPCMATCHERtask['name'] if isActive("TOF") else None, TRDTRACKINGtask2['name'] if isActive("TRD") else None] hmpmatchneeds = [ n for n in hmpmatchneeds if n != None ] hmp_match_sources = cleanDetectorInputList(dpl_option_from_config(anchorConfig, 'o2-hmpid-matcher-workflow', '--track-sources', default_value='ITS-TPC,ITS-TPC-TRD,TPC-TRD')) @@ -1702,7 +1702,7 @@ def getDigiTaskName(det): HMPMATCHtask['cmd'] = task_finalizer( ['${O2_ROOT}/bin/o2-hmpid-matcher-workflow', '--track-sources ' + hmp_match_sources, - getDPL_global_options(), + getDPL_global_options(), putConfigValues() ]) workflow['stages'].append(HMPMATCHtask) @@ -1737,14 +1737,14 @@ def getDigiTaskName(det): PVFINDERtask = createTask(name='pvfinder_'+str(tf), needs=pvfinderneeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu=NWORKERS_TF, mem='4000') PVFINDERtask['cmd'] = task_finalizer( - [ '${O2_ROOT}/bin/o2-primary-vertexing-workflow', + [ '${O2_ROOT}/bin/o2-primary-vertexing-workflow', getDPL_global_options(), putConfigValues(['ITSAlpideParam', - 'MFTAlpideParam', - 'pvertexer', - 'TPCGasParam', - 'TPCCorrMap', - 'ft0tag'], + 'MFTAlpideParam', + 'pvertexer', + 'TPCGasParam', + 'TPCCorrMap', + 'ft0tag'], {"NameConf.mDirMatLUT" : ".."}), '--vertexing-sources ' + pvfinder_sources, '--vertex-track-matching-sources ' + pvfinder_matching_sources, @@ -1759,15 +1759,15 @@ def getDigiTaskName(det): if COLTYPE == "PbPb" or (doembedding and COLTYPEBKG == "PbPb"): svfinder_threads = ' --threads 8 ' svfinder_cpu = 8 - + svfinder_sources = dpl_option_from_config(anchorConfig, 'o2-primary-vertexing-workflow', '--vertex-track-matching-sources', default_value='ITS-TPC,TPC-TRD,ITS-TPC-TRD,TPC-TOF,ITS-TPC-TOF,TPC-TRD-TOF,ITS-TPC-TRD-TOF,MFT-MCH,MCH-MID,ITS,MFT,TPC,TOF,FT0,MID,EMC,PHS,CPV,ZDC,FDD,HMP,FV0,TRD,MCH,CTP') svfinder_sources = cleanDetectorInputList(svfinder_sources) - SVFINDERtask = createTask(name='svfinder_'+str(tf), needs=[PVFINDERtask['name'], FT0FV0EMCCTPDIGItask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu=svfinder_cpu, mem='5000') + SVFINDERtask = createTask(name='svfinder_'+str(tf), needs=[PVFINDERtask['name'], FT0FV0EMCCTPDIGItask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu=svfinder_cpu, mem='5000') SVFINDERtask['cmd'] = task_finalizer( - [ '${O2_ROOT}/bin/o2-secondary-vertexing-workflow', + [ '${O2_ROOT}/bin/o2-secondary-vertexing-workflow', getDPL_global_options(bigshm=True), svfinder_threads, putConfigValues(['svertexer', 'TPCCorrMap', 'GlobalParams'], {"NameConf.mDirMatLUT" : ".."} | tpcLocalCFreco), @@ -2093,7 +2093,7 @@ def remove_json_prefix(path): needs=[MIDRECOtask['name']], readerCommand='o2-mid-digits-reader-workflow | o2-mid-tracks-reader-workflow', configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mid-task.json') - + ### MCH if isActive('MCH'): addQCPerTF(taskName='MCHDigitsTaskQC', @@ -2119,7 +2119,7 @@ def remove_json_prefix(path): needs=[MCHMIDMATCHtask['name']], readerCommand='o2-global-track-cluster-reader --track-types "MCH,MID,MCH-MID" --cluster-types "MCH,MID"', configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mchmid-tracks-task.json') - + ### MCH && MID && MFT || MCH && MFT if isActive('MCH') and isActive('MID') and isActive('MFT') : addQCPerTF(taskName='MUONTracksMFTTaskQC', @@ -2132,7 +2132,7 @@ def remove_json_prefix(path): readerCommand='o2-global-track-cluster-reader --track-types "MCH,MFT,MFT-MCH" --cluster-types "MCH,MFT"', configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mftmch-tracks-task.json') - + #<------------------ TPC - time-series objects # initial implementation taken from comments in https://its.cern.ch/jira/browse/O2-4612 # TODO: this needs to be made configurable (as a function of which detectors are actually present) From 467683a358399b19b22cf982442627082dbf2f9f Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Mon, 31 Aug 2026 14:18:49 +0200 Subject: [PATCH 052/144] o2dpg_sim_workflow.py: always require MFTRECOtask for MFTMCHMATCHtask --- MC/bin/o2dpg_sim_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index da406b45a..b70da6742 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -1658,7 +1658,7 @@ def getDigiTaskName(det): #<--------- MFT-MCH forward matching forwardmatchneeds = [MCHRECOtask['name'], - MFTRECOtask['name'] if isActive("MFT") else None, + MFTRECOtask['name'], MCHMIDMATCHtask['name'] if isActive("MID") else None] MFTMCHMATCHtask = createTask(name='mftmchMatch_'+str(tf), needs=forwardmatchneeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') MFTMCHMATCHtask['cmd'] = task_finalizer( From 4e2c5243ae56a8c300893ebe0a657d25adba698b Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Mon, 31 Aug 2026 14:19:17 +0200 Subject: [PATCH 053/144] o2dpg_sim_workflow.py: only add reco tasks of smaller detectors if they are active --- MC/bin/o2dpg_sim_workflow.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index b70da6742..f9b7bc8c5 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -1422,7 +1422,8 @@ def getDigiTaskName(det): '--disable-slewing-calib', # because effect not simulated in MC putConfigValues() ]) - workflow['stages'].append(FT0RECOtask) + if isActive("FT0"): + workflow['stages'].append(FT0RECOtask) #<--------- ITS-TPC track matching task ITSTPCMATCHtask=createTask(name='itstpcMatch_'+str(tf), needs=[TPCRECOtask['name'], ITSRECOtask['name'], FT0RECOtask['name'] if isActive("FT0") else None], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='8000', relative_cpu=3/8) @@ -1553,7 +1554,8 @@ def getDigiTaskName(det): putConfigValues(), ('',' --disable-mc')[args.no_mc_labels], '--enable-clusters-root-output']) - workflow['stages'].append(MCHRECOtask) + if isActive("MCH"): + workflow['stages'].append(MCHRECOtask) #<--------- MID reco workflow MIDRECOtask = createTask(name='midreco_'+str(tf), needs=[getDigiTaskName("MID")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1573,7 +1575,8 @@ def getDigiTaskName(det): getDPL_global_options(ccdbbackend=False), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - workflow['stages'].append(FDDRECOtask) + if isActive("FDD"): + workflow['stages'].append(FDDRECOtask) #<--------- FV0 reco workflow FV0RECOtask = createTask(name='fv0reco_'+str(tf), needs=[getDigiTaskName("FV0")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1581,7 +1584,8 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - workflow['stages'].append(FV0RECOtask) + if isActive("FV0"): + workflow['stages'].append(FV0RECOtask) # calorimeters #<--------- EMC reco workflow @@ -1613,7 +1617,8 @@ def getDigiTaskName(det): getDPL_global_options(), '--subspec 0', ('',' --disable-mc')[args.no_mc_labels]]) - workflow['stages'].append(EMCRECOtask) + if isActive("EMC"): + workflow['stages'].append(EMCRECOtask) #<--------- PHS reco workflow PHSRECOtask = createTask(name='phsreco_'+str(tf), needs=[getDigiTaskName("PHS")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1674,7 +1679,8 @@ def getDigiTaskName(det): ['${O2_ROOT}/bin/o2-globalfwd-assessment-workflow', getDPL_global_options(), ('',' --disable-mc')[args.no_mc_labels]]) - workflow['stages'].append(MFTMCHMATCHtask) + if isActive("MFT") and isActive("MCH"): + workflow['stages'].append(MFTMCHMATCHtask) if args.fwdmatching_save_trainingdata == True: MFTMCHMATCHTraintask = createTask(name='mftmchMatchTrain_'+str(tf), needs=[MCHMIDMATCHtask['name'], MFTRECOtask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1689,7 +1695,8 @@ def getDigiTaskName(det): ['${O2_ROOT}/bin/o2-hmpid-digits-to-clusters-workflow', getDPL_global_options(ccdbbackend=False), putConfigValues()]) - workflow['stages'].append(HMPRECOtask) + if isActive("HMP"): + workflow['stages'].append(HMPRECOtask) #<--------- HMP forward matching hmpmatchneeds = [HMPRECOtask['name'], @@ -1705,7 +1712,8 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues() ]) - workflow['stages'].append(HMPMATCHtask) + if isActive("HMP"): + workflow['stages'].append(HMPMATCHtask) #<---------- primary vertex finding pvfinder_sources = dpl_option_from_config(anchorConfig, From 51ea14db9451ff5ee9f85fd3b0027c50a37052cc Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 1 Sep 2026 09:00:06 +0200 Subject: [PATCH 054/144] Example Pythia8 for HY (#2446) --- MC/config/examples/hybrid/examplehyp8.json | 79 +++++++++++++++++++ MC/config/examples/ini/GeneratorHYPythia8.ini | 4 + .../examples/ini/tests/GeneratorHYPythia8.C | 38 +++++++++ 3 files changed, 121 insertions(+) create mode 100644 MC/config/examples/hybrid/examplehyp8.json create mode 100644 MC/config/examples/ini/GeneratorHYPythia8.ini create mode 100644 MC/config/examples/ini/tests/GeneratorHYPythia8.C diff --git a/MC/config/examples/hybrid/examplehyp8.json b/MC/config/examples/hybrid/examplehyp8.json new file mode 100644 index 000000000..b7d66de30 --- /dev/null +++ b/MC/config/examples/hybrid/examplehyp8.json @@ -0,0 +1,79 @@ +{ + "mode": "parallel", + "generators": [ + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + }, + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + }, + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + }, + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + }, + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + }, + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + }, + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + }, + { + "name": "external", + "config": { + "fileName": "", + "funcName": "", + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/ini/pythia8_pp_136tev.ini" + } + } + ], + "fractions": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] +} \ No newline at end of file diff --git a/MC/config/examples/ini/GeneratorHYPythia8.ini b/MC/config/examples/ini/GeneratorHYPythia8.ini new file mode 100644 index 000000000..da82b263d --- /dev/null +++ b/MC/config/examples/ini/GeneratorHYPythia8.ini @@ -0,0 +1,4 @@ +[GeneratorHybrid] +configFile = ${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/hybrid/examplehyp8.json +switchExtToHybrid = true +num_workers = 8 diff --git a/MC/config/examples/ini/tests/GeneratorHYPythia8.C b/MC/config/examples/ini/tests/GeneratorHYPythia8.C new file mode 100644 index 000000000..2694d57e4 --- /dev/null +++ b/MC/config/examples/ini/tests/GeneratorHYPythia8.C @@ -0,0 +1,38 @@ +int Hybrid() +{ + std::string path{"o2sim_Kine.root"}; + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + // Check if there are 100 events, as simulated in the o2dpg-test + if (nEvents != 100) + { + std::cerr << "Expected 100 events, got " << nEvents << "\n"; + return 1; + } + return 0; +} From 89ef54082411959291346eaafe18c74ceb626c0d Mon Sep 17 00:00:00 2001 From: sgaretti <129837066+sgaretti@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:01:20 +0200 Subject: [PATCH 055/144] [PWG-DQ] Add new ini file to generate single muons at fwd y (#2444) * [PWG-DQ] Add new ini file to generate single muons at fwd y GeneratorParamSingleMuon_PbPb_536TeV.ini is a configuration file to use the external generator GeneratorParamSingleMuon_PbPb_5TeV.C --- .../GeneratorParamSingleMuon_PbPb_5TeV.ini | 4 + .../GeneratorParamSingleMuon_PbPb_5TeV.C | 78 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 MC/config/PWGDQ/ini/GeneratorParamSingleMuon_PbPb_5TeV.ini create mode 100644 MC/config/PWGDQ/ini/tests/GeneratorParamSingleMuon_PbPb_5TeV.C diff --git a/MC/config/PWGDQ/ini/GeneratorParamSingleMuon_PbPb_5TeV.ini b/MC/config/PWGDQ/ini/GeneratorParamSingleMuon_PbPb_5TeV.ini new file mode 100644 index 000000000..ee8dccc0e --- /dev/null +++ b/MC/config/PWGDQ/ini/GeneratorParamSingleMuon_PbPb_5TeV.ini @@ -0,0 +1,4 @@ +### The external generator derives from /MC/config/PWGDQ/external/generator/GeneratorParamSingleMuon_PbPb_5TeV.C +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/GeneratorParamSingleMuon_PbPb_5TeV.C +funcName=paramMuGen(797.446,0.830278,0.632177,10.2202,-0.000614809,-1.70993,1.87732,0.00658212,-0.0988071,-0.000452746,0.00269782,2,13,-4.,-2.3,0.0,999.,1) diff --git a/MC/config/PWGDQ/ini/tests/GeneratorParamSingleMuon_PbPb_5TeV.C b/MC/config/PWGDQ/ini/tests/GeneratorParamSingleMuon_PbPb_5TeV.C new file mode 100644 index 000000000..bbdb872b8 --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/GeneratorParamSingleMuon_PbPb_5TeV.C @@ -0,0 +1,78 @@ +int External() +{ + int checkPdgDecay = 13; + double yMin = -4.3; + double yMax = -2.3; + double ptMin = 0.0; + double ptMax = 999.; + + std::string path{"o2sim_Kine.root"}; + std::cout << "Check for primary muons, PDG +-" << checkPdgDecay << ", " << yMin << " < y < " << yMax << ", " << ptMin << " < pt < " << ptMax << " GeV/c\n"; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree*)file.Get("o2sim"); + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nMuons{}; + int nAntiMuons{}; + int nOutsideY{}; + int nOutsidePt{}; + int nNonPrimary{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto pt = track.GetPt(); + auto idMoth = track.getMotherTrackId(); + if (std::abs(pdg) != checkPdgDecay) { + continue; + } + if (pdg == checkPdgDecay) { + nMuons++; //counts muons + } else { + nAntiMuons++; //counts anti-muons + } + if (idMoth >= 0) { + nNonPrimary++; //counts non-primary muons (those with a mother track) + } + + if (rapidity < yMin || rapidity > yMax) { + nOutsideY++; + } + if (pt < ptMin || pt > ptMax) { + nOutsidePt++; + } + } + } + + std::cout << "#events: " << nEvents << "\n" + << "#mu-: " << nMuons << "\n" + << "#mu+: " << nAntiMuons << "\n" + << "#non-primary muons: " << nNonPrimary << "\n" + << "#muons outside y-range: " << nOutsideY << "\n" + << "#muons outside pt-range: " << nOutsidePt << "\n"; + + if (nMuons == 0 || nAntiMuons == 0) { + std::cerr << "Expected both mu- and mu+, found nMuons=" << nMuons << " nAntiMuons=" << nAntiMuons << "\n"; + return 1; + } + if (nNonPrimary != 0) { + std::cerr << "All generated muons should be primaries (no mother track).\n"; + return 1; + } + if (nOutsideY != 0 || nOutsidePt != 0) { + std::cerr << "Some muons were generated outside the configured (y, pt) window.\n"; + return 1; + } + + return 0; +} From 4fc4713b13949d9301c6d05c745c1f2758bc548b Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Tue, 1 Sep 2026 08:11:19 +0200 Subject: [PATCH 056/144] o2dpg_sim_workflow.py: use the helper function to add workflows --- MC/bin/o2dpg_sim_workflow.py | 74 ++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index f9b7bc8c5..304cbd905 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -331,7 +331,17 @@ def cleanDetectorInputList(inputlist): del activeDetectors['ZDC'] def addWhenActive(detID, needslist, appendstring): - if isActive(detID): + """Adds item to a list if detector(s) is (are) active + + Args: + detID: str + detector to check, can also be comma-separated list of detectors + needslist: list + list, e.g. of task dicts added to workflow['stages'] + appendstring: str, dict, ... + to be appended to needslist + """ + if all(isActive(part) for part in detID.split(",")): needslist.append(appendstring) def retrieve_sor(run_number): @@ -1175,8 +1185,7 @@ def putConfigValues(listOfMainKeys=[], localCF = {}, globalTFConfig = True): + ' --onlyDet TRD --interactionRate ' + str(INTRATE) + ' --incontext ' + str(CONTEXTFILE) + ' --disable-write-ini' \ + putConfigValues(localCF={"TRDSimParams.digithreads" : NWORKERS_TF, "DigiParams.seed" : str(TFSEED)}) + " --forceSelectedDets" TRDDigitask['cmd'] += ('',' --disable-mc')[args.no_mc_labels] - if isActive("TRD"): - workflow['stages'].append(TRDDigitask) + addWhenActive("TRD", workflow['stages'], TRDDigitask) # these are digitizers which are single threaded def createRestDigiTask(name, det='ALLSMALLER'): @@ -1422,8 +1431,7 @@ def getDigiTaskName(det): '--disable-slewing-calib', # because effect not simulated in MC putConfigValues() ]) - if isActive("FT0"): - workflow['stages'].append(FT0RECOtask) + addWhenActive("FT0", workflow['stages'], FT0RECOtask) #<--------- ITS-TPC track matching task ITSTPCMATCHtask=createTask(name='itstpcMatch_'+str(tf), needs=[TPCRECOtask['name'], ITSRECOtask['name'], FT0RECOtask['name'] if isActive("FT0") else None], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='8000', relative_cpu=3/8) @@ -1455,8 +1463,7 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("TRD"): - workflow['stages'].append(TRDTRACKINGtask) + addWhenActive("TRD", workflow['stages'], TRDTRACKINGtask) #<--------- TRD global tracking # FIXME This is so far a workaround to avoud a race condition for trdcalibratedtracklets.root @@ -1476,8 +1483,7 @@ def getDigiTaskName(det): '--track-sources ' + trd_track_sources, tpc_corr_scaling_options, tpc_corr_options_mc]) - if isActive("TRD"): - workflow['stages'].append(TRDTRACKINGtask2) + addWhenActive("TRD", workflow['stages'], TRDTRACKINGtask2) #<--------- TOF reco task TOFRECOtask = createTask(name='tofmatch_'+str(tf), needs=[ITSTPCMATCHtask['name'], getDigiTaskName("TOF")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1488,8 +1494,7 @@ def getDigiTaskName(det): putConfigValues(), ('',' --disable-mc')[args.no_mc_labels] ]) - if isActive('TOF'): - workflow['stages'].append(TOFRECOtask) + addWhenActive("TOF", workflow['stages'], TOFRECOtask) #<--------- TOF-TPC(-ITS) global track matcher workflow toftpcmatchneeds = [TOFRECOtask['name'], @@ -1517,8 +1522,7 @@ def getDigiTaskName(det): tpc_corr_options_mc ] TOFTPCMATCHERtask['cmd'] = task_finalizer(tofmatcher_cmd_parts) - if isActive('TOF'): - workflow['stages'].append(TOFTPCMATCHERtask) + addWhenActive("TOF", workflow['stages'], TOFTPCMATCHERtask) # MFT reco: needing access to kinematics (when assessment enabled) mftreconeeds = [getDigiTaskName("MFT")] @@ -1537,8 +1541,7 @@ def getDigiTaskName(det): 'MFTClustererParam']), ('','--disable-mc')[args.no_mc_labels], ('','--run-assessment')[args.mft_assessment_full]]) - if isActive("MFT"): - workflow['stages'].append(MFTRECOtask) + addWhenActive("MFT", workflow['stages'], MFTRECOtask) # MCH reco: needing access to kinematics ... so some extra logic needed here mchreconeeds = [getDigiTaskName("MCH")] @@ -1554,8 +1557,7 @@ def getDigiTaskName(det): putConfigValues(), ('',' --disable-mc')[args.no_mc_labels], '--enable-clusters-root-output']) - if isActive("MCH"): - workflow['stages'].append(MCHRECOtask) + addWhenActive("MCH", workflow['stages'], MCHRECOtask) #<--------- MID reco workflow MIDRECOtask = createTask(name='midreco_'+str(tf), needs=[getDigiTaskName("MID")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1566,8 +1568,7 @@ def getDigiTaskName(det): MIDRECOtask['cmd'] += task_finalizer(['${O2_ROOT}/bin/o2-mid-reco-workflow', getDPL_global_options(), putConfigValues(),('',' --disable-mc')[args.no_mc_labels]]) - if isActive('MID'): - workflow['stages'].append(MIDRECOtask) + addWhenActive("MID", workflow['stages'], MIDRECOtask) #<--------- FDD reco workflow FDDRECOtask = createTask(name='fddreco_'+str(tf), needs=[getDigiTaskName("FDD")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1575,8 +1576,7 @@ def getDigiTaskName(det): getDPL_global_options(ccdbbackend=False), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("FDD"): - workflow['stages'].append(FDDRECOtask) + addWhenActive("FDD", workflow['stages'], FDDRECOtask) #<--------- FV0 reco workflow FV0RECOtask = createTask(name='fv0reco_'+str(tf), needs=[getDigiTaskName("FV0")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1584,8 +1584,7 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("FV0"): - workflow['stages'].append(FV0RECOtask) + addWhenActive("FV0", workflow['stages'], FV0RECOtask) # calorimeters #<--------- EMC reco workflow @@ -1617,8 +1616,7 @@ def getDigiTaskName(det): getDPL_global_options(), '--subspec 0', ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("EMC"): - workflow['stages'].append(EMCRECOtask) + addWhenActive("EMC", workflow['stages'], EMCRECOtask) #<--------- PHS reco workflow PHSRECOtask = createTask(name='phsreco_'+str(tf), needs=[getDigiTaskName("PHS")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1627,8 +1625,7 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("PHS"): - workflow['stages'].append(PHSRECOtask) + addWhenActive("PHS", workflow['stages'], PHSRECOtask) #<--------- CPV reco workflow CPVRECOtask = createTask(name='cpvreco_'+str(tf), needs=[getDigiTaskName("CPV")], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1637,8 +1634,7 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("CPV"): - workflow['stages'].append(CPVRECOtask) + addWhenActive("CPV", workflow['stages'], CPVRECOtask) #<--------- ZDC reco workflow ZDCRECOtask = createTask(name='zdcreco_'+str(tf), needs=[getDigiTaskName("ZDC")], tf=tf, cwd=timeframeworkdir, lab=["RECO", "ZDC"]) @@ -1647,8 +1643,7 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("ZDC"): - workflow['stages'].append(ZDCRECOtask) + addWhenActive("ZDC", workflow['stages'], ZDCRECOtask) ## forward matching #<--------- MCH-MID forward matching @@ -1658,8 +1653,7 @@ def getDigiTaskName(det): getDPL_global_options(ccdbbackend=False), putConfigValues(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("MID") and isActive("MCH"): - workflow['stages'].append(MCHMIDMATCHtask) + addWhenActive("MID,MCH", workflow['stages'], MCHMIDMATCHtask) #<--------- MFT-MCH forward matching forwardmatchneeds = [MCHRECOtask['name'], @@ -1679,8 +1673,7 @@ def getDigiTaskName(det): ['${O2_ROOT}/bin/o2-globalfwd-assessment-workflow', getDPL_global_options(), ('',' --disable-mc')[args.no_mc_labels]]) - if isActive("MFT") and isActive("MCH"): - workflow['stages'].append(MFTMCHMATCHtask) + addWhenActive("MFT,MCH", workflow['stages'], MFTMCHMATCHtask) if args.fwdmatching_save_trainingdata == True: MFTMCHMATCHTraintask = createTask(name='mftmchMatchTrain_'+str(tf), needs=[MCHMIDMATCHtask['name'], MFTRECOtask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') @@ -1695,8 +1688,7 @@ def getDigiTaskName(det): ['${O2_ROOT}/bin/o2-hmpid-digits-to-clusters-workflow', getDPL_global_options(ccdbbackend=False), putConfigValues()]) - if isActive("HMP"): - workflow['stages'].append(HMPRECOtask) + addWhenActive("HMP", workflow['stages'], HMPRECOtask) #<--------- HMP forward matching hmpmatchneeds = [HMPRECOtask['name'], @@ -1712,8 +1704,7 @@ def getDigiTaskName(det): getDPL_global_options(), putConfigValues() ]) - if isActive("HMP"): - workflow['stages'].append(HMPMATCHtask) + addWhenActive("HMP", workflow['stages'], HMPMATCHtask) #<---------- primary vertex finding pvfinder_sources = dpl_option_from_config(anchorConfig, @@ -2156,9 +2147,8 @@ def remove_json_prefix(path): TPCTStask['cmd'] += ' --primary-vertices ' TPCTStask['cmd'] += ' | o2-tpc-time-series-workflow --enable-unbinned-root-output --sample-unbinned-tsallis --sampling-factor 0.01 ' TPCTStask['cmd'] += putConfigValues() + ' ' + getDPL_global_options(bigshm=True) - if isActive('TOF') and isActive('TPC') and isActive('FT0'): - # could be relaxed or changed once the timerseries worklow is more reactive to input cluster- and track-types - workflow['stages'].append(TPCTStask) + # could be relaxed or changed once the timerseries worklow is more reactive to input cluster- and track-types + addWhenActive("TOF,TPC,FT0", workflow['stages'], TPCTStask) # cleanup # -------- From 04450fa283fea416244b017274ef28bbc5cdac57 Mon Sep 17 00:00:00 2001 From: Chuntai <48704924+wuctlby@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:07:29 +0800 Subject: [PATCH 057/144] Tunable cb patch (#2448) * Update and rename GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini to GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.ini * Rename GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C to GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.C --- ...ar3_PbPb.ini => GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.ini} | 4 +++- ..._bbbar3_PbPb.C => GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.C} | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename MC/config/PWGHF/ini/{GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini => GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.ini} (60%) rename MC/config/PWGHF/ini/tests/{GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C => GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.C} (100%) diff --git a/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.ini similarity index 60% rename from MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini rename to MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.ini index 418304b17..37b778d4a 100644 --- a/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.ini +++ b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.ini @@ -2,7 +2,9 @@ ### The external generator derives from GeneratorPythia8. [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C -funcName=GeneratorPythia8EmbedHFRatio(3.0) +funcName=GeneratorPythia8EmbedHFRatio(0.2) +### bOverCRatio = 0.2 generates quarkDpgList = [5,4,4,4] +### because the loop stops at the first valid iteration (c=3, b=1) due to the early break, rather than finding the true closest ratio [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_Mode2_hardQCD_5TeV.cfg diff --git a/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.C similarity index 100% rename from MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar3_PbPb.C rename to MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar3_and_bbbar_PbPb.C From f6d13d0897ba78a8d4ef226fab71f4334fbe0968 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Thu, 3 Sep 2026 23:59:00 +0200 Subject: [PATCH 058/144] Adapt o2dpg_sim_workflow.py to new TPC corr.maps (#2451) Given that the new maps are provided by the tpc-scaler device and the latter is added in the MC by the o2-tpc-reco workflow, the corrections and CTP input request should be disable when the o2-tpc-reco is used for clusterization only (correction not used). In general, all settings of o2-tpc-scaler from the anchoring workflowconfig.log should be added to options of all MC reco workflows requiring TPC corrections (except the MC-specific settings like imposing --lumi-type 1 --corrmap-lumi-mode 2), but since at the moment MC does not simulate sector edge fluctuations, we simply add --disable-sec-edge-fluc-correction. --- MC/bin/o2dpg_sim_workflow.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 304cbd905..8f48700e6 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -1280,6 +1280,7 @@ def getDigiTaskName(det): if not args.combine_tpc_clusterization: # We treat TPC clusterization in multiple (sector) steps in order to # stay within the memory limit or to parallelize over sector from outside (not yet supported within cluster algo) + # For the clusterization we disable the TPC SC corrections tpcclustertasks=[] sectorpertask=18 for s in range(0,35,sectorpertask): @@ -1293,6 +1294,8 @@ def getDigiTaskName(det): getDPL_global_options(bigshm=True), '--input-type ' + ('digitizer','digits')[args.no_tpc_digitchunking], '--output-type clusters,send-clusters-per-sector', + '--corrmap-lumi-mode 3', + '--disable-ctp-lumi-request', f'--tpc-native-cluster-writer \" --outfile tpc-native-clusters-part{(int)(s/sectorpertask)}.root\"', f'--tpc-sectors {s}-{s+sectorpertask-1}', putConfigValues(["GPU_global", @@ -1302,6 +1305,7 @@ def getDigiTaskName(det): '--disable-mc' if args.no_mc_labels else None ], configname="tpcclusterizertask" ) + # RS: note that --disable-IDC-scalers should be added to options above once it is added by the CorrectionMapsOptions::addGlobalOptions) tpcclussect['env'] = { "OMP_NUM_THREADS" : "4" , "TBB_NUM_THREADS" : "4" } tpcclussect['semaphore'] = "tpctriggers.root" @@ -1316,7 +1320,7 @@ def getDigiTaskName(det): # TODO: adapt this to the case above and merge code / avoid code duplication tpcclus = createTask(name='tpccluster_' + str(tf), needs=tpcclusterneed, tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu=NWORKERS_TF, mem='2000') tpcclus['cmd'] = '${O2_ROOT}/bin/o2-tpc-chunkeddigit-merger --tpc-lanes ' + str(NWORKERS_TF) - tpcclus['cmd'] += ' | ${O2_ROOT}/bin/o2-tpc-reco-workflow ' + getDPL_global_options() + ' --input-type digitizer --output-type clusters,send-clusters-per-sector ' + putConfigValues(["GPU_global","TPCGasParam","TPCCorrMap"],{"GPU_proc.ompThreads" : 1}) + ('',' --disable-mc')[args.no_mc_labels] + tpcclus['cmd'] += ' | ${O2_ROOT}/bin/o2-tpc-reco-workflow ' + getDPL_global_options() + ' --input-type digitizer --output-type clusters,send-clusters-per-sector --corrmap-lumi-mode 3 --disable-ctp-lumi-request ' + putConfigValues(["GPU_global","TPCGasParam","TPCCorrMap"],{"GPU_proc.ompThreads" : 1}) + ('',' --disable-mc')[args.no_mc_labels] workflow['stages'].append(tpcclus) tpcreconeeds.append(tpcclus['name']) @@ -1361,10 +1365,13 @@ def getDigiTaskName(det): if not isActive('CTP'): # CTP digits won't be produced for this timeframe (CTP not in the readout detector list) tpc_corr_scaling_options += ' --disable-ctp-lumi-request' - + # why not simply? # tpc_corr_scaling_options = ('--lumi-type 1', '')[tpcDistortionType != 0] + # at the moment MC is not ready for the sector edge fluctuations (RS: and does not use IDC but --disable-IDC-scalers can be added only once it is added by the CorrectionMapsOptions::addGlobalOptions) + tpc_corr_scaling_options += option_if_available('o2-tpc-reco-workflow', '--disable-sec-edge-fluc-correction', envfile=async_envfile) + #<--------- TPC reco task if includeTPCSyncMode: tpcSyncreconeeds = tpcreconeeds.copy() From 3418de0e52f63ba1a3f11cdffe7f1b5829f8d547 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Fri, 4 Sep 2026 10:51:33 +0200 Subject: [PATCH 059/144] Fix missing comment character (#2453) --- MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns | 2 +- MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns b/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns index a8659654a..5a5a445d4 100644 --- a/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns +++ b/MC/config/examples/epos4/generator/NeNe_536TeV_EPOS4HQ.optns @@ -23,7 +23,7 @@ end !---EPOS4HQ specific changes--- set hqnbcol 1 set hqnbcolsaveEPOS 1 -------------------------------- +!------------------------------- set ninicon 1 !number of initial conditions used for hydro evolution core full !core/corona activated hydro hlle !hydro activated diff --git a/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns b/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns index b3d37d7e1..4100c362a 100644 --- a/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns +++ b/MC/config/examples/epos4/generator/OO_536TeV_EPOS4HQ.optns @@ -23,7 +23,7 @@ end !---EPOS4HQ specific changes--- set hqnbcol 1 set hqnbcolsaveEPOS 1 -------------------------------- +!------------------------------- set ninicon 1 !number of initial conditions used for hydro evolution core full !core/corona activated hydro hlle !hydro activated From 5afa9432055e3119232b27c9a3e840e75bf3db56 Mon Sep 17 00:00:00 2001 From: mj525 <99000728+mjkim525@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:54:46 +0900 Subject: [PATCH 060/144] Fix independent LF resonance gun RNG seeding (#2456) Seed the separate Pythia gun RNG from ROOT before reading decayer settings. Preserve ROOT SetSeed(0) and explicit decayer configuration priority. Log the effective gun seed once during construction and warn when seeding is disabled or nonpositive. --- .../pythia8/generator_pythia8_LF_rapidity_width.C | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C b/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C index 7caf0fdd3..ed0a8c990 100644 --- a/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +++ b/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C @@ -153,6 +153,12 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 /** switch off process level **/ mPythiaGun.readString("ProcessLevel:all off"); + // The gun owns a separate RNG: seeding the inherited mPythia does not + // seed its mass sampler or decays. Derive a valid, nonzero Pythia seed + // from ROOT's RNG; explicit decayer configuration can still override it. + mPythiaGun.readString("Random:setSeed = on"); + mPythiaGun.readString("Random:seed = " + std::to_string(1 + gRandom->Integer(900000000))); + auto& param = o2::eventgen::DecayerPythia8Param::Instance(); LOG(info) << "Init \'GeneratorPythia8LFRapidity\' with following parameters"; LOG(info) << param; @@ -177,6 +183,13 @@ class GeneratorPythia8LFRapidity : public o2::eventgen::GeneratorPythia8 mPythiaGun.readString(std::string("Init:showChangedParticleData off")); } + LOG(info) << "LF gun RNG: Random:setSeed=" << mPythiaGun.settings.flag("Random:setSeed") + << ", Random:seed=" << mPythiaGun.settings.mode("Random:seed"); + if (!mPythiaGun.settings.flag("Random:setSeed") || mPythiaGun.settings.mode("Random:seed") <= 0) { + LOG(warn) << "Decayer configuration overrides the ROOT-derived gun seed; " + << "the gun may use default or time-based seeding"; + } + /** initialise **/ if (!mPythiaGun.init()) { LOG(fatal) << "Failed to init \'DecayerPythia8\': init returned with error"; From 2a8aa17e76f28ac0908353992b1f10d42cb9c54c Mon Sep 17 00:00:00 2001 From: alcaliva <32872606+alcaliva@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:53:49 +0200 Subject: [PATCH 061/144] Update ini file for cascades in jets (#2458) --- .../ini/GeneratorLFCascadesInJets_pp536TeV.ini | 13 +++++++++---- .../pythia8/generator/pythia8_jet_ropes_536tev.cfg | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV.ini b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV.ini index 7f96b4461..c24165d49 100644 --- a/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV.ini +++ b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV.ini @@ -1,10 +1,15 @@ +### Cascades in jets - pp collisions at 5.36 TeV + [GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one cascade species fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C -# funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/strangeparticlelist.gun", 0) -funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/strangeparticlelist.gun", 4) +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/strangeparticlelist.gun", 2) -[GeneratorPythia8] # if triggered then this will be used as the background event +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false [DecayerPythia8] -config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg \ No newline at end of file diff --git a/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg b/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg index eb7aa7a86..308492494 100644 --- a/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +++ b/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg @@ -12,7 +12,7 @@ ParticleDecays:limitTau0 = on ParticleDecays:tau0Max = 10. ### phase space cuts -PhaseSpace:pTHatMin = 8 +PhaseSpace:pTHatMin = 5 PhaseSpace:pTHatMax = 600 PhaseSpace:bias2Selection = on PhaseSpace:bias2SelectionPow = 4 From b448e9d46ebb7d0f0e07c12601a2c3ab1b78c8dc Mon Sep 17 00:00:00 2001 From: alcaliva <32872606+alcaliva@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:29:25 +0200 Subject: [PATCH 062/144] Add generator for V0s in jets with injection (#2459) --- .../ini/GeneratorLFV0sInJets_pp536TeV.ini | 15 +++++ .../ini/tests/GeneratorLFV0sInJets_pp536TeV.C | 57 +++++++++++++++++++ .../pythia8/generator/v0particlelist.gun | 4 ++ 3 files changed, 76 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLFV0sInJets_pp536TeV.C create mode 100644 MC/config/PWGLF/pythia8/generator/v0particlelist.gun diff --git a/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV.ini b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV.ini new file mode 100644 index 000000000..ba7f9fbb0 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV.ini @@ -0,0 +1,15 @@ +### V0s in jets - pp collisions at 5.36 TeV + +[GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one V0 particle +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/v0particlelist.gun", 2) + +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false + +[DecayerPythia8] +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg \ No newline at end of file diff --git a/MC/config/PWGLF/ini/tests/GeneratorLFV0sInJets_pp536TeV.C b/MC/config/PWGLF/ini/tests/GeneratorLFV0sInJets_pp536TeV.C new file mode 100644 index 000000000..4fec0a0f0 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLFV0sInJets_pp536TeV.C @@ -0,0 +1,57 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + int numberOfInjectedSignalsPerEvent{1}; + std::vector injectedPDGs = { + 310, // K0S + 3122, // Lambda + -3122 // Anti-Lambda + }; + auto nInjection = injectedPDGs.size(); + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + std::vector nSignal; + for (int i = 0; i < nInjection; i++) { + nSignal.push_back(0); + } + + auto nEvents = tree->GetEntries(); + for (int i = 0; i < nEvents; i++) { + auto check = tree->GetEntry(i); + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) { + auto track = tracks->at(idxMCTrack); + auto pdg = track.GetPdgCode(); + auto it = std::find(injectedPDGs.begin(), injectedPDGs.end(), pdg); + int index = std::distance(injectedPDGs.begin(), it); // index of injected PDG + if (it != injectedPDGs.end()) // found + { + // count signal PDG + nSignal[index]++; + } + } + } + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + for (int i = 0; i < nInjection; i++) { + std::cout << "# Injected nuclei \n"; + std::cout << injectedPDGs[i] << ": " << nSignal[i] << "\n"; + if (nSignal[i] == 0) { + std::cerr << "No generated: " << injectedPDGs[i] << "\n"; + return 1; // At least one of the injected particles should be generated + } + } + return 0; +} diff --git a/MC/config/PWGLF/pythia8/generator/v0particlelist.gun b/MC/config/PWGLF/pythia8/generator/v0particlelist.gun new file mode 100644 index 000000000..1c2ab6979 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/v0particlelist.gun @@ -0,0 +1,4 @@ +# PDG N ptMin ptMax etaMin etaMax genDecayed +310 1 0.2 10 -1.2 1.2 +3122 1 0.2 10 -1.2 1.2 +-3122 1 0.2 10 -1.2 1.2 From 0bb96707e62c7d214aaa24b8297f7c92e90dbda8 Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Fri, 11 Sep 2026 13:34:37 +0200 Subject: [PATCH 063/144] Fix reassignment of mother indices after pruning HF events in embedding (#2457) --- .../generator/generator_pythia8_embed_hf.C | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C b/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C index 7f325eb67..2dfbf7c60 100644 --- a/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C +++ b/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C @@ -280,7 +280,7 @@ Bool_t importParticles() override /// Establish if this particle comes from charm or beauty /// If not, ignore this particle and increase the number of discarded particles from the pp event - if(!isFromCharmOrBeauty(iPart, particlesHfEvent)) { + if(!(nEvsHF == 0 && iPart < 1) && !isFromCharmOrBeauty(iPart, particlesHfEvent)) { continue; } /// if we arrive here, then the current particle is from charm or beauty, keep it! @@ -314,11 +314,13 @@ Bool_t importParticles() override idFirstMother = findKey(mapHfParticles, idFirstMother); /// If idFirstMother>=0, the 1st mother is from charm or beauty, i.e. is not a light-flavoured parton /// Instead, if idFirstMother==-1 from findKey this means that the first mother was a light-flavoured parton --> not stored in the map - if(idFirstMother >=0) { + if(idFirstMother >= 0) { /// the 1st mother is from charm or beauty, i.e. is not a light-flavoured parton if(idLastMother != idFirstMotherOrig) { - if(idLastMother != -1) { - /// idLastMother is >= 0 + idLastMother = findKey(mapHfParticles, idLastMother); + if(idLastMother < 0) { + // second mother not found, we set it to 0 + idLastMother = 0; } } else { /// idLastMother is equal to idFirstMother @@ -343,15 +345,17 @@ Bool_t importParticles() override const int idMother = findKey(mapHfParticles, idMotherOrig); if(idMother >= 0) { /// this should mean that the mother is from HF, i.e. that we found the correct one - idFirstMother = idMother; - idLastMother = idFirstMother; + if (idFirstMother < 0) { + idFirstMother = idMother; + } + idLastMother = idMother; foundAnyMother = true; - break; } } - // set last mother to -1 if no mother has been found so far + // set last mother to 0 if no mother has been found so far if (!foundAnyMother) { - idLastMother = -1; + idLastMother = 0; + idFirstMother = 0; } } @@ -360,10 +364,10 @@ Bool_t importParticles() override idLastDaughter = findKey(mapHfParticles, idLastDaughter); /// adjust the particle mother and daughter indices - particle.SetFirstMother((idFirstMother >= 0) ? idFirstMother + offset : idFirstMother); - particle.SetLastMother((idLastMother >= 0) ? idLastMother + offset : idLastMother); - particle.SetFirstDaughter((idFirstDaughter >= 0) ? idFirstDaughter + offset : idFirstDaughter); - particle.SetLastDaughter((idLastDaughter >= 0) ? idLastDaughter + offset : idLastDaughter); + particle.SetFirstMother((idFirstMother > 0) ? idFirstMother + offset : idFirstMother); + particle.SetLastMother((idLastMother > 0) ? idLastMother + offset : idLastMother); + particle.SetFirstDaughter((idFirstDaughter > 0) ? idFirstDaughter + offset : idFirstDaughter); + particle.SetLastDaughter((idLastDaughter > 0) ? idLastDaughter + offset : idLastDaughter); /// copy inside this.mParticles from mGeneratorEvHF.mParticles, i.e. the particles generated in mGeneratorEvHF mParticles.push_back(particle); @@ -371,10 +375,10 @@ Bool_t importParticles() override } // for debug - // LOG(info) << "-----------------------------------------------"; - // LOG(info) << "============ After HF event " << nEvsHF; - // LOG(info) << "Full stack:"; - // printParticleVector(mParticles); + // LOG(info) << "-----------------------------------------------"; + // LOG(info) << "============ After HF event " << nEvsHF; + // LOG(info) << "Full stack:"; + // printParticleVector(mParticles); /// one more event generated, let's update the counter and clear it, to allow the next generation nEvsHF++; From 860289e5393d114fe1248086c983b585ca86eb39 Mon Sep 17 00:00:00 2001 From: alcaliva <32872606+alcaliva@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:48:21 +0200 Subject: [PATCH 064/144] [PWGLF] Add generator for antideuterons in jets (#2460) --- .../ini/GeneratorLFAntideuteronInJets.ini | 7 +++ .../ini/tests/GeneratorLFAntideuteronInJets.C | 61 +++++++++++++++++++ .../PWGLF/pythia8/generator/antideuteron.gun | 2 + 3 files changed, 70 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLFAntideuteronInJets.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLFAntideuteronInJets.C create mode 100644 MC/config/PWGLF/pythia8/generator/antideuteron.gun diff --git a/MC/config/PWGLF/ini/GeneratorLFAntideuteronInJets.ini b/MC/config/PWGLF/ini/GeneratorLFAntideuteronInJets.ini new file mode 100644 index 000000000..31463851b --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFAntideuteronInJets.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_longlived_gaptriggered.C +funcName=generateLongLivedGapTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/antideuteron.gun", 2, false) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg +includePartonEvent=false \ No newline at end of file diff --git a/MC/config/PWGLF/ini/tests/GeneratorLFAntideuteronInJets.C b/MC/config/PWGLF/ini/tests/GeneratorLFAntideuteronInJets.C new file mode 100644 index 000000000..148a08c58 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLFAntideuteronInJets.C @@ -0,0 +1,61 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + int numberOfInjectedSignalsPerEvent{1}; + + std::vector injectedPDGs = { + -1000010020 // anti-deuteron + }; + + auto nInjection = injectedPDGs.size(); + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + std::vector nSignal(nInjection, 0); + + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) { + auto track = tracks->at(idxMCTrack); + auto pdg = track.GetPdgCode(); + + auto it = std::find(injectedPDGs.begin(), injectedPDGs.end(), pdg); + + if (it != injectedPDGs.end()) { + int index = std::distance(injectedPDGs.begin(), it); + nSignal[index]++; + } + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + + for (int i = 0; i < nInjection; i++) { + std::cout << "# Injected particles\n"; + std::cout << injectedPDGs[i] << ": " << nSignal[i] << "\n"; + + if (nSignal[i] == 0) { + std::cerr << "No generated: " << injectedPDGs[i] << "\n"; + return 1; + } + } + + return 0; +} diff --git a/MC/config/PWGLF/pythia8/generator/antideuteron.gun b/MC/config/PWGLF/pythia8/generator/antideuteron.gun new file mode 100644 index 000000000..a3342f2c2 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/antideuteron.gun @@ -0,0 +1,2 @@ +# PDG N ptMin ptMax yMin yMax +-1000010020 1 0.5 10 -1 1 From 82d1ca0a9e1f4f4a3932b9b792a58094eec2dbd7 Mon Sep 17 00:00:00 2001 From: jaimenorman Date: Mon, 14 Sep 2026 10:12:40 +0100 Subject: [PATCH 065/144] add different gap configs for 13 TeV (#2450) --- .../GeneratorJE_gapgen3_hook_pp13600GeV.ini | 9 +++ .../GeneratorJE_gapgen4_hook_pp13600GeV.ini | 9 +++ .../GeneratorJE_gapgen5_hook_pp13600GeV.ini | 9 +++ .../GeneratorJE_gapgen3_hook_pp13600GeV.C | 71 +++++++++++++++++++ .../GeneratorJE_gapgen4_hook_pp13600GeV.C | 71 +++++++++++++++++++ .../GeneratorJE_gapgen5_hook_pp13600GeV.C | 71 +++++++++++++++++++ 6 files changed, 240 insertions(+) create mode 100644 MC/config/PWGGAJE/ini/GeneratorJE_gapgen3_hook_pp13600GeV.ini create mode 100644 MC/config/PWGGAJE/ini/GeneratorJE_gapgen4_hook_pp13600GeV.ini create mode 100644 MC/config/PWGGAJE/ini/GeneratorJE_gapgen5_hook_pp13600GeV.ini create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen3_hook_pp13600GeV.C create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen4_hook_pp13600GeV.C create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen5_hook_pp13600GeV.C diff --git a/MC/config/PWGGAJE/ini/GeneratorJE_gapgen3_hook_pp13600GeV.ini b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen3_hook_pp13600GeV.ini new file mode 100644 index 000000000..60872a4fb --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen3_hook_pp13600GeV.ini @@ -0,0 +1,9 @@ +### jet-jet production with MB Gap 3 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_jets_hook.C +funcName = getGeneratorPythia8GapGenJE(3,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/GeneratorJE_gapgen4_hook_pp13600GeV.ini b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen4_hook_pp13600GeV.ini new file mode 100644 index 000000000..a551ef6ca --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen4_hook_pp13600GeV.ini @@ -0,0 +1,9 @@ +### jet-jet production with MB Gap 4 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_jets_hook.C +funcName = getGeneratorPythia8GapGenJE(4,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/GeneratorJE_gapgen5_hook_pp13600GeV.ini b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen5_hook_pp13600GeV.ini new file mode 100644 index 000000000..947b04251 --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen5_hook_pp13600GeV.ini @@ -0,0 +1,9 @@ +### jet-jet production with MB Gap 5 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_jets_hook.C +funcName = getGeneratorPythia8GapGenJE(5,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen3_hook_pp13600GeV.C b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen3_hook_pp13600GeV.C new file mode 100644 index 000000000..db42706cb --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen3_hook_pp13600GeV.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./3; // one event triggered out of 3 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# Jet-jet events " << nEventsJetJet << "\n"; + std::cout << " sum of weights jet-jet events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.95 || nEventsJetJet > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of jet-jet generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.95 || nEventsMB > sumWeightsMB * 1.05) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen4_hook_pp13600GeV.C b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen4_hook_pp13600GeV.C new file mode 100644 index 000000000..6dfa0975e --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen4_hook_pp13600GeV.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./4; // one event triggered out of 4 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# Jet-jet events " << nEventsJetJet << "\n"; + std::cout << " sum of weights jet-jet events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.95 || nEventsJetJet > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of jet-jet generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.95 || nEventsMB > sumWeightsMB * 1.05) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen5_hook_pp13600GeV.C b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen5_hook_pp13600GeV.C new file mode 100644 index 000000000..5ac7f5173 --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen5_hook_pp13600GeV.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./5; // one event triggered out of 5 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# Jet-jet events " << nEventsJetJet << "\n"; + std::cout << " sum of weights jet-jet events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.95 || nEventsJetJet > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of jet-jet generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.95 || nEventsMB > sumWeightsMB * 1.05) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} From a75fe0d2cdd12a5ed0a81a749d3657e6e85e519f Mon Sep 17 00:00:00 2001 From: aimeric-landou <46970521+aimeric-landou@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:34:39 +0100 Subject: [PATCH 066/144] [PWGGAJE] jet param model: add bkg-only, fix eta (#2452) * [PWGGAJE] jet param model: add bkg-only, fix eta --- ...x_parametrisedModel_pythia6Fragmentation.C | 200 +++++++++--------- ...ythia6Fragmentation_PbPb_cent0010_Bkg.ini} | 2 +- ...ythia6Fragmentation_PbPb_cent0010_Sgl.ini} | 2 +- ...ia6Fragmentation_PbPb_cent0010_SglBkg.ini} | 2 +- ...pythia6Fragmentation_PbPb_cent5080_Bkg.ini | 7 + ...pythia6Fragmentation_PbPb_cent5080_Sgl.ini | 7 + ...ia6Fragmentation_PbPb_cent5080_SglBkg.ini} | 2 +- ..._pythia6Fragmentation_PbPb_cent0010_Bkg.C} | 0 ..._pythia6Fragmentation_PbPb_cent0010_Sgl.C} | 0 ...thia6Fragmentation_PbPb_cent0010_SglBkg.C} | 0 ..._pythia6Fragmentation_PbPb_cent5080_Bkg.C} | 0 ...l_pythia6Fragmentation_PbPb_cent5080_Sgl.C | 34 +++ ...ythia6Fragmentation_PbPb_cent5080_SglBkg.C | 34 +++ 13 files changed, 190 insertions(+), 100 deletions(-) rename MC/config/PWGGAJE/ini/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini} (89%) rename MC/config/PWGGAJE/ini/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini} (89%) rename MC/config/PWGGAJE/ini/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini} (89%) create mode 100644 MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini create mode 100644 MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini rename MC/config/PWGGAJE/ini/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini} (89%) rename MC/config/PWGGAJE/ini/tests/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.C => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.C} (100%) rename MC/config/PWGGAJE/ini/tests/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.C => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.C} (100%) rename MC/config/PWGGAJE/ini/tests/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.C => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.C} (100%) rename MC/config/PWGGAJE/ini/tests/{GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.C => GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.C} (100%) create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.C create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.C diff --git a/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C index e07298280..2fd75aa34 100644 --- a/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C +++ b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C @@ -29,8 +29,8 @@ using namespace Pythia8; // inputFilePathName file is expected to be a json file with the structure like so: // { // "simLog": false, -// "sglGenRAA": 1, -// "sglGenTAA": 1, +// "sglGenRAA": 0.45, +// "sglGenTAA": 23.38, // "sglCutoffSteepNess": 15, // "sglCutoffAbscissa": 10, // "fallSpecterSlopeLog": 10.833274, @@ -46,7 +46,12 @@ using namespace Pythia8; class GeneratorParametrisedJetModel : public o2::eventgen::GeneratorPythia8 { public: /// constructor - GeneratorParametrisedJetModel(std::string inputSimParametersPath, std::string inputSimParametersFileName, bool generateUE = true) : mInputSimParametersPath{inputSimParametersPath}, mInputSimParametersFileName{inputSimParametersFileName}, mGenerateUE{generateUE} { + GeneratorParametrisedJetModel(std::string inputSimParametersPath, std::string inputSimParametersFileName, bool generateUE, bool generateSignal = true) : mInputSimParametersPath{inputSimParametersPath}, mInputSimParametersFileName{inputSimParametersFileName}, mGenerateUE{generateUE}, mGenerateSignal{generateSignal} { + + if (!generateUE && !generateSignal) { + cout << "Both the UE and the signal generation should not be off at the same time" << endl; + exit(1); + } std::string inputFilePathName = "alien://" + inputSimParametersPath + inputSimParametersFileName; if (!gGrid) { @@ -182,100 +187,102 @@ public: ////////////////// Jet signal ///////////////// /////////////////////////////////////////////// - int nJets = gRandom->Poisson(mNJetsAverage); - - if (mDebug) { - cout << "####################### creating partons signal #######################" << endl; - cout << "signal: count " << nJets << " for mNJetsAverage = " << mNJetsAverage << "" << endl; - } - - TClonesArray *genParticlesArray = new TClonesArray("TParticle", 1000); - - int particleCountCurrent = 1; // counts the system fake particle pdg 90 - for (int iJet{0}; iJet < nJets; ++iJet) { - genParticlesArray->Delete(); - - const bool isQuark = gRandom->Uniform(0, 1) > 1. / 3 ? true : false; // ratio quarks:gluons = 2:1 - const int pdgQuark = gRandom->Uniform(0, 1) > 1. / 2 ? mPdgQuarkU : mPdgQuarkD; // half and half for u and d quarks - const int pdgJet = (int)isQuark * pdgQuark + (1 - (int)isQuark) * mPdgGluon; - const double sglMass = TDatabasePDG::Instance()->GetParticle(pdgJet)->Mass(); - const double sglPt = mJetYieldFit->GetRandom(mSglCutoffAbscissa, mPtInfinity); - const double sglEta = gRandom->Uniform(mGenMinEta, mGenMaxEta); - const double sglPhi = gRandom->Uniform(0, o2::constants::math::TwoPI); - const double sglPx{sglPt * std::cos(sglPhi)}; - const double sglPy{sglPt * std::sin(sglPhi)}; - const double sglPz{sglPt * std::sinh(sglEta)}; - const double sglEt{std::hypot(std::hypot(sglPt, sglPz), sglMass)}; - - Particle myJet; - myJet.id(pdgJet); - myJet.status(23); - myJet.px(sglPx); - myJet.py(sglPy); - myJet.pz(sglPz); - myJet.e(sglEt); - myJet.m(sglMass); - myJet.xProd(0); - myJet.yProd(0); - myJet.zProd(0); - int jetCol = 101 + 2 * iJet; // each jet gets a different colour and acolour value, - // so that the pythia knows they're distinct colour - // lines, i.e. different strings - int jetACol = 101 + 2 * iJet + 1; + if (mGenerateSignal) { + int nJets = gRandom->Poisson(mNJetsAverage); if (mDebug) { - cout << "-- || jet parton #" << iJet - << ", index=" << mPythia.event.back().index() + 1 - << ": isQuark = " << isQuark << ", pdg = " << pdgJet - << ", pt = " << sglPt << ", mass = " << sglMass << endl; + cout << "####################### creating partons signal #######################" << endl; + cout << "signal: count " << nJets << " for mNJetsAverage = " << mNJetsAverage << "" << endl; } - auto pythia6Event = TPythia6::Instance(); - - int lineNumber = 0; // line number seems to be index of particle in array; set to 0 in - // PM code? but doc says it runs Pyexec() right after; iJet doesn't - // work for ijet=1+; set to 0 and it seems to work - double sglTheta = 2.0 * std::atan(std::exp(-1 * sglEta)); - pythia6Event->Py1ent(lineNumber, pdgJet, sglEt, sglTheta, sglPhi); - - int final = pythia6Event->ImportParticles(genParticlesArray, "Final"); // only saves final state particles; "All" would instead give all the particles - int nConstituents = genParticlesArray->GetEntries(); - - mPythia.event.append(myJet); - for (int iParticle = 0; iParticle < nConstituents; ++iParticle) { - TParticle *tParticle = (TParticle *)genParticlesArray->At(iParticle); - - Particle pythiaParticle; - pythiaParticle.id(tParticle->GetPdgCode()); - pythiaParticle.status(tParticle->GetStatusCode()); // in pythia6 all the particles that are not - // the initial partons have status code 1; - // this is because apparently pythia6 does - // not decay them automatically yet; they - // are by no means all stable particles; but - // we do not care for this study - pythiaParticle.px(tParticle->Px()); - pythiaParticle.py(tParticle->Py()); - pythiaParticle.pz(tParticle->Pz()); - pythiaParticle.e(tParticle->Energy()); - pythiaParticle.m(tParticle->GetMass()); - pythiaParticle.xProd(tParticle->Vx()); - pythiaParticle.yProd(tParticle->Vy()); - pythiaParticle.zProd(tParticle->Vz()); - pythiaParticle.mother1(particleCountCurrent); // particleCountCurrent is the offset to account for existing IDs of constituents of previous jets. - // Not saving the actual mother ID because ImportParticles(genParticlesArray, "Final") only saves the final - // particles, and the mother id still refer to particles not saved in - // genParticlesArray a priori the mother-daughter links aren't needed - // for the closure test this will be used for; keeping the initial - // parton as mother for now - - mPythia.event.append(pythiaParticle); + TClonesArray *genParticlesArray = new TClonesArray("TParticle", 1000); + + int particleCountCurrent = 1; // counts the system fake particle pdg 90 + for (int iJet{0}; iJet < nJets; ++iJet) { + genParticlesArray->Delete(); + + const bool isQuark = gRandom->Uniform(0, 1) > 1. / 3 ? true : false; // ratio quarks:gluons = 2:1 + const int pdgQuark = gRandom->Uniform(0, 1) > 1. / 2 ? mPdgQuarkU : mPdgQuarkD; // half and half for u and d quarks + const int pdgJet = (int)isQuark * pdgQuark + (1 - (int)isQuark) * mPdgGluon; + const double sglMass = TDatabasePDG::Instance()->GetParticle(pdgJet)->Mass(); + const double sglPt = mJetYieldFit->GetRandom(mSglCutoffAbscissa, mPtInfinity); + const double sglEta = gRandom->Uniform(mGenMinEta, mGenMaxEta); + const double sglPhi = gRandom->Uniform(0, o2::constants::math::TwoPI); + const double sglPx{sglPt * std::cos(sglPhi)}; + const double sglPy{sglPt * std::sin(sglPhi)}; + const double sglPz{sglPt * std::sinh(sglEta)}; + const double sglEt{std::hypot(std::hypot(sglPt, sglPz), sglMass)}; + + Particle myJet; + myJet.id(pdgJet); + myJet.status(23); + myJet.px(sglPx); + myJet.py(sglPy); + myJet.pz(sglPz); + myJet.e(sglEt); + myJet.m(sglMass); + myJet.xProd(0); + myJet.yProd(0); + myJet.zProd(0); + int jetCol = 101 + 2 * iJet; // each jet gets a different colour and acolour value, + // so that the pythia knows they're distinct colour + // lines, i.e. different strings + int jetACol = 101 + 2 * iJet + 1; + + if (mDebug) { + cout << "-- || jet parton #" << iJet + << ", index=" << mPythia.event.back().index() + 1 + << ": isQuark = " << isQuark << ", pdg = " << pdgJet + << ", pt = " << sglPt << ", mass = " << sglMass << endl; + } + + auto pythia6Event = TPythia6::Instance(); + + int lineNumber = 0; // line number seems to be index of particle in array; set to 0 in + // PM code? but doc says it runs Pyexec() right after; iJet doesn't + // work for ijet=1+; set to 0 and it seems to work + double sglTheta = 2.0 * std::atan(std::exp(-1 * sglEta)); + pythia6Event->Py1ent(lineNumber, pdgJet, sglEt, sglTheta, sglPhi); + + int final = pythia6Event->ImportParticles(genParticlesArray, "Final"); // only saves final state particles; "All" would instead give all the particles + int nConstituents = genParticlesArray->GetEntries(); + + mPythia.event.append(myJet); + for (int iParticle = 0; iParticle < nConstituents; ++iParticle) { + TParticle *tParticle = (TParticle *)genParticlesArray->At(iParticle); + + Particle pythiaParticle; + pythiaParticle.id(tParticle->GetPdgCode()); + pythiaParticle.status(tParticle->GetStatusCode()); // in pythia6 all the particles that are not + // the initial partons have status code 1; + // this is because apparently pythia6 does + // not decay them automatically yet; they + // are by no means all stable particles; but + // we do not care for this study + pythiaParticle.px(tParticle->Px()); + pythiaParticle.py(tParticle->Py()); + pythiaParticle.pz(tParticle->Pz()); + pythiaParticle.e(tParticle->Energy()); + pythiaParticle.m(tParticle->GetMass()); + pythiaParticle.xProd(tParticle->Vx()); + pythiaParticle.yProd(tParticle->Vy()); + pythiaParticle.zProd(tParticle->Vz()); + pythiaParticle.mother1(particleCountCurrent); // particleCountCurrent is the offset to account for existing IDs of constituents of previous jets. + // Not saving the actual mother ID because ImportParticles(genParticlesArray, "Final") only saves the final + // particles, and the mother id still refer to particles not saved in + // genParticlesArray a priori the mother-daughter links aren't needed + // for the closure test this will be used for; keeping the initial + // parton as mother for now + + mPythia.event.append(pythiaParticle); + } + particleCountCurrent += nConstituents + 1; // +1 is for the jet parton itself, which is not in the final state } - particleCountCurrent += nConstituents + 1; // +1 is for the jet parton itself, which is not in the final state - } - delete genParticlesArray; + delete genParticlesArray; - if (mDebug) { - mPythia.event.list(); + if (mDebug) { + mPythia.event.list(); + } } /////////////////////////////////////////////// @@ -339,10 +346,11 @@ private: //////////////////////////////////////////////// const double mPtInfinity = 300; // maximum pt (in GeV/c) for generated particles, and upper pT limit for integral and TF1 purposes; too high and GetRandom struggles - const double mGenMinEta = -1.; /// minimum pseudorapidity for generated particles - const double mGenMaxEta = +1.; /// maximum pseudorapidity for generated particles + const double mGenMinEta = -0.9; /// minimum pseudorapidity for generated particles + const double mGenMaxEta = +0.9; /// maximum pseudorapidity for generated particles int mCollTotalMultWithBkg; /// total multiplicity of the collision - bool mGenerateUE = false; /// boolean to request (or not) embedding of the jet signal inside underlying event modelled by a thermal background + bool mGenerateSignal = true; /// boolean to request (or not) the generation of the jet signal + bool mGenerateUE = false; /// boolean to request (or not) embedding of the jet signal inside underlying event modelled by a thermal background; if mGenerateSignal = false, only the UE is generated const std::vector mConfigurableSimParameterNames = { "sglGenRAA", "sglGenTAA", @@ -392,6 +400,6 @@ private: ///___________________________________________________________ FairGenerator *generateParametrisedJetModel(std::string inputSimParametersPath, std::string inputSimParametersFileName, - bool generateUE = true) { - return new GeneratorParametrisedJetModel(inputSimParametersPath, inputSimParametersFileName, generateUE); + bool generateUE, bool generateSignal = true) { + return new GeneratorParametrisedJetModel(inputSimParametersPath, inputSimParametersFileName, generateUE, generateSignal); } \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini similarity index 89% rename from MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini rename to MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini index b51c7ded5..4b924e121 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", false) +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", true, false) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini similarity index 89% rename from MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini rename to MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini index 654469b4d..fdb2f19c1 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", true) +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", false, true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini similarity index 89% rename from MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini rename to MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini index 1fdc88fb9..2be7f0af6 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", false) +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", true, true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini new file mode 100644 index 000000000..dcaf2be8d --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini @@ -0,0 +1,7 @@ +### bkg production using pythia8 box generator, to embed jet-jet production into +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", true, false) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini new file mode 100644 index 000000000..52bc153a7 --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini @@ -0,0 +1,7 @@ +### bkg production using pythia8 box generator, to embed jet-jet production into +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", false, true) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini similarity index 89% rename from MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini rename to MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini index 57227ffe1..4800459c8 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", true) +funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", true, true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.C b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.C similarity index 100% rename from MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_noBkg.C rename to MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.C diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.C b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.C similarity index 100% rename from MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_withBkg.C rename to MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.C diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.C b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.C similarity index 100% rename from MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_noBkg.C rename to MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.C diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.C b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.C similarity index 100% rename from MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_withBkg.C rename to MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.C diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.C b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.C new file mode 100644 index 000000000..fddd963ae --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.C @@ -0,0 +1,34 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks + << "\n"; + + if (sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.C b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.C new file mode 100644 index 000000000..fddd963ae --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.C @@ -0,0 +1,34 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks + << "\n"; + + if (sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} \ No newline at end of file From fd75cee2d17d05c5dde002fbfaf4a80897eddcf9 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Mon, 14 Sep 2026 12:36:47 +0200 Subject: [PATCH 067/144] Add a few EPOS4 configurations (#2455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EPOS4 physics generation always happens internally in the symmetric nucleon-nucleon CM frame. hepmc_rapcms does not change that internal physics, but it applies a rigid rapidity boost (dy = -hepmc_rapcms, applied in src/KW/out.f → fillhepmc) to every particle written to output, which means re-expressing the same collision in the lab frame. This is here used for pO and should be used for p-Pb as well (the shift there is 0.465, from the same `Δy = 0.5·ln(E_p/E_target-per-nucleon` formula). --- .../epos4/generator/NeNe_536TeV_EPOS4.optns | 33 +++++++ .../epos4/generator/OO_536TeV_EPOS4.optns | 33 +++++++ .../epos4/generator/pO_962TeV_EPOS4.optns | 34 +++++++ .../generator/pO_962TeV_EPOS4_hydro.optns | 34 +++++++ .../common/ini/GeneratorEPOS4NeNe536TeV.ini | 12 +++ .../common/ini/GeneratorEPOS4OO536TeV.ini | 12 +++ .../common/ini/GeneratorEPOS4pO962TeV.ini | 12 +++ .../ini/GeneratorEPOS4pO962TeV_hydro.ini | 13 +++ .../ini/tests/GeneratorEPOS4NeNe536TeV.C | 78 +++++++++++++++ .../common/ini/tests/GeneratorEPOS4OO536TeV.C | 78 +++++++++++++++ .../common/ini/tests/GeneratorEPOS4pO962TeV.C | 99 +++++++++++++++++++ .../ini/tests/GeneratorEPOS4pO962TeV_hydro.C | 99 +++++++++++++++++++ 12 files changed, 537 insertions(+) create mode 100644 MC/config/common/epos4/generator/NeNe_536TeV_EPOS4.optns create mode 100644 MC/config/common/epos4/generator/OO_536TeV_EPOS4.optns create mode 100644 MC/config/common/epos4/generator/pO_962TeV_EPOS4.optns create mode 100644 MC/config/common/epos4/generator/pO_962TeV_EPOS4_hydro.optns create mode 100644 MC/config/common/ini/GeneratorEPOS4NeNe536TeV.ini create mode 100644 MC/config/common/ini/GeneratorEPOS4OO536TeV.ini create mode 100644 MC/config/common/ini/GeneratorEPOS4pO962TeV.ini create mode 100644 MC/config/common/ini/GeneratorEPOS4pO962TeV_hydro.ini create mode 100644 MC/config/common/ini/tests/GeneratorEPOS4NeNe536TeV.C create mode 100644 MC/config/common/ini/tests/GeneratorEPOS4OO536TeV.C create mode 100644 MC/config/common/ini/tests/GeneratorEPOS4pO962TeV.C create mode 100644 MC/config/common/ini/tests/GeneratorEPOS4pO962TeV_hydro.C diff --git a/MC/config/common/epos4/generator/NeNe_536TeV_EPOS4.optns b/MC/config/common/epos4/generator/NeNe_536TeV_EPOS4.optns new file mode 100644 index 000000000..ca9538d55 --- /dev/null +++ b/MC/config/common/epos4/generator/NeNe_536TeV_EPOS4.optns @@ -0,0 +1,33 @@ +!-------------------------------------------------------------------- +! Neon-Neon collisions without hydro and with hadronic cascade +!-------------------------------------------------------------------- + +!--------------------------------------- +! Define run +!--------------------------------------- + +application hadron !hadron-hadron, hadron-nucleus, or nucleus-nucleus +set laproj 10 !projectile atomic number +set maproj 20 !projectile mass number +set latarg 10 !target atomic number +set matarg 20 !target mass number +set ecms 5360 !sqrt(s)_pp +set istmax 25 !max status considered for storage + +ftime on !string formation time non-zero +!suppressed decays: +nodecays + 110 20 2130 -2130 2230 -2230 1130 -1130 1330 -1330 2330 -2330 3331 -3331 +end + +set ninicon 1 !number of initial conditions used for hydro evolution +core full !core/corona activated +hydro off !hydro disabled +eos off !eos disabled (epos standard EoS) +hacas full !hadronic cascade activated (UrQMD) +set nfreeze 1 !number of freeze out events per hydro event +set modsho 1 !printout every modsho events +set centrality 0 !0=min bias +set ihepmc 2 !HepMC output enabled on stdout +fillTree4(C1) !C1 sets impact parameter as centrality variable +set nfull 10 diff --git a/MC/config/common/epos4/generator/OO_536TeV_EPOS4.optns b/MC/config/common/epos4/generator/OO_536TeV_EPOS4.optns new file mode 100644 index 000000000..55c84a98e --- /dev/null +++ b/MC/config/common/epos4/generator/OO_536TeV_EPOS4.optns @@ -0,0 +1,33 @@ +!-------------------------------------------------------------------- +! Oxygen-Oxygen collisions without hydro and with hadronic cascade +!-------------------------------------------------------------------- + +!--------------------------------------- +! Define run +!--------------------------------------- + +application hadron !hadron-hadron, hadron-nucleus, or nucleus-nucleus +set laproj 8 !projectile atomic number +set maproj 16 !projectile mass number +set latarg 8 !target atomic number +set matarg 16 !target mass number +set ecms 5360 !sqrt(s)_pp +set istmax 25 !max status considered for storage + +ftime on !string formation time non-zero +!suppressed decays: +nodecays + 110 20 2130 -2130 2230 -2230 1130 -1130 1330 -1330 2330 -2330 3331 -3331 +end + +set ninicon 1 !number of initial conditions used for hydro evolution +core full !core/corona activated +hydro off !hydro disabled +eos off !eos disabled (epos standard EoS) +hacas full !hadronic cascade activated (UrQMD) +set nfreeze 1 !number of freeze out events per hydro event +set modsho 1 !printout every modsho events +set centrality 0 !0=min bias +set ihepmc 2 !HepMC output enabled on stdout +fillTree4(C1) !C1 sets impact parameter as centrality variable +set nfull 10 diff --git a/MC/config/common/epos4/generator/pO_962TeV_EPOS4.optns b/MC/config/common/epos4/generator/pO_962TeV_EPOS4.optns new file mode 100644 index 000000000..a54feecb4 --- /dev/null +++ b/MC/config/common/epos4/generator/pO_962TeV_EPOS4.optns @@ -0,0 +1,34 @@ +!-------------------------------------------------------------------- +! proton-Oxigen collisions at 9.62 TeV without hydro and with hadronic cascade +!-------------------------------------------------------------------- + +!--------------------------------------- +! Define run +!--------------------------------------- + +application hadron !hadron-hadron, hadron-nucleus, or nucleus-nucleus +set laproj 1 !projectile atomic number +set maproj 1 !projectile mass number +set latarg 8 !target atomic number +set matarg 16 !target mass number +set ecms 9620 !sqrt(s_NN) +set istmax 25 !max status considered for storage + +ftime on !string formation time non-zero +!suppressed decays: +nodecays + 110 20 2130 -2130 2230 -2230 1130 -1130 1330 -1330 2330 -2330 3331 -3331 +end + +set ninicon 1 !number of initial conditions used for hydro evolution +core full !core/corona activated +hydro off !hydro disabled +eos off !eos disabled (epos standard EoS) +hacas full !hadronic cascade activated (UrQMD) +set nfreeze 1 !number of freeze out events per hydro event +set modsho 1 !printout every modsho events +set centrality 0 !0=min bias +set ihepmc 2 !HepMC output enabled on stdout +fillTree4(C1) !C1 sets impact parameter as centrality variable +hepmc_rapcms 0.346225 !boost output to real asymmetric p/O beam frame (p: 6800 GeV, O: 3400 GeV) +set nfull 10 diff --git a/MC/config/common/epos4/generator/pO_962TeV_EPOS4_hydro.optns b/MC/config/common/epos4/generator/pO_962TeV_EPOS4_hydro.optns new file mode 100644 index 000000000..ee97644c4 --- /dev/null +++ b/MC/config/common/epos4/generator/pO_962TeV_EPOS4_hydro.optns @@ -0,0 +1,34 @@ +!-------------------------------------------------------------------- +! proton-Oxigen collisions at 9.62 TeV with hydro and with hadronic cascade +!-------------------------------------------------------------------- + +!--------------------------------------- +! Define run +!--------------------------------------- + +application hadron !hadron-hadron, hadron-nucleus, or nucleus-nucleus +set laproj 1 !projectile atomic number +set maproj 1 !projectile mass number +set latarg 8 !target atomic number +set matarg 16 !target mass number +set ecms 9620 !sqrt(s_NN) +set istmax 25 !max status considered for storage + +ftime on !string formation time non-zero +!suppressed decays: +nodecays + 110 20 2130 -2130 2230 -2230 1130 -1130 1330 -1330 2330 -2330 3331 -3331 +end + +set ninicon 1 !number of initial conditions used for hydro evolution +core full !core/corona activated +hydro hlle !hydro activated +eos x3ff !eos activated (epos standard EoS) +hacas full !hadronic cascade activated (UrQMD) +set nfreeze 1 !number of freeze out events per hydro event +set modsho 1 !printout every modsho events +set centrality 0 !0=min bias +set ihepmc 2 !HepMC output enabled on stdout +fillTree4(C1) !C1 sets impact parameter as centrality variable +hepmc_rapcms 0.346225 !boost output to real asymmetric p/O beam frame (p: 6800 GeV, O: 3400 GeV) +set nfull 10 diff --git a/MC/config/common/ini/GeneratorEPOS4NeNe536TeV.ini b/MC/config/common/ini/GeneratorEPOS4NeNe536TeV.ini new file mode 100644 index 000000000..efc2b466a --- /dev/null +++ b/MC/config/common/ini/GeneratorEPOS4NeNe536TeV.ini @@ -0,0 +1,12 @@ +#NEV_TEST> 10 +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/generator_EPOS4.C +funcName=generateEPOS4("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/epos4/generator/NeNe_536TeV_EPOS4.optns", 2147483647) + +[GeneratorFileOrCmd] +cmd=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/epos.sh +bMaxSwitch=none + +# Set to version 2 if EPOS4.0.0 is used +[HepMC] +version=3 diff --git a/MC/config/common/ini/GeneratorEPOS4OO536TeV.ini b/MC/config/common/ini/GeneratorEPOS4OO536TeV.ini new file mode 100644 index 000000000..0daaa3eb3 --- /dev/null +++ b/MC/config/common/ini/GeneratorEPOS4OO536TeV.ini @@ -0,0 +1,12 @@ +#NEV_TEST> 10 +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/generator_EPOS4.C +funcName=generateEPOS4("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/epos4/generator/OO_536TeV_EPOS4.optns", 2147483647) + +[GeneratorFileOrCmd] +cmd=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/epos.sh +bMaxSwitch=none + +# Set to version 2 if EPOS4.0.0 is used +[HepMC] +version=3 diff --git a/MC/config/common/ini/GeneratorEPOS4pO962TeV.ini b/MC/config/common/ini/GeneratorEPOS4pO962TeV.ini new file mode 100644 index 000000000..23afdd552 --- /dev/null +++ b/MC/config/common/ini/GeneratorEPOS4pO962TeV.ini @@ -0,0 +1,12 @@ +#NEV_TEST> 10 +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/generator_EPOS4.C +funcName=generateEPOS4("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/epos4/generator/pO_962TeV_EPOS4.optns", 2147483647) + +[GeneratorFileOrCmd] +cmd=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/epos.sh +bMaxSwitch=none + +# Set to version 2 if EPOS4.0.0 is used +[HepMC] +version=3 diff --git a/MC/config/common/ini/GeneratorEPOS4pO962TeV_hydro.ini b/MC/config/common/ini/GeneratorEPOS4pO962TeV_hydro.ini new file mode 100644 index 000000000..aff71abaa --- /dev/null +++ b/MC/config/common/ini/GeneratorEPOS4pO962TeV_hydro.ini @@ -0,0 +1,13 @@ +#NEV_TEST> 2 +#---> GeneratorEPOS4pO962TeV +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/generator_EPOS4.C +funcName=generateEPOS4("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/epos4/generator/pO_962TeV_EPOS4_hydro.optns", 2147483647) + +[GeneratorFileOrCmd] +cmd=${O2DPG_MC_CONFIG_ROOT}/MC/config/examples/epos4/epos.sh +bMaxSwitch=none + +# Set to version 2 if EPOS4.0.0 is used +[HepMC] +version=3 diff --git a/MC/config/common/ini/tests/GeneratorEPOS4NeNe536TeV.C b/MC/config/common/ini/tests/GeneratorEPOS4NeNe536TeV.C new file mode 100644 index 000000000..24e857dbc --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorEPOS4NeNe536TeV.C @@ -0,0 +1,78 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + + // Check if there are 10 events, as customarily set in the ini file + // Heavy-ion collisions with hydro and hadronic cascade are very slow to simulate + if (nEvents != 10) + { + std::cerr << "Expected 10 events, got " << nEvents << "\n"; + return 1; + } + + // ---- Neon-Neon parameters ---- + constexpr int kNeonPDG = 1000100200; // Ne-20 ion + constexpr double kEnucleon = 5360.; // GeV per nucleon + constexpr int kA = 20; // Neon mass number + constexpr double kNeonEnergy = kA * kEnucleon / 2.0; // beam energy in GeV + + // Check if each event has two neon ions at expected energy + for (int i = 0; i < nEvents; i++) + { + tree->GetEntry(i); + int count = 0; + + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) + { + auto track = tracks->at(idxMCTrack); + double energy = track.GetEnergy(); + + // 50 MeV tolerance (floating point safety) + if (std::abs(energy - kNeonEnergy) < 5e-2 && + track.GetPdgCode() == kNeonPDG) + { + count++; + } + } + + if (count < 2) + { + std::cerr << "Event " << i + << " has less than 2 neon ions at " + << kNeonEnergy << " GeV\n"; + return 1; + } + } + + return 0; +} diff --git a/MC/config/common/ini/tests/GeneratorEPOS4OO536TeV.C b/MC/config/common/ini/tests/GeneratorEPOS4OO536TeV.C new file mode 100644 index 000000000..1f26e1962 --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorEPOS4OO536TeV.C @@ -0,0 +1,78 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + + // Check if there are 10 events, as customly set in the ini file + // Heavy-ion collisions with hydro and hadronic cascade are very slow to simulate + if (nEvents != 10) + { + std::cerr << "Expected 10 event, got " << nEvents << "\n"; + return 1; + } + + // ---- Oxygen-Oxygen parameters ---- + constexpr int kOxygenPDG = 1000080160; // O-16 ion + constexpr double kEnucleon = 5360.; // GeV per nucleon + constexpr int kA = 16; // Oxygen mass number + constexpr double kOxygenEnergy = kA * kEnucleon / 2.0; // 85760 / 2 GeV + + // Check if each event has two oxygen ions at expected energy + for (int i = 0; i < nEvents; i++) + { + tree->GetEntry(i); + int count = 0; + + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) + { + auto track = tracks->at(idxMCTrack); + double energy = track.GetEnergy(); + + // 50 MeV tolerance (floating point safety) + if (std::abs(energy - kOxygenEnergy) < 5e-2 && + track.GetPdgCode() == kOxygenPDG) + { + count++; + } + } + + if (count < 2) + { + std::cerr << "Event " << i + << " has less than 2 oxygen ions at " + << kOxygenEnergy << " GeV\n"; + return 1; + } + } + + return 0; +} diff --git a/MC/config/common/ini/tests/GeneratorEPOS4pO962TeV.C b/MC/config/common/ini/tests/GeneratorEPOS4pO962TeV.C new file mode 100644 index 000000000..859bb015e --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorEPOS4pO962TeV.C @@ -0,0 +1,99 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + + // Check if there are 10 events, as customly set in the ini file (NEV_TEST) + if (nEvents != 10) + { + std::cerr << "Expected 10 events, got " << nEvents << "\n"; + return 1; + } + + // ---- proton-Oxygen parameters ---- + // The .optns file boosts the output from the symmetric NN CM frame (where + // each nucleon carries ecms/2) to the real asymmetric p/O beam frame via + // "hepmc_rapcms ", so E_p = (ecms/2)*exp(+kRapcms) and + // E_O-nucleon = (ecms/2)*exp(-kRapcms). Keep kRapcms in sync with the value + // set in pO_962TeV_EPOS4.optns / pO_962TeV_EPOS4_hydro.optns. + constexpr int kProtonPDG = 2212; + constexpr int kOxygenPDG = 1000080160; // O-16 ion + constexpr double kEcms = 9620.; // GeV, sqrt(s_NN) as set in the .optns file + constexpr double kRapcms = 0.346225; // rapidity boost set via hepmc_rapcms + constexpr int kA = 16; // Oxygen mass number + const double kProtonEnergy = (kEcms / 2.0) * std::exp(kRapcms); // boosted proton energy + const double kOxygenEnergy = kA * (kEcms / 2.0) * std::exp(-kRapcms); // total energy of the O-16 ion + + // Check if each event has one proton and one oxygen ion at expected energies + for (int i = 0; i < nEvents; i++) + { + tree->GetEntry(i); + int countProton = 0; + int countOxygen = 0; + + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) + { + auto track = tracks->at(idxMCTrack); + double energy = track.GetEnergy(); + + // 1 GeV tolerance (the rapcms boost is applied in single precision in EPOS4) + if (std::abs(energy - kProtonEnergy) < 1. && + track.GetPdgCode() == kProtonPDG) + { + countProton++; + } + else if (std::abs(energy - kOxygenEnergy) < 1. && + track.GetPdgCode() == kOxygenPDG) + { + countOxygen++; + } + } + + if (countProton < 1) + { + std::cerr << "Event " << i + << " has no proton beam particle at " + << kProtonEnergy << " GeV\n"; + return 1; + } + + if (countOxygen < 1) + { + std::cerr << "Event " << i + << " has no oxygen ion at " + << kOxygenEnergy << " GeV\n"; + return 1; + } + } + + return 0; +} diff --git a/MC/config/common/ini/tests/GeneratorEPOS4pO962TeV_hydro.C b/MC/config/common/ini/tests/GeneratorEPOS4pO962TeV_hydro.C new file mode 100644 index 000000000..475f8d958 --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorEPOS4pO962TeV_hydro.C @@ -0,0 +1,99 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Check if all events are filled + auto nEvents = tree->GetEntries(); + for (Long64_t i = 0; i < nEvents; ++i) + { + tree->GetEntry(i); + if (tracks->empty()) + { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + + // Check if there are 2 events, as customly set in the ini file (NEV_TEST) + if (nEvents != 2) + { + std::cerr << "Expected 2 events, got " << nEvents << "\n"; + return 1; + } + + // ---- proton-Oxygen parameters ---- + // The .optns file boosts the output from the symmetric NN CM frame (where + // each nucleon carries ecms/2) to the real asymmetric p/O beam frame via + // "hepmc_rapcms ", so E_p = (ecms/2)*exp(+kRapcms) and + // E_O-nucleon = (ecms/2)*exp(-kRapcms). Keep kRapcms in sync with the value + // set in pO_962TeV_EPOS4.optns / pO_962TeV_EPOS4_hydro.optns. + constexpr int kProtonPDG = 2212; + constexpr int kOxygenPDG = 1000080160; // O-16 ion + constexpr double kEcms = 9620.; // GeV, sqrt(s_NN) as set in the .optns file + constexpr double kRapcms = 0.346225; // rapidity boost set via hepmc_rapcms + constexpr int kA = 16; // Oxygen mass number + const double kProtonEnergy = (kEcms / 2.0) * std::exp(kRapcms); // boosted proton energy + const double kOxygenEnergy = kA * (kEcms / 2.0) * std::exp(-kRapcms); // total energy of the O-16 ion + + // Check if each event has one proton and one oxygen ion at expected energies + for (int i = 0; i < nEvents; i++) + { + tree->GetEntry(i); + int countProton = 0; + int countOxygen = 0; + + for (int idxMCTrack = 0; idxMCTrack < tracks->size(); ++idxMCTrack) + { + auto track = tracks->at(idxMCTrack); + double energy = track.GetEnergy(); + + // 1 GeV tolerance (the rapcms boost is applied in single precision in EPOS4) + if (std::abs(energy - kProtonEnergy) < 1. && + track.GetPdgCode() == kProtonPDG) + { + countProton++; + } + else if (std::abs(energy - kOxygenEnergy) < 1. && + track.GetPdgCode() == kOxygenPDG) + { + countOxygen++; + } + } + + if (countProton < 1) + { + std::cerr << "Event " << i + << " has no proton beam particle at " + << kProtonEnergy << " GeV\n"; + return 1; + } + + if (countOxygen < 1) + { + std::cerr << "Event " << i + << " has no oxygen ion at " + << kOxygenEnergy << " GeV\n"; + return 1; + } + } + + return 0; +} From 6d163ef50c214d02eeb7e7de5423ef4980939fde Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Wed, 16 Sep 2026 16:20:57 +0200 Subject: [PATCH 068/144] Run the alternative-reco step of anchorMC in a subshell This fixes a problem in the 2-tag setup of anchorMC.sh and anchorMC_DataEmbedding.sh. - The MC software environment was stashed with 'module save' and brought back with 'module restore'. - 'module restore' fails for a module whose version contains '@', as gpu-system did since alidist of 2026-09-05. - ONNXRuntime depends on gpu-system, so O2, O2Physics, QualityControl and O2PDPSuite were not restored. - o2dpg_sim_workflow_anchored.py then failed with "o2 has no attribute ccdb" and no AO2D was produced. - The alternative-reco step now runs in a subshell, so the MC environment is never modified and nothing needs restoring. Co-Authored-By: Claude Opus 5 --- MC/run/ANCHOR/anchorMC.sh | 117 ++++++++++-------------- MC/run/ANCHOR/anchorMC_DataEmbedding.sh | 110 +++++++++------------- 2 files changed, 92 insertions(+), 135 deletions(-) diff --git a/MC/run/ANCHOR/anchorMC.sh b/MC/run/ANCHOR/anchorMC.sh index 0aa4832f4..c4ea783de 100755 --- a/MC/run/ANCHOR/anchorMC.sh +++ b/MC/run/ANCHOR/anchorMC.sh @@ -180,8 +180,8 @@ fi export > env_base.env -# The 2-tag / alternative-reco setup below uses 'module save/purge/restore' to stash -# and swap the software environment. If the shell provides no 'module' function, build +# The 2-tag / alternative-reco setup below uses 'module purge' to swap the software +# environment inside a subshell. If the shell provides no 'module' function, build # one on a 'modulecmd' binary. Its location varies by version, so resolve it flexibly # (was hardwired to /usr/bin/modulecmd) and verify it supports saved collections. Fail # hard -- but only when an alternative-reco tag is set, since single-tag jobs never call @@ -237,61 +237,61 @@ if [ ! "${MODULEPATH}" ]; then fi #<----- START OF part that should run under a clean alternative software environment if this was given ------ -if [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then - if [ "${LOADEDMODULES}" ]; then - printenv > env_before_stashing.printenv - echo "Stashing initial modules" - module save initial_modules.list # we stash the current modules environment - module list --no-pager - module purge --no-pager - printenv > env_after_stashing.printenv - echo "Modules after purge" - module list --no-pager +# It runs in a subshell, so the MC software environment of this shell stays untouched +# and does not need to be restored afterwards. +( + if [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then + if [ "${LOADEDMODULES}" ]; then + echo "Unloading initial modules" + module purge --no-pager + fi + echo_info "Using tag ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG} to setup anchored MC" + /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" &> async_environment.env + source async_environment.env + export > env_async.env fi - echo_info "Using tag ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG} to setup anchored MC" - /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" &> async_environment.env - source async_environment.env - export > env_async.env -fi -# default async_pass.sh script -DPGRECO=$O2DPG_ROOT/DATA/production/configurations/asyncReco/async_pass.sh -# default destenv_extra.sh script -DPGSETENV=$O2DPG_ROOT/DATA/production/configurations/asyncReco/setenv_extra.sh + # default async_pass.sh script + DPGRECO=$O2DPG_ROOT/DATA/production/configurations/asyncReco/async_pass.sh + # default destenv_extra.sh script + DPGSETENV=$O2DPG_ROOT/DATA/production/configurations/asyncReco/setenv_extra.sh -# a specific async_pass.sh script is in the current directory, assume that one should be used -if [[ -f async_pass.sh ]]; then - # the default is executable, however, this may not be, so make it so - chmod +x async_pass.sh - DPGRECO=./async_pass.sh -else - cp -v $DPGRECO . -fi + # a specific async_pass.sh script is in the current directory, assume that one should be used + if [[ -f async_pass.sh ]]; then + # the default is executable, however, this may not be, so make it so + chmod +x async_pass.sh + DPGRECO=./async_pass.sh + else + cp -v $DPGRECO . + fi -# if there is no setenv_extra.sh in this directory (so no special version is "shipped" with this rpodcution), copy the default one -if [[ ! -f setenv_extra.sh ]] ; then - cp ${DPGSETENV} . - echo_info "Use default setenv_extra.sh from ${DPGSETENV}." -else - echo_info "setenv_extra.sh was found in the current working directory, use it." -fi + # if there is no setenv_extra.sh in this directory (so no special version is "shipped" with this rpodcution), copy the default one + if [[ ! -f setenv_extra.sh ]] ; then + cp ${DPGSETENV} . + echo_info "Use default setenv_extra.sh from ${DPGSETENV}." + else + echo_info "setenv_extra.sh was found in the current working directory, use it." + fi -chmod u+x setenv_extra.sh + chmod u+x setenv_extra.sh -echo_info "Setting up DPGRECO to ${DPGRECO}" + echo_info "Setting up DPGRECO to ${DPGRECO}" -# take out line running the workflow (if we don't have data input) -[ ${CTF_TEST_FILE} ] || sed -i '/WORKFLOWMODE=run/d' async_pass.sh + # take out line running the workflow (if we don't have data input) + [ ${CTF_TEST_FILE} ] || sed -i '/WORKFLOWMODE=run/d' async_pass.sh -# create workflow ---> creates the file that can be parsed -export IGNORE_EXISTING_SHMFILES=1 -touch list.list + # create workflow ---> creates the file that can be parsed + export IGNORE_EXISTING_SHMFILES=1 + touch list.list -# run the async_pass.sh and store output to log file for later inspection and extraction of information -./async_pass.sh ${CTF_TEST_FILE:-""} 2&> async_pass_log.log -RECO_RC=$? + # run the async_pass.sh and store output to log file for later inspection and extraction of information + ./async_pass.sh ${CTF_TEST_FILE:-""} 2&> async_pass_log.log + RECO_RC=$? -echo_info "async_pass.sh finished with ${RECO_RC}" + echo_info "async_pass.sh finished with ${RECO_RC}" + exit ${RECO_RC} +) +RECO_RC=$? if [[ "${RECO_RC}" != "0" ]] ; then exit ${RECO_RC} @@ -305,29 +305,6 @@ fi export ALIEN_JDL_LPMPRODUCTIONTAG=$ALIEN_JDL_LPMPRODUCTIONTAG_KEEP echo_info "Setting back ALIEN_JDL_LPMPRODUCTIONTAG to $ALIEN_JDL_LPMPRODUCTIONTAG" - -# get rid of the temporary software environment -if [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then - module purge --no-pager - # restore the initial software environment - echo "Restoring initial environment" - module --no-pager restore initial_modules.list - module saverm initial_modules.list - - # Restore overwritten O2DPG variables set by modules but changed by user - # (in particular custom O2DPG_ROOT and O2DPG_MC_CONFIG_ROOT) - # We must avoid piping into a while loop (otherwise the internal export is executed in sub-shell) - printenv > env_after_restore.printenv - while read -r var; do - b=$(grep "^$var=" env_before_stashing.printenv | cut -d= -f2-) - a=$(grep "^$var=" env_after_restore.printenv | cut -d= -f2-) - [[ "$b" != "$a" ]] && export "$var=$b" && echo "Reapplied: $var to ${b}" - done < <( - comm -12 \ - <(grep '^O2DPG' env_before_stashing.printenv | cut -d= -f1 | sort) \ - <(grep '^O2DPG' env_after_restore.printenv | cut -d= -f1 | sort)) - -fi #<----- END OF part that should run under a clean alternative software environment if this was given ------ # now create the local MC config file --> config-json.json diff --git a/MC/run/ANCHOR/anchorMC_DataEmbedding.sh b/MC/run/ANCHOR/anchorMC_DataEmbedding.sh index 80cb1daae..b92cf82af 100755 --- a/MC/run/ANCHOR/anchorMC_DataEmbedding.sh +++ b/MC/run/ANCHOR/anchorMC_DataEmbedding.sh @@ -196,61 +196,61 @@ if [ ! "${MODULEPATH}" ]; then fi #<----- START OF part that should run under a clean alternative software environment if this was given ------ -if [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then - if [ "${LOADEDMODULES}" ]; then - printenv > env_before_stashing.printenv - echo "Stashing initial modules" - module save initial_modules.list # we stash the current modules environment - module list --no-pager - module purge --no-pager - printenv > env_after_stashing.printenv - echo "Modules after purge" - module list --no-pager +# It runs in a subshell, so the MC software environment of this shell stays untouched +# and does not need to be restored afterwards. +( + if [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then + if [ "${LOADEDMODULES}" ]; then + echo "Unloading initial modules" + module purge --no-pager + fi + echo_info "Using tag ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG} to setup anchored MC" + /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" &> async_environment.env + source async_environment.env + export > env_async.env fi - echo_info "Using tag ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG} to setup anchored MC" - /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" &> async_environment.env - source async_environment.env - export > env_async.env -fi -# default async_pass.sh script -DPGRECO=$O2DPG_ROOT/DATA/production/configurations/asyncReco/async_pass.sh -# default destenv_extra.sh script -DPGSETENV=$O2DPG_ROOT/DATA/production/configurations/asyncReco/setenv_extra.sh + # default async_pass.sh script + DPGRECO=$O2DPG_ROOT/DATA/production/configurations/asyncReco/async_pass.sh + # default destenv_extra.sh script + DPGSETENV=$O2DPG_ROOT/DATA/production/configurations/asyncReco/setenv_extra.sh -# a specific async_pass.sh script is in the current directory, assume that one should be used -if [[ -f async_pass.sh ]]; then - # the default is executable, however, this may not be, so make it so - chmod +x async_pass.sh - DPGRECO=./async_pass.sh -else - cp -v $DPGRECO . -fi + # a specific async_pass.sh script is in the current directory, assume that one should be used + if [[ -f async_pass.sh ]]; then + # the default is executable, however, this may not be, so make it so + chmod +x async_pass.sh + DPGRECO=./async_pass.sh + else + cp -v $DPGRECO . + fi -# if there is no setenv_extra.sh in this directory (so no special version is "shipped" with this rpodcution), copy the default one -if [[ ! -f setenv_extra.sh ]] ; then - cp ${DPGSETENV} . - echo_info "Use default setenv_extra.sh from ${DPGSETENV}." -else - echo_info "setenv_extra.sh was found in the current working directory, use it." -fi + # if there is no setenv_extra.sh in this directory (so no special version is "shipped" with this rpodcution), copy the default one + if [[ ! -f setenv_extra.sh ]] ; then + cp ${DPGSETENV} . + echo_info "Use default setenv_extra.sh from ${DPGSETENV}." + else + echo_info "setenv_extra.sh was found in the current working directory, use it." + fi -chmod u+x setenv_extra.sh + chmod u+x setenv_extra.sh -echo_info "Setting up DPGRECO to ${DPGRECO}" + echo_info "Setting up DPGRECO to ${DPGRECO}" -# take out line running the workflow (if we don't have data input) -[ ${CTF_TEST_FILE} ] || sed -i '/WORKFLOWMODE=run/d' async_pass.sh + # take out line running the workflow (if we don't have data input) + [ ${CTF_TEST_FILE} ] || sed -i '/WORKFLOWMODE=run/d' async_pass.sh -# create workflow ---> creates the file that can be parsed -export IGNORE_EXISTING_SHMFILES=1 -touch list.list + # create workflow ---> creates the file that can be parsed + export IGNORE_EXISTING_SHMFILES=1 + touch list.list -# run the async_pass.sh and store output to log file for later inspection and extraction of information -./async_pass.sh ${CTF_TEST_FILE:-""} 2&> async_pass_log.log -RECO_RC=$? + # run the async_pass.sh and store output to log file for later inspection and extraction of information + ./async_pass.sh ${CTF_TEST_FILE:-""} 2&> async_pass_log.log + RECO_RC=$? -echo_info "async_pass.sh finished with ${RECO_RC}" + echo_info "async_pass.sh finished with ${RECO_RC}" + exit ${RECO_RC} +) +RECO_RC=$? if [[ "${RECO_RC}" != "0" ]] ; then exit ${RECO_RC} @@ -264,26 +264,6 @@ fi export ALIEN_JDL_LPMPRODUCTIONTAG=$ALIEN_JDL_LPMPRODUCTIONTAG_KEEP echo_info "Setting back ALIEN_JDL_LPMPRODUCTIONTAG to $ALIEN_JDL_LPMPRODUCTIONTAG" - -# get rid of the temporary software environment -if [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then - module purge --no-pager - # restore the initial software environment - echo "Restoring initial environment" - module --no-pager restore initial_modules.list - module saverm initial_modules.list - - # Restore overwritten O2DPG variables set by modules but changed by user - # (in particular custom O2DPG_ROOT and O2DPG_MC_CONFIG_ROOT) - printenv > env_after_restore.printenv - comm -12 <(grep '^O2DPG' env_before_stashing.printenv | cut -d= -f1 | sort) \ - <(grep '^O2DPG' env_after_restore.printenv | cut -d= -f1 | sort) | - while read -r var; do - b=$(grep "^$var=" env_before_stashing.printenv | cut -d= -f2-) - a=$(grep "^$var=" env_after_restore.printenv | cut -d= -f2-) - [[ "$b" != "$a" ]] && export "$var=$b" && echo "Reapplied: $var to ${b}" - done -fi #<----- END OF part that should run under a clean alternative software environment if this was given ------ # now create the local MC config file --> config-json.json From 39ee30b8d717de14614ef4420d4865ee95f40269 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Wed, 16 Sep 2026 17:38:12 +0200 Subject: [PATCH 069/144] Harden module setup and async environment handling in anchorMC This addresses review comments on the 2-tag subshell change. - The modulecmd probe now checks 'bash list' instead of 'savelist', since save/restore is no longer used. - anchorMC_DataEmbedding.sh now uses the same modulecmd resolution as anchorMC.sh. - A failing 'alienv printenv' of the async tag now stops with a clear error. - The embedding script passes env_async.env with the path of the top directory, not the per-timeframe one. - '2&>' is replaced by '&>' when calling async_pass.sh. Co-Authored-By: Claude Opus 5 --- MC/run/ANCHOR/anchorMC.sh | 16 ++++--- MC/run/ANCHOR/anchorMC_DataEmbedding.sh | 56 +++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/MC/run/ANCHOR/anchorMC.sh b/MC/run/ANCHOR/anchorMC.sh index c4ea783de..3d634269e 100755 --- a/MC/run/ANCHOR/anchorMC.sh +++ b/MC/run/ANCHOR/anchorMC.sh @@ -183,7 +183,7 @@ export > env_base.env # The 2-tag / alternative-reco setup below uses 'module purge' to swap the software # environment inside a subshell. If the shell provides no 'module' function, build # one on a 'modulecmd' binary. Its location varies by version, so resolve it flexibly -# (was hardwired to /usr/bin/modulecmd) and verify it supports saved collections. Fail +# (was hardwired to /usr/bin/modulecmd) and verify that it runs. Fail # hard -- but only when an alternative-reco tag is set, since single-tag jobs never call # 'module' -- instead of silently no-op'ing every 'module' call (cf. O2-7070). # The candidate list also resolves symlinks ('readlink -f') and probes the canonical @@ -198,7 +198,7 @@ if ! declare -F module > /dev/null; then "${MODULESHOME:+${MODULESHOME}/libexec/modulecmd}" \ "$(readlink -f /usr/bin/modulecmd 2>/dev/null)" \ /usr/share/Modules/libexec/modulecmd* /usr/share/Modules/*/libexec/modulecmd*; do - if [ -n "${_cand}" ] && [ -x "${_cand}" ] && "${_cand}" bash savelist > /dev/null 2>&1; then + if [ -n "${_cand}" ] && [ -x "${_cand}" ] && "${_cand}" bash list > /dev/null 2>&1; then MODULECMD_BIN="${_cand}" break fi @@ -207,7 +207,7 @@ if ! declare -F module > /dev/null; then echo_info "Using modulecmd at ${MODULECMD_BIN}" elif [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then echo_error "Alternative-reco (2-tag) requested (ALIEN_JDL_O2DPG_ASYNC_RECO_TAG=${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG})" - echo_error "but no 'modulecmd' supporting saved collections (save/restore) was found." + echo_error "but no working 'modulecmd' (needed for 'module purge') was found." echo_error " Looked at: \$MODULECMD='${MODULECMD}', /usr/bin/modulecmd (+readlink -f), PATH," echo_error " \$MODULESHOME/libexec/modulecmd, /usr/share/Modules/{,*/}libexec/modulecmd*." echo_error " Diagnostics: MODULESHOME='${MODULESHOME}' /usr/bin/modulecmd -> '$(readlink -f /usr/bin/modulecmd 2>/dev/null)' PATH='${PATH}'" @@ -246,7 +246,13 @@ fi module purge --no-pager fi echo_info "Using tag ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG} to setup anchored MC" - /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" &> async_environment.env + /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" > async_environment.env 2> async_environment.err + # alienv printenv returns 0 also for an unknown tag, so check its output instead + if [ ! -s async_environment.env ] || grep -q "ERROR" async_environment.err; then + echo_error "Could not set up the software environment for ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}:" + cat async_environment.err + exit 1 + fi source async_environment.env export > env_async.env fi @@ -285,7 +291,7 @@ fi touch list.list # run the async_pass.sh and store output to log file for later inspection and extraction of information - ./async_pass.sh ${CTF_TEST_FILE:-""} 2&> async_pass_log.log + ./async_pass.sh ${CTF_TEST_FILE:-""} &> async_pass_log.log RECO_RC=$? echo_info "async_pass.sh finished with ${RECO_RC}" diff --git a/MC/run/ANCHOR/anchorMC_DataEmbedding.sh b/MC/run/ANCHOR/anchorMC_DataEmbedding.sh index b92cf82af..0f49dda6c 100755 --- a/MC/run/ANCHOR/anchorMC_DataEmbedding.sh +++ b/MC/run/ANCHOR/anchorMC_DataEmbedding.sh @@ -178,9 +178,48 @@ fi export > env_base.env +# The 2-tag / alternative-reco setup below uses 'module purge' to swap the software +# environment inside a subshell. If the shell provides no 'module' function, build +# one on a 'modulecmd' binary. Its location varies by version, so resolve it flexibly +# (was hardwired to /usr/bin/modulecmd) and verify that it runs. Fail +# hard -- but only when an alternative-reco tag is set, since single-tag jobs never call +# 'module' -- instead of silently no-op'ing every 'module' call (cf. O2-7070). +# The candidate list also resolves symlinks ('readlink -f') and probes the canonical +# environment-modules install (/usr/share/Modules): on some HPC/container setups +# /usr/bin/modulecmd is a *dangling* symlink (-> /etc/alternatives -> the real .tcl), +# which exec's as "No such file or directory"; this recovers when the real binary is +# still reachable, and otherwise still fails hard (a genuinely missing Modules tree in +# the job's mount namespace is a site-side problem). if ! declare -F module > /dev/null; then + MODULECMD_BIN="" + for _cand in "${MODULECMD}" "/usr/bin/modulecmd" "$(command -v modulecmd 2>/dev/null)" \ + "${MODULESHOME:+${MODULESHOME}/libexec/modulecmd}" \ + "$(readlink -f /usr/bin/modulecmd 2>/dev/null)" \ + /usr/share/Modules/libexec/modulecmd* /usr/share/Modules/*/libexec/modulecmd*; do + if [ -n "${_cand}" ] && [ -x "${_cand}" ] && "${_cand}" bash list > /dev/null 2>&1; then + MODULECMD_BIN="${_cand}" + break + fi + done + if [ -n "${MODULECMD_BIN}" ]; then + echo_info "Using modulecmd at ${MODULECMD_BIN}" + elif [ "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" ]; then + echo_error "Alternative-reco (2-tag) requested (ALIEN_JDL_O2DPG_ASYNC_RECO_TAG=${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG})" + echo_error "but no working 'modulecmd' (needed for 'module purge') was found." + echo_error " Looked at: \$MODULECMD='${MODULECMD}', /usr/bin/modulecmd (+readlink -f), PATH," + echo_error " \$MODULESHOME/libexec/modulecmd, /usr/share/Modules/{,*/}libexec/modulecmd*." + echo_error " Diagnostics: MODULESHOME='${MODULESHOME}' /usr/bin/modulecmd -> '$(readlink -f /usr/bin/modulecmd 2>/dev/null)' PATH='${PATH}'" + exit 1 + else + echo_info "No 'modulecmd' found, but no alternative-reco tag is set, so 'module' is not needed; continuing." + fi + export MODULECMD_BIN module() { - eval "$(/usr/bin/modulecmd bash "$@")"; + if [ -z "${MODULECMD_BIN}" ]; then + echo_error "'module ${*}' invoked but no usable 'modulecmd' was found (see earlier diagnostics)." + return 127 + fi + eval "$("${MODULECMD_BIN}" bash "$@")"; } export -f module fi @@ -205,7 +244,13 @@ fi module purge --no-pager fi echo_info "Using tag ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG} to setup anchored MC" - /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" &> async_environment.env + /cvmfs/alice.cern.ch/bin/alienv printenv "${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}" > async_environment.env 2> async_environment.err + # alienv printenv returns 0 also for an unknown tag, so check its output instead + if [ ! -s async_environment.env ] || grep -q "ERROR" async_environment.err; then + echo_error "Could not set up the software environment for ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}:" + cat async_environment.err + exit 1 + fi source async_environment.env export > env_async.env fi @@ -244,7 +289,7 @@ fi touch list.list # run the async_pass.sh and store output to log file for later inspection and extraction of information - ./async_pass.sh ${CTF_TEST_FILE:-""} 2&> async_pass_log.log + ./async_pass.sh ${CTF_TEST_FILE:-""} &> async_pass_log.log RECO_RC=$? echo_info "async_pass.sh finished with ${RECO_RC}" @@ -266,6 +311,9 @@ export ALIEN_JDL_LPMPRODUCTIONTAG=$ALIEN_JDL_LPMPRODUCTIONTAG_KEEP echo_info "Setting back ALIEN_JDL_LPMPRODUCTIONTAG to $ALIEN_JDL_LPMPRODUCTIONTAG" #<----- END OF part that should run under a clean alternative software environment if this was given ------ +# the async software environment, referenced from within the per-timeframe workspaces below +ASYNC_ENV_FILE=${PWD}/env_async.env + # now create the local MC config file --> config-json.json # we create the new config output with blacklist functionality ASYNC_CONFIG_BLACKLIST=${ASYNC_CONFIG_BLACKLIST:-${O2DPG_ROOT}/MC/run/ANCHOR/anchor-dpl-options-blacklist.json} @@ -346,7 +394,7 @@ for external_context in collission_context_*.root; do # apply software tagging choice # remainingargs="${remainingargs} ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG:+--alternative-reco-software ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG}}" ALIEN_JDL_O2DPG_ASYNC_RECO_FROMSTAGE=${ALIEN_JDL_O2DPG_ASYNC_RECO_FROMSTAGE:-RECO} - remainingargs="${remainingargs} ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG:+--alternative-reco-software ${PWD}/env_async.env@${ALIEN_JDL_O2DPG_ASYNC_RECO_FROMSTAGE}}" + remainingargs="${remainingargs} ${ALIEN_JDL_O2DPG_ASYNC_RECO_TAG:+--alternative-reco-software ${ASYNC_ENV_FILE}@${ALIEN_JDL_O2DPG_ASYNC_RECO_FROMSTAGE}}" # potentially add CCDB timemachine timestamp remainingargs="${remainingargs} ${ALIEN_JDL_CCDB_CONDITION_NOT_AFTER:+--condition-not-after ${ALIEN_JDL_CCDB_CONDITION_NOT_AFTER}}" # add external collision context injection From 90c0a3d259b814aeb6f687bcfdea6dec4694e94f Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 17 Sep 2026 00:06:13 +0200 Subject: [PATCH 070/144] Shift the first orbit along with the ITS ramp-up start of run This fixes a problem in the ITS ramp-up anchoring shift and adds a unit test. - The shift moved --sor and --timestamp but left --first-orbit at the orbit of start-of-run, while the timeframes a job simulates are placed by orbit. - A job at production offset 0 therefore still sampled the first orbit of the run, where ITS/Calib/TimeDeadMap masks all 24120 chips, and wrote no ITS digits at all: no ITS tracks, no reconstructed collisions, and an AOD whose collision table is empty. - HBFUtils.startTime and HBFUtils.orbitFirst also disagreed by the ramp-up duration. - Both coordinates now move together in shift_anchor_past_ITS_rampup. - For run 571781 with SPLITID=1 the first orbit moves from 20505888 to 20562116, past the 34080 orbits that run's dead map masks. https://its.cern.ch/jira/browse/O2-6565 https://its.cern.ch/jira/browse/O2-6894 Co-Authored-By: Claude Opus 5 --- MC/bin/o2dpg_sim_workflow_anchored.py | 25 ++++++++-- MC/bin/tests/test_anchoring_rampup.py | 69 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 MC/bin/tests/test_anchoring_rampup.py diff --git a/MC/bin/o2dpg_sim_workflow_anchored.py b/MC/bin/o2dpg_sim_workflow_anchored.py index e6ad5a803..c8a815598 100755 --- a/MC/bin/o2dpg_sim_workflow_anchored.py +++ b/MC/bin/o2dpg_sim_workflow_anchored.py @@ -320,6 +320,24 @@ def retrieve_ITS_RampDuration(ccdbreader, timestamp): print("WARNING: ITS ramp duration vector is empty, using 0") return 0 +def milliseconds_to_orbits(milliseconds): + """ + Number of complete LHC orbits contained in a time span given in milliseconds. + Rounded up, so that a span used to skip something is never cut short. + """ + return int(math.ceil(1000. * milliseconds / LHCOrbitMUS)) + +def shift_anchor_past_ITS_rampup(run_start, first_orbit, ITS_rampup): + """ + Moves the anchoring point past the ITS ramp-up period, in both of its + coordinates, and returns the pair (start of run in ms, first orbit). + The two have to move together: the timeframes a job simulates are placed by + orbit (first orbit plus the production offset), so shifting only the + timestamp leaves a job at production offset 0 inside the ramp, where the ITS + time-dead map masks every chip. + """ + return run_start + ITS_rampup, first_orbit + milliseconds_to_orbits(ITS_rampup) + def retrieve_MinBias_CTPScaler_Rate(raw_rate_at, finaltime, trig_eff_arg, NBunches, ColSystem, eCM, run_number = -1): """ Turns the raw CTP counting rate at finaltime (in milliseconds) into the interaction rate for @@ -590,10 +608,10 @@ def main(): run_start = GLOparams["SOR"] run_end = GLOparams["EOR"] - # Adjust start of run using ITS ramp-up period + # Adjust the anchoring point using the ITS ramp-up period ITS_rampup = retrieve_ITS_RampDuration(ccdbreader, run_start) print(f"ITS ramp-up time: {ITS_rampup} ms") - effective_run_start = run_start + ITS_rampup + effective_run_start, effective_first_orbit = shift_anchor_past_ITS_rampup(run_start, GLOparams["FirstOrbit"], ITS_rampup) mid_run_timestamp = (effective_run_start + run_end) // 2 # -------- @@ -676,6 +694,7 @@ def main(): # this is anchored to print ("Determined start-of-run to be: ", effective_run_start) + print ("Determined first orbit to be: ", effective_first_orbit) print ("Determined end-of-run to be: ", run_end) print ("Determined timestamp to be : ", timestamp) print ("Determined offset to be : ", prod_offset) @@ -721,7 +740,7 @@ def main(): # needs to be handled as further below: energyarg = (" -eCM " + str(eCM)) if A1 == A2 else (" -eA " + str(eA) + " -eB " + str(eB)) forwardargs += " -tf " + str(args.tf) + " --sor " + str(effective_run_start) + " --timestamp " + str(timestamp) + " --production-offset " + str(prod_offset) + " -run " + str(args.run_number) + " --run-anchored --first-orbit " \ - + str(GLOparams["FirstOrbit"]) + " --orbitsPerTF " + str(GLOparams["OrbitsPerTF"]) + str(energyarg) + + str(effective_first_orbit) + " --orbitsPerTF " + str(GLOparams["OrbitsPerTF"]) + str(energyarg) # the following options can be overwritten/influenced from the outside if not '-col' in forwardargs: forwardargs += ' -col ' + ColSystem diff --git a/MC/bin/tests/test_anchoring_rampup.py b/MC/bin/tests/test_anchoring_rampup.py new file mode 100644 index 000000000..2dac50f68 --- /dev/null +++ b/MC/bin/tests/test_anchoring_rampup.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Offline test: the ITS ramp-up shift must move the first orbit, not only the timestamp.""" +import os +import sys +import unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.dirname(HERE)) + +os.environ.setdefault("O2DPG_ROOT", os.path.dirname(os.path.dirname(os.path.dirname(HERE)))) + +try: + import o2dpg_sim_workflow_anchored as anchored # needs ROOT +except ImportError as exc: # pragma: no cover + anchored = None + IMPORT_ERROR = exc + +# Run 571781 of LHC26e9, whose SPLITID=1 job reconstructed no collisions at all: +# the ITS time-dead map masks all 24120 chips up to orbit 20539968 and the job was +# placed at the run's first orbit. See https://its.cern.ch/jira/browse/O2-6894 +FIRST_ORBIT = 20505888 +SOR = 1778806732526 +ITS_RAMPUP_MS = 5000 +FIRST_ALIVE_ORBIT = 20539968 + + +@unittest.skipIf(anchored is None, "o2dpg_sim_workflow_anchored not importable (needs ROOT)") +class TestRampUpShift(unittest.TestCase): + + def test_orbits_from_milliseconds(self): + orbit_ms = anchored.LHCOrbitMUS / 1000. + self.assertEqual(anchored.milliseconds_to_orbits(0), 0) + # a span of one orbit minus an epsilon still has to cover a full orbit + self.assertEqual(anchored.milliseconds_to_orbits(0.5 * orbit_ms), 1) + self.assertEqual(anchored.milliseconds_to_orbits(orbit_ms), 1) + + def test_both_coordinates_move(self): + """A ramp-up of a few seconds must move the orbit as well as the timestamp.""" + start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS) + self.assertEqual(start, SOR + ITS_RAMPUP_MS) + self.assertGreater(orbit, FIRST_ORBIT) + + def test_nothing_moves_without_a_ramp(self): + self.assertEqual(anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, 0), + (SOR, FIRST_ORBIT)) + + def test_shifted_orbit_is_never_inside_the_ramp(self): + """The shifted orbit must sit at or after the shifted timestamp, never before.""" + for ramp_ms in (0, 1, 500, ITS_RAMPUP_MS, 30000): + start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ramp_ms) + time_of_orbit = SOR + (orbit - FIRST_ORBIT) * anchored.LHCOrbitMUS / 1000. + self.assertGreaterEqual(time_of_orbit, start, + f"orbit shift falls short of the ramp for {ramp_ms} ms") + + def test_shift_agrees_with_the_timestamp_to_orbit_conversion(self): + """Closure: the shifted orbit is what main() derives from the shifted timestamp.""" + start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS) + # this is the conversion main() uses for the exclude_timestamp() check + derived = FIRST_ORBIT + int((start - SOR) / (anchored.LHCOrbitMUS / 1000.)) + self.assertLessEqual(abs(orbit - derived), 1) + + def test_split_id_one_clears_the_its_dead_window(self): + """Regression: the first job of a production must not sample the dead window.""" + _, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS) + self.assertGreater(orbit, FIRST_ALIVE_ORBIT) + + +if __name__ == "__main__": + unittest.main() From 41b8e47db7e65fca1737c1c60995df05af99e806 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Mon, 21 Sep 2026 08:49:00 +0200 Subject: [PATCH 071/144] Added user hook for triggered events (#2461) --- .../generator_pythia8_hfhadron_to_nuclei.C | 16 ++++++++++++++++ .../pythia8/hooks/pythia8_userhooks_qqbar.C | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/MC/config/PWGHF/external/generator/generator_pythia8_hfhadron_to_nuclei.C b/MC/config/PWGHF/external/generator/generator_pythia8_hfhadron_to_nuclei.C index a2aadef89..339b79eb8 100644 --- a/MC/config/PWGHF/external/generator/generator_pythia8_hfhadron_to_nuclei.C +++ b/MC/config/PWGHF/external/generator/generator_pythia8_hfhadron_to_nuclei.C @@ -9,6 +9,7 @@ R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT) #include "MC/config/common/external/generator/CoalescencePythia8.h" +#include "MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C" using namespace Pythia8; @@ -60,6 +61,16 @@ class GeneratorPythia8HFHadToNuclei : public o2::eventgen::GeneratorPythia8 { addSubGenerator(0, "Minimum bias"); addSubGenerator(1, "HF + Coalescence"); + + // Bias parton-level generation towards bbbar production at midrapidity, but only + // while generating the triggered events. Without this, finding the requested b-hadron + // can stall the jobs for a very long time + mBbbarBiasHook = new UserHooks_qqbar(); + mBbbarBiasHook->setPDG(5); + mBbbarBiasHook->setRapidity(mHadRapidityMin, mHadRapidityMax); + mBbbarBiasHook->setActive(false); + setUserHooks(mBbbarBiasHook); + return o2::eventgen::GeneratorPythia8::Init(); } @@ -93,11 +104,13 @@ class GeneratorPythia8HFHadToNuclei : public o2::eventgen::GeneratorPythia8 // Generate event of interest bool genOk = false; + mBbbarBiasHook->setActive(true); while (!genOk) { if (GeneratorPythia8::generateEvent()) { genOk = selectEvent(mPythia.event); } } + mBbbarBiasHook->setActive(false); notifySubGenerator(1); } else { // Generate minimum-bias event @@ -178,6 +191,9 @@ class GeneratorPythia8HFHadToNuclei : public o2::eventgen::GeneratorPythia8 float mHadRapidityMax; unsigned int mUsedSeed; + // Bias towards bbbar at midrapidity, active only while generating HF-triggered events + UserHooks_qqbar* mBbbarBiasHook{nullptr}; + // Control gap-triggering unsigned long long mGeneratedEvents; int mInverseTriggerRatio; diff --git a/MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C b/MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C index 9cbf6fe27..8dcf2b4cd 100644 --- a/MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C +++ b/MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C @@ -16,6 +16,10 @@ class UserHooks_qqbar : public Pythia8::UserHooks bool canVetoPartonLevel() override { return true; }; bool doVetoPartonLevel(const Pythia8::Event& event) override { + // Veto runtime condition + if (!mActive) { + return false; + } // search for c-cbar mother with at least one c at midrapidity for (int ipa = 0; ipa < event.size(); ++ipa) { auto daughterList = event[ipa].daughterList(); @@ -40,11 +44,15 @@ class UserHooks_qqbar : public Pythia8::UserHooks mRapidityMin = valMin; mRapidityMax = valMax; }; + // Allows the veto to be switched on/off at run time, e.g. so that a generator + // alternating between biased and unbiased events can apply the bias selectively. + void setActive(bool val) { mActive = val; }; private: int mPDG = 4; double mRapidityMin = -1.5; double mRapidityMax = 1.5; + bool mActive = true; }; Pythia8::UserHooks* From 046c4e247560a0d301285f6ef889680970f1ed34 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Wed, 2 Sep 2026 11:27:47 +0200 Subject: [PATCH 072/144] Keep the ZEM geometry instead of skipping the whole ZDC module This changes the simulation default from skipping the ZDC module to dropping only its +-113 m beam line, so the ZEM material stays in the transport geometry. - The ZDC geometry is the +-113 m beam line, its magnets, ZN, ZP and ZEM; the transport cost is the beam line, while ZEM sits at z = 7.6 m. - ZEM carries 217 kg of material between z = 745 and 862 cm, spanning eta 2.8 to 5.6, which is inside the FIT and FDD acceptance. - --skipModules no longer defaults to ZDC; create_geant_config() sets ZDCSimParam.buildBeamLine=false instead, unless --with-ZDC is given. - An explicit --skipModules ZDC still removes the module altogether. - ZDC stays out of digitisation and reconstruction as before, which the existing deactivate_detector('ZDC') already handles. - The MODULES variable removed from the two anchor scripts was never referenced; it read as if anchored MC opted out of ZDC, which the workflow default decided. - Requires ZDCSimParam.buildBeamLine in AliceO2. https://its.cern.ch/jira/browse/O2-7158 Co-Authored-By: Claude Opus 5 --- MC/bin/o2dpg_sim_config.py | 6 ++++++ MC/bin/o2dpg_sim_workflow.py | 20 +++++++------------- MC/run/ANCHOR/anchorMC.sh | 1 - MC/run/ANCHOR/anchorMC_DataEmbedding.sh | 1 - 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/MC/bin/o2dpg_sim_config.py b/MC/bin/o2dpg_sim_config.py index 79c6a4179..0ffc69e9c 100755 --- a/MC/bin/o2dpg_sim_config.py +++ b/MC/bin/o2dpg_sim_config.py @@ -183,6 +183,12 @@ def add(cfg, flatconfig): # ----- add default settings ----- add(config, {"MFTBase.buildAlignment" : "true"}) + + # Keep the ZEM calorimeters, drop the +-113 m beam line that makes the ZDC + # expensive to transport. --with-ZDC asks for the whole thing, and an explicit + # "--skipModules ZDC" still removes the module altogether. + if not getattr(args, "with_ZDC", False) and "ZDC" not in getattr(args, "skipModules", []): + add(config, {"ZDCSimParam.buildBeamLine" : "false"}) add(config, {"GenTPCLoopers.colsys" : args.col}) # ----- apply external overwrites from command line ------- diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 8f48700e6..6858c8281 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -110,7 +110,7 @@ parser.add_argument('--force-n-workers', dest='force_n_workers', action='store_true', help='by default, number of workers is re-computed ' 'for given interaction rate; ' 'pass this to avoid that') -parser.add_argument('--skipModules',nargs="*", help="List of modules to skip in geometry budget (and therefore processing)", default=["ZDC"]) +parser.add_argument('--skipModules',nargs="*", help="List of modules to skip in geometry budget (and therefore processing)", default=[]) parser.add_argument('--skipReadout',nargs="*", help="List of modules to take out from readout", default=[""]) parser.add_argument('--with-ZDC', action='store_true', help='Enable ZDC in workflow') parser.add_argument('-seed',help='random seed number', default=None) @@ -273,18 +273,12 @@ def load_external_config(configfile): print(f"INFO: Written additional config key parameters to JSON {config_key_param_path}") json.dump(anchorConfig, f, indent=2) -# Processing skipped material budget (modules): -# - If user did NOT specify --with-ZDC -# - AND ZDC is not already in the list -# --> append ZDC automatically -if args.with_ZDC: - # User wants ZDC to *not* be skipped → ensure it's removed - args.skipModules = [m for m in args.skipModules if m != "ZDC"] -else: - # If user did not request --with-ZDC, - # auto-append ZDC unless already present - if "ZDC" not in args.skipModules: - args.skipModules.append("ZDC") +# The ZDC geometry is the +-113 m beam line, its magnets, ZN, ZP and ZEM. What it +# costs in transport time is the beam line; ZEM sits at z = 7.6 m and carries +# 217 kg of material inside the FIT and FDD acceptance. Without --with-ZDC we +# therefore drop the beam line and keep ZEM, instead of dropping the whole module +# -- see ZDCSimParam.buildBeamLine in create_geant_config(). ZDC stays out of the +# readout either way, which the deactivate_detector('ZDC') below takes care of. # with this we can tailor the workflow to the presence of # certain detectors diff --git a/MC/run/ANCHOR/anchorMC.sh b/MC/run/ANCHOR/anchorMC.sh index 3d634269e..00615a253 100755 --- a/MC/run/ANCHOR/anchorMC.sh +++ b/MC/run/ANCHOR/anchorMC.sh @@ -327,7 +327,6 @@ fi # -- CREATE THE MC JOB DESCRIPTION ANCHORED TO RUN -- -MODULES="--skipModules ZDC" # publish MCPRODINFO for first few jobs of a production # if external script exported PUBLISH_MCPRODINFO, it will be published anyways diff --git a/MC/run/ANCHOR/anchorMC_DataEmbedding.sh b/MC/run/ANCHOR/anchorMC_DataEmbedding.sh index 0f49dda6c..8ceee6799 100755 --- a/MC/run/ANCHOR/anchorMC_DataEmbedding.sh +++ b/MC/run/ANCHOR/anchorMC_DataEmbedding.sh @@ -328,7 +328,6 @@ fi # -- CREATE THE MC JOB DESCRIPTION ANCHORED TO RUN -- -MODULES="--skipModules ZDC" # publish MCPRODINFO for first few jobs of a production # if external script exported PUBLISH_MCPRODINFO, it will be published anyways From 8af99c34a071fb1c892d4060959e827d9284e20b Mon Sep 17 00:00:00 2001 From: Michal Broz Date: Mon, 21 Sep 2026 12:05:51 +0200 Subject: [PATCH 073/144] Fix typo in Tau decay tables --- .../PWGUD/external/generator/GeneratorStarlightToEvtGen.C | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C b/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C index 9d59d3c44..3d30c48d2 100644 --- a/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C +++ b/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C @@ -50,8 +50,8 @@ FairGenerator* else if (configuration.find("JpsiToElRad") != std::string::npos) gen->SetDecayTable(Form("%s/JPSI.EE.DEC",pathO2.Data())); else if (configuration.find("ToL+3Pi") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.LP3PI.DEC",pathO2.Data())); else if (configuration.find("ToL-3Pi") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.LN3PI.DEC",pathO2.Data())); - else if (configuration.find("ToPi+3Pi") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.LP3PI.DEC",pathO2.Data())); - else if (configuration.find("ToPi-3Pi") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.LN3PI.DEC",pathO2.Data())); + else if (configuration.find("ToPi+3Pi") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.PP3PI.DEC",pathO2.Data())); + else if (configuration.find("ToPi-3Pi") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.PN3PI.DEC",pathO2.Data())); else if (configuration.find("To6Pi") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.6PI.DEC",pathO2.Data())); else if (configuration.find("ToElMu") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.ELMU.DEC",pathO2.Data())); else if (configuration.find("ToElPiPi0") != std::string::npos) gen->SetDecayTable(Form("%s/TAUTAU.ELPI.DEC",pathO2.Data())); From 8d5dc8be388135d13c6c5efdb04a0a8517062a43 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 21 Sep 2026 22:18:09 +0200 Subject: [PATCH 074/144] Run the O2DPG simulation tests against a CVMFS release This replaces the aliBuild-based simulation-test check with a GitHub Actions job that runs the existing tests against a published O2PDPSuite release, and fixes two exit-code problems in the workflow tests. - test/run_tests.sh now owns the environment setup, sub-test selection and exit-code aggregation that lived in the alidist recipe body. - The new job runs on a self-hosted runner with CVMFS and compiles nothing. - run_workflow_tests.sh dropped ret_global_pwg from its final exit code, so a failed PWG workflow creation reported FAILED and still passed. - The AnalysisQC execution block was gated differently from the announcement above it; both now agree. - Three offline harnesses under test/tests/ check the entrypoint, the tag selection and the exit-code aggregation, and syntax-checks.yml runs them. - Touching test/needs-o2-dev opts a pull request into a source build against O2 dev, for changes that cannot use a published release. - curl's include directory is added to ROOT_INCLUDE_PATH, derived from curl-config, because O2's CCDB dictionary payload includes curl/curl.h and a runtime environment carries no curl headers. - The alidist recipe and the ali-bot check are left in place for now. Co-Authored-By: Claude Opus 5 --- .github/workflows/sim-tests.yml | 192 ++++++++++++++++++++++++++++ .github/workflows/syntax-checks.yml | 18 +++ MC/bin/o2dpg_sim_workflow.py | 1 + test/README.md | 64 +++++++--- test/ci/resolve_tag.sh | 38 ++++++ test/needs-o2-dev | 5 + test/run_tests.sh | 105 +++++++++++++++ test/run_workflow_tests.sh | 4 +- test/tests/exit_code_aggregation.sh | 46 +++++++ test/tests/resolve_tag.sh | 65 ++++++++++ test/tests/run_tests_selection.sh | 94 ++++++++++++++ 11 files changed, 615 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/sim-tests.yml create mode 100644 test/ci/resolve_tag.sh create mode 100644 test/needs-o2-dev create mode 100755 test/run_tests.sh create mode 100755 test/tests/exit_code_aggregation.sh create mode 100755 test/tests/resolve_tag.sh create mode 100755 test/tests/run_tests_selection.sh diff --git a/.github/workflows/sim-tests.yml b/.github/workflows/sim-tests.yml new file mode 100644 index 000000000..60c05358f --- /dev/null +++ b/.github/workflows/sim-tests.yml @@ -0,0 +1,192 @@ +--- +name: Simulation tests + +'on': + pull_request: {} + workflow_dispatch: + inputs: + tag: + description: 'O2PDPSuite tag to test against (default: newest daily)' + type: string + required: false + +permissions: {} + +concurrency: + group: sim-tests-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + sim-tests: + name: Simulation tests against CVMFS + runs-on: [self-hosted, cvmfs] + timeout-minutes: 180 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # The changed-file logic diffs against the merge base, so the full + # history is needed, not a shallow clone. + fetch-depth: 0 + + - name: Resolve the diff base + id: base + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -eu + git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null || { + echo "::error title=Cannot resolve diff base::pull request base commit $BASE_SHA does not resolve in this checkout" + exit 1 + } + git rev-parse --verify "$HEAD_SHA^{commit}" >/dev/null || { + echo "::error title=Cannot resolve diff head::pull request head commit $HEAD_SHA does not resolve in this checkout" + exit 1 + } + merge_base=$(git merge-base "$BASE_SHA" "$HEAD_SHA") || { + echo "::error title=Cannot compute diff base::git merge-base failed" + exit 1 + } + [ -n "$merge_base" ] || { + echo "::error title=Cannot compute diff base::merge base is empty" + exit 1 + } + echo "sha=$merge_base" >> "$GITHUB_OUTPUT" + + - name: Skip when not relevant or opted into the source build + id: gate + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ steps.base.outputs.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -eu + changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") + # Opting in means *editing* the existing sentinel, so only count it + # when it is Modified. A pull request that Adds it — which is what the + # pull request introducing the sentinel does — would otherwise trip its + # own opt-out and pass without testing anything. + opted=$(git diff --name-only --diff-filter=M "$BASE_SHA" "$HEAD_SHA") + if grep -qx 'test/needs-o2-dev' <<< "$opted" ; then + echo "::notice title=Skipped::this pull request touches test/needs-o2-dev, so it is tested by build/O2DPG/sim/o2dev against O2 dev instead" + echo "skip=true" >> "$GITHUB_OUTPUT" + elif ! grep -qE '^(DATA/|MC/|test/|RelVal/)' <<< "$changed" ; then + echo "::notice title=Skipped::no changed file matches DATA/, MC/, test/ or RelVal/" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Check the CVMFS environment + if: steps.gate.outputs.skip != 'true' + run: | + set -eu + test -d /cvmfs/alice.cern.ch || { + echo "::error title=CVMFS unavailable::/cvmfs/alice.cern.ch is not mounted on this runner" + exit 1 + } + test -x /cvmfs/alice.cern.ch/bin/alienv || { + echo "::error title=CVMFS unavailable::/cvmfs/alice.cern.ch/bin/alienv is missing" + exit 1 + } + + - name: Check the AliEn token + if: steps.gate.outputs.skip != 'true' + env: + JALIEN_TOKEN_CERT: /run/alien-ci/tokencert.pem + JALIEN_TOKEN_KEY: /run/alien-ci/tokenkey.pem + run: | + set -eu + # DPL's CCDB backend refuses to talk to alice-ccdb.cern.ch without a + # token and aborts the whole device, so check up front rather than + # letting it surface as a confusing task crash deep in a workflow. + for f in "$JALIEN_TOKEN_CERT" "$JALIEN_TOKEN_KEY" ; do + test -r "$f" || { + echo "::error title=No AliEn token::$f is missing or unreadable. On the runner host: systemctl start alien-ci-token.service" + exit 1 + } + done + if ! openssl x509 -in "$JALIEN_TOKEN_CERT" -noout -checkend 3600 >/dev/null 2>&1 ; then + echo "::error title=AliEn token expiring::the token expires within the hour. On the runner host: systemctl start alien-ci-token.service" + exit 1 + fi + echo "AliEn token valid until $(openssl x509 -in "$JALIEN_TOKEN_CERT" -noout -enddate | cut -d= -f2)" + + - name: Resolve the O2PDPSuite tag + id: tag + if: steps.gate.outputs.skip != 'true' + env: + REQUESTED_TAG: ${{ inputs.tag }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + set -eu + # shellcheck source=test/ci/resolve_tag.sh + . test/ci/resolve_tag.sh + moduledir=/cvmfs/alice.cern.ch/el9-x86_64/Modules/modulefiles/O2PDPSuite + requested=$REQUESTED_TAG + if [ -z "$requested" ]; then + # A PR can pin the release with a line "sim-tests-tag: ". + requested=$(printf '%s\n' "$PR_BODY" | + sed -n 's/^[[:space:]]*sim-tests-tag:[[:space:]]*//p' | head -n 1 | + tr -d '[:space:]') + fi + tag=$(resolve_o2pdpsuite_tag "$moduledir" "$requested") || { + echo "::error title=No usable O2PDPSuite release::see the message above" + exit 1 + } + echo "Testing against O2PDPSuite::$tag" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Run the O2DPG tests + if: steps.gate.outputs.skip != 'true' + env: + O2PDPSUITE_TAG: ${{ steps.tag.outputs.tag }} + O2DPG_TEST_HASH_BASE: ${{ steps.base.outputs.sha }} + O2DPG_TEST_HASH_HEAD: ${{ github.event.pull_request.head.sha }} + JOBS: 8 + # o2dpg_sim_workflow.py calls JAlien(['whoami']) purely to fill the + # AOD's --created-by field, and only when JALIEN_USER is unset. Setting + # it keeps the tests from needing a GRID credential at all, which + # matters here: anything readable by this account is readable by the + # fork-pull-request code that runs as it. + JALIEN_USER: alien-ci + # DPL's CCDB backend needs a GRID token for alice-ccdb.cern.ch. These + # are *paths* to a short-lived token minted from a service certificate + # by a root-owned timer on the runner host; the certificate itself is + # never readable by this account. Verified that paths work — the same + # variables also accept PEM content, which is what ali-bot passes. + JALIEN_TOKEN_CERT: /run/alien-ci/tokencert.pem + JALIEN_TOKEN_KEY: /run/alien-ci/tokenkey.pem + run: | + set -eu + # Everything after "-c" is joined into one string and re-evaluated + # by the CVMFS alienv via "bash -c \"$*\"". Quoting here is applied + # once then discarded, so it is safe for the runner's workspace path, + # but a path with a space or "$" would break or double-evaluate. + /cvmfs/alice.cern.ch/bin/alienv setenv "O2PDPSuite/$O2PDPSUITE_TAG" -c \ + env O2DPG_ROOT="$PWD" O2DPG_MC_CONFIG_ROOT="$PWD" \ + O2DPG_TEST_REPO_DIR="$PWD" \ + O2DPG_TEST_HASH_BASE="$O2DPG_TEST_HASH_BASE" \ + O2DPG_TEST_HASH_HEAD="$O2DPG_TEST_HASH_HEAD" \ + JOBS="$JOBS" \ + bash test/run_tests.sh + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: o2dpg-test-logs + path: | + o2dpg_tests/**/*.log + o2dpg_tests/**/*serverlog* + o2dpg_tests/**/*workerlog* + o2dpg_tests/**/*mergerlog* + if-no-files-found: ignore + retention-days: 14 + + - name: Prune test artifacts + if: always() + run: find o2dpg_tests -type f ! -name '*.log' ! -name '*serverlog*' ! -name '*workerlog*' ! -name '*mergerlog*' -delete || true diff --git a/.github/workflows/syntax-checks.yml b/.github/workflows/syntax-checks.yml index bd9b5de6e..73b888523 100644 --- a/.github/workflows/syntax-checks.yml +++ b/.github/workflows/syntax-checks.yml @@ -134,6 +134,24 @@ jobs: - name: Run the FileIOGraph test suite run: python3 -m unittest discover -s UTILS/FileIOGraph/tests -t UTILS/FileIOGraph/tests + bash-harnesses: + name: Test-harness unit tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run the offline test harnesses + run: | + error=0 + for t in test/tests/*.sh ; do + echo "::group::$t" + bash "$t" || error=1 + echo "::endgroup::" + done + exit "$error" + pylint: name: Pylint runs-on: ubuntu-latest diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 6858c8281..54352b80a 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -5,6 +5,7 @@ # It aims to handle the different MC possible configurations # It just creates a workflow.json txt file, to execute the workflow one must execute right after # ${O2DPG_ROOT}/MC/bin/o2_dpg_workflow_runner.py -f workflow.json +# The tests covering this script are described in test/README.md. # # Execution examples: # - pp PYTHIA jets, 2 events, triggered on high pT decay photons on all barrel calorimeters acceptance, eCMS 13 TeV diff --git a/test/README.md b/test/README.md index 7109e4e70..276e9eaf7 100644 --- a/test/README.md +++ b/test/README.md @@ -7,7 +7,7 @@ At the moment, the tests focus on generator configurations and custom generators Tests are run via ```bash -${O2DPG_ROOT}/test/run_tests.sh [--fail-immediately] +${O2DPG_ROOT}/test/run_tests.sh [--fail-immediately] [--keep-artifacts] [SUBTEST...] ``` Tests are run for changed @@ -56,7 +56,32 @@ O2DPG_TEST_REPO_DIR= ${O2DPG_ROOT}/test/run_tests.sh [--f ``` If you are inside the source directory, you can simply run ```bash -${O2DPG_ROOT}/test/run_tests.sh [--fail-immediately] +${O2DPG_ROOT}/test/run_tests.sh [--fail-immediately] [--keep-artifacts] [SUBTEST...] +``` +If the change you are testing is to a test script itself (`run_tests.sh` or +any `run_*_tests.sh`), invoke the checkout's own entrypoint instead, e.g. +`bash test/run_tests.sh` from inside the checkout: `run_tests.sh` finds its +sub-scripts next to itself, so calling `${O2DPG_ROOT}/test/run_tests.sh` +tests the *released* copy of the script you just edited, not your change, +even with `O2DPG_TEST_REPO_DIR` pointed at the checkout. + +### Running a subset + +`run_tests.sh` runs the generator, workflow and RelVal sub-tests. To run only +some of them, name them: + +```bash +${O2DPG_ROOT}/test/run_tests.sh generator relval +``` + +The offline harnesses under `test/tests/` check the entrypoint's selection, +exit-code aggregation and O2PDPSuite tag resolution without needing an O2 +environment: + +```bash +bash test/tests/run_tests_selection.sh +bash test/tests/exit_code_aggregation.sh +bash test/tests/resolve_tag.sh ``` ### Keeping all test artifacts @@ -74,24 +99,33 @@ ${O2DPG_ROOT}/test/run_tests.sh -h ``` which will give you ``` -usage: run_tests.sh [--fail-immediately] [--keep-artifacts] + +usage: run_tests.sh [--fail-immediately] [--keep-artifacts] [SUBTEST...] + + SUBTEST : one or more of: generator workflow relval (default: all) FLAGS: - --fail-immediately : abort as soon as the first tests fails - --keep-artifacts : keep simulation and tests artifacts, by default everything but the logs is removed after each test + --fail-immediately : stop after the first failing sub-test + --keep-artifacts : keep simulation artifacts, not just the logs ENVIRONMENT VARIABLES: - O2DPG_TEST_REPO_DIR : Point to the source repository you want to test. - O2DPG_TEST_HASH_BASE : The base hash you want to use for comparison (optional) - O2DPG_TEST_HASH_HEAD : The head hash you want to use for comparison (optional) - - If O2DPG_TEST_HASH_BASE is not set, it will be looked for ALIBUILD_BASE_HASH. - If also not set, this will be set to HEAD~1. However, if there are unstaged - changes, it will be set to HEAD. + O2DPG_TEST_REPO_DIR : the source repository to test + O2DPG_TEST_HASH_BASE : base hash for the changed-file diff (optional) + O2DPG_TEST_HASH_HEAD : head hash for the changed-file diff (optional) - If O2DPG_TEST_HASH_HEAD is not set, it will be looked for ALIBUILD_HEAD_HASH. - If also not set, this will be set to HEAD. However, if there are unstaged - changes, it will left blank. ``` + +## When your change needs an unreleased O2 + +The `Simulation tests against CVMFS` check runs against a published +`O2PDPSuite` release, so a change that depends on an unmerged or unreleased O2 +commit cannot pass it. Two escape hatches, in order of preference: + +1. If the O2 change is already in a published daily, pin it: add a line + `sim-tests-tag: daily-YYYYMMDD-HHMM-1` to the pull request description. +2. If it is not published anywhere yet, touch `test/needs-o2-dev` with a + one-line reason and a link to the O2 pull request. That enables + `build/O2DPG/sim/o2dev`, which builds O2 from source against `dev`. It is + much slower, so it is opt-in. diff --git a/test/ci/resolve_tag.sh b/test/ci/resolve_tag.sh new file mode 100644 index 000000000..940090ba1 --- /dev/null +++ b/test/ci/resolve_tag.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Pick the O2PDPSuite release to test against. Kept free of CVMFS paths and of +# CI variables so it can be exercised offline. + +resolve_o2pdpsuite_tag() +{ + local moduledir=${1:-} + local requested=${2:-} + + if [[ ! -d "${moduledir}" ]] ; then + echo "resolve_o2pdpsuite_tag: no such directory: ${moduledir}" >&2 + return 1 + fi + + if [[ -n "${requested}" ]] ; then + case ${requested} in + *[!A-Za-z0-9._-]* ) + echo "resolve_o2pdpsuite_tag: invalid tag: ${requested}" >&2 + return 1 ;; + esac + if [[ ! -e "${moduledir}/${requested}" ]] ; then + echo "resolve_o2pdpsuite_tag: requested tag not available: ${requested}" >&2 + return 1 + fi + echo "${requested}" + return 0 + fi + + local newest + newest=$(find "${moduledir}" -maxdepth 1 -name 'daily-*' -printf '%f\n' 2>/dev/null | + sort -V | tail -n 1) + if [[ -z "${newest}" ]] ; then + echo "resolve_o2pdpsuite_tag: no daily-* tag in ${moduledir}" >&2 + return 1 + fi + echo "${newest}" +} diff --git a/test/needs-o2-dev b/test/needs-o2-dev new file mode 100644 index 000000000..ee9799078 --- /dev/null +++ b/test/needs-o2-dev @@ -0,0 +1,5 @@ +Touch this file in a pull request that cannot be tested against a published +O2PDPSuite release because it depends on an unreleased change in O2. +Replace this text with a one-line reason and a link to the O2 pull request. +Touching it enables the build/O2DPG/sim/o2dev check, which builds O2 from +source against AliceO2Group/AliceO2 dev. diff --git a/test/run_tests.sh b/test/run_tests.sh new file mode 100755 index 000000000..400f4d5e4 --- /dev/null +++ b/test/run_tests.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Single entrypoint for the O2DPG tests. Runs the generator, workflow and +# RelVal sub-tests, aggregates their exit codes and prints the collected log. + +set -u + +SUBTEST_DIR=${O2DPG_TEST_SUBTEST_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)} + +ALL_SUBTESTS="generator workflow relval" + +print_usage() +{ + echo + echo "usage: run_tests.sh [--fail-immediately] [--keep-artifacts] [SUBTEST...]" + echo + echo " SUBTEST : one or more of: ${ALL_SUBTESTS} (default: all)" + echo + echo " FLAGS:" + echo + echo " --fail-immediately : stop after the first failing sub-test" + echo " --keep-artifacts : keep simulation artifacts, not just the logs" + echo + echo " ENVIRONMENT VARIABLES:" + echo + echo " O2DPG_TEST_REPO_DIR : the source repository to test" + echo " O2DPG_TEST_HASH_BASE : base hash for the changed-file diff (optional)" + echo " O2DPG_TEST_HASH_HEAD : head hash for the changed-file diff (optional)" + echo +} + +fail_immediately= +subtest_args=() +selected=() + +while [ "$#" -gt 0 ] ; do + case $1 in + --fail-immediately ) fail_immediately=1 ; subtest_args+=("$1") ; shift ;; + --keep-artifacts ) subtest_args+=("$1") ; shift ;; + --help|-h ) print_usage ; exit 1 ;; + generator|workflow|relval ) selected+=("$1") ; shift ;; + * ) echo "Unknown argument ${1}" ; print_usage ; exit 1 ;; + esac +done + +[[ "${#selected[@]}" == "0" ]] && read -r -a selected <<< "${ALL_SUBTESTS}" + +# Geant4 dataset variables, if a Geant4 installation is in the environment. +if [[ -n "${G4INSTALL:-}" ]] ; then + eval "$("${G4INSTALL}/bin/geant4-config" --datasets | + sed -e 's/[^ ]* //' -e 's/G4/export G4/' -e 's/DATA /DATA=/')" +fi + +# O2's CCDB dictionary payload includes . An aliBuild build +# environment has curl's headers; a plain runtime environment from CVMFS does +# not, and cling then dies compiling the payload. Derive the prefix from +# curl-config rather than hardcoding a version. +if command -v curl-config > /dev/null 2>&1 ; then + curl_include=$(curl-config --prefix 2>/dev/null)/include + if [[ -f "${curl_include}/curl/curl.h" && ":${ROOT_INCLUDE_PATH:-}:" != *":${curl_include}:"* ]] ; then + export ROOT_INCLUDE_PATH="${curl_include}${ROOT_INCLUDE_PATH:+:${ROOT_INCLUDE_PATH}}" + echo "Added ${curl_include} to ROOT_INCLUDE_PATH for the CCDB dictionary" + fi +fi + +# LHAPDF data, needed by several generator configurations. +if [[ -z "${LHAPDF_DATA_PATH:-}" && -n "${LHAPDF_ROOT:-}" ]] ; then + export LHAPDF_DATA_PATH="${LHAPDF_ROOT}/share/LHAPDF:${LHAPDF_PDFSETS_ROOT:-}/share/LHAPDF" + echo "Set LHAPDF_DATA_PATH to ${LHAPDF_DATA_PATH}" +fi + +ret_global=0 +for subtest in "${selected[@]}" ; do + script="${SUBTEST_DIR}/run_${subtest}_tests.sh" + if [[ ! -x "${script}" ]] ; then + echo "ERROR: sub-test script ${script} not found or not executable" + ret_global=1 + [[ "${fail_immediately}" == "1" ]] && break + continue + fi + echo + echo "==> START SUBTEST: ${subtest} <==" + # Only run_generator_tests.sh accepts --fail-immediately/--keep-artifacts; + # run_workflow_tests.sh and run_relval_tests.sh accept only --help/-h. + case ${subtest} in + generator ) forward=("${subtest_args[@]:-}") ;; + * ) forward=() ;; + esac + "${script}" "${forward[@]}" + ret_this=$? + echo "==> END SUBTEST: ${subtest} (exit ${ret_this}) <==" + if [[ "${ret_this}" != "0" ]] ; then + ret_global=${ret_this} + [[ "${fail_immediately}" == "1" ]] && break + fi +done + +echo +if [[ "${ret_global}" != "0" ]] ; then + echo "error detected in O2DPG tests, see above" +else + echo "O2DPG tests passed" +fi + +exit ${ret_global} diff --git a/test/run_workflow_tests.sh b/test/run_workflow_tests.sh index 5c7fb4117..e6fb3717a 100755 --- a/test/run_workflow_tests.sh +++ b/test/run_workflow_tests.sh @@ -293,7 +293,7 @@ fi # prepare our local test directory for bin tests # global return code for PWGs ret_analysis_qc=0 -if [[ "${changed_analysis_qc}" != "" ]] ; then +if [[ "${changed_analysis_qc}" != "" || "${changed_sim_bin}" != "" ]] ; then rm -rf ${TEST_PARENT_DIR_BIN} 2>/dev/null mkdir -p ${TEST_PARENT_DIR_BIN} 2>/dev/null pushd ${TEST_PARENT_DIR_BIN} > /dev/null @@ -336,7 +336,7 @@ if [[ "${changed_anchored}" != "" ]] ; then popd > /dev/null fi -RET=$(( ret_analysis_qc + ret_global_anchored )) +RET=$(( ret_global_pwg + ret_analysis_qc + ret_global_anchored )) echo [[ "${RET}" != "0" ]] && echo_red "There were errors, please check!" || echo_green "All required workflow tests successful" diff --git a/test/tests/exit_code_aggregation.sh b/test/tests/exit_code_aggregation.sh new file mode 100755 index 000000000..1172f23d9 --- /dev/null +++ b/test/tests/exit_code_aggregation.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Static guard against the exit-code-aggregation bug in run_workflow_tests.sh: +# the final RET=$(( ... )) line must include every aggregate return-code +# variable the script computes, and each of those variables must actually be +# assigned somewhere in the script. + +set -u +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +SCRIPT=${O2DPG_TEST_WORKFLOW_SCRIPT:-"${HERE}/../run_workflow_tests.sh"} +FAILURES=0 + +check() +{ + local label=$1 expected=$2 actual=$3 + if [[ "${expected}" == "${actual}" ]] ; then + echo "ok - ${label}" + else + echo "FAIL - ${label}: expected '${expected}', got '${actual}'" + FAILURES=$((FAILURES + 1)) + fi +} + +# The three aggregate return-code variables the final exit code must include. +# Explicit and hard-coded on purpose: a fourth aggregate added later must make +# whoever adds it come here and update this list. +REQUIRED_VARS="ret_global_pwg ret_analysis_qc ret_global_anchored" + +RET_LINE=$(grep -m1 -E '^RET=\$\(\(' "${SCRIPT}") + +for var in ${REQUIRED_VARS} ; do + present=$(grep -c -- "${var}" <<< "${RET_LINE}") + check "RET= line includes ${var}" 1 "${present}" +done + +for var in ${REQUIRED_VARS} ; do + assigned=$(grep -c -E "^[[:space:]]*${var}=" "${SCRIPT}") + check "${var} is assigned in the script" 1 "$( [[ "${assigned}" -ge 1 ]] && echo 1 || echo 0 )" +done + +echo +if [[ "${FAILURES}" == "0" ]] ; then + echo "All exit-code aggregation checks passed" +else + echo "${FAILURES} test(s) failed" +fi +exit $(( FAILURES > 0 ? 1 : 0 )) diff --git a/test/tests/resolve_tag.sh b/test/tests/resolve_tag.sh new file mode 100755 index 000000000..d896ece11 --- /dev/null +++ b/test/tests/resolve_tag.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Offline harness for resolve_o2pdpsuite_tag: builds a fake modulefile +# directory so the function can be checked without CVMFS. + +set -u +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=../ci/resolve_tag.sh +source "${HERE}/../ci/resolve_tag.sh" +FAILURES=0 + +check() +{ + local label=$1 expected=$2 actual=$3 + if [[ "${expected}" == "${actual}" ]] ; then + echo "ok - ${label}" + else + echo "FAIL - ${label}: expected '${expected}', got '${actual}'" + FAILURES=$((FAILURES + 1)) + fi +} + +TMP=$(mktemp -d) +D="${TMP}/O2PDPSuite" +mkdir -p "${D}" +# Deliberately out of lexical-creation order, and with non-daily neighbours. +touch "${D}/daily-20260919-0000-1" \ + "${D}/daily-20260921-0000-1" \ + "${D}/daily-20260920-0000-1" \ + "${D}/async-2024-pp-apass1-1" \ + "${D}/slc9-nightly-20260921-1" + +out=$(resolve_o2pdpsuite_tag "${D}") ; rc=$? +check "newest daily is chosen" "daily-20260921-0000-1" "${out}" +check "success exit code" 0 "${rc}" + +out=$(resolve_o2pdpsuite_tag "${D}" "daily-20260919-0000-1") ; rc=$? +check "requested tag honoured" "daily-20260919-0000-1" "${out}" +check "requested tag exit code" 0 "${rc}" + +out=$(resolve_o2pdpsuite_tag "${D}" "daily-20991231-0000-1" 2>/dev/null) ; rc=$? +check "absent requested tag fails" 1 "${rc}" +check "absent requested tag prints nothing on stdout" "" "${out}" + +out=$(resolve_o2pdpsuite_tag "${TMP}/does-not-exist" 2>/dev/null) ; rc=$? +check "missing directory fails" 1 "${rc}" + +mkdir -p "${TMP}/empty" +out=$(resolve_o2pdpsuite_tag "${TMP}/empty" 2>/dev/null) ; rc=$? +check "directory with no dailies fails" 1 "${rc}" + +# A requested tag from a fork PR's body must not be able to escape the +# O2PDPSuite module directory via "..". +mkdir -p "${TMP}/O2" +touch "${TMP}/O2/something" +out=$(resolve_o2pdpsuite_tag "${D}" "../O2/something" 2>/dev/null) ; rc=$? +check "path traversal tag fails" 1 "${rc}" +check "path traversal tag prints nothing on stdout" "" "${out}" + +echo +if [[ "${FAILURES}" == "0" ]] ; then + echo "All resolve_tag tests passed" +else + echo "${FAILURES} test(s) failed" +fi +exit $(( FAILURES > 0 ? 1 : 0 )) diff --git a/test/tests/run_tests_selection.sh b/test/tests/run_tests_selection.sh new file mode 100755 index 000000000..a876b0a29 --- /dev/null +++ b/test/tests/run_tests_selection.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Offline harness for run_tests.sh: stubs the sub-test scripts and checks that +# selection and exit-code aggregation behave. + +set -u +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ENTRY="${HERE}/../run_tests.sh" +FAILURES=0 + +check() +{ + local label=$1 expected=$2 actual=$3 + if [[ "${expected}" == "${actual}" ]] ; then + echo "ok - ${label}" + else + echo "FAIL - ${label}: expected '${expected}', got '${actual}'" + FAILURES=$((FAILURES + 1)) + fi +} + +# A stub directory with three scripts whose exit codes we control via env +# vars, and which each record the arguments they received to a sibling +# "*.args" file (one per line) so forwarding can be checked. +make_stubs() +{ + local dir=$1 + mkdir -p "${dir}" + for name in generator workflow relval ; do + cat > "${dir}/run_${name}_tests.sh" < '${dir}/run_${name}_tests.args' +exit \${STUB_${name^^}_RC:-0} +EOF + chmod +x "${dir}/run_${name}_tests.sh" + done +} + +run_entry() +{ + # usage: run_entry [args...]; prints nothing, sets OUT and RC + local dir=$1 ; shift + OUT=$(O2DPG_TEST_SUBTEST_DIR="${dir}" O2DPG_ROOT="${HERE}/../.." \ + O2DPG_TEST_REPO_DIR="${HERE}/../.." \ + bash "${ENTRY}" "$@" 2>&1) + RC=$? +} + +TMP=$(mktemp -d) +make_stubs "${TMP}/subtests" + +# 1. all three sub-tests run by default and a clean run exits 0 +run_entry "${TMP}/subtests" +check "default selection exits 0" 0 "${RC}" +check "default runs generator" 1 "$(grep -c 'STUB generator ran' <<< "${OUT}")" +check "default runs workflow" 1 "$(grep -c 'STUB workflow ran' <<< "${OUT}")" +check "default runs relval" 1 "$(grep -c 'STUB relval ran' <<< "${OUT}")" + +# 2. an explicit selection runs only what was asked for +run_entry "${TMP}/subtests" relval +check "explicit selection exits 0" 0 "${RC}" +check "explicit selection skips generator" 0 "$(grep -c 'STUB generator ran' <<< "${OUT}")" +check "explicit selection runs relval" 1 "$(grep -c 'STUB relval ran' <<< "${OUT}")" + +# 3. a failing sub-test makes the entrypoint fail, and the others still run +STUB_GENERATOR_RC=3 run_entry "${TMP}/subtests" +check "failing generator propagates" 3 "${RC}" +check "failure does not stop workflow" 1 "$(grep -c 'STUB workflow ran' <<< "${OUT}")" + +# 4. --fail-immediately stops after the first failure +STUB_GENERATOR_RC=3 run_entry "${TMP}/subtests" --fail-immediately +check "fail-immediately propagates" 3 "${RC}" +check "fail-immediately stops early" 0 "$(grep -c 'STUB relval ran' <<< "${OUT}")" + +# 5. a later failure is not masked by an earlier success +STUB_RELVAL_RC=4 run_entry "${TMP}/subtests" +check "late failure propagates" 4 "${RC}" + +# 6. flag forwarding is per sub-test: only "generator" accepts these flags +run_entry "${TMP}/subtests" --keep-artifacts +check "keep-artifacts reaches generator" \ + 1 "$(grep -cx -- '--keep-artifacts' "${TMP}/subtests/run_generator_tests.args")" +check "keep-artifacts does not reach workflow" \ + 0 "$(grep -cx -- '--keep-artifacts' "${TMP}/subtests/run_workflow_tests.args")" +check "keep-artifacts does not reach relval" \ + 0 "$(grep -cx -- '--keep-artifacts' "${TMP}/subtests/run_relval_tests.args")" + +echo +if [[ "${FAILURES}" == "0" ]] ; then + echo "All run_tests.sh selection tests passed" +else + echo "${FAILURES} test(s) failed" +fi +exit $(( FAILURES > 0 ? 1 : 0 )) From d07c87f87039bf33d4b98e21398b3b712b23eb9c Mon Sep 17 00:00:00 2001 From: mj525 <99000728+mjkim525@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:11:34 +0900 Subject: [PATCH 075/144] [PWGLF] Revise injected-particle pT and rapidity ranges and add .ini file for rare-resonance MC in pp at 5.36 TeV (#2471) * Adjust LF resonance injection pT and rapidity ranges Use pT 0-15 GeV/c for f0 and 0-12 GeV/c for the remaining resonances. Set rapidity and eta configuration bounds to +/-0.8. Current Rare INIs use rapidity mode, so the eta bounds do not add an eta cut. Validation: both eta settings produce identical particles in a fixed-seed 500-event comparison with the actual O2 generator; all nine species, pT/y bounds, and gap=5 pass. * Add LF resonance injection INI for pp at 5.36 TeV Use the existing 5.36 TeV inelastic Pythia configuration with the shared resonance injection JSON, gap 5, rapidity mode, and the existing resonance decay settings. Validation: INI parsing and tracked references verified; differs from the 13.6 TeV INI only in its two MB configuration paths. * Register CI test for 5.36 TeV LF resonance INI Use the supported INI test redirection to share the Rare generator kinematics test with the 13.6 TeV setup. This fixes the missing-test failure without changing generator settings. Validation: run_generator_tests.sh --keep-artifacts --fail-immediately passed for the 5.36 TeV INI (100-event DPL eventgen, o2-sim --noGeant, generic kinematics, and Rare-specific checks). * Add dedicated CI macro for 5.36 TeV LF resonances Provide GeneratorLF_Resonances_pp5360_Rare.C with the existing Rare injection PDG, decay, and gap sanity checks. Remove the INI test redirection so CI discovers the dedicated macro by filename. Validation: the actual generator CI runner passed with 100 events, including DPL eventgen, o2-sim --noGeant, generic kinematics, and the dedicated External macro. --- .../GeneratorLF_Resonances_pp5360_Rare.ini | 10 ++ .../GeneratorLF_Resonances_pp5360_Rare.C | 112 ++++++++++++++++++ .../generator/resonancelistgun_width_inj.json | 90 +++++++------- 3 files changed, 167 insertions(+), 45 deletions(-) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_Resonances_pp5360_Rare.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp5360_Rare.C diff --git a/MC/config/PWGLF/ini/GeneratorLF_Resonances_pp5360_Rare.ini b/MC/config/PWGLF/ini/GeneratorLF_Resonances_pp5360_Rare.ini new file mode 100644 index 000000000..f39d7ab4f --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_Resonances_pp5360_Rare.ini @@ -0,0 +1,10 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json", true, 5, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonances_width.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp5360_Rare.C b/MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp5360_Rare.C new file mode 100644 index 000000000..b88244335 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_Resonances_pp5360_Rare.C @@ -0,0 +1,112 @@ +// Dedicated kinematics test for Rare resonance injection in pp at 5.36 TeV. + +int External() +{ + const std::string path{"o2sim_Kine.root"}; + const int numberOfGapEvents{4}; // generateLFRapidity(..., gap=5) + const std::vector injectedPDGs = { + 9010221, // f0(980) + 3324, -3324, // Xi(1530)0 and anti-Xi(1530)0 + 123324, -123324, // Xi(1820)0 and anti-Xi(1820)0 + 123314, -123314, // Xi(1820)- and Xi(1820)+ + 123334, -123334 // Omega(2012)- and Omega(2012)+ + }; + const std::vector> decayDaughters = { + {211, -211}, + {3312, 211}, {-3312, -211}, + {3122, 310}, {-3122, 310}, + {3122, -321}, {-3122, 321}, + {3312, 310}, {-3312, 310} + }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + std::vector nSignal(injectedPDGs.size(), 0); + std::vector nNotDecayed(injectedPDGs.size(), 0); + std::vector> nDecays; + for (const auto& daughters : decayDaughters) { + nDecays.emplace_back(daughters.size(), 0); + } + + int numberOfEventsProcessed{0}; + int numberOfEventsProcessedWithoutInjection{0}; + for (Long64_t i = 0; i < tree->GetEntries(); ++i) { + tree->GetEntry(i); + ++numberOfEventsProcessed; + bool hasInjection{false}; + for (const auto& track : *tracks) { + const auto pdg = track.GetPdgCode(); + const auto it = std::find(injectedPDGs.begin(), injectedPDGs.end(), pdg); + if (it == injectedPDGs.end()) { + continue; + } + const auto index = static_cast(std::distance(injectedPDGs.begin(), it)); + ++nSignal[index]; + if (track.getFirstDaughterTrackId() < 0) { + ++nNotDecayed[index]; + continue; + } + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { + const auto pdgDau = tracks->at(j).GetPdgCode(); + bool foundDau{false}; + for (size_t k = 0; k < decayDaughters[index].size(); ++k) { + if (pdgDau == decayDaughters[index][k]) { + ++nDecays[index][k]; + foundDau = true; + hasInjection = true; + break; + } + } + if (!foundDau) { + std::cerr << "Decay daughter not found: " << pdg << " -> " << pdgDau + << " (mother=" << track.getMotherTrackId() + << ", secondMother=" << track.getSecondMotherTrackId() << ")\n"; + } + } + } + if (!hasInjection) { + ++numberOfEventsProcessedWithoutInjection; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << tree->GetEntries() << "\n"; + for (size_t i = 0; i < injectedPDGs.size(); ++i) { + std::cout << "# Mother\n"; + std::cout << injectedPDGs[i] << " generated: " << nSignal[i] + << ", " << nNotDecayed[i] << " did not decay\n"; + for (size_t j = 0; j < decayDaughters[i].size(); ++j) { + std::cout << "# Daughter " << decayDaughters[i][j] << ": " << nDecays[i][j] << "\n"; + } + } + std::cout << "--------------------------------\n"; + std::cout << "Number of events processed: " << numberOfEventsProcessed << "\n"; + std::cout << "Number of input for the gap events: " << numberOfGapEvents << "\n"; + std::cout << "Number of events processed without injection: " + << numberOfEventsProcessedWithoutInjection << "\n"; + const double ratioOfNormalEvents = numberOfEventsProcessed + ? static_cast(numberOfEventsProcessedWithoutInjection) / + numberOfEventsProcessed + : 0.0; + std::cout << "Fraction without injection: " << ratioOfNormalEvents << "\n"; + const double expectedRatio = static_cast(numberOfGapEvents) / (numberOfGapEvents + 1); + std::cout << "Expected fraction for 1+" << numberOfGapEvents << " pattern: " << expectedRatio << "\n"; + + // Same basic gap sanity check as the referenced O2DPG test. + if (ratioOfNormalEvents > 0.90 || ratioOfNormalEvents < 0.70) { + std::cerr << "The number of injected events is too low or too high\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json index 6aa349acb..119411991 100644 --- a/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json +++ b/MC/config/PWGLF/pythia8/generator/resonancelistgun_width_inj.json @@ -3,99 +3,99 @@ "pdg": 9010221, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 15, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "Xi(1530)0" : { "pdg": 3324, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "anti-Xi(1530)0" : { "pdg": -3324, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "Xi(1820)0" : { "pdg": 123324, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "Anti-Xi(1820)0" : { "pdg": -123324, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "Xi(1820)-" : { "pdg": 123314, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "Xi(1820)+" : { "pdg": -123314, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "Omega(2012)-" : { "pdg": 123334, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true }, "Omega(2012)+" : { "pdg": -123334, "n": 1, "ptMin": 0.0, - "ptMax": 20, - "etaMin": -1.2, - "etaMax": 1.2, - "rapidityMin": -1.2, - "rapidityMax": 1.2, + "ptMax": 12, + "etaMin": -0.8, + "etaMax": 0.8, + "rapidityMin": -0.8, + "rapidityMax": 0.8, "genDecayed": true } } From 9b87eb1e1510a59574489b5a7c67729c86f3e0be Mon Sep 17 00:00:00 2001 From: rbailhac Date: Tue, 22 Sep 2026 21:43:17 +0200 Subject: [PATCH 076/144] Change the number of embedded signal in the Pb--Pb collisions (#2467) --- .../external/generator/Generator_pythia8_embed_HFLepton.C | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MC/config/PWGEM/external/generator/Generator_pythia8_embed_HFLepton.C b/MC/config/PWGEM/external/generator/Generator_pythia8_embed_HFLepton.C index fe55587aa..f82693d89 100644 --- a/MC/config/PWGEM/external/generator/Generator_pythia8_embed_HFLepton.C +++ b/MC/config/PWGEM/external/generator/Generator_pythia8_embed_HFLepton.C @@ -92,10 +92,10 @@ public: /// number of events to be embedded in a background event // gen->setFormula("max(1.,120.*(x<5.)+80.*(1.-x/20.)*(x>5.)*(x<11.)+240.*(1.-x/13.)*(x>11.))"); - //mNumSigEvs = static_cast(std::lround(5.0 + 0.886202881 * std::pow(std::max(0.0f, 17.5f - x), 1.7))); - mNumSigEvs = static_cast(std::max(1.,120.*(x<5.)+80.*(1.-x/20.)*(x>5.)*(x<11.)+240.*(1.-x/13.)*(x>11.))); - //mNumSigEvs = 1; - LOG(info) << "[notifyEmbedding] ----- generating " << mNumSigEvs << " signal events " << std::endl; + mNumSigEvs = static_cast(std::lround(5.0 + 0.886202881 * std::pow(std::max(0.0f, 17.5f - x), 1.7))); + //mNumSigEvs = static_cast(std::max(1.,120.*(x<5.)+80.*(1.-x/20.)*(x>5.)*(x<11.)+240.*(1.-x/13.)*(x>11.))); + //mNumSigEvs = 1; + LOG(info) << "[notifyEmbedding] ----- generating " << mNumSigEvs << " signal events " << std::endl; }; protected: From 514cd6664c82f70e90d5bc2d655bd5265462e27d Mon Sep 17 00:00:00 2001 From: sawan <124118453+sawankumawat@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:00:42 +0530 Subject: [PATCH 077/144] [PWGLF] MC for Uncorrelated+Correlated phi-phi pairs for phi-phi resonance study (#2469) * MC for Uncorrelated+Correlated phi-phi pairs for phi-phi resonance study * modified the test script to print relevant info regarding injected particles and their daughters * fixed path error * Rename function to GeneratorLF_phiphiInvMass.C * Renamed to GeneratorLF_phiphiInvMass.ini --- .../PWGLF/ini/GeneratorLF_phiphiInvMass.ini | 10 + .../ini/tests/GeneratorLF_phiphiInvMass.C | 168 +++++++++++++ MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C | 237 ++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C create mode 100644 MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C diff --git a/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini b/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini new file mode 100644 index 000000000..097c63c3e --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini @@ -0,0 +1,10 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C +funcName=generatePhiResonanceGun(999999, 0.0, 50.0, -1.0, 1.0, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", 3) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonances.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C b/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C new file mode 100644 index 000000000..1ae3c2e12 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C @@ -0,0 +1,168 @@ +int External() +{ + const std::string path{"o2sim_Kine.root"}; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Counters + int nResonance999999 = 0; + int nNotDecayed999999 = 0; + int nPhiFromResonance = 0; + + // Decay counts into K+ K- (PDG 321, -321) + int nKPlusFromResonancePhi = 0; + int nKMinusFromResonancePhi = 0; + + int nDirectInjectedPhi = 0; + int nMBPhi = 0; + + int nKPlusFromDirectPhi = 0; + int nKMinusFromDirectPhi = 0; + int nKPlusFromMBPhi = 0; + int nKMinusFromMBPhi = 0; + + int numberOfEventsProcessed = 0; + int numberOfEventsProcessedWithoutInjection = 0; + + for (Long64_t i = 0; i < tree->GetEntries(); ++i) + { + tree->GetEntry(i); + ++numberOfEventsProcessed; + bool hasInjection = false; + + for (size_t idx = 0; idx < tracks->size(); ++idx) + { + const auto &track = tracks->at(idx); + const auto pdg = track.GetPdgCode(); + + // 1. Process Custom Resonance 999999 + if (pdg == 999999) + { + ++nResonance999999; + hasInjection = true; + + if (track.getFirstDaughterTrackId() < 0) + { + ++nNotDecayed999999; + continue; + } + + // Loop through daughters of 999999 (Phi mesons) + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) + { + const auto &phiTrack = tracks->at(j); + if (phiTrack.GetPdgCode() == 333) + { + ++nPhiFromResonance; + + // Check daughters of this Phi (granddaughters of 999999) + if (phiTrack.getFirstDaughterTrackId() >= 0) + { + for (int k = phiTrack.getFirstDaughterTrackId(); k <= phiTrack.getLastDaughterTrackId(); ++k) + { + auto grandDauPdg = tracks->at(k).GetPdgCode(); + if (grandDauPdg == 321) ++nKPlusFromResonancePhi; + if (grandDauPdg == -321) ++nKMinusFromResonancePhi; + } + } + } + } + } + + // 2. Process Phi (333) Mesons + else if (pdg == 333) + { + int motherId = track.getMotherTrackId(); + int motherPdg = (motherId >= 0 && motherId < (int)tracks->size()) ? tracks->at(motherId).GetPdgCode() : 0; + + // Skip Phi from 999999 here as it was handled above + if (motherPdg == 999999) + { + continue; + } + + bool isDirectInjected = (motherId < 0); + + if (isDirectInjected) + { + ++nDirectInjectedPhi; + hasInjection = true; + + if (track.getFirstDaughterTrackId() >= 0) + { + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) + { + auto dauPdg = tracks->at(j).GetPdgCode(); + if (dauPdg == 321) ++nKPlusFromDirectPhi; + if (dauPdg == -321) ++nKMinusFromDirectPhi; + } + } + } + else + { + // Minimum Bias Phi + ++nMBPhi; + + if (track.getFirstDaughterTrackId() >= 0) + { + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) + { + auto dauPdg = tracks->at(j).GetPdgCode(); + if (dauPdg == 321) ++nKPlusFromMBPhi; + if (dauPdg == -321) ++nKMinusFromMBPhi; + } + } + } + } + } + + if (!hasInjection) + { + ++numberOfEventsProcessedWithoutInjection; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "Total Events Processed: " << tree->GetEntries() << "\n\n"; + + std::cout << "--- 1. INJECTED RESONANCE (999999) ---\n"; + std::cout << "Total Injected Resonance 999999: " << nResonance999999 << "\n"; + std::cout << "Resonances not decayed: " << nNotDecayed999999 << "\n"; + std::cout << "Daughter Phi (333) produced from 999999: " << nPhiFromResonance << "\n"; + std::cout << " -> Decayed to K+: " << nKPlusFromResonancePhi << "\n"; + std::cout << " -> Decayed to K-: " << nKMinusFromResonancePhi << "\n\n"; + + std::cout << "--- 2. DIRECTLY INJECTED PHI (333) ---\n"; + std::cout << "Total Directly Injected Phi (333): " << nDirectInjectedPhi << "\n"; + std::cout << " -> Decayed to K+: " << nKPlusFromDirectPhi << "\n"; + std::cout << " -> Decayed to K-: " << nKMinusFromDirectPhi << "\n\n"; + + std::cout << "--- 3. MINIMUM BIAS PHI (333) ---\n"; + std::cout << "Total Minimum Bias Phi (333): " << nMBPhi << "\n"; + std::cout << " -> Decayed to K+: " << nKPlusFromMBPhi << "\n"; + std::cout << " -> Decayed to K-: " << nKMinusFromMBPhi << "\n"; + std::cout << "--------------------------------\n"; + std::cout << "Events processed without signal injection: " << numberOfEventsProcessedWithoutInjection << "\n"; + + return 0; +} + +void GeneratorLF_phiphiInvMass() +{ + External(); +} diff --git a/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C b/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C new file mode 100644 index 000000000..330f0a61b --- /dev/null +++ b/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C @@ -0,0 +1,237 @@ +/// generator_phi_resonance.C +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "FairGenerator.h" +#include "Generators/GeneratorPythia8.h" +#include "Generators/GeneratorPythia8Param.h" +#include "Pythia8/Pythia.h" +#include "TRandom3.h" +#include "TMath.h" +#include "TF1.h" +#include "TParticle.h" +#include "TSystem.h" +#if __has_include("SimulationDataFormat/MCGenStatus.h") +#include "SimulationDataFormat/MCGenStatus.h" +#else +#include "SimulationDataFormat/MCGenProperties.h" +#endif +#if __has_include("SimulationDataFormat/MCUtils.h") +#include "SimulationDataFormat/MCUtils.h" +#endif +#include +#include +#endif + +Double_t FuncLavy(Double_t *x, Double_t *par) +{ + + Double_t p = (par[0] - 1) * (par[0] - 2) * par[1] * x[0] / (((pow((1 + (((sqrt((par[2] * par[2]) + (x[0] * x[0]))) - par[2]) / (par[0] * par[3]))), par[0]) * (par[0] * par[3] * ((par[0] * par[3]) + (par[2] * (par[0] - 2))))))); + return (p); +} + +class GeneratorPhiResonance : public o2::eventgen::GeneratorPythia8 +{ +public: + GeneratorPhiResonance(int resoPDG = 999999, + float ptMin = 0.0, float ptMax = 50.0, + float yMin = -1.0, float yMax = 1.0, + std::string pythiaCfgMb = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", + int signalInterval = 3) + : GeneratorPythia8(), mResoPDG(resoPDG), mPtMin(ptMin), mPtMaxPhiPhi(ptMax), mYMin(yMin), mYMax(yMax), mSignalInterval(signalInterval) + { + // 1. Initialize Gun Pythia object & define custom resonance + // # id::all = name antiName spinType chargeType colType m0 mWidth mMin mMax tau0 + std::string createReso = std::to_string(mResoPDG) + ":new = f2_Custom void 5 0 0 2.714 0.012 2.05 3.50 0.0"; + mPythiaGun.readString(createReso); + mPythiaGun.readString(std::to_string(mResoPDG) + ":mayDecay = on"); + // id:addChannel = onMode bRatio meMode product1 product2, (onMode = 1: allow decay, bRatio = branching ratio, meMode = matrix element mode where 0 is isotropic decay) + std::string addDecay = std::to_string(mResoPDG) + ":addChannel = 1 1.0 0 333 333"; + mPythiaGun.readString(addDecay); + + mPythiaGun.readString("ProcessLevel:all off"); + mPythiaGun.readString("Random:setSeed = on"); + mPythiaGun.readString("Random:seed = " + std::to_string(1 + gRandom->Integer(900000000))); + mPythiaGun.init(); + + // 2. Initialize Minimum Bias Pythia Engine + if (pythiaCfgMb.empty()) + { + auto ¶m = o2::eventgen::GeneratorPythia8Param::Instance(); + pythiaCfgMb = param.config; + } + pythiaCfgMb = gSystem->ExpandPathName(pythiaCfgMb.c_str()); + + if (!pythiaObjectMinimumBias.readFile(pythiaCfgMb)) + { + std::cerr << "Fatal: Could not read MB configuration file: " << pythiaCfgMb << std::endl; + } + pythiaObjectMinimumBias.readString("Random:setSeed = on"); + pythiaObjectMinimumBias.readString("Random:seed = " + std::to_string(1 + gRandom->Integer(900000000))); + + // Add custom particle definition to MB instance so particle table matches + pythiaObjectMinimumBias.readString(createReso); + pythiaObjectMinimumBias.readString(std::to_string(mResoPDG) + ":mayDecay = on"); + pythiaObjectMinimumBias.readString(addDecay); + + pythiaObjectMinimumBias.init(); + + // // Thermal pT distribution for phi-phi resonance + // mThermal = new TF1("mThermal", "x*sqrt(x*x+[0]*[0])*exp(-sqrt(x*x+[0]*[0])/[1])", mPtMin, mPtMaxPhiPhi); + + // Lévy-Tsallis pT distribution for direct phi + mLevyTsallis = new TF1("mLevyTsallis", FuncLavy, mPtMin, 100.0, 4); + + mLevyTsallis->SetParameters( + 7.60279, // n + 0.0374237, // dN/dy + 1.01946, // mass + 0.338379 // T + ); + } + + Bool_t generateEvent() override + { + mEventCounter++; + + // 1. Generate Minimum Bias Background Event + bool mbOK = false; + while (!mbOK) + { + mbOK = pythiaObjectMinimumBias.next(); + } + + // Copy MB event into mPythia + mPythia.event = pythiaObjectMinimumBias.event; + + // 2. Clear Gun event container + mPythiaGun.event.reset(); + + // 3. Inject Signal Gun Particles into mPythiaGun + if (mEventCounter % mSignalInterval == 0) + { + // Theramal distribution + injectParticle(mResoPDG, 1, true); + } + else + { + // From published + injectParticle(333, 2, false); + } + + // 4. Force Decay of injected particles using Pythia's Decayer + for (int i = 1; i < mPythiaGun.event.size(); ++i) + { + if (mPythiaGun.event[i].status() > 0) + { // Active injected particles + mPythiaGun.particleData.mayDecay(mPythiaGun.event[i].id(), true); + mPythiaGun.moreDecays(); + } + } + + // 5. Merge mPythiaGun event into mPythia.event + int offset = mPythia.event.size(); + + for (int i = 1; i < mPythiaGun.event.size(); ++i) + { // Skip system particle 0 + Pythia8::Particle p = mPythiaGun.event[i]; + + // Adjust history indices accurately + int mother1 = (p.mother1() > 0) ? p.mother1() + offset - 1 : p.mother1(); + int mother2 = (p.mother2() > 0) ? p.mother2() + offset - 1 : p.mother2(); + int daughter1 = (p.daughter1() > 0) ? p.daughter1() + offset - 1 : p.daughter1(); + int daughter2 = (p.daughter2() > 0) ? p.daughter2() + offset - 1 : p.daughter2(); + + p.mothers(mother1, mother2); + p.daughters(daughter1, daughter2); + + mPythia.event.append(p); + } + + // 6. CRITICAL: Restore Pythia particleData pointers for O2 exporter + mPythia.event.restorePtrs(); + + // 7. Invoke base generator hooks to sync O2 event record + // return GeneratorPythia8::generateEvent(); + return true; // Skip base generator processing to avoid overwriting injected particles + } + +private: + void injectParticle(int pdg, int nParticles, bool thermalPt) + { + const double phiMass = 1.019461; + + for (int i = 0; i < nParticles; ++i) + { + // const double pt = gRandom->Uniform(mPtMin, mPtMaxPhiPhi); + const double y = gRandom->Uniform(mYMin, mYMax); + const double phi = gRandom->Uniform(0, TMath::TwoPi()); + + double mass = 0.0; + if (pdg == mResoPDG) + { + do + { + mass = gRandom->BreitWigner(2.714, 0.012); + } while (mass <= 2.0 * phiMass || mass < 2.05 || mass > 3.50); + } + else + { + mass = mPythiaGun.particleData.mSel(pdg); + } + + double pt; + + if (thermalPt) + { + // const double T = 0.160; + + // mThermal->SetParameter(0, mass); + // mThermal->SetParameter(1, T); + + // pt = mThermal->GetRandom(); + + pt = gRandom->Uniform(mPtMin, mPtMaxPhiPhi); // Falling back to flat pT due to low statistics in high pT + } + else + { + pt = mLevyTsallis->GetRandom(); + } + + const double px = pt * std::cos(phi); + const double py = pt * std::sin(phi); + const double mT = std::sqrt(mass * mass + pt * pt); + const double pz = mT * std::sinh(y); + const double et = mT * std::cosh(y); + + Pythia8::Particle particle; + particle.id(pdg); + particle.status(11); + particle.m(mass); + particle.px(px); + particle.py(py); + particle.pz(pz); + particle.e(et); + particle.xProd(0.); + particle.yProd(0.); + particle.zProd(0.); + + mPythiaGun.event.append(particle); + } + } + + int mEventCounter = 0; + int mResoPDG; + int mSignalInterval; + float mPtMin, mPtMaxPhiPhi, mYMin, mYMax; + + Pythia8::Pythia mPythiaGun; + Pythia8::Pythia pythiaObjectMinimumBias; + + // TF1 *mThermal; + TF1 *mLevyTsallis; +}; + +/// Entry point for o2-sim +FairGenerator *generatePhiResonanceGun(int resoPDG = 999999, float ptMin = 0.0, float ptMax = 50.0, float yMin = -1.0, float yMax = 1.0, std::string pythiaCfgMb = "", int signalInterval = 3) +{ + return new GeneratorPhiResonance(resoPDG, ptMin, ptMax, yMin, yMax, pythiaCfgMb, signalInterval); +} \ No newline at end of file From 813d35b806c20271692fc303358ef56f8d13defa Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Thu, 24 Sep 2026 17:19:44 +0200 Subject: [PATCH 078/144] Update decay table for b-hadron to open charm MC (#2472) * Update decay table for b-hadron to open charm MC * Update test macro * Fix typo --- ...GeneratorHF_D2H_bbbar_Bforced_gap5_Mode2.C | 13 ++- ...thia8_beautyhadronic_with_decays_Mode2.cfg | 83 +++++-------------- 2 files changed, 27 insertions(+), 69 deletions(-) diff --git a/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_bbbar_Bforced_gap5_Mode2.C b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_bbbar_Bforced_gap5_Mode2.C index 629aa3ae1..7a5fdab70 100644 --- a/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_bbbar_Bforced_gap5_Mode2.C +++ b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_bbbar_Bforced_gap5_Mode2.C @@ -6,17 +6,14 @@ int External() { std::vector checkPdgHadron{411, 421, 431, 4122, 4132, 4232, 4332, 511, 521, 531, 5122}; std::map>> checkHadronDecays{ // sorted pdg of daughters - {411, {{-321, 211, 211}, {-313, 211}, {211, 311}, {211, 333}}}, // D+ + {411, {{-321, 211, 211}, {-313, 211}, {211, 333}}}, // D+ {421, {{-321, 211}, {-321, 111, 211}}}, // D0 {431, {{211, 333}, {-313, 321}}}, // Ds+ - {4122, {{-313, 2212}, {-321, 2224}, {211, 102134}, {-321, 211, 2212}, {311, 2212}}}, // Lc+ - {4132, {{211, 3312}}}, // Xic0 - {4232, {{-313, 2212}, {-321, 3324}, {211, 211, 3312}, {-321, 211, 2212}}}, // Xic+ - {4332, {{211, 3334}}}, // Omegac+ - {511, {{-411, 211}, {-413, 211}, {-411, 213}, {431, -211}}}, // B0 - {521, {{-421, 211}, {-423, 211}, {-421, 213}}}, // B+ + {4122, {{-313, 2212}, {-321, 2224}, {211, 102134}, {-321, 211, 2212}}}, // Lc+ + {511, {{-411, 211}, {-413, 211}, {-411, 213}, {-411, 321}, {-413, 213}, {-413, 321}}}, // B0 + {521, {{-421, 211}, {-423, 211}, {-421, 213}, {-421, 321}}}, // B+ {531, {{-431, 211}, {-433, 211}, {-431, 213}}}, // Bs0 - {5122, {{4122, -211}, {4122, -213}, {4122, 211, -211, -211}, {4212, -211}}} // Lb0 + {5122, {{4122, -211}, {4122, -213}, {14122, -211}, {4124, -211}, {4212, -211}}} // Lb0 }; TFile file(path.c_str(), "READ"); diff --git a/MC/config/PWGHF/pythia8/generator/pythia8_beautyhadronic_with_decays_Mode2.cfg b/MC/config/PWGHF/pythia8/generator/pythia8_beautyhadronic_with_decays_Mode2.cfg index 3ac775eb6..0cb1bbaf5 100644 --- a/MC/config/PWGHF/pythia8/generator/pythia8_beautyhadronic_with_decays_Mode2.cfg +++ b/MC/config/PWGHF/pythia8/generator/pythia8_beautyhadronic_with_decays_Mode2.cfg @@ -48,24 +48,12 @@ BeamRemnants:saturation 5 ### add D+ decays absent in PYTHIA8 decay table and set BRs from PDG for other 411:oneChannel = 1 0.0752 0 -321 211 211 411:addChannel = 1 0.0104 0 -313 211 -411:addChannel = 1 0.0156 0 311 211 -411:addChannel = 1 0.0752 0 333 211 # to have the same amount of D+->KKpi and D+->Kpipi +411:addChannel = 1 0.0100 0 333 211 # to have the same amount of D+->KKpi and D+->Kpipi ## add Lc decays absent in PYTHIA8 decay table and set BRs from PDG for other 4122:oneChannel = 1 0.0196 100 2212 -313 4122:addChannel = 1 0.0108 100 2224 -321 4122:addChannel = 1 0.022 100 102134 211 4122:addChannel = 1 0.035 0 2212 -321 211 -4122:addChannel = 1 0.0159 0 2212 311 -### add Xic+ decays absent in PYTHIA8 decay table -4232:addChannel = 1 0.2 0 2212 -313 -4232:addChannel = 1 0.2 0 2212 -321 211 -4232:addChannel = 1 0.2 0 3324 211 -4232:addChannel = 1 0.2 0 3312 211 211 -### add Xic0 decays absent in PYTHIA8 decay table -4132:addChannel = 1 0.0143 0 3312 211 -### add OmegaC decays absent in PYTHIA8 decay table -4332:addChannel = 1 0.5 0 3334 211 -4332:addChannel = 1 0.5 0 3312 211 ### K* -> K pi 313:onMode = off @@ -82,25 +70,12 @@ BeamRemnants:saturation 5 ### for Lambda_c -> Lambda(1520) K- 102134:onMode = off 102134:onIfAll = 2212 321 -### for Xic0 -> pi Xi -> pi pi Lambda -> pi pi pi p -### and Omega_c -> pi Xi -> pi pi Lambda -> pi pi pi p -3312:onMode = off -3312:onIfAll = 3122 -211 -3122:onMode = off -3122:onIfAll = 2212 -211 -### for Omega_c -> pi Omega -> pi K Lambda -> pi K pi p -3334:onMode = off -3334:onIfAll = 3122 -321 ### switch off all decay channels 411:onMode = off 421:onMode = off 431:onMode = off 4122:onMode = off -4232:onMode = off -4132:onMode = off -443:onMode = off -4332:onMode = off 511:onMode = off 521:onMode = off 531:onMode = off @@ -131,60 +106,46 @@ BeamRemnants:saturation 5 4122:onIfMatch = 102134 211 ### Lambda_c -> p K pi 4122:onIfMatch = 2212 321 211 -### Lambda_c -> pK0s -4122:onIfMatch = 2212 311 - -### Xic+ -> pK*0 -4232:onIfMatch = 2212 313 -### Xic+ -> p K- pi+ -4232:onIfMatch = 2212 321 211 -### Xic+ -> Xi*0 pi+, Xi*->Xi- pi+ -4232:onIfMatch = 3324 211 -### Xic+ -> Xi- pi+ pi+ -4232:onIfMatch = 3312 211 211 - -### Xic0 -> Xi- pi+ -4132:onIfMatch = 3312 211 - -### Omega_c -> Omega pi -4332:onIfMatch = 3334 211 -### Omega_c -> Xi pi -4332:onIfMatch = 3312 211 ### Force also golden beauty hadrons decay modes for D2H studies ### add B0 decays -511:oneChannel = 1 0.4 0 -411 211 -511:addChannel = 1 0.3 0 -413 211 -511:addChannel = 1 0.2 0 431 -211 +511:oneChannel = 1 0.35 0 -411 211 +511:addChannel = 1 0.35 0 -413 211 ### add B+ decays -521:oneChannel = 1 0.8 0 -421 211 +521:oneChannel = 1 0.7 0 -421 211 ### add Bs0 decays -531:oneChannel = 1 0.8 0 -431 211 +531:oneChannel = 1 0.7 0 -431 211 ### add Lb decays -5122:oneChannel = 1 0.8 0 4122 -211 +5122:oneChannel = 1 0.6 0 4122 -211 ### we also add channels useful for studies of partly reconstructed decays / correlated backgrounds ### add B0 decays 511:addChannel = 1 0.1 0 -411 213 +511:addChannel = 1 0.1 0 -413 213 +511:addChannel = 1 0.05 0 -411 321 +511:addChannel = 1 0.05 0 -413 321 ### add B+ decays 521:addChannel = 1 0.1 0 -421 213 521:addChannel = 1 0.1 0 -423 211 +521:addChannel = 1 0.1 0 -421 321 ### add Bs0 decays 531:addChannel = 1 0.1 0 -431 213 531:addChannel = 1 0.1 0 -433 211 +531:addChannel = 1 0.1 0 -433 321 ### add Lb decays -5122:addChannel = 1 0.1 0 4122 -213 -5122:addChannel = 1 0.05 0 4122 211 -211 -211 -5122:addChannel = 1 0.05 0 4212 -211 +5122:addChannel = 1 0.10 0 14122 -211 +5122:addChannel = 1 0.10 0 4124 -211 +5122:addChannel = 1 0.10 0 4212 -211 +5122:addChannel = 1 0.10 0 4122 -321 ### B0 -> D pi 511:onIfMatch = 411 211 ### B0 -> D* pi 511:onIfMatch = 413 211 -### B0 -> Ds pi -511:onIfMatch = 431 211 ### B0 -> D rho 511:onIfMatch = 411 213 +### B0 -> D rho +511:onIfMatch = 411 321 ### B+ -> D0 pi 521:onIfMatch = 421 211 @@ -202,9 +163,9 @@ BeamRemnants:saturation 5 ### Lb -> Lc pi 5122:onIfMatch = 4122 211 -### Lb -> Lc rho -5122:onIfMatch = 4122 213 -### Lb -> Lc pi pi pi -5122:onIfMatch = 4122 211 211 211 -### Lb -> Sc pi +### Lb -> Lc(2595)+ pi +5122:onIfMatch = 14122 211 +### Lb -> Lc(2625)+ pi +5122:onIfMatch = 4124 211 +### Lb -> Sc(2455)+ pi 5122:onIfMatch = 4212 211 From 4b2ac040b1fbe8be50ac23d190a5402dfe0d0876 Mon Sep 17 00:00:00 2001 From: sawan <124118453+sawankumawat@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:42:05 +0530 Subject: [PATCH 079/144] Flat pt for phi meson (#2473) --- MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C | 33 ++++++++++---------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C b/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C index 330f0a61b..e8f7dad5e 100644 --- a/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C +++ b/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C @@ -6,7 +6,7 @@ #include "Pythia8/Pythia.h" #include "TRandom3.h" #include "TMath.h" -#include "TF1.h" +// #include "TF1.h" #include "TParticle.h" #include "TSystem.h" #if __has_include("SimulationDataFormat/MCGenStatus.h") @@ -21,12 +21,12 @@ #include #endif -Double_t FuncLavy(Double_t *x, Double_t *par) -{ +// Double_t FuncLavy(Double_t *x, Double_t *par) +// { - Double_t p = (par[0] - 1) * (par[0] - 2) * par[1] * x[0] / (((pow((1 + (((sqrt((par[2] * par[2]) + (x[0] * x[0]))) - par[2]) / (par[0] * par[3]))), par[0]) * (par[0] * par[3] * ((par[0] * par[3]) + (par[2] * (par[0] - 2))))))); - return (p); -} +// Double_t p = (par[0] - 1) * (par[0] - 2) * par[1] * x[0] / (((pow((1 + (((sqrt((par[2] * par[2]) + (x[0] * x[0]))) - par[2]) / (par[0] * par[3]))), par[0]) * (par[0] * par[3] * ((par[0] * par[3]) + (par[2] * (par[0] - 2))))))); +// return (p); +// } class GeneratorPhiResonance : public o2::eventgen::GeneratorPythia8 { @@ -77,15 +77,15 @@ public: // // Thermal pT distribution for phi-phi resonance // mThermal = new TF1("mThermal", "x*sqrt(x*x+[0]*[0])*exp(-sqrt(x*x+[0]*[0])/[1])", mPtMin, mPtMaxPhiPhi); - // Lévy-Tsallis pT distribution for direct phi - mLevyTsallis = new TF1("mLevyTsallis", FuncLavy, mPtMin, 100.0, 4); + // // Lévy-Tsallis pT distribution for direct phi + // mLevyTsallis = new TF1("mLevyTsallis", FuncLavy, mPtMin, 100.0, 4); - mLevyTsallis->SetParameters( - 7.60279, // n - 0.0374237, // dN/dy - 1.01946, // mass - 0.338379 // T - ); + // mLevyTsallis->SetParameters( + // 7.60279, // n + // 0.0374237, // dN/dy + // 1.01946, // mass + // 0.338379 // T + // ); } Bool_t generateEvent() override @@ -193,7 +193,8 @@ private: } else { - pt = mLevyTsallis->GetRandom(); + // pt = mLevyTsallis->GetRandom(); + pt = gRandom->Uniform(mPtMin, 100.0); // Falling back to flat pT due to low statistics in high pT } const double px = pt * std::cos(phi); @@ -227,7 +228,7 @@ private: Pythia8::Pythia pythiaObjectMinimumBias; // TF1 *mThermal; - TF1 *mLevyTsallis; + // TF1 *mLevyTsallis; }; /// Entry point for o2-sim From 768e660f3e61ebdc6f4b1b9371a080daee0ee7b2 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Fri, 25 Sep 2026 10:41:41 +0200 Subject: [PATCH 080/144] Replace simple string search with regular expression When adding -colBkg the simple search was wrongly returning true --- MC/bin/o2dpg_sim_workflow_anchored.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MC/bin/o2dpg_sim_workflow_anchored.py b/MC/bin/o2dpg_sim_workflow_anchored.py index c8a815598..2ece8ad7f 100755 --- a/MC/bin/o2dpg_sim_workflow_anchored.py +++ b/MC/bin/o2dpg_sim_workflow_anchored.py @@ -742,7 +742,7 @@ def main(): forwardargs += " -tf " + str(args.tf) + " --sor " + str(effective_run_start) + " --timestamp " + str(timestamp) + " --production-offset " + str(prod_offset) + " -run " + str(args.run_number) + " --run-anchored --first-orbit " \ + str(effective_first_orbit) + " --orbitsPerTF " + str(GLOparams["OrbitsPerTF"]) + str(energyarg) # the following options can be overwritten/influenced from the outside - if not '-col' in forwardargs: + if not re.search(r'(? Date: Sat, 26 Sep 2026 23:10:36 +0200 Subject: [PATCH 081/144] Update generator_pythia8_NonPromptSignals_gaptriggered_dq.C (#2475) Changed the decay table of GeneratorBplusToJpsiKaon_EvtGen to a version that includes the chic and psi(2S) decay channels. Needed for the correlated background in B->jpsiK in pp analysis --- .../generator_pythia8_NonPromptSignals_gaptriggered_dq.C | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_gaptriggered_dq.C b/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_gaptriggered_dq.C index b2050175b..9f21d5b28 100644 --- a/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_gaptriggered_dq.C +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_NonPromptSignals_gaptriggered_dq.C @@ -259,7 +259,8 @@ FairGenerator* TString pathO2 = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen"); //gen->SetDecayTable(Form("%s/BPLUSTOKAONJPSITOELE.DEC", pathO2.Data())); //gen->SetDecayTable(Form("%s/BPLUSTOKAONJPSITOELEALLMODES.DEC", pathO2.Data())); // decay table including decay modes for correlated background - gen->SetDecayTable(Form("%s/BTOJPSITOELE.DEC", pathO2.Data())); // decay table including decay modes for correlated background + //gen->SetDecayTable(Form("%s/BTOJPSITOELE.DEC", pathO2.Data())); // decay table including decay modes for correlated background + gen->SetDecayTable(Form("%s/BTOPSIJPSITODIELECTRON.DEC", pathO2.Data())); // decay table including decay modes for correlated background and jpsi feeddown // print debug // gen->PrintDebug(); // set random seed From ea6d40d3f61e14cfaf2466780b4509f1c0d89e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Jacazio?= Date: Sun, 27 Sep 2026 17:01:42 +0200 Subject: [PATCH 082/144] Add DittoMC (#2464) * Add DittoMC * Update ditto_pp_136tev.ini * Add test * Update generator_DittoMC.C * Update ditto_pp_136tev.ini --- MC/config/ALICE3/ini/ditto_pp_136tev.ini | 3 + MC/config/ALICE3/ini/tests/ditto_pp_136tev.C | 1 + MC/config/ALICE3/pythia8/generator_DittoMC.C | 64 ++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 MC/config/ALICE3/ini/ditto_pp_136tev.ini create mode 120000 MC/config/ALICE3/ini/tests/ditto_pp_136tev.C create mode 100644 MC/config/ALICE3/pythia8/generator_DittoMC.C diff --git a/MC/config/ALICE3/ini/ditto_pp_136tev.ini b/MC/config/ALICE3/ini/ditto_pp_136tev.ini new file mode 100644 index 000000000..e44eb26f7 --- /dev/null +++ b/MC/config/ALICE3/ini/ditto_pp_136tev.ini @@ -0,0 +1,3 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C +funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.0/Ditto_tune_pythia8_inel_136tev.root") diff --git a/MC/config/ALICE3/ini/tests/ditto_pp_136tev.C b/MC/config/ALICE3/ini/tests/ditto_pp_136tev.C new file mode 120000 index 000000000..4103e578f --- /dev/null +++ b/MC/config/ALICE3/ini/tests/ditto_pp_136tev.C @@ -0,0 +1 @@ +pythia8_pp_ropes.C \ No newline at end of file diff --git a/MC/config/ALICE3/pythia8/generator_DittoMC.C b/MC/config/ALICE3/pythia8/generator_DittoMC.C new file mode 100644 index 000000000..27fd8b66d --- /dev/null +++ b/MC/config/ALICE3/pythia8/generator_DittoMC.C @@ -0,0 +1,64 @@ +#include +#include + +#include + +#include +#include +#include +#include + +// Ditto event generator +class GeneratorDittoMC : public o2::eventgen::GeneratorPythia8 +{ + public: + /// Constructor + explicit GeneratorDittoMC(const TString& tuneFile) : mGenerator(makeConfig(tuneFile)) + { + // We use the Pythia8 event only as the container passed to the O2 + // GeneratorPythia8 machinery. Particle production is handled by Ditto. + mPythia.readString("ProcessLevel:all = off"); + } + + /// Generate one Ditto event and export it to the Pythia8 event record. + bool generateEvent() override + { + mGenerator.generate(); + mGenerator.loadParticles(mPythia.event, true); + return true; + } + + /// Destructor + ~GeneratorDittoMC() override = default; + + private: + /// Build the Ditto runtime configuration. + static Ditto::Config makeConfig(const TString& tuneFile) + { + if (tuneFile.IsNull()) { + throw std::runtime_error("Ditto tune file must be specified"); + } + + Ditto::Config config; + config.mTuneFile = tuneFile.Data(); + + // Use the Grid process ID as the Ditto seed when available. + // Outside the Grid, seed 0 is used. + if (const char* alienProcId = std::getenv("ALIEN_PROC_ID")) { + config.mSeed = static_cast(std::strtoull(alienProcId, nullptr, 10)); + } else { + config.mSeed = static_cast(std::time(nullptr)); + } + + Printf("Using Ditto tune: %s and seed: %llu", config.mTuneFile.c_str(), static_cast(config.mSeed)); + + return config; + } + + Ditto::Generator mGenerator; +}; + +FairGenerator* generator_DittoMC(const TString& tuneFile) +{ + return new GeneratorDittoMC(tuneFile); +} From be360c34711c698c71ce2fbe974d0b4bc6107c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Jacazio?= Date: Mon, 28 Sep 2026 18:05:34 +0200 Subject: [PATCH 083/144] Add dittoMC configurations for pbpb at 5.36 TeV (#2478) --- MC/config/ALICE3/ini/ditto_pbpb_536tev.ini | 3 +++ MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C | 1 + 2 files changed, 4 insertions(+) create mode 100644 MC/config/ALICE3/ini/ditto_pbpb_536tev.ini create mode 120000 MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C diff --git a/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini b/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini new file mode 100644 index 000000000..219f1ad36 --- /dev/null +++ b/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini @@ -0,0 +1,3 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C +funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.0/Ditto_tune_pythia8_PbPb_536tev.root") diff --git a/MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C b/MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C new file mode 120000 index 000000000..7785297db --- /dev/null +++ b/MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C @@ -0,0 +1 @@ +ditto_pp_136tev.C \ No newline at end of file From 2cc0f9a9e6212b803a64d0721b904f0eba6e90c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Jacazio?= Date: Mon, 28 Sep 2026 18:55:38 +0200 Subject: [PATCH 084/144] Cleanup test macros (#2480) --- MC/config/ALICE3/ini/ditto_pbpb_536tev.ini | 1 + MC/config/ALICE3/ini/ditto_pp_136tev.ini | 1 + MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini | 1 + MC/config/ALICE3/ini/pythia8_pp_clr-blc2_13tev.ini | 1 + MC/config/ALICE3/ini/pythia8_pp_clr-blc3_13tev.ini | 1 + MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C | 1 - MC/config/ALICE3/ini/tests/ditto_pp_136tev.C | 1 - MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C | 1 - MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc2_13tev.C | 1 - MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc3_13tev.C | 1 - 10 files changed, 5 insertions(+), 5 deletions(-) delete mode 120000 MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C delete mode 120000 MC/config/ALICE3/ini/tests/ditto_pp_136tev.C delete mode 120000 MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C delete mode 120000 MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc2_13tev.C delete mode 120000 MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc3_13tev.C diff --git a/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini b/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini index 219f1ad36..301eed05f 100644 --- a/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini +++ b/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini @@ -1,3 +1,4 @@ +#---> pythia8_pp_136tev [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.0/Ditto_tune_pythia8_PbPb_536tev.root") diff --git a/MC/config/ALICE3/ini/ditto_pp_136tev.ini b/MC/config/ALICE3/ini/ditto_pp_136tev.ini index e44eb26f7..39b752dc3 100644 --- a/MC/config/ALICE3/ini/ditto_pp_136tev.ini +++ b/MC/config/ALICE3/ini/ditto_pp_136tev.ini @@ -1,3 +1,4 @@ +#---> pythia8_pp_136tev [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.0/Ditto_tune_pythia8_inel_136tev.root") diff --git a/MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini b/MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini index f1b57ecf1..564329d38 100644 --- a/MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini +++ b/MC/config/ALICE3/ini/pythia8_pp_136tev_inel_difftuned.ini @@ -1,3 +1,4 @@ +#---> pythia8_pp_136tev [Diamond] width[2]=6.0 diff --git a/MC/config/ALICE3/ini/pythia8_pp_clr-blc2_13tev.ini b/MC/config/ALICE3/ini/pythia8_pp_clr-blc2_13tev.ini index 25474d61c..6572e3d11 100644 --- a/MC/config/ALICE3/ini/pythia8_pp_clr-blc2_13tev.ini +++ b/MC/config/ALICE3/ini/pythia8_pp_clr-blc2_13tev.ini @@ -1,3 +1,4 @@ +#---> pythia8_pp_clr-blc0_13tev [Diamond] width[2]=6.0 diff --git a/MC/config/ALICE3/ini/pythia8_pp_clr-blc3_13tev.ini b/MC/config/ALICE3/ini/pythia8_pp_clr-blc3_13tev.ini index 00ee82a3f..6ebad106e 100644 --- a/MC/config/ALICE3/ini/pythia8_pp_clr-blc3_13tev.ini +++ b/MC/config/ALICE3/ini/pythia8_pp_clr-blc3_13tev.ini @@ -1,3 +1,4 @@ +#---> pythia8_pp_clr-blc0_13tev [Diamond] width[2]=6.0 diff --git a/MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C b/MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C deleted file mode 120000 index 7785297db..000000000 --- a/MC/config/ALICE3/ini/tests/ditto_pbpb_536tev.C +++ /dev/null @@ -1 +0,0 @@ -ditto_pp_136tev.C \ No newline at end of file diff --git a/MC/config/ALICE3/ini/tests/ditto_pp_136tev.C b/MC/config/ALICE3/ini/tests/ditto_pp_136tev.C deleted file mode 120000 index 4103e578f..000000000 --- a/MC/config/ALICE3/ini/tests/ditto_pp_136tev.C +++ /dev/null @@ -1 +0,0 @@ -pythia8_pp_ropes.C \ No newline at end of file diff --git a/MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C b/MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C deleted file mode 120000 index 860755581..000000000 --- a/MC/config/ALICE3/ini/tests/pythia8_pp_136tev_inel_difftuned.C +++ /dev/null @@ -1 +0,0 @@ -pythia8_pp_136tev.C \ No newline at end of file diff --git a/MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc2_13tev.C b/MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc2_13tev.C deleted file mode 120000 index fbe2c4ed8..000000000 --- a/MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc2_13tev.C +++ /dev/null @@ -1 +0,0 @@ -pythia8_pp_clr-blc0_13tev.C \ No newline at end of file diff --git a/MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc3_13tev.C b/MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc3_13tev.C deleted file mode 120000 index fbe2c4ed8..000000000 --- a/MC/config/ALICE3/ini/tests/pythia8_pp_clr-blc3_13tev.C +++ /dev/null @@ -1 +0,0 @@ -pythia8_pp_clr-blc0_13tev.C \ No newline at end of file From e956baeca35aeab877e617e878d83aec80964f94 Mon Sep 17 00:00:00 2001 From: sawan <124118453+sawankumawat@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:05:07 +0530 Subject: [PATCH 085/144] [PWGLF] Appended the injected event in MB event correctly (#2481) * Appended the injected event in MB correctly * Made phi max pT configurable * Custom phi injected resonance --- .../PWGLF/ini/GeneratorLF_phiphiInvMass.ini | 2 +- .../ini/tests/GeneratorLF_phiphiInvMass.C | 60 +++---- MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C | 155 +++++++++--------- 3 files changed, 105 insertions(+), 112 deletions(-) diff --git a/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini b/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini index 097c63c3e..d9ad4041c 100644 --- a/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini +++ b/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini @@ -1,6 +1,6 @@ [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C -funcName=generatePhiResonanceGun(999999, 0.0, 50.0, -1.0, 1.0, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", 3) +funcName=generatePhiResonanceGun(999999, 888888, 0.0, 50.0, 100.0, -1.0, 1.0, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", 3) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C b/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C index 1ae3c2e12..4f08d32f8 100644 --- a/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C @@ -76,14 +76,34 @@ int External() for (int k = phiTrack.getFirstDaughterTrackId(); k <= phiTrack.getLastDaughterTrackId(); ++k) { auto grandDauPdg = tracks->at(k).GetPdgCode(); - if (grandDauPdg == 321) ++nKPlusFromResonancePhi; - if (grandDauPdg == -321) ++nKMinusFromResonancePhi; + if (grandDauPdg == 321) + ++nKPlusFromResonancePhi; + if (grandDauPdg == -321) + ++nKMinusFromResonancePhi; } } } } } + else if (pdg == 888888) + { + ++nDirectInjectedPhi; + hasInjection = true; + + if (track.getFirstDaughterTrackId() >= 0) + { + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) + { + auto dauPdg = tracks->at(j).GetPdgCode(); + if (dauPdg == 321) + ++nKPlusFromDirectPhi; + if (dauPdg == -321) + ++nKMinusFromDirectPhi; + } + } + } + // 2. Process Phi (333) Mesons else if (pdg == 333) { @@ -96,36 +116,18 @@ int External() continue; } - bool isDirectInjected = (motherId < 0); + // Minimum Bias Phi + ++nMBPhi; - if (isDirectInjected) + if (track.getFirstDaughterTrackId() >= 0) { - ++nDirectInjectedPhi; - hasInjection = true; - - if (track.getFirstDaughterTrackId() >= 0) + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { - for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) - { - auto dauPdg = tracks->at(j).GetPdgCode(); - if (dauPdg == 321) ++nKPlusFromDirectPhi; - if (dauPdg == -321) ++nKMinusFromDirectPhi; - } - } - } - else - { - // Minimum Bias Phi - ++nMBPhi; - - if (track.getFirstDaughterTrackId() >= 0) - { - for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) - { - auto dauPdg = tracks->at(j).GetPdgCode(); - if (dauPdg == 321) ++nKPlusFromMBPhi; - if (dauPdg == -321) ++nKMinusFromMBPhi; - } + auto dauPdg = tracks->at(j).GetPdgCode(); + if (dauPdg == 321) + ++nKPlusFromMBPhi; + if (dauPdg == -321) + ++nKMinusFromMBPhi; } } } diff --git a/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C b/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C index e8f7dad5e..8537ebd3a 100644 --- a/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C +++ b/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C @@ -6,7 +6,6 @@ #include "Pythia8/Pythia.h" #include "TRandom3.h" #include "TMath.h" -// #include "TF1.h" #include "TParticle.h" #include "TSystem.h" #if __has_include("SimulationDataFormat/MCGenStatus.h") @@ -19,40 +18,51 @@ #endif #include #include +#include #endif -// Double_t FuncLavy(Double_t *x, Double_t *par) -// { - -// Double_t p = (par[0] - 1) * (par[0] - 2) * par[1] * x[0] / (((pow((1 + (((sqrt((par[2] * par[2]) + (x[0] * x[0]))) - par[2]) / (par[0] * par[3]))), par[0]) * (par[0] * par[3] * ((par[0] * par[3]) + (par[2] * (par[0] - 2))))))); -// return (p); -// } - class GeneratorPhiResonance : public o2::eventgen::GeneratorPythia8 { public: GeneratorPhiResonance(int resoPDG = 999999, - float ptMin = 0.0, float ptMax = 50.0, + int customPhiPDG = 888888, + float ptMin = 0.0, float ptMax = 50.0, float ptMaxPhi = 100.0, float yMin = -1.0, float yMax = 1.0, std::string pythiaCfgMb = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", int signalInterval = 3) - : GeneratorPythia8(), mResoPDG(resoPDG), mPtMin(ptMin), mPtMaxPhiPhi(ptMax), mYMin(yMin), mYMax(yMax), mSignalInterval(signalInterval) + : GeneratorPythia8(), mResoPDG(resoPDG), mCustomPhiPDG(customPhiPDG), mPtMin(ptMin), mPtMaxPhiPhi(ptMax), mPtMaxPhi(ptMaxPhi), mYMin(yMin), mYMax(yMax), mSignalInterval(signalInterval) { - // 1. Initialize Gun Pythia object & define custom resonance - // # id::all = name antiName spinType chargeType colType m0 mWidth mMin mMax tau0 + // 1. Define Custom Directly Injected Phi (PDG: 888888) with mass, width, and decay to kaons + std::string createCustomPhi = std::to_string(mCustomPhiPDG) + ":new = custom_phi custom_phi 3 0 0 1.019461 0.004249 0.980 1.100 0.0"; + std::string customPhiMayDecay = std::to_string(mCustomPhiPDG) + ":mayDecay = on"; + std::string addPhiDecayKPlusKMinus = std::to_string(mCustomPhiPDG) + ":addChannel = 1 0.492 0 321 -321"; + + // 2. Define Custom Signal Resonance (PDG: 999999) decay into standard Phis (333 333) std::string createReso = std::to_string(mResoPDG) + ":new = f2_Custom void 5 0 0 2.714 0.012 2.05 3.50 0.0"; - mPythiaGun.readString(createReso); - mPythiaGun.readString(std::to_string(mResoPDG) + ":mayDecay = on"); - // id:addChannel = onMode bRatio meMode product1 product2, (onMode = 1: allow decay, bRatio = branching ratio, meMode = matrix element mode where 0 is isotropic decay) - std::string addDecay = std::to_string(mResoPDG) + ":addChannel = 1 1.0 0 333 333"; - mPythiaGun.readString(addDecay); + std::string resoMayDecay = std::to_string(mResoPDG) + ":mayDecay = on"; + std::string addResoDecay = std::to_string(mResoPDG) + ":addChannel = 1 1.0 0 333 333"; + + // Helper lambda to load custom particle definitions across ALL Pythia engines + auto applyCustomParticles = [&](Pythia8::Pythia &pythiaInst) + { + pythiaInst.readString(createCustomPhi); + pythiaInst.readString(customPhiMayDecay); + pythiaInst.readString(addPhiDecayKPlusKMinus); + pythiaInst.readString(createReso); + pythiaInst.readString(resoMayDecay); + pythiaInst.readString(addResoDecay); + }; + + // 1: Apply particle definitions to mPythia, mPythiaGun, and pythiaObjectMinimumBias + applyCustomParticles(mPythia); + applyCustomParticles(mPythiaGun); mPythiaGun.readString("ProcessLevel:all off"); mPythiaGun.readString("Random:setSeed = on"); mPythiaGun.readString("Random:seed = " + std::to_string(1 + gRandom->Integer(900000000))); mPythiaGun.init(); - // 2. Initialize Minimum Bias Pythia Engine + // 3. Initialize Minimum Bias Pythia Engine if (pythiaCfgMb.empty()) { auto ¶m = o2::eventgen::GeneratorPythia8Param::Instance(); @@ -67,25 +77,8 @@ public: pythiaObjectMinimumBias.readString("Random:setSeed = on"); pythiaObjectMinimumBias.readString("Random:seed = " + std::to_string(1 + gRandom->Integer(900000000))); - // Add custom particle definition to MB instance so particle table matches - pythiaObjectMinimumBias.readString(createReso); - pythiaObjectMinimumBias.readString(std::to_string(mResoPDG) + ":mayDecay = on"); - pythiaObjectMinimumBias.readString(addDecay); - + applyCustomParticles(pythiaObjectMinimumBias); pythiaObjectMinimumBias.init(); - - // // Thermal pT distribution for phi-phi resonance - // mThermal = new TF1("mThermal", "x*sqrt(x*x+[0]*[0])*exp(-sqrt(x*x+[0]*[0])/[1])", mPtMin, mPtMaxPhiPhi); - - // // Lévy-Tsallis pT distribution for direct phi - // mLevyTsallis = new TF1("mLevyTsallis", FuncLavy, mPtMin, 100.0, 4); - - // mLevyTsallis->SetParameters( - // 7.60279, // n - // 0.0374237, // dN/dy - // 1.01946, // mass - // 0.338379 // T - // ); } Bool_t generateEvent() override @@ -99,8 +92,8 @@ public: mbOK = pythiaObjectMinimumBias.next(); } - // Copy MB event into mPythia - mPythia.event = pythiaObjectMinimumBias.event; + // 2: Copy MB event using exact pointer binding from generator_pythia8_LF_rapidity_width.C + copyMinimumBiasEventForInjection(); // 2. Clear Gun event container mPythiaGun.event.reset(); @@ -108,60 +101,68 @@ public: // 3. Inject Signal Gun Particles into mPythiaGun if (mEventCounter % mSignalInterval == 0) { - // Theramal distribution + // Resonant signal -> Decays into 333 333 (Standard Phis) injectParticle(mResoPDG, 1, true); } else { - // From published - injectParticle(333, 2, false); + // Directly injected uncorrelated Phi -> Uses Custom PDG 888888 + injectParticle(mCustomPhiPDG, 2, false); } // 4. Force Decay of injected particles using Pythia's Decayer - for (int i = 1; i < mPythiaGun.event.size(); ++i) - { - if (mPythiaGun.event[i].status() > 0) - { // Active injected particles - mPythiaGun.particleData.mayDecay(mPythiaGun.event[i].id(), true); - mPythiaGun.moreDecays(); - } - } + mPythiaGun.moreDecays(); + mPythiaGun.next(); - // 5. Merge mPythiaGun event into mPythia.event + // 3: Index Mapping during Event Merging int offset = mPythia.event.size(); + std::vector indexMap(mPythiaGun.event.size(), 0); for (int i = 1; i < mPythiaGun.event.size(); ++i) - { // Skip system particle 0 + { + indexMap[i] = mPythia.event.size(); Pythia8::Particle p = mPythiaGun.event[i]; + mPythia.event.append(p); + } - // Adjust history indices accurately - int mother1 = (p.mother1() > 0) ? p.mother1() + offset - 1 : p.mother1(); - int mother2 = (p.mother2() > 0) ? p.mother2() + offset - 1 : p.mother2(); - int daughter1 = (p.daughter1() > 0) ? p.daughter1() + offset - 1 : p.daughter1(); - int daughter2 = (p.daughter2() > 0) ? p.daughter2() + offset - 1 : p.daughter2(); + // Re-link mother and daughter index relationships accurately + for (int i = 1; i < mPythiaGun.event.size(); ++i) + { + int newIdx = indexMap[i]; + Pythia8::Particle &p = mPythia.event[newIdx]; - p.mothers(mother1, mother2); - p.daughters(daughter1, daughter2); + int m1 = p.mother1(); + int m2 = p.mother2(); + int d1 = p.daughter1(); + int d2 = p.daughter2(); - mPythia.event.append(p); + p.mothers((m1 > 0 && m1 < (int)indexMap.size()) ? indexMap[m1] : 0, + (m2 > 0 && m2 < (int)indexMap.size()) ? indexMap[m2] : 0); + + p.daughters((d1 > 0 && d1 < (int)indexMap.size()) ? indexMap[d1] : 0, + (d2 > 0 && d2 < (int)indexMap.size()) ? indexMap[d2] : 0); } - // 6. CRITICAL: Restore Pythia particleData pointers for O2 exporter + // 4: Restore Pythia particleData pointers for O2 exporter mPythia.event.restorePtrs(); - // 7. Invoke base generator hooks to sync O2 event record - // return GeneratorPythia8::generateEvent(); - return true; // Skip base generator processing to avoid overwriting injected particles + return true; } private: + void copyMinimumBiasEventForInjection() + { + mPythia.event = pythiaObjectMinimumBias.event; + mPythia.event.init("Minimum-bias event with injected particles", &mPythia.particleData); + mPythia.event.restorePtrs(); + } + void injectParticle(int pdg, int nParticles, bool thermalPt) { const double phiMass = 1.019461; for (int i = 0; i < nParticles; ++i) { - // const double pt = gRandom->Uniform(mPtMin, mPtMaxPhiPhi); const double y = gRandom->Uniform(mYMin, mYMax); const double phi = gRandom->Uniform(0, TMath::TwoPi()); @@ -175,26 +176,17 @@ private: } else { - mass = mPythiaGun.particleData.mSel(pdg); + mass = mPythiaGun.particleData.mSel(333); // Use standard phi mass for directly injected custom phi } double pt; - if (thermalPt) { - // const double T = 0.160; - - // mThermal->SetParameter(0, mass); - // mThermal->SetParameter(1, T); - - // pt = mThermal->GetRandom(); - - pt = gRandom->Uniform(mPtMin, mPtMaxPhiPhi); // Falling back to flat pT due to low statistics in high pT + pt = gRandom->Uniform(mPtMin, mPtMaxPhiPhi); } else { - // pt = mLevyTsallis->GetRandom(); - pt = gRandom->Uniform(mPtMin, 100.0); // Falling back to flat pT due to low statistics in high pT + pt = gRandom->Uniform(mPtMin, mPtMaxPhi); } const double px = pt * std::cos(phi); @@ -215,24 +207,23 @@ private: particle.yProd(0.); particle.zProd(0.); + mPythiaGun.particleData.mayDecay(pdg, true); mPythiaGun.event.append(particle); } } int mEventCounter = 0; int mResoPDG; + int mCustomPhiPDG; int mSignalInterval; - float mPtMin, mPtMaxPhiPhi, mYMin, mYMax; + float mPtMin, mPtMaxPhiPhi, mPtMaxPhi, mYMin, mYMax; Pythia8::Pythia mPythiaGun; Pythia8::Pythia pythiaObjectMinimumBias; - - // TF1 *mThermal; - // TF1 *mLevyTsallis; }; /// Entry point for o2-sim -FairGenerator *generatePhiResonanceGun(int resoPDG = 999999, float ptMin = 0.0, float ptMax = 50.0, float yMin = -1.0, float yMax = 1.0, std::string pythiaCfgMb = "", int signalInterval = 3) +FairGenerator *generatePhiResonanceGun(int resoPDG = 999999, int customPhiPDG = 888888, float ptMin = 0.0, float ptMax = 50.0, float ptMaxPhi = 100.0, float yMin = -1.0, float yMax = 1.0, std::string pythiaCfgMb = "", int signalInterval = 3) { - return new GeneratorPhiResonance(resoPDG, ptMin, ptMax, yMin, yMax, pythiaCfgMb, signalInterval); + return new GeneratorPhiResonance(resoPDG, customPhiPDG, ptMin, ptMax, ptMaxPhi, yMin, yMax, pythiaCfgMb, signalInterval); } \ No newline at end of file From 050960116dced4d735e5840cc4ccab56dfc97158 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Wed, 30 Sep 2026 11:04:27 +0200 Subject: [PATCH 086/144] Implemented first external generator version of AMPT (#2479) --- .../ampt/generator/PbPb_536TeV_AMPT.ampt | 151 +++++++++++ .../external/generator/generator_AMPT.C | 245 ++++++++++++++++++ .../common/ini/GeneratorAMPTPbPb536TeV.ini | 6 + .../ini/tests/GeneratorAMPTPbPb536TeV.C | 69 +++++ 4 files changed, 471 insertions(+) create mode 100644 MC/config/common/ampt/generator/PbPb_536TeV_AMPT.ampt create mode 100644 MC/config/common/external/generator/generator_AMPT.C create mode 100644 MC/config/common/ini/GeneratorAMPTPbPb536TeV.ini create mode 100644 MC/config/common/ini/tests/GeneratorAMPTPbPb536TeV.C diff --git a/MC/config/common/ampt/generator/PbPb_536TeV_AMPT.ampt b/MC/config/common/ampt/generator/PbPb_536TeV_AMPT.ampt new file mode 100644 index 000000000..31eb92c85 --- /dev/null +++ b/MC/config/common/ampt/generator/PbPb_536TeV_AMPT.ampt @@ -0,0 +1,151 @@ +5360 ! EFRM (sqrt(S_NN) in GeV if FRAME is CMS) +CMS ! FRAME +A ! PROJ +A ! TARG +208 ! IAP (projectile A number) +82 ! IZP (projectile Z number) +208 ! IAT (target A number) +82 ! IZT (target Z number) +1 ! NEVNT (total number of events) - overwritten by generator_AMPT.C +0. ! BMIN (mininum impact parameter in fm) +20. ! BMAX (maximum impact parameter in fm, also see below) +4 ! ISOFT (D=4): select Default AMPT or String Melting(see below) +1000 ! NTMAX: number of timesteps (D=150), see below +0.2 ! DT: timestep in fm (hadron cascade time= DT*NTMAX) (D=0.2) +0.30 ! PARJ(41): parameter a in Lund symmetric splitting function +0.15 ! PARJ(42): parameter b in Lund symmetric splitting function +1 ! (D=1,yes;0,no) flag for popcorn mechanism(netbaryon stopping) +1.0 ! PARJ(5) to control BMBbar vs BBbar in popcorn (D=1.0) +1 ! shadowing flag (Default=1,yes; 0,no) +0 ! quenching flag (D=0,no; 1,yes) +2.0 ! quenching parameter -dE/dx (GeV/fm) in case quenching flag=1 +2.0 ! p0 cutoff in HIJING for minijet productions (D=2.0) +3.2264d0 ! parton screening mass in fm^(-1) (D=2.265d0), see below +0 ! IZPC: (D=0 forward-angle parton scatterings; 100,isotropic) +0.33d0 ! alpha in parton cascade (D=0.33d0), see parton screening mass +1d6 ! dpcoal in GeV +1d6 ! drcoal in fm +0 ! ihjsed: take HIJING seed from below (D=0)or at runtime(11) - overwritten by generator_AMPT.C +13150909 ! random seed for HIJING - overwritten by generator_AMPT.C +8 ! random seed for parton cascade - overwritten by generator_AMPT.C +0 ! flag for K0s weak decays (D=0,no; 1,yes) +1 ! flag for phi decays at end of hadron cascade (D=1,yes; 0,no) +0 ! flag for pi0 decays at end of hadron cascade (D=0,no; 1,yes) +0 ! optional OSCAR output (D=0,no; 1,yes; 2&3,more parton info) +0 ! flag for perturbative deuteron calculation (D=0,no; 1or2,yes) +1 ! integer factor for perturbative deuterons(>=1 & <=10000) +1 ! choice of cross section assumptions for deuteron reactions +-7. ! Pt in GeV: generate events with >=1 minijet above this value +1000 ! maxmiss (D=1000): maximum # of tries to repeat a HIJING event +3 ! flag on initial and final state radiation (D=3,both yes; 0,no) +1 ! flag on Kt kick (D=1,yes; 0,no) +0 ! flag to turn on quark pair embedding (D=0,no; 1,yes) +7., 0. ! Initial Px and Py values (GeV) of the embedded quark (u or d) +0., 0. ! Initial x & y values (fm) of the embedded back-to-back q/qbar +1, 5., 0. ! nsembd(D=0), psembd (in GeV),tmaxembd (in radian). +0 ! Flag to enable users to modify shadowing (D=0,no; 1,yes) +1.d0 ! Factor used to modify nuclear shadowing +1 ! Flag for random orientation of reaction plane (D=0,no; 1,yes) + +%%%%%%%%%% O2DPG notes: +Pb-Pb at sqrt(s_NN) = 5.36 TeV, minimum bias (b = 0-20 fm), String Melting +with the LHC settings of arXiv:1403.6321 (a=0.30, b=0.15/GeV^2, 1.5 mb parton +cross section: alpha=0.33 and screening mass 3.2264/fm). +Values are read by AMPT line by line: do NOT add or remove lines above. +NEVNT (line 9), ihjsed (line 28) and the two seeds (lines 29-30) are +overwritten by MC/config/common/external/generator/generator_AMPT.C. +%%%%%%%%%% Further explanations: +BMAX: the upper limit HIPR1(34)+HIPR1(35)=19.87fm (dAu), 25.60fm(AuAu). +ISOFT: 1 Default, + 4 String Melting. +PARJ(41) & (42): for string melting AMPT, 0.55 & 0.15/GeV^2 are recommended + for top RHIC energies and 0.30 & 0.15/GeV^2 are recommended for + LHC energies (see arXiv:1403.6321 for details). +NTMAX: number of time-steps for hadron cascade. + Use a large value (e.g. 1000) for LHC studies or HBT studies at RHIC. + Using NTMAX=2 or 3 effectively turns off hadronic cascade. +parton screening mass (in 1/fm): its square is inversely proportional to + the parton cross section. Use D=2.265d0 for 3mb cross section + when alpha in parton cascade is set to 0.33; + (note: 3.2264d0 for 3mb cross section when alpha is set to 0.47). + Using 1d4 effectively turns off parton cascade. +ihjsed: if =11, take HIJING random seed at runtime so that + every run may be automatically different (see file 'exec'). +iksdcy: flag for K0s weak decays for comparison with data. +iphidcy: flag for phi meson decays at the end of hadron cascade for comparison + with data; default is yes; use 0 to turn off these decays. + Note: phi meson decay during hadron cascade is always enabled. +ipi0dcy: flag for pi0 electromagnetic decays at the end of hadron cascade for + comparison with data; set to 1 to turn on pi0 decays. +ioscar: 0 Dafault, + 1 Write output in the OSCAR format, + 2 Write out the complete parton information + (ana/parton-initial-afterPropagation.dat) + right after string melting (before parton cascade), + 3 Write out several more files on parton information (see readme). +idpert: flag for perturbative deuteron and antideuteron calculations + with results in ana/ampt_pert.dat: + 0 No perturbative calculations, + 1 Trigger a production of NPERTD perturbative deuterons + in each NN collision, + 2 Trigger a production of NPERTD perturbative deuterons only in + an NN collision where a conventional deuteron is produced. + Note: conventional deuteron calculations are always performed + with results in ana/ampt.dat. +NPERTD: number of perturbative deuterons produced in each triggered collision; + setting it to 0 turns off perturbative deuteron productions. +idxsec: choose a cross section model for deuteron inelastic/elastic collisions: + 1: same |matrix element|**2/s (after averaging over initial spins + and isospins) for B+B -> deuteron+meson at the same sqrt(s); + 2: same |matrix element|**2/s for B+B -> deuteron+meson + at the same sqrt(s)-threshold; + 3: same |matrix element|**2/s for deuteron+meson -> B+B + at the same sqrt(s); + 4: same |matrix element|**2/s for deuteron+meson -> B+B + at the same sqrt(s)-threshold; + 1 or 3 also chooses the same cross section for deuteron+meson or baryon + elastic collision at the same sqrt(s); + 2 or 4 also chooses the same cross section for deuteron+meson or baryon + elastic collision at the same sqrt(s)-threshold. +%%%%%%%%%% For jet studies: +pttrig: generate events with at least 1 initial minijet parton above this Pt + value, otherwise repeat HIJING event until reaching maxmiss tries; + use a negative value to disable this requirement and get normal events. +maxmiss: maximum number of tries for the repetition of a HIJING event to obtain + a minijet above the Pt value of pttrig; increase maxmiss if some events + fail to generate at least 1 initial minijet parton above pttrig. + it is safer to set a large value for high pttrig and/or large b value + and/or smaller colliding nuclei. +IHPR2(2): flag to turn off initial and final state radiation: + 0 both radiation off, 1 only final off, 2 only initial off, 3 both on. +IHPR2(5): flag to turn off Pt kick due to soft interactions: 0 off, 1 on. + Setting both IHPR2(2) and IHPR2(5) to zero makes it more likely to + have two high-Pt minijet partons that are close to back-to-back. +%%%%%%%%%% To embed a back-to-back light q/qbar jet pair +%%%%%%%%%% and a given number of soft pions along each jet into each event: +iembed: flag to turn on quark pair embedding: + 1: on with fixed position(xembd,pembd) and Pt(pxqembd,pyqembd); + 2: on with fixed position(xembd,pembd) and random azimuthal angle + with Pt-magnitude given by sqrt(pxqembd^2+pyqembd^2); + 3: on with random position and fixed Pt(pxqembd,pyqembd); + 4: on with random position and random random azimuthal angle + with Pt-magnitude given by sqrt(pxqembd^2+pyqembd^2); + for iembed=3 or 4: need a position file "embed-jet-xy.txt"; + Other integers: off. +pxqembd, pyqembd: sqrt(pxqembd^2+pyqembd^2) > 70MeV/c is required; + the embedded quark and antiquark have pz=0. +xembd, yembd: the embedded quark and antiquark jets have z=0 initially. Note: + the x-axis is defined as the direction along the impact parameter. +nsembd: number of soft pions to be embedded with each high-Pt parton + in the embedded jet pair. +psembd: Momentum of each embedded soft pion in GeV. +tmaxembd: maximum angle(rad) of embedded soft pions relative to high-Pt parton. +%%%%%%%%%% User modification of nuclear shadowing: +ishadow: set to 1 to enable users to adjust nuclear shadowing + provided the shadowing flag IHPR2(6) is turned on; default value is 0. +dshadow: valid when ishadow=1; this parameter modifies the HIJING shadowing + parameterization Ra(x,r)==1+fa(x,r) via Ra(x,r)==1+fa(x,r)*dshadow, + so the value of 0.d0 turns off shadowing + and the value of 1.d0 uses the default HIJING shadowing; + currently limited to 0.d0<=dshadow<=1.d0 to make sure Ra(x,r)>0. +iphirp: set to 1 to turn on random orientation of reaction plane (D=0) diff --git a/MC/config/common/external/generator/generator_AMPT.C b/MC/config/common/external/generator/generator_AMPT.C new file mode 100644 index 000000000..2de19ffeb --- /dev/null +++ b/MC/config/common/external/generator/generator_AMPT.C @@ -0,0 +1,245 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "TRandom.h" +#include "TParticle.h" +#include "TString.h" +#include "Generators/Generator.h" +#include "Generators/GeneratorFileOrCmd.h" +#include "CommonUtils/FileSystemUtils.h" +#include "SimulationDataFormat/MCEventHeader.h" +#include "SimulationDataFormat/MCUtils.h" + +/// AMPT adapted external generator +/// +/// @author Marco Giacalone (marco.giacalone@cern.ch) +/// @date 09/26 +// The AMPT output (ana/ampt.dat) is converted directly into TParticles, without any HepMC step, as done in the past. +// Two modes are available: +// - "run" : the path is an AMPT input card (input.ampt). AMPT is started in the background in a dedicated +// working directory, where ana/ampt.dat is a named pipe, so events are streamed to O2 as soon as +// they are produced. NEVNT and the random seeds of the card are replaced by the macro. +// - "file" : the path is an existing ampt.dat file, whose events are read sequentially. +// All the particles are transported, including the spectator nucleons (needed by the ZDC). +// This could be changed in case it's actually not needed. To verify + +// o2-sim -g external --noGeant -n 2 --configFile ${O2DPG_MC_CONFIG_ROOT}/MC/config/common/ini/GeneratorAMPTPbPb536TeV.ini +// or, reading an existing AMPT output +// o2-sim -g external --noGeant -n 2 --configKeyValues "GeneratorExternal.fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/external/generator/generator_AMPT.C;GeneratorExternal.funcName=generateAMPT(\"/path/to/ampt.dat\",\"file\")" + +namespace o2 +{ +namespace eventgen +{ + +// Inheriting from GeneratorFileOrCmd as well to use the pipe mechanism +class GeneratorAMPT : public Generator, public GeneratorFileOrCmd +{ + public: + GeneratorAMPT(const std::string& path, bool runAMPT, unsigned int nEvents) : mPath(path) + { + setNEvents(nEvents); + if (runAMPT) { + // AMPT does NSEED = 2 * NSEED + 1 internally, hence the seed must stay below 2^30 + setSeed(gRandom->Integer(536870911) + 1); + mWorkDir = Form("ampt_%lu", mSeed); + // AMPT always reads a number from stdin (used as seed only when ihjsed = 11). If AMPT fails, + // the pipe is opened and closed by the shell, so the reader gets an end of file instead of hanging. + // The timeout avoids a shell blocked forever on the pipe when the reader is gone (this was seen if o2-sim crashed mid-run for some reason) + setCmd("cd " + mWorkDir + " && { echo 0 | \"$AMPT_ROOT/bin/ampt\" > ampt.log 2>&1 || timeout 60 sh -c ': > ana/ampt.dat'; }"); + } + } + ~GeneratorAMPT() override + { + stop(); + if (not mCmd.empty()) { + removeTemp(); + } + } + + Bool_t Init() override + { + if (mCmd.empty()) { + mInput.open(mPath); + } else { + if (!getenv("AMPT_ROOT")) { + LOG(fatal) << "AMPT_ROOT is not set, load the AMPT package"; + return false; + } + if (!prepareWorkDir() || !makeFifo() || !executeCmdLine(mCmd)) { + return false; + } + LOG(info) << "AMPT started in " << mWorkDir << " with seed " << mSeed << " for " << mNEvents << " events"; + mInput.open(mTemporary); // blocks until AMPT opens the pipe for writing + } + if (!mInput.is_open()) { + LOG(fatal) << "Cannot open AMPT output " << (mCmd.empty() ? mPath : mTemporary); + return false; + } + return Generator::Init(); + } + + // Reads the next event from ampt.dat: one header line with 11 fields followed by + // exactly nParticles lines with 9 fields (pdg px py pz m x y z t) + Bool_t generateEvent() override + { + std::string line; + if (!nextLine(line)) { + LOG(fatal) << "AMPT output ended after " << mEventCounter << " events (" << mNEvents << " requested)" + << (mCmd.empty() ? "" : ", see " + mWorkDir + "/ampt.log"); + return false; + } + std::istringstream header(line); + header >> mHeader.event >> mHeader.run >> mHeader.nParticles >> mHeader.b >> mHeader.nPartProj >> mHeader.nPartTarg >> mHeader.nElP >> mHeader.nInP >> mHeader.nElT >> mHeader.nInT >> mHeader.phiRP; + if (header.fail() || mHeader.nParticles < 0) { + LOG(fatal) << "Malformed AMPT event header: " << line; + return false; + } + mTracks.clear(); + mTracks.reserve(mHeader.nParticles); + for (int i = 0; i < mHeader.nParticles; ++i) { + AMPTTrack t; + if (!nextLine(line)) { + LOG(fatal) << "AMPT event " << mHeader.event << " is truncated: " << i << " out of " << mHeader.nParticles << " particles"; + return false; + } + std::istringstream particle(line); + particle >> t.pdg >> t.px >> t.py >> t.pz >> t.m; + if (particle.fail()) { + LOG(fatal) << "Malformed AMPT particle line: " << line; + return false; + } + mTracks.push_back(t); + } + mEventCounter++; + LOG(info) << "AMPT event " << mEventCounter << "/" << mNEvents << ": " << mHeader.nParticles << " particles, b = " << mHeader.b << " fm"; + return true; + } + + Bool_t importParticles() override + { + mParticles.clear(); + for (const auto& t : mTracks) { + // AMPT uses PDG codes, apart from (anti)deuterons + int pdg = std::abs(t.pdg) == 42 ? (t.pdg > 0 ? 1 : -1) * 1000010020 : t.pdg; + double e = std::sqrt(t.px * t.px + t.py * t.py + t.pz * t.pz + t.m * t.m); + // Freeze-out coordinates are in fm, hence negligible: particles are produced at the interaction vertex + TParticle particle(pdg, 1, -1, -1, -1, -1, t.px, t.py, t.pz, e, 0., 0., 0., 0.); + o2::mcutils::MCGenHelper::encodeParticleStatusAndTracking(particle, true); + mParticles.push_back(particle); + } + return true; + } + + void updateHeader(o2::dataformats::MCEventHeader* eventHeader) override + { + using Key = o2::dataformats::MCInfoKeys; + eventHeader->putInfo(Key::generator, "ampt"); + eventHeader->SetB(mHeader.b); + eventHeader->putInfo(Key::impactParameter, mHeader.b); + eventHeader->putInfo(Key::nPart, mHeader.nPartProj + mHeader.nPartTarg); + eventHeader->putInfo(Key::nPartProjectile, mHeader.nPartProj); + eventHeader->putInfo(Key::nPartTarget, mHeader.nPartTarg); + eventHeader->putInfo(Key::planeAngle, mHeader.phiRP); + // Participant nucleons from elastic and inelastic collisions in projectile and target + eventHeader->putInfo("ampt_NELP", mHeader.nElP); + eventHeader->putInfo("ampt_NINP", mHeader.nInP); + eventHeader->putInfo("ampt_NELT", mHeader.nElT); + eventHeader->putInfo("ampt_NINTHJ", mHeader.nInT); + } + + void stop() override + { + mInput.close(); + if (not mCmd.empty()) { + // AMPT exits by itself once all the events are written, otherwise it is terminated + terminateCmd(sStopGraceMillis); + } + } + + private: + struct AMPTHeader { + int event = 0, run = 0, nParticles = 0; + double b = 0.; + int nPartProj = 0, nPartTarg = 0, nElP = 0, nInP = 0, nElT = 0, nInT = 0; + double phiRP = 0.; + }; + struct AMPTTrack { + int pdg = 0; + double px = 0., py = 0., pz = 0., m = 0.; + }; + + // Creates the working directory with the edited card. ana/ampt.dat becomes the named pipe, while + // the outputs growing with the number of events and not needed here points to /dev/null + bool prepareWorkDir() + { + std::filesystem::create_directories(mWorkDir + "/ana"); + // AMPT reads the card sequentially, one value per line: lines are replaced by their position + const std::map replacements = { + {9, std::to_string(mNEvents) + "\t\t! NEVNT (set by generator_AMPT.C)"}, + {28, "0\t\t! ihjsed (set by generator_AMPT.C)"}, + {29, std::to_string(mSeed) + "\t\t! random seed for HIJING (set by generator_AMPT.C)"}, + {30, std::to_string(gRandom->Integer(536870911) + 1) + "\t\t! random seed for parton cascade (set by generator_AMPT.C)"}}; + std::ifstream src(mPath); + std::ofstream dst(mWorkDir + "/input.ampt"); + std::string line; + for (int lineNumber = 1; std::getline(src, line); ++lineNumber) { + auto replacement = replacements.find(lineNumber); + dst << (replacement != replacements.end() ? replacement->second : line) << "\n"; + } + for (const auto& file : {"zpc.dat", "npart-xy.dat"}) { + std::filesystem::remove(mWorkDir + "/ana/" + file); + std::filesystem::create_symlink("/dev/null", mWorkDir + "/ana/" + file); + } + // Named pipe used by makeFifo and removed by removeTemp + mTemporary = mWorkDir + "/ana/ampt.dat"; + mFileNames = {mTemporary}; + return true; + } + + bool nextLine(std::string& line) + { + while (std::getline(mInput, line)) { + if (line.find_first_not_of(" \t\r") != std::string::npos) { + return true; + } + } + return false; + } + + std::string mPath; + std::string mWorkDir; + std::ifstream mInput; + unsigned int mEventCounter = 0; + AMPTHeader mHeader; + std::vector mTracks; +}; + +} // namespace eventgen +} // namespace o2 + +// path : AMPT input card (mode "run") or existing ampt.dat file (mode "file") +// maxEvents : number of events for AMPT when the total number of events is not known (e.g. hyperloop) +FairGenerator* generateAMPT(std::string path, std::string mode = "run", int maxEvents = 2147483647) +{ + path = o2::utils::expandShellVarsInFileName(path); + if (mode != "run" && mode != "file") { + LOG(fatal) << "Unknown AMPT generator mode " << mode << ", use \"run\" or \"file\""; + return nullptr; + } + if (!std::filesystem::exists(path)) { + LOG(fatal) << "AMPT " << (mode == "run" ? "input card " : "output file ") << path << " does not exist"; + return nullptr; + } + unsigned int nEvents = o2::eventgen::Generator::getTotalNEvents(); + if (nEvents == 0) { + nEvents = maxEvents; + } + LOG(info) << "AMPT generator in \"" << mode << "\" mode using " << path; + return new o2::eventgen::GeneratorAMPT(path, mode == "run", nEvents); +} diff --git a/MC/config/common/ini/GeneratorAMPTPbPb536TeV.ini b/MC/config/common/ini/GeneratorAMPTPbPb536TeV.ini new file mode 100644 index 000000000..2900caf2b --- /dev/null +++ b/MC/config/common/ini/GeneratorAMPTPbPb536TeV.ini @@ -0,0 +1,6 @@ +#NEV_TEST> 2 +### AMPT Pb-Pb 5.36 TeV (String Melting), converted directly to MCTracks without HepMC +### Use generateAMPT("/path/to/ampt.dat", "file") to read an existing AMPT output instead +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/external/generator/generator_AMPT.C +funcName=generateAMPT("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/ampt/generator/PbPb_536TeV_AMPT.ampt") diff --git a/MC/config/common/ini/tests/GeneratorAMPTPbPb536TeV.C b/MC/config/common/ini/tests/GeneratorAMPTPbPb536TeV.C new file mode 100644 index 000000000..1d68673f4 --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorAMPTPbPb536TeV.C @@ -0,0 +1,69 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader* eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + // Check if there are 2 events, as simulated in the o2dpg-test + auto nEvents = tree->GetEntries(); + if (nEvents != 2) { + std::cerr << "Expected 2 events, got " << nEvents << "\n"; + return 1; + } + // Beam momentum per nucleon for Pb-Pb at sqrt(s_NN) = 5.36 TeV + const double beamMomentum = std::sqrt(2680. * 2680. - 0.938 * 0.938); + for (Long64_t i = 0; i < nEvents; ++i) { + tree->GetEntry(i); + if (tracks->empty()) { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + // Check the heavy-ion information of the header + bool isValid = false; + auto nPart = eventHeader->getInfo(o2::dataformats::MCInfoKeys::nPart, isValid); + if (!isValid || nPart <= 0) { + std::cerr << "Event " << i << " has no valid number of participants\n"; + return 1; + } + if (eventHeader->GetB() < 0. || eventHeader->GetB() > 20.) { + std::cerr << "Event " << i << " has impact parameter " << eventHeader->GetB() << " outside the [0, 20] fm range\n"; + return 1; + } + // Spectator nucleons (pT = 0, beam momentum) must be kept for both beams. + // AMPT writes momenta with 4 decimals, hence the tolerance + int nSpectatorsA = 0, nSpectatorsC = 0; + for (const auto& track : *tracks) { + if (track.GetPdgCode() != 2212 && track.GetPdgCode() != 2112) { + continue; + } + if (track.GetPt() == 0. && std::abs(std::abs(track.GetStartVertexMomentumZ()) - beamMomentum) < 1e-2) { + (track.GetStartVertexMomentumZ() > 0 ? nSpectatorsA : nSpectatorsC)++; + } + } + // The total number of nucleons in each nucleus must be conserved + if (nSpectatorsA + nSpectatorsC + nPart > 2 * 208) { + std::cerr << "Event " << i << " has more nucleons than the colliding nuclei\n"; + return 1; + } + if (nSpectatorsA == 0 || nSpectatorsC == 0) { + std::cerr << "Event " << i << " has no spectator nucleons (A side: " << nSpectatorsA << ", C side: " << nSpectatorsC << ")\n"; + return 1; + } + std::cout << "Event " << i << ": " << tracks->size() << " tracks, b = " << eventHeader->GetB() << " fm, Npart = " << nPart + << ", spectators A/C = " << nSpectatorsA << "/" << nSpectatorsC << "\n"; + } + return 0; +} From 6b38f862a62477b1d9190857d1341f32a516900a Mon Sep 17 00:00:00 2001 From: Raphaelle Bailhache Date: Mon, 28 Sep 2026 10:37:19 +0200 Subject: [PATCH 087/144] remove EvtGen ini file breaking the DQ update --- .../GeneratorHF_bbbarToDDbarToDielectrons.ini | 22 ---- .../ini/GeneratorHF_bbbarToDielectrons.ini | 22 ---- .../ini/GeneratorHF_ccbarToDielectrons.ini | 22 ---- .../GeneratorHF_bbbarToDDbarToDielectrons.C | 90 ------------- .../tests/GeneratorHF_bbbarToDielectrons.C | 86 ------------- .../tests/GeneratorHF_ccbarToDielectrons.C | 119 ------------------ 6 files changed, 361 deletions(-) delete mode 100644 MC/config/PWGEM/ini/GeneratorHF_bbbarToDDbarToDielectrons.ini delete mode 100644 MC/config/PWGEM/ini/GeneratorHF_bbbarToDielectrons.ini delete mode 100644 MC/config/PWGEM/ini/GeneratorHF_ccbarToDielectrons.ini delete mode 100644 MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDDbarToDielectrons.C delete mode 100644 MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDielectrons.C delete mode 100644 MC/config/PWGEM/ini/tests/GeneratorHF_ccbarToDielectrons.C diff --git a/MC/config/PWGEM/ini/GeneratorHF_bbbarToDDbarToDielectrons.ini b/MC/config/PWGEM/ini/GeneratorHF_bbbarToDDbarToDielectrons.ini deleted file mode 100644 index 0201b00b8..000000000 --- a/MC/config/PWGEM/ini/GeneratorHF_bbbarToDDbarToDielectrons.ini +++ /dev/null @@ -1,22 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/GeneratorBeautyToEle_EvtGen.C -funcName = GeneratorBeautyToEle_EvtGen(-1.5,1.5,true,false) - -### The external generator derives from GeneratorPythia8. -### This part configures the bits of the interface: configuration and user hooks - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_bbbar.cfg -hooksFileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C -hooksFuncName = pythia8_userhooks_bbbar(-1.5,1.5) - -### The setup uses an external even generator trigger which is -### defined in the following file and it is retrieved and configured -### according to the specified function call - -[TriggerExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/trigger/selectDaughterFromHFwithinAcc.C -funcName = selectDaughterFromHFwithinAcc(11,kTRUE,-1.,1.,2) diff --git a/MC/config/PWGEM/ini/GeneratorHF_bbbarToDielectrons.ini b/MC/config/PWGEM/ini/GeneratorHF_bbbarToDielectrons.ini deleted file mode 100644 index 9f0de6bdc..000000000 --- a/MC/config/PWGEM/ini/GeneratorHF_bbbarToDielectrons.ini +++ /dev/null @@ -1,22 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/GeneratorBeautyToEle_EvtGen.C -funcName = GeneratorBeautyToEle_EvtGen(-1.5,1.5,true) - -### The external generator derives from GeneratorPythia8. -### This part configures the bits of the interface: configuration and user hooks - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_bbbar.cfg -hooksFileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C -hooksFuncName = pythia8_userhooks_bbbar(-1.5,1.5) - -### The setup uses an external even generator trigger which is -### defined in the following file and it is retrieved and configured -### according to the specified function call - -[TriggerExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/trigger/selectDaughterFromHFwithinAcc.C -funcName = selectDaughterFromHFwithinAcc(11,kFALSE,-1.,1.) diff --git a/MC/config/PWGEM/ini/GeneratorHF_ccbarToDielectrons.ini b/MC/config/PWGEM/ini/GeneratorHF_ccbarToDielectrons.ini deleted file mode 100644 index 6be80e1b8..000000000 --- a/MC/config/PWGEM/ini/GeneratorHF_ccbarToDielectrons.ini +++ /dev/null @@ -1,22 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/GeneratorCharmToEle_EvtGen.C -funcName = GeneratorCharmToEle_EvtGen(-1.5,1.5) - -### The external generator derives from GeneratorPythia8. -### This part configures the bits of the interface: configuration and user hooks - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_pp_cr2.cfg -hooksFileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/hooks/pythia8_userhooks_qqbar.C -hooksFuncName = pythia8_userhooks_ccbar(-1.5,1.5) - -### The setup uses an external even generator trigger which is -### defined in the following file and it is retrieved and configured -### according to the specified function call - -[TriggerExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/trigger/selectDaughterFromHFwithinAcc.C -funcName = selectDaughterFromHFwithinAcc(11,kFALSE,-1.,1.) diff --git a/MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDDbarToDielectrons.C b/MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDDbarToDielectrons.C deleted file mode 100644 index 21e68170b..000000000 --- a/MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDDbarToDielectrons.C +++ /dev/null @@ -1,90 +0,0 @@ -int External() -{ - - int checkPdgDecay = 11; - std::string path{"o2sim_Kine.root"}; - TFile file(path.c_str(), "READ"); - if (file.IsZombie()) { - std::cerr << "Cannot open ROOT file " << path << "\n"; - return 1; - } - auto tree = (TTree*)file.Get("o2sim"); - std::vector* tracks{}; - tree->SetBranchAddress("MCTrack", &tracks); - - int nLeptons{}; - int nLeptonsInAcceptance{}; - int nLeptonsToBeDone{}; - int nSignalPairs{}; - int nLeptonPairs{}; - int nLeptonPairsInAcceptance{}; - int nLeptonPairsToBeDone{}; - auto nEvents = tree->GetEntries(); - - for (int i = 0; i < nEvents; i++) { - tree->GetEntry(i); - int nleptonseinacc = 0; - int nleptonse = 0; - int nleptonseToBeDone = 0; - int nopenHeavy = 0; - for (auto& track : *tracks) { - auto pdg = track.GetPdgCode(); - auto y = track.GetRapidity(); - if (std::abs(pdg) == checkPdgDecay) { - int igmother = track.getMotherTrackId(); - if (igmother > 0) { - auto gmTrack = (*tracks)[igmother]; - int gmpdg = gmTrack.GetPdgCode(); - if ( int(std::abs(gmpdg)/100.) == 4 || int(std::abs(gmpdg)/1000.) == 4 || int(std::abs(gmpdg)/100.) == 5 || int(std::abs(gmpdg)/1000.) == 5 ) { - nLeptons++; - nleptonse++; - if (-1 < y && y < 1) { - nleptonseinacc++; - nLeptonsInAcceptance++; - } - if (track.getToBeDone()) { - nLeptonsToBeDone++; - nleptonseToBeDone++; - } - } - } - } else if (std::abs(pdg) == 411 || std::abs(pdg) == 421 || std::abs(pdg) == 431 || std::abs(pdg) == 4122 || std::abs(pdg) == 4132 || std::abs(pdg) == 4232 || std::abs(pdg) == 4332 || std::abs(pdg) == 511 || std::abs(pdg) == 521 || std::abs(pdg) == 531 || std::abs(pdg) == 541 || std::abs(pdg) == 5112 || std::abs(pdg) == 5122 || std::abs(pdg) == 5232 || std::abs(pdg) == 5132 || std::abs(pdg) == 5332) { - nopenHeavy++; - } - } - if (nopenHeavy > 1) nSignalPairs++; - if (nleptonse > 1) nLeptonPairs++; - if (nleptonseToBeDone > 1) nLeptonPairsToBeDone++; - if (nleptonseinacc > 1) nLeptonPairsInAcceptance++; - } - std::cout << "#events: " << nEvents << "\n" - << "#leptons in acceptance: " << nLeptonsInAcceptance << "\n" - << "#lepton pairs in acceptance: " << nLeptonPairsInAcceptance << "\n" - << "#leptons: " << nLeptons << "\n" - << "#leptons to be done: " << nLeptonsToBeDone << "\n" - << "#signal pairs: " << nSignalPairs << "\n" - << "#lepton pairs: " << nLeptonPairs << "\n" - << "#lepton pairs to be done: " << nLeptonPairsToBeDone << "\n"; - if (nLeptons == 0) { - std::cerr << "Number of leptons should be greater than 1.\n"; - return 1; - } - if (nLeptonPairs < nSignalPairs) { - std::cerr << "Number of lepton pairs should be at least equaled to the number of open charm hadron pairs\n"; - return 1; - } - if (nLeptonPairsInAcceptance < nEvents) { - std::cerr << "Number of lepton pairs should be at least equaled to the number of events\n"; - return 1; - } - if (nLeptonPairs != nLeptonPairsToBeDone) { - std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; - return 1; - } - if (nLeptons != nLeptonsToBeDone) { - std::cerr << "The number of leptons should be the same as the number of leptons which should be transported.\n"; - return 1; - } - - return 0; -} diff --git a/MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDielectrons.C b/MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDielectrons.C deleted file mode 100644 index 9fd1cb188..000000000 --- a/MC/config/PWGEM/ini/tests/GeneratorHF_bbbarToDielectrons.C +++ /dev/null @@ -1,86 +0,0 @@ -int External() -{ - - int checkPdgDecay = 11; - std::string path{"o2sim_Kine.root"}; - TFile file(path.c_str(), "READ"); - if (file.IsZombie()) { - std::cerr << "Cannot open ROOT file " << path << "\n"; - return 1; - } - auto tree = (TTree*)file.Get("o2sim"); - std::vector* tracks{}; - tree->SetBranchAddress("MCTrack", &tracks); - - int nLeptonsInAcceptance{}; - int nLeptons{}; - int nLeptonsToBeDone{}; - int nSignalPairs{}; - int nLeptonPairs{}; - int nLeptonPairsToBeDone{}; - auto nEvents = tree->GetEntries(); - - for (int i = 0; i < nEvents; i++) { - tree->GetEntry(i); - int nleptonse = 0; - int nleptonseToBeDone = 0; - int nopenHeavy = 0; - for (auto& track : *tracks) { - auto pdg = track.GetPdgCode(); - auto y = track.GetRapidity(); - if (std::abs(pdg) == checkPdgDecay) { - int igmother = track.getMotherTrackId(); - if (igmother > 0) { - auto gmTrack = (*tracks)[igmother]; - int gmpdg = gmTrack.GetPdgCode(); - if (int(std::abs(gmpdg)/100.) == 4 || int(std::abs(gmpdg)/1000.) == 4 || int(std::abs(gmpdg)/100.) == 5 || int(std::abs(gmpdg)/1000.) == 5) { - nLeptons++; - nleptonse++; - if (-1 < y && y < 1) nLeptonsInAcceptance++; - if (track.getToBeDone()) { - nLeptonsToBeDone++; - nleptonseToBeDone++; - } - } - } - } else if (std::abs(pdg) == 411 || std::abs(pdg) == 421 || std::abs(pdg) == 431 || std::abs(pdg) == 4122 || std::abs(pdg) == 4132 || std::abs(pdg) == 4232 || std::abs(pdg) == 4332 || std::abs(pdg) == 511 || std::abs(pdg) == 521 || std::abs(pdg) == 531 || std::abs(pdg) == 541 || std::abs(pdg) == 5112 || std::abs(pdg) == 5122 || std::abs(pdg) == 5232 || std::abs(pdg) == 5132 || std::abs(pdg) == 5332) { - nopenHeavy++; - } - } - if (nopenHeavy > 1) nSignalPairs++; - if (nleptonse > 1) nLeptonPairs++; - if (nleptonseToBeDone > 1) nLeptonPairsToBeDone++; - } - std::cout << "#events: " << nEvents << "\n" - << "#leptons in acceptance: " << nLeptonsInAcceptance << "\n" - << "#leptons: " << nLeptons << "\n" - << "#leptons to be done: " << nLeptonsToBeDone << "\n" - << "#signal pairs: " << nSignalPairs << "\n" - << "#lepton pairs: " << nLeptonPairs << "\n" - << "#lepton pairs to be done: " << nLeptonPairsToBeDone << "\n"; - if (nLeptons == 0) { - std::cerr << "Number of leptons should be greater than 1.\n"; - return 1; - } - if (nLeptonPairs < nSignalPairs) { - std::cerr << "Number of lepton pairs should be at least equaled to the number of open charm hadron pairs\n"; - return 1; - } - if (nLeptonPairs != nLeptonPairsToBeDone) { - std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; - return 1; - } - if (nLeptons != nLeptonsToBeDone) { - std::cerr << "The number of leptons should be the same as the number of leptons which should be transported.\n"; - return 1; - } - if (nLeptonsInAcceptance < nEvents) { - std::cerr << "The number of leptons in acceptance should be at least equaled to the number of events.\n"; - return 1; - } - if (nLeptonsInAcceptance != nLeptons) { - std::cerr << "The number of leptons in acceptance should be the same as the total number of leptons\n"; - return 1; - } - return 0; -} diff --git a/MC/config/PWGEM/ini/tests/GeneratorHF_ccbarToDielectrons.C b/MC/config/PWGEM/ini/tests/GeneratorHF_ccbarToDielectrons.C deleted file mode 100644 index efe6027a9..000000000 --- a/MC/config/PWGEM/ini/tests/GeneratorHF_ccbarToDielectrons.C +++ /dev/null @@ -1,119 +0,0 @@ -int External() -{ - - int checkPdgDecay = -11; - std::string path{"o2sim_Kine.root"}; - TFile file(path.c_str(), "READ"); - if (file.IsZombie()) { - std::cerr << "Cannot open ROOT file " << path << "\n"; - return 1; - } - - auto tree = (TTree*)file.Get("o2sim"); - std::vector* tracks{}; - tree->SetBranchAddress("MCTrack", &tracks); - - int nLeptonsInAcceptance{}; - int nLeptons{}; - int nAntileptons{}; - int nAntileptonsInAcceptance{}; - int nLeptonsToBeDone{}; - int nAntileptonsToBeDone{}; - int nSignalPairs{}; - int nLeptonPairs{}; - int nLeptonPairsToBeDone{}; - auto nEvents = tree->GetEntries(); - - for (int i = 0; i < nEvents; i++) { - tree->GetEntry(i); - int nElectrons = 0; - int nPositrons = 0; - int nElectronsToBeDone = 0; - int nPositronsToBeDone = 0; - int nOpenCharmPos = 0; - int nOpenCharmNeg = 0; - for (auto& track : *tracks) { - auto pdg = track.GetPdgCode(); - auto y = track.GetRapidity(); - if (pdg == checkPdgDecay) { - int igmother = track.getMotherTrackId(); - if (igmother > 0) { - auto gmTrack = (*tracks)[igmother]; - int gmpdg = gmTrack.GetPdgCode(); - if (int(std::abs(gmpdg)/100.) == 4 || int(std::abs(gmpdg)/1000.) == 4) { - nLeptons++; - nElectrons++; - if (-1 < y && y < 1) nLeptonsInAcceptance++; - if (track.getToBeDone()) { - nLeptonsToBeDone++; - nElectronsToBeDone++; - } - } - } - } else if (pdg == -checkPdgDecay) { - int igmother = track.getMotherTrackId(); - if (igmother > 0) { - auto gmTrack = (*tracks)[igmother]; - int gmpdg = gmTrack.GetPdgCode(); - if (int(TMath::Abs(gmpdg)/100.) == 4 || int(TMath::Abs(gmpdg)/1000.) == 4) { - nAntileptons++; - nPositrons++; - if (-1 < y && y < 1) nAntileptonsInAcceptance++; - if (track.getToBeDone()) { - nAntileptonsToBeDone++; - nPositronsToBeDone++; - } - } - } - } else if (pdg == 411 || pdg == 421 || pdg == 431 || pdg == 4122 || pdg == 4132 || pdg == 4232 || pdg == 4332) { - nOpenCharmPos++; - } else if (pdg == -411 || pdg == -421 || pdg == -431 || pdg == -4122 || pdg == -4132 || pdg == -4232 || pdg == -4332) { - nOpenCharmNeg++; - } - } - if (nOpenCharmPos > 0 && nOpenCharmNeg > 0) nSignalPairs++; - if (nElectrons > 0 && nPositrons > 0) nLeptonPairs++; - if (nElectronsToBeDone > 0 && nPositronsToBeDone > 0) nLeptonPairsToBeDone++; - } - std::cout << "#events: " << nEvents << "\n" - << "#leptons: " << nLeptons << "\n" - << "#leptons in acceptance: " << nLeptonsInAcceptance << "\n" - << "#antileptons: " << nAntileptons << "\n" - << "#antileptons in acceptance: " << nAntileptonsInAcceptance << "\n" - << "#leptons to be done: " << nLeptonsToBeDone << "\n" - << "#antileptons to be done: " << nAntileptonsToBeDone << "\n" - << "#signal pairs: " << nSignalPairs << "\n" - << "#lepton pairs: " << nLeptonPairs << "\n" - << "#lepton pairs to be done: " << nLeptonPairsToBeDone << "\n"; - int nTotalLeptonsInAcceptance = nLeptonsInAcceptance + nAntileptonsInAcceptance; - if (nLeptons == 0 && nAntileptons == 0) { - std::cerr << "Number of leptons, number of anti-leptons should all be greater than 1.\n"; - return 1; - } - if (nLeptonPairs < nSignalPairs) { - std::cerr << "Number of lepton pairs should be at least equaled to the number of open charm hadron pairs\n"; - return 1; - } - if (nLeptonPairs != nLeptonPairsToBeDone) { - std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; - return 1; - } - if (nLeptons != nLeptonsToBeDone) { - std::cerr << "The number of leptons should be the same as the number of leptons which should be transported.\n"; - return 1; - } - if (nTotalLeptonsInAcceptance < nEvents) { - std::cerr << "The number of leptons in acceptance should be at least equaled to the number of events.\n"; - return 1; - } - if (nLeptons != nLeptonsInAcceptance) { - std::cerr << "The number of leptons in acceptance should be the same as the number of leptons.\n"; - return 1; - } - if (nAntileptons != nAntileptonsInAcceptance) { - std::cerr << "The number of anti-leptons in acceptance should be the same as the number of anti-leptons.\n"; - return 1; - } - - return 0; -} From aeebc784c75e435b448041d12f50a31d0e692e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Jacazio?= Date: Wed, 30 Sep 2026 17:06:17 +0200 Subject: [PATCH 088/144] Add pp at 5.36 TeV AMPT (#2482) --- .../common/ampt/generator/pp_536TeV_AMPT.ampt | 155 ++++++++++++++++++ .../common/ini/GeneratorAMPTpp536TeV.ini | 3 + .../common/ini/tests/GeneratorAMPTpp536TeV.C | 35 ++++ 3 files changed, 193 insertions(+) create mode 100644 MC/config/common/ampt/generator/pp_536TeV_AMPT.ampt create mode 100644 MC/config/common/ini/GeneratorAMPTpp536TeV.ini create mode 100644 MC/config/common/ini/tests/GeneratorAMPTpp536TeV.C diff --git a/MC/config/common/ampt/generator/pp_536TeV_AMPT.ampt b/MC/config/common/ampt/generator/pp_536TeV_AMPT.ampt new file mode 100644 index 000000000..740d085ed --- /dev/null +++ b/MC/config/common/ampt/generator/pp_536TeV_AMPT.ampt @@ -0,0 +1,155 @@ +5360 ! EFRM (sqrt(S_NN) in GeV if FRAME is CMS) +CMS ! FRAME +A ! PROJ +A ! TARG +1 ! IAP (projectile A number) +1 ! IZP (projectile Z number) +1 ! IAT (target A number) +1 ! IZT (target Z number) +1 ! NEVNT (total number of events) - overwritten by generator_AMPT.C +0.0 ! BMIN (mininum impact parameter in fm) +2.5 ! BMAX (maximum impact parameter in fm, also see below) +4 ! ISOFT (D=4): select Default AMPT or String Melting(see below) +150 ! NTMAX: number of timesteps (D=150), see below +0.2 ! DT: timestep in fm (hadron cascade time= DT*NTMAX) (D=0.2) +0.30 ! PARJ(41): parameter a in Lund symmetric splitting function +0.15 ! PARJ(42): parameter b in Lund symmetric splitting function +1 ! (D=1,yes;0,no) flag for popcorn mechanism(netbaryon stopping) +1.0 ! PARJ(5) to control BMBbar vs BBbar in popcorn (D=1.0) +1 ! shadowing flag (Default=1,yes; 0,no) +0 ! quenching flag (D=0,no; 1,yes) +2.0 ! quenching parameter -dE/dx (GeV/fm) in case quenching flag=1 +2.0 ! p0 cutoff in HIJING for minijet productions (D=2.0) +2.265d0 ! parton screening mass in fm^(-1) (D=2.265d0), see below +0 ! IZPC: (D=0 forward-angle parton scatterings; 100,isotropic) +0.33d0 ! alpha in parton cascade (D=0.33d0), see parton screening mass +1d6 ! dpcoal in GeV +1d6 ! drcoal in fm +0 ! ihjsed: take HIJING seed from below (D=0)or at runtime(11) - overwritten by generator_AMPT.C +13150909 ! random seed for HIJING - overwritten by generator_AMPT.C +8 ! random seed for parton cascade - overwritten by generator_AMPT.C +0 ! flag for K0s weak decays (D=0,no; 1,yes) +1 ! flag for phi decays at end of hadron cascade (D=1,yes; 0,no) +0 ! flag for pi0 decays at end of hadron cascade (D=0,no; 1,yes) +0 ! optional OSCAR output (D=0,no; 1,yes; 2&3,more parton info) +0 ! flag for perturbative deuteron calculation (D=0,no; 1or2,yes) +1 ! integer factor for perturbative deuterons(>=1 & <=10000) +1 ! choice of cross section assumptions for deuteron reactions +-7. ! Pt in GeV: generate events with >=1 minijet above this value +1000 ! maxmiss (D=1000): maximum # of tries to repeat a HIJING event +3 ! flag on initial and final state radiation (D=3,both yes; 0,no) +1 ! flag on Kt kick (D=1,yes; 0,no) +0 ! flag to turn on quark pair embedding (D=0,no; 1,yes) +7., 0. ! Initial Px and Py values (GeV) of the embedded quark (u or d) +0., 0. ! Initial x & y values (fm) of the embedded back-to-back q/qbar +1, 5., 0. ! nsembd(D=0), psembd (in GeV),tmaxembd (in radian). +0 ! Flag to enable users to modify shadowing (D=0,no; 1,yes) +1.d0 ! Factor used to modify nuclear shadowing +0 ! Flag for random orientation of reaction plane (D=0,no; 1,yes) +0 ! iexotnp, Flag for exotic nuclear structure of projectile (=0, no; =1,yes) +0 ! iexotnt, Flag for exotic nuclear structure of target (=0, no; =1,yes) +0 ! nstrucp, nucl. struct. of projectile, C12, 1(Triangle), 2(Chain); O16, 1(Tetrahedron), 2(Square), 3(Kite), 4(Chain) +0 ! nstruct, nucl. struct. of target, C12, 1(Triangle), 2(Chain); O16, 1(Tetrahedron), 2(Square), 3(Kite), 4(Chain) + +%%%%%%%%%% O2DPG notes: +Pb-Pb at sqrt(s_NN) = 5.36 TeV, minimum bias (b = 0-20 fm), String Melting +with the LHC settings of arXiv:1403.6321 (a=0.30, b=0.15/GeV^2, 1.5 mb parton +cross section: alpha=0.33 and screening mass 3.2264/fm). +Values are read by AMPT line by line: do NOT add or remove lines above. +NEVNT (line 9), ihjsed (line 28) and the two seeds (lines 29-30) are +overwritten by MC/config/common/external/generator/generator_AMPT.C. +%%%%%%%%%% Further explanations: +BMAX: the upper limit HIPR1(34)+HIPR1(35)=19.87fm (dAu), 25.60fm(AuAu). +ISOFT: 1 Default, + 4 String Melting. +PARJ(41) & (42): for string melting AMPT, 0.55 & 0.15/GeV^2 are recommended + for top RHIC energies and 0.30 & 0.15/GeV^2 are recommended for + LHC energies (see arXiv:1403.6321 for details). +NTMAX: number of time-steps for hadron cascade. + Use a large value (e.g. 1000) for LHC studies or HBT studies at RHIC. + Using NTMAX=2 or 3 effectively turns off hadronic cascade. +parton screening mass (in 1/fm): its square is inversely proportional to + the parton cross section. Use D=2.265d0 for 3mb cross section + when alpha in parton cascade is set to 0.33; + (note: 3.2264d0 for 3mb cross section when alpha is set to 0.47). + Using 1d4 effectively turns off parton cascade. +ihjsed: if =11, take HIJING random seed at runtime so that + every run may be automatically different (see file 'exec'). +iksdcy: flag for K0s weak decays for comparison with data. +iphidcy: flag for phi meson decays at the end of hadron cascade for comparison + with data; default is yes; use 0 to turn off these decays. + Note: phi meson decay during hadron cascade is always enabled. +ipi0dcy: flag for pi0 electromagnetic decays at the end of hadron cascade for + comparison with data; set to 1 to turn on pi0 decays. +ioscar: 0 Dafault, + 1 Write output in the OSCAR format, + 2 Write out the complete parton information + (ana/parton-initial-afterPropagation.dat) + right after string melting (before parton cascade), + 3 Write out several more files on parton information (see readme). +idpert: flag for perturbative deuteron and antideuteron calculations + with results in ana/ampt_pert.dat: + 0 No perturbative calculations, + 1 Trigger a production of NPERTD perturbative deuterons + in each NN collision, + 2 Trigger a production of NPERTD perturbative deuterons only in + an NN collision where a conventional deuteron is produced. + Note: conventional deuteron calculations are always performed + with results in ana/ampt.dat. +NPERTD: number of perturbative deuterons produced in each triggered collision; + setting it to 0 turns off perturbative deuteron productions. +idxsec: choose a cross section model for deuteron inelastic/elastic collisions: + 1: same |matrix element|**2/s (after averaging over initial spins + and isospins) for B+B -> deuteron+meson at the same sqrt(s); + 2: same |matrix element|**2/s for B+B -> deuteron+meson + at the same sqrt(s)-threshold; + 3: same |matrix element|**2/s for deuteron+meson -> B+B + at the same sqrt(s); + 4: same |matrix element|**2/s for deuteron+meson -> B+B + at the same sqrt(s)-threshold; + 1 or 3 also chooses the same cross section for deuteron+meson or baryon + elastic collision at the same sqrt(s); + 2 or 4 also chooses the same cross section for deuteron+meson or baryon + elastic collision at the same sqrt(s)-threshold. +%%%%%%%%%% For jet studies: +pttrig: generate events with at least 1 initial minijet parton above this Pt + value, otherwise repeat HIJING event until reaching maxmiss tries; + use a negative value to disable this requirement and get normal events. +maxmiss: maximum number of tries for the repetition of a HIJING event to obtain + a minijet above the Pt value of pttrig; increase maxmiss if some events + fail to generate at least 1 initial minijet parton above pttrig. + it is safer to set a large value for high pttrig and/or large b value + and/or smaller colliding nuclei. +IHPR2(2): flag to turn off initial and final state radiation: + 0 both radiation off, 1 only final off, 2 only initial off, 3 both on. +IHPR2(5): flag to turn off Pt kick due to soft interactions: 0 off, 1 on. + Setting both IHPR2(2) and IHPR2(5) to zero makes it more likely to + have two high-Pt minijet partons that are close to back-to-back. +%%%%%%%%%% To embed a back-to-back light q/qbar jet pair +%%%%%%%%%% and a given number of soft pions along each jet into each event: +iembed: flag to turn on quark pair embedding: + 1: on with fixed position(xembd,pembd) and Pt(pxqembd,pyqembd); + 2: on with fixed position(xembd,pembd) and random azimuthal angle + with Pt-magnitude given by sqrt(pxqembd^2+pyqembd^2); + 3: on with random position and fixed Pt(pxqembd,pyqembd); + 4: on with random position and random random azimuthal angle + with Pt-magnitude given by sqrt(pxqembd^2+pyqembd^2); + for iembed=3 or 4: need a position file "embed-jet-xy.txt"; + Other integers: off. +pxqembd, pyqembd: sqrt(pxqembd^2+pyqembd^2) > 70MeV/c is required; + the embedded quark and antiquark have pz=0. +xembd, yembd: the embedded quark and antiquark jets have z=0 initially. Note: + the x-axis is defined as the direction along the impact parameter. +nsembd: number of soft pions to be embedded with each high-Pt parton + in the embedded jet pair. +psembd: Momentum of each embedded soft pion in GeV. +tmaxembd: maximum angle(rad) of embedded soft pions relative to high-Pt parton. +%%%%%%%%%% User modification of nuclear shadowing: +ishadow: set to 1 to enable users to adjust nuclear shadowing + provided the shadowing flag IHPR2(6) is turned on; default value is 0. +dshadow: valid when ishadow=1; this parameter modifies the HIJING shadowing + parameterization Ra(x,r)==1+fa(x,r) via Ra(x,r)==1+fa(x,r)*dshadow, + so the value of 0.d0 turns off shadowing + and the value of 1.d0 uses the default HIJING shadowing; + currently limited to 0.d0<=dshadow<=1.d0 to make sure Ra(x,r)>0. +iphirp: set to 1 to turn on random orientation of reaction plane (D=0) diff --git a/MC/config/common/ini/GeneratorAMPTpp536TeV.ini b/MC/config/common/ini/GeneratorAMPTpp536TeV.ini new file mode 100644 index 000000000..b59152f81 --- /dev/null +++ b/MC/config/common/ini/GeneratorAMPTpp536TeV.ini @@ -0,0 +1,3 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/external/generator/generator_AMPT.C +funcName=generateAMPT("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/ampt/generator/pp_536TeV_AMPT.ampt") diff --git a/MC/config/common/ini/tests/GeneratorAMPTpp536TeV.C b/MC/config/common/ini/tests/GeneratorAMPTpp536TeV.C new file mode 100644 index 000000000..78a31725e --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorAMPTpp536TeV.C @@ -0,0 +1,35 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader* eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + // Check if there are 100 events, as simulated in the o2dpg-test + auto nEvents = tree->GetEntries(); + if (nEvents != 100) { + std::cerr << "Expected 100 events, got " << nEvents << "\n"; + return 1; + } + // Beam momentum per nucleon for Pb-Pb at sqrt(s_NN) = 5.36 TeV + for (Long64_t i = 0; i < nEvents; ++i) { + tree->GetEntry(i); + if (tracks->empty()) { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + } + return 0; +} From 752c3e7d2edcc434a3786483cf3ea8dad64cb8c3 Mon Sep 17 00:00:00 2001 From: Daiki Sekihata Date: Thu, 1 Oct 2026 00:22:58 +0200 Subject: [PATCH 089/144] [MC/PWGEM] remove unnecessary files (#2484) --- .../Generator_pythia8_GapTriggered_LFee.C | 456 ------------------ .../PWGEM/ini/Generator_GapTriggered_LFee.ini | 9 - ...erator_GapTriggered_LFee_all_np1_gap10.ini | 9 - ...nerator_GapTriggered_LFee_all_np1_gap3.ini | 9 - ...nerator_GapTriggered_LFee_all_np1_gap5.ini | 9 - ...nerator_GapTriggered_LFee_all_np1_gap8.ini | 9 - ...ator_GapTriggered_LFee_random_np1_gap0.ini | 9 - ...ator_GapTriggered_LFee_random_np1_gap2.ini | 9 - ...ator_GapTriggered_LFee_random_np1_gap4.ini | 9 - ...ator_GapTriggered_LFee_random_np1_gap6.ini | 9 - ...ator_GapTriggered_LFee_random_np3_gap0.ini | 9 - ...ator_GapTriggered_LFee_random_np5_gap0.ini | 9 - .../ini/tests/Generator_GapTriggered_LFee.C | 64 --- ...redPythia8GapTriggeredLFee_pp_np1_gap10.sh | 50 -- ...oredPythia8GapTriggeredLFee_pp_np1_gap3.sh | 50 -- ...oredPythia8GapTriggeredLFee_pp_np1_gap5.sh | 50 -- ...oredPythia8GapTriggeredLFee_pp_np1_gap8.sh | 50 -- ...hia8GapTriggeredLFee_pp_random_np1_gap2.sh | 50 -- ...hia8GapTriggeredLFee_pp_random_np1_gap4.sh | 50 -- ...hia8GapTriggeredLFee_pp_random_np1_gap6.sh | 50 -- MC/run/PWGEM/runPythia8GapTriggeredLFee_pp.sh | 26 - 21 files changed, 995 deletions(-) delete mode 100644 MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap10.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap3.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap5.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap8.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap0.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap2.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap4.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap6.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np3_gap0.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np5_gap0.ini delete mode 100644 MC/config/PWGEM/ini/tests/Generator_GapTriggered_LFee.C delete mode 100644 MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap10.sh delete mode 100644 MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap3.sh delete mode 100644 MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap5.sh delete mode 100644 MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap8.sh delete mode 100644 MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap2.sh delete mode 100644 MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap4.sh delete mode 100644 MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap6.sh delete mode 100644 MC/run/PWGEM/runPythia8GapTriggeredLFee_pp.sh diff --git a/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C b/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C deleted file mode 100644 index 34b4f9840..000000000 --- a/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C +++ /dev/null @@ -1,456 +0,0 @@ -R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/EvtGen) -R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/external/generator) -R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT/MC/config/PWGEM/external/generator) -R__LOAD_LIBRARY(libpythia6) -R__LOAD_LIBRARY(libEGPythia6) -#include "GeneratorEvtGen.C" -#include "GeneratorCocktail.C" -#include "Generators/GeneratorPythia8.h" -#include "Pythia8/Pythia.h" - -using namespace std; -using namespace Pythia8; - -namespace o2 { -namespace eventgen { - -class CocktailParam : public GeneratorTGenerator { -public: - CocktailParam(GeneratorParam *thisGenerator) - : GeneratorTGenerator(thisGenerator->GetName()) { - setTGenerator(thisGenerator); - }; - - ~CocktailParam() { delete thisGenerator; }; - -private: - GeneratorParam *thisGenerator = nullptr; -}; - -class O2_GeneratorParamJpsi : public GeneratorTGenerator { - public: - O2_GeneratorParamJpsi() : GeneratorTGenerator("ParamJpsi") - { - paramJpsi = new GeneratorParam(1, -1, Flat, Flat, V2JPsi, IpJPsi); - paramJpsi->SetMomentumRange(0., 25.); - paramJpsi->SetPtRange(0., 25.); - paramJpsi->SetYRange(-1.2, 1.2); - paramJpsi->SetPhiRange(0., 360.); - paramJpsi->SetDecayer(new TPythia6Decayer()); // Pythia - paramJpsi->SetForceDecay(kNoDecay); // particle left undecayed - setTGenerator(paramJpsi); - }; - - ~O2_GeneratorParamJpsi() - { - delete paramJpsi; - }; - - Bool_t Init() override - { - GeneratorTGenerator::Init(); - paramJpsi->Init(); - return true; - } - - void SetNSignalPerEvent(Int_t nsig) { paramJpsi->SetNumberParticles(nsig); } - - //-------------------------------------------------------------------------// - static Double_t Flat(const Double_t* px, const Double_t* /*dummy*/) - { - return 1.; - } - - //-------------------------------------------------------------------------// - static Double_t V2JPsi(const Double_t* /*dummy*/, const Double_t* /*dummy*/) - { - // jpsi v2 - return 0.; - } - - //-------------------------------------------------------------------------// - static Int_t IpJPsi(TRandom*) - { - return 443; - } - - private: - GeneratorParam* paramJpsi = nullptr; -}; - -class O2_GeneratorParamPsi : public GeneratorTGenerator { - public: - O2_GeneratorParamPsi() : GeneratorTGenerator("ParamPsi") - { - paramPsi = new GeneratorParam(1, -1, PtPsi, YPsi, V2Psi, IpPsi); - paramPsi->SetMomentumRange(0., 25.); // Momentum range added from me - paramPsi->SetPtRange(0., 25.); // transverse of momentum range - paramPsi->SetYRange(-1.2, 1.2); // rapidity range - paramPsi->SetPhiRange(0., 360.); // phi range - paramPsi->SetDecayer(new TPythia6Decayer()); // Pythia decayer - paramPsi->SetForceDecay(kNoDecay); // particle left undecayed - setTGenerator(paramPsi); - }; - - ~O2_GeneratorParamPsi() - { - delete paramPsi; - }; - - Bool_t Init() override - { - GeneratorTGenerator::Init(); - paramPsi->Init(); - return true; - } - void SetNSignalPerEvent(Int_t nsig) { paramPsi->SetNumberParticles(nsig); } - - //-------------------------------------------------------------------------// - static Double_t PtPsi(const Double_t* px, const Double_t* /*dummy*/) - { - return 1.; - } - - //-------------------------------------------------------------------------// - static Double_t YPsi(const Double_t* py, const Double_t* /*dummy*/) - { - return 1.; - } - - //-------------------------------------------------------------------------// - static Double_t V2Psi(const Double_t* /*dummy*/, const Double_t* /*dummy*/) - { - // psi(2s) v2 - return 0.; - } - - //-------------------------------------------------------------------------// - static Int_t IpPsi(TRandom*) - { - return 100443; - } - - private: - GeneratorParam* paramPsi = nullptr; -}; - -//my generator class -class GeneratorPythia8GapTriggeredLFee : public GeneratorPythia8 { - - public: - GeneratorPythia8GapTriggeredLFee() : GeneratorPythia8() { - mGeneratedEvents = 0; - mInverseTriggerRatio = 1; - fGeneratorCocktail = 0x0; - mMode = -1; - mTargetPDG = 0; - }; - - GeneratorPythia8GapTriggeredLFee(int lInputTriggerRatio, float yMin, float yMax, int nPart, int mode) : GeneratorPythia8() { - mGeneratedEvents = 0; - mInverseTriggerRatio = lInputTriggerRatio; - mMode = mode; - // LMee cocktail settings: - float minPt = 0; - float maxPt = 25; - float phiMin = 0.; - float phiMax = 360.; - Weighting_t weightMode = kNonAnalog; - - //create cocktail generator : pi0, eta, eta', rho, omega, phi, j/psi, psi(2s) - fGeneratorCocktail = new o2::eventgen::GeneratorEvtGen(); - - // EXODUS decayer - TString O2DPG_ROOT = TString(getenv("O2DPG_MC_CONFIG_ROOT")); - auto decayer = new PythiaDecayerConfig(); - decayer->SetDecayerExodus(); - TString useLMeeDecaytable = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/decaytables/decaytable_LMee.dat"; - useLMeeDecaytable=useLMeeDecaytable.ReplaceAll("$O2DPG_MC_CONFIG_ROOT",O2DPG_ROOT); - useLMeeDecaytable=useLMeeDecaytable.ReplaceAll("${O2DPG_MC_CONFIG_ROOT}",O2DPG_ROOT); - decayer->SetDecayTableFile(useLMeeDecaytable.Data()); - decayer->ReadDecayTable(); - - //Param - GeneratorParamEMlib *emlib = new GeneratorParamEMlib(); - - // pi0 - auto genPizero = new GeneratorParam(nPart, emlib, GeneratorParamEMlib::kPizero, "pizero"); // 111 - genPizero->SetName("pizero"); - genPizero->SetMomentumRange(0., 25.); - genPizero->SetPtRange(minPt, maxPt); - genPizero->SetYRange(yMin, yMax); - genPizero->SetPhiRange(phiMin, phiMax); - genPizero->SetWeighting(weightMode); // flat pt, y and v2 zero - genPizero->SetDecayer(decayer); // EXOUS; - genPizero->SetForceDecay(kDiElectronEM); // Dielectrons - genPizero->SetForceGammaConversion(kFALSE); - genPizero->SetSelectAll(kTRUE); // Store also the gamma in pi0->e+e-gamma - genPizero->Init(); - CocktailParam *newgenpizero = new CocktailParam(genPizero); - - // eta - auto geneta = new GeneratorParam(nPart,emlib,GeneratorParamEMlib::kEta,"eta"); // 221 - geneta->SetName("eta"); - geneta->SetMomentumRange(0., 25.); - geneta->SetPtRange(minPt, maxPt); - geneta->SetYRange(yMin, yMax); - geneta->SetPhiRange(phiMin, phiMax); - geneta->SetWeighting(weightMode); // flat pt, y and v2 zero - geneta->SetDecayer(decayer); // EXOUS; - geneta->SetForceDecay(kDiElectronEM); // Dielectrons - geneta->SetForceGammaConversion(kFALSE); - geneta->SetSelectAll(kTRUE); // Store also the gamma in pi0->e+e-gamma - geneta->Init(); - CocktailParam *newgeneta = new CocktailParam(geneta); - - // etaprime - auto genetaprime = new GeneratorParam(nPart,emlib,GeneratorParamEMlib::kEtaprime,"etaprime"); // 331 - genetaprime->SetName("etaprime"); - genetaprime->SetMomentumRange(0., 25.); - genetaprime->SetPtRange(minPt, maxPt); - genetaprime->SetYRange(yMin, yMax); - genetaprime->SetPhiRange(phiMin, phiMax); - genetaprime->SetWeighting(weightMode); // flat pt, y and v2 zero - genetaprime->SetDecayer(decayer); // EXOUS; - genetaprime->SetForceDecay(kDiElectronEM); // Dielectrons - genetaprime->SetForceGammaConversion(kFALSE); - genetaprime->SetSelectAll(kTRUE); // Store also the gamma in pi0->e+e-gamma - genetaprime->Init(); - CocktailParam *newgenetaprime = new CocktailParam(genetaprime); - - // rho - auto genrho = new GeneratorParam(nPart,emlib,GeneratorParamEMlib::kRho0,"rho"); // 113 - genrho->SetName("rho"); - genrho->SetMomentumRange(0., 25.); - genrho->SetPtRange(minPt, maxPt); - genrho->SetYRange(yMin, yMax); - genrho->SetPhiRange(phiMin, phiMax); - genrho->SetWeighting(weightMode); // flat pt, y and v2 zero - genrho->SetDecayer(decayer); // EXOUS; - genrho->SetForceDecay(kDiElectronEM); // Dielectrons - genrho->SetForceGammaConversion(kFALSE); - genrho->SetSelectAll(kTRUE); // Store also the gamma in pi0->e+e-gamma - genrho->Init(); - CocktailParam *newgenrho = new CocktailParam(genrho); - - // Omega - auto genomega = new GeneratorParam(nPart,emlib,GeneratorParamEMlib::kOmega,"omega"); //223 - genomega->SetName("omega"); - genomega->SetMomentumRange(0., 25.); - genomega->SetPtRange(minPt, maxPt); - genomega->SetYRange(yMin, yMax); - genomega->SetPhiRange(phiMin, phiMax); - genomega->SetWeighting(weightMode); // flat pt, y and v2 zero - genomega->SetDecayer(decayer); // EXOUS; - genomega->SetForceDecay(kDiElectronEM); // Dielectrons - genomega->SetForceGammaConversion(kFALSE); - genomega->SetSelectAll(kTRUE); // Store also the gamma in pi0->e+e-gamma - genomega->Init(); - CocktailParam *newgenomega = new CocktailParam(genomega); - - // phi - auto genphi = new GeneratorParam(nPart,emlib,GeneratorParamEMlib::kPhi,"phi"); //333 - genphi->SetName("phi"); - genphi->SetMomentumRange(0., 25.); - genphi->SetPtRange(minPt, maxPt); - genphi->SetYRange(yMin, yMax); - genphi->SetPhiRange(phiMin, phiMax); - genphi->SetWeighting(weightMode); // flat pt, y and v2 zero - genphi->SetDecayer(decayer); // EXOUS; - genphi->SetForceDecay(kDiElectronEM); // Dielectrons - genphi->SetForceGammaConversion(kFALSE); - genphi->SetSelectAll(kTRUE); // Store also the gamma in pi0->e+e-gamma - genphi->Init(); - CocktailParam *newgenphi = new CocktailParam(genphi); - -// // J/psi and psi(2S) need to be slightly different since no EXODUS but EvtGen decayer -// auto genJpsi = new o2::eventgen::O2_GeneratorParamJpsi; -// genJpsi->SetNSignalPerEvent(nPart); // signal per event for J/Psi -// -// auto genPsi = new o2::eventgen::O2_GeneratorParamPsi; -// genPsi->SetNSignalPerEvent(nPart); // signal per event for Psi(2s) -// -// TString pdgs = "443;100443"; -// std::string spdg; -// TObjArray* obj = pdgs.Tokenize(";"); -// fGeneratorCocktail->SetSizePdg(obj->GetEntriesFast()); -// for (int i = 0; i < obj->GetEntriesFast(); i++) { -// spdg = obj->At(i)->GetName(); -// fGeneratorCocktail->AddPdg(std::stoi(spdg), i); -// printf("PDG %d \n", std::stoi(spdg)); -// } -// fGeneratorCocktail->SetForceDecay(kEvtDiElectron); - - int target_pdg = 1; - - if (mMode < 0) { - target_pdg = 1; - cout << "all-particle mode is selected. all 6 mesons are injected in each event" << endl; - cout << "add pi0 for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenpizero, 1); - cout << "add eta for signal" << endl; - fGeneratorCocktail->AddGenerator(newgeneta, 1); - cout << "add etaprime for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenetaprime, 1); - cout << "add rho for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenrho, 1); - cout << "add omega for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenomega, 1); - cout << "add phi for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenphi, 1); - //cout << "add j/psi for signal" << endl; - //fGeneratorCocktail->AddGenerator(genJpsi, 1); // add cocktail --> J/Psi - //cout << "add psi(2S) for signal" << endl; - //fGeneratorCocktail->AddGenerator(genPsi, 1); // add cocktail --> Psi(2s) - } else if (mMode < 100) { - cout << "1-particle Mode is selected. 1 meson selected randomly per job is injected in each event" << endl; - TRandom3 *r3 = new TRandom3(0); - double rndm = r3->Rndm(); - printf("rndm = %f\n", rndm); - - if(rndm < 1/6.) { - cout << "add pi0 for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenpizero, 1); - target_pdg = 111; - } else if (rndm < 2/6.) { - cout << "add eta for signal" << endl; - fGeneratorCocktail->AddGenerator(newgeneta, 1); - target_pdg = 221; - } else if (rndm < 3/6.) { - cout << "add etaprime for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenetaprime, 1); - target_pdg = 331; - } else if (rndm < 4/6.) { - cout << "add rho for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenrho, 1); - target_pdg = 113; - } else if (rndm < 5/6.) { - cout << "add omega for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenomega, 1); - target_pdg = 223; - } else if (rndm < 6/6.) { - cout << "add phi for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenphi, 1); - target_pdg = 333; - } - //else if (rndm < 7/8.) { - // cout << "add j/psi for signal" << endl; - // fGeneratorCocktail->AddGenerator(genJpsi, 1); // add cocktail --> J/Psi - // target_pdg = 443; - //} else { - // cout << "add psi(2S) for signal" << endl; - // fGeneratorCocktail->AddGenerator(genPsi, 1); // add cocktail --> Psi(2s) - // target_pdg = 100443; - //} - delete r3; - } else { //directly select meson pdg - target_pdg = mMode; - switch (mMode) { - case 111 : - cout << "add pi0 for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenpizero, 1); - break; - case 221 : - cout << "add eta for signal" << endl; - fGeneratorCocktail->AddGenerator(newgeneta, 1); - break; - case 331 : - cout << "add etaprime for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenetaprime, 1); - break; - case 113 : - cout << "add rho for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenrho, 1); - break; - case 223 : - cout << "add omega for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenomega, 1); - break; - case 333 : - cout << "add phi for signal" << endl; - fGeneratorCocktail->AddGenerator(newgenphi, 1); - break; -// case 443 : -// cout << "add j/psi for signal" << endl; -// fGeneratorCocktail->AddGenerator(genJpsi, 1); // add cocktail --> J/Psi -// break; -// case 100443 : -// cout << "add psi(2S) for signal" << endl; -// fGeneratorCocktail->AddGenerator(genPsi, 1); // add cocktail --> Psi(2s) -// break; - default: - cout << "!WARNING! default : nothing is added to cocktail generator" << endl; - target_pdg = 1; - break; - } - } - - // print debug - fGeneratorCocktail->PrintDebug(); - fGeneratorCocktail->Init(); - - cout << "target_pdg for subGeneratorId is " << target_pdg << endl; - addSubGenerator(0, "gap mb pythia"); - addSubGenerator(target_pdg, "injected cocktail"); - mTargetPDG = target_pdg; - }; - - ~GeneratorPythia8GapTriggeredLFee() = default; - - protected: - bool generateEvent() override - { - GeneratorPythia8::generateEvent(); - - if (mGeneratedEvents % mInverseTriggerRatio == 0){ // add injected prompt signals to the stack - fGeneratorCocktail->generateEvent(); - notifySubGenerator(mTargetPDG); - } else { // gap event - notifySubGenerator(0); - } - mGeneratedEvents++; - return true; - } - - bool importParticles() override - { - GeneratorPythia8::importParticles(); - - bool genOk = false; - if ((mGeneratedEvents-1) % mInverseTriggerRatio == 0){ // add injected prompt signals to the stack - fGeneratorCocktail->importParticles(); - int originalSize = mParticles.size(); - for(int ipart=0; ipart < fGeneratorCocktail->getParticles().size(); ipart++){ - TParticle part = TParticle(fGeneratorCocktail->getParticles().at(ipart)); - if(part.GetFirstMother() >= 0) part.SetFirstMother(part.GetFirstMother() + originalSize); - if(part.GetFirstDaughter() >= 0) part.SetFirstDaughter(part.GetFirstDaughter() + originalSize); - if(part.GetLastDaughter() >= 0) part.SetLastDaughter(part.GetLastDaughter() + originalSize); - mParticles.push_back(part); - // encodeParticleStatusAndTracking method already called in GeneratorEvtGen.C - } - fGeneratorCocktail->clearParticles(); - } - - return true; - } - - private: - GeneratorEvtGen *fGeneratorCocktail; - // Control gap-triggering - unsigned long long mGeneratedEvents; - int mInverseTriggerRatio; - int mMode; - int mTargetPDG; -}; - -} // close eventgen -} // close o2 - -// Predefined generators: // this function should be called in ini file. -FairGenerator *GeneratorPythia8GapTriggeredLFee_ForEM(int inputTriggerRatio = 5, float yMin=-1.2, float yMax=1.2, int nPart = 1, int mode = -1) { - auto myGen = new GeneratorPythia8GapTriggeredLFee(inputTriggerRatio, yMin, yMax, nPart, mode); - auto seed = (gRandom->TRandom::GetSeed() % 900000000); - myGen->readString("Random:setSeed on"); - myGen->readString("Random:seed " + std::to_string(seed)); - return myGen; -} diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee.ini deleted file mode 100644 index 99f440adb..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(5+1, -1.2, +1.2, 1, -1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap10.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap10.ini deleted file mode 100644 index 6973e0d33..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap10.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(10+1, -1.2, +1.2, 1, -1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap3.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap3.ini deleted file mode 100644 index 4113292c2..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap3.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(3+1, -1.2, +1.2, 1, -1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap5.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap5.ini deleted file mode 100644 index 99f440adb..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap5.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(5+1, -1.2, +1.2, 1, -1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap8.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap8.ini deleted file mode 100644 index 6d843e227..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_all_np1_gap8.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(8+1, -1.2, +1.2, 1, -1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap0.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap0.ini deleted file mode 100644 index 9f7045a75..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap0.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(0+1, -1.2, +1.2, 1, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap2.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap2.ini deleted file mode 100644 index 5269d3645..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap2.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(2+1, -1.2, +1.2, 1, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap4.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap4.ini deleted file mode 100644 index 029ffd48f..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap4.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(4+1, -1.2, +1.2, 1, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap6.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap6.ini deleted file mode 100644 index a77e21efa..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np1_gap6.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(6+1, -1.2, +1.2, 1, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np3_gap0.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np3_gap0.ini deleted file mode 100644 index de7bb740d..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np3_gap0.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(0+1, -1.2, +1.2, 3, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np5_gap0.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np5_gap0.ini deleted file mode 100644 index 0b8cfcc94..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFee_random_np5_gap0.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFee.C -funcName = GeneratorPythia8GapTriggeredLFee_ForEM(0+1, -1.2, +1.2, 5, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/tests/Generator_GapTriggered_LFee.C b/MC/config/PWGEM/ini/tests/Generator_GapTriggered_LFee.C deleted file mode 100644 index 98bab329c..000000000 --- a/MC/config/PWGEM/ini/tests/Generator_GapTriggered_LFee.C +++ /dev/null @@ -1,64 +0,0 @@ -int External() -{ - - int checkPdgDecay = -11; - std::string path{"o2sim_Kine.root"}; - TFile file(path.c_str(), "READ"); - if (file.IsZombie()) { - std::cerr << "Cannot open ROOT file " << path << "\n"; - return 1; - } - - auto tree = (TTree*)file.Get("o2sim"); - std::vector* tracks{}; - tree->SetBranchAddress("MCTrack", &tracks); - - int nMesons{}; - int nMesonsDiElectronDecay{}; - auto nEvents = tree->GetEntries(); - - for (int i = 0; i < nEvents; i++) { - tree->GetEntry(i); - for (auto& track : *tracks) { - auto pdg = track.GetPdgCode(); - auto y = track.GetRapidity(); - if ((pdg == 111) || (pdg == 221) || (pdg == 331) || (pdg == 223) || (pdg == 113) || (pdg == 333) || (pdg == 443) || (pdg == 100443)) { - if ((y>-1.2) && (y<1.2)) { - nMesons++; - Int_t counterel = 0; - Int_t counterpos = 0; - int k1 = track.getFirstDaughterTrackId(); - int k2 = track.getLastDaughterTrackId(); - // k1 < k2 and no -1 for k2 - for (int d=k1; d <= k2; d++) { - if (d>0) { - auto decay = (*tracks)[d]; - int pdgdecay = decay.GetPdgCode(); - if (pdgdecay == 11) { - counterel++; - } - if (pdgdecay == -11) { - counterpos++; - } - } - } - if ((counterel>0) && (counterpos>0)) nMesonsDiElectronDecay++; - } - } - } - } - - std::cout << "#events: " << nEvents << "\n" - << "#mesons: " << nMesons << "\n"; - -// if (nMesonsDiElectronDecay < (8*nEvents)) { -// std::cerr << "One should have at least 8 mesons that decay into dielectrons per event.\n"; -// return 1; -// } -// if (nMesons < nEvents) { -// std::cerr << "One meson per event should be produced.\n"; -// return 1; -// } - - return 0; -} diff --git a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap10.sh b/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap10.sh deleted file mode 100644 index eaaf8b423..000000000 --- a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap10.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# -# Steering script for LF->ee enhanced dielectron MC anchored to LHC22o apass6 -# - -# example anchoring -# taken from https://its.cern.ch/jira/browse/O2-4586 -export ALIEN_JDL_LPMANCHORPASSNAME=apass6 -export ALIEN_JDL_MCANCHOR=apass6 -export ALIEN_JDL_CPULIMIT=8 -export ALIEN_JDL_LPMRUNNUMBER=526641 -export ALIEN_JDL_LPMPRODUCTIONTYPE=MC -export ALIEN_JDL_LPMINTERACTIONTYPE=pp -export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24b1b -export ALIEN_JDL_LPMANCHORRUN=526641 -export ALIEN_JDL_LPMANCHORPRODUCTION=LHC22o -export ALIEN_JDL_LPMANCHORYEAR=2022 -export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 - -export NTIMEFRAMES=1 -export NSIGEVENTS=20 -export SPLITID=100 -export PRODSPLIT=153 -export CYCLE=0 - -# on the GRID, this is set and used as seed; when set, it takes precedence over SEED -#export ALIEN_PROC_ID=2963436952 -export SEED=0 - -# for pp and 50 events per TF, we launch only 4 workers. -export NWORKERS=4 - -# define the generator via ini file -# use 20/40/40 sampling for different generators -# generate random number -RNDSIG=$(($RANDOM % 100)) - -CONFIGNAME="Generator_GapTriggered_LFee_all_np1_gap10.ini" - -export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" - -# run the central anchor steering script; this includes -# * derive timestamp -# * derive interaction rate -# * extract and prepare configurations (which detectors are contained in the run etc.) -# * run the simulation (and QC) -# To disable QC, uncomment the following line -#export DISABLE_QC=1 -${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap3.sh b/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap3.sh deleted file mode 100644 index 23f9b5952..000000000 --- a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap3.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# -# Steering script for LF->ee enhanced dielectron MC anchored to LHC22o apass6 -# - -# example anchoring -# taken from https://its.cern.ch/jira/browse/O2-4586 -export ALIEN_JDL_LPMANCHORPASSNAME=apass6 -export ALIEN_JDL_MCANCHOR=apass6 -export ALIEN_JDL_CPULIMIT=8 -export ALIEN_JDL_LPMRUNNUMBER=526641 -export ALIEN_JDL_LPMPRODUCTIONTYPE=MC -export ALIEN_JDL_LPMINTERACTIONTYPE=pp -export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24b1b -export ALIEN_JDL_LPMANCHORRUN=526641 -export ALIEN_JDL_LPMANCHORPRODUCTION=LHC22o -export ALIEN_JDL_LPMANCHORYEAR=2022 -export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 - -export NTIMEFRAMES=1 -export NSIGEVENTS=20 -export SPLITID=100 -export PRODSPLIT=153 -export CYCLE=0 - -# on the GRID, this is set and used as seed; when set, it takes precedence over SEED -#export ALIEN_PROC_ID=2963436952 -export SEED=0 - -# for pp and 50 events per TF, we launch only 4 workers. -export NWORKERS=4 - -# define the generator via ini file -# use 20/40/40 sampling for different generators -# generate random number -RNDSIG=$(($RANDOM % 100)) - -CONFIGNAME="Generator_GapTriggered_LFee_all_np1_gap3.ini" - -export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" - -# run the central anchor steering script; this includes -# * derive timestamp -# * derive interaction rate -# * extract and prepare configurations (which detectors are contained in the run etc.) -# * run the simulation (and QC) -# To disable QC, uncomment the following line -#export DISABLE_QC=1 -${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap5.sh b/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap5.sh deleted file mode 100644 index e977da87b..000000000 --- a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap5.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# -# Steering script for LF->ee enhanced dielectron MC anchored to LHC22o apass6 -# - -# example anchoring -# taken from https://its.cern.ch/jira/browse/O2-4586 -export ALIEN_JDL_LPMANCHORPASSNAME=apass6 -export ALIEN_JDL_MCANCHOR=apass6 -export ALIEN_JDL_CPULIMIT=8 -export ALIEN_JDL_LPMRUNNUMBER=526641 -export ALIEN_JDL_LPMPRODUCTIONTYPE=MC -export ALIEN_JDL_LPMINTERACTIONTYPE=pp -export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24b1b -export ALIEN_JDL_LPMANCHORRUN=526641 -export ALIEN_JDL_LPMANCHORPRODUCTION=LHC22o -export ALIEN_JDL_LPMANCHORYEAR=2022 -export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 - -export NTIMEFRAMES=1 -export NSIGEVENTS=20 -export SPLITID=100 -export PRODSPLIT=153 -export CYCLE=0 - -# on the GRID, this is set and used as seed; when set, it takes precedence over SEED -#export ALIEN_PROC_ID=2963436952 -export SEED=0 - -# for pp and 50 events per TF, we launch only 4 workers. -export NWORKERS=4 - -# define the generator via ini file -# use 20/40/40 sampling for different generators -# generate random number -RNDSIG=$(($RANDOM % 100)) - -CONFIGNAME="Generator_GapTriggered_LFee_all_np1_gap5.ini" - -export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" - -# run the central anchor steering script; this includes -# * derive timestamp -# * derive interaction rate -# * extract and prepare configurations (which detectors are contained in the run etc.) -# * run the simulation (and QC) -# To disable QC, uncomment the following line -#export DISABLE_QC=1 -${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap8.sh b/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap8.sh deleted file mode 100644 index 1a5566fba..000000000 --- a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_np1_gap8.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# -# Steering script for LF->ee enhanced dielectron MC anchored to LHC22o apass6 -# - -# example anchoring -# taken from https://its.cern.ch/jira/browse/O2-4586 -export ALIEN_JDL_LPMANCHORPASSNAME=apass6 -export ALIEN_JDL_MCANCHOR=apass6 -export ALIEN_JDL_CPULIMIT=8 -export ALIEN_JDL_LPMRUNNUMBER=526641 -export ALIEN_JDL_LPMPRODUCTIONTYPE=MC -export ALIEN_JDL_LPMINTERACTIONTYPE=pp -export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24b1b -export ALIEN_JDL_LPMANCHORRUN=526641 -export ALIEN_JDL_LPMANCHORPRODUCTION=LHC22o -export ALIEN_JDL_LPMANCHORYEAR=2022 -export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 - -export NTIMEFRAMES=1 -export NSIGEVENTS=20 -export SPLITID=100 -export PRODSPLIT=153 -export CYCLE=0 - -# on the GRID, this is set and used as seed; when set, it takes precedence over SEED -#export ALIEN_PROC_ID=2963436952 -export SEED=0 - -# for pp and 50 events per TF, we launch only 4 workers. -export NWORKERS=4 - -# define the generator via ini file -# use 20/40/40 sampling for different generators -# generate random number -RNDSIG=$(($RANDOM % 100)) - -CONFIGNAME="Generator_GapTriggered_LFee_all_np1_gap8.ini" - -export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" - -# run the central anchor steering script; this includes -# * derive timestamp -# * derive interaction rate -# * extract and prepare configurations (which detectors are contained in the run etc.) -# * run the simulation (and QC) -# To disable QC, uncomment the following line -#export DISABLE_QC=1 -${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap2.sh b/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap2.sh deleted file mode 100644 index 80a00b6cd..000000000 --- a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap2.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# -# Steering script for LF->ee enhanced dielectron MC anchored to LHC22o apass6 -# - -# example anchoring -# taken from https://its.cern.ch/jira/browse/O2-4586 -export ALIEN_JDL_LPMANCHORPASSNAME=apass6 -export ALIEN_JDL_MCANCHOR=apass6 -export ALIEN_JDL_CPULIMIT=8 -export ALIEN_JDL_LPMRUNNUMBER=526641 -export ALIEN_JDL_LPMPRODUCTIONTYPE=MC -export ALIEN_JDL_LPMINTERACTIONTYPE=pp -export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24b1b -export ALIEN_JDL_LPMANCHORRUN=526641 -export ALIEN_JDL_LPMANCHORPRODUCTION=LHC22o -export ALIEN_JDL_LPMANCHORYEAR=2022 -export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 - -export NTIMEFRAMES=1 -export NSIGEVENTS=20 -export SPLITID=100 -export PRODSPLIT=153 -export CYCLE=0 - -# on the GRID, this is set and used as seed; when set, it takes precedence over SEED -#export ALIEN_PROC_ID=2963436952 -export SEED=0 - -# for pp and 50 events per TF, we launch only 4 workers. -export NWORKERS=4 - -# define the generator via ini file -# use 20/40/40 sampling for different generators -# generate random number -RNDSIG=$(($RANDOM % 100)) - -CONFIGNAME="Generator_GapTriggered_LFee_random_np1_gap2.ini" - -export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" - -# run the central anchor steering script; this includes -# * derive timestamp -# * derive interaction rate -# * extract and prepare configurations (which detectors are contained in the run etc.) -# * run the simulation (and QC) -# To disable QC, uncomment the following line -#export DISABLE_QC=1 -${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap4.sh b/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap4.sh deleted file mode 100644 index 6c8706265..000000000 --- a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap4.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# -# Steering script for LF->ee enhanced dielectron MC anchored to LHC22o apass6 -# - -# example anchoring -# taken from https://its.cern.ch/jira/browse/O2-4586 -export ALIEN_JDL_LPMANCHORPASSNAME=apass6 -export ALIEN_JDL_MCANCHOR=apass6 -export ALIEN_JDL_CPULIMIT=8 -export ALIEN_JDL_LPMRUNNUMBER=526641 -export ALIEN_JDL_LPMPRODUCTIONTYPE=MC -export ALIEN_JDL_LPMINTERACTIONTYPE=pp -export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24b1b -export ALIEN_JDL_LPMANCHORRUN=526641 -export ALIEN_JDL_LPMANCHORPRODUCTION=LHC22o -export ALIEN_JDL_LPMANCHORYEAR=2022 -export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 - -export NTIMEFRAMES=1 -export NSIGEVENTS=20 -export SPLITID=100 -export PRODSPLIT=153 -export CYCLE=0 - -# on the GRID, this is set and used as seed; when set, it takes precedence over SEED -#export ALIEN_PROC_ID=2963436952 -export SEED=0 - -# for pp and 50 events per TF, we launch only 4 workers. -export NWORKERS=4 - -# define the generator via ini file -# use 20/40/40 sampling for different generators -# generate random number -RNDSIG=$(($RANDOM % 100)) - -CONFIGNAME="Generator_GapTriggered_LFee_random_np1_gap4.ini" - -export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" - -# run the central anchor steering script; this includes -# * derive timestamp -# * derive interaction rate -# * extract and prepare configurations (which detectors are contained in the run etc.) -# * run the simulation (and QC) -# To disable QC, uncomment the following line -#export DISABLE_QC=1 -${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap6.sh b/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap6.sh deleted file mode 100644 index b01bf810d..000000000 --- a/MC/run/PWGEM/runAnchoredPythia8GapTriggeredLFee_pp_random_np1_gap6.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# -# Steering script for LF->ee enhanced dielectron MC anchored to LHC22o apass6 -# - -# example anchoring -# taken from https://its.cern.ch/jira/browse/O2-4586 -export ALIEN_JDL_LPMANCHORPASSNAME=apass6 -export ALIEN_JDL_MCANCHOR=apass6 -export ALIEN_JDL_CPULIMIT=8 -export ALIEN_JDL_LPMRUNNUMBER=526641 -export ALIEN_JDL_LPMPRODUCTIONTYPE=MC -export ALIEN_JDL_LPMINTERACTIONTYPE=pp -export ALIEN_JDL_LPMPRODUCTIONTAG=LHC24b1b -export ALIEN_JDL_LPMANCHORRUN=526641 -export ALIEN_JDL_LPMANCHORPRODUCTION=LHC22o -export ALIEN_JDL_LPMANCHORYEAR=2022 -export ALIEN_JDL_OUTPUT=*.dat@disk=1,*.txt@disk=1,*.root@disk=2 - -export NTIMEFRAMES=1 -export NSIGEVENTS=20 -export SPLITID=100 -export PRODSPLIT=153 -export CYCLE=0 - -# on the GRID, this is set and used as seed; when set, it takes precedence over SEED -#export ALIEN_PROC_ID=2963436952 -export SEED=0 - -# for pp and 50 events per TF, we launch only 4 workers. -export NWORKERS=4 - -# define the generator via ini file -# use 20/40/40 sampling for different generators -# generate random number -RNDSIG=$(($RANDOM % 100)) - -CONFIGNAME="Generator_GapTriggered_LFee_random_np1_gap6.ini" - -export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME" - -# run the central anchor steering script; this includes -# * derive timestamp -# * derive interaction rate -# * extract and prepare configurations (which detectors are contained in the run etc.) -# * run the simulation (and QC) -# To disable QC, uncomment the following line -#export DISABLE_QC=1 -${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh diff --git a/MC/run/PWGEM/runPythia8GapTriggeredLFee_pp.sh b/MC/run/PWGEM/runPythia8GapTriggeredLFee_pp.sh deleted file mode 100644 index 8b34e2b78..000000000 --- a/MC/run/PWGEM/runPythia8GapTriggeredLFee_pp.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# make sure O2DPG + O2 is loaded -[ ! "${O2DPG_ROOT}" ] && echo "Error: This needs O2DPG loaded" && exit 1 -[ ! "${O2_ROOT}" ] && echo "Error: This needs O2 loaded" && exit 1 - -# ----------- SETUP LOCAL CCDB CACHE -------------------------- -export ALICEO2_CCDB_LOCALCACHE=$PWD/.ccdb - - - -NSIGEVENTS=${NSIGEVENTS:-10} -NWORKERS=${NWORKERS:-8} -NTIMEFRAMES=${NTIMEFRAMES:-1} -INTRATE=${INTRATE:-500000} -GAP=${GAP:-5} -NP=${NP:-1} - -CONFIGNAME="Generator_GapTriggered_LFee_all_np${NP}_gap${GAP}.ini" - -${O2DPG_ROOT}/MC/bin/o2dpg_sim_workflow.py -eCM 13600 -col pp -gen external -j ${NWORKERS} -ns ${NSIGEVENTS} -tf ${NTIMEFRAMES} -e TGeant4 \ - -ini $O2DPG_ROOT/MC/config/PWGEM/ini/$CONFIGNAME \ - -confKeyBkg "Diamond.width[2]=6" -interactionRate ${INTRATE} - -# run workflow -${O2DPG_ROOT}/MC/bin/o2_dpg_workflow_runner.py -f workflow.json -tt aod --cpu-limit 32 From b5ea46e9f1d3a02a5b5d9aac61118703995d0f0d Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Tue, 25 Aug 2026 12:18:36 +0200 Subject: [PATCH 090/144] Add a CCDB TimeMachine timestamp harvester for anchored MC This commit provides a new tool GRID/utils/getCCDBTimeMachineTimestamp.py that estimates a CCDB_TIMEMACHINE_TIMESTAMP for a given run and reco pass, for use as ALIEN_JDL_CCDB_CONDITION_NOT_AFTER in anchorMC.sh. - Derives it from AliEn catalog timestamps of the pass's own per-timeframe reco output for that run, not a guess. - Cross-checks the result against CCDB's own object history for a configurable calibration path. Usage: GRID/utils/getCCDBTimeMachineTimestamp.py --year 2023 --period LHC23s --run 536683 --pass apass4 Related to https://its.cern.ch/jira/browse/O2-6543 --- GRID/utils/getCCDBTimeMachineTimestamp.py | 175 ++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100755 GRID/utils/getCCDBTimeMachineTimestamp.py diff --git a/GRID/utils/getCCDBTimeMachineTimestamp.py b/GRID/utils/getCCDBTimeMachineTimestamp.py new file mode 100755 index 000000000..fb1faea9e --- /dev/null +++ b/GRID/utils/getCCDBTimeMachineTimestamp.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 + +# Estimates a CCDB TimeMachine "not-after" timestamp for a given run + reco pass, +# for use as ALIEN_JDL_CCDB_CONDITION_NOT_AFTER in anchored MC. +# +# Async-reco passes are not themselves pinned to a single CCDB instant: each +# per-timeframe job sees CCDB at its own wall-clock execution time. This tool +# approximates that instant from the AliEn catalog timestamps of the pass's own +# per-timeframe reco output for the given run, since that is the closest thing +# to "when did this run actually get processed under this pass" that is left on +# record. It never guesses when it cannot find that record. +# +# started 25.08.2026; Sandro Wenzel + +import argparse +import json +import os +import statistics +import subprocess +import sys +import urllib.request +from datetime import datetime, timezone + +CCDB_HOST = "http://alice-ccdb.cern.ch" + + +def alien_json(*args): + """Run alien.py with -json and return the parsed 'results' list.""" + cmd = ["alien.py", "-json"] + list(args) + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode != 0: + return None + try: + return json.loads(proc.stdout)["results"] + except (json.JSONDecodeError, KeyError): + return None + + +def find_tf_output_files(pass_dir, run): + """LFNs of root_archive.zip directly inside this run's o2_ctf_run* per-TF + output directories (excludes same-named files from downstream QC/skim/train + jobs that live deeper under the same pass directory).""" + results = alien_json("find", pass_dir, "root_archive.zip") + if not results: + return [] + marker = f"/o2_ctf_run{int(run):08d}_orbit" + return sorted(r["lfn"] for r in results if marker in r["lfn"]) + + +def sample_evenly(items, n): + if len(items) <= n: + return items + step = len(items) / n + return [items[int(i * step)] for i in range(n)] + + +def stat_ctime(lfn): + results = alien_json("stat", lfn) + if not results: + return None + try: + return int(results[0]["ctime"]) + except (KeyError, ValueError, TypeError): + return None + + +def human(ts_ms): + return datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + + +def ccdb_cross_check(path, not_after, timeout=15): + """Report which object a correctly time-machine-pinned query at not_after + would actually resolve to, and flag if CCDB has since been updated for the + same validity window. Never raises; returns None on any failure.""" + url = f"{CCDB_HOST}/browse/{path}/{not_after}" + req = urllib.request.Request(url, headers={"Accept": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + data = json.load(resp) + except Exception as exc: + return {"error": str(exc)} + + objects = data.get("objects", []) + if not objects: + return {"error": "no CCDB object covers this instant"} + + as_of = [o for o in objects if o.get("Created", 0) <= not_after] + newest = max(objects, key=lambda o: o.get("Created", 0)) + result = {"newest": newest, "as_of": None, "outdated": False} + if as_of: + result["as_of"] = max(as_of, key=lambda o: o.get("Created", 0)) + result["outdated"] = newest["Created"] > result["as_of"]["Created"] + return result + + +def describe_object(o): + return (f"Created {human(o['Created'])}, JIRA={o.get('JIRA', '?')}, " + f"comment=\"{o.get('comment', '')}\"") + + +def main(): + parser = argparse.ArgumentParser( + description="Estimate a CCDB TimeMachine not-after timestamp for a run+pass, " + "from AliEn catalog timestamps of that pass's own reco output.") + parser.add_argument("--year", default=os.environ.get("ALIEN_JDL_LPMANCHORYEAR"), + help="data-taking year, e.g. 2023 (default: $ALIEN_JDL_LPMANCHORYEAR)") + parser.add_argument("--period", default=os.environ.get("ALIEN_JDL_LPMANCHORPRODUCTION"), + help="period name, e.g. LHC23s (default: $ALIEN_JDL_LPMANCHORPRODUCTION)") + parser.add_argument("--run", default=os.environ.get("ALIEN_JDL_LPMANCHORRUN"), + help="run number (default: $ALIEN_JDL_LPMANCHORRUN)") + parser.add_argument("--pass", dest="passname", default=os.environ.get("ALIEN_JDL_LPMANCHORPASSNAME"), + help="pass name, e.g. apass4 (default: $ALIEN_JDL_LPMANCHORPASSNAME)") + parser.add_argument("--samples", type=int, default=9, + help="number of per-timeframe output files to sample (default: 9)") + parser.add_argument("--ccdb-check-path", default="TPC/Calib/CorrectionMapV2", + help="CCDB path to cross-check the estimate against (default: TPC/Calib/CorrectionMapV2)") + parser.add_argument("--no-ccdb-check", action="store_true", + help="skip the CCDB cross-check") + args = parser.parse_args() + + missing = [n for n in ("year", "period", "run", "passname") if not getattr(args, n)] + if missing: + parser.error("missing required value(s): " + ", ".join(missing)) + + pass_dir = f"/alice/data/{args.year}/{args.period}/{args.run}/{args.passname}/" + print(f"[getCCDBTimeMachineTimestamp] Looking for TF output under {pass_dir}", file=sys.stderr) + + matches = find_tf_output_files(pass_dir, args.run) + if not matches: + sys.exit(f"[getCCDBTimeMachineTimestamp] No o2_ctf_run{int(args.run):08d}_orbit* output found " + f"under {pass_dir}. Check year/period/run/pass, or this run+pass may not use this " + f"catalog layout. Refusing to guess a timestamp.") + + sampled = sample_evenly(matches, args.samples) + ctimes = [] + for lfn in sampled: + ctime = stat_ctime(lfn) + if ctime is not None: + ctimes.append(ctime) + print(f"[getCCDBTimeMachineTimestamp] {human(ctime)} {lfn}", file=sys.stderr) + + if len(ctimes) < 2: + sys.exit(f"[getCCDBTimeMachineTimestamp] Only got {len(ctimes)} usable timestamp(s) out of " + f"{len(sampled)} sampled files ({len(matches)} found in total); refusing to guess.") + + not_after = int(statistics.median(ctimes)) + spread_h = (max(ctimes) - min(ctimes)) / 3.6e6 + print(f"[getCCDBTimeMachineTimestamp] {len(ctimes)} samples out of {len(matches)} TF outputs found; " + f"span {human(min(ctimes))} .. {human(max(ctimes))} ({spread_h:.1f} h)", file=sys.stderr) + print(f"[getCCDBTimeMachineTimestamp] Recommended not-after: {not_after} ({human(not_after)})", + file=sys.stderr) + + if not args.no_ccdb_check: + check = ccdb_cross_check(args.ccdb_check_path, not_after) + if check is None or "error" in check: + reason = check["error"] if check else "unknown error" + print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {args.ccdb_check_path} skipped: {reason}", + file=sys.stderr) + elif check["as_of"] is None: + print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {args.ccdb_check_path}: no object " + f"existed yet at the recommended timestamp (earliest is {describe_object(check['newest'])})", + file=sys.stderr) + else: + print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {args.ccdb_check_path}: as-of object is " + f"{describe_object(check['as_of'])}", file=sys.stderr) + if check["outdated"]: + print(f"[getCCDBTimeMachineTimestamp] note: CCDB's current object for this window is newer " + f"({describe_object(check['newest'])}) - it postdates this pass's processing and was " + f"not what this run actually saw.", file=sys.stderr) + + print(not_after) + + +if __name__ == "__main__": + main() From 3415bc601f38410ded6eee86c28c0cf5ae29193d Mon Sep 17 00:00:00 2001 From: Farsh86 <133318250+Farsh86@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:00:48 +0200 Subject: [PATCH 091/144] Add NeNe HF generator configurations (D2H ccbar+bbbar gap2 QCDCR) (#2476) * Add NeNe HF generator configurations (D2H ccbar+bbbar gap2 QCDCR) * Add test macro for NeNe HF generator configuration --------- Co-authored-by: Fedir Shirokopetlev --- ...HF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.ini | 9 + ...orHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.C | 190 +++++++++++ ...8_charmhadronic_with_decays_QCDCR_NeNe.cfg | 299 ++++++++++++++++++ 3 files changed, 498 insertions(+) create mode 100755 MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.ini create mode 100644 MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.C create mode 100644 MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_QCDCR_NeNe.cfg diff --git a/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.ini b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.ini new file mode 100755 index 000000000..1e313ae7a --- /dev/null +++ b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.ini @@ -0,0 +1,9 @@ +#NEV_TEST> 16 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_hf.C +funcName=GeneratorPythia8GapTriggeredCharmAndBeauty(2, -1.5, 1.5) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_QCDCR_NeNe.cfg +includePartonEvent=true diff --git a/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.C b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.C new file mode 100644 index 000000000..07c5f8837 --- /dev/null +++ b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap2_QCDCR_NeNe.C @@ -0,0 +1,190 @@ +int External(std::string path = "o2sim_Kine.root") { + + int checkPdgQuarkOne{4}; + int checkPdgQuarkTwo{5}; + float ratioTrigger = 1./2; // one event triggered out of 2 + + std::vector checkPdgHadron{411, 421, 431, 4122, 4132, 4232, 4332}; + std::map>> checkHadronDecays{ // sorted pdg of daughters + {421, { + {-321, 211}, // D0 -> K-, pi+ + {-321, 211, 111}, // D0 -> K-, pi+, pi0 + {213, -321}, // D0 -> rho(770)+, K- + {-313, 111}, // D0 -> Kbar^*(892)0, pi0 + {-323, 211}, // D0 -> K^*(892)-, pi+ + {-211, 211}, // D0 -> pi-, pi+ + {213, -211}, // D0 -> rho(770)+, pi- + {-211, 211, 111}, // D0 -> pi-, pi+, pi0 + {-321, 321}, // D0 -> K-, K+ + }}, + + {411, { + {-321, 211, 211}, // D+ -> K-, pi+, pi+ + {-10311, 211}, // D+ -> Kbar0^*(1430)0, pi+ + {-313, 211}, // D+ -> Kbar^*(892)0, pi+ + {-321, 211, 211, 111}, // D+ -> K-, pi+, pi+, pi0 + {333, 211}, // D+ -> phi(1020)0, pi+ + {-313, 321}, // D+ -> Kbar^*(892)0, K+ + {-10311, 321}, // D+ -> Kbar0^*(1430)0, K+ + {-321, 321, 211}, // D+ -> K-, K+, pi+ + {113, 211}, // D+ -> rho(770)0, pi+ + {225, 211}, // D+ -> f2(1270)0, pi+ + {-211, 211, 211}, // D+ -> pi-, pi+, pi+ + }}, + + {431, { + {333, 211}, // Ds+ -> phi(1020)0, pi+ + {-313, 321}, // Ds+ -> Kbar^*(892)0, K+ + {333, 213}, // Ds+ -> phi(1020)0, rho(770)+ + {113, 211}, // Ds+ -> rho(770)0, pi+ + {225, 211}, // Ds+ -> f2(1270)0, pi+ + {-211, 211, 211}, // Ds+ -> pi-, pi+, pi+ + {313, 211}, // Ds+ -> K^*(892)0, pi+ + {10221, 321}, // Ds+ -> f0(1370)0, K+ + {113, 321}, // Ds+ -> rho(770)0, K+ + {-211, 321, 211}, // Ds+ -> pi-, K+, pi+ + {221, 211}, // Ds+ -> eta, pi+ + }}, + + {4122, { + {2212, -321, 211}, // Lambdac+ -> p, K-, pi+ + {2212, -313}, // Lambdac+ -> p, Kbar^*(892)0 + {2224, -321}, // Lambdac+ -> Delta(1232)++, K- + {102134, 211}, // Lambdac+ -> 102134, pi+ + {2212, 311}, // Lambdac+ -> p, K0 + {2212, -321, 211, 111}, // Lambdac+ -> p, K-, pi+, pi0 + {2212, -211, 211}, // Lambdac+ -> p, pi-, pi+ + {2212, 333}, // Lambdac+ -> p, phi(1020)0 + }}, + + {4232, { + {2212, -321, 211}, // Xic+ -> p, K-, pi+ + {2212, -313}, // Xic+ -> p, Kbar^*(892)0 + {3312, 211, 211}, // Xic+ -> Xi-, pi+, pi+ + {2212, 333}, // Xic+ -> p, phi(1020)0 + {3222, -211, 211}, // Xic+ -> Sigma+, pi-, pi+ + {3324, 211}, // Xic+ -> Xi(1530)0, pi+ + }}, + + {4132, { + {3312, 211}, // Xic0 -> Xi-, pi+ + }}, + + {4332, { + {3334, 211}, // Omegac0 -> Omega-, pi+ + {3312, 211}, // Omegac0 -> Xi-, pi+ + }}, + }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsInjOne{}, nEventsInjTwo{}; + int nQuarksOne{}, nQuarksTwo{}, nSignals{}, nSignalGoodDecay{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (subGeneratorId == 0) { + nEventsMB++; + } else if (subGeneratorId == checkPdgQuarkOne) { + nEventsInjOne++; + } else if (subGeneratorId == checkPdgQuarkTwo) { + nEventsInjTwo++; + } + } + + for (auto &track : *tracks) { + auto pdg = track.GetPdgCode(); + if (std::abs(pdg) == checkPdgQuarkOne) { + nQuarksOne++; + continue; + } + if (std::abs(pdg) == checkPdgQuarkTwo) { + nQuarksTwo++; + continue; + } + if (std::find(checkPdgHadron.begin(), checkPdgHadron.end(), std::abs(pdg)) != checkPdgHadron.end()) { // found signal + nSignals++; // count signal PDG + + std::vector pdgsDecay{}; + std::vector pdgsDecayAntiPart{}; + for (int j{track.getFirstDaughterTrackId()}; j <= track.getLastDaughterTrackId(); ++j) { + auto pdgDau = tracks->at(j).GetPdgCode(); + pdgsDecay.push_back(pdgDau); + if (pdgDau != 333 && pdgDau != 111 && pdgDau != 221 && pdgDau != 113 && pdgDau != 225) { // phi is antiparticle of itself + pdgsDecayAntiPart.push_back(-pdgDau); + } else { + pdgsDecayAntiPart.push_back(pdgDau); + } + } + + std::sort(pdgsDecay.begin(), pdgsDecay.end()); + std::sort(pdgsDecayAntiPart.begin(), pdgsDecayAntiPart.end()); + + for (auto &decay : checkHadronDecays[std::abs(pdg)]) { + std::sort(decay.begin(), decay.end()); + if (pdgsDecay == decay || pdgsDecayAntiPart == decay) { + nSignalGoodDecay++; + break; + } + } + } + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << Form("# events injected with %d quark pair: ", checkPdgQuarkOne) << nEventsInjOne << "\n"; + std::cout << Form("# events injected with %d quark pair: ", checkPdgQuarkTwo) << nEventsInjTwo << "\n"; + std::cout << Form("# %d (anti)quarks: ", checkPdgQuarkOne) << nQuarksOne << "\n"; + std::cout << Form("# %d (anti)quarks: ", checkPdgQuarkTwo) << nQuarksTwo << "\n"; + std::cout <<"# signal hadrons: " << nSignals << "\n"; + std::cout <<"# signal hadrons decaying in the correct channel: " << nSignalGoodDecay << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsInjOne < nEvents * ratioTrigger * 0.5 * 0.95 || nEventsInjOne > nEvents * ratioTrigger * 0.5 * 1.05) { + std::cerr << "Number of generated events injected with " << checkPdgQuarkOne << " different than expected\n"; + return 1; + } + if (nEventsInjTwo < nEvents * ratioTrigger * 0.5 * 0.95 || nEventsInjTwo > nEvents * ratioTrigger * 0.5 * 1.05) { + std::cerr << "Number of generated events injected with " << checkPdgQuarkTwo << " different than expected\n"; + return 1; + } + + if (nQuarksOne < nEvents * ratioTrigger) { // we expect anyway more because the same quark is repeated several time, after each gluon radiation + std::cerr << "Number of generated (anti)quarks " << checkPdgQuarkOne << " lower than expected\n"; + return 1; + } + if (nQuarksTwo < nEvents * ratioTrigger) { // we expect anyway more because the same quark is repeated several time, after each gluon radiation + std::cerr << "Number of generated (anti)quarks " << checkPdgQuarkTwo << " lower than expected\n"; + return 1; + } + + float fracForcedDecays = nSignals ? float(nSignalGoodDecay) / nSignals : 0.0f; + float uncFracForcedDecays = nSignals ? std::sqrt(fracForcedDecays * (1 - fracForcedDecays) / nSignals) : 1.0f; + if (1 - fracForcedDecays > 0.15 + uncFracForcedDecays) { // we put some tolerance (e.g. due to oscillations which might change the final state) + std::cerr << "Fraction of signals decaying into the correct channel " << fracForcedDecays << " lower than expected\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_QCDCR_NeNe.cfg b/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_QCDCR_NeNe.cfg new file mode 100644 index 000000000..e059ebc8c --- /dev/null +++ b/MC/config/PWGHF/pythia8/generator/pythia8_charmhadronic_with_decays_QCDCR_NeNe.cfg @@ -0,0 +1,299 @@ +# Pythia8 configuration file for charm hadronic production in OO collisions at 5.36 TeV + +### NeNe beams +Beams:idA = 1000100200 # Neon +Beams:idB = 1000100200 # Neon +Beams:eCM = 5360.0 ### energy + +### processes +SoftQCD:inelastic on # all inelastic processes + +### decays +Beams:frameType = 1 +ParticleDecays:limitTau0 = on +ParticleDecays:tau0Max = 10. ### match alice: 1cm/c = 10.0mm/c + +### Save some CPU at init of jobs +### To avoid refitting, add the following lines to your configuration file: +HeavyIon:SigFitNGen 0 +HeavyIon:SigFitDefPar 2.15,18.42,0.33 + +Random:setSeed on + +### switching on QCD colour reconnection mode +StringPT:sigma = 0.335 +StringZ:aLund = 0.36 +StringZ:bLund = 0.56 +StringFlav:probQQtoQ = 0.078 +StringFlav:ProbStoUD = 0.2 +StringFlav:probQQ1toQQ0join = 0.0275,0.0275,0.0275,0.0275 +BeamRemnants:remnantMode = 1 +BeamRemnants:saturation = 5 +ColourReconnection:mode = 1 +ColourReconnection:allowDoubleJunRem = off +ColourReconnection:allowJunctions = on +ColourReconnection:timeDilationMode = 0 +ColourReconnection:reconnect = on +ColourReconnection:forceHadronLevelCR = off +MultiPartonInteractions:pT0Ref = 2.12 +ColourReconnection:m0 = 2.9 +ColourReconnection:junctionCorrection = 1.43 +HIMultipartonInteractions:pT0Ref = 2.12 +BeamRemnants:beamJunction = off +HIBeamRemnants:beamJunction = off + +# Correct decay lengths (wrong in PYTHIA8 decay table) +# Lb +5122:tau0 = 0.4390 +# Xic0 +4132:tau0 = 0.0455 +# OmegaC +4332:tau0 = 0.0803 + +### Force golden charm hadrons decay modes for D2H studies +# HF decays +### BR are set to yield 50% of signal in golden channel and uniform abundance of corr. bkg channels (weighted by BR from PDG) +### +### D0 decays +### D0 -> K- π+ (50%) +421:oneChannel = 1 0.50000 0 -321 211 ### D0 -> K- π+ 3.94% +### D0 -> K- π+ π0 (12.50%) +421:addChannel = 1 0.00625 0 -321 211 111 ### D0 -> K- π+ π0 (non-resonant) 1.15% (e.g. 115/(115+231+195+1120)*0.2) +421:addChannel = 1 0.08750 0 213 -321 ### D0 -> rho+ K- 11.2% +421:addChannel = 1 0.01250 0 -313 111 ### D0 -> antiK*0(892) π0 1.95% +421:addChannel = 1 0.01875 0 -323 211 ### D0 -> K*-(892) π+ 2.31% +### D0 -> π- π+ (12.50%) +421:addChannel = 1 0.12500 0 -211 211 ### D0 -> π- π+ (non-resonant) 1.0e-4 +### D0 -> π- π+ π0 (12.50%) +421:addChannel = 1 0.08750 0 213 -211 ### D0 -> rho+ π- 1.01% +421:addChannel = 1 0.03750 0 -211 211 111 ### D0 -> π- π+ π0 (non-resonant) 1.3e-4 +### D0 -> K- K+ (12.50%) +421:addChannel = 1 0.12500 0 -321 321 ### D0 -> K- K+ (non-resonant) 4.08e-3 + +### D+ decays +### D+ -> K- π+ π+ (50%) +411:oneChannel = 1 0.40189 0 -321 211 211 ### D+ -> K- π+ π+ 9.38% +411:addChannel = 1 0.05356 0 -10311 211 ### D+ -> antiK*0(1430) π+ 1.25% +411:addChannel = 1 0.04455 0 -313 211 ### D+ -> K*0(892) pi+ 1.04% +### D+ -> K- π+ π+ π0 (small, 3%) +411:addChannel = 1 0.03000 0 -321 211 211 111 ### D+ -> K- π+ π+ π0 6.25% +### D+ -> K- K+ π+ (36.00%, set 25% for D+ -> φ π+, 11% for the rest) +411:addChannel = 1 0.25000 0 333 211 ### D+ -> φ π+ 2.69e-3 !needed for signal +411:addChannel = 1 0.03929 0 -313 321 ### D+ -> K*0(892) K+ 2.49e-3 +411:addChannel = 1 0.02865 0 -10311 321 ### D+ -> antiK*0(1430) K+ 1.82e-3 +411:addChannel = 1 0.04206 0 -321 321 211 ### D+ -> K- K+ π+ (non-resonant) 2.68e-3 +### D+ -> π- π+ π+ (11.00%) +411:addChannel = 1 0.02911 0 113 211 ### D+ -> rho0 π+ 8.4e-4 +411:addChannel = 1 0.01618 0 225 211 ### D+ -> f2(1270) π+ 4.6e-4 +411:addChannel = 1 0.06471 0 -211 211 211 ### D+ -> π- π+ π+ (non-resonant) 1.0e-4 + +### Ds+ decays +### Ds+ -> K- K+ π+ (50%) +431:oneChannel = 1 0.50000 0 333 211 ### Ds+ -> φ(1020) π+ 2.21% +431:addChannel = 1 0.15000 0 -313 321 ### Ds+ -> antiK*(892) K+ 2.58% +### Ds+ -> K- K+ π+ π0 (small, 2%) +431:addChannel = 1 0.02000 0 333 213 ### Ds+ -> φ(1020) rho 5.50% +### Ds+ -> π- π+ π+ (11.00%) +431:addChannel = 1 0.00220 0 113 211 ### Ds+ -> rho π+ 1.1e-4 +431:addChannel = 1 0.00220 0 225 211 ### Ds+ -> f2(1270) π+ 1.4e-3 +431:addChannel = 1 0.10560 0 -211 211 211 ### Ds+ -> π- π+ π+ 9.12e-3 (s-wave) +### Ds+ -> π- K+ π+ (11.00%) +431:addChannel = 1 0.03080 0 313 211 ### Ds+ -> K*(892)0 π+ 1.67e-3 +431:addChannel = 1 0.02200 0 10221 321 ### Ds+ -> f0(1370) K+ 1.2e-3 +431:addChannel = 1 0.03960 0 113 321 ### Ds+ -> rho0 K+ 2.17e-3 +431:addChannel = 1 0.01760 0 -211 321 211 ### Ds+ -> π- K+ π+ (non-resonant) 1.16-3 +### Ds+ -> π+ π- π+ π0 (11.00%) +431:addChannel = 1 0.11000 0 221 211 ### Ds+ -> eta π+ -> π0 π+ π+ π- (affects D+ golden channel) + +## Λc decays +### Λc+ -> p K- π+ (36%) +4122:oneChannel = 1 0.14400 0 2212 -321 211 ### Λc+ -> p K- π+ (non-resonant) 3.5% +4122:addChannel = 1 0.08100 100 2212 -313 ### Λc+ -> p K*0(892) 1.96% +4122:addChannel = 1 0.04500 100 2224 -321 ### Λc+ -> Delta++ K- 1.08% +4122:addChannel = 1 0.09000 100 102134 211 ### Λc+ -> Lambda(1520) K- 2.20e-3 +### Λc+ -> p K0S (36%) +4122:addChannel = 1 0.36000 0 2212 311 ### Λc+ -> p K0S 1.59% +### Λc+ -> p K- π+ π0 (small, 3%) +4122:addChannel = 1 0.03000 0 2212 -321 211 111 ### Λc+ -> p K- π+ π0 (non-resonant) 4.6% +### Λc+ -> p π- π+ (12.50%) +4122:addChannel = 1 0.12500 0 2212 -211 211 ### Λc+ -> p π+ π+ 4.59% +### Λc+ -> p K- K+ (12.50%) +4122:addChannel = 1 0.12500 0 2212 333 ### Λc+ -> p phi 1.06% + +## Xic decays +### Ξc+ -> p K- π+ (35%) +4232:oneChannel = 1 0.17500 0 2212 -321 211 ### Ξc+ -> p K- π+ 6.18e-3 +4232:addChannel = 1 0.17500 0 2212 -313 ### Ξc+ -> p antiK*0(892) +### Ξc+ -> Ξ- π+ π+ (35%) (set the same as Ξc+ -> p K- π+) +4232:addChannel = 1 0.35000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% +### Ξc+ -> p φ (10%) +4232:addChannel = 1 0.10000 0 2212 333 ### Ξc+ -> p φ +### Ξc+ -> sigma+ π+ π- (10%) +4232:addChannel = 1 0.12500 0 3222 -211 211 ### Ξc+ -> sigma+ π- π+ 1.37% +### Ξc+ -> Ξ*0 π+ (10%) +4232:addChannel = 1 0.12500 0 3324 211 + +### add Xic0 decays absent in PYTHIA8 decay table +4132:oneChannel = 1 0.0143 0 3312 211 + +### add OmegaC decays absent in PYTHIA8 decay table +4332:oneChannel = 1 0.5 0 3334 211 +4332:addChannel = 1 0.5 0 3312 211 + +# Allow the decay of resonances in the decay chain +### K*0(892) -> K- π+ +313:onMode = off +313:onIfAll = 321 211 +### K*(892)+ -> K- π0 +323:onMode = off +323:onIfAll = 321 111 +### K*(1430)0 -> K- π+ +10311:onMode = off +10311:onIfAll = 321 211 +### rho+ -> π+ π0 +213:onMode = off +213:onIfAll = 211 111 +### φ -> K+ K- +333:onMode = off +333:onIfAll = 321 321 +### rho0 -> π+ π- +113:onMode = off +113:onIfAll = 211 211 +### f2(1270) -> π+ π- +225:onMode = off +225:onIfAll = 211 211 +### f0(1370) -> π+ π- +10221:onMode = off +10221:onIfAll = 211 211 +### eta -> π+ π- +221:onMode = off +221:onIfAll = 111 211 211 +### for Λc -> Delta++ K- +2224:onMode = off +2224:onIfAll = 2212 211 +### for Λc -> Lambda(1520) K- +102134:onMode = off +102134:onIfAll = 2212 321 +### for Xic0 -> pi Xi -> pi pi Lambda -> pi pi pi p +### and Omega_c -> pi Xi -> pi pi Lambda -> pi pi pi p +3312:onMode = off +3312:onIfAll = 3122 -211 +3122:onMode = off +3122:onIfAll = 2212 -211 +### for Omega_c -> pi Omega -> pi K Lambda -> pi K pi p +3334:onMode = off +3334:onIfAll = 3122 -321 + +# switch off all decay channels +411:onMode = off +421:onMode = off +431:onMode = off +4122:onMode = off +4232:onMode = off +4132:onMode = off +443:onMode = off +4332:onMode = off + +# Allow the decay of HF +### D0 -> K π +421:onIfMatch = 321 211 +### D0 -> K π π0 +421:onIfMatch = 321 211 111 +### D0 -> rho K +421:onIfMatch = 213 321 +### D0 -> antiK*0 π0 +421:onIfMatch = 313 111 +### D0 -> K*- π+ +421:onIfMatch = 323 211 +### D0 -> π π +421:onIfMatch = 211 211 +### D0 -> rho+ π- +421:onIfMatch = 213 -211 +### D0 -> π π π0 +421:onIfMatch = 211 211 111 +### D0 -> K K +421:onIfMatch = 321 321 + +### D+/- -> K π π +411:onIfMatch = 321 211 211 +### D+/- -> K π π +411:onIfMatch = 321 211 211 111 +### D+/- -> K* π +411:onIfMatch = 313 211 +### D+/- -> K* K +411:onIfMatch = 313 321 +### D+/- -> antiK* π +411:onIfMatch = 10311 211 +### D+/- -> antiK* K +411:onIfMatch = 10311 321 +### D+/- -> φ π +411:onIfMatch = 333 211 +### D+/- -> K K π +411:onIfMatch = 321 321 211 +### D+/- -> f2(1270) π +411:onIfMatch = 225 211 +### D+/- -> rho π +411:onIfMatch = 113 211 +### D+/- -> π π π +411:onIfMatch = 211 211 211 + +### Ds -> φ π +431:onIfMatch = 333 211 +### Ds -> K* K +431:onIfMatch = 313 321 +### Ds -> φ rho+ +431:onIfMatch = 333 213 +### Ds -> rho π +431:onIfMatch = 113 211 +### Ds -> f2 π +431:onIfMatch = 225 211 +### Ds -> π π π +431:onIfMatch = 211 211 211 +### Ds -> K*(892)0 π+ +431:onIfMatch = 313 211 +### Ds -> f0(1370) K+ +431:onIfMatch = 10221 321 +### Ds -> rho0 K+ +431:onIfMatch = 113 321 +### Ds -> K π π +431:onIfMatch = 321 211 211 +### Ds -> eta π+ +431:onIfMatch = 221 211 + +### Λc -> pK0s +4122:onIfMatch = 2212 311 +### Λc -> p K- π+ π0 +4122:onIfMatch = 2212 321 211 +### Λc -> p K* +4122:onIfMatch = 2212 313 +### Λc -> Delta++ K +4122:onIfMatch = 2224 321 +### Λc -> Lambda(1520) π +4122:onIfMatch = 102134 211 +### Λc -> p K- π+ π0 +4122:onIfMatch = 2212 321 211 111 +### Λc -> p π π +4122:onIfMatch = 2212 211 211 +### Λc -> p K K +4122:onIfMatch = 2212 333 + +### Ξc+ -> p K- π+ +4232:onIfMatch = 2212 321 211 +### Ξc+ -> p antiK*0(892) +4232:onIfMatch = 2212 313 +### Ξc+ -> p φ +4232:onIfMatch = 2212 333 +### Ξc+ -> sigma- π+ π+ +4232:onIfMatch = 3222 211 211 +### Ξc+ -> Ξ*0 π+, Ξ*0 -> Ξ- π+ +4232:onIfMatch = 3324 211 +### Ξc+ -> Ξ- π+ π+ +4232:onIfMatch = 3312 211 211 + +### Xic0 -> Xi- pi+ +4132:onIfMatch = 3312 211 + +### Omega_c -> Omega pi +4332:onIfMatch = 3334 211 +### Omega_c -> Xi pi +4332:onIfMatch = 3312 211 From cba47e1f0a9fc202291482b2b797db2208e1ed54 Mon Sep 17 00:00:00 2001 From: glegras <71757105+glegras@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:34:43 +0200 Subject: [PATCH 092/144] [PWGDQ] adding generator to trigger all inclusive quarkonia in pp (#2466) * [PWGDQ] possiblity to trigger all quarkonia * possibility to pass rejection factors by string * [PWGDQ] adding generator to trigger all inclusive quarkonia in pp * clang format * small fix * update with possibility to choose which decay table we want * small fix for failing tests * fix missing tests * fix more failing tests * fixing another small bug for EM, hoping it is the last * correct fix * removing all PWGEM files --- .../BTOPSIJPSITODIELECTRON.DEC | 224 +++++++----- .../CHARMONIATOJPSIANDDIELECTRON.DEC | 42 +++ MC/config/PWGDQ/EvtGen/GeneratorEvtGen.C | 6 +- ...enerator_pythia8_HadronTriggered_withGap.C | 331 ++++++++++++++++-- ...dInclusiveQuarkonia_Pythia8_TriggerGap.ini | 7 + ...Quarkonia_enhancedB_Pythia8_TriggerGap.ini | 7 + ...ctedPromptJpsiMidy_PythiaOnia_TriggerGap.C | 102 ++++++ ...tJpsiPsi2SFwdy_Pythia8_TriggerGap_pp5TeV.C | 102 ++++++ ...redInclusiveQuarkonia_Pythia8_TriggerGap.C | 77 ++++ ...veQuarkonia_enhancedB_Pythia8_TriggerGap.C | 77 ++++ .../pythia8/decayer/switchOffAllQuarkonia.cfg | 10 + .../switchOffAllQuarkoniaAndBhadrons.cfg | 18 + .../PWGDQ/pythia8/decayer/switchOffJpsi.cfg | 4 +- .../pythia8/decayer/switchOffJpsiUpsilon.cfg | 7 + .../generator/pythia8_oniaAll_triggerGap.cfg | 12 + .../ini/Generator_GapTriggered_LFgamma.ini | 2 +- ...enerator_GapTriggered_LFgamma_np1_gap2.ini | 9 - ...enerator_GapTriggered_LFgamma_np1_gap4.ini | 9 - ...enerator_GapTriggered_LFgamma_np1_gap6.ini | 9 - ...enerator_GapTriggered_LFgamma_np3_gap5.ini | 9 - ...enerator_GapTriggered_LFgamma_np5_gap5.ini | 9 - 21 files changed, 918 insertions(+), 155 deletions(-) create mode 100644 MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC create mode 100644 MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini create mode 100644 MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiMidy_PythiaOnia_TriggerGap.C create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiPsi2SFwdy_Pythia8_TriggerGap_pp5TeV.C create mode 100644 MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.C create mode 100644 MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.C create mode 100644 MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkonia.cfg create mode 100644 MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkoniaAndBhadrons.cfg create mode 100644 MC/config/PWGDQ/pythia8/decayer/switchOffJpsiUpsilon.cfg create mode 100644 MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap2.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap4.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap6.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np3_gap5.ini delete mode 100644 MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np5_gap5.ini diff --git a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC index 4c3e5a129..9d2f62ea5 100644 --- a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC +++ b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC @@ -1,40 +1,36 @@ ### Decay B0 ### -0.000310000 psi(2S) K_S0 SVS; #[Reconstructed PDG2011] -0.000310000 psi(2S) K_L0 SVS; #[Reconstructed PDG2011] # -# -0.000610000 psi(2S) K*0 SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] +0.000590000 psi(2S) K*0 SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] 0.0004 psi(2S) K+ pi- PHSP; 0.0002 psi(2S) K0 pi0 PHSP; -0.0002 psi(2S) K0 pi- pi+ PHSP; +0.000281 psi(2S) K0 pi- pi+ PHSP; 0.0001 psi(2S) K0 pi0 pi0 PHSP; 0.0001 psi(2S) K+ pi- pi0 PHSP; 0.0004 psi(2S) K_10 PHSP; ### -0.000620000 psi(2S) K0 SVS; #[New mode added] #[Reconstructed PDG2011] +0.000580000 psi(2S) K0 SVS; #[New mode added] #[Reconstructed PDG2011] -0.000435500 J/psi K_S0 SVS; #[Reconstructed PDG2011] -0.000435500 J/psi K_L0 SVS; #[Reconstructed PDG2011] +0.000891000 J/psi K0 SVS; #[Reconstructed PDG2011] # # -0.001330000 J/psi K*0 SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] -0.000017600 J/psi pi0 SVS; #[Reconstructed PDG2011] -0.000027000 J/psi rho0 SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] -0.00003 J/psi omega SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; +0.001270000 J/psi K*0 SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] +0.000017300 J/psi pi0 SVS; #[Reconstructed PDG2011] +0.000025500 J/psi rho0 SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] +0.0000208 J/psi omega SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; 0.000000000 J/psi K+ pi- PHSP; #[Reconstructed PDG2011] 0.0001 J/psi K0 pi0 PHSP; 0.001300000 J/psi K_10 SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; #[Reconstructed PDG2011] 0.0001 J/psi K'_10 SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; 0.0005 J/psi K_2*0 PHSP; -0.000094000 J/psi phi K0 PHSP; #[Reconstructed PDG2011] +0.000049000 J/psi phi K0 PHSP; #[Reconstructed PDG2011] #### -0.000871000 J/psi K0 SVS; #[New mode added] #[Reconstructed PDG2011] -0.000310000 J/psi omega K0 PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000009500 J/psi eta PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000230000 J/psi omega K0 PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000012300 J/psi eta PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000019000 J/psi pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000460000 J/psi K0 pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000450000 J/psi K0 pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000025000 J/psi K0 K+ K- PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000540000 J/psi K0 rho0 PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000800000 J/psi K*+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000660000 J/psi K*0 pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] @@ -56,39 +52,36 @@ Enddecay Decay anti-B0 ### -0.000310000 psi(2S) K_S0 SVS; #[Reconstructed PDG2011] -0.000310000 psi(2S) K_L0 SVS; #[Reconstructed PDG2011] # -0.000610000 psi(2S) anti-K*0 SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] +0.000590000 psi(2S) anti-K*0 SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] 0.0004 psi(2S) K- pi+ PHSP; 0.0002 psi(2S) anti-K0 pi0 PHSP; -0.0002 psi(2S) anti-K0 pi+ pi- PHSP; +0.000281 psi(2S) anti-K0 pi+ pi- PHSP; 0.0001 psi(2S) anti-K0 pi0 pi0 PHSP; 0.0001 psi(2S) K- pi+ pi0 PHSP; 0.0004 psi(2S) anti-K_10 PHSP; ### -0.000620000 psi(2S) anti-K0 SVS; #[New mode added] #[Reconstructed PDG2011] +0.000580000 psi(2S) anti-K0 SVS; #[New mode added] #[Reconstructed PDG2011] -0.000435500 J/psi K_S0 SVS; #[Reconstructed PDG2011] -0.000435500 J/psi K_L0 SVS; #[Reconstructed PDG2011] +0.000891000 J/psi anti-K0 SVS; #[Reconstructed PDG2011] # updated BR and corrected for double-couting (https://repo.hepforge.org/rEVTGEN16d483d4962aeeb93e857939666a9ba3383bb71f) # # -0.001330000 J/psi anti-K*0 SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] -0.000017600 J/psi pi0 SVS; #[Reconstructed PDG2011] -0.000027000 J/psi rho0 SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] -0.000030 J/psi omega SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; +0.001270000 J/psi anti-K*0 SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] +0.000017300 J/psi pi0 SVS; #[Reconstructed PDG2011] +0.000025500 J/psi rho0 SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] +0.0000208 J/psi omega SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; 0.000000000 J/psi K- pi+ PHSP; #[Reconstructed PDG2011] 0.0001 J/psi anti-K0 pi0 PHSP; 0.001300000 J/psi anti-K_10 SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; #[Reconstructed PDG2011] 0.0001 J/psi anti-K'_10 SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; 0.0005 J/psi anti-K_2*0 PHSP; -0.000094000 J/psi phi anti-K0 PHSP; #[Reconstructed PDG2011] +0.000049000 J/psi phi anti-K0 PHSP; #[Reconstructed PDG2011] ### -0.000871000 J/psi anti-K0 SVS; #[New mode added] #[Reconstructed PDG2011] -0.000310000 J/psi omega anti-K0 PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000009500 J/psi eta PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000230000 J/psi omega anti-K0 PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000012300 J/psi eta PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000019000 J/psi pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000460000 J/psi anti-K0 pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000450000 J/psi anti-K0 pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.0000250000 J/psi anti-K0 K- K+ PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000540000 J/psi anti-K0 rho0 PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000800000 J/psi K*- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000660000 J/psi anti-K*0 pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] @@ -110,32 +103,34 @@ Enddecay Decay B+ ### -0.000646000 psi(2S) K+ SVS; #[Reconstructed PDG2011] -0.000620000 psi(2S) K*+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] +0.000625000 psi(2S) K+ SVS; #[Reconstructed PDG2011] +0.000670000 psi(2S) K*+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] 0.0004 psi(2S) K0 pi+ PHSP; 0.0002 psi(2S) K+ pi0 PHSP; -0.001900000 psi(2S) K+ pi- pi+ PHSP; #[Reconstructed PDG2011] +0.000430 psi(2S) K+ pi- pi+ PHSP; #[Reconstructed PDG2011] 0.0001 psi(2S) K+ pi0 pi0 PHSP; 0.0001 psi(2S) K0 pi+ pi0 PHSP; 0.0004 psi(2S) K_1+ PHSP; ### -0.000025800 psi(2S) pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000024400 psi(2S) pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] -0.001014000 J/psi K+ SVS; #[Reconstructed PDG2011] +0.001019000 J/psi K+ SVS; #[Reconstructed PDG2011] 0.001430000 J/psi K*+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] -0.000049000 J/psi pi+ SVS; #[Reconstructed PDG2011] -0.000050000 J/psi rho+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] -0.0002 J/psi K0 pi+ PHSP; -0.0001 J/psi K+ pi0 PHSP; +0.000041000 J/psi pi+ SVS; #[Reconstructed PDG2011] +0.000041000 J/psi rho+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; #[Reconstructed PDG2011] +0.00113 J/psi K0 pi+ PHSP; +0.0001 J/psi K+ pi0 PHSP; 0.0001 J/psi K'_1+ SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; 0.0005 J/psi K_2*+ PHSP; 0.001800000 J/psi K_1+ SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; #[Reconstructed PDG2011] 0.000052000 J/psi phi K+ PHSP; #[Reconstructed PDG2011] # #### -0.001070000 J/psi K+ pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000108000 J/psi eta K+ PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000350000 J/psi omega K+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000810000 J/psi K+ pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000033700 J/psi K+ K- K+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000124000 J/psi eta K+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000031000 J/psi eta' K+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000320000 J/psi omega K+ PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000011800 J/psi p+ anti-Lambda0 PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- @@ -156,31 +151,33 @@ Enddecay Decay B- ### -0.000646000 psi(2S) K- SVS; #[Reconstructed PDG2011] -0.000620000 psi(2S) K*- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] +0.000625000 psi(2S) K- SVS; #[Reconstructed PDG2011] +0.000670000 psi(2S) K*- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] 0.0004 psi(2S) anti-K0 pi- PHSP; 0.0002 psi(2S) K- pi0 PHSP; -0.001900000 psi(2S) K- pi+ pi- PHSP; #[Reconstructed PDG2011] +0.000430 psi(2S) K- pi+ pi- PHSP; #[Reconstructed PDG2011] 0.0001 psi(2S) K- pi0 pi0 PHSP; 0.0001 psi(2S) anti-K0 pi- pi0 PHSP; 0.0004 psi(2S) K_1- PHSP; ### -0.000025800 psi(2S) pi- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000024400 psi(2S) pi- PHSP; #[New mode added] #[Reconstructed PDG2011] -0.001014000 J/psi K- SVS; #[Reconstructed PDG2011] +0.001019000 J/psi K- SVS; #[Reconstructed PDG2011] 0.001430000 J/psi K*- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] -0.000049000 J/psi pi- SVS; #[Reconstructed PDG2011] -0.000050000 J/psi rho- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] -0.0002 J/psi anti-K0 pi- PHSP; +0.000041000 J/psi pi- SVS; #[Reconstructed PDG2011] +0.000041000 J/psi rho- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; #[Reconstructed PDG2011] +0.00113 J/psi anti-K0 pi- PHSP; 0.0001 J/psi K- pi0 PHSP; 0.0001 J/psi K'_1- SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; 0.0005 J/psi K_2*- PHSP; 0.001800000 J/psi K_1- SVV_HELAMP 0.5 0.0 1.0 0.0 0.5 0.0; #[Reconstructed PDG2011] 0.000052000 J/psi phi K- PHSP; #[Reconstructed PDG2011] # -0.001070000 J/psi K- pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000108000 J/psi eta K- PHSP; #[New mode added] #[Reconstructed PDG2011] -0.000350000 J/psi omega K- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000810000 J/psi K- pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000033700 J/psi K- K+ K- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000124000 J/psi eta K- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000031000 J/psi eta' K- PHSP; #[New mode added] #[Reconstructed PDG2011] +0.000320000 J/psi omega K- PHSP; #[New mode added] #[Reconstructed PDG2011] 0.000011800 J/psi anti-p- Lambda0 PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- @@ -201,9 +198,9 @@ Enddecay Decay B_s0 ### psi' = 0.34% CLNS 94/1315 -0.000465 psi(2S) eta' SVS; -0.000235 psi(2S) eta SVS; -0.000680000 psi(2S) phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] +0.000137 psi(2S) eta' SVS; +0.00037 psi(2S) eta SVS; +0.000510000 psi(2S) phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] 0.0003 psi(2S) K- K+ PHSP; 0.0003 psi(2S) anti-K0 K0 PHSP; 0.0003 psi(2S) K0 K- pi+ PHSP; @@ -215,19 +212,22 @@ Decay B_s0 0.0002 psi(2S) eta pi0 pi0 PHSP; 0.0004 psi(2S) eta' pi+ pi- PHSP; 0.0004 psi(2S) eta' pi0 pi0 PHSP; -0.0002 psi(2S) pi+ pi- PHSP; +0.000068 psi(2S) pi+ pi- PHSP; 0.0002 psi(2S) pi0 pi0 PHSP; #### -0.00064 J/psi eta' SVS; -0.00032 J/psi eta SVS; -0.001300000 J/psi phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] -0.00008 J/psi K0 SVS; -0.00070 J/psi K- K+ PHSP; +0.000353 J/psi eta' SVS; +0.000445 J/psi eta SVS; +0.00026 J/psi f'_2 PHSP; +0.001010000 J/psi phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] +0.0000384 J/psi K0 SVS; +0.00079 J/psi K- K+ PHSP; 0.00070 J/psi anti-K0 K0 PHSP; -0.00070 J/psi K0 K- pi+ PHSP; +0.000475 J/psi K0 K- pi+ PHSP; +0.000475 J/psi K0 K+ pi- PHSP; 0.00070 J/psi anti-K0 K0 pi0 PHSP; 0.00070 J/psi K- K+ pi0 PHSP; +0.000107 J/psi K*0 anti-K*0 PHSP; # LHCb PR 04/02/04 Add (cc) phi n pi(+/0) 0.00039 J/psi phi pi+ pi- PHSP; 0.00039 J/psi phi pi0 pi0 PHSP; @@ -242,14 +242,14 @@ Decay B_s0 0.0000023 phi e+ e- BTOSLLALI; # --- chi_c1 --- - 0.0001970 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; + 0.0001920 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; Enddecay Decay anti-B_s0 ### -0.000465 psi(2S) eta' SVS; -0.000235 psi(2S) eta SVS; -0.000680000 psi(2S) phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] +0.000137 psi(2S) eta' SVS; +0.00037 psi(2S) eta SVS; +0.000510000 psi(2S) phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] 0.0003 psi(2S) K- K+ PHSP; 0.0003 psi(2S) anti-K0 K0 PHSP; 0.0003 psi(2S) anti-K0 K+ pi- PHSP; @@ -261,18 +261,21 @@ Decay anti-B_s0 0.0002 psi(2S) eta pi0 pi0 PHSP; 0.0004 psi(2S) eta' pi+ pi- PHSP; 0.0004 psi(2S) eta' pi0 pi0 PHSP; -0.0002 psi(2S) pi+ pi- PHSP; +0.000068 psi(2S) pi+ pi- PHSP; 0.0002 psi(2S) pi0 pi0 PHSP; ### -0.00064 J/psi eta' SVS; -0.00032 J/psi eta SVS; -0.001300000 J/psi phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] -0.00008 J/psi K0 SVS; -0.00070 J/psi K- K+ PHSP; +0.000353 J/psi eta' SVS; +0.000445 J/psi eta SVS; +0.00026 J/psi f'_2 PHSP; +0.001010000 J/psi phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; #[Reconstructed PDG2011] +0.0000384 J/psi K0 SVS; +0.00079 J/psi K- K+ PHSP; 0.00070 J/psi anti-K0 K0 PHSP; -0.00070 J/psi anti-K0 K+ pi- PHSP; +0.000475 J/psi anti-K0 K+ pi- PHSP; +0.000475 J/psi anti-K0 K- pi+ PHSP; 0.00070 J/psi anti-K0 K0 pi0 PHSP; 0.00070 J/psi K- K+ pi0 PHSP; +0.000107 J/psi K*0 anti-K*0 PHSP; # LHCb PR 04/02/04 Add (cc) phi n pi(+/0) 0.00039 J/psi phi pi+ pi- PHSP; 0.00039 J/psi phi pi0 pi0 PHSP; @@ -292,8 +295,13 @@ Enddecay Decay Lambda_b0 ### -0.00038 Lambda0 psi(2S) PHSP; -0.00047 Lambda0 J/psi PHSP; +0.00015 Lambda0 psi(2S) PHSP; +0.0003 Lambda0 J/psi PHSP; +0.000012 Lambda0 J/psi phi PHSP; +0.000026 p+ J/psi pi- PHSP; +0.00032 p+ J/psi K- PHSP; +0.000066 p+ J/psi pi- pi+ K- PHSP; +0.000066 p+ psi(2S) K- PHSP; 0.0000760 chi_c1 p+ K- PHSP; 0.0000050 chi_c1 p+ pi- PHSP; 0.0000770 chi_c2 p+ K- PHSP; @@ -302,8 +310,13 @@ Enddecay Decay anti-Lambda_b0 ### -0.00038 anti-Lambda0 psi(2S) PHSP; -0.00047 anti-Lambda0 J/psi PHSP; +0.00015 anti-Lambda0 psi(2S) PHSP; +0.0003 anti-Lambda0 J/psi PHSP; +0.000012 Lambda0 J/psi phi PHSP; +0.000026 anti-p- J/psi pi+ PHSP; +0.00032 anti-p- J/psi K+ PHSP; +0.000066 anti-p- J/psi pi- pi+ K+ PHSP; +0.000066 anti-p- psi(2S) K+ PHSP; 0.0000760 chi_c1 anti-p- K+ PHSP; 0.0000050 chi_c1 anti-p- pi+ PHSP; 0.0000770 chi_c2 anti-p- K+ PHSP; @@ -312,11 +325,15 @@ Enddecay Decay Xi_b- 0.00047 Xi- J/psi PHSP; + 0.000108 J/psi Lambda0 K- PHSP; + 0.000394 Xi- psi(2S) PHSP; Enddecay Decay anti-Xi_b+ 0.00047 anti-Xi+ J/psi PHSP; + 0.000108 J/psi anti-Lambda0 K+ PHSP; + 0.000394 anti-Xi+ psi(2S) PHSP; Enddecay Decay Xi_b0 @@ -335,28 +352,63 @@ Decay anti-Omega_b+ 0.00047 anti-Omega+ J/psi PHSP; Enddecay +Decay B_c- +# SemiLeptonic Decays +0.01900 J/psi e- anti-nu_e PHOTOS BC_VMN 1; +0.00094 psi(2S) e- anti-nu_e PHOTOS BC_VMN 1; +Enddecay + +Decay B_c+ +# SemiLeptonic Decays +0.01900 J/psi e+ nu_e PHOTOS BC_VMN 1; +0.00094 psi(2S) e+ nu_e PHOTOS BC_VMN 1; +Enddecay + # ============================================================================================================================= -# JPsi, Psi(2S), ChiC Decays +# JPsi, Psi(2S), ChiC, Upsilon Decays # ============================================================================================================================= +### +Decay chi_c0 +0.0135 gamma J/psi PHSP; +0.000129 J/psi e+ e- PHOTOS PHSP; +Enddecay + ### Decay chi_c1 - 1.000 J/psi gamma PHSP; +0.339 J/psi gamma VVP 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0; +0.00342 J/psi e+ e- PHOTOS PHSP; Enddecay ### Decay chi_c2 - 1.000 J/psi gamma PHSP; +0.190 gamma J/psi PHSP; +0.00214 J/psi e+ e- PHOTOS PHSP; Enddecay Decay psi(2S) -### from DECAY.DEC -1.000 e+ e- PHOTOS VLL; +0.00795 e+ e- PHOTOS VLL; +0.0208 J/psi pi+ pi- VVPIPI; # multiplied by J/psi->ee BR to keep relative statistics, since J/psi->ee itself is also enhanced +0.0000819 gamma chi_c0 PHSP; # * BR(J/psi->ee) * BR(chi_c0->J/psi) +0.00201 gamma chi_c1 PHSP; # * BR(J/psi->ee) * BR(chi_c1->J/psi) +0.00110 gamma chi_c2 PHSP; # * BR(J/psi->ee) * BR(chi_c2->J/psi) Enddecay Decay J/psi 1.000 e+ e- PHOTOS VLL; Enddecay +Decay Upsilon +1.000 e+ e- PHOTOS VLL; +Enddecay + +Decay Upsilon(2S) +1.000 e+ e- PHOTOS VLL; +Enddecay + +Decay Upsilon(3S) +1.000 e+ e- PHOTOS VLL; +Enddecay + End diff --git a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC new file mode 100644 index 000000000..fc554fb28 --- /dev/null +++ b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC @@ -0,0 +1,42 @@ +Decay chi_c0 +0.0135 gamma J/psi PHSP; +0.000129 J/psi e+ e- PHOTOS PHSP; +Enddecay + +### +Decay chi_c1 +0.339 J/psi gamma VVP 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0; +0.00342 J/psi e+ e- PHOTOS PHSP; +Enddecay + +### +Decay chi_c2 +0.190 gamma J/psi PHSP; +0.00214 J/psi e+ e- PHOTOS PHSP; +Enddecay + +Decay psi(2S) +0.00795 e+ e- PHOTOS VLL; +0.0208 J/psi pi+ pi- VVPIPI; # multiplied by J/psi->ee BR to keep relative statistics, since J/psi->ee itself is also enhanced +0.0000819 gamma chi_c0 PHSP; # * BR(J/psi->ee) * BR(chi_c0->J/psi) +0.00201 gamma chi_c1 PHSP; # * BR(J/psi->ee) * BR(chi_c1->J/psi) +0.00110 gamma chi_c2 PHSP; # * BR(J/psi->ee) * BR(chi_c2->J/psi) +Enddecay + +Decay J/psi +1.000 e+ e- PHOTOS VLL; +Enddecay + +Decay Upsilon +1.000 e+ e- PHOTOS VLL; +Enddecay + +Decay Upsilon(2S) +1.000 e+ e- PHOTOS VLL; +Enddecay + +Decay Upsilon(3S) +1.000 e+ e- PHOTOS VLL; +Enddecay + +End diff --git a/MC/config/PWGDQ/EvtGen/GeneratorEvtGen.C b/MC/config/PWGDQ/EvtGen/GeneratorEvtGen.C index 1f6d6b867..9c073b5ea 100644 --- a/MC/config/PWGDQ/EvtGen/GeneratorEvtGen.C +++ b/MC/config/PWGDQ/EvtGen/GeneratorEvtGen.C @@ -43,7 +43,8 @@ enum DecayModeEvt { kEvtAll = 0, kEvtDiElectronEM, kEvtGammaEM, kEvtBtoPsi2SToJpsiPiPi, - kEvtBeautyUpgrade }; + kEvtBeautyUpgrade, + kCharmoniaToJpsiAndDielectron}; namespace o2 { @@ -359,6 +360,9 @@ class GeneratorEvtGen : public T case kEvtBtoPsi2SToJpsiPiPi: SetDecayTable(Form("%s/BTOPSITOJPSIPIPI.DEC", pathO2.Data())); break; + case kCharmoniaToJpsiAndDielectron: + SetDecayTable(Form("%s/CHARMONIATOJPSIANDDIELECTRON.DEC", pathO2.Data())); + break; } return; }; diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C b/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C index d34092a8c..823b0b322 100644 --- a/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C @@ -39,7 +39,29 @@ public: /// Destructor ~GeneratorPythia8HadronTriggeredWithGap() = default; - void addHadronPDGs(int pdg) { mHadronsPDGs.push_back(pdg); }; + void addHadronPDGs(int pdg) { + mHadronsPDGs.push_back(pdg); + mRejFactorPrompt.push_back(1.0); + mRejFactorNonPrompt.push_back(1.0); + } + + void decrementGeneratedEvents() { mGeneratedEvents--; } + + void setRejFactorPrompt(int pdg, float rejFactor) { + for (size_t i = 0; i < mHadronsPDGs.size(); i++) { + if (pdg == mHadronsPDGs[i]) { + mRejFactorPrompt[i] = rejFactor; + } + } + } + + void setRejFactorNonPrompt(int pdg, float rejFactor) { + for (size_t i = 0; i < mHadronsPDGs.size(); i++) { + if (pdg == mHadronsPDGs[i]) { + mRejFactorNonPrompt[i] = rejFactor; + } + } + } void setRapidityRange(double valMin, double valMax) { @@ -53,6 +75,10 @@ public: void setVerbose(bool val) { mVerbose = val; }; + int getNGeneratedEvents() { return mGeneratedEvents; } + + int getTriggerGap() { return mInverseTriggerRatio; } + protected: bool generateEvent() override { @@ -93,30 +119,106 @@ bool Init() override { addSubGenerator(1, "Hadron triggered"); GeneratorPythia8::Init(); pythiaMBgen.init(); + + for (size_t i = 0; i < mHadronsPDGs.size(); i++) { + LOGF(info, + "triggering hadron %d with rejection factor (prompt/non-prompt) %f/%f", + mHadronsPDGs[i], mRejFactorPrompt[i], mRejFactorNonPrompt[i]); + } + return true; -} +} + +bool isOpenBhadron(int pdg) { + // all open beauty hadrons, no upsilon + return ((abs(pdg) >= 500 && abs(pdg) < 599) || + (abs(pdg) >= 5000 && abs(pdg) < 5999)) && + pdg != 553; +} + +bool isCharmonium(int pdg) { + int firstQuark = (abs(pdg) % 100 - abs(pdg) % 10) / 10; + int secondQuark = (abs(pdg) % 1000 - abs(pdg) % 100) / 100; + int thirdQuark = (abs(pdg) % 10000 - abs(pdg) % 1000) / 1000; + return (firstQuark == 4 && secondQuark == 4 && thirdQuark == 0); +} // search for the presence of at least one of the required hadrons in a selected rapidity window -bool findHadrons(Pythia8::Event& event) { - - for (int ipa = 0; ipa < event.size(); ++ipa) { - - auto daughterList = event[ipa].daughterList(); - - for (auto ida : daughterList) { - for (int pdg : mHadronsPDGs) { // check that at least one of the pdg code is found in the event - if (event[ida].id() == pdg) { - if ((event[ida].y() > mRapidityMin) && (event[ida].y() < mRapidityMax)) { - cout << "============= Found jpsi y,pt " << event[ida].y() << ", " << event[ida].pT() << endl; - std::vector daughters = event[ida].daughterList(); - for (int d : daughters) { - cout << "###### daughter " << d << ": code " << event[d].id() << ", pt " << event[d].pT() << endl; +bool findHadrons(Pythia8::Event& event) { + int ihad = 0; + for (int ida = 0; ida < event.size(); ++ida) { + //auto daughterList = event[ipa].daughterList(); + //for (auto ida : daughterList) { + ihad = 0; + for (int pdg : mHadronsPDGs) { // check that at least one of the pdg code is found in the event + if (abs(event[ida].id()) == pdg) { + if ((event[ida].y() > mRapidityMin) && (event[ida].y() < mRapidityMax)) { + cout << "============= Found jpsi y,pt,pdg " << event[ida].y() << ", " << event[ida].pT() << ", " << event[ida].id() << endl; + std::vector daughters = event[ida].daughterList(); + for (int d : daughters) { + cout << "###### daughter " << d << ": code " << event[d].id() << ", pt " << event[d].pT() << endl; + } + if (event[ida].daughter1() == event[ida].daughter2() && event[ida].daughter1() > 0) { + continue; // particle has a carbon-copy as daughter, its daughter will already be considered for triggering + } + + // check whether particle is prompt or non-prompt, since rejection + // factor can depend on it + bool isNonPrompt = false; + if (isOpenBhadron(pdg)) { + isNonPrompt = true; + LOGF(info, "particle is non-prompt"); + + // if the B hadron decays to something else than a charmonia, we don't trigger it + bool hasCharmoniaDecay = false; + if (daughters.size() == 0) { + // in this case B is left undecayed and will be decayed to enhanced charmonia BR with EVTGEN + hasCharmoniaDecay = true; + } + else { + for (int d : daughters) { + if (isCharmonium(event[d].id())) { + hasCharmoniaDecay = true; + } + } + } + if (!hasCharmoniaDecay) { + continue; } + } else { + // check history + int currentIdx = ida; + int currentPdg = pdg; + cout << "particle history: "; + while (isCharmonium(currentPdg) && currentIdx >= 0) { + currentIdx = event[currentIdx].mother1(); + if (currentIdx < 0) { + break; + } + currentPdg = abs(event[currentIdx].id()); + cout << currentPdg << " "; + if (isOpenBhadron(currentPdg)) { + isNonPrompt = true; + LOGF(info, "particle is non-prompt"); + break; + } + } + cout << endl; + } + + // rejection factor given in the ini file + float randomNumber = gRandom->Rndm(); + cout << randomNumber << " rej factor: " << (isNonPrompt ? mRejFactorNonPrompt[ihad] : mRejFactorPrompt[ihad]) << endl; + if ((!isNonPrompt && (randomNumber <= mRejFactorPrompt[ihad])) || + (isNonPrompt && (randomNumber <= mRejFactorNonPrompt[ihad]))) { + cout << "event triggered " << endl; return true; } } } + ihad++; } + //} } return false; @@ -133,6 +235,10 @@ private: Pythia8::Pythia pythiaMBgen; // minimum bias event TString mConfigMBdecays; std::vector mHadronsPDGs; + std::vector + mRejFactorPrompt; // rejection factors for possibility to trigger a given + // particle only a fraction of the time, 1 by default + std::vector mRejFactorNonPrompt; double mRapidityMin; double mRapidityMax; bool mVerbose; @@ -142,6 +248,22 @@ private: } +o2::eventgen::Trigger triggerPDGRap(double rapMin, double rapMax, int pdg, GeneratorPythia8HadronTriggeredWithGap *gen) { + auto trigger = [rapMin, rapMax, pdg, gen](const std::vector &particles) -> bool { + if (gen->getTriggerGap() != 1 && gen->getNGeneratedEvents() % gen->getTriggerGap() != 1) { + // this is a MB event + return true; + } + for (const auto &p : particles) { + if (p.Y() > rapMin && p.Y() < rapMax && std::abs(p.GetPdgCode()) == pdg) { + return true; + } + } + return false; + }; + return trigger; +} + // Predefined generators: FairGenerator* GeneratorInclusiveJpsi_EvtGenMidY(int triggerGap, double rapidityMin = -1.5, double rapidityMax = 1.5, bool verbose = false) @@ -221,7 +343,7 @@ GeneratorInclusiveJpsiPsi2SChiC_EvtGenMidY(int triggerGap, double rapidityMin = gen->addHadronPDGs(443); gen->addHadronPDGs(100443); gen->addHadronPDGs(445); - gen->addHadronPDGs(200443); + gen->addHadronPDGs(20443); gen->setVerbose(verbose); TString pathO2table = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/decayer/switchOffJpsi.cfg"); @@ -229,11 +351,11 @@ GeneratorInclusiveJpsiPsi2SChiC_EvtGenMidY(int triggerGap, double rapidityMin = gen->setConfigMBdecays(pathO2table); gen->PrintDebug(true); - gen->SetSizePdg(4); + gen->SetSizePdg(2); gen->AddPdg(443, 0); gen->AddPdg(100443, 1); - gen->AddPdg(443, 2); - gen->AddPdg(100443, 3); + //gen->AddPdg(445, 2); + //gen->AddPdg(20443, 3); gen->SetForceDecay(kEvtDiElectron); @@ -250,3 +372,172 @@ GeneratorInclusiveJpsiPsi2SChiC_EvtGenMidY(int triggerGap, double rapidityMin = return gen; } +FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double rapidityMin = -1.0, double rapidityMax = 1.0, TString rejFactors = "", int decayMode = 0, bool verbose = false) { + + // This generator allow to choose from the ini file which decay file is to be used (decayMode parameter) and implement rejection factors in order to enhance one particle with respect to others by randomly rejecting trigger (configured through a string) + // The rapidity cut triggers both on the B hadrons rapidities and on quarkonia rapidities + + // This generator should be able to satisfy any needs, although you might have to add some other decayModes and additional particles in the particleList to be triggered + + // decay mode: 0 = enhance B hadrons decays + charmonia to charmonia decays + dileptons decay + // decay mode: 1 = enhance charmonia to charmonia decays + dileptons decays + // decay mode: 2 = enhance only dilepton decays + + // rejFactors string: + // possibility to enhance a particle compared to another (or completely reject one particle) using rejection factors configured from a string + // The rejection factors can be kept in a comma separated list (e.g. "pdg1:rejFactor1,pdg2:rejFactor2") + // The keywords "prompt" and "non-prompt" can be used to modify all prompt and all non-prompt (e.g. "prompt:rejFactor1,non-prompt:rejFactor2") + // The keyword can be used for only one particle (e.g. "pdg1:rejFactor1:prompt,pdg1:rejFactor2:non-prompt") + // Example if you want only to trigger on a given particle p: "prompt:0,non-prompt:0,p:1" + + int particleList[16] = { + 443, // Jpsi + 100443, // psi(2S) + 553, // upsilon(1S) + 100553, // upsilon(2S) + 200553, // upsilon(3S) + 10441, // chic0 + 20443, // chic1 + 445, // chic2 + // we also add B hadrons to trigger correct rapidity range (e.g. B is + // within |y|<1 but non-prompt J/psi has |y|>1) + 511, // B0 + 521, // B+ + 531, // Bs + 541, // Bc + 5122, // Lambdab + 5132, // Xib+ + 5232, // Xib0 + 5332 // Omegab + }; + + auto gen = new o2::eventgen::GeneratorEvtGen(); + gen->setTriggerGap(triggerGap); + // this is a trigger before EvtGen decays. After EVTGEN decay the rapidities of the particles are modified. + // There is therefore a loose rapidity cut before EVTGEN, and the correct rapidity cut after EVTGEN decay + if (decayMode == 2) { + // In this case EvtGen will not affect charmonia rapidities, so no need to start with a looser rapidity cut + gen->setRapidityRange(rapidityMin, rapidityMax); + } + else { + gen->setRapidityRange(rapidityMin - 1., rapidityMax + 1.); + } + // specify particles to be triggered + for (int i = 0; i < 16; i++) { + gen->addHadronPDGs(particleList[i]); + } + gen->setVerbose(verbose); + + TObjArray *objArray = rejFactors.Tokenize(","); + for (int i = 0; i < objArray->GetEntries(); i++) { + TString rejStr = TString(objArray->At(i)->GetName()); + TObjArray *objArrayCurrent = rejStr.Tokenize(":"); + if (objArrayCurrent->GetEntries() != 2 && objArrayCurrent->GetEntries() != 3) { + LOGF(fatal, "Problem when configuring string for particle rejection factors: %s, incorrect length", + rejStr.Data()); + } + TString str0 = TString(objArrayCurrent->At(0)->GetName()); + TString str1 = TString(objArrayCurrent->At(1)->GetName()); + if (!str1.IsFloat()) { + LOGF(fatal, + "Problem when configuring string for particle rejection factors: %s, is not float", + rejStr.Data()); + } + if (str0.CompareTo("prompt") == 0) { + // Common switch for all prompt particles + for (int ihad = 0; ihad < 16; ihad++) { + gen->setRejFactorPrompt(particleList[ihad], str1.Atof()); + } + continue; + } + if (str0.CompareTo("non-prompt") == 0) { + // Common switch for all non-prompt particles + for (int ihad = 0; ihad < 16; ihad++) { + gen->setRejFactorNonPrompt(particleList[ihad], str1.Atof()); + } + continue; + } + if (str0.IsDigit()) { + // Setting the rejection factor for a specific particle + if (objArrayCurrent->GetEntries() == 2) { + gen->setRejFactorPrompt(str0.Atoi(), str1.Atof()); + gen->setRejFactorNonPrompt(str0.Atoi(), str1.Atof()); + continue; + } else { + TString str2 = TString(objArrayCurrent->At(2)->GetName()); + if (str2.CompareTo("prompt") == 0) { + gen->setRejFactorPrompt(str0.Atoi(), str1.Atof()); + continue; + } + if (str2.CompareTo("non-prompt") == 0) { + gen->setRejFactorNonPrompt(str0.Atoi(), str1.Atof()); + continue; + } + } + } + LOGF(fatal, "Problem when configuring string for particle rejection factors: %s, incorrect template", + rejStr.Data()); + } + + TString pathO2table; + if (decayMode == 0) { + pathO2table = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkoniaAndBhadrons.cfg"); + } + if (decayMode == 1) { + pathO2table = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkonia.cfg"); + } + if (decayMode == 2) { + pathO2table = gSystem->ExpandPathName("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/decayer/switchOffJpsiUpsilon.cfg"); + } + gen->readFile(pathO2table.Data()); + gen->setConfigMBdecays(pathO2table); + gen->PrintDebug(true); + + // specify particles to be decayed with EvtGen + gen->SetSizePdg(16); + for (int i = 0; i < 16; i++) { + // Decay only what was left undecayed by Pythia + if (decayMode == 0 || (i <= 7 && decayMode == 1) || (i <= 4 && decayMode == 2)) { + gen->AddPdg(particleList[i], i); + } + } + + if (decayMode == 0) { + gen->SetForceDecay(kEvtBPsiAndJpsiDiElectron); + } + if (decayMode == 1) { + gen->SetForceDecay(kCharmoniaToJpsiAndDielectron); + } + if (decayMode == 2) { + gen->SetForceDecay(kEvtDiElectron); + } + + // set random seed + gen->readString("Random:setSeed on"); + uint random_seed; + unsigned long long int random_value = 0; + ifstream urandom("/dev/urandom", ios::in | ios::binary); + urandom.read(reinterpret_cast(&random_value), sizeof(random_seed)); + gen->readString(Form("Random:seed = %llu", random_value % 900000001)); + + // print debug + // gen->PrintDebug(); + + // add trigger on the correct rapidity range after EvtGen decays + if (decayMode != 2) { + // In this case EvtGen will not affect charmonia rapidities, so no need to start with a looser rapidity cut, first trigger already does the job + gen->setTriggerMode(Generator::kTriggerOR); + for (int i = 0; i < 16; i++) { + // we trigger either on B hadron rapidity or on charmonia rapidity + gen->addTrigger(triggerPDGRap(rapidityMin, rapidityMax, particleList[i], gen)); + } + + // what to do if the trigger was rejected + gen->setTriggerFalseHook( + [gen](std::vector const &p, int eventCount) { + gen->decrementGeneratedEvents(); + }); + } + + return gen; +} diff --git a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini new file mode 100644 index 000000000..b79dcc87a --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini @@ -0,0 +1,7 @@ +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C +funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"",1) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg diff --git a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini new file mode 100644 index 000000000..cff8aa385 --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini @@ -0,0 +1,7 @@ +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C +funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"non-prompt:0.2",0) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiMidy_PythiaOnia_TriggerGap.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiMidy_PythiaOnia_TriggerGap.C new file mode 100644 index 000000000..45dafa928 --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiMidy_PythiaOnia_TriggerGap.C @@ -0,0 +1,102 @@ +int External() +{ + int checkPdgSignal[] = {443, 100443}; + int checkPdgDecay = 11; + double rapiditymin = -4.3; + double rapiditymax = -2.3; + std::string path{"o2sim_Kine.root"}; + std::cout << "Check for\nsignal PDG " << checkPdgSignal << "\ndecay PDG " << checkPdgDecay << "\n"; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + auto nEvents = tree->GetEntries(); + o2::steer::MCKinematicsReader mcreader("o2sim", o2::steer::MCKinematicsReader::Mode::kMCKine); + Bool_t isInjected = kFALSE; + + for (int i = 0; i < nEvents; i++) + { + tree->GetEntry(i); + for (auto &track : *tracks) + { + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) + { + // count leptons + nLeptons++; + } + else if (pdg == -checkPdgDecay) + { + // count anti-leptons + nAntileptons++; + } + else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) + { + if (idMoth < 0) + { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) + { + pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++; + } + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) + { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) + { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) + { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#lepton pairs: " << nLeptonPairs << "\n" + << "#lepton pairs to be done: " << nLeptonPairs << "\n"; + + if (nLeptonPairs == 0 || nLeptons == 0 || nAntileptons == 0) + { + std::cerr << "Number of leptons, number of anti-leptons as well as number of lepton pairs should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) + { + std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiPsi2SFwdy_Pythia8_TriggerGap_pp5TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiPsi2SFwdy_Pythia8_TriggerGap_pp5TeV.C new file mode 100644 index 000000000..d559770ed --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedPromptJpsiPsi2SFwdy_Pythia8_TriggerGap_pp5TeV.C @@ -0,0 +1,102 @@ +int External() +{ + int checkPdgSignal[] = {443, 100443}; + int checkPdgDecay = 13; + double rapiditymin = -4.3; + double rapiditymax = -2.3; + std::string path{"o2sim_Kine.root"}; + std::cout << "Check for\nsignal PDG " << checkPdgSignal << "\ndecay PDG " << checkPdgDecay << "\n"; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + auto nEvents = tree->GetEntries(); + o2::steer::MCKinematicsReader mcreader("o2sim", o2::steer::MCKinematicsReader::Mode::kMCKine); + Bool_t isInjected = kFALSE; + + for (int i = 0; i < nEvents; i++) + { + tree->GetEntry(i); + for (auto &track : *tracks) + { + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) + { + // count leptons + nLeptons++; + } + else if (pdg == -checkPdgDecay) + { + // count anti-leptons + nAntileptons++; + } + else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) + { + if (idMoth < 0) + { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) + { + pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++; + } + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) + { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) + { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) + { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#lepton pairs: " << nLeptonPairs << "\n" + << "#lepton pairs to be done: " << nLeptonPairs << "\n"; + + if (nLeptonPairs == 0 || nLeptons == 0 || nAntileptons == 0) + { + std::cerr << "Number of leptons, number of anti-leptons as well as number of lepton pairs should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) + { + std::cerr << "The number of lepton pairs should be the same as the number of lepton pairs which should be transported.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.C b/MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.C new file mode 100644 index 000000000..d23d3e654 --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.C @@ -0,0 +1,77 @@ +int External() { + int checkPdgSignal = 443; + int checkPdgDecay = 11; + std::string path{"o2sim_Kine.root"}; + std::cout << "Check for\nsignal PDG " << checkPdgSignal << "\ndecay PDG " + << checkPdgDecay << "\n"; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + int nSignal{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto &track : *tracks) { + auto pdg = track.GetPdgCode(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + } else if (pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + } else if (pdg == checkPdgSignal) { + // count signal PDG + nSignal++; + auto child0 = + o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = + o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + std::cout << "First and last children of parent " << checkPdgSignal + << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && + std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#signal: " << nSignal << "\n" + << "#lepton pairs: " << nLeptonPairs << "\n" + << "#lepton pairs to be done: " << nLeptonPairs << "\n"; + + if (nLeptonPairs == 0 || nLeptons == 0 || nAntileptons == 0) { + std::cerr << "Number of leptons, number of anti-leptons as well as number " + "of lepton pairs should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) { + std::cerr << "The number of lepton pairs should be the same as the number " + "of lepton pairs which should be transported.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.C b/MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.C new file mode 100644 index 000000000..d23d3e654 --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.C @@ -0,0 +1,77 @@ +int External() { + int checkPdgSignal = 443; + int checkPdgDecay = 11; + std::string path{"o2sim_Kine.root"}; + std::cout << "Check for\nsignal PDG " << checkPdgSignal << "\ndecay PDG " + << checkPdgDecay << "\n"; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nLeptonPairsToBeDone{}; + int nSignal{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto &track : *tracks) { + auto pdg = track.GetPdgCode(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + } else if (pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + } else if (pdg == checkPdgSignal) { + // count signal PDG + nSignal++; + auto child0 = + o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = + o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + std::cout << "First and last children of parent " << checkPdgSignal + << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && + std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#signal: " << nSignal << "\n" + << "#lepton pairs: " << nLeptonPairs << "\n" + << "#lepton pairs to be done: " << nLeptonPairs << "\n"; + + if (nLeptonPairs == 0 || nLeptons == 0 || nAntileptons == 0) { + std::cerr << "Number of leptons, number of anti-leptons as well as number " + "of lepton pairs should all be greater than 1.\n"; + return 1; + } + if (nLeptonPairs != nLeptonPairsToBeDone) { + std::cerr << "The number of lepton pairs should be the same as the number " + "of lepton pairs which should be transported.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkonia.cfg b/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkonia.cfg new file mode 100644 index 000000000..2d5c57ade --- /dev/null +++ b/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkonia.cfg @@ -0,0 +1,10 @@ +#switch off all quarkonia and B hadrons decays + +443:mayDecay off # J/psi +100443:mayDecay off # psi(2S) +10441:mayDecay off # chic0 +20443:mayDecay off # chic1 +445:mayDecay off # chic2 +553:mayDecay off # upsilon1S +100553:mayDecay off # upsilon2S +200553:mayDecay off # upsilon3S diff --git a/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkoniaAndBhadrons.cfg b/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkoniaAndBhadrons.cfg new file mode 100644 index 000000000..2711096f1 --- /dev/null +++ b/MC/config/PWGDQ/pythia8/decayer/switchOffAllQuarkoniaAndBhadrons.cfg @@ -0,0 +1,18 @@ +#switch off all quarkonia and B hadrons decays + +443:mayDecay off # J/psi +100443:mayDecay off # psi(2S) +10441:mayDecay off # chic0 +20443:mayDecay off # chic1 +445:mayDecay off # chic2 +553:mayDecay off # upsilon1S +100553:mayDecay off # upsilon2S +200553:mayDecay off # upsilon3S +511:mayDecay off # B0 +521:mayDecay off # B+ +531:mayDecay off # Bs +541:mayDecay off # Bc +5122:mayDecay off # Lambdab +5132:mayDecay off # Xib+ +5232:mayDecay off # Xib0 +5332:mayDecay off # Omegab diff --git a/MC/config/PWGDQ/pythia8/decayer/switchOffJpsi.cfg b/MC/config/PWGDQ/pythia8/decayer/switchOffJpsi.cfg index 2cc29e535..4bc262efc 100644 --- a/MC/config/PWGDQ/pythia8/decayer/switchOffJpsi.cfg +++ b/MC/config/PWGDQ/pythia8/decayer/switchOffJpsi.cfg @@ -1,4 +1,4 @@ #switch off jpsi decays -443:mayDecay off # J/psi -100443:mayDecay off # psi(2S) +443:mayDecay off # J/psi +100443:mayDecay off # psi(2S) diff --git a/MC/config/PWGDQ/pythia8/decayer/switchOffJpsiUpsilon.cfg b/MC/config/PWGDQ/pythia8/decayer/switchOffJpsiUpsilon.cfg new file mode 100644 index 000000000..e63c5365f --- /dev/null +++ b/MC/config/PWGDQ/pythia8/decayer/switchOffJpsiUpsilon.cfg @@ -0,0 +1,7 @@ +#switch off jpsi decays + +443:mayDecay off # J/psi +100443:mayDecay off # psi(2S) +553:mayDecay off # Upsilon(1S) +100553:mayDecay off # Upsilon(2S) +200553:mayDecay off # Upsilon(3S) diff --git a/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg b/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg new file mode 100644 index 000000000..b72c19fb9 --- /dev/null +++ b/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg @@ -0,0 +1,12 @@ +### beams +Beams:idA 2212 # proton +Beams:idB 2212 # proton +Beams:eCM 13600. # GeV + +### processes +SoftQCD:inelastic on # all inelastic processes +OniaShower:all = on + +### decays +ParticleDecays:limitTau0 on +ParticleDecays:tau0Max 10. diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma.ini index e85fd52ec..64eca1631 100644 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma.ini +++ b/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma.ini @@ -3,7 +3,7 @@ [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFgamma.C -funcName = GeneratorPythia8GapTriggeredLFgamma_ForEM("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg", 5, -1.2, +1.2, 1) +funcName = GeneratorPythia8GapTriggeredLFgamma_ForEM(5, -1.2, +1.2, 1) [GeneratorPythia8] config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap2.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap2.ini deleted file mode 100644 index bd946b5d4..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap2.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFgamma.C -funcName = GeneratorPythia8GapTriggeredLFgamma_ForEM(2+1, -1.2, +1.2, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap4.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap4.ini deleted file mode 100644 index fa7589f4e..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap4.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFgamma.C -funcName = GeneratorPythia8GapTriggeredLFgamma_ForEM(4+1, -1.2, +1.2, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap6.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap6.ini deleted file mode 100644 index 907d50479..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np1_gap6.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFgamma.C -funcName = GeneratorPythia8GapTriggeredLFgamma_ForEM(6+1, -1.2, +1.2, 1) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np3_gap5.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np3_gap5.ini deleted file mode 100644 index 8a421664d..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np3_gap5.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFgamma.C -funcName = GeneratorPythia8GapTriggeredLFgamma_ForEM(5+1, -1.2, +1.2, 3) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg diff --git a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np5_gap5.ini b/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np5_gap5.ini deleted file mode 100644 index f78a94205..000000000 --- a/MC/config/PWGEM/ini/Generator_GapTriggered_LFgamma_np5_gap5.ini +++ /dev/null @@ -1,9 +0,0 @@ -### The setup uses an external event generator -### This part sets the path of the file and the function call to retrieve it - -[GeneratorExternal] -fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/external/generator/Generator_pythia8_GapTriggered_LFgamma.C -funcName = GeneratorPythia8GapTriggeredLFgamma_ForEM(5+1, -1.2, +1.2, 5) - -[GeneratorPythia8] -config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGEM/pythia8/generator/pythia8_MB_gapevent.cfg From d1a26b634d70d47e33077cacca90698817d5c602 Mon Sep 17 00:00:00 2001 From: shahoian Date: Fri, 2 Oct 2026 15:14:53 +0200 Subject: [PATCH 093/144] Ensure the ITS ramp-up offset to SOR orbit is multiple of orbitsPerTF --- MC/bin/o2dpg_sim_workflow_anchored.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow_anchored.py b/MC/bin/o2dpg_sim_workflow_anchored.py index 2ece8ad7f..b9d5f1404 100755 --- a/MC/bin/o2dpg_sim_workflow_anchored.py +++ b/MC/bin/o2dpg_sim_workflow_anchored.py @@ -327,7 +327,7 @@ def milliseconds_to_orbits(milliseconds): """ return int(math.ceil(1000. * milliseconds / LHCOrbitMUS)) -def shift_anchor_past_ITS_rampup(run_start, first_orbit, ITS_rampup): +def shift_anchor_past_ITS_rampup(run_start, first_orbit, orbitsPerTF, ITS_rampup): """ Moves the anchoring point past the ITS ramp-up period, in both of its coordinates, and returns the pair (start of run in ms, first orbit). @@ -336,7 +336,10 @@ def shift_anchor_past_ITS_rampup(run_start, first_orbit, ITS_rampup): timestamp leaves a job at production offset 0 inside the ramp, where the ITS time-dead map masks every chip. """ - return run_start + ITS_rampup, first_orbit + milliseconds_to_orbits(ITS_rampup) + # convert ITS_rampup to orbits multiple to orbitsPerTF + rampupOrbits = ((milliseconds_to_orbits(ITS_rampup) + orbitsPerTF - 1) // orbitsPerTF ) * orbitsPerTF + + return run_start + int(rampupOrbits * LHCOrbitMUS / 1000.), first_orbit + rampupOrbits def retrieve_MinBias_CTPScaler_Rate(raw_rate_at, finaltime, trig_eff_arg, NBunches, ColSystem, eCM, run_number = -1): """ @@ -611,7 +614,7 @@ def main(): # Adjust the anchoring point using the ITS ramp-up period ITS_rampup = retrieve_ITS_RampDuration(ccdbreader, run_start) print(f"ITS ramp-up time: {ITS_rampup} ms") - effective_run_start, effective_first_orbit = shift_anchor_past_ITS_rampup(run_start, GLOparams["FirstOrbit"], ITS_rampup) + effective_run_start, effective_first_orbit = shift_anchor_past_ITS_rampup(run_start, GLOparams["FirstOrbit"], GLOparams["OrbitsPerTF"], ITS_rampup) mid_run_timestamp = (effective_run_start + run_end) // 2 # -------- From 1d96c11f4f7d7dbc02e5b03a26f823259c0b1fc7 Mon Sep 17 00:00:00 2001 From: sawan <124118453+sawankumawat@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:03:07 +0530 Subject: [PATCH 094/144] [PWGLF] Added twoPhi trigger MC and renamed codes based on convention (#2490) * Added twoPhi trigger MC and renamed codes based on convention * commented out recurring print statements and injection logic --- .../ini/GeneratorLF_doublephi_trigger.ini | 10 ++ .../PWGLF/ini/GeneratorLF_phiphiInvMass.ini | 4 +- .../ini/tests/GeneratorLF_doublephi_trigger.C | 76 ++++++++ .../ini/tests/GeneratorLF_phiphiInvMass.C | 28 +-- ...LF_phiphi.C => generator_pythia8_phiphi.C} | 63 +++---- .../generator_pythia8_twophi_trigger.C | 169 ++++++++++++++++++ 6 files changed, 280 insertions(+), 70 deletions(-) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_doublephi_trigger.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_trigger.C rename MC/config/PWGLF/pythia8/{GeneratorLF_phiphi.C => generator_pythia8_phiphi.C} (71%) create mode 100644 MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C diff --git a/MC/config/PWGLF/ini/GeneratorLF_doublephi_trigger.ini b/MC/config/PWGLF/ini/GeneratorLF_doublephi_trigger.ini new file mode 100644 index 000000000..6f1dcd71a --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_doublephi_trigger.ini @@ -0,0 +1,10 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C +funcName=generateDoublePhi(0, 0.0, 100.0, 0.8) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonances.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini b/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini index d9ad4041c..e74204392 100644 --- a/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini +++ b/MC/config/PWGLF/ini/GeneratorLF_phiphiInvMass.ini @@ -1,6 +1,6 @@ [GeneratorExternal] -fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C -funcName=generatePhiResonanceGun(999999, 888888, 0.0, 50.0, 100.0, -1.0, 1.0, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", 3) +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_phiphi.C +funcName=generatePhiResonanceGun(999999, 0.0, 50.0, -1.0, 1.0, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", 0) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_trigger.C b/MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_trigger.C new file mode 100644 index 000000000..58767f843 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_trigger.C @@ -0,0 +1,76 @@ +int External() +{ + const std::string path{"o2sim_Kine.root"}; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Counters + int nMBPhi = 0; + int nKPlusFromMBPhi = 0; + int nKMinusFromMBPhi = 0; + int numberOfEventsProcessed = 0; + + for (Long64_t i = 0; i < tree->GetEntries(); ++i) + { + tree->GetEntry(i); + ++numberOfEventsProcessed; + + for (size_t idx = 0; idx < tracks->size(); ++idx) + { + const auto &track = tracks->at(idx); + const auto pdg = track.GetPdgCode(); + + if (pdg == 333) + { + ++nMBPhi; + + if (track.getFirstDaughterTrackId() >= 0) + { + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) + { + auto dauPdg = tracks->at(j).GetPdgCode(); + if (dauPdg == 321) + { + ++nKPlusFromMBPhi; + } + if (dauPdg == -321) + { + ++nKMinusFromMBPhi; + } + } + } + } + } + } + + // --------------------------- Output --------------------------- + std::cout << "=================================================\n"; + std::cout << "Total Events: " << tree->GetEntries() << "\n\n"; + std::cout << "Total events processed: " << numberOfEventsProcessed << "\n"; + std::cout << "Total Minimum Bias Phi (333): " << nMBPhi << "\n"; + std::cout << " -> Decayed to K+: " << nKPlusFromMBPhi << "\n"; + std::cout << " -> Decayed to K-: " << nKMinusFromMBPhi << "\n"; + std::cout << "=================================================\n"; + + return 0; +} + +void GeneratorLF_doublephi_trigger() +{ + External(); +} diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C b/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C index 4f08d32f8..f86d9e588 100644 --- a/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_phiphiInvMass.C @@ -28,11 +28,8 @@ int External() int nKPlusFromResonancePhi = 0; int nKMinusFromResonancePhi = 0; - int nDirectInjectedPhi = 0; int nMBPhi = 0; - int nKPlusFromDirectPhi = 0; - int nKMinusFromDirectPhi = 0; int nKPlusFromMBPhi = 0; int nKMinusFromMBPhi = 0; @@ -86,24 +83,6 @@ int External() } } - else if (pdg == 888888) - { - ++nDirectInjectedPhi; - hasInjection = true; - - if (track.getFirstDaughterTrackId() >= 0) - { - for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) - { - auto dauPdg = tracks->at(j).GetPdgCode(); - if (dauPdg == 321) - ++nKPlusFromDirectPhi; - if (dauPdg == -321) - ++nKMinusFromDirectPhi; - } - } - } - // 2. Process Phi (333) Mesons else if (pdg == 333) { @@ -149,12 +128,7 @@ int External() std::cout << " -> Decayed to K+: " << nKPlusFromResonancePhi << "\n"; std::cout << " -> Decayed to K-: " << nKMinusFromResonancePhi << "\n\n"; - std::cout << "--- 2. DIRECTLY INJECTED PHI (333) ---\n"; - std::cout << "Total Directly Injected Phi (333): " << nDirectInjectedPhi << "\n"; - std::cout << " -> Decayed to K+: " << nKPlusFromDirectPhi << "\n"; - std::cout << " -> Decayed to K-: " << nKMinusFromDirectPhi << "\n\n"; - - std::cout << "--- 3. MINIMUM BIAS PHI (333) ---\n"; + std::cout << "--- 2. MINIMUM BIAS PHI (333) ---\n"; std::cout << "Total Minimum Bias Phi (333): " << nMBPhi << "\n"; std::cout << " -> Decayed to K+: " << nKPlusFromMBPhi << "\n"; std::cout << " -> Decayed to K-: " << nKMinusFromMBPhi << "\n"; diff --git a/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C b/MC/config/PWGLF/pythia8/generator_pythia8_phiphi.C similarity index 71% rename from MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C rename to MC/config/PWGLF/pythia8/generator_pythia8_phiphi.C index 8537ebd3a..46aa5b75d 100644 --- a/MC/config/PWGLF/pythia8/GeneratorLF_phiphi.C +++ b/MC/config/PWGLF/pythia8/generator_pythia8_phiphi.C @@ -25,19 +25,14 @@ class GeneratorPhiResonance : public o2::eventgen::GeneratorPythia8 { public: GeneratorPhiResonance(int resoPDG = 999999, - int customPhiPDG = 888888, - float ptMin = 0.0, float ptMax = 50.0, float ptMaxPhi = 100.0, + float ptMin = 0.0, float ptMax = 50.0, float yMin = -1.0, float yMax = 1.0, std::string pythiaCfgMb = "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", - int signalInterval = 3) - : GeneratorPythia8(), mResoPDG(resoPDG), mCustomPhiPDG(customPhiPDG), mPtMin(ptMin), mPtMaxPhiPhi(ptMax), mPtMaxPhi(ptMaxPhi), mYMin(yMin), mYMax(yMax), mSignalInterval(signalInterval) + int signalInterval = 0) + : GeneratorPythia8(), mResoPDG(resoPDG), mPtMin(ptMin), mPtMaxPhiPhi(ptMax), mYMin(yMin), mYMax(yMax), mSignalInterval(signalInterval) { - // 1. Define Custom Directly Injected Phi (PDG: 888888) with mass, width, and decay to kaons - std::string createCustomPhi = std::to_string(mCustomPhiPDG) + ":new = custom_phi custom_phi 3 0 0 1.019461 0.004249 0.980 1.100 0.0"; - std::string customPhiMayDecay = std::to_string(mCustomPhiPDG) + ":mayDecay = on"; - std::string addPhiDecayKPlusKMinus = std::to_string(mCustomPhiPDG) + ":addChannel = 1 0.492 0 321 -321"; - // 2. Define Custom Signal Resonance (PDG: 999999) decay into standard Phis (333 333) + // 1. Define Custom Signal Resonance (PDG: 999999) decay into standard Phis (333 333) std::string createReso = std::to_string(mResoPDG) + ":new = f2_Custom void 5 0 0 2.714 0.012 2.05 3.50 0.0"; std::string resoMayDecay = std::to_string(mResoPDG) + ":mayDecay = on"; std::string addResoDecay = std::to_string(mResoPDG) + ":addChannel = 1 1.0 0 333 333"; @@ -45,12 +40,11 @@ public: // Helper lambda to load custom particle definitions across ALL Pythia engines auto applyCustomParticles = [&](Pythia8::Pythia &pythiaInst) { - pythiaInst.readString(createCustomPhi); - pythiaInst.readString(customPhiMayDecay); - pythiaInst.readString(addPhiDecayKPlusKMinus); pythiaInst.readString(createReso); pythiaInst.readString(resoMayDecay); pythiaInst.readString(addResoDecay); + pythiaInst.readString("333:mayDecay = on"); + pythiaInst.readString("333:oneChannel = 1 1.0 0 321 -321"); }; // 1: Apply particle definitions to mPythia, mPythiaGun, and pythiaObjectMinimumBias @@ -99,15 +93,15 @@ public: mPythiaGun.event.reset(); // 3. Inject Signal Gun Particles into mPythiaGun - if (mEventCounter % mSignalInterval == 0) + if (mSignalInterval > 0 && mEventCounter % mSignalInterval == 0) { - // Resonant signal -> Decays into 333 333 (Standard Phis) - injectParticle(mResoPDG, 1, true); + // Resonant signal -> Decays into 333 333 + injectParticle(mResoPDG, 1); } - else + else if (mSignalInterval <= 0) { - // Directly injected uncorrelated Phi -> Uses Custom PDG 888888 - injectParticle(mCustomPhiPDG, 2, false); + // Continuous signal -> Decays into 333 333 + injectParticle(mResoPDG, 1); } // 4. Force Decay of injected particles using Pythia's Decayer @@ -157,7 +151,7 @@ private: mPythia.event.restorePtrs(); } - void injectParticle(int pdg, int nParticles, bool thermalPt) + void injectParticle(int pdg, int nParticles) { const double phiMass = 1.019461; @@ -167,27 +161,15 @@ private: const double phi = gRandom->Uniform(0, TMath::TwoPi()); double mass = 0.0; - if (pdg == mResoPDG) - { - do - { - mass = gRandom->BreitWigner(2.714, 0.012); - } while (mass <= 2.0 * phiMass || mass < 2.05 || mass > 3.50); - } - else + + do { - mass = mPythiaGun.particleData.mSel(333); // Use standard phi mass for directly injected custom phi - } + mass = gRandom->BreitWigner(2.714, 0.012); + } while (mass <= 2.0 * phiMass || mass < 2.05 || mass > 3.50); double pt; - if (thermalPt) - { - pt = gRandom->Uniform(mPtMin, mPtMaxPhiPhi); - } - else - { - pt = gRandom->Uniform(mPtMin, mPtMaxPhi); - } + + pt = gRandom->Uniform(mPtMin, mPtMaxPhiPhi); const double px = pt * std::cos(phi); const double py = pt * std::sin(phi); @@ -214,16 +196,15 @@ private: int mEventCounter = 0; int mResoPDG; - int mCustomPhiPDG; int mSignalInterval; - float mPtMin, mPtMaxPhiPhi, mPtMaxPhi, mYMin, mYMax; + float mPtMin, mPtMaxPhiPhi, mYMin, mYMax; Pythia8::Pythia mPythiaGun; Pythia8::Pythia pythiaObjectMinimumBias; }; /// Entry point for o2-sim -FairGenerator *generatePhiResonanceGun(int resoPDG = 999999, int customPhiPDG = 888888, float ptMin = 0.0, float ptMax = 50.0, float ptMaxPhi = 100.0, float yMin = -1.0, float yMax = 1.0, std::string pythiaCfgMb = "", int signalInterval = 3) +FairGenerator *generatePhiResonanceGun(int resoPDG = 999999, float ptMin = 0.0, float ptMax = 50.0, float yMin = -1.0, float yMax = 1.0, std::string pythiaCfgMb = "", int signalInterval = 0) { - return new GeneratorPhiResonance(resoPDG, customPhiPDG, ptMin, ptMax, ptMaxPhi, yMin, yMax, pythiaCfgMb, signalInterval); + return new GeneratorPhiResonance(resoPDG, ptMin, ptMax, yMin, yMax, pythiaCfgMb, signalInterval); } \ No newline at end of file diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C new file mode 100644 index 000000000..8de46d445 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C @@ -0,0 +1,169 @@ +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "FairGenerator.h" +#include "FairPrimaryGenerator.h" +#include "Generators/GeneratorPythia8.h" +#include "Pythia8/Pythia.h" +#include "TDatabasePDG.h" +#include "TMath.h" +#include "TParticlePDG.h" +#include "TRandom3.h" +#include "TSystem.h" +#include "TVector2.h" +#include "fairlogger/Logger.h" +#include +#include +#include +#include +using namespace Pythia8; +#endif + +/// Event generator using Pythia ropes (Adapted from task generator_pythia8_doubleLambdas.C) +/// Triggers events containing at least two generated phi(1020) mesons. + +class GeneratorPythia8DoublePhi : public o2::eventgen::GeneratorPythia8 +{ +public: + /// Constructor + GeneratorPythia8DoublePhi(int gapSize = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8) + : o2::eventgen::GeneratorPythia8(), + mGapSize(gapSize), + mMinPt(minPt), + mMaxPt(maxPt), + mMaxEta(maxEta) + { + fmt::printf(">> Pythia8 generator: two phi(1020) mesons, gap = %d, minPtPhi = %f, maxPtPhi = %f, |etaPhi| < %f\n", gapSize, minPt, maxPt, maxEta); + } + /// Destructor + ~GeneratorPythia8DoublePhi() = default; + + bool Init() override + { + addSubGenerator(0, "Pythia8 events with two phi(1020) mesons"); + return o2::eventgen::GeneratorPythia8::Init(); + } + +protected: + bool isPhiFromHFDecay(const Pythia8::Particle &p, const Pythia8::Event &event) + { + + // Walk up ancestry + int motherId = p.mother1(); + + while (motherId > 0) + { + // Get mother + const auto &mother = event[motherId]; + const int absMotherPdg = std::abs(mother.id()); + + // Check if particle is from HF decay + if (((absMotherPdg / 100) % 10 == 4) || + ((absMotherPdg / 100) % 10 == 5) || + ((absMotherPdg / 1000) % 10 == 4) || + ((absMotherPdg / 1000) % 10 == 5)) + { + return true; + } + + motherId = mother.mother1(); + } + return false; + } + + bool generateEvent() override + { + // fmt::printf(">> Generating event %d\n", mGeneratedEvents); + + bool genOk = false; + int localCounter{0}; + constexpr int kMaxTries{100000}; + + // If mGapSize <= 0, filter ALL events to contain two phis. + // Otherwise, generate mGapSize gap events before 1 triggered event. + if (mGapSize > 0 && (mGeneratedEvents % (mGapSize + 1) < mGapSize)) + { + genOk = GeneratorPythia8::generateEvent(); + // fmt::printf(">> Gap-event (no phi check)\n"); + } + else + { + while (!genOk && localCounter < kMaxTries) + { + if (GeneratorPythia8::generateEvent()) + { + genOk = selectEvent(mPythia.event); + } + localCounter++; + } + if (!genOk) + { + fmt::printf("Failed to generate triggered event after %d tries\n", kMaxTries); + return false; + } + // fmt::printf(">> Triggered event: event accepted after %d iterations (double phi(1020))\n", localCounter); + } + + notifySubGenerator(0); + mGeneratedEvents++; + return true; + } + + bool selectEvent(Pythia8::Event &event) + { + int nPhi{0}; + + for (int i = 0; i < event.size(); i++) + { + const auto &p = event[i]; + + if (std::abs(p.id()) != 333) + continue; + + if (p.pT() < mMinPt || p.pT() > mMaxPt) + continue; + + if (std::abs(p.eta()) > mMaxEta) + continue; + + if (isPhiFromHFDecay(p, event)) + continue; + + // // Avoid double-counting copy/history entries: + // // Ensure this is the physical produced phi (e.g. check if its daughter is a copy of itself) + // int d1 = p.daughter1(); + // int d2 = p.daughter2(); + // if (d1 > 0 && d1 == d2 && std::abs(event[d1].id()) == 333) + // { + // // p decayed into another copy of phi, so skip this intermediate entry + // continue; + // } + + nPhi++; + } + if (nPhi < 2) + return false; + + return true; + } + +private: + int mGapSize{0}; + double mMinPt{0.0}; + double mMaxPt{100.0}; + double mMaxEta{0.8}; + uint64_t mGeneratedEvents{0}; +}; + +///___________________________________________________________ +FairGenerator *generateDoublePhi(int gap = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8) +{ + auto myGenerator = new GeneratorPythia8DoublePhi(gap, minPt, maxPt, maxEta); + + myGenerator->readString("333:onMode = off"); + myGenerator->readString("333:onIfMatch = 321 -321"); + + auto seed = (gRandom->TRandom::GetSeed() % 900000000); + myGenerator->readString("Random:setSeed on"); + myGenerator->readString("Random:seed " + std::to_string(seed)); + + return myGenerator; +} \ No newline at end of file From a12b1f7cb521bbf90e71df25074953b320eddd21 Mon Sep 17 00:00:00 2001 From: sawan <124118453+sawankumawat@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:38:37 +0530 Subject: [PATCH 095/144] Added phi-phi trigger code with additional selections to reduce total number of events (#2492) --- ...GeneratorLF_doublephi_triggerMasspTcut.ini | 10 + .../GeneratorLF_doublephi_triggerMasspTcut.C | 76 +++++++ .../generator_pythia8_twophi_trigger.C | 18 +- .../generator_pythia8_twophi_triggerMassCut.C | 188 ++++++++++++++++++ 4 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_triggerMasspTcut.C create mode 100644 MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C diff --git a/MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini b/MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini new file mode 100644 index 000000000..a51496b44 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini @@ -0,0 +1,10 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C +funcName=generateDoublePhi(0, 0.0, 100.0, 0.8) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonances.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_triggerMasspTcut.C b/MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_triggerMasspTcut.C new file mode 100644 index 000000000..58767f843 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_doublephi_triggerMasspTcut.C @@ -0,0 +1,76 @@ +int External() +{ + const std::string path{"o2sim_Kine.root"}; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) + { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + // Counters + int nMBPhi = 0; + int nKPlusFromMBPhi = 0; + int nKMinusFromMBPhi = 0; + int numberOfEventsProcessed = 0; + + for (Long64_t i = 0; i < tree->GetEntries(); ++i) + { + tree->GetEntry(i); + ++numberOfEventsProcessed; + + for (size_t idx = 0; idx < tracks->size(); ++idx) + { + const auto &track = tracks->at(idx); + const auto pdg = track.GetPdgCode(); + + if (pdg == 333) + { + ++nMBPhi; + + if (track.getFirstDaughterTrackId() >= 0) + { + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) + { + auto dauPdg = tracks->at(j).GetPdgCode(); + if (dauPdg == 321) + { + ++nKPlusFromMBPhi; + } + if (dauPdg == -321) + { + ++nKMinusFromMBPhi; + } + } + } + } + } + } + + // --------------------------- Output --------------------------- + std::cout << "=================================================\n"; + std::cout << "Total Events: " << tree->GetEntries() << "\n\n"; + std::cout << "Total events processed: " << numberOfEventsProcessed << "\n"; + std::cout << "Total Minimum Bias Phi (333): " << nMBPhi << "\n"; + std::cout << " -> Decayed to K+: " << nKPlusFromMBPhi << "\n"; + std::cout << " -> Decayed to K-: " << nKMinusFromMBPhi << "\n"; + std::cout << "=================================================\n"; + + return 0; +} + +void GeneratorLF_doublephi_trigger() +{ + External(); +} diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C index 8de46d445..af6366971 100644 --- a/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C +++ b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_trigger.C @@ -127,15 +127,15 @@ protected: if (isPhiFromHFDecay(p, event)) continue; - // // Avoid double-counting copy/history entries: - // // Ensure this is the physical produced phi (e.g. check if its daughter is a copy of itself) - // int d1 = p.daughter1(); - // int d2 = p.daughter2(); - // if (d1 > 0 && d1 == d2 && std::abs(event[d1].id()) == 333) - // { - // // p decayed into another copy of phi, so skip this intermediate entry - // continue; - // } + // Avoid double-counting copy/history entries: + // Ensure this is the physical produced phi (e.g. check if its daughter is a copy of itself) + int d1 = p.daughter1(); + int d2 = p.daughter2(); + if (d1 > 0 && d1 == d2 && std::abs(event[d1].id()) == 333) + { + // p decayed into another copy of phi, so skip this intermediate entry + continue; + } nPhi++; } diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C new file mode 100644 index 000000000..4cb177bc3 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C @@ -0,0 +1,188 @@ +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "FairGenerator.h" +#include "FairPrimaryGenerator.h" +#include "Generators/GeneratorPythia8.h" +#include "Pythia8/Pythia.h" +#include "TDatabasePDG.h" +#include "TMath.h" +#include "TParticlePDG.h" +#include "TRandom3.h" +#include "TSystem.h" +#include "TVector2.h" +#include "fairlogger/Logger.h" +#include +#include +#include +#include "TLorentzVector.h" +#include +using namespace Pythia8; +#endif + +/// Event generator using Pythia ropes +/// Triggers events containing at least two generated phi(1020) mesons. + +class GeneratorPythia8DoublePhi : public o2::eventgen::GeneratorPythia8 +{ +public: + /// Constructor + GeneratorPythia8DoublePhi(int gapSize = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8) + : o2::eventgen::GeneratorPythia8(), + mGapSize(gapSize), + mMinPt(minPt), + mMaxPt(maxPt), + mMaxEta(maxEta) + { + fmt::printf(">> Pythia8 generator: two phi(1020) mesons, gap = %d, minPtPhi = %f, maxPtPhi = %f, |etaPhi| < %f\n", gapSize, minPt, maxPt, maxEta); + } + /// Destructor + ~GeneratorPythia8DoublePhi() = default; + + bool Init() override + { + addSubGenerator(0, "Pythia8 events with two phi(1020) mesons"); + return o2::eventgen::GeneratorPythia8::Init(); + } + +protected: + bool isPhiFromHFDecay(const Pythia8::Particle &p, const Pythia8::Event &event) + { + + // Walk up ancestry + int motherId = p.mother1(); + + while (motherId > 0) + { + // Get mother + const auto &mother = event[motherId]; + const int absMotherPdg = std::abs(mother.id()); + + // Check if particle is from HF decay + if (((absMotherPdg / 100) % 10 == 4) || + ((absMotherPdg / 100) % 10 == 5) || + ((absMotherPdg / 1000) % 10 == 4) || + ((absMotherPdg / 1000) % 10 == 5)) + { + return true; + } + + motherId = mother.mother1(); + } + return false; + } + + bool generateEvent() override + { + // fmt::printf(">> Generating event %d\n", mGeneratedEvents); + + bool genOk = false; + int localCounter{0}; + constexpr int kMaxTries{300000}; + + // If mGapSize <= 0, filter ALL events to contain two phis. + // Otherwise, generate mGapSize gap events before 1 triggered event. + if (mGapSize > 0 && (mGeneratedEvents % (mGapSize + 1) < mGapSize)) + { + genOk = GeneratorPythia8::generateEvent(); + // fmt::printf(">> Gap-event (no phi check)\n"); + } + else + { + while (!genOk && localCounter < kMaxTries) + { + if (GeneratorPythia8::generateEvent()) + { + genOk = selectEvent(mPythia.event); + } + localCounter++; + } + if (!genOk) + { + fmt::printf("Failed to generate triggered event after %d tries\n", kMaxTries); + return false; + } + fmt::printf(">> Triggered event: event accepted after %d iterations (double phi(1020))\n", localCounter); + } + + notifySubGenerator(0); + mGeneratedEvents++; + return true; + } + + bool selectEvent(Pythia8::Event &event) + { + std::vector phiCandidates; + + for (int i = 0; i < event.size(); i++) + { + const auto &p = event[i]; + + if (std::abs(p.id()) != 333) + continue; + + if (p.pT() < mMinPt || p.pT() > mMaxPt) + continue; + + if (std::abs(p.eta()) > mMaxEta) + continue; + + if (isPhiFromHFDecay(p, event)) + continue; + + // Avoid double-counting copy/history entries: + // Ensure this is the physical produced phi (e.g. check if its daughter is a copy of itself) + int d1 = p.daughter1(); + int d2 = p.daughter2(); + if (d1 > 0 && d1 == d2 && std::abs(event[d1].id()) == 333) + { + // p decayed into another copy of phi, so skip this intermediate entry + continue; + } + + TLorentzVector phi; + phi.SetPtEtaPhiM(p.pT(), p.eta(), p.phi(), p.m()); + + phiCandidates.push_back(phi); + } + if (phiCandidates.size() < 2) + return false; + + // Check all possible phi-phi pairs + for (size_t i = 0; i < phiCandidates.size(); i++) + { + for (size_t j = i + 1; j < phiCandidates.size(); j++) + { + TLorentzVector phiPhi = phiCandidates[i] + phiCandidates[j]; + + double mass = phiPhi.M(); + double pt = phiPhi.Pt(); + + if (mass > 2.4 && pt > 4.0) + return true; + } + } + + return false; + } + +private: + int mGapSize{0}; + double mMinPt{0.0}; + double mMaxPt{100.0}; + double mMaxEta{0.8}; + uint64_t mGeneratedEvents{0}; +}; + +///___________________________________________________________ +FairGenerator *generateDoublePhi(int gap = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8) +{ + auto myGenerator = new GeneratorPythia8DoublePhi(gap, minPt, maxPt, maxEta); + + myGenerator->readString("333:onMode = off"); + myGenerator->readString("333:onIfMatch = 321 -321"); + + auto seed = (gRandom->TRandom::GetSeed() % 900000000); + myGenerator->readString("Random:setSeed on"); + myGenerator->readString("Random:seed " + std::to_string(seed)); + + return myGenerator; +} \ No newline at end of file From 99f735855b0d5088dccb9462694ecb446aee1326 Mon Sep 17 00:00:00 2001 From: mbroz84 Date: Tue, 6 Oct 2026 10:36:06 +0200 Subject: [PATCH 096/144] Inclusive Jpsi decays and trigger (#2493) --- .../generator/DecayTablesEvtGen/OPENCHARM.DEC | 5 +++++ .../external/generator/GeneratorStarlightToEvtGen.C | 3 ++- MC/config/PWGUD/ini/makeStarlightConfig.py | 4 +++- MC/config/PWGUD/trigger/triggerDpmjetParticle.C | 12 ++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/MC/config/PWGUD/external/generator/DecayTablesEvtGen/OPENCHARM.DEC b/MC/config/PWGUD/external/generator/DecayTablesEvtGen/OPENCHARM.DEC index c8e964d75..f18b42332 100644 --- a/MC/config/PWGUD/external/generator/DecayTablesEvtGen/OPENCHARM.DEC +++ b/MC/config/PWGUD/external/generator/DecayTablesEvtGen/OPENCHARM.DEC @@ -50,4 +50,9 @@ Decay anti-K*0 1.0 K- pi+ VSS; Enddecay +Decay J/psi +0.5 mu+ mu- PHOTOS VLL; +0.5 e+ e- PHOTOS VLL; +Enddecay + End diff --git a/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C b/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C index 3d30c48d2..6a4788302 100644 --- a/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C +++ b/MC/config/PWGUD/external/generator/GeneratorStarlightToEvtGen.C @@ -18,7 +18,7 @@ FairGenerator* gen->AddPdg(-15,1); } else if(configuration.find("kDpmjet") != std::string::npos){ - gen->SetSizePdg(14); + gen->SetSizePdg(15); gen->AddPdg( 411,0); gen->AddPdg(-411,1); gen->AddPdg( 421,2); @@ -33,6 +33,7 @@ FairGenerator* gen->AddPdg(-333,11); gen->AddPdg( 313,12); gen->AddPdg(-313,13); + gen->AddPdg( 443,14); } else{ gen->SetPolarization(1); //Transversal diff --git a/MC/config/PWGUD/ini/makeStarlightConfig.py b/MC/config/PWGUD/ini/makeStarlightConfig.py index 1655624c7..3c2b85c6f 100755 --- a/MC/config/PWGUD/ini/makeStarlightConfig.py +++ b/MC/config/PWGUD/ini/makeStarlightConfig.py @@ -17,7 +17,7 @@ parser.add_argument('--rapidity', default='cent', choices=['cent_rap', 'muon_rap', 'cent_eta', 'muon_eta'], help='Rapidity to select') -parser.add_argument('--process',default=None, choices=['kTwoGammaToMuLow', 'kTwoGammaToElLow', 'kTwoGammaToMuMedium', 'kTwoGammaToElMedium', 'kTwoGammaToMuHigh', 'kTwoGammaToElHigh', 'kTwoGammaToRhoRho', 'kTwoGammaToF2', 'kCohRhoToPi', 'kCohRhoToElEl', 'kCohRhoToMuMu', 'kCohRhoToPiWithCont', 'kCohRhoToPiFlat', 'kCohPhiToKa', 'kDirectPhiToKaKa', 'kCohPhiToEl', 'kCohOmegaTo2Pi', 'kCohOmegaTo3Pi', 'kCohOmegaToPiPiPi', 'kCohRhoPrimeTo4Pi', 'kCohJpsiToMu', 'kCohJpsiToEl', 'kCohJpsiToElRad', 'kCohJpsiToProton', 'kCohJpsiToLLbar', 'kCohJpsi4Prong', 'kCohJpsi6Prong', 'kCohPsi2sToMu','kCohPsi2sToEl', 'kCohPsi2sToMuPi', 'kCohPsi2sToElPi', 'kCohUpsilonToMu', 'kCohUpsilonToEl', 'kIncohRhoToPi', 'kIncohRhoToElEl', 'kIncohRhoToMuMu', 'kIncohRhoToPiWithCont', 'kIncohRhoToPiFlat', 'kIncohPhiToKa', 'kIncohOmegaTo2Pi', 'kIncohOmegaTo3Pi', 'kIncohOmegaToPiPiPi', 'kIncohRhoPrimeTo4Pi', 'kIncohJpsiToMu', 'kIncohJpsiToEl', 'kIncohJpsiToElRad', 'kIncohJpsiToProton', 'kIncohJpsiToLLbar', 'kIncohPsi2sToMu', 'kIncohPsi2sToEl', 'kIncohPsi2sToMuPi', 'kIncohPsi2sToElPi', 'kIncohUpsilonToMu', 'kIncohUpsilonToEl', 'kDpmjetSingleA', 'kDpmjetSingleA_Dzero', 'kDpmjetSingleA_Lc', 'kDpmjetSingleA_Dcharged', 'kDpmjetSingleA_Dstar', 'kDpmjetSingleA_Phi', 'kDpmjetSingleA_Kstar', 'kDpmjetSingleC', 'kDpmjetSingleC_Dzero', 'kDpmjetSingleC_Lc', 'kDpmjetSingleC_Dcharged', 'kDpmjetSingleC_Dstar', 'kDpmjetSingleC_Phi', 'kDpmjetSingleC_Kstar', 'kTauLowToL+3Pi', 'kTauLowToL-3Pi', 'kTauLowToPi+3Pi', 'kTauLowToPi-3Pi', 'kTauLowTo6Pi', 'kTauLowToElMu', 'kTauLowToElPiPi0', 'kTauLowToPoPiPi0'], +parser.add_argument('--process',default=None, choices=['kTwoGammaToMuLow', 'kTwoGammaToElLow', 'kTwoGammaToMuMedium', 'kTwoGammaToElMedium', 'kTwoGammaToMuHigh', 'kTwoGammaToElHigh', 'kTwoGammaToRhoRho', 'kTwoGammaToF2', 'kCohRhoToPi', 'kCohRhoToElEl', 'kCohRhoToMuMu', 'kCohRhoToPiWithCont', 'kCohRhoToPiFlat', 'kCohPhiToKa', 'kDirectPhiToKaKa', 'kCohPhiToEl', 'kCohOmegaTo2Pi', 'kCohOmegaTo3Pi', 'kCohOmegaToPiPiPi', 'kCohRhoPrimeTo4Pi', 'kCohJpsiToMu', 'kCohJpsiToEl', 'kCohJpsiToElRad', 'kCohJpsiToProton', 'kCohJpsiToLLbar', 'kCohJpsi4Prong', 'kCohJpsi6Prong', 'kCohPsi2sToMu','kCohPsi2sToEl', 'kCohPsi2sToMuPi', 'kCohPsi2sToElPi', 'kCohUpsilonToMu', 'kCohUpsilonToEl', 'kIncohRhoToPi', 'kIncohRhoToElEl', 'kIncohRhoToMuMu', 'kIncohRhoToPiWithCont', 'kIncohRhoToPiFlat', 'kIncohPhiToKa', 'kIncohOmegaTo2Pi', 'kIncohOmegaTo3Pi', 'kIncohOmegaToPiPiPi', 'kIncohRhoPrimeTo4Pi', 'kIncohJpsiToMu', 'kIncohJpsiToEl', 'kIncohJpsiToElRad', 'kIncohJpsiToProton', 'kIncohJpsiToLLbar', 'kIncohPsi2sToMu', 'kIncohPsi2sToEl', 'kIncohPsi2sToMuPi', 'kIncohPsi2sToElPi', 'kIncohUpsilonToMu', 'kIncohUpsilonToEl', 'kDpmjetSingleA', 'kDpmjetSingleA_Dzero', 'kDpmjetSingleA_Lc', 'kDpmjetSingleA_Dcharged', 'kDpmjetSingleA_Dstar', 'kDpmjetSingleA_Phi', 'kDpmjetSingleA_Kstar', 'kDpmjetSingleA_Jpsi', 'kDpmjetSingleC', 'kDpmjetSingleC_Dzero', 'kDpmjetSingleC_Lc', 'kDpmjetSingleC_Dcharged', 'kDpmjetSingleC_Dstar', 'kDpmjetSingleC_Phi', 'kDpmjetSingleC_Kstar', 'kDpmjetSingleC_Jpsi', 'kTauLowToL+3Pi', 'kTauLowToL-3Pi', 'kTauLowToPi+3Pi', 'kTauLowToPi-3Pi', 'kTauLowTo6Pi', 'kTauLowToElMu', 'kTauLowToElPiPi0', 'kTauLowToPoPiPi0'], help='Process to switch on') @@ -129,6 +129,8 @@ fout.write('funcName = triggerKstar(-0.9,0.9) \n') if 'Lc' in args.process: fout.write('funcName = triggerLc(-0.9,0.9) \n') + if 'Jpsi' in args.process: + fout.write('funcName = triggerJpsi(-0.9,0.9) \n') ### close outout file fout.close() diff --git a/MC/config/PWGUD/trigger/triggerDpmjetParticle.C b/MC/config/PWGUD/trigger/triggerDpmjetParticle.C index acc9cf258..7f53aa904 100644 --- a/MC/config/PWGUD/trigger/triggerDpmjetParticle.C +++ b/MC/config/PWGUD/trigger/triggerDpmjetParticle.C @@ -78,5 +78,17 @@ o2::eventgen::Trigger triggerLc(double rapidityMin = -1., double rapidityMax = - }; } +o2::eventgen::Trigger triggerJpsi(double rapidityMin = -1., double rapidityMax = -1.) +{ + return [rapidityMin, rapidityMax](const std::vector& particles) -> bool { + for (const auto& particle : particles) { + if (TMath::Abs(particle.GetPdgCode()) == 443) + if ((particle.Y() > rapidityMin) && (particle.Y() < rapidityMax)) + return kTRUE; + } + return kFALSE; + }; +} + From 1fd2fb7641f49bba02dd7aa8c23e6002e3693f09 Mon Sep 17 00:00:00 2001 From: mbroz84 Date: Tue, 6 Oct 2026 13:08:51 +0200 Subject: [PATCH 097/144] Various Upsilon states (#2494) --- .../external/generator/GeneratorStarlight.C | 16 ++++++++++++---- MC/config/PWGUD/ini/makeStarlightConfig.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/MC/config/PWGUD/external/generator/GeneratorStarlight.C b/MC/config/PWGUD/external/generator/GeneratorStarlight.C index be7e4d367..e7365efc7 100644 --- a/MC/config/PWGUD/external/generator/GeneratorStarlight.C +++ b/MC/config/PWGUD/external/generator/GeneratorStarlight.C @@ -126,8 +126,12 @@ class GeneratorStarlight_class : public Generator {"kCohPsi2sToEl", 2, 444011, 20, -1.0, -1.0, 100443, 0 }, // {"kCohPsi2sToMuPi", 2, 444013, 20, -1.0, -1.0, 100443, 1 }, // {"kCohPsi2sToElPi", 2, 444011, 20, -1.0, -1.0, 100443, 1 }, // - {"kCohUpsilonToMu", 2, 553013, 20, -1.0, -1.0, 553, 0 }, // - {"kCohUpsilonToEl", 2, 553011, 20, -1.0, -1.0, 553, 0 }, // + {"kCohUpsilon1SToMu", 2, 553013, 20, -1.0, -1.0, 553, 0 }, // + {"kCohUpsilon1SToEl", 2, 553011, 20, -1.0, -1.0, 553, 0 }, // + {"kCohUpsilon2SToMu", 2, 554013, 20, -1.0, -1.0, 554, 0 }, // + {"kCohUpsilon2SToEl", 2, 554011, 20, -1.0, -1.0, 554, 0 }, // + {"kCohUpsilon3SToMu", 2, 555013, 20, -1.0, -1.0, 555, 0 }, // + {"kCohUpsilon3SToEl", 2, 555011, 20, -1.0, -1.0, 555, 0 }, // {"kIncohRhoToPi", 4, 113, 1200, -1.0, -1.0, 113, 0 }, // {"kIncohRhoToElEl", 4, 113011, 1200, -1.0, -1.0, 113, 0 }, // {"kIncohRhoToMuMu", 4, 113013, 1200, -1.0, -1.0, 113, 0 }, // @@ -147,8 +151,12 @@ class GeneratorStarlight_class : public Generator {"kIncohPsi2sToEl", 4, 444011, 20, -1.0, -1.0, 100443, 0 }, // {"kIncohPsi2sToMuPi", 4, 444013, 20, -1.0, -1.0, 100443, 1 }, // {"kIncohPsi2sToElPi", 4, 444011, 20, -1.0, -1.0, 100443, 1 }, // - {"kIncohUpsilonToMu", 4, 553013, 20, -1.0, -1.0, 553, 0 }, // - {"kIncohUpsilonToEl", 4, 553011, 20, -1.0, -1.0, 553, 0 }, // + {"kIncohUpsilon1SToMu", 4, 553013, 20, -1.0, -1.0, 553, 0 }, // + {"kIncohUpsilon1SToEl", 4, 553011, 20, -1.0, -1.0, 553, 0 }, // + {"kIncohUpsilon2SToMu", 4, 554013, 20, -1.0, -1.0, 554, 0 }, // + {"kIncohUpsilon2SToEl", 4, 554011, 20, -1.0, -1.0, 554, 0 }, // + {"kIncohUpsilon3SToMu", 4, 555013, 20, -1.0, -1.0, 555, 0 }, // + {"kIncohUpsilon3SToEl", 4, 555011, 20, -1.0, -1.0, 555, 0 }, // {"kDpmjetSingleA", 5, 113, 20, -1.0, -1.0, -1, 0 }, // {"kDpmjetSingleC", 5, 113, 20, -1.0, -1.0, -1, 0 }, // {"kTauLowToL+3Pi", 1, 15, 990, 3.5, 20.0, -1, 1 }, // from 0.4 to 15 GeV diff --git a/MC/config/PWGUD/ini/makeStarlightConfig.py b/MC/config/PWGUD/ini/makeStarlightConfig.py index 3c2b85c6f..31f79357c 100755 --- a/MC/config/PWGUD/ini/makeStarlightConfig.py +++ b/MC/config/PWGUD/ini/makeStarlightConfig.py @@ -17,7 +17,7 @@ parser.add_argument('--rapidity', default='cent', choices=['cent_rap', 'muon_rap', 'cent_eta', 'muon_eta'], help='Rapidity to select') -parser.add_argument('--process',default=None, choices=['kTwoGammaToMuLow', 'kTwoGammaToElLow', 'kTwoGammaToMuMedium', 'kTwoGammaToElMedium', 'kTwoGammaToMuHigh', 'kTwoGammaToElHigh', 'kTwoGammaToRhoRho', 'kTwoGammaToF2', 'kCohRhoToPi', 'kCohRhoToElEl', 'kCohRhoToMuMu', 'kCohRhoToPiWithCont', 'kCohRhoToPiFlat', 'kCohPhiToKa', 'kDirectPhiToKaKa', 'kCohPhiToEl', 'kCohOmegaTo2Pi', 'kCohOmegaTo3Pi', 'kCohOmegaToPiPiPi', 'kCohRhoPrimeTo4Pi', 'kCohJpsiToMu', 'kCohJpsiToEl', 'kCohJpsiToElRad', 'kCohJpsiToProton', 'kCohJpsiToLLbar', 'kCohJpsi4Prong', 'kCohJpsi6Prong', 'kCohPsi2sToMu','kCohPsi2sToEl', 'kCohPsi2sToMuPi', 'kCohPsi2sToElPi', 'kCohUpsilonToMu', 'kCohUpsilonToEl', 'kIncohRhoToPi', 'kIncohRhoToElEl', 'kIncohRhoToMuMu', 'kIncohRhoToPiWithCont', 'kIncohRhoToPiFlat', 'kIncohPhiToKa', 'kIncohOmegaTo2Pi', 'kIncohOmegaTo3Pi', 'kIncohOmegaToPiPiPi', 'kIncohRhoPrimeTo4Pi', 'kIncohJpsiToMu', 'kIncohJpsiToEl', 'kIncohJpsiToElRad', 'kIncohJpsiToProton', 'kIncohJpsiToLLbar', 'kIncohPsi2sToMu', 'kIncohPsi2sToEl', 'kIncohPsi2sToMuPi', 'kIncohPsi2sToElPi', 'kIncohUpsilonToMu', 'kIncohUpsilonToEl', 'kDpmjetSingleA', 'kDpmjetSingleA_Dzero', 'kDpmjetSingleA_Lc', 'kDpmjetSingleA_Dcharged', 'kDpmjetSingleA_Dstar', 'kDpmjetSingleA_Phi', 'kDpmjetSingleA_Kstar', 'kDpmjetSingleA_Jpsi', 'kDpmjetSingleC', 'kDpmjetSingleC_Dzero', 'kDpmjetSingleC_Lc', 'kDpmjetSingleC_Dcharged', 'kDpmjetSingleC_Dstar', 'kDpmjetSingleC_Phi', 'kDpmjetSingleC_Kstar', 'kDpmjetSingleC_Jpsi', 'kTauLowToL+3Pi', 'kTauLowToL-3Pi', 'kTauLowToPi+3Pi', 'kTauLowToPi-3Pi', 'kTauLowTo6Pi', 'kTauLowToElMu', 'kTauLowToElPiPi0', 'kTauLowToPoPiPi0'], +parser.add_argument('--process',default=None, choices=['kTwoGammaToMuLow', 'kTwoGammaToElLow', 'kTwoGammaToMuMedium', 'kTwoGammaToElMedium', 'kTwoGammaToMuHigh', 'kTwoGammaToElHigh', 'kTwoGammaToRhoRho', 'kTwoGammaToF2', 'kCohRhoToPi', 'kCohRhoToElEl', 'kCohRhoToMuMu', 'kCohRhoToPiWithCont', 'kCohRhoToPiFlat', 'kCohPhiToKa', 'kDirectPhiToKaKa', 'kCohPhiToEl', 'kCohOmegaTo2Pi', 'kCohOmegaTo3Pi', 'kCohOmegaToPiPiPi', 'kCohRhoPrimeTo4Pi', 'kCohJpsiToMu', 'kCohJpsiToEl', 'kCohJpsiToElRad', 'kCohJpsiToProton', 'kCohJpsiToLLbar', 'kCohJpsi4Prong', 'kCohJpsi6Prong', 'kCohPsi2sToMu','kCohPsi2sToEl', 'kCohPsi2sToMuPi', 'kCohPsi2sToElPi', 'kCohUpsilon1SToMu', 'kCohUpsilon1SToEl', 'kCohUpsilon2SToMu', 'kCohUpsilon2SToEl', 'kCohUpsilon3SToMu', 'kCohUpsilon3SToEl', 'kIncohRhoToPi', 'kIncohRhoToElEl', 'kIncohRhoToMuMu', 'kIncohRhoToPiWithCont', 'kIncohRhoToPiFlat', 'kIncohPhiToKa', 'kIncohOmegaTo2Pi', 'kIncohOmegaTo3Pi', 'kIncohOmegaToPiPiPi', 'kIncohRhoPrimeTo4Pi', 'kIncohJpsiToMu', 'kIncohJpsiToEl', 'kIncohJpsiToElRad', 'kIncohJpsiToProton', 'kIncohJpsiToLLbar', 'kIncohPsi2sToMu', 'kIncohPsi2sToEl', 'kIncohPsi2sToMuPi', 'kIncohPsi2sToElPi', 'kIncohUpsilon1SToMu', 'kIncohUpsilon1SToEl', 'kIncohUpsilon2SToMu', 'kIncohUpsilon2SToEl', 'kIncohUpsilon3SToMu', 'kIncohUpsilon3SToEl', 'kDpmjetSingleA', 'kDpmjetSingleA_Dzero', 'kDpmjetSingleA_Lc', 'kDpmjetSingleA_Dcharged', 'kDpmjetSingleA_Dstar', 'kDpmjetSingleA_Phi', 'kDpmjetSingleA_Kstar', 'kDpmjetSingleA_Jpsi', 'kDpmjetSingleC', 'kDpmjetSingleC_Dzero', 'kDpmjetSingleC_Lc', 'kDpmjetSingleC_Dcharged', 'kDpmjetSingleC_Dstar', 'kDpmjetSingleC_Phi', 'kDpmjetSingleC_Kstar', 'kDpmjetSingleC_Jpsi', 'kTauLowToL+3Pi', 'kTauLowToL-3Pi', 'kTauLowToPi+3Pi', 'kTauLowToPi-3Pi', 'kTauLowTo6Pi', 'kTauLowToElMu', 'kTauLowToElPiPi0', 'kTauLowToPoPiPi0'], help='Process to switch on') From b543836712398b91dfcfc48cbfa6be65f4ddf9f8 Mon Sep 17 00:00:00 2001 From: ldellost <47105254+DelloStritto@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:36:47 +0200 Subject: [PATCH 098/144] Add Lstar gun generator, ini and config (#2495) --- .../ini/GeneratorLF_Strangeness_ppLStar.ini | 6 + .../tests/GeneratorLF_Strangeness_ppLStar.C | 77 ++++++ .../generator/pythia8_inel_forceLStar.cfg | 20 ++ .../PWGLF/pythia8/generator_pythia8_Lstar.C | 226 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C create mode 100644 MC/config/PWGLF/pythia8/generator/pythia8_inel_forceLStar.cfg create mode 100644 MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C diff --git a/MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini b/MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini new file mode 100644 index 000000000..9836791ed --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini @@ -0,0 +1,6 @@ +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C +funcName=generator_Lstar() + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_forceLStar.cfg \ No newline at end of file diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C b/MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C new file mode 100644 index 000000000..ef04688a0 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C @@ -0,0 +1,77 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + const int pdgMother = 3124; // Lambda(1520)0 (sign randomized by the generator) + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + const auto nEvents = tree->GetEntries(); + int nMother = 0; + int nNotDecayed = 0; + int nBadDecay = 0; // mothers not decaying exactly to Lambda + gamma + int nEventsWrongCount = 0; // events without exactly one injected mother + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + int nInEvent = 0; + for (auto &track : *tracks) { + const int pdg = track.GetPdgCode(); + if (std::abs(pdg) != pdgMother) { + continue; + } + nInEvent++; + nMother++; + + if (track.getFirstDaughterTrackId() < 0) { + nNotDecayed++; + continue; + } + const int expectedLambda = (pdg > 0) ? 3122 : -3122; + int nLambda = 0, nGamma = 0, nDau = 0; + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { + const int pdgDau = tracks->at(j).GetPdgCode(); + nDau++; + if (pdgDau == expectedLambda) nLambda++; + if (pdgDau == 22) nGamma++; + } + if (nDau != 2 || nLambda != 1 || nGamma != 1) { + nBadDecay++; + std::cerr << "Unexpected decay of " << pdg << " (" << nDau << " daughters)\n"; + } + } + if (nInEvent != 1) { + nEventsWrongCount++; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# Lambda(1520) + anti: " << nMother << "\n"; + std::cout << "# not decayed: " << nNotDecayed << "\n"; + std::cout << "# unexpected decays: " << nBadDecay << "\n"; + std::cout << "# events with != 1 mother: " << nEventsWrongCount << "\n"; + std::cout << "--------------------------------\n"; + + if (nEventsWrongCount > 0) { + std::cerr << "Each event must contain exactly one injected Lambda(1520)\n"; + return 1; + } + if (nNotDecayed > 0 || nBadDecay > 0) { + std::cerr << "Lambda(1520) must always decay to Lambda + gamma\n"; + return 1; + } + return 0; +} + +void GeneratorLF_Strangeness_ppLStar() { External(); } \ No newline at end of file diff --git a/MC/config/PWGLF/pythia8/generator/pythia8_inel_forceLStar.cfg b/MC/config/PWGLF/pythia8/generator/pythia8_inel_forceLStar.cfg new file mode 100644 index 000000000..2230a1537 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/pythia8_inel_forceLStar.cfg @@ -0,0 +1,20 @@ +### beams +Beams:idA 2212 # proton +Beams:idB 2212 # proton +Beams:eCM 13600. # GeV + +### processes +SoftQCD:inelastic on # all inelastic processes + +### decays +ParticleDecays:limitTau0 on +ParticleDecays:tau0Max 10. + +### Add Lambda* decay 3124 102134 +# id::all = name antiName spinType chargeType colType m0 mWidth mMin mMax tau0 +3124:all = Lambda1520 Lambda1520bar 4 0 0 1.51950 0.01560 1.47 1.60 0 + +### add Resonance decays absent in PYTHIA8 decay table and set BRs from PDG for other +3124:oneChannel = 1 1.000 0 3122 22 +3124:onMode = off +3124:onIfMatch = 3122 22 \ No newline at end of file diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C b/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C new file mode 100644 index 000000000..66cb4646b --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C @@ -0,0 +1,226 @@ + +#include "Pythia8/Pythia.h" +#include "Pythia8/HeavyIons.h" +#include "FairGenerator.h" +#include "FairPrimaryGenerator.h" +#include "Generators/GeneratorPythia8.h" +#include "TF1.h" +#include "TRandom3.h" +//#include "TParticlePDG.h" +//#include "TDatabasePDG.h" + +#include +#include + +class GeneratorPythia8ExtraStrangeness : public o2::eventgen::GeneratorPythia8 +{ +public: + /// default constructor + GeneratorPythia8ExtraStrangeness() = default; + + /// Constructor + GeneratorPythia8ExtraStrangeness(int input_pdg) + { + genMinPt=0.0; + genMaxPt=12.0; + genminY=-1.5; + genmaxY=1.5; + genminEta=-1.5; + genmaxEta=1.5; + + pdg=input_pdg; + m = 0; + E=0; + px=0; + py=0; + pz=0; + p=0; + y=0; + eta=0; + xProd=0.; yProd=0.; zProd=0.; + + fLVHelper = std::make_unique(); + + randomizePDGsign = false; + + fSpectra = makeLevySpectrum("fSpectra", genMinPt, genMaxPt, mPythia.particleData.m0(input_pdg), 0.30, 7.); + } + + /// randomize the PDG code sign of core particle + void setRandomizePDGsign() { randomizePDGsign = true; } + + Double_t y2eta(Double_t pt, Double_t mass, Double_t y){ + Double_t mt = TMath::Sqrt(mass * mass + pt * pt); + return TMath::ASinH(mt / pt * TMath::SinH(y)); + } + + + /// set mass + double sampleMass(int input_pdg) + { + auto& pd = mPythia.particleData; + const double mass = pd.mWidth(input_pdg) > 0. ? pd.mSel(input_pdg) : pd.m0(input_pdg); + return mass; + } + + static double myLevyPt(double* pt, double* par) + { + const double lMass = par[0]; + const double ldNdy = par[1]; + const double lTemp = par[2]; + const double lPower = par[3]; + + const double lBigCoef = ((lPower-1)*(lPower-2)) / (lPower*lTemp*(lPower*lTemp+lMass*(lPower-2))); + const double lInPower = 1 + (TMath::Sqrt(pt[0]*pt[0]+lMass*lMass)-lMass) / (lPower*lTemp); + + return ldNdy * pt[0] * lBigCoef * TMath::Power(lInPower, -lPower); + } + + /// build a Levy-Tsallis pT spectrum for a particle of given mass +TF1* makeLevySpectrum(const char* name, double ptMin, double ptMax, double mass, double T, double n, double norm = 1.) + { + TF1* f = new TF1(name, myLevyPt, ptMin, ptMax, 4); + f->SetParNames("mass", "norm", "T", "n"); + f->FixParameter(0, mass); // mass [GeV/c^2] + f->SetParameter(1, norm); // normalization (irrelevant for GetRandom) + f->SetParameter(2, T); // slope parameter [GeV] + f->SetParameter(3, n); // power-law exponent + f->SetNpx(1000); + return f; + } + + /// set 4-momentum + void set4momentum(double input_px, double input_py, double input_pz){ + px = input_px; + py = input_py; + pz = input_pz; + E = sqrt( m*m+px*px+py*py+pz*pz ); + fourMomentum.px(px); + fourMomentum.py(py); + fourMomentum.pz(pz); + fourMomentum.e(E); + p = sqrt( px*px+py*py+pz*pz ); + y = 0.5*log( (E+pz)/(E-pz) ); + eta = 0.5*log( (p+pz)/(p-pz) ); + } + + + //_________________________________________________________________________________ + /// generate uniform eta and uniform momentum + void genSpectraMomentumEta(double minPt, double maxPt, double minY, double maxY){ + // random generator + std::unique_ptr ranGenerator { new TRandom3() }; + ranGenerator->SetSeed(0); + + // generate transverse momentum + //const double gen_pT = ranGenerator->Uniform(minPt, maxPt); + const double gen_pT = fSpectra->GetRandom(minPt,maxPt); + + //Actually could be something else without loss of generality but okay + const double gen_phi = ranGenerator->Uniform(0,2*TMath::Pi()); + + // sample flat in rapidity, calculate eta + Double_t gen_Y=10, gen_eta=10; + + while( gen_eta>genmaxEta || gen_etaUniform(minY,maxY); + gen_eta = y2eta(gen_pT, m, gen_Y); + } + + fLVHelper->SetPtEtaPhiM(gen_pT, gen_eta, gen_phi, m); + set4momentum(fLVHelper->Px(),fLVHelper->Py(),fLVHelper->Pz()); + } + + //__________________________________________________________________ + Pythia8::Particle createParticle(){ + //std::cout << "createParticle() mass " << m << " pdgCode " << pdg << std::endl; + Pythia8::Particle myparticle; + myparticle.id(pdg); + myparticle.status(11); + myparticle.px(px); + myparticle.py(py); + myparticle.pz(pz); + myparticle.e(E); + myparticle.m(m); + myparticle.xProd(xProd); + myparticle.yProd(yProd); + myparticle.zProd(zProd); + + return myparticle; + } + + //__________________________________________________________________ + int randomizeSign() + { + std::unique_ptr gen_random{new TRandom3(0)}; + const float n = gen_random->Uniform(-1, 1); + + return n / abs(n); + } + + //__________________________________________________________________ + Bool_t generateEvent() override { + // Generate PYTHIA event + + mPythia.event.reset(); + + /// go to next Pythia event + Bool_t lPythiaOK = kFALSE; + while (!lPythiaOK){ + lPythiaOK = mPythia.next(); + } + + /// reset event + //mPythia.event.reset(); + + /// create and append the desired particle + m = sampleMass(pdg); + genSpectraMomentumEta(genMinPt, genMaxPt, genminY, genmaxY); + + if (randomizePDGsign) + pdg *= randomizeSign(); + + Pythia8::Particle particle = createParticle(); + mPythia.event.append(particle); + mPythia.moreDecays(); + + return true; + } + +private: + + double genMinPt; /// minimum 3-momentum for generated particles + double genMaxPt; /// maximum 3-momentum for generated particles + double genminY; /// minimum pseudorapidity for generated particles + double genmaxY; /// maximum pseudorapidity for generated particles + double genminEta; + double genmaxEta; + + Pythia8::Vec4 fourMomentum; /// four-momentum (px,py,pz,E) + //std::unique_ptr lutGen; + + double E; /// energy: sqrt( m*m+px*px+py*py+pz*pz ) [GeV/c] + double m; /// particle mass [GeV/c^2] + int pdg; /// particle pdg code + double px; /// x-component momentum [GeV/c] + double py; /// y-component momentum [GeV/c] + double pz; /// z-component momentum [GeV/c] + double p; /// momentum + double y; /// rapidity + double eta; /// pseudorapidity + double xProd; /// x-coordinate position production vertex [cm] + double yProd; /// y-coordinate position production vertex [cm] + double zProd; /// z-coordinate position production vertex [cm] + + bool randomizePDGsign; /// bool to randomize the PDG code of the core particle + + TF1 *fSpectra = nullptr; /// TF1 to store more realistic shape of spectrum + std::unique_ptr fLVHelper; +}; + + FairGenerator *generator_Lstar() + { + auto myGen = new GeneratorPythia8ExtraStrangeness(3124); + myGen->setRandomizePDGsign(); // randomization of PDG switched on + return myGen; + } \ No newline at end of file From 8e2fcba112162e9ab6d602d5a141d40afcc003e1 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 4 Oct 2026 13:58:02 +0200 Subject: [PATCH 099/144] Fix the ITS ramp-up shift rounding and the Pylint errors on master This fixes the millisecond rounding of the TF-aligned ITS ramp-up shift, updates the ramp-up test to the new signature and clears the remaining Pylint error. - The ramp-up is now rounded up to whole milliseconds before it is rounded up to whole TFs, so the shifted timestamp can no longer lie inside the ramp. - test_anchoring_rampup.py passes the TF length to shift_anchor_past_ITS_rampup and has a regression test for a ramp ending just below a TF boundary. - ccdb_cross_check in getCCDBTimeMachineTimestamp.py keeps the pinned object in a local variable, which Pylint can follow. Co-Authored-By: Claude Sonnet 5.5 --- GRID/utils/getCCDBTimeMachineTimestamp.py | 8 +++----- MC/bin/o2dpg_sim_workflow_anchored.py | 6 +++--- MC/bin/tests/test_anchoring_rampup.py | 23 ++++++++++++++++------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/GRID/utils/getCCDBTimeMachineTimestamp.py b/GRID/utils/getCCDBTimeMachineTimestamp.py index fb1faea9e..3a90db77a 100755 --- a/GRID/utils/getCCDBTimeMachineTimestamp.py +++ b/GRID/utils/getCCDBTimeMachineTimestamp.py @@ -86,11 +86,9 @@ def ccdb_cross_check(path, not_after, timeout=15): as_of = [o for o in objects if o.get("Created", 0) <= not_after] newest = max(objects, key=lambda o: o.get("Created", 0)) - result = {"newest": newest, "as_of": None, "outdated": False} - if as_of: - result["as_of"] = max(as_of, key=lambda o: o.get("Created", 0)) - result["outdated"] = newest["Created"] > result["as_of"]["Created"] - return result + pinned = max(as_of, key=lambda o: o.get("Created", 0)) if as_of else None + outdated = pinned is not None and newest["Created"] > pinned["Created"] + return {"newest": newest, "as_of": pinned, "outdated": outdated} def describe_object(o): diff --git a/MC/bin/o2dpg_sim_workflow_anchored.py b/MC/bin/o2dpg_sim_workflow_anchored.py index b9d5f1404..56d11a0c9 100755 --- a/MC/bin/o2dpg_sim_workflow_anchored.py +++ b/MC/bin/o2dpg_sim_workflow_anchored.py @@ -336,9 +336,9 @@ def shift_anchor_past_ITS_rampup(run_start, first_orbit, orbitsPerTF, ITS_rampup timestamp leaves a job at production offset 0 inside the ramp, where the ITS time-dead map masks every chip. """ - # convert ITS_rampup to orbits multiple to orbitsPerTF - rampupOrbits = ((milliseconds_to_orbits(ITS_rampup) + orbitsPerTF - 1) // orbitsPerTF ) * orbitsPerTF - + # round the ramp-up up to whole milliseconds, then to a whole number of timeframes; + # the truncated millisecond shift then still ends after the ramp and before the orbit + rampupOrbits = -(-milliseconds_to_orbits(math.ceil(ITS_rampup)) // orbitsPerTF) * orbitsPerTF return run_start + int(rampupOrbits * LHCOrbitMUS / 1000.), first_orbit + rampupOrbits def retrieve_MinBias_CTPScaler_Rate(raw_rate_at, finaltime, trig_eff_arg, NBunches, ColSystem, eCM, run_number = -1): diff --git a/MC/bin/tests/test_anchoring_rampup.py b/MC/bin/tests/test_anchoring_rampup.py index 2dac50f68..4b3c1f20b 100644 --- a/MC/bin/tests/test_anchoring_rampup.py +++ b/MC/bin/tests/test_anchoring_rampup.py @@ -21,6 +21,7 @@ FIRST_ORBIT = 20505888 SOR = 1778806732526 ITS_RAMPUP_MS = 5000 +ORBITS_PER_TF = 128 FIRST_ALIVE_ORBIT = 20539968 @@ -36,32 +37,40 @@ def test_orbits_from_milliseconds(self): def test_both_coordinates_move(self): """A ramp-up of a few seconds must move the orbit as well as the timestamp.""" - start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS) - self.assertEqual(start, SOR + ITS_RAMPUP_MS) + start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ORBITS_PER_TF, ITS_RAMPUP_MS) + self.assertGreaterEqual(start, SOR + ITS_RAMPUP_MS) self.assertGreater(orbit, FIRST_ORBIT) + self.assertEqual((orbit - FIRST_ORBIT) % ORBITS_PER_TF, 0) def test_nothing_moves_without_a_ramp(self): - self.assertEqual(anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, 0), + self.assertEqual(anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ORBITS_PER_TF, 0), (SOR, FIRST_ORBIT)) def test_shifted_orbit_is_never_inside_the_ramp(self): """The shifted orbit must sit at or after the shifted timestamp, never before.""" for ramp_ms in (0, 1, 500, ITS_RAMPUP_MS, 30000): - start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ramp_ms) + start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ORBITS_PER_TF, ramp_ms) time_of_orbit = SOR + (orbit - FIRST_ORBIT) * anchored.LHCOrbitMUS / 1000. self.assertGreaterEqual(time_of_orbit, start, f"orbit shift falls short of the ramp for {ramp_ms} ms") def test_shift_agrees_with_the_timestamp_to_orbit_conversion(self): """Closure: the shifted orbit is what main() derives from the shifted timestamp.""" - start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS) + start, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ORBITS_PER_TF, ITS_RAMPUP_MS) # this is the conversion main() uses for the exclude_timestamp() check derived = FIRST_ORBIT + int((start - SOR) / (anchored.LHCOrbitMUS / 1000.)) - self.assertLessEqual(abs(orbit - derived), 1) + # the timestamp has millisecond resolution, which is about 11 orbits + self.assertLessEqual(abs(orbit - derived), anchored.milliseconds_to_orbits(1)) + + def test_timestamp_is_not_inside_the_ramp_at_a_tf_boundary(self): + """Regression: a ramp ending just below a TF boundary must not be cut short by the ms truncation.""" + ramp_ms = 12928 * anchored.LHCOrbitMUS / 1000. - 0.001 + start, _ = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ORBITS_PER_TF, ramp_ms) + self.assertGreaterEqual(start - SOR, ramp_ms) def test_split_id_one_clears_the_its_dead_window(self): """Regression: the first job of a production must not sample the dead window.""" - _, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ITS_RAMPUP_MS) + _, orbit = anchored.shift_anchor_past_ITS_rampup(SOR, FIRST_ORBIT, ORBITS_PER_TF, ITS_RAMPUP_MS) self.assertGreater(orbit, FIRST_ALIVE_ORBIT) From f98e628fbabd7e06de6918d23d3155aaac24018f Mon Sep 17 00:00:00 2001 From: Bong-Hwi Lim Date: Thu, 8 Oct 2026 13:12:14 +0700 Subject: [PATCH 100/144] [PWGLF] Add single-channel K1(1270) and Omega(2012) injection configs for ML signal samples (pp 13.6 and 5.36 TeV) (#2497) * [PWGLF] Add single-channel K1(1270) and Omega(2012) injection configs for ML signal samples One stratum (charge x decay channel) per job: one injected parent per pp 13.6 TeV inelastic event, no gap, |y| < 0.7, flat pT (K1: 0-8 GeV/c, Omega(2012): 0-15 GeV/c), finite-width sampling with generator_pythia8_LF_rapidity_width.C. - K1(1270)+-: K rho0 (rho0 -> pi+ pi-) and K*(892) pi (K* -> K pi) - Omega(2012)-+: Xi K0S and Xi(1530)0 K (Xi(1530)0 -> Xi pi) The forced-decay tables are new files; resonances_width.cfg and the existing recipes are unchanged. Branching fractions are forced to unity and must be weighted in the analysis. Xi, Lambda and K0S are decayed by the transport code with their natural branching fractions. Each INI redirects to a per-channel test that requires exactly one injected parent per event and the forced chain for both charges; parents produced in the underlying event are counted but not checked. * [PWGLF] Add pp 5.36 TeV and OO 5.36 TeV variants of the K1(1270) and Omega(2012) ML injection configs Same 8 strata, gun configurations and forced-decay tables as the pp 13.6 TeV INIs; only the underlying-event configuration differs (pythia8_inel_pp536tev.cfg, pythia8_OO_536.cfg). The per-channel tests are renamed without the collision-system suffix and shared by all INIs. * [PWGLF] Name the ML injection tests after one INI of each channel The generator CI requires every changed test macro to match an INI file name; the other INIs of the channel redirect to it with #--->. * [PWGLF] Remove the OO 5.36 TeV variants of the ML injection configs generator_pythia8_LF_rapidity_width.C builds the underlying event in a separate Pythia object, while GeneratorPythia8::updateHeader reads the heavy-ion information of the base object, which never generated an event: o2-sim crashes in getNcoll with the Angantyr OO configuration (the existing GeneratorLF_ResonancesBaryonic_OO_injection.ini crashes in the same way). To be revisited once the macro handles heavy-ion backgrounds. --------- Co-authored-by: Bong-Hwi Lim --- ...LF_ResonancesML_K1Minus_KstarPi_pp1360.ini | 13 +++ ...LF_ResonancesML_K1Minus_KstarPi_pp5360.ini | 13 +++ ...torLF_ResonancesML_K1Minus_RhoK_pp1360.ini | 13 +++ ...torLF_ResonancesML_K1Minus_RhoK_pp5360.ini | 13 +++ ...rLF_ResonancesML_K1Plus_KstarPi_pp1360.ini | 12 +++ ...rLF_ResonancesML_K1Plus_KstarPi_pp5360.ini | 13 +++ ...atorLF_ResonancesML_K1Plus_RhoK_pp1360.ini | 12 +++ ...atorLF_ResonancesML_K1Plus_RhoK_pp5360.ini | 13 +++ ...nancesML_Omega2012Minus_Xi1530K_pp1360.ini | 12 +++ ...nancesML_Omega2012Minus_Xi1530K_pp5360.ini | 13 +++ ...sonancesML_Omega2012Minus_XiK0s_pp1360.ini | 12 +++ ...sonancesML_Omega2012Minus_XiK0s_pp5360.ini | 13 +++ ...onancesML_Omega2012Plus_Xi1530K_pp1360.ini | 13 +++ ...onancesML_Omega2012Plus_Xi1530K_pp5360.ini | 13 +++ ...esonancesML_Omega2012Plus_XiK0s_pp1360.ini | 13 +++ ...esonancesML_Omega2012Plus_XiK0s_pp5360.ini | 13 +++ ...torLF_ResonancesML_K1Plus_KstarPi_pp1360.C | 96 +++++++++++++++++++ ...eratorLF_ResonancesML_K1Plus_RhoK_pp1360.C | 96 +++++++++++++++++++ ...sonancesML_Omega2012Minus_Xi1530K_pp1360.C | 96 +++++++++++++++++++ ...ResonancesML_Omega2012Minus_XiK0s_pp1360.C | 96 +++++++++++++++++++ .../generator/resonance_ml_k1_kstarpi.cfg | 8 ++ .../generator/resonance_ml_k1_rhok.cfg | 8 ++ .../resonance_ml_omega2012_xi1530k.cfg | 11 +++ .../resonance_ml_omega2012_xik0s.cfg | 9 ++ .../resonancegun_ml_k1minus_kstarpi.json | 13 +++ .../resonancegun_ml_k1minus_rhok.json | 13 +++ .../resonancegun_ml_k1plus_kstarpi.json | 13 +++ .../resonancegun_ml_k1plus_rhok.json | 13 +++ ...esonancegun_ml_omega2012minus_xi1530k.json | 13 +++ .../resonancegun_ml_omega2012minus_xik0s.json | 13 +++ ...resonancegun_ml_omega2012plus_xi1530k.json | 13 +++ .../resonancegun_ml_omega2012plus_xik0s.json | 13 +++ 32 files changed, 728 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp5360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp5360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp5360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp5360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp5360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp5360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp5360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp1360.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp5360.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.C create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.C create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.C create mode 100644 MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.C create mode 100644 MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg create mode 100644 MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg create mode 100644 MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg create mode 100644 MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_kstarpi.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_rhok.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_kstarpi.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_rhok.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xi1530k.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xik0s.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xi1530k.json create mode 100644 MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xik0s.json diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp1360.ini new file mode 100644 index 000000000..fe2129761 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp1360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360 +# Single-channel ML signal sample: one K1(1270)- per pp 13.6 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_kstarpi.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_kstarpi.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp5360.ini new file mode 100644 index 000000000..f886c011b --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_KstarPi_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360 +# Single-channel ML signal sample: one K1(1270)- per pp 5.36 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_kstarpi.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_kstarpi.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp1360.ini new file mode 100644 index 000000000..30793f3c2 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp1360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360 +# Single-channel ML signal sample: one K1(1270)- per pp 13.6 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_rhok.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_rhok.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp5360.ini new file mode 100644 index 000000000..1aecef325 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Minus_RhoK_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360 +# Single-channel ML signal sample: one K1(1270)- per pp 5.36 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_rhok.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_rhok.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.ini new file mode 100644 index 000000000..c3fae3e14 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.ini @@ -0,0 +1,12 @@ +# Single-channel ML signal sample: one K1(1270)+ per pp 13.6 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_kstarpi.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_kstarpi.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp5360.ini new file mode 100644 index 000000000..92d9f6131 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360 +# Single-channel ML signal sample: one K1(1270)+ per pp 5.36 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_kstarpi.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_kstarpi.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.ini new file mode 100644 index 000000000..e94f3b17e --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.ini @@ -0,0 +1,12 @@ +# Single-channel ML signal sample: one K1(1270)+ per pp 13.6 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_rhok.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_rhok.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp5360.ini new file mode 100644 index 000000000..7f1c12b03 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_K1Plus_RhoK_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360 +# Single-channel ML signal sample: one K1(1270)+ per pp 5.36 TeV inelastic event (no gap), flat pT 0-8 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_k1_rhok.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_rhok.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.ini new file mode 100644 index 000000000..1fc017a75 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.ini @@ -0,0 +1,12 @@ +# Single-channel ML signal sample: one Omega(2012)- per pp 13.6 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xi1530k.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xi1530k.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp5360.ini new file mode 100644 index 000000000..cf1849986 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360 +# Single-channel ML signal sample: one Omega(2012)- per pp 5.36 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xi1530k.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xi1530k.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.ini new file mode 100644 index 000000000..c711e9b4f --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.ini @@ -0,0 +1,12 @@ +# Single-channel ML signal sample: one Omega(2012)- per pp 13.6 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xik0s.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xik0s.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp5360.ini new file mode 100644 index 000000000..0956fa592 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360 +# Single-channel ML signal sample: one Omega(2012)- per pp 5.36 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xik0s.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xik0s.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp1360.ini new file mode 100644 index 000000000..860aad35c --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp1360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360 +# Single-channel ML signal sample: one Omega(2012)+ per pp 13.6 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xi1530k.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xi1530k.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp5360.ini new file mode 100644 index 000000000..fb32b1e09 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_Xi1530K_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360 +# Single-channel ML signal sample: one Omega(2012)+ per pp 5.36 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xi1530k.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xi1530k.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp1360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp1360.ini new file mode 100644 index 000000000..f033dffee --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp1360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360 +# Single-channel ML signal sample: one Omega(2012)+ per pp 13.6 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xik0s.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xik0s.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp5360.ini b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp5360.ini new file mode 100644 index 000000000..d3bb9a3cf --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_ResonancesML_Omega2012Plus_XiK0s_pp5360.ini @@ -0,0 +1,13 @@ +#---> GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360 +# Single-channel ML signal sample: one Omega(2012)+ per pp 5.36 TeV inelastic event (no gap), flat pT 0-15 GeV/c, +# |y| < 0.7, forced decay chain from resonance_ml_omega2012_xik0s.cfg. One stratum (charge x channel) per job. +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF_rapidity_width.C +funcName=generateLFRapidity("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xik0s.json", true, 0, false, true, "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg", "") + +[GeneratorPythia8] # if triggered then this will be used as the background event +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_pp536tev.cfg + +[DecayerPythia8] # after for transport code! +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg +config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.C b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.C new file mode 100644 index 000000000..c46abf9f4 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_KstarPi_pp1360.C @@ -0,0 +1,96 @@ +// Test of the single-channel ML signal samples GeneratorLF_ResonancesML_*.ini of this channel (both charges, pp 13.6 and 5.36 TeV; the others redirect here). +// Every event carries exactly one injected parent (no gap); the parent and the intermediate resonances decayed by the +// generator must follow the forced chain (charge conjugated for the negative-PDG parent). Parents with a mother come +// from the underlying Pythia event, decay with the default table, and are only counted. +int External() +{ + const std::string path{"o2sim_Kine.root"}; + const int parentPDG{10323}; + const std::vector daughtersOfPositive{313, 211}; + const std::map> intermediateDecaysOfPositive{{313, {321, -211}}}; + const std::set selfConjugate{111, 113, 310}; + auto conjugate = [&](int pdg, int sign) { return (sign > 0 || selfConjugate.count(pdg)) ? pdg : -pdg; }; + auto sorted = [](std::vector v) { std::sort(v.begin(), v.end()); return v; }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nEventsWrongParentCount{0}, nFromUnderlyingEvent{0}, nParents{0}, nNotDecayed{0}, nWrongDecay{0}, nIntermediates{0}, nWrongIntermediate{0}; + std::map nParentsBySign; + for (Long64_t i = 0; i < tree->GetEntries(); ++i) { + tree->GetEntry(i); + int nParentsInEvent{0}; + for (const auto& track : *tracks) { + const int pdg = track.GetPdgCode(); + if (std::abs(pdg) != parentPDG) { + continue; + } + if (track.getMotherTrackId() >= 0) { + ++nFromUnderlyingEvent; + continue; + } + ++nParentsInEvent; + ++nParents; + const int sign = pdg > 0 ? 1 : -1; + ++nParentsBySign[sign]; + if (track.getFirstDaughterTrackId() < 0) { + ++nNotDecayed; + continue; + } + std::vector expected, found; + for (int d : daughtersOfPositive) { + expected.push_back(conjugate(d, sign)); + } + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { + const auto& daughter = tracks->at(j); + found.push_back(daughter.GetPdgCode()); + const auto it = intermediateDecaysOfPositive.find(sign > 0 ? daughter.GetPdgCode() : conjugate(daughter.GetPdgCode(), -1)); + if (it == intermediateDecaysOfPositive.end()) { + continue; + } + ++nIntermediates; + std::vector expectedGrand, foundGrand; + for (int g : it->second) { + expectedGrand.push_back(conjugate(g, sign)); + } + for (int k = daughter.getFirstDaughterTrackId(); k >= 0 && k <= daughter.getLastDaughterTrackId(); ++k) { + foundGrand.push_back(tracks->at(k).GetPdgCode()); + } + if (sorted(expectedGrand) != sorted(foundGrand)) { + ++nWrongIntermediate; + } + } + if (sorted(expected) != sorted(found)) { + ++nWrongDecay; + } + } + if (nParentsInEvent != 1) { + ++nEventsWrongParentCount; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << tree->GetEntries() << "\n"; + std::cout << "# Parents " << parentPDG << ": " << nParentsBySign[1] << ", anti: " << nParentsBySign[-1] << "\n"; + std::cout << "# Parents from the underlying event (not checked): " << nFromUnderlyingEvent << "\n"; + std::cout << "# Events without exactly one parent: " << nEventsWrongParentCount << "\n"; + std::cout << "# Parents not decayed: " << nNotDecayed << ", with wrong daughters: " << nWrongDecay << "\n"; + std::cout << "# Intermediate resonances: " << nIntermediates << ", with wrong daughters: " << nWrongIntermediate << "\n"; + std::cout << "--------------------------------\n"; + if (tree->GetEntries() == 0 || nEventsWrongParentCount || nNotDecayed || nWrongDecay || nWrongIntermediate || + (!intermediateDecaysOfPositive.empty() && nIntermediates != nParents)) { + std::cerr << "Forced decay chain not reproduced\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.C b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.C new file mode 100644 index 000000000..25d22a966 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_K1Plus_RhoK_pp1360.C @@ -0,0 +1,96 @@ +// Test of the single-channel ML signal samples GeneratorLF_ResonancesML_*.ini of this channel (both charges, pp 13.6 and 5.36 TeV; the others redirect here). +// Every event carries exactly one injected parent (no gap); the parent and the intermediate resonances decayed by the +// generator must follow the forced chain (charge conjugated for the negative-PDG parent). Parents with a mother come +// from the underlying Pythia event, decay with the default table, and are only counted. +int External() +{ + const std::string path{"o2sim_Kine.root"}; + const int parentPDG{10323}; + const std::vector daughtersOfPositive{321, 113}; + const std::map> intermediateDecaysOfPositive{{113, {211, -211}}}; + const std::set selfConjugate{111, 113, 310}; + auto conjugate = [&](int pdg, int sign) { return (sign > 0 || selfConjugate.count(pdg)) ? pdg : -pdg; }; + auto sorted = [](std::vector v) { std::sort(v.begin(), v.end()); return v; }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nEventsWrongParentCount{0}, nFromUnderlyingEvent{0}, nParents{0}, nNotDecayed{0}, nWrongDecay{0}, nIntermediates{0}, nWrongIntermediate{0}; + std::map nParentsBySign; + for (Long64_t i = 0; i < tree->GetEntries(); ++i) { + tree->GetEntry(i); + int nParentsInEvent{0}; + for (const auto& track : *tracks) { + const int pdg = track.GetPdgCode(); + if (std::abs(pdg) != parentPDG) { + continue; + } + if (track.getMotherTrackId() >= 0) { + ++nFromUnderlyingEvent; + continue; + } + ++nParentsInEvent; + ++nParents; + const int sign = pdg > 0 ? 1 : -1; + ++nParentsBySign[sign]; + if (track.getFirstDaughterTrackId() < 0) { + ++nNotDecayed; + continue; + } + std::vector expected, found; + for (int d : daughtersOfPositive) { + expected.push_back(conjugate(d, sign)); + } + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { + const auto& daughter = tracks->at(j); + found.push_back(daughter.GetPdgCode()); + const auto it = intermediateDecaysOfPositive.find(sign > 0 ? daughter.GetPdgCode() : conjugate(daughter.GetPdgCode(), -1)); + if (it == intermediateDecaysOfPositive.end()) { + continue; + } + ++nIntermediates; + std::vector expectedGrand, foundGrand; + for (int g : it->second) { + expectedGrand.push_back(conjugate(g, sign)); + } + for (int k = daughter.getFirstDaughterTrackId(); k >= 0 && k <= daughter.getLastDaughterTrackId(); ++k) { + foundGrand.push_back(tracks->at(k).GetPdgCode()); + } + if (sorted(expectedGrand) != sorted(foundGrand)) { + ++nWrongIntermediate; + } + } + if (sorted(expected) != sorted(found)) { + ++nWrongDecay; + } + } + if (nParentsInEvent != 1) { + ++nEventsWrongParentCount; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << tree->GetEntries() << "\n"; + std::cout << "# Parents " << parentPDG << ": " << nParentsBySign[1] << ", anti: " << nParentsBySign[-1] << "\n"; + std::cout << "# Parents from the underlying event (not checked): " << nFromUnderlyingEvent << "\n"; + std::cout << "# Events without exactly one parent: " << nEventsWrongParentCount << "\n"; + std::cout << "# Parents not decayed: " << nNotDecayed << ", with wrong daughters: " << nWrongDecay << "\n"; + std::cout << "# Intermediate resonances: " << nIntermediates << ", with wrong daughters: " << nWrongIntermediate << "\n"; + std::cout << "--------------------------------\n"; + if (tree->GetEntries() == 0 || nEventsWrongParentCount || nNotDecayed || nWrongDecay || nWrongIntermediate || + (!intermediateDecaysOfPositive.empty() && nIntermediates != nParents)) { + std::cerr << "Forced decay chain not reproduced\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.C b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.C new file mode 100644 index 000000000..82afde70c --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_Xi1530K_pp1360.C @@ -0,0 +1,96 @@ +// Test of the single-channel ML signal samples GeneratorLF_ResonancesML_*.ini of this channel (both charges, pp 13.6 and 5.36 TeV; the others redirect here). +// Every event carries exactly one injected parent (no gap); the parent and the intermediate resonances decayed by the +// generator must follow the forced chain (charge conjugated for the negative-PDG parent). Parents with a mother come +// from the underlying Pythia event, decay with the default table, and are only counted. +int External() +{ + const std::string path{"o2sim_Kine.root"}; + const int parentPDG{123334}; + const std::vector daughtersOfPositive{3324, -321}; + const std::map> intermediateDecaysOfPositive{{3324, {3312, 211}}}; + const std::set selfConjugate{111, 113, 310}; + auto conjugate = [&](int pdg, int sign) { return (sign > 0 || selfConjugate.count(pdg)) ? pdg : -pdg; }; + auto sorted = [](std::vector v) { std::sort(v.begin(), v.end()); return v; }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nEventsWrongParentCount{0}, nFromUnderlyingEvent{0}, nParents{0}, nNotDecayed{0}, nWrongDecay{0}, nIntermediates{0}, nWrongIntermediate{0}; + std::map nParentsBySign; + for (Long64_t i = 0; i < tree->GetEntries(); ++i) { + tree->GetEntry(i); + int nParentsInEvent{0}; + for (const auto& track : *tracks) { + const int pdg = track.GetPdgCode(); + if (std::abs(pdg) != parentPDG) { + continue; + } + if (track.getMotherTrackId() >= 0) { + ++nFromUnderlyingEvent; + continue; + } + ++nParentsInEvent; + ++nParents; + const int sign = pdg > 0 ? 1 : -1; + ++nParentsBySign[sign]; + if (track.getFirstDaughterTrackId() < 0) { + ++nNotDecayed; + continue; + } + std::vector expected, found; + for (int d : daughtersOfPositive) { + expected.push_back(conjugate(d, sign)); + } + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { + const auto& daughter = tracks->at(j); + found.push_back(daughter.GetPdgCode()); + const auto it = intermediateDecaysOfPositive.find(sign > 0 ? daughter.GetPdgCode() : conjugate(daughter.GetPdgCode(), -1)); + if (it == intermediateDecaysOfPositive.end()) { + continue; + } + ++nIntermediates; + std::vector expectedGrand, foundGrand; + for (int g : it->second) { + expectedGrand.push_back(conjugate(g, sign)); + } + for (int k = daughter.getFirstDaughterTrackId(); k >= 0 && k <= daughter.getLastDaughterTrackId(); ++k) { + foundGrand.push_back(tracks->at(k).GetPdgCode()); + } + if (sorted(expectedGrand) != sorted(foundGrand)) { + ++nWrongIntermediate; + } + } + if (sorted(expected) != sorted(found)) { + ++nWrongDecay; + } + } + if (nParentsInEvent != 1) { + ++nEventsWrongParentCount; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << tree->GetEntries() << "\n"; + std::cout << "# Parents " << parentPDG << ": " << nParentsBySign[1] << ", anti: " << nParentsBySign[-1] << "\n"; + std::cout << "# Parents from the underlying event (not checked): " << nFromUnderlyingEvent << "\n"; + std::cout << "# Events without exactly one parent: " << nEventsWrongParentCount << "\n"; + std::cout << "# Parents not decayed: " << nNotDecayed << ", with wrong daughters: " << nWrongDecay << "\n"; + std::cout << "# Intermediate resonances: " << nIntermediates << ", with wrong daughters: " << nWrongIntermediate << "\n"; + std::cout << "--------------------------------\n"; + if (tree->GetEntries() == 0 || nEventsWrongParentCount || nNotDecayed || nWrongDecay || nWrongIntermediate || + (!intermediateDecaysOfPositive.empty() && nIntermediates != nParents)) { + std::cerr << "Forced decay chain not reproduced\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.C b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.C new file mode 100644 index 000000000..a55c402d4 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_ResonancesML_Omega2012Minus_XiK0s_pp1360.C @@ -0,0 +1,96 @@ +// Test of the single-channel ML signal samples GeneratorLF_ResonancesML_*.ini of this channel (both charges, pp 13.6 and 5.36 TeV; the others redirect here). +// Every event carries exactly one injected parent (no gap); the parent and the intermediate resonances decayed by the +// generator must follow the forced chain (charge conjugated for the negative-PDG parent). Parents with a mother come +// from the underlying Pythia event, decay with the default table, and are only counted. +int External() +{ + const std::string path{"o2sim_Kine.root"}; + const int parentPDG{123334}; + const std::vector daughtersOfPositive{3312, 310}; + const std::map> intermediateDecaysOfPositive{}; + const std::set selfConjugate{111, 113, 310}; + auto conjugate = [&](int pdg, int sign) { return (sign > 0 || selfConjugate.count(pdg)) ? pdg : -pdg; }; + auto sorted = [](std::vector v) { std::sort(v.begin(), v.end()); return v; }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int nEventsWrongParentCount{0}, nFromUnderlyingEvent{0}, nParents{0}, nNotDecayed{0}, nWrongDecay{0}, nIntermediates{0}, nWrongIntermediate{0}; + std::map nParentsBySign; + for (Long64_t i = 0; i < tree->GetEntries(); ++i) { + tree->GetEntry(i); + int nParentsInEvent{0}; + for (const auto& track : *tracks) { + const int pdg = track.GetPdgCode(); + if (std::abs(pdg) != parentPDG) { + continue; + } + if (track.getMotherTrackId() >= 0) { + ++nFromUnderlyingEvent; + continue; + } + ++nParentsInEvent; + ++nParents; + const int sign = pdg > 0 ? 1 : -1; + ++nParentsBySign[sign]; + if (track.getFirstDaughterTrackId() < 0) { + ++nNotDecayed; + continue; + } + std::vector expected, found; + for (int d : daughtersOfPositive) { + expected.push_back(conjugate(d, sign)); + } + for (int j = track.getFirstDaughterTrackId(); j <= track.getLastDaughterTrackId(); ++j) { + const auto& daughter = tracks->at(j); + found.push_back(daughter.GetPdgCode()); + const auto it = intermediateDecaysOfPositive.find(sign > 0 ? daughter.GetPdgCode() : conjugate(daughter.GetPdgCode(), -1)); + if (it == intermediateDecaysOfPositive.end()) { + continue; + } + ++nIntermediates; + std::vector expectedGrand, foundGrand; + for (int g : it->second) { + expectedGrand.push_back(conjugate(g, sign)); + } + for (int k = daughter.getFirstDaughterTrackId(); k >= 0 && k <= daughter.getLastDaughterTrackId(); ++k) { + foundGrand.push_back(tracks->at(k).GetPdgCode()); + } + if (sorted(expectedGrand) != sorted(foundGrand)) { + ++nWrongIntermediate; + } + } + if (sorted(expected) != sorted(found)) { + ++nWrongDecay; + } + } + if (nParentsInEvent != 1) { + ++nEventsWrongParentCount; + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << tree->GetEntries() << "\n"; + std::cout << "# Parents " << parentPDG << ": " << nParentsBySign[1] << ", anti: " << nParentsBySign[-1] << "\n"; + std::cout << "# Parents from the underlying event (not checked): " << nFromUnderlyingEvent << "\n"; + std::cout << "# Events without exactly one parent: " << nEventsWrongParentCount << "\n"; + std::cout << "# Parents not decayed: " << nNotDecayed << ", with wrong daughters: " << nWrongDecay << "\n"; + std::cout << "# Intermediate resonances: " << nIntermediates << ", with wrong daughters: " << nWrongIntermediate << "\n"; + std::cout << "--------------------------------\n"; + if (tree->GetEntries() == 0 || nEventsWrongParentCount || nNotDecayed || nWrongDecay || nWrongIntermediate || + (!intermediateDecaysOfPositive.empty() && nIntermediates != nParents)) { + std::cerr << "Forced decay chain not reproduced\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg b/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg new file mode 100644 index 000000000..4ecb4c80d --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_kstarpi.cfg @@ -0,0 +1,8 @@ +# Forced K1(1270) -> K*(892) pi -> K pi pi for single-channel ML signal samples (one decay channel per job). +# Used for both charges: Pythia applies the charge conjugate of the positive-PDG table to K1(1270)-. +# Mass and width are the Pythia defaults of 10323; the width macro samples the line shape (mSel). +# Branching fractions are forced to unity: weight with the natural fractions in the analysis. +### K1(1270)+ -> K*(892)0 pi+ +10323:oneChannel = 1 1.000 0 313 211 +### K*(892)0 -> K+ pi- +313:oneChannel = 1 1.000 0 321 -211 diff --git a/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg b/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg new file mode 100644 index 000000000..8dbe76e82 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonance_ml_k1_rhok.cfg @@ -0,0 +1,8 @@ +# Forced K1(1270) -> K rho0 -> K pi+ pi- for single-channel ML signal samples (one decay channel per job). +# Used for both charges: Pythia applies the charge conjugate of the positive-PDG table to K1(1270)-. +# Mass and width are the Pythia defaults of 10323; the width macro samples the line shape (mSel). +# Branching fractions are forced to unity: weight with the natural fractions in the analysis. +### K1(1270)+ -> K+ rho0 +10323:oneChannel = 1 1.000 0 321 113 +### rho0 -> pi+ pi- +113:oneChannel = 1 1.000 0 211 -211 diff --git a/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg b/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg new file mode 100644 index 000000000..fb912c3b6 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xi1530k.cfg @@ -0,0 +1,11 @@ +# Forced Omega(2012)- -> Xi(1530)0 K- -> Xi- pi+ K- for single-channel ML signal samples (one decay channel per job). +# Used for both charges: Pythia applies the charge conjugate of the positive-PDG table to Omega(2012)+. +# Particle definition as in resonances_width.cfg (123334 is not a Pythia default particle). +# Xi and Lambda are not decayed by the generator (base.cfg: mayDecay off) but by the transport code with +# their natural branching fractions; forcing them here would have no effect. +# id::all = name antiName spinType chargeType colType m0 mWidth mMin mMax tau0 +123334:all = Omega2012Minus Omega2012Minusbar 4 -3 0 2.0125 0.0064 1.8210 2.0765 0 +### Omega(2012)- -> Xi(1530)0 K- +123334:oneChannel = 1 1.000 0 3324 -321 +### Xi(1530)0 -> Xi- pi+ +3324:oneChannel = 1 1.000 0 3312 211 diff --git a/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg b/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg new file mode 100644 index 000000000..a6536ad79 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonance_ml_omega2012_xik0s.cfg @@ -0,0 +1,9 @@ +# Forced Omega(2012)- -> Xi- K0S for single-channel ML signal samples (one decay channel per job). +# Used for both charges: Pythia applies the charge conjugate of the positive-PDG table to Omega(2012)+. +# Particle definition as in resonances_width.cfg (123334 is not a Pythia default particle). +# Xi, Lambda and K0S are not decayed by the generator (base.cfg: mayDecay off) but by the transport code with +# its natural branching fractions; forcing them here would have no effect. +# id::all = name antiName spinType chargeType colType m0 mWidth mMin mMax tau0 +123334:all = Omega2012Minus Omega2012Minusbar 4 -3 0 2.0125 0.0064 1.8210 2.0765 0 +### Omega(2012)- -> Xi- K0S +123334:oneChannel = 1 1.000 0 3312 310 diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_kstarpi.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_kstarpi.json new file mode 100644 index 000000000..d9f1a037d --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_kstarpi.json @@ -0,0 +1,13 @@ +{ + "K1(1270)-": { + "pdg": -10323, + "n": 1, + "ptMin": 0.0, + "ptMax": 8.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_rhok.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_rhok.json new file mode 100644 index 000000000..d9f1a037d --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1minus_rhok.json @@ -0,0 +1,13 @@ +{ + "K1(1270)-": { + "pdg": -10323, + "n": 1, + "ptMin": 0.0, + "ptMax": 8.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_kstarpi.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_kstarpi.json new file mode 100644 index 000000000..c1737f238 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_kstarpi.json @@ -0,0 +1,13 @@ +{ + "K1(1270)+": { + "pdg": 10323, + "n": 1, + "ptMin": 0.0, + "ptMax": 8.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_rhok.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_rhok.json new file mode 100644 index 000000000..c1737f238 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_k1plus_rhok.json @@ -0,0 +1,13 @@ +{ + "K1(1270)+": { + "pdg": 10323, + "n": 1, + "ptMin": 0.0, + "ptMax": 8.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xi1530k.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xi1530k.json new file mode 100644 index 000000000..6e13333ff --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xi1530k.json @@ -0,0 +1,13 @@ +{ + "Omega(2012)-": { + "pdg": 123334, + "n": 1, + "ptMin": 0.0, + "ptMax": 15.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xik0s.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xik0s.json new file mode 100644 index 000000000..6e13333ff --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012minus_xik0s.json @@ -0,0 +1,13 @@ +{ + "Omega(2012)-": { + "pdg": 123334, + "n": 1, + "ptMin": 0.0, + "ptMax": 15.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xi1530k.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xi1530k.json new file mode 100644 index 000000000..e3a27d8e0 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xi1530k.json @@ -0,0 +1,13 @@ +{ + "Omega(2012)+": { + "pdg": -123334, + "n": 1, + "ptMin": 0.0, + "ptMax": 15.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} diff --git a/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xik0s.json b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xik0s.json new file mode 100644 index 000000000..e3a27d8e0 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator/resonancegun_ml_omega2012plus_xik0s.json @@ -0,0 +1,13 @@ +{ + "Omega(2012)+": { + "pdg": -123334, + "n": 1, + "ptMin": 0.0, + "ptMax": 15.0, + "etaMin": -0.7, + "etaMax": 0.7, + "rapidityMin": -0.7, + "rapidityMax": 0.7, + "genDecayed": true + } +} From 392eb4defa587e7de105c70731d3b9cf8d96b396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Jacazio?= Date: Thu, 8 Oct 2026 09:00:48 +0200 Subject: [PATCH 101/144] Add dittoMC oo, add injection (#2483) * Add dittoMC oo * Add Ditto with injection --- MC/config/ALICE3/ini/ditto_oo_536tev.ini | 4 ++++ MC/config/ALICE3/ini/ditto_pbpb_536tev.ini | 2 +- .../ALICE3/ini/ditto_pbpb_536tev_inj.ini | 4 ++++ MC/config/ALICE3/ini/ditto_pp_136tev.ini | 2 +- MC/config/ALICE3/ini/ditto_pp_136tev_inj.ini | 4 ++++ MC/config/ALICE3/pythia8/generator_DittoMC.C | 23 +++++++++++-------- 6 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 MC/config/ALICE3/ini/ditto_oo_536tev.ini create mode 100644 MC/config/ALICE3/ini/ditto_pbpb_536tev_inj.ini create mode 100644 MC/config/ALICE3/ini/ditto_pp_136tev_inj.ini diff --git a/MC/config/ALICE3/ini/ditto_oo_536tev.ini b/MC/config/ALICE3/ini/ditto_oo_536tev.ini new file mode 100644 index 000000000..3fe23499a --- /dev/null +++ b/MC/config/ALICE3/ini/ditto_oo_536tev.ini @@ -0,0 +1,4 @@ +#---> pythia8_pp_136tev +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C +funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.1/Ditto_tune_pythia8_OO_536.root") diff --git a/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini b/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini index 301eed05f..70d1ddbab 100644 --- a/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini +++ b/MC/config/ALICE3/ini/ditto_pbpb_536tev.ini @@ -1,4 +1,4 @@ #---> pythia8_pp_136tev [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C -funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.0/Ditto_tune_pythia8_PbPb_536tev.root") +funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.1/Ditto_tune_pythia8_PbPb_536tev.root") diff --git a/MC/config/ALICE3/ini/ditto_pbpb_536tev_inj.ini b/MC/config/ALICE3/ini/ditto_pbpb_536tev_inj.ini new file mode 100644 index 000000000..4ae5c8b87 --- /dev/null +++ b/MC/config/ALICE3/ini/ditto_pbpb_536tev_inj.ini @@ -0,0 +1,4 @@ +#---> pythia8_pp_136tev +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C +funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.1/Ditto_tune_pythia8_PbPb_536tev.root", "https://github.com/njacazio/DittoMC/releases/download/v1.0.1/signal_pbpb.root") diff --git a/MC/config/ALICE3/ini/ditto_pp_136tev.ini b/MC/config/ALICE3/ini/ditto_pp_136tev.ini index 39b752dc3..a6ac925af 100644 --- a/MC/config/ALICE3/ini/ditto_pp_136tev.ini +++ b/MC/config/ALICE3/ini/ditto_pp_136tev.ini @@ -1,4 +1,4 @@ #---> pythia8_pp_136tev [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C -funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.0/Ditto_tune_pythia8_inel_136tev.root") +funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.1/Ditto_tune_pythia8_inel_136tev.root") diff --git a/MC/config/ALICE3/ini/ditto_pp_136tev_inj.ini b/MC/config/ALICE3/ini/ditto_pp_136tev_inj.ini new file mode 100644 index 000000000..068718d16 --- /dev/null +++ b/MC/config/ALICE3/ini/ditto_pp_136tev_inj.ini @@ -0,0 +1,4 @@ +#---> pythia8_pp_136tev +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_DittoMC.C +funcName=generator_DittoMC("https://github.com/njacazio/DittoMC/releases/download/v1.0.1/Ditto_tune_pythia8_inel_136tev.root", "https://github.com/njacazio/DittoMC/releases/download/v1.0.1/signal.root") diff --git a/MC/config/ALICE3/pythia8/generator_DittoMC.C b/MC/config/ALICE3/pythia8/generator_DittoMC.C index 27fd8b66d..62da98509 100644 --- a/MC/config/ALICE3/pythia8/generator_DittoMC.C +++ b/MC/config/ALICE3/pythia8/generator_DittoMC.C @@ -11,9 +11,9 @@ // Ditto event generator class GeneratorDittoMC : public o2::eventgen::GeneratorPythia8 { - public: +public: /// Constructor - explicit GeneratorDittoMC(const TString& tuneFile) : mGenerator(makeConfig(tuneFile)) + explicit GeneratorDittoMC(const TString &tuneFile, const TString &signalFile) : mGenerator(makeConfig(tuneFile, signalFile)) { // We use the Pythia8 event only as the container passed to the O2 // GeneratorPythia8 machinery. Particle production is handled by Ditto. @@ -31,22 +31,27 @@ class GeneratorDittoMC : public o2::eventgen::GeneratorPythia8 /// Destructor ~GeneratorDittoMC() override = default; - private: +private: /// Build the Ditto runtime configuration. - static Ditto::Config makeConfig(const TString& tuneFile) + static Ditto::Config makeConfig(const TString &tuneFile, const TString &signalFile) { - if (tuneFile.IsNull()) { + if (tuneFile.IsNull()) + { throw std::runtime_error("Ditto tune file must be specified"); } Ditto::Config config; config.mTuneFile = tuneFile.Data(); + config.mSignalFile = signalFile.Data(); // Use the Grid process ID as the Ditto seed when available. // Outside the Grid, seed 0 is used. - if (const char* alienProcId = std::getenv("ALIEN_PROC_ID")) { + if (const char *alienProcId = std::getenv("ALIEN_PROC_ID")) + { config.mSeed = static_cast(std::strtoull(alienProcId, nullptr, 10)); - } else { + } + else + { config.mSeed = static_cast(std::time(nullptr)); } @@ -58,7 +63,7 @@ class GeneratorDittoMC : public o2::eventgen::GeneratorPythia8 Ditto::Generator mGenerator; }; -FairGenerator* generator_DittoMC(const TString& tuneFile) +FairGenerator *generator_DittoMC(const TString &tuneFile, const TString &signalFile = "") { - return new GeneratorDittoMC(tuneFile); + return new GeneratorDittoMC(tuneFile, signalFile); } From 272c31d42c8f62d1e20d6f75d4cdc037eb846677 Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Thu, 8 Oct 2026 13:52:51 +0200 Subject: [PATCH 102/144] Fix indices in case of no mother in HF embedding generator (#2498) --- .../generator/generator_pythia8_embed_hf.C | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C b/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C index 2dfbf7c60..9d75081ea 100644 --- a/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C +++ b/MC/config/PWGHF/external/generator/generator_pythia8_embed_hf.C @@ -280,7 +280,7 @@ Bool_t importParticles() override /// Establish if this particle comes from charm or beauty /// If not, ignore this particle and increase the number of discarded particles from the pp event - if(!(nEvsHF == 0 && iPart < 1) && !isFromCharmOrBeauty(iPart, particlesHfEvent)) { + if(!isFromCharmOrBeauty(iPart, particlesHfEvent)) { continue; } /// if we arrive here, then the current particle is from charm or beauty, keep it! @@ -319,8 +319,8 @@ Bool_t importParticles() override if(idLastMother != idFirstMotherOrig) { idLastMother = findKey(mapHfParticles, idLastMother); if(idLastMother < 0) { - // second mother not found, we set it to 0 - idLastMother = 0; + // second mother not found, we set it to -1 + idLastMother = -1; } } else { /// idLastMother is equal to idFirstMother @@ -352,10 +352,10 @@ Bool_t importParticles() override foundAnyMother = true; } } - // set last mother to 0 if no mother has been found so far + // set last mother to -1 if no mother has been found so far if (!foundAnyMother) { - idLastMother = 0; - idFirstMother = 0; + idLastMother = -1; + idFirstMother = -1; } } @@ -364,10 +364,10 @@ Bool_t importParticles() override idLastDaughter = findKey(mapHfParticles, idLastDaughter); /// adjust the particle mother and daughter indices - particle.SetFirstMother((idFirstMother > 0) ? idFirstMother + offset : idFirstMother); - particle.SetLastMother((idLastMother > 0) ? idLastMother + offset : idLastMother); - particle.SetFirstDaughter((idFirstDaughter > 0) ? idFirstDaughter + offset : idFirstDaughter); - particle.SetLastDaughter((idLastDaughter > 0) ? idLastDaughter + offset : idLastDaughter); + particle.SetFirstMother((idFirstMother >= 0) ? idFirstMother + offset : idFirstMother); + particle.SetLastMother((idLastMother >= 0) ? idLastMother + offset : idLastMother); + particle.SetFirstDaughter((idFirstDaughter >= 0) ? idFirstDaughter + offset : idFirstDaughter); + particle.SetLastDaughter((idLastDaughter >= 0) ? idLastDaughter + offset : idLastDaughter); /// copy inside this.mParticles from mGeneratorEvHF.mParticles, i.e. the particles generated in mGeneratorEvHF mParticles.push_back(particle); From a769ab660e1df98cc4a8113f5b256135c62e056f Mon Sep 17 00:00:00 2001 From: sawan <124118453+sawankumawat@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:39:16 +0530 Subject: [PATCH 103/144] made the mass and pt cut configurable (#2500) --- .../GeneratorLF_doublephi_triggerMasspTcut.ini | 2 +- .../generator_pythia8_twophi_triggerMassCut.C | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini b/MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini index a51496b44..753c01d31 100644 --- a/MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini +++ b/MC/config/PWGLF/ini/GeneratorLF_doublephi_triggerMasspTcut.ini @@ -1,6 +1,6 @@ [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C -funcName=generateDoublePhi(0, 0.0, 100.0, 0.8) +funcName=generateDoublePhi(0, 0.0, 100.0, 0.8, 2.4, 6.0) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C index 4cb177bc3..4851840c8 100644 --- a/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C +++ b/MC/config/PWGLF/pythia8/generator_pythia8_twophi_triggerMassCut.C @@ -25,14 +25,17 @@ class GeneratorPythia8DoublePhi : public o2::eventgen::GeneratorPythia8 { public: /// Constructor - GeneratorPythia8DoublePhi(int gapSize = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8) + GeneratorPythia8DoublePhi(int gapSize = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8, double minPairMass = 2.4, double minPairPt = 6.0) : o2::eventgen::GeneratorPythia8(), mGapSize(gapSize), mMinPt(minPt), mMaxPt(maxPt), - mMaxEta(maxEta) + mMaxEta(maxEta), + mMinPairMass(minPairMass), + mMinPairPt(minPairPt) { - fmt::printf(">> Pythia8 generator: two phi(1020) mesons, gap = %d, minPtPhi = %f, maxPtPhi = %f, |etaPhi| < %f\n", gapSize, minPt, maxPt, maxEta); + fmt::printf(">> Pythia8 generator: two phi(1020) mesons, gap = %d, minPtPhi = %f, maxPtPhi = %f, |etaPhi| < %f, minPairMass = %f, minPairPt = %f\n", + gapSize, minPt, maxPt, maxEta, minPairMass, minPairPt); } /// Destructor ~GeneratorPythia8DoublePhi() = default; @@ -156,7 +159,7 @@ protected: double mass = phiPhi.M(); double pt = phiPhi.Pt(); - if (mass > 2.4 && pt > 4.0) + if (mass > mMinPairMass && pt > mMinPairPt) return true; } } @@ -169,13 +172,15 @@ private: double mMinPt{0.0}; double mMaxPt{100.0}; double mMaxEta{0.8}; + double mMinPairMass{2.4}; + double mMinPairPt{6.0}; uint64_t mGeneratedEvents{0}; }; ///___________________________________________________________ -FairGenerator *generateDoublePhi(int gap = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8) +FairGenerator *generateDoublePhi(int gap = 0, double minPt = 0.0, double maxPt = 100.0, double maxEta = 0.8, double minPairMass = 2.4, double minPairPt = 6.0) { - auto myGenerator = new GeneratorPythia8DoublePhi(gap, minPt, maxPt, maxEta); + auto myGenerator = new GeneratorPythia8DoublePhi(gap, minPt, maxPt, maxEta, minPairMass, minPairPt); myGenerator->readString("333:onMode = off"); myGenerator->readString("333:onIfMatch = 321 -321"); From 82f09e259a45712afd0721445969bd1f53dde083 Mon Sep 17 00:00:00 2001 From: Matthias Kleiner <48915672+matthias-kleiner@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:02:02 +0800 Subject: [PATCH 104/144] asyncReco: optional cosmics matching with raw-cluster output (EXTRACTCOSMICS) (#2501) JDL EXTRACTCOSMICS=1 enables the calibration workflows (ADD_CALIB, all other calibrations stay off) and CALIB_ASYNC_EXTRACTCOSMICS, which makes O2's calib-workflow.sh add o2-cosmics-match-workflow with the raw-cluster output (o2_cosmics_full.root, to be added to the job's output files). Allowed in async with TPC reconstructed, like the TPC time series. Optional JDL variables: - COSMICSPRESET: matcher preset (default physics-v1) - COSMICSCONFIG: matcher settings on top of the preset (configKeyValues), e.g. cosmicsMatch.minSeedDCAxyTOF=0;cosmicsMatch.minSeedDCAxyNSigmaTOF=0 - COSMICSARGS: extra workflow options, e.g. --debug-tree (cosmics_collector_debug.root, also to be added to the output files) - COSMICSPVVETO=0: no veto of legs belonging to primary-vertex contributors (on by default in calib-workflow.sh) Needs the O2 counterpart in calib-workflow.sh. Co-authored-by: Claude Opus 5.5 --- DATA/common/setenv_calib.sh | 2 ++ .../configurations/asyncReco/async_pass.sh | 4 ++++ .../configurations/asyncReco/setenv_extra.sh | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/DATA/common/setenv_calib.sh b/DATA/common/setenv_calib.sh index 59d457889..6ae6749cc 100755 --- a/DATA/common/setenv_calib.sh +++ b/DATA/common/setenv_calib.sh @@ -35,6 +35,7 @@ if [[ $SYNCMODE == 1 ]] && has_processing_step ENTROPY_ENCODER && [[ ! -z "$WORK if has_detector_calib EMC && has_detector_reco EMC && [[ $SYNCMODE != 1 ]]; then CAN_DO_CALIB_EMC_ASYNC_RECALIB=1; else CAN_DO_CALIB_EMC_ASYNC_RECALIB=0; fi if [[ $SYNCMODE != 1 ]] && has_detector_reco TPC; then CAN_DO_CALIB_ASYNC_EXTRACTTPCCURRENTS=1; else CAN_DO_CALIB_ASYNC_EXTRACTTPCCURRENTS=0; fi if [[ $SYNCMODE != 1 ]] && has_detector_reco TPC; then CAN_DO_CALIB_ASYNC_EXTRACTTIMESERIES=1; else CAN_DO_CALIB_ASYNC_EXTRACTTIMESERIES=0; fi +if [[ $SYNCMODE != 1 ]] && has_detector_reco TPC; then CAN_DO_CALIB_ASYNC_EXTRACTCOSMICS=1; else CAN_DO_CALIB_ASYNC_EXTRACTCOSMICS=0; fi # additional individual settings for calibration workflows if has_detector CTP; then export CALIB_TPC_SCDCALIB_CTP_INPUT="--enable-ctp"; else export CALIB_TPC_SCDCALIB_CTP_INPUT=""; fi @@ -222,6 +223,7 @@ fi ( [[ -z ${CALIB_ASYNC_DISABLE3DCURRENTS:-} ]] || [[ $CAN_DO_CALIB_ASYNC_DISABLE3DCURRENTS == 0 ]] ) && CALIB_ASYNC_DISABLE3DCURRENTS=0 : ${ON_SKIMMED_DATA:=0} ( [[ -z ${CALIB_ASYNC_EXTRACTTIMESERIES:-} ]] || [[ $CAN_DO_CALIB_ASYNC_EXTRACTTIMESERIES == 0 ]] ) && CALIB_ASYNC_EXTRACTTIMESERIES=0 +( [[ -z ${CALIB_ASYNC_EXTRACTCOSMICS:-} ]] || [[ $CAN_DO_CALIB_ASYNC_EXTRACTCOSMICS == 0 ]] ) && CALIB_ASYNC_EXTRACTCOSMICS=0 if [[ "0${GEN_TOPO_VERBOSE:-}" == "01" ]]; then diff --git a/DATA/production/configurations/asyncReco/async_pass.sh b/DATA/production/configurations/asyncReco/async_pass.sh index 5a178344d..6b8e60c2c 100755 --- a/DATA/production/configurations/asyncReco/async_pass.sh +++ b/DATA/production/configurations/asyncReco/async_pass.sh @@ -205,6 +205,10 @@ if [[ "$ALIEN_JDL_EXTRACTTIMESERIES" == "1" ]]; then export ADD_CALIB=1 fi +if [[ "${ALIEN_JDL_EXTRACTCOSMICS:-}" == "1" ]]; then + export ADD_CALIB=1 +fi + if [[ "$ALIEN_JDL_DOTPCTIMEGAINCALIB" == "1" ]]; then export ADD_CALIB=1 fi diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index 5f6d70e7d..78f6885c1 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -797,6 +797,26 @@ if [[ $ADD_CALIB == "1" ]]; then export TPCTIMESERIES_MULT_MAX=$ALIEN_JDL_TPCTIMESERIESMULTMAX fi fi + + # cosmic muons in collision data: matcher (TPC-only legs) + raw clusters of each cosmic -> o2_cosmics_full.root + if [[ -n ${ALIEN_JDL_EXTRACTCOSMICS:-} ]] ; then + echo "Adding cosmics matching and raw-cluster output in setenv_extra.sh" + export CALIB_ASYNC_EXTRACTCOSMICS=$ALIEN_JDL_EXTRACTCOSMICS + if [[ -n ${ALIEN_JDL_COSMICSPRESET:-} ]]; then + export COSMICS_PRESET=$ALIEN_JDL_COSMICSPRESET + fi + # optional: matcher settings on top of the preset (e.g. cosmicsMatch.minSeedDCAxyTOF=0), extra options (e.g. --debug-tree), and + # COSMICSPVVETO=0 to switch off the veto of legs belonging to primary-vertex contributors (on by default in calib-workflow.sh) + if [[ -n ${ALIEN_JDL_COSMICSCONFIG:-} ]]; then + export CONFIG_EXTRA_PROCESS_o2_cosmics_match_workflow+=";$ALIEN_JDL_COSMICSCONFIG" + fi + if [[ -n ${ALIEN_JDL_COSMICSARGS:-} ]]; then + export ARGS_EXTRA_PROCESS_o2_cosmics_match_workflow+=" $ALIEN_JDL_COSMICSARGS" + fi + if [[ -n ${ALIEN_JDL_COSMICSPVVETO:-} ]]; then + export COSMICS_PV_VETO=$ALIEN_JDL_COSMICSPVVETO + fi + fi if [[ $ALIEN_JDL_DOUPLOADSLOCALLY == 1 ]]; then export CCDB_POPULATOR_UPLOAD_PATH="file://$PWD" fi From c91c959698640dd20e10baa3ba53fac6eadd3167 Mon Sep 17 00:00:00 2001 From: aimeric-landou <46970521+aimeric-landou@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:49:38 +0100 Subject: [PATCH 105/144] [PWGGAJE] Param Model: better multiplicity model (#2496) * [PWGGAJE] Param Model: better multiplicity model --- ...x_parametrisedModel_pythia6Fragmentation.C | 145 ++++++++++++------ ...rametrisedModel_PbPb_5p36TeV_cent0010.json | 15 ++ ...rametrisedModel_PbPb_5p36TeV_cent5080.json | 15 ++ ...pythia6Fragmentation_PbPb_cent0010_Bkg.ini | 2 +- ...pythia6Fragmentation_PbPb_cent0010_Sgl.ini | 2 +- ...hia6Fragmentation_PbPb_cent0010_SglBkg.ini | 2 +- ...pythia6Fragmentation_PbPb_cent5080_Bkg.ini | 2 +- ...pythia6Fragmentation_PbPb_cent5080_Sgl.ini | 2 +- ...hia6Fragmentation_PbPb_cent5080_SglBkg.ini | 2 +- 9 files changed, 137 insertions(+), 50 deletions(-) create mode 100644 MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent0010.json create mode 100644 MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent5080.json diff --git a/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C index 2fd75aa34..69ae93711 100644 --- a/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C +++ b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C @@ -26,18 +26,22 @@ using namespace Pythia8; // #include "SimulationDataFormat/MCEventHeader.h" // Input to simulation: -// inputFilePathName file is expected to be a json file with the structure like so: +// inputFilePathName file is expected to be a json file with the structure like +// so: // { // "simLog": false, // "sglGenRAA": 0.45, // "sglGenTAA": 23.38, -// "sglCutoffSteepNess": 15, +// "sglCutoffSteepness": 15, // "sglCutoffAbscissa": 10, // "fallSpecterSlopeLog": 10.833274, // "fallSpecterAffinePowerConstantTerm": -5.476804, // "fallSpecterAffinePowerSlope": 0.001110, // "bkgAveragePt": 0.670, -// "collTotalMultWithBkg": 2000 +// "collMultPowerLawAmplitude": ?, +// "collMultPowerLawExponent": ?, +// "collMultMin": ?, +// "collMultMax": ? // } // can be uploaded to grid using for example: alien.py cp // file:/local/path/parametrisedModel_PbPb_5p36TeV_cent0010.json @@ -46,36 +50,40 @@ using namespace Pythia8; class GeneratorParametrisedJetModel : public o2::eventgen::GeneratorPythia8 { public: /// constructor - GeneratorParametrisedJetModel(std::string inputSimParametersPath, std::string inputSimParametersFileName, bool generateUE, bool generateSignal = true) : mInputSimParametersPath{inputSimParametersPath}, mInputSimParametersFileName{inputSimParametersFileName}, mGenerateUE{generateUE}, mGenerateSignal{generateSignal} { + GeneratorParametrisedJetModel(std::string inputSimParametersFileName, bool generateUE, bool generateSignal = true) : mInputSimParametersFileName{inputSimParametersFileName}, mGenerateUE{generateUE}, mGenerateSignal{generateSignal} { if (!generateUE && !generateSignal) { cout << "Both the UE and the signal generation should not be off at the same time" << endl; exit(1); } - std::string inputFilePathName = "alien://" + inputSimParametersPath + inputSimParametersFileName; - if (!gGrid) { - TGrid::Connect("alien://"); - if (!gGrid) { - LOG(fatal) << "AliEn connection failed, check token."; - exit(1); - } - } - // fetch and copy the .json file to the sim work directory - std::string outputPath = "./"; - TString aliencp = Form("alien_cp alien://%s%s file:%s%s", inputSimParametersPath.c_str(), - inputSimParametersFileName.c_str(), outputPath.c_str(), - inputSimParametersFileName.c_str()); // an internal operation in ROOT that discards the - // JSON file directly (smaller than 300 bytes and if - // a bit bigger it would discard the file because it - // understands it's not a ROOT file), thus one - // cannot use TFile::Cp() - if (gSystem->Exec(aliencp.Data()) != 0) { - cout << "Error: Sim parameters .json file " << inputFilePathName << " does not exist!" << endl; - exit(1); - } + // old: retrieving from GRID; very flexible but to be avoided because poor reproduceability if file is changed etc for CI tests and analyses + // std::string inputFilePathName = "alien://" + inputSimParametersPath + inputSimParametersFileName; + // if (!gGrid) { + // TGrid::Connect("alien://"); + // if (!gGrid) { + // LOG(fatal) << "AliEn connection failed, check token."; + // exit(1); + // } + // } + // // fetch and copy the .json file to the sim work directory + // std::string outputPath = "./"; + // TString aliencp = Form("alien_cp alien://%s%s file:%s%s", inputSimParametersPath.c_str(), + // inputSimParametersFileName.c_str(), outputPath.c_str(), + // inputSimParametersFileName.c_str()); // an internal operation in ROOT that discards the + // // JSON file directly (smaller than 300 bytes and if + // // a bit bigger it would discard the file because it + // // understands it's not a ROOT file), thus one + // // cannot use TFile::Cp() + // if (gSystem->Exec(aliencp.Data()) != 0) { + // cout << "Error: Sim parameters .json file " << inputFilePathName << " does not exist!" << endl; + // exit(1); + // } + std::string O2DPG_ROOT(getenv("O2DPG_MC_CONFIG_ROOT")); + std::string inputSimParametersPath = O2DPG_ROOT+"/MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/"; + std::string inputFilePathName = inputSimParametersPath + inputSimParametersFileName; // open the file - std::FILE *fjson = std::fopen(inputSimParametersFileName.c_str(), "r"); + std::FILE *fjson = std::fopen(inputFilePathName.c_str(), "r"); if (!fjson) { cout << "Could not open sim parameters file " << inputFilePathName << endl; exit(1); @@ -114,32 +122,50 @@ public: // get parameters for sim mSglGenRAA = jsonDocument[mConfigurableSimParameterNames.at(0).c_str()].GetDouble(); mSglGenTAA = jsonDocument[mConfigurableSimParameterNames.at(1).c_str()].GetDouble(); - mSglCutoffSteepNess = jsonDocument[mConfigurableSimParameterNames.at(2).c_str()].GetDouble(); + msglCutoffSteepness = jsonDocument[mConfigurableSimParameterNames.at(2).c_str()].GetDouble(); mSglCutoffAbscissa = jsonDocument[mConfigurableSimParameterNames.at(3).c_str()].GetDouble(); mFallSpecterSlopeLog = jsonDocument[mConfigurableSimParameterNames.at(4).c_str()].GetDouble(); mFallSpecterAffinePowerConstantTerm = jsonDocument[mConfigurableSimParameterNames.at(5).c_str()].GetDouble(); mFallSpecterAffinePowerSlope = jsonDocument[mConfigurableSimParameterNames.at(6).c_str()].GetDouble(); mBkgAveragePt = jsonDocument[mConfigurableSimParameterNames.at(7).c_str()].GetDouble(); - mCollTotalMultWithBkg = jsonDocument[mConfigurableSimParameterNames.at(8).c_str()].GetDouble(); + mCollMultPowerLawAmplitude = + jsonDocument[mConfigurableSimParameterNames.at(8).c_str()].GetDouble(); + mCollMultPowerLawExponent = + jsonDocument[mConfigurableSimParameterNames.at(9).c_str()].GetDouble(); + mCollMultMin = + jsonDocument[mConfigurableSimParameterNames.at(10).c_str()].GetInt(); + mCollMultMax = + jsonDocument[mConfigurableSimParameterNames.at(11).c_str()].GetInt(); // clean up std::fclose(fjson); cout << "param retrieved: mSglGenRAA = " << mSglGenRAA << endl; cout << "param retrieved: mSglGenTAA = " << mSglGenTAA << endl; - cout << "param retrieved: mSglCutoffSteepNess = " << mSglCutoffSteepNess << endl; + cout << "param retrieved: msglCutoffSteepness = " << msglCutoffSteepness << endl; cout << "param retrieved: mSglCutoffAbscissa = " << mSglCutoffAbscissa << endl; cout << "param retrieved: mFallSpecterSlopeLog = " << mFallSpecterSlopeLog << endl; cout << "param retrieved: mFallSpecterAffinePowerConstantTerm = " << mFallSpecterAffinePowerConstantTerm << endl; cout << "param retrieved: mFallSpecterAffinePowerSlope = " << mFallSpecterAffinePowerSlope << endl; cout << "param retrieved: mBkgAveragePt = " << mBkgAveragePt << endl; - cout << "param retrieved: mCollTotalMultWithBkg = " << mCollTotalMultWithBkg << endl; - - // thermal background function + cout << "param retrieved: mCollMultPowerLawAmplitude = " + << mCollMultPowerLawAmplitude << endl; + cout << "param retrieved: mCollMultPowerLawExponent = " + << mCollMultPowerLawExponent << endl; + cout << "param retrieved: mcollMultMin = " << mCollMultMin << endl; + cout << "param retrieved: mcollMultMax = " << mCollMultMax << endl; + + // thermal background pdf mBoltzmannPDF = new TF1("f1", "[0]*[0]*x*exp(-[0]*x)", mBkgGenPtMin, mPtInfinity); mBoltzmannPDF->SetParameter(0, 2. / mBkgAveragePt); - // jet signal function + // collision multiplicity pdf + mCollisionMultPDF = + new TF1("f1", "exp([0])*pow(x,[1])", mCollMultMin, mCollMultMax); + mCollisionMultPDF->SetParameter(0, mCollMultPowerLawAmplitude); + mCollisionMultPDF->SetParameter(1, mCollMultPowerLawExponent); + + // jet signal pdf // this thesis says that the jet distrib used to sample parton pt is // actually full jet -> solves neutral particle fragments issue (better than // scaling) https://drupal.star.bnl.gov/STAR/files/phd_thesis_rusnak.pdf for @@ -151,7 +177,7 @@ public: mJetYieldFit = new TF1("f2", "[0]*[1]*exp(-exp(-[2]*(x-[3]))) * exp([4])*pow(x, [5]+[6]*x)", 0, mPtInfinity); // RAA * TAA * sigmoid(cutoff at [2]) * fit to fulljetSpectrum in pp PYTHIA mJetYieldFit->SetParameter(0, mSglGenRAA); // rAA (single value for all pt) mJetYieldFit->SetParameter(1, mSglGenTAA); // - mJetYieldFit->SetParameter(2, mSglCutoffSteepNess); // steepness for the cutoff shape; higher is + mJetYieldFit->SetParameter(2, msglCutoffSteepness); // steepness for the cutoff shape; higher is // steeper, but looks more and more like a // Heavyside step function as it gets to 50 or 100 mJetYieldFit->SetParameter(3, mSglCutoffAbscissa); // cutoffpoint of hard population, in GeV/c @@ -295,7 +321,24 @@ public: if (mDebug) { cout << "####################### Adding Thermal Background #######################" << endl; } - for (int iBkg{0}; iBkg < mCollTotalMultWithBkg - nHardParticles; ++iBkg) { + + int mCollTotalMultWithBkg = 0; + if (std::abs(mCollMultMax - mCollMultMin) == 0) { + mCollTotalMultWithBkg = mCollMultMin; + } else { + mCollTotalMultWithBkg = + mCollisionMultPDF->GetRandom(mCollMultMin, mCollMultMax); + } + + if (mDebug) { + cout << "mCollTotalMultWithBkg = " << mCollTotalMultWithBkg << endl; + } + + int nBkgParticles = (mCollTotalMultWithBkg - nHardParticles) > 0 + ? mCollTotalMultWithBkg - nHardParticles + : 0; + + for (int iBkg{0}; iBkg < nBkgParticles; ++iBkg) { const double bkgPt = mBoltzmannPDF->GetRandom(mBkgGenPtMin, mPtInfinity); const double bkgEta = gRandom->Uniform(mGenMinEta, mGenMaxEta); const double bkgPhi = gRandom->Uniform(0, o2::constants::math::TwoPI); @@ -339,7 +382,7 @@ public: private: bool mDebug = false; // setting to true will display particle lists - std::string mInputSimParametersPath, mInputSimParametersFileName; // input path and file name of .json used to read simulation parameters + std::string mInputSimParametersFileName; // input path and file name of .json used to read simulation parameters //////////////////////////////////////////////// ///////// Common signal and background parameters //////// @@ -347,20 +390,35 @@ private: const double mPtInfinity = 300; // maximum pt (in GeV/c) for generated particles, and upper pT limit for integral and TF1 purposes; too high and GetRandom struggles const double mGenMinEta = -0.9; /// minimum pseudorapidity for generated particles - const double mGenMaxEta = +0.9; /// maximum pseudorapidity for generated particles - int mCollTotalMultWithBkg; /// total multiplicity of the collision + const double mGenMaxEta = + +0.9; /// maximum pseudorapidity for generated particles + + TF1 *mCollisionMultPDF; /// TF1 to store pdf function from which collision + /// multiplicity is drawn + double mCollMultPowerLawAmplitude; /// collision total multiplicity: power law + /// amplitude of the PDF + double mCollMultPowerLawExponent; /// collision total multiplicity: power law + /// exponent of the PDF + int mCollMultMin; /// collision total multiplicity: minimum abscissa of the + /// PDF + int mCollMultMax; /// collision total multiplicity: maximum abscissa of the + /// PDF + bool mGenerateSignal = true; /// boolean to request (or not) the generation of the jet signal bool mGenerateUE = false; /// boolean to request (or not) embedding of the jet signal inside underlying event modelled by a thermal background; if mGenerateSignal = false, only the UE is generated const std::vector mConfigurableSimParameterNames = { "sglGenRAA", "sglGenTAA", - "sglCutoffSteepNess", + "sglCutoffSteepness", "sglCutoffAbscissa", "fallSpecterSlopeLog", "fallSpecterAffinePowerConstantTerm", "fallSpecterAffinePowerSlope", "bkgAveragePt", - "collTotalMultWithBkg"}; + "collMultPowerLawAmplitude", + "collMultPowerLawExponent", + "collMultMin", + "collMultMax"}; ///////////////////////////////////////////// /////// Thermal background parameters /////// @@ -383,7 +441,7 @@ private: // signal fit double mSglGenRAA; double mSglGenTAA; - double mSglCutoffSteepNess; /// steepness for the cutoff shape; higher is steeper, but looks more and more like a Heavyside step function as it gets to 50 or 100 + double msglCutoffSteepness; /// steepness for the cutoff shape; higher is steeper, but looks more and more like a Heavyside step function as it gets to 50 or 100 double mSglCutoffAbscissa; /// minimum pt (in GeV/c) for jet signal distribution; it's a smooth cutoff double mFallSpecterSlopeLog; double mFallSpecterAffinePowerConstantTerm; @@ -398,8 +456,7 @@ private: }; ///___________________________________________________________ -FairGenerator *generateParametrisedJetModel(std::string inputSimParametersPath, - std::string inputSimParametersFileName, +FairGenerator *generateParametrisedJetModel(std::string inputSimParametersFileName, bool generateUE, bool generateSignal = true) { - return new GeneratorParametrisedJetModel(inputSimParametersPath, inputSimParametersFileName, generateUE, generateSignal); + return new GeneratorParametrisedJetModel(inputSimParametersFileName, generateUE, generateSignal); } \ No newline at end of file diff --git a/MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent0010.json b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent0010.json new file mode 100644 index 000000000..78ed15e99 --- /dev/null +++ b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent0010.json @@ -0,0 +1,15 @@ +{ + "simLog": false, + "sglGenRAA": 0.45, + "sglGenTAA": 23.38, + "sglCutoffSteepness": 15, + "sglCutoffAbscissa": 4, + "fallSpecterSlopeLog": 6.619076, + "fallSpecterAffinePowerConstantTerm": -3.918165, + "fallSpecterAffinePowerSlope": -0.002032, + "bkgAveragePt": 0.764567, + "collMultPowerLawAmplitude": 20.66, + "collMultPowerLawExponent": -1.26971, + "collMultMin": 1681, + "collMultMax": 2400 +} \ No newline at end of file diff --git a/MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent5080.json b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent5080.json new file mode 100644 index 000000000..891e7b5fb --- /dev/null +++ b/MC/config/PWGGAJE/external/generator/parametrisedJetModel/paramConfigs/parametrisedModel_PbPb_5p36TeV_cent5080.json @@ -0,0 +1,15 @@ +{ + "simLog": false, + "sglGenRAA": 0.8, + "sglGenTAA": 0.6933, + "sglCutoffSteepness": 15, + "sglCutoffAbscissa": 4, + "fallSpecterSlopeLog": 6.619076, + "fallSpecterAffinePowerConstantTerm": -3.918165, + "fallSpecterAffinePowerSlope": -0.002032, + "bkgAveragePt": 0.732677, + "collMultPowerLawAmplitude": 16.43, + "collMultPowerLawExponent": -0.683784, + "collMultMin": 47, + "collMultMax": 302 +} \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini index 4b924e121..6a83e56fb 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Bkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", true, false) +funcName = generateParametrisedJetModel("parametrisedModel_PbPb_5p36TeV_cent0010.json", true, false) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini index fdb2f19c1..6729dc50b 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_Sgl.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", false, true) +funcName = generateParametrisedJetModel("parametrisedModel_PbPb_5p36TeV_cent0010.json", false, true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini index 2be7f0af6..04e70fd25 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent0010_SglBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent0010.json", true, true) +funcName = generateParametrisedJetModel("parametrisedModel_PbPb_5p36TeV_cent0010.json", true, true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini index dcaf2be8d..383fda3fe 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Bkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", true, false) +funcName = generateParametrisedJetModel("parametrisedModel_PbPb_5p36TeV_cent5080.json", true, false) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini index 52bc153a7..df4f91cb7 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_Sgl.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", false, true) +funcName = generateParametrisedJetModel("parametrisedModel_PbPb_5p36TeV_cent5080.json", false, true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file diff --git a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini index 4800459c8..3ffde9809 100644 --- a/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini +++ b/MC/config/PWGGAJE/ini/GeneratorPythia8Box_parametrisedModel_pythia6Fragmentation_PbPb_cent5080_SglBkg.ini @@ -1,7 +1,7 @@ ### bkg production using pythia8 box generator, to embed jet-jet production into [GeneratorExternal] fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/parametrisedJetModel/generator_pythia8_box_parametrisedModel_pythia6Fragmentation.C -funcName = generateParametrisedJetModel("/alice/cern.ch/user/a/alandou/Analysis/PWGJE/simSettings/combinatorialBkgClosure/", "parametrisedModel_PbPb_5p36TeV_cent5080.json", true, true) +funcName = generateParametrisedJetModel("parametrisedModel_PbPb_5p36TeV_cent5080.json", true, true) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8box_parametrisedModel.cfg \ No newline at end of file From ca014390560d4cacd631759f1328ea409fb162aa Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 13 Oct 2026 23:45:54 +0200 Subject: [PATCH 106/144] First implementation of HGPythia (#2485) * First implementation of HGPythia * Removed shadowing + added sigmaHard for < 9.7 TeV systems --- .../external/generator/GeneratorHGPythia8.C | 445 ++++++++++++++++++ .../ini/GeneratorHGPythia8PbPb536TeV.ini | 8 + .../ini/tests/GeneratorHGPythia8PbPb536TeV.C | 56 +++ 3 files changed, 509 insertions(+) create mode 100644 MC/config/common/external/generator/GeneratorHGPythia8.C create mode 100644 MC/config/common/ini/GeneratorHGPythia8PbPb536TeV.ini create mode 100644 MC/config/common/ini/tests/GeneratorHGPythia8PbPb536TeV.C diff --git a/MC/config/common/external/generator/GeneratorHGPythia8.C b/MC/config/common/external/generator/GeneratorHGPythia8.C new file mode 100644 index 000000000..1e2a1d5c1 --- /dev/null +++ b/MC/config/common/external/generator/GeneratorHGPythia8.C @@ -0,0 +1,445 @@ +// HG-PYTHIA: heavy-ion events built from a Glauber model with eikonal NN +// interactions and minijet (hard scattering) counting, where each NN +// collision is represented by a PYTHIA 8 pp event with the matching number +// of multiparton interactions. The heavy-ion event is the sum of the pp +// events. +// +// Model: C. Loizides, A. Morsch, Phys. Lett. B773 (2017) 408 (arXiv:1705.08856) +// Reference implementation: https://github.com/abaty/HGPythia (A. Baty) +// +// The PYTHIA pp configuration (beams at sqrt(s_NN), SoftQCD:inelastic, +// decays...) is given through the usual GeneratorPythia8 parameters, e.g. +// GeneratorPythia8.config=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_inel_536.cfg +// The collision energy used for the Glauber model is taken from PYTHIA. +// +// Differences with respect to the reference implementation: +// - the unused J/psi and bookkeeping parts are not ported +// +/// @author Marco Giacalone (marco.giacalone@cern.ch) +/// @date 09-2026 + +#include "Generators/GeneratorPythia8.h" +#include "SimConfig/SimConfig.h" +#include "SimulationDataFormat/MCEventHeader.h" +#include "SimulationDataFormat/MCGenProperties.h" +#include "SimulationDataFormat/ParticleStatus.h" +#include "Pythia8/Pythia.h" +#include "TF1.h" +#include "TF2.h" +#include "TMath.h" +#include "TParticle.h" +#include "TRandom3.h" +#include +#include +#include +#include +#include + +namespace o2 +{ +namespace eventgen +{ + +class GeneratorHGPythia8 : public GeneratorPythia8 +{ + public: + /// Number of bins for the hard scatterings per NN collision (and MPIs per PYTHIA event), as in the + /// reference implementation: collisions with 20 or more hard scatterings are counted in the last bin (19) + static constexpr int kMaxMPI = 20; + /// Maximum number of consecutive PYTHIA events not matching any requested number of MPIs + static constexpr long kMaxRejected = 10000000; + + GeneratorHGPythia8(int A = 208, int B = 208) : GeneratorPythia8(), mA(A), mB(B) + { + // the PYTHIA interface only describes the last NN collision: do not expose it as "pythia8" + mInterfaceName = "hgpythia8"; + // defaults, overridden by the PYTHIA configuration file (GeneratorPythia8.config) read in Init + readString("Beams:idA 2212"); + readString("Beams:idB 2212"); + readString("SoftQCD:inelastic on"); + readString("ParticleDecays:limitTau0 on"); + readString("ParticleDecays:tau0Max 10."); + } + ~GeneratorHGPythia8() override = default; + + /// Impact parameter range in fm. A non-positive bMax selects o2-sim --bMax, + /// if given, otherwise 20 fm (10 fm for p-A, 5 fm for pp) + void setImpactParameterRange(double bMin, double bMax) + { + mBMin = bMin; + mBMax = bMax; + } + /// Hard (minijet) NN cross-section in mb, <0 takes it from the built-in energy table + void setSigmaHard(double sigma) { mSigmaHardIn = sigma; } + /// Soft NN cross-section in mb + void setSigmaSoft(double sigma) { mSigmaSoft = sigma; } + /// Include elastic NN scattering in the eikonal + void setElastic(bool val) { mElastic = val; } + + /// Glauber information of the current event + double getImpactParameter() const { return mImpactParameter; } + int getNcoll() const { return mNcoll; } + int getNcollHard() const { return mNcollHard; } + int getNhard() const { return mNhard; } + int getNpartProjectile() const { return mNpartProj; } + int getNpartTarget() const { return mNpartTarg; } + int getNpartBlackDisc() const { return mNpartBlackDisc; } + int getNcollBlackDisc() const { return mNcollBlackDisc; } + double getEccentricity() const { return mEccentricity; } + + Bool_t Init() override + { + if (mA < 1 || mA > 208 || mB < 1 || mB > 208) { + LOG(fatal) << "GeneratorHGPythia8: mass numbers must be between 1 and 208, got A = " << mA << ", B = " << mB; + return false; + } + // PYTHIA configuration, seeding and initialisation + if (!GeneratorPythia8::Init()) { + return false; + } + if (mPythia.settings.mode("Beams:idA") != 2212 || mPythia.settings.mode("Beams:idB") != 2212) { + LOG(warn) << "GeneratorHGPythia8: PYTHIA is expected to generate pp collisions"; + } + // Glauber random numbers seeded from the PYTHIA seed (0 means time dependent in both cases) + mRandom = std::make_unique(mPythia.settings.mode("Random:seed")); + + const double energy = mPythia.info.eCM(); + mSigmaHard = mSigmaHardIn >= 0 ? mSigmaHardIn : sigmaHardFromTable(energy); + if (mSigmaHard < 0) { + LOG(fatal) << "GeneratorHGPythia8: no hard cross-section available for sqrt(s_NN) = " << energy + << " GeV, please set it with setSigmaHard()"; + return false; + } + if (mBMax <= 0) { + const auto simBMax = o2::conf::SimConfig::Instance().getBMax(); + mBMax = simBMax > 0 ? simBMax : ((mA == 1 && mB == 1) ? 5. : ((mA == 1 || mB == 1) ? 10. : 20.)); + } + mDensity[0].reset(makeNucleonDensity(mA, "HGPythia8A")); + mDensity[1].reset(makeNucleonDensity(mB, "HGPythia8B")); + for (int j = 0; j < 2; ++j) { + mX[j].resize(j == 0 ? mA : mB); + mY[j].resize(j == 0 ? mA : mB); + mWounded[j].resize(j == 0 ? mA : mB); + mWoundedBlackDisc[j].resize(j == 0 ? mA : mB); + } + mHIEvent.init("HG-PYTHIA event", &mPythia.particleData); + + LOG(info) << "GeneratorHGPythia8: A = " << mA << ", B = " << mB << ", sqrt(s_NN) = " << energy + << " GeV, sigma_hard = " << mSigmaHard << " mb, sigma_soft = " << mSigmaSoft + << " mb, b in [" << mBMin << ", " << mBMax << "] fm, elastic " << mElastic; + return true; + } + + Bool_t generateEvent() override + { + std::array nMPI{}; + sampleGlauber(nMPI); + + // Generate PYTHIA events until each NN collision has a partner with the + // matching number of MPIs, and sum them + mHIEvent.reset(); + int nNeeded = mNcoll; + long nRejected = 0; + const bool prune = !mGenConfig.includePartonEvent; + auto select = [this](const Pythia8::Particle& p) { + const int st = p.statusHepMC(); + return (st == 1 || st == 2 || st == 4) && mUserFilterFcn(p); + }; + while (nNeeded > 0) { + if (!mPythia.next()) { + continue; + } + const int mpi = mPythia.info.nMPI(); + if (mpi >= kMaxMPI || nMPI[mpi] == 0) { + if (++nRejected > kMaxRejected) { + LOG(fatal) << "GeneratorHGPythia8: " << kMaxRejected << " consecutive PYTHIA events do not match the " + << "requested number of MPIs, check the PYTHIA configuration (SoftQCD:inelastic needed)"; + return false; + } + continue; + } + nRejected = 0; + nMPI[mpi]--; + nNeeded--; + if (prune) { + pruneEvent(mPythia.event, select); + } + mHIEvent += mPythia.event; + } + return true; + } + + Bool_t importParticles() override + { + // same conversion as GeneratorPythia8::importParticles, without pruning again the summed event + for (int i = 1; i < mHIEvent.size(); ++i) { + const auto& particle = mHIEvent[i]; + auto st = o2::mcgenstatus::MCGenStatusEncoding(particle.statusHepMC(), particle.status()).fullEncoding; + mParticles.push_back(TParticle(particle.id(), st, + particle.mother1() - 1, particle.mother2() - 1, + particle.daughter1() - 1, particle.daughter2() - 1, + particle.px(), particle.py(), particle.pz(), particle.e(), + particle.xProd(), particle.yProd(), particle.zProd(), particle.tProd())); + mParticles.back().SetBit(ParticleStatus::kToBeDone, particle.statusHepMC() == 1); + } + return true; + } + + void updateHeader(o2::dataformats::MCEventHeader* eventHeader) override + { + using Key = o2::dataformats::MCInfoKeys; + eventHeader->putInfo(Key::generator, "hgpythia8"); + eventHeader->putInfo(Key::generatorVersion, PYTHIA_VERSION_INTEGER); + eventHeader->SetB(mImpactParameter); + eventHeader->putInfo(Key::impactParameter, mImpactParameter); + eventHeader->putInfo(Key::planeAngle, 0.); // impact parameter along x + eventHeader->putInfo(Key::nColl, mNcoll); + eventHeader->putInfo(Key::nCollHard, mNcollHard); + eventHeader->putInfo(Key::nPart, mNpartProj + mNpartTarg); + eventHeader->putInfo(Key::nPartProjectile, mNpartProj); + eventHeader->putInfo(Key::nPartTarget, mNpartTarg); + eventHeader->putInfo(Key::sigmaInelNN, mSigmaSoft + mSigmaHard); + eventHeader->putInfo("nHard", mNhard); + eventHeader->putInfo("Npart_blackdisc", mNpartBlackDisc); + eventHeader->putInfo("Ncoll_blackdisc", mNcollBlackDisc); + eventHeader->putInfo("eccentricity", mEccentricity); + } + + private: + /// Hard cross-section (pT > 2 GeV) in mb as a function of sqrt(s_NN), from the reference implementation + double sigmaHardFromTable(double energy) const + { + if (energy < 20) return 0.161440969; + if (energy < 40) return 1.07414019; + if (energy < 64) return 2.32993174; + if (energy < 201) return 11.6641903; + if (energy < 2800) return 85.2298813; + if (energy < 5100) return 124.296341; + if (energy < 5500) return 130.82; + if (energy < 6400) return 144.17; + if (energy < 8100) return 166.184998; + if (energy < 9700) return 180.87; + return -1; + } + + /// Radial (radial and polar for deformed nuclei) nucleon density; a single nucleon (A = 1) is + /// sampled from the exponential proton profile with R = 0.234 fm (rms charge radius), as in TGlauberMC + static TF1* makeNucleonDensity(int A, const std::string& suffix) + { + auto name = [&suffix](const char* n) { return std::string(n) + suffix; }; + if (A == 208) { + return new TF1(name("wsPb").c_str(), "7.208e-4*4.*TMath::Pi()*x^2/(1+exp((x-6.62)/0.546))", 0., 20.); + } + if (A == 197) { + return new TF1(name("wsAu").c_str(), "8.596e-04*4.*TMath::Pi()*x^2/(1+exp((x-6.38)/0.535))", 0., 20.); + } + if (A == 129) { + auto f = new TF2(name("wsXe2a").c_str(), "x*x*TMath::Sin(y)/(1+exp((x-[0]*(1+[2]*0.315*(3*pow(cos(y),2)-1.0)+[3]*0.105*(35*pow(cos(y),4)-30*pow(cos(y),2)+3)))/[1]))", 0, 15, 0.0, TMath::Pi()); + f->SetNpx(120); + f->SetNpy(120); + f->SetParameters(5.36, 0.59, 0.18, 0); + return f; + } + if (A == 40) { + return new TF1(name("wsAr").c_str(), "1.*TMath::Pi()*x^2/(1+exp((x-3.53)/0.542))", 0., 15.); + } + if (A == 20) { + auto f = new TF1(name("wsNe").c_str(), "x*x*(1+[2]*(x/[0])**2)/(1+exp((x-[0])/[1]))", 0, 10.); + f->SetParameters(2.791, 0.698, -0.168); + return f; + } + if (A == 16) { + auto f = new TF1(name("wsO").c_str(), "x*x*(1+[2]*(x/[0])**2)/(1+exp((x-[0])/[1]))", 0, 10.); + f->SetParameters(2.608, 0.513, -0.051); + return f; + } + if (A == 6) { + return new TF1(name("wsC").c_str(), "7.208e-4*4.*TMath::Pi()*x^2*(1.-0.149*(x/2.46)**2)/(1+exp((x-2.46)/0.522))", 0., 10.); + } + if (A == 3) { + return new TF1(name("wsHe").c_str(), "7.208e-4*4.*TMath::Pi()*x^2*(1.+0.517*(x/0.964)**2)/(1+exp((x-0.964)/0.322))", 0., 10.); + } + if (A == 1) { + return new TF1(name("prot").c_str(), "x*x*exp(-x/0.234)", 0., 5.); + } + LOG(fatal) << "GeneratorHGPythia8: nucleus with A = " << A << " not supported (1, 3, 6, 16, 20, 40, 129, 197, 208)"; + return nullptr; + } + + /// Matter distribution in the proton (eikonal) + static double eikonal(double x) + { + constexpr double p0 = 3.9, p1 = 96.; + return p0 * p0 / p1 * TMath::Power(p0 * x, 3) * TMath::BesselK(3, p0 * x); + } + + /// Sample the nucleon positions in the transverse plane for nucleus j, centred at x = dx + void sampleNucleus(int j, double dx) + { + auto density = mDensity[j].get(); + auto f2 = dynamic_cast(density); + for (size_t k = 0; k < mX[j].size(); ++k) { + double x = 0., y = 0.; + if (f2) { + double r, theta; + f2->GetRandom2(r, theta, mRandom.get()); + const double phi = 2. * TMath::Pi() * mRandom->Rndm(); + x = r * TMath::Sin(phi) * TMath::Sin(theta); + y = r * TMath::Cos(phi) * TMath::Sin(theta); + } else if (density) { + const double r = density->GetRandom(mRandom.get()); + const double phi = 2. * TMath::Pi() * mRandom->Rndm(); + const double costh = 2. * mRandom->Rndm() - 1.; + const double sinth = costh * costh < 1. ? TMath::Sqrt(1. - costh * costh) : 0.; + x = r * sinth * TMath::Cos(phi); + y = r * sinth * TMath::Sin(phi); + } + mX[j][k] = x + dx; + mY[j][k] = y; + } + } + + /// Sample a Glauber configuration with at least one inelastic NN collision and count, for + /// each NN collision, the number of hard scatterings (nMPI[0]: collisions without any) + void sampleGlauber(std::array& nMPI) + { + constexpr double dmax = 1.43; // black-disc NN distance (fm) + const double b02 = 0.5 * mSigmaSoft * 0.1 / TMath::Pi(); + do { + nMPI.fill(0); + const double b = TMath::Sqrt(mBMin * mBMin + mRandom->Rndm() * (mBMax * mBMax - mBMin * mBMin)); + sampleNucleus(0, b / 2.); + sampleNucleus(1, -b / 2.); + for (int j = 0; j < 2; ++j) { + std::fill(mWounded[j].begin(), mWounded[j].end(), 0); + std::fill(mWoundedBlackDisc[j].begin(), mWoundedBlackDisc[j].end(), 0); + } + const double gstot0 = mElastic ? 2. * (1. - TMath::Exp(-(mSigmaSoft + mSigmaHard) / mSigmaSoft * eikonal(0.001))) : 1.; + + mImpactParameter = b; + mNcoll = mNcollHard = mNhard = mNcollBlackDisc = 0; + for (int i = 0; i < mA; ++i) { + for (int j = 0; j < mB; ++j) { + const double dx = mX[0][i] - mX[1][j]; + const double dy = mY[0][i] - mY[1][j]; + double r2 = dx * dx + dy * dy; + if (r2 < dmax * dmax) { + mWoundedBlackDisc[0][i] = 1; + mWoundedBlackDisc[1][j] = 1; + mNcollBlackDisc++; + } + if (r2 > 25.) { + continue; + } + // interaction probability + r2 /= b02; + r2 /= gstot0; + const double chi = eikonal(TMath::Sqrt(r2)); + const double gs = 1. - TMath::Exp(-2. * (mSigmaSoft + mSigmaHard) / mSigmaSoft * chi); + const double gstot = 2. * (1. - TMath::Sqrt(1. - gs)); + const double rantot = mRandom->Rndm() * gstot0; + if (rantot > gstot && mElastic) { + continue; + } + if (rantot > gs) { + continue; + } + mWounded[0][i] = 1; + mWounded[1][j] = 1; + mNcoll++; + // minijets + const double tt = 2. * chi * mSigmaHard / mSigmaSoft; + const double ts = 2. * chi; + if (rantot < TMath::Exp(-tt) * (1. - TMath::Exp(-ts))) { + nMPI[0]++; + continue; + } + double xr = -TMath::Log(TMath::Exp(-tt) + mRandom->Rndm() * (1. - TMath::Exp(-tt))); + int njet = 0; + while (true) { + njet++; + xr -= TMath::Log(mRandom->Rndm()); + if (xr > tt) { + break; + } + } + njet = TMath::Min(njet, kMaxMPI - 1); + nMPI[njet]++; + mNhard += njet; + mNcollHard++; + } + } + } while (mNcoll < 1); + + // participants and participant eccentricity + mNpartProj = mNpartTarg = mNpartBlackDisc = 0; + double mx = 0., my = 0., mx2 = 0., my2 = 0., mxy = 0.; + for (int j = 0; j < 2; ++j) { + for (size_t k = 0; k < mX[j].size(); ++k) { + mNpartBlackDisc += mWoundedBlackDisc[j][k]; + if (!mWounded[j][k]) { + continue; + } + (j == 0 ? mNpartProj : mNpartTarg)++; + mx += mX[j][k]; + my += mY[j][k]; + mx2 += mX[j][k] * mX[j][k]; + my2 += mY[j][k] * mY[j][k]; + mxy += mX[j][k] * mY[j][k]; + } + } + const double iw = mNpartProj + mNpartTarg; + mx2 -= mx * mx / iw; + my2 -= my * my / iw; + mxy -= mx * my / iw; + mEccentricity = (mx2 + my2) > 0 ? TMath::Sqrt((my2 - mx2) * (my2 - mx2) + 4. * mxy * mxy) / (mx2 + my2) : 0.; + } + + // configuration + int mA = 208; + int mB = 208; + double mBMin = 0.; + double mBMax = -1.; + double mSigmaHardIn = -1.; + double mSigmaHard = -1.; + double mSigmaSoft = 57.; + bool mElastic = false; + + // Glauber state + std::unique_ptr mRandom; + std::array, 2> mDensity; + std::array, 2> mX; + std::array, 2> mY; + std::array, 2> mWounded; + std::array, 2> mWoundedBlackDisc; + double mImpactParameter = 0.; + int mNcoll = 0; + int mNcollHard = 0; + int mNhard = 0; + int mNpartProj = 0; + int mNpartTarg = 0; + int mNpartBlackDisc = 0; + int mNcollBlackDisc = 0; + double mEccentricity = 0.; + + // sum of the PYTHIA events of the current heavy-ion event + Pythia8::Event mHIEvent; +}; + +} // namespace eventgen +} // namespace o2 + +/// HG-PYTHIA generator for nuclei with mass numbers A (along +z, PYTHIA beam A) and B. +/// bMax <= 0: o2-sim --bMax if given, otherwise 20 fm (10 fm for p-A, 5 fm for pp). +/// sigmaHard < 0: hard cross-section from the built-in sqrt(s_NN) table (up to 8.1 TeV). +FairGenerator* generateHGPythia8(int A = 208, int B = 208, double bMin = 0., double bMax = -1., + double sigmaHard = -1., double sigmaSoft = 57., + bool elastic = false) +{ + auto gen = new o2::eventgen::GeneratorHGPythia8(A, B); + gen->setImpactParameterRange(bMin, bMax); + gen->setSigmaHard(sigmaHard); + gen->setSigmaSoft(sigmaSoft); + gen->setElastic(elastic); + return gen; +} diff --git a/MC/config/common/ini/GeneratorHGPythia8PbPb536TeV.ini b/MC/config/common/ini/GeneratorHGPythia8PbPb536TeV.ini new file mode 100644 index 000000000..ea1e57c76 --- /dev/null +++ b/MC/config/common/ini/GeneratorHGPythia8PbPb536TeV.ini @@ -0,0 +1,8 @@ +#NEV_TEST> 5 +# HG-PYTHIA (Glauber + PYTHIA 8 pp collisions) Pb-Pb at sqrt(s_NN) = 5.36 TeV, minimum bias +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/external/generator/GeneratorHGPythia8.C +funcName=generateHGPythia8(208, 208) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_inel_536.cfg diff --git a/MC/config/common/ini/tests/GeneratorHGPythia8PbPb536TeV.C b/MC/config/common/ini/tests/GeneratorHGPythia8PbPb536TeV.C new file mode 100644 index 000000000..a54dcabba --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorHGPythia8PbPb536TeV.C @@ -0,0 +1,56 @@ +int External() +{ + const int A = 208; // mass number of both nuclei + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader* header = nullptr; + tree->SetBranchAddress("MCEventHeader.", &header); + + using Key = o2::dataformats::MCInfoKeys; + const auto nEvents = tree->GetEntries(); + if (nEvents == 0) { + std::cerr << "No events found\n"; + return 1; + } + for (Long64_t i = 0; i < nEvents; ++i) { + tree->GetEntry(i); + bool valid = false; + const int nColl = header->getInfo(Key::nColl, valid); + if (!valid || nColl < 1 || nColl > A * A) { + std::cerr << "Missing or invalid Ncoll in event " << i << "\n"; + return 1; + } + const int nPart = header->getInfo(Key::nPart, valid); + if (!valid || nPart < 2 || nPart > 2 * A) { + std::cerr << "Missing or invalid Npart in event " << i << "\n"; + return 1; + } + // each NN collision is a PYTHIA pp event: two beam protons with half of sqrt(s_NN) each + int nBeams = 0; + for (const auto& track : *tracks) { + if (o2::mcgenstatus::getHepMCStatusCode(track.getStatusCode()) == 4) { + if (track.GetPdgCode() != 2212 || std::abs(track.GetEnergy() - 2680.) > 1e-3) { + std::cerr << "Unexpected beam particle " << track.GetPdgCode() << " with energy " << track.GetEnergy() << " in event " << i << "\n"; + return 1; + } + nBeams++; + } + } + if (nBeams != 2 * nColl) { + std::cerr << "Found " << nBeams << " beam protons for " << nColl << " NN collisions in event " << i << "\n"; + return 1; + } + } + return 0; +} From a60401c0d0194a2124d8bfc548d59176435f05d5 Mon Sep 17 00:00:00 2001 From: shahor02 Date: Thu, 15 Oct 2026 13:28:07 +0200 Subject: [PATCH 107/144] Support ITS(MFT) staggerd MC with ALIEN_JDL_ITS(MFT)_STAGGERED=1 (#2487) * Support ITS(MFT) staggerd MC with ALIEN_JDL_ITS(MFT)_STAGGERED=1 Validated with https://github.com/AliceO2Group/AliceO2/pull/15878, but the QC tasks are not yet ready for the staggered data * At the moment disable the tasks not yet ready for per-layer input * Add a workflow test with ITS and MFT staggering enabled This commit adds a CI workflow test that creates and runs a pp workflow with ALIEN_JDL_ITS_STAGGERED=1 and ALIEN_JDL_MFT_STAGGERED=1. - wf_test_pp.sh does not set the variables, so the staggering code paths of this PR were not exercised by CI. - The test is picked up automatically from MC/bin/tests. Co-Authored-By: Claude Sonnet 5.5 --------- Co-authored-by: Sandro Wenzel Co-authored-by: Claude Sonnet 5.5 --- MC/bin/o2dpg_sim_workflow.py | 67 ++++++++++++++++++++-------- MC/bin/tests/wf_test_pp_staggered.sh | 30 +++++++++++++ 2 files changed, 79 insertions(+), 18 deletions(-) create mode 100755 MC/bin/tests/wf_test_pp_staggered.sh diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 54352b80a..1dbd25b7c 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -241,10 +241,20 @@ def load_external_config(configfile): # merge the dictionaries into anchorConfig, the latter takes precedence merge_dicts(anchorConfig, config_overwrite) +async_envfile = 'env_async.env' if environ.get('ALIEN_JDL_O2DPG_ASYNC_RECO_TAG') is not None else None + +# Check if either ITS or MFT are in the staggered mode (either from the async reco workflow or imposed externally). +# Note thas some QC tracks at the moment need to be suppressed in this mode +staggITS = ' --enable-its-staggering ' if environ.get('ALIEN_JDL_ITS_STAGGERED') == '1' and option_if_available('o2-its-reco-workflow', '--enable-its-staggering', envfile=async_envfile) != '' else '' +# MFT still needs to update its reconstruction to account for per-layer input, uncomment line below whtb it is ready and remove "Ignoring ..." stuff. +#staggMFT = ' --enable-mft-staggering ' if environ.get('ALIEN_JDL_MFT_STAGGERED') == '1' and option_if_available('o2-mft-reco-workflow', '--enable-mft-staggering', envfile=async_envfile) != '' else '' +#if environ.get('ALIEN_JDL_MFT_STAGGERED') == '1': +# print ("Ignoring ALIEN_JDL_MFT_STAGGERED as the MFT reconstruction is not yet ready to take it") +staggMFT = '' + # We still may need adjust configurations manually for consistency: # # * Force simpler TPC digitization of if TPC reco does not have the mc-time-gain option or remap to a different CCDB object if we are anchored to 2023: -async_envfile = 'env_async.env' if environ.get('ALIEN_JDL_O2DPG_ASYNC_RECO_TAG') is not None else None tpcreco_mctimegain = option_if_available('o2-tpc-reco-workflow', '--tpc-mc-time-gain', envfile=async_envfile) if tpcreco_mctimegain == '': # TODO: Upload all MC time gain objects to TestReco and remove year dependence @@ -1203,6 +1213,10 @@ def createRestDigiTask(name, det='ALLSMALLER'): detlist = ','.join(smallsensorlist) detlist = cleanDetectorInputList(detlist) t['cmd'] += commondigicmd + ' --onlyDet ' + detlist + if 'ITS' in detlist: + t['cmd'] += staggITS + if 'MFT' in detlist: + t['cmd'] += staggMFT t['cmd'] += ' --ccdb-tof-sa --forceSelectedDets ' t['cmd'] += (' --combine-devices ','')[args.no_combine_dpl_devices] t['cmd'] += ('',' --disable-mc')[args.no_mc_labels] @@ -1215,6 +1229,10 @@ def createRestDigiTask(name, det='ALLSMALLER'): t = createTask(name=name, needs=tneeds, tf=tf, cwd=timeframeworkdir, lab=["DIGI","SMALLDIGI"], cpu='1') t['cmd'] = ('','ln -nfs ../bkg_Hits' + str(det) + '.root . ;')[doembedding] t['cmd'] += commondigicmd + ' --onlyDet ' + str(det) + if det == 'ITS': + t['cmd'] += staggITS + if det == 'MFT': + t['cmd'] += staggMFT t['cmd'] += ('',' --disable-mc')[args.no_mc_labels] if det == 'TOF': t['cmd'] += ' --ccdb-tof-sa' @@ -1374,7 +1392,7 @@ def getDigiTaskName(det): TPCSyncRECOtask['cmd'] = '${O2_ROOT}/bin/o2-tpc-reco-workflow ' + getDPL_global_options(bigshm=True, ccdbbackend=False, runcommand=False) \ + '--input-type clusters --output-type clusters,disable-writer ' \ + putConfigValues() - TPCSyncRECOtask['cmd'] += ' | ${O2_ROOT}/bin/o2-gpu-reco-workflow' + getDPL_global_options(bigshm=True, ccdbbackend=True, runcommand=False) \ + TPCSyncRECOtask['cmd'] += ' | ${O2_ROOT}/bin/o2-gpu-reco-workflow ' + getDPL_global_options(bigshm=True, ccdbbackend=True, runcommand=False) \ + '--input-type clusters --output-type compressed-clusters-flat,clusters,send-clusters-per-sector --filtered-output-specs ' \ + tpc_corr_scaling_options + ' ' + tpc_corr_options_mc \ + putConfigValues(["TPCGasParam", "TPCCorrMap", "trackTuneParams"], @@ -1414,6 +1432,7 @@ def getDigiTaskName(det): ITSRECOtask['cmd'] = task_finalizer([ "${O2_ROOT}/bin/o2-its-reco-workflow" if args.detectorList == 'ALICE2' else "${O2_ROOT}/bin/o2-its3-reco-workflow", getDPL_global_options(bigshm=havePbPb), + staggITS, '--tracking-mode async', putConfigValues(["ITSVertexerParam", "ITSAlpideParam", @@ -1439,6 +1458,7 @@ def getDigiTaskName(det): ITSTPCMATCHtask=createTask(name='itstpcMatch_'+str(tf), needs=[TPCRECOtask['name'], ITSRECOtask['name'], FT0RECOtask['name'] if isActive("FT0") else None], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='8000', relative_cpu=3/8) ITSTPCMATCHtask["cmd"] = task_finalizer([ '${O2_ROOT}/bin/o2-tpcits-match-workflow', + staggITS, getDPL_global_options(bigshm=True), ' --tpc-track-reader tpctracks.root', '--tpc-native-cluster-reader \"--infile tpc-native-clusters.root\"', @@ -1474,6 +1494,7 @@ def getDigiTaskName(det): TRDTRACKINGtask2['cmd'] = task_finalizer([ '${O2_ROOT}/bin/o2-trd-global-tracking', getDPL_global_options(bigshm=True), + staggITS, ('',' --disable-mc')[args.no_mc_labels], putConfigValues(['ITSClustererParam', 'ITSCATrackerParam', @@ -1537,6 +1558,7 @@ def getDigiTaskName(det): MFTRECOtask['cmd'] += task_finalizer([ '${O2_ROOT}/bin/o2-mft-reco-workflow', getDPL_global_options(), + staggMFT, putConfigValues(['MFTTracking', 'MFTAlpideParam', 'ITSClustererParam', @@ -1664,6 +1686,7 @@ def getDigiTaskName(det): MFTMCHMATCHtask = createTask(name='mftmchMatch_'+str(tf), needs=forwardmatchneeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') MFTMCHMATCHtask['cmd'] = task_finalizer( ['${O2_ROOT}/bin/o2-globalfwd-matcher-workflow', + staggMFT, putConfigValues(['ITSAlpideConfig', 'MFTAlpideConfig', 'FwdMatching'],{"FwdMatching.useMIDMatch": "true" if isActive("MID") else "false"}), @@ -1679,7 +1702,7 @@ def getDigiTaskName(det): if args.fwdmatching_save_trainingdata == True: MFTMCHMATCHTraintask = createTask(name='mftmchMatchTrain_'+str(tf), needs=[MCHMIDMATCHtask['name'], MFTRECOtask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='1500') - MFTMCHMATCHTraintask['cmd'] = '${O2_ROOT}/bin/o2-globalfwd-matcher-workflow ' + putConfigValues(['ITSAlpideConfig','MFTAlpideConfig'],{"FwdMatching.useMIDMatch":"true"}) + MFTMCHMATCHTraintask['cmd'] = '${O2_ROOT}/bin/o2-globalfwd-matcher-workflow ' + staggMFT + putConfigValues(['ITSAlpideConfig','MFTAlpideConfig'],{"FwdMatching.useMIDMatch":"true"}) MFTMCHMATCHTraintask['cmd']+= getDPL_global_options() workflow['stages'].append(MFTMCHMATCHTraintask) @@ -1769,6 +1792,7 @@ def getDigiTaskName(det): SVFINDERtask = createTask(name='svfinder_'+str(tf), needs=[PVFINDERtask['name'], FT0FV0EMCCTPDIGItask['name']], tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu=svfinder_cpu, mem='5000') SVFINDERtask['cmd'] = task_finalizer( [ '${O2_ROOT}/bin/o2-secondary-vertexing-workflow', + staggITS, getDPL_global_options(bigshm=True), svfinder_threads, putConfigValues(['svertexer', 'TPCCorrMap', 'GlobalParams'], {"NameConf.mDirMatLUT" : ".."} | tpcLocalCFreco), @@ -1825,6 +1849,8 @@ def getDigiTaskName(det): AODtask['cmd'] += '[ -f AO2D.root ] && rm AO2D.root; ' AODtask['cmd'] += task_finalizer([ "${O2_ROOT}/bin/o2-aod-producer-workflow", + staggITS, + staggMFT, "--reco-mctracks-only 1", "--aod-writer-keep dangling", "--aod-writer-resfile AO2D", @@ -1934,24 +1960,25 @@ def remove_json_prefix(path): ### MFT # to be enabled once MFT Digits should run 5 times with different configurations - if isActive("MFT"): + # At the moment QC does not support per-layer digits/clusters, disable these tasks if staggering is requested + if isActive("MFT") and not staggMFT: for flp in range(5): addQCPerTF(taskName='mftDigitsQC' + str(flp), - needs=[getDigiTaskName("MFT")], - readerCommand='o2-qc-mft-digits-root-file-reader --mft-digit-infile=mftdigits.root', - configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mft-digits-' + str(flp) + '.json', - objectsFile='mftDigitsQC.root') + needs=[getDigiTaskName("MFT")], + readerCommand='o2-qc-mft-digits-root-file-reader --mft-digit-infile=mftdigits.root' + staggMFT, + configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mft-digits-' + str(flp) + '.json', + objectsFile='mftDigitsQC.root') addQCPerTF(taskName='mftClustersQC', needs=[MFTRECOtask['name']], - readerCommand='o2-global-track-cluster-reader --track-types none --cluster-types MFT', + readerCommand='o2-global-track-cluster-reader --track-types none --cluster-types MFT' + staggMFT, configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mft-clusters.json') addQCPerTF(taskName='mftTracksQC', needs=[MFTRECOtask['name']], - readerCommand='o2-global-track-cluster-reader --track-types MFT --cluster-types MFT', + readerCommand='o2-global-track-cluster-reader --track-types MFT --cluster-types MFT' + staggMFT, configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mft-tracks.json') addQCPerTF(taskName='mftMCTracksQC', needs=[MFTRECOtask['name']], - readerCommand='o2-global-track-cluster-reader --track-types MFT --cluster-types MFT', + readerCommand='o2-global-track-cluster-reader --track-types MFT --cluster-types MFT' + staggMFT, configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mft-tracks-mc.json') ### TPC @@ -2062,13 +2089,15 @@ def remove_json_prefix(path): ### ITS addQCPerTF(taskName='ITSTrackSimTaskQC', needs=[ITSRECOtask['name']], - readerCommand='o2-global-track-cluster-reader --track-types "ITS" --cluster-types "ITS"', + readerCommand='o2-global-track-cluster-reader --track-types "ITS" --cluster-types "ITS"' + staggITS, configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/its-mc-tracks-qc.json') - addQCPerTF(taskName='ITSTracksClustersQC', - needs=[ITSRECOtask['name']], - readerCommand='o2-global-track-cluster-reader --track-types "ITS" --cluster-types "ITS"', - configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/its-clusters-tracks-qc.json') + # At the moment QC does not support per-layer digits/clusters, disable these tasks if staggering is requested + if not staggITS: + addQCPerTF(taskName='ITSTracksClustersQC', + needs=[ITSRECOtask['name']], + readerCommand='o2-global-track-cluster-reader --track-types "ITS" --cluster-types "ITS"' + staggITS, + configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/its-clusters-tracks-qc.json') ### CPV if isActive('CPV'): @@ -2125,12 +2154,12 @@ def remove_json_prefix(path): if isActive('MCH') and isActive('MID') and isActive('MFT') : addQCPerTF(taskName='MUONTracksMFTTaskQC', needs=[MFTMCHMATCHtask['name'], MCHMIDMATCHtask['name']], - readerCommand='o2-global-track-cluster-reader --track-types "MFT,MCH,MID,MCH-MID,MFT-MCH,MFT-MCH-MID" --cluster-types "MCH,MID,MFT"', + readerCommand='o2-global-track-cluster-reader --track-types "MFT,MCH,MID,MCH-MID,MFT-MCH,MFT-MCH-MID" --cluster-types "MCH,MID,MFT"' + staggMFT, configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mftmchmid-tracks-task.json') elif isActive('MCH') and isActive('MFT') : addQCPerTF(taskName='MCHMFTTaskQC', needs=[MFTMCHMATCHtask['name']], - readerCommand='o2-global-track-cluster-reader --track-types "MCH,MFT,MFT-MCH" --cluster-types "MCH,MFT"', + readerCommand='o2-global-track-cluster-reader --track-types "MCH,MFT,MFT-MCH" --cluster-types "MCH,MFT"' + staggMFT, configFilePath='json://${O2DPG_ROOT}/MC/config/QC/json/mftmch-tracks-task.json') @@ -2147,7 +2176,9 @@ def remove_json_prefix(path): TPCTStask = createTask(name='tpctimeseries_'+str(tf), needs=tpctsneeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='2000', cpu='1') TPCTStask['cmd'] = 'o2-global-track-cluster-reader --disable-mc --cluster-types "FT0,TOF,TPC" --track-types "ITS,TPC,ITS-TPC,ITS-TPC-TOF,ITS-TPC-TRD-TOF"' TPCTStask['cmd'] += ' --primary-vertices ' + TPCTStask['cmd'] += staggITS TPCTStask['cmd'] += ' | o2-tpc-time-series-workflow --enable-unbinned-root-output --sample-unbinned-tsallis --sampling-factor 0.01 ' + TPCTStask['cmd'] += staggITS TPCTStask['cmd'] += putConfigValues() + ' ' + getDPL_global_options(bigshm=True) # could be relaxed or changed once the timerseries worklow is more reactive to input cluster- and track-types addWhenActive("TOF,TPC,FT0", workflow['stages'], TPCTStask) diff --git a/MC/bin/tests/wf_test_pp_staggered.sh b/MC/bin/tests/wf_test_pp_staggered.sh new file mode 100755 index 000000000..6d3b7a0a6 --- /dev/null +++ b/MC/bin/tests/wf_test_pp_staggered.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# +# A pp workflow MC->RECO->AOD with ITS and MFT staggering enabled +# + +# make sure O2DPG + O2 is loaded +[ ! "${O2DPG_ROOT}" ] && echo "Error: This needs O2DPG loaded" && exit 1 +[ ! "${O2_ROOT}" ] && echo "Error: This needs O2 loaded" && exit 1 + + +# ----------- START ACTUAL JOB ----------------------------- + +NSIGEVENTS=${NSIGEVENTS:-5} +SIGPROC=${SIGPROC:-cdiff} +NTIMEFRAMES=${NTIMEFRAMES:-2} +SIMENGINE=${SIMENGINE:-TGeant3} +NWORKERS=${NWORKERS:-1} +SEED=${SEED:-624} +INTERACTIONRATE=${INTERACTIONRATE:-50000} + +# enable ITS and MFT staggering in all consumers +export ALIEN_JDL_ITS_STAGGERED=1 +export ALIEN_JDL_MFT_STAGGERED=1 + +# create workflow +${O2DPG_ROOT}/MC/bin/o2dpg_sim_workflow.py -eCM 5020 -col pp -gen pythia8 -proc ${SIGPROC} -ns ${NSIGEVENTS} \ + -tf ${NTIMEFRAMES} -e ${SIMENGINE} -j ${NWORKERS} -seed ${SEED} \ + --include-analysis -run 310000 -interactionRate ${INTERACTIONRATE} \ + --include-local-qc From da52329b57647cfad60f1676c8cd85ffbc481bb1 Mon Sep 17 00:00:00 2001 From: spucillo <93769017+spucillo@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:43:01 +0200 Subject: [PATCH 108/144] Add ini files for gap-trigger scan (#2505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add ini files for gap-trigger scan * Add generator tests for gap-trigger scan * Renamed files --------- Co-authored-by: Nicolò Jacazio --- ... GeneratorLFCascadesInJets_pp536TeV_gap2.ini} | 3 ++- .../GeneratorLFCascadesInJets_pp536TeV_gap3.ini | 16 ++++++++++++++++ .../GeneratorLFCascadesInJets_pp536TeV_gap4.ini | 16 ++++++++++++++++ .../GeneratorLFCascadesInJets_pp536TeV_gap5.ini | 16 ++++++++++++++++ ...ni => GeneratorLFV0sInJets_pp536TeV_gap2.ini} | 3 ++- .../ini/GeneratorLFV0sInJets_pp536TeV_gap3.ini | 16 ++++++++++++++++ .../ini/GeneratorLFV0sInJets_pp536TeV_gap4.ini | 16 ++++++++++++++++ .../ini/GeneratorLFV0sInJets_pp536TeV_gap5.ini | 16 ++++++++++++++++ 8 files changed, 100 insertions(+), 2 deletions(-) rename MC/config/PWGLF/ini/{GeneratorLFCascadesInJets_pp536TeV.ini => GeneratorLFCascadesInJets_pp536TeV_gap2.ini} (92%) create mode 100644 MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap3.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap4.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap5.ini rename MC/config/PWGLF/ini/{GeneratorLFV0sInJets_pp536TeV.ini => GeneratorLFV0sInJets_pp536TeV_gap2.ini} (93%) create mode 100644 MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap3.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap4.ini create mode 100644 MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap5.ini diff --git a/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV.ini b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap2.ini similarity index 92% rename from MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV.ini rename to MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap2.ini index c24165d49..5bdd49711 100644 --- a/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV.ini +++ b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap2.ini @@ -1,3 +1,4 @@ +#---> GeneratorLFCascadesInJets_pp536TeV ### Cascades in jets - pp collisions at 5.36 TeV [GeneratorExternal] @@ -12,4 +13,4 @@ includePartonEvent=false [DecayerPythia8] # Standard Pythia8 decay configuration -config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg \ No newline at end of file +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap3.ini b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap3.ini new file mode 100644 index 000000000..dbd1d8f33 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap3.ini @@ -0,0 +1,16 @@ +#---> GeneratorLFCascadesInJets_pp536TeV +### Cascades in jets - pp collisions at 5.36 TeV + +[GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one cascade species +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/strangeparticlelist.gun", 3) + +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false + +[DecayerPythia8] +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap4.ini b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap4.ini new file mode 100644 index 000000000..0554bb10a --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap4.ini @@ -0,0 +1,16 @@ +#---> GeneratorLFCascadesInJets_pp536TeV +### Cascades in jets - pp collisions at 5.36 TeV + +[GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one cascade species +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/strangeparticlelist.gun", 4) + +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false + +[DecayerPythia8] +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap5.ini b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap5.ini new file mode 100644 index 000000000..b5672f10b --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFCascadesInJets_pp536TeV_gap5.ini @@ -0,0 +1,16 @@ +#---> GeneratorLFCascadesInJets_pp536TeV +### Cascades in jets - pp collisions at 5.36 TeV + +[GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one cascade species +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/strangeparticlelist.gun", 5) + +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false + +[DecayerPythia8] +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV.ini b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap2.ini similarity index 93% rename from MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV.ini rename to MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap2.ini index ba7f9fbb0..553ad5617 100644 --- a/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV.ini +++ b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap2.ini @@ -1,3 +1,4 @@ +#---> GeneratorLFV0sInJets_pp536TeV ### V0s in jets - pp collisions at 5.36 TeV [GeneratorExternal] @@ -12,4 +13,4 @@ includePartonEvent=false [DecayerPythia8] # Standard Pythia8 decay configuration -config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg \ No newline at end of file +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap3.ini b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap3.ini new file mode 100644 index 000000000..15550b855 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap3.ini @@ -0,0 +1,16 @@ +#---> GeneratorLFV0sInJets_pp536TeV +### V0s in jets - pp collisions at 5.36 TeV + +[GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one V0 particle +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/v0particlelist.gun", 3) + +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false + +[DecayerPythia8] +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap4.ini b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap4.ini new file mode 100644 index 000000000..88766bf6b --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap4.ini @@ -0,0 +1,16 @@ +#---> GeneratorLFV0sInJets_pp536TeV +### V0s in jets - pp collisions at 5.36 TeV + +[GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one V0 particle +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/v0particlelist.gun", 4) + +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false + +[DecayerPythia8] +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg diff --git a/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap5.ini b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap5.ini new file mode 100644 index 000000000..ef89de2c7 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFV0sInJets_pp536TeV_gap5.ini @@ -0,0 +1,16 @@ +#---> GeneratorLFV0sInJets_pp536TeV +### V0s in jets - pp collisions at 5.36 TeV + +[GeneratorExternal] +# Generate jet-biased Pythia8 events and require the presence of one V0 particle +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/v0particlelist.gun", 5) + +[GeneratorPythia8] +# Underlying pp event: HardQCD production with jet-pT bias and rope hadronization +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_jet_ropes_536tev.cfg +includePartonEvent=false + +[DecayerPythia8] +# Standard Pythia8 decay configuration +config[0]=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/decayer/base.cfg From af47d5f971ccc2e23d19a58876ff23502784e6f8 Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Thu, 15 Oct 2026 10:40:47 +0200 Subject: [PATCH 109/144] GenTopo: update paths to el10 --- DATA/README.md | 6 +++--- DATA/tools/epn/gen_topo.sh | 4 ++-- DATA/tools/epn/gen_topo_logged.sh | 2 +- DATA/tools/epn/run.sh | 4 ++-- DATA/tools/parse | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/DATA/README.md b/DATA/README.md index f3533ca26..8b3afcb9e 100644 --- a/DATA/README.md +++ b/DATA/README.md @@ -194,7 +194,7 @@ Resolving deltas: 100% (64/64), done. [drohr@epn245 ~]$ cd O2DPG/DATA/testing/ [drohr@epn245 testing]$ mkdir -p private/drohr [drohr@epn245 testing]$ ls examples/ -example-workflow.sh workflows.desc +example-workflow.sh workflows.desc [drohr@epn245 testing]$ cp examples/* private/drohr/ [drohr@epn245 testing]$ vi private/drohr/workflows.desc [drohr@epn245 testing]$ mv private/drohr/example-workflow.sh private/drohr/my-workflow.sh @@ -227,7 +227,7 @@ export WORKFLOW_PARAMETERS= # Additiona export RECO_NUM_NODES_OVERRIDE=0 # Override the number of EPN compute nodes to use (default is specified in description library file) export NHBPERTF=256 # Number of HBF per TF -/opt/alisw/el9/GenTopo/bin/gen_topo.sh > $HOME/gen_topo_output.xml +/opt/alisw/el10/GenTopo/bin/gen_topo.sh > $HOME/gen_topo_output.xml [drohr@epn245 test]$ ./run.sh Loading ODC/0.36-1 Loading requirement: BASE/1.0 GCC-Toolchain/v10.2.0-alice2-3 fmt/7.1.0-10 FairLogger/v1.9.1-7 zlib/v1.2.8-8 OpenSSL/v1.0.2o-9 libpng/v1.6.34-9 sqlite/v3.15.0-2 libffi/v3.2.1-2 FreeType/v2.10.1-8 Python/v3.6.10-12 Python-modules/1.0-16 boost/v1.75.0-13 ZeroMQ/v4.3.3-6 ofi/v1.7.1-8 asio/v1.19.1-2 asiofi/v0.5.1-2 DDS/3.5.16-5 FairMQ/v1.4.40-4 @@ -277,7 +277,7 @@ export WORKFLOW_PARAMETERS=EVENT_DISPLAY,CTF,GPU # Additiona export RECO_NUM_NODES_OVERRIDE=0 # Override the number of EPN compute nodes to use (default is specified in description library file) export NHBPERTF=256 # Number of HBF per TF -/opt/alisw/el9/GenTopo/bin/gen_topo.sh > $HOME/gen_topo_output.xml +/opt/alisw/el10/GenTopo/bin/gen_topo.sh > $HOME/gen_topo_output.xml [drohr@epn245 test]$ ./run.sh Loading ODC/0.36-1 Loading requirement: BASE/1.0 GCC-Toolchain/v10.2.0-alice2-3 fmt/7.1.0-10 FairLogger/v1.9.1-7 zlib/v1.2.8-8 OpenSSL/v1.0.2o-9 libpng/v1.6.34-9 sqlite/v3.15.0-2 libffi/v3.2.1-2 FreeType/v2.10.1-8 Python/v3.6.10-12 Python-modules/1.0-16 boost/v1.75.0-13 ZeroMQ/v4.3.3-6 ofi/v1.7.1-8 asio/v1.19.1-2 asiofi/v0.5.1-2 DDS/3.5.16-5 FairMQ/v1.4.40-4 diff --git a/DATA/tools/epn/gen_topo.sh b/DATA/tools/epn/gen_topo.sh index 0a90ab1da..5e464027f 100755 --- a/DATA/tools/epn/gen_topo.sh +++ b/DATA/tools/epn/gen_topo.sh @@ -4,7 +4,7 @@ # Author: David Rohr # This script is developed within O2DPG: https://github.com/AliceO2Group/O2DPG/blob/master/DATA/tools/epn/gen_topo.sh -# It is installed as package GenTopo to the updateable RPM path /opt/alisw/el9/GenTopo/bin/ on the EPNs +# It is installed as package GenTopo to the updateable RPM path /opt/alisw/el10/GenTopo/bin/ on the EPNs # The purpose of this script is to separate the topology generation (which is in O2DPG) from the setting of the EPN-related settings # This script contains only the EPN related settings @@ -55,7 +55,7 @@ else fi fi # Run stage 2 of GenTopo, which does the PDP part, still from hardcoded updatable RPM path -/opt/alisw/el9/GenTopo/bin/gen_topo_o2dpg.sh +/opt/alisw/el10/GenTopo/bin/gen_topo_o2dpg.sh if [ $? != 0 ]; then exit 1 fi diff --git a/DATA/tools/epn/gen_topo_logged.sh b/DATA/tools/epn/gen_topo_logged.sh index 7d2afbb37..3e0ce0260 100755 --- a/DATA/tools/epn/gen_topo_logged.sh +++ b/DATA/tools/epn/gen_topo_logged.sh @@ -17,7 +17,7 @@ if [[ ! -z $GEN_TOPO_LOGDATE ]]; then fi STDERRFILE=$(mktemp) -/opt/alisw/el9/GenTopo/bin/gen_topo.sh 2> $STDERRFILE +/opt/alisw/el10/GenTopo/bin/gen_topo.sh 2> $STDERRFILE RETVAL=$? if [[ ! -z $GEN_TOPO_LOGDATE ]]; then diff --git a/DATA/tools/epn/run.sh b/DATA/tools/epn/run.sh index cdd422c0a..48bb688b9 100755 --- a/DATA/tools/epn/run.sh +++ b/DATA/tools/epn/run.sh @@ -35,8 +35,8 @@ if [[ "0$GEN_TOPO_RUN_HOME" == "01" ]]; then [[ -z $O2DPG_ROOT || -z $O2_ROOT ]] && { echo "ERROR: O2 and O2DPG must be in the environment!"; exit 1; } $O2DPG_ROOT/DATA/tools/epn/gen_topo.sh > $OUTPUT_FILE_NAME else - [[ ! -f /opt/alisw/el9/GenTopo/bin/gen_topo.sh ]] && { echo "ERROR: EPN installation of gen_topo.sh missing. Are you trying to run at home? Then please set GEN_TOPO_RUN_HOME=1!"; exit 1; } - /opt/alisw/el9/GenTopo/bin/gen_topo.sh > $OUTPUT_FILE_NAME + [[ ! -f /opt/alisw/el10/GenTopo/bin/gen_topo.sh ]] && { echo "ERROR: EPN installation of gen_topo.sh missing. Are you trying to run at home? Then please set GEN_TOPO_RUN_HOME=1!"; exit 1; } + /opt/alisw/el10/GenTopo/bin/gen_topo.sh > $OUTPUT_FILE_NAME fi if [[ $? == 0 ]]; then echo Generated XML topology $OUTPUT_FILE_NAME diff --git a/DATA/tools/parse b/DATA/tools/parse index cf0335f3f..8c68e55f4 100755 --- a/DATA/tools/parse +++ b/DATA/tools/parse @@ -60,9 +60,9 @@ else: modulecmd = " module purge ; " if 'GEN_TOPO_OVERRIDE_MODULE_DIR' in os.environ: - mod.module('unuse', '/opt/alisw/el9/modulefiles') + mod.module('unuse', '/opt/alisw/el10/modulefiles') mod.module('use', os.environ['GEN_TOPO_OVERRIDE_MODULE_DIR']) - modulecmd += ' module unuse /opt/alisw/el9/modulefiles 2>&1 ; module use ' + os.environ['GEN_TOPO_OVERRIDE_MODULE_DIR'] + ' 2>&1 ; ' + modulecmd += ' module unuse /opt/alisw/el10/modulefiles 2>&1 ; module use ' + os.environ['GEN_TOPO_OVERRIDE_MODULE_DIR'] + ' 2>&1 ; ' f = open(sys.argv[1], 'r') for line in f: From bf4b0b5073a547d9935e93cf6340f5054badd94e Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Mon, 19 Oct 2026 16:55:57 +0200 Subject: [PATCH 110/144] Update PYTHONPATH in gen_topo.sh --- DATA/tools/epn/gen_topo.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DATA/tools/epn/gen_topo.sh b/DATA/tools/epn/gen_topo.sh index 5e464027f..3785fa5ac 100755 --- a/DATA/tools/epn/gen_topo.sh +++ b/DATA/tools/epn/gen_topo.sh @@ -31,12 +31,12 @@ if [[ "0$GEN_TOPO_ONTHEFLY" == "01" ]]; then export SHM_MANAGER_SHMID=1 ;fi # Command for topology merging if [[ -z "$GEN_TOPO_ODC_EPN_TOPO_CMD" ]]; then - export GEN_TOPO_ODC_EPN_TOPO_CMD='env - PYTHONPATH+=/usr/local/lib/python3.9/site-packages:/usr/local/lib64/python3.9/site-packages /usr/local/bin/epn-topo-merger' + export GEN_TOPO_ODC_EPN_TOPO_CMD='env - PYTHONPATH+=/usr/local/lib/python3.12/site-packages:/usr/local/lib64/python3.12/site-packages /usr/local/bin/epn-topo-merger' fi # Command for postprocessing of topology generation after topology caching if [[ -z "$GEN_TOPO_ODC_EPN_TOPO_POST_CACHING_CMD" ]]; then - export GEN_TOPO_ODC_EPN_TOPO_POST_CACHING_CMD='env - PYTHONPATH+=/usr/local/lib/python3.9/site-packages:/usr/local/lib64/python3.9/site-packages /usr/local/bin/epn-topo-alloc' + export GEN_TOPO_ODC_EPN_TOPO_POST_CACHING_CMD='env - PYTHONPATH+=/usr/local/lib/python3.12/site-packages:/usr/local/lib64/python3.12/site-packages /usr/local/bin/epn-topo-alloc' fi # Extra arguments for topology merger From ae342a14dafb0c66c534b80ae176c51dd4a3a2cd Mon Sep 17 00:00:00 2001 From: rbailhac Date: Tue, 20 Oct 2026 12:05:33 +0200 Subject: [PATCH 111/144] Add Pb--Pb collisions at 5.02 TeV (#2513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Pb--Pb collisions at 5.02 TeV * Update pythia8_PbPb_502tev.ini for new configuration * Delete MC/config/ALICE3/ini/tests/pythia8_PbPb_502tev.C --------- Co-authored-by: Nicolò Jacazio --- MC/config/ALICE3/ini/pythia8_PbPb_502tev.ini | 10 ++++++++++ .../pythia8/generator/pythia8_PbPb_502tev.cfg | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 MC/config/ALICE3/ini/pythia8_PbPb_502tev.ini create mode 100644 MC/config/ALICE3/pythia8/generator/pythia8_PbPb_502tev.cfg diff --git a/MC/config/ALICE3/ini/pythia8_PbPb_502tev.ini b/MC/config/ALICE3/ini/pythia8_PbPb_502tev.ini new file mode 100644 index 000000000..8a8641bdf --- /dev/null +++ b/MC/config/ALICE3/ini/pythia8_PbPb_502tev.ini @@ -0,0 +1,10 @@ +#---> pythia8_PbPb_536tev +[Diamond] +width[2]=6.0 + +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator_pythia8_ALICE3.C +funcName=generator_pythia8_ALICE3() + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_502tev.cfg diff --git a/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_502tev.cfg b/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_502tev.cfg new file mode 100644 index 000000000..5d0843644 --- /dev/null +++ b/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_502tev.cfg @@ -0,0 +1,17 @@ +### Specify beams +Beams:idA = 1000822080 +Beams:idB = 1000822080 +Beams:eCM = 5020.0 ### energy + +Beams:frameType = 1 +ParticleDecays:limitTau0 = on +ParticleDecays:tau0Max = 10. ### match alice: 1cm/c = 10.0mm/c + +### Initialize the Angantyr model to fit the total and semi-includive +### cross sections in Pythia within some tolerance. +HeavyIon:SigFitErr = 0.02,0.02,0.1,0.05,0.05,0.0,0.1,0.0 + +### These parameters are typicall suitable for sqrt(S_NN)=5TeV +HeavyIon:SigFitDefPar = 17.24,2.15,0.33,0.0,0.0,0.0,0.0,0.0 + +Random:setSeed = on From 4aebb445f19ebae3641ff8dc8a929ae47c432793 Mon Sep 17 00:00:00 2001 From: Matthias Kleiner <48915672+matthias-kleiner@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:57:40 +0200 Subject: [PATCH 112/144] MC: store the MC truth in the TPC residuals whenever MC labels are on (#2503) With ALIEN_JDL_DOTPCRESIDUALEXTRACTION=1 the residual extraction and aggregation tasks run with --enable-mc unless --no-mc-labels is given, since the MC truth needs the MC labels of the ITS, TPC and ITS-TPC tracks. It stores the MC truth of the residual tracks as branch trkMC of the trackData tree. The option is passed only if the O2 version used for the reconstruction provides it. Co-authored-by: Claude Opus 5.5 --- MC/bin/o2dpg_sim_workflow.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 1dbd25b7c..24bf25aba 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -1892,6 +1892,9 @@ def getDigiTaskName(det): '--tracking-sources-map-extraction', default_value='ITS-TPC')) + # store the MC truth of the residual tracks (branch trkMC of the trackData tree) whenever MC labels are available + scdcalib_mc_option = '' if args.no_mc_labels else option_if_available('o2-tpc-scdcalib-interpolation-workflow', '--enable-mc', envfile=async_envfile) + SCDCALIBtask = createTask(name='scdcalib_'+str(tf), needs=[PVFINDERtask['name']], tf=tf, cwd=timeframeworkdir, lab=["CALIB"], mem='4000') SCDCALIBtask['cmd'] = task_finalizer( [ '${O2_ROOT}/bin/o2-tpc-scdcalib-interpolation-workflow', @@ -1901,7 +1904,8 @@ def getDigiTaskName(det): '--tracking-sources ' + scdcalib_track_sources, '--tracking-sources-map-extraction ' + scdcalib_track_extraction, '--sec-per-slot 1 ', - '--send-track-data' + '--send-track-data', + scdcalib_mc_option ]) workflow['stages'].append(SCDCALIBtask) @@ -1924,7 +1928,8 @@ def getDigiTaskName(det): '--enable-track-input', '--output-dir ./', '--output-type ' + scdaggreg_outputtype, - '--meta-output-dir /dev/null' + '--meta-output-dir /dev/null', + scdcalib_mc_option ]) workflow['stages'].append(SCDAGGREGtask) From 0567fe5d94d2e94265efac42f63623089ca81211 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Tue, 16 Jun 2026 10:22:34 +0200 Subject: [PATCH 113/144] Add --inputfile to pre-stage extra files via JDL InputFile --- GRID/utils/grid_submit.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/GRID/utils/grid_submit.sh b/GRID/utils/grid_submit.sh index dcf6f78da..5220ff5c3 100755 --- a/GRID/utils/grid_submit.sh +++ b/GRID/utils/grid_submit.sh @@ -219,6 +219,7 @@ while [ $# -gt 0 ] ; do --wait-any) WAITFORALIENANY=ON; WAITFORALIEN=ON; shift 1 ;; #wait for any good==done alien jobs to return --outputspec) OUTPUTSPEC=$2; shift 2 ;; #provide comma separate list of JDL file specs to be put as part of JDL Output field (example '"*.log@disk=1","*.root@disk=2"') --split-on-collection) DATACOLLECTION=$2; shift 2 ;; # this will split the jobs on InputDataCollection and "file" mode + --inputfile) INPUTFILES+=("$2"); shift 2 ;; # extra file to pre-stage via JDL InputFile (repeatable). Accepts a bare LFN or "LF:". Staged by AliEn before the job starts, so it is NOT charged to job CPU/walltime (unlike an in-job 'alien.py cp'). -h) Usage ; exit ;; --help) Usage ; exit ;; --fetch-output) FETCHOUTPUT=ON; shift 1 ;; # if to fetch all JOB output locally (to make this job as if it ran locally); only works when we block until all JOBS EXIT @@ -366,12 +367,20 @@ if [[ "${IS_ALIEN_JOB_SUBMITTER}" ]]; then if [ "${DATACOLLECTION}" ]; then SPLITMODE="file" fi + + # Assemble the JDL InputFile list: always the job script, plus any extra files + # requested via --inputfile (each pre-staged by AliEn before the job starts). + INPUTFILE_ENTRIES="${QUOT}LF:${MY_JOBWORKDIR}/alien_jobscript.sh${QUOT}" + for _extra in "${INPUTFILES[@]}"; do + [[ "${_extra}" == LF:* ]] || _extra="LF:${_extra}" + INPUTFILE_ENTRIES="${INPUTFILE_ENTRIES}, ${QUOT}${_extra}${QUOT}" + done # ---- Generate JDL ---------------- # TODO: Make this configurable or read from a preamble section in the jobfile cat > "${MY_JOBNAMEDATE}.jdl" < Date: Mon, 19 Oct 2026 18:33:53 +0200 Subject: [PATCH 114/144] grid_submit: wait until no subjob is active This fixes a problem in the --wait and --wait-any modes of grid_submit.sh. - AliEn can briefly report a masterjob as DONE while one of its subjobs in ERROR_A is put back to WAITING. - --wait read only the masterjob status and returned at that moment, although the job was then retried. - Both modes now finish only when no subjob is still active, with ERROR_A counted as active. - A failed or empty status query also counts as active. Co-Authored-By: Claude Opus 5.5 --- GRID/utils/grid_submit.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/GRID/utils/grid_submit.sh b/GRID/utils/grid_submit.sh index 5220ff5c3..5d11e1a2b 100755 --- a/GRID/utils/grid_submit.sh +++ b/GRID/utils/grid_submit.sh @@ -454,6 +454,13 @@ EOF fi + # true while a subjob is not final; ERROR_A (resubmitted) and a failed query count as active + subjobs_active() { + jq -e '(.results | length) == 0 or (.results | any(.status | IN("INSERTING", "SPLITTING", "WAITING", "ASSIGNED", "STARTED", "RUNNING", "SAVING", "SAVED", "ERROR_A")))' \ + <<<"$1" >/dev/null + [ $? -ne 1 ] + } + # wait here until all ALIEN jobs have returned spin[3]="-" spin[2]="/" @@ -488,12 +495,16 @@ EOF else # check if there are still jobs running/waiting; if not also finish # this could happen when all jobs are zombies (in which case we also finish) - if ! jq -e '.results | any(.status == "WAITING" or .status == "RUNNING" or .status == "SAVING" or .status == "INSERTING")' \ - <<<"${DETAILED_STATUS_JSON}" >/dev/null; then + if ! subjobs_active "${DETAILED_STATUS_JSON}"; then JOBSTATUS="D" # some job finished successfully echo "No remaining good job" fi fi + elif [ "${JOBSTATUS}" == "D" ]; then + # the masterjob can briefly show DONE while an ERROR_A subjob is resubmitted + if subjobs_active "$(ALIENPY_JSON=true alien.py ps -a -m "${MY_JOBID}")"; then + JOBSTATUS="S" + fi fi if [ "${JOBSTATUS}" == "D" ]; then From 2034a92e64a86875de89df4e5c73ce8579349290 Mon Sep 17 00:00:00 2001 From: ehellbar Date: Wed, 21 Oct 2026 10:36:16 +0200 Subject: [PATCH 115/144] getCCDBTimeMachineTimestamp.py: publish the NotAfter estimate to CCDB (#2509) This allows to store the estimated NotAfter timestamp of a period and pass in CCDB as a `CCDBTimeMachineTS` object, so that the estimate is done once per pass and not in every job. - The script first queries CCDB for an existing object for the Period and Pass, and only estimates from the catalogue when none is found. - `--publish-to-ccdb` uploads the estimate, `--force-overwrite` replaces an existing object and `--skip-ccdb-query` skips the lookup. - The CCDB cross-check now uses the run start timestamp. - Only the value goes to stdout; all log lines go to stderr. Nothing uses the script in production yet. https://its.cern.ch/jira/browse/O2-6543 --- GRID/utils/getCCDBTimeMachineTimestamp.py | 268 ++++++++++++++++++---- 1 file changed, 224 insertions(+), 44 deletions(-) diff --git a/GRID/utils/getCCDBTimeMachineTimestamp.py b/GRID/utils/getCCDBTimeMachineTimestamp.py index 3a90db77a..f5b4619e6 100755 --- a/GRID/utils/getCCDBTimeMachineTimestamp.py +++ b/GRID/utils/getCCDBTimeMachineTimestamp.py @@ -13,17 +13,40 @@ # started 25.08.2026; Sandro Wenzel import argparse +import dataclasses +from dataclasses import dataclass, asdict, fields import json import os +import requests import statistics import subprocess import sys import urllib.request from datetime import datetime, timezone +# hack to find the script with helper functions +o2dpg_root = os.environ.get("O2DPG_ROOT") +if o2dpg_root is None: + raise EnvironmentError("O2DPG_ROOT is not set in the environment.") +mc_prodinfo_path = os.path.abspath(os.path.join(o2dpg_root, "MC", "prodinfo")) +sys.path.append(mc_prodinfo_path) +# FIXME: these should probably be moved to a common script +from mcprodinfo_ccdb_upload import extract_metadata_blocks_from_CCDB + CCDB_HOST = "http://alice-ccdb.cern.ch" +@dataclass(frozen=True) +class CCDBTimeMachineTS: + """ + struct for CCDB time machine timestamp + """ + LPMProductionTag: str + Period: str + Pass: str + NotAfter: int + + def alien_json(*args): """Run alien.py with -json and return the parsed 'results' list.""" cmd = ["alien.py", "-json"] + list(args) @@ -68,11 +91,7 @@ def human(ts_ms): return datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") -def ccdb_cross_check(path, not_after, timeout=15): - """Report which object a correctly time-machine-pinned query at not_after - would actually resolve to, and flag if CCDB has since been updated for the - same validity window. Never raises; returns None on any failure.""" - url = f"{CCDB_HOST}/browse/{path}/{not_after}" +def get_ccdb_objects(url, timeout = 15): req = urllib.request.Request(url, headers={"Accept": "application/json"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: @@ -81,14 +100,35 @@ def ccdb_cross_check(path, not_after, timeout=15): return {"error": str(exc)} objects = data.get("objects", []) + return objects + + +def get_run_start_tf(run): + run_info_url = f"{CCDB_HOST}/browse/RCT/Info/RunInformation/{run}" + objects = get_ccdb_objects(run_info_url) + + return objects[0]["STF"] + + +def ccdb_cross_check(run, path, not_after, timeout=15): + """Report which object a correctly time-machine-pinned query at not_after + would actually resolve to, and flag if CCDB has since been updated for the + same validity window. Never raises; returns None on any failure.""" + timestamp = get_run_start_tf(run) + url = f"{CCDB_HOST}/browse/{path}/{timestamp}" + result = {"timestamp": timestamp} + + objects = get_ccdb_objects(url) if not objects: - return {"error": "no CCDB object covers this instant"} + return {**result, "error": "no CCDB object covers this instant"} as_of = [o for o in objects if o.get("Created", 0) <= not_after] newest = max(objects, key=lambda o: o.get("Created", 0)) - pinned = max(as_of, key=lambda o: o.get("Created", 0)) if as_of else None - outdated = pinned is not None and newest["Created"] > pinned["Created"] - return {"newest": newest, "as_of": pinned, "outdated": outdated} + result.update({"newest": newest, "as_of": None, "outdated": False}) + if as_of: + result["as_of"] = max(as_of, key=lambda o: o.get("Created", 0)) + result["outdated"] = newest["Created"] > result["as_of"]["Created"] + return result def describe_object(o): @@ -96,40 +136,36 @@ def describe_object(o): f"comment=\"{o.get('comment', '')}\"") -def main(): - parser = argparse.ArgumentParser( - description="Estimate a CCDB TimeMachine not-after timestamp for a run+pass, " - "from AliEn catalog timestamps of that pass's own reco output.") - parser.add_argument("--year", default=os.environ.get("ALIEN_JDL_LPMANCHORYEAR"), - help="data-taking year, e.g. 2023 (default: $ALIEN_JDL_LPMANCHORYEAR)") - parser.add_argument("--period", default=os.environ.get("ALIEN_JDL_LPMANCHORPRODUCTION"), - help="period name, e.g. LHC23s (default: $ALIEN_JDL_LPMANCHORPRODUCTION)") - parser.add_argument("--run", default=os.environ.get("ALIEN_JDL_LPMANCHORRUN"), - help="run number (default: $ALIEN_JDL_LPMANCHORRUN)") - parser.add_argument("--pass", dest="passname", default=os.environ.get("ALIEN_JDL_LPMANCHORPASSNAME"), - help="pass name, e.g. apass4 (default: $ALIEN_JDL_LPMANCHORPASSNAME)") - parser.add_argument("--samples", type=int, default=9, - help="number of per-timeframe output files to sample (default: 9)") - parser.add_argument("--ccdb-check-path", default="TPC/Calib/CorrectionMapV2", - help="CCDB path to cross-check the estimate against (default: TPC/Calib/CorrectionMapV2)") - parser.add_argument("--no-ccdb-check", action="store_true", - help="skip the CCDB cross-check") - args = parser.parse_args() +def make_cert_paths(cert_dir="/tmp"): + key_path = os.environ.get("JALIEN_TOKEN_KEY") + cert_path = os.environ.get("JALIEN_TOKEN_CERT") + if key_path == None and cert_path == None: + uid = os.getuid() + cert_path = os.path.join(cert_dir, f"tokencert_{uid}.pem") + key_path = os.path.join(cert_dir, f"tokenkey_{uid}.pem") + + return cert_path, key_path - missing = [n for n in ("year", "period", "run", "passname") if not getattr(args, n)] - if missing: - parser.error("missing required value(s): " + ", ".join(missing)) - pass_dir = f"/alice/data/{args.year}/{args.period}/{args.run}/{args.passname}/" +def get_NotAfter(year, period, run, passname, samples=9, ccdb_check_path="TPC/Calib/CorrectionMapV2", ccdb_check=True): + """ + Estimates the CCDBTimeMachine NotAfter timestamp from the output of the production of a given period/run/pass. + Cross-checks the estimated NotAfter timestamp vs the creation time of a CCDB object which is currently + valid for the given run. + + Returns: + int: CCDBTimeMachineTS NotAfter timestamp + """ + pass_dir = f"/alice/data/{year}/{period}/{run}/{passname}/" print(f"[getCCDBTimeMachineTimestamp] Looking for TF output under {pass_dir}", file=sys.stderr) - matches = find_tf_output_files(pass_dir, args.run) + matches = find_tf_output_files(pass_dir, run) if not matches: - sys.exit(f"[getCCDBTimeMachineTimestamp] No o2_ctf_run{int(args.run):08d}_orbit* output found " - f"under {pass_dir}. Check year/period/run/pass, or this run+pass may not use this " - f"catalog layout. Refusing to guess a timestamp.") + sys.exit(f"[getCCDBTimeMachineTimestamp] No o2_ctf_run{int(run):08d}_orbit* output found " + f"under {pass_dir}. Check year/period/run/pass, or this run+pass may not use this " + f"catalog layout. Refusing to guess a timestamp.") - sampled = sample_evenly(matches, args.samples) + sampled = sample_evenly(matches, samples) ctimes = [] for lfn in sampled: ctime = stat_ctime(lfn) @@ -139,33 +175,177 @@ def main(): if len(ctimes) < 2: sys.exit(f"[getCCDBTimeMachineTimestamp] Only got {len(ctimes)} usable timestamp(s) out of " - f"{len(sampled)} sampled files ({len(matches)} found in total); refusing to guess.") + f"{len(sampled)} sampled files ({len(matches)} found in total); refusing to guess.") not_after = int(statistics.median(ctimes)) spread_h = (max(ctimes) - min(ctimes)) / 3.6e6 print(f"[getCCDBTimeMachineTimestamp] {len(ctimes)} samples out of {len(matches)} TF outputs found; " f"span {human(min(ctimes))} .. {human(max(ctimes))} ({spread_h:.1f} h)", file=sys.stderr) - print(f"[getCCDBTimeMachineTimestamp] Recommended not-after: {not_after} ({human(not_after)})", + print(f"[getCCDBTimeMachineTimestamp] Recommended NotAfter: {not_after} ({human(not_after)})", file=sys.stderr) - if not args.no_ccdb_check: - check = ccdb_cross_check(args.ccdb_check_path, not_after) + if ccdb_check: + check = ccdb_cross_check(run, ccdb_check_path, not_after) + check_timestamp = f"/{check['timestamp']}" if check else "" if check is None or "error" in check: reason = check["error"] if check else "unknown error" - print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {args.ccdb_check_path} skipped: {reason}", + print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {ccdb_check_path}{check_timestamp} skipped: {reason}", file=sys.stderr) elif check["as_of"] is None: - print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {args.ccdb_check_path}: no object " + print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {ccdb_check_path}{check_timestamp}: no object " f"existed yet at the recommended timestamp (earliest is {describe_object(check['newest'])})", file=sys.stderr) else: - print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {args.ccdb_check_path}: as-of object is " + print(f"[getCCDBTimeMachineTimestamp] CCDB cross-check on {ccdb_check_path}{check_timestamp}: as-of object is " f"{describe_object(check['as_of'])}", file=sys.stderr) if check["outdated"]: print(f"[getCCDBTimeMachineTimestamp] note: CCDB's current object for this window is newer " f"({describe_object(check['newest'])}) - it postdates this pass's processing and was " f"not what this run actually saw.", file=sys.stderr) + return not_after + + +def query_CCDBTimeMachineTS(base_url, user, period, passname): + """ + Queries CCDBTimeMachineTS from CCDB for a given period and pass. + + Returns: + obtained NotAfter timestamp (int) or None + """ + # Construct user ID-specific cert and key paths + cert_path, key_path = make_cert_paths() + + # Build full URL + query = f"1/Period={period}/Pass={passname}" + user_path = 'Users/' + user[0] + '/' + user + url = f"{base_url}/browse/{user_path}/CCDBTimeMachineTS/{query}" + + print(f"[getCCDBTimeMachineTimestamp] Checking if CCDBTimeMachineTS already exists: {url}", file=sys.stderr) + + response = requests.get(url, cert=(cert_path, key_path), verify=False) + if response.status_code == 200: + meta = extract_metadata_blocks_from_CCDB(response.content.decode('utf-8')) + if (len(meta) > 0): + def filter_known_fields(cls, data: dict) -> dict: + valid_keys = {f.name for f in fields(cls)} + return {k: v for k, v in data.items() if k in valid_keys} + + clean_meta = filter_known_fields(CCDBTimeMachineTS, meta[0]) + print(f"[getCCDBTimeMachineTimestamp] Found object for {clean_meta['Period']}, Pass: {clean_meta['Pass']}, " + f"NotAfter: {clean_meta['NotAfter']} ({human(clean_meta['NotAfter'])}), uploaded by LPMProductionTag {clean_meta['LPMProductionTag']}", + file=sys.stderr) + return clean_meta['NotAfter'] + + return None + + +def upload_CCDBTimeMachineTS(base_url, user, keys): + """ + Uploads an empty .dat file as CCDBTimeMachineTS with the corresponding meta data provided by `keys` + + Returns: + Response object from the POST request + """ + # Create an empty file + empty_file = "empty.dat" + with open(empty_file, "w") as f: + f.write("0") + + # Construct user ID-specific cert and key paths + cert_path, key_path = make_cert_paths() + + # Build full URL + query = "/".join(f"{k}={v}" for k, v in keys.items()) + user_path = 'Users/' + user[0] + '/' + user + start = 1 + stop = start + 1 + url = f"{base_url}/{user_path}/CCDBTimeMachineTS/{start}/{stop}/{query}" + + print(f"[getCCDBTimeMachineTimestamp] Uploading CCDBTimeMachineTS: {url}", file=sys.stderr) + + # Prepare request + with open(empty_file, 'rb') as f: + files = {'blob': f} + response = requests.post(url, files=files, cert=(cert_path, key_path), verify=False) + + os.remove(empty_file) + + if response.status_code != 201: + print(f"[getCCDBTimeMachineTimestamp] Upload failed, response: {response.status_code}", file=sys.stderr) + + +def get_CCDBTimeMachineTS(production_tag, year, period, run, passname, + samples=9, ccdb_check_path="TPC/Calib/CorrectionMapV2", ccdb_check=True, + ccdb_url="https://alice-ccdb.cern.ch", username="aliprod", + force_overwrite=False, publish_to_ccdb=True, skip_ccdb_query=False): + """ + Makes a query to CCDB for a CCDBTimeMachineTS for given period and pass. + If an object is found, the NotAfter timestamp is returned. + If no object is found, the NotAfter timestamp is estimated from the reco output of the given period/run/pass. + In case an object is found, a new upload can be forced by setting the argument `force_overwrite=True` + + Returns: + int: Either the queried or the estimated NotAfter timestamp + """ + if not skip_ccdb_query: + not_after = query_CCDBTimeMachineTS(ccdb_url, username, period, passname) + else: + not_after = None + + if not_after is not None and force_overwrite == True: + print("[getCCDBTimeMachineTimestamp] Existing object will be overwritten", file=sys.stderr) + + if not_after == None or force_overwrite == True: + print("[getCCDBTimeMachineTimestamp] Estimating CCDBTimeMachineTS for " + f"LPMProductionTag: {production_tag}, Period: {period}, Pass: {passname}", file=sys.stderr) + not_after = get_NotAfter(year, period, run, passname, samples, ccdb_check_path, ccdb_check) + time_machine_ts = CCDBTimeMachineTS(production_tag, period, passname, not_after) + if publish_to_ccdb: + upload_CCDBTimeMachineTS(ccdb_url, username, dataclasses.asdict(time_machine_ts)) + + return not_after + + +def main(): + parser = argparse.ArgumentParser( + description="Estimate a CCDB TimeMachine not-after timestamp for a run+pass, " + "from AliEn catalog timestamps of that pass's own reco output.") + parser.add_argument("--production-tag", default=os.environ.get("ALIEN_JDL_LPMPRODUCTIONTAG"), + help="production tag to mark which production uploads an object (default: $ALIEN_JDL_LPMPRODUCTIONTAG)") + parser.add_argument("--year", default=os.environ.get("ALIEN_JDL_LPMANCHORYEAR"), + help="data-taking year, e.g. 2023 (default: $ALIEN_JDL_LPMANCHORYEAR)") + parser.add_argument("--period", default=os.environ.get("ALIEN_JDL_LPMANCHORPRODUCTION"), + help="period name, e.g. LHC23s (default: $ALIEN_JDL_LPMANCHORPRODUCTION)") + parser.add_argument("--run", default=os.environ.get("ALIEN_JDL_LPMANCHORRUN"), + help="run number (default: $ALIEN_JDL_LPMANCHORRUN)") + parser.add_argument("--pass", dest="passname", default=os.environ.get("ALIEN_JDL_LPMANCHORPASSNAME"), + help="pass name, e.g. apass4 (default: $ALIEN_JDL_LPMANCHORPASSNAME)") + parser.add_argument("--samples", type=int, default=9, + help="number of per-timeframe output files to sample (default: 9)") + parser.add_argument("--ccdb-check-path", default="TPC/Calib/CorrectionMapV2", + help="CCDB path to cross-check the estimate against (default: TPC/Calib/CorrectionMapV2)") + parser.add_argument("--no-ccdb-check", action="store_true", + help="skip the CCDB cross-check after calculating the NotAfter") + parser.add_argument("--publish-to-ccdb", action="store_true", + help="upload the CCDBTimeMachineTS object to the CCDB") + parser.add_argument("--skip-ccdb-query", action="store_true", + help="skip CCDB query for existing objects while publishing") + parser.add_argument("--force-overwrite", action="store_true", + help="overwrite the current CCDBTimeMachineTS object in the CCDB if one exists") + parser.add_argument("--ccdb-url", default="https://alice-ccdb.cern.ch", + help="ccdb host for the upload") + parser.add_argument("--username", default="aliprod", + help="username to be used for the upload to CCDB") + args = parser.parse_args() + + missing = [n for n in ("year", "period", "run", "passname") if not getattr(args, n)] + if missing: + parser.error("missing required value(s): " + ", ".join(missing)) + + not_after = get_CCDBTimeMachineTS(args.production_tag, args.year, args.period, args.run, args.passname, + args.samples, args.ccdb_check_path, not args.no_ccdb_check, args.ccdb_url, args.username, + args.force_overwrite, args.publish_to_ccdb, args.skip_ccdb_query) print(not_after) From 46900edf8758560c347a9159455deb3d4527d3ed Mon Sep 17 00:00:00 2001 From: spucillo <93769017+spucillo@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:19:07 +0200 Subject: [PATCH 116/144] Strangeness gap-triggered MC for OO collisions (#2516) * Strangeness gap-triggered MC for OO collisions * Fix OO strangeness generator test symlink * Update GeneratorLFStrangenessTriggered_OO.ini * Update GeneratorLFStrangenessTriggered_OO.ini * Update GeneratorLFStrangenessTriggered_OO.ini --- MC/config/PWGLF/ini/GeneratorLFStrangenessTriggered_OO.ini | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLFStrangenessTriggered_OO.ini diff --git a/MC/config/PWGLF/ini/GeneratorLFStrangenessTriggered_OO.ini b/MC/config/PWGLF/ini/GeneratorLFStrangenessTriggered_OO.ini new file mode 100644 index 000000000..580a3a640 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLFStrangenessTriggered_OO.ini @@ -0,0 +1,5 @@ +#---> GeneratorLFStrangenessTriggered +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_LF.C +funcName=generateLFTriggered("${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/strangeparticlelist.gun", 4, "${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_OO_536.cfg", "${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_OO_536.cfg") + From fc9fea6e5c00d464232abdce25a96560959d55c9 Mon Sep 17 00:00:00 2001 From: Matthias Kleiner <48915672+matthias-kleiner@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:47:40 +0200 Subject: [PATCH 117/144] TPC residuals: JDL hook for extra scdcalib keys (#2517) The residual extraction's track selection (minPtNoOuterPoint, minTOFTRDPVContributors, ITS / TPC cluster cuts, ...) is hard-coded in setenv_extra.sh and cannot be changed from the JDL; in anchored MC the anchored configuration also overrides keys given on the command line. ALIEN_JDL_TPCRESIDUAL_EXTRACONFIG is appended after the defaults, so its keys win (later keys override earlier ones in configKeyValues). Example, keeping all tracks and clusters (needs O2 with scdcalib.keepClustersOnPropFail, >= 2026-10-07; unknown keys are fatal): scdcalib.keepClustersOnPropFail=true;scdcalib.clampTgSlp=true;scdcalib.minPtNoOuterPoint=0;scdcalib.minTOFTRDPVContributors=0 Unset: the configuration is unchanged. Co-authored-by: Claude Opus 5.5 --- DATA/production/configurations/asyncReco/setenv_extra.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index 78f6885c1..e3ee7a7d5 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -734,6 +734,10 @@ if [[ $ADD_CALIB == "1" ]]; then export CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=";scdcalib.maxTracksPerCalibSlot=$ALIEN_JDL_TPCRESIDUALMAXTRACKSPERSLOT;scdcalib.additionalTracksMap=$ALIEN_JDL_TPCRESIDUALADDTRACKSMAP;scdcalib.minPtNoOuterPoint=0.2;scdcalib.maxQ2Pt=5;scdcalib.minITSNClsNoOuterPoint=6;scdcalib.minITSNCls=4;scdcalib.minTPCNClsNoOuterPoint=50;scdcalib.minTOFTRDPVContributors=2;" CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=$([[ "$ALIEN_JDL_TPCRESIDUAL_WRITEUNFILTERED" == "1" ]] && printf "scdcalib.writeUnfiltered=true;" || printf "scdcalib.writeUnfiltered=false;") CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=$([[ "$ALIEN_JDL_TPCRESIDUAL_KEEPREJECTEDRES" == "1" ]] && printf "scdcalib.keepRejectedResiduals=true;" || printf "scdcalib.keepRejectedResiduals=false;") + # additional scdcalib keys from the JDL, appended last so that they override the defaults above + if [[ -n "$ALIEN_JDL_TPCRESIDUAL_EXTRACONFIG" ]]; then + CONFIG_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=";$ALIEN_JDL_TPCRESIDUAL_EXTRACONFIG;" + fi export ARGS_EXTRA_PROCESS_o2_tpc_scdcalib_interpolation_workflow+=" --tracking-sources-map-extraction $ALIEN_JDL_TPCRESIDUALTRKSOURCESMAPEXTRACTION" if [[ ! $DO_TPC_RESIDUAL_EXTRACTION =~ "NOEXTCLROAD" ]]; then From 73f585d44c00d97876fc1a244c1b8526b719647a Mon Sep 17 00:00:00 2001 From: Rashi gupta <167059733+rashigupt@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:22:51 +0530 Subject: [PATCH 118/144] =?UTF-8?q?PYTHIA8=20Box=20Generator:=20Minimum-Bi?= =?UTF-8?q?as=20Events=20with=20Injected=20=CF=80=E2=81=B0/=CE=B7=20Decays?= =?UTF-8?q?=20(#2504)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create the Non-HFE–enhanced dataset. We will produce a Non-HFE–enhanced dataset by generating Monte Carlo samples with increased π⁰ and η production, to improve the statistical precision of the non-HFE efficiency measurement. * Create the Non-HFE–enhanced dataset We will produce a Non-HFE–enhanced dataset by generating Monte Carlo samples with increased π⁰ and η production, to improve the statistical precision of the non-HFE efficiency measurement. * Create the Non-HFE–enhanced dataset We will produce a Non-HFE–enhanced dataset by generating Monte Carlo samples with increased π⁰ and η production, to improve the statistical precision of the non-HFE efficiency measurement. * Update and rename GeneratorHF_Non_Hfe_enhance.ini to GeneratorHF_Non_Hfe.ini Rename File and add nonHfe.c file * Create generator_pythia8_gaptriggered_nonhfe.C Add generator_pythia8_gaptriggered_nonhfe.C file for enance pion and eta * Update generator_pythia8_gaptriggered_nonhfe.C * remove build error * Update generator_pythia8_gaptriggered_nonhfe.C * Correct name * correct name * remove build error * remove e and add pion , eta ID * Remove build error * Update generator_pythia8_gaptriggered_nonhfe.C * Update GeneratorHF_Non_Hfe.ini * Replace 1/2 to 1/3 * ncrease the tolerance to 10% * Update and rename MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_nonhfe.C to MC/config/PWGHF/trigger/selectNonHfe.C remove gaptrigger and add trigger selection * Update GeneratorHF_Non_Hfe.ini Change .c file * Update pythia8_NonHfe.cfg Remove Heavy flavour decay off * Update GeneratorHF_Non_Hfe.C Remove quark level information * Update GeneratorHF_Non_Hfe.ini * Add correctly pDg cut * Update GeneratorHF_Non_Hfe.ini * usetriggerexternal C * Rename MC/config/PWGHF/trigger/selectNonHfe.C to MC/config/PWGHF/external/generator/selectNonHfe.C change path * Update GeneratorHF_Non_Hfe.ini change filepath * Update GeneratorHF_Non_Hfe.ini * Rename MC/config/PWGHF/external/generator/selectNonHfe.C to MC/config/PWGHF/trigger/selectNonHfe.C * Update GeneratorHF_Non_Hfe.ini * Use triggerexternal * use configuration file * Create GeneratorHF_Non_Hfe.json * Update GeneratorHF_Non_Hfe.C * Rename MC/config/PWGHF/ini/hybrid/GeneratorHF_Non_Hfe.json to MC/config/PWGHF/hybrid/GeneratorHF_Non_Hfe.json * Rename MC/config/PWGHF/ini/hybrid/GeneratorHF_Non_Hfe.ini to MC/config/PWGHF/hybrid/GeneratorHF_Non_Hfe.ini * Rename MC/config/PWGHF/ini/tests/GeneratorHF_Non_Hfe.C to MC/config/PWGHF/trigger/GeneratorHF_Non_Hfe.C * Rename GeneratorHF_Non_Hfe.C to GeneratorHF_Non_Hfe.C * Rename MC/config/PWGHF/hybrid/GeneratorHF_Non_Hfe.C to MC/config/PWGHF/ini/tests/GeneratorHF_Non_Hfe.C * Rename GeneratorHF_Non_Hfe.ini to GeneratorHF_Non_Hfe.ini * Update pythia8_NonHfe.cfg * Update and rename MC/config/PWGHF/trigger/selectNonHfe.C to MC/config/PWGHF/external/generator/selectNonHfe.C Add Boxgen with Minimum bias * Update GeneratorHF_Non_Hfe.ini --- .../PWGHF/external/generator/selectNonHfe.C | 131 +++++++++++------- .../PWGHF/hybrid/GeneratorHF_Non_Hfe.json | 40 ------ MC/config/PWGHF/ini/GeneratorHF_Non_Hfe.ini | 6 +- .../pythia8/generator/pythia8_NonHfe.cfg | 2 +- 4 files changed, 83 insertions(+), 96 deletions(-) diff --git a/MC/config/PWGHF/external/generator/selectNonHfe.C b/MC/config/PWGHF/external/generator/selectNonHfe.C index 7c685c585..7dd325335 100644 --- a/MC/config/PWGHF/external/generator/selectNonHfe.C +++ b/MC/config/PWGHF/external/generator/selectNonHfe.C @@ -1,10 +1,3 @@ -/// Select π⁰ and η within a given rapidity window for enhancement -/// pdgPartForAccCut: PDG of the particle to select (111=π⁰, 221=η) -/// minNb: minimum number of such particles per event for enhancement - -//// authors: Rashi Gupta (rashi.gupta@cern.ch) -/// authors: Ravindra Singh (ravindra.singh@cern.ch) - #if !defined(__CLING__) || defined(__ROOTCLING__) #include "FairGenerator.h" @@ -14,83 +7,117 @@ #include "TParticlePDG.h" #include "TDatabasePDG.h" #include "TMath.h" +#include #include #include #endif #include "Pythia8/Pythia.h" -using namespace Pythia8; class GeneratorPythia8Box : public o2::eventgen::GeneratorPythia8 { public: - - GeneratorPythia8Box(std::vector pdgList, int nInject = 3, float ptMin = 0.1, float ptMax = 50.0, float etaMin = -0.8, float etaMax = 0.8) - : mPdgList(pdgList), nParticles(nInject), genMinPt(ptMin), genMaxPt(ptMax), genMinEta(etaMin), genMaxEta(etaMax) + + GeneratorPythia8Box(std::vector pdgList, int nInject = 3, + float ptMin = 0.1, float ptMax = 50.0, + float etaMin = -0.8, float etaMax = 0.8) + : mPdgList(pdgList), nParticles(nInject), + genMinPt(ptMin), genMaxPt(ptMax), + genMinEta(etaMin), genMaxEta(etaMax) { + // Initialize dedicated Pythia instance for injection/decay only + // Reuses same config as main Pythia; beam settings are ignored by moreDecays() + auto& param = o2::eventgen::GeneratorPythia8Param::Instance(); + std::string cfg = gSystem->ExpandPathName(param.config.c_str()); + if (!mPythiaGun.readFile(cfg, true)) { + LOG(fatal) << "Failed to read config for mPythiaGun: " << cfg; + } + if (!mPythiaGun.init()) { + LOG(fatal) << "Failed to init mPythiaGun"; + } } ~GeneratorPythia8Box() = default; -Bool_t generateEvent() override -{ - bool hasElectron = false; - - while (!hasElectron) { - mPythia.event.reset(); + bool Init() override + { + return o2::eventgen::GeneratorPythia8::Init(); + } - for (int i{0}; i < nParticles; ++i) - { - int currentPdg = mPdgList[gRandom->Integer(mPdgList.size())]; - double mass = TDatabasePDG::Instance()->GetParticle(currentPdg)->Mass(); +protected: - const double pt = gRandom->Uniform(genMinPt, genMaxPt); - const double eta = gRandom->Uniform(genMinEta, genMaxEta); - const double phi = gRandom->Uniform(0, TMath::TwoPi()); + // ── generateEvent: MB + injection ── + Bool_t generateEvent() override + { + bool hasElectron = false; + + while (!hasElectron) { + // ===== 1. Generate minimum-bias event ===== + if (!mPythia.next()) continue; + + // ===== 2. Inject π⁰/η into separate gun, decay only them ===== + mPythiaGun.event.reset(); + for (int i = 0; i < nParticles; ++i) { + int pdg = mPdgList[gRandom->Integer(mPdgList.size())]; + auto* pPDG = TDatabasePDG::Instance()->GetParticle(pdg); + if (!pPDG) continue; + double mass = pPDG->Mass(); + + double pt = gRandom->Uniform(genMinPt, genMaxPt); + double eta = gRandom->Uniform(genMinEta, genMaxEta); + double phi = gRandom->Uniform(0., TMath::TwoPi()); + double px = pt * cos(phi); + double py = pt * sin(phi); + double pz = pt * sinh(eta); + double e = sqrt(px * px + py * py + pz * pz + mass * mass); + + mPythiaGun.event.append(pdg, 11, 0, 0, px, py, pz, e, mass); + } - const double px{pt * std::cos(phi)}; - const double py{pt * std::sin(phi)}; - const double pz{pt * std::sinh(eta)}; - const double et{std::hypot(std::hypot(pt, pz), mass)}; + mPythiaGun.moreDecays(); - - mPythia.event.append(currentPdg, 11, 0, 0, px, py, pz, et, mass); + // ===== 3. Check for electron from injection in acceptance ===== + for (int i = 0; i < mPythiaGun.event.size(); ++i) { + auto& part = mPythiaGun.event[i]; + if (std::abs(part.id()) == 11 && part.isFinal()) { + if (part.pT() > 0.1 && std::abs(part.eta()) < 1.2) { + hasElectron = true; + break; + } + } + } } - - if (!mPythia.next()) continue; + // ===== 4. Copy injection products to MB event with index offset ===== + int offset = mPythia.event.size(); - - for (int i = 0; i < mPythia.event.size(); ++i) { - if (std::abs(mPythia.event[i].id()) == 11) { - - - double childPt = mPythia.event[i].pT(); - double childEta = mPythia.event[i].eta(); - - if (childPt > 0.1 && std::abs(childEta) < 1.2) { - hasElectron = true; // Mil gaya! - break; - } - } + for (int i = 0; i < mPythiaGun.event.size(); ++i) { + auto part = mPythiaGun.event[i]; + if (part.id() == 90) continue; // skip system placeholder + if (!part.isFinal() && part.id() != 111 && part.id() != 221) continue; + + if (part.mother1() > 0) part.mother1(part.mother1() + offset - 1); + if (part.mother2() > 0) part.mother2(part.mother2() + offset - 1); + if (part.daughter1() > 0) part.daughter1(part.daughter1() + offset - 1); + if (part.daughter2() > 0) part.daughter2(part.daughter2() + offset - 1); + + mPythia.event.append(part); } - - } - return true; -} + return true; + } private: std::vector mPdgList; double genMinPt, genMaxPt; double genMinEta, genMaxEta; int nParticles; + + Pythia8::Pythia mPythiaGun; }; - FairGenerator *generatePythia8Box(float ptMin = 0.1, float ptMax = 50.0) { - std::vector pdgList = {111, 221}; return new GeneratorPythia8Box(pdgList, 3, ptMin, ptMax, -0.8, 0.8); -} +} \ No newline at end of file diff --git a/MC/config/PWGHF/hybrid/GeneratorHF_Non_Hfe.json b/MC/config/PWGHF/hybrid/GeneratorHF_Non_Hfe.json index c729060bb..e69de29bb 100644 --- a/MC/config/PWGHF/hybrid/GeneratorHF_Non_Hfe.json +++ b/MC/config/PWGHF/hybrid/GeneratorHF_Non_Hfe.json @@ -1,40 +0,0 @@ -{ - "generators": [ - { - "name": "pythia8", - "config": { - "config": "${O2DPG_MC_CONFIG_ROOT}/MC/config/common/pythia8/generator/pythia8_NonHfe.cfg" - }, - "triggers": { - "mode": "or", - "specs": [ - { - "macro": "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/trigger/selectNonHfe.C", - "function": "selectPionEtaWithinAcc(\"111;221\",-1.5,1.5,1)" - } - ] - } - }, - { - "name": "boxgen", - "config": { - "pdg": 111, - "prange[0]": 0.1, - "prange[1]": 50.0, - "eta[0]": -0.8, - "eta[1]": 0.8 - } - }, - { - "name": "boxgen", - "config": { - "pdg": 221, - "prange[0]": 0.1, - "prange[1]": 50.0, - "eta[0]": -0.8, - "eta[1]": 0.8 - } - } - ], - "fractions": [1, 1, 1] -} diff --git a/MC/config/PWGHF/ini/GeneratorHF_Non_Hfe.ini b/MC/config/PWGHF/ini/GeneratorHF_Non_Hfe.ini index f5455e34d..7ace23bea 100644 --- a/MC/config/PWGHF/ini/GeneratorHF_Non_Hfe.ini +++ b/MC/config/PWGHF/ini/GeneratorHF_Non_Hfe.ini @@ -1,7 +1,7 @@ -#### This configuration uses the Box generator to trigger π⁰/η production within the specified rapidity window [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/external/generator/selectNonHfe.C -funcName = generatePythia8Box(0.1, 50.0) +funcName=generatePythia8Box(0.1,50.0) + [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/generator/pythia8_NonHfe.cfg -includePartonEvent=true +includePartonEvent=true \ No newline at end of file diff --git a/MC/config/PWGHF/pythia8/generator/pythia8_NonHfe.cfg b/MC/config/PWGHF/pythia8/generator/pythia8_NonHfe.cfg index 3feaa6585..f16d04171 100644 --- a/MC/config/PWGHF/pythia8/generator/pythia8_NonHfe.cfg +++ b/MC/config/PWGHF/pythia8/generator/pythia8_NonHfe.cfg @@ -9,8 +9,8 @@ Beams:eCM 13600. # GeV ### processes -ProcessLevel:all = off +SoftQCD:inelastic = on ### decays ParticleDecays:limitTau0 on ParticleDecays:tau0Max 10. From a65aa550443f5e1fa7bdb514bc7ebe85bbf9dc5a Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 29 Oct 2026 15:56:08 +0100 Subject: [PATCH 119/144] Run the AnalysisQC execution only for AnalysisQC changes The announcement claimed MC/bin changes while the execution was gated on changed_analysis_qc, so the two disagreed. Making them agree the other way ran a full sim-reco-AOD-analysis chain, about 33 minutes and a GRID token, on every MC/bin pull request. Both now follow changed_analysis_qc, which is what the execution did before. Co-Authored-By: Claude Opus 5 --- test/run_workflow_tests.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/run_workflow_tests.sh b/test/run_workflow_tests.sh index e6fb3717a..027a5488a 100755 --- a/test/run_workflow_tests.sh +++ b/test/run_workflow_tests.sh @@ -26,6 +26,20 @@ get_git_repo_directory() } +first_nonzero() +{ + # Echo the first non-zero argument, or 0. Used instead of summing exit + # codes: a sum can land on a multiple of 256 and exit 0 -- two phases + # failing with 128 each would report success -- and it also loses which + # phase failed. + local code + for code in "$@" ; do + [[ "${code}" != "0" ]] && { echo "${code}" ; return ; } + done + echo 0 +} + + test_single_wf() { local wf_script=${1} @@ -55,7 +69,7 @@ test_single_wf() # use -k|--keep-going feature to not stop after the first failure but see, if there are more [[ "${ret_this}" == "0" ]] && { ${O2DPG_ROOT}/MC/bin/o2_dpg_workflow_runner.py -f workflow.json --cpu-limit 8 --target-labels QC ${memlimit} -k >> ${LOG_FILE_WF} 2>&1 ; ret_this_qc=${?} ; } [[ "${ret_this}" == "0" ]] && { ${O2DPG_ROOT}/MC/bin/o2_dpg_workflow_runner.py -f workflow.json --cpu-limit 8 --target-labels Analysis ${memlimit} -k >> ${LOG_FILE_WF} 2>&1 ; ret_this_analysis=${?} ; } - ret_this=$((ret_this + ret_this_qc + ret_this_analysis)) + ret_this=$(first_nonzero "${ret_this}" "${ret_this_qc}" "${ret_this_analysis}") [[ "${ret_this}" != "0" ]] && echo "[FATAL]: O2DPG_TEST Workflow execution failed" >> ${LOG_FILE_WF} || echo_green " -> PASSED" else echo_green " -> PASSED" @@ -241,7 +255,7 @@ else [[ "${changed_workflows}" != "" ]] && echo " - The creation of simulation workflows from changed run scripts (sub-sect of MC/run/**/*.sh) will be tested." fi -if [[ "${changed_analysis_qc}" != "" || "${changed_sim_bin}" ]] ; then +if [[ "${changed_analysis_qc}" != "" ]] ; then for p in $(find "MC/bin/tests" -name "*.sh") ; do execute_workflows+="$(realpath ${p}) " done @@ -293,7 +307,7 @@ fi # prepare our local test directory for bin tests # global return code for PWGs ret_analysis_qc=0 -if [[ "${changed_analysis_qc}" != "" || "${changed_sim_bin}" != "" ]] ; then +if [[ "${changed_analysis_qc}" != "" ]] ; then rm -rf ${TEST_PARENT_DIR_BIN} 2>/dev/null mkdir -p ${TEST_PARENT_DIR_BIN} 2>/dev/null pushd ${TEST_PARENT_DIR_BIN} > /dev/null @@ -336,7 +350,7 @@ if [[ "${changed_anchored}" != "" ]] ; then popd > /dev/null fi -RET=$(( ret_global_pwg + ret_analysis_qc + ret_global_anchored )) +RET=$(first_nonzero "${ret_global_pwg}" "${ret_analysis_qc}" "${ret_global_anchored}") echo [[ "${RET}" != "0" ]] && echo_red "There were errors, please check!" || echo_green "All required workflow tests successful" From 80b120551fab5934b8e3d53ae81f139d3b40aa0c Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 29 Oct 2026 15:56:55 +0100 Subject: [PATCH 120/144] Keep the first non-zero exit code instead of summing Summing can land on a multiple of 256: two phases failing with 128 each summed to 256 and exited 0, reporting success. Keeping the first non-zero code also makes the exit status identify the phase that failed. The static guard in test/tests/ now checks that neither site sums again. Co-Authored-By: Claude Opus 5 --- test/tests/exit_code_aggregation.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/test/tests/exit_code_aggregation.sh b/test/tests/exit_code_aggregation.sh index 1172f23d9..c7cbef736 100755 --- a/test/tests/exit_code_aggregation.sh +++ b/test/tests/exit_code_aggregation.sh @@ -1,8 +1,11 @@ #!/bin/bash -# Static guard against the exit-code-aggregation bug in run_workflow_tests.sh: -# the final RET=$(( ... )) line must include every aggregate return-code -# variable the script computes, and each of those variables must actually be -# assigned somewhere in the script. +# Static guard against the exit-code-aggregation bugs in run_workflow_tests.sh. +# Two things must hold: +# 1. the final RET= line must reference every aggregate return-code variable +# the script computes, and each must actually be assigned somewhere; +# 2. no exit code may be produced by *summing* codes. Sums can land on a +# multiple of 256 — two phases failing with 128 each sum to 256, which +# exits 0 — so a real failure would report success. set -u HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) @@ -25,7 +28,7 @@ check() # whoever adds it come here and update this list. REQUIRED_VARS="ret_global_pwg ret_analysis_qc ret_global_anchored" -RET_LINE=$(grep -m1 -E '^RET=\$\(\(' "${SCRIPT}") +RET_LINE=$(grep -m1 -E '^RET=' "${SCRIPT}") for var in ${REQUIRED_VARS} ; do present=$(grep -c -- "${var}" <<< "${RET_LINE}") @@ -37,6 +40,13 @@ for var in ${REQUIRED_VARS} ; do check "${var} is assigned in the script" 1 "$( [[ "${assigned}" -ge 1 ]] && echo 1 || echo 0 )" done +# No exit code may be a sum of codes. These are the two sites that had it. +summed_ret=$(grep -c -E '^RET=\$\(\(' "${SCRIPT}") +check "RET is not a sum of codes" 0 "${summed_ret}" + +summed_this=$(grep -c -E 'ret_this=\$\(\(ret_this[[:space:]]*\+' "${SCRIPT}") +check "test_single_wf does not sum codes" 0 "${summed_this}" + echo if [[ "${FAILURES}" == "0" ]] ; then echo "All exit-code aggregation checks passed" From 8f1be357d99c1feb0441aab46ea6e6ea3f6f90cd Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 29 Oct 2026 19:31:59 +0100 Subject: [PATCH 121/144] Remove leftovers of the aliBuild sim-test check The recipe now calls test/run_tests.sh and the build/O2DPG/sim/o2 check is gone, so three things no longer have a consumer. - run_analysisqc_tests.sh was a stub whose body is "exit 0". Its content moved into run_workflow_tests.sh in #1517 and nothing calls the file any more. - clean-ci.yml offered to clean a build of a check that no longer exists. - run_tests.sh added curl's include directory to ROOT_INCLUDE_PATH for the CCDB dictionary. alidist/curl.sh now does this with --root, and the published dailies carry it. test/README.md now points at "aliBuild build O2DPG-sim-tests" for changes that need an unreleased O2, which tests against your own build rather than dev. Co-Authored-By: Claude Opus 5 --- .github/workflows/clean-ci.yml | 4 ---- test/README.md | 18 ++++++++++++++---- test/run_analysisqc_tests.sh | 4 ---- test/run_tests.sh | 12 ------------ 4 files changed, 14 insertions(+), 24 deletions(-) delete mode 100755 test/run_analysisqc_tests.sh diff --git a/.github/workflows/clean-ci.yml b/.github/workflows/clean-ci.yml index aa5364983..982db579b 100644 --- a/.github/workflows/clean-ci.yml +++ b/.github/workflows/clean-ci.yml @@ -14,10 +14,6 @@ name: Clean PR builds # Warning: the check_* keys are magic and must consist of the string # "check_" followed by the applicable check name exactly. The # "description" field is only the human-readable label for the input. - 'check_build/O2DPG/sim/o2': - description: build/O2DPG/sim/o2 - type: boolean - default: true 'check_build/O2DPG/O2fst/o2': description: build/O2DPG/O2fst/o2 type: boolean diff --git a/test/README.md b/test/README.md index 276e9eaf7..80bdc2bc9 100644 --- a/test/README.md +++ b/test/README.md @@ -125,7 +125,17 @@ commit cannot pass it. Two escape hatches, in order of preference: 1. If the O2 change is already in a published daily, pin it: add a line `sim-tests-tag: daily-YYYYMMDD-HHMM-1` to the pull request description. -2. If it is not published anywhere yet, touch `test/needs-o2-dev` with a - one-line reason and a link to the O2 pull request. That enables - `build/O2DPG/sim/o2dev`, which builds O2 from source against `dev`. It is - much slower, so it is opt-in. +2. If it is not published anywhere yet, run the tests against your own build: + +```bash +aliBuild build O2DPG-sim-tests +``` + + This builds the O2sim stack from your work area and runs `run_tests.sh` + against your O2DPG checkout, so it covers O2 changes that are not committed + anywhere yet. It needs O2DPG as a development package (`aliBuild init + O2DPG`); without one, the sub-tests stop with `is not a git repository`. + + Touching `test/needs-o2-dev` makes the CVMFS check skip, which is useful to + keep a pull request from being gated by a check that cannot pass for it. No + check replaces it, so pair it with the command above. diff --git a/test/run_analysisqc_tests.sh b/test/run_analysisqc_tests.sh deleted file mode 100755 index a4064ba30..000000000 --- a/test/run_analysisqc_tests.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# for now, obsolete -exit 0 diff --git a/test/run_tests.sh b/test/run_tests.sh index 400f4d5e4..df636d98b 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -51,18 +51,6 @@ if [[ -n "${G4INSTALL:-}" ]] ; then sed -e 's/[^ ]* //' -e 's/G4/export G4/' -e 's/DATA /DATA=/')" fi -# O2's CCDB dictionary payload includes . An aliBuild build -# environment has curl's headers; a plain runtime environment from CVMFS does -# not, and cling then dies compiling the payload. Derive the prefix from -# curl-config rather than hardcoding a version. -if command -v curl-config > /dev/null 2>&1 ; then - curl_include=$(curl-config --prefix 2>/dev/null)/include - if [[ -f "${curl_include}/curl/curl.h" && ":${ROOT_INCLUDE_PATH:-}:" != *":${curl_include}:"* ]] ; then - export ROOT_INCLUDE_PATH="${curl_include}${ROOT_INCLUDE_PATH:+:${ROOT_INCLUDE_PATH}}" - echo "Added ${curl_include} to ROOT_INCLUDE_PATH for the CCDB dictionary" - fi -fi - # LHAPDF data, needed by several generator configurations. if [[ -z "${LHAPDF_DATA_PATH:-}" && -n "${LHAPDF_ROOT:-}" ]] ; then export LHAPDF_DATA_PATH="${LHAPDF_ROOT}/share/LHAPDF:${LHAPDF_PDFSETS_ROOT:-}/share/LHAPDF" From 8c74863a05fd620095bdeebf66f1e2432ad3e3ca Mon Sep 17 00:00:00 2001 From: shahor02 Date: Fri, 30 Oct 2026 12:10:59 +0100 Subject: [PATCH 122/144] Bring remaining 2025/26 passes under new ITS alignment conditions (#2527) --- .../configurations/asyncReco/setenv_extra.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index e3ee7a7d5..79d0991fb 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -426,6 +426,7 @@ elif [[ $ALIGNLEVEL == 1 ]]; then if [[ $ALIEN_JDL_LPMANCHORYEAR == "2022" ]]; then INST_IR_FOR_TPC=${ALIEN_JDL_INSTIRFORTPC-CTPCCDB} # by default override inst.IR by the mean IR from CCDB and use it for scaling fi + if [[ $PERIOD == "LHC22s" ]]; then TPC_SCALING_SOURCE=${ALIEN_JDL_TPCSCALINGSOURCE-NO_SCALING} # in this way, only TPC/Calib/CorrectionMaps is applied, and we know that for 22s it is the same as TPC/Calib/CorrectionMapsRef; elif [[ $PERIOD == @(LHC22c|LHC22d|LHC22e|JUN|LHC22f) ]]; then @@ -645,13 +646,19 @@ fi # ad-hoc settings for TRD matching export CONFIG_EXTRA_PROCESS_o2_trd_global_tracking+=";$ITSEXTRAERR;$TRACKTUNETPC;$VDRIFTPARAMOPTION;GPU_rec_trd.minTrackPt=0.3;" -# Pb26 reconstruction uses improved alignment with shifted ITS (mean vertex) -if [[ $RUNNUMBER -ge 572013 ]] ; then +# 2025/2026 are covered by better ITS alignment and it requires mean vertex. Also the TRD errors are improved +case "$PERIOD:$ANCHORED_PASS_NUMBER" in + "LHC26ak:1" | \ + "LHC26ah:2" | "LHC26aj:2" | \ + "LHC25an:2" | "LHC25ap:2" | "LHC25aq:2" | "LHC25ar:2" | "LHC25as:2" | \ + "LHC25ao:3" | \ + "LHC25aj:3" | "LHC25ak:3" | "LHC25al:3" | \ + "LHC25ad:3" | "LHC25ae:3" | "LHC25af:3" ) export CONFIG_EXTRA_PROCESS_o2_trd_global_tracking+=";GPU_rec_trd.trkltResRPhiIdeal=0.2;GPU_rec_trd.trkltResVsTanPhiMisalign=1.;" if [[ -z $ALIEN_JDL_MVBIAS ]]; then export O2_DPL_MVBIAS=";mvbias.xyz[0]=0.0332;mvbias.xyz[1]=-0.1825;mvbias.xyz[2]=-0.2846;mvbias.slopeX=-4.27e-05;mvbias.slopeY=-6.96e-05;"; fi -fi +esac # ad-hoc settings for FT0 export ARGS_EXTRA_PROCESS_o2_ft0_reco_workflow+=" --ft0-reconstructor" From fe14f741695e1f189b265dd4a7c5d1759fc4847c Mon Sep 17 00:00:00 2001 From: shahor02 Date: Fri, 30 Oct 2026 16:13:33 +0100 Subject: [PATCH 123/144] Extra setenv_extra settings to steed cosmics reco from JDL (#2529) --- .../configurations/asyncReco/setenv_extra.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index 79d0991fb..070fbb6ad 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -79,6 +79,16 @@ else fi fi +if [[ -n $ALIEN_JDL_WORKFLOWDETECTORSRECO ]]; then + echo "WORKFLOW_DETECTORS_RECO taken fron JDL, ALIEN_JDL_WORKFLOWDETECTORSRECO = $ALIEN_JDL_WORKFLOWDETECTORSRECO" + export WORKFLOW_DETECTORS_RECO="$ALIEN_JDL_WORKFLOWDETECTORSRECO" +fi + +if [[ -n $ALIEN_JDL_WORKFLOWDETECTORSMATCHING ]]; then + echo "WORKFLOW_DETECTORS_MATCHING taken fron JDL, ALIEN_JDL_WORKFLOWDETECTORSMATCHING = $ALIEN_JDL_WORKFLOWDETECTORSMATCHING" + export WORKFLOW_DETECTORS_MATCHING="$ALIEN_JDL_WORKFLOWDETECTORSMATCHING" +fi + echo "Final settings for detectors to be processed:" echo "WORKFLOW_DETECTORS = $WORKFLOW_DETECTORS" echo "WORKFLOW_DETECTORS_EXCLUDE = $WORKFLOW_DETECTORS_EXCLUDE" From 6f4f32d384c436b1f110457709e94d9e3ecad60a Mon Sep 17 00:00:00 2001 From: shahor02 Date: Fri, 30 Oct 2026 16:57:04 +0100 Subject: [PATCH 124/144] Possibility to pass a CL option to ITS reco from JDL (#2530) E.g. in the cosmic mode we want to pass --disable-tracking to run only reclustering --- DATA/production/configurations/asyncReco/setenv_extra.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DATA/production/configurations/asyncReco/setenv_extra.sh b/DATA/production/configurations/asyncReco/setenv_extra.sh index 070fbb6ad..a0de92f8b 100644 --- a/DATA/production/configurations/asyncReco/setenv_extra.sh +++ b/DATA/production/configurations/asyncReco/setenv_extra.sh @@ -583,6 +583,10 @@ elif [[ $BEAMTYPE == "pp" || $LIGHTNUCLEI == "1" ]]; then fi export CONFIG_EXTRA_PROCESS_o2_its_reco_workflow+=";$MAXBCDIFFTOMASKBIAS_ITS;$MAXBCDIFFTOSQUASHBIAS_ITS;$EXTRA_ITSRECO_CONFIG;" +if [[ -n $ALIEN_JDL_ITSRECOARGS ]] ; then + export ARGS_EXTRA_PROCESS_o2_its_reco_workflow+="$ALIEN_JDL_ITSRECOARGS" +fi + # in the ALIGNLEVEL there was inconsistency between the internal errors of sync_misaligned and ITSEXTRAERR if [[ $ALIGNLEVEL != 0 ]]; then export CONFIG_EXTRA_PROCESS_o2_its_reco_workflow+=";$ITSEXTRAERR;" From d0a41907fb4b138f77bc2ccc0a11480469ab3828 Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Mon, 2 Nov 2026 17:37:47 +0100 Subject: [PATCH 125/144] Add configs for decays of charm-strange baryons from pools (#2521) * Add configs for decays of charm-strange baryons from pools * Remove file added in another PR * Retrigger the CI --- .../pythia8/decayer/force_hadronic_Xic_trigger.cfg | 12 ++++++++---- .../pythia8/decayer/force_hadronic_charmbaryon.cfg | 11 +++++++---- .../decayer/force_hadronic_charmbaryon_trigger.cfg | 9 ++++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg b/MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg index 404f675bc..c2d45d92e 100644 --- a/MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg +++ b/MC/config/PWGHF/pythia8/decayer/force_hadronic_Xic_trigger.cfg @@ -7,10 +7,10 @@ Init:showChangedParticleData = on 4132:onMode = off ## Xic decays -### Ξc+ -> Ξ- π+ π+ (70%) -4232:oneChannel = 1 0.35000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% -### Ξc+ -> Ξ*0 π+ (30%) -4232:addChannel = 1 0.12500 0 3324 211 +### Ξc+ -> Ξ- π+ π+ (90%) +4232:oneChannel = 1 0.029000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% +### Ξc+ -> Ξ*0 π+ (10%) +4232:addChannel = 1 0.002900 0 3324 211 ### Ξc+ -> Ξ*0 π+, Ξ*0 -> Ξ- π+ 4232:onIfMatch = 3324 211 @@ -31,3 +31,7 @@ Init:showChangedParticleData = on # Xi_c()0 --> Xi- pi+ pi+ pi- 0.04775082700379833 # -> PDG Codes: 3312, 211, 211, -211 4132:onIfMatch = 3334 321 + +### Ξc+ -> Ξ*0 π+, Ξ*0 -> Ξ- π+ +3324:onMode = off +3324:onIfMatch = 3312 211 diff --git a/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg index a6128cb23..0089831bc 100644 --- a/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg +++ b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg @@ -12,14 +12,14 @@ Init:showChangedParticleData = on ### Ξc+ -> p K- π+ (30%) 4232:oneChannel = 1 0.1500 0 2212 -321 211 ### Ξc+ -> p K- π+ 6.18e-3 4232:addChannel = 1 0.1500 0 2212 -313 ### Ξc+ -> p antiK*0(892) -### Ξc+ -> Ξ- π+ π+ (40%) (set the same as Ξc+ -> p K- π+) -4232:addChannel = 1 0.4000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% +### Ξc+ -> Ξ- π+ π+ (45%) +4232:addChannel = 1 0.4500 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.9% ### Ξc+ -> p φ (10%) 4232:addChannel = 1 0.10000 0 2212 333 ### Ξc+ -> p φ ### Ξc+ -> sigma+ π+ π- (10%) 4232:addChannel = 1 0.10000 0 3222 -211 211 ### Ξc+ -> sigma+ π- π+ 1.37% -### Ξc+ -> Ξ*0 π+ (10%) -4232:addChannel = 1 0.10000 0 3324 211 +### Ξc+ -> Ξ*0 π+ (4.5%) +4232:addChannel = 1 0.045000 0 3324 211 ### Ξc+ -> p K- π+ 4232:onIfMatch = 2212 321 211 @@ -92,3 +92,6 @@ Init:showChangedParticleData = on ### for Omega_c -> pi Omega -> pi K Lambda -> pi K pi p 3334:onMode = off 3334:onIfAll = 3122 -321 +### Ξc+ -> Ξ*0 π+, Ξ*0 -> Ξ- π+ +3324:onMode = off +3324:onIfMatch = 3312 211 diff --git a/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg index 5983a65f1..30b1f218c 100644 --- a/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg +++ b/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon_trigger.cfg @@ -9,10 +9,10 @@ Init:showChangedParticleData = on 4132:onMode = off ## Xic decays -### Ξc+ -> Ξ- π+ π+ (40%) (set the same as Ξc+ -> p K- π+) -4232:oneChannel = 1 0.8000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.86% +### Ξc+ -> Ξ- π+ π+ (90%) +4232:oneChannel = 1 0.029000 0 3312 211 211 ### Ξc+ -> Ξ- π+ π+ 2.9% ### Ξc+ -> Ξ*0 π+ (10%) -4232:addChannel = 1 0.20000 0 3324 211 +4232:addChannel = 1 0.002900 0 3324 211 ### Ξc+ -> Ξ*0 π+, Ξ*0 -> Ξ- π+ 4232:onIfMatch = 3324 211 @@ -70,3 +70,6 @@ Init:showChangedParticleData = on ### for Omega_c -> pi Omega -> pi K Lambda -> pi K pi p 3334:onMode = off 3334:onIfAll = 3122 -321 +### Ξc+ -> Ξ*0 π+ Ξ*0 -> Ξ- π+ +3324:onMode = off +3324:onIfMatch = 3312 211 From e9d5f2dbd9245252bd3c47a97b0f8fbe281cb544 Mon Sep 17 00:00:00 2001 From: ldellost <47105254+DelloStritto@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:57:29 +0100 Subject: [PATCH 126/144] Improve Lambda(1520) generator (#2533) --- .../ini/GeneratorLF_Strangeness_ppLStar.ini | 2 +- .../tests/GeneratorLF_Strangeness_ppLStar.C | 28 +++- .../PWGLF/pythia8/generator_pythia8_Lstar.C | 141 ++++++++++++------ 3 files changed, 117 insertions(+), 54 deletions(-) diff --git a/MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini b/MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini index 9836791ed..5f3809903 100644 --- a/MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini +++ b/MC/config/PWGLF/ini/GeneratorLF_Strangeness_ppLStar.ini @@ -1,6 +1,6 @@ [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C -funcName=generator_Lstar() +funcName=generator_Lstar(5) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_forceLStar.cfg \ No newline at end of file diff --git a/MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C b/MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C index ef04688a0..c69a84d5d 100644 --- a/MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C +++ b/MC/config/PWGLF/ini/tests/GeneratorLF_Strangeness_ppLStar.C @@ -1,7 +1,8 @@ int External() { std::string path{"o2sim_Kine.root"}; - const int pdgMother = 3124; // Lambda(1520)0 (sign randomized by the generator) + const int pdgMother = 3124; // Lambda(1520)0 + const int triggerRatio = 5; // must match generator_Lstar(N) in the .ini file TFile file(path.c_str(), "READ"); if (file.IsZombie()) { @@ -20,10 +21,18 @@ int External() int nMother = 0; int nNotDecayed = 0; int nBadDecay = 0; // mothers not decaying exactly to Lambda + gamma - int nEventsWrongCount = 0; // events without exactly one injected mother + int nEventsWrongCount = 0; // events with the wrong number of injected mothers + int nGapEvents = 0; // events expected to contain the injected mother for (int i = 0; i < nEvents; i++) { tree->GetEntry(i); + + // the generator injects in every triggerRatio-th event (it counts events from 1) + const bool isGapEvent = ((i + 1) % triggerRatio == 0); + if (isGapEvent) { + nGapEvents++; + } + int nInEvent = 0; for (auto &track : *tracks) { const int pdg = track.GetPdgCode(); @@ -50,21 +59,30 @@ int External() std::cerr << "Unexpected decay of " << pdg << " (" << nDau << " daughters)\n"; } } - if (nInEvent != 1) { + + const int nExpected = isGapEvent ? 1 : 0; + if (nInEvent != nExpected) { nEventsWrongCount++; + std::cerr << "Event " << i << ": found " << nInEvent + << " Lambda(1520), expected " << nExpected << "\n"; } } std::cout << "--------------------------------\n"; std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# Gap-trigger events: " << nGapEvents << " (ratio " << triggerRatio << ")\n"; std::cout << "# Lambda(1520) + anti: " << nMother << "\n"; std::cout << "# not decayed: " << nNotDecayed << "\n"; std::cout << "# unexpected decays: " << nBadDecay << "\n"; - std::cout << "# events with != 1 mother: " << nEventsWrongCount << "\n"; + std::cout << "# events wrong count: " << nEventsWrongCount << "\n"; std::cout << "--------------------------------\n"; + if (nMother != nGapEvents) { + std::cerr << "Expected one injected Lambda(1520) per gap-trigger event\n"; + return 1; + } if (nEventsWrongCount > 0) { - std::cerr << "Each event must contain exactly one injected Lambda(1520)\n"; + std::cerr << "Lambda(1520) must appear in every gap-triggered events\n"; return 1; } if (nNotDecayed > 0 || nBadDecay > 0) { diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C b/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C index 66cb4646b..3a732db28 100644 --- a/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C +++ b/MC/config/PWGLF/pythia8/generator_pythia8_Lstar.C @@ -1,4 +1,3 @@ - #include "Pythia8/Pythia.h" #include "Pythia8/HeavyIons.h" #include "FairGenerator.h" @@ -19,14 +18,25 @@ public: GeneratorPythia8ExtraStrangeness() = default; /// Constructor - GeneratorPythia8ExtraStrangeness(int input_pdg) + GeneratorPythia8ExtraStrangeness(int input_pdg, int inputTriggerRatio) { + + if (inputTriggerRatio <= 0) { + throw std::invalid_argument("inputTriggerRatio must be positive"); + } + + LOG(info) << "Gap trigger ratio N = " << inputTriggerRatio << " events"; + + + mGeneratedEvents = 0; + mTriggerRatio = inputTriggerRatio; + genMinPt=0.0; - genMaxPt=12.0; - genminY=-1.5; - genmaxY=1.5; - genminEta=-1.5; - genmaxEta=1.5; + genMaxPt=10.0; + genminY=-1.0; + genmaxY=1.0; + genminEta=-2.5; + genmaxEta=2.5; pdg=input_pdg; m = 0; @@ -40,14 +50,8 @@ public: xProd=0.; yProd=0.; zProd=0.; fLVHelper = std::make_unique(); - - randomizePDGsign = false; - - fSpectra = makeLevySpectrum("fSpectra", genMinPt, genMaxPt, mPythia.particleData.m0(input_pdg), 0.30, 7.); } - /// randomize the PDG code sign of core particle - void setRandomizePDGsign() { randomizePDGsign = true; } Double_t y2eta(Double_t pt, Double_t mass, Double_t y){ Double_t mt = TMath::Sqrt(mass * mass + pt * pt); @@ -108,22 +112,19 @@ TF1* makeLevySpectrum(const char* name, double ptMin, double ptMax, double mass, //_________________________________________________________________________________ /// generate uniform eta and uniform momentum void genSpectraMomentumEta(double minPt, double maxPt, double minY, double maxY){ - // random generator - std::unique_ptr ranGenerator { new TRandom3() }; - ranGenerator->SetSeed(0); - + // generate transverse momentum //const double gen_pT = ranGenerator->Uniform(minPt, maxPt); - const double gen_pT = fSpectra->GetRandom(minPt,maxPt); + const double gen_pT = fSpectra->GetRandom(minPt, maxPt, &mRandom); //Actually could be something else without loss of generality but okay - const double gen_phi = ranGenerator->Uniform(0,2*TMath::Pi()); + const double gen_phi = mRandom.Uniform(0,2*TMath::Pi()); // sample flat in rapidity, calculate eta Double_t gen_Y=10, gen_eta=10; while( gen_eta>genmaxEta || gen_etaUniform(minY,maxY); + gen_Y = mRandom.Uniform(minY,maxY); gen_eta = y2eta(gen_pT, m, gen_Y); } @@ -132,10 +133,10 @@ TF1* makeLevySpectrum(const char* name, double ptMin, double ptMax, double mass, } //__________________________________________________________________ - Pythia8::Particle createParticle(){ + Pythia8::Particle createParticle(int id){ //std::cout << "createParticle() mass " << m << " pdgCode " << pdg << std::endl; Pythia8::Particle myparticle; - myparticle.id(pdg); + myparticle.id(id); myparticle.status(11); myparticle.px(px); myparticle.py(py); @@ -149,46 +150,92 @@ TF1* makeLevySpectrum(const char* name, double ptMin, double ptMax, double mass, return myparticle; } + //__________________________________________________________________ - int randomizeSign() + // look for final-state (anti)Lambdas in the event; if there are any, remove one + // (chosen at random) from the event record and return the Lambda id, otherwise return 0 + int removeLambda() { - std::unique_ptr gen_random{new TRandom3(0)}; - const float n = gen_random->Uniform(-1, 1); + int kLambdaPdg = 3122; + auto& event = mPythia.event; - return n / abs(n); + std::vector candidates; + for (int i = 0; i < event.size(); ++i) { + bool isFromDecay = event[i].status() >= 91 && event[i].status() <= 99; + bool isAcc = std::abs(event[i].y()) < genmaxY; + + if (event[i].idAbs() == kLambdaPdg && event[i].isFinal() && isAcc && !isFromDecay) { + // not considering lambdas that are the first daughter in the event record. + bool isEdge = false; + for (int mom : event[i].motherList()) { + if (i == event[mom].daughter1() || i == event[mom].daughter2()) isEdge = true; + } + + if (!isEdge) { + candidates.push_back(i); + } + } + } + if (candidates.empty()) { + return 0; + } + + const int idx = candidates[static_cast(mRandom.Uniform(0,candidates.size()))]; + const int lambdaId = event[idx].id(); + + // remove the entry and shift the indices of the others + event.remove(idx, idx, true); + + return lambdaId; } - + //__________________________________________________________________ Bool_t generateEvent() override { // Generate PYTHIA event - mPythia.event.reset(); + if (!fSpectra) { + fSpectra = makeLevySpectrum("fSpectra", genMinPt, genMaxPt, mPythia.particleData.m0(pdg), 0.3942, 9.943); + } - /// go to next Pythia event + mPythia.event.reset(); Bool_t lPythiaOK = kFALSE; - while (!lPythiaOK){ - lPythiaOK = mPythia.next(); - } - /// reset event - //mPythia.event.reset(); + mGeneratedEvents++; + if (mGeneratedEvents % mTriggerRatio == 0) { + int lambdaId = 0; - /// create and append the desired particle - m = sampleMass(pdg); - genSpectraMomentumEta(genMinPt, genMaxPt, genminY, genmaxY); + // regenerate until the event contains a Lambda or an anti-Lambda, which is removed + while (lambdaId == 0) + { + lPythiaOK = mPythia.next(); + if (lPythiaOK) { + lambdaId = removeLambda(); + } + } - if (randomizePDGsign) - pdg *= randomizeSign(); + m = sampleMass(pdg); + genSpectraMomentumEta(genMinPt, genMaxPt, genminY, genmaxY); - Pythia8::Particle particle = createParticle(); - mPythia.event.append(particle); - mPythia.moreDecays(); + // Lambda -> Lambda*, anti-Lambda -> anti-Lambda* + const int sign = lambdaId > 0 ? 1 : -1; + Pythia8::Particle particle = createParticle(sign*pdg); + int idxL = mPythia.event.append(particle); + mPythia.moreDecays(idxL); + } else { + while (!lPythiaOK){ + lPythiaOK = mPythia.next(); + } + } return true; } + private: - + + unsigned long long mGeneratedEvents; + int mTriggerRatio; + double genMinPt; /// minimum 3-momentum for generated particles double genMaxPt; /// maximum 3-momentum for generated particles double genminY; /// minimum pseudorapidity for generated particles @@ -211,16 +258,14 @@ private: double xProd; /// x-coordinate position production vertex [cm] double yProd; /// y-coordinate position production vertex [cm] double zProd; /// z-coordinate position production vertex [cm] - - bool randomizePDGsign; /// bool to randomize the PDG code of the core particle TF1 *fSpectra = nullptr; /// TF1 to store more realistic shape of spectrum std::unique_ptr fLVHelper; + TRandom3 mRandom{0}; }; - FairGenerator *generator_Lstar() + FairGenerator *generator_Lstar(int inputTriggerRatio = 3) { - auto myGen = new GeneratorPythia8ExtraStrangeness(3124); - myGen->setRandomizePDGsign(); // randomization of PDG switched on + auto myGen = new GeneratorPythia8ExtraStrangeness(3124, inputTriggerRatio); return myGen; } \ No newline at end of file From f5b305939df7d5c3858cdfa2822aae60549ab528 Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Wed, 4 Nov 2026 10:01:44 +0100 Subject: [PATCH 127/144] Add ini file for combined c->D->mu and b->B->D->mu (#2523) * Add ini file for combined c->D->mu and b->B->D->mu * Retrigger CI * Fix typo * Fix issue due to limited number of generated events for test * Update number of test events to prevent tests to fail * Fix tests * Fix tests * Add checking of diquarks also and not only c quarks * Reset tests * Reset tests --- .../generator_pythia8_gaptriggered_hf.C | 16 +++ ...bar_and_bbbar_gap5_Mode2_corrBkgSigmaC.ini | 2 +- ..._bbbar_charmdecays_gap3_Mode2_accSmall.ini | 8 ++ ...ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.C | 12 +- ...ar_bbbar_charmdecays_gap3_Mode2_accSmall.C | 114 ++++++++++++++++++ 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 MC/config/PWGHF/ini/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.ini create mode 100644 MC/config/PWGHF/ini/tests/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.C diff --git a/MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_hf.C b/MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_hf.C index 2b801b47f..dc3407130 100644 --- a/MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_hf.C +++ b/MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_hf.C @@ -479,6 +479,22 @@ FairGenerator *GeneratorPythia8GapTriggeredCharmAndBeauty(int inputTriggerRatio, return myGen; } +// Charm and beauty enriched (2/3 charm 1/3 beauty) +FairGenerator *GeneratorPythia8GapTriggeredCharmBeautyCharm(int inputTriggerRatio, float yQuarkMin = -1.5, float yQuarkMax = 1.5, float yHadronMin = -1.5, float yHadronMax = 1.5, std::vector hadronPdgList = {}, std::vector> partPdgToReplaceList = {}, std::vector freqReplaceList = {}) +{ + auto myGen = new GeneratorPythia8GapTriggeredHF(inputTriggerRatio, std::vector{4, 5, 4}, hadronPdgList, partPdgToReplaceList, freqReplaceList); + auto seed = (gRandom->TRandom::GetSeed() % 900000000); + myGen->setUsedSeed(seed); + myGen->readString("Random:setSeed on"); + myGen->readString("Random:seed " + std::to_string(seed)); + myGen->setQuarkRapidity(yQuarkMin, yQuarkMax); + if (hadronPdgList.size() != 0) + { + myGen->setHadronRapidity(yHadronMin, yHadronMax); + } + return myGen; +} + FairGenerator *GeneratorPythia8GapHF(int inputTriggerRatio, float yQuarkMin = -1.5, float yQuarkMax = 1.5, float yHadronMin = -1.5, float yHadronMax = 1.5, std::vector quarkPdgList = {}, std::vector hadronPdgList = {}, std::vector> partPdgToReplaceList = {}, std::vector freqReplaceList = {}) { if (hadronPdgList.size() == 0 && quarkPdgList.size() == 0) diff --git a/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.ini b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.ini index d484e6d0f..ddb61c610 100644 --- a/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.ini +++ b/MC/config/PWGHF/ini/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.ini @@ -1,4 +1,4 @@ -#NEV_TEST> 100 +#NEV_TEST> 150 ### The external generator derives from GeneratorPythia8. [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_hf.C diff --git a/MC/config/PWGHF/ini/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.ini b/MC/config/PWGHF/ini/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.ini new file mode 100644 index 000000000..3cdcaa54a --- /dev/null +++ b/MC/config/PWGHF/ini/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.ini @@ -0,0 +1,8 @@ +#NEV_TEST> 150 +### The external generator derives from GeneratorPythia8 +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/external/generator/generator_pythia8_gaptriggered_hf.C +funcName=GeneratorPythia8GapTriggeredCharmBeautyCharm(3, -4.3, -2.2) +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/generator/pythia8_charm_with_mudecays_Mode2.cfg +includePartonEvent=true diff --git a/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.C b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.C index e6622a0fa..db96c1a06 100644 --- a/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.C +++ b/MC/config/PWGHF/ini/tests/GeneratorHF_D2H_ccbar_and_bbbar_gap5_Mode2_corrBkgSigmaC.C @@ -12,7 +12,7 @@ int External() { float ratioTrigger = 1./5.; // one event triggered out of 5 std::array, 2> pdgReplParticles = {std::array{413, 14122}, std::array{413, 4124}}; std::array, 2> pdgReplPartCounters = {std::array{0, 0}, std::array{0, 0}}; - std::array freqRepl = {0.5, 0.5}; + std::array freqRepl = {0.35, 0.5}; std::map sumOrigReplacedParticles = {{413, 0}}; std::array checkPdgHadron{413, 14122, 4124}; @@ -120,7 +120,15 @@ int External() { /// check that among the mothers there is a c/cbar quark /// This means that the charm hadron comes from the c-quark hadronization, where the c/cbar quark /// comes from a c-cbar pair present in the current event, tagged with a b-bbar (e.g. double-parton scattering) - if(std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4) == motherPdgCodes.end()) { + /// we check also if it comes from a charmed diquark + if(std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4) == motherPdgCodes.end() + && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4101) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4101) == motherPdgCodes.end() + && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4103) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4103) == motherPdgCodes.end() + && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4201) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4201) == motherPdgCodes.end() + && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4203) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4203) == motherPdgCodes.end() + && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4301) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4301) == motherPdgCodes.end() + && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4303) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4303) == motherPdgCodes.end() + && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 4403) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -4403) == motherPdgCodes.end()){ /// if we arrive here, it means that the hadron comes from the decay of a beauty hadron. /// This can happen if and only if this is not a replaced one (i.e. native from Lambdab0 decay) if (std::find(motherPdgCodes.begin(), motherPdgCodes.end(), 5122) == motherPdgCodes.end() && std::find(motherPdgCodes.begin(), motherPdgCodes.end(), -5122) == motherPdgCodes.end()) { diff --git a/MC/config/PWGHF/ini/tests/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.C b/MC/config/PWGHF/ini/tests/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.C new file mode 100644 index 000000000..d308ba734 --- /dev/null +++ b/MC/config/PWGHF/ini/tests/GeneratorHF_mu_ccbar_bbbar_charmdecays_gap3_Mode2_accSmall.C @@ -0,0 +1,114 @@ +int External() { + + int checkPdgDecayMuon = 13; + int checkPdgQuarkOne{4}; + int checkPdgQuarkTwo{5}; + float ratioQuarkOneTwo{2.}; + + float ratioTrigger = 1./3; // one event triggered out of 5 + + std::string path{"o2sim_Kine.root"}; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file" << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file" << path << "\n"; + return 1; + } + + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{0}; + int nEventsInjOne{0}; + int nEventsInjTwo{0}; + int nQuarks{0}; + int nMuons{0}; + + int nMuonsInAcceptance{}; + + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + // check subgenerator information + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (subGeneratorId == 0) { + nEventsMB++; + } else if (subGeneratorId == checkPdgQuarkOne) { + nEventsInjOne++; + } else if (subGeneratorId == checkPdgQuarkTwo) { + nEventsInjTwo++; + } + } + + int nmuonsev = 0; + int nmuonsevinacc = 0; + + for (auto &track : *tracks) { + auto pdg = track.GetPdgCode(); + if (std::abs(pdg) == checkPdgQuarkOne || std::abs(pdg) == checkPdgQuarkTwo) { + nQuarks++; + continue; + } // pdgquark + auto y = track.GetRapidity(); + if (std::abs(pdg) == checkPdgDecayMuon) { + int igmother = track.getMotherTrackId(); + auto gmTrack = (*tracks)[igmother]; + int gmpdg = gmTrack.GetPdgCode(); + if (int(std::abs(gmpdg) / 100.) == 4 || + int(std::abs(gmpdg) / 1000.) == 4) { + nMuons++; + nmuonsev++; + if (-4.3 < y && y < -2.2) { + nMuonsInAcceptance++; + nmuonsevinacc++; + } + } // gmpdg + + } // pdgdecay + + } // loop track + // std::cout << "#muons per event: " << nmuonsev << "\n"; + // std::cout << "#muons in acceptance per event: " << nmuonsev << "\n"; + } // events + + std::cout << "#events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << Form("# events injected with %d quark pair: ", checkPdgQuarkOne) + << nEventsInjOne << "\n"; + std::cout << Form("# events injected with %d quark pair: ", checkPdgQuarkTwo) + << nEventsInjTwo << "\n"; + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || + nEventsMB > nEvents * (1 - ratioTrigger) * + 1.05) { // we put some tolerance since the number of + // generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if ((nEventsInjOne + nEventsInjTwo) < nEvents * ratioTrigger * 0.95 || + (nEventsInjOne + nEventsInjTwo) > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of generated events injected with " << checkPdgQuarkOne << " and " << checkPdgQuarkTwo + << " different than expected\n"; + return 1; + } + if (std::abs(float(nEventsInjOne) / nEventsInjTwo - ratioQuarkOneTwo) / ratioQuarkOneTwo > 0.1) { + std::cerr << "Ratio between quark " << checkPdgQuarkOne << " and quark " << checkPdgQuarkTwo + << " different than expected\n"; + return 1; + } + std::cout << "#muons: " << nMuons << "\n"; + std::cout << "#muons in acceptance: " << nMuonsInAcceptance << "\n"; + + return 0; +} // external From cf926bc10a8320f0f3827107915bea6bddb68822 Mon Sep 17 00:00:00 2001 From: Cristian-Moscatelli Date: Wed, 4 Nov 2026 15:28:08 +0100 Subject: [PATCH 128/144] Add coalescence-based deuteron production generator (#2535) * "Update Pythia8 config for 136 TeV" * Fix file path for GeneratorLF_Coalescence_pp1360TeV.ini * Update script to use new coalescence generator * Specified test script --- .../ini/GeneratorLF_Coalescence_pp1360TeV.ini | 7 +++++ .../run_coalescence_deuteron_pp_1360TeV.sh | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 MC/config/PWGLF/ini/GeneratorLF_Coalescence_pp1360TeV.ini create mode 100644 MC/run/PWGLF/run_coalescence_deuteron_pp_1360TeV.sh diff --git a/MC/config/PWGLF/ini/GeneratorLF_Coalescence_pp1360TeV.ini b/MC/config/PWGLF/ini/GeneratorLF_Coalescence_pp1360TeV.ini new file mode 100644 index 000000000..84eac76ac --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorLF_Coalescence_pp1360TeV.ini @@ -0,0 +1,7 @@ +#---> GeneratorLF_Coalescence_pp536TeV +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_coalescence.C +funcName = generateCoalescence({1000010020, 1000010030, 1000020030}, 1, 0.239) + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator/pythia8_inel_136tev.cfg diff --git a/MC/run/PWGLF/run_coalescence_deuteron_pp_1360TeV.sh b/MC/run/PWGLF/run_coalescence_deuteron_pp_1360TeV.sh new file mode 100644 index 000000000..a12185824 --- /dev/null +++ b/MC/run/PWGLF/run_coalescence_deuteron_pp_1360TeV.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# +# A example workflow MC->RECO->AOD for a simple pp min bias +# production, with the LF coalescence generator (GeneratorLF_Coalescence_pp1360TeV.ini) + +# make sure O2DPG + O2 is loaded +[ ! "${O2DPG_ROOT}" ] && echo "Error: This needs O2DPG loaded" && exit 1 +[ ! "${O2_ROOT}" ] && echo "Error: This needs O2 loaded" && exit 1 + + +# ----------- START ACTUAL JOB ----------------------------- + +NWORKERS=${NWORKERS:-8} +MODULES="--skipModules ZDC" +SIMENGINE=${SIMENGINE:-TGeant4} +NSIGEVENTS=${NSIGEVENTS:-1} +NTIMEFRAMES=${NTIMEFRAMES:-1} +INTRATE=${INTRATE:-500000} +SYSTEM=${SYSTEM:-pp} +ENERGY=${ENERGY:-13600} +[[ ${SPLITID} != "" ]] && SEED="-seed ${SPLITID}" || SEED="" + +# create workflow +${O2DPG_ROOT}/MC/bin/o2dpg_sim_workflow.py -eCM ${ENERGY} -col ${SYSTEM} -gen external -j ${NWORKERS} -ns ${NSIGEVENTS} -tf ${NTIMEFRAMES} -interactionRate ${INTRATE} -confKey "Diamond.width[0]=0.005;Diamond.width[1]=0.005;Diamond.width[2]=6." -e ${SIMENGINE} ${SEED} \ + -ini $O2DPG_ROOT/MC/config/PWGLF/ini/GeneratorLF_Coalescence_pp1360TeV.ini + +# run workflow +${O2DPG_ROOT}/MC/bin/o2_dpg_workflow_runner.py -f workflow.json -tt aod --cpu-limit 32 From 65e9025aeb31326de11ff9e4389485ee9226eefd Mon Sep 17 00:00:00 2001 From: Alberto Caliva Date: Sat, 24 Oct 2026 12:39:27 +0200 Subject: [PATCH 129/144] Add MC filtering tools for AO2D files --- .../filterAO2DGapTriggeredMCEvents.C | 1261 ++++++++++++++++ .../filterAO2DMCParticlesPdg.C | 1293 +++++++++++++++++ 2 files changed, 2554 insertions(+) create mode 100644 MC/utils/MCFilteringTools/filterAO2DGapTriggeredMCEvents.C create mode 100644 MC/utils/MCFilteringTools/filterAO2DMCParticlesPdg.C diff --git a/MC/utils/MCFilteringTools/filterAO2DGapTriggeredMCEvents.C b/MC/utils/MCFilteringTools/filterAO2DGapTriggeredMCEvents.C new file mode 100644 index 000000000..ad0a172d5 --- /dev/null +++ b/MC/utils/MCFilteringTools/filterAO2DGapTriggeredMCEvents.C @@ -0,0 +1,1261 @@ +//============================================================================= +// filterAO2DGapTriggeredMCEvents.C +// +// Filter MC particles in gap-triggered AO2Ds while keeping all reconstructed +// information unchanged. minimumBiasSubGeneratorId identifies MB events; any +// other sub-generator is treated as interesting. +// +// Keep: +// - all particles from interesting MC events; +// - MB particles matched to tracks reconstructed in interesting events; +// - recursively, all mothers and descendants of rescued cross-event particles. +// Removed MC-particle references are remapped to -1. Daughter slices are kept +// for the surviving daughters, preserving their order. +// +// Rewritten: O2mcparticle_001, O2mctracklabel, O2mcmfttracklabel, +// O2mcfwdtracklabel, O2mccalolabel_001. +// All other AO2D objects are copied unchanged. +// +// Usage: +// root -l -b -q 'filterAO2DGapTriggeredMCEvents.C("AO2D.root","AO2D_filter.root",0)' +// +// Alberto Calivà (alberto.caliva@cern.ch) +//============================================================================= +#include +#include +#include +#include +#include +#include "SimulationDataFormat/MCGenProperties.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace { +constexpr int kMaxMothers = 256; +constexpr int kMaxCaloLabels = 4096; +struct LabelDiagnostics { + Long64_t crossEventAssociations = 0; + Long64_t uniqueParticlesAdded = 0; + Long64_t rows = 0; + Long64_t invalidated = 0; +}; +struct DirectoryDiagnostics { + std::string name; + Long64_t mcCollisionsTotal = 0; + Long64_t mcCollisionsInteresting = 0; + Long64_t mcCollisionsNonInteresting = 0; + Long64_t recoCollisionsTotal = 0; + Long64_t recoCollisionsInteresting = 0; + Long64_t particlesInitial = 0; + Long64_t particlesInterestingEvents = 0; + Long64_t particlesMinimumBiasEvents = 0; + Long64_t particlesInterestingSeeds = 0; + Long64_t particlesCrossEventAdded = 0; + Long64_t particlesDirectDaughtersAdded = 0; + Long64_t particlesMotherAncestryAdded = 0; + Long64_t particlesRetained = 0; + Long64_t particlesRemoved = 0; + Long64_t emptyDaughterSlicesCleared = 0; + LabelDiagnostics tracks; + LabelDiagnostics mft; + LabelDiagnostics fwd; + Long64_t caloRows = 0; + Long64_t caloInvalidated = 0; +}; +struct GlobalDiagnostics { + Long64_t directoriesProcessed = 0; + Long64_t directoriesFailed = 0; + Long64_t mcCollisionsTotal = 0; + Long64_t mcCollisionsInteresting = 0; + Long64_t mcCollisionsNonInteresting = 0; + Long64_t recoCollisionsTotal = 0; + Long64_t recoCollisionsInteresting = 0; + Long64_t particlesInitial = 0; + Long64_t particlesInterestingEvents = 0; + Long64_t particlesMinimumBiasEvents = 0; + Long64_t particlesInterestingSeeds = 0; + Long64_t particlesCrossEventAdded = 0; + Long64_t particlesDirectDaughtersAdded = 0; + Long64_t particlesMotherAncestryAdded = 0; + Long64_t particlesRetained = 0; + Long64_t particlesRemoved = 0; + Long64_t emptyDaughterSlicesCleared = 0; + LabelDiagnostics tracks; + LabelDiagnostics mft; + LabelDiagnostics fwd; + Long64_t caloRows = 0; + Long64_t caloInvalidated = 0; +}; +void accumulateLabelDiagnostics(LabelDiagnostics& global, const LabelDiagnostics& local) { + global.crossEventAssociations += local.crossEventAssociations; + global.uniqueParticlesAdded += local.uniqueParticlesAdded; + global.rows += local.rows; + global.invalidated += local.invalidated; +} +void accumulateDirectoryDiagnostics(GlobalDiagnostics& global, + const DirectoryDiagnostics& local) { + ++global.directoriesProcessed; + global.mcCollisionsTotal += local.mcCollisionsTotal; + global.mcCollisionsInteresting += local.mcCollisionsInteresting; + global.mcCollisionsNonInteresting += local.mcCollisionsNonInteresting; + global.recoCollisionsTotal += local.recoCollisionsTotal; + global.recoCollisionsInteresting += local.recoCollisionsInteresting; + global.particlesInitial += local.particlesInitial; + global.particlesInterestingEvents += local.particlesInterestingEvents; + global.particlesMinimumBiasEvents += local.particlesMinimumBiasEvents; + global.particlesInterestingSeeds += local.particlesInterestingSeeds; + global.particlesCrossEventAdded += local.particlesCrossEventAdded; + global.particlesDirectDaughtersAdded += local.particlesDirectDaughtersAdded; + global.particlesMotherAncestryAdded += local.particlesMotherAncestryAdded; + global.particlesRetained += local.particlesRetained; + global.particlesRemoved += local.particlesRemoved; + global.emptyDaughterSlicesCleared += local.emptyDaughterSlicesCleared; + accumulateLabelDiagnostics(global.tracks, local.tracks); + accumulateLabelDiagnostics(global.mft, local.mft); + accumulateLabelDiagnostics(global.fwd, local.fwd); + global.caloRows += local.caloRows; + global.caloInvalidated += local.caloInvalidated; +} +double fractionPercent(Long64_t numerator, Long64_t denominator) { + return denominator > 0 ? 100.0 * static_cast(numerator) / static_cast(denominator) : 0.0; +} +Long64_t getFileSizeBytes(const char* fileName) { + std::ifstream stream(fileName, std::ios::binary | std::ios::ate); + if (!stream) { + return -1; + } + return static_cast(stream.tellg()); +} +std::string makeDefaultLogFileName(const char* outputFileName) { + std::string name(outputFileName); + const std::string suffix = ".root"; + if (name.size() >= suffix.size() && + name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) { + name.resize(name.size() - suffix.size()); + } + return name + "_log.txt"; +} +std::string currentTimeString() { + const std::time_t now = std::time(nullptr); + char buffer[64] = {}; + const std::tm* localTime = std::localtime(&now); + if (!localTime) { + return "unknown"; + } + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localTime); + return buffer; +} +void writeDirectoryDiagnostics(std::ostream& out, const DirectoryDiagnostics& d) { + out << "------------------------------------------------------------\n" + << d.name << "\n" + << "------------------------------------------------------------\n\n"; + out << "MC collisions:\n" + << " total: " << d.mcCollisionsTotal << "\n" + << " interesting: " << d.mcCollisionsInteresting << "\n" + << " non-interesting / MB: " << d.mcCollisionsNonInteresting << "\n\n"; + out << "Reconstructed collisions:\n" + << " total: " << d.recoCollisionsTotal << "\n" + << " associated with interesting MC events: " << d.recoCollisionsInteresting << "\n\n"; + out << "MC particles:\n" + << " initial: " << d.particlesInitial << "\n" + << " particles in interesting MC events: " << d.particlesInterestingEvents << "\n" + << " particles in minimum-bias MC events: " << d.particlesMinimumBiasEvents << "\n" + << std::fixed << std::setprecision(4) + << " interesting / MB particle ratio: " + << (d.particlesMinimumBiasEvents > 0 + ? static_cast(d.particlesInterestingEvents) / + static_cast(d.particlesMinimumBiasEvents) + : 0.0) + << "\n" + << " interesting-event seeds: " << d.particlesInterestingSeeds << "\n" + << " cross-event MB particles added: " << d.particlesCrossEventAdded << "\n" + << " descendants added: " << d.particlesDirectDaughtersAdded << "\n" + << " mother-ancestry particles added: " << d.particlesMotherAncestryAdded << "\n" + << " retained: " << d.particlesRetained << "\n" + << " removed: " << d.particlesRemoved << "\n" + << std::fixed << std::setprecision(2) + << " retained fraction: " + << fractionPercent(d.particlesRetained, d.particlesInitial) << " %\n" + << " removed fraction: " + << fractionPercent(d.particlesRemoved, d.particlesInitial) << " %\n\n"; + const Long64_t interestingParticles = d.particlesInterestingSeeds; + const Long64_t mbParticles = d.particlesInitial - d.particlesInterestingSeeds; + out << std::fixed << std::setprecision(2) + << "Average MC-particle multiplicity:\n" + << " particles / interesting MC collision: " + << (d.mcCollisionsInteresting > 0 + ? static_cast(interestingParticles) / + static_cast(d.mcCollisionsInteresting) + : 0.0) + << "\n" + << " particles / non-interesting MC collision: " + << (d.mcCollisionsNonInteresting > 0 + ? static_cast(mbParticles) / + static_cast(d.mcCollisionsNonInteresting) + : 0.0) + << "\n\n"; + out << "Cross-event associations (interesting reco -> non-interesting MC):\n" + << " central-barrel track labels: " << d.tracks.crossEventAssociations << "\n" + << " unique MC particles newly added: " << d.tracks.uniqueParticlesAdded << "\n" + << " MFT track labels: " << d.mft.crossEventAssociations << "\n" + << " unique MC particles newly added: " << d.mft.uniqueParticlesAdded << "\n" + << " forward track labels: " << d.fwd.crossEventAssociations << "\n" + << " unique MC particles newly added: " << d.fwd.uniqueParticlesAdded << "\n\n"; + out << "Label remapping:\n" + << " O2mctracklabel rows: " << d.tracks.rows << "\n" + << " newly invalidated: " << d.tracks.invalidated << "\n" + << " O2mcmfttracklabel rows: " << d.mft.rows << "\n" + << " newly invalidated: " << d.mft.invalidated << "\n" + << " O2mcfwdtracklabel rows: " << d.fwd.rows << "\n" + << " newly invalidated: " << d.fwd.invalidated << "\n" + << " O2mccalolabel_001 rows: " << d.caloRows << "\n" + << " newly invalidated references: " << d.caloInvalidated << "\n\n"; + out << "Genealogy:\n" + << " empty daughter slices cleared: " + << d.emptyDaughterSlicesCleared << "\n\n"; +} +void writeGlobalDiagnostics(std::ostream& out, + const GlobalDiagnostics& g, + Long64_t inputSizeBytes, + Long64_t outputSizeBytes) { + out << "============================================================\n" + << "GLOBAL SUMMARY\n" + << "============================================================\n\n"; + out << "Directories:\n" + << " successfully processed: " << g.directoriesProcessed << "\n" + << " failed: " << g.directoriesFailed << "\n\n"; + out << "MC collisions:\n" + << " total: " << g.mcCollisionsTotal << "\n" + << " interesting: " << g.mcCollisionsInteresting << "\n" + << " non-interesting / MB: " << g.mcCollisionsNonInteresting << "\n\n"; + out << "Reconstructed collisions:\n" + << " total: " << g.recoCollisionsTotal << "\n" + << " associated with interesting MC events: " << g.recoCollisionsInteresting << "\n\n"; + out << "MC particles:\n" + << " initial: " << g.particlesInitial << "\n" + << " particles in interesting MC events: " << g.particlesInterestingEvents << "\n" + << " particles in minimum-bias MC events: " << g.particlesMinimumBiasEvents << "\n" + << std::fixed << std::setprecision(4) + << " interesting / MB particle ratio: " + << (g.particlesMinimumBiasEvents > 0 + ? static_cast(g.particlesInterestingEvents) / + static_cast(g.particlesMinimumBiasEvents) + : 0.0) + << "\n" + << std::fixed << std::setprecision(2) + << " avg particles / interesting MC event: " + << (g.mcCollisionsInteresting > 0 + ? static_cast(g.particlesInterestingEvents) / static_cast(g.mcCollisionsInteresting) + : 0.0) + << "\n" + << " avg particles / minimum-bias MC event: " + << (g.mcCollisionsNonInteresting > 0 + ? static_cast(g.particlesMinimumBiasEvents) / static_cast(g.mcCollisionsNonInteresting) + : 0.0) + << "\n" + << " interesting-event seeds: " << g.particlesInterestingSeeds << "\n" + << " cross-event MB particles added: " << g.particlesCrossEventAdded << "\n" + << " descendants added: " << g.particlesDirectDaughtersAdded << "\n" + << " mother-ancestry particles added: " << g.particlesMotherAncestryAdded << "\n" + << " retained: " << g.particlesRetained << "\n" + << " removed: " << g.particlesRemoved << "\n" + << std::fixed << std::setprecision(2) + << " retained fraction: " + << fractionPercent(g.particlesRetained, g.particlesInitial) << " %\n" + << " removed fraction: " + << fractionPercent(g.particlesRemoved, g.particlesInitial) << " %\n\n"; + const Long64_t rescuedMBParticles = + g.particlesCrossEventAdded + g.particlesDirectDaughtersAdded + g.particlesMotherAncestryAdded; + out << "MB rescue contribution:\n" + << " rescued MB particles: " << rescuedMBParticles << "\n" + << " fraction of retained MC particles: " + << fractionPercent(rescuedMBParticles, g.particlesRetained) << " %\n\n"; + out << "Cross-event associations:\n" + << " central barrel: " << g.tracks.crossEventAssociations << "\n" + << " MFT: " << g.mft.crossEventAssociations << "\n" + << " forward: " << g.fwd.crossEventAssociations << "\n\n"; + out << "Label remapping:\n" + << " track labels newly invalidated: " << g.tracks.invalidated << "\n" + << " MFT labels newly invalidated: " << g.mft.invalidated << "\n" + << " forward labels newly invalidated: " << g.fwd.invalidated << "\n" + << " calorimeter references invalidated: " << g.caloInvalidated << "\n\n"; + out << "Genealogy:\n" + << " empty daughter slices cleared: " + << g.emptyDaughterSlicesCleared << "\n\n"; + if (inputSizeBytes >= 0 && outputSizeBytes >= 0) { + const double inputMB = static_cast(inputSizeBytes) / (1024.0 * 1024.0); + const double outputMB = static_cast(outputSizeBytes) / (1024.0 * 1024.0); + const double reduction = + inputSizeBytes > 0 + ? 100.0 * (1.0 - static_cast(outputSizeBytes) / + static_cast(inputSizeBytes)) + : 0.0; + const double compression = + outputSizeBytes > 0 + ? static_cast(inputSizeBytes) / + static_cast(outputSizeBytes) + : 0.0; + out << std::fixed << std::setprecision(2) + << "File size:\n" + << " input: " << inputMB << " MB\n" + << " output: " << outputMB << " MB\n" + << " storage reduction: " << reduction << " %\n" + << " compression factor: " << compression << "x\n\n"; + } +} +struct StoredMCParticle { + int mcCollisionId = -1; + int pdgCode = 0; + int statusCode = 0; + UChar_t flags = 0; + std::vector mothers; + std::array daughters = {-1, -1}; + float weight = 0.f; + float px = 0.f; + float py = 0.f; + float pz = 0.f; + float e = 0.f; + float vx = 0.f; + float vy = 0.f; + float vz = 0.f; + float vt = 0.f; +}; +int getSubGeneratorIdFromGeneratorsID(Short_t generatorsID) { + return o2::mcgenid::getSubGeneratorId(generatorsID); +} +bool isInterestingSubGenerator(int subGeneratorId, int minimumBiasSubGeneratorId) { + return subGeneratorId != minimumBiasSubGeneratorId; +} +int remapMCParticleId(int oldId, const std::vector& oldToNew) { + if (oldId < 0 || oldId >= static_cast(oldToNew.size())) { + return -1; + } + return oldToNew[oldId]; +} +bool isRewrittenTree(const std::string& treeName) { + static const std::unordered_set rewrittenTrees = { + "O2mcparticle_001", + "O2mctracklabel", + "O2mcmfttracklabel", + "O2mcfwdtracklabel", + "O2mccalolabel_001"}; + return rewrittenTrees.count(treeName) != 0; +} +TTree* findTree(TDirectory* directory, std::initializer_list candidates) { + for (const char* name : candidates) { + if (auto* tree = dynamic_cast(directory->Get(name))) { + return tree; + } + } + return nullptr; +} +TTree* findMCCollisionTree(TDirectory* directory) { + return findTree(directory,{"O2mccollision_001", "O2mccollision", "O2mccollision_000"}); +} +TTree* findMCCollisionLabelTree(TDirectory* directory) { + return findTree(directory,{"O2mccollisionlabel", "O2mccollisionlabel_001", "O2mccollisionlabel_000"}); +} +TTree* findCollisionTree(TDirectory* directory) { + return findTree(directory,{"O2collision_001", "O2collision", "O2collision_000"}); +} +TTree* findTrackTreeForLabel(TDirectory* directory, const char* labelTreeName, Long64_t expectedEntries) { + TTree* tree = nullptr; + const std::string labelName(labelTreeName); + if (labelName == "O2mctracklabel") { + tree = findTree(directory, {"O2track_iu", "O2track", "O2track_001", "O2track_000"}); + } else if (labelName == "O2mcmfttracklabel") { + tree = findTree(directory, {"O2mfttrack_001", "O2mfttrack", "O2mfttrack_000"}); + } else if (labelName == "O2mcfwdtracklabel") { + tree = findTree(directory, {"O2fwdtrack_001", "O2fwdtrack", "O2fwdtrack_000"}); + } + if (!tree) { + return nullptr; + } + if (tree->GetEntries() != expectedEntries) { + std::cerr << "Warning: " << labelTreeName << " has " << expectedEntries + << " rows, but candidate reconstructed track table " << tree->GetName() + << " has " << tree->GetEntries() + << " rows. Cross-event seeding from this table will be skipped.\n"; + return nullptr; + } + if (!tree->GetBranch("fIndexCollisions")) { + std::cerr << "Warning: " << tree->GetName() + << " has no fIndexCollisions branch. Cross-event seeding from " + << labelTreeName << " will be skipped.\n"; + return nullptr; + } + return tree; +} +bool buildMCCollisionSubGeneratorMap(TDirectory* inputDirectory, std::vector& subGeneratorIdByCollision) { + auto* mcCollisionTree = findMCCollisionTree(inputDirectory); + if (!mcCollisionTree) { + std::cerr << "Missing MC-collision table\n"; + return false; + } + if (!mcCollisionTree->GetBranch("fGeneratorsID")) { + std::cerr << mcCollisionTree->GetName() + << ": missing required branch fGeneratorsID\n"; + return false; + } + Short_t generatorsID = 0; + mcCollisionTree->SetBranchAddress("fGeneratorsID", &generatorsID); + const Long64_t numberOfCollisions = mcCollisionTree->GetEntries(); + subGeneratorIdByCollision.resize(numberOfCollisions, -1); + std::unordered_map collisionCountsBySubGeneratorId; + for (Long64_t collisionId = 0; collisionId < numberOfCollisions; ++collisionId) { + mcCollisionTree->GetEntry(collisionId); + const int subGeneratorId = getSubGeneratorIdFromGeneratorsID(generatorsID); + subGeneratorIdByCollision[collisionId] = subGeneratorId; + ++collisionCountsBySubGeneratorId[subGeneratorId]; + } + std::cout << mcCollisionTree->GetName() << ": decoded sub-generator-ID population:\n"; + for (const auto& entry : collisionCountsBySubGeneratorId) { + std::cout << " sub-generator ID " << entry.first << ": " << entry.second << " MC collisions\n"; + } + return true; +} +bool buildRecoCollisionInterestingMap(TDirectory* inputDirectory, + const std::vector& subGeneratorIdByCollision, + int minimumBiasSubGeneratorId, + std::vector& isInterestingRecoCollision, + DirectoryDiagnostics& diagnostics) { + auto* collisionTree = findCollisionTree(inputDirectory); + auto* mcCollisionLabelTree = findMCCollisionLabelTree(inputDirectory); + if (!collisionTree) { + std::cerr << "Missing reconstructed collision table; cannot identify " + << "interesting reconstructed events\n"; + return false; + } + if (!mcCollisionLabelTree) { + std::cerr << "Missing O2mccollisionlabel table; cannot identify " + << "interesting reconstructed events\n"; + return false; + } + if (!mcCollisionLabelTree->GetBranch("fIndexMcCollisions")) { + std::cerr << mcCollisionLabelTree->GetName() + << ": missing fIndexMcCollisions\n"; + return false; + } + const Long64_t numberOfRecoCollisions = collisionTree->GetEntries(); + if (mcCollisionLabelTree->GetEntries() != numberOfRecoCollisions) { + std::cerr << "Mismatch: " << collisionTree->GetName() << " has " + << numberOfRecoCollisions << " rows while " + << mcCollisionLabelTree->GetName() << " has " + << mcCollisionLabelTree->GetEntries() << " rows\n"; + return false; + } + int mcCollisionId = -1; + mcCollisionLabelTree->SetBranchAddress("fIndexMcCollisions", &mcCollisionId); + isInterestingRecoCollision.assign(numberOfRecoCollisions, 0); + Long64_t interestingRecoCollisions = 0; + Long64_t invalidMcCollisionLabels = 0; + for (Long64_t recoCollisionId = 0; + recoCollisionId < numberOfRecoCollisions; + ++recoCollisionId) { + mcCollisionLabelTree->GetEntry(recoCollisionId); + if (mcCollisionId < 0 || + mcCollisionId >= static_cast(subGeneratorIdByCollision.size())) { + if (mcCollisionId >= 0) { + ++invalidMcCollisionLabels; + } + continue; + } + if (isInterestingSubGenerator(subGeneratorIdByCollision[mcCollisionId], minimumBiasSubGeneratorId)) { + isInterestingRecoCollision[recoCollisionId] = 1; + ++interestingRecoCollisions; + } + } + diagnostics.recoCollisionsTotal = numberOfRecoCollisions; + diagnostics.recoCollisionsInteresting = interestingRecoCollisions; + std::cout << "Reco collisions associated with non-MB MC events: " << interestingRecoCollisions + << " / " << numberOfRecoCollisions << "\n"; + if (invalidMcCollisionLabels > 0) { + std::cerr << "Warning: " << invalidMcCollisionLabels + << " reconstructed collisions have out-of-range MC-collision labels\n"; + } + return true; +} +void copyUnchangedObjects(TDirectory* inputDirectory, TDirectory* outputDirectory) { + TIter nextKey(inputDirectory->GetListOfKeys()); + TKey* key = nullptr; + while ((key = static_cast(nextKey()))) { + if (key->GetCycle() != inputDirectory->GetKey(key->GetName())->GetCycle()) { + continue; + } + TObject* object = key->ReadObj(); + const std::string objectName = object->GetName(); + if (object->InheritsFrom(TDirectory::Class())) { + outputDirectory->cd(); + auto* newDirectory = outputDirectory->mkdir(objectName.c_str()); + copyUnchangedObjects(static_cast(object), newDirectory); + delete object; + continue; + } + if (object->InheritsFrom(TTree::Class())) { + if (isRewrittenTree(objectName)) { + delete object; + continue; + } + auto* inputTree = static_cast(object); + outputDirectory->cd(); + auto* outputTree = inputTree->CloneTree(-1, "fast"); + outputTree->Write(objectName.c_str(), TObject::kOverwrite); + delete object; + continue; + } + outputDirectory->cd(); + object->Write(objectName.c_str(), TObject::kOverwrite); + delete object; + } +} +void markCrossEventTrackReferences(TDirectory* inputDirectory, + const char* labelTreeName, + const std::vector& isInterestingRecoCollision, + const std::vector& particleMcCollisionId, + const std::vector& subGeneratorIdByCollision, + int minimumBiasSubGeneratorId, + std::vector& keepParticle, + std::vector& motherWorkList, + std::vector& crossEventSeeds, + LabelDiagnostics& diagnostics) { + auto* labelTree = dynamic_cast(inputDirectory->Get(labelTreeName)); + if (!labelTree) { + return; + } + if (!labelTree->GetBranch("fIndexMcParticles")) { + std::cerr << labelTreeName + << ": missing fIndexMcParticles; cross-event seeding skipped\n"; + return; + } + auto* trackTree = findTrackTreeForLabel(inputDirectory, + labelTreeName, + labelTree->GetEntries()); + if (!trackTree) { + return; + } + int mcParticleId = -1; + int recoCollisionId = -1; + labelTree->SetBranchAddress("fIndexMcParticles", &mcParticleId); + trackTree->SetBranchAddress("fIndexCollisions", &recoCollisionId); + Long64_t crossEventMatches = 0; + Long64_t newlyMarked = 0; + Long64_t invalidParticleReferences = 0; + Long64_t invalidRecoCollisionReferences = 0; + const Long64_t numberOfRows = labelTree->GetEntries(); + for (Long64_t row = 0; row < numberOfRows; ++row) { + labelTree->GetEntry(row); + trackTree->GetEntry(row); + if (mcParticleId < 0) { + continue; + } + if (mcParticleId >= static_cast(particleMcCollisionId.size())) { + ++invalidParticleReferences; + continue; + } + if (recoCollisionId < 0 || + recoCollisionId >= static_cast(isInterestingRecoCollision.size())) { + if (recoCollisionId >= 0) { + ++invalidRecoCollisionReferences; + } + continue; + } + // Only tracks reconstructed in an interesting event may rescue an MB MC + // particle. Normal MB-reco -> MB-MC labels do not seed the keep mask. + if (!isInterestingRecoCollision[recoCollisionId]) { + continue; + } + const int mcCollisionId = particleMcCollisionId[mcParticleId]; + if (mcCollisionId < 0 || + mcCollisionId >= static_cast(subGeneratorIdByCollision.size())) { + continue; + } + // If the MC particle already belongs to the interesting event, it is already + // kept by the primary seed rule. We only count/mark genuine cross-event cases. + if (isInterestingSubGenerator(subGeneratorIdByCollision[mcCollisionId], minimumBiasSubGeneratorId)) { + continue; + } + ++crossEventMatches; + // Record every unique genuine cross-event particle. This separate seed list is + // used later to keep ONLY its direct daughters. + if (std::find(crossEventSeeds.begin(), crossEventSeeds.end(), mcParticleId) == + crossEventSeeds.end()) { + crossEventSeeds.push_back(mcParticleId); + } + if (!keepParticle[mcParticleId]) { + keepParticle[mcParticleId] = 1; + motherWorkList.push_back(mcParticleId); + ++newlyMarked; + } + } + diagnostics.crossEventAssociations = crossEventMatches; + diagnostics.uniqueParticlesAdded = newlyMarked; + std::cout << labelTreeName << ": found " << crossEventMatches + << " interesting-reco -> non-interesting-MC associations; added " + << newlyMarked << " MC particles to the keep set\n"; + if (invalidParticleReferences > 0) { + std::cerr << "Warning: " << labelTreeName << " contains " + << invalidParticleReferences + << " out-of-range MC-particle references\n"; + } + if (invalidRecoCollisionReferences > 0) { + std::cerr << "Warning: " << trackTree->GetName() << " contains " + << invalidRecoCollisionReferences + << " out-of-range reconstructed-collision references\n"; + } +} +bool rewriteMCParticles(TDirectory* inputDirectory, + TDirectory* outputDirectory, + const std::vector& subGeneratorIdByCollision, + const std::vector& isInterestingRecoCollision, + int minimumBiasSubGeneratorId, + std::vector& oldToNew, + DirectoryDiagnostics& diagnostics) { + auto* inputTree = dynamic_cast(inputDirectory->Get("O2mcparticle_001")); + if (!inputTree) { + std::cerr << "Missing O2mcparticle_001\n"; + return false; + } + int mcCollisionId = -1; + int pdgCode = 0; + int statusCode = 0; + UChar_t flags = 0; + int mothersSize = 0; + int mothers[kMaxMothers] = {}; + int daughtersSlice[2] = {-1, -1}; + float weight = 0.f; + float px = 0.f; + float py = 0.f; + float pz = 0.f; + float e = 0.f; + float vx = 0.f; + float vy = 0.f; + float vz = 0.f; + float vt = 0.f; + inputTree->SetBranchAddress("fIndexMcCollisions", &mcCollisionId); + inputTree->SetBranchAddress("fPdgCode", &pdgCode); + inputTree->SetBranchAddress("fStatusCode", &statusCode); + inputTree->SetBranchAddress("fFlags", &flags); + inputTree->SetBranchAddress("fIndexArray_Mothers_size", &mothersSize); + inputTree->SetBranchAddress("fIndexArray_Mothers", mothers); + inputTree->SetBranchAddress("fIndexSlice_Daughters", daughtersSlice); + inputTree->SetBranchAddress("fWeight", &weight); + inputTree->SetBranchAddress("fPx", &px); + inputTree->SetBranchAddress("fPy", &py); + inputTree->SetBranchAddress("fPz", &pz); + inputTree->SetBranchAddress("fE", &e); + inputTree->SetBranchAddress("fVx", &vx); + inputTree->SetBranchAddress("fVy", &vy); + inputTree->SetBranchAddress("fVz", &vz); + inputTree->SetBranchAddress("fVt", &vt); + const Long64_t numberOfParticles = inputTree->GetEntries(); + diagnostics.particlesInitial = numberOfParticles; + oldToNew.assign(numberOfParticles, -1); + std::vector particleMcCollisionId(numberOfParticles, -1); + std::vector> originalMothers(numberOfParticles); + std::vector> originalDaughters(numberOfParticles, {-1, -1}); + std::vector keepParticle(numberOfParticles, 0); + std::vector motherWorkList; + std::vector crossEventSeeds; + motherWorkList.reserve(numberOfParticles); + crossEventSeeds.reserve(1024); + Long64_t particlesFromInterestingEvents = 0; + Long64_t particlesFromMinimumBiasEvents = 0; + Long64_t invalidCollisionReferences = 0; + Long64_t invalidMotherReferences = 0; + Long64_t invalidDaughterSlices = 0; + const int maxMothersSize = static_cast(inputTree->GetMaximum("fIndexArray_Mothers_size")); + if (maxMothersSize > kMaxMothers) { + std::cerr << "Error: maximum number of mothers (" << maxMothersSize + << ") exceeds buffer size (" << kMaxMothers << ")\n"; + return false; + } + // First pass: store genealogy and seed with ALL particles from interesting MC events. + for (Long64_t oldParticleId = 0; oldParticleId < numberOfParticles; ++oldParticleId) { + inputTree->GetEntry(oldParticleId); + particleMcCollisionId[oldParticleId] = mcCollisionId; + const int safeMothersSize = std::min(mothersSize, kMaxMothers); + if (mothersSize > kMaxMothers) { + std::cerr << "Warning: particle " << oldParticleId << " has " << mothersSize + << " mothers; only the first " << kMaxMothers << " are read\n"; + } + for (int motherIndex = 0; motherIndex < safeMothersSize; ++motherIndex) { + const int motherId = mothers[motherIndex]; + if (motherId < 0) { + continue; + } + if (motherId >= numberOfParticles) { + ++invalidMotherReferences; + continue; + } + originalMothers[oldParticleId].push_back(motherId); + } + const int firstDaughter = daughtersSlice[0]; + const int lastDaughter = daughtersSlice[1]; + if (firstDaughter >= 0 || lastDaughter >= 0) { + if (firstDaughter >= 0 && + lastDaughter >= firstDaughter && + lastDaughter < numberOfParticles) { + originalDaughters[oldParticleId] = {firstDaughter, lastDaughter}; + } else { + ++invalidDaughterSlices; + } + } + if (mcCollisionId < 0 || + mcCollisionId >= static_cast(subGeneratorIdByCollision.size())) { + ++invalidCollisionReferences; + continue; + } + if (isInterestingSubGenerator(subGeneratorIdByCollision[mcCollisionId], minimumBiasSubGeneratorId)) { + keepParticle[oldParticleId] = 1; + ++particlesFromInterestingEvents; + } else { + ++particlesFromMinimumBiasEvents; + } + } + diagnostics.particlesInterestingEvents = particlesFromInterestingEvents; + diagnostics.particlesMinimumBiasEvents = particlesFromMinimumBiasEvents; + diagnostics.particlesInterestingSeeds = particlesFromInterestingEvents; + std::cout << "O2mcparticle_001: initially marked " + << particlesFromInterestingEvents + << " particles from non-MB MC events\n"; + // Cross-event exceptions only: + // interesting reco track -> MC particle from a non-interesting MC event. + markCrossEventTrackReferences(inputDirectory, + "O2mctracklabel", + isInterestingRecoCollision, + particleMcCollisionId, + subGeneratorIdByCollision, + minimumBiasSubGeneratorId, + keepParticle, + motherWorkList, + crossEventSeeds, + diagnostics.tracks); + markCrossEventTrackReferences(inputDirectory, + "O2mcmfttracklabel", + isInterestingRecoCollision, + particleMcCollisionId, + subGeneratorIdByCollision, + minimumBiasSubGeneratorId, + keepParticle, + motherWorkList, + crossEventSeeds, + diagnostics.mft); + markCrossEventTrackReferences(inputDirectory, + "O2mcfwdtracklabel", + isInterestingRecoCollision, + particleMcCollisionId, + subGeneratorIdByCollision, + minimumBiasSubGeneratorId, + keepParticle, + motherWorkList, + crossEventSeeds, + diagnostics.fwd); + diagnostics.particlesCrossEventAdded = + diagnostics.tracks.uniqueParticlesAdded + + diagnostics.mft.uniqueParticlesAdded + + diagnostics.fwd.uniqueParticlesAdded; + // Keep all descendants recursively for the genuine cross-event seed particles. + // Every newly retained descendant is also added to the mother-ancestry work list + // so that every retained particle still has valid mother references. + Long64_t directDaughtersAdded = 0; + std::vector daughterWorkList = crossEventSeeds; + std::vector daughterQueued(numberOfParticles, 0); + for (int crossEventParticleId : crossEventSeeds) { + daughterQueued[crossEventParticleId] = 1; + } + std::size_t nextDaughterParticle = 0; + while (nextDaughterParticle < daughterWorkList.size()) { + const int particleId = daughterWorkList[nextDaughterParticle++]; + const int firstDaughter = originalDaughters[particleId][0]; + const int lastDaughter = originalDaughters[particleId][1]; + if (firstDaughter < 0 || lastDaughter < firstDaughter) { + continue; + } + for (int daughterId = firstDaughter; daughterId <= lastDaughter; ++daughterId) { + if (!keepParticle[daughterId]) { + keepParticle[daughterId] = 1; + motherWorkList.push_back(daughterId); + ++directDaughtersAdded; + } + if (!daughterQueued[daughterId]) { + daughterQueued[daughterId] = 1; + daughterWorkList.push_back(daughterId); + } + } + } + diagnostics.particlesDirectDaughtersAdded = directDaughtersAdded; + std::cout << "O2mcparticle_001: added " << directDaughtersAdded + << " daughters through recursive descendant closure\n"; + // Recursive mother-ancestry closure. This is applied only to rescued cross-event + // particles and all their recursively retained descendants. Particles from interesting + // MC events are already kept together with their full event and do not seed this traversal. + // Newly retained mothers do NOT cause their other daughters/siblings to be kept. + std::size_t nextParticle = 0; + Long64_t particlesAddedByMotherAncestry = 0; + while (nextParticle < motherWorkList.size()) { + const int particleId = motherWorkList[nextParticle++]; + for (int motherId : originalMothers[particleId]) { + if (!keepParticle[motherId]) { + keepParticle[motherId] = 1; + motherWorkList.push_back(motherId); + ++particlesAddedByMotherAncestry; + } + } + } + diagnostics.particlesMotherAncestryAdded = particlesAddedByMotherAncestry; + std::cout << "O2mcparticle_001: added " << particlesAddedByMotherAncestry + << " particles through recursive mother-ancestry closure\n"; + Long64_t keptCount = 0; + for (Long64_t oldParticleId = 0; oldParticleId < numberOfParticles; ++oldParticleId) { + if (keepParticle[oldParticleId]) { + oldToNew[oldParticleId] = static_cast(keptCount++); + } + } + std::vector retainedParticles; + retainedParticles.reserve(keptCount); + Long64_t brokenMotherReferences = 0; + Long64_t emptyDaughterSlices = 0; + for (Long64_t oldParticleId = 0; oldParticleId < numberOfParticles; ++oldParticleId) { + if (!keepParticle[oldParticleId]) { + continue; + } + inputTree->GetEntry(oldParticleId); + StoredMCParticle particle; + particle.mcCollisionId = mcCollisionId; + particle.pdgCode = pdgCode; + particle.statusCode = statusCode; + particle.flags = flags; + particle.weight = weight; + particle.px = px; + particle.py = py; + particle.pz = pz; + particle.e = e; + particle.vx = vx; + particle.vy = vy; + particle.vz = vz; + particle.vt = vt; + for (int oldMotherId : originalMothers[oldParticleId]) { + const int newMotherId = remapMCParticleId(oldMotherId, oldToNew); + if (newMotherId >= 0) { + particle.mothers.push_back(newMotherId); + } else { + ++brokenMotherReferences; + } + } + const int oldFirstDaughter = originalDaughters[oldParticleId][0]; + const int oldLastDaughter = originalDaughters[oldParticleId][1]; + if (oldFirstDaughter >= 0 && oldLastDaughter >= oldFirstDaughter) { + // A daughter slice represents a contiguous interval + // [firstDaughter, lastDaughter]. Preserve the surviving daughters. + int newFirstDaughter = -1; + int previousNewDaughter = -1; + for (int oldDaughterId = oldFirstDaughter; + oldDaughterId <= oldLastDaughter; + ++oldDaughterId) { + const int newDaughterId = remapMCParticleId(oldDaughterId, oldToNew); + if (newDaughterId < 0) { + continue; + } + if (newFirstDaughter < 0) { + newFirstDaughter = newDaughterId; + } + previousNewDaughter = newDaughterId; + } + if (newFirstDaughter >= 0) { + particle.daughters = {newFirstDaughter, previousNewDaughter}; + } else { + particle.daughters = {-1, -1}; + ++emptyDaughterSlices; + } + } + retainedParticles.push_back(std::move(particle)); + } + outputDirectory->cd(); + auto* outputTree = inputTree->CloneTree(0); + for (const auto& particle : retainedParticles) { + mcCollisionId = particle.mcCollisionId; + pdgCode = particle.pdgCode; + statusCode = particle.statusCode; + flags = particle.flags; + weight = particle.weight; + px = particle.px; + py = particle.py; + pz = particle.pz; + e = particle.e; + vx = particle.vx; + vy = particle.vy; + vz = particle.vz; + vt = particle.vt; + mothersSize = static_cast(particle.mothers.size()); + for (int motherIndex = 0; motherIndex < mothersSize; ++motherIndex) { + mothers[motherIndex] = particle.mothers[motherIndex]; + } + daughtersSlice[0] = particle.daughters[0]; + daughtersSlice[1] = particle.daughters[1]; + outputTree->Fill(); + } + outputTree->Write("O2mcparticle_001", TObject::kOverwrite); + const double survivalFraction = numberOfParticles > 0 + ? static_cast(keptCount) / + static_cast(numberOfParticles) + : 0.; + diagnostics.particlesRetained = keptCount; + diagnostics.particlesRemoved = numberOfParticles - keptCount; + diagnostics.emptyDaughterSlicesCleared = emptyDaughterSlices; + std::cout << "O2mcparticle_001: retained " << keptCount << " / " + << numberOfParticles << " particles; survival fraction = " + << survivalFraction << "\n"; + if (invalidCollisionReferences > 0) { + std::cerr << "Warning: " << invalidCollisionReferences + << " MC particles had invalid MC-collision references\n"; + } + if (invalidMotherReferences > 0) { + std::cerr << "Warning: " << invalidMotherReferences + << " out-of-range mother references were found\n"; + } + if (invalidDaughterSlices > 0) { + std::cerr << "Warning: " << invalidDaughterSlices + << " invalid daughter slices were found\n"; + } + if (brokenMotherReferences > 0) { + std::cerr << "Warning: genealogy closure left " << brokenMotherReferences + << " broken mother references. This should normally be zero.\n"; + } + if (emptyDaughterSlices > 0) { + std::cout << "O2mcparticle_001: cleared " << emptyDaughterSlices + << " empty daughter slices after filtering\n"; + } + return true; +} +void rewriteSingleMCLabelTable(TDirectory* inputDirectory, + TDirectory* outputDirectory, + const char* treeName, + const std::vector& oldToNew, + LabelDiagnostics& diagnostics) { + auto* inputTree = dynamic_cast(inputDirectory->Get(treeName)); + if (!inputTree) { + return; + } + if (!inputTree->GetBranch("fIndexMcParticles")) { + std::cerr << treeName << ": missing fIndexMcParticles; copying unchanged\n"; + outputDirectory->cd(); + auto* outputTree = inputTree->CloneTree(-1, "fast"); + outputTree->Write(treeName, TObject::kOverwrite); + return; + } + int mcParticleId = -1; + inputTree->SetBranchAddress("fIndexMcParticles", &mcParticleId); + outputDirectory->cd(); + auto* outputTree = inputTree->CloneTree(0); + const Long64_t numberOfLabels = inputTree->GetEntries(); + Long64_t newlyInvalidatedLabels = 0; + for (Long64_t labelId = 0; labelId < numberOfLabels; ++labelId) { + inputTree->GetEntry(labelId); + const int oldMcParticleId = mcParticleId; + const int remappedId = remapMCParticleId(oldMcParticleId, oldToNew); + if (oldMcParticleId >= 0 && remappedId < 0) { + ++newlyInvalidatedLabels; + } + mcParticleId = remappedId; + outputTree->Fill(); + } + outputTree->Write(treeName, TObject::kOverwrite); + diagnostics.rows = numberOfLabels; + diagnostics.invalidated = newlyInvalidatedLabels; + std::cout << treeName << ": remapped " << numberOfLabels + << " rows; newly invalidated " << newlyInvalidatedLabels + << " labels\n"; +} +void rewriteCaloMCLabels(TDirectory* inputDirectory, + TDirectory* outputDirectory, + const std::vector& oldToNew, + DirectoryDiagnostics& diagnostics) { + constexpr const char* treeName = "O2mccalolabel_001"; + auto* inputTree = dynamic_cast(inputDirectory->Get(treeName)); + if (!inputTree) { + return; + } + auto* sizeBranch = inputTree->GetBranch("fIndexArrayMcParticles_size"); + auto* idsBranch = inputTree->GetBranch("fIndexArrayMcParticles"); + if (!sizeBranch || !idsBranch) { + std::cerr << treeName + << ": expected MC-particle array branches not found; copying unchanged\n"; + outputDirectory->cd(); + auto* outputTree = inputTree->CloneTree(-1, "fast"); + outputTree->Write(treeName, TObject::kOverwrite); + return; + } + int idsSize = 0; + int ids[kMaxCaloLabels] = {}; + inputTree->SetBranchAddress("fIndexArrayMcParticles_size", &idsSize); + inputTree->SetBranchAddress("fIndexArrayMcParticles", ids); + outputDirectory->cd(); + auto* outputTree = inputTree->CloneTree(0); + const Long64_t numberOfRows = inputTree->GetEntries(); + const int maxCaloLabelsSize = static_cast(inputTree->GetMaximum("fIndexArrayMcParticles_size")); + if (maxCaloLabelsSize > kMaxCaloLabels) { + std::cerr << "Error: maximum number of calorimeter labels (" << maxCaloLabelsSize + << ") exceeds buffer size (" << kMaxCaloLabels << ")\n"; + return; + } + Long64_t newlyInvalidatedReferences = 0; + for (Long64_t row = 0; row < numberOfRows; ++row) { + inputTree->GetEntry(row); + if (idsSize > kMaxCaloLabels) { + std::cerr << "Error: " << treeName << " row " << row << " contains " + << idsSize << " labels, exceeding buffer size " + << kMaxCaloLabels << ". Aborting this table rewrite.\n"; + delete outputTree; + return; + } + for (int labelIndex = 0; labelIndex < idsSize; ++labelIndex) { + const int oldMcParticleId = ids[labelIndex]; + ids[labelIndex] = remapMCParticleId(oldMcParticleId, oldToNew); + if (oldMcParticleId >= 0 && ids[labelIndex] < 0) { + ++newlyInvalidatedReferences; + } + } + outputTree->Fill(); + } + outputTree->Write(treeName, TObject::kOverwrite); + diagnostics.caloRows = numberOfRows; + diagnostics.caloInvalidated = newlyInvalidatedReferences; + std::cout << treeName + << ": remapped all MC-particle references; newly invalidated " + << newlyInvalidatedReferences << " references\n"; +} +} // namespace +void filterAO2DGapTriggeredMCEvents(const char* inputFileName = "AO2D.root", + const char* outputFileName = "AO2D_filter.root", + int minimumBiasSubGeneratorId = 0, + const char* logFileName = nullptr) { + const std::string resolvedLogFileName = logFileName ? std::string(logFileName) : makeDefaultLogFileName(outputFileName); + const Long64_t inputFileSizeBytes = getFileSizeBytes(inputFileName); + std::ofstream logFile(resolvedLogFileName); + if (!logFile) { + std::cerr << "Cannot create diagnostic log file " << resolvedLogFileName << "\n"; + return; + } + logFile << "AO2D GAP-TRIGGER MC FILTER REPORT\n\n" + << "Date: " << currentTimeString() << "\n" + << "Input: " << inputFileName << "\n" + << "Output: " << outputFileName << "\n" + << "Minimum-bias sub-generator ID: " << minimumBiasSubGeneratorId << "\n\n" + << "Filtering strategy:\n" + << " - keep all reconstructed AO2D tables\n" + << " - keep all MC particles from interesting events\n" + << " - rescue interesting-reco -> non-interesting-MC cross particles\n" + << " - keep all daughters recursively for rescued cross particles\n" + << " - keep all mothers recursively\n" + << " - remove all remaining MC particles and remap references\n\n"; + auto* inputFile = TFile::Open(inputFileName, "READ"); + if (!inputFile || inputFile->IsZombie()) { + std::cerr << "Cannot open input file " << inputFileName << "\n"; + return; + } + auto* outputFile = TFile::Open(outputFileName, "RECREATE", "", 505); + if (!outputFile || outputFile->IsZombie()) { + std::cerr << "Cannot create output file " << outputFileName << "\n"; + inputFile->Close(); + return; + } + std::cout << "Filtering MC particles in gap-trigger production [V2]\n" + << " input file: " << inputFileName << "\n" + << " output file: " << outputFileName << "\n" + << " minimum-bias sub-gen ID: " << minimumBiasSubGeneratorId << "\n" + << " reconstructed tables: kept unchanged\n" + << " extra MC seeds: interesting-reco -> MB-MC only\n" + << " genealogy: recursive mothers + recursive daughters of cross-event MB particles\n"; + TIter nextKey(inputFile->GetListOfKeys()); + TKey* key = nullptr; + int processedDirectories = 0; + int failedDirectories = 0; + GlobalDiagnostics globalDiagnostics; + while ((key = static_cast(nextKey()))) { + if (key->GetCycle() != inputFile->GetKey(key->GetName())->GetCycle()) { + continue; + } + TObject* object = key->ReadObj(); + const std::string objectName = object->GetName(); + if (std::string(key->GetName()) == "metaData") { + outputFile->cd(); + object->Write("metaData", TObject::kSingleKey | TObject::kOverwrite); + delete object; + continue; + } + if (key->GetName() == std::string("parentFiles")) { + outputFile->cd(); + object->Write("parentFiles", TObject::kSingleKey | TObject::kOverwrite); + delete object; + continue; + } + if (!object->InheritsFrom(TDirectory::Class())) { + outputFile->cd(); + object->Write(objectName.c_str(), TObject::kOverwrite); + delete object; + continue; + } + auto* inputDirectory = static_cast(object); + outputFile->cd(); + auto* outputDirectory = outputFile->mkdir(objectName.c_str()); + std::cout << "Processing directory " << objectName << "\n"; + DirectoryDiagnostics directoryDiagnostics; + directoryDiagnostics.name = objectName; + copyUnchangedObjects(inputDirectory, outputDirectory); + std::vector subGeneratorIdByCollision; + std::vector isInterestingRecoCollision; + std::vector oldToNew; + const bool subGeneratorMapBuilt = + buildMCCollisionSubGeneratorMap(inputDirectory, subGeneratorIdByCollision); + if (subGeneratorMapBuilt) { + directoryDiagnostics.mcCollisionsTotal = + static_cast(subGeneratorIdByCollision.size()); + for (int subGeneratorId : subGeneratorIdByCollision) { + if (isInterestingSubGenerator(subGeneratorId, minimumBiasSubGeneratorId)) { + ++directoryDiagnostics.mcCollisionsInteresting; + } else { + ++directoryDiagnostics.mcCollisionsNonInteresting; + } + } + } + const bool recoCollisionMapBuilt = + subGeneratorMapBuilt && + buildRecoCollisionInterestingMap(inputDirectory, + subGeneratorIdByCollision, + minimumBiasSubGeneratorId, + isInterestingRecoCollision, + directoryDiagnostics); + const bool particlesRewritten = + recoCollisionMapBuilt && + rewriteMCParticles(inputDirectory, + outputDirectory, + subGeneratorIdByCollision, + isInterestingRecoCollision, + minimumBiasSubGeneratorId, + oldToNew, + directoryDiagnostics); + if (!particlesRewritten) { + std::cerr << "Failed to filter directory " << objectName << "\n"; + ++failedDirectories; + ++globalDiagnostics.directoriesFailed; + delete object; + continue; + } + rewriteSingleMCLabelTable(inputDirectory, + outputDirectory, + "O2mctracklabel", + oldToNew, + directoryDiagnostics.tracks); + rewriteSingleMCLabelTable(inputDirectory, + outputDirectory, + "O2mcmfttracklabel", + oldToNew, + directoryDiagnostics.mft); + rewriteSingleMCLabelTable(inputDirectory, + outputDirectory, + "O2mcfwdtracklabel", + oldToNew, + directoryDiagnostics.fwd); + rewriteCaloMCLabels(inputDirectory, + outputDirectory, + oldToNew, + directoryDiagnostics); + writeDirectoryDiagnostics(logFile, directoryDiagnostics); + accumulateDirectoryDiagnostics(globalDiagnostics, directoryDiagnostics); + ++processedDirectories; + delete object; + } + outputFile->Close(); + inputFile->Close(); + const Long64_t outputFileSizeBytes = getFileSizeBytes(outputFileName); + globalDiagnostics.directoriesFailed = failedDirectories; + writeGlobalDiagnostics(logFile, + globalDiagnostics, + inputFileSizeBytes, + outputFileSizeBytes); + logFile.close(); + if (failedDirectories > 0) { + std::cerr << "Completed with failures: " << failedDirectories + << " directory/directories could not be filtered.\n"; + } + std::cout << "Done. Successfully filtered " << processedDirectories + << " directory/directories and wrote " << outputFileName << "\n" + << "Diagnostic report written to " << resolvedLogFileName << "\n"; + const Long64_t rescuedMBParticles = + globalDiagnostics.particlesCrossEventAdded + + globalDiagnostics.particlesDirectDaughtersAdded + + globalDiagnostics.particlesMotherAncestryAdded; + std::cout << "\n============================================================\n" + << "GLOBAL SUMMARY [V3]\n" + << "============================================================\n" + << "MC particles initial: " << globalDiagnostics.particlesInitial << "\n" + << "MC particles interesting events: " << globalDiagnostics.particlesInterestingEvents << "\n" + << "MC particles minimum-bias events: " << globalDiagnostics.particlesMinimumBiasEvents << "\n" + << std::fixed << std::setprecision(4) + << "Interesting / MB particle ratio: " + << (globalDiagnostics.particlesMinimumBiasEvents > 0 + ? static_cast(globalDiagnostics.particlesInterestingEvents) / + static_cast(globalDiagnostics.particlesMinimumBiasEvents) + : 0.0) + << "\n" + << std::fixed << std::setprecision(2) + << "Avg particles / interesting event: " + << (globalDiagnostics.mcCollisionsInteresting > 0 + ? static_cast(globalDiagnostics.particlesInterestingEvents) / + static_cast(globalDiagnostics.mcCollisionsInteresting) + : 0.0) + << "\n" + << "Avg particles / minimum-bias event: " + << (globalDiagnostics.mcCollisionsNonInteresting > 0 + ? static_cast(globalDiagnostics.particlesMinimumBiasEvents) / + static_cast(globalDiagnostics.mcCollisionsNonInteresting) + : 0.0) + << "\n" + << "MC particles retained: " << globalDiagnostics.particlesRetained << "\n" + << "MC particles removed: " << globalDiagnostics.particlesRemoved << "\n" + << std::fixed << std::setprecision(2) + << "Retained fraction: " + << fractionPercent(globalDiagnostics.particlesRetained, + globalDiagnostics.particlesInitial) + << " %\n" + << "Removed fraction: " + << fractionPercent(globalDiagnostics.particlesRemoved, + globalDiagnostics.particlesInitial) + << " %\n" + << "Cross-event MB seeds: " + << globalDiagnostics.particlesCrossEventAdded << "\n" + << "Descendants added: " + << globalDiagnostics.particlesDirectDaughtersAdded << "\n" + << "Mother ancestry added: " + << globalDiagnostics.particlesMotherAncestryAdded << "\n" + << "Rescued MB particles: " << rescuedMBParticles << "\n" + << "MB rescue fraction: " + << fractionPercent(rescuedMBParticles, + globalDiagnostics.particlesRetained) + << " % of retained MC particles\n"; + if (inputFileSizeBytes >= 0 && outputFileSizeBytes >= 0) { + const double inputMB = + static_cast(inputFileSizeBytes) / (1024.0 * 1024.0); + const double outputMB = + static_cast(outputFileSizeBytes) / (1024.0 * 1024.0); + const double reduction = + inputFileSizeBytes > 0 + ? 100.0 * (1.0 - static_cast(outputFileSizeBytes) / + static_cast(inputFileSizeBytes)) + : 0.0; + std::cout << "Input file size: " << inputMB << " MB\n" + << "Output file size: " << outputMB << " MB\n" + << "Storage reduction: " << reduction << " %\n"; + } +} +//============================================================================================================================== diff --git a/MC/utils/MCFilteringTools/filterAO2DMCParticlesPdg.C b/MC/utils/MCFilteringTools/filterAO2DMCParticlesPdg.C new file mode 100644 index 000000000..d477a6799 --- /dev/null +++ b/MC/utils/MCFilteringTools/filterAO2DMCParticlesPdg.C @@ -0,0 +1,1293 @@ +//============================================================================= +// filterAO2DMCParticlesPdg.C +// +// Filter AO2D MC particles using a list of PDG-code seeds. +// +// Selection policy: +// +// keep = selected PDG particles +// + all ancestors of selected PDG particles +// + all descendants of selected PDG particles +// +// IMPORTANT: +// +// The upward and downward traversals are completely independent. +// +// - Ancestor traversal: +// seed -> mother -> grandmother -> ... +// Ancestors DO NOT become daughter-expansion seeds. +// +// - Descendant traversal: +// seed -> daughter -> granddaughter -> ... +// Descendants DO NOT become mother-expansion seeds. +// +// Therefore sibling branches of an ancestor are NOT automatically retained. +// +// All surviving MC-particle indices are remapped. +// Reconstructed tables are copied unchanged, while MC-label tables are remapped. +// +// fIndexSlice_Daughters stores: +// {firstDaughter, lastDaughterInclusive} +// +// Usage: +// +// root -l -b -q 'filterAO2DMCParticlesPdg.C("AO2D.root","AO2D_filter.root",{3122,-3122})' +// +//============================================================================= + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +// Mirror everything written to std::cout/std::cerr to a text log file. +class TeeBuf : public std::streambuf +{ + public: + TeeBuf(std::streambuf* first, std::streambuf* second) + : mFirst(first), mSecond(second) + { + } + + protected: + int overflow(int c) override + { + if (c == traits_type::eof()) { + return traits_type::not_eof(c); + } + + const char ch = static_cast(c); + const bool firstOk = mFirst->sputc(ch) != traits_type::eof(); + const bool secondOk = mSecond->sputc(ch) != traits_type::eof(); + + return (firstOk && secondOk) ? c : traits_type::eof(); + } + + int sync() override + { + const int firstStatus = mFirst->pubsync(); + const int secondStatus = mSecond->pubsync(); + return (firstStatus == 0 && secondStatus == 0) ? 0 : -1; + } + + private: + std::streambuf* mFirst = nullptr; + std::streambuf* mSecond = nullptr; +}; + +constexpr int kMaxMothers = 256; +constexpr int kMaxCaloLabels = 4096; + +//_____________________________________________________________________________ +struct GlobalSummary { + Long64_t directoriesProcessed = 0; + + Long64_t mcParticlesInitial = 0; + Long64_t pdgSeeds = 0; + Long64_t ancestorsAdded = 0; + Long64_t descendantsAdded = 0; + Long64_t mcParticlesKept = 0; + Long64_t mcParticlesRemoved = 0; + + Long64_t preservedDaughterSlices = 0; + Long64_t clearedDaughterSlices = 0; + + Long64_t invalidMotherReferences = 0; + Long64_t invalidDaughterSlices = 0; + + Long64_t validTrackLabels = 0; + Long64_t invalidatedTrackLabels = 0; + Long64_t validMFTTrackLabels = 0; + Long64_t invalidatedMFTTrackLabels = 0; + Long64_t validFwdTrackLabels = 0; + Long64_t invalidatedFwdTrackLabels = 0; + Long64_t validCaloLabels = 0; + Long64_t invalidatedCaloLabels = 0; +}; + +//_____________________________________________________________________________ +struct KeptParticle { + int oldId = -1; + int mcCollisionId = -1; + int pdgCode = 0; + int statusCode = 0; + UChar_t flags = 0; + + std::vector mothers; + + int firstDaughter = -1; + int lastDaughter = -1; + + float weight = 0.f; + float px = 0.f; + float py = 0.f; + float pz = 0.f; + float e = 0.f; + float vx = 0.f; + float vy = 0.f; + float vz = 0.f; + float vt = 0.f; +}; + +//_____________________________________________________________________________ +int remapId(int oldId, const std::vector& oldToNew) +{ + if (oldId < 0 || oldId >= static_cast(oldToNew.size())) { + return -1; + } + return oldToNew[oldId]; +} + +//_____________________________________________________________________________ +bool hasBranch(TTree* tree, const char* branchName) +{ + if (!tree->GetBranch(branchName)) { + std::cerr << "Missing branch " << branchName + << " in tree " << tree->GetName() << "\n"; + return false; + } + return true; +} + +//_____________________________________________________________________________ +bool isAffectedTree(const std::string& name) +{ + static const std::unordered_set affected = { + "O2mcparticle_001", + "O2mctracklabel", + "O2mcmfttracklabel", + "O2mcfwdtracklabel", + "O2mccalolabel_001" + }; + + return affected.count(name) > 0; +} + +//_____________________________________________________________________________ +void copyDirectory(TDirectory* inDir, TDirectory* outDir) +{ + TIter next(inDir->GetListOfKeys()); + TKey* key = nullptr; + + while ((key = static_cast(next()))) { + const TKey* latestKey = inDir->GetKey(key->GetName()); + if (latestKey && key->GetCycle() != latestKey->GetCycle()) { + continue; + } + TObject* obj = key->ReadObj(); + if (!obj) { + continue; + } + + const std::string name = obj->GetName(); + + if (obj->InheritsFrom(TDirectory::Class())) { + outDir->cd(); + auto* newDir = outDir->mkdir(name.c_str()); + + if (!newDir) { + std::cerr << "Could not create output directory " + << name << "\n"; + delete obj; + continue; + } + + copyDirectory(static_cast(obj), newDir); + delete obj; + continue; + } + + if (obj->InheritsFrom(TTree::Class())) { + if (isAffectedTree(name)) { + delete obj; + continue; + } + + auto* inTree = static_cast(obj); + + outDir->cd(); + auto* outTree = inTree->CloneTree(-1, "fast"); + + if (!outTree) { + std::cerr << "Could not clone tree " << name << "\n"; + delete obj; + continue; + } + + outTree->Write(name.c_str(), TObject::kOverwrite); + delete obj; + continue; + } + + outDir->cd(); + obj->Write(name.c_str(), TObject::kOverwrite); + delete obj; + } +} + +//_____________________________________________________________________________ +bool rewriteMCParticles(TDirectory* inDir, + TDirectory* outDir, + std::vector& oldToNew, + const std::unordered_set& keptPDGs, + GlobalSummary& summary) +{ + auto* inTree = + dynamic_cast(inDir->Get("O2mcparticle_001")); + + if (!inTree) { + std::cerr << "Missing O2mcparticle_001 in directory " + << inDir->GetName() << "\n"; + return false; + } + + const std::vector requiredBranches = { + "fIndexMcCollisions", + "fPdgCode", + "fStatusCode", + "fFlags", + "fIndexArray_Mothers_size", + "fIndexArray_Mothers", + "fIndexSlice_Daughters", + "fWeight", + "fPx", + "fPy", + "fPz", + "fE", + "fVx", + "fVy", + "fVz", + "fVt" + }; + + for (const char* branchName : requiredBranches) { + if (!hasBranch(inTree, branchName)) { + return false; + } + } + + const int maximumMothers = + static_cast(inTree->GetMaximum("fIndexArray_Mothers_size")); + + if (maximumMothers > kMaxMothers) { + std::cerr + << "Fatal: maximum mother-array size in " + << inDir->GetName() + << " is " << maximumMothers + << ", while the allocated buffer supports only " + << kMaxMothers << "\n"; + return false; + } + + int mcCollisionId = -1; + int pdgCode = 0; + int statusCode = 0; + UChar_t flags = 0; + + int mothersSize = 0; + int mothers[kMaxMothers] = {}; + + int daughtersSlice[2] = {-1, -1}; + + float weight = 0.f; + float px = 0.f; + float py = 0.f; + float pz = 0.f; + float e = 0.f; + float vx = 0.f; + float vy = 0.f; + float vz = 0.f; + float vt = 0.f; + + inTree->SetBranchAddress("fIndexMcCollisions", &mcCollisionId); + inTree->SetBranchAddress("fPdgCode", &pdgCode); + inTree->SetBranchAddress("fStatusCode", &statusCode); + inTree->SetBranchAddress("fFlags", &flags); + inTree->SetBranchAddress("fIndexArray_Mothers_size", &mothersSize); + inTree->SetBranchAddress("fIndexArray_Mothers", mothers); + inTree->SetBranchAddress("fIndexSlice_Daughters", daughtersSlice); + inTree->SetBranchAddress("fWeight", &weight); + inTree->SetBranchAddress("fPx", &px); + inTree->SetBranchAddress("fPy", &py); + inTree->SetBranchAddress("fPz", &pz); + inTree->SetBranchAddress("fE", &e); + inTree->SetBranchAddress("fVx", &vx); + inTree->SetBranchAddress("fVy", &vy); + inTree->SetBranchAddress("fVz", &vz); + inTree->SetBranchAddress("fVt", &vt); + + const Long64_t nEntries = inTree->GetEntries(); + + std::vector> originalMothers(nEntries); + std::vector firstDaughter(nEntries, -1); + std::vector lastDaughter(nEntries, -1); + + std::vector selectedPDG(nEntries, 0); + std::vector keep(nEntries, 0); + + Long64_t nPDGSelected = 0; + Long64_t nInvalidMotherReferences = 0; + Long64_t nInvalidDaughterSlices = 0; + + //=========================================================================== + // First pass: cache genealogy and mark the original PDG seeds. + //=========================================================================== + for (Long64_t i = 0; i < nEntries; ++i) { + inTree->GetEntry(i); + + if (mothersSize < 0 || mothersSize > kMaxMothers) { + std::cerr << "Invalid mother-array size " + << mothersSize + << " at MC-particle entry " + << i << "\n"; + return false; + } + + originalMothers[i].reserve(mothersSize); + + for (int im = 0; im < mothersSize; ++im) { + const int mother = mothers[im]; + + if (mother < 0) { + continue; + } + + if (mother >= nEntries) { + ++nInvalidMotherReferences; + continue; + } + + originalMothers[i].push_back(mother); + } + + const int first = daughtersSlice[0]; + const int last = daughtersSlice[1]; + + if (first < 0 || last < first) { + firstDaughter[i] = -1; + lastDaughter[i] = -1; + } else if (first >= nEntries || last >= nEntries) { + ++nInvalidDaughterSlices; + firstDaughter[i] = -1; + lastDaughter[i] = -1; + } else { + firstDaughter[i] = first; + lastDaughter[i] = last; + } + + if (keptPDGs.count(pdgCode) > 0) { + selectedPDG[i] = 1; + keep[i] = 1; + ++nPDGSelected; + } + } + + //=========================================================================== + // UPWARD traversal. + // + // Start ONLY from original PDG seeds. + // Newly found mothers can generate further mothers, but NEVER daughters. + //=========================================================================== + std::vector visitedUp(nEntries, 0); + std::vector ancestorQueue; + ancestorQueue.reserve(nPDGSelected); + + for (Long64_t i = 0; i < nEntries; ++i) { + if (selectedPDG[i]) { + visitedUp[i] = 1; + ancestorQueue.push_back(static_cast(i)); + } + } + + Long64_t nAncestorsAdded = 0; + size_t upPosition = 0; + + while (upPosition < ancestorQueue.size()) { + const int particleId = ancestorQueue[upPosition++]; + + for (const int mother : originalMothers[particleId]) { + if (!visitedUp[mother]) { + visitedUp[mother] = 1; + ancestorQueue.push_back(mother); + + if (!keep[mother]) { + keep[mother] = 1; + ++nAncestorsAdded; + } + } + } + } + + //=========================================================================== + // DOWNWARD traversal. + // + // Again start ONLY from original PDG seeds. + // Newly found daughters can generate further daughters, but NEVER mothers. + //=========================================================================== + std::vector visitedDown(nEntries, 0); + std::vector descendantQueue; + descendantQueue.reserve(nPDGSelected); + + for (Long64_t i = 0; i < nEntries; ++i) { + if (selectedPDG[i]) { + visitedDown[i] = 1; + descendantQueue.push_back(static_cast(i)); + } + } + + Long64_t nDescendantsAdded = 0; + size_t downPosition = 0; + + while (downPosition < descendantQueue.size()) { + const int particleId = descendantQueue[downPosition++]; + + const int first = firstDaughter[particleId]; + const int last = lastDaughter[particleId]; + + if (first < 0 || last < first) { + continue; + } + + for (int daughter = first; daughter <= last; ++daughter) { + if (!visitedDown[daughter]) { + visitedDown[daughter] = 1; + descendantQueue.push_back(daughter); + + if (!keep[daughter]) { + keep[daughter] = 1; + ++nDescendantsAdded; + } + } + } + } + + //=========================================================================== + // Construct old-to-new index map. + //=========================================================================== + oldToNew.assign(nEntries, -1); + + int newId = 0; + + for (Long64_t i = 0; i < nEntries; ++i) { + if (keep[i]) { + oldToNew[i] = newId++; + } + } + + const Long64_t nKept = newId; + const Long64_t nRemoved = nEntries - nKept; + + std::cout + << "O2mcparticle_001: PDG seeds = " + << nPDGSelected << " / " << nEntries << "\n"; + + std::cout + << "O2mcparticle_001: ancestors added = " + << nAncestorsAdded << "\n"; + + std::cout + << "O2mcparticle_001: descendants added = " + << nDescendantsAdded << "\n"; + + std::cout + << "O2mcparticle_001: kept after selection = " + << nKept + << " / " << nEntries + << ", survival fraction = " + << (nEntries > 0 + ? static_cast(nKept) / + static_cast(nEntries) + : 0.0) + << "\n"; + + //=========================================================================== + // Second pass: cache retained particle payloads and remap mothers. + //=========================================================================== + std::vector kept; + kept.reserve(newId); + + for (Long64_t i = 0; i < nEntries; ++i) { + if (!keep[i]) { + continue; + } + + inTree->GetEntry(i); + + KeptParticle particle; + + particle.oldId = static_cast(i); + particle.mcCollisionId = mcCollisionId; + particle.pdgCode = pdgCode; + particle.statusCode = statusCode; + particle.flags = flags; + particle.weight = weight; + particle.px = px; + particle.py = py; + particle.pz = pz; + particle.e = e; + particle.vx = vx; + particle.vy = vy; + particle.vz = vz; + particle.vt = vt; + + // Keep only mother references that survived. + for (const int oldMother : originalMothers[i]) { + const int newMother = remapId(oldMother, oldToNew); + if (newMother >= 0) { + particle.mothers.push_back(newMother); + } + } + + particle.firstDaughter = firstDaughter[i]; + particle.lastDaughter = lastDaughter[i]; + + kept.push_back(std::move(particle)); + } + + //=========================================================================== + // Create output tree. + //=========================================================================== + outDir->cd(); + + auto* outTree = inTree->CloneTree(0); + + if (!outTree) { + std::cerr << "Could not create output O2mcparticle_001\n"; + return false; + } + + Long64_t nPreservedDaughterSlices = 0; + Long64_t nClearedDaughterSlices = 0; + + //=========================================================================== + // Third pass: write retained particles with remapped indices. + // + // Preserve the surviving daughters if their remapped indices remain + // contiguous in the filtered table. + //=========================================================================== + for (const auto& particle : kept) { + mcCollisionId = particle.mcCollisionId; + pdgCode = particle.pdgCode; + statusCode = particle.statusCode; + flags = particle.flags; + weight = particle.weight; + px = particle.px; + py = particle.py; + pz = particle.pz; + e = particle.e; + vx = particle.vx; + vy = particle.vy; + vz = particle.vz; + vt = particle.vt; + + mothersSize = + static_cast(particle.mothers.size()); + + if (mothersSize > kMaxMothers) { + std::cerr + << "Fatal: remapped mother list has size " + << mothersSize + << ", exceeding the buffer size " + << kMaxMothers << "\n"; + delete outTree; + return false; + } + + for (int im = 0; im < kMaxMothers; ++im) { + mothers[im] = -1; + } + + for (int im = 0; im < mothersSize; ++im) { + mothers[im] = particle.mothers[im]; + } + + daughtersSlice[0] = -1; + daughtersSlice[1] = -1; + + if (particle.firstDaughter >= 0 && + particle.lastDaughter >= particle.firstDaughter) { + + int firstNewDaughter = -1; + int lastNewDaughter = -1; + + for (int oldDaughter = particle.firstDaughter; + oldDaughter <= particle.lastDaughter; + ++oldDaughter) { + + const int newDaughter = + remapId(oldDaughter, oldToNew); + + if (newDaughter < 0) { + continue; + } + + if (firstNewDaughter < 0) { + firstNewDaughter = newDaughter; + } + + lastNewDaughter = newDaughter; + } + + if (firstNewDaughter >= 0 && + lastNewDaughter >= firstNewDaughter) { + + daughtersSlice[0] = firstNewDaughter; + daughtersSlice[1] = lastNewDaughter; + ++nPreservedDaughterSlices; + + } else { + ++nClearedDaughterSlices; + } + } + + outTree->Fill(); + } + + outTree->Write( + "O2mcparticle_001", + TObject::kOverwrite); + + std::cout + << "O2mcparticle_001: preserved daughter slices = " + << nPreservedDaughterSlices << "\n"; + + std::cout + << "O2mcparticle_001: cleared daughter slices = " + << nClearedDaughterSlices << "\n"; + + summary.mcParticlesInitial += nEntries; + summary.pdgSeeds += nPDGSelected; + summary.ancestorsAdded += nAncestorsAdded; + summary.descendantsAdded += nDescendantsAdded; + summary.mcParticlesKept += nKept; + summary.mcParticlesRemoved += nRemoved; + summary.preservedDaughterSlices += nPreservedDaughterSlices; + summary.clearedDaughterSlices += nClearedDaughterSlices; + summary.invalidMotherReferences += nInvalidMotherReferences; + summary.invalidDaughterSlices += nInvalidDaughterSlices; + + return true; +} + +//_____________________________________________________________________________ +void rewriteSingleLabelTable( + TDirectory* inDir, + TDirectory* outDir, + const char* treeName, + const std::vector& oldToNew, + Long64_t& globalValidLabels, + Long64_t& globalInvalidatedLabels) +{ + auto* inTree = + dynamic_cast(inDir->Get(treeName)); + + if (!inTree) { + return; + } + + if (!hasBranch(inTree, "fIndexMcParticles")) { + std::cerr + << "Tree " << treeName + << " will not be written because its expected " + << "MC-particle index branch is missing.\n"; + return; + } + + int mcParticleId = -1; + + inTree->SetBranchAddress( + "fIndexMcParticles", + &mcParticleId); + + outDir->cd(); + + auto* outTree = inTree->CloneTree(0); + + if (!outTree) { + std::cerr << "Could not create output tree " + << treeName << "\n"; + return; + } + + const Long64_t nEntries = inTree->GetEntries(); + + Long64_t nValidLabels = 0; + Long64_t nInvalidatedLabels = 0; + + for (Long64_t i = 0; i < nEntries; ++i) { + inTree->GetEntry(i); + + mcParticleId = + remapId(mcParticleId, oldToNew); + + if (mcParticleId >= 0) { + ++nValidLabels; + } else { + ++nInvalidatedLabels; + } + + outTree->Fill(); + } + + outTree->Write( + treeName, + TObject::kOverwrite); + + std::cout + << treeName + << ": valid remapped labels = " + << nValidLabels + << ", invalidated labels = " + << nInvalidatedLabels + << "\n"; + + globalValidLabels += nValidLabels; + globalInvalidatedLabels += nInvalidatedLabels; +} + +//_____________________________________________________________________________ +void rewriteCaloLabels( + TDirectory* inDir, + TDirectory* outDir, + const std::vector& oldToNew, + GlobalSummary& summary) +{ + auto* inTree = + dynamic_cast( + inDir->Get("O2mccalolabel_001")); + + if (!inTree) { + return; + } + + auto* sizeBranch = + inTree->GetBranch( + "fIndexArrayMcParticles_size"); + + auto* idsBranch = + inTree->GetBranch( + "fIndexArrayMcParticles"); + + if (!sizeBranch || !idsBranch) { + std::cout + << "O2mccalolabel_001: expected array branches " + << "not found; copying unchanged\n"; + + outDir->cd(); + + auto* outTree = + inTree->CloneTree(-1, "fast"); + + if (outTree) { + outTree->Write( + "O2mccalolabel_001", + TObject::kOverwrite); + } + + return; + } + + const int maximumLabels = + static_cast( + inTree->GetMaximum("fIndexArrayMcParticles_size")); + + if (maximumLabels > kMaxCaloLabels) { + std::cerr + << "Fatal: maximum number of calorimeter labels is " + << maximumLabels + << ", while the buffer supports only " + << kMaxCaloLabels + << ". O2mccalolabel_001 will not be written.\n"; + return; + } + + int idsSize = 0; + int ids[kMaxCaloLabels] = {}; + + inTree->SetBranchAddress( + "fIndexArrayMcParticles_size", + &idsSize); + + inTree->SetBranchAddress( + "fIndexArrayMcParticles", + ids); + + outDir->cd(); + + auto* outTree = inTree->CloneTree(0); + + if (!outTree) { + std::cerr + << "Could not create output O2mccalolabel_001\n"; + return; + } + + const Long64_t nEntries = inTree->GetEntries(); + + Long64_t nValidLabels = 0; + Long64_t nInvalidatedLabels = 0; + + for (Long64_t i = 0; i < nEntries; ++i) { + inTree->GetEntry(i); + + if (idsSize < 0 || idsSize > kMaxCaloLabels) { + std::cerr + << "Invalid calorimeter-label array size " + << idsSize + << " at entry " << i << "\n"; + delete outTree; + return; + } + + for (int j = 0; j < idsSize; ++j) { + ids[j] = remapId(ids[j], oldToNew); + + if (ids[j] >= 0) { + ++nValidLabels; + } else { + ++nInvalidatedLabels; + } + } + + outTree->Fill(); + } + + outTree->Write( + "O2mccalolabel_001", + TObject::kOverwrite); + + std::cout + << "O2mccalolabel_001: valid remapped labels = " + << nValidLabels + << ", invalidated labels = " + << nInvalidatedLabels + << "\n"; + + summary.validCaloLabels += nValidLabels; + summary.invalidatedCaloLabels += nInvalidatedLabels; +} + +//_____________________________________________________________________________ +double bytesToMB(Long64_t bytes) +{ + return static_cast(bytes) / (1024.0 * 1024.0); +} + +//_____________________________________________________________________________ +void printGlobalSummary(const GlobalSummary& summary, + Long64_t inputBytes, + Long64_t outputBytes) +{ + const double retainedFraction = + summary.mcParticlesInitial > 0 + ? 100.0 * + static_cast(summary.mcParticlesKept) / + static_cast(summary.mcParticlesInitial) + : 0.0; + + const double removedFraction = + summary.mcParticlesInitial > 0 + ? 100.0 * + static_cast(summary.mcParticlesRemoved) / + static_cast(summary.mcParticlesInitial) + : 0.0; + + const double seedFraction = + summary.mcParticlesInitial > 0 + ? 100.0 * + static_cast(summary.pdgSeeds) / + static_cast(summary.mcParticlesInitial) + : 0.0; + + const double ancestorFractionOfKept = + summary.mcParticlesKept > 0 + ? 100.0 * + static_cast(summary.ancestorsAdded) / + static_cast(summary.mcParticlesKept) + : 0.0; + + const double descendantFractionOfKept = + summary.mcParticlesKept > 0 + ? 100.0 * + static_cast(summary.descendantsAdded) / + static_cast(summary.mcParticlesKept) + : 0.0; + + const double storageReduction = + inputBytes > 0 + ? 100.0 * + (1.0 - + static_cast(outputBytes) / + static_cast(inputBytes)) + : 0.0; + + std::cout + << "\n============================================================\n" + << "\nGLOBAL SUMMARY [ASYMMETRIC PDG FILTER]\n" + << "\n============================================================\n\n"; + + std::cout << std::fixed << std::setprecision(2); + + std::cout + << "Directories processed: " + << summary.directoriesProcessed << "\n"; + + std::cout + << "MC particles initial: " + << summary.mcParticlesInitial << "\n"; + + std::cout + << "PDG-selected seed particles: " + << summary.pdgSeeds + << " (" << seedFraction << " % of initial)\n"; + + std::cout + << "Ancestors added: " + << summary.ancestorsAdded + << " (" << ancestorFractionOfKept << " % of kept)\n"; + + std::cout + << "Descendants added: " + << summary.descendantsAdded + << " (" << descendantFractionOfKept << " % of kept)\n"; + + std::cout + << "MC particles kept: " + << summary.mcParticlesKept << "\n"; + + std::cout + << "MC particles removed: " + << summary.mcParticlesRemoved << "\n"; + + std::cout + << "Retained fraction: " + << retainedFraction << " %\n"; + + std::cout + << "Removed fraction: " + << removedFraction << " %\n"; + + std::cout + << "Preserved daughter slices: " + << summary.preservedDaughterSlices << "\n"; + + std::cout + << "Cleared daughter slices: " + << summary.clearedDaughterSlices << "\n"; + + if (summary.invalidMotherReferences > 0) { + std::cout + << "Invalid original mother refs: " + << summary.invalidMotherReferences << "\n"; + } + + if (summary.invalidDaughterSlices > 0) { + std::cout + << "Invalid original daughter slices: " + << summary.invalidDaughterSlices << "\n"; + } + + std::cout + << "\nTrack labels valid / invalidated: " + << summary.validTrackLabels + << " / " << summary.invalidatedTrackLabels << "\n"; + + std::cout + << "MFT labels valid / invalidated: " + << summary.validMFTTrackLabels + << " / " << summary.invalidatedMFTTrackLabels << "\n"; + + std::cout + << "Fwd labels valid / invalidated: " + << summary.validFwdTrackLabels + << " / " << summary.invalidatedFwdTrackLabels << "\n"; + + std::cout + << "Calo labels valid / invalidated: " + << summary.validCaloLabels + << " / " << summary.invalidatedCaloLabels << "\n"; + + std::cout + << "\nInput file size: " + << bytesToMB(inputBytes) << " MB\n"; + + std::cout + << "Output file size: " + << bytesToMB(outputBytes) << " MB\n"; + + std::cout + << "Storage reduction: " + << storageReduction << " %\n"; + + std::cout + << "\n============================================================\n"; +} + +} // namespace + +//_____________________________________________________________________________ +void filterAO2DMCParticlesPdg( + const char* input = "AO2D.root", + const char* output = "AO2D_filter.root", + const std::vector& pdgs = {}, + const char* logFileName = "filterAO2DMCParticlesPdg.log") +{ + if (!input || !output) { + std::cerr << "Input or output filename is null\n"; + return; + } + + if (pdgs.empty()) { + std::cerr + << "The requested PDG-code list is empty. " + << "Nothing will be filtered.\n"; + return; + } + + std::ofstream logFile; + std::streambuf* originalCoutBuf = std::cout.rdbuf(); + std::streambuf* originalCerrBuf = std::cerr.rdbuf(); + + if (logFileName && logFileName[0] != '\0') { + logFile.open(logFileName, std::ios::out | std::ios::trunc); + + if (!logFile.is_open()) { + std::cerr << "Cannot create log file " + << logFileName << "\n"; + return; + } + } + + TeeBuf coutTee(originalCoutBuf, logFile.is_open() ? logFile.rdbuf() : originalCoutBuf); + TeeBuf cerrTee(originalCerrBuf, logFile.is_open() ? logFile.rdbuf() : originalCerrBuf); + + if (logFile.is_open()) { + std::cout.rdbuf(&coutTee); + std::cerr.rdbuf(&cerrTee); + } + + // Restore the original stream buffers automatically before returning. + struct StreamRestorer { + std::streambuf* coutBuf; + std::streambuf* cerrBuf; + ~StreamRestorer() + { + std::cout.flush(); + std::cerr.flush(); + std::cout.rdbuf(coutBuf); + std::cerr.rdbuf(cerrBuf); + } + } streamRestorer{originalCoutBuf, originalCerrBuf}; + + const std::unordered_set keptPDGs( + pdgs.begin(), + pdgs.end()); + + auto* inFile = TFile::Open(input, "READ"); + + if (!inFile || inFile->IsZombie()) { + std::cerr + << "Cannot open input file " + << input << "\n"; + + if (inFile) { + inFile->Close(); + delete inFile; + } + + return; + } + + const Long64_t inputBytes = inFile->GetSize(); + + auto* outFile = TFile::Open(output, "RECREATE", "", 505); + + if (!outFile || outFile->IsZombie()) { + std::cerr + << "Cannot create output file " + << output << "\n"; + + if (outFile) { + outFile->Close(); + delete outFile; + } + + inFile->Close(); + delete inFile; + return; + } + + std::cout + << "\nFiltering MC particles by PDG code\n\n"; + + std::cout + << " input file: " << input << "\n" + << " output file: " << output << "\n" + << " log file: " + << (logFile.is_open() ? logFileName : "(disabled)") << "\n" + << " PDG seeds: "; + + for (const int pdg : pdgs) { + std::cout << " " << pdg; + } + + std::cout + << "\n" + << " ancestry: recursive mothers from original PDG seeds only\n" + << " descendants: recursive daughters from original PDG seeds only\n" + << " side branches from ancestors: NOT kept\n" + << " reco tables: kept unchanged; MC-particle labels remapped\n"; + + GlobalSummary summary; + + TIter next(inFile->GetListOfKeys()); + TKey* key = nullptr; + + while ((key = static_cast(next()))) { + const TKey* latestKey = inFile->GetKey(key->GetName()); + if (latestKey && key->GetCycle() != latestKey->GetCycle()) { + continue; + } + TObject* obj = key->ReadObj(); + + if (!obj) { + continue; + } + + if (std::string(key->GetName()) == "metaData") { + outFile->cd(); + obj->Write("metaData", TObject::kSingleKey | TObject::kOverwrite); + delete obj; + continue; + } + + const std::string name = obj->GetName(); + + if (name == "parentFiles") { + outFile->cd(); + obj->Write("parentFiles", TObject::kSingleKey | TObject::kOverwrite); + delete obj; + continue; + } + + if (!obj->InheritsFrom(TDirectory::Class())) { + outFile->cd(); + obj->Write(name.c_str(), TObject::kOverwrite); + delete obj; + continue; + } + + auto* inDir = + static_cast(obj); + + outFile->cd(); + + auto* outDir = + outFile->mkdir(name.c_str()); + + if (!outDir) { + std::cerr + << "Could not create output directory " + << name << "\n"; + delete obj; + continue; + } + + std::cout + << "\nProcessing directory " + << name << "\n"; + + copyDirectory(inDir, outDir); + + std::vector oldToNew; + + const bool mcParticlesRewritten = + rewriteMCParticles( + inDir, + outDir, + oldToNew, + keptPDGs, + summary); + + if (mcParticlesRewritten) { + ++summary.directoriesProcessed; + + rewriteSingleLabelTable( + inDir, + outDir, + "O2mctracklabel", + oldToNew, + summary.validTrackLabels, + summary.invalidatedTrackLabels); + + rewriteSingleLabelTable( + inDir, + outDir, + "O2mcmfttracklabel", + oldToNew, + summary.validMFTTrackLabels, + summary.invalidatedMFTTrackLabels); + + rewriteSingleLabelTable( + inDir, + outDir, + "O2mcfwdtracklabel", + oldToNew, + summary.validFwdTrackLabels, + summary.invalidatedFwdTrackLabels); + + rewriteCaloLabels( + inDir, + outDir, + oldToNew, + summary); + + } else { + std::cerr + << "MC particles were not rewritten in directory " + << name + << "; associated label tables were not rewritten.\n"; + } + + delete obj; + } + + outFile->Flush(); + + const Long64_t outputBytes = outFile->GetSize(); + + printGlobalSummary( + summary, + inputBytes, + outputBytes); + + outFile->Close(); + inFile->Close(); + + delete outFile; + delete inFile; + + std::cout + << "\nDone. Written " + << output << "\n"; + + if (logFile.is_open()) { + std::cout + << "Log written to " + << logFileName << "\n"; + } +} From c56a13459138ab760ae792283f47d32774c2e36f Mon Sep 17 00:00:00 2001 From: swenzel Date: Tue, 30 Sep 2025 13:15:39 +0200 Subject: [PATCH 130/144] Make the TPC time-series task follow the active detectors This fixes the TRD/CTRACKLETS failure of the TPC time-series task in the MC workflow and makes its inputs follow the active detectors. - The reader used fixed cluster types without TRD, so the time series failed with "No matching output found for TRD/CTRACKLETS/0" once it requested TRD tracklets. - Track types now follow the active detectors and the anchored TRD and TOF matching sources. - The same track list goes to o2-tpc-time-series-workflow via --track-sources. - Builds with their own readers get --disable-root-input, so only one reader list exists. - TOF and TRD are optional, and the task requires ITS, TPC and FT0. - Builds without --track-sources keep the previous command and require TOF and TRD. https://its.cern.ch/jira/browse/O2-7264 https://github.com/AliceO2Group/O2DPG/pull/2532 Co-Authored-By: Claude Opus 5.5 --- MC/bin/o2dpg_sim_workflow.py | 43 +++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 24bf25aba..470e538f7 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -2170,23 +2170,50 @@ def remove_json_prefix(path): #<------------------ TPC - time-series objects # initial implementation taken from comments in https://its.cern.ch/jira/browse/O2-4612 - # TODO: this needs to be made configurable (as a function of which detectors are actually present) + # the inputs follow the active detectors where the time series accepts --track-sources + tpcts_sources = '' + tpcts_ownreaders = '' + if isActive("ITS") and isActive("TPC") and isActive("FT0"): + tpcts_sources = option_if_available('o2-tpc-time-series-workflow', '--track-sources', envfile=async_envfile) + tpcts_ownreaders = option_if_available('o2-tpc-time-series-workflow', '--disable-root-input', envfile=async_envfile) + if not tpcts_sources and tf == 1: + print("WARNING: o2-tpc-time-series-workflow has no --track-sources; the TPC time series runs only with TOF and TRD active") + if tpcts_sources: + # only the tracks which the TRD and TOF matching produce with the anchored configuration + trdsrc = trd_track_sources.split(',') + tofsrc = toftracksrcdefault.split(',') + tpcts_tracklist = ["ITS", "TPC", "ITS-TPC"] + tpcts_tracklist += ["ITS-TPC-TRD"] if "ITS-TPC" in trdsrc else [] + tpcts_tracklist += ["ITS-TPC-TOF"] if "ITS-TPC" in tofsrc else [] + tpcts_tracklist += ["ITS-TPC-TRD-TOF"] if "ITS-TPC" in trdsrc and "ITS-TPC-TRD" in tofsrc else [] + tpcts_tracktypes = cleanDetectorInputList(",".join(tpcts_tracklist)) + tpcts_clustertypes = "FT0,TPC" + tpcts_clustertypes += ",TOF" if "TOF" in tpcts_tracktypes else "" + tpcts_clustertypes += ",TRD" if "TRD" in tpcts_tracktypes else "" + tpcts_detectors = "ITS,TPC,FT0" + else: + # older async tags (e.g. 2023 apass4, 2024 pp apass1) always ask for TOF and TRD inputs + tpcts_tracktypes = "ITS,TPC,ITS-TPC,ITS-TPC-TOF,ITS-TPC-TRD-TOF" + tpcts_clustertypes = "FT0,TOF,TPC" + tpcts_detectors = "ITS,TPC,FT0,TOF,TRD" tpctsneeds = [ TPCRECOtask['name'], ITSTPCMATCHtask['name'], - TOFTPCMATCHERtask['name'], FT0RECOtask['name'], - PVFINDERtask['name'], - TRDTRACKINGtask2['name'] + PVFINDERtask['name'] ] + addWhenActive("TOF", tpctsneeds, TOFTPCMATCHERtask['name']) + addWhenActive("TRD", tpctsneeds, TRDTRACKINGtask2['name']) TPCTStask = createTask(name='tpctimeseries_'+str(tf), needs=tpctsneeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], mem='2000', cpu='1') - TPCTStask['cmd'] = 'o2-global-track-cluster-reader --disable-mc --cluster-types "FT0,TOF,TPC" --track-types "ITS,TPC,ITS-TPC,ITS-TPC-TOF,ITS-TPC-TRD-TOF"' + TPCTStask['cmd'] = f'o2-global-track-cluster-reader --disable-mc --cluster-types "{tpcts_clustertypes}" --track-types "{tpcts_tracktypes}"' TPCTStask['cmd'] += ' --primary-vertices ' TPCTStask['cmd'] += staggITS - TPCTStask['cmd'] += ' | o2-tpc-time-series-workflow --enable-unbinned-root-output --sample-unbinned-tsallis --sampling-factor 0.01 ' + # newer builds have their own readers; switch them off so that only the reader above runs + TPCTStask['cmd'] += f' | o2-tpc-time-series-workflow{tpcts_ownreaders}' + TPCTStask['cmd'] += f'{tpcts_sources} "{tpcts_tracktypes},FT0"' if tpcts_sources else '' + TPCTStask['cmd'] += ' --enable-unbinned-root-output --sample-unbinned-tsallis --sampling-factor 0.01 ' TPCTStask['cmd'] += staggITS TPCTStask['cmd'] += putConfigValues() + ' ' + getDPL_global_options(bigshm=True) - # could be relaxed or changed once the timerseries worklow is more reactive to input cluster- and track-types - addWhenActive("TOF,TPC,FT0", workflow['stages'], TPCTStask) + addWhenActive(tpcts_detectors, workflow['stages'], TPCTStask) # cleanup # -------- From 5b58ea9f04e0ee5cf547090339cb4c82a88be0df Mon Sep 17 00:00:00 2001 From: jaimenorman Date: Thu, 5 Nov 2026 09:32:14 +0000 Subject: [PATCH 131/144] higher MB gaps for testing (#2515) * higher MB gaps for testing * add tests * increase tolerance --- .../GeneratorJE_gapgen11_hook_pp13600GeV.ini | 9 +++ .../GeneratorJE_gapgen7_hook_pp13600GeV.ini | 9 +++ .../GeneratorJE_gapgen9_hook_pp13600GeV.ini | 9 +++ .../GeneratorJE_gapgen11_hook_pp13600GeV.C | 71 +++++++++++++++++++ .../GeneratorJE_gapgen7_hook_pp13600GeV.C | 71 +++++++++++++++++++ .../GeneratorJE_gapgen9_hook_pp13600GeV.C | 71 +++++++++++++++++++ 6 files changed, 240 insertions(+) create mode 100644 MC/config/PWGGAJE/ini/GeneratorJE_gapgen11_hook_pp13600GeV.ini create mode 100644 MC/config/PWGGAJE/ini/GeneratorJE_gapgen7_hook_pp13600GeV.ini create mode 100644 MC/config/PWGGAJE/ini/GeneratorJE_gapgen9_hook_pp13600GeV.ini create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen11_hook_pp13600GeV.C create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen7_hook_pp13600GeV.C create mode 100644 MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen9_hook_pp13600GeV.C diff --git a/MC/config/PWGGAJE/ini/GeneratorJE_gapgen11_hook_pp13600GeV.ini b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen11_hook_pp13600GeV.ini new file mode 100644 index 000000000..8a061b68b --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen11_hook_pp13600GeV.ini @@ -0,0 +1,9 @@ +### jet-jet production with MB Gap 11 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_jets_hook.C +funcName = getGeneratorPythia8GapGenJE(11,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/GeneratorJE_gapgen7_hook_pp13600GeV.ini b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen7_hook_pp13600GeV.ini new file mode 100644 index 000000000..6e3625e80 --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen7_hook_pp13600GeV.ini @@ -0,0 +1,9 @@ +### jet-jet production with MB Gap 7 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_jets_hook.C +funcName = getGeneratorPythia8GapGenJE(7,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/GeneratorJE_gapgen9_hook_pp13600GeV.ini b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen9_hook_pp13600GeV.ini new file mode 100644 index 000000000..eeb94e5e5 --- /dev/null +++ b/MC/config/PWGGAJE/ini/GeneratorJE_gapgen9_hook_pp13600GeV.ini @@ -0,0 +1,9 @@ +### jet-jet production with MB Gap 9 +### The external generator derives from GeneratorPythia8. +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_jets_hook.C +funcName = getGeneratorPythia8GapGenJE(9,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen11_hook_pp13600GeV.C b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen11_hook_pp13600GeV.C new file mode 100644 index 000000000..f8f35ea10 --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen11_hook_pp13600GeV.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./11; // one event triggered out of 11 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# Jet-jet events " << nEventsJetJet << "\n"; + std::cout << " sum of weights jet-jet events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.85 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.15) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.85 || nEventsJetJet > nEvents * ratioTrigger * 1.15) { + std::cerr << "Number of jet-jet generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.85 || nEventsMB > sumWeightsMB * 1.15) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen7_hook_pp13600GeV.C b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen7_hook_pp13600GeV.C new file mode 100644 index 000000000..3c3ebe1f3 --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen7_hook_pp13600GeV.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./7; // one event triggered out of 7 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# Jet-jet events " << nEventsJetJet << "\n"; + std::cout << " sum of weights jet-jet events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.95 || nEventsJetJet > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of jet-jet generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.95 || nEventsMB > sumWeightsMB * 1.05) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen9_hook_pp13600GeV.C b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen9_hook_pp13600GeV.C new file mode 100644 index 000000000..0c35fc634 --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/GeneratorJE_gapgen9_hook_pp13600GeV.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./9; // one event triggered out of 9 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# Jet-jet events " << nEventsJetJet << "\n"; + std::cout << " sum of weights jet-jet events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (jet-jet + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.90 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.10) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.90 || nEventsJetJet > nEvents * ratioTrigger * 1.10) { + std::cerr << "Number of jet-jet generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.90 || nEventsMB > sumWeightsMB * 1.10) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} From 5eb59f1921936749f7a27ebd3af35b73096b2b9d Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Thu, 5 Nov 2026 11:14:38 +0100 Subject: [PATCH 132/144] Add a decay config that lets the EventDecayer decay charm baryons (#2518) This commit adds eventdecayer_charmbaryon.cfg, which switches on the decays of Xi_c0, Xi_c+ and Omega_c0 for the EventDecayer. - base.cfg keeps these charm baryons stable, because Geant4 hands them to the external decayer one at a time. - The EventDecayer decays them before transport and keeps the full decay chain in the MC truth. Usage: -confKey 'DecayerPythia8.config[1]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/decayer/force_hadronic_charmbaryon.cfg;EventDecayer.enable=true;EventDecayer.config[2]=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGHF/pythia8/decayer/eventdecayer_charmbaryon.cfg' https://github.com/AliceO2Group/AliceO2/pull/15995 https://its.cern.ch/jira/browse/O2-5903 Co-authored-by: Claude Opus 5.5 --- .../PWGHF/pythia8/decayer/eventdecayer_charmbaryon.cfg | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 MC/config/PWGHF/pythia8/decayer/eventdecayer_charmbaryon.cfg diff --git a/MC/config/PWGHF/pythia8/decayer/eventdecayer_charmbaryon.cfg b/MC/config/PWGHF/pythia8/decayer/eventdecayer_charmbaryon.cfg new file mode 100644 index 000000000..eb6fb800c --- /dev/null +++ b/MC/config/PWGHF/pythia8/decayer/eventdecayer_charmbaryon.cfg @@ -0,0 +1,6 @@ +### Lets the EventDecayer (EventDecayer.enable=true) decay the charm baryons that base.cfg keeps stable. +### Use it in a free EventDecayer.config slot next to the force_hadronic_charmbaryon*.cfg decay channels, +### e.g. DecayerPythia8.config[1]=force_hadronic_charmbaryon.cfg;EventDecayer.enable=true;EventDecayer.config[2]=eventdecayer_charmbaryon.cfg +4132:mayDecay = on # Xi_c0 +4232:mayDecay = on # Xi_c+ +4332:mayDecay = on # Omega_c0 From 19691bbb90347b2258597e67711078b28df3e50f Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Tue, 20 Oct 2026 15:32:04 +0200 Subject: [PATCH 133/144] Add an example for UPC events in hadronic Pb-Pb pile-up This commit provides an example script that embeds STARlight UPC events into hadronic Pb-Pb pile-up, anchored to a real run. - Every N-th collision (default 10) is a UPC event in its own bunch crossing; the others are Angantyr Pb-Pb collisions. - The interaction rate and the timeframe length come from the anchor run. - The number of background events is set so that none repeats within a timeframe. Co-Authored-By: Claude Opus 5.5 --- MC/run/examples/UPC/upc_in_hadronic_pileup.sh | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100755 MC/run/examples/UPC/upc_in_hadronic_pileup.sh diff --git a/MC/run/examples/UPC/upc_in_hadronic_pileup.sh b/MC/run/examples/UPC/upc_in_hadronic_pileup.sh new file mode 100755 index 000000000..83f1c38b6 --- /dev/null +++ b/MC/run/examples/UPC/upc_in_hadronic_pileup.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +#JDL_PACKAGE=VO_ALICE@O2PDPSuite::MC-prod-2026-v11-1 +#JDL_OUTPUT=AO2D.root@disk=2,debug_log_archive.tgz@disk=2 +#JDL_ERROROUTPUT=debug_log_archive.tgz@disk=2 + +# Anchored Pb-Pb MC: STARlight UPC events embedded in hadronic (Pythia8 Angantyr) pile-up +# at the interaction rate of the anchor run. +# +# Every UPC_EVERY_N-th collision of the anchor run's collision sequence is replaced by a +# UPC event in its own bunch crossing; all other collisions are hadronic Pb-Pb collisions. +# The interaction-rate dependence is obtained by choosing anchor runs at different rates. +# In the AOD, the UPC collisions have generator ID 51 (mcCollision.getGeneratorId()). +# +# Knobs: +# UPC_PROCESS STARlight process (as in makeStarlightConfig.py) +# UPC_EVERY_N every N-th collision is a UPC event; +# the hadronic pile-up density is (N-1)/N of the real one +# NBKG distinct Angantyr events per job; should exceed the number of +# hadronic collisions in one timeframe, so that none repeats within it +# ANCHOR_RUN anchor run; the interaction rate is taken from its CTP scalers +# NTIMEFRAMES timeframes per job; the background is simulated once per job, +# so more timeframes per job is cheaper than many short jobs +# +# The timeframe length is taken from the anchor run (32 orbits for 2023 Pb-Pb). +# Do not force longer timeframes with ALIEN_JDL_MC_ORBITS_PER_TF: with realistic +# hadronic pile-up, TPC digitisation then exceeds the memory of a GRID slot. +# +# Submit to the GRID with +# ${O2DPG_ROOT}/GRID/utils/grid_submit.sh --script upc_in_hadronic_pileup.sh --jobname upc --cores 8 --ttl 43200 + +UPC_PROCESS=${UPC_PROCESS:-kCohJpsiToMu} +UPC_EVERY_N=${UPC_EVERY_N:-10} +NBKG=${NBKG:-150} +ANCHOR_RUN=${ANCHOR_RUN:-544868} + +export O2DPG_MC_CONFIG_ROOT=${O2DPG_MC_CONFIG_ROOT:-${O2DPG_ROOT}} + +# anchoring: Pb-Pb 2023, apass5 +export ALIEN_JDL_LPMANCHORPASSNAME=apass5 +export ALIEN_JDL_MCANCHOR=apass5 +export ALIEN_JDL_LPMANCHORYEAR=2023 +export ALIEN_JDL_LPMINTERACTIONTYPE=PbPb +export ALIEN_JDL_LPMPRODUCTIONTYPE=MC +export ALIEN_JDL_LPMPRODUCTIONTAG=${ALIEN_JDL_LPMPRODUCTIONTAG:-UPCPileupExample} +export ALIEN_JDL_LPMRUNNUMBER=${ANCHOR_RUN} +export ALIEN_JDL_LPMANCHORRUN=${ANCHOR_RUN} +export ALIEN_JDL_LPMANCHORPRODUCTION=LHC23zzh +# reconstruction with the software of the data pass (2-tag setup) +export ALIEN_JDL_O2DPG_ASYNC_RECO_TAG=VO_ALICE@O2PDPSuite::async-async-2023-PbPb-apass5-v8-slc9-alidist-async-2023-PbPb-apass5-v8-1 +export ALIEN_JDL_TPCDEDXCLMASK=7 +export ALIEN_JDL_TPCCLUSTERFILTER=2 +export ALIEN_JDL_TPCCHICUTOPT=0 +export ALIEN_JDL_TPCCLUSEDGEREDEF=0.5 + +export NTIMEFRAMES=${NTIMEFRAMES:-2} +export NSIGEVENTS=10000 # no cap: the timeframe is filled by the interaction rate +export SPLITID=${ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID:-1} +export PRODSPLIT=${ALIEN_O2DPG_GRIDSUBMIT_PRODSPLIT:-1} +export CYCLE=0 +export DISABLE_QC=1 +export PUBLISH_MCPRODINFO=0 # example: do not publish to CCDB + +cat > simconfig.json <<'EOF' +{ + "ConfigParams": { + "TPCEleParam": { + "doSaturationTail" : "1" + } + } +} +EOF + +${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGUD/ini/makeStarlightConfig.py --process ${UPC_PROCESS} --collType PbPb --eCM 5360 --rapidity cent_rap + +export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -trigger external -ini ../GenStarlight.ini \ + -confKey 'PrimaryGenerator.id=51;SimCutParams.stepTrackRefHook=true' \ + --embedding -genBkg pythia8 -procBkg heavy_ion -nb ${NBKG} --embeddPattern r0:e${UPC_EVERY_N} \ + --overwrite-config simconfig.json --no-tpc-digitchunking --tpc-distortion-type 2 --tpc-corrmap-lumi-mode 2" + +${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh From 6ab618e32a88ff22bdd04cfad067e062cf68fe67 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Fri, 23 Oct 2026 19:15:25 +0200 Subject: [PATCH 134/144] Inject UPC events as an independent stream in the UPC example This changes the UPC example to keep the full hadronic rate and add the UPC events on top. - The UPC events now come at their own rate (default 67 kHz, about one per ITS readout frame) instead of replacing every 10th hadronic collision. - The number of distinct background events is reduced to 30. - The release is MC-prod-2026-v13-1, since older tags anchor the first job where the ITS dead map masks all chips. Co-Authored-By: Claude Opus 5.5 --- MC/run/examples/UPC/upc_in_hadronic_pileup.sh | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/MC/run/examples/UPC/upc_in_hadronic_pileup.sh b/MC/run/examples/UPC/upc_in_hadronic_pileup.sh index 83f1c38b6..eb5cf07c8 100755 --- a/MC/run/examples/UPC/upc_in_hadronic_pileup.sh +++ b/MC/run/examples/UPC/upc_in_hadronic_pileup.sh @@ -1,22 +1,20 @@ #!/usr/bin/env bash -#JDL_PACKAGE=VO_ALICE@O2PDPSuite::MC-prod-2026-v11-1 +#JDL_PACKAGE=VO_ALICE@O2PDPSuite::MC-prod-2026-v13-1 #JDL_OUTPUT=AO2D.root@disk=2,debug_log_archive.tgz@disk=2 #JDL_ERROROUTPUT=debug_log_archive.tgz@disk=2 -# Anchored Pb-Pb MC: STARlight UPC events embedded in hadronic (Pythia8 Angantyr) pile-up -# at the interaction rate of the anchor run. +# Anchored Pb-Pb MC: STARlight UPC events on top of hadronic (Pythia8 Angantyr) pile-up. # -# Every UPC_EVERY_N-th collision of the anchor run's collision sequence is replaced by a -# UPC event in its own bunch crossing; all other collisions are hadronic Pb-Pb collisions. -# The interaction-rate dependence is obtained by choosing anchor runs at different rates. +# The hadronic Pb-Pb collisions follow the interaction rate of the anchor run. On top of them, +# UPC events are injected as an independent random stream at UPC_RATE, each in its own bunch +# crossing. The interaction-rate dependence is obtained by choosing anchor runs at different rates. # In the AOD, the UPC collisions have generator ID 51 (mcCollision.getGeneratorId()). # # Knobs: # UPC_PROCESS STARlight process (as in makeStarlightConfig.py) -# UPC_EVERY_N every N-th collision is a UPC event; -# the hadronic pile-up density is (N-1)/N of the real one -# NBKG distinct Angantyr events per job; should exceed the number of -# hadronic collisions in one timeframe, so that none repeats within it +# UPC_RATE rate of the UPC stream in Hz; 67000 is about one per ITS readout frame +# NBKG distinct Angantyr events per job, reused in turn; a repeated event +# appears milliseconds later, far beyond an ITS readout frame or the TPC drift time # ANCHOR_RUN anchor run; the interaction rate is taken from its CTP scalers # NTIMEFRAMES timeframes per job; the background is simulated once per job, # so more timeframes per job is cheaper than many short jobs @@ -25,12 +23,15 @@ # Do not force longer timeframes with ALIEN_JDL_MC_ORBITS_PER_TF: with realistic # hadronic pile-up, TPC digitisation then exceeds the memory of a GRID slot. # +# Use MC-prod-2026-v12-1 or later: older tags anchor the first job at the start of the run, +# where the ITS dead map masks all chips and the AOD has no ITS and no collisions. +# # Submit to the GRID with # ${O2DPG_ROOT}/GRID/utils/grid_submit.sh --script upc_in_hadronic_pileup.sh --jobname upc --cores 8 --ttl 43200 UPC_PROCESS=${UPC_PROCESS:-kCohJpsiToMu} -UPC_EVERY_N=${UPC_EVERY_N:-10} -NBKG=${NBKG:-150} +UPC_RATE=${UPC_RATE:-67000} +NBKG=${NBKG:-30} ANCHOR_RUN=${ANCHOR_RUN:-544868} export O2DPG_MC_CONFIG_ROOT=${O2DPG_MC_CONFIG_ROOT:-${O2DPG_ROOT}} @@ -74,7 +75,7 @@ ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGUD/ini/makeStarlightConfig.py --process ${U export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-gen external -trigger external -ini ../GenStarlight.ini \ -confKey 'PrimaryGenerator.id=51;SimCutParams.stepTrackRefHook=true' \ - --embedding -genBkg pythia8 -procBkg heavy_ion -nb ${NBKG} --embeddPattern r0:e${UPC_EVERY_N} \ + --embedding -genBkg pythia8 -procBkg heavy_ion -nb ${NBKG} --embeddPattern ${UPC_RATE},1000000:1000000 \ --overwrite-config simconfig.json --no-tpc-digitchunking --tpc-distortion-type 2 --tpc-corrmap-lumi-mode 2" ${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh From 904a359a956fe2b8883ee15ed1a5aed29580984f Mon Sep 17 00:00:00 2001 From: alcaliva <32872606+alcaliva@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:51:14 +0100 Subject: [PATCH 135/144] Add hybrid genberator for antideuterons in jets (#2537) --- .../PWGLF/hybrid/hybrid_coalescence.json | 24 ++++ .../GeneratorAntideuteronCoalescenceJet.ini | 6 + .../GeneratorAntideuteronCoalescenceJet.C | 85 +++++++++++++ ...generator_pythia8_antideuteron_injection.C | 114 ++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 MC/config/PWGLF/hybrid/hybrid_coalescence.json create mode 100644 MC/config/PWGLF/ini/GeneratorAntideuteronCoalescenceJet.ini create mode 100644 MC/config/PWGLF/ini/tests/GeneratorAntideuteronCoalescenceJet.C create mode 100644 MC/config/PWGLF/pythia8/generator_pythia8_antideuteron_injection.C diff --git a/MC/config/PWGLF/hybrid/hybrid_coalescence.json b/MC/config/PWGLF/hybrid/hybrid_coalescence.json new file mode 100644 index 000000000..32c3f54ae --- /dev/null +++ b/MC/config/PWGLF/hybrid/hybrid_coalescence.json @@ -0,0 +1,24 @@ + +{ + "mode": "sequential", + "generators": [ + { + "name": "pythia8", + "config": { + "config": "${O2_ROOT}/share/Generators/egconfig/pythia8_inel.cfg", + "hooksFileName": "", + "hooksFuncName": "", + "includePartonEvent": false, + "particleFilter": "", + "verbose": 0 + } + }, + { + "name": "external", + "config": { + "iniFile": "${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/ini/GeneratorAntideuteronCoalescenceJet.ini" + } + } + ], + "fractions": [4, 1] +} diff --git a/MC/config/PWGLF/ini/GeneratorAntideuteronCoalescenceJet.ini b/MC/config/PWGLF/ini/GeneratorAntideuteronCoalescenceJet.ini new file mode 100644 index 000000000..1a6394c65 --- /dev/null +++ b/MC/config/PWGLF/ini/GeneratorAntideuteronCoalescenceJet.ini @@ -0,0 +1,6 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGLF/pythia8/generator_pythia8_antideuteron_injection.C +funcName = generateAntideuterons(0.8) + +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_jet.cfg \ No newline at end of file diff --git a/MC/config/PWGLF/ini/tests/GeneratorAntideuteronCoalescenceJet.C b/MC/config/PWGLF/ini/tests/GeneratorAntideuteronCoalescenceJet.C new file mode 100644 index 000000000..f456070a6 --- /dev/null +++ b/MC/config/PWGLF/ini/tests/GeneratorAntideuteronCoalescenceJet.C @@ -0,0 +1,85 @@ + +#if !defined(__CLING__) || defined(__ROOTCLING__) +#include "SimulationDataFormat/MCTrack.h" +#include "TFile.h" +#include "TTree.h" + +#include +#include +#include +#include +#endif // !defined(__CLING__) || defined(__ROOTCLING__) + +int External() +{ + std::string path{"o2sim_Kine.root"}; + + std::vector injectedPDGs = { + -1000010020 // anti-deuteron + }; + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto* tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + + std::vector* tracks = nullptr; + tree->SetBranchAddress("MCTrack", &tracks); + + std::vector nSignal(injectedPDGs.size(), 0); + std::vector nEventsWithSignal(injectedPDGs.size(), 0); + + const auto nEvents = tree->GetEntries(); + + for (Long64_t i = 0; i < nEvents; ++i) { + tree->GetEntry(i); + + std::vector found(injectedPDGs.size(), false); + + for (const auto& track : *tracks) { + const int pdg = track.GetPdgCode(); + + auto it = std::find( + injectedPDGs.begin(), + injectedPDGs.end(), + pdg); + + if (it != injectedPDGs.end()) { + const auto index = std::distance(injectedPDGs.begin(), it); + ++nSignal[index]; + found[index] = true; + } + } + + for (size_t j = 0; j < injectedPDGs.size(); ++j) { + if (found[j]) { + ++nEventsWithSignal[j]; + } + } + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + + for (size_t i = 0; i < injectedPDGs.size(); ++i) { + std::cout << "PDG: " << injectedPDGs[i] << "\n"; + std::cout << "# Generated particles: " << nSignal[i] << "\n"; + std::cout << "# Events containing particle: " + << nEventsWithSignal[i] << "\n"; + + if (nSignal[i] == 0) { + std::cerr << "No generated particles with PDG " + << injectedPDGs[i] << "\n"; + return 1; + } + } + + return 0; +} diff --git a/MC/config/PWGLF/pythia8/generator_pythia8_antideuteron_injection.C b/MC/config/PWGLF/pythia8/generator_pythia8_antideuteron_injection.C new file mode 100644 index 000000000..c4f17ea52 --- /dev/null +++ b/MC/config/PWGLF/pythia8/generator_pythia8_antideuteron_injection.C @@ -0,0 +1,114 @@ +#if !defined(__CLING__) || defined(__ROOTCLING__) + +#include "FairGenerator.h" +#include "Generators/GeneratorPythia8.h" +#include "Pythia8/Pythia.h" +#include "TRandom.h" +#include "fairlogger/Logger.h" + +#include +#include +#include + +#endif // !defined(__CLING__) || defined(__ROOTCLING__) + +/// PYTHIA8 event generator with antideuteron injection. +/// Events are generated until a final-state antiproton within the specified etaMax is found. +/// The antiproton is then replaced by an antideuteron with twice its momentum. + +class GeneratorPythia8AntideuteronInjection + : public o2::eventgen::GeneratorPythia8 { + + public: + + GeneratorPythia8AntideuteronInjection(double etaMax = 0.8) + : o2::eventgen::GeneratorPythia8(), + mEtaMax(etaMax) {} + + ~GeneratorPythia8AntideuteronInjection() override = default; + + bool Init() override { + addSubGenerator(0, "Pythia8 events with antideuterons"); + return o2::eventgen::GeneratorPythia8::Init(); + } + + protected: + + + bool generateEvent() override { + constexpr int maxAttempts = 10000; + for (int attempt = 0; attempt < maxAttempts; ++attempt) { + if (!GeneratorPythia8::generateEvent()) { + return false; + } + + if (EventHasAntideuteron(mPythia.event)) { + LOG(debug) << ">> An antideuteron was formed!"; + + notifySubGenerator(0); + ++mGeneratedEvents; + + return true; + } + } + + LOG(error) << "Failed to generate an antideuteron after " << maxAttempts << " attempts"; + return false; + } + + bool EventHasAntideuteron(Pythia8::Event& event) { + + const double md = 1.87561294257; // Deuteron mass in GeV/c^2 + const int pdgAntid = -1000010020; // Antideuteron PDG code + + for (int iPart = 0; iPart < event.size(); ++iPart) { + + if (!event[iPart].isFinal()) { + continue; + } + + if (std::abs(event[iPart].eta()) > mEtaMax) { + continue; + } + + if (event[iPart].id() != -2212) { + continue; + } + + double px = 2.0 * event[iPart].px(); + double py = 2.0 * event[iPart].py(); + double pz = 2.0 * event[iPart].pz(); + double energy = std::sqrt(md * md + px * px + py * py + pz * pz); + + // Remove antiproton from final state + event[iPart].statusNeg(); + + // Add antideuteron + const int iAntid = event.size(); + event.append(pdgAntid, 121, iPart, 0, 0, 0, 0, 0, px, py, pz, energy, md); + // Update daughter indices + event[iPart].daughter1(iAntid); + event[iPart].daughter2(iAntid); + return true; + } + return false; + } + + private: + + double mEtaMax = 0.8; + uint64_t mGeneratedEvents = 0; +}; + +/// Generator factory + +FairGenerator* generateAntideuterons(double etaMax = 0.8) { + + auto* myGenerator = new GeneratorPythia8AntideuteronInjection(etaMax); + auto seed = gRandom->TRandom::GetSeed() % 900000000; + + myGenerator->readString("Random:setSeed = on"); + myGenerator->readString("Random:seed = " + std::to_string(seed)); + + return myGenerator; +} From f8c0a6daa3e1915e5489247110b65f2dfa30cc20 Mon Sep 17 00:00:00 2001 From: glegras <71757105+glegras@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:35:55 +0100 Subject: [PATCH 136/144] PWGDQ: adding chi_c1(3872) and updating quarkonia proportions (#2536) * adding chi_c1(3872) in the B decay table * replacing a fraction of chi_c1(1P) by chi_c1(3872) * comments from Alex * dummy commit --- .../BTOPSIJPSITODIELECTRON.DEC | 146 +++++++++++------- .../CHARMONIATOJPSIANDDIELECTRON.DEC | 5 + ...enerator_pythia8_HadronTriggered_withGap.C | 111 ++++++++++--- ...dInclusiveQuarkonia_Pythia8_TriggerGap.ini | 5 +- ...Quarkonia_enhancedB_Pythia8_TriggerGap.ini | 6 +- .../generator/pythia8_oniaAll_triggerGap.cfg | 1 + 6 files changed, 201 insertions(+), 73 deletions(-) diff --git a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC index 9d2f62ea5..06b609242 100644 --- a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC +++ b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/BTOPSIJPSITODIELECTRON.DEC @@ -36,18 +36,22 @@ Decay B0 0.000660000 J/psi K*0 pi+ pi- PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000011200 chi_c1 pi0 PHSP; - 0.000395000 chi_c1 K0 SVS; - 0.000497000 chi_c1 K+ pi- PHSP; - 0.000320000 chi_c1 K0 pi+ pi- PHSP; - 0.000350000 chi_c1 K+ pi0 pi- PHSP; +0.000011200 chi_c1 pi0 PHSP; +0.000395000 chi_c1 K0 SVS; +0.000497000 chi_c1 K+ pi- PHSP; +0.000320000 chi_c1 K0 pi+ pi- PHSP; +0.000350000 chi_c1 K+ pi0 pi- PHSP; # --- chi_c2 --- - 0.000015000 chi_c2 K0 STS; - 0.000049000 chi_c2 K*0 PHSP; - 0.000072000 chi_c2 K+ pi- PHSP; - 0.000174000 chi_c2 pi+ pi- K0 PHSP; - 0.000074000 chi_c2 pi- pi0 K+ PHSP; +0.000015000 chi_c2 K0 STS; +0.000049000 chi_c2 K*0 PHSP; +0.000072000 chi_c2 K+ pi- PHSP; +0.000174000 chi_c2 pi+ pi- K0 PHSP; +0.000074000 chi_c2 pi- pi0 K+ PHSP; + +# --- chi_c1(3872) --- +0.000110000 X_1(3872) K0 SVS; +0.000180000 X_1(3872) K+ pi- PHSP; Enddecay Decay anti-B0 @@ -87,18 +91,23 @@ Decay anti-B0 0.000660000 J/psi anti-K*0 pi- pi+ PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000011200 chi_c1 pi0 PHSP; - 0.000395000 chi_c1 anti-K0 SVS; - 0.000497000 chi_c1 K- pi+ PHSP; - 0.000320000 chi_c1 anti-K0 pi- pi+ PHSP; - 0.000350000 chi_c1 K- pi0 pi+ PHSP; +0.000011200 chi_c1 pi0 PHSP; +0.000395000 chi_c1 anti-K0 SVS; +0.000497000 chi_c1 K- pi+ PHSP; +0.000320000 chi_c1 anti-K0 pi- pi+ PHSP; +0.000350000 chi_c1 K- pi0 pi+ PHSP; # --- chi_c2 --- - 0.000015000 chi_c2 anti-K0 STS; - 0.000049000 chi_c2 anti-K*0 PHSP; - 0.000072000 chi_c2 K- pi+ PHSP; - 0.000174000 chi_c2 pi- pi+ anti-K0 PHSP; - 0.000074000 chi_c2 pi+ pi0 K- PHSP; +0.000015000 chi_c2 anti-K0 STS; +0.000049000 chi_c2 anti-K*0 PHSP; +0.000072000 chi_c2 K- pi+ PHSP; +0.000174000 chi_c2 pi- pi+ anti-K0 PHSP; +0.000074000 chi_c2 pi+ pi0 K- PHSP; + +# --- chi_c1(3872) --- +0.000110000 X_1(3872) anti-K0 SVS; +0.000180000 X_1(3872) K- pi+ PHSP; + Enddecay Decay B+ @@ -134,19 +143,23 @@ Decay B+ 0.000011800 J/psi p+ anti-Lambda0 PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000022000 chi_c1 pi+ PHSP; - 0.000474000 chi_c1 K+ SVS; - 0.000300000 chi_c1 K*+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; - 0.000329000 chi_c1 K+ pi0 PHSP; - 0.000580000 chi_c1 K0 pi+ PHSP; - 0.000374000 chi_c1 K+ pi+ pi- PHSP; +0.000022000 chi_c1 pi+ PHSP; +0.000474000 chi_c1 K+ SVS; +0.000300000 chi_c1 K*+ SVV_HELAMP PKHplus PKphHplus PKHzero PKphHzero PKHminus PKphHminus; +0.000329000 chi_c1 K+ pi0 PHSP; +0.000580000 chi_c1 K0 pi+ PHSP; +0.000374000 chi_c1 K+ pi+ pi- PHSP; # --- chi_c2 --- - 0.000110000 chi_c2 K+ STS; - 0.000120000 chi_c2 K*+ PHSP; - 0.000062000 chi_c2 K+ pi0 PHSP; - 0.000124000 chi_c2 K0 pi+ PHSP; - 0.000134000 chi_c2 K+ pi+ pi- PHSP; +0.000110000 chi_c2 K+ STS; +0.000120000 chi_c2 K*+ PHSP; +0.000062000 chi_c2 K+ pi0 PHSP; +0.000124000 chi_c2 K0 pi+ PHSP; +0.000134000 chi_c2 K+ pi+ pi- PHSP; + +# --- chi_c1(3872) --- +0.000190000 X_1(3872) K+ SVS; +0.000240000 X_1(3872) K0 pi+ PHSP; Enddecay Decay B- @@ -181,19 +194,24 @@ Decay B- 0.000011800 J/psi anti-p- Lambda0 PHSP; #[New mode added] #[Reconstructed PDG2011] # --- chi_c1 --- - 0.000022000 chi_c1 pi- PHSP; - 0.000474000 chi_c1 K- SVS; - 0.000300000 chi_c1 K*- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; - 0.000329000 chi_c1 K- pi0 PHSP; - 0.000580000 chi_c1 anti-K0 pi- PHSP; - 0.000374000 chi_c1 K- pi- pi+ PHSP; +0.000022000 chi_c1 pi- PHSP; +0.000474000 chi_c1 K- SVS; +0.000300000 chi_c1 K*- SVV_HELAMP PKHminus PKphHminus PKHzero PKphHzero PKHplus PKphHplus; +0.000329000 chi_c1 K- pi0 PHSP; +0.000580000 chi_c1 anti-K0 pi- PHSP; +0.000374000 chi_c1 K- pi- pi+ PHSP; # --- chi_c2 --- - 0.000110000 chi_c2 K- STS; - 0.000120000 chi_c2 K*- PHSP; - 0.000062000 chi_c2 K- pi0 PHSP; - 0.000124000 chi_c2 anti-K0 pi- PHSP; - 0.000134000 chi_c2 K- pi- pi+ PHSP; +0.000110000 chi_c2 K- STS; +0.000120000 chi_c2 K*- PHSP; +0.000062000 chi_c2 K- pi0 PHSP; +0.000124000 chi_c2 anti-K0 pi- PHSP; +0.000134000 chi_c2 K- pi- pi+ PHSP; + +# --- chi_c1(3872) --- +0.000190000 X_1(3872) K- SVS; +0.000240000 X_1(3872) K0 pi- PHSP; + Enddecay Decay B_s0 @@ -242,7 +260,13 @@ Decay B_s0 0.0000023 phi e+ e- BTOSLLALI; # --- chi_c1 --- - 0.0001920 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; +0.0001920 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; + +# --- chi_c1(3872) --- +0.000096000 X_1(3872) phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; +0.000076000 X_1(3872) K+ K- PHSP; +0.000037000 X_1(3872) pi+ pi- PHSP; + Enddecay Decay anti-B_s0 @@ -290,7 +314,12 @@ Decay anti-B_s0 0.0000023 phi e- e+ BTOSLLALI; # --- chi_c1 --- - 0.0001970 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; +0.0001970 chi_c1 phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; + +# --- chi_c1(3872) --- +0.000096000 X_1(3872) phi SVV_HELAMP 1.0 0.0 1.0 0.0 1.0 0.0; +0.000076000 X_1(3872) K+ K- PHSP; +0.000037000 X_1(3872) pi+ pi- PHSP; Enddecay Decay Lambda_b0 @@ -302,10 +331,14 @@ Decay Lambda_b0 0.00032 p+ J/psi K- PHSP; 0.000066 p+ J/psi pi- pi+ K- PHSP; 0.000066 p+ psi(2S) K- PHSP; - 0.0000760 chi_c1 p+ K- PHSP; - 0.0000050 chi_c1 p+ pi- PHSP; - 0.0000770 chi_c2 p+ K- PHSP; - 0.0000048 chi_c2 p+ pi- PHSP; +0.0000760 chi_c1 p+ K- PHSP; +0.0000050 chi_c1 p+ pi- PHSP; +0.0000770 chi_c2 p+ K- PHSP; +0.0000048 chi_c2 p+ pi- PHSP; + +# --- chi_c1(3872) --- +0.0000280 X_1(3872) p+ K- PHSP; +0.0000160 X_1(3872) Lambda(1520)0 PHSP; Enddecay Decay anti-Lambda_b0 @@ -317,10 +350,14 @@ Decay anti-Lambda_b0 0.00032 anti-p- J/psi K+ PHSP; 0.000066 anti-p- J/psi pi- pi+ K+ PHSP; 0.000066 anti-p- psi(2S) K+ PHSP; - 0.0000760 chi_c1 anti-p- K+ PHSP; - 0.0000050 chi_c1 anti-p- pi+ PHSP; - 0.0000770 chi_c2 anti-p- K+ PHSP; - 0.0000048 chi_c2 anti-p- pi+ PHSP; +0.0000760 chi_c1 anti-p- K+ PHSP; +0.0000050 chi_c1 anti-p- pi+ PHSP; +0.0000770 chi_c2 anti-p- K+ PHSP; +0.0000048 chi_c2 anti-p- pi+ PHSP; + +# --- chi_c1(3872) --- +0.0000280 X_1(3872) anti-p- K+ PHSP; +0.0000160 X_1(3872) anti-Lambda(1520)0 PHSP; Enddecay Decay Xi_b- @@ -410,5 +447,10 @@ Decay Upsilon(3S) 1.000 e+ e- PHOTOS VLL; Enddecay +Decay X_1(3872) +#### Breit-Wigner function for the pion distribution (S-Wave approximation) +1.000 J/psi pi+ pi- XJPSIRHO0PIPI_S; +Enddecay + End diff --git a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC index fc554fb28..094ce961f 100644 --- a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC +++ b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/CHARMONIATOJPSIANDDIELECTRON.DEC @@ -39,4 +39,9 @@ Decay Upsilon(3S) 1.000 e+ e- PHOTOS VLL; Enddecay +Decay X_1(3872) +#### Breit-Wigner function for the pion distribution (S-Wave approximation) +1.000 J/psi pi+ pi- XJPSIRHO0PIPI_S; +Enddecay + End diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C b/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C index 823b0b322..20b5660dc 100644 --- a/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C @@ -2,6 +2,7 @@ #include "Generators/GeneratorPythia8.h" #include "Pythia8/Pythia.h" #include "TRandom.h" +#include "CommonConstants/PhysicsConstants.h" R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/EvtGen) #include "GeneratorEvtGen.C" @@ -33,7 +34,7 @@ public: mConfigMBdecays = ""; mRapidityMin = -1.; mRapidityMax = 1.; - mVerbose = false; + mVerbose = false; } /// Destructor @@ -70,14 +71,14 @@ public: }; void setTriggerGap(int triggerGap) {mInverseTriggerRatio = triggerGap;} - void setConfigMBdecays(TString val){mConfigMBdecays = val;} - void setVerbose(bool val) { mVerbose = val; }; int getNGeneratedEvents() { return mGeneratedEvents; } - int getTriggerGap() { return mInverseTriggerRatio; } + int getHadronPDG(int i) { return mHadronsPDGs[i]; } + float getRejFactorPrompt(int i) { return mRejFactorPrompt[i]; } + float getRejFactorNonPrompt(int i) { return mRejFactorNonPrompt[i]; } protected: @@ -110,6 +111,69 @@ bool generateEvent() override { return true; } +bool importParticles() override + { + // Possibility to make more actions after particles are imported, such as modifying some PDG values (for chi_c1) + if (!GeneratorPythia8::importParticles()) { + return false; + } + + if (!(mInverseTriggerRatio == 1 || mGeneratedEvents % mInverseTriggerRatio == 1) || !mHasChic1){ + return true; // This is a min bias event or event without prompt chi_c1 + } + + // Since chi_c1(3872) is not produced in PYTHIA we replace prompt chi_c1(1P) with prompt chi_c1(3872) with a given probabiliyt (from the rejFactor) + int iChic3872 = -1; + for (int ihad = 0; ihad < mHadronsPDGs.size(); ihad++) { + if (mHadronsPDGs[ihad] == 9920443 && mRejFactorPrompt[ihad] > 1e-8) { + iChic3872 = ihad; + } + } + if (iChic3872 == -1) { + return true; // In this case we don't request any chi_c1(3872) + } + if (gRandom->Rndm() > mRejFactorPrompt[iChic3872]) { + return true; + } + + for (int idx = 0; idx < mParticles.size(); idx++) { + if (mParticles[idx].GetPdgCode() != 20443 || mParticles[idx].Y() < mRapidityMin || mParticles[idx].Y() > mRapidityMax) { + continue; + } + + LOGF(info, "replacing one chi_c1(1P) with pt=%.3f and y=%.3f by a chi_c1(3872)", mParticles[idx].Pt(), mParticles[idx].Y()); + if (mParticles[idx].GetFirstDaughter() >= 0) { + LOGF(info, "particle has already been decayed, we do not replace it"); + return true; + } + + // modify e and pz to conserve rapidity and modify the mass + float deltaMassSquared = o2::constants::physics::MassX3872 * o2::constants::physics::MassX3872 - o2::constants::physics::MassChiC1 * o2::constants::physics::MassChiC1; + float pz_old = mParticles[idx].Pz(); + float e_old = mParticles[idx].Energy(); + float pz_new = 0.; + float e_new = sqrt(deltaMassSquared + e_old*e_old); + if (std::fabs(pz_old) > 1e-8) { + if (e_old*e_old <= pz_old*pz_old) { + LOGF(error, "|E| <= |p_z|, this is not expected, so we don't replace the chi_c1"); + break; + } + pz_new = sqrt(deltaMassSquared + e_old*e_old - pz_old*pz_old) / sqrt(e_old*e_old/pz_old/pz_old - 1.); + if (pz_old < 0.) { + pz_new = -pz_new; + } + e_new = e_old / pz_old * pz_new; + } + mParticles[idx].SetMomentum(mParticles[idx].Px(), mParticles[idx].Py(), pz_new, e_new); + + mParticles[idx].SetPdgCode(9920443); + + break; + } + + return true; + } + bool Init() override { if(mConfigMBdecays.Contains("cfg")) { @@ -145,6 +209,7 @@ bool isCharmonium(int pdg) { // search for the presence of at least one of the required hadrons in a selected rapidity window bool findHadrons(Pythia8::Event& event) { + mHasChic1 = false; int ihad = 0; for (int ida = 0; ida < event.size(); ++ida) { //auto daughterList = event[ipa].daughterList(); @@ -167,7 +232,6 @@ bool findHadrons(Pythia8::Event& event) { bool isNonPrompt = false; if (isOpenBhadron(pdg)) { isNonPrompt = true; - LOGF(info, "particle is non-prompt"); // if the B hadron decays to something else than a charmonia, we don't trigger it bool hasCharmoniaDecay = false; @@ -199,19 +263,18 @@ bool findHadrons(Pythia8::Event& event) { cout << currentPdg << " "; if (isOpenBhadron(currentPdg)) { isNonPrompt = true; - LOGF(info, "particle is non-prompt"); break; } } cout << endl; } + + if (event[ida].id() == 20443 && !isNonPrompt) mHasChic1 = true; // rejection factor given in the ini file float randomNumber = gRandom->Rndm(); - cout << randomNumber << " rej factor: " << (isNonPrompt ? mRejFactorNonPrompt[ihad] : mRejFactorPrompt[ihad]) << endl; if ((!isNonPrompt && (randomNumber <= mRejFactorPrompt[ihad])) || (isNonPrompt && (randomNumber <= mRejFactorNonPrompt[ihad]))) { - cout << "event triggered " << endl; return true; } } @@ -220,7 +283,6 @@ bool findHadrons(Pythia8::Event& event) { } //} } - return false; }; @@ -239,9 +301,10 @@ private: mRejFactorPrompt; // rejection factors for possibility to trigger a given // particle only a fraction of the time, 1 by default std::vector mRejFactorNonPrompt; - double mRapidityMin; + double mRapidityMin; double mRapidityMax; bool mVerbose; + bool mHasChic1; // in case we want to replace chi_c1(1P) by chi_c1(3872), avoid to recheck the full stack of particles if unnecessary }; } @@ -389,8 +452,10 @@ FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double // The keywords "prompt" and "non-prompt" can be used to modify all prompt and all non-prompt (e.g. "prompt:rejFactor1,non-prompt:rejFactor2") // The keyword can be used for only one particle (e.g. "pdg1:rejFactor1:prompt,pdg1:rejFactor2:non-prompt") // Example if you want only to trigger on a given particle p: "prompt:0,non-prompt:0,p:1" + + // For particle which are not produced by default in Pythia (chi_c1(3872)), the rejFactors string should be understood as a probabiliy to inject it (default being 0.) - int particleList[16] = { + std::vector particleList = { 443, // Jpsi 100443, // psi(2S) 553, // upsilon(1S) @@ -399,6 +464,7 @@ FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double 10441, // chic0 20443, // chic1 445, // chic2 + 9920443, // chic1(3872) // we also add B hadrons to trigger correct rapidity range (e.g. B is // within |y|<1 but non-prompt J/psi has |y|>1) 511, // B0 @@ -410,6 +476,9 @@ FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double 5232, // Xib0 5332 // Omegab }; + + int nCharmonia = 9; + int nCharmoniaWoChic = 5; auto gen = new o2::eventgen::GeneratorEvtGen(); gen->setTriggerGap(triggerGap); @@ -423,8 +492,12 @@ FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double gen->setRapidityRange(rapidityMin - 1., rapidityMax + 1.); } // specify particles to be triggered - for (int i = 0; i < 16; i++) { + for (int i = 0; i < particleList.size(); i++) { gen->addHadronPDGs(particleList[i]); + if (particleList[i] == 9920443) { + // chi_c1(3872) is by default not produced + gen->setRejFactorPrompt(particleList[i], 0.); + } } gen->setVerbose(verbose); @@ -445,14 +518,14 @@ FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double } if (str0.CompareTo("prompt") == 0) { // Common switch for all prompt particles - for (int ihad = 0; ihad < 16; ihad++) { + for (int ihad = 0; ihad < particleList.size(); ihad++) { gen->setRejFactorPrompt(particleList[ihad], str1.Atof()); } continue; } if (str0.CompareTo("non-prompt") == 0) { // Common switch for all non-prompt particles - for (int ihad = 0; ihad < 16; ihad++) { + for (int ihad = 0; ihad < particleList.size(); ihad++) { gen->setRejFactorNonPrompt(particleList[ihad], str1.Atof()); } continue; @@ -494,10 +567,10 @@ FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double gen->PrintDebug(true); // specify particles to be decayed with EvtGen - gen->SetSizePdg(16); - for (int i = 0; i < 16; i++) { + gen->SetSizePdg(particleList.size()); + for (int i = 0; i < particleList.size(); i++) { // Decay only what was left undecayed by Pythia - if (decayMode == 0 || (i <= 7 && decayMode == 1) || (i <= 4 && decayMode == 2)) { + if (decayMode == 0 || (i < nCharmonia && decayMode == 1) || (i < nCharmoniaWoChic && decayMode == 2)) { gen->AddPdg(particleList[i], i); } } @@ -525,9 +598,9 @@ FairGenerator *GeneratorInclusiveAllQuarkonia_EvtGenMidY(int triggerGap, double // add trigger on the correct rapidity range after EvtGen decays if (decayMode != 2) { - // In this case EvtGen will not affect charmonia rapidities, so no need to start with a looser rapidity cut, first trigger already does the job + // If decayMode == 2, EvtGen will not affect charmonia rapidities, so no need to start with a looser rapidity cut, first trigger already does the job gen->setTriggerMode(Generator::kTriggerOR); - for (int i = 0; i < 16; i++) { + for (int i = 0; i < particleList.size(); i++) { // we trigger either on B hadron rapidity or on charmonia rapidity gen->addTrigger(triggerPDGRap(rapidityMin, rapidityMax, particleList[i], gen)); } diff --git a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini index b79dcc87a..db679dbc5 100644 --- a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini +++ b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_Pythia8_TriggerGap.ini @@ -1,7 +1,10 @@ ### The external generator derives from GeneratorPythia8. [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C -funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"",1) +funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"443:0.5,20443:0.21,445:0.095,10441:0.014,9920443:0.2:prompt",1) +# enhances psi(2S) compared to J/psi and chi_c1,2 +# reduces chi_c so that their feeddown to J/psi is the same as if decay to J/psi was not forced +# 20% of chi_c1(1P) are replaced with chi_c1(3872) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg diff --git a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini index cff8aa385..e615a3ad0 100644 --- a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini +++ b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini @@ -1,7 +1,11 @@ ### The external generator derives from GeneratorPythia8. [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C -funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"non-prompt:0.2",0) +funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"non-prompt:0.08,443:0.5:prompt,20443:0.21:prompt,445:0.095:prompt,10441:0.014:prompt,9920443:0.2:prompt",0) +# non-prompt J/psi is enhanced through branching ratio, we decrease a bit the amount of enhancement relative to prompt J/psi +# enhances prompt psi(2S) and Upsilon(1S) compared to J/psi and chi_c1,2 +# reduces prompt chi_c so that their feeddown to J/psi is the same as if decay chi_c to J/psi was not forced +# 20% of chi_c1(1P) are replaced with chi_c1(3872) [GeneratorPythia8] config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg diff --git a/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg b/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg index b72c19fb9..3daba4229 100644 --- a/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg +++ b/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg @@ -6,6 +6,7 @@ Beams:eCM 13600. # GeV ### processes SoftQCD:inelastic on # all inelastic processes OniaShower:all = on +OniaShower:ldmeFac = 3. ### decays ParticleDecays:limitTau0 on From eff59ef4377b0c7621c48e46ee4bdf4e079d636b Mon Sep 17 00:00:00 2001 From: abmodak <67369858+abmodak@users.noreply.github.com> Date: Sun, 8 Nov 2026 09:18:09 +0100 Subject: [PATCH 137/144] Add AMPT Generator for OO collisions at 5.36 TeV (#2534) * Add AMPT configuration for OO 5.36 TeV --- .../common/ampt/generator/OO_536TeV_AMPT.ampt | 151 ++++++++++++++++++ .../common/ini/GeneratorAMPTOO536TeV.ini | 6 + .../common/ini/tests/GeneratorAMPTOO536TeV.C | 69 ++++++++ 3 files changed, 226 insertions(+) create mode 100644 MC/config/common/ampt/generator/OO_536TeV_AMPT.ampt create mode 100644 MC/config/common/ini/GeneratorAMPTOO536TeV.ini create mode 100644 MC/config/common/ini/tests/GeneratorAMPTOO536TeV.C diff --git a/MC/config/common/ampt/generator/OO_536TeV_AMPT.ampt b/MC/config/common/ampt/generator/OO_536TeV_AMPT.ampt new file mode 100644 index 000000000..d6d7a38b9 --- /dev/null +++ b/MC/config/common/ampt/generator/OO_536TeV_AMPT.ampt @@ -0,0 +1,151 @@ +5360 ! EFRM (sqrt(S_NN) in GeV if FRAME is CMS) +CMS ! FRAME +A ! PROJ +A ! TARG +16 ! IAP (projectile A number) +8 ! IZP (projectile Z number) +16 ! IAT (target A number) +8 ! IZT (target Z number) +1 ! NEVNT (total number of events) - overwritten by generator_AMPT.C +0. ! BMIN (mininum impact parameter in fm) +20. ! BMAX (maximum impact parameter in fm, also see below) +4 ! ISOFT (D=4): select Default AMPT or String Melting(see below) +1000 ! NTMAX: number of timesteps (D=150), see below +0.2 ! DT: timestep in fm (hadron cascade time= DT*NTMAX) (D=0.2) +0.30 ! PARJ(41): parameter a in Lund symmetric splitting function +0.15 ! PARJ(42): parameter b in Lund symmetric splitting function +1 ! (D=1,yes;0,no) flag for popcorn mechanism(netbaryon stopping) +1.0 ! PARJ(5) to control BMBbar vs BBbar in popcorn (D=1.0) +1 ! shadowing flag (Default=1,yes; 0,no) +0 ! quenching flag (D=0,no; 1,yes) +2.0 ! quenching parameter -dE/dx (GeV/fm) in case quenching flag=1 +2.0 ! p0 cutoff in HIJING for minijet productions (D=2.0) +3.2264d0 ! parton screening mass in fm^(-1) (D=2.265d0), see below +0 ! IZPC: (D=0 forward-angle parton scatterings; 100,isotropic) +0.33d0 ! alpha in parton cascade (D=0.33d0), see parton screening mass +1d6 ! dpcoal in GeV +1d6 ! drcoal in fm +0 ! ihjsed: take HIJING seed from below (D=0)or at runtime(11) - overwritten by generator_AMPT.C +13150909 ! random seed for HIJING - overwritten by generator_AMPT.C +8 ! random seed for parton cascade - overwritten by generator_AMPT.C +0 ! flag for K0s weak decays (D=0,no; 1,yes) +1 ! flag for phi decays at end of hadron cascade (D=1,yes; 0,no) +0 ! flag for pi0 decays at end of hadron cascade (D=0,no; 1,yes) +0 ! optional OSCAR output (D=0,no; 1,yes; 2&3,more parton info) +0 ! flag for perturbative deuteron calculation (D=0,no; 1or2,yes) +1 ! integer factor for perturbative deuterons(>=1 & <=10000) +1 ! choice of cross section assumptions for deuteron reactions +-7. ! Pt in GeV: generate events with >=1 minijet above this value +1000 ! maxmiss (D=1000): maximum # of tries to repeat a HIJING event +3 ! flag on initial and final state radiation (D=3,both yes; 0,no) +1 ! flag on Kt kick (D=1,yes; 0,no) +0 ! flag to turn on quark pair embedding (D=0,no; 1,yes) +7., 0. ! Initial Px and Py values (GeV) of the embedded quark (u or d) +0., 0. ! Initial x & y values (fm) of the embedded back-to-back q/qbar +1, 5., 0. ! nsembd(D=0), psembd (in GeV),tmaxembd (in radian). +0 ! Flag to enable users to modify shadowing (D=0,no; 1,yes) +1.d0 ! Factor used to modify nuclear shadowing +1 ! Flag for random orientation of reaction plane (D=0,no; 1,yes) + +%%%%%%%%%% O2DPG notes: +OO at sqrt(s_NN) = 5.36 TeV, minimum bias (b = 0-20 fm), String Melting +with the LHC settings of arXiv:1403.6321 (a=0.30, b=0.15/GeV^2, 1.5 mb parton +cross section: alpha=0.33 and screening mass 3.2264/fm). +Values are read by AMPT line by line: do NOT add or remove lines above. +NEVNT (line 9), ihjsed (line 28) and the two seeds (lines 29-30) are +overwritten by MC/config/common/external/generator/generator_AMPT.C. +%%%%%%%%%% Further explanations: +BMAX: the upper limit HIPR1(34)+HIPR1(35)=19.87fm (dAu), 25.60fm(AuAu). +ISOFT: 1 Default, + 4 String Melting. +PARJ(41) & (42): for string melting AMPT, 0.55 & 0.15/GeV^2 are recommended + for top RHIC energies and 0.30 & 0.15/GeV^2 are recommended for + LHC energies (see arXiv:1403.6321 for details). +NTMAX: number of time-steps for hadron cascade. + Use a large value (e.g. 1000) for LHC studies or HBT studies at RHIC. + Using NTMAX=2 or 3 effectively turns off hadronic cascade. +parton screening mass (in 1/fm): its square is inversely proportional to + the parton cross section. Use D=2.265d0 for 3mb cross section + when alpha in parton cascade is set to 0.33; + (note: 3.2264d0 for 3mb cross section when alpha is set to 0.47). + Using 1d4 effectively turns off parton cascade. +ihjsed: if =11, take HIJING random seed at runtime so that + every run may be automatically different (see file 'exec'). +iksdcy: flag for K0s weak decays for comparison with data. +iphidcy: flag for phi meson decays at the end of hadron cascade for comparison + with data; default is yes; use 0 to turn off these decays. + Note: phi meson decay during hadron cascade is always enabled. +ipi0dcy: flag for pi0 electromagnetic decays at the end of hadron cascade for + comparison with data; set to 1 to turn on pi0 decays. +ioscar: 0 Dafault, + 1 Write output in the OSCAR format, + 2 Write out the complete parton information + (ana/parton-initial-afterPropagation.dat) + right after string melting (before parton cascade), + 3 Write out several more files on parton information (see readme). +idpert: flag for perturbative deuteron and antideuteron calculations + with results in ana/ampt_pert.dat: + 0 No perturbative calculations, + 1 Trigger a production of NPERTD perturbative deuterons + in each NN collision, + 2 Trigger a production of NPERTD perturbative deuterons only in + an NN collision where a conventional deuteron is produced. + Note: conventional deuteron calculations are always performed + with results in ana/ampt.dat. +NPERTD: number of perturbative deuterons produced in each triggered collision; + setting it to 0 turns off perturbative deuteron productions. +idxsec: choose a cross section model for deuteron inelastic/elastic collisions: + 1: same |matrix element|**2/s (after averaging over initial spins + and isospins) for B+B -> deuteron+meson at the same sqrt(s); + 2: same |matrix element|**2/s for B+B -> deuteron+meson + at the same sqrt(s)-threshold; + 3: same |matrix element|**2/s for deuteron+meson -> B+B + at the same sqrt(s); + 4: same |matrix element|**2/s for deuteron+meson -> B+B + at the same sqrt(s)-threshold; + 1 or 3 also chooses the same cross section for deuteron+meson or baryon + elastic collision at the same sqrt(s); + 2 or 4 also chooses the same cross section for deuteron+meson or baryon + elastic collision at the same sqrt(s)-threshold. +%%%%%%%%%% For jet studies: +pttrig: generate events with at least 1 initial minijet parton above this Pt + value, otherwise repeat HIJING event until reaching maxmiss tries; + use a negative value to disable this requirement and get normal events. +maxmiss: maximum number of tries for the repetition of a HIJING event to obtain + a minijet above the Pt value of pttrig; increase maxmiss if some events + fail to generate at least 1 initial minijet parton above pttrig. + it is safer to set a large value for high pttrig and/or large b value + and/or smaller colliding nuclei. +IHPR2(2): flag to turn off initial and final state radiation: + 0 both radiation off, 1 only final off, 2 only initial off, 3 both on. +IHPR2(5): flag to turn off Pt kick due to soft interactions: 0 off, 1 on. + Setting both IHPR2(2) and IHPR2(5) to zero makes it more likely to + have two high-Pt minijet partons that are close to back-to-back. +%%%%%%%%%% To embed a back-to-back light q/qbar jet pair +%%%%%%%%%% and a given number of soft pions along each jet into each event: +iembed: flag to turn on quark pair embedding: + 1: on with fixed position(xembd,pembd) and Pt(pxqembd,pyqembd); + 2: on with fixed position(xembd,pembd) and random azimuthal angle + with Pt-magnitude given by sqrt(pxqembd^2+pyqembd^2); + 3: on with random position and fixed Pt(pxqembd,pyqembd); + 4: on with random position and random random azimuthal angle + with Pt-magnitude given by sqrt(pxqembd^2+pyqembd^2); + for iembed=3 or 4: need a position file "embed-jet-xy.txt"; + Other integers: off. +pxqembd, pyqembd: sqrt(pxqembd^2+pyqembd^2) > 70MeV/c is required; + the embedded quark and antiquark have pz=0. +xembd, yembd: the embedded quark and antiquark jets have z=0 initially. Note: + the x-axis is defined as the direction along the impact parameter. +nsembd: number of soft pions to be embedded with each high-Pt parton + in the embedded jet pair. +psembd: Momentum of each embedded soft pion in GeV. +tmaxembd: maximum angle(rad) of embedded soft pions relative to high-Pt parton. +%%%%%%%%%% User modification of nuclear shadowing: +ishadow: set to 1 to enable users to adjust nuclear shadowing + provided the shadowing flag IHPR2(6) is turned on; default value is 0. +dshadow: valid when ishadow=1; this parameter modifies the HIJING shadowing + parameterization Ra(x,r)==1+fa(x,r) via Ra(x,r)==1+fa(x,r)*dshadow, + so the value of 0.d0 turns off shadowing + and the value of 1.d0 uses the default HIJING shadowing; + currently limited to 0.d0<=dshadow<=1.d0 to make sure Ra(x,r)>0. +iphirp: set to 1 to turn on random orientation of reaction plane (D=0) diff --git a/MC/config/common/ini/GeneratorAMPTOO536TeV.ini b/MC/config/common/ini/GeneratorAMPTOO536TeV.ini new file mode 100644 index 000000000..948c7a356 --- /dev/null +++ b/MC/config/common/ini/GeneratorAMPTOO536TeV.ini @@ -0,0 +1,6 @@ +#NEV_TEST> 2 +### AMPT OO 5.36 TeV (String Melting), converted directly to MCTracks without HepMC +### Use generateAMPT("/path/to/ampt.dat", "file") to read an existing AMPT output instead +[GeneratorExternal] +fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/common/external/generator/generator_AMPT.C +funcName=generateAMPT("${O2DPG_MC_CONFIG_ROOT}/MC/config/common/ampt/generator/OO_536TeV_AMPT.ampt") diff --git a/MC/config/common/ini/tests/GeneratorAMPTOO536TeV.C b/MC/config/common/ini/tests/GeneratorAMPTOO536TeV.C new file mode 100644 index 000000000..c48f72d63 --- /dev/null +++ b/MC/config/common/ini/tests/GeneratorAMPTOO536TeV.C @@ -0,0 +1,69 @@ +int External() +{ + std::string path{"o2sim_Kine.root"}; + // Check that file exists, can be opened and has the correct tree + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree*)file.Get("o2sim"); + if (!tree) { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader* eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + // Check if there are 2 events, as simulated in the o2dpg-test + auto nEvents = tree->GetEntries(); + if (nEvents != 2) { + std::cerr << "Expected 2 events, got " << nEvents << "\n"; + return 1; + } + // Beam momentum per nucleon for OO at sqrt(s_NN) = 5.36 TeV + const double beamMomentum = std::sqrt(2680. * 2680. - 0.938 * 0.938); + for (Long64_t i = 0; i < nEvents; ++i) { + tree->GetEntry(i); + if (tracks->empty()) { + std::cerr << "Empty entry found at event " << i << "\n"; + return 1; + } + // Check the heavy-ion information of the header + bool isValid = false; + auto nPart = eventHeader->getInfo(o2::dataformats::MCInfoKeys::nPart, isValid); + if (!isValid || nPart <= 0) { + std::cerr << "Event " << i << " has no valid number of participants\n"; + return 1; + } + if (eventHeader->GetB() < 0. || eventHeader->GetB() > 20.) { + std::cerr << "Event " << i << " has impact parameter " << eventHeader->GetB() << " outside the [0, 20] fm range\n"; + return 1; + } + // Spectator nucleons (pT = 0, beam momentum) must be kept for both beams. + // AMPT writes momenta with 4 decimals, hence the tolerance + int nSpectatorsA = 0, nSpectatorsC = 0; + for (const auto& track : *tracks) { + if (track.GetPdgCode() != 2212 && track.GetPdgCode() != 2112) { + continue; + } + if (track.GetPt() == 0. && std::abs(std::abs(track.GetStartVertexMomentumZ()) - beamMomentum) < 1e-2) { + (track.GetStartVertexMomentumZ() > 0 ? nSpectatorsA : nSpectatorsC)++; + } + } + // The total number of nucleons in each nucleus must be conserved + if (nSpectatorsA + nSpectatorsC + nPart > 2 * 208) { + std::cerr << "Event " << i << " has more nucleons than the colliding nuclei\n"; + return 1; + } + if (nSpectatorsA == 0 || nSpectatorsC == 0) { + std::cerr << "Event " << i << " has no spectator nucleons (A side: " << nSpectatorsA << ", C side: " << nSpectatorsC << ")\n"; + return 1; + } + std::cout << "Event " << i << ": " << tracks->size() << " tracks, b = " << eventHeader->GetB() << " fm, Npart = " << nPart + << ", spectators A/C = " << nSpectatorsA << "/" << nSpectatorsC << "\n"; + } + return 0; +} From 5561d030d451a314ebbac97a4aea491a51d4ea3e Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 5 Oct 2026 16:50:16 +0200 Subject: [PATCH 138/144] Count backfill memory in the memory gate of the new runner This fixes a problem in the memory admission of the new workflow runner and adds unit tests. - The default tier ignored memory booked by backfill tasks, so `--mem-limit` could be exceeded. - `fits_default` and `mem_free_default` now include the backfill memory. - The backfill memory factor now defaults to 1.0 and can be set with `--backfill-mem-factor`. - The simulator default is aligned with the runner. Co-Authored-By: Claude Sonnet 5.5 --- MC/workflow_runner/o2dpg_runner/cli.py | 3 +++ MC/workflow_runner/o2dpg_runner/config.py | 1 + MC/workflow_runner/o2dpg_runner/executor.py | 1 + MC/workflow_runner/o2dpg_runner/resources.py | 6 ++--- .../o2dpg_runner/tests/test_resources.py | 27 +++++++++++++++++++ .../o2dpg_schedule_simulator.py | 8 +++--- 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/MC/workflow_runner/o2dpg_runner/cli.py b/MC/workflow_runner/o2dpg_runner/cli.py index 222780782..79e0cfe46 100644 --- a/MC/workflow_runner/o2dpg_runner/cli.py +++ b/MC/workflow_runner/o2dpg_runner/cli.py @@ -60,6 +60,8 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--dynamic-resources", dest="dynamic_resources", action="store_true") p.add_argument("--optimistic-resources", dest="optimistic_resources", action="store_true") p.add_argument("--n-backfill", dest="n_backfill", type=int, default=1) + p.add_argument("--backfill-mem-factor", dest="backfill_mem_factor", type=float, default=1.0, + help="memory over-commit allowed for backfill tasks, as a factor of --mem-limit") p.add_argument("--mem-limit", type=float, default=default_mem, help="in MB") p.add_argument("--cpu-limit", type=float, default=8) @@ -135,6 +137,7 @@ def _args_to_config(ns: argparse.Namespace) -> RunnerConfig: mem_limit=ns.mem_limit, cpu_limit=ns.cpu_limit, n_backfill=ns.n_backfill, + backfill_mem_factor=ns.backfill_mem_factor, update_resources=ns.update_resources, dynamic_resources=ns.dynamic_resources, optimistic_resources=ns.optimistic_resources, diff --git a/MC/workflow_runner/o2dpg_runner/config.py b/MC/workflow_runner/o2dpg_runner/config.py index 5769d0223..3bf9735bb 100644 --- a/MC/workflow_runner/o2dpg_runner/config.py +++ b/MC/workflow_runner/o2dpg_runner/config.py @@ -21,6 +21,7 @@ class RunnerConfig: mem_limit: float = 0.0 # MB; 0 means "auto from psutil" cpu_limit: float = 8.0 n_backfill: int = 1 + backfill_mem_factor: float = 1.0 update_resources: Optional[str] = None dynamic_resources: bool = False optimistic_resources: bool = False diff --git a/MC/workflow_runner/o2dpg_runner/executor.py b/MC/workflow_runner/o2dpg_runner/executor.py index ca2d178ab..fe5cb87c6 100644 --- a/MC/workflow_runner/o2dpg_runner/executor.py +++ b/MC/workflow_runner/o2dpg_runner/executor.py @@ -112,6 +112,7 @@ def __init__( mem_limit=config.mem_limit, procs_parallel_max=config.maxjobs, n_backfill_max=config.n_backfill, + backfill_mem_factor=config.backfill_mem_factor, dynamic_resources=config.dynamic_resources, optimistic_resources=config.optimistic_resources, ) diff --git a/MC/workflow_runner/o2dpg_runner/resources.py b/MC/workflow_runner/o2dpg_runner/resources.py index 4b187d90c..79e604b98 100644 --- a/MC/workflow_runner/o2dpg_runner/resources.py +++ b/MC/workflow_runner/o2dpg_runner/resources.py @@ -182,7 +182,7 @@ def __init__( procs_parallel_max: int = 100, n_backfill_max: int = 1, backfill_cpu_factor: float = 1.5, - backfill_mem_factor: float = 1.5, + backfill_mem_factor: float = 1.0, dynamic_resources: bool = False, optimistic_resources: bool = False, ): @@ -308,12 +308,12 @@ def cpu_free_default(self) -> float: return self.boundaries.cpu_limit - self.cpu_booked def mem_free_default(self) -> float: - return self.boundaries.mem_limit - self.mem_booked + return self.boundaries.mem_limit - self.mem_booked - self.mem_booked_backfill def fits_default(self, res: TaskResources) -> bool: return ( self.cpu_booked + res.cpu_assigned <= self.boundaries.cpu_limit - and self.mem_booked + res.mem_assigned <= self.boundaries.mem_limit + and self.mem_booked + self.mem_booked_backfill + res.mem_assigned <= self.boundaries.mem_limit ) def fits_backfill( diff --git a/MC/workflow_runner/o2dpg_runner/tests/test_resources.py b/MC/workflow_runner/o2dpg_runner/tests/test_resources.py index 2a723fc87..83c58fafc 100644 --- a/MC/workflow_runner/o2dpg_runner/tests/test_resources.py +++ b/MC/workflow_runner/o2dpg_runner/tests/test_resources.py @@ -135,3 +135,30 @@ def test_at_proc_cap(): rm.resources[i].nice_value = rm.nice_default rm.book(i, rm.nice_default) assert rm.at_proc_cap() + + +def test_default_gate_counts_memory_booked_in_backfill(): + """Memory held by a backfill task must count against the default-tier gate.""" + rm = _make_rm(cpu=8, mem=16000) + rm.add_task("a", None, 1, 1, 11000) + rm.add_task("b", None, 1, 1, 11000) + rm.resources[0].nice_value = rm.nice_backfill + rm.book(0, rm.nice_backfill) + assert rm.mem_booked == 0 and rm.mem_booked_backfill == 11000 + assert not rm.fits_default(rm.resources[1]) + assert rm.mem_free_default() == 5000 + + +def test_backfill_mem_factor_default_does_not_overcommit_memory(): + rm = _make_rm(cpu=8, mem=16000) + rm.add_task("a", None, 1, 1, 9000) + rm.add_task("b", None, 1, 1, 9000) + rm.resources[0].nice_value = rm.nice_default + rm.book(0, rm.nice_default) + assert not rm.fits_backfill(rm.resources[1]) + rm15 = _make_rm(cpu=8, mem=16000, backfill_mem_factor=1.5) + rm15.add_task("a", None, 1, 1, 9000) + rm15.add_task("b", None, 1, 1, 9000) + rm15.resources[0].nice_value = rm15.nice_default + rm15.book(0, rm15.nice_default) + assert rm15.fits_backfill(rm15.resources[1]) diff --git a/MC/workflow_runner/o2dpg_schedule_simulator.py b/MC/workflow_runner/o2dpg_schedule_simulator.py index bb9e80819..016dc82b2 100755 --- a/MC/workflow_runner/o2dpg_schedule_simulator.py +++ b/MC/workflow_runner/o2dpg_schedule_simulator.py @@ -261,7 +261,7 @@ def _build_rm( cpu_overrides: Optional[Dict[int, float]] = None, n_backfill_max: int = 0, backfill_cpu_factor: float = 1.5, - backfill_mem_factor: float = 1.5, + backfill_mem_factor: float = 1.0, maxjobs: int = 10_000, ) -> Tuple[ResourceManager, Set[int]]: """Fresh ResourceManager with no backfill tier and unlimited job slots. @@ -374,7 +374,7 @@ def simulate( backfill_model: str = "off", n_backfill: int = 1, backfill_cpu_factor: float = 1.5, - backfill_mem_factor: float = 1.5, + backfill_mem_factor: float = 1.0, backfill_slowdown_factor: float = 1.15, maxjobs: int = 10_000, ) -> SimResult: @@ -802,7 +802,7 @@ def optimize_workers( backfill_model: str = "off", n_backfill: int = 1, backfill_cpu_factor: float = 1.5, - backfill_mem_factor: float = 1.5, + backfill_mem_factor: float = 1.0, backfill_slowdown_factor: float = 1.15, maxjobs: int = 10_000, ) -> Tuple[Dict[str, int], float]: @@ -911,7 +911,7 @@ def build_parser() -> argparse.ArgumentParser: help="Maximum concurrent backfill tasks when backfill simulation is enabled.") p.add_argument("--backfill-cpu-factor", type=float, default=1.5, metavar="X", help="Total CPU oversubscription factor allowed for backfill admission.") - p.add_argument("--backfill-mem-factor", type=float, default=1.5, metavar="X", + p.add_argument("--backfill-mem-factor", type=float, default=1.0, metavar="X", help="Total memory oversubscription factor allowed for backfill admission.") p.add_argument("--backfill-slowdown-factor", type=float, default=1.15, metavar="X", help="Walltime multiplier applied to backfill tasks in " From 47f4874c105a8deb77b3e142bcbe32cdd6865c19 Mon Sep 17 00:00:00 2001 From: jaimenorman Date: Mon, 9 Nov 2026 10:50:39 +0000 Subject: [PATCH 139/144] add increased MB gaps for prompt photon config --- MC/config/PWGGAJE/ini/hook_prompt_gamma_gap3.ini | 7 +++++++ MC/config/PWGGAJE/ini/hook_prompt_gamma_gap5.ini | 7 +++++++ MC/config/PWGGAJE/ini/hook_prompt_gamma_gap7.ini | 7 +++++++ MC/config/PWGGAJE/ini/hook_prompt_gamma_gap9.ini | 7 +++++++ 4 files changed, 28 insertions(+) create mode 100644 MC/config/PWGGAJE/ini/hook_prompt_gamma_gap3.ini create mode 100644 MC/config/PWGGAJE/ini/hook_prompt_gamma_gap5.ini create mode 100644 MC/config/PWGGAJE/ini/hook_prompt_gamma_gap7.ini create mode 100644 MC/config/PWGGAJE/ini/hook_prompt_gamma_gap9.ini diff --git a/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap3.ini b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap3.ini new file mode 100644 index 000000000..c8d7ed83b --- /dev/null +++ b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap3.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_promptphotons_hook.C +funcName = getGeneratorPythia8GapGenJEPhoton(3,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap5.ini b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap5.ini new file mode 100644 index 000000000..503ad27fe --- /dev/null +++ b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap5.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_promptphotons_hook.C +funcName = getGeneratorPythia8GapGenJEPhoton(5,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap7.ini b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap7.ini new file mode 100644 index 000000000..8035d312b --- /dev/null +++ b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap7.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_promptphotons_hook.C +funcName = getGeneratorPythia8GapGenJEPhoton(7,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg +includePartonEvent=true diff --git a/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap9.ini b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap9.ini new file mode 100644 index 000000000..539ff761b --- /dev/null +++ b/MC/config/PWGGAJE/ini/hook_prompt_gamma_gap9.ini @@ -0,0 +1,7 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/external/generator/generator_pythia8_gaptrigger_promptphotons_hook.C +funcName = getGeneratorPythia8GapGenJEPhoton(9,"${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_minbias.cfg","${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg") + +[GeneratorPythia8] +config=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGGAJE/pythia8/generator/pythia8_promptphoton.cfg +includePartonEvent=true From 63939230c57496ed654f7e859201cff247fd2f9d Mon Sep 17 00:00:00 2001 From: jaimenorman Date: Mon, 9 Nov 2026 10:56:01 +0000 Subject: [PATCH 140/144] tests for prompt gamma config --- .../ini/tests/hook_prompt_gamma_gap3.C | 71 +++++++++++++++++++ .../ini/tests/hook_prompt_gamma_gap5.C | 71 +++++++++++++++++++ .../ini/tests/hook_prompt_gamma_gap7.C | 71 +++++++++++++++++++ .../ini/tests/hook_prompt_gamma_gap9.C | 71 +++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap3.C create mode 100644 MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap5.C create mode 100644 MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap7.C create mode 100644 MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap9.C diff --git a/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap3.C b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap3.C new file mode 100644 index 000000000..a4ed3aa31 --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap3.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./3; // one event triggered out of 3 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# prompt_photon events " << nEventsJetJet << "\n"; + std::cout << " sum of weights prompt_photon events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (prompt_photon + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.95 || nEventsJetJet > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of prompt_photon generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.95 || nEventsMB > sumWeightsMB * 1.05) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap5.C b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap5.C new file mode 100644 index 000000000..571ab5c6b --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap5.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./5; // one event triggered out of 5 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# prompt_photon events " << nEventsJetJet << "\n"; + std::cout << " sum of weights prompt_photon events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (prompt_photon + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.95 || nEventsJetJet > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of prompt_photon generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.95 || nEventsMB > sumWeightsMB * 1.05) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap7.C b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap7.C new file mode 100644 index 000000000..22f252875 --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap7.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./7; // one event triggered out of 7 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# prompt_photon events " << nEventsJetJet << "\n"; + std::cout << " sum of weights prompt_photon events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (prompt_photon + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.95 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.05) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.95 || nEventsJetJet > nEvents * ratioTrigger * 1.05) { + std::cerr << "Number of prompt_photon generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.95 || nEventsMB > sumWeightsMB * 1.05) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} diff --git a/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap9.C b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap9.C new file mode 100644 index 000000000..0a75b6d50 --- /dev/null +++ b/MC/config/PWGGAJE/ini/tests/hook_prompt_gamma_gap9.C @@ -0,0 +1,71 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + + float ratioTrigger = 1./9; // one event triggered out of 9 + + + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + + auto tree = (TTree *)file.Get("o2sim"); + std::vector *tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + o2::dataformats::MCEventHeader *eventHeader = nullptr; + tree->SetBranchAddress("MCEventHeader.", &eventHeader); + + int nEventsMB{}, nEventsJetJet{}; + float sumWeightsMB{}, sumWeightsJetJet{}; + int sumTracks{}; + auto nEvents = tree->GetEntries(); + + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + + // check subgenerator information and event weights + if (eventHeader->hasInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID)) { + bool isValid = false; + int subGeneratorId = eventHeader->getInfo(o2::mcgenid::GeneratorProperty::SUBGENERATORID, isValid); + if (eventHeader->hasInfo(o2::dataformats::MCInfoKeys::weight)) { + float weight = eventHeader->getInfo(o2::dataformats::MCInfoKeys::weight,isValid); + if (subGeneratorId == 0) { + nEventsMB++; + sumWeightsMB += weight; + } + else if (subGeneratorId == 1) { + nEventsJetJet++; + sumWeightsJetJet += weight; + } + } + } + sumTracks += tracks->size(); + } + + std::cout << "--------------------------------\n"; + std::cout << "# Events: " << nEvents << "\n"; + std::cout << "# MB events: " << nEventsMB << "\n"; + std::cout << " sum of weights for MB events: " << sumWeightsMB << "\n"; + std::cout << "# prompt_photon events " << nEventsJetJet << "\n"; + std::cout << " sum of weights prompt_photon events: " << sumWeightsJetJet << "\n"; + std::cout << "# tracks summed over all events (prompt_photon + MB): " << sumTracks << "\n"; + + if (nEventsMB < nEvents * (1 - ratioTrigger) * 0.90 || nEventsMB > nEvents * (1 - ratioTrigger) * 1.10) { // we put some tolerance since the number of generated events is small + std::cerr << "Number of generated MB events different than expected\n"; + return 1; + } + if (nEventsJetJet < nEvents * ratioTrigger * 0.90 || nEventsJetJet > nEvents * ratioTrigger * 1.10) { + std::cerr << "Number of prompt_photon generated events different than expected\n"; + return 1; + } + if(nEventsMB < sumWeightsMB * 0.90 || nEventsMB > sumWeightsMB * 1.10) { + std::cerr << "Weights of MB events do not = 1 as expected\n"; + return 1; + } + if(sumTracks < 1) { + std::cerr << "No tracks in simulated events\n"; + return 1; + } + return 0; +} From 9132c20711bf577b95dadae55344c3c4a97f86ed Mon Sep 17 00:00:00 2001 From: Yiping Wang <142303052+ypwangg@users.noreply.github.com> Date: Mon, 9 Nov 2026 23:00:12 +0800 Subject: [PATCH 141/144] Photoproduction injection in PbPb hadronic event --- .../DecayTablesEvtgen/PSITOJPSITOMU.DEC | 9 + .../GeneratorCocktailStarlight_PbPb5TeV.C | 121 ++++++++++ ...rator_pythia8_StarlightTriggered_withGap.C | 212 ++++++++++++++++++ ...InjectedCoherentCharmoniaFwdy_PbPb5TeV.ini | 5 + ...InjectedCoherentCharmoniaMidy_PbPb5TeV.ini | 5 + ...jectedIncoherentCharmoniaFwdy_PbPb5TeV.ini | 5 + ...jectedIncoherentCharmoniaMidy_PbPb5TeV.ini | 5 + ...ctedPhotoproucedCharmoniaMidy_PbPb5TeV.ini | 5 + ...r_InjectedCoherentCharmoniaFwdy_PbPb5TeV.C | 132 +++++++++++ ...r_InjectedCoherentCharmoniaMidy_PbPb5TeV.C | 131 +++++++++++ ...InjectedIncoherentCharmoniaFwdy_PbPb5TeV.C | 131 +++++++++++ ...InjectedIncoherentCharmoniaMidy_PbPb5TeV.C | 131 +++++++++++ ...jectedPhotoproucedCharmoniaMidy_PbPb5TeV.C | 104 +++++++++ .../pythia8/decayer/jpsiPsi2Sdielectron.cfg | 9 + ...runPhotoproducedCharmonia_midy_PbPb5TeV.sh | 34 +++ 15 files changed, 1039 insertions(+) create mode 100644 MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/PSITOJPSITOMU.DEC create mode 100644 MC/config/PWGDQ/external/generator/GeneratorCocktailStarlight_PbPb5TeV.C create mode 100644 MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C create mode 100644 MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.ini create mode 100644 MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.ini create mode 100644 MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.ini create mode 100644 MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.ini create mode 100644 MC/config/PWGDQ/ini/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.ini create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.C create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.C create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.C create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.C create mode 100644 MC/config/PWGDQ/ini/tests/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.C create mode 100644 MC/config/PWGDQ/pythia8/decayer/jpsiPsi2Sdielectron.cfg create mode 100644 MC/run/PWGDQ/runPhotoproducedCharmonia_midy_PbPb5TeV.sh diff --git a/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/PSITOJPSITOMU.DEC b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/PSITOJPSITOMU.DEC new file mode 100644 index 000000000..764f43237 --- /dev/null +++ b/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen/PSITOJPSITOMU.DEC @@ -0,0 +1,9 @@ +Decay psi(2S) +1.000 J/psi pi+ pi- VVPIPI; +Enddecay + +Decay J/psi +1.000 mu+ mu- PHOTOS VLL; +Enddecay + +End diff --git a/MC/config/PWGDQ/external/generator/GeneratorCocktailStarlight_PbPb5TeV.C b/MC/config/PWGDQ/external/generator/GeneratorCocktailStarlight_PbPb5TeV.C new file mode 100644 index 000000000..a67aeb7f2 --- /dev/null +++ b/MC/config/PWGDQ/external/generator/GeneratorCocktailStarlight_PbPb5TeV.C @@ -0,0 +1,121 @@ +R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT/MC/config/PWGUD/external/generator) +R__ADD_INCLUDE_PATH($O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/EvtGen) +#include "GeneratorCocktail.C" +#include "GeneratorStarlight.C" +#include "GeneratorEvtGen.C" +#include + +namespace +{ +o2::eventgen::GeneratorStarlight_class* makeStarlightGenerator(std::string configuration, + float energyCM, + int beam1Z, + int beam1A, + int beam2Z, + int beam2A, + std::string extraParams = "", + std::string dpmjetconf = "") +{ + auto generator = new o2::eventgen::GeneratorStarlight_class(); + generator->selectConfiguration(configuration); + generator->setCollisionSystem(energyCM, beam1Z, beam1A, beam2Z, beam2A); + generator->setExtraParams(extraParams); + // generator->setupDpmjet(dpmjetconf); + return generator; +} +void configureEvtGen(o2::eventgen::GeneratorEvtGen* generator, bool midrapidity) +{ + generator->SetPolarization(1); // Transverse + generator->SetSizePdg(2); + generator->AddPdg(443, 0); + generator->AddPdg(100443, 1); + TString pathO2 = gSystem->ExpandPathName("$O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen"); + if (midrapidity) { + // generator->SetSizePdg(2); + // generator->AddPdg(443, 0); + // generator->AddPdg(100443, 1); + // TString pathO2 = gSystem->ExpandPathName("$O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen"); + generator->SetDecayTable(Form("%s/PSITOJPSIPIPI.DEC", pathO2.Data())); + } else { + // generator->SetSizePdg(1); + // generator->AddPdg(100443, 0); + // generator->SetSizePdg(2); + // generator->AddPdg(443, 0); + // generator->AddPdg(100443, 1); + // TString pathO2 = gSystem->ExpandPathName("$O2DPG_MC_CONFIG_ROOT/MC/config/PWGDQ/EvtGen/DecayTablesEvtgen"); + generator->SetDecayTable(Form("%s/PSITOJPSITOMU.DEC", pathO2.Data())); + // TString pathO2 = gSystem->ExpandPathName("$O2DPG_MC_CONFIG_ROOT/MC/config/PWGUD/external/generator/DecayTablesEvtGen"); + // generator->SetDecayTable(Form("%s/PSI2S.MUMUPIPI.DEC", pathO2.Data())); + } +} +} // namespace + +FairGenerator* GeneratorCocktailStarlightMidy_PbPb5TeV(float energyCM = 5360, int beam1Z = 82, int beam1A = 208, int beam2Z = 82, int beam2A = 208, std::string extrapars = "",std::string dpmjetconf = "") +{ + auto genCocktailEvtGen = new o2::eventgen::GeneratorEvtGen(); + configureEvtGen(genCocktailEvtGen, true); + auto genCohJpsi = makeStarlightGenerator("kCohJpsiToElRad", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genCohJpsi, 1); + auto genIncohJpsi = makeStarlightGenerator("kIncohJpsiToElRad", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genIncohJpsi, 1); + auto genTwoGammaToEl = makeStarlightGenerator("kTwoGammaToElLow", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genTwoGammaToEl, 1); + auto genCohPsi2S = makeStarlightGenerator("kCohPsi2sToElPi", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genCohPsi2S, 1); + auto genIncohPsi2S = makeStarlightGenerator("kIncohPsi2sToElPi", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genIncohPsi2S, 1); + + return genCocktailEvtGen; +} + +FairGenerator* GeneratorCocktailStarlightCoherentMidy_PbPb5TeV(float energyCM = 5360, int beam1Z = 82, int beam1A = 208, int beam2Z = 82, int beam2A = 208, std::string extrapars = "",std::string dpmjetconf = "") +{ + auto genCocktailEvtGen = new o2::eventgen::GeneratorEvtGen(); + configureEvtGen(genCocktailEvtGen, true); + auto genCohJpsi = makeStarlightGenerator("kCohJpsiToElRad", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genCohJpsi, 1); + auto genTwoGammaToEl = makeStarlightGenerator("kTwoGammaToElLow", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genTwoGammaToEl, 1); + auto genCohPsi2S = makeStarlightGenerator("kCohPsi2sToElPi", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genCohPsi2S, 1); + + return genCocktailEvtGen; +} + +FairGenerator* GeneratorCocktailStarlightIncoherentMidy_PbPb5TeV(float energyCM = 5360, int beam1Z = 82, int beam1A = 208, int beam2Z = 82, int beam2A = 208, std::string extrapars = "",std::string dpmjetconf = "") +{ + auto genCocktailEvtGen = new o2::eventgen::GeneratorEvtGen(); + configureEvtGen(genCocktailEvtGen, true); + auto genIncohJpsi = makeStarlightGenerator("kIncohJpsiToElRad", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genIncohJpsi, 1); + auto genIncohPsi2S = makeStarlightGenerator("kIncohPsi2sToElPi", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genIncohPsi2S, 1); + + return genCocktailEvtGen; +} + +FairGenerator* GeneratorCocktailStarlightCoherentFwdy_PbPb5TeV(float energyCM = 5360, int beam1Z = 82, int beam1A = 208, int beam2Z = 82, int beam2A = 208, std::string extrapars = "",std::string dpmjetconf = "") +{ + auto genCocktailEvtGen = new o2::eventgen::GeneratorEvtGen(); + configureEvtGen(genCocktailEvtGen, false); + auto genCohJpsi = makeStarlightGenerator("kCohJpsiToElRad", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genCohJpsi, 1); + auto genTwoGammaToMu = makeStarlightGenerator("kTwoGammaToMuLow", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genTwoGammaToMu, 1); + auto genCohPsi2S = makeStarlightGenerator("kCohPsi2sToMuPi", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genCohPsi2S, 1); + + return genCocktailEvtGen; +} + +FairGenerator* GeneratorCocktailStarlightIncoherentFwdy_PbPb5TeV(float energyCM = 5360, int beam1Z = 82, int beam1A = 208, int beam2Z = 82, int beam2A = 208, std::string extrapars = "",std::string dpmjetconf = "") +{ + auto genCocktailEvtGen = new o2::eventgen::GeneratorEvtGen(); + configureEvtGen(genCocktailEvtGen, false); + auto genIncohJpsi = makeStarlightGenerator("kIncohJpsiToElRad", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genIncohJpsi, 1); + auto genIncohPsi2S = makeStarlightGenerator("kIncohPsi2sToMuPi", energyCM, beam1Z, beam1A, beam2Z, beam2A, extrapars, dpmjetconf); + genCocktailEvtGen->AddGenerator(genIncohPsi2S, 1); + + return genCocktailEvtGen; +} diff --git a/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C b/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C new file mode 100644 index 000000000..d4ab1fedf --- /dev/null +++ b/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C @@ -0,0 +1,212 @@ +#include "FairGenerator.h" +#include "Generators/GeneratorPythia8.h" +#include "Pythia8/HeavyIons.h" +#include "Pythia8/Pythia.h" +#include "TRandom.h" +#include "GeneratorCocktailStarlight_PbPb5TeV.C" +#include +#include +#include +#include + +using namespace o2::eventgen; +using namespace Pythia8; + +namespace o2 +{ +namespace eventgen +{ + + +class GeneratorPythia8StarlightTriggeredWithGap : public o2::eventgen::GeneratorPythia8 { +public: + + /// default constructor + GeneratorPythia8StarlightTriggeredWithGap() = default; + + /// constructor + GeneratorPythia8StarlightTriggeredWithGap(int inputTriggerRatio = 5, int gentype = 0) { + mGeneratedEvents = 0; + mInverseTriggerRatio = inputTriggerRatio; + mNumSignals = 0; + switch (gentype) { + case 0: // generate photoproduced charmonia cocktail at mid rapidity at 5TeV + mGeneratorParam = (Generator*)GeneratorCocktailStarlightMidy_PbPb5TeV(); + break; + case 1: // generate coherent charmonia cocktail at mid rapidity at 5TeV + mGeneratorParam = (Generator*)GeneratorCocktailStarlightCoherentMidy_PbPb5TeV(); + break; + case 2: // generate incoherent charmonia cocktail at mid rapidity at 5TeV + mGeneratorParam = (Generator*)GeneratorCocktailStarlightIncoherentMidy_PbPb5TeV(); + break; + case 3: // generate coherent charmonia cocktail at forward rapidity at 5TeV + mGeneratorParam = (Generator*)GeneratorCocktailStarlightCoherentFwdy_PbPb5TeV(); + break; + case 4: // generate incoherent charmonia cocktail at forward rapidity at 5TeV + mGeneratorParam = (Generator*)GeneratorCocktailStarlightIncoherentFwdy_PbPb5TeV(); + break; + } + mGeneratorParam->Init(); + + addSubGenerator(0, "Minimum bias"); + addSubGenerator(1, "event with injected signals"); + } + + /// Deconstructor + ~GeneratorPythia8StarlightTriggeredWithGap() = default; + + void addSignalPDGs(int pdg) { mSignalsPDGs.push_back(pdg); }; + + void setRapidityRange(double valMin, double valMax) + { + mHadronRapidityMin = valMin; + mHadronRapidityMax = valMax; + }; + +protected: + Bool_t generateEvent() override + { + // GeneratorPythia8::generateEvent(); + mNumSignals = 0; + bool genOk = false; + if (mGeneratedEvents % mInverseTriggerRatio == 0){ // add injected prompt signals to the stack + std::cout<<"generating event with injected signals"<b(); + mNumSignals = getNumSignalsForImpactParameter(impactParameter); + } else { + std::cout<<"no heavy-ion info available; embedding no signal events"< 0 ? 1 : 0); + } else { // gap event + while (!genOk) { + genOk = GeneratorPythia8::generateEvent(); + } + notifySubGenerator(0); + } + mGeneratedEvents++; + std::cout<<"generated events: "<generateEvent()) { + continue; + } + if (!mGeneratorParam->importParticles()) { + mGeneratorParam->clearParticles(); + continue; + } + found = findSignalInAcceptance(); + if (found) { + appendCurrentSignalParticles(isig); + } + mGeneratorParam->clearParticles(); + } + } + } + + return true; + } + + bool findSignalInAcceptance() { + // std::cout<<"loop over" << mGeneratorParam->getParticles().size()<<" particles"<getParticles().size(); ipart++) { + TParticle part = TParticle(mGeneratorParam->getParticles().at(ipart)); + // make sure all signals are in the acceptance + for (int pdg : mSignalsPDGs) { + if (part.GetPdgCode() == pdg) { + // std::cout<<"found signal with pdg: "< mHadronRapidityMax) { + return false; + } + } + } + } + } + std::cout<<"generated signal in acceptance"< 10% centrality + // if (impactParameter < photoproductionImpactParameterCut) { + // std::cout<<"impact parameter: "<(std::lround(5.0 + 0.886202881 * std::pow(std::max(0.0, 17.5 - impactParameter), 1.7)))); + } + + void appendCurrentSignalParticles(int signalIndex) + { + int originalSize = mParticles.size(); + // std::cout<<"adding "<getParticles().size()<<" particles from signal event "<getParticles().size(); ipart++){ + TParticle part = TParticle(mGeneratorParam->getParticles().at(ipart)); + if(part.GetFirstMother() >= 0) part.SetFirstMother(part.GetFirstMother() + originalSize); + if(part.GetSecondMother() >= 0) part.SetLastMother(part.GetSecondMother() + originalSize); + if(part.GetFirstDaughter() >= 0) part.SetFirstDaughter(part.GetFirstDaughter() + originalSize); + if(part.GetLastDaughter() >= 0) part.SetLastDaughter(part.GetLastDaughter() + originalSize); + mParticles.push_back(part); + // encodeParticleStatusAndTracking method already called in GeneratorEvtGen.C + } + } + +private: + Generator* mGeneratorParam = nullptr; + unsigned long long mGeneratedEvents = 0; + int mInverseTriggerRatio = 1; + int mNumSignals = 0; + // Pythia8::Pythia pythiaMBgen; // minimum bias event + std::vector mSignalsPDGs; + double mHadronRapidityMin; + double mHadronRapidityMax; +}; +} +} + +FairGenerator* + GeneratorPhotoproduction(int triggerGap, int gentype = 0, double rapidityMin = -1.5, double rapidityMax = 1.5) +{ + auto gen = new o2::eventgen::GeneratorPythia8StarlightTriggeredWithGap(triggerGap, gentype); + gen->setRapidityRange(rapidityMin, rapidityMax); + gen->addSignalPDGs(443); // J/Psi + gen->addSignalPDGs(100443); // Psi(2S) + gen->addSignalPDGs(11); // e +// auto seed = (gRandom->TRandom::GetSeed() % 900000000); +// gen->readString("Random:setSeed on"); +// gen->readString("Random:seed " + std::to_string(seed)); + return gen; +} + +FairGenerator* + GeneratorPhotoproductionFwd(int triggerGap, int gentype = 0, double rapidityMin = -1.5, double rapidityMax = 1.5) +{ + auto gen = new o2::eventgen::GeneratorPythia8StarlightTriggeredWithGap(triggerGap, gentype); + gen->setRapidityRange(rapidityMin, rapidityMax); + gen->addSignalPDGs(443); // J/Psi + gen->addSignalPDGs(100443); // Psi(2S) + gen->addSignalPDGs(13); // mu +// auto seed = (gRandom->TRandom::GetSeed() % 900000000); +// gen->readString("Random:setSeed on"); +// gen->readString("Random:seed " + std::to_string(seed)); + return gen; +} \ No newline at end of file diff --git a/MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.ini new file mode 100644 index 000000000..0a6fbdf09 --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.ini @@ -0,0 +1,5 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C +funcName = GeneratorPhotoproductionFwd(2, 3, -4.5, -2.0) +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_536tev.cfg diff --git a/MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.ini new file mode 100644 index 000000000..6393747f4 --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.ini @@ -0,0 +1,5 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C +funcName = GeneratorPhotoproduction(2, 1, -1.5, 1.5) +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_536tev.cfg diff --git a/MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.ini new file mode 100644 index 000000000..9c1d3aaa0 --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.ini @@ -0,0 +1,5 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C +funcName = GeneratorPhotoproductionFwd(2, 4, -4.5, -2.0) +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_536tev.cfg diff --git a/MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.ini new file mode 100644 index 000000000..cf889af81 --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.ini @@ -0,0 +1,5 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C +funcName = GeneratorPhotoproduction(2, 2, -1.5, 1.5) +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_536tev.cfg diff --git a/MC/config/PWGDQ/ini/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.ini b/MC/config/PWGDQ/ini/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.ini new file mode 100644 index 000000000..f49c6a994 --- /dev/null +++ b/MC/config/PWGDQ/ini/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.ini @@ -0,0 +1,5 @@ +[GeneratorExternal] +fileName = ${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_StarlightTriggered_withGap.C +funcName = GeneratorPhotoproduction(2, 0, -1.5, 1.5) +[GeneratorPythia8] +config = ${O2DPG_MC_CONFIG_ROOT}/MC/config/ALICE3/pythia8/generator/pythia8_PbPb_536tev.cfg diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.C new file mode 100644 index 000000000..05d937161 --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaFwdy_PbPb5TeV.C @@ -0,0 +1,132 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int checkPdgSignal[2] = {443, 100443}; + int checkPdgDecay = 13; + double rapiditymin = -4.5; double rapiditymax = -2.0; + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nPrimaryLepton{}; + int nPrimaryAntilepton{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nPions{}; + int nPhotonsFromPsi{}; + int nPhotonsFromJPsi{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + int nDaughtersWithWrongMother{}; + + auto nEvents = tree->GetEntries(); + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + int trackId = static_cast(&track - tracks->data()); + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + if (idMoth == -1) nPrimaryLepton++; + } else if (pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + if (idMoth == -1) nPrimaryAntilepton++; + } else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) { + if (idMoth < 0) { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) { + pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++; + } + } + int child0Id = track.getFirstDaughterTrackId(); + int child1Id = track.getLastDaughterTrackId(); + if (child0Id < 0 || child1Id < child0Id || + child1Id >= static_cast(tracks->size())) { + std::cout << "Skip charmonium without valid daughters:" + << " pdg=" << pdg + << " mother=" << idMoth + << " firstDaughter=" << child0Id + << " lastDaughter=" << child1Id + << " y=" << rapidity << '\n'; + continue; + } + + for (int iDaugh = child0Id; iDaugh <= child1Id; iDaugh++) { + const auto& daugh = tracks->at(iDaugh); + if (daugh.getMotherTrackId() != trackId) { + nDaughtersWithWrongMother++; + std::cerr << "Daughter does not point back to charmonium:" + << " daughter=" << iDaugh + << " daughterPdg=" << daugh.GetPdgCode() + << " mother=" << daugh.getMotherTrackId() + << " expectedMother=" << trackId + << " expectedMotherPdg=" << pdg << '\n'; + } + if (daugh.GetPdgCode() == 22) { + if (pdg == checkPdgSignal[0]) { + nPhotonsFromJPsi++; + } else if (pdg == checkPdgSignal[1]) { + nPhotonsFromPsi++; + } + } + if (TMath::Abs(daugh.GetPdgCode()) == 211) nPions++; + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + // std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[0]) std::cout << "First and last children of parent Jpsi are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[1]) std::cout << "First and last children of parent Psi(2S) are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#primary leptons: " << nPrimaryLepton << "\n" + << "#primary antileptons: " << nPrimaryAntilepton << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#photons from Jpsi: " << nPhotonsFromJPsi <<" #photons from Psi(2S): "<< nPhotonsFromPsi<<"\n" + << "#pions from Psi(2S): "<< nPions<<"\n" + << "#daughters with wrong mother: " << nDaughtersWithWrongMother << "\n" + << "#lepton pairs: " << nLeptonPairs <<" #lepton pairs to be done: "<< nLeptonPairsToBeDone<<"\n"; + + if (nDaughtersWithWrongMother != 0) { + std::cerr << "Some charmonium daughters do not point back to their expected mother.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.C new file mode 100644 index 000000000..760f7827a --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedCoherentCharmoniaMidy_PbPb5TeV.C @@ -0,0 +1,131 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int checkPdgSignal[2] = {443, 100443}; + int checkPdgDecay = 11; + double rapiditymin = -1.5; double rapiditymax = 1.5; + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nPrimaryLepton{}; + int nPrimaryAntilepton{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nPions{}; + int nPhotonsFromPsi{}; + int nPhotonsFromJPsi{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + int nDaughtersWithWrongMother{}; + + auto nEvents = tree->GetEntries(); + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + int trackId = static_cast(&track - tracks->data()); + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + if (idMoth == -1) nPrimaryLepton++; + } else if (pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + if (idMoth == -1) nPrimaryAntilepton++; + } else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) { + if (idMoth < 0) { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) { + pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++; + } + } + int child0Id = track.getFirstDaughterTrackId(); + int child1Id = track.getLastDaughterTrackId(); + if (child0Id < 0 || child1Id < child0Id || + child1Id >= static_cast(tracks->size())) { + std::cout << "Skip charmonium without valid daughters:" + << " pdg=" << pdg + << " mother=" << idMoth + << " firstDaughter=" << child0Id + << " lastDaughter=" << child1Id + << " y=" << rapidity << '\n'; + continue; + } + for (int iDaugh = child0Id; iDaugh <= child1Id; iDaugh++) { + const auto& daugh = tracks->at(iDaugh); + if (daugh.getMotherTrackId() != trackId) { + nDaughtersWithWrongMother++; + std::cerr << "Daughter does not point back to charmonium:" + << " daughter=" << iDaugh + << " daughterPdg=" << daugh.GetPdgCode() + << " mother=" << daugh.getMotherTrackId() + << " expectedMother=" << trackId + << " expectedMotherPdg=" << pdg << '\n'; + } + if (daugh.GetPdgCode() == 22) { + if (pdg == checkPdgSignal[0]) { + nPhotonsFromJPsi++; + } else if (pdg == checkPdgSignal[1]) { + nPhotonsFromPsi++; + } + } + if (TMath::Abs(daugh.GetPdgCode()) == 211) nPions++; + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + // std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[0]) std::cout << "First and last children of parent Jpsi are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[1]) std::cout << "First and last children of parent Psi(2S) are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#primary leptons: " << nPrimaryLepton << "\n" + << "#primary antileptons: " << nPrimaryAntilepton << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#photons from Jpsi: " << nPhotonsFromJPsi <<" #photons from Psi(2S): "<< nPhotonsFromPsi<<"\n" + << "#pions from Psi(2S): "<< nPions<<"\n" + << "#daughters with wrong mother: " << nDaughtersWithWrongMother << "\n" + << "#lepton pairs: " << nLeptonPairs <<" #lepton pairs to be done: "<< nLeptonPairsToBeDone<<"\n"; + + if (nDaughtersWithWrongMother != 0) { + std::cerr << "Some charmonium daughters do not point back to their expected mother.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.C new file mode 100644 index 000000000..d958f5d90 --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaFwdy_PbPb5TeV.C @@ -0,0 +1,131 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int checkPdgSignal[2] = {443, 100443}; + int checkPdgDecay = 13; + double rapiditymin = -4.5; double rapiditymax = -2.0; + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nPrimaryLepton{}; + int nPrimaryAntilepton{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nPions{}; + int nPhotonsFromPsi{}; + int nPhotonsFromJPsi{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + int nDaughtersWithWrongMother{}; + + auto nEvents = tree->GetEntries(); + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + int trackId = static_cast(&track - tracks->data()); + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + if (idMoth == -1) nPrimaryLepton++; + } else if (pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + if (idMoth == -1) nPrimaryAntilepton++; + } else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) { + if (idMoth < 0) { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) { + pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++; + } + } + int child0Id = track.getFirstDaughterTrackId(); + int child1Id = track.getLastDaughterTrackId(); + if (child0Id < 0 || child1Id < child0Id || + child1Id >= static_cast(tracks->size())) { + std::cout << "Skip charmonium without valid daughters:" + << " pdg=" << pdg + << " mother=" << idMoth + << " firstDaughter=" << child0Id + << " lastDaughter=" << child1Id + << " y=" << rapidity << '\n'; + continue; + } + for (int iDaugh = child0Id; iDaugh <= child1Id; iDaugh++) { + const auto& daugh = tracks->at(iDaugh); + if (daugh.getMotherTrackId() != trackId) { + nDaughtersWithWrongMother++; + std::cerr << "Daughter does not point back to charmonium:" + << " daughter=" << iDaugh + << " daughterPdg=" << daugh.GetPdgCode() + << " mother=" << daugh.getMotherTrackId() + << " expectedMother=" << trackId + << " expectedMotherPdg=" << pdg << '\n'; + } + if (daugh.GetPdgCode() == 22) { + if (pdg == checkPdgSignal[0]) { + nPhotonsFromJPsi++; + } else if (pdg == checkPdgSignal[1]) { + nPhotonsFromPsi++; + } + } + if (TMath::Abs(daugh.GetPdgCode()) == 211) nPions++; + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + // std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[0]) std::cout << "First and last children of parent Jpsi are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[1]) std::cout << "First and last children of parent Psi(2S) are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#primary leptons: " << nPrimaryLepton << "\n" + << "#primary antileptons: " << nPrimaryAntilepton << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#photons from Jpsi: " << nPhotonsFromJPsi <<" #photons from Psi(2S): "<< nPhotonsFromPsi<<"\n" + << "#pions from Psi(2S): "<< nPions<<"\n" + << "#daughters with wrong mother: " << nDaughtersWithWrongMother << "\n" + << "#lepton pairs: " << nLeptonPairs <<" #lepton pairs to be done: "<< nLeptonPairsToBeDone<<"\n"; + + if (nDaughtersWithWrongMother != 0) { + std::cerr << "Some charmonium daughters do not point back to their expected mother.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.C new file mode 100644 index 000000000..760f7827a --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedIncoherentCharmoniaMidy_PbPb5TeV.C @@ -0,0 +1,131 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int checkPdgSignal[2] = {443, 100443}; + int checkPdgDecay = 11; + double rapiditymin = -1.5; double rapiditymax = 1.5; + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nPrimaryLepton{}; + int nPrimaryAntilepton{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nPions{}; + int nPhotonsFromPsi{}; + int nPhotonsFromJPsi{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + int nDaughtersWithWrongMother{}; + + auto nEvents = tree->GetEntries(); + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + int trackId = static_cast(&track - tracks->data()); + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + if (idMoth == -1) nPrimaryLepton++; + } else if (pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + if (idMoth == -1) nPrimaryAntilepton++; + } else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) { + if (idMoth < 0) { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) { + pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++; + } + } + int child0Id = track.getFirstDaughterTrackId(); + int child1Id = track.getLastDaughterTrackId(); + if (child0Id < 0 || child1Id < child0Id || + child1Id >= static_cast(tracks->size())) { + std::cout << "Skip charmonium without valid daughters:" + << " pdg=" << pdg + << " mother=" << idMoth + << " firstDaughter=" << child0Id + << " lastDaughter=" << child1Id + << " y=" << rapidity << '\n'; + continue; + } + for (int iDaugh = child0Id; iDaugh <= child1Id; iDaugh++) { + const auto& daugh = tracks->at(iDaugh); + if (daugh.getMotherTrackId() != trackId) { + nDaughtersWithWrongMother++; + std::cerr << "Daughter does not point back to charmonium:" + << " daughter=" << iDaugh + << " daughterPdg=" << daugh.GetPdgCode() + << " mother=" << daugh.getMotherTrackId() + << " expectedMother=" << trackId + << " expectedMotherPdg=" << pdg << '\n'; + } + if (daugh.GetPdgCode() == 22) { + if (pdg == checkPdgSignal[0]) { + nPhotonsFromJPsi++; + } else if (pdg == checkPdgSignal[1]) { + nPhotonsFromPsi++; + } + } + if (TMath::Abs(daugh.GetPdgCode()) == 211) nPions++; + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + // std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[0]) std::cout << "First and last children of parent Jpsi are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[1]) std::cout << "First and last children of parent Psi(2S) are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#primary leptons: " << nPrimaryLepton << "\n" + << "#primary antileptons: " << nPrimaryAntilepton << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#photons from Jpsi: " << nPhotonsFromJPsi <<" #photons from Psi(2S): "<< nPhotonsFromPsi<<"\n" + << "#pions from Psi(2S): "<< nPions<<"\n" + << "#daughters with wrong mother: " << nDaughtersWithWrongMother << "\n" + << "#lepton pairs: " << nLeptonPairs <<" #lepton pairs to be done: "<< nLeptonPairsToBeDone<<"\n"; + + if (nDaughtersWithWrongMother != 0) { + std::cerr << "Some charmonium daughters do not point back to their expected mother.\n"; + return 1; + } + + return 0; +} diff --git a/MC/config/PWGDQ/ini/tests/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.C b/MC/config/PWGDQ/ini/tests/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.C new file mode 100644 index 000000000..fe55e024a --- /dev/null +++ b/MC/config/PWGDQ/ini/tests/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.C @@ -0,0 +1,104 @@ +int External() { + std::string path{"o2sim_Kine.root"}; + TFile file(path.c_str(), "READ"); + if (file.IsZombie()) { + std::cerr << "Cannot open ROOT file " << path << "\n"; + return 1; + } + auto tree = (TTree *)file.Get("o2sim"); + if (!tree) + { + std::cerr << "Cannot find tree o2sim in file " << path << "\n"; + return 1; + } + std::vector* tracks{}; + tree->SetBranchAddress("MCTrack", &tracks); + + int checkPdgSignal[2] = {443, 100443}; + int checkPdgDecay = 11; + double rapiditymin = -1.5; double rapiditymax = 1.5; + + int nLeptons{}; + int nAntileptons{}; + int nLeptonPairs{}; + int nPrimaryLepton{}; + int nPrimaryAntilepton{}; + int nLeptonPairsToBeDone{}; + int nSignalJpsi{}; + int nSignalPsi2S{}; + int nPions{}; + int nPhotonsFromPsi{}; + int nPhotonsFromJPsi{}; + int nSignalJpsiWithinAcc{}; + int nSignalPsi2SWithinAcc{}; + + auto nEvents = tree->GetEntries(); + for (int i = 0; i < nEvents; i++) { + tree->GetEntry(i); + for (auto& track : *tracks) { + auto pdg = track.GetPdgCode(); + auto rapidity = track.GetRapidity(); + auto idMoth = track.getMotherTrackId(); + if (pdg == checkPdgDecay) { + // count leptons + nLeptons++; + if (idMoth == -1) nPrimaryLepton++; + } else if (pdg == -checkPdgDecay) { + // count anti-leptons + nAntileptons++; + if (idMoth == -1) nPrimaryAntilepton++; + } else if (pdg == checkPdgSignal[0] || pdg == checkPdgSignal[1]) { + if (idMoth < 0) { + // count signal PDG + pdg == checkPdgSignal[0] ? nSignalJpsi++ : nSignalPsi2S++; + // count signal PDG within acceptance + if (rapidity > rapiditymin && rapidity < rapiditymax) { + pdg == checkPdgSignal[0] ? nSignalJpsiWithinAcc++ : nSignalPsi2SWithinAcc++; + } + } + int child0Id = track.getFirstDaughterTrackId(); + int child1Id = track.getLastDaughterTrackId(); + for (int iDaugh = child0Id; iDaugh <= child1Id; iDaugh++) { + auto daugh = tracks->at(iDaugh); + if (daugh.GetPdgCode() == 22) { + if (pdg == checkPdgSignal[0]) { + nPhotonsFromJPsi++; + } else if (pdg == checkPdgSignal[1]) { + nPhotonsFromPsi++; + } + } + if (TMath::Abs(daugh.GetPdgCode()) == 211) nPions++; + } + auto child0 = o2::mcutils::MCTrackNavigator::getDaughter0(track, *tracks); + auto child1 = o2::mcutils::MCTrackNavigator::getDaughter1(track, *tracks); + if (child0 != nullptr && child1 != nullptr) { + // check for parent-child relations + auto pdg0 = child0->GetPdgCode(); + auto pdg1 = child1->GetPdgCode(); + // std::cout << "First and last children of parent " << checkPdgSignal << " are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[0]) std::cout << "First and last children of parent Jpsi are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (pdg == checkPdgSignal[1]) std::cout << "First and last children of parent Psi(2S) are PDG0: " << pdg0 << " PDG1: " << pdg1 << "\n"; + if (std::abs(pdg0) == checkPdgDecay && std::abs(pdg1) == checkPdgDecay && pdg0 == -pdg1) { + nLeptonPairs++; + if (child0->getToBeDone() && child1->getToBeDone()) { + nLeptonPairsToBeDone++; + } + } + } + } + } + } + + std::cout << "#events: " << nEvents << "\n" + << "#leptons: " << nLeptons << "\n" + << "#antileptons: " << nAntileptons << "\n" + << "#primary leptons: " << nPrimaryLepton << "\n" + << "#primary antileptons: " << nPrimaryAntilepton << "\n" + << "#signal (prompt Jpsi): " << nSignalJpsi << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalJpsiWithinAcc << "\n" + << "#signal (prompt Psi(2S)): " << nSignalPsi2S << "; within acceptance " << rapiditymin << " < y < " << rapiditymax << " : " << nSignalPsi2SWithinAcc << "\n" + << "#photons from Jpsi: " << nPhotonsFromJPsi <<" #photons from Psi(2S): "<< nPhotonsFromPsi<<"\n" + << "#pions from Psi(2S): "<< nPions<<"\n" + << "#lepton pairs: " << nLeptonPairs <<" #lepton pairs to be done: "<< nLeptonPairsToBeDone<<"\n"; + + return 0; +} \ No newline at end of file diff --git a/MC/config/PWGDQ/pythia8/decayer/jpsiPsi2Sdielectron.cfg b/MC/config/PWGDQ/pythia8/decayer/jpsiPsi2Sdielectron.cfg new file mode 100644 index 000000000..145dc8cec --- /dev/null +++ b/MC/config/PWGDQ/pythia8/decayer/jpsiPsi2Sdielectron.cfg @@ -0,0 +1,9 @@ +### Force J/psi and psi(2S) -> e+ e- + +### J/psi -> e+ e- +443:onMode = off # switch off all decay channels +443:onIfAll = 11 -11 # switch on e+ e- decay channel + +### psi(2S) -> e+ e- +100443:onMode = off # switch off all decay channels +100443:onIfAll = 11 -11 # switch on e+ e- decay channel diff --git a/MC/run/PWGDQ/runPhotoproducedCharmonia_midy_PbPb5TeV.sh b/MC/run/PWGDQ/runPhotoproducedCharmonia_midy_PbPb5TeV.sh new file mode 100644 index 000000000..4347822d7 --- /dev/null +++ b/MC/run/PWGDQ/runPhotoproducedCharmonia_midy_PbPb5TeV.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +RNDSEED=${RNDSEED:-0} +NWORKERS=${NWORKERS:-8} + + +export ALIEN_JDL_LPMPRODUCTIONTYPE=MC +export ALIEN_JDL_CPULIMIT=20 +export ALIEN_JDL_LPMANCHORPASSNAME=apass4 +export ALIEN_JDL_MCANCHOR=apass4 +export ALIEN_JDL_COLLISIONSYSTEM=PbPb +export ALIEN_JDL_LPMPASSNAME=apass4 +export ALIEN_JDL_LPMRUNNUMBER=544474 +export ALIEN_JDL_LPMANCHORRUN=544474 + +export ALIEN_JDL_LPMINTERACTIONTYPE=PbPb +export ALIEN_JDL_LPMPRODUCTIONTAG="TestProd" +export ALIEN_JDL_LPMANCHORPRODUCTION="TestProd" +export ALIEN_JDL_LPMANCHORYEAR=2023 + +export ALIEN_JDL_O2DPGWORKFLOWTARGET="aod" + +export PRODSPLIT=${ALIEN_O2DPG_GRIDSUBMIT_PRODSPLIT:-10} +export SPLITID=${ALIEN_O2DPG_GRIDSUBMIT_SUBJOBID:-10} +export CYCLE=0 +export NTIMEFRAMES=8 +export NSIGEVENTS=20 +export NBKGEVENTS=10 + +CONFIGNAME=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/ini/Generator_InjectedPhotoproucedCharmoniaMidy_PbPb5TeV.ini + +export ALIEN_JDL_ANCHOR_SIM_OPTIONS="-eCM 5360 -gen external -j ${NWORKERS} -tf ${NTIMEFRAMES} -e TGeant4 -seed 0 -ini ${CONFIGNAME} -genBkg pythia8 -procBkg \"heavy_ion\" -colBkg PbPb --embedding -nb $NBKGEVENTS --embeddPattern @0:e2" + +${O2DPG_ROOT}/MC/run/ANCHOR/anchorMC.sh \ No newline at end of file From 42c2682004af721717cef41c5a8a697b6834a4d7 Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Mon, 9 Nov 2026 15:15:15 +0100 Subject: [PATCH 142/144] fix dependencies of PV finder and AOD producer in o2dpg_sim_workflow.py --- MC/bin/o2dpg_sim_workflow.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 470e538f7..9eadf6d9c 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -1378,13 +1378,13 @@ def getDigiTaskName(det): if not isActive('CTP'): # CTP digits won't be produced for this timeframe (CTP not in the readout detector list) tpc_corr_scaling_options += ' --disable-ctp-lumi-request' - + # why not simply? # tpc_corr_scaling_options = ('--lumi-type 1', '')[tpcDistortionType != 0] # at the moment MC is not ready for the sector edge fluctuations (RS: and does not use IDC but --disable-IDC-scalers can be added only once it is added by the CorrectionMapsOptions::addGlobalOptions) tpc_corr_scaling_options += option_if_available('o2-tpc-reco-workflow', '--disable-sec-edge-fluc-correction', envfile=async_envfile) - + #<--------- TPC reco task if includeTPCSyncMode: tpcSyncreconeeds = tpcreconeeds.copy() @@ -1745,6 +1745,10 @@ def getDigiTaskName(det): pvfinder_matching_sources = cleanDetectorInputList(pvfinder_matching_sources) pvfinderneeds = [TRDTRACKINGtask2['name'] if isActive("TRD") else None, + ITSRECOtask['name'] if isActive("ITS") else None, + TPCRECOtask['name'] if isActive("TPC") else None, + MFTRECOtask['name'] if isActive("MFT") else None, + MCHRECOtask['name'] if isActive("MCH") else None, FT0RECOtask['name'] if isActive("FT0") else None, FV0RECOtask['name'] if isActive("FV0") else None, EMCRECOtask['name'] if isActive("EMC") else None, @@ -1757,7 +1761,7 @@ def getDigiTaskName(det): TOFTPCMATCHERtask['name'] if isActive("TPC") and isActive("TOF") else None, MFTMCHMATCHtask['name'] if isActive("MFT") and isActive("MCH") else None, MCHMIDMATCHtask['name'] if isActive("MCH") and isActive("MID") else None] - pvfinderneeds = [ p for p in pvfinderneeds if p != None ] + pvfinderneeds = [p for p in pvfinderneeds if p != None] PVFINDERtask = createTask(name='pvfinder_'+str(tf), needs=pvfinderneeds, tf=tf, cwd=timeframeworkdir, lab=["RECO"], cpu=NWORKERS_TF, mem='4000') PVFINDERtask['cmd'] = task_finalizer( @@ -1810,7 +1814,8 @@ def getDigiTaskName(det): aod_info_souces_default = 'ITS-TPC,TPC-TRD,ITS-TPC-TRD,TPC-TOF,ITS-TPC-TOF,TPC-TRD-TOF,ITS-TPC-TRD-TOF,MFT-MCH,MCH-MID,ITS,MFT,TPC,TOF,FT0,MID,EMC,PHS,CPV,ZDC,FDD,HMP,FV0,TRD,MCH,CTP' aodinfosources = dpl_option_from_config(anchorConfig, 'o2-aod-producer-workflow', '--info-sources', default_value=aod_info_souces_default) aodinfosources = cleanDetectorInputList(aodinfosources) - aodneeds = [PVFINDERtask['name'], SVFINDERtask['name']] + aodneeds = [PVFINDERtask['name'], SVFINDERtask['name'], + MIDRECOtask['name'] if isActive("MID") else None] if usebkgcache: aodneeds += [ BKG_KINEDOWNLOADER_TASK['name'] ] From e5708b88f5a5f713802bd2a9fb3f9f843c211fa1 Mon Sep 17 00:00:00 2001 From: Ernst Hellbar Date: Mon, 9 Nov 2026 15:30:22 +0100 Subject: [PATCH 143/144] move unused dependencies in PV finder to AOD producer in o2dpg_sim_workflow.py The first commit fixes the issue reported in https://its.cern.ch/jira/browse/O2-7313 1) add missing ITS, TPC, MCH and MFT reco dependencies to pvfinder task, MID reco dependency to aod task The second one moves dependencies which are unnecessary for pvfinder to the aod task 2) remove HMP, ZDC dependencies from pvfinder since they are filtered out in the workflow itself (https://github.com/AliceO2Group/AliceO2/blob/b02000319f85bddcfa11525a7d4e8e5b86fcca45/Detectors/GlobalTrackingWorkflow/src/primary-vertexing-workflow.cxx#L89), add them to the aod task where they are needed after the removal from the pvfinder --- MC/bin/o2dpg_sim_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MC/bin/o2dpg_sim_workflow.py b/MC/bin/o2dpg_sim_workflow.py index 9eadf6d9c..89d98cede 100755 --- a/MC/bin/o2dpg_sim_workflow.py +++ b/MC/bin/o2dpg_sim_workflow.py @@ -1755,8 +1755,6 @@ def getDigiTaskName(det): PHSRECOtask['name'] if isActive("PHS") else None, CPVRECOtask['name'] if isActive("CPV") else None, FDDRECOtask['name'] if isActive("FDD") else None, - ZDCRECOtask['name'] if isActive("ZDC") else None, - HMPMATCHtask['name'] if isActive("HMP") else None, ITSTPCMATCHtask['name'] if isActive("ITS") and isActive("TPC") else None, TOFTPCMATCHERtask['name'] if isActive("TPC") and isActive("TOF") else None, MFTMCHMATCHtask['name'] if isActive("MFT") and isActive("MCH") else None, @@ -1815,6 +1813,8 @@ def getDigiTaskName(det): aodinfosources = dpl_option_from_config(anchorConfig, 'o2-aod-producer-workflow', '--info-sources', default_value=aod_info_souces_default) aodinfosources = cleanDetectorInputList(aodinfosources) aodneeds = [PVFINDERtask['name'], SVFINDERtask['name'], + HMPMATCHtask['name'] if isActive("HMP") else None, + ZDCRECOtask['name'] if isActive("ZDC") else None, MIDRECOtask['name'] if isActive("MID") else None] if usebkgcache: From 8263fe70e861c92f416a3233d9ae797a944d18bd Mon Sep 17 00:00:00 2001 From: glegras <71757105+glegras@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:08:15 +0100 Subject: [PATCH 144/144] update parameters (#2542) --- ...TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini | 2 +- .../PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini index e615a3ad0..39e84939d 100644 --- a/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini +++ b/MC/config/PWGDQ/ini/Generator_TriggeredInclusiveQuarkonia_enhancedB_Pythia8_TriggerGap.ini @@ -1,7 +1,7 @@ ### The external generator derives from GeneratorPythia8. [GeneratorExternal] fileName=${O2DPG_MC_CONFIG_ROOT}/MC/config/PWGDQ/external/generator/generator_pythia8_HadronTriggered_withGap.C -funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"non-prompt:0.08,443:0.5:prompt,20443:0.21:prompt,445:0.095:prompt,10441:0.014:prompt,9920443:0.2:prompt",0) +funcName=GeneratorInclusiveAllQuarkonia_EvtGenMidY(5,-1.0,1.0,"non-prompt:0.15,443:0.5:prompt,20443:0.21:prompt,445:0.095:prompt,10441:0.014:prompt,9920443:0.2:prompt",0) # non-prompt J/psi is enhanced through branching ratio, we decrease a bit the amount of enhancement relative to prompt J/psi # enhances prompt psi(2S) and Upsilon(1S) compared to J/psi and chi_c1,2 # reduces prompt chi_c so that their feeddown to J/psi is the same as if decay chi_c to J/psi was not forced diff --git a/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg b/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg index 3daba4229..6358e615d 100644 --- a/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg +++ b/MC/config/PWGDQ/pythia8/generator/pythia8_oniaAll_triggerGap.cfg @@ -6,7 +6,7 @@ Beams:eCM 13600. # GeV ### processes SoftQCD:inelastic on # all inelastic processes OniaShower:all = on -OniaShower:ldmeFac = 3. +OniaShower:ldmeFac = 5. ### decays ParticleDecays:limitTau0 on