diff --git a/PWGLF/Tasks/Nuspex/spectraKinkPiKa.cxx b/PWGLF/Tasks/Nuspex/spectraKinkPiKa.cxx index 9e61c53de3e..1b3c900fc35 100644 --- a/PWGLF/Tasks/Nuspex/spectraKinkPiKa.cxx +++ b/PWGLF/Tasks/Nuspex/spectraKinkPiKa.cxx @@ -14,13 +14,29 @@ /// \author sandeep dudi sandeep.dudi@cern.ch #include "PWGLF/DataModel/LFKinkDecayTables.h" +#include "PWGLF/DataModel/LFParticleIdentification.h" +#include "PWGLF/Utils/svPoolCreator.h" +#include "Common/DataModel/Centrality.h" #include "Common/DataModel/EventSelection.h" +#include "Common/DataModel/Multiplicity.h" + +#include "CCDB/BasicCCDBManager.h" +#include "DCAFitter/DCAFitterN.h" +#include "DataFormatsParameters/GRPMagField.h" +#include "DataFormatsParameters/GRPObject.h" +#include "DetectorsBase/GeometryManager.h" +#include "DetectorsBase/Propagator.h" +#include "Framework/ASoAHelpers.h" +#include "Framework/AnalysisDataModel.h" +#include "Framework/AnalysisTask.h" +#include "Framework/runDataProcessing.h" +#include "ReconstructionDataFormats/Track.h" + +////////////// #include "Common/DataModel/PIDResponse.h" -#include "Framework/AnalysisTask.h" #include "Framework/O2DatabasePDGPlugin.h" -#include "Framework/runDataProcessing.h" #include "ReconstructionDataFormats/PID.h" #include "Math/GenVector/Boost.h" @@ -31,7 +47,11 @@ #include #include +#include +#include #include +#include +#include #include using namespace std; @@ -40,10 +60,297 @@ using namespace o2::aod; using namespace o2::framework; using namespace o2::framework::expressions; using namespace o2::constants::physics; +using VBracket = o2::math_utils::Bracket; -using TracksFull = soa::Join; +using TracksFull = soa::Join; using CollisionsFull = soa::Join; using CollisionsFullMC = soa::Join; +namespace +{ +constexpr std::array LayerRadii{2.33959f, 3.14076f, 3.91924f, 19.6213f, 24.5597f, 34.388f, 39.3329f}; +constexpr double betheBlochDefault[1][6]{{-1.e32, -1.e32, -1.e32, -1.e32, -1.e32, -1.e32}}; +static const std::vector betheBlochParNames{"p0", "p1", "p2", "p3", "p4", "resolution"}; +static const std::vector particleNames{"Daughter"}; + +} // namespace + +struct kinkCandidate { + int mothTrackID; + int daugTrackID; + int collisionID; + + int mothSign; + std::array momMoth = {-999, -999, -999}; + std::array momDaug = {-999, -999, -999}; + std::array primVtx = {-999, -999, -999}; + std::array decVtx = {-999, -999, -999}; + + float dcaKinkTopo = -999; + float dcaXYdaug = -999; + float dcaXYmoth = -999; + float kinkAngle = -999; +}; +struct kinkBuilder { + // kink analysis + Produces outputDataTable; + Service ccdb; + // Selection criteria + Configurable maxDCAMothToPV{"maxDCAMothToPV", 0.1, "Max DCA of the mother to the PV"}; + Configurable minDCADaugToPV{"minDCADaugToPV", 0., "Min DCA of the daughter to the PV"}; + Configurable minPtMoth{"minPtMoth", 0.5, "Minimum pT of the hypercandidate"}; + Configurable maxZDiff{"maxZDiff", 20., "Max z difference between the kink daughter and the mother"}; + Configurable maxPhiDiff{"maxPhiDiff", 100, "Max phi difference between the kink daughter and the mother"}; + Configurable timeMarginNS{"timeMarginNS", 600, "Additional time res tolerance in ns"}; + Configurable etaMax{"etaMax", 1., "eta daughter"}; + Configurable nTPCClusMinDaug{"nTPCClusMinDaug", 30, "mother NTPC clusters cut"}; + Configurable itsChi2cut{"itsChi2cut", 30, "mother itsChi2 cut"}; + Configurable askTOFforDaug{"askTOFforDaug", false, "If true, ask for TOF signal"}; + Configurable kaontopologhy{"kaontopologhy", true, "If true, selected mother have both ITS+TPC "}; + + o2::vertexing::DCAFitterN<2> fitter; + o2::base::MatLayerCylSet* lut = nullptr; + + // constants + float radToDeg = o2::constants::math::Rad2Deg; + svPoolCreator svCreator; + + // bethe bloch parameters + Configurable> cfgBetheBlochParams{"cfgBetheBlochParams", {betheBlochDefault[0], 1, 6, particleNames, betheBlochParNames}, "TPC Bethe-Bloch parameterisation for charged daughter"}; + Configurable cfgMaterialCorrection{"cfgMaterialCorrection", static_cast(o2::base::Propagator::MatCorrType::USEMatCorrNONE), "Type of material correction"}; + Configurable customVertexerTimeMargin{"customVertexerTimeMargin", 800, "Time margin for custom vertexer (ns)"}; + Configurable skipAmbiTracks{"skipAmbiTracks", false, "Skip ambiguous tracks"}; + Configurable unlikeSignBkg{"unlikeSignBkg", false, "Use unlike sign background"}; + + // CCDB options + Configurable ccdbPath{"ccdbPath", "http://alice-ccdb.cern.ch", "url of the ccdb repository"}; + Configurable grpmagPath{"grpmagPath", "GLO/Config/GRPMagField", "CCDB path of the GRPMagField object"}; + Configurable lutPath{"lutPath", "GLO/Param/MatLUT", "Path of the Lut parametrization"}; + + // std vector of candidates + std::vector kinkCandidates; + int mRunNumber; + float mBz; + std::array mBBparamsDaug; + + // mother and daughter tracks' properties (absolute charge and mass) + int charge = 1; + void init(InitContext const&) + { + // dummy values, 1 for mother, 0 for daughter + svCreator.setPDGs(1, 0); + + mRunNumber = 0; + mBz = 0; + + ccdb->setURL(ccdbPath); + ccdb->setCaching(true); + ccdb->setLocalObjectValidityChecking(); + fitter.setPropagateToPCA(true); + fitter.setMaxR(200.); + fitter.setMinParamChange(1e-3); + fitter.setMinRelChi2Change(0.9); + fitter.setMaxDZIni(1e9); + fitter.setMaxChi2(1e9); + fitter.setUseAbsDCA(true); + + svCreator.setTimeMargin(customVertexerTimeMargin); + if (skipAmbiTracks) { + svCreator.setSkipAmbiTracks(); + } + for (int i = 0; i < 5; i++) { + mBBparamsDaug[i] = cfgBetheBlochParams->get("Daughter", Form("p%i", i)); + } + mBBparamsDaug[5] = cfgBetheBlochParams->get("Daughter", "resolution"); + } + + template + bool selectMothTrack(const T& candidate) + { + // ITS-standalone (no TPC, no TOF) + if (!kaontopologhy) { + if (candidate.has_collision() && candidate.hasITS() && !candidate.hasTPC() && !candidate.hasTOF() && + candidate.itsNCls() < 6 && + candidate.itsNClsInnerBarrel() == 3 && + candidate.itsChi2NCl() < 36 && + candidate.pt() > minPtMoth) { + return true; + } + return false; + } + // Kaon topology: ITS+TPC, no TOF + if (kaontopologhy) { + if (candidate.has_collision() && candidate.hasITS() && candidate.hasTPC() && !candidate.hasTOF() && + candidate.pt() > minPtMoth && + candidate.tpcNClsCrossedRows() >= nTPCClusMinDaug && + candidate.itsChi2NCl() <= itsChi2cut) { + return true; + } + return false; + } + + return false; // fallback + } + + template + bool selectDaugTrack(const T& candidate) + { + if (!kaontopologhy && (!candidate.hasTPC() || !candidate.hasITS())) { + return false; + } + + if (kaontopologhy && (!candidate.hasTPC() || candidate.hasITS())) { + return false; + } + + if (askTOFforDaug && !candidate.hasTOF()) { + return false; + } + return true; + } + + template + void fillCandidateData(const Tcolls& collisions, const Ttracks& tracks, aod::AmbiguousTracks const& ambiguousTracks, aod::BCs const& bcs) + { + svCreator.clearPools(); + svCreator.fillBC2Coll(collisions, bcs); + + for (const auto& track : tracks) { + if (std::abs(track.eta()) > etaMax) + continue; + + bool isDaug = selectDaugTrack(track); + bool isMoth = selectMothTrack(track); + + if (!isDaug && !isMoth) + continue; + + int pdgHypo = isMoth ? 1 : 0; + svCreator.appendTrackCand(track, collisions, pdgHypo, ambiguousTracks, bcs); + } + auto& kinkPool = svCreator.getSVCandPool(collisions, !unlikeSignBkg); + + for (const auto& svCand : kinkPool) { + kinkCandidate kinkCand; + + auto trackMoth = tracks.rawIteratorAt(svCand.tr0Idx); + auto trackDaug = tracks.rawIteratorAt(svCand.tr1Idx); + + auto const& collision = trackMoth.template collision_as(); + auto const& bc = collision.template bc_as(); + initCCDB(bc); + + o2::dataformats::VertexBase primaryVertex; + primaryVertex.setPos({collision.posX(), collision.posY(), collision.posZ()}); + primaryVertex.setCov(collision.covXX(), collision.covXY(), collision.covYY(), collision.covXZ(), collision.covYZ(), collision.covZZ()); + kinkCand.primVtx = {primaryVertex.getX(), primaryVertex.getY(), primaryVertex.getZ()}; + + o2::track::TrackParCov trackParCovMoth = getTrackParCov(trackMoth); + o2::track::TrackParCov trackParCovMothPV{trackParCovMoth}; + o2::base::Propagator::Instance()->PropagateToXBxByBz(trackParCovMoth, LayerRadii[trackMoth.itsNCls() - 1]); + std::array dcaInfoMoth; + o2::base::Propagator::Instance()->propagateToDCABxByBz({primaryVertex.getX(), primaryVertex.getY(), primaryVertex.getZ()}, trackParCovMothPV, 2.f, static_cast(cfgMaterialCorrection.value), &dcaInfoMoth); + + o2::track::TrackParCov trackParCovDaug = getTrackParCov(trackDaug); + + // check if the kink daughter is close to the mother + if (std::abs(trackParCovMoth.getZ() - trackParCovDaug.getZ()) > maxZDiff) { + continue; + } + if ((std::abs(trackParCovMoth.getPhi() - trackParCovDaug.getPhi()) * radToDeg) > maxPhiDiff) { + continue; + } + + // propagate to PV + std::array dcaInfoDaug; + o2::base::Propagator::Instance()->propagateToDCABxByBz({primaryVertex.getX(), primaryVertex.getY(), primaryVertex.getZ()}, trackParCovDaug, 2.f, static_cast(cfgMaterialCorrection.value), &dcaInfoDaug); + if (std::abs(dcaInfoDaug[0]) < minDCADaugToPV) { + continue; + } + + int nCand = 0; + try { + nCand = fitter.process(trackParCovMoth, trackParCovDaug); + } catch (...) { + LOG(error) << "Exception caught in DCA fitter process call!"; + continue; + } + if (nCand == 0) { + continue; + } + + if (!fitter.propagateTracksToVertex()) { + continue; + } + + auto propMothTrack = fitter.getTrack(0); + auto propDaugTrack = fitter.getTrack(1); + kinkCand.decVtx = fitter.getPCACandidatePos(); + + for (int i = 0; i < 3; i++) { + kinkCand.decVtx[i] -= kinkCand.primVtx[i]; + } + propMothTrack.getPxPyPzGlo(kinkCand.momMoth); + propDaugTrack.getPxPyPzGlo(kinkCand.momDaug); + for (int i = 0; i < 3; i++) { + kinkCand.momMoth[i] *= charge; + kinkCand.momDaug[i] *= charge; + } + float pMoth = propMothTrack.getP() * charge; + float pDaug = propDaugTrack.getP() * charge; + float spKink = kinkCand.momMoth[0] * kinkCand.momDaug[0] + kinkCand.momMoth[1] * kinkCand.momDaug[1] + kinkCand.momMoth[2] * kinkCand.momDaug[2]; + kinkCand.kinkAngle = std::acos(spKink / (pMoth * pDaug)); + + kinkCand.collisionID = collision.globalIndex(); + kinkCand.mothTrackID = trackMoth.globalIndex(); + kinkCand.daugTrackID = trackDaug.globalIndex(); + + kinkCand.dcaXYmoth = dcaInfoMoth[0]; + kinkCand.mothSign = trackMoth.sign(); + kinkCand.dcaXYdaug = dcaInfoDaug[0]; + kinkCand.dcaKinkTopo = std::sqrt(fitter.getChi2AtPCACandidate()); + kinkCandidates.push_back(kinkCand); + } + } + + void initCCDB(aod::BCs::iterator const& bc) + { + if (mRunNumber == bc.runNumber()) { + return; + } + mRunNumber = bc.runNumber(); + LOG(info) << "Initializing CCDB for run " << mRunNumber; + o2::parameters::GRPMagField* grpmag = ccdb->getForRun(grpmagPath, mRunNumber); + o2::base::Propagator::initFieldFromGRP(grpmag); + mBz = grpmag->getNominalL3Field(); + fitter.setBz(mBz); + + if (!lut) { + lut = o2::base::MatLayerCylSet::rectifyPtrFromFile(ccdb->get(lutPath)); + int mat{static_cast(cfgMaterialCorrection)}; + fitter.setMatCorrType(static_cast(mat)); + } + o2::base::Propagator::Instance()->setMatLUT(lut); + LOG(info) << "Task initialized for run " << mRunNumber << " with magnetic field " << mBz << " kZG"; + } + + void process(aod::Collisions const& collisions, TracksFull const& tracks, aod::AmbiguousTracks const& ambiTracks, aod::BCs const& bcs) + { + kinkCandidates.clear(); + fillCandidateData(collisions, tracks, ambiTracks, bcs); + // sort kinkCandidates by collisionID to allow joining with collision table + std::sort(kinkCandidates.begin(), kinkCandidates.end(), [](const kinkCandidate& a, const kinkCandidate& b) { return a.collisionID < b.collisionID; }); + + for (const auto& kinkCand : kinkCandidates) { + outputDataTable(kinkCand.collisionID, kinkCand.mothTrackID, kinkCand.daugTrackID, + kinkCand.decVtx[0], kinkCand.decVtx[1], kinkCand.decVtx[2], + kinkCand.mothSign, kinkCand.momMoth[0], kinkCand.momMoth[1], kinkCand.momMoth[2], + kinkCand.momDaug[0], kinkCand.momDaug[1], kinkCand.momDaug[2], + kinkCand.dcaXYmoth, kinkCand.dcaXYdaug, kinkCand.dcaKinkTopo); + } + } + PROCESS_SWITCH(kinkBuilder, process, "Produce kink tables", false); +}; + struct spectraKinkPiKa { Service pdg; // Histograms are defined with HistogramRegistry @@ -54,16 +361,21 @@ struct spectraKinkPiKa { Configurable cutzvertex{"cutzvertex", 10.0f, "Accepted z-vertex range (cm)"}; Configurable cutNSigmaPi{"cutNSigmaPi", 4, "NSigmaTPCPion"}; Configurable cutNSigmaKa{"cutNSigmaKa", 4, "NSigmaTPCKaon"}; + Configurable cutNSigmaMu{"cutNSigmaMu", 4, "cutNSigmaMu"}; Configurable rapCut{"rapCut", 0.8, "rapCut"}; Configurable kinkanglecut{"kinkanglecut", 2.0, "kinkanglecut"}; - Configurable minradius{"minradius", 1.0, "minradiuscut"}; + Configurable minradius{"minradius", 130.0, "minradiuscut"}; Configurable maxradius{"maxradius", 200.0, "maxradiuscut"}; + Configurable dcaXYcut{"dcaXYcut", 0.2, "dcaXYcut"}; + Configurable dcaZcut{"dcaZcut", 0.2, "dcaZcut"}; + Configurable tpcChi2Cut{"tpcChi2Cut", 4.0, "tpcChi2Cut"}; Configurable pid{"pidMother", 321, ""}; Configurable dpid{"pidDaughter", 13, ""}; Configurable d0pid{"dopid", 0, ""}; Preslice mPerCol = aod::track::collisionId; + Preslice mtPerCol = aod::track::collisionId; void init(InitContext const&) { @@ -74,6 +386,7 @@ struct spectraKinkPiKa { const AxisSpec etaAxis{200, -5.0, 5.0, "#eta"}; const AxisSpec vertexAxis{1200, -300., 300., "vrtx [cm]"}; const AxisSpec radiusAxis{600, 0., 300., "vrtx [cm]"}; + const AxisSpec massAxis{600, 0.1, 0.7, "Inv mass (GeV/#it{c}^{2})"}; // Event selection rEventSelection.add("hVertexZRec", "hVertexZRec", {HistType::kTH1F, {vertexAxis}}); @@ -92,6 +405,10 @@ struct spectraKinkPiKa { rpiKkink.add("h2_moth_pt_vs_eta_rec_pion", "pt_vs_eta_moth", {HistType::kTH2F, {ptAxis, etaAxis}}); rpiKkink.add("h2_pt_moth_vs_dau_rec_pion", "pt_moth_vs_dau", {HistType::kTH2F, {ptAxis, ptAxis}}); + // inv mass + rpiKkink.add("h2_invmass_kaon", "Inv mass vs Pt", {HistType::kTH3F, {massAxis, ptAxis, ptAxis}}); + rpiKkink.add("h2_invmass_pion", "Inv mass vs Pt", {HistType::kTH3F, {massAxis, ptAxis, ptAxis}}); + rpiKkink.add("h2_qt_pion", "qt", {HistType::kTH1F, {qtAxis}}); rpiKkink.add("h2_qt_vs_ptpion", "qt_pt", {HistType::kTH2F, {qtAxis, ptAxis}}); rpiKkink.add("h2_kink_angle_pion", "kink angle", {HistType::kTH1F, {kinkAxis}}); @@ -112,6 +429,26 @@ struct spectraKinkPiKa { } } + double computeMotherMass(ROOT::Math::PxPyPzMVector p_moth, ROOT::Math::PxPyPzMVector p_daug) + { + // Infer neutrino momentum from conservation + ROOT::Math::XYZVector p_nu_vec = p_moth.Vect() - p_daug.Vect(); + + // Neutrino energy (massless): E_nu = |p_nu| + double E_nu = p_nu_vec.R(); + + // Total energy of the system + double E_total = p_daug.E() + E_nu; + + // Total momentum = p_nu + p_daug + ROOT::Math::XYZVector p_total_vec = p_nu_vec + p_daug.Vect(); + double p_total_sq = p_total_vec.Mag2(); + + // Invariant mass from E² - |p|² + double m2 = E_total * E_total - p_total_sq; + return (m2 > 0) ? std::sqrt(m2) : -1.0; + } + void processData(CollisionsFull::iterator const& collision, aod::KinkCands const& KinkCands, TracksFull const&) { ROOT::Math::PxPyPzMVector v0; @@ -127,8 +464,25 @@ struct spectraKinkPiKa { for (const auto& kinkCand : KinkCands) { auto dauTrack = kinkCand.trackDaug_as(); auto mothTrack = kinkCand.trackMoth_as(); + if (mothTrack.collisionId() != collision.globalIndex()) { + continue; // not from this event + } + if (!mothTrack.has_collision() || !dauTrack.has_collision()) { + continue; + } + if (mothTrack.collisionId() != dauTrack.collisionId()) { + continue; // skip mismatched collision tracks + } bool kaon = false; bool pion = false; + /* + if (mothTrack.dcaXY() > dcaXYcut) + continue; + if (mothTrack.dcaZ() > dcaZcut) + continue; + */ + if (mothTrack.tpcChi2NCl() > tpcChi2Cut) + continue; if (std::abs(mothTrack.tpcNSigmaKa()) < cutNSigmaKa) { kaon = true; } @@ -138,6 +492,9 @@ struct spectraKinkPiKa { if (!kaon && !pion) { continue; } + if (cutNSigmaMu != -1 && std::abs(dauTrack.tpcNSigmaMu()) > cutNSigmaMu) { + continue; + } double radiusxy = std::sqrt(kinkCand.xDecVtx() * kinkCand.xDecVtx() + kinkCand.yDecVtx() * kinkCand.yDecVtx()); if (radiusxy < minradius || radiusxy > maxradius) continue; @@ -154,6 +511,7 @@ struct spectraKinkPiKa { float radToDeg = o2::constants::math::Rad2Deg; if (kinkangle * radToDeg < kinkanglecut) continue; + if (kaon) { rpiKkink.fill(HIST("h2_moth_pt_vs_eta_rec"), v0.Pt(), v0.Eta()); rpiKkink.fill(HIST("h2_dau_pt_vs_eta_rec"), v1.Pt(), v1.Eta()); @@ -172,12 +530,18 @@ struct spectraKinkPiKa { double ptd = pdlab.Perp(motherDir); // or p_d_lab.Mag() * sin(theta) if (kaon) { + v0.SetCoordinates(mothTrack.px(), mothTrack.py(), mothTrack.pz(), o2::constants::physics::MassKaonCharged); + double mass = computeMotherMass(v0, v1); rpiKkink.fill(HIST("h2_qt"), ptd); rpiKkink.fill(HIST("h2_qt_vs_pt"), ptd, v1.Pt()); + rpiKkink.fill(HIST("h2_invmass_kaon"), mass, v0.Pt(), ptd); } if (pion) { + v0.SetCoordinates(mothTrack.px(), mothTrack.py(), mothTrack.pz(), o2::constants::physics::MassPionCharged); + double mass = computeMotherMass(v0, v1); rpiKkink.fill(HIST("h2_qt_pion"), ptd); rpiKkink.fill(HIST("h2_qt_vs_ptpion"), ptd, v1.Pt()); + rpiKkink.fill(HIST("h2_invmass_pion"), mass, v0.Pt(), ptd); } } } @@ -194,6 +558,7 @@ struct spectraKinkPiKa { if (!collision.selection_bit(o2::aod::evsel::kNoTimeFrameBorder) || !collision.selection_bit(o2::aod::evsel::kNoITSROFrameBorder)) { continue; } + rEventSelection.fill(HIST("hVertexZRec"), collision.posZ()); auto kinkCandPerColl = KinkCands.sliceBy(mPerCol, collision.globalIndex()); for (const auto& kinkCand : kinkCandPerColl) { @@ -218,7 +583,6 @@ struct spectraKinkPiKa { continue; rpiKkink.fill(HIST("h2_kinkradius_vs_vz"), kinkCand.zDecVtx(), radiusxy); rpiKkink.fill(HIST("h2_kink_vx_vs_vy"), kinkCand.xDecVtx(), kinkCand.yDecVtx()); - v0.SetCoordinates(mothTrack.px(), mothTrack.py(), mothTrack.pz(), o2::constants::physics::MassPionCharged); v1.SetCoordinates(dauTrack.px(), dauTrack.py(), dauTrack.pz(), o2::constants::physics::MassMuon); @@ -245,7 +609,9 @@ struct spectraKinkPiKa { // do MC association auto mcLabMoth = trackLabelsMC.rawIteratorAt(mothTrack.globalIndex()); auto mcLabDau = trackLabelsMC.rawIteratorAt(dauTrack.globalIndex()); + if (mcLabMoth.has_mcParticle() && mcLabDau.has_mcParticle()) { + auto mcTrackMoth = mcLabMoth.mcParticle_as(); auto mcTrackDau = mcLabDau.mcParticle_as(); if (!mcTrackDau.has_mothers()) { @@ -266,11 +632,11 @@ struct spectraKinkPiKa { for (const auto& mcPart : particlesMC) { ROOT::Math::PxPyPzMVector v0; ROOT::Math::PxPyPzMVector v1; - if (!d0pid && (std::abs(mcPart.pdgCode()) != pid || std::abs(mcPart.y()) > rapCut)) { + if (!d0pid && (std::abs(mcPart.pdgCode()) != pid || std::abs(mcPart.eta()) > rapCut)) { continue; } bool isDmeson = std::abs(mcPart.pdgCode()) == kD0 || std::abs(mcPart.pdgCode()) == kDPlus || std::abs(mcPart.pdgCode()) == kDStar; - if (d0pid && (!isDmeson || std::abs(mcPart.y()) > rapCut)) { + if (d0pid && (!isDmeson || std::abs(mcPart.eta()) > rapCut)) { continue; } if (!mcPart.has_daughters()) { @@ -320,6 +686,8 @@ struct spectraKinkPiKa { WorkflowSpec defineDataProcessing(ConfigContext const& cfgc) { - return WorkflowSpec{ - adaptAnalysisTask(cfgc)}; + auto builderTask = adaptAnalysisTask(cfgc); + auto spectraTask = adaptAnalysisTask(cfgc); + + return {builderTask, spectraTask}; // Just return both tasks }