File indexing completed on 2026-04-10 07:49:17
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019
0020
0021 """
0022 ddpart.py
0023 ===========
0024
0025 Approach:
0026
0027 * kicking off from Dddb.logvol_ wraps results of
0028 lxml find into do-everything Elem subclass appropriate to the tags of the elements
0029
0030 * primary purpose is the Elem.parts method, trace back from there to understand
0031
0032
0033 Crucial Methods
0034 -----------------
0035
0036 ElemPartitioner.parts
0037 nexus of control of the partitioning, which gets manually mixed into Elem
0038
0039
0040
0041
0042 Need to play some mixin tricks
0043
0044 * http://stackoverflow.com/questions/8544983/dynamically-mixin-a-base-class-to-an-instance-in-python
0045
0046
0047 """
0048 import os, re, logging, math
0049 from opticks.ana.base import opticks_main, manual_mixin
0050
0051
0052 from ddbase import Dddb
0053 from ddbase import Elem, Sphere, Tubs, Primitive, Physvol, Logvol, PosXYZ
0054
0055 from geom import Part, BBox, ZPlane, Rev
0056 from gcsg import GCSG
0057
0058 X,Y,Z = 0,1,2
0059
0060
0061 log = logging.getLogger(__name__)
0062
0063
0064 class Parts(list):
0065 def __repr__(self):
0066 return "Parts " + list.__repr__(self)
0067
0068 def __init__(self, *args, **kwa):
0069 list.__init__(self, *args, **kwa)
0070
0071
0072 class Uncoincide(object):
0073 def __init__(self, top="CONTAINING_MATERIAL", sensor="SENSOR_SURFACE"):
0074 self.top = top
0075 self.sensor = sensor
0076 pass
0077 def face_bottom_tubs(self, name):
0078 """
0079 Only face and bottom have coincident surfaces in literal
0080 translation that need fixing.
0081
0082 ::
0083
0084 tubs bottom face
0085
0086 OM///Pyrex OM///Pyrex OM///Pyrex
0087
0088 Pyrex///Vacuum Pyrex///OpaqueVacuum Pyrex/SENSOR//Bialkali
0089
0090 Vacuum///OpaqueVacuum OpaqueVacuum///Vacuum Bialkali///Vacuum
0091
0092
0093 """
0094 log.warning("UNCOINCIDE boundary setting for %s " % name)
0095 face = bottom = tubs = None
0096 if name == "pmt-hemi":
0097 pass
0098 boundary = "%s///Pyrex" % self.top
0099 face = boundary
0100 bottom = boundary
0101 tubs = boundary
0102 pass
0103 elif name == "pmt-hemi-vac":
0104 pass
0105 face = "Pyrex/%s//Bialkali" % self.sensor
0106 bottom = "Pyrex///OpaqueVacuum"
0107 tubs = "Pyrex///Vacuum"
0108 pass
0109 elif name == "pmt-hemi-cathode":
0110 pass
0111 face = "Bialkali///Vacuum"
0112 bottom = None
0113 tubs = None
0114 pass
0115 elif name == "pmt-hemi-bot":
0116 pass
0117 face = None
0118 bottom = "OpaqueVacuum///Vacuum"
0119 tubs = None
0120 pass
0121 elif name == "pmt-hemi-dynode":
0122 pass
0123 face = None
0124 bottom = None
0125 tubs = "Vacuum///OpaqueVacuum"
0126 pass
0127 else:
0128 assert 0
0129 pass
0130 return face, bottom, tubs
0131
0132 UNCOINCIDE = Uncoincide()
0133
0134
0135
0136 class ElemPartitioner(object):
0137 def partition_intersection_3spheres(self, spheres, material=None):
0138 """
0139 :param spheres: list of three *Sphere* in ascending center z order, which are assumed to intersect
0140 :return parts: list of three sphere *Part* instances
0141
0142 Consider splitting the lens shape made from the boolean CSG intersection
0143 of two spheres along the plane of the intersection.
0144 The left part of the lens comes from the right Sphere
0145 and the right part comes left Sphere.
0146
0147 Extending from two sphere intersection to three spheres
0148 numbered s1,s2,s3 from left to right, with two ZPlane intersections z23, z12.
0149
0150 left
0151 from s3, bounded by z23 on right
0152
0153 middle
0154 from s2, bounded by z23 on left, z12 on right
0155
0156 right
0157 from s1, bounded by z12 on left
0158
0159 """
0160 s1, s2, s3 = spheres
0161
0162 assert s1.z < s2.z < s3.z
0163
0164 z12 = Sphere.intersect("z12",s1,s2)
0165 z23 = Sphere.intersect("z23",s2,s3)
0166
0167 assert z23.z < z12.z
0168
0169 p1 = s3.part_zleft(z23)
0170 p2 = s2.part_zmiddle(z23, z12)
0171 p3 = s1.part_zright(z12)
0172
0173 assert p1.bbox.z < p2.bbox.z < p3.bbox.z
0174
0175 pts = Parts([p3,p2,p1])
0176 return pts
0177
0178 def partition_intersection_2spheres(self, spheres, material=None, unoverlap=True):
0179 """
0180 :param spheres: list of two *Sphere* in ascending center z order
0181 :return parts: list of two sphere *Part* instances
0182
0183 Used for the very thin photocathode
0184
0185 For pmt-hemi-cathode-face_part pmt-hemi-cathode-belly_part the
0186 bbox of the Sphere parts from just the theta angle ranges are overlapping
0187 each other very slightly with the plane of sphere intersection in between.
0188 Because of the thin photocathode this creates an dangerous sharp protuding edge.
0189
0190 Suspect overlapping bbox may be problematic, so unoverlap by adjusting z
0191 edges of bbox, and adjust in xy.
0192 """
0193 s1, s2 = spheres
0194 assert s1.z < s2.z
0195
0196
0197 p1, p2 = Part.ascending_bbox_zmin([s1.as_part(),s2.as_part()])
0198 assert p1.bbox.zmin < p2.bbox.zmin
0199 p12 = Sphere.intersect("p12",s1,s2)
0200
0201 if unoverlap:
0202 p1.bbox.zmax = p12.z
0203 p2.bbox.zmin = p12.z
0204 p2.bbox.xymax = p12.y
0205 p2.bbox.xymin = -p12.y
0206
0207 log.warning("material %s name %s " % (material, self.name))
0208 log.warning("p1 %s " % p1)
0209 log.warning("p2 %s " % p2)
0210
0211
0212 i1, i2 = Part.ascending_bbox_zmin([s1.as_part(inner=True),s2.as_part(inner=True)])
0213 assert i1.bbox.zmin < i2.bbox.zmin
0214 i12 = Sphere.intersect("p12",s1,s2, inner=True)
0215
0216 if unoverlap:
0217 i1.bbox.zmax = i12.z
0218 i2.bbox.zmin = i12.z
0219 i2.bbox.xymax = i12.y
0220 i2.bbox.xymin = -i12.y
0221
0222 log.warning("i1 %s" % i1)
0223 log.warning("i2 %s" % i2)
0224 if UNCOINCIDE and self.name == "pmt-hemi-cathode":
0225 face, bottom, tubs = UNCOINCIDE.face_bottom_tubs(self.name)
0226 i2.boundary = face
0227 i1.boundary = face
0228 ret = [i2,i1]
0229 else:
0230
0231
0232 ret = [p2,i2,p1,i1]
0233 pass
0234 return Parts(ret)
0235
0236 def partition_intersection(self, material=None):
0237
0238
0239 spheres = []
0240 comps = self.findall_("./*")
0241 self.link_prior_posXYZ(comps)
0242
0243 other = []
0244 for c in comps:
0245 if type(c) is Sphere:
0246 spheres.append(c)
0247 elif type(c) is PosXYZ:
0248 pass
0249 else:
0250 other.append(c)
0251 pass
0252
0253 assert len(other) == 0, ("only 2 or 3-sphere intersections handled", other )
0254
0255 for i,s in enumerate(spheres):
0256 log.debug("s%d: %s %s " % (i, s.desc, s.outerRadius.value))
0257
0258 if len(spheres) == 3:
0259 pts = self.partition_intersection_3spheres(spheres, material=material)
0260 elif len(spheres) == 2:
0261 pts = self.partition_intersection_2spheres(spheres, material=material)
0262 else:
0263 assert 0
0264
0265
0266 pts.gcsg = GCSG(self, spheres)
0267
0268 return pts
0269
0270
0271 def partition_union_intersection_tubs(self, comps, material=None, verbose=False):
0272 """
0273 """
0274 log.info("material %s name %s " % (material, self.name))
0275
0276 ipts = comps[0].partition_intersection()
0277 sparts = Part.ascending_bbox_zmin(ipts)
0278 assert len(sparts) == 3
0279
0280 tpart = comps[1].as_part()
0281 ts = Part.intersect_tubs_sphere("ts", tpart, sparts[0], -1)
0282
0283
0284 if verbose:
0285 log.info(self)
0286 log.info("ts %s " % repr(ts))
0287 for i, s in enumerate(sparts):
0288 log.info("sp(%s) %s " % (i, repr(s)))
0289 log.info("tp(0) %s " % repr(tpart))
0290 pass
0291
0292
0293
0294
0295
0296 tpart.bbox.zmax = ts.z
0297 sparts[0].bbox.zmin = ts.z
0298
0299 tpart.enable_endcap("P")
0300
0301
0302 if UNCOINCIDE:
0303 if self.name == "pmt-hemi" or self.name == "pmt-hemi-vac":
0304 face, bottom, tubs = UNCOINCIDE.face_bottom_tubs(self.name)
0305 else:
0306 assert 0
0307 pass
0308 tpart.boundary = tubs
0309 sparts[0].boundary = bottom
0310 sparts[1].boundary = face
0311 sparts[2].boundary = face
0312 pass
0313
0314 rparts = Parts()
0315 rparts.extend(sparts)
0316 rparts.extend([tpart])
0317
0318
0319
0320
0321
0322 rparts.gcsg = GCSG(self, [ipts.gcsg, comps[1]] )
0323
0324 return rparts
0325
0326
0327 def partition_union(self, material=None, verbose=False):
0328 """
0329 union of a 3-sphere lens shape and a tubs requires:
0330
0331 * adjust bbox of the abutting part Sphere to the intersection z of tubs and Sphere
0332 * avoid a surface at the interface of tubs endcap and part Sphere
0333
0334 """
0335 comps = self.findall_("./*")
0336 self.link_prior_posXYZ(comps)
0337
0338 rparts = Parts()
0339 xret = None
0340
0341 if len(comps) == 3 and comps[0].is_intersection and comps[1].is_tubs and comps[2].is_posXYZ:
0342
0343 xret = self.partition_union_intersection_tubs(comps[0:3], material=material)
0344 elif len(comps) == 3 and comps[0].is_sphere and comps[1].is_sphere and comps[2].is_posXYZ:
0345 xret = self.partition_intersection_2spheres(comps[0:2], material=material)
0346 if not hasattr(xret, 'gcsg'):
0347 xret.gcsg = GCSG(self, comps[0:2])
0348 else:
0349 xret = self.parts()
0350
0351 if xret is not None:
0352 rparts.extend(xret)
0353 if hasattr(xret, 'gcsg'):
0354 rparts.gcsg = xret.gcsg
0355 pass
0356 return rparts ;
0357
0358 def parts_sphere_with_inner(self, c):
0359 log.info("name %s " % c.name)
0360
0361 p = c.as_part()
0362 i = c.as_part(inner=True)
0363 log.info(" part %s " % p )
0364 log.info(" inner %s " % i )
0365
0366 if UNCOINCIDE and c.name == "pmt-hemi-bot":
0367 face, bottom, tubs = UNCOINCIDE.face_bottom_tubs(c.name)
0368 i.boundary = bottom
0369 ret = [i]
0370 else:
0371
0372 ret = [p,i]
0373 pass
0374 pts = Parts(ret)
0375 pts.gcsg = GCSG(c)
0376 return pts
0377
0378
0379 def parts_primitive(self, c):
0380 log.info("name %s " % c.name)
0381 p = c.as_part()
0382 if UNCOINCIDE and c.name == "pmt-hemi-dynode":
0383 face, bottom, tubs = UNCOINCIDE.face_bottom_tubs(c.name)
0384 p.boundary = tubs
0385 pass
0386 pts = Parts([p])
0387 pts.gcsg = GCSG(c)
0388 return pts
0389
0390
0391 def parts(self, analytic_version=0):
0392 """
0393 :return: list of Part instances
0394
0395 Provides parts from a single LV only, ie not
0396 following pv refs. Recursion is needed
0397 in order to do link posXYZ transforms with geometry
0398 and skip them from the parts returned.
0399 """
0400 if type(self) is Physvol:
0401 return []
0402
0403 if type(self) is Logvol and not self.is_logvol:
0404 log.warning("inconsistent LV %s " % repr(self))
0405
0406 if self.is_logvol:
0407 base = self.posXYZ
0408 material = self.material
0409 else:
0410 base = None
0411 material = None
0412 pass
0413
0414 comps = self.findall_("./*")
0415 self.link_prior_posXYZ(comps, base)
0416
0417 if not base is None:
0418 log.info("&&&&&&&&&&&&&&&& name:%s ncomp:%d c0:%r ElemPartitioner.parts base translate %r " % (self.name, len(comps), comps[0], base) )
0419 pass
0420
0421 rparts = Parts()
0422
0423 gcsg_ = []
0424
0425 for c in comps:
0426 xret = []
0427 if c.is_sphere and c.has_inner():
0428 log.info("-> sphere with non-zero inner radius %s " % c.name)
0429 xret = self.parts_sphere_with_inner(c)
0430 elif c.is_primitive:
0431 log.info("-> primitive %s " % c.name)
0432 xret = self.parts_primitive(c)
0433 elif c.is_intersection:
0434 log.info("-> intersection %s " % c.name)
0435 xret = c.partition_intersection(material=material)
0436 elif c.is_union:
0437 log.info("-> union %s " % c.name)
0438 xret = c.partition_union(material=material)
0439 elif c.is_composite:
0440 log.info("-> composite %s " % c.name )
0441 xret = c.parts()
0442 elif c.is_posXYZ:
0443 pass
0444 else:
0445 log.warning("skipped component %s " % repr(c))
0446 pass
0447
0448 if len(xret) > 0:
0449 rparts.extend(xret)
0450 if hasattr(xret, 'gcsg'):
0451 gcsg_.append(xret.gcsg)
0452 pass
0453 pass
0454 pass
0455
0456 log.info("%s : %s comps yield %s rparts %s gcsg " % (repr(self), len(comps), len(rparts), len(gcsg_)))
0457
0458 if self.is_logvol:
0459 for p in rparts:
0460 if p.material is None:
0461 p.material = self.material
0462 pass
0463 pass
0464 for cn in gcsg_:
0465
0466 if cn.lv is None:
0467 cn.lv = self
0468
0469
0470
0471 rparts.gcsg = gcsg_
0472
0473 return rparts
0474
0475 def geometry(self):
0476 return filter(lambda c:c.is_geometry, self.components())
0477
0478
0479
0480
0481
0482
0483 class SpherePartitioner(object):
0484
0485 @classmethod
0486 def intersect(cls, name, a_, b_, inner=False):
0487 """
0488 Find Z intersect of two Z offset spheres
0489
0490 * http://mathworld.wolfram.com/Circle-CircleIntersection.html
0491
0492 :param name: identifer passed to ZPlane
0493 :param a_: *Sphere* instance
0494 :param b_: *Sphere* instance
0495
0496 :return zpl: *ZPlane* instance with z and y attributes where:
0497 z is intersection coordinate
0498 y is radius of the intersection circle
0499 """
0500
0501 if inner:
0502 R = a_.innerRadius.value
0503 r = b_.innerRadius.value
0504 else:
0505 R = a_.outerRadius.value
0506 r = b_.outerRadius.value
0507 pass
0508 assert R is not None and r is not None
0509
0510 a = a_.xyz
0511 b = b_.xyz
0512
0513 log.debug(" R %s a %s " % ( R, repr(a)) )
0514 log.debug(" r %s b %s " % ( r, repr(b)) )
0515
0516 dx = b[X] - a[X]
0517 dy = b[Y] - a[Y]
0518 dz = b[Z] - a[Z]
0519
0520 assert dx == 0
0521 assert dy == 0
0522 assert dz != 0
0523
0524 d = dz
0525 dd_m_rr_p_RR = d*d - r*r + R*R
0526 z = dd_m_rr_p_RR/(2.*d)
0527 yy = (4.*d*d*R*R - dd_m_rr_p_RR*dd_m_rr_p_RR)/(4.*d*d)
0528 y = math.sqrt(yy)
0529
0530
0531 return ZPlane(name, z+a[Z], y )
0532
0533
0534 def as_gcsg(self):
0535 R = self.outerRadius.value
0536 if self.has_inner():
0537 r = self.innerRadius.value
0538 else:
0539 r = -1.0
0540 pass
0541
0542 sta = self.startThetaAngle.value
0543 dta = self.deltaThetaAngle.value
0544 if sta is None:
0545 sta = 0.
0546 if dta is None:
0547 dta = 180.
0548
0549 gcsg = []
0550 gcsg.append( [self.xyz[0], self.xyz[1], self.xyz[2], R] )
0551 gcsg.append( [sta,dta,0,r] )
0552 gcsg.append( [0,0,0,0] )
0553 gcsg.append( [0,0,0,0] )
0554 return gcsg
0555
0556 def as_part(self, inner=False):
0557 """
0558 Spheres sliced via startThetaAngle and deltaThetaAngle
0559 are handled by setting of the bbox
0560 """
0561 if inner:
0562 radius = self.innerRadius.value
0563 else:
0564 radius = self.outerRadius.value
0565 pass
0566 if radius is None:
0567 return None
0568
0569 sta = self.startThetaAngle.value
0570 dta = self.deltaThetaAngle.value
0571
0572 assert self.xyz[2] == self.z
0573 z = self.z
0574
0575 p = Part('Sphere', self.name + "_part", self.xyz, radius )
0576
0577 if sta is None and dta is None:
0578 thetacut = False
0579 bb = self.bbox(z-radius, z+radius, -radius, radius)
0580 else:
0581
0582 if sta is None:
0583 sta = 0.
0584 rta = sta
0585 lta = sta + dta
0586 log.debug("Sphere.as_part %s leftThetaAngle %s rightThetaAngle %s " % (self.name, lta, rta))
0587 assert rta >= 0. and rta <= 180.
0588 assert lta >= 0. and lta <= 180.
0589 zl = radius*math.cos(lta*math.pi/180.)
0590 yl = radius*math.sin(lta*math.pi/180.)
0591 zr = radius*math.cos(rta*math.pi/180.)
0592 yr = radius*math.sin(rta*math.pi/180.)
0593 ym = max(abs(yl),abs(yr))
0594 bb = self.bbox(z+zl, z+zr, -ym, ym)
0595 pass
0596 p.bbox = bb
0597
0598 return p
0599
0600 def part_zleft(self, zpl):
0601 radius = self.outerRadius.value
0602 z = self.xyz[2]
0603 ymax = zpl.y
0604 p = Part('Sphere', self.name + "_part_zleft", self.xyz, radius )
0605 p.bbox = self.bbox(z-radius, zpl.z, -ymax, ymax)
0606 return p
0607
0608 def part_zright(self, zpr):
0609 radius = self.outerRadius.value
0610 z = self.xyz[2]
0611 ymax = zpr.y
0612 p = Part('Sphere', self.name + "_part_zright", self.xyz, radius )
0613 p.bbox = self.bbox(zpr.z,z+radius, -ymax, ymax)
0614 return p
0615
0616 def part_zmiddle(self, zpl, zpr):
0617 p = Part('Sphere', self.name + "_part_zmiddle", self.xyz, self.outerRadius.value )
0618 ymax = max(zpl.y,zpr.y)
0619 p.bbox = self.bbox(zpl.z,zpr.z,-ymax,ymax )
0620 return p
0621
0622 def asrev(self):
0623 xyz = self.xyz
0624 ro = self.outerRadius.value
0625 ri = self.innerRadius.value
0626 st = self.startThetaAngle.value
0627 dt = self.deltaThetaAngle.value
0628 sz = None
0629 wi = None
0630 if ri is not None and ri > 0:
0631 wi = ro - ri
0632
0633 return [Rev('Sphere', self.name,xyz, ro, sz, st, dt, wi)]
0634
0635
0636
0637 class TubsPartitioner(object):
0638 def as_part(self):
0639 sizeZ = self.sizeZ.value
0640 radius = self.outerRadius.value
0641 z = self.xyz[2]
0642 p = Part('Tubs', self.name + "_part", self.xyz, radius, sizeZ )
0643 p.bbox = self.bbox(z-sizeZ/2, z+sizeZ/2, -radius, radius)
0644 return p
0645
0646 def as_gcsg(self):
0647 sizeZ = self.sizeZ.value
0648 outer = self.outerRadius.value
0649 inner = self.innerRadius.value
0650 gcsg = []
0651 gcsg.append( [self.xyz[0], self.xyz[1], self.xyz[2], outer] )
0652 gcsg.append( [0,0,sizeZ, inner] )
0653 gcsg.append( [0,0,0,0] )
0654 gcsg.append( [0,0,0,0] )
0655 return gcsg
0656
0657 def asrev(self):
0658 sz = self.sizeZ.value
0659 r = self.outerRadius.value
0660 xyz = self.xyz
0661 return [Rev('Tubs', self.name,xyz, r, sz )]
0662
0663
0664 class PrimitivePartitioner(object):
0665 def bbox(self, zl, zr, yn, yp ):
0666 assert yn < 0 and yp > 0 and zr > zl
0667 return BBox([yn,yn,zl], [yp,yp,zr])
0668
0669
0670
0671 def ddpart_manual_mixin():
0672 """
0673 Using manual mixin approach to avoid changing
0674 the class hierarchy whilst still splitting base
0675 functionality from partitioner methods.
0676 """
0677 manual_mixin(Tubs, TubsPartitioner)
0678 manual_mixin(Sphere, SpherePartitioner)
0679 manual_mixin(Elem, ElemPartitioner)
0680 manual_mixin(Primitive, PrimitivePartitioner)
0681
0682
0683
0684 if __name__ == '__main__':
0685 args = opticks_main(apmtidx=2)
0686
0687 ddpart_manual_mixin()
0688
0689 g = Dddb.parse(args.apmtddpath)
0690
0691 g.dump_context('PmtHemi')
0692
0693 lv = g.logvol_("lvPmtHemi")
0694
0695 pts = lv.parts()
0696
0697 for pt in pts:
0698 log.info(pt)
0699
0700
0701