Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-09 07:48:52

0001 #!/usr/bin/env python
0002 #
0003 # Copyright (c) 2019 Opticks Team. All Rights Reserved.
0004 #
0005 # This file is part of Opticks
0006 # (see https://bitbucket.org/simoncblyth/opticks).
0007 #
0008 # Licensed under the Apache License, Version 2.0 (the "License"); 
0009 # you may not use this file except in compliance with the License.  
0010 # You may obtain a copy of the License at
0011 #
0012 #   http://www.apache.org/licenses/LICENSE-2.0
0013 #
0014 # Unless required by applicable law or agreed to in writing, software 
0015 # distributed under the License is distributed on an "AS IS" BASIS, 
0016 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
0017 # See the License for the specific language governing permissions and 
0018 # limitations under the License.
0019 #
0020 
0021 """
0022 treebase.py
0023 =============
0024 
0025 * Node and Tree classes work together to provide tree "auto" assemblage 
0026   using parent digest lookup
0027 
0028 * Stripped volume PV/LV/PV/LV source trees in detdesc or gdml flavors are converted 
0029   into a homogenous Node trees (PV,LV)/(PV,LV)/...
0030 
0031 
0032 Users
0033 -------
0034 
0035 analytic/sc.py
0036     parsing/conversion of GLTF into NCSG serialization    
0037 
0038 ana/pmt/treepart.py
0039     uses treepart_manual_mixin to add *parts*, *num_parts* methods
0040     to both treebase.Node and treebase.Tree and *convert* to treebase.Tree
0041 
0042 ana/pmt/analytic.py
0043     parsing/conversion of DETDESC XML using ana/pmt/treepart.py
0044 
0045 
0046 
0047 """
0048 
0049 
0050 
0051 
0052 import logging, hashlib, os
0053 # from collections import Counter
0054 import numpy as np
0055 np.set_printoptions(precision=2) 
0056 
0057 from opticks.ana.main import opticks_main
0058 from opticks.ana.base import u_, b_, d_
0059 from opticks.ana.nbase import Buf
0060 
0061 log = logging.getLogger(__name__)
0062 
0063 def log_info(msg):
0064     sys.stderr.write(msg)
0065 
0066 
0067 class DummyTopPV(object):
0068     name = "top"  # match the G4DAE name
0069     #name = "dummyTopPV"
0070 
0071     def _get_transform(self):
0072         #assert 0
0073         log.warning("returning DummyTopPV placeholder transform")
0074         return np.eye(4, dtype=np.float32)
0075     transform = property(_get_transform) 
0076 
0077     def find_(self, smth):
0078         return None
0079     def __repr__(self):
0080         return self.name
0081 
0082 
0083 class Node(object):
0084     """
0085     treebase.Node
0086     ===============
0087 
0088     Collects XML elements into more easily navigable tree structure...
0089     Via mixin techniques this works with both GDML and detdesc sources.
0090 
0091     pv 
0092        opticks.ana.pmt.gdml.PhysVol : placement of the node
0093 
0094     pv.transform
0095        4x4 np.array 
0096 
0097     posXYZ 
0098         opticks.ana.pmt.gdml.Position
0099         TODO: remove this, it was a kludge ? should be using transform 
0100 
0101     pv.volume 
0102     lv   
0103        opticks.ana.pmt.gdml.Volume : same as the lv
0104 
0105     lv.physvol
0106        list of opticks.ana.pmt.gdml.PhysVol
0107 
0108     lv.solid
0109        geometry eg opticks.ana.pmt.gdml.Tube, opticks.ana.pmt.gdml.Union
0110 
0111     parent
0112        opticks.ana.pmt.treebase.Node
0113 
0114     children
0115        list of opticks.ana.pmt.treebase.Node 
0116 
0117     """
0118 
0119     selected_count = 0 
0120 
0121     @classmethod
0122     def md5digest(cls, volpath ):
0123         """  
0124         Uses the top-down ordered list of memory locations of the instances
0125         of the volpath for a node to provide a unique identifier for that node within
0126         the tree. Use of id memory locations means that the digest will change from run to run. 
0127         """
0128         dig = ",".join(map(lambda _:str(id(_)),volpath))
0129         dig = hashlib.md5(b_(dig)).hexdigest() 
0130         return dig
0131 
0132     @classmethod
0133     def _depth(cls, node):
0134         d = 0
0135         while node.parent is not None:
0136             node = node.parent
0137             d += 1 
0138         pass
0139         return d 
0140 
0141     @classmethod
0142     def create(cls, volpath, lvtype="Logvol", pvtype="Physvol", postype="posXYZ" ):
0143         """
0144         :param volpath: ancestor volume instances, 
0145                         and the volume itself ordered top down 
0146 
0147         Invoked from Tree.create_r, as the recursive traverse starts from the
0148         root node, parents should always be found (other than for the top node).
0149 
0150         This is how the tree auto-assembles : thanks to the node.pdigest identifying
0151         the parent.  
0152 
0153         """
0154         assert len(volpath) >= 2 
0155         
0156         node = cls(volpath) 
0157 
0158         ndig = node.digest   
0159         assert ndig not in Tree.registry, "each node must have a unique digest" 
0160 
0161         node.index  = len(Tree.registry)
0162 
0163         Tree.byindex[node.index] = node 
0164         Tree.registry[ndig] = node
0165 
0166         parent = Tree.lookup(node.pdigest)
0167         node.parent = parent
0168 
0169         if node.parent:
0170             node.parent.add_child(node)  
0171         pass
0172         node.depth = cls._depth(node)
0173 
0174         assert type(volpath[-2]).__name__ in (pvtype, "DummyTopPV") 
0175         assert type(volpath[-1]).__name__ == lvtype
0176 
0177         pv = volpath[-2] 
0178         lv = volpath[-1] 
0179 
0180         assert lv
0181   
0182         if node.index > 0:
0183             if pv is None:
0184                 log.fatal("all nodes other than root must have a pv %r " % node)
0185             assert pv 
0186             ## huh, surely root will also have DummyTopPV ?
0187         pass
0188 
0189         node.pv = pv
0190         node.lv = lv
0191 
0192         if node.pv is not None:
0193             node.posXYZ = node.pv.find_(postype) 
0194         else:
0195             node.posXYZ = None
0196         pass
0197 
0198         log.debug("################ node.posXYZ:%r  node:%r ##" % (node.posXYZ, node) )
0199 
0200         ## HMM ? is this missing node.lv transforms ? See ddbase.py Elem._get_children
0201         #node.dump("visitWrap_")
0202         return node
0203 
0204     def __init__(self, volpath):
0205         """
0206         :param volpath: list of volume instances thru the volume tree
0207 
0208         Each node in the derived tree corresponds to two levels of the 
0209         source XML nodes tree, ie the lv and pv.
0210         So pdigest from backing up two levels gives access to parent node.
0211         """
0212         self.volpath = volpath
0213         self.digest = self.md5digest( volpath[0:len(volpath)] )
0214         self.pdigest = self.md5digest( volpath[0:len(volpath)-2] ) # parent digest 
0215 
0216         # Node constituents are set by Tree
0217         self.parent = None
0218         self.index = None
0219         self.posXYZ = None
0220         self.children = []
0221         self.lv = None
0222         self.pv = None
0223         self.depth = None
0224 
0225 
0226     def _get_boundary(self):
0227         """
0228         ::
0229 
0230             In [23]: target.lv.material.shortname
0231             Out[23]: 'StainlessSteel'
0232 
0233             In [24]: target.parent.lv.material.shortname
0234             Out[24]: 'IwsWater'
0235 
0236 
0237         What about root volume
0238 
0239         * for actual root, the issue is mute as world boundary is not a real one
0240         * but for sub-roots maybe need use input, actually its OK as always parse 
0241           the entire GDML file
0242 
0243         """
0244         omat = 'Vacuum' if self.parent is None else self.parent.lv.material.shortname 
0245         osur = ""
0246         isur = ""
0247         imat = self.lv.material.shortname
0248         return "/".join([omat,osur,isur,imat])
0249     boundary = property(_get_boundary)
0250 
0251 
0252 
0253     def visit(self, depth):
0254         log.info("visit depth %s %s " % (depth, repr(self)))
0255 
0256     def traverse(self, depth=0):
0257         self.visit(depth)
0258         for child in self.children:
0259             child.traverse(depth+1)
0260 
0261     def selection_traverse_r(self, query, depth=0, recursive_select_=False):
0262         """
0263         Unclear what is the appropriate name ? pv.name is not unique for instances
0264         but not currently used.
0265         """
0266         #print "selection_traverse ", self.index, self.pv.name, depth
0267         selected, recursive_select_ = query.selected(self.pv.name, self.index, depth, recursive_select_=recursive_select_)
0268         if selected:
0269             self.__class__.selected_count+= 1 
0270             self.selected = 1
0271             #log_info("selected index %5d depth %2d name %s mat %s so %s" % (self.index, depth, self.pv.name, self.lv.material.shortname, self.lv.solid.name)) 
0272         else:
0273             self.selected = 0
0274         pass 
0275         for child in self.children:
0276             child.selection_traverse_r(query, depth+1, recursive_select_=recursive_select_)
0277         pass
0278 
0279 
0280     def rprogeny(self, maxdepth=0, maxnode=0):
0281         """
0282         :return list of nodes:  
0283         """
0284         progeny = []
0285         skip = dict(total=0,count=0,depth=0)
0286 
0287         def progeny_r(node, depth):
0288             count_ok = maxnode == 0 or len(progeny) < maxnode
0289             depth_ok = maxdepth == 0 or depth < maxdepth 
0290             if count_ok and depth_ok:
0291                 progeny.append(node) 
0292             else:
0293                 skip["total"] += 1
0294                 if not count_ok: skip["count"] += 1 
0295                 if not depth_ok: skip["depth"] += 1 
0296             pass
0297             for child in node.children:
0298                 progeny_r(child, depth+1)
0299             pass
0300         pass
0301 
0302         progeny_r(self, 0)
0303         pass
0304         log.info("rprogeny numProgeny:%s (maxnode:%s maxdepth:%s skip:%r ) " % (len(progeny),maxnode,maxdepth,skip ))
0305         return progeny
0306 
0307     def add_child(self, child):
0308         log.debug("add_child %s " % repr(child))
0309         self.children.append(child)
0310 
0311     def filter_children_by_lvn(self, lvn):
0312         return filter(lambda node:node.lv.name.startswith(lvn), self.children )
0313 
0314     siblings = property(lambda self:self.parent.filter_children_by_lvn(self.lv.name), doc="siblings of this node with same lv" )
0315 
0316     def find_nodes_lvn(self, lvn):
0317         selection = []
0318         def find_nodes_lvn_r(node):
0319             if node.lv.name.startswith(lvn):
0320                 selection.append(node)
0321             for child in node.children:
0322                 find_nodes_lvn_r(child)
0323             pass     
0324         pass
0325         find_nodes_lvn_r(self)
0326         return selection
0327 
0328     def find_nodes_nchild(self, nchild):
0329         selection = []
0330         def find_nodes_nchild_r(node):
0331             if len(node.children) > nchild:
0332                 selection.append(node)
0333             for child in node.children:
0334                 find_nodes_nchild_r(child)
0335             pass     
0336         pass
0337         find_nodes_nchild_r(self)
0338         return selection
0339 
0340 
0341     def analyse_child_lv_occurrence(self):
0342         """
0343         """
0344         lvn = [c.lv.name for c in self.children]
0345         # lvc = Counter(lvn) 
0346         # for k,v in sorted(lvc.items(), key=lambda kv:kv[1]):
0347         #     print " %5d : %s " % (v, k )
0348 
0349 
0350     def dump(self, msg="Node.dump"):
0351         log.info(msg + " " + repr(self))
0352         #print "\n".join(map(str, self.geometry))   
0353 
0354     nchild = property(lambda self:len(self.children))
0355     name = property(lambda self:"Node %2d : dig %s pig %s depth %s nchild %s " % (self.index, self.digest[:4], self.pdigest[:4], self.depth, self.nchild) )
0356 
0357     brief = property(lambda self:"%5d : %40s %s " % (self.index, self.lv.name, self.pv.name))
0358 
0359     def __repr__(self):
0360         return "%s" % (self.name) 
0361 
0362     def __str__(self):
0363         return "%s \npv:%s\nlv:%s : %s " % (self.name, repr(self.pv),repr(self.lv), repr(self.posXYZ) ) 
0364 
0365 
0366 
0367     def _get_meta(self):
0368         m = {}
0369         m['treeindex'] = self.index
0370         m['nchild'] = self.nchild
0371         m['depth'] = self.depth
0372 
0373         m['digest'] = self.digest
0374         m['pdigest'] = self.pdigest
0375         m['lvname'] = self.lv.name
0376         m['pvname'] = self.pv.name
0377         m['soname'] = self.lv.solid.name
0378         return m 
0379     meta = property(_get_meta)
0380 
0381 
0382 
0383 
0384 class Tree(object):
0385     """
0386     Following the pattern used in assimpwrap-/AssimpTree 
0387     creates paired volume tree::
0388 
0389          (pv,lv)/(pv,lv)/ ...
0390 
0391     Which is more convenient to work with than the 
0392     striped volume tree obtained from the XML parse
0393     (with Elem wrapping) of form:
0394 
0395          pv/lv/pv/lv/.. 
0396 
0397     Note that the point of this is to create a tree at the 
0398     desired granularity (with nodes encompassing PV and LV)
0399     which can be serialized into primitives for analytic geometry ray tracing.
0400 
0401     """
0402     registry = {}
0403     byindex = {}
0404 
0405     @classmethod
0406     def clear(cls):
0407         cls.registry.clear()
0408         cls.byindex.clear()
0409 
0410     @classmethod
0411     def lookup(cls, digest):
0412         return cls.registry.get(digest, None)  
0413 
0414     @classmethod
0415     def get(cls, index):
0416         return cls.byindex.get(index, None)  
0417 
0418     @classmethod
0419     def filternodes_pv(cls, pfx):
0420         return list(filter(lambda node:node.pv.name.startswith(pfx), cls.byindex.values()))
0421 
0422     @classmethod
0423     def filternodes_lv(cls, pfx):
0424         return list(filter(lambda node:node.lv.name.startswith(pfx), cls.byindex.values()))
0425 
0426     @classmethod
0427     def filternodes_so(cls, pfx):
0428         """
0429         NB this only finds top level solids 
0430         ::
0431         
0432             t.filternodes_so("gds0xc28d3f0")  # works
0433             t.filternodes_so("gds_polycone0xc404f40")  # nope thats hidden in union
0434 
0435         """
0436         def filter_lv_solid(node):
0437             return node.lv.solid is not None and node.lv.solid.name.startswith(pfx)  
0438         pass
0439         return list(filter(filter_lv_solid, cls.byindex.values()))
0440 
0441 
0442     @classmethod
0443     def findnode_lv(cls, lvn, idx=0):
0444         """
0445         TODO: use the idx within filternodes for short circuiting 
0446         """
0447         nodes = cls.filternodes_lv(lvn)    
0448 
0449         numNodes = len(nodes) 
0450         log.info("found %s nodes with lvn(LV name prefix) starting:%s " % (numNodes, lvn))
0451         if not idx < numNodes:
0452              log.warning("requested node index idx %s not within numNodes %s " % (idx, numNodes)) 
0453         pass
0454 
0455         targetNode = nodes[idx] if idx < numNodes else None 
0456 
0457         #log.info("selected targetNode:[%s]\n%r " % (idx, targetNode)) 
0458         return targetNode
0459 
0460     @classmethod
0461     def findnode(cls, sel, idx=None):
0462         try:
0463             isel = int(sel)
0464         except ValueError:
0465             isel = None 
0466         pass
0467 
0468         if isel is not None:  ## integer node lookup
0469             targetNode = cls.get(isel)
0470         else:
0471             targetNode = cls.findnode_lv(sel, idx)
0472         pass
0473 
0474         if targetNode is None:
0475             log.warning("failed to findnode with sel %s and idx %s " % (sel, idx))
0476             return None
0477         return targetNode
0478 
0479 
0480     @classmethod
0481     def subtree(cls, sel, maxdepth=0, maxnode=0, idx=0):
0482         """
0483         :param sel: lvn prefix or integer tree index
0484         :param maxdepth: node depth limit
0485         :param maxnode: node count limit
0486         :param idx: used to pick target node within an lvn selection that yields multiple nodes
0487 
0488         :return progeny: recursively obtained flat list of all progeny nodes including selected arget node 
0489 
0490 
0491         TODO: review similar node selection code from previous DAE based approach
0492         """
0493         targetNode = cls.findnode(sel, idx)
0494         pass
0495         progeny = targetNode.rprogeny(maxdepth, maxnode)   # including the idxNode 
0496         return progeny
0497 
0498     @classmethod
0499     def description(cls):
0500         return "\n".join(["%s : %s " % (k,v) for k,v in cls.byindex.items()])
0501 
0502     @classmethod
0503     def dump(cls):
0504         print(cls.description())
0505 
0506     @classmethod
0507     def num_nodes(cls):
0508         assert len(cls.registry) == len(cls.byindex)
0509         return len(cls.registry)
0510 
0511     def __call__(self, arg):
0512         if type(arg) is int:
0513             return self.get(arg)
0514         elif type(arg) is slice:
0515             return [self.get(index) for index in range(arg.start, arg.stop, arg.step or 1 )]
0516         else:
0517              log.warning("expecting int or slice")
0518              return None
0519 
0520 
0521     def __getitem__(self, arg):
0522         slice_ = arg if type(arg) is slice else slice(arg,arg+1,1) 
0523         self.slice_ = slice_ 
0524         return self
0525 
0526     typ = property(lambda self:self.__class__.__name__)
0527 
0528     def __repr__(self):
0529         nn = self.num_nodes()
0530         smry  =  "%s num_nodes %s " % (self.typ, nn)
0531         lines = [smry]
0532         s = self.slice_
0533         if s is not None:
0534             step = s.step if s.step is not None else 1
0535             lines.extend(map(lambda index:repr(self.byindex[index]), range(s.start,s.stop,step)))
0536         pass
0537         return "\n\n".join(lines)
0538 
0539 
0540     def traverse(self):
0541         self.root.traverse()
0542 
0543     def find_crowds(self, minchild=22):
0544         return self.root.find_nodes_nchild(minchild)
0545 
0546     def analyse_crowds(self):
0547         nn = self.find_crowds()
0548         for n in nn:
0549             print("%s %d " % (n.lv.name, len(n.children)))
0550             n.analyse_child_lv_occurrence()
0551             
0552     def apply_selection(self, query):
0553         """
0554         Applying volume selection query whilst creating the Node tree is not 
0555         easy as the source GDML tree us stripped lv/pv/lv tree 
0556 
0557         Easier to do it in a traverse after the tree is created
0558         """     
0559         assert Node.selected_count == 0 
0560         self.root.selection_traverse_r(query)
0561         log.info("apply_selection %r Node.selected_count %d " % (query, Node.selected_count ))
0562         query.check_selected_count(Node.selected_count)
0563 
0564         self.query = query 
0565 
0566 
0567     def __init__(self, base):
0568         """
0569         :param base: top ddbase.Elem or gdml.G instance of lv of interest, eg lvPmtHemi
0570         """
0571         self.clear()  # prevent interactive re-running from doubling up nodes
0572 
0573         self.lvtype = base.lvtype
0574         self.pvtype = base.pvtype
0575         self.postype = base.postype
0576         assert self.postype and self.lvtype and self.pvtype
0577 
0578         self.slice_ = None
0579 
0580         self.base = base
0581 
0582         top = DummyTopPV()
0583         ancestors = [top] # dummy to regularize striping TOP-LV-PV-LV-PV
0584         self.root = self.create_r(self.base, ancestors)
0585 
0586 
0587     def create_r(self, vol, ancestors):
0588         """
0589         :param vol: lv logical volume 
0590         :param ancestors: list of ancestors of the vol but not including it, starting from top
0591                           eg TOP-LV-PV 
0592 
0593         Source tree traversal creating nodes as desired in destination tree
0594 
0595         #. vital to make a copy with [:] as need separate volpath for every node
0596 
0597         #. only form destination nodes at Logvol points in the tree
0598 
0599         #. this is kept simple as the parent digest approach to tree hookup
0600            means that the Nodes assemble themselves into the tree, just need
0601            to create nodes where desired and make sure to traverse the entire 
0602            source tree
0603         """
0604         volpath = ancestors[:] 
0605         volpath.append(vol) 
0606 
0607         node = None
0608         if type(volpath[-1]).__name__ == self.lvtype:
0609             node = Node.create(volpath, lvtype=self.lvtype, pvtype=self.pvtype, postype=self.postype )
0610         pass
0611 
0612         for child in vol.children:
0613             self.create_r(child, volpath)
0614         pass 
0615         return node
0616 
0617 
0618 
0619 
0620 
0621 if __name__ == '__main__':
0622 
0623     args = opticks_main()
0624 
0625     from opticks.ana.pmt.ddbase import Dddb
0626 
0627     g = Dddb.parse(args.apmtddpath)
0628 
0629     lv = g.logvol_("lvPmtHemi")
0630 
0631     tr = Tree(lv)
0632 
0633 
0634 
0635 
0636