#!/usr/bin/env seiscomp-python ############################################################################ # Copyright (C) 2016 by gempa GmbH # # # # All Rights Reserved. # # # # NOTICE: All information contained herein is, and remains # # the property of gempa GmbH and its suppliers, if any. The intellectual # # and technical concepts contained herein are proprietary to gempa GmbH # # and its suppliers. # # Dissemination of this information or reproduction of this material # # is strictly forbidden unless prior written permission is obtained # # from gempa GmbH. # # # # Author: Enrico Ellguth, Dirk Roessler # # Email: enrico.ellguth@gempa.de, roessler@gempa.de # ############################################################################ import os import sys import time import seiscomp.client import seiscomp.core import seiscomp.io import seiscomp.datamodel def timing_pickTime(obj): """ Sort picks, origins by their time values Sort amplitudes by their reference time """ po = seiscomp.datamodel.Pick.Cast(obj[0]) oo = seiscomp.datamodel.Origin.Cast(obj[0]) if po or oo: t = obj[0].time().value() else: t = obj[0].timeWindow().reference() return t def timing_creationTime(obj): """ Sort all objects by their creation time """ ct = obj[0].creationInfo().creationTime() return ct def listPicks(self, objects): print( "\n#phase, time , streamID , author, to previous pick " "[s], delay [s]", file=sys.stdout, ) t0 = None for obj, _ in objects: p = seiscomp.datamodel.Pick.Cast(obj) if not p: continue time = p.time().value() try: phase = p.phaseHint().code() except ValueError: phase = "None" wfID = p.waveformID() net = wfID.networkCode() sta = wfID.stationCode() loc = wfID.locationCode() cha = wfID.channelCode() try: author = p.creationInfo().author() except ValueError: author = "None" try: delay = f"{(p.creationInfo().creationTime() - time).toDouble():.3f}" except ValueError: delay = "None" if t0 is not None: deltaT = f"{(time - t0).toDouble():.3f}" else: deltaT = "None" streamID = f"{net}.{sta}.{loc}.{cha}" print( f"{phase: <6}, {time.toString('%FT%T')}.{int(time.microseconds()/1000):03d}" f", {streamID: <15}, {author}, {deltaT}, {delay}", file=sys.stdout, ) t0 = time return True def printStatistics(ep): minPickTime = None maxPickTime = None minPickCTime = None maxPickCTime = None minAmplitudeTime = None maxAmplitudeTime = None minOriginTime = None maxOriginTime = None minOriginCTime = None maxOriginCTime = None # read picks nslc = set() authorPick = set() authorAmplitude = set() authorOrigin = set() detectionStreams = set() cntPick = ep.pickCount() for i in range(cntPick): pick = ep.pick(i) try: authorPick.add(pick.creationInfo().author()) except ValueError: print( f"Author information not found in pick {pick.publicID()}: NSLC list may" " be incomplete", file=sys.stderr, ) try: net = pick.waveformID().networkCode() sta = pick.waveformID().stationCode() loc = pick.waveformID().locationCode() cha = pick.waveformID().channelCode() nslc.add(f"{net}.{sta}.{loc}.{cha}") detectionStreams.add(f".{loc}.{cha}") except ValueError: print( f"Stream information not found in pick {pick.publicID()}: NSLC list " "may be incomplete", file=sys.stderr, ) if not minPickTime: minPickTime = pick.time().value() elif pick.time().value() < minPickTime: minPickTime = pick.time().value() if not maxPickTime: maxPickTime = pick.time().value() elif pick.time().value() > maxPickTime: maxPickTime = pick.time().value() try: pick.creationInfo().creationTime() except ValueError: print( f"Creation time not found in pick {pick.publicID()}: Statistics may " "be incomplete", file=sys.stderr, ) continue if not minPickCTime: minPickCTime = pick.creationInfo().creationTime() elif pick.creationInfo().creationTime() < minPickCTime: minPickCTime = pick.creationInfo().creationTime() if not maxPickCTime: maxPickCTime = pick.creationInfo().creationTime() elif pick.creationInfo().creationTime() > maxPickCTime: maxPickCTime = pick.creationInfo().creationTime() # read amplitudes cntAmp = ep.amplitudeCount() for i in range(cntAmp): amp = ep.amplitude(i) try: authorAmplitude.add(amp.creationInfo().author()) except ValueError: print( f"Author information not found in amplitude {amp.publicID()}: NSLC " "list may be incomplete", file=sys.stderr, ) try: net = amp.waveformID().networkCode() sta = amp.waveformID().stationCode() loc = amp.waveformID().locationCode() cha = amp.waveformID().channelCode() nslc.add(f"{net}.{sta}.{loc}.{cha}") detectionStreams.add(f".{loc}.{cha}") except ValueError: print( f"Stream information not found in amplitude {amp.publicID()}: NSLC " "list may be incomplete", file=sys.stderr, ) if not minAmplitudeTime: minAmplitudeTime = amp.timeWindow().reference() elif amp.timeWindow().reference() < minAmplitudeTime: minAmplitudeTime = amp.timeWindow().reference() if not maxAmplitudeTime: maxAmplitudeTime = amp.timeWindow().reference() elif amp.timeWindow().reference() > maxAmplitudeTime: maxAmplitudeTime = amp.timeWindow().reference() # read origins cntOrg = ep.originCount() for i in range(cntOrg): oo = ep.origin(i) try: authorOrigin.add(oo.creationInfo().author()) except ValueError: print( f"Author information not found in origin {oo.publicID()}:", file=sys.stderr, ) if not minOriginTime: minOriginTime = oo.time().value() elif oo.time().value() < minOriginTime: minOriginTime = oo.time().value() if not maxOriginTime: maxOriginTime = oo.time().value() elif oo.time().value() > maxOriginTime: maxOriginTime = oo.time().value() try: oo.creationInfo().creationTime() except ValueError: print( f"Creation time not found in oo {oo.publicID()}: Statistics may " "be incomplete", file=sys.stderr, ) continue if not minOriginCTime: minOriginCTime = oo.creationInfo().creationTime() elif oo.creationInfo().creationTime() < minOriginCTime: minOriginCTime = oo.creationInfo().creationTime() if not maxOriginCTime: maxOriginCTime = oo.creationInfo().creationTime() elif oo.creationInfo().creationTime() > maxOriginCTime: maxOriginCTime = oo.creationInfo().creationTime() print( f""" Picks + number: {cntPick} + first pick: {minPickTime} + last pick: {maxPickTime}""", file=sys.stdout, ) if cntPick > 0: print( f""" + interval: {(maxPickTime - minPickTime).toDouble():.3f} s""", file=sys.stdout, ) try: print( f""" + first created: {minPickCTime} + last created: {maxPickCTime} + interval: {(maxPickCTime - minPickCTime).toDouble():.3f} s""", file=sys.stdout, ) except TypeError: print( """ + first created: no creation information + last created: no creation information + interval: no creation information""", file=sys.stdout, ) pass print(f" + found {len(authorPick)} pick author(s):", file=sys.stdout) for i in authorPick: print(f" + {i}", file=sys.stdout) print( f""" Amplitudes + number: {cntAmp}""", file=sys.stdout, ) if cntAmp > 0: print( f""" + first amplitude: {minAmplitudeTime} + last amplitude: {maxAmplitudeTime} + interval: {(maxAmplitudeTime - minAmplitudeTime).toDouble():.3f} s""", file=sys.stdout, ) print(f" + found {len(authorAmplitude)} amplitude author(s):", file=sys.stdout) for i in authorAmplitude: print(f" + {i}", file=sys.stdout) print( f""" Origins + number: {cntOrg}""", file=sys.stdout, ) if cntOrg > 0: print( f""" + first origin: {minOriginTime} + last origin: {maxOriginTime} + interval: {(maxOriginTime - minOriginTime).toDouble():.3f} s""", file=sys.stdout, ) try: print( f""" + first created: {minOriginCTime} + last created: {maxOriginCTime} + interval: {(maxOriginCTime - minOriginCTime).toDouble():.3f} s""", file=sys.stdout, ) except TypeError: print( """ + first created: no creation information + last created: no creation information + interval: no creation information""", file=sys.stdout, ) pass print(f" + found {len(authorOrigin)} origin author(s):", file=sys.stdout) for i in authorOrigin: print(f" + {i}", file=sys.stdout) # stream information print(f"\nFound {len(detectionStreams)} SensorLocation.Channel:", file=sys.stdout) for i in detectionStreams: print(f" + {i}", file=sys.stdout) print(f"\nFound {len(nslc)} streams:", file=sys.stdout) for i in sorted(nslc): print(f" + {i}", file=sys.stdout) return True class PickPlayback(seiscomp.client.Application): def __init__(self, argc, argv): super().__init__(argc, argv) self.speed = 1.0 self.timing = "creationTime" self.jump = 0.0 self.print = False self.printList = False self.group = "PICK" self.ampGroup = "AMPLITUDE" self.orgGroup = "LOCATION" self.fileNames = None self.mode = "historic" self.authors = None self.objects = None self.setMessagingUsername("pbpick") self.setMessagingEnabled(True) self.setPrimaryMessagingGroup("PICK") self.setDatabaseEnabled(False, False) def createCommandLineDescription(self): self.commandline().addGroup("Playback") self.commandline().addStringOption( "Playback", "authors", "Author of objects to filter before playing back. Objects from all other " "authors are ignored. Separate multiple authors by comma.", ) self.commandline().addDoubleOption( "Playback", "jump,j", "Minutes to skip objects in the beginning." ) self.commandline().addOption( "Playback", "list", "Just list important pick information from the read XML file and then " "exit without playing back. The sorting of the list depends on '--timing'." "Information include: phase hint, pick time, stream ID, author, time to " "previous pick, delay.", ) self.commandline().addStringOption( "Playback", "mode", "Playback mode: 'historic' or 'realTime'. " "'realTime' mimics current situation. Default: 'historic'.", ) self.commandline().addStringOption( "Playback", "object,o", "Limit the playback to the given list of objects. Supported values are: \n" "pick, amplitude, origin.", ) self.commandline().addOption( "Playback", "print", "Just print some statistics of the read XML file and then " "exit without playing back. The list of stream codes (NSLC) is printed to " "stdout. All other information is printed to stderr. The information can " "be used for filtering waveforms (scart) or inventory (invextr), for " "creating global bindings or applying author filtering, e.g., in " "dump_picks.", ) self.commandline().addDoubleOption( "Playback", "speed", "Speed of playback.\n1: true speed." ) self.commandline().addStringOption( "Playback", "timing", "Timing reference: pickTime or creationTime. Default: creationTime. " "'pickTime' plays back in order of actual times of objects, " "'creationTime' considers their creation times instead. Use 'pickTime' if " "creation times are not representative of the order of objects, e.g., when " "created in playbacks. 'creationTime' should be considered for playing " "back origins since their actual origin time values are always before " "picks and amplitudes.", ) def printUsage(self): print( f"""Usage: {os.path.basename(__file__)} [options] [XML file][:PICK:AMPLITUDE:LOCATION] Play back pick, amplitude and origin objects from one or more XML files in SCML format sending them to the SeisComP messaging in timely order. Default message groups: * PICK for picks, * AMPLITUDE for amplitudes. * LOCATION for origins,""" ) super().printUsage() print( f"""Examples: Play back picks and other objects in file 'pick.xml' at true speed jumping the first 2 minutes {os.path.basename(__file__)} -j 2 picks.xml Play back picks and other objects from 2 XML files sending the picks, amplitudes and origins ordered by creation time to different message groups but amplitudes to the same default group (AMPLITUDE). {os.path.basename(__file__)} origins.xml l1origins.xml:L1PICK:AMPLITUDE:L1LOCATION Just print statistics and stream information {os.path.basename(__file__)} --print picks.xml """ ) def init(self): if not super().init(): return False return True def validateParameters(self): if not super().validateParameters(): return False try: self.authors = self.commandline().optionString("authors").split(",") except RuntimeError: pass try: self.mode = self.commandline().optionString("mode") except RuntimeError: pass try: self.objects = self.commandline().optionString("object") except RuntimeError: pass if self.mode not in ("historic", "realTime"): print(f"Unknown mode: {self.mode}", file=sys.stderr) return False try: self.print = self.commandline().hasOption("print") except RuntimeError: pass try: self.printList = self.commandline().hasOption("list") except RuntimeError: pass try: self.speed = self.commandline().optionDouble("speed") except RuntimeError: pass try: self.timing = self.commandline().optionString("timing") except RuntimeError: pass try: self.jump = self.commandline().optionDouble("jump") except RuntimeError: pass if self.timing not in ("pickTime", "creationTime"): print(f"Unknown timing: {self.timing}", file=sys.stderr) return False try: self.group = self.commandline().optionString("primary-group") except RuntimeError: pass files = self.commandline().unrecognizedOptions() if not files: print("At least one XML file must be given!", file=sys.stderr) return False print(files, file=sys.stderr) self.fileNames = list(files) if self.print or self.printList: self.setMessagingEnabled(False) return True def run(self): seiscomp.datamodel.PublicObject.SetRegistrationEnabled(False) objects = [] eps = [] minTime = None maxTime = None print("Input:", file=sys.stdout) for fileName in self.fileNames: group = self.group ampGroup = self.ampGroup orgGroup = self.orgGroup toks = fileName.split(":") if len(toks) == 2: fileName = toks[0] group = toks[1] elif len(toks) == 3: fileName = toks[0] group = toks[1] ampGroup = toks[2] elif len(toks) == 4: fileName = toks[0] group = toks[1] ampGroup = toks[2] orgGroup = toks[3] print( f" + file: {fileName}", file=sys.stdout, ) ar = seiscomp.io.XMLArchive() if not ar.open(fileName): print(f"Could not open {fileName}", file=sys.stderr) return False obj = ar.readObject() ar.close() if obj is None: print("Empty document", file=sys.stderr) return False ep = seiscomp.datamodel.EventParameters.Cast(obj) if self.print: printStatistics(ep) if not self.printList: return True eps.append(ep) if ep is None: print( f"Expected event parameters, got {obj.className()}", file=sys.stderr ) return False # read picks cntPick = ep.pickCount() if cntPick == 0: print(f"No picks found in file {fileName}", file=sys.stderr) if self.objects is not None and "pick" not in self.objects: print( f"Skipping picks. Supported objects: {self.objects}", file=sys.stderr, ) cntPick = 0 for i in range(cntPick): pick = ep.pick(i) if self.authors is not None: try: if ( pick.creationInfo().author() not in self.authors and not self.printList ): print( f"Skipping pick {pick.publicID()}: " f"{pick.creationInfo().author()} not in author list", file=sys.stderr, ) continue except ValueError: if not self.printList: print( f"Skipping pick {pick.publicID()}: " f"author is not available", file=sys.stderr, ) continue if self.timing == "creationTime": try: pick.creationInfo().creationTime() except Exception: if not self.printList: print( f"Skipping pick {pick.publicID()}: no creation time", file=sys.stderr, ) continue # filter by time if minTime and pick.time().value() < minTime: continue if maxTime and pick.time().value() >= maxTime: continue objects.append((pick, group)) # read amplitudes and add to objects cntAmp = ep.amplitudeCount() if cntAmp == 0: print("No Amplitudes found", file=sys.stderr) if self.objects is not None and "amplitude" not in self.objects: print( f"Skipping amplitudes. Supported objects: {self.objects}", file=sys.stderr, ) cntAmp = 0 for i in range(cntAmp): amp = ep.amplitude(i) if self.authors is not None: try: if ( amp.creationInfo().author() not in self.authors and not self.printList ): print( f"Skipping amplitude {amp.publicID()}: " f"{amp.creationInfo().author()} not in author list", file=sys.stderr, ) continue except ValueError: if not self.printList: print( f"Skipping amplitude {amp.publicID()}: " f"author is not available", file=sys.stderr, ) continue if self.timing == "creationTime": try: amp.creationInfo().creationTime() except Exception: print( f"Skipping amplitude {amp.publicID()}: no creation time", file=sys.stderr, ) continue objects.append((amp, ampGroup)) # read origins and add to objects cntOrgs = ep.originCount() if cntOrgs == 0: print("No Origins found", file=sys.stderr) if self.objects is not None and "origin" not in self.objects: print( f"Skipping origins. Supported objects: {self.objects}", file=sys.stderr, ) cntOrgs = 0 for i in range(cntOrgs): oo = ep.origin(i) if self.authors is not None: try: if ( oo.creationInfo().author() not in self.authors and not self.printList ): print( f"Skipping origin {oo.publicID()}: " f"{oo.creationInfo().author()} not in author list", file=sys.stderr, ) continue except ValueError: if not self.printList: print( f"Skipping origin {oo.publicID()}: " f"author is not available", file=sys.stderr, ) continue if self.timing == "creationTime": try: oo.creationInfo().creationTime() except Exception: try: string = oo.publicID().split("/")[1].split(".")[:2] timeString = string[0] + "." + string[1] timeFormat = "%Y%m%d%H%M%S.%f" t = seiscomp.core.Time() t.fromString(str(timeString), timeFormat) ci = seiscomp.datamodel.CreationInfo() ci.setCreationTime(t) oo.setCreationInfo(ci) print( f"creation time not found in origin {oo.publicID()}: " f"assuming {oo.creationInfo().creationTime()} from " "originID ", file=sys.stderr, ) except Exception: if not self.printList: print( f"Skipping origin {oo.publicID()}: no creation time", file=sys.stderr, ) continue objects.append((oo, orgGroup)) print( f" + considering {cntPick} picks, {cntAmp} amplitudes, {cntOrgs} origins", file=sys.stdout, ) if self.print or self.printList: print(" + do not send objects to messaging") else: print( f""" + sending objects to groups + picks: {group} + amplitudes: {ampGroup} + origins: {orgGroup}""", file=sys.stdout, ) if self.timing == "pickTime": try: objects.sort(key=timing_pickTime) except ValueError: print("Time value not set in at least 1 object", file=sys.stderr) if not self.printList: return False elif self.timing == "creationTime": try: objects.sort(key=timing_creationTime) except ValueError: print("Creation time not set in at least 1 object", file=sys.stderr) if not self.printList: return False else: print(f"Unknown timing: {self.timing}", file=sys.stderr) return False print("Setup:", file=sys.stdout) print(f" + author filter: {self.authors}", file=sys.stdout) print(f" + timing/sorting: {self.timing}", file=sys.stdout) if self.printList: listPicks(self, objects) return True seiscomp.datamodel.Notifier.Enable() firstTime = None lastTime = None refTime = None addSeconds = 0.0 sys.stdout.flush() for obj, group in objects: po = seiscomp.datamodel.Pick.Cast(obj) ao = seiscomp.datamodel.Amplitude.Cast(obj) oo = seiscomp.datamodel.Origin.Cast(obj) if self.isExitRequested(): break if self.timing == "pickTime": if ao: refTime = obj.timeWindow().reference() elif po: refTime = obj.time().value() elif oo: refTime = obj.time().value() else: print( "Object neither pick nor amplitude or origin- ignoring", file=sys.stderr, ) return False else: refTime = obj.creationInfo().creationTime() if not firstTime: firstTime = refTime print(f" + first time: {firstTime}", file=sys.stderr) print(f" + playback mode: {self.mode}", file=sys.stderr) print(f" + speed factor: {self.speed}", file=sys.stderr) if self.mode == "realTime": now = seiscomp.core.Time.GMT() addSeconds = (now - firstTime).toDouble() print( f" + adding {addSeconds: .3f} s to: pick time, amplitude " "reference time, origin time, creation time", file=sys.stderr, ) print("Playback progress:", file=sys.stderr) objectType = "pick" if ao: objectType = "amplitude" if oo: objectType = "origin" print( f" + {obj.publicID()} {objectType}: {group} - reference time: {refTime}", end="", file=sys.stderr, ) # add addSeconds to all times in real-time mode if self.mode == "realTime": objectInfo = obj.creationInfo() creationTime = objectInfo.creationTime() + seiscomp.core.TimeSpan( addSeconds ) obj.creationInfo().setCreationTime(creationTime) if ao: objectInfo = obj.timeWindow() amplitudeTime = objectInfo.reference() + seiscomp.core.TimeSpan( addSeconds ) obj.timeWindow().setReference(amplitudeTime) print( "\n + real-time mode - using modified reference time: " f"{obj.timeWindow().reference()}, creation time: {creationTime}", end="", file=sys.stderr, ) elif po or oo: objectTime = obj.time() objectTime.setValue( objectTime.value() + seiscomp.core.TimeSpan(addSeconds) ) obj.setTime(objectTime) print( f"\n + real-time mode - using modified {objectType} time: " f"{obj.time().value()}, creation time: {creationTime}", end="", file=sys.stderr, ) else: print( "\n + object not pick, amplitude or origin - ignoring", file=sys.stderr, ) return False delay = 0 if lastTime: delay = (refTime - lastTime).toDouble() / self.speed if (refTime - firstTime).toDouble() / 60.0 >= self.jump: delay = max(delay, 0) print(f" - time to sending: {delay:.4f} s", file=sys.stderr) time.sleep(delay) lastTime = refTime nc = seiscomp.datamodel.NotifierCreator(seiscomp.datamodel.OP_ADD) obj.accept(nc) msg = seiscomp.datamodel.Notifier.GetMessage() self.connection().send(group, msg) else: print(" - skipping", file=sys.stderr) sys.stdout.flush() print("") return True def main(argv): app = PickPlayback(len(argv), argv) return app() if __name__ == "__main__": sys.exit(main(sys.argv))