File indexing completed on 2026-04-09 07:48:51
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019
0020
0021 """
0022 gdml.py : parsing GDML
0023 =========================
0024
0025
0026 """
0027 import os, re, logging, math, collections, argparse
0028
0029 log = logging.getLogger(__name__)
0030
0031
0032
0033 from opticks.ana.key import key_
0034 from opticks.ana.base import u_, b_, d_
0035 from opticks.ana.nbase import find_ranges, np_fromstring, np_tostring, np_digest
0036 from opticks.ana.shape import X, SEllipsoid, STubs, STorus, SCons, SPolycone, SSubtractionSolid, SUnionSolid, SIntersectionSolid
0037
0038 from opticks.analytic.csg import CSG
0039 from opticks.analytic.treebuilder import TreeBuilder
0040 from opticks.analytic.glm import make_trs, make_transform
0041 from opticks.analytic.prism import make_trapezoid
0042
0043
0044 import numpy as np
0045 import lxml.etree as ET
0046 import lxml.html as HT
0047
0048
0049 tostring_ = lambda _:ET.tostring(_)
0050 exists_ = lambda _:os.path.exists(os.path.expandvars(_))
0051 parse_ = lambda _:ET.parse(os.path.expandvars(_)).getroot()
0052 fparse_ = lambda _:HT.fragments_fromstring(file(os.path.expandvars(_)).read())
0053 pp_ = lambda d:"\n".join([" %30s : %f " % (k,d[k]) for k in sorted(d.keys())])
0054
0055 unref_ = lambda n:n[:-9] if n[-9:-7] == '0x' else n
0056
0057
0058 def construct_transform(obj):
0059 tla = obj.position.xyz if obj.position is not None else None
0060 rot = obj.rotation.xyz if obj.rotation is not None else None
0061 sca = obj.scale.xyz if obj.scale is not None else None
0062 order = "trs"
0063
0064
0065
0066
0067
0068
0069 return make_transform( order, tla, rot, sca , three_axis_rotate=True, transpose_rotation=True, suppress_identity=False, dtype=np.float32 )
0070
0071
0072 class G(object):
0073 """
0074 Base wrapper type for lxml parsed GDML elements
0075 """
0076 pvtype = 'Physvol'
0077 lvtype = 'Volume'
0078 postype = 'position'
0079
0080 typ = property(lambda self:self.__class__.__name__)
0081 name = property(lambda self:self.elem.attrib.get('name',None))
0082 xml = property(lambda self:tostring_(self.elem))
0083 gidx = property(lambda self:"[%d]" % self.idx if hasattr(self, 'idx') else '[]' )
0084
0085
0086
0087 is_primitive = property(lambda self:issubclass(self.__class__ , Primitive ))
0088 is_boolean = property(lambda self:issubclass(self.__class__ , Boolean ))
0089
0090 def _get_shortname(self):
0091 """/dd/Geometry/PMT/lvPmtHemi0xc133740 -> lvPmtHemi"""
0092 base = self.name.split("/")[-1]
0093 return unref_(base)
0094 shortname = property(_get_shortname)
0095
0096 def att(self, name, default=None, typ=None):
0097 assert typ is not None
0098 v = self.elem.attrib.get(name, default)
0099 return typ(v)
0100
0101 def __init__(self, elem, g=None):
0102 """
0103 :param elem: lxml element
0104 :param g:
0105 """
0106 self.elem = elem
0107 self.g = g
0108
0109 def findall_(self, expr):
0110 """
0111 lxml findall result elements are wrapped in the class appropriate to their tags,
0112 note the global g gets passed into all Elem
0113
0114 g.kls is a dict associating tag names to classes, Elem
0115 is fallback, all the classes have signature of elem-instance, g
0116
0117 """
0118 wrap_ = lambda e:self.g.kls.get(e.tag,G)(e,self.g)
0119 fa = list(map(wrap_, self.elem.xpath(expr) ))
0120 kln = self.__class__.__name__
0121 name = self.name
0122 log.debug("findall_ from %s:%s expr:%s returned %s " % (kln, name, expr, len(fa)))
0123 return fa
0124
0125 def findone_(self, expr):
0126 """simple dumb lxml find"""
0127 all_ = self.findall_(expr)
0128 assert len(all_) == 1
0129 return all_[0]
0130
0131 def find1_(self, expr):
0132 all_ = self.findall_(expr)
0133 assert len(all_) in [0,1]
0134 return all_[0] if len(all_) == 1 else None
0135
0136 def find_(self, expr):
0137 e = self.elem.find(expr)
0138 wrap_ = lambda e:self.g.kls.get(e.tag,G)(e,self.g)
0139 return wrap_(e) if e is not None else None
0140
0141 def __repr__(self):
0142 return "path %s path_label %s " % (self.path, self.path_label)
0143
0144 def __repr__0(self):
0145 return "%15s : %s " % ( self.elem.tag, repr(self.elem.attrib) )
0146
0147 def __str__(self):
0148 return self.xml
0149
0150
0151
0152 class Material(G):
0153 state = property(lambda self:self.att('state', '', typ=str ))
0154 def __repr__(self):
0155 return "%s %s %s %s" % (self.gidx, self.typ, self.name, self.state )
0156
0157
0158 class Transform(G):
0159 unit = property(lambda self:self.att('unit', "", typ=str ))
0160 x = property(lambda self:self.att('x', 0, typ=float))
0161 y = property(lambda self:self.att('y', 0, typ=float))
0162 z = property(lambda self:self.att('z', 0, typ=float))
0163 xyz = property(lambda self:[self.x, self.y, self.z] )
0164
0165 def __repr__(self):
0166 return "%s %s %s %s %s " % (self.typ, self.unit, self.x, self.y, self.z )
0167
0168 class Position(Transform):
0169 pass
0170 class Rotation(Transform):
0171 pass
0172 class Scale(Transform):
0173 pass
0174
0175
0176
0177 class Geometry(G):
0178 """
0179 Boolean and Primitive are Geometry subclasses
0180 """
0181 def as_ncsg(self):
0182 assert 0, "Geometry.as_ncsg needs to be overridden in the subclass: %s " % self.__class__
0183
0184 def as_shape(self, **kwa):
0185 assert 0, "Geometry.as_shape needs to be overridden in the subclass: %s " % self.__class__
0186
0187 def _get_subsolids(self):
0188 ss = []
0189 def subsolids_r(solid, top=False):
0190 if not top:
0191 ss.append(solid.idx)
0192 pass
0193 if solid.is_boolean:
0194 subsolids_r(solid.first)
0195 subsolids_r(solid.second)
0196 pass
0197 pass
0198 subsolids_r(self, top=True)
0199 return ss
0200 subsolids = property(_get_subsolids)
0201 subsolidranges = property(lambda self:list(find_ranges(sorted(self.subsolids))))
0202 subsolidcount = property(lambda self:len(self.subsolids))
0203
0204 def find_solids(self, klsname):
0205 """
0206 GDML boolean aware find
0207
0208 :param klsname: eg Union, Ellipsoid
0209 :return ss: list of solids
0210 """
0211 ss = []
0212 def find_solids_r(solid):
0213 if solid.__class__.__name__ == klsname:
0214 ss.append(solid)
0215 pass
0216 if solid.is_boolean:
0217 find_solids_r(solid.first)
0218 find_solids_r(solid.second)
0219 pass
0220 pass
0221 find_solids_r(self)
0222 return ss
0223
0224 def __repr__(self):
0225 """for indents need to sub_traverse from root solid of interest"""
0226 sub_depth = getattr(self, 'sub_depth', 0)
0227 sub_indent = " " * sub_depth
0228 sub_name = getattr(self, 'sub_name', self.name)
0229 sub_prefix = getattr(self, 'sub_prefix', None)
0230
0231 sub_root = getattr(self, 'sub_root', None)
0232 is_sub_root = sub_root == self
0233 sub_root_marker = "[%s]" % sub_prefix if sub_root == self else ""
0234
0235 right_xyz = self.right_xyz if self.is_boolean else None
0236 right_xyz = " : right_xyz:%s/%s/%6.3f" % tuple(right_xyz) if not right_xyz is None else ""
0237
0238 line = "%d%s %s %s %s%s " % (sub_depth, sub_indent, self.gidx, self.typ, sub_root_marker, sub_name )
0239 line = "%-45s %s" % (line, right_xyz )
0240
0241 if is_sub_root:
0242 line = "*:%s" % line
0243 pass
0244
0245 lrep_ = lambda label,obj:"%s:%r"%(label,obj)
0246 lines = [line]
0247 if self.is_boolean:
0248 lines.append( lrep_("l",self.first) )
0249 lines.append( lrep_("r",self.second) )
0250 pass
0251 return "\n".join(lines)
0252
0253
0254
0255 class Boolean(Geometry):
0256 all_transforms = []
0257
0258 @classmethod
0259 def SaveBuffer(cls):
0260 num_boolean_transforms = len(cls.all_transforms)
0261 path = os.path.expandvars("$TMP/Boolean_all_transforms.npy")
0262 print("Boolean.all_transforms %d save to %s " % ( num_boolean_transforms, path ))
0263 if num_boolean_transforms > 0:
0264 tbuf = np.vstack(cls.all_transforms).reshape(-1,4,4)
0265 np.save(path, tbuf)
0266 pass
0267 pass
0268
0269 firstref = property(lambda self:self.elem.find("first").attrib["ref"])
0270 secondref = property(lambda self:self.elem.find("second").attrib["ref"])
0271
0272 position = property(lambda self:self.find1_("position"))
0273 rotation = property(lambda self:self.find1_("rotation"))
0274 scale = None
0275 secondtransform = property(lambda self:construct_transform(self))
0276
0277 first = property(lambda self:self.g.solids[self.firstref])
0278 second = property(lambda self:self.g.solids[self.secondref])
0279
0280 @classmethod
0281 def SubTraverse(cls, root):
0282 """
0283 :param root: Geometry instance, Boolean or Primitive
0284
0285 Traverses from root setting "temporary" properties, sub_root, sub_depth, sub_name
0286
0287 As solids are reused within others cannot define a fixed
0288 depth or even a fixed parent link.
0289 Instead can only define temporary depths and parents within
0290 the context of a traversal from some root.
0291
0292 """
0293 names = []
0294 def collect_names_r(node, depth):
0295 names.append(node.name)
0296 if node.is_boolean:
0297 collect_names_r(node.first, depth+1)
0298 collect_names_r(node.second, depth+1)
0299 pass
0300 pass
0301 collect_names_r(root, 0)
0302 prefix = os.path.commonprefix(names)
0303 log.debug("\n".join(["names", "prefix:%s" % prefix] + names))
0304
0305 subnames = []
0306 def sub_traverse_r(node, depth):
0307
0308 sub_name = unref_(node.name[len(prefix):])
0309 subnames.append(sub_name)
0310
0311 node.sub_root = root
0312 node.sub_depth = depth
0313 node.sub_prefix = prefix
0314 node.sub_name = sub_name
0315
0316 if node.is_boolean:
0317 node.first.sub_parent = node
0318 node.second.sub_parent = node
0319 sub_traverse_r(node.first, depth+1)
0320 sub_traverse_r(node.second, depth+1)
0321 pass
0322 pass
0323 sub_traverse_r(root, 0)
0324 log.debug("\n".join(["subnames"] + subnames))
0325
0326 def sub_traverse(self):
0327 self.SubTraverse(self)
0328
0329 def as_ncsg(self):
0330 if not hasattr(self.first, 'as_ncsg'):
0331 print(self.first)
0332 pass
0333 left = self.first.as_ncsg()
0334 right = self.second.as_ncsg()
0335
0336 assert left, " left fail as_ncsg for first : %r self: %r " % (self.first, self)
0337 assert right, "right fail as_ncsg for second : %r self: %r " % (self.second, self)
0338
0339 transform = self.secondtransform
0340
0341 right.transform = transform
0342 self.__class__.all_transforms.append(transform)
0343
0344 cn = CSG(self.operation, name=self.name)
0345 cn.left = left
0346 cn.right = right
0347 return cn
0348
0349 def _get_right_xyz(self):
0350 transform = self.secondtransform
0351 no_rotation = np.all(np.eye(3) == transform[:3,:3])
0352 assert no_rotation, transform
0353 xyz = transform[3,:3]
0354 return xyz
0355 right_xyz = property(_get_right_xyz)
0356
0357 def as_shape(self, **kwa):
0358 """
0359 Only looking at single level of transform
0360 """
0361 left = self.first.as_shape(**kwa)
0362 right = self.second.as_shape(**kwa)
0363 transform = self.secondtransform
0364
0365 no_rotation = np.all(np.eye(3) == transform[:3,:3])
0366 assert no_rotation, transform
0367 xyz = transform[3,:3]
0368
0369 log.debug(xyz)
0370
0371 no_xy_shifts = xyz[0] == xyz[1] == 0
0372 if not no_xy_shifts:
0373 log.fatal("xy_shifts not handled %s " % repr(xyz))
0374 pass
0375
0376
0377
0378 assert left, " left fail as_shape for first : %r self: %r " % (self.first, self)
0379 assert right, "right fail as_shape for second : %r self: %r " % (self.second, self)
0380
0381
0382 tr = np.array( [xyz[0], xyz[2]] )
0383 shape = self.shape_kls(self.name, [left, right, tr], **kwa)
0384 return shape
0385
0386
0387 class Intersection(Boolean):
0388 operation = "intersection"
0389 shape_kls = SIntersectionSolid
0390
0391 class Subtraction(Boolean):
0392 operation = "difference"
0393 shape_kls = SSubtractionSolid
0394
0395 class Union(Boolean):
0396 operation = "union"
0397 shape_kls = SUnionSolid
0398
0399 class Primitive(Geometry):
0400 lunit = property(lambda self:self.att('lunit', 'mm', typ=str))
0401 aunit = property(lambda self:self.att('aunit', 'deg', typ=str))
0402 startphi = property(lambda self:self.att('startphi', 0, typ=float))
0403 deltaphi = property(lambda self:self.att('deltaphi', 360, typ=float))
0404 starttheta = property(lambda self:self.att('starttheta', 0, typ=float))
0405 deltatheta = property(lambda self:self.att('deltatheta', 180, typ=float))
0406 rmin = property(lambda self:self.att('rmin', 0, typ=float))
0407 rmax = property(lambda self:self.att('rmax', 0, typ=float))
0408
0409 x = property(lambda self:self.att('x', 0, typ=float))
0410 y = property(lambda self:self.att('y', 0, typ=float))
0411 z = property(lambda self:self.att('z', 0, typ=float))
0412
0413 @classmethod
0414 def fromstring(cls, st ):
0415 gg = GDML.fromstring("<gdml><solids>" + st + "</solids></gdml>")
0416 so = gg.solids(-1)
0417 return so
0418
0419 @classmethod
0420 def deltaphi_segment_via_slab_DEPRECATED(cls, obj, phi0, phi1, dist):
0421 xyzw_ = lambda phi:(np.cos(phi*np.pi/180.), np.sin(phi*np.pi/180.),0,0)
0422
0423 slab_a = 0
0424 slab_b = dist
0425
0426 slab0 = CSG("slab", param=xyzw_(phi0+90),param1=[slab_a,slab_b,0,0] )
0427 slab1 = CSG("slab", param=xyzw_(phi1-90),param1=[slab_a,slab_b,0,0] )
0428
0429
0430
0431 obj_slab0 = CSG("intersection", left=obj, right=slab0 )
0432 obj_slab0_slab1 = CSG("intersection", left=obj_slab0, right=slab1 )
0433
0434 return obj_slab0_slab1
0435
0436 def __repr__0(self):
0437 depth = getattr(self, 'depth', 0)
0438 indent = " " * depth
0439 return "%d:%s %s %s %s %s rmin %s rmax %s x %s y %s z %s " % (depth, indent, self.gidx, self.typ, self.name, self.lunit, self.rmin, self.rmax, self.x, self.y, self.z)
0440
0441 def __repr__(self):
0442 grepr = Geometry.__repr__(self)
0443 return "%-45s : xyz %s,%s,%5.3f " % (grepr, self.x, self.y, self.z)
0444
0445
0446 class Tube(Primitive):
0447 """
0448 G4Tubs is GDML serialized as tube
0449 """
0450 deltaphi_segment_enabled = True
0451
0452 def __repr__(self):
0453 hz = self.z/2.
0454 prepr = Primitive.__repr__(self)
0455 return "%-80s : rmin %s rmax %6.3f hz %6.3f " % (prepr, self.rmin, self.rmax, hz )
0456
0457
0458 @classmethod
0459 def make_cylinder(cls, radius, z1, z2, name):
0460 cn = CSG("cylinder", name=name)
0461 cn.param[0] = 0
0462 cn.param[1] = 0
0463 cn.param[2] = 0
0464 cn.param[3] = radius
0465 cn.param1[0] = z1
0466 cn.param1[1] = z2
0467 return cn
0468
0469 @classmethod
0470 def make_disc(cls, x, y, inner, radius, z1, z2, name):
0471 cn = CSG("disc", name=name)
0472 cn.param[0] = x
0473 cn.param[1] = y
0474 cn.param[2] = inner
0475 cn.param[3] = radius
0476 cn.param1[0] = z1
0477 cn.param1[1] = z2
0478 return cn
0479
0480 def as_cylinder(self, nudge_inner=0.01):
0481 hz = self.z/2.
0482 has_inner = self.rmin > 0.
0483
0484 if has_inner:
0485 dz = hz*nudge_inner
0486 inner = self.make_cylinder(self.rmin, -(hz+dz), (hz+dz), self.name + "_inner")
0487 else:
0488 inner = None
0489 pass
0490 outer = self.make_cylinder(self.rmax, -hz, hz, self.name + "_outer" )
0491 tube = CSG("difference", left=outer, right=inner, name=self.name + "_difference" ) if has_inner else outer
0492
0493 has_deltaphi = self.deltaphi < 360
0494 if has_deltaphi and self.deltaphi_segment_enabled:
0495
0496 assert self.aunit == 'deg'
0497 phi0 = self.startphi
0498 phi1 = self.startphi + self.deltaphi
0499 sz = self.z*1.01
0500 sr = self.rmax*1.5
0501
0502
0503
0504
0505 segment = CSG.MakeSegment(phi0, phi1, sz, sr )
0506 log.info("as_cylinder doing phi0/phi1/sz/sr segmenting : name %s phi0 %s phi1 %s sz %s sr %s " % (self.name, phi0, phi1, sz, sr))
0507 tube_segment = CSG("intersection", left=tube, right=segment )
0508
0509
0510
0511
0512 result = tube_segment
0513 else:
0514 result = tube
0515 pass
0516 return result
0517
0518 def as_disc(self):
0519 hz = self.z/2.
0520 return self.make_disc( self.x, self.y, self.rmin, self.rmax, -hz, hz, self.name + "_disc" )
0521
0522 def as_ncsg(self, hz_disc_cylinder_cut=1.):
0523 hz = self.z/2.
0524 rmin = self.rmin
0525 rmax = self.rmax
0526 pick_disc = hz < hz_disc_cylinder_cut
0527 if pick_disc:
0528 log.debug("Tube.as_ncsg.CSG_DISC %s as hz < cut, hz:%s cut:%s rmin:%s rmax:%s " % (self.name, hz, hz_disc_cylinder_cut, rmin, rmax))
0529 pass
0530 return self.as_disc() if pick_disc else self.as_cylinder()
0531
0532 def as_shape(self, **kwa):
0533 hz = self.z/2.
0534 shape = STubs(self.name, [self.rmax, hz], **kwa )
0535 return shape
0536
0537
0538 class Sphere(Primitive):
0539 deltaphi_slab_segment_enabled = False
0540
0541 def __repr__(self):
0542 prepr = Primitive.__repr__(self)
0543 return "%-80s : rmin %s rmax %6.3f " % (prepr, self.rmin, self.rmax )
0544
0545 def as_shape(self, **kwa):
0546 shape = SEllipsoid(self.name, [self.rmax, self.rmax], **kwa )
0547 return shape
0548
0549 def as_ncsg(self, only_inner=False):
0550 pass
0551 assert self.aunit == "deg" and self.lunit == "mm"
0552
0553 has_inner = not only_inner and self.rmin > 0.
0554 if has_inner:
0555 inner = self.as_ncsg(only_inner=True)
0556 pass
0557
0558 radius = self.rmin if only_inner else self.rmax
0559 assert radius is not None
0560
0561 startThetaAngle = self.starttheta
0562 deltaThetaAngle = self.deltatheta
0563
0564 x = 0
0565 y = 0
0566 z = 0
0567
0568
0569 rTheta = startThetaAngle
0570 lTheta = startThetaAngle + deltaThetaAngle
0571
0572 assert rTheta >= 0. and rTheta <= 180.
0573 assert lTheta >= 0. and lTheta <= 180.
0574
0575 log.debug("Sphere.as_ncsg radius:%s only_inner:%s has_inner:%s " % (radius, only_inner, has_inner))
0576
0577 zslice = startThetaAngle > 0. or deltaThetaAngle < 180.
0578
0579 if zslice:
0580 zmin = radius*math.cos(lTheta*math.pi/180.)
0581 zmax = radius*math.cos(rTheta*math.pi/180.)
0582 assert zmax > zmin, (startThetaAngle, deltaThetaAngle, rTheta, lTheta, zmin, zmax )
0583
0584 log.debug("Sphere.as_ncsg rTheta:%5.2f lTheta:%5.2f zmin:%5.2f zmax:%5.2f azmin:%5.2f azmax:%5.2f " % (rTheta, lTheta, zmin, zmax, z+zmin, z+zmax ))
0585
0586 cn = CSG("zsphere", name=self.name, param=[x,y,z,radius], param1=[zmin,zmax,0,0], param2=[0,0,0,0] )
0587
0588 ZSPHERE_QCAP = 0x1 << 1
0589 ZSPHERE_PCAP = 0x1 << 0
0590 flags = ZSPHERE_QCAP | ZSPHERE_PCAP
0591
0592 cn.param2.view(np.uint32)[0] = flags
0593 pass
0594 else:
0595 cn = CSG("sphere", name=self.name)
0596 cn.param[0] = x
0597 cn.param[1] = y
0598 cn.param[2] = z
0599 cn.param[3] = radius
0600 pass
0601 if has_inner:
0602 ret = CSG("difference", left=cn, right=inner, name=self.name + "_difference" )
0603 else:
0604 ret = cn
0605 pass
0606
0607 has_deltaphi = self.deltaphi < 360
0608 if has_deltaphi and not only_inner and self.deltaphi_slab_segment_enabled:
0609 assert self.aunit == 'deg'
0610 phi0 = self.startphi
0611 phi1 = self.startphi + self.deltaphi
0612 rmax = self.rmax + 1
0613 ret_segment = self.deltaphi_slab_segment(ret, phi0, phi1, rmax)
0614 result = ret_segment
0615 else:
0616 result = ret
0617 pass
0618
0619 return result
0620
0621
0622
0623
0624
0625 class Ellipsoid(Primitive):
0626 ax = property(lambda self:self.att('ax', 0, typ=float))
0627 by = property(lambda self:self.att('by', 0, typ=float))
0628 cz = property(lambda self:self.att('cz', 0, typ=float))
0629 zcut1 = property(lambda self:self.att('zcut1', 0, typ=float))
0630 zcut2 = property(lambda self:self.att('zcut2', 0, typ=float))
0631
0632 def _get_semi_axes(self):
0633 ax = self.ax
0634 by = self.by
0635 cz = self.cz
0636 a = np.array([ax,by,cz], dtype=np.float32)
0637 return a
0638 semi_axes = property(_get_semi_axes)
0639
0640 def as_ncsg(self):
0641 ax = self.semi_axes
0642 cn = CSG.MakeEllipsoid(axes=ax, name=self.name, zcut1=self.zcut1, zcut2=self.zcut2)
0643 return cn
0644
0645 def as_shape(self, **kwa):
0646 assert self.ax == self.by
0647 shape = SEllipsoid(self.name, [self.ax, self.cz], **kwa )
0648 return shape
0649
0650 def __repr__(self):
0651 prepr = Primitive.__repr__(self)
0652 return "%-80s : ax/by/cz %6.3f/%6.3f/%6.3f zcut1 %6.3f zcut2 %6.3f " % (prepr, self.ax, self.by, self.cz, self.zcut1, self.zcut2 )
0653
0654
0655
0656
0657
0658 class Torus(Primitive):
0659 rtor = property(lambda self:self.att('rtor', 0, typ=float))
0660 def as_ncsg(self):
0661 cn = CSG.MakeTorus(R=self.rtor, r=self.rmax, name=self.name)
0662 return cn
0663
0664 def as_shape(self, **kwa):
0665 shape = STorus(self.name, [self.rmax, self.rtor], **kwa)
0666 return shape
0667
0668 def __repr__(self):
0669 prepr = Primitive.__repr__(self)
0670 return "%-80s : rmin %6.3f rmax %6.3f rtor %6.3f " % (prepr, self.rmin, self.rmax, self.rtor )
0671
0672
0673
0674 class Box(Primitive):
0675 def as_ncsg(self):
0676 assert self.lunit == 'mm'
0677 cn = CSG("box3", name=self.name)
0678 cn.param[0] = self.x
0679 cn.param[1] = self.y
0680 cn.param[2] = self.z
0681 cn.param[3] = 0
0682 return cn
0683
0684 class Cone(Primitive):
0685 """
0686 GDML inner cone translated into CSG difference
0687
0688 rmin1: inner radius at base of cone
0689 rmax1: outer radius at base of cone
0690 rmin2: inner radius at top of cone
0691 rmax2: outer radius at top of cone
0692 z height of cone segment
0693
0694 """
0695 rmin1 = property(lambda self:self.att('rmin1', 0, typ=float))
0696 rmin2 = property(lambda self:self.att('rmin2', 0, typ=float))
0697
0698 rmax1 = property(lambda self:self.att('rmax1', 0, typ=float))
0699 rmax2 = property(lambda self:self.att('rmax2', 0, typ=float))
0700 z = property(lambda self:self.att('z', 0, typ=float))
0701 pass
0702
0703 @classmethod
0704 def make_cone(cls, r1,z1,r2,z2, name):
0705 cn = CSG("cone", name=name)
0706 cn.param[0] = r1
0707 cn.param[1] = z1
0708 cn.param[2] = r2
0709 cn.param[3] = z2
0710 return cn
0711
0712 def as_ncsg(self, only_inner=False):
0713 pass
0714 assert self.aunit == "deg" and self.lunit == "mm" and self.deltaphi == 360. and self.startphi == 0.
0715 has_inner = not only_inner and (self.rmin1 > 0. or self.rmin2 > 0. )
0716 if has_inner:
0717 inner = self.as_ncsg(only_inner=True)
0718 pass
0719
0720 r1 = self.rmin1 if only_inner else self.rmax1
0721 z1 = -self.z/2
0722
0723 r2 = self.rmin2 if only_inner else self.rmax2
0724 z2 = self.z/2
0725
0726 cn = self.make_cone( r1,z1,r2,z2, self.name )
0727
0728 return CSG("difference", left=cn, right=inner ) if has_inner else cn
0729
0730 def __repr__(self):
0731 return "%s %s z:%s rmin1:%s rmin2:%s rmax1:%s rmax2:%s " % (self.gidx, self.typ, self.z, self.rmin1, self.rmin2, self.rmax1, self.rmax2 )
0732
0733 def plot(self, ax):
0734 zp0 = FakeZPlane(z=0., rmin=self.rmin1,rmax=self.rmax1)
0735 zp1 = FakeZPlane(z=self.z, rmin=self.rmin2,rmax=self.rmax2)
0736 zp = [zp0, zp1]
0737 Polycone.Plot(ax, zp)
0738
0739
0740
0741 class FakeZPlane(object):
0742 def __init__(self, z, rmin, rmax):
0743 self.z = z
0744 self.rmin = rmin
0745 self.rmax = rmax
0746
0747 def __repr__(self):
0748 return "%s %s %s %s " % (self.__class__.__name__, self.z, self.rmin, self.rmax)
0749
0750
0751 class ZPlane(Primitive):
0752 pass
0753
0754
0755 class Trapezoid(Primitive):
0756 """
0757 The GDML Trapezoid is formed using 5 dimensions:
0758
0759 x1: x length at -z
0760 x2: x length at +z
0761 y1: y length at -z
0762 y2: y length at +z
0763 z: z length
0764
0765 The general form is ConvexPolyhedron, modelled by a list of planes
0766 defining half spaces which come together to define the polyhedron.
0767
0768 * 4:tetrahedron
0769 * 5:triangular prism
0770 * 6:trapezoid, cube, box
0771
0772 Q&A:
0773
0774 * where to transition from specific Trapezoid to generic ConvexPolyhedron ? Did this immediately with python input.
0775 * where to extract the bbox, thats easier before conversion to generic ? Again immediately in python.
0776
0777 * :google:`convex polyhedron bounding box`
0778
0779 Enumerating the vertices of a convex polyhedron defined by its planes
0780 looks complicated, hence need to extract and store the bbox prior
0781 to generalization of the shape.
0782
0783 * http://cgm.cs.mcgill.ca/~avis/doc/avis/AF92b.pdf
0784
0785 ::
0786
0787 In [1]: run gdml.py
0788
0789 In [2]: trs = g.findall_("solids//trd")
0790
0791 In [3]: len(trs)
0792 Out[3]: 2
0793
0794 In [13]: trs = g.findall_("solids//trd")
0795
0796 In [14]: trs
0797 Out[14]:
0798 [Trapezoid name:SstTopRadiusRibBase0xc271078 z:2228.5 x1:160.0 y1:20.0 x2:691.02 y2:20.0 ,
0799 Trapezoid name:SstInnVerRibCut0xbf31118 z:50.02 x1:100.0 y1:27.0 x2:237.2 y2:27.0 ]
0800
0801 In [4]: print trs[0]
0802 <trd xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" lunit="mm" name="SstTopRadiusRibBase0xc271078" x1="160" x2="691.02" y1="20" y2="20" z="2228.5"/>
0803
0804
0805 In [5]: print trs[1]
0806 <trd xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" lunit="mm" name="SstInnVerRibCut0xbf31118" x1="100" x2="237.2" y1="27" y2="27" z="50.02"/>
0807
0808
0809 Tips for finding LV with a solid... use filternodes_so to get some nodes
0810 then can use parent links to get idea of local tree around the solid.
0811
0812 ::
0813
0814 In [17]: sivr = t.filternodes_so("SstInnVerRib")
0815
0816 In [18]: sivr[0]
0817 Out[18]:
0818 Node 4473 : dig a60e pig 8be9 depth 11 nchild 0
0819 pv:PhysVol /dd/Geometry/AD/lvOIL#pvSstInnVerRibs#SstInnVerRibs#SstInnVerRibRot0xbf1abc0
0820 Position mm 2428.0 0.0 -40.0 None
0821 lv:Volume /dd/Geometry/AdDetails/lvSstInnVerRibBase0xbf31748 /dd/Materials/StainlessSteel0xc2adc00 SstInnVerRibBase0xbf30b50
0822 Subtraction SstInnVerRibBase0xbf30b50
0823 l:Box SstInnVerRibBox0xbf310d8 mm rmin 0.0 rmax 0.0 x 120.0 y 25.0 z 4875.0
0824 r:Trapezoid name:SstInnVerRibCut0xbf31118 z:50.02 x1:100.0 y1:27.0 x2:237.2 y2:27.0
0825 Material /dd/Materials/StainlessSteel0xc2adc00 solid : Position mm 2428.0 0.0 -40.0
0826
0827 In [26]: print list(map(lambda n:n.index, sivr))
0828 [4473, 4474, 4475, 4476, 4477, 4478, 4479, 4480, 6133, 6134, 6135, 6136, 6137, 6138, 6139, 6140]
0829
0830
0831 In [28]: sivr[0].parent
0832 Out[28]:
0833 Node 3155 : dig 8be9 pig a856 depth 10 nchild 520
0834 pv:PhysVol /dd/Geometry/AD/lvSST#pvOIL0xc241510
0835 Position mm 0.0 0.0 7.5 Rotation deg 0.0 0.0 -180.0
0836 lv:Volume /dd/Geometry/AD/lvOIL0xbf5e0b8 /dd/Materials/MineralOil0xbf5c830 oil0xbf5ed48
0837 Tube oil0xbf5ed48 mm rmin 0.0 rmax 2488.0 x 0.0 y 0.0 z 4955.0
0838 Material /dd/Materials/MineralOil0xbf5c830 solid
0839 PhysVol /dd/Geometry/AD/lvOIL#pvOAV0xbf8f638
0840
0841
0842
0843 """
0844 x1 = property(lambda self:self.att('x1', 0, typ=float))
0845 x2 = property(lambda self:self.att('x2', 0, typ=float))
0846 y1 = property(lambda self:self.att('y1', 0, typ=float))
0847 y2 = property(lambda self:self.att('y2', 0, typ=float))
0848 z = property(lambda self:self.att('z', 0, typ=float))
0849
0850 def __repr__(self):
0851 return "%s name:%s z:%s x1:%s y1:%s x2:%s y2:%s " % (self.typ, self.name, self.z, self.x1, self.y1, self.x2, self.y2 )
0852
0853 def as_ncsg(self):
0854 assert self.lunit == 'mm'
0855 cn = CSG("trapezoid", name=self.name)
0856 planes, verts, bbox, src = make_trapezoid(z=self.z, x1=self.x1, y1=self.y1, x2=self.x2, y2=self.y2 )
0857 cn.planes = planes
0858 cn.param2[:3] = bbox[0]
0859 cn.param3[:3] = bbox[1]
0860
0861 cn.meta.update(src.srcmeta)
0862
0863 return cn
0864
0865
0866 class Polycone(Primitive):
0867 """
0868 ::
0869
0870 In [1]: run gdml.py
0871
0872 In [2]: pcs = gdml.findall_("solids//polycone")
0873
0874 In [3]: len(pcs)
0875 Out[3]: 13
0876
0877 In [6]: print pcs[0]
0878 <polycone xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" aunit="deg" deltaphi="360" lunit="mm" name="gds_polycone0xc404f40" startphi="0">
0879 <zplane rmax="1520" rmin="0" z="3070"/> # really flat cone
0880 <zplane rmax="75" rmin="0" z="3145.72924106399"/> # little cylinder
0881 <zplane rmax="75" rmin="0" z="3159.43963177189"/>
0882 </polycone>
0883
0884 In [7]: print pcs[1]
0885 <polycone xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" aunit="deg" deltaphi="360" lunit="mm" name="iav_polycone0xc346448" startphi="0">
0886 <zplane rmax="1565" rmin="0" z="3085"/> # flat cylinder (repeated rmax)
0887 <zplane rmax="1565" rmin="0" z="3100"/> # repeated z, changed r <--- polycone abuse, zero height truncated cone
0888 <zplane rmax="1520.39278882354" rmin="0" z="3100"/> # big taper cone
0889 <zplane rmax="100" rmin="0" z="3174.43963177189"/>
0890 </polycone>
0891
0892
0893 Summarizing with zp, see that
0894
0895 * all using same rmin, thats polycone abuse : better to subtract a cylinder
0896 * common pattern repeated z with different rmax to model "lips"
0897
0898 ::
0899
0900 pcs[ 0].zp : gds_polycone0xc404f40 3 z: [3145.72924106399, 3159.43963177189, 3070.0] rmax: [1520.0, 75.0] rmin: [0.0]
0901 pcs[ 7].zp : SstTopHubBot0xc2635b8 2 z: [-320.0, -340.0] rmax: [220.5] rmin: [150.5]
0902 pcs[ 8].zp : SstTopHubMain0xc263d80 2 z: [-320.0, 0.0] rmax: [170.5] rmin: [150.5]
0903
0904
0905 pcs[ 1].zp : iav_polycone0xc346448 4 z: [3100.0, 3085.0, 3174.43963177189] rmax: [100.0, 1520.39278882354, 1565.0] rmin: [0.0]
0906 pcs[ 2].zp : IavTopHub0xc405968 4 z: [0.0, 85.5603682281126, 110.560368228113] rmax: [100.0, 150.0] rmin: [75.0]
0907 pcs[ 3].zp : CtrGdsOflBotClp0xbf5dec0 4 z: [0.0, 25.0, 30.0] rmax: [36.5, 150.0] rmin: [31.5]
0908 pcs[ 4].zp : OcrGdsPrtPln0xbfa1408 4 z: [0.0, 160.0, 185.0] rmax: [100.0, 150.0] rmin: [75.0]
0909 pcs[ 5].zp : lso_polycone0xc02a418 4 z: [4076.62074383385, 4058.59604160589, 3964.0] rmax: [1930.0, 50.0, 125.0] rmin: [0.0]
0910 pcs[ 6].zp : oav_polycone0xbf1c840 4 z: [4094.62074383385, 3937.0, 4000.02470222796] rmax: [2040.0, 1930.0, 125.0] rmin: [0.0]
0911
0912 pcs[ 9].zp : OavTopHub0xc2c9030 6 z: [0.0, 57.0, 90.0, 120.0] rmax: [98.0, 68.0, 125.0] rmin: [50.0]
0913 pcs[10].zp : CtrLsoOflTopClp0xc178498 6 z: [0.0, 16.0, 184.0, 200.0] rmax: [102.5, 112.5, 100.0] rmin: [50.0]
0914 pcs[11].zp : OcrGdsLsoPrtPln0xc104000 4 z: [0.0, 214.596041605889, 184.596041605889] rmax: [98.0, 68.0] rmin: [50.0]
0915 pcs[12].zp : OcrCalLsoPrtPln0xc2fadd8 4 z: [0.0, 214.596041605889, 184.596041605889] rmax: [98.0, 68.0] rmin: [50.0]
0916
0917
0918 """
0919 pass
0920 zplane = property(lambda self:self.findall_("zplane"))
0921
0922 zp_num = property(lambda self:len(self.zplane))
0923 zp_rmin = property(lambda self:list(set(map(lambda _:_.rmin,self.zplane))))
0924 zp_rmax = property(lambda self:list(set(map(lambda _:_.rmax,self.zplane))))
0925 zp_z = property(lambda self:list(set(map(lambda _:_.z,self.zplane))))
0926 zp = property(lambda self:"%s %30s %2d z:%50r rmax:%35r rmin:%20r " % (self.gidx, self.name, self.zp_num, self.zp_z, self.zp_rmax, self.zp_rmin))
0927
0928
0929
0930
0931 def __repr__(self):
0932 prepr = Primitive.__repr__(self)
0933 return "%-80s : zp_num %2d z:%r rmax:%r rmin:%r " % (prepr, self.zp_num, self.zp_z, self.zp_rmax, self.zp_rmin )
0934
0935
0936 def zp_array(self):
0937 a = np.zeros( [len(self.zplane), 3] )
0938 for i in range(len(self.zplane)):
0939 zp = self.zplane[i]
0940 a[i] = [ zp.rmin, zp.rmax, zp.z ]
0941 pass
0942 return a
0943
0944 def prims(self):
0945 """
0946 Auto correct simple case of wrong z-order::
0947
0948 421 <polycone aunit="deg" deltaphi="360" lunit="mm" name="PMT_3inch_pmt_solid_cyl0x1c9da50" startphi="0">
0949 422 <zplane rmax="30.001" rmin="0" z="-15.8745078663875"/>
0950 423 <zplane rmax="30.001" rmin="0" z="-75.8755078663876"/>
0951 424 </polycone>
0952
0953 """
0954 zp = self.zplane
0955 zz = list(map(lambda _:_.z, zp))
0956
0957 if len(zp) == 2 and zz[0] > zz[1]:
0958 log.warning("Polycone swap misordered pair of zplanes for %s " % self.name)
0959 zp = list(reversed(zp))
0960 pass
0961
0962 prims = []
0963 for i in range(1,len(zp)):
0964 zp1 = zp[i-1]
0965 zp2 = zp[i]
0966
0967 r1 = zp1.rmax
0968 r2 = zp2.rmax
0969 z1 = zp1.z
0970 z2 = zp2.z
0971
0972 if z2 == z1:
0973 log.debug("skipping z2 == z1 zp" )
0974 else:
0975
0976 if not z2 > z1:
0977 raise ValueError("Polycone bad z-order expect z2>z1 : but z1 %s z2 %s " % (z1,z2 ))
0978 pass
0979 name = self.name + "_zp_%d" % i
0980 pr = Tube.make_cylinder( r1, z1, z2, name ) if r1 == r2 else Cone.make_cone( r1, z1, r2, z2, name )
0981 prims.append(pr)
0982 pass
0983 pass
0984 return prims
0985
0986 def inner(self):
0987 rmin = self.zp_rmin
0988 assert len(rmin) == 1
0989 rmin = rmin[0]
0990
0991 zp = self.zplane
0992 z = list(map(lambda _:_.z, zp))
0993 zmax = max(z)
0994 zmin = min(z)
0995
0996 has_inner = rmin > 0.
0997 if has_inner:
0998 inner = Tube.make_cylinder( rmin, zmin, zmax, self.name + "_inner_cylinder" )
0999 else:
1000 inner = None
1001 pass
1002 return inner
1003
1004
1005 def as_ncsg(self):
1006 assert self.aunit == "deg" and self.lunit == "mm" and self.deltaphi == 360. and self.startphi == 0.
1007 try:
1008 prims = self.prims()
1009 except ValueError as e:
1010 log.fatal("Polycone.as_ncsg failed ValueError : %r " % e )
1011 return None
1012 pass
1013 cn = TreeBuilder.uniontree(prims, name=self.name + "_uniontree")
1014 inner = self.inner()
1015
1016 return cn
1017
1018 def as_shape(self, **kwa):
1019 param = self.zp_array()
1020 shape = SPolycone(self.name, param, **kwa)
1021 return shape
1022
1023 def plot(self, ax):
1024 self.Plot(ax, self.zplane)
1025
1026 @classmethod
1027 def Plot(cls, ax, zp):
1028
1029 rmin = list(map(lambda _:_.rmin, zp))
1030 rmax = list(map(lambda _:_.rmax, zp))
1031
1032 z = list(map(lambda _:_.z, zp))
1033 zmax = max(z)
1034 zmin = min(z)
1035 zsiz = zmax - zmin
1036
1037 for i in range(1,len(zp)):
1038 zp0 = zp[i-1]
1039 zp1 = zp[i]
1040 ax.plot( [zp0.rmax,zp1.rmax], [zp0.z,zp1.z])
1041 ax.plot( [-zp0.rmax,-zp1.rmax], [zp0.z,zp1.z])
1042
1043 if i == 1:
1044 ax.plot( [-zp0.rmax, zp0.rmax], [zp0.z, zp0.z] )
1045 pass
1046 if i == len(zp) - 1:
1047 ax.plot( [-zp1.rmax, zp1.rmax], [zp1.z, zp1.z] )
1048 pass
1049 pass
1050
1051 for i in range(1,len(zp)):
1052 zp0 = zp[i-1]
1053 zp1 = zp[i]
1054 ax.plot( [zp0.rmin,zp1.rmin], [zp0.z,zp1.z])
1055 ax.plot( [-zp0.rmin,-zp1.rmin], [zp0.z,zp1.z])
1056
1057 if i == 1:
1058 ax.plot( [-zp0.rmin, zp0.rmin], [zp0.z, zp0.z] )
1059 pass
1060 if i == len(zp) - 1:
1061 ax.plot( [-zp1.rmin, zp1.rmin], [zp1.z, zp1.z] )
1062 pass
1063 pass
1064
1065 xmax = max(rmax)*1.2
1066 ax.set_xlim(-xmax, xmax )
1067 ax.set_ylim( zmin - 0.1*zsiz, zmax + 0.1*zsiz )
1068
1069
1070
1071
1072
1073
1074 class Volume(G):
1075 """
1076 Volume : GDML volume are Geant4 Logical Volumes (LV)
1077 ======================================================
1078
1079 As very many pv (placements of lv) share the same lv
1080 it does not make sense to simply jump from an lv to its pv.
1081 Conversely that is a question that must be asked of the whole
1082 geometry tree to select all the placements (pv) that have a particular lv.
1083
1084 ::
1085
1086 In [15]: for v in gdml.volumes.values():print v.material.shortname
1087 PPE
1088 MixGas
1089 Air
1090 Bakelite
1091 Air
1092 Bakelite
1093 Foam
1094 Aluminium
1095 Air
1096 ...
1097
1098 """
1099 materialref = property(lambda self:self.elem.find("materialref").attrib["ref"])
1100 solidref = property(lambda self:self.elem.find("solidref").attrib["ref"])
1101 solid = property(lambda self:self.g.solids[self.solidref])
1102 material = property(lambda self:self.g.materials[self.materialref])
1103
1104 physvol = property(lambda self:self.findall_("physvol"))
1105 children = property(lambda self:self.physvol)
1106
1107 def physvol_xyz(self, pfx="pLPMT_Hamamatsu", sub="position"):
1108 """
1109 :param pfx: pv name prefix
1110 :param sub: element name, must be either "position" or "rotation"
1111 :return xyz: array of shape (npv, 3)
1112 """
1113 assert sub in ["position", "rotation"]
1114 x = list(map(float,self.elem.xpath("./physvol[starts-with(@name,'%s')]/%s/@x" % (pfx,sub))))
1115 y = list(map(float,self.elem.xpath("./physvol[starts-with(@name,'%s')]/%s/@y" % (pfx,sub))))
1116 z = list(map(float,self.elem.xpath("./physvol[starts-with(@name,'%s')]/%s/@z" % (pfx,sub))))
1117 assert len(x) == len(y) == len(z)
1118 xyz = np.zeros( [len(x), 3])
1119 xyz[:,0] = x
1120 xyz[:,1] = y
1121 xyz[:,2] = z
1122 return xyz
1123
1124
1125
1126 def filterpv(self, pfx):
1127 return list(filter(lambda pv:pv.name.startswith(pfx), self.physvol))
1128
1129 def rdump(self, depth=0):
1130 print(self)
1131 for pv in self.physvol:
1132 lv = pv.volume
1133 lv.rdump(depth=depth+1)
1134 pass
1135
1136 def as_ncsg(self):
1137 """
1138 Hmm pv level transforms need to be applied to the
1139 csg top nodes of the solids, as the pv transforms
1140 are not being propagated GPU side, and even when they
1141 are will want to be able to handle a csg instance
1142 comprising a few solids only (eg the PMT)
1143 as is with its own transforms.
1144
1145 Essentially are collapsing a subtree for the
1146 handful of solids into the self contained instance
1147 of a list of csgnodes.
1148 """
1149 pass
1150
1151
1152 def __repr__(self):
1153 repr_ = lambda _:" %r" % _
1154 pvs = list(map(repr_, self.physvol))
1155 line = "%s %s %s" % (self.gidx, self.typ, self.name )
1156 return "\n".join([line, "solid", repr(self.solid), "material", repr(self.material), "physvol %d" % len(self.physvol)] + pvs )
1157
1158 class Physvol(G):
1159 """
1160
1161 volume
1162 opticks.ana.pmt.gdml.Volume
1163
1164 transform
1165 4x4 np.ndarray homogeneous TRS matrix ie Translate-Rotate-Scale
1166 combining the position, rotation and scale attributes
1167
1168 position
1169 rotation
1170 scale
1171 dont use these, use transform that combines them
1172
1173
1174 """
1175 volumeref = property(lambda self:self.elem.find("volumeref").attrib["ref"])
1176 volume = property(lambda self:self.g.volumes[self.volumeref])
1177 children = property(lambda self:[self.volume])
1178
1179 position = property(lambda self:self.find1_("position"))
1180 rotation = property(lambda self:self.find1_("rotation"))
1181 scale = property(lambda self:self.find1_("scale"))
1182 transform = property(lambda self:construct_transform(self))
1183
1184 def __repr__(self):
1185 return "\n".join(["%s %s" % (self.typ, self.name)," %r %r " % ( self.position, self.rotation)])
1186
1187
1188
1189 class odict(collections.OrderedDict):
1190 """
1191 Used for GDML collections of materials, solids and volumes which are
1192 always keyed by name.
1193
1194 Call method gives integer index access to materials, solids and volumes (LV)
1195 The idx is set in GDML.init from the order the elements appear in the GDML.
1196
1197 ::
1198
1199 g.material(idx)
1200 g.solids(idx)
1201 g.volumes(idx) # integer index
1202
1203 """
1204 def __call__(self, iarg):
1205 return list(self.items())[iarg][1]
1206
1207
1208 class GDML(G):
1209 """
1210 Parsing and wrapping
1211 """
1212 kls = {
1213 "material":Material,
1214
1215 "tube":Tube,
1216 "torus":Torus,
1217 "sphere":Sphere,
1218 "ellipsoid":Ellipsoid,
1219 "box":Box,
1220 "cone":Cone,
1221 "polycone":Polycone,
1222 "zplane":ZPlane,
1223 "trd":Trapezoid,
1224
1225 "intersection":Intersection,
1226 "subtraction":Subtraction,
1227 "union":Union,
1228
1229 "position":Position,
1230 "rotation":Rotation,
1231 "scale":Scale,
1232
1233 "volume":Volume,
1234 "physvol":Physvol,
1235 }
1236
1237 @classmethod
1238 def parse(cls, path):
1239 """
1240 :param path: to GDML file
1241 """
1242
1243
1244
1245 pass
1246 log.info("parsing gdmlpath %s " % (path) )
1247 gdml_elem = parse_(path)
1248 wgg = cls.wrap(gdml_elem, path=path)
1249 return wgg
1250
1251 @classmethod
1252 def fromstring(cls, st ):
1253 log.info("parsing string %s " % st )
1254 gg = ET.fromstring(st)
1255 wgg = cls.wrap(gg)
1256 return wgg
1257
1258 @classmethod
1259 def wrap(cls, gdml, path=None):
1260 """
1261 :param gdml: lxml root "gdml" element of parsed GDML document
1262 :param path: of the gdml as metadata
1263 """
1264 log.debug("wrapping gdml elements with python instances")
1265
1266 if not path is None:
1267 label = os.path.splitext(os.path.basename(path))[0]
1268 else:
1269 label = None
1270 pass
1271
1272 gg = cls(gdml)
1273 gg.g = gg
1274 gg.path = path
1275 gg.path_label = label
1276 gg.string = tostring_(gdml)
1277 gg.init()
1278 return gg
1279
1280 def get_traversed_volumes(self, lv_base, maxdepth=-1):
1281 """
1282 :param lv_base: base logical volume
1283 :param maxdepth: -1 for no limit
1284 :return tvol: odict of logical volumes within lv_base, obtained by recursion and keyed by index
1285
1286 Recursive traversal of the lv/pv GDML structure collecting
1287 and ordered dict keyed on the traversal order index
1288
1289 Note this was originally intended for assigning shortened local names
1290 within subtrees of associated volumes, eg those forming the PMT-mask and its PMT inside,
1291 that have associated names with a common prefix.
1292 """
1293 tvol = odict()
1294 def traverse_r(lv0, depth):
1295
1296 pvs = lv0.physvol
1297 indent = " " * depth
1298 print("[%2d] %s %4d %s " % (depth, indent,len(pvs),lv0.name))
1299
1300 local_index = len(tvol)
1301 tvol[local_index] = lv0
1302 lv0.local_index = local_index
1303 lv0.local_depth = depth
1304
1305 if depth < maxdepth or maxdepth == -1:
1306 for pv in pvs:
1307 lv = pv.volume
1308 traverse_r(lv, depth+1)
1309 pass
1310 pass
1311 pass
1312 traverse_r(lv_base, 0)
1313 self.label_traversed_volumes(tvol)
1314 return tvol
1315
1316 def label_traversed_volumes(self, tvol ):
1317 """
1318 Assign shortened local_name to the lv
1319 by removing common prefix and pointer refs
1320
1321 NB this assumes that associated volumes have associated names
1322 with a common prefix
1323
1324 """
1325 names = list(map(lambda _:_.name, tvol.values()))
1326 unref_ = lambda _:_[:_.find("0x")] if _.find("0x") > -1 else _
1327
1328 prefix = os.path.commonprefix(names)
1329 for lv in tvol.values():
1330 lv.local_name = unref_( lv.name[len(prefix):] )
1331 lv.local_title = "%d:%s/%s" % ( lv.local_index, lv.local_name, unref_(lv.material.name) )
1332 lv.local_prefix = "%s/%s" % (lv.g.path_label, prefix)
1333 pass
1334
1335 def find_by_prefix(self, d, prefix):
1336 """
1337 :param d: dict
1338 :param prefix: str
1339 :return sel: list of objects from d with name starting with prefix
1340 """
1341 return list(filter(lambda v:v.name.startswith(prefix), d.values()))
1342
1343 def find_volumes(self, prefix):
1344 return self.find_by_prefix(self.volumes, prefix)
1345
1346 def find_solids(self, prefix):
1347 return self.find_by_prefix(self.solids, prefix)
1348
1349 def find_materials(self, prefix):
1350 return self.find_by_prefix(self.materials, prefix)
1351
1352 def smry(self):
1353 g = self
1354 ns = len(g.solids.keys())
1355 nv = len(g.volumes.keys())
1356 log.info(" g.path %s g.path_label %s ns:%d nv:%d (logical volumes) " % (g.path, g.path_label,ns,nv))
1357
1358 def find_one_volume(self, prefix):
1359 """
1360 :param prefix: str
1361 :return lv: first Volume instance with name matching starting with prefix
1362 """
1363 lvs = self.find_volumes(prefix)
1364 log.info("prefix argument %s matched %d volumes" % (prefix, len(lvs)))
1365 for i,lv in enumerate(lvs):
1366 log.info(" %2d : %s" % (i,lv.name) )
1367 pass
1368 return lvs[0] if len(lvs) > 0 else None
1369
1370
1371
1372
1373
1374
1375 def volume_summary(self):
1376 """
1377 1. get unique list of all volumeref/@ref lv names referenced from physvol
1378 2. for each lv name count the number of physvol that references it
1379 3. for each physvol get all parent lv names
1380
1381 25600 : PMT_3inch_log0x3a2d940 : lInnerWater0x30e6330
1382 12612 : NNVTMCPPMTlMaskVirtual0x32a49b0 : lInnerWater0x30e6330
1383 5000 : HamamatsuR12860lMaskVirtual0x32906d0 : lInnerWater0x30e6330
1384 2400 : mask_PMT_20inch_vetolMaskVirtual0x32a6960 : lOuterWaterPool0x30e5560
1385 590 : lUpper0x31765d0 : lInnerWater0x30e6330
1386 590 : lAddition0x31bd440 : lInnerWater0x30e6330
1387 590 : lFasteners0x312ddf0 : lInnerWater0x30e6330
1388 590 : lSteel0x30e9b10 : lInnerWater0x30e6330
1389 64 : lCoating0x45073a0 : lPanelTape0x4507210
1390 64 : lXJfixture0x320b950 : lTarget0x30e7010 lInnerWater0x30e6330
1391 63 : lWallff_0x4506d50 : lAirTT0x4506ad0
1392 56 : lXJanchor0x3205390 : lInnerWater0x30e6330
1393 36 : lSJFixture0x3210610 : lTarget0x30e7010
1394 8 : lSJReceiver0x3215340 : lTarget0x30e7010
1395 4 : lPanel0x4507080 : lPlanef_0x4506f70
1396 2 : lPlanef_0x4506f70 : lWallff_0x4506d50
1397 2 : lSJCLSanchor0x320f400 : lTarget0x30e7010
1398 1 : lLowerChimney0x4504230 : lInnerWater0x30e6330
1399 1 : lLowerChimneySteel0x4504880 : lLowerChimney0x4504230
1400 1 : lTarget0x30e7010 : lAcrylic0x30e69a0
1401 1 : lUpperChimney0x4501f50 : lExpHall0x30dfe10
1402 1 : lLowerChimneyLS0x4504660 : lLowerChimney0x4504230
1403 1 : lBar0x4507530 : lCoating0x45073a0
1404 1 : lLowerChimneyAcrylic0x4504450 : lLowerChimney0x4504230
1405 1 : lBtmRock0x30e4820 : lWorld0x30d4f70
1406 1 : HamamatsuR12860_PMT_20inch_inner2_log0x32b85a0 : HamamatsuR12860_PMT_20inch_body_log0x32b8250
1407 1 : PMT_3inch_inner2_log0x3a2db60 : PMT_3inch_body_log0x3a2d830
1408 1 : lInnerWater0x30e6330 : lReflectorInCD0x30e5cc0
1409
1410
1411 """
1412 lvns = list(set(self.elem.xpath("//physvol/volumeref/@ref")))
1413
1414 fmt = "%5d : %50s : %s"
1415 d = {}
1416 p = {}
1417 for lvn in lvns:
1418 d[lvn] = len(self.elem.xpath("//physvol[volumeref/@ref = '%s']" % lvn))
1419 p[lvn] = self.elem.xpath("//physvol[volumeref/@ref = '%s']/../@name" % lvn)
1420 log.debug(fmt % (d[lvn], lvn, p[lvn] ))
1421 pass
1422 print("\n".join([fmt % (d[lvn], lvn, " ".join(p[lvn]) ) for lvn in sorted(d, reverse=True, key=lambda lvn:d[lvn])]))
1423
1424
1425
1426
1427 world = property(lambda self:self.volumes[self.worldvol])
1428
1429 def init(self):
1430 """
1431 Heart of the GDML parsing.
1432 """
1433 self.materials = odict()
1434 self.solids = odict()
1435 self.volumes = odict()
1436
1437 for i, e in enumerate(self.findall_("materials/material")):
1438 e.idx = i
1439 self.materials[e.name] = e
1440 pass
1441
1442 for i, e in enumerate(self.findall_("solids/*[local-name() != 'opticalsurface']")):
1443 e.idx = i
1444 self.solids[e.name] = e
1445 pass
1446
1447 for i, e in enumerate(self.findall_("structure/volume")):
1448 e.idx = i
1449 self.volumes[e.name] = e
1450 pass
1451 self.setup_world = self.elem.find("setup/world")
1452 self.worldvol = self.setup_world.attrib["ref"] if self.setup_world is not None else None
1453
1454 vv = self.volumes.values()
1455
1456 vvs = list(filter(lambda v:hasattr(v,'solid'), vv))
1457
1458 log.info("vv %s (number of logical volumes) vvs %s (number of lv with associated solid) " % (len(vv),len(vvs)))
1459
1460
1461
1462
1463 self.lv2so = dict([(v.idx, v.solid.idx) for v in vvs])
1464
1465
1466
1467
1468
1469
1470
1471
1472 def oneplot(obj):
1473 fig = plt.figure()
1474 ax = fig.add_subplot(1,1,1, aspect='equal')
1475 obj.plot(ax)
1476 plt.show()
1477
1478 def multiplot(fig, objs, nx=4, ny=4):
1479 for i in range(len(objs)):
1480 ax = fig.add_subplot(nx,ny,i+1, aspect='equal')
1481 objs[i].plot(ax)
1482 pass
1483
1484
1485
1486
1487 def test_children(t):
1488
1489 lvn = "/dd/Geometry/PMT/lvPmtHemi0x"
1490
1491 l = t.filternodes_lv(lvn)
1492 assert len(l) == 672
1493
1494 n = l[0]
1495 sibs = list(n.siblings)
1496 assert len(sibs) == 192
1497
1498 cn = n.lv.solid.as_ncsg()
1499 cn.dump()
1500
1501 v = n.children[0]
1502 for c in v.children:
1503 print(c.pv.transform)
1504
1505
1506 def test_polycone(g):
1507 pcs = g.findall_("solids//polycone")
1508 for i,pc in enumerate(pcs):
1509 print("pcs[%2d].zp : %s " % (i, pc.zp))
1510 cn = pc.as_ncsg()
1511
1512 def test_gdml(g):
1513 print(g.world)
1514 g.world.rdump()
1515 g.volumes["/dd/Geometry/PMT/lvPmtHemi0xc133740"].rdump()
1516
1517 def test_trd(g):
1518 trs = g.findall_("solids//trd")
1519 cns = []
1520 for i,tr in enumerate(trs):
1521 cn = tr.as_ncsg()
1522 cns.append(cn)
1523
1524
1525 def test_gdml_fromstring():
1526 gg = GDML.fromstring(r"""<gdml><solids><tube aunit="deg" deltaphi="360" lunit="mm" name="AdPmtCollar0xc2c5260" rmax="106" rmin="105" startphi="0" z="12.7"/></solids></gdml>""")
1527 so = gg.solids(0)
1528 assert type(so) is Tube
1529 cn = so.as_ncsg()
1530 assert cn.typ == cn.DIFFERENCE
1531
1532 def test_primitive_fromstring():
1533 so = Primitive.fromstring(r"""<tube aunit="deg" deltaphi="360" lunit="mm" name="AdPmtCollar0xc2c5260" rmax="106" rmin="105" startphi="0" z="12.7"/>""")
1534 assert type(so) is Tube
1535 cn = so.as_ncsg()
1536 assert cn.typ == cn.DIFFERENCE
1537
1538
1539
1540 def parse_args(doc, **kwa):
1541 np.set_printoptions(suppress=True, precision=3, linewidth=200)
1542 parser = argparse.ArgumentParser(doc)
1543 parser.add_argument( "idx", type=int, nargs="*", help="Node index to dump.")
1544 parser.add_argument( "--level", default="info", help="logging level" )
1545 parser.add_argument( "-g","--gdmlpath", default=kwa.get("gdmlpath",None), help="Override the default key.gdmlpath:\n%(default)s " )
1546 args = parser.parse_args()
1547 fmt = '[%(asctime)s] p%(process)s {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s'
1548 logging.basicConfig(level=getattr(logging,args.level.upper()), format=fmt)
1549 if len(args.idx) == 0:
1550 args.idx = [0]
1551 pass
1552 return args
1553
1554
1555 def define_matrix_values(elem, scale=1e6, startswith=None):
1556 log.info("define_matrix_values startswith:%s scale:%s " % (startswith, scale))
1557 mm = elem.findall("define/matrix")
1558 fmt = " {coldim:3s} {a.shape!r:8s} {dig:32s} {name:40s} "
1559 d = odict()
1560 for m in mm:
1561 name = m.attrib["name"]
1562 if startswith is None or name.startswith(startswith):
1563 coldim = int(m.attrib["coldim"])
1564 values = m.attrib["values"]
1565 a = np_fromstring(values, coldim=coldim, scale=scale)
1566 d[name] = a
1567 dig = np_digest(a)
1568 print(fmt.format(**m.attrib,a=a, dig=dig))
1569
1570
1571 values2 = np_tostring(a, scale=scale)
1572 a2 = np_fromstring(values2, coldim=coldim, scale=scale)
1573 assert np.allclose( a, a2)
1574
1575 pass
1576 pass
1577 if startswith is None:
1578 assert len(d) == len(mm)
1579 pass
1580 return d
1581
1582
1583
1584 if __name__ == '__main__':
1585
1586 key = key_()
1587 args = parse_args(__doc__, gdmlpath=key.gdmlpath)
1588
1589 g = GDML.parse(args.gdmlpath)
1590
1591 from opticks.analytic.treebase import Tree
1592 t = Tree(g.world)
1593
1594
1595
1596
1597
1598
1599
1600 mv = define_matrix_values(g.elem, startswith="EFFICIENCY")
1601
1602
1603
1604