File indexing completed on 2026-04-09 07:48:50
0001
0002 """
0003 snap.py : analysis of scan metadata
0004 ======================================
0005
0006 ::
0007
0008
0009 snap.py # list the snaps in speed order with labels
0010
0011 open $(snap.py --jpg) # open the jpg ordered by render speed
0012
0013
0014
0015 Typical call stack using snap.py::
0016
0017 ~/o/CSGOptiX/elv.sh
0018 ~/o/CSGOptiX/cxr.sh : jstab
0019 ~/o/bin/BASE_grab.sh : jstab
0020 snap.py
0021
0022 The reason for this stack is to do the definition of the BASE directory
0023 in one place and not repeat that.
0024
0025 Which inputs are present depends on recent scan runs::
0026
0027 ~/o/CSGOptiX/cxr_scan.sh
0028
0029
0030 """
0031 import os, sys, logging, glob, json, re, argparse
0032 log = logging.getLogger(__name__)
0033 import numpy as np
0034 from opticks.ana.rsttable import RSTTable
0035 from opticks.CSG.CSGFoundry import CSGFoundry, LV, MM
0036
0037
0038 class MM(object):
0039 """
0040 The input file is now a standard resident of the CSGFoundry directory
0041 Note that the input file was formerly created using::
0042
0043 ggeo.py --mmtrim > $TMP/mm.txt
0044 # created list of mm names used for labels at $TMP/mm.txt
0045
0046 """
0047 PTN = re.compile("\d+")
0048 def __init__(self, path ):
0049 mm = os.path.expandvars(path)
0050 mm = open(mm, "r").read().splitlines() if os.path.exists(mm) else None
0051 self.mm = mm
0052 if mm is None:
0053 log.fatal("missing %s, which is now a standard part of CSGFoundry " % path )
0054 sys.exit(1)
0055 pass
0056
0057 def imm(self, emm):
0058 return list(map(int, self.PTN.findall(emm)))
0059
0060 def label(self, emm):
0061 imm = self.imm(emm)
0062 labs = [self.mm[i] for i in imm]
0063 lab = " ".join(labs)
0064
0065 tilde = emm[0] == "t" or emm[0] == "~"
0066 pfx = ( "NOT: " if tilde else " " )
0067
0068 if emm == "~0" or emm == "t0":
0069 return "ALL"
0070 elif imm == [1,2,3,4]:
0071 return "ONLY PMT"
0072 elif "," in emm:
0073 return ( "EXCL: " if tilde else "ONLY: " ) + lab
0074 else:
0075 return lab
0076 pass
0077
0078 def __repr__(self):
0079 return "\n".join(self.mm)
0080
0081
0082 class DummyCandleSnap(object):
0083 def __init__(self):
0084 self.av = 1
0085
0086
0087 class Snap(object):
0088 """
0089
0090 cxr_overview_emm_t0,_elv_t_moi__ALL00000.jpg
0091 cxr_overview_emm_t0,_elv_t_moi__ALL.jpg
0092
0093 """
0094 PTN0 = re.compile("(?P<pfx>cxr_\S*)_emm_(?P<emm>\S*)_elv_(?P<elv>\S*)_moi_(?P<moi>\S*)(?P<jdx>\d{5})")
0095 PTN1 = re.compile("(?P<pfx>cxr_\S*)_emm_(?P<emm>\S*)_elv_(?P<elv>\S*)_moi_(?P<moi>\S*)")
0096
0097 PTN_EMM_ELV = re.compile("emm_(?P<emm>\S+?)_elv_(?P<elv>\S+?)_")
0098
0099 @classmethod
0100 def is_valid(cls, jpg_path):
0101
0102 json_path = jpg_path.replace(".jpg", ".json")
0103 valid = (not json_path is None) and json_path[-5:] == ".json"
0104 log.debug("is_valid %r %r %r " % (jpg_path, json_path, valid ) )
0105 return valid
0106
0107 @classmethod
0108 def ParseStem_OLD(cls, jpg_stem):
0109 m0 = cls.PTN0.match(jpg_stem)
0110 m1 = cls.PTN1.match(jpg_stem)
0111 m = m0
0112 if m0 is None:
0113 m = m1
0114 pass
0115 d = m.groupdict() if not m is None else {}
0116 return d
0117
0118 @classmethod
0119 def ParseStem(cls, jpg_stem):
0120 m = cls.PTN_EMM_ELV.search(jpg_stem)
0121 d = m.groupdict() if not m is None else {}
0122 return d
0123
0124 def __init__(self, jpg_path, selectmode="elv" ):
0125 """
0126 1. determine path to json sidecar by replacing extension .jpg with .json
0127 2. load json
0128 3. access some json values such as av
0129 4. parse the stem to give elv, emm, moi
0130 """
0131 json_path = jpg_path.replace(".jpg", ".json")
0132 jpg_stem = os.path.splitext(os.path.basename(jpg_path))[0]
0133
0134
0135 js = json.load(open(json_path,"r"))
0136
0137 self.js = js
0138 self.jpg = jpg_path
0139 self.path = json_path
0140 self.av = js['av']
0141 self.argline = js.get('argline', "no-argline")
0142
0143 dstem = self.ParseStem(jpg_stem)
0144
0145 elv = dstem.get("elv", None)
0146 emm = dstem.get("emm", None)
0147 moi = dstem.get("moi", None)
0148 jdx = dstem.get("jdx", None)
0149
0150
0151
0152
0153 self.dstem = dstem
0154 self.elv = elv
0155 self.emm = emm
0156 self.moi = moi
0157 self.jdx = jdx
0158
0159 self.selectmode = selectmode
0160 self.enabled = elv if selectmode == "elv" else emm
0161
0162
0163 self.sc = None
0164 self.idx = None
0165
0166 def jpg_(self):
0167 """
0168 :return jpg_path: avoiding strings that cause RST issues
0169
0170 tilde ~ and __t0__ causing RST problems : so replace tham
0171 """
0172 fold = os.path.dirname(self.jpg)
0173 tname = self.jpg_tname()
0174 return os.path.join(fold, tname)
0175
0176 def jpg_tname(self):
0177 """
0178 This is a workaround for problems with s5 machinery for names
0179 containing strings like the below, presumably due to some RST meaning
0180 the names get mangled preventing the association between presentation pages and
0181 background image definition causes the images to not appear::
0182
0183 __~0__
0184 __t0__
0185 _ALL_
0186
0187 ACTUALLY THIS ISSUE MAY BE FROM NON-UNIQUE IDENTIFIERS DERIVED FROM THE
0188 SLIDE TITLES BY REMOVING SOME CHARS SUCH AS "," "_" "~"
0189 """
0190 name = os.path.basename(self.jpg)
0191 tname = name.replace("~","t")
0192 tname = tname.replace("__t0__", "_all_")
0193 return tname
0194
0195 def mvjpg(self):
0196 """
0197 :return cmdstring: to change the name of the jpg to jpg_tname avoiding RST issues
0198 """
0199 name = os.path.basename(self.jpg)
0200 tname = self.jpg_tname()
0201 return None if name == tname else "mv %s %s" % (name, tname)
0202
0203 def cpjpg(self, pfx, s5base):
0204 """
0205 :return cmdstring: that copies the absolute jpg path into presentation tree
0206 """
0207 s5base = os.path.expandvars(s5base)
0208 name = os.path.basename(self.jpg)
0209 ppath = "%s%s/%s"%(s5base,pfx,name)
0210 if os.path.exists(ppath):
0211 ret = None
0212 else:
0213 ret = "cp %s %s" % (self.jpg, ppath)
0214 return ret
0215
0216 def refjpg(self, pfx, afx="1280px_720px", indent=" "):
0217 """
0218 :return cmdstring: for inclusion into s5_background_image.txt
0219 """
0220 name = os.path.basename(self.jpg_())
0221 return "\n".join([indent+self.title(), indent+pfx+"/"+name+" "+afx, ""])
0222
0223 def title(self):
0224 name = os.path.basename(self.jpg_())
0225 stem = name.replace(".jpg","")
0226 return "[%d]%s" % (self.idx, stem)
0227
0228 def pagetitle(self,kls="blue"):
0229 return ":%s:`%s`" % (kls,self.title())
0230
0231 def pagejpg(self):
0232 title = self.pagetitle()
0233 return "\n".join([title, "-" * len(title), ""])
0234
0235
0236 over_fast = property(lambda self:float(self.av)/float(self.sc.fast.av))
0237 over_slow = property(lambda self:float(self.av)/float(self.sc.slow.av))
0238 over_candle = property(lambda self:float(self.av)/float(self.sc.candle.av))
0239
0240 label = property(lambda self:self.sc.label(self.enabled))
0241 imm = property(lambda self:self.sc.mm.imm(self.emm))
0242
0243 def row(self):
0244 return (int(self.idx), self.enabled, self.av, self.over_candle, self.label )
0245
0246
0247 SPACER = " "
0248 LABELS = ["idx", "-e", "time(s)", "relative", "enabled geometry description" ]
0249 WIDS = [ 3, 10, 10, 10, 70 ]
0250 HFMT = [ "%3s", "%10s", "%10s", "%10s", "%-70s" ]
0251 RFMT = [ "%3d", "%10s", "%10.4f", "%10.4f", "%-70s" ]
0252 PRE = [ "" , "" , SPACER , SPACER , SPACER ]
0253 POST = [ "" , "" , SPACER , SPACER , SPACER ]
0254
0255
0256 def __repr__(self):
0257 RFMT = [ self.PRE[i] + self.RFMT[i] +self.POST[i] for i in range(len(self.RFMT))]
0258 rfmt = " ".join(RFMT)
0259
0260 return rfmt % self.row()
0261
0262 @classmethod
0263 def Hdr(cls):
0264 HFMT = [cls.PRE[i] + cls.HFMT[i] + cls.POST[i] for i in range(len(cls.HFMT))]
0265 hfmt = " ".join(HFMT)
0266 labels = cls.LABELS
0267 return hfmt % tuple(labels)
0268
0269
0270 class SnapScan(object):
0271 @classmethod
0272 def MakeSnaps(cls, globptn_, reverse=False, selectmode="elv"):
0273 """
0274 1. resolve globptn into a sorted list of paths, typically of jpg render files
0275 2. select paths considered valid, currently all paths found
0276 3. create Snap instances for every path
0277 4. order the Snap instances based in Snap.av obtained from the sidecar json file
0278 5. return the snaps
0279 """
0280 globptn = os.path.expandvars(globptn_)
0281 log.info("globptn_ %s globptn %s " % (globptn_, globptn) )
0282 raw_paths = glob.glob(globptn)
0283 log.info("globptn raw_paths %d : 1st %s " % (len(raw_paths), raw_paths[0]))
0284 paths = list(filter(lambda p:Snap.is_valid(p), raw_paths))
0285 log.info("after is_valid filter len(paths): %d " % len(paths))
0286 snaps = list(map(lambda p:Snap(p,selectmode=selectmode),paths))
0287 snaps = list(sorted(snaps, key=lambda s:s.av, reverse=reverse))
0288 return snaps
0289
0290 @classmethod
0291 def SelectSnaps_imm(cls, all_snaps, selectspec):
0292 """
0293 Selection of snaps based on the imm list of ints for each snap
0294 """
0295 snaps = []
0296 for s in all_snaps:
0297
0298 if len(s.imm) in [1,] or s.imm == [8, 0] or s.imm == [1,2,3,4]:
0299 snaps.append(s)
0300 else:
0301 log.info("skip %s " % str(s.imm))
0302 pass
0303 pass
0304 return snaps
0305
0306 @classmethod
0307 def SelectSnaps_elv(cls, all_snaps, selectspec):
0308 SNAP_LIMIT = int(os.environ.get("SNAP_LIMIT", "256" ))
0309 snaps = []
0310 for s in all_snaps:
0311
0312 if selectspec == "all":
0313 snaps.append(s)
0314 elif selectspec == "not_elv_t":
0315 if not s.elv.startswith("t"):
0316 snaps.append(s)
0317 pass
0318 elif selectspec == "only_elv_t":
0319 if s.elv.startswith("t"):
0320 snaps.append(s)
0321 pass
0322 else:
0323 log.fatal("selectspec %s must be one of : all not_elv_t only_elv_t " % selectspec )
0324 assert 0
0325 pass
0326 pass
0327 pass
0328 lim_snaps = snaps[:SNAP_LIMIT]
0329 log.info("all_snaps %d selectspec %s snaps %d SNAP_LIMIT %d lim_snaps %d " % (len(all_snaps), selectspec, len(snaps), SNAP_LIMIT, len(lim_snaps) ))
0330 return lim_snaps
0331
0332
0333 @classmethod
0334 def GEOMInfo(cls):
0335 """
0336 Returns MM and LV instances
0337
0338 """
0339 cfptn = "$HOME/.opticks/GEOM/$GEOM/CSGFoundry"
0340 cfdir = os.path.expandvars(cfptn)
0341 log.info("cfptn %s cfdir %s " % (cfptn, cfdir) )
0342
0343 mmlabel_path = os.path.join(cfdir, "mmlabel.txt")
0344 log.info("mmlabel_path %s " % mmlabel_path )
0345 mm = MM(mmlabel_path)
0346
0347 meshname_path = os.path.join(cfdir, "meshname.txt")
0348 log.info("meshname_path %s " % meshname_path )
0349 lv = LV(meshname_path)
0350
0351 return mm, lv
0352
0353
0354 @classmethod
0355 def Create(cls, globptn, reverse, selectmode, selectspec, candle):
0356 """
0357 :param globptn:
0358 :param reverse: bool
0359 :param selectmode: "imm" or "elv" or "all"
0360 :param selectspec: argument depending on selectmode
0361 :param candle:
0362 """
0363 ss = cls(globptn, candle=candle, reverse=reverse, selectmode=selectmode, selectspec=selectspec )
0364 return ss
0365
0366
0367 def __init__(self, globptn, candle="1,2,3,4", reverse=False, selectmode="elv", selectspec=""):
0368 """
0369 :param globptn: eg
0370
0371 1. find all_snaps Snap instances using the globptn
0372 2. set s_candle to the Snap instance for which s.enabled matches candle, eg "1,2,3,4"
0373
0374
0375 """
0376 mm,lv = self.GEOMInfo()
0377 self.mm = mm
0378 self.lv = lv
0379 self.selectmode = selectmode
0380 self.selectspec = selectspec
0381
0382 all_snaps = self.MakeSnaps(globptn, reverse=reverse, selectmode=selectmode)
0383 s_candle = None
0384
0385 n_candle = 0
0386
0387 for s in all_snaps:
0388 s.sc = self
0389 if s.enabled == candle:
0390 s_candle = s
0391 n_candle += 1
0392 pass
0393 pass
0394 log.info("all_snaps:%d candle:%s n_candle:%d selectmode:%s " % (len(all_snaps), candle, n_candle, selectmode ) )
0395
0396
0397
0398 if self.selectmode in ["imm","emm"]:
0399 snaps = self.SelectSnaps_imm(all_snaps, selectspec)
0400 elif self.selectmode == "elv":
0401 snaps = self.SelectSnaps_elv(all_snaps, selectspec)
0402 elif self.selectmode == "all":
0403 snaps = all_snaps
0404 else:
0405 snaps = all_snaps
0406 pass
0407
0408 log.info("after selectmode:%s selectspec:%s snaps:%d " % (selectmode, selectspec, len(snaps)))
0409
0410 for idx, s in enumerate(snaps):
0411 s.idx = idx
0412 pass
0413 self.snaps = snaps
0414 self.all_snaps = all_snaps
0415
0416 if s_candle is None:
0417 s_candle = DummyCandleSnap()
0418 pass
0419 self.candle = s_candle
0420
0421 def label(self, enabled):
0422
0423 return self.lv.label(enabled) if self.selectmode == "elv" else self.mm.label(enabled)
0424
0425 def table(self):
0426 table = np.empty([len(self.snaps),len(Snap.LABELS)], dtype=np.object )
0427 for idx, snap in enumerate(self.snaps):
0428 table[idx] = snap.row()
0429 pass
0430 return table
0431
0432 def rst_table(self):
0433 t = self.table()
0434
0435 labels = Snap.LABELS
0436
0437
0438 rst = RSTTable.Render(t, labels, Snap.WIDS, Snap.HFMT, Snap.RFMT, Snap.PRE, Snap.POST )
0439 return rst
0440
0441
0442 fast = property(lambda self:self.snaps[0])
0443 slow = property(lambda self:self.snaps[-1])
0444
0445 def __repr__(self):
0446 return "\n".join( [Snap.Hdr()] + list(map(repr,self.snaps)) + [Snap.Hdr()] )
0447
0448 def jpg(self):
0449 return "\n".join(list(map(lambda s:s.jpg,self.snaps)))
0450
0451 def mvjpg(self):
0452 return "\n".join(list(filter(None,map(lambda s:s.mvjpg(),self.snaps))))
0453
0454 def cpjpg(self, pfx, s5base):
0455 return "\n".join(list(filter(None,map(lambda s:s.cpjpg(pfx, s5base),self.snaps))))
0456
0457 def argline(self):
0458 return "\n".join(list(map(lambda s:s.argline,self.snaps)))
0459
0460 def refjpg(self, pfx):
0461 return "\n".join(list(map(lambda s:s.refjpg(pfx),self.snaps)))
0462
0463 def pagejpg(self):
0464 return "\n".join(list(map(lambda s:s.pagejpg(),self.snaps)))
0465
0466
0467 def edef(key, fallback, prefix="SNAP_"):
0468 return os.environ.get(prefix+key,fallback)
0469
0470 def parse_args(doc, **kwa):
0471 np.set_printoptions(suppress=True, precision=3, linewidth=200)
0472 parser = argparse.ArgumentParser(doc)
0473
0474 SNAP_level = edef("level", "info")
0475 SNAP_globptn = edef("globptn", "$TMP/snap/*.jpg")
0476 SNAP_refjpgpfx = edef("refjpgpfx", "/env/presentation/snap/lLowerChimney_phys" )
0477 SNAP_s5base = edef("s5base", "$HOME/simoncblyth.bitbucket.io")
0478 SNAP_outpath = edef("outpath", "/tmp/ana_snap.txt" )
0479 SNAP_selectmode = edef("selectmode", "elv" )
0480 SNAP_selectspec = edef("selectspec", "not_elv_t")
0481 SNAP_candle = edef("candle","t")
0482 SNAP_dump = edef("dump","txt")
0483 SNAP_out = edef("out",True)
0484 SNAP_reverse = edef("reverse",False)
0485
0486
0487 parser.add_argument( "--level", default=SNAP_level, help="logging level" )
0488 parser.add_argument( "--globptn", default=SNAP_globptn, help="base" )
0489 parser.add_argument( "--refjpgpfx", default=SNAP_refjpgpfx, help="List jpg paths s5 background image presentation format" )
0490 parser.add_argument( "--s5base", default=SNAP_s5base, help="Presentation repo base" )
0491 parser.add_argument( "--outpath", default=SNAP_outpath, help="Path where to write output" )
0492 parser.add_argument( "--selectmode", default=SNAP_selectmode, help="SnapScan selection mode" )
0493 parser.add_argument( "--selectspec", default=SNAP_selectspec, help="all/only_elv_t/not_elv_t : May be used to configure snap selection" )
0494 parser.add_argument( "--candle", default=SNAP_candle, help="Enabled setting to use as the reference candle for relative column in the table" )
0495 parser.add_argument( "--out" , default=SNAP_out, help="Enable saving output to *outpath*", action="store_true" )
0496 parser.add_argument( "--reverse", default=SNAP_reverse, help="Reverse time order with slowest first", action="store_true" )
0497
0498
0499 doc = {}
0500 doc["jpg"] = "Dump list of jpg paths in speed order"
0501 doc["rst"] = "Dump table in RST format"
0502 doc["txt"] = "Dump table in TXT format"
0503 doc["refjpg"] = "List jpg paths s5 background image presentation format"
0504 doc["pagejpg"] = "List jpg for inclusion into s5 presentation"
0505 doc["mvjpg"] = "List commands to mv the jpg"
0506 doc["cpjpg"] = "List cp commands to place into presentation repo"
0507 doc["argline"] = "List argline in speed order"
0508 doc["snaps"] = "Debug: just create SnapScan "
0509
0510 parser.add_argument( "--dump", choices=list(doc.keys()), default=SNAP_dump, help="Specify what to dump" )
0511
0512
0513
0514 args = parser.parse_args()
0515 fmt = '[%(asctime)s] p%(process)s {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s'
0516 logging.basicConfig(level=getattr(logging,args.level.upper()), format=fmt)
0517 return args
0518
0519
0520 if __name__ == '__main__':
0521 args = parse_args(__doc__)
0522 log.debug(" args %s " % str(args))
0523
0524 log.info(" args.globptn : %s " % (args.globptn) )
0525 log.info(" args.dump : %s " % (args.dump) )
0526
0527 ss = SnapScan.Create(args.globptn, args.reverse, args.selectmode, args.selectspec, args.candle)
0528
0529 out = None
0530 if args.dump == "jpg":
0531 out = str(ss.jpg())
0532 elif args.dump == "refjpg":
0533 out = str(ss.refjpg(args.refjpgpfx))
0534 elif args.dump == "pagejpg":
0535 out = str(ss.pagejpg())
0536 elif args.dump == "mvjpg":
0537 out = str(ss.mvjpg())
0538 elif args.dump == "cpjpg":
0539 out = str(ss.cpjpg(args.refjpgpfx, args.s5base))
0540 elif args.dump == "argline":
0541 out = str(ss.argline())
0542 elif args.dump == "snaps":
0543 out = str(ss.snaps)
0544 elif args.dump == "rst":
0545 out = str(ss.rst_table())
0546 elif args.dump == "txt":
0547 out = str(ss)
0548 else:
0549 out = str(ss)
0550 pass
0551 out += "\n"
0552
0553 if args.out:
0554 log.info("--out writing to %s " % args.outpath)
0555 open(args.outpath, "w").write(out)
0556 else:
0557 print(out)
0558 pass
0559
0560