File indexing completed on 2026-04-09 07:48:52
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
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
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"
0069
0070
0071 def _get_transform(self):
0072
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
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
0201
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] )
0215
0216
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
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
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
0346
0347
0348
0349
0350 def dump(self, msg="Node.dump"):
0351 log.info(msg + " " + repr(self))
0352
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
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:
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)
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()
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]
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