Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pixi-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ jobs:
build:
uses: ShipSoft/.github/.github/workflows/pixi-cmake-build.yml@main
with:
tasks: '["configure", "build", "install", "test"]'
tasks: '["configure", "build", "install", "test", "smoke"]'
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ target_link_libraries(
spdlog::spdlog
)

# Smoke-test input generator (build-only, not installed)
add_executable(make_test_input tools/make_test_input.cpp)
target_include_directories(
make_test_input
PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_include_directories(
make_test_input
SYSTEM
PRIVATE "${Random123_INCLUDE_DIR}"
)
target_link_libraries(
make_test_input
PRIVATE SHiP::SHiPDataModel ROOT::RIO ROOT::ROOTNTuple
)

# Install
include(GNUInstallDirs)

Expand Down
24 changes: 24 additions & 0 deletions scripts/smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration
#
# SPDX-License-Identifier: LGPL-3.0-or-later

# Quick smoke test for CI: generate a small simulation-like input, run the
# digitisation workflow end to end, and check that the outputs were produced.
set -euo pipefail

./build/make_test_input smoke_input.root 10

# Remove outputs from previous runs so stale files cannot pass the checks.
rm -f smoke_digi_output.root smoke_digi_validation.root

phlex -c workflows/smoke.jsonnet

for f in smoke_digi_output.root smoke_digi_validation.root; do
if ! [ -s "$f" ]; then
echo "missing output: $f" >&2
exit 1
fi
done
Comment thread
coderabbitai[bot] marked this conversation as resolved.
python scripts/validate_smoke.py smoke_digi_output.root smoke_digi_validation.root
echo "Smoke test OK"
77 changes: 77 additions & 0 deletions scripts/validate_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration
#
# SPDX-License-Identifier: LGPL-3.0-or-later

"""Validate the smoke-test outputs of the digitisation workflow.

Checks that the digitised-output file contains all per-detector RNTuples and
that the validation file contains all histograms, each with entries.
"""

import os
import sys

import ROOT

NTUPLES = [
"calorimeter_hits",
"sbt_hits",
"straw_tubes_hits",
"timing_detector_hits",
"ubt_hits",
]

HISTOGRAMS = [
"h_ubt_multiplicity",
"h_sbt_multiplicity",
"h_straw_tubes_multiplicity",
"h_calorimeter_multiplicity",
"h_timing_detector_multiplicity",
"h_hit_x",
"h_hit_y",
"h_hit_z",
]


def check_ntuples(path):
errors = []
for name in NTUPLES:
try:
reader = ROOT.RNTupleReader.Open(name, path)
except Exception as e:
errors.append(f"{path}: cannot open RNTuple '{name}': {e}")
continue
if reader.GetNEntries() == 0:
errors.append(f"{path}: RNTuple '{name}' has no entries")
return errors


def check_histograms(path):
try:
file = ROOT.TFile.Open(path)
except OSError as e:
return [f"{path}: cannot open file: {e}"]
if not file or file.IsZombie():
return [f"{path}: cannot open file"]
errors = []
for name in HISTOGRAMS:
hist = file.Get(name)
if not hist:
errors.append(f"{path}: missing histogram '{name}'")
elif hist.GetEntries() == 0:
errors.append(f"{path}: histogram '{name}' has no entries")
return errors


if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <digi_output.root> <validation.root>", file=sys.stderr)
sys.exit(2)
errors = check_ntuples(sys.argv[1]) + check_histograms(sys.argv[2])
for error in errors:
print(error, file=sys.stderr)
sys.stdout.flush()
sys.stderr.flush()
# Skip interpreter shutdown to dodge a PyROOT RNTuple teardown crash.
os._exit(1 if errors else 0)
79 changes: 79 additions & 0 deletions tools/make_test_input.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration
//
// SPDX-License-Identifier: LGPL-3.0-or-later

// make_test_input.cpp — deterministic simulation-like input for smoke tests
//
// Writes an RNTuple in the layout produced by the simulation chain
// (sim_particles + sim_hits per event), with hits covering all detector
// IDs, so the digitisation workflow can run without Geant4 or aegir.

#include <ROOT/RNTupleModel.hxx>
#include <ROOT/RNTupleWriter.hxx>

#include <SHiP/SimHit.hpp>
#include <SHiP/SimParticle.hpp>
#include <SHiP/detectors/detector_id.hpp>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <philox_rng.hpp>
#include <string>
#include <vector>

int main(int argc, char* argv[]) {
std::string const filename = argc > 1 ? argv[1] : "smoke_input.root";
auto const n_events = argc > 2 ? std::stoul(argv[2]) : 10UL;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// The event loop counts (and seeds the RNG) with a uint32_t.
if (n_events > std::numeric_limits<std::uint32_t>::max()) {
std::fprintf(stderr, "n_events out of range: %lu\n", n_events);
return 1;
}

auto model = ROOT::RNTupleModel::Create();
auto particles = model->MakeField<std::vector<SHiP::SimParticle>>("sim_particles");
auto hits = model->MakeField<std::vector<SHiP::SimHit>>("sim_hits");
auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), "events", filename);

constexpr SHiP::detector_id detectors[] = {
SHiP::detector_id::UpstreamTagger, SHiP::detector_id::SurroundTagger,
SHiP::detector_id::StrawTubes, SHiP::detector_id::Calorimeter,
SHiP::detector_id::TimingDetector};

for (std::uint32_t event = 0; event < n_events; ++event) {
Shannon::PhiloxRng rng{0, 0x7E57DA7A, event};

particles->clear();
hits->clear();

auto const n_tracks = 1 + static_cast<int>(rng.uniform(0.0, 3.0));
for (int track = 0; track < n_tracks; ++track) {
SHiP::SimParticle particle;
particle.trackId = track;
particle.parentId = -1;
particle.pdgCode = 13;
particle.momentum = {rng.uniform(-0.5, 0.5), rng.uniform(-0.5, 0.5),
rng.uniform(10.0, 100.0)};
particle.energy = particle.momentum[2];
particles->push_back(particle);

for (auto const detector : detectors) {
SHiP::SimHit hit;
hit.detectorId = static_cast<std::int32_t>(detector);
hit.trackId = track;
hit.pdgCode = particle.pdgCode;
hit.position = {rng.uniform(-500.0, 500.0), rng.uniform(-500.0, 500.0),
rng.uniform(0.0, 10000.0)};
hit.momentum = particle.momentum;
hit.energyDeposit = rng.uniform(0.0, 0.1);
hit.time = rng.uniform(0.0, 100.0);
hit.pathLength = rng.uniform(0.0, 10.0);
hits->push_back(hit);
}
}
writer->Fill();
}

std::printf("Wrote %lu events to %s\n", n_events, filename.c_str());
return 0;
}
33 changes: 33 additions & 0 deletions workflows/smoke.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
driver: {
cpp: 'generate_layers',
layers: {
spill: { parent: 'job', total: 10 },
},
},

sources: {
rntuple_source: {
cpp: 'read_sim_file',
input_file: 'smoke_input.root',
ntuple_name: 'events',
layer: 'spill',
},
},

modules: {
digitise_hits: {
cpp: 'digitise_hits',
layer: 'spill',
seed: 42,
},
digitised_output: {
cpp: 'digitised_output_module',
mode: 'digi',
rntuple_file: 'smoke_digi_output.root',
histo_file: 'smoke_digi_validation.root',
creator: 'digitise_hits',
layer: 'spill',
},
},
}
Loading