Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
CylindricalDetectorHelper.cpp
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file CylindricalDetectorHelper.cpp
1 // This file is part of the Acts project.
2 //
3 // Copyright (C) 2023 CERN for the benefit of the Acts project
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 
10 
14 #include "Acts/Detector/Portal.hpp"
32 
33 #include <algorithm>
34 #include <cmath>
35 #include <cstddef>
36 #include <iterator>
37 #include <map>
38 #include <ostream>
39 #include <stdexcept>
40 #include <string>
41 #include <tuple>
42 #include <utility>
43 
44 // Indexing of the portals follows the generation order of portals in the
45 // CylinderVolumeBounds and BevelledCylinderVolumeBounds (latter for wrapping)
46 //
47 // In short:
48 //
49 // 0: index of the low-z disc bound
50 // 1: index of the high-z disc bound
51 // 2: index of the outer cylinder bound
52 //
53 // If the volume doesn't extend inward to inner radius=0:
54 // 3: index of the inner cylinder bound
55 //
56 // Cylindrical sectors have up to 6 portals, enumerated as follows:
57 // 0: index of the low-z disc sector bound
58 // 1: index of the high-z disc sector bound
59 // 2: index of the outer cylinder sector bound
60 //
61 // If no inner cylinder sector bound is present:
62 // 3: index of the low-phi sector bound
63 // 4: index of the high-phi sector bound
64 // If an inner cylinder sector bound exists, it takes index 3 and the phi sector
65 // bounds are shifted by one index: 3: index of the inner cylinder sector bound
66 // 4: index of the low-phi sector bound
67 // 5: index of the high-phi sector bound
68 //
69 
70 namespace {
71 
80 Acts::Experimental::PortalReplacement createDiscReplacement(
82  const std::vector<Acts::ActsScalar>& rBoundaries,
83  const std::vector<Acts::ActsScalar>& phiBoundaries, unsigned int index,
84  Acts::Direction dir) {
85  // Autodetector stitch value
86  Acts::BinningValue stitchValue =
87  phiBoundaries.size() == 2u ? Acts::binR : Acts::binPhi;
88  // Estimate ranges
89  auto [minR, maxR] = Acts::min_max(rBoundaries);
90  auto [sectorPhi, avgPhi] = Acts::range_medium(phiBoundaries);
91 
92  // Transform and bounds
93  auto bounds =
94  std::make_unique<Acts::RadialBounds>(minR, maxR, 0.5 * sectorPhi, avgPhi);
95  // A new surface on the negative side over the full range
96  auto surface = Acts::Surface::makeShared<Acts::DiscSurface>(
98  // Make a portal and indicate the new link direction
99  const auto& stitchBoundaries =
100  (stitchValue == Acts::binR) ? rBoundaries : phiBoundaries;
102  Acts::Experimental::Portal::makeShared(surface), index, dir,
103  stitchBoundaries, stitchValue);
104 }
105 
116 Acts::Experimental::PortalReplacement createCylinderReplacement(
117  const Acts::Transform3& transform, Acts::ActsScalar r,
118  const std::vector<Acts::ActsScalar>& zBoundaries,
119  const std::vector<Acts::ActsScalar>& phiBoundaries, unsigned int index,
120  Acts::Direction dir) {
121  // Autodetector stitch value
122  Acts::BinningValue stitchValue =
123  phiBoundaries.size() == 2u ? Acts::binZ : Acts::binPhi;
124  auto [lengthZ, medZ] = Acts::range_medium(zBoundaries);
125  auto [sectorPhi, avgPhi] = Acts::range_medium(phiBoundaries);
126 
127  // New bounds, with current length and sector values
128  auto bounds = std::make_unique<Acts::CylinderBounds>(r, 0.5 * lengthZ,
129  0.5 * sectorPhi, avgPhi);
130  // A new surface on the negative side over the full range
131  auto surface = Acts::Surface::makeShared<Acts::CylinderSurface>(
133 
134  // A make a portal and indicate the new link direction
135  const auto& stitchBoundaries =
136  (stitchValue == Acts::binZ) ? zBoundaries : phiBoundaries;
138  Acts::Experimental::Portal::makeShared(surface), index, dir,
139  stitchBoundaries, stitchValue);
140 }
141 
152 Acts::Experimental::PortalReplacement createSectorReplacement(
153  const Acts::GeometryContext& gctx, const Acts::Vector3& volumeCenter,
154  const Acts::Surface& refSurface,
155  const std::vector<Acts::ActsScalar>& boundaries, Acts::BinningValue binning,
156  unsigned int index, Acts::Direction dir) {
157  // Get a reference transform
158  const auto& refTransform = refSurface.transform(gctx);
159  auto refRotation = refTransform.rotation();
160  // Bounds handling
161  const auto& boundValues = refSurface.bounds().values();
162  std::unique_ptr<Acts::PlanarBounds> bounds = nullptr;
163 
164  // Create a new transform
165  Acts::Transform3 transform = Acts::Transform3::Identity();
166  if (binning == Acts::binR) {
167  // Range and center-r calculation
168  auto [range, medium] = Acts::range_medium(boundaries);
169  // New joint center:
170  // - you start from the center of the volume, and then head the distence
171  // of medium-r along the y-axis of the former (and new) portal
172  Acts::Vector3 pCenter = volumeCenter + medium * refRotation.col(1u);
173  transform.pretranslate(pCenter);
174  // Create the halflength
176  0.5 * (boundValues[Acts::RectangleBounds::BoundValues::eMaxX] -
177  boundValues[Acts::RectangleBounds::BoundValues::eMinX]);
178  // New joint bounds
179  bounds = std::make_unique<Acts::RectangleBounds>(halfX, 0.5 * range);
180  } else if (binning == Acts::binZ) {
181  // Range and medium z alculation
182  auto [range, medium] = Acts::range_medium(boundaries);
183  // Center R calculation, using projection onto vector
184  const auto& surfaceCenter = refSurface.center(gctx);
185  Acts::Vector3 centerDiffs = (surfaceCenter - volumeCenter);
186  Acts::ActsScalar centerR = centerDiffs.dot(refRotation.col(2));
187  // New joint center
188  Acts::Vector3 pCenter = volumeCenter + centerR * refRotation.col(2);
189  transform.pretranslate(pCenter);
190  // New joint bounds
192  0.5 * (boundValues[Acts::RectangleBounds::BoundValues::eMaxY] -
193  boundValues[Acts::RectangleBounds::BoundValues::eMinY]);
194  bounds = std::make_unique<Acts::RectangleBounds>(0.5 * range, halfY);
195  }
196  // Set the rotation
197  transform.prerotate(refRotation);
198  // A new surface on the negative side over the full range
199  auto surface = Acts::Surface::makeShared<Acts::PlaneSurface>(
200  transform, std::move(bounds));
201  // A make a portal and indicate the new link direction
203  Acts::Experimental::Portal::makeShared(surface), index, dir, boundaries,
204  binning};
205  return pRep;
206 }
207 
215 std::map<unsigned int,
216  std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>>
217 stripSideVolumes(
218  const std::vector<Acts::Experimental::DetectorComponent::PortalContainer>&
219  containers,
220  const std::vector<unsigned int>& sides,
221  const std::vector<unsigned int>& selectedOnly = {},
223  ACTS_LOCAL_LOGGER(Acts::getDefaultLogger("::stripSideVolumes", logLevel));
224 
225  // These are the stripped off outside volumes
226  std::map<unsigned int,
227  std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>>
228  sideVolumes;
229 
230  // Principle sides and selected sides, make an intersection
231  std::vector<unsigned int> selectedSides;
232  if (not selectedOnly.empty()) {
233  std::set_intersection(sides.begin(), sides.end(), selectedOnly.begin(),
234  selectedOnly.end(),
235  std::back_inserter(selectedSides));
236  } else {
237  selectedSides = sides;
238  }
239 
240  // Loop through the containers
241  for (const auto& pc : containers) {
242  // Loop through the selected sides and check if they are contained
243  for (const auto& s : selectedSides) {
244  auto cSide = pc.find(s);
245  if (cSide != pc.end()) {
246  auto p = cSide->second;
247  auto& sVolumes = sideVolumes[s];
248  auto aVolumes =
250  *p);
251  sVolumes.insert(sVolumes.end(), aVolumes.begin(), aVolumes.end());
252  }
253  }
254  }
255  // return them
256  return sideVolumes;
257 }
258 
269 void checkAlignment(
270  const Acts::GeometryContext& gctx,
271  const std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>&
272  volumes) {
273  // Take first transform as reference transform
274  auto refRotation = volumes[0u]->transform(gctx).rotation();
275  // Loop over rest and recursively test
276  for (auto [iv, v] : Acts::enumerate(volumes)) {
277  if (iv > 0) {
278  auto curRotation = v->transform(gctx).rotation();
279  if (not curRotation.isApprox(refRotation)) {
280  std::string message = "CylindricalDetectorHelper: rotation of volume ";
281  message += std::to_string(iv);
282  message += std::string(" is not aligned with previous volume");
283  throw std::invalid_argument(message.c_str());
284  }
285  }
286  }
287 }
288 
295 void checkVolumes(
296  const Acts::GeometryContext& gctx,
297  const std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>&
298  volumes) {
299  // A minimal set of checks - exceptions are thrown
300  // - not enough volumes given
301  std::string message = "CylindricalDetectorHelper: ";
302  if (volumes.size() < 2u) {
303  message += std::string("not enough volume given (") +
304  std::to_string(volumes.size());
305  message += std::string(" ), when required >=2.");
306  throw std::invalid_argument(message.c_str());
307  }
308  // - null pointer detected or non-cylindrical volume detected
309  for (auto [iv, v] : Acts::enumerate(volumes)) {
310  // Check for nullptr
311  if (v == nullptr) {
312  message += std::string("nullptr detector instead of volume " +
313  std::to_string(iv));
314  throw std::invalid_argument(message.c_str());
315  }
316  // Check for cylindrical volume type
317  if (v->volumeBounds().type() != Acts::VolumeBounds::BoundsType::eCylinder) {
318  message +=
319  std::string("non-cylindrical volume bounds detected for volume " +
320  std::to_string(iv));
321  throw std::invalid_argument(message.c_str());
322  }
323  }
324  // Check the alignment of the volumes
325  checkAlignment(gctx, volumes);
326 }
327 
335 void checkBounds(
336  [[maybe_unused]] const Acts::GeometryContext& gctx,
337  const std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>&
338  volumes,
339  const std::vector<std::array<unsigned int, 2u>>& refCur) {
340  // Reference values
341  auto refValues = volumes[0u]->volumeBounds().values();
342  for (auto [iv, v] : Acts::enumerate(volumes)) {
343  if (iv > 0u) {
344  auto curValues = v->volumeBounds().values();
345  for (auto [r, c] : refCur) {
346  if (std::abs(refValues[r] - curValues[c]) >
348  std::string message = "CylindricalDetectorHelper: '";
349  message += volumes[iv - 1]->name();
350  if (r != c) {
351  message += "' does not attach to '";
352  } else {
353  message += "' does not match with '";
354  }
355  message += volumes[iv]->name();
356  message += "\n";
357  message += " - at bound values ";
358  message += std::to_string(refValues[r]);
359  message += " / " + std::to_string(curValues[c]);
360  throw std::runtime_error(message.c_str());
361  }
362  }
363  refValues = curValues;
364  }
365  }
366 }
367 
368 } // namespace
369 
372  const GeometryContext& gctx,
373  std::vector<std::shared_ptr<Experimental::DetectorVolume>>& volumes,
374  const std::vector<unsigned int>& selectedOnly,
375  Acts::Logging::Level logLevel) {
376  // Basic checks for eligability of the volumes
377  checkVolumes(gctx, volumes);
378  // Check for bounds values of volumes (i) and (i+1) succesively for
379  // compatibility:
380  // - check outer (1u) of volume (i) vs inner radius (0u) of volume (i+1)
381  // - phi sector (3u) and average phi (4u) between volumes (i), (i+1)
382  std::vector<std::array<unsigned int, 2u>> checks = {
383  {1u, 0u}, {3u, 3u}, {4u, 4u}};
384  // - And we check the half length if it is not a selective attachment
385  if (selectedOnly.empty()) {
386  checks.push_back({2u, 2u});
387  }
388  checkBounds(gctx, volumes, checks);
389 
390  // The local logger
391  ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
392 
393  ACTS_DEBUG("Connect " << volumes.size() << " detector volumes in R.");
394 
395  // Return proto container
396  DetectorComponent::PortalContainer dShell;
397 
398  // Innermost volume boundaries
399  std::vector<ActsScalar> rBoundaries = {};
400  auto refValues = volumes[0u]->volumeBounds().values();
401 
402  // Reference boundary values
403  rBoundaries.push_back(refValues[CylinderVolumeBounds::BoundValues::eMinR]);
404  rBoundaries.push_back(refValues[CylinderVolumeBounds::BoundValues::eMaxR]);
405 
406  // Connect in R ? (2u is the index of the outer cylinder)
407  bool connectR = selectedOnly.empty() or
408  std::find(selectedOnly.begin(), selectedOnly.end(), 2u) !=
409  selectedOnly.end();
410 
411  // Get phi sector and average phi
412  ActsScalar phiSector =
413  refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
414  ActsScalar avgPhi = refValues[CylinderVolumeBounds::BoundValues::eAveragePhi];
415 
416  // Fuse the cylinders - portals can be reused for this operation
417  for (unsigned int iv = 1; iv < volumes.size(); ++iv) {
418  refValues = volumes[iv]->volumeBounds().values();
419  // Keep on collecting the outside maximum r for the overall r boundaries
420  rBoundaries.push_back(refValues[CylinderVolumeBounds::BoundValues::eMaxR]);
421  // Only connect if configured to do so
422  if (connectR) {
423  ACTS_VERBOSE("Connect volume '" << volumes[iv - 1]->name() << "' to "
424  << volumes[iv]->name() << "'.");
425 
426  // When fusing volumes at a cylinder boundary, we *keep* one
427  // portal and transfer the portal link information from the other
428  //
429  // In this case the outer cylinder portal of the inner volume is kept,
430  // the inner cylinder of the outer portal goes to waste
431  auto& keepCylinder = volumes[iv - 1]->portalPtrs()[2u];
432  auto& wasteCylinder = volumes[iv]->portalPtrs()[3u];
433  keepCylinder->fuse(wasteCylinder);
434  volumes[iv]->updatePortal(keepCylinder, 3u);
435  }
436  }
437 
438  // Proto container setting
439  if (connectR) {
440  // The number of portals indicate again if the inner is present or not,
441  if (volumes[0u]->portals().size() == 4u or
442  volumes[0u]->portals().size() == 6u) {
443  dShell[3u] = volumes[0u]->portalPtrs()[3u];
444  }
445  dShell[2u] = volumes[volumes.size() - 1u]->portalPtrs()[2u];
446  }
447 
448  // Check if sectors are present by the number of portals, check is done on the
449  // outermost volume as this is required to have an inner cylinder, and hence
450  // no offset needs to be respected
451  bool sectorsPresent = volumes[volumes.size() - 1u]->portals().size() > 4u;
452 
453  // A portal replacement, it comprises of the portal, the index, the
454  // direction, the binning and bins
455  std::vector<PortalReplacement> pReplacements = {};
456 
457  // Disc assignments are forward for negative disc, backward for positive
458  std::vector<Acts::Direction> discDirs = {Acts::Direction::Forward,
460  for (const auto [iu, idir] : enumerate(discDirs)) {
461  if (selectedOnly.empty() or
462  std::find(selectedOnly.begin(), selectedOnly.end(), iu) !=
463  selectedOnly.end()) {
464  const Surface& refSurface = volumes[0u]->portals()[iu]->surface();
465  const Transform3& refTransform = refSurface.transform(gctx);
466  pReplacements.push_back(createDiscReplacement(
467  refTransform, rBoundaries, {avgPhi - phiSector, avgPhi + phiSector},
468  iu, idir));
469  }
470  }
471 
472  // If volume sectors are present, these have to be respected
473  if (sectorsPresent) {
474  ACTS_VERBOSE("Sector planes are present, they need replacement.");
475  // Sector assignments are forward backward
476  std::vector<Acts::Direction> sectorDirs = {Acts::Direction::Forward,
478  Acts::Vector3 vCenter = volumes[0u]->transform(gctx).translation();
479  for (const auto [iu, idir] : enumerate(sectorDirs)) {
480  // (iu + 4u) corresponds to the indices of the phi-low and phi-high sector
481  // planes.
482  if (selectedOnly.empty() or
483  std::find(selectedOnly.begin(), selectedOnly.end(), iu + 4u) !=
484  selectedOnly.end()) {
485  // As it is r-wrapping, the inner tube is guaranteed
486  const Surface& refSurface =
487  volumes[volumes.size() - 1u]->portals()[iu + 4u]->surface();
488  pReplacements.push_back(createSectorReplacement(
489  gctx, vCenter, refSurface, rBoundaries, Acts::binR, iu + 4u, idir));
490  }
491  }
492  } else {
493  ACTS_VERBOSE(
494  "No sector planes present, full 2 * M_PI cylindrical geometry.");
495  }
496 
497  // Attach the new volume multi links
499 
500  // Exchange the portals of the volumes
501  ACTS_VERBOSE("Portals of " << volumes.size() << " volumes need updating.");
502  // Exchange the portals of the volumes
503  for (auto& iv : volumes) {
504  ACTS_VERBOSE("- update portals of volume '" << iv->name() << "'.");
505  for (auto& [p, i, dir, boundaries, binning] : pReplacements) {
506  // Fill the map
507  dShell[i] = p;
508 
509  // Potential offset for tube vs/ cylinder
510  // - if the volume doesn't have an inner portal, indices need to
511  // be shifted by -1 to update the correct index, that's the case for
512  // size 3 and 5 for portals
513  size_t nPortals = iv->portals().size();
514  bool innerPresent = (nPortals == 3u or nPortals == 5u);
515  int iOffset = (innerPresent and i > 2u) ? -1 : 0;
516  ACTS_VERBOSE("-- update portal with index "
517  << i + iOffset << " (including offset " << iOffset << ")");
518  iv->updatePortal(p, static_cast<unsigned int>(i + iOffset));
519  }
520  }
521  // Done.
522  return dShell;
523 }
524 
527  const Acts::GeometryContext& gctx,
528  std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>& volumes,
529  const std::vector<unsigned int>& selectedOnly,
530  Acts::Logging::Level logLevel) {
531  // Basic checks for eligability of the volumes
532  checkVolumes(gctx, volumes);
533  // Check for bounds compatibility
534  // We check phi sector (3u) and average phi (4u)
535  std::vector<std::array<unsigned int, 2u>> checks = {{3u, 3u}, {4u, 4u}};
536  // And we check the inner radius [0u], outer radius[1u] if it is not a
537  // selective attachment
538  if (selectedOnly.empty()) {
539  checks.push_back({0u, 0u});
540  checks.push_back({1u, 1u});
541  }
542  checkBounds(gctx, volumes, checks);
543 
544  // The local logger
545  ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
546 
547  ACTS_DEBUG("Connect " << volumes.size() << " detector volumes in Z.");
548 
549  // Return proto container
550  DetectorComponent::PortalContainer dShell;
551 
552  // Connect in Z ?
553  // - 1u corresponds to the index of the high-z disc portal for the reference
554  // volume.
555  bool connectZ = selectedOnly.empty() or
556  std::find(selectedOnly.begin(), selectedOnly.end(), 1u) !=
557  selectedOnly.end();
558  // Reference z axis
559  const auto rotation = volumes[0u]->transform(gctx).rotation();
560 
561  std::vector<Vector3> zBoundaries3D = {};
562 
567  auto addZboundary3D = [&](const Experimental::DetectorVolume& volume,
568  int side) -> void {
569  const auto boundValues = volume.volumeBounds().values();
570  ActsScalar halflengthZ =
571  boundValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ];
572  zBoundaries3D.push_back(volume.transform(gctx).translation() +
573  side * halflengthZ * rotation.col(2));
574  };
575 
576  // Fuse the discs - portals can be reused
577  addZboundary3D(*volumes[0u].get(), -1);
578  addZboundary3D(*volumes[0u].get(), 1);
579  for (unsigned int iv = 1; iv < volumes.size(); ++iv) {
580  // Add the z boundary
581  addZboundary3D(*volumes[iv].get(), 1u);
582  // Do the connection
583  if (connectZ) {
584  ACTS_VERBOSE("Connect volume '" << volumes[iv - 1]->name() << "' to "
585  << volumes[iv]->name() << "'.");
586  // When fusing, one portal survives (keep) and gets the
587  // portal linking from the waste transferred
588  //
589  // In this case we keep the disc at positive z of the volume
590  // at lower relative z, and trash the disc at negative z of the
591  // following volume
592  auto& keepDisc = volumes[iv - 1]->portalPtrs()[1u];
593  auto& wasteDisc = volumes[iv]->portalPtrs()[0u];
594  // Throw an exception if the discs are not at the same position
595  Vector3 keepPosition = keepDisc->surface().center(gctx);
596  Vector3 wastePosition = wasteDisc->surface().center(gctx);
597  if (not keepPosition.isApprox(wastePosition)) {
598  std::string message = "CylindricalDetectorHelper: '";
599  message += volumes[iv - 1]->name();
600  message += "' does not attach to '";
601  message += volumes[iv]->name();
602  message += "\n";
603  message += " - along z with values ";
604  message += Acts::toString(keepPosition);
605  message += " / " + Acts::toString(wastePosition);
606  throw std::runtime_error(message.c_str());
607  }
608  keepDisc->fuse(wasteDisc);
609  volumes[iv]->updatePortal(keepDisc, 0u);
610  }
611  }
612 
613  // Register what we have from the container
614  if (connectZ) {
615  dShell[0u] = volumes[0u]->portalPtrs()[0u];
616  dShell[1u] = volumes[volumes.size() - 1u]->portalPtrs()[1u];
617  }
618 
619  // Centre of gravity
620  Vector3 combinedCenter =
621  0.5 * (zBoundaries3D[zBoundaries3D.size() - 1u] + zBoundaries3D[0u]);
622 
623  ACTS_VERBOSE("New combined center calculated at "
624  << toString(combinedCenter));
625 
626  // Evaluate the series of z boundaries
627  std::vector<ActsScalar> zBoundaries = {};
628  for (const auto& zb3D : zBoundaries3D) {
629  auto proj3D = (zb3D - combinedCenter).dot(rotation.col(2));
630  ActsScalar zBoundary =
631  std::copysign((zb3D - combinedCenter).norm(), proj3D);
632  zBoundaries.push_back(zBoundary);
633  }
634 
635  Transform3 combinedTransform = Transform3::Identity();
636  combinedTransform.pretranslate(combinedCenter);
637  combinedTransform.rotate(rotation);
638 
639  // Get phi sector and average phi
640  const auto& refVolume = volumes[0u];
641  const auto refValues = refVolume->volumeBounds().values();
642 
643  // Get phi sector and average phi
644  ActsScalar minR = refValues[CylinderVolumeBounds::BoundValues::eMinR];
645  ActsScalar maxR = refValues[CylinderVolumeBounds::BoundValues::eMaxR];
646  ActsScalar phiSector =
647  refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
648  ActsScalar avgPhi = refValues[CylinderVolumeBounds::BoundValues::eAveragePhi];
649 
650  // Check if inner cylinder and sectors are present by the number of portals
651  size_t nPortals = volumes[volumes.size() - 1u]->portals().size();
652  bool innerPresent = (nPortals != 3u and nPortals != 5u);
653  bool sectorsPresent = nPortals > 4u;
654 
655  // A portal replacement, it comprises of the portal, the index, the
656  // direction, the binning and bins
657  std::vector<PortalReplacement> pReplacements = {};
658 
659  // Disc assignments are forward for negative disc, backward for positive
660  std::vector<Acts::Direction> cylinderDirs = {Acts::Direction::Backward};
661  // Cylinder radii
662  std::vector<Acts::ActsScalar> cylinderR = {maxR};
663  if (innerPresent) {
664  ACTS_VERBOSE("Inner surface present, tube geometry detected.");
665  cylinderDirs.push_back(Direction::Forward);
666  cylinderR.push_back(minR);
667  } else {
668  ACTS_VERBOSE("No inner surface present, solid cylinder geometry detected.");
669  }
670  // Tube/cylinder offset
671  unsigned int iSecOffset = innerPresent ? 4u : 3u;
672  // Prepare the cylinder replacements
673  for (const auto [iu, idir] : enumerate(cylinderDirs)) {
674  if (selectedOnly.empty() or
675  std::find(selectedOnly.begin(), selectedOnly.end(), iu + 2u) !=
676  selectedOnly.end()) {
677  pReplacements.push_back(createCylinderReplacement(
678  combinedTransform, cylinderR[iu], zBoundaries,
679  {avgPhi - phiSector, avgPhi + phiSector}, iu + 2u, idir));
680  }
681  }
682 
683  // Prepare the sector side replacements
684  if (sectorsPresent) {
685  ACTS_VERBOSE("Sector planes are present, they need replacement.");
686  // Sector assignmenta are forward backward
687  std::vector<Acts::Direction> sectorDirs = {Acts::Direction::Forward,
689  for (const auto [iu, idir] : enumerate(sectorDirs)) {
690  // Access with 3u or 4u but always write 4u (to be caught later)
691  if (selectedOnly.empty() or
692  std::find(selectedOnly.begin(), selectedOnly.end(), iu + 4u) !=
693  selectedOnly.end()) {
694  const Surface& refSurface =
695  volumes[0u]->portals()[iu + iSecOffset]->surface();
696  pReplacements.push_back(
697  createSectorReplacement(gctx, combinedCenter, refSurface,
698  zBoundaries, Acts::binZ, iu + 4u, idir));
699  }
700  }
701  } else {
702  ACTS_VERBOSE(
703  "No sector planes present, full 2 * M_PI cylindrical geometry.");
704  }
705 
706  // Attach the new volume multi links
707  PortalHelper::attachDetectorVolumeUpdators(gctx, volumes, pReplacements);
708 
709  // Exchange the portals of the volumes
710  ACTS_VERBOSE("Portals of " << volumes.size() << " volumes need updating.");
711  for (auto& iv : volumes) {
712  ACTS_VERBOSE("- update portals of volume '" << iv->name() << "'.");
713  for (auto& [p, i, dir, boundaries, binning] : pReplacements) {
714  // Potential offset for tube vs/ cylinder
715  // if the volume doesn't have an inner portal, indices need to be shifted
716  // by -1 to update the correct index.
717  int iOffset = (i > 2u and not innerPresent) ? -1 : 0;
718  ACTS_VERBOSE("-- update portal with index " << i);
719  iv->updatePortal(p, static_cast<unsigned int>(i + iOffset));
720  // Fill the map
721  dShell[i] = p;
722  }
723  }
724  // Done.
725  return dShell;
726 }
727 
730  const Acts::GeometryContext& gctx,
731  std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>& volumes,
732  const std::vector<unsigned int>& /*selectedOnly*/,
733  Acts::Logging::Level logLevel) {
734  // Basic checks for eligability of the volumes
735  checkVolumes(gctx, volumes);
736 
737  // The local logger
738  ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
739 
740  ACTS_DEBUG("Connect " << volumes.size() << " detector volumes in phi.");
741 
742  // Return proto container
743  DetectorComponent::PortalContainer dShell;
744 
745  // Check if inner cylinder and sectors are present by the number of portals
746  size_t nPortals = volumes[volumes.size() - 1u]->portals().size();
747  bool innerPresent = (nPortals != 3u and nPortals != 5u);
748 
749  Transform3 refTransform = volumes[0u]->transform(gctx);
750 
751  // Sector offset
752  unsigned int iSecOffset = innerPresent ? 4u : 3u;
753  std::vector<ActsScalar> phiBoundaries = {};
754  auto refValues = volumes[0u]->volumeBounds().values();
755  phiBoundaries.push_back(
756  refValues[CylinderVolumeBounds::BoundValues::eAveragePhi] -
757  refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector]);
758  phiBoundaries.push_back(
759  refValues[CylinderVolumeBounds::BoundValues::eAveragePhi] +
760  refValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector]);
761  // Fuse the sectors - portals can be reused for this operation
762  for (unsigned int iv = 1; iv < volumes.size(); ++iv) {
763  ACTS_VERBOSE("Connect volume '" << volumes[iv - 1]->name() << "' to "
764  << volumes[iv]->name() << "'.");
765 
766  // Fuse and swap
767  auto& keepSector = volumes[iv - 1]->portalPtrs()[iSecOffset + 1u];
768  auto& wasteSector = volumes[iv]->portalPtrs()[iSecOffset];
769  keepSector->fuse(wasteSector);
770  volumes[iv]->updatePortal(keepSector, iSecOffset);
771  // The current values
772  auto curValues = volumes[iv]->volumeBounds().values();
773  // Bail out if they do not match
774  ActsScalar lowPhi =
775  curValues[CylinderVolumeBounds::BoundValues::eAveragePhi] -
776  curValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
777  ActsScalar highPhi =
778  curValues[CylinderVolumeBounds::BoundValues::eAveragePhi] +
779  curValues[CylinderVolumeBounds::BoundValues::eHalfPhiSector];
780  // Check phi attachment
781  if (std::abs(phiBoundaries[phiBoundaries.size() - 1u] - lowPhi) >
783  std::string message = "CylindricalDetectorHelper: '";
784  message += volumes[iv - 1]->name();
785  message += "' does not attach to '";
786  message += volumes[iv]->name();
787  message += "\n";
788  message += " - within phi sectors ";
789  message += std::to_string(lowPhi);
790  message +=
791  " / " + std::to_string(phiBoundaries[phiBoundaries.size() - 1u]);
792  throw std::runtime_error(message.c_str());
793  }
794  // Check radial and longitudinal compatibility - @TODO
795  phiBoundaries.push_back(highPhi);
796  // Recursive setting of the values
797  refValues = curValues;
798  }
799 
800  // A portal replacement, it comprises of the portal, the index, the
801  // direction, the binning and bins
802  std::vector<PortalReplacement> pReplacements = {};
803  // Negative disc
804  pReplacements.push_back(createDiscReplacement(
805  refTransform,
806  {refValues[CylinderVolumeBounds::BoundValues::eMinR],
807  refValues[CylinderVolumeBounds::BoundValues::eMaxR]},
808  phiBoundaries, 0u, Acts::Direction::Forward));
809 
810  // Positive disc
811  pReplacements.push_back(createDiscReplacement(
812  refTransform,
813  {refValues[CylinderVolumeBounds::BoundValues::eMinR],
814  refValues[CylinderVolumeBounds::BoundValues::eMaxR]},
815  phiBoundaries, 1u, Acts::Direction::Backward));
816 
817  // Outside cylinder
818  pReplacements.push_back(createCylinderReplacement(
819  refTransform, refValues[CylinderVolumeBounds::BoundValues::eMaxR],
820  {-refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ],
821  refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ]},
822  phiBoundaries, 2u, Acts::Direction::Backward));
823 
824  // If the volume has a different inner radius than 0, it MUST have
825  // an inner cylinder
826  if (refValues[CylinderVolumeBounds::BoundValues::eMinR] > 0.) {
827  // Inner cylinder
828  pReplacements.push_back(createCylinderReplacement(
829  refTransform, refValues[CylinderVolumeBounds::BoundValues::eMinR],
830  {-refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ],
831  refValues[CylinderVolumeBounds::BoundValues::eHalfLengthZ]},
832  phiBoundaries, 3u, Acts::Direction::Forward));
833  }
834 
835  // Attach the new volume multi links
836  PortalHelper::attachDetectorVolumeUpdators(gctx, volumes, pReplacements);
837  // Exchange the portals of the volumes
838  ACTS_VERBOSE("Portals of " << volumes.size() << " volumes need updating.");
839  for (auto& iv : volumes) {
840  ACTS_VERBOSE("- update portals of volume '" << iv->name() << "'.");
841  for (auto& [p, i, dir, boundaries, binning] : pReplacements) {
842  ACTS_VERBOSE("-- update portal with index " << i);
843  iv->updatePortal(p, static_cast<unsigned int>(i));
844  // Fill the map
845  dShell[i] = p;
846  }
847  }
848 
849  // Done.
850  return dShell;
851 }
852 
855  const Acts::GeometryContext& gctx,
856  std::vector<std::shared_ptr<Acts::Experimental::DetectorVolume>>& volumes,
857  Acts::Logging::Level logLevel) {
858  // The local logger
859  ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
860 
861  ACTS_DEBUG("Wrapping volumes in Z-R.");
862 
863  // Minimal set of checks
864  if (volumes.size() != 2u) {
865  throw std::invalid_argument(
866  "Wrapping the detector volume requires exactly 2 volumes.");
867  }
868 
869  // Return the new container
870  DetectorComponent::PortalContainer dShell;
871 
872  // Keep the outer shells
873  dShell[0u] = volumes[1u]->portalPtrs()[0u];
874  dShell[1u] = volumes[1u]->portalPtrs()[1u];
875  dShell[2u] = volumes[1u]->portalPtrs()[2u];
876 
877  // Fuse outer cover of first with inner cylinder of wrapping volume
878  auto& keepCover = volumes[0u]->portalPtrs()[2u];
879  auto& wasteCover = volumes[1u]->portalPtrs()[3u];
880  keepCover->fuse(wasteCover);
881  volumes[1u]->updatePortal(keepCover, 3u);
882 
883  // Stitch sides - negative
884  auto& keepDiscN = volumes[1u]->portalPtrs()[4u];
885  auto& wasteDiscN = volumes[0u]->portalPtrs()[0u];
886  keepDiscN->fuse(wasteDiscN);
887  volumes[0u]->updatePortal(keepDiscN, 0u);
888 
889  // Stich sides - positive
890  auto& keepDiscP = volumes[0u]->portalPtrs()[1u];
891  auto& wasteDiscP = volumes[1u]->portalPtrs()[5u];
892  keepDiscP->fuse(wasteDiscP);
893  volumes[1u]->updatePortal(keepDiscP, 5u);
894 
895  // If needed, insert new cylinder
896  if (volumes[0u]->portalPtrs().size() == 4u and
897  volumes[1u]->portalPtrs().size() == 8u) {
898  // We need a new cylinder spanning over the entire inner tube
899  ActsScalar hlZ =
900  volumes[0u]
901  ->volumeBounds()
902  .values()[Acts::CylinderVolumeBounds::BoundValues::eHalfLengthZ];
903  ActsScalar HlZ =
904  volumes[1u]->volumeBounds().values()
905  [Acts::CutoutCylinderVolumeBounds::BoundValues::eHalfLengthZ];
906  ActsScalar innerR =
907  volumes[0u]
908  ->volumeBounds()
909  .values()[Acts::CylinderVolumeBounds::BoundValues::eMinR];
910  // Create the inner replacement
911  std::vector<PortalReplacement> pReplacements;
912  pReplacements.push_back(createCylinderReplacement(
913  volumes[0u]->transform(gctx), innerR, {-HlZ, -hlZ, hlZ, HlZ},
914  {-M_PI, M_PI}, 3u, Direction::Forward));
915  std::vector<std::shared_ptr<DetectorVolume>> zVolumes = {
916  volumes[1u], volumes[0u], volumes[1u]};
917  // Attach the new volume multi links
918  PortalHelper::attachDetectorVolumeUpdators(gctx, zVolumes, pReplacements);
919  auto& [p, i, dir, boundaries, binning] = pReplacements[0u];
920  // Update the portals
921  volumes[1u]->updatePortal(p, 6u);
922  volumes[0u]->updatePortal(p, 3u);
923  volumes[1u]->updatePortal(p, 7u);
924  // Inner skin
925  dShell[3u] = p;
926  }
927  // Done.
928  return dShell;
929 }
930 
933  const GeometryContext& gctx,
934  const std::vector<DetectorComponent::PortalContainer>& containers,
935  const std::vector<unsigned int>& selectedOnly,
936  Acts::Logging::Level logLevel) noexcept(false) {
937  // The local logger
938  ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
939 
940  ACTS_DEBUG("Connect " << containers.size() << " proto containers in R.");
941 
942  // Return the new container
944 
945  // Fuse the cylinders - portals can be reused for this operation
946  for (unsigned int ic = 1; ic < containers.size(); ++ic) {
947  auto& formerContainer = containers[ic - 1];
948  auto& currentContainer = containers[ic];
949  // Check and throw exception
950  if (formerContainer.find(2u) == formerContainer.end()) {
951  throw std::invalid_argument(
952  "CylindricalDetectorHelper: proto container has no outer cover, "
953  "can "
954  "not be connected in R");
955  }
956  if (currentContainer.find(3u) == currentContainer.end()) {
957  throw std::invalid_argument(
958  "CylindricalDetectorHelper: proto container has no inner cover, "
959  "can "
960  "not be connected in R");
961  }
962 
963  // Fuse and swap
964  std::shared_ptr<Portal> keepCylinder = containers[ic - 1].find(2u)->second;
965  std::shared_ptr<Portal> wasteCylinder = containers[ic].find(3u)->second;
966  keepCylinder->fuse(wasteCylinder);
967  for (auto& av : wasteCylinder->attachedDetectorVolumes()[1u]) {
968  av->updatePortal(keepCylinder, 3u);
969  }
970  }
971 
972  // Proto container refurbishment
973  if (containers[0u].find(3u) != containers[0u].end()) {
974  dShell[3u] = containers[0u].find(3u)->second;
975  }
976  dShell[2u] = containers[containers.size() - 1u].find(2u)->second;
977 
978  auto sideVolumes =
979  stripSideVolumes(containers, {0u, 1u, 4u, 5u}, selectedOnly, logLevel);
980 
981  for (auto [s, volumes] : sideVolumes) {
982  auto pR = connectInR(gctx, volumes, {s});
983  if (pR.find(s) != pR.end()) {
984  dShell[s] = pR.find(s)->second;
985  }
986  }
987 
988  // Done.
989  return dShell;
990 }
991 
994  const GeometryContext& gctx,
995  const std::vector<DetectorComponent::PortalContainer>& containers,
996  const std::vector<unsigned int>& selectedOnly,
997  Acts::Logging::Level logLevel) noexcept(false) {
998  // The local logger
999  ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
1000 
1001  ACTS_DEBUG("Connect " << containers.size() << " proto containers in Z.");
1002 
1003  // Return the new container
1005 
1006  for (unsigned int ic = 1; ic < containers.size(); ++ic) {
1007  auto& formerContainer = containers[ic - 1];
1008  auto& currentContainer = containers[ic];
1009  // Check and throw exception
1010  if (formerContainer.find(1u) == formerContainer.end()) {
1011  throw std::invalid_argument(
1012  "CylindricalDetectorHelper: proto container has no negative disc, "
1013  "can not be connected in Z");
1014  }
1015  if (currentContainer.find(0u) == currentContainer.end()) {
1016  throw std::invalid_argument(
1017  "CylindricalDetectorHelper: proto container has no positive disc, "
1018  "can not be connected in Z");
1019  }
1020  std::shared_ptr<Portal> keepDisc = formerContainer.find(1u)->second;
1021  std::shared_ptr<Portal> wasteDisc = currentContainer.find(0u)->second;
1022  keepDisc->fuse(wasteDisc);
1023  for (auto& av : wasteDisc->attachedDetectorVolumes()[1u]) {
1024  ACTS_VERBOSE("Update portal of detector volume '" << av->name() << "'.");
1025  av->updatePortal(keepDisc, 0u);
1026  }
1027  }
1028 
1029  // Proto container refurbishment
1030  dShell[0u] = containers[0u].find(0u)->second;
1031  dShell[1u] = containers[containers.size() - 1u].find(1u)->second;
1032 
1033  // Check if this is a tube or a cylinder container (check done on 1st)
1034  std::vector<unsigned int> nominalSides = {2u, 4u, 5u};
1035  if (containers[0u].find(3u) != containers[0u].end()) {
1036  nominalSides.push_back(3u);
1037  }
1038 
1039  // Strip the side volumes
1040  auto sideVolumes =
1041  stripSideVolumes(containers, nominalSides, selectedOnly, logLevel);
1042 
1043  ACTS_VERBOSE("There remain " << sideVolumes.size()
1044  << " side volume packs to be connected");
1045  for (auto [s, volumes] : sideVolumes) {
1046  ACTS_VERBOSE(" - connect " << volumes.size() << " at selected side " << s);
1047  auto pR = connectInZ(gctx, volumes, {s}, logLevel);
1048  if (pR.find(s) != pR.end()) {
1049  dShell[s] = pR.find(s)->second;
1050  }
1051  }
1052 
1053  // Done.
1054  return dShell;
1055 }
1056 
1059  [[maybe_unused]] const GeometryContext& gctx,
1060  [[maybe_unused]] const std::vector<DetectorComponent::PortalContainer>&
1061  containers,
1062  [[maybe_unused]] const std::vector<unsigned int>& selectedOnly,
1063  [[maybe_unused]] Acts::Logging::Level logLevel) noexcept(false) {
1064  throw std::invalid_argument(
1065  "CylindricalDetectorHelper: container connection in phi not implemented "
1066  "yet.");
1068  // Done.
1069  return dShell;
1070 }
1071 
1074  [[maybe_unused]] const GeometryContext& gctx,
1075  const std::vector<DetectorComponent::PortalContainer>& containers,
1076  Acts::Logging::Level logLevel) {
1077  if (containers.size() != 2u) {
1078  throw std::invalid_argument(
1079  "CylindricalDetectorHelper: wrapping must take exactly two "
1080  "containers.");
1081  }
1082 
1083  // The inner one is a container
1084  auto innerContainer = containers.front();
1085  // The outer one is a single volume represented as a container
1086  auto outerContainer = containers.back();
1087  std::shared_ptr<DetectorVolume> wrappingVolume = nullptr;
1088  for (auto [key, value] : outerContainer) {
1089  auto attachedVolumes = value->attachedDetectorVolumes();
1090  for (const auto& ava : attachedVolumes) {
1091  for (const auto& av : ava) {
1092  if (wrappingVolume == nullptr and av != nullptr) {
1093  wrappingVolume = av;
1094  } else if (wrappingVolume != nullptr and av != wrappingVolume) {
1095  throw std::invalid_argument(
1096  "CylindricalDetectorHelper: wrapping container must represent a "
1097  "single volume.");
1098  }
1099  }
1100  }
1101  }
1102  if (wrappingVolume == nullptr) {
1103  throw std::invalid_argument(
1104  "CylindricalDetectorHelper: wrapping volume could not be "
1105  "determined.");
1106  }
1107 
1108  // The local logger
1109  ACTS_LOCAL_LOGGER(getDefaultLogger("CylindricalDetectorHelper", logLevel));
1110 
1111  ACTS_DEBUG("Wrapping a container with volume `" << wrappingVolume->name()
1112  << "'.");
1113  // Return the new container
1114  DetectorComponent::PortalContainer dShell;
1115 
1116  // Keep the outer shells of the proto container
1117  dShell[0u] = wrappingVolume->portalPtrs()[0u];
1118  dShell[1u] = wrappingVolume->portalPtrs()[1u];
1119  dShell[2u] = wrappingVolume->portalPtrs()[2u];
1120 
1121  // Fuse outer cover of first with inner cylinder of wrapping volume
1122  auto& keepCover = innerContainer[2u];
1123  auto& wasteCover = wrappingVolume->portalPtrs()[3u];
1124  keepCover->fuse(wasteCover);
1125  wrappingVolume->updatePortal(keepCover, 3u);
1126 
1127  // Stitch sides - negative
1128  auto& keepDiscN = innerContainer[0u];
1129  auto& wasteDiscN = wrappingVolume->portalPtrs()[4u];
1130  keepDiscN->fuse(wasteDiscN);
1131  wrappingVolume->updatePortal(keepDiscN, 4u);
1132 
1133  // Stich sides - positive
1134  auto& keepDiscP = innerContainer[1u];
1135  auto& wasteDiscP = wrappingVolume->portalPtrs()[5u];
1136  keepDiscP->fuse(wasteDiscP);
1137  wrappingVolume->updatePortal(keepDiscP, 5u);
1138 
1139  // If inner stitching is necessary
1140  if (innerContainer.size() == 4u and
1141  wrappingVolume->portalPtrs().size() == 8u) {
1142  // Inner Container portal
1143  auto& centralSegment = innerContainer[3u];
1144  auto centralValues = centralSegment->surface().bounds().values();
1145  ActsScalar centralHalfLengthZ =
1146  centralValues[CylinderBounds::BoundValues::eHalfLengthZ];
1147  // The two segments
1148  auto& nSegment = wrappingVolume->portalPtrs()[6u];
1149  auto nValues = nSegment->surface().bounds().values();
1150  ActsScalar nHalfLengthZ =
1151  nValues[CylinderBounds::BoundValues::eHalfLengthZ];
1152  auto& pSegment = wrappingVolume->portalPtrs()[7u];
1153  auto pValues = pSegment->surface().bounds().values();
1154  ActsScalar pHalfLengthZ =
1155  pValues[CylinderBounds::BoundValues::eHalfLengthZ];
1156 
1157  auto sideVolumes = stripSideVolumes({innerContainer}, {3u}, {3u}, logLevel);
1158 
1159  // First the left volume sector
1160  std::vector<std::shared_ptr<DetectorVolume>> innerVolumes = {
1161  wrappingVolume->getSharedPtr()};
1162 
1163  std::vector<ActsScalar> zBoundaries = {
1164  -centralHalfLengthZ - 2 * nHalfLengthZ, centralHalfLengthZ};
1165  // Loop over side volume and register the z boundaries
1166  for (auto& svs : sideVolumes) {
1167  for (auto& v : svs.second) {
1168  ActsScalar hlZ = v->volumeBounds().values()[2u];
1169  zBoundaries.push_back(zBoundaries.back() + 2 * hlZ);
1170  innerVolumes.push_back(v);
1171  }
1172  }
1173  // Last the right volume sector
1174  zBoundaries.push_back(zBoundaries.back() + 2 * pHalfLengthZ);
1175  innerVolumes.push_back(wrappingVolume);
1176  }
1177 
1178  // Done.
1179  return dShell;
1180 }