2222// / * where does it already express it wrongly -- a medium with ifield == 0,
2323// / which asks the transport engine for straight lines, sitting in real field.
2424// /
25+ // / It also answers one question about the geometry alone, and therefore runs that
26+ // / part without a field under --reachability-only: does every placement actually
27+ // / occupy the space it was built in? A daughter outside its mother, or one shadowed
28+ // / by an overlapping sibling, is never reached by the navigator, carries no material
29+ // / and produces no hits, and nothing in the construction code says so.
30+ // /
2531// / It detects and proposes. It never modifies a geometry.
2632// /
2733// / The field enters in two ways. A support model (an outer bound on where |B|
@@ -697,6 +703,167 @@ bool supportFromJson(const json& in, Support& support)
697703 return true ;
698704}
699705
706+ // ---------------------------------------------------------------------------
707+ // reachability
708+ // ---------------------------------------------------------------------------
709+
710+ // / A placement can be perfectly built and still occupy no space. TGeo never
711+ // / descends into a daughter lying outside its mother, and an overlapping sibling
712+ // / can shadow one that does not; either way the volume carries no material, takes
713+ // / no steps and produces no hits, and nothing in the construction code complains.
714+ // / Only the navigator can settle it, so ask it: draw points inside a placement's
715+ // / own shape and check that FindNode() comes back through that placement.
716+ // /
717+ // / One representative path per node object, which is the granularity at which the
718+ // / common defect lives -- a daughter outside its mother is a property of the node,
719+ // / not of the path that reaches it. A node whose mother is itself placed many
720+ // / times is therefore sampled once, in the first of those placements.
721+ struct Reach {
722+ std::string medium, mother, worstPath;
723+ long sampled = 0 ;
724+ double fraction = 1 .;
725+ };
726+
727+ constexpr int kReachRejectionTries = 400 ;
728+
729+ class ReachAudit
730+ {
731+ public:
732+ explicit ReachAudit (int samples) : mSamples(samples) {}
733+
734+ void walk (TGeoNode* node) { walk (node, TGeoHMatrix (), " " ); }
735+
736+ const std::vector<Reach>& dead () const { return mDead ; }
737+ const std::vector<Reach>& partial () const { return mPartial ; }
738+ long nodesVisited () const { return mVisited ; }
739+ long nodesSampled () const { return mSampled ; }
740+ long nodesUnsampleable () const { return mUnsampleable ; }
741+
742+ private:
743+ void walk (TGeoNode* node, const TGeoHMatrix& parent, const std::string& path);
744+ bool samplePoint (TGeoShape* shape, double * local);
745+
746+ int mSamples ;
747+ long mVisited = 0 , mSampled = 0 , mUnsampleable = 0 ;
748+ TRandom3 mRandom {20260901 };
749+ std::set<TGeoNode*> mSeen ;
750+ std::vector<Reach> mDead , mPartial ;
751+ };
752+
753+ // / Rejection sampling against the shape itself. A TGeoCompositeShape inherits
754+ // / TGeoBBox, so its DX/DY/DZ describe a box that still contains the holes and
755+ // / subtractions -- only Contains() knows the difference. GetOrigin() matters too:
756+ // / the box need not be centred on the local origin.
757+ bool ReachAudit::samplePoint (TGeoShape* shape, double * local)
758+ {
759+ auto * box = dynamic_cast <TGeoBBox*>(shape);
760+ if (box == nullptr ) {
761+ return false ;
762+ }
763+ const double * origin = box->GetOrigin ();
764+ for (int attempt = 0 ; attempt < kReachRejectionTries ; ++attempt) {
765+ local[0 ] = origin[0 ] + box->GetDX () * (2 . * mRandom .Rndm () - 1 .);
766+ local[1 ] = origin[1 ] + box->GetDY () * (2 . * mRandom .Rndm () - 1 .);
767+ local[2 ] = origin[2 ] + box->GetDZ () * (2 . * mRandom .Rndm () - 1 .);
768+ if (shape->Contains (local)) {
769+ return true ;
770+ }
771+ }
772+ return false ;
773+ }
774+
775+ void ReachAudit::walk (TGeoNode* node, const TGeoHMatrix& parent, const std::string& path)
776+ {
777+ if (!mSeen .insert (node).second ) {
778+ return ; // this node object, and therefore its whole subtree, is already covered
779+ }
780+ TGeoHMatrix here = parent;
781+ here.Multiply (node->GetMatrix ());
782+ TGeoVolume* volume = node->GetVolume ();
783+ const std::string myPath = path + " /" + node->GetName ();
784+ ++mVisited ;
785+
786+ // an assembly is expanded away at closure, so FindNode never returns one
787+ if (!volume->IsAssembly ()) {
788+ int drawn = 0 , reached = 0 ;
789+ for (int i = 0 ; i < mSamples ; ++i) {
790+ double local[3 ], global[3 ];
791+ if (!samplePoint (volume->GetShape (), local)) {
792+ break ;
793+ }
794+ ++drawn;
795+ here.LocalToMaster (local, global);
796+ if (gGeoManager ->FindNode (global[0 ], global[1 ], global[2 ]) == nullptr ) {
797+ continue ;
798+ }
799+ const std::string found = gGeoManager ->GetPath ();
800+ // reached if the navigator's own path passes through this placement
801+ if (found.rfind (myPath, 0 ) == 0 ) {
802+ ++reached;
803+ }
804+ }
805+ if (drawn == 0 ) {
806+ ++mUnsampleable ; // a sliver too thin for the rejection budget; says nothing
807+ } else {
808+ ++mSampled ;
809+ const double fraction = double (reached) / drawn;
810+ if (fraction < 0.999 ) {
811+ auto * medium = volume->GetMedium ();
812+ Reach entry;
813+ entry.medium = medium != nullptr ? medium->GetName () : " (none)" ;
814+ entry.mother = node->GetMotherVolume () != nullptr ? node->GetMotherVolume ()->GetName () : " -" ;
815+ entry.worstPath = myPath;
816+ entry.sampled = drawn;
817+ entry.fraction = fraction;
818+ (fraction == 0 . ? mDead : mPartial ).push_back (entry);
819+ }
820+ }
821+ }
822+
823+ for (int i = 0 ; i < node->GetNdaughters (); ++i) {
824+ walk (node->GetDaughter (i), here, myPath);
825+ }
826+ }
827+
828+
829+ // / Prints the audit and returns how many placements the navigator cannot reach at all.
830+ long reportReachability (int samples, Report& report)
831+ {
832+ if (samples <= 0 ) {
833+ return 0 ;
834+ }
835+ progress (" reachability: asking the navigator to find every placement from inside its own shape" );
836+ ReachAudit audit (samples);
837+ audit.walk (gGeoManager ->GetTopNode ());
838+
839+ report (form (" reachability: %ld node objects visited, %ld sampled, %ld too thin to sample" ,
840+ audit.nodesVisited (), audit.nodesSampled (), audit.nodesUnsampleable ()));
841+ report (form (" %ld placements the navigator never reaches, %zu it reaches only in part" ,
842+ (long )audit.dead ().size (), audit.partial ().size ()));
843+ if (!audit.dead ().empty ()) {
844+ report (" unreachable -- these carry no material and produce no hits:" );
845+ report (form (" %-12s %-18s %10s %s" , " mother" , " medium" , " sampled" , " path" ));
846+ for (const auto & entry : audit.dead ()) {
847+ report (form (" %-12s %-18s %10ld %s" , entry.mother .c_str (), entry.medium .c_str (), entry.sampled ,
848+ entry.worstPath .c_str ()));
849+ }
850+ }
851+ for (size_t i = 0 ; i < audit.partial ().size () && i < 20 ; ++i) {
852+ const auto & entry = audit.partial ()[i];
853+ if (i == 0 ) {
854+ report (" partially shadowed -- an overlapping sibling or an extruding placement:" );
855+ report (form (" %-12s %-18s %8s %s" , " mother" , " medium" , " reached" , " path" ));
856+ }
857+ report (form (" %-12s %-18s %7.1f%% %s" , entry.mother .c_str (), entry.medium .c_str (),
858+ 100 . * entry.fraction , entry.worstPath .c_str ()));
859+ }
860+ if (audit.partial ().size () > 20 ) {
861+ report (form (" ... and %zu more" , audit.partial ().size () - 20 ));
862+ }
863+ report (" " );
864+ return (long )audit.dead ().size ();
865+ }
866+
700867// ---------------------------------------------------------------------------
701868// the placement table
702869// ---------------------------------------------------------------------------
@@ -1513,6 +1680,8 @@ struct Options {
15131680 std::string anchorFile;
15141681 std::vector<double > thresholdsGauss;
15151682 double margin = 5.0 ;
1683+ int reachSamples = 32 ;
1684+ bool reachabilityOnly = false ;
15161685 std::string outputPrefix = " geometry-doctor" ;
15171686};
15181687
@@ -1541,7 +1710,11 @@ int main(int argc, char** argv)
15411710 (" output-prefix" , bpo::value<std::string>(&options.outputPrefix )->default_value (" geometry-doctor" ), //
15421711 " prefix for the report, the proposals and the placement table" ) //
15431712 (" verify-anchors" , bpo::value<std::string>(&options.anchorFile ), //
1544- " check the classification against known-good volumes listed in this JSON file" );
1713+ " check the classification against known-good volumes listed in this JSON file" ) //
1714+ (" reachability-samples" , bpo::value<int >(&options.reachSamples )->default_value (32 ), //
1715+ " points drawn inside each placement for the reachability audit; 0 disables it" ) //
1716+ (" reachability-only" , bpo::bool_switch (&options.reachabilityOnly ), //
1717+ " run only the reachability audit, which needs no magnetic field" );
15451718
15461719 bpo::variables_map arguments;
15471720 try {
@@ -1559,6 +1732,29 @@ int main(int argc, char** argv)
15591732
15601733 const bool haveFieldFile = arguments.count (" field-file" ) != 0u ;
15611734 const bool haveFieldCurrent = arguments.count (" field-current" ) != 0u ;
1735+
1736+ // The reachability audit is a question about the geometry alone, so it is the one
1737+ // part of this tool that can run without a field.
1738+ if (options.reachabilityOnly ) {
1739+ TGeoManager::Import (options.geometryFile .c_str ());
1740+ if (gGeoManager == nullptr ) {
1741+ std::cerr << " error: no TGeoManager in " << options.geometryFile << ' \n ' ;
1742+ return 1 ;
1743+ }
1744+ Report report;
1745+ report (" ALICE simulation geometry doctor -- reachability audit" );
1746+ report (" " );
1747+ report (" geometry : " + options.geometryFile );
1748+ report (form (" volumes : %d, media %d" , gGeoManager ->GetListOfVolumes ()->GetEntries (),
1749+ gGeoManager ->GetListOfMedia ()->GetEntries ()));
1750+ report (" " );
1751+ const long dead = reportReachability (options.reachSamples , report);
1752+ const std::string reportPath = options.outputPrefix + " -report.txt" ;
1753+ report (" wrote " + reportPath);
1754+ report.write (reportPath);
1755+ return dead == 0 ? 0 : 3 ;
1756+ }
1757+
15621758 if (haveFieldFile == haveFieldCurrent) {
15631759 std::cerr << " error: give exactly one of --field-file and --field-current\n " ;
15641760 return 1 ;
@@ -1668,6 +1864,8 @@ int main(int argc, char** argv)
16681864 }
16691865 report (form (" volumes : %d, media %d" , gGeoManager ->GetListOfVolumes ()->GetEntries (),
16701866 gGeoManager ->GetListOfMedia ()->GetEntries ()));
1867+ report (" " );
1868+ reportReachability (options.reachSamples , report);
16711869
16721870 Doctor doctor (field, support);
16731871 doctor.walk (gGeoManager ->GetTopNode ());
0 commit comments