Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-17 08:21:11

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #pragma once
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 #include "Acts/Utilities/AxisDefinitions.hpp"
0013 #include "Acts/Utilities/BinningType.hpp"
0014 #include "Acts/Utilities/Helpers.hpp"
0015 #include "Acts/Utilities/ProtoAxis.hpp"
0016 #include "Acts/Utilities/ThrowAssert.hpp"
0017 #include "Acts/Utilities/VectorHelpers.hpp"
0018 
0019 #include <algorithm>
0020 #include <cmath>
0021 #include <memory>
0022 #include <sstream>
0023 #include <string>
0024 #include <utility>
0025 #include <vector>
0026 
0027 namespace Acts {
0028 
0029 /// @class BinningData
0030 ///
0031 ///   This class holds all the data necessary for the bin calculation
0032 ///
0033 ///   phi has a very particular behaviour:
0034 ///   - there's the change around +/- PI
0035 ///
0036 ///   - it can be multiplicative or additive
0037 ///   multiplicative : each major bin has the same sub structure
0038 ///                    i.e. first binnning
0039 ///
0040 /// structure is equidistant
0041 ///   additive : sub structure replaces one bin (and one bin only)
0042 ///
0043 ///
0044 class BinningData {
0045  public:
0046   BinningType type{};        ///< binning type: equidistant, arbitrary
0047   BinningOption option{};    ///< binning option: open, closed
0048   AxisDirection binvalue{};  ///< axis direction: AxisX, AxisY, AxisZ, ...
0049   float min{};               ///< minimum value
0050   float max{};               ///< maximum value
0051   float step{};              ///< binning step
0052   bool zdim{};               ///< zero dimensional binning : direct access
0053 
0054   /// sub structure: describe some sub binning
0055   std::unique_ptr<const BinningData> subBinningData;
0056   /// sub structure: additive or multiplicative
0057   bool subBinningAdditive{};
0058 
0059   /// Constructor for 0D binning
0060   ///
0061   /// @param bValue is the axis direction AxisX, AxisY, etc.
0062   /// @param bMin is the minimum value
0063   /// @param bMax is the maximum value
0064   BinningData(AxisDirection bValue, float bMin, float bMax)
0065       : type(equidistant),
0066         option(open),
0067         binvalue(bValue),
0068         min(bMin),
0069         max(bMax),
0070         step((bMax - bMin)),
0071         zdim(true),
0072         subBinningData(nullptr),
0073         m_bins(1),
0074         m_boundaries({{min, max}}),
0075         m_totalBins(1),
0076         m_totalBoundaries(std::vector<float>()),
0077         m_functionPtr(&searchEquidistantWithBoundary) {}
0078 
0079   /// Constructor for equidistant binning
0080   /// and optional sub structure can be
0081   /// multiplicative or additive
0082   ///
0083   /// @param bOption is the binning option : open, closed
0084   /// @param bValue is the axis direction: Axis, AxisY, etc.
0085   /// @param bBins is number of equidistant bins
0086   /// @param bMin is the minimum value
0087   /// @param bMax is the maximum value
0088   /// @param sBinData is (optional) sub structure
0089   /// @param sBinAdditive is the prescription for the sub structure
0090   BinningData(BinningOption bOption, AxisDirection bValue, std::size_t bBins,
0091               float bMin, float bMax,
0092               std::unique_ptr<const BinningData> sBinData = nullptr,
0093               bool sBinAdditive = false)
0094       : type(equidistant),
0095         option(bOption),
0096         binvalue(bValue),
0097         min(bMin),
0098         max(bMax),
0099         step((bMax - bMin) / static_cast<float>(bBins)),
0100         zdim(bBins == 1 ? true : false),
0101         subBinningData(std::move(sBinData)),
0102         subBinningAdditive(sBinAdditive),
0103         m_bins(bBins),
0104         m_boundaries(std::vector<float>()),
0105         m_totalBins(bBins),
0106         m_totalBoundaries(std::vector<float>()) {
0107     // set to equidistant search
0108     m_functionPtr = &searchEquidistantWithBoundary;
0109     // fill the boundary vector for fast access to center & boundaries
0110     m_boundaries.reserve(m_bins + 1);
0111     for (std::size_t ib = 0; ib < m_bins + 1; ++ib) {
0112       m_boundaries.push_back(min + static_cast<float>(ib) * step);
0113     }
0114     // the binning data has sub structure - multiplicative or additive
0115     checkSubStructure();
0116   }
0117 
0118   /// Constructor for non-equidistant binning
0119   ///
0120   /// @param bOption is the binning option : open / closed
0121   /// @param bValue is the axis direction : AxisX, AxisY, etc.
0122   /// @param bBoundaries are the bin boundaries
0123   /// @param sBinData is (optional) sub structure
0124   BinningData(BinningOption bOption, AxisDirection bValue,
0125               const std::vector<float>& bBoundaries,
0126               std::unique_ptr<const BinningData> sBinData = nullptr)
0127       : type(arbitrary),
0128         option(bOption),
0129         binvalue(bValue),
0130         zdim(bBoundaries.size() == 2 ? true : false),
0131         subBinningData(std::move(sBinData)),
0132         subBinningAdditive(true),
0133         m_bins(bBoundaries.size() - 1),
0134         m_boundaries(bBoundaries),
0135         m_totalBins(bBoundaries.size() - 1),
0136         m_totalBoundaries(bBoundaries) {
0137     // assert a no-size case
0138     throw_assert(m_boundaries.size() > 1, "Must have more than one boundary");
0139     min = m_boundaries[0];
0140     max = m_boundaries[m_boundaries.size() - 1];
0141     // set to equidistant search
0142     m_functionPtr = &searchInVectorWithBoundary;
0143     // the binning data has sub structure - multiplicative
0144     checkSubStructure();
0145   }
0146 
0147   /// Copy constructor
0148   ///
0149   /// @param bdata is the source object
0150   BinningData(const BinningData& bdata)
0151       : type(bdata.type),
0152         option(bdata.option),
0153         binvalue(bdata.binvalue),
0154         min(bdata.min),
0155         max(bdata.max),
0156         step(bdata.step),
0157         zdim(bdata.zdim),
0158         subBinningData(nullptr),
0159         subBinningAdditive(bdata.subBinningAdditive),
0160         m_bins(bdata.m_bins),
0161         m_boundaries(bdata.m_boundaries),
0162         m_totalBins(bdata.m_totalBins),
0163         m_totalBoundaries(bdata.m_totalBoundaries) {
0164     // get the binning data
0165     subBinningData =
0166         bdata.subBinningData
0167             ? std::make_unique<const BinningData>(*bdata.subBinningData)
0168             : nullptr;
0169     // set the pointer depending on the type
0170     // set the correct function pointer
0171     if (type == equidistant) {
0172       m_functionPtr = &searchEquidistantWithBoundary;
0173     } else {
0174       m_functionPtr = &searchInVectorWithBoundary;
0175     }
0176   }
0177 
0178   /// Constructor from a type-erased axis carrying its axis direction
0179   ///
0180   /// @param axis is the axis object, its direction must be set
0181   ///
0182   /// @throws std::invalid_argument if the axis has no direction
0183   explicit BinningData(const IAxis& axis)
0184       : BinningData(directionOf(axis), axis) {}
0185 
0186   /// Constructor from DirectedProtoAxis
0187   ///
0188   /// @param dpAxis is the ProtoAxis object
0189   ///
0190   /// @deprecated Use BinningData(const IAxis&) with a directed axis instead
0191   [[deprecated(
0192       "Use BinningData(const IAxis&) with a directed axis "
0193       "instead")]] explicit BinningData(const DirectedProtoAxis& dpAxis)
0194       : BinningData(dpAxis.getAxisDirection(), dpAxis.getAxis()) {}
0195 
0196   /// Constructor from an axis direction and a type-erased axis
0197   ///
0198   /// @param axisDir is the axis direction
0199   /// @param axis is the axis object
0200   ///
0201   BinningData(AxisDirection axisDir, const IAxis& axis)
0202       : binvalue(axisDir), subBinningData(nullptr) {
0203     type = axis.getType() == AxisType::Equidistant ? equidistant : arbitrary;
0204     option = axis.getBoundaryType() == AxisBoundaryType::Closed ? closed : open;
0205     min = static_cast<float>(axis.getMin());
0206     max = static_cast<float>(axis.getMax());
0207     m_bins = axis.getNBins();
0208     step = (max - min) / static_cast<float>(m_bins);
0209     zdim = (m_bins == 1);
0210     m_boundaries.reserve(axis.getBinEdges().size());
0211     for (const auto& edge : axis.getBinEdges()) {
0212       m_boundaries.push_back(static_cast<float>(edge));
0213     }
0214     m_totalBins = m_bins;
0215     m_totalBoundaries = m_boundaries;
0216     // Set the search function pointer based on axis type
0217     m_functionPtr = (type == equidistant) ? &searchEquidistantWithBoundary
0218                                           : &searchInVectorWithBoundary;
0219   }
0220 
0221   /// Assignment operator
0222   ///
0223   /// @param bdata is the source object
0224   /// @return Reference to this BinningData after assignment
0225   BinningData& operator=(const BinningData& bdata) {
0226     if (this != &bdata) {
0227       type = bdata.type;
0228       option = bdata.option;
0229       binvalue = bdata.binvalue;
0230       min = bdata.min;
0231       max = bdata.max;
0232       step = bdata.step;
0233       zdim = bdata.zdim;
0234       subBinningAdditive = bdata.subBinningAdditive;
0235       subBinningData =
0236           bdata.subBinningData
0237               ? std::make_unique<const BinningData>(*bdata.subBinningData)
0238               : nullptr;
0239       m_bins = bdata.m_bins;
0240       m_boundaries = bdata.m_boundaries;
0241       m_totalBins = bdata.m_totalBins;
0242       m_totalBoundaries = bdata.m_totalBoundaries;
0243       // set the correct function pointer
0244       if (type == equidistant) {
0245         m_functionPtr = &searchEquidistantWithBoundary;
0246       } else {
0247         m_functionPtr = &searchInVectorWithBoundary;
0248       }
0249     }
0250     return (*this);
0251   }
0252 
0253   BinningData() = default;
0254   ~BinningData() = default;
0255 
0256   /// Equality operator
0257   ///
0258   /// @param bData is the binning data to be checked against
0259   ///
0260   /// @return a boolean indicating if they are the same
0261   bool operator==(const BinningData& bData) const {
0262     return (type == bData.type && option == bData.option &&
0263             binvalue == bData.binvalue && min == bData.min &&
0264             max == bData.max && step == bData.step && zdim == bData.zdim &&
0265             ((subBinningData == nullptr && bData.subBinningData == nullptr) ||
0266              (subBinningData != nullptr && bData.subBinningData != nullptr &&
0267               (*subBinningData == *bData.subBinningData))) &&
0268             subBinningAdditive == bData.subBinningAdditive);
0269   }
0270 
0271   /// Return the number of bins - including sub bins
0272   /// @return Total number of bins including sub-bins
0273   std::size_t bins() const { return m_totalBins; }
0274 
0275   /// Return the boundaries  - including sub boundaries
0276   /// @return vector of floats indicating the boundary values
0277   const std::vector<float>& boundaries() const {
0278     if (subBinningData) {
0279       return m_totalBoundaries;
0280     }
0281     return m_boundaries;
0282   }
0283 
0284   /// Take the right float value
0285   ///
0286   /// @param lposition assumes the correct local position expression
0287   ///
0288   /// @return float value according to the binning setup
0289   float value(const Vector2& lposition) const {
0290     // ordered after occurrence
0291     if (binvalue == AxisDirection::AxisR ||
0292         binvalue == AxisDirection::AxisRPhi ||
0293         binvalue == AxisDirection::AxisX ||
0294         binvalue == AxisDirection::AxisTheta) {
0295       return static_cast<float>(lposition[0]);
0296     }
0297 
0298     return static_cast<float>(lposition[1]);
0299   }
0300 
0301   /// Take the right float value
0302   ///
0303   /// @param position is the global position
0304   ///
0305   /// @return float value according to the binning setup
0306   float value(const Vector3& position) const {
0307     using VectorHelpers::eta;
0308     using VectorHelpers::perp;
0309     using VectorHelpers::phi;
0310     // ordered after occurrence
0311     if (binvalue == AxisDirection::AxisR ||
0312         binvalue == AxisDirection::AxisTheta) {
0313       return static_cast<float>(perp(position));
0314     }
0315     if (binvalue == AxisDirection::AxisRPhi) {
0316       return static_cast<float>(perp(position) * phi(position));
0317     }
0318     if (binvalue == AxisDirection::AxisEta) {
0319       return static_cast<float>(eta(position));
0320     }
0321     if (toUnderlying(binvalue) < 3) {
0322       return static_cast<float>(position[toUnderlying(binvalue)]);
0323     }
0324     // phi gauging
0325     return static_cast<float>(phi(position));
0326   }
0327 
0328   /// Get the center value of a bin
0329   ///
0330   /// @param bin is the bin for which the center value is requested
0331   ///
0332   /// @return float value according to the bin center
0333   float center(std::size_t bin) const {
0334     const std::vector<float>& bvals = boundaries();
0335     // take the center between bin boundaries
0336     float value =
0337         bin < (bvals.size() - 1) ? 0.5f * (bvals[bin] + bvals[bin + 1]) : 0.f;
0338     return value;
0339   }
0340 
0341   /// Get the width of a bin
0342   ///
0343   /// @param bin is the bin for which the width is requested
0344   ///
0345   /// @return float value of width
0346   float width(std::size_t bin) const {
0347     const std::vector<float>& bvals = boundaries();
0348     // take the center between bin boundaries
0349     float value = bin < (bvals.size() - 1) ? bvals[bin + 1] - bvals[bin] : 0.f;
0350     return value;
0351   }
0352 
0353   /// Check if bin is inside from Vector3
0354   ///
0355   /// @param position is the search position in global coordinated
0356   ///
0357   /// @return boolean if this is inside() method is true
0358   bool inside(const Vector3& position) const {
0359     // closed one is always inside
0360     if (option == closed) {
0361       return true;
0362     }
0363     // all other options
0364     // @todo remove hard-coded tolerance parameters
0365     float val = value(position);
0366     return (val > min - 0.001 && val < max + 0.001);
0367   }
0368 
0369   /// Check if bin is inside from Vector2
0370   ///
0371   /// @param lposition is the search position in global coordinated
0372   ///
0373   /// @return boolean if this is inside() method is true
0374   bool inside(const Vector2& lposition) const {
0375     // closed one is always inside
0376     if (option == closed) {
0377       return true;
0378     }
0379     // all other options
0380     // @todo remove hard-coded tolerance parameters
0381     float val = value(lposition);
0382     return (val > min - 0.001 && val < max + 0.001);
0383   }
0384 
0385   /// Generic search from a 2D position
0386   /// -- corresponds to local coordinate schema
0387   /// @param lposition is the search position in local coordinated
0388   ///
0389   /// @return bin according tot this
0390   std::size_t searchLocal(const Vector2& lposition) const {
0391     if (zdim) {
0392       return 0;
0393     }
0394     return search(value(lposition));
0395   }
0396 
0397   /// Generic search from a 3D position
0398   /// -- corresponds to global coordinate schema
0399   /// @param position is the search position in global coordinated
0400   ///
0401   /// @return bin according tot this
0402   std::size_t searchGlobal(const Vector3& position) const {
0403     if (zdim) {
0404       return 0;
0405     }
0406     return search(value(position));
0407   }
0408 
0409   /// Generic search - forwards to correct function pointer
0410   ///
0411   /// @param value is the searchvalue as float
0412   ///
0413   /// @return bin according tot this
0414   std::size_t search(float value) const {
0415     if (zdim) {
0416       return 0;
0417     }
0418     assert(m_functionPtr != nullptr);
0419     return (!subBinningData) ? (*m_functionPtr)(value, *this)
0420                              : searchWithSubStructure(value);
0421   }
0422 
0423   ///  Generic search with sub structure
0424   /// - forwards to correct function pointer
0425   ///
0426   /// @param value is the searchvalue as float
0427   ///
0428   /// @return bin according tot this
0429   std::size_t searchWithSubStructure(float value) const {
0430     // find the masterbin with the correct function pointer
0431     std::size_t masterbin = (*m_functionPtr)(value, *this);
0432     // additive sub binning -
0433     if (subBinningAdditive) {
0434       // no gauging done, for additive sub structure
0435       return masterbin + subBinningData->search(value);
0436     }
0437     // gauge the value to the subBinData
0438     float gvalue = value - static_cast<float>(masterbin) *
0439                                (subBinningData->max - subBinningData->min);
0440     // now go / additive or multiplicative
0441     std::size_t subbin = subBinningData->search(gvalue);
0442     // now return
0443     return masterbin * subBinningData->bins() + subbin;
0444   }
0445 
0446   /// Layer next direction is needed
0447   ///
0448   /// @param position is the start search position
0449   /// @param dir is the direction
0450   /// @todo check if this can be changed
0451   ///
0452   /// @return integer that indicates which direction to move
0453   int nextDirection(const Vector3& position, const Vector3& dir) const {
0454     if (zdim) {
0455       return 0;
0456     }
0457     float val = value(position);
0458     Vector3 probe = position + dir.normalized();
0459     float nextval = value(probe);
0460     return (nextval > val) ? 1 : -1;
0461   }
0462 
0463   /// access to the center value
0464   /// this uses the bin boundary vector, it also works with sub structure
0465   ///
0466   /// @param bin is the bin for which the value is requested, if bin > nbins
0467   /// it is set to max
0468   ///
0469   /// @return the center value of the bin is given
0470   float centerValue(std::size_t bin) const {
0471     if (zdim) {
0472       return 0.5f * (min + max);
0473     }
0474     float bmin = m_boundaries[bin];
0475     float bmax = bin < m_boundaries.size() ? m_boundaries[bin + 1] : max;
0476     return 0.5f * (bmin + bmax);
0477   }
0478 
0479   /// Create a scaled version of this BinningData
0480   /// @param factor is the scaling factor to be applied to the binning parameters
0481   /// @return a new BinningData object with scaled parameters
0482   BinningData scale(float factor) const {
0483     BinningData scaled = *this;
0484     scaled.min *= factor;
0485     scaled.max *= factor;
0486     scaled.step *= factor;
0487     for (auto& boundary : scaled.m_boundaries) {
0488       boundary *= factor;
0489     }
0490     for (auto& boundary : scaled.m_totalBoundaries) {
0491       boundary *= factor;
0492     }
0493     if (scaled.subBinningData) {
0494       scaled.subBinningData = std::make_unique<const BinningData>(
0495           scaled.subBinningData->scale(factor));
0496     }
0497     return scaled;
0498   }
0499 
0500  private:
0501   /// helper method to require the direction of a type-erased axis
0502   ///
0503   /// @param axis is the axis object
0504   ///
0505   /// @throws std::invalid_argument if the axis has no direction
0506   ///
0507   /// @return the direction of the axis
0508   static AxisDirection directionOf(const IAxis& axis) {
0509     if (!axis.getDirection().has_value()) {
0510       throw std::invalid_argument(
0511           "BinningData: axis has no direction assigned");
0512     }
0513     return axis.getDirection().value();
0514   }
0515 
0516   std::size_t m_bins{};             ///< number of bins
0517   std::vector<float> m_boundaries;  ///< vector of holding the bin boundaries
0518   std::size_t m_totalBins{};        ///< including potential substructure
0519   std::vector<float> m_totalBoundaries;  ///< including potential substructure
0520 
0521   std::size_t (*m_functionPtr)(float,
0522                                const BinningData&){};  /// function pointer
0523 
0524   /// helper method to set the sub structure
0525   void checkSubStructure() {
0526     // sub structure is only checked when sBinData is defined
0527     if (subBinningData) {
0528       m_totalBoundaries.clear();
0529       // (A) additive sub structure
0530       if (subBinningAdditive) {
0531         // one bin is replaced by the sub bins
0532         m_totalBins = m_bins + subBinningData->bins() - 1;
0533         // the tricky one - exchange one bin by many others
0534         m_totalBoundaries.reserve(m_totalBins + 1);
0535         // get the sub bin boundaries
0536         const std::vector<float>& subBinBoundaries =
0537             subBinningData->boundaries();
0538         float sBinMin = subBinBoundaries[0];
0539         // get the min value of the sub bin boundaries
0540         std::vector<float>::const_iterator mbvalue = m_boundaries.begin();
0541         for (; mbvalue != m_boundaries.end(); ++mbvalue) {
0542           // should define numerically stable
0543           if (std::abs((*mbvalue) - sBinMin) < 10e-10) {
0544             // copy the sub bin boundaries into the vector
0545             m_totalBoundaries.insert(m_totalBoundaries.begin(),
0546                                      subBinBoundaries.begin(),
0547                                      subBinBoundaries.end());
0548             ++mbvalue;
0549           } else {
0550             m_totalBoundaries.push_back(*mbvalue);
0551           }
0552         }
0553       } else {  // (B) multiplicative sub structure
0554         // every bin is just replaced by the sub binning structure
0555         m_totalBins = m_bins * subBinningData->bins();
0556         m_totalBoundaries.reserve(m_totalBins + 1);
0557         // get the sub bin boundaries if there are any
0558         const std::vector<float>& subBinBoundaries =
0559             subBinningData->boundaries();
0560         // create the boundary vector
0561         m_totalBoundaries.push_back(min);
0562         for (std::size_t ib = 0; ib < m_bins; ++ib) {
0563           float offset = static_cast<float>(ib) * step;
0564           for (std::size_t isb = 1; isb < subBinBoundaries.size(); ++isb) {
0565             m_totalBoundaries.push_back(offset + subBinBoundaries[isb]);
0566           }
0567         }
0568       }
0569       // sort the total boundary vector
0570       std::ranges::sort(m_totalBoundaries);
0571     }
0572   }
0573 
0574   // Equidistant search
0575   // - fastest method
0576   static std::size_t searchEquidistantWithBoundary(float value,
0577                                                    const BinningData& bData) {
0578     // vanilla
0579 
0580     int bin = static_cast<int>((value - bData.min) / bData.step);
0581     // special treatment of the 0 bin for closed
0582     if (bData.option == closed) {
0583       if (value < bData.min) {
0584         return (bData.m_bins - 1);
0585       }
0586       if (value > bData.max) {
0587         return 0;
0588       }
0589     }
0590     // if outside boundary : return boundary for open, opposite bin for closed
0591     bin =
0592         bin < 0
0593             ? ((bData.option == open) ? 0 : static_cast<int>(bData.m_bins - 1))
0594             : bin;
0595     return static_cast<std::size_t>(
0596         (bin <= static_cast<int>(bData.m_bins - 1))
0597             ? static_cast<std::size_t>(bin)
0598             : ((bData.option == open) ? (bData.m_bins - 1) : 0));
0599   }
0600 
0601   // Search in arbitrary boundary
0602   static std::size_t searchInVectorWithBoundary(float value,
0603                                                 const BinningData& bData) {
0604     // lower boundary
0605     if (value <= bData.m_boundaries[0]) {
0606       return (bData.option == closed) ? (bData.m_bins - 1) : 0;
0607     }
0608     // higher boundary
0609     if (value >= bData.max) {
0610       return (bData.option == closed) ? 0 : (bData.m_bins - 1);
0611     }
0612 
0613     auto lb = std::ranges::lower_bound(bData.m_boundaries, value);
0614     return static_cast<std::size_t>(
0615         std::ranges::distance(bData.m_boundaries.begin(), lb) - 1);
0616   }
0617 
0618  public:
0619   /// String screen output method
0620   /// @param indent the current indentation
0621   /// @return a string containing the screen information
0622   std::string toString(const std::string& indent = "") const {
0623     std::stringstream sl;
0624     sl << indent << "BinningData object:" << '\n';
0625     sl << indent << "  - type       : " << static_cast<std::size_t>(type)
0626        << '\n';
0627     sl << indent << "  - option     : " << static_cast<std::size_t>(option)
0628        << '\n';
0629     sl << indent << "  - value      : " << static_cast<std::size_t>(binvalue)
0630        << '\n';
0631     sl << indent << "  - bins       : " << bins() << '\n';
0632     sl << indent << "  - min/max    : " << min << " / " << max << '\n';
0633     if (type == equidistant) {
0634       sl << indent << "  - step       : " << step << '\n';
0635     }
0636     sl << indent << "  - boundaries : | ";
0637     for (const auto& b : boundaries()) {
0638       sl << b << " | ";
0639     }
0640     sl << '\n';
0641     return sl.str();
0642   }
0643 };
0644 
0645 }  // namespace Acts