nano.cpp (also named as NanoAODTools.Cpp) is a C++ rewrite of selected NanoAOD-tools/NanoHRTTools workflows.
The goal is to keep the analysis logic human-readable while making the event loop faster and easier to validate. The style is intentionally close to the traditional ROOT event loop:
- read one event
- build objects
- apply selections
- compute new features
- write a skim tree
The guiding idea is:
Agents write, you review.
The framework is designed so AI agents can write straightforward C++ while humans can still review the physics logic in a direct, readable way.
The original NanoAOD-tools code is Python-based and flexible, but it is not as fast as columnar analysis frameworks such as RDataFrame or awkward-array/coffea. This repository keeps the useful NanoAOD-tools programming model and moves the event processing to C++ for faster ntuplization.
The intended style is explicit event-level code:
auto fatjets = event.collection("FatJet").objects();
for (auto &jet : event.collection("Jet").objects()) {
const auto btag = jet.get<float>("btagUParTAK4B");
if (jet.pt() > 30.0f && std::abs(jet.eta()) < 2.4f && btag > btag_wp) {
bjets.push_back(jet);
}
}
event.set("bjets", bjets);
event.set("leptonicW", leptonic_w);
for (auto &fj : fatjets) {
fj.set("subjets", linked_subjets);
fj.set("dr_T", delta_r_to_top);
fj.set("is_qualified", true);
}In practice this means:
- Object collections are accessed from the event, for example
event.collection("FatJet"). - NanoAOD branches are accessed as typed object attributes, for example
fj.get<float>("msoftdrop"). - New event-level values can be attached to
event, for example:- attach selected muons:
event.set("muons", selected_muons); - attach corrected MET:
event.set("met_pt", corrected_met_pt); - attach the reconstructed W candidate:
event.set("leptonicW", leptonic_w);
- attach selected muons:
- New object-level features can be attached to each object, for example:
- attach corrected four-vectors:
auto corrected_p4 = polar_p4(obj); obj.set("p4", corrected_p4); - attach linked subjets to a given fatjet:
fj.set("subjets", linked_subjets);
- attach corrected four-vectors:
- Channel producers are plain C++ event loops. In the main
analyze()function, usereturn falseto veto an event. - A YAML card in
configs/run/contains all information to guide the run. - Corrections use modern correctionlib payloads where possible. JEC and MET corrections build on the CMSJMECalculators project.
You do not need to write this code or worry about C++ syntax; agents will fill in the implementation, and you only need to review it.
The implemented channel is:
muon: a heavy-flavour muon control region targeting semileptonic ttbar-like phase space, enriched in boosted top/W jets.
Main files:
app/nano_run.cpp: local runner.app/nano_make_condor.cpp: Condor submission builder.configs/run/: runnable YAML cards.configs/samples/: dataset YAML files for batch submission.
For agents: for framework details, read docs/framework-structure.md.
Use the ROOT/LCG runtime before configuring, building, or running:
source /cvmfs/sft.cern.ch/lcg/views/LCG_108/x86_64-el9-gcc13-opt/setup.shBuild:
cmake -S . -B build
cmake --build build -jExample using a local validation file:
build/nano_run \
--input-files /store/mc/RunIISummer20UL18NanoAODv9/TTToSemiLeptonic_TuneCP5_13TeV-powheg-pythia8/NANOAODSIM/106X_upgrade2018_realistic_v16_L1v1-v1/120000/87DEE912-70CF-A549-B10B-1A229B256E88.root \
--output-file muon_2018_test.root \
--config configs/run/muon_2018_v9.yaml \
--channel muon \
--num-events 5000--input-files accepts one file or a comma-separated list. Local paths, root://... paths, and /store/... paths are supported.
If --variations is omitted, it defaults to nominal. Outputs are always written with a variation suffix, so the example above writes muon_2018_test_nominal.root.
Useful options:
--tree-name Events
--set output.include_lhe_weights=true
--variations nominal,jes_up,jes_down--variations takes a comma-separated list and writes one ROOT file per requested variation. Supported JME names currently include nominal, jes_up, jes_down, jer_up, jer_down, met_up, and met_down.
See tests/README.md.
Create a Condor work directory from a sample YAML:
build/nano_make_condor \
--input-yaml configs/samples/muon_2018_v9_MC.yaml \
--job-dir jobs/condor_muon_2018_v9_MC \
--output-dir /path/to/output \
--config configs/run/muon_2018_v9.yaml \
--channel muon \
--nfiles-per-job 5 \
--num-events -1This creates the requested Condor work directory, copies a merged config snapshot, packs the repository, and writes submit.jdl.
Submit manually:
cd jobs/condor_muon_2018_v9_MC
condor_submit submit.jdlEach job runs process.sh, unpacks the repository, builds it if needed, prints the full nano_run command, and writes variation-suffixed ROOT pieces under <output-dir>/pieces/. Without --variations, Condor jobs also default to nominal and write *_nominal.root pieces.
After jobs finish, return to the repository root and merge Condor pieces with:
build/nano_merge /path/to/outputPass the base output directory, not the pieces/ subdirectory. nano_merge reads input pieces from <output-dir>/pieces/.
It first writes merged ROOT files to a temporary directory, then copies all merged outputs back under <output-dir>/.
Follow docs/create-new-channel.md.
The intended workflow is that you define the physics purpose and review the logic, while agents help write a new channel by following the existing producer pattern.