Source code for mttime.core.configure
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: (LGPL-3.0)
# LLNL-CODE-814839
# author: Andrea Chiang (andrea@llnl.gov)
"""
Set global mttime configuration
.. rubric:: Example
.. code-block:: python
>>> import mttime
>>> # read input file mtinv.in
>>> config = mttime.Configure(path_to_file="mtinv.in")
"""
import os
import warnings
import numpy as np
import pandas as pd
[docs]class Configure(object):
"""
Configure object for :class:`~mttime.core.inversion.Inversion`
Sets up the moment tensor inverse routine. ``**kwargs`` can be provided
either in ``path_to_file`` or during class instantiation. ``df`` and ``depth``
are required if not reading from a file.
``**kwargs`` will override the values in ``path_to_file`` and any missing
keyword arguments will be set to their default values.
:param path_to_file: path to input file containing headers and station information.
Input file should be located in the project root directory.
:type path_to_file: str
:param df: station table, required if ``path_to_file=None``.
:type df: :class:`~pandas.DataFrame`
:param datetime: event origin time.
:type datetime: str, optional
:param longitude: event longitude.
:type longitude: float, optional
:param latitude: event latitude.
:type latitude: float, optional
:param depth: source depths to invert, required if ``path_to_file=None``
:type depth: float, int, list
:param path_to_data: path to data files, relative to root directory.
Defaults is ``"."``.
:type path_to_data: str
:param path_to_green: path to Green's function files, relative to root directory.
Default is ``"."``.
:type path_to_green: str
:param green: Green's function format, options are ``"herrmann"`` or ``"tensor"``.
Default is ``"herrmann"``.
:type green: str
:param components: waveform components, options are ``"Z"`` for vertical component,
or ``"ZRT"`` for three-component data in vertical, radial and transverse components, and
``"ZNE"`` for vertical, north and east. Default is ``"ZRT"``.
:type components: str
:param degree: degrees of freedom allowed in the inversion, options are
``5`` for deviatoric or ``6`` for full. Default is ``5``.
:type degree: int
:param weight: data weights, options are ``"none"``, ``"distance"`` or ``"variance"``
for no weights, inverse distance, or inverse variance, respectively.
Default is ``"none"``.
:type weight: str
:param plot: If ``True`` will plot the solution and waveform fits. Default is ``False``.
:type plot: int, bool
:param correlate: Flag to cross-correlate data and Green's functions
for best time shift (in time points). Default is ``False``.
:type correlate: int, bool
"""
# keyword argument types
_types = {"datetime": str,
"longitude": float,
"latitude": float,
"depth": str, # if read from file
"path_to_data": str,
"path_to_green": str,
"green": str,
"components": str,
"degree": int,
"weight": str,
"plot": int,
"correlate": int
}
# station table dtypes
_df_dtypes = {"station": str,
"distance": np.float,
"azimuth": np.float,
"ts": np.int,
"npts": np.int,
"dt": np.float,
"used": str,
"longitude": np.float,
"latitude": np.float,
"filter": str, # optional
"nc": np.int,
"np": np.int,
"lcrn": np.float,
"hcrn": np.float,
"model": str
}
def __init__(self, path_to_file=None, df=None, **kwargs):
# Read keywords from file first
if path_to_file is None:
header = {}
if df is None:
raise ValueError("'df' value is missing.")
if kwargs.get("depth") is None:
raise ValueError("'depth' value is missing.")
file = df
else:
header = self._read(path_to_file, self._types)
file = path_to_file
# Override parameters from file if provided kwargs during class instantiation
# then set class attributes
self._set_attributes(header, **kwargs)
# Read station table
self._update_table_index(file)
@staticmethod
def _read(path_to_file, types):
"""
Read inversion parameters from a text file.
Parse input text file to a python dictionary and returns the python dictionary.
:param path_to_file: file to read
:type path_to_file: str
:param types: keyword arguments
:type types: dict
:return: a dictionary of parameters for a single :class:`~mttime.core.configure.Configure` object
:rtype: dict
"""
# Check file
path_to_file = os.path.abspath(path_to_file)
if not isinstance(path_to_file, str):
raise TypeError("File name must be a string.\n")
try:
with open(path_to_file):
pass
except IOError:
print("Cannot open '%s'" % path_to_file)
# Read keyword arguments from file
header = {}
with open(path_to_file, "r") as f:
for key, parse in types.items():
try:
header[key] = parse(next(f).split()[1])
except IndexError:
msg = "'%s' missing in %s." % (key, path_to_file)
warnings.warn(msg)
continue
if "depth" in header:
header["depth"] = [float(depth) for depth in header["depth"].split(",") if depth]
# project root directory
home = path_to_file.rsplit("/", 1)
if len(home) > 1:
home = home[0]
else:
home = "."
header["path_to_data"] = os.path.abspath(
"/".join([home, header["path_to_data"]])
)
header["path_to_green"] = os.path.abspath(
"/".join([home, header["path_to_green"]])
)
return header
[docs] def write(self, fileout="config.out"):
"""
Function to write inversion configuration to a file
Write inverse routine parameters and station table to a file.
:param fileout: output file name. Default is ``"config.out"``.
:type fileout: str
.. rubric:: Example
.. code-block:: python
>>> config = Configure()
>>> config.write(fileout="configure.out")
"""
with open(fileout, "w") as f:
f.write(self.__str__())
def _set_attributes(self, header, **kwargs):
"""
Set class attributes
:param header: a dictionary of parameters
:type header: dict
:param kwargs: inputs from console
;type kwargs: dict
"""
# Over-ride parameters and set type
for key, parse in self._types.items():
if key in kwargs:
if key != "depth":
kwargs[key] = parse(kwargs[key])
else:
try:
kwargs[key] = header[key]
except KeyError:
continue
# Optional event attribute
event = dict()
event["datetime"] = kwargs.get("datetime","")
event["longitude"] = kwargs.get("longitude",np.nan)
event["latitude"] = kwargs.get("latitude",np.nan)
self.event = event
# Required attributes
self.depth = []
self._set_depth(kwargs["depth"])
self.path_to_data = os.path.abspath(kwargs.get("path_to_data", "."))
self.path_to_green = os.path.abspath(kwargs.get("path_to_green", "."))
kwargs["green"] = kwargs.get("green", "herrmann")
if kwargs["green"].lower() not in ("herrmann", "tensor"):
raise ValueError("green not supported.")
self.green = kwargs["green"].lower()
kwargs["components"] = kwargs.get("components", "ZRT")
if any(c == kwargs["components"].upper() for c in ("Z", "ZRT", "ZNE")):
self.components = list(kwargs["components"].upper())
else:
raise ValueError("components not supported.")
self.degree = kwargs.get("degree", 5)
kwargs["weight"] = kwargs.get("weight", "none")
if kwargs["weight"].lower() not in ("none", "distance", "variance"):
raise ValueError("weight not supported.")
self.weight = kwargs["weight"].lower()
self.plot = bool(kwargs.get("plot", False))
self.correlate = bool(kwargs.get("correlate", False))
# Additional attributes
self.inversion_type = {5: "Deviatoric", 6: "Full"}[self.degree]
def _set_depth(self, depth):
"""
Set depths
:param depth: source depths to invert
:type depth: float, int, list of float/ints
:return:
"""
if isinstance(depth, (int, float)):
self.depth = [depth]
elif isinstance(depth, list):
if len(depth) < 1:
msg = "Depth is empty."
raise IndexError(msg)
for _i in depth:
# Make sure each item in the list is a number.
if not isinstance(_i, (int, float)):
msg = "Depth must be a list of numbers."
raise TypeError(msg)
self.depth = depth
else:
msg = "Depth must be a number or a list of numbers."
raise TypeError(msg)
def _update_table_index(self, file):
"""
Constructs the station table from input file
:param file: path to input file or pandas DataFrame object
containing headers and station information
:type file: str or :class:`~pandas.DataFrame`
"""
# Read station table
if isinstance(file, str):
df = pd.read_table(file, sep=r"\s+", dtype=self._df_dtypes, skiprows=12)
elif isinstance(file, pd.DataFrame):
df = file
else:
msg = "'file' is not supported."
raise TypeError(msg)
self.nsta = len(df.index)
self.ncomp = len(self.components)
# Components to invert
df_col = df["used"].apply(lambda x: pd.Series(list(x)))
if len(df_col.columns) < self.ncomp:
df_col = pd.concat([df_col] * (self.ncomp), axis=1, ignore_index=True)
else:
df_col = df_col.iloc[:, [i for i in range(self.ncomp)]]
df_col.columns = self.components
location = 6
for component in self.components:
df.insert(loc=location, column=component, value=df_col[component])
df[component].fillna(df.Z, inplace=True)
location += 1
df.drop(columns="used", axis=1, inplace=True)
df[self.components] = df[self.components].astype(np.int)
# Indexing linear equations
index2 = np.cumsum(np.repeat(df.npts.to_numpy(), self.ncomp), dtype=np.int)
index1 = np.zeros(index2.shape, dtype=np.int)
index1[1::] = index2[0:-1]
self.index1 = index1.reshape(self.nsta, self.ncomp)
self.index2 = index2.reshape(self.nsta, self.ncomp)
self.station_table = df
def __str__(self):
keys = ("event",
"depth",
"green",
"components",
"degree",
"weight",
"plot",
"correlate"
)
f = "{0:>12}: {1}\n"
ret = "".join([f.format(key, str(getattr(self, key))) for key in keys])
ret = "\n".join(
[ret,
"| STATION TABLE |",
self.station_table.to_string(float_format="{:.2f}".format, index=False),
]
)
return ret
def _repr_pretty_(self, p, cycle):
p.text(str(self))