@@ -882,6 +882,98 @@ static const PermMap *findPermByPrefix(
882882 return nullptr ;
883883}
884884
885+ // ============================================================================
886+ // SECTION 9b — Stage 1b: Collision-grouped track tables
887+ // ============================================================================
888+ //
889+ // The primary track tables (O2track_iu, O2mfttrack_*, O2fwdtrack) are GROUPED
890+ // by collision. O2's slicing cache (ArrowTableSlicingCache::validateOrder)
891+ // requires every fIndexCollisions group — including the "-1" ambiguous group —
892+ // to be a single contiguous run; otherwise it aborts with
893+ // "Table ... index fIndexCollisions has a group with index -1 that is split".
894+ //
895+ // When several MC sub-timeframes are merged into one DF_ folder (data-embedding
896+ // anchoring, which stores MC timeframes under the same DF_ as the parent data
897+ // file), each sub-frame contributes its own [collision-grouped][-1 ambiguous]
898+ // block. Concatenating them splits the -1 group into N runs, so the table is
899+ // no longer sliceable. Stage 1 only reorders BC-indexed tables, and tracks are
900+ // otherwise written in input row order, so the split survives into the output.
901+ //
902+ // This stage re-establishes the grouping: it reorders each collision-grouped
903+ // track table by its remapped fIndexCollisions (stable, with -1 sinking to the
904+ // end so the ambiguous group is one contiguous run — matching the Stage 1
905+ // convention) and publishes the resulting row permutation. Downstream:
906+ // * paste-join children (O2trackextra, O2trackcov_iu, O2mctracklabel, ...)
907+ // follow the published parent permutation;
908+ // * every fIndexTracks* / fIndexMFTTracks / fIndexFwdTracks reference is
909+ // remapped through it in processPasteJoinTables.
910+ static bool isCollGroupedTrackTable (const std ::string & tname ) {
911+ static const char * kPrefixes [] = {"O2track_iu ", "O2track ",
912+ "O2mfttrack ", "O2fwdtrack "};
913+ for (auto * p : kPrefixes )
914+ if (TString (tname .c_str ()).BeginsWith (p )) return true;
915+ return false ;
916+ }
917+
918+ static void stage1b_reorderTrackTables (
919+ TDirectory * dirIn , TDirectory * dirOut ,
920+ std ::unordered_map < std ::string , PermMap > & allPerms ,
921+ std ::unordered_set < std ::string > & written ) {
922+
923+ const PermMap * collPermP = findPermByPrefix (allPerms , "O2collision_" );
924+ if (!collPermP ) return ; // no collisions present — nothing to regroup against
925+
926+ TIter it (dirIn -> GetListOfKeys ());
927+ while (TKey * key = static_cast < TKey * > (it ())) {
928+ if (TString (key -> GetClassName ()) != "TTree" ) continue ;
929+ std ::unique_ptr < TObject > obj (key -> ReadObj ());
930+ TTree * src = dynamic_cast < TTree * > (obj .get ());
931+ if (!src ) continue ;
932+
933+ std ::string tname = src -> GetName ();
934+ if (written .count (tname )) continue ; // BC-indexed tracks etc. already done
935+ if (!isCollGroupedTrackTable (tname )) continue ;
936+ if (isPasteJoinChild (tname )) continue ; // children follow their parent below
937+ if (!src -> GetBranch ("fIndexCollisions" )) continue ;
938+
939+ std ::cout << " Stage1b [coll-grouped]: " << tname << "\n" ;
940+
941+ Long64_t nSrc = src -> GetEntries ();
942+ TBranch * inIdxBr = src -> GetBranch ("fIndexCollisions" );
943+ TLeaf * idxLeaf = static_cast < TLeaf * > (inIdxBr -> GetListOfLeaves ()-> At (0 ));
944+ ScalarTag idxTag = tagOf (idxLeaf );
945+ std ::vector < unsigned char > idxBuf (byteSize (idxTag ), 0 );
946+ inIdxBr -> SetAddress (idxBuf .data ());
947+
948+ struct SortEntry { Long64_t newColl ; Long64_t srcRow ; };
949+ std ::vector < SortEntry > entries ;
950+ entries .reserve (nSrc );
951+ for (Long64_t i = 0 ; i < nSrc ; ++ i ) {
952+ inIdxBr -> GetEntry (i );
953+ Long64_t oldColl = readAsInt (idxBuf .data (), idxTag );
954+ Long64_t newColl = (oldColl >= 0 && oldColl < (Long64_t )collPermP -> size ())
955+ ? (* collPermP )[oldColl ] : -1 ;
956+ entries .push_back ({newColl , i });
957+ }
958+ // Stable-sort by remapped collision; the ambiguous group (-1) sinks to the
959+ // end as a single contiguous run. Stable keeps the within-collision order.
960+ std ::stable_sort (entries .begin (), entries .end (),
961+ [](const SortEntry & a , const SortEntry & b ){
962+ if (a .newColl < 0 && b .newColl >= 0 ) return false;
963+ if (a .newColl >= 0 && b .newColl < 0 ) return true;
964+ return a .newColl < b .newColl ;
965+ });
966+ std ::vector < Long64_t > rowOrder ;
967+ rowOrder .reserve (nSrc );
968+ for (auto & e : entries ) rowOrder .push_back (e .srcRow );
969+
970+ // Reorder rows and remap fIndexCollisions values through collPerm.
971+ PermMap perm = rewriteTable (src , dirOut , rowOrder , "fIndexCollisions" , * collPermP );
972+ allPerms [tname ] = std ::move (perm );
973+ written .insert (tname );
974+ }
975+ }
976+
885977static void processPasteJoinTables (
886978 TDirectory * dirIn , TDirectory * dirOut ,
887979 const std ::unordered_map < std ::string , PermMap > & allPerms ,
@@ -894,6 +986,12 @@ static void processPasteJoinTables(
894986 const PermMap * mcParticlePerm = findPermByPrefix (allPerms , "O2mcparticle" );
895987 const PermMap * mcCollPermP = findPermByPrefix (allPerms , "O2mccollision_" );
896988 const PermMap * collPermP = findPermByPrefix (allPerms , "O2collision_" );
989+ // Track tables reordered in Stage 1b: every reference into them must be
990+ // remapped through their permutation (null if the table is absent / wasn't
991+ // reordered, in which case no remap is needed).
992+ const PermMap * trkPerm = findPermByPrefix (allPerms , "O2track_iu" );
993+ const PermMap * mftPerm = findPermByPrefix (allPerms , "O2mfttrack" );
994+ const PermMap * fwdPerm = findPermByPrefix (allPerms , "O2fwdtrack" );
897995 // bcPermP is passed in from processDF (the BC table is the only stage
898996 // whose permutation isn't already published in allPerms).
899997
@@ -942,6 +1040,23 @@ static void processPasteJoinTables(
9421040 extraRemaps .push_back ({"fIndexBC" , bcPermP });
9431041 }
9441042
1043+ // Track-pointing indices: the track tables may have been reordered in
1044+ // Stage 1b, so every reference into them must be remapped through the
1045+ // corresponding permutation. (No-op when the perm is null / absent.)
1046+ auto addTrkRemap = [& ](const char * br , const PermMap * pm ) {
1047+ if (pm && src -> GetBranch (br )) extraRemaps .push_back ({br , pm });
1048+ };
1049+ addTrkRemap ("fIndexTracks ", trkPerm );
1050+ addTrkRemap ("fIndexTracks_0" , trkPerm );
1051+ addTrkRemap ("fIndexTracks_1" , trkPerm );
1052+ addTrkRemap ("fIndexTracks_2" , trkPerm );
1053+ addTrkRemap ("fIndexTracks_Pos" , trkPerm );
1054+ addTrkRemap ("fIndexTracks_Neg" , trkPerm );
1055+ addTrkRemap ("fIndexTracks_ITS" , trkPerm );
1056+ addTrkRemap ("fIndexMFTTracks" , mftPerm );
1057+ addTrkRemap ("fIndexFwdTracks" , fwdPerm );
1058+ addTrkRemap ("fIndexFwdTracks_MatchMCHTrack" , fwdPerm );
1059+
9451060 // Find a paste-join parent for this table (kPasteJoins lookup).
9461061 const PermMap * parentPerm = nullptr ;
9471062 std ::string parentName ;
@@ -1080,6 +1195,13 @@ static void processDF(TDirectory *dirIn, TDirectory *dirOut) {
10801195 std ::cout << " (no MCCollision table found — skipping stage 2)\n" ;
10811196 }
10821197
1198+ // ---- Stage 1b: regroup collision-grouped track tables ----
1199+ // Must run after Stage 1 (needs the collision permutation) and before the
1200+ // paste-join stage (so children follow the new track order and fIndexTracks*
1201+ // references are remapped). Publishes track permutations into stage1Perms.
1202+ std ::cout << "-- Stage 1b: collision-grouped track tables --\n" ;
1203+ stage1b_reorderTrackTables (dirIn , dirOut , stage1Perms , written );
1204+
10831205 // ---- Paste-join tables + unrelated tables ----
10841206 std ::cout << "-- Paste-join and unrelated tables --\n" ;
10851207 processPasteJoinTables (dirIn , dirOut , stage1Perms , written , & s0 .bcPerm );
0 commit comments