File indexing completed on 2025-08-28 08:12:08
0001
0002
0003
0004
0005
0006
0007
0008
0009 #include "ActsExamples/Digitization/MuonSpacePointDigitizer.hpp"
0010
0011 #include "Acts/Definitions/Units.hpp"
0012 #include "Acts/Geometry/GeometryContext.hpp"
0013 #include "Acts/Geometry/VolumeBounds.hpp"
0014 #include "Acts/Surfaces/LineBounds.hpp"
0015 #include "Acts/Surfaces/RectangleBounds.hpp"
0016 #include "Acts/Surfaces/TrapezoidBounds.hpp"
0017 #include "Acts/Surfaces/detail/LineHelper.hpp"
0018 #include "Acts/Surfaces/detail/PlanarHelper.hpp"
0019 #include "Acts/Utilities/ArrayHelpers.hpp"
0020 #include "Acts/Utilities/Helpers.hpp"
0021 #include "Acts/Utilities/MathHelpers.hpp"
0022 #include "Acts/Utilities/StringHelpers.hpp"
0023 #include "ActsExamples/Digitization/Smearers.hpp"
0024 #include "ActsExamples/EventData/MuonSpacePoint.hpp"
0025
0026 #include <algorithm>
0027 #include <format>
0028 #include <map>
0029 #include <ranges>
0030
0031 #include "TArrow.h"
0032 #include "TBox.h"
0033 #include "TCanvas.h"
0034 #include "TEllipse.h"
0035 #include "TH2I.h"
0036 #include "TROOT.h"
0037 #include "TStyle.h"
0038
0039 using namespace Acts;
0040 using namespace detail::LineHelper;
0041 using namespace PlanarHelper;
0042 using namespace UnitLiterals;
0043
0044 namespace {
0045
0046
0047
0048
0049 constexpr double quantize(const double x, const double pitch) {
0050 if (x >= 0.) {
0051 return std::max(std::floor(x - 0.5 * pitch) / pitch, 0.) * pitch;
0052 }
0053 return -quantize(-x, pitch);
0054 }
0055
0056
0057 double halfHeight(const SurfaceBounds& bounds) {
0058 if (bounds.type() == SurfaceBounds::BoundsType::eRectangle) {
0059 return static_cast<const RectangleBounds&>(bounds).get(
0060 RectangleBounds::eMaxY);
0061 }
0062
0063 else if (bounds.type() == SurfaceBounds::BoundsType::eTrapezoid) {
0064 return static_cast<const TrapezoidBounds&>(bounds).get(
0065 TrapezoidBounds::eHalfLengthY);
0066 }
0067 return std::numeric_limits<double>::max();
0068 }
0069
0070 std::unique_ptr<TEllipse> drawCircle(const double x, double y, const double r,
0071 const int color = kBlack,
0072 const int fillStyle = 0) {
0073 auto circle = std::make_unique<TEllipse>(x, y, r);
0074 circle->SetLineColor(color);
0075 circle->SetFillStyle(fillStyle);
0076 circle->SetLineWidth(1);
0077 circle->SetFillColorAlpha(color, 0.2);
0078 return circle;
0079 }
0080
0081 std::unique_ptr<TBox> drawBox(const double cX, const double cY, const double wX,
0082 const double wY, const int color = kBlack,
0083 const int fillStyle = 3344) {
0084 auto box = std::make_unique<TBox>(cX - 0.5 * wX, cY - 0.5 * wY, cX + 0.5 * wX,
0085 cY + 0.5 * wY);
0086 box->SetLineColor(color);
0087 box->SetFillStyle(fillStyle);
0088 box->SetLineWidth(1);
0089 box->SetFillColorAlpha(color, 0.2);
0090 return box;
0091 }
0092
0093 constexpr GeometryIdentifier toChamberId(const GeometryIdentifier& id) {
0094 return GeometryIdentifier{}.withVolume(id.volume()).withLayer(id.layer());
0095 }
0096
0097 }
0098
0099 namespace ActsExamples {
0100
0101 MuonSpacePointDigitizer::MuonSpacePointDigitizer(const Config& cfg,
0102 Logging::Level lvl)
0103 : IAlgorithm("MuonSpacePointDigitizer", lvl), m_cfg{cfg} {}
0104
0105 ProcessCode MuonSpacePointDigitizer::initialize() {
0106 using enum ActsExamples::ProcessCode;
0107 if (!m_cfg.trackingGeometry) {
0108 ACTS_ERROR("No tracking geometry was parsed");
0109 return ABORT;
0110 }
0111 if (!m_cfg.randomNumbers) {
0112 ACTS_ERROR("No random number generator was parsed");
0113 return ABORT;
0114 }
0115 MuonSpacePointCalibrator::Config calibCfg{};
0116 m_cfg.calibrator =
0117 std::make_unique<MuonSpacePointCalibrator>(calibCfg, logger().clone());
0118
0119 if (m_cfg.inputSimHits.empty()) {
0120 ACTS_ERROR("No sim hits have been parsed ");
0121 return ABORT;
0122 }
0123 if (m_cfg.inputParticles.empty()) {
0124 ACTS_ERROR("No simulated particles were parsed");
0125 return ABORT;
0126 }
0127 if (m_cfg.outputSpacePoints.empty()) {
0128 ACTS_ERROR("No output space points were defined");
0129 return ABORT;
0130 }
0131 ACTS_DEBUG("Retrieve sim hits and particles from "
0132 << m_cfg.inputSimHits << " & " << m_cfg.inputParticles);
0133 ACTS_DEBUG("Write produced space points to " << m_cfg.outputSpacePoints);
0134 m_inputSimHits.initialize(m_cfg.inputSimHits);
0135 m_inputParticles.initialize(m_cfg.inputParticles);
0136 m_outputSpacePoints.initialize(m_cfg.outputSpacePoints);
0137 gROOT->SetStyle("ATLAS");
0138 return SUCCESS;
0139 }
0140
0141 const MuonSpacePointCalibrator& MuonSpacePointDigitizer::calibrator() const {
0142 assert(m_cfg.calibrator != nullptr);
0143 return *m_cfg.calibrator;
0144 }
0145 const TrackingGeometry& MuonSpacePointDigitizer::trackingGeometry() const {
0146 assert(m_cfg.trackingGeometry != nullptr);
0147 return *m_cfg.trackingGeometry;
0148 }
0149 Transform3 MuonSpacePointDigitizer::toSpacePointFrame(
0150 const GeometryContext& gctx, const GeometryIdentifier& hitId) const {
0151 const Surface* hitSurf = trackingGeometry().findSurface(hitId);
0152 assert(hitSurf != nullptr);
0153
0154
0155
0156 const TrackingVolume* volume =
0157 trackingGeometry().findVolume(toChamberId(hitId));
0158 assert(volume != nullptr);
0159
0160 const Transform3 parentTrf{AngleAxis3{90._degree, Vector3::UnitZ()} *
0161 volume->itransform() * hitSurf->transform(gctx)};
0162 ACTS_VERBOSE("Transform into space point frame for surface "
0163 << hitId << " is \n"
0164 << toString(parentTrf));
0165 return parentTrf;
0166 }
0167 ProcessCode MuonSpacePointDigitizer::execute(
0168 const AlgorithmContext& ctx) const {
0169 const SimHitContainer& gotSimHits = m_inputSimHits(ctx);
0170 const SimParticleContainer& simParticles = m_inputParticles(ctx);
0171 ACTS_DEBUG("Retrieved " << gotSimHits.size() << " hits & "
0172 << simParticles.size() << " associated particles.");
0173
0174 MuonSpacePointContainer outSpacePoints{};
0175
0176 GeometryContext gctx{};
0177
0178 using MuonId_t = MuonSpacePoint::MuonId;
0179 auto rndEngine = m_cfg.randomNumbers->spawnGenerator(ctx);
0180
0181 std::map<GeometryIdentifier, MuonSpacePointBucket> spacePointsPerChamber{};
0182 std::unordered_map<GeometryIdentifier, double> strawTimes{};
0183 std::multimap<GeometryIdentifier, std::array<double, 3>> stripTimes{};
0184
0185 for (const auto& hit : gotSimHits) {
0186 const GeometryIdentifier hitId = hit.geometryId();
0187
0188 const Surface* hitSurf = trackingGeometry().findSurface(hitId);
0189 assert(hitSurf != nullptr);
0190
0191 const Transform3& surfLocToGlob{hitSurf->transform(gctx)};
0192
0193
0194 const Vector3 locPos = surfLocToGlob.inverse() * hit.position();
0195 const Vector3 locDir = surfLocToGlob.inverse().linear() * hit.direction();
0196
0197 const auto& bounds = hitSurf->bounds();
0198 ACTS_DEBUG("Process hit: "
0199 << toString(locPos) << ", dir: " << toString(locDir)
0200 << "recorded in a "
0201 << Surface::s_surfaceTypeNames[toUnderlying(hitSurf->type())]
0202 << " surface with id: " << hitId << ", bounds: " << bounds);
0203 bool convertSp{true};
0204
0205 MuonSpacePoint newSp{};
0206 newSp.setGeometryId(hitId);
0207
0208
0209 const Transform3 parentTrf{toSpacePointFrame(gctx, hitId)};
0210
0211 const auto& calibCfg = calibrator().config();
0212 switch (hitSurf->type()) {
0213
0214 using enum Surface::SurfaceType;
0215 case Plane: {
0216 ACTS_VERBOSE("Hit is recorded in a strip detector ");
0217 auto planeCross = intersectPlane(locPos, locDir, Vector3::UnitZ(), 0.);
0218 const auto hitPos = planeCross.position();
0219 Vector3 smearedHit{Vector3::Zero()};
0220 switch (bounds.type()) {
0221 case SurfaceBounds::BoundsType::eRectangle: {
0222 smearedHit[ePos0] =
0223 quantize(hitPos[ePos0], calibCfg.rpcPhiStripPitch);
0224 smearedHit[ePos1] =
0225 quantize(hitPos[ePos1], calibCfg.rpcEtaStripPitch);
0226 ACTS_VERBOSE("Position before "
0227 << toString(hitPos) << ", after smearing"
0228 << toString(smearedHit) << ", " << bounds);
0229
0230 if (!bounds.inside(Vector2{smearedHit[ePos0], smearedHit[ePos1]})) {
0231 convertSp = false;
0232 break;
0233 }
0234 auto ranges = stripTimes.equal_range(hitId);
0235 for (auto digitHitItr = ranges.first; digitHitItr != ranges.second;
0236 ++digitHitItr) {
0237 const auto& existCoords = digitHitItr->second;
0238
0239 if (std::abs(existCoords[0] - smearedHit[ePos0]) <
0240 std::numeric_limits<double>::epsilon() &&
0241 std::abs(existCoords[1] - smearedHit[ePos1]) <
0242 std::numeric_limits<double>::epsilon() &&
0243 hit.time() - existCoords[2] < config().rpcDeadTime) {
0244 convertSp = false;
0245 break;
0246 }
0247 if (!convertSp) {
0248 break;
0249 }
0250 }
0251
0252 stripTimes.insert(std::make_pair(
0253 hitId,
0254 std::array{smearedHit[ePos0], smearedHit[ePos1], hit.time()}));
0255
0256
0257 if (config().digitizeTime) {
0258 assert(calibCfg.rpcTimeResolution > 0.);
0259 const double stripTime =
0260 (*Digitization::Gauss{calibCfg.rpcTimeResolution}(hit.time(),
0261 rndEngine))
0262 .first;
0263 newSp.setTime(stripTime);
0264 }
0265 newSp.setCovariance(
0266 calibCfg.rpcPhiStripPitch, calibCfg.rpcEtaStripPitch,
0267 m_cfg.digitizeTime ? calibCfg.rpcTimeResolution : 0.);
0268
0269 break;
0270 }
0271
0272 case SurfaceBounds::BoundsType::eTrapezoid:
0273 break;
0274 default:
0275 convertSp = false;
0276 }
0277
0278 if (convertSp) {
0279 newSp.defineCoordinates(
0280 Vector3{parentTrf * smearedHit},
0281 Vector3{parentTrf.linear() * Vector3::UnitY()},
0282 Vector3{parentTrf.linear() * Vector3::UnitX()});
0283 MuonId_t id{};
0284
0285 id.setChamber(MuonId_t::StationName::BIS,
0286 hit.position().z() > 0 ? MuonId_t::DetSide::A
0287 : MuonId_t::DetSide::C,
0288 1, MuonId_t::TechField::Rpc);
0289 id.setCoordFlags(true, true);
0290 newSp.setId(id);
0291 }
0292
0293 break;
0294 }
0295 case Straw: {
0296 auto closeApproach =
0297 lineIntersect<3>(Vector3::Zero(), Vector3::UnitZ(), locPos, locDir);
0298 const auto nominalPos = closeApproach.position();
0299 const double unsmearedR = fastHypot(nominalPos.x(), nominalPos.y());
0300 ACTS_VERBOSE("Hit is recorded in a straw detector, R: "
0301 << unsmearedR << ", " << bounds);
0302
0303 const double uncert = calibrator().driftRadiusUncert(unsmearedR);
0304
0305 if (uncert <= std::numeric_limits<double>::epsilon()) {
0306 convertSp = false;
0307 break;
0308 }
0309 const double driftR =
0310 (*Digitization::Gauss{uncert}(unsmearedR, rndEngine)).first;
0311
0312 const auto& lBounds = static_cast<const LineBounds&>(bounds);
0313 const double maxR = lBounds.get(LineBounds::eR);
0314 const double maxZ = lBounds.get(LineBounds::eHalfLengthZ);
0315
0316 if (driftR < 0. || driftR > maxR || std::abs(nominalPos.z()) > maxZ) {
0317 convertSp = false;
0318 break;
0319 }
0320 if (auto insertItr =
0321 strawTimes.insert(std::make_pair(hitId, hit.time()));
0322 !insertItr.second) {
0323 if (hit.time() - insertItr.first->second > m_cfg.strawDeadTime) {
0324 insertItr.first->second = hit.time();
0325 } else {
0326 convertSp = false;
0327 break;
0328 }
0329 }
0330
0331 newSp.setRadius(driftR);
0332 newSp.setCovariance(square(0.5 * maxZ),
0333 calibrator().driftRadiusUncert(driftR), 0.);
0334 newSp.defineCoordinates(Vector3{parentTrf.translation()},
0335 Vector3{parentTrf.linear() * Vector3::UnitZ()},
0336 Vector3{parentTrf.linear() * Vector3::UnitX()});
0337 MuonId_t id{};
0338
0339 id.setChamber(MuonId_t::StationName::BIS,
0340 hit.position().z() > 0 ? MuonId_t::DetSide::A
0341 : MuonId_t::DetSide::C,
0342 1, MuonId_t::TechField::Mdt);
0343 id.setCoordFlags(true, false);
0344 newSp.setId(id);
0345 break;
0346 }
0347
0348 default:
0349 convertSp = false;
0350 }
0351
0352 if (convertSp) {
0353 spacePointsPerChamber[toChamberId(hitId)].push_back(std::move(newSp));
0354 }
0355 }
0356
0357 for (auto& [volId, bucket] : spacePointsPerChamber) {
0358 std::ranges::sort(bucket,
0359 [](const MuonSpacePoint& a, const MuonSpacePoint& b) {
0360 return a.localPosition().z() < b.localPosition().z();
0361 });
0362 if (logger().doPrint(Logging::Level::VERBOSE)) {
0363 std::stringstream sstr{};
0364 for (const auto& spacePoint : bucket) {
0365 sstr << " *** " << spacePoint << std::endl;
0366 }
0367 ACTS_VERBOSE("Safe " << bucket.size() << " space points for chamber "
0368 << volId << "\n"
0369 << sstr.str());
0370 }
0371 visualizeBucket(ctx, gctx, bucket);
0372 outSpacePoints.push_back(std::move(bucket));
0373 }
0374 m_outputSpacePoints(ctx, std::move(outSpacePoints));
0375
0376 return ProcessCode::SUCCESS;
0377 }
0378
0379 RangeXD<2, double> MuonSpacePointDigitizer::canvasRanges(
0380 const MuonSpacePointBucket& bucket) const {
0381 RangeXD<2, double> ranges{
0382 filledArray<double, 2>(std::numeric_limits<double>::max()),
0383 filledArray<double, 2>(-std::numeric_limits<double>::max())};
0384
0385
0386 constexpr double extra = 3._cm;
0387 for (const auto& sp : bucket) {
0388 const Vector3& pos = sp.localPosition();
0389 ranges.expand(0, pos.z() - extra, pos.z() + extra);
0390 ranges.expand(1, pos.y() - extra, pos.y() + extra);
0391 }
0392 return ranges;
0393 }
0394
0395 bool MuonSpacePointDigitizer::isSurfaceToDraw(
0396 const Acts::GeometryContext& gctx, const Surface& surface,
0397 const RangeXD<2, double>& canvasBoundaries) const {
0398
0399 if (surface.associatedDetectorElement() == nullptr) {
0400 return false;
0401 }
0402
0403 const Vector3 pos =
0404 toSpacePointFrame(gctx, surface.geometryId()).translation();
0405 const auto& bounds = surface.bounds();
0406
0407 if (surface.type() == Surface::SurfaceType::Plane) {
0408 const double hL{halfHeight(bounds)};
0409
0410 const double minZ = std::max(pos.z() - hL, canvasBoundaries.min(0));
0411 const double maxZ = std::min(pos.z() + hL, canvasBoundaries.max(0));
0412
0413
0414 return maxZ > pos.z() - hL && minZ < pos.z() + hL &&
0415 pos.y() > canvasBoundaries.min(1) &&
0416 pos.y() < canvasBoundaries.max(1);
0417 } else if (surface.type() == Surface::SurfaceType::Straw) {
0418 const double r = static_cast<const LineBounds&>(bounds).get(LineBounds::eR);
0419
0420 return pos.y() - r > canvasBoundaries.min(1) &&
0421 pos.y() + r < canvasBoundaries.max(1) &&
0422 pos.z() - r > canvasBoundaries.min(0) &&
0423 pos.z() + r < canvasBoundaries.max(0);
0424 }
0425
0426 return false;
0427 }
0428 void MuonSpacePointDigitizer::visualizeBucket(
0429 const AlgorithmContext& ctx, const GeometryContext& gctx,
0430 const MuonSpacePointBucket& bucket) const {
0431 if (!m_cfg.dumpVisualization) {
0432 return;
0433 }
0434 auto canvas = std::make_unique<TCanvas>("can", "can", 600, 600);
0435 canvas->cd();
0436
0437 const GeometryIdentifier chambId = toChamberId(bucket.front().geometryId());
0438
0439 std::vector<std::unique_ptr<TObject>> primitives{};
0440
0441 const RangeXD<2, double> canvasBound{canvasRanges(bucket)};
0442
0443 auto frame = std::make_unique<TH2I>("frame", "frame;z [mm];y [mm]", 1,
0444 canvasBound.min(0), canvasBound.max(0), 1,
0445 canvasBound.min(1), canvasBound.max(1));
0446 frame->Draw("AXIS");
0447
0448
0449
0450 const TrackingVolume* chambVolume = trackingGeometry().findVolume(chambId);
0451 assert(chambVolume != nullptr);
0452 chambVolume->apply(overloaded{
0453 [this, &canvasBound, &gctx, &primitives](const Surface& surface) {
0454 if (!isSurfaceToDraw(gctx, surface, canvasBound)) {
0455 return;
0456 }
0457 const Vector3 pos =
0458 toSpacePointFrame(gctx, surface.geometryId()).translation();
0459 const auto& bounds = surface.bounds();
0460 if (surface.type() == Surface::SurfaceType::Plane) {
0461 const double hL{halfHeight(bounds)};
0462 const double minZ = std::max(pos.z() - hL, canvasBound.min(0));
0463 const double maxZ = std::min(pos.z() + hL, canvasBound.max(0));
0464 primitives.push_back(drawBox(0.5 * (minZ + maxZ), pos.y(),
0465 maxZ - minZ, 0.3_cm, kBlack, 0));
0466
0467 } else if (surface.type() == Surface::SurfaceType::Straw) {
0468 const double r =
0469 static_cast<const LineBounds&>(bounds).get(LineBounds::eR);
0470 primitives.push_back(drawCircle(pos.z(), pos.y(), r, kBlack, 0));
0471 }
0472 }});
0473
0474 for (auto& sp : bucket) {
0475 const Vector3& pos = sp.localPosition();
0476 if (sp.isStraw()) {
0477 primitives.push_back(
0478 drawCircle(pos.z(), pos.y(), sp.driftRadius(), kRed, 0));
0479 } else {
0480 primitives.push_back(
0481 drawBox(pos.z(), pos.y(), 3._cm, 0.5_cm, kRed, 1001));
0482 }
0483 }
0484
0485 const SimHitContainer& gotSimHits = m_inputSimHits(ctx);
0486 const SimParticleContainer& simParticles = m_inputParticles(ctx);
0487 for (const auto& simHit : gotSimHits) {
0488 if (chambId != toChamberId(simHit.geometryId())) {
0489 continue;
0490 }
0491 const auto simPartItr = simParticles.find(simHit.particleId());
0492 if (simPartItr == simParticles.end() ||
0493 (*simPartItr).hypothesis() != ParticleHypothesis::muon()) {
0494 continue;
0495 }
0496 const auto toSpTrf = toSpacePointFrame(gctx, simHit.geometryId()) *
0497 trackingGeometry()
0498 .findSurface(simHit.geometryId())
0499 ->transform(gctx)
0500 .inverse();
0501 const Vector3 pos = toSpTrf * simHit.position();
0502 const Vector3 dir = toSpTrf.linear() * simHit.direction();
0503 constexpr double arrowL = 1._cm;
0504 const Vector3 start = pos - 0.5 * arrowL * dir;
0505 const Vector3 end = pos + 0.5 * arrowL * dir;
0506 auto arrow =
0507 std::make_unique<TArrow>(start.z(), start.y(), end.z(), end.y(), 0.03);
0508 arrow->SetLineColor(kBlue + 1);
0509 arrow->SetLineWidth(1);
0510 arrow->SetLineStyle(kSolid);
0511 primitives.push_back(std::move(arrow));
0512 }
0513
0514 for (auto& prim : primitives) {
0515 prim->Draw();
0516 }
0517 canvas->SaveAs(
0518 std::format("Event_{}_{}.pdf", ctx.eventNumber, chambVolume->volumeName())
0519 .c_str());
0520 }
0521
0522 }