577 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Plaintext
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			577 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Plaintext
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env seiscomp-python
 | 
						|
# -*- coding: utf-8 -*-
 | 
						|
############################################################################
 | 
						|
# Copyright (C) GFZ Potsdam                                                #
 | 
						|
# All rights reserved.                                                     #
 | 
						|
#                                                                          #
 | 
						|
# GNU Affero General Public License Usage                                  #
 | 
						|
# This file may be used under the terms of the GNU Affero                  #
 | 
						|
# Public License version 3.0 as published by the Free Software Foundation  #
 | 
						|
# and appearing in the file LICENSE included in the packaging of this      #
 | 
						|
# file. Please review the following information to ensure the GNU Affero   #
 | 
						|
# Public License version 3.0 requirements will be met:                     #
 | 
						|
# https://www.gnu.org/licenses/agpl-3.0.html.                              #
 | 
						|
############################################################################
 | 
						|
 | 
						|
import sys
 | 
						|
import re
 | 
						|
 | 
						|
from seiscomp import client, core, datamodel, io
 | 
						|
 | 
						|
 | 
						|
def readStreamList(listFile):
 | 
						|
    """
 | 
						|
    Read list of streams from file
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    file : file
 | 
						|
        Input list file, one line per stream
 | 
						|
        format: NET.STA.LOC.CHA
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    list
 | 
						|
        streams.
 | 
						|
 | 
						|
    """
 | 
						|
    streams = []
 | 
						|
 | 
						|
    try:
 | 
						|
        if listFile == "-":
 | 
						|
            f = sys.stdin
 | 
						|
            listFile = "stdin"
 | 
						|
        else:
 | 
						|
            f = open(listFile, "r", encoding="utf8")
 | 
						|
    except Exception:
 | 
						|
        print(f"error: unable to open '{listFile}'", file=sys.stderr)
 | 
						|
        return []
 | 
						|
 | 
						|
    lineNumber = -1
 | 
						|
    for line in f:
 | 
						|
        lineNumber = lineNumber + 1
 | 
						|
        line = line.strip()
 | 
						|
        # ignore comments
 | 
						|
        if len(line) > 0 and line[0] == "#":
 | 
						|
            continue
 | 
						|
 | 
						|
        if len(line) == 0:
 | 
						|
            continue
 | 
						|
 | 
						|
        if len(line.split(".")) != 4:
 | 
						|
            f.close()
 | 
						|
            print(
 | 
						|
                f"error: {listFile} in line {lineNumber} has invalid line format, "
 | 
						|
                "expecting NET.STA.LOC.CHA - 1 line per stream",
 | 
						|
                file=sys.stderr,
 | 
						|
            )
 | 
						|
            return []
 | 
						|
        streams.append(line)
 | 
						|
 | 
						|
    f.close()
 | 
						|
 | 
						|
    if len(streams) == 0:
 | 
						|
        return []
 | 
						|
 | 
						|
    return streams
 | 
						|
 | 
						|
 | 
						|
class EventStreams(client.Application):
 | 
						|
    def __init__(self, argc, argv):
 | 
						|
        client.Application.__init__(self, argc, argv)
 | 
						|
 | 
						|
        self.setMessagingEnabled(False)
 | 
						|
        self.setDatabaseEnabled(True, False)
 | 
						|
        self.setDaemonEnabled(False)
 | 
						|
 | 
						|
        self.eventID = None
 | 
						|
        self.inputFile = None
 | 
						|
        self.inputFormat = "xml"
 | 
						|
        self.margin = [300]
 | 
						|
 | 
						|
        self.allNetworks = True
 | 
						|
        self.allStations = True
 | 
						|
        self.allLocations = True
 | 
						|
        self.allStreams = True
 | 
						|
        self.allComponents = True
 | 
						|
 | 
						|
        # filter
 | 
						|
        self.network = None
 | 
						|
        self.station = None
 | 
						|
 | 
						|
        self.streams = []
 | 
						|
        self.streamFilter = None
 | 
						|
 | 
						|
        # output format
 | 
						|
        self.caps = False
 | 
						|
        self.fdsnws = False
 | 
						|
 | 
						|
    def createCommandLineDescription(self):
 | 
						|
        self.commandline().addGroup("Input")
 | 
						|
        self.commandline().addStringOption(
 | 
						|
            "Input",
 | 
						|
            "input,i",
 | 
						|
            "Input XML file name. Reads event from the XML file instead of database. "
 | 
						|
            "Use '-' to read from stdin.",
 | 
						|
        )
 | 
						|
        self.commandline().addStringOption(
 | 
						|
            "Input",
 | 
						|
            "format,f",
 | 
						|
            "Input format to use (xml [default], zxml (zipped xml), binary). "
 | 
						|
            "Only relevant with --input.",
 | 
						|
        )
 | 
						|
 | 
						|
        self.commandline().addGroup("Dump")
 | 
						|
        self.commandline().addStringOption(
 | 
						|
            "Dump", "event,E", "The ID of the event to consider."
 | 
						|
        )
 | 
						|
        self.commandline().addStringOption(
 | 
						|
            "Dump",
 | 
						|
            "net-sta",
 | 
						|
            "Filter read picks by network code or network and station code. Format: "
 | 
						|
            "NET or NET.STA.",
 | 
						|
        )
 | 
						|
        self.commandline().addStringOption(
 | 
						|
            "Dump",
 | 
						|
            "nslc",
 | 
						|
            "Stream list file to be used for filtering read picks by stream code. "
 | 
						|
            "'--net-sta' will be ignored. One line per stream, line format: "
 | 
						|
            "NET.STA.LOC.CHA.",
 | 
						|
        )
 | 
						|
 | 
						|
        self.commandline().addGroup("Output")
 | 
						|
        self.commandline().addStringOption(
 | 
						|
            "Output",
 | 
						|
            "margin,m",
 | 
						|
            "Time margin around the picked time window, default is 300. Added "
 | 
						|
            "before the first and after the last pick, respectively. Use 2 "
 | 
						|
            "comma-separted values (before,after) for asymmetric margins, e.g. "
 | 
						|
            "-m 120,300.",
 | 
						|
        )
 | 
						|
        self.commandline().addStringOption(
 | 
						|
            "Output",
 | 
						|
            "streams,S",
 | 
						|
            "Comma-separated list of streams per station to add, e.g. BH,SH,HH.",
 | 
						|
        )
 | 
						|
        self.commandline().addOption(
 | 
						|
            "Output",
 | 
						|
            "all-streams",
 | 
						|
            "Dump all streams. If unused, just streams with picks are dumped.",
 | 
						|
        )
 | 
						|
        self.commandline().addIntOption(
 | 
						|
            "Output",
 | 
						|
            "all-components,C",
 | 
						|
            "All components or just the picked ones (0). Default is 1",
 | 
						|
        )
 | 
						|
        self.commandline().addIntOption(
 | 
						|
            "Output",
 | 
						|
            "all-locations,L",
 | 
						|
            "All locations or just the picked ones (0). Default is 1",
 | 
						|
        )
 | 
						|
        self.commandline().addOption(
 | 
						|
            "Output",
 | 
						|
            "all-stations",
 | 
						|
            "Dump all stations from the same network. If unused, just stations "
 | 
						|
            "with picks are dumped.",
 | 
						|
        )
 | 
						|
        self.commandline().addOption(
 | 
						|
            "Output",
 | 
						|
            "all-networks",
 | 
						|
            "Dump all networks. If unused, just networks with picks are dumped."
 | 
						|
            " This option implies --all-stations, --all-locations, --all-streams, "
 | 
						|
            "--all-components and will only provide the time window.",
 | 
						|
        )
 | 
						|
        self.commandline().addOption(
 | 
						|
            "Output",
 | 
						|
            "resolve-wildcards,R",
 | 
						|
            "If all components are used, use inventory to resolve stream "
 | 
						|
            "components instead of using '?' (important when Arclink should be "
 | 
						|
            "used).",
 | 
						|
        )
 | 
						|
        self.commandline().addOption(
 | 
						|
            "Output",
 | 
						|
            "caps",
 | 
						|
            "Output in capstool format (Common Acquisition Protocol Server by "
 | 
						|
            "gempa GmbH).",
 | 
						|
        )
 | 
						|
        self.commandline().addOption(
 | 
						|
            "Output", "fdsnws", "Output in FDSN dataselect webservice POST format."
 | 
						|
        )
 | 
						|
        return True
 | 
						|
 | 
						|
    def validateParameters(self):
 | 
						|
        if not client.Application.validateParameters(self):
 | 
						|
            return False
 | 
						|
 | 
						|
        if self.commandline().hasOption("resolve-wildcards"):
 | 
						|
            self.setLoadStationsEnabled(True)
 | 
						|
 | 
						|
        try:
 | 
						|
            self.inputFile = self.commandline().optionString("input")
 | 
						|
            self.setDatabaseEnabled(False, False)
 | 
						|
        except BaseException:
 | 
						|
            pass
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    def init(self):
 | 
						|
        if not client.Application.init(self):
 | 
						|
            return False
 | 
						|
 | 
						|
        try:
 | 
						|
            self.inputFormat = self.commandline().optionString("format")
 | 
						|
        except BaseException:
 | 
						|
            pass
 | 
						|
 | 
						|
        try:
 | 
						|
            self.eventID = self.commandline().optionString("event")
 | 
						|
        except BaseException as exc:
 | 
						|
            if not self.inputFile:
 | 
						|
                raise ValueError(
 | 
						|
                    "An eventID is mandatory if no input file is specified"
 | 
						|
                ) from exc
 | 
						|
 | 
						|
        try:
 | 
						|
            self.margin = self.commandline().optionString("margin").split(",")
 | 
						|
        except BaseException:
 | 
						|
            pass
 | 
						|
 | 
						|
        try:
 | 
						|
            self.streams = self.commandline().optionString("streams").split(",")
 | 
						|
        except BaseException:
 | 
						|
            pass
 | 
						|
 | 
						|
        try:
 | 
						|
            self.allComponents = self.commandline().optionInt("all-components") != 0
 | 
						|
        except BaseException:
 | 
						|
            pass
 | 
						|
 | 
						|
        try:
 | 
						|
            self.allLocations = self.commandline().optionInt("all-locations") != 0
 | 
						|
        except BaseException:
 | 
						|
            pass
 | 
						|
 | 
						|
        self.allStreams = self.commandline().hasOption("all-streams")
 | 
						|
        self.allStations = self.commandline().hasOption("all-stations")
 | 
						|
        self.allNetworks = self.commandline().hasOption("all-networks")
 | 
						|
 | 
						|
        try:
 | 
						|
            networkStation = self.commandline().optionString("net-sta")
 | 
						|
        except RuntimeError:
 | 
						|
            networkStation = None
 | 
						|
 | 
						|
        try:
 | 
						|
            nslcFile = self.commandline().optionString("nslc")
 | 
						|
        except RuntimeError:
 | 
						|
            nslcFile = None
 | 
						|
 | 
						|
        if nslcFile:
 | 
						|
            networkStation = None
 | 
						|
            self.streamFilter = readStreamList(nslcFile)
 | 
						|
 | 
						|
        if networkStation:
 | 
						|
            try:
 | 
						|
                self.network = networkStation.split(".")[0]
 | 
						|
            except IndexError:
 | 
						|
                print(
 | 
						|
                    f"Error in network code '{networkStation}': Use '--net-sta' with "
 | 
						|
                    "format NET or NET.STA",
 | 
						|
                    file=sys.stderr,
 | 
						|
                )
 | 
						|
                return False
 | 
						|
 | 
						|
            try:
 | 
						|
                self.station = networkStation.split(".")[1]
 | 
						|
            except IndexError:
 | 
						|
                pass
 | 
						|
 | 
						|
        self.caps = self.commandline().hasOption("caps")
 | 
						|
        self.fdsnws = self.commandline().hasOption("fdsnws")
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    def printUsage(self):
 | 
						|
        print(
 | 
						|
            """Usage:
 | 
						|
  scevtstreams [options]
 | 
						|
 | 
						|
Extract stream information and time windows from an event"""
 | 
						|
        )
 | 
						|
 | 
						|
        client.Application.printUsage(self)
 | 
						|
 | 
						|
        print(
 | 
						|
            """Examples:
 | 
						|
Get the time windows for an event in the database:
 | 
						|
  scevtstreams -E gfz2012abcd -d mysql://sysop:sysop@localhost/seiscomp
 | 
						|
 | 
						|
Create lists compatible with fdsnws:
 | 
						|
  scevtstreams -E gfz2012abcd -i event.xml -m 120,500 --fdsnws
 | 
						|
"""
 | 
						|
        )
 | 
						|
 | 
						|
    def run(self):
 | 
						|
        resolveWildcards = self.commandline().hasOption("resolve-wildcards")
 | 
						|
 | 
						|
        picks = []
 | 
						|
 | 
						|
        # read picks from input file
 | 
						|
        if self.inputFile:
 | 
						|
            picks = self.readXML()
 | 
						|
            if not picks:
 | 
						|
                raise ValueError("Could not find picks in input file")
 | 
						|
 | 
						|
        # read picks from database
 | 
						|
        else:
 | 
						|
            for obj in self.query().getEventPicks(self.eventID):
 | 
						|
                pick = datamodel.Pick.Cast(obj)
 | 
						|
                if pick is None:
 | 
						|
                    continue
 | 
						|
                picks.append(pick)
 | 
						|
 | 
						|
            if not picks:
 | 
						|
                raise ValueError(
 | 
						|
                    f"Could not find picks for event {self.eventID} in database"
 | 
						|
                )
 | 
						|
 | 
						|
        # filter picks
 | 
						|
        if self.streamFilter:
 | 
						|
            # # filter channel by --nslc option
 | 
						|
            channels = self.streamFilter
 | 
						|
            channelsRe = []
 | 
						|
            for channel in channels:
 | 
						|
                channel = re.sub(r"\.", r"\.", channel)  # . becomes \.
 | 
						|
                channel = re.sub(r"\?", ".", channel)  # ? becomes .
 | 
						|
                channel = re.sub(r"\*", ".*", channel)  # * becomes.*
 | 
						|
                channel = re.compile(channel)
 | 
						|
                channelsRe.append(channel)
 | 
						|
 | 
						|
        if self.streamFilter or self.network:
 | 
						|
            pickFiltered = []
 | 
						|
            for pick in picks:
 | 
						|
                net = pick.waveformID().networkCode()
 | 
						|
                sta = pick.waveformID().stationCode()
 | 
						|
                loc = pick.waveformID().locationCode()
 | 
						|
                cha = pick.waveformID().channelCode()
 | 
						|
 | 
						|
                filtered = False
 | 
						|
                if self.streamFilter:
 | 
						|
                    stream = f"{net}.{sta}.{loc}.{cha}"
 | 
						|
                    for chaRe in channelsRe:
 | 
						|
                        if chaRe.match(stream):
 | 
						|
                            filtered = True
 | 
						|
                            continue
 | 
						|
 | 
						|
                elif self.network:
 | 
						|
                    if net != self.network:
 | 
						|
                        continue
 | 
						|
                    if self.station and sta != self.station:
 | 
						|
                        continue
 | 
						|
                    filtered = True
 | 
						|
 | 
						|
                if filtered:
 | 
						|
                    pickFiltered.append(pick)
 | 
						|
                else:
 | 
						|
                    print(
 | 
						|
                        f"Ignoring channel {stream}: not considered by configuration",
 | 
						|
                        file=sys.stderr,
 | 
						|
                    )
 | 
						|
 | 
						|
            picks = pickFiltered
 | 
						|
 | 
						|
        if not picks:
 | 
						|
            raise ValueError("Info: All picks are filtered out")
 | 
						|
 | 
						|
        # calculate minimum and maximum pick time
 | 
						|
        minTime = None
 | 
						|
        maxTime = None
 | 
						|
        for pick in picks:
 | 
						|
            if minTime is None or minTime > pick.time().value():
 | 
						|
                minTime = pick.time().value()
 | 
						|
 | 
						|
            if maxTime is None or maxTime < pick.time().value():
 | 
						|
                maxTime = pick.time().value()
 | 
						|
 | 
						|
        # add time margin(s), no need for None check since pick time is
 | 
						|
        # mandatory and at least on pick exists
 | 
						|
        minTime = minTime - core.TimeSpan(float(self.margin[0]))
 | 
						|
        maxTime = maxTime + core.TimeSpan(float(self.margin[-1]))
 | 
						|
 | 
						|
        # convert times to string dependend on requested output format
 | 
						|
        if self.caps:
 | 
						|
            timeFMT = "%Y,%m,%d,%H,%M,%S"
 | 
						|
        elif self.fdsnws:
 | 
						|
            timeFMT = "%FT%T"
 | 
						|
        else:
 | 
						|
            timeFMT = "%F %T"
 | 
						|
        minTime = minTime.toString(timeFMT)
 | 
						|
        maxTime = maxTime.toString(timeFMT)
 | 
						|
 | 
						|
        inv = client.Inventory.Instance().inventory()
 | 
						|
 | 
						|
        lines = set()
 | 
						|
        for pick in picks:
 | 
						|
            net = pick.waveformID().networkCode()
 | 
						|
            station = pick.waveformID().stationCode()
 | 
						|
            loc = pick.waveformID().locationCode()
 | 
						|
            streams = [pick.waveformID().channelCode()]
 | 
						|
            rawStream = streams[0][:2]
 | 
						|
 | 
						|
            if self.allComponents:
 | 
						|
                if resolveWildcards:
 | 
						|
                    iloc = datamodel.getSensorLocation(inv, pick)
 | 
						|
                    if iloc:
 | 
						|
                        tc = datamodel.ThreeComponents()
 | 
						|
                        datamodel.getThreeComponents(
 | 
						|
                            tc, iloc, rawStream, pick.time().value()
 | 
						|
                        )
 | 
						|
                        streams = []
 | 
						|
                        if tc.vertical():
 | 
						|
                            streams.append(tc.vertical().code())
 | 
						|
                        if tc.firstHorizontal():
 | 
						|
                            streams.append(tc.firstHorizontal().code())
 | 
						|
                        if tc.secondHorizontal():
 | 
						|
                            streams.append(tc.secondHorizontal().code())
 | 
						|
                else:
 | 
						|
                    streams = [rawStream + "?"]
 | 
						|
 | 
						|
            if self.allLocations:
 | 
						|
                loc = "*"
 | 
						|
 | 
						|
            if self.allStations:
 | 
						|
                station = "*"
 | 
						|
 | 
						|
            if self.allNetworks:
 | 
						|
                net = "*"
 | 
						|
                station = "*"
 | 
						|
                loc = "*"
 | 
						|
 | 
						|
            # FDSNWS requires empty location to be encoded by 2 dashes
 | 
						|
            if not loc and self.fdsnws:
 | 
						|
                loc = "--"
 | 
						|
 | 
						|
            # line format
 | 
						|
            if self.caps:
 | 
						|
                lineFMT = "{0} {1} {2} {3} {4} {5}"
 | 
						|
            elif self.fdsnws:
 | 
						|
                lineFMT = "{2} {3} {4} {5} {0} {1}"
 | 
						|
            else:
 | 
						|
                lineFMT = "{0};{1};{2}.{3}.{4}.{5}"
 | 
						|
 | 
						|
            for s in streams:
 | 
						|
                if self.allStreams or self.allNetworks:
 | 
						|
                    s = "*"
 | 
						|
 | 
						|
                lines.add(lineFMT.format(minTime, maxTime, net, station, loc, s))
 | 
						|
 | 
						|
            for s in self.streams:
 | 
						|
                if s == rawStream:
 | 
						|
                    continue
 | 
						|
 | 
						|
                if self.allStreams or self.allNetworks:
 | 
						|
                    s = "*"
 | 
						|
 | 
						|
                lines.add(
 | 
						|
                    lineFMT.format(
 | 
						|
                        minTime, maxTime, net, station, loc, s + streams[0][2]
 | 
						|
                    )
 | 
						|
                )
 | 
						|
 | 
						|
        for line in sorted(lines):
 | 
						|
            print(line, file=sys.stdout)
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    def readXML(self):
 | 
						|
        if self.inputFormat == "xml":
 | 
						|
            ar = io.XMLArchive()
 | 
						|
        elif self.inputFormat == "zxml":
 | 
						|
            ar = io.XMLArchive()
 | 
						|
            ar.setCompression(True)
 | 
						|
        elif self.inputFormat == "binary":
 | 
						|
            ar = io.VBinaryArchive()
 | 
						|
        else:
 | 
						|
            raise TypeError(f"unknown input format '{self.inputFormat}'")
 | 
						|
 | 
						|
        if not ar.open(self.inputFile):
 | 
						|
            raise IOError("unable to open input file")
 | 
						|
 | 
						|
        obj = ar.readObject()
 | 
						|
        if obj is None:
 | 
						|
            raise TypeError("invalid input file format")
 | 
						|
 | 
						|
        ep = datamodel.EventParameters.Cast(obj)
 | 
						|
        if ep is None:
 | 
						|
            raise ValueError("no event parameters found in input file")
 | 
						|
 | 
						|
        # we require at least one origin which references to picks via arrivals
 | 
						|
        if ep.originCount() == 0:
 | 
						|
            raise ValueError("no origin found in input file")
 | 
						|
 | 
						|
        originIDs = []
 | 
						|
 | 
						|
        # search for a specific event id
 | 
						|
        if self.eventID:
 | 
						|
            ev = datamodel.Event.Find(self.eventID)
 | 
						|
            if ev:
 | 
						|
                originIDs = [
 | 
						|
                    ev.originReference(i).originID()
 | 
						|
                    for i in range(ev.originReferenceCount())
 | 
						|
                ]
 | 
						|
            else:
 | 
						|
                raise ValueError(f"Event ID {self.eventID} not found in input file")
 | 
						|
 | 
						|
        # use first event/origin if no id was specified
 | 
						|
        else:
 | 
						|
            # no event, use first available origin
 | 
						|
            if ep.eventCount() == 0:
 | 
						|
                if ep.originCount() > 1:
 | 
						|
                    print(
 | 
						|
                        "WARNING: Input file contains no event but more than "
 | 
						|
                        "1 origin. Considering only first origin",
 | 
						|
                        file=sys.stderr,
 | 
						|
                    )
 | 
						|
                originIDs.append(ep.origin(0).publicID())
 | 
						|
 | 
						|
            # use origin references of first available event
 | 
						|
            else:
 | 
						|
                if ep.eventCount() > 1:
 | 
						|
                    print(
 | 
						|
                        "WARNING: Input file contains more than 1 event. "
 | 
						|
                        "Considering only first event",
 | 
						|
                        file=sys.stderr,
 | 
						|
                    )
 | 
						|
                ev = ep.event(0)
 | 
						|
                originIDs = [
 | 
						|
                    ev.originReference(i).originID()
 | 
						|
                    for i in range(ev.originReferenceCount())
 | 
						|
                ]
 | 
						|
 | 
						|
        # collect pickIDs
 | 
						|
        pickIDs = set()
 | 
						|
        for oID in originIDs:
 | 
						|
            o = datamodel.Origin.Find(oID)
 | 
						|
            if o is None:
 | 
						|
                continue
 | 
						|
 | 
						|
            for i in range(o.arrivalCount()):
 | 
						|
                pickIDs.add(o.arrival(i).pickID())
 | 
						|
 | 
						|
        # lookup picks
 | 
						|
        picks = []
 | 
						|
        for pickID in pickIDs:
 | 
						|
            pick = datamodel.Pick.Find(pickID)
 | 
						|
            if pick:
 | 
						|
                picks.append(pick)
 | 
						|
 | 
						|
        return picks
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    try:
 | 
						|
        app = EventStreams(len(sys.argv), sys.argv)
 | 
						|
        sys.exit(app())
 | 
						|
    except (ValueError, TypeError) as e:
 | 
						|
        print(f"ERROR: {e}", file=sys.stderr)
 | 
						|
    sys.exit(1)
 |