shared-snippets/python/seedconverter/seedconverter.py
2024-08-28 17:22:21 +02:00

328 lines
14 KiB
Python

import sys
import os
import warnings
import obspy
import argparse
import unit
import sacutil
import numpy as np
from obspy.io.xseed import Parser
from obspy.io.xseed.utils import SEEDParserException
from obspy.core.util import AttribDict
from obspy.core import Stream
def convert_seed(seed_file,
inv_file=None,
input_unit=None,
output_filetype=None,
remove_response=False,
zero_mean=False,
pre_filtering=None,
output_unit=None,
single_output_file=False,
output_dir="."):
"""
Function to convert provided file to SAC, ASCII or MSEED
:type seed_file: str
:param seed_file: File name or path to the seed (seed or msd) file.
:type inv_file: str
:param inv_file: File name or path to inventory file in any inventory format supported by ObsPy.
:type input_unit: str
:param input_unit: Unit of the input file: 'ACC', 'DISP' or 'VEL'. Specified only for miniSEED files (full SEED
files are always in 'COUNTS'). If the value is not set, no unit ('COUNTS') is assumed.
:type output_filetype: str
:param output_filetype: Output filetype. Choose either SAC, MSEED, SLIST or INTERNAL_ASCII.
When used SLIST, sample values are saved with `%+.10e` formatting.
INTERNAL_ASCII is an internal format used for some apps, that uses only one column of values
This will be default formatting since ObsPy 1.1.0
:type remove_response: bool
:param remove_response: Response removal from the waveform
Defaults to False.
Removal of the instrument's response from the waveform
By default it will save waveform without response removal procedure
This function uses obspy.core.stream.Stream.remove_response.
When set to True, user has to specify parameters zero_mean,
pre_filtering and output_unit. Otherwise the script will raise an
exception.
:type zero_mean: bool, optional
:param zero_mean: If true the mean of the data is subtracted
For details check the ObsPy's documentation for
obspy.core.stream.Stream.remove_response
:type pre_filtering: (float, float, float, float), optional
:param pre_filtering: Apply a bandpass filter to the data trace before
deconvolution.
The list or tuple defines the four corner frequencies (f1,f2,f3,f4)
of a cosine taper which is one between f2 and f3
and tapers to zero for f1 < f < f2 and f3 < f < f4.
For details check the ObsPy's documentation for
obspy.core.stream.Stream.remove_response
:type output_unit: str, optional
:param output_unit: Unit of the output waveform.
This parameter converts counts to real values during response removal,
or, if response_removal is false is just written to the converted file.
When used ``auto`` the script will determine the unit automatically
with use of second letter of a channel code which determines
instrument type.
For details see Appendix A to SEED Manual v2.4.
In case user need to convert all channels to common unit,
there can be chosen ``DISP`` for displacement,
``VEL`` for velocity and ``ACC`` for acceleration.
When this parameter is None and remove_response is set to True
script will throw an Exception.
:type single_output_file: bool
:param single_output_file: if True, a single file with all traces will be created
(available only for conversion to MSEED), otherwise, separate files with names in format
``network.station.location.channel.starttime_microsecond.output_filetype`` will be created
:type output_dir: str, optional
:param output_dir: Directory to which output files are written.
Defaults to current directory.
:return: Filenames of created files. Filenames follow pattern:
``network.station.location.channel.starttime_microsecond.SAC``
:raises
.. note::
For more information on parameters used in this function see
ObsPy's documentation in ``obspy.core.stream.Stream.remove_response``
"""
st = obspy.read(seed_file)
if inv_file:
inv = obspy.read_inventory(inv_file)
elif remove_response:
raise ValueError("Cannot remove response if inventory is not specified")
else:
inv = None
if output_unit:
if inv:
# assuming all the traces have the same unit
input_type = unit.determine_instrument_type_from_inventory(inv, st[0].id, st[0].stats.starttime)
else:
try:
# if we don't have the inventory and we have a full SEED file, we can attempt to read unit from it
resp = Parser(seed_file)
# assuming all the traces have the same unit
input_type = unit.determine_instrument_type_from_blockette(resp, st[0].id)
except (OSError, SEEDParserException):
warnings.warn("The provided file is not a SEED volume - cannot determine unit automatically")
if output_unit == "auto":
output_unit = input_type
if remove_response and output_unit is None:
raise ValueError("You have to provide output_unit parameter"
" for response removal procedure.\n"
"Available output_unit values are DISP, "
"VEL, ACC. Provide one or skip removing "
"response by passing "
"remove_response=False parameter")
if remove_response:
if pre_filtering is None:
tr_sampling_rate = st[0].stats.sampling_rate
pre_filt = [0.005, 0.006, 0.9 * 0.5 * tr_sampling_rate, 0.5 * tr_sampling_rate]
else:
pre_filt = pre_filtering
try:
_remove_response(st, inv, input_type, output_unit, pre_filt, zero_mean)
except (ValueError, SEEDParserException) as exc:
raise Exception("There was a problem with removing response from the file: " + str(exc))
elif output_unit and input_unit and input_unit != output_unit:
_convert_unit(st, input_unit, output_unit)
# we are not converting anything, but the input file already had a unit (might happen only with mseed files),
# so it should be written to the output file
elif input_unit:
output_unit = input_unit
if single_output_file:
if output_filetype != "MSEED":
raise ValueError("Writing all traces to one file is available only for conversion to MSEED format")
return _convert_to_single_mseed(seed_file, st, output_dir)
else:
return _convert_to_separate_files(st, inv, remove_response, output_unit, output_filetype, output_dir)
# remove response and correct numerical errors (depending on the input and output type)
def _remove_response(stream, inv, input_type, output_unit, pre_filt, zero_mean):
if input_type == "ACC" and output_unit == "DISP":
stream.remove_response(inventory=inv, pre_filt=pre_filt, zero_mean=zero_mean, output="VEL")
stream.filter("highpass", freq=1.0)
stream.integrate(method="cumtrapz")
stream.filter("highpass", freq=0.5)
else:
stream.remove_response(inventory=inv, pre_filt=pre_filt, zero_mean=zero_mean, output=output_unit)
if ((input_type == "ACC" and output_unit == "VEL") or
(input_type == "VEL" and (output_unit == "DISP" or output_unit == "VEL"))):
stream.filter("highpass", freq=1.0)
# unit conversion - applied when the input data already has response removed and is converted to one of the units
def _convert_unit(stream, input_unit, output_unit):
if input_unit == "DISP":
if output_unit == "ACC": # DIFF x2
stream.differentiate(method="gradient")
stream.differentiate(method="gradient")
elif output_unit == "VEL": # DIFF
stream.differentiate(method="gradient")
elif input_unit == "VEL":
if output_unit == "ACC": # DIFF
stream.differentiate(method="gradient")
elif output_unit == "DISP": # INTEG + FILTER
stream.integrate(method="cumtrapz")
stream.filter("highpass", freq=1.0)
elif input_unit == "ACC":
if output_unit == "VEL": # INTEG + FILTER
stream.integrate(method="cumtrapz")
stream.filter("highpass", freq=1.0)
elif output_unit == "DISP": # (INTEG + FILTER) x2
stream.integrate(method="cumtrapz")
stream.filter("highpass", freq=1.0)
stream.integrate(method="cumtrapz")
stream.filter("highpass", freq=0.5)
else:
raise TypeError("Cannot convert from ", input_unit)
def _convert_to_single_mseed(seed_filename, stream, output_dir):
result_filename = os.path.splitext(os.path.basename(seed_filename))[0] + ".msd"
stream.write(output_dir + "/" + result_filename, format="MSEED")
return [result_filename]
def _convert_to_separate_files(stream, inv, remove_response, output_unit, output_filetype, output_dir):
output_stream = Stream()
output_file_extension = output_filetype
for tr in stream:
if output_filetype == "SAC":
output_file_extension = "sac"
tr = sacutil.to_sac_trace(tr, output_unit, remove_response, inv)
elif output_filetype in ["SLIST", "TSPAIR", "SH_ASC"]:
output_file_extension = output_filetype.lower() + ".ascii"
slist_unit = unit.translate_instrument_type_to_unit(output_unit) if output_unit else "COUNTS"
tr.stats.ascii = AttribDict({"unit": slist_unit})
elif output_filetype == "MSEED":
output_file_extension = "msd"
elif output_filetype == "INTERNAL_ASCII":
output_file_extension = "ascii"
output_stream.append(tr)
return _write_separate_output_files(output_stream, output_filetype, output_file_extension, output_dir)
def _write_separate_output_files(stream, output_filetype, file_extension, output_dir):
"""
Writes all Trace objects present in Stream to separate files. Filetype
is set according to User's will.
:type stream: obspy.core.stream.Stream
:param stream: Obspy stream with traces to save separately.
:type output_filetype: str
:param output_filetype: Contains filetype to save. Currently supported
``SAC``, ``SLIST`` and ``MSEED``.
"""
result_filenames = list()
for tr in stream:
result_filename = ".".join([tr.id,
str(tr.stats.starttime.microsecond),
file_extension])
result_filepath = output_dir + "/" + result_filename
if output_filetype == "INTERNAL_ASCII":
_write_ascii_one_column(tr, result_filepath)
else:
tr.write(result_filepath, format=output_filetype)
result_filenames.append(result_filename)
return result_filenames
def _write_ascii_one_column(trace, filename):
"""
Writes trace data into an ASCII file in one-column format (i.e. no header, single column of values).
"""
with open(filename, 'wb') as fh:
data = trace.data.reshape((-1, 1))
dtype = data.dtype.name
fmt = '%f'
if dtype.startswith('int'):
fmt = '%d'
elif dtype.startswith('float64'):
fmt = '%+.16e'
np.savetxt(fh, data, delimiter=b'\t', fmt=fmt.encode('ascii', 'strict'))
def main(argv):
def str2bool(v):
if v.lower() in ("True", "TRUE", "yes", "true", "t", "y", "1"):
return True
elif v.lower() in ("False", "FALSE", "no", "false", "f", "n", "0"):
return False
else:
raise argparse.ArgumentTypeError("Boolean value expected.")
parser = argparse.ArgumentParser(description="Convert provided file"
" to SAC or SLIST (ASCII).")
parser.add_argument("seed_file", help="Provide SEED or mSEED file to convert")
parser.add_argument("--inv_file", help="Provide inventory file")
parser.add_argument("--input_unit",
help="Provide input unit. "
"ACC, VEL or DISP are available.",
type=str, default=None, required=False)
parser.add_argument("--output_filetype",
help="Provide output filetype. "
"MSEED, SAC, SLIST and INTERNAL_ASCII are available.",
type=str, default=None, required=True)
parser.add_argument("--remove_response",
help="Remove instrument's response from the waveform",
type=str2bool, default=False)
parser.add_argument("--zero_mean",
help="If true the mean of the data is subtracted",
type=str2bool, default=False, required=False)
parser.add_argument("--pre_filtering",
help="Apply a bandpass filter to the data trace "
"before deconvolution",
type=float, nargs="+", default=None,
required=False)
parser.add_argument("--output_unit",
help="Unit to which waveform should be converted "
"during response removal. When set to auto, "
"the unit will be automatically determined with "
"use of the second letter of channel code which "
"determines the instrument type."
"For details see Appendix A, SEED Manual v2.4.",
type=str, default=None, required=False)
parser.add_argument("--single_output_file",
help="Flag deciding whether all the traces will be written to "
"a single file, if set to False, each trace will be "
"written to a separate file. True is available only "
"for conversion to MSEED",
type=str2bool, default=False, required=False)
parser.add_argument("--output_dir",
help="Directory to which output files are written. "
"Defaults to current directory.",
type=str, default=".", required=False)
args = parser.parse_args()
filenames = convert_seed(**vars(args))
print('Created files:')
print(', '.join(filenames))
return
if __name__ == "__main__":
main(sys.argv)