File indexing completed on 2026-04-09 07:49:02
0001
0002 """
0003 cxt_min.py
0004 ===========
0005
0006 1. loads simtrace SEvt from $MFOLD
0007 2. checks validity with asserts on the simtrace gensteps
0008 3. prepare genstep ugrid positions and upos simtrace intersect positions
0009 depending on GLOBAL envvar
0010 4. plot the ugrid and upos points
0011
0012
0013 MODE=2
0014 use matplotlib 2D plotting, little used now : as lacks flexibility and performance
0015
0016 MODE=3
0017 use pyvista 3D plotting.
0018 Have previously observed pyvista issue that sometimes start with a blank screen.
0019 It is necessary to slighty move the mouse wheel to get the render to appear.
0020
0021 GLOBAL=1
0022 leave positions as is resulting in global frame plotting
0023
0024 With global frame the plane of the genstep rays will often not correspond
0025 to the plane of the view causing the geometry to appear squeezed until
0026 rotate around with pyvista (MODE:3) manually.
0027 For the same reason it is difficult to target using EYE
0028 when using GLOBAL frame.
0029
0030 TODO: provide option to handle EYE,LOOK,UP inputs as local frame
0031 even when using GLOBAL frame ?
0032
0033 GLOBAL=0
0034 apply the e.f.sframe.w2m (world2model) 4x4 transform to
0035 the global positions converting them into the model frame
0036
0037
0038 GSGRID=1
0039 plot the grid of genstep positions
0040
0041 PRIMTAB=1
0042 configure identity coloring and key labelling of intersects to use globalPrimIdx
0043 (this succeeded to color both Opticks and Geant4 intersects)
0044
0045 PRIMTAB=0 (default)
0046 configure identity coloring and key labelling of intersects to use boundary index
0047 (note that when plotting U4Simtrace.h Geant4 simtrace intersects the boundary is
0048 currently always zero)
0049
0050
0051 KEY=red,white,powderblue
0052 select intersects included in the plot according to their key color
0053
0054 KEY=~red,white,powderblue
0055 select intersects excluded from the plot according to their key color
0056
0057
0058 MODE=3 EYE=0,10000,0
0059 EYE controls 3D viewpoint in pyvista plotting using mm length units,
0060 NB unlike cxr_min.sh there is currently no target extent scaling so
0061 the EYE,LOOK,UP values need to be chosen according to the size
0062 of the target geometry
0063
0064
0065 TODO: cherry pick from tests/CSGOptiXSimtraceTest.py and simplify
0066 for minimal simtrace plotting
0067
0068
0069
0070
0071 This python script is also used from ipc InputPhotonCheck, eg::
0072
0073 ipc env_info ## get into env
0074 MODE=3 PRIMTAB=1 GSGRID=1 EYE=0,1000,0 ipc cxt ## viewpoint 1m offset in Y from center of target frame
0075 MODE=3 PRIMTAB=1 GSGRID=1 EYE=0,1000,0 GLOBAL=1 ipc cxt
0076
0077
0078 MODE=3 PRIMTAB=1 GSGRID=1 EYE=0,1000,0 GLOBAL=0 KEY=red,white,powderblue ipc cxt
0079 MODE=3 PRIMTAB=1 GSGRID=1 EYE=0,1000,0 GLOBAL=0 KEY=~red,white,powderblue ipc cxt
0080
0081
0082 """
0083 import os, sys, re, logging, textwrap, numpy as np
0084 log = logging.getLogger(__name__)
0085
0086 from collections import OrderedDict
0087 from opticks.ana.fold import Fold
0088 from opticks.sysrap.sevt import SEvt
0089 from opticks.ana.p import cf
0090 from opticks.CSG.CSGFoundry import KeyNameConfig
0091
0092 try:
0093 from scipy.spatial import KDTree
0094 except ImportError:
0095 KDTree = None
0096 pass
0097
0098
0099 MODE = int(os.environ.get("MODE","2"))
0100 NORMAL = int(os.environ.get("NORMAL","0"))
0101 NORMAL_FILTER = int(os.environ.get("NORMAL_FILTER","10000"))
0102 PRIMTAB = int(os.environ.get("PRIMTAB","0"))
0103 assert PRIMTAB in [0,1]
0104
0105
0106 A = int(os.environ.get("A","0"))
0107 B = int(os.environ.get("B","0"))
0108
0109
0110 GLOBAL = 1 == int(os.environ.get("GLOBAL","0"))
0111 GSGRID = 1 == int(os.environ.get("GSGRID","0"))
0112 FRAME = 1 == int(os.environ.get("FRAME","0"))
0113 POINT = np.fromstring(os.environ["POINT"],sep=",").reshape(-1,3) if "POINT" in os.environ else None
0114 POINTSIZE = float(os.environ.get("POINTSIZE",1.))
0115 APOINTSIZE = float(os.environ.get("APOINTSIZE",10.))
0116 BPOINTSIZE = float(os.environ.get("BPOINTSIZE",10.))
0117
0118
0119
0120 if MODE in [2,3]:
0121 import opticks.ana.pvplt as pvp
0122 pass
0123
0124
0125
0126 class UniqueTable(object):
0127 def __init__(self, symbol, ii, names=None ):
0128 u, x, c = np.unique(ii, return_index=True, return_counts=True )
0129 o = np.argsort(c)[::-1]
0130 nn = names[u] if not names is None else None
0131
0132 _tab = "np.c_[u,c,x,nn][o]" if not nn is None else "np.c_[u,c,x][o]"
0133 tab = eval(_tab)
0134
0135 self.symbol = symbol
0136 self._tab = _tab
0137 self.tab = tab
0138
0139 self.u = u[o]
0140 self.x = x[o]
0141 self.c = c[o]
0142 self.n = nn[o] if not nn is None else None
0143
0144
0145 def __repr__(self):
0146 lines = []
0147 lines.append("[%s" % self.symbol)
0148 lines.append(repr(self.tab))
0149 lines.append("]%s" % self.symbol)
0150 return "\n".join(lines)
0151
0152
0153
0154
0155 if __name__ == '__main__':
0156
0157 logging.basicConfig(
0158 level=logging.INFO,
0159 format='%(asctime)s - %(message)s',
0160 datefmt='%H:%M:%S'
0161 )
0162
0163 log.info("GLOBAL:%d MODE:%d" % (GLOBAL,MODE))
0164
0165 prn_config = KeyNameConfig.Parse("$HOME/.opticks/GEOM/cxt_min.ini", "key_prim_regexp")
0166
0167
0168 if not "MFOLD" in os.environ:
0169 print("FATAL cxt_min.py simtrace plotting now needs MFOLD envvar, not the former FOLD : as that is too common")
0170 pass
0171 e = SEvt.Load("$MFOLD", symbol="e")
0172 a = SEvt.Load("$AFOLD", symbol="a") if A == 1 else None
0173 b = SEvt.Load("$BFOLD", symbol="b") if B == 1 else None
0174
0175
0176 print(repr(e))
0177 if e is None:
0178 fmt = "ABORT script as failed to SEvt.Load from MFOLD[%s] GEOM is [%s]"
0179 print(fmt % (os.environ.get("MFOLD", None), os.environ.get("GEOM", None)))
0180 sys.exit(0)
0181 pass
0182
0183 label = e.f.base
0184 sf = e.f.sframe
0185 w2m = sf.w2m
0186 m2w = sf.m2w
0187
0188 gs = e.f.genstep
0189 st = e.f.simtrace
0190
0191 gs_pos = gs[:,1]
0192 all_one = np.all( gs_pos[:,3] == 1. )
0193 all_zero = np.all( gs_pos[:,:3] == 0 )
0194
0195
0196
0197 assert all_one
0198
0199 gs_tra = gs[:,2:]
0200 assert gs_tra.shape[1:] == (4,4)
0201
0202
0203 ggrid = np.zeros( (len(gs), 4 ), dtype=np.float32 )
0204 for i in range(len(ggrid)): ggrid[i] = np.dot(gs_pos[i], gs_tra[i])
0205
0206 lgrid = np.dot( ggrid, w2m )
0207 ugrid = ggrid if GLOBAL else lgrid
0208
0209 st_x = st[:,1,0]
0210 st_y = st[:,1,1]
0211 st_z = st[:,1,2]
0212 st_gp = st[:,2,3].view(np.int32) >> 16
0213 st_bn = st[:,2,3].view(np.int32) & 0xffff
0214 st_id = st[:,3,3].view(np.int32)
0215
0216 presel = slice(None)
0217
0218
0219
0220
0221 PRESEL = os.environ.get("PRESEL", "")
0222 if PRESEL.startswith("PRIM:"):
0223 spec = PRESEL[len("PRIM:"):]
0224 gps = np.array([], dtype=np.int64)
0225 for elem in spec.split(","):
0226 if str.isdigit(elem):
0227 gps = np.concatenate(gps, int(elem))
0228 else:
0229 egp = np.unique(np.where( cf.primname == elem ))
0230 gps = np.concatenate( (gps, egp) )
0231 pass
0232 pass
0233 gps = np.unique(gps)
0234 presel = np.isin(st_gp, gps )
0235 pass
0236 elif PRESEL == "LPMT":
0237 presel = np.logical_and( st_id > 0, st_id < 30000 )
0238 elif PRESEL == "SPMT":
0239 presel = np.logical_and( st_id >= 30000, st_id < 50000 )
0240 elif PRESEL == "PPMT":
0241 presel = st_id >= 50000
0242 elif PRESEL == "GLOBAL":
0243 presel = st_id == 0
0244 else:
0245 presel = slice(None)
0246 pass
0247
0248
0249
0250
0251
0252
0253
0254 ust = st[presel]
0255
0256
0257 _ii = ust[:,3,3].view(np.int32)
0258 _gp_bn = ust[:,2,3].view(np.int32)
0259 _gp = _gp_bn >> 16
0260 _bn = _gp_bn & 0xffff
0261
0262 gnrm = ust[:,0].copy()
0263 gpos = ust[:,1].copy()
0264
0265 gnrm[...,3] = 0.
0266 gpos[...,3] = 1.
0267
0268
0269 lnrm = np.dot( gnrm, w2m )
0270 lpos = np.dot( gpos, w2m )
0271
0272 elu_m2w = m2w if GLOBAL else np.eye(4)
0273 _unrm = gnrm if GLOBAL else lnrm
0274 _upos = gpos if GLOBAL else lpos
0275
0276 if "BOXSEL" in os.environ:
0277 BOXSEL = np.fromstring(os.environ.get("BOXSEL","-1000,1000,-1000,1000,-1000,1000"),sep=",").reshape(3,2)
0278 boxsel = np.all( (_upos[:,:3] > BOXSEL[:,0]) & (_upos[:,:3] < BOXSEL[:,1]), axis=1)
0279 upos = _upos[boxsel]
0280 unrm = _unrm[boxsel]
0281
0282 ii = _ii[boxsel]
0283 gp = _gp[boxsel]
0284 bn = _bn[boxsel]
0285 else:
0286 upos = _upos
0287 unrm = _unrm
0288 ii = _ii
0289 gp = _gp
0290 bn = _bn
0291 pass
0292
0293
0294
0295
0296
0297 EXPR = list(map(str.strip,textwrap.dedent(r"""
0298 st.shape
0299 ust.shape
0300 np.c_[np.unique(gp,return_counts=True)]
0301 np.c_[np.unique(bn,return_counts=True)]
0302 np.c_[np.unique(ii,return_counts=True)]
0303 """).split("\n")))
0304
0305 for expr in EXPR:
0306 print(expr)
0307 if expr == "" or expr[0] == "#": continue
0308 print(repr(eval(expr)))
0309 pass
0310
0311
0312
0313
0314
0315
0316
0317 idtab = UniqueTable("idtab", ii, None)
0318 print(repr(idtab))
0319
0320 gptab = UniqueTable("gptab", gp, cf.primname)
0321 print(repr(gptab))
0322
0323
0324
0325 KEY = os.environ.get("KEY", None)
0326 KEY_invert = False
0327 if not KEY is None:
0328 if KEY.startswith("~"):
0329 KEY = KEY[1:]
0330 KEY_invert = True
0331 pass
0332 KK = KEY.split(",")
0333 else:
0334 KK = None
0335 pass
0336
0337
0338 cxtb, btab = cf.simtrace_boundary_analysis(bn, KEY)
0339 print(repr(cxtb))
0340 print(repr(btab))
0341
0342 cxtp, ptab = cf.simtrace_prim_analysis(gp, KEY)
0343 print(repr(cxtp))
0344 print(repr(ptab))
0345
0346 cxtable = cxtp if PRIMTAB==1 else cxtb
0347
0348
0349
0350 ASLICE = None
0351 AIDX = int(os.environ.get("AIDX","0"))
0352 if not a is None:
0353 if "AFOLD_RECORD_SLICE" in os.environ:
0354 ASLICE = os.environ["AFOLD_RECORD_SLICE"]
0355 ase = a.q_startswith(ASLICE)
0356 g_apos = a.f.record[ase][:,:,0].copy()
0357 else:
0358 ase = np.where( a.f.record[:,:,3,3].view(np.int32) > 0 )
0359 g_apos = a.f.record[ase][:,0].copy()
0360
0361 pass
0362
0363 g_apos[...,3] = 1.
0364 l_apos = np.dot( g_apos, w2m )
0365 u_apos = g_apos if GLOBAL else l_apos
0366 else:
0367 g_apos = None
0368 l_apos = None
0369 u_apos = None
0370 pass
0371
0372 if not b is None:
0373 if "BFOLD_RECORD_SLICE" in os.environ:
0374 bse = b.q_startswith(os.environ["BFOLD_RECORD_SLICE"])
0375 g_bpos = b.f.record[bse][:,:,0].copy()
0376 else:
0377 bse = np.where( b.f.record[:,:,3,3].view(np.int32) > 0 )
0378 g_bpos = b.f.record[bse][:,0].copy()
0379 pass
0380 g_bpos[...,3] = 1.
0381 l_bpos = np.dot( g_bpos, w2m )
0382 u_bpos = g_bpos if GLOBAL else l_bpos
0383 else:
0384 g_bpos = None
0385 l_bpos = None
0386 u_bpos = None
0387 pass
0388
0389
0390
0391 X,Y,Z = 0,1,2
0392 H,V = X,Z
0393
0394
0395
0396 class SimtracePlot(object):
0397 """
0398 defined inline as uses::
0399
0400 KK
0401 upos
0402 unrm
0403 NORMAL_FILTER
0404
0405 """
0406 def __init__(self, pl, cxtable, hide=False):
0407 """
0408 :param pl: pyvista plotter
0409 :param cxtable: analysis of simtrace integer such as boundary or globalPrimIdx
0410 """
0411 self.cxtable = cxtable
0412 pl.enable_point_picking(callback=self, use_picker=True, show_message=False)
0413 pcloud = {}
0414 pts = {}
0415 nrm = {}
0416 pcinfo = np.zeros( (len(cxtable.wdict),3), dtype=np.int64 )
0417 i = 0
0418 keys = np.array(list(cxtable.wdict.keys()))
0419
0420 for k, w in cxtable.wdict.items():
0421 if not KK is None:
0422 skip0 = KEY_invert is False and not k in KK
0423 skip1 = KEY_invert is True and k in KK
0424 if skip0: continue
0425 if skip1: continue
0426 pass
0427 label = self.get_label(k)
0428
0429
0430 if os.environ.get("KEYOFF","").startswith(k+":"):
0431 KEYOFF = np.fromstring(os.environ["KEYOFF"][len(k+":"):],sep=",").reshape(3)
0432 else:
0433 KEYOFF = np.array([0,0,0], dtype=np.float32)
0434 pass
0435 pts[k] = upos[w,:3] + KEYOFF
0436 nrm[k] = unrm[w,:3]
0437 if not hide:
0438 pcloud[k] = pvp.pvplt_add_points(pl, pts[k], color=k, label=label )
0439 else:
0440 pcloud[k] = None
0441 pass
0442 n_verts = pcloud[k].n_verts if not pcloud[k] is None else 0
0443 pcinfo[i,0] = n_verts
0444 if NORMAL > 0: pvp.pvplt_add_delta_lines(pl, upos[w,:3][::NORMAL_FILTER], 20*unrm[w,:3][::NORMAL_FILTER], color=k, pickable=False )
0445 i += 1
0446 pass
0447 pcinfo[:,1] = np.cumsum(pcinfo[:,0])
0448 pcinfo[:,2] = pcinfo[:,0] + pcinfo[:,1] - 1
0449 self.pcloud = pcloud
0450 self.pcinfo = pcinfo
0451 self.keys = keys
0452 self.pts = pts
0453 self.nrm = nrm
0454
0455 def get_label(self, k):
0456 cxtable = self.cxtable
0457 w = cxtable.wdict[k]
0458 label = "%15s %8d %s" % (k,len(w),cxtable.d_anno[k])
0459 return label
0460
0461 def __call__(self, picked_point, picker):
0462 """
0463 """
0464 self.picked_point = picked_point
0465 self.picker = picker
0466
0467 pcloud = self.pcloud
0468 pcinfo = self.pcinfo
0469
0470 dataSet = picker.GetDataSet()
0471 pointId = picker.GetPointId()
0472
0473 idx= list(pcloud.values()).index(dataSet)
0474 k = list(pcloud.keys())[idx]
0475 label = self.get_label(k)
0476
0477 print("SimtracePlot.__call__ picked_point %s pointId %d idx %d k %s label %s " % (str(picked_point), pointId, idx, k, label) )
0478 pass
0479 pass
0480
0481
0482
0483
0484
0485 if MODE == 2:
0486 pl = pvp.mpplt_plotter(label=label)
0487 fig, axs = pl
0488 ax = axs[0]
0489
0490 for k, w in cxtb.wdict.items():
0491 if not KEY is None and not k in KK: continue
0492 ax.scatter( upos[w,H], upos[w,V], s=0.1, color=k )
0493 pass
0494
0495 if GSGRID: ax.scatter( ugrid[:,H], ugrid[:,V], s=0.1, color="r" )
0496
0497
0498
0499
0500 fig.show()
0501 elif MODE == 3:
0502 pl = pvp.pvplt_plotter(label)
0503
0504 pvp.pvplt_viewpoint(pl, verbose=True, m2w=elu_m2w)
0505
0506
0507 if FRAME: pvp.pvplt_frame(pl, sf, local=not GLOBAL, pickable=False )
0508
0509
0510 HIDE="HIDE" in os.environ
0511 spl = SimtracePlot(pl, cxtable, hide=HIDE)
0512
0513
0514 if "OVERLAP" in os.environ and len(spl.pcloud) == 2 and not KDTree is None:
0515 overlap_tol = float(os.environ["OVERLAP"])
0516 assert overlap_tol > 0
0517 close_tol = overlap_tol*10
0518 print("OVERLAP check overlap_tol : %s close_tol %s " % (overlap_tol, close_tol ))
0519 k0, k1 = list(spl.pts.keys())
0520 pt0 = spl.pts[k0]
0521 pt1 = spl.pts[k1]
0522 nr0 = spl.nrm[k0]
0523 nr1 = spl.nrm[k1]
0524
0525
0526 log.info("-OVERLAP-form KDTree kt0 pt0.shape %s" % (str(pt0.shape)))
0527 kt0 = KDTree(pt0)
0528 log.info("-OVERLAP-form KDTree kt1 pt1.shape %s" % (str(pt1.shape)))
0529 kt1 = KDTree(pt1)
0530
0531
0532 log.info("-OVERLAP-kt0.query pt1.shape %s " % (str(pt1.shape)))
0533 dist_1to0, idx_1to0 = kt0.query(pt1, k=1)
0534 log.info("-OVERLAP-kt0.query.DONE")
0535 log.info("-OVERLAP-kt1.query pt0.shape %s " % (str(pt0.shape)))
0536 dist_0to1, idx_0to1 = kt1.query(pt0, k=1)
0537 log.info("-OVERLAP-kt1.query.DONE")
0538
0539 close_0 = dist_0to1 < close_tol
0540 close_1 = dist_1to0 < close_tol
0541
0542 close_pt0 = pt0[close_0]
0543 close_pt1 = pt1[close_1]
0544 close_pt = np.unique(np.vstack([close_pt0, close_pt1]), axis=0)
0545
0546 all_sd0 = np.sum( (pt0 - pt1[idx_0to1])*nr1[idx_0to1], axis=1 )
0547 all_sd1 = np.sum( (pt1 - pt0[idx_1to0])*nr0[idx_1to0], axis=1 )
0548
0549 close_sd0 = np.sum( (pt0 - pt1[idx_0to1])[close_0]*nr1[idx_0to1][close_0], axis=1 )
0550 close_sd1 = np.sum( (pt1 - pt0[idx_1to0])[close_1]*nr0[idx_1to0][close_1], axis=1 )
0551
0552
0553
0554
0555 overlap_pt0 = pt0[close_0][close_sd0 < -overlap_tol]
0556 overlap_pt1 = pt1[close_1][close_sd1 < -overlap_tol]
0557
0558
0559 overlap_pt = np.unique(np.vstack([overlap_pt0, overlap_pt1]), axis=0)
0560
0561 cegs_npy = os.environ.get("CEGS_NPY","")
0562 if cegs_npy.endswith(".npy"):
0563 np.save(cegs_npy, overlap_pt)
0564 pass
0565
0566 close_label = "close_pt %s " % (len(close_pt))
0567 overlap_label = "overlap_pt %s " % (len(overlap_pt))
0568
0569
0570 pvp.pvplt_add_points(pl, overlap_pt, color="yellow", label=overlap_label, copy_mesh=True, point_size=5 )
0571 else:
0572 close_pd = None
0573 pass
0574
0575
0576
0577 if GSGRID: pl.add_points(ugrid[:,:3], color="r", pickable=False )
0578
0579 if not u_apos is None: pvp.pvplt_add_points_adaptive(pl, u_apos, color="red", label="u_apos" )
0580 if not u_bpos is None: pvp.pvplt_add_points_adaptive(pl, u_bpos, color="blue", label="u_bpos" )
0581
0582 if not u_apos is None and u_apos.ndim == 3 and not ASLICE is None:
0583 num_step_point = len(ASLICE.split())
0584 pvp.pvplt_add_contiguous_line_segments( pl, u_apos[AIDX,:num_step_point,:3], point_size=4 )
0585 pass
0586
0587 def callback_click_position(xy):
0588 print("callback_click_position xy : %s " % str(xy))
0589 pass
0590 pl.track_click_position(callback_click_position)
0591 if not POINT is None: pl.add_points(POINT, color="r", label="POINT")
0592
0593
0594
0595 cp = pvp.pvplt_show(pl, incpoi=-5, legend=True, title=None )
0596 else:
0597 pass
0598 pass
0599 pass
0600
0601