Source code for mspasspy.algorithms.edit
from abc import ABC, abstractmethod
from mspasspy.ccore.seismic import (
TimeSeries,
Seismogram,
TimeSeriesEnsemble,
SeismogramEnsemble,
)
from mspasspy.ccore.utility import MsPASSError, ErrorSeverity
from mspasspy.util.decorators import mspass_method_wrapper
# These internals are used throughout this module to reduce redundnacy
# in the code base. I'm putting them here because it will be less confusing
# to a reader where they come from - a common alternative is to put them
# at the end of the file on interwoven
def _input_is_valid(d):
"""
This internal function standardizes the test to certify the
input datum, d, is or is not a valid MsPASS data object. Putting it
in one place makes extending the code base for other data types much
easier. It uses an isinstance tests of d to standardize the test that
the input is valid data. It returns True if d is one a valid data
object known to mspass. Returns false it not. Caller must decide
what to do if the function returns false.
"""
return isinstance(
d, (TimeSeries, Seismogram, TimeSeriesEnsemble, SeismogramEnsemble)
)
def _is_ensemble(d):
"""
Standardized (simple) test if the data are an ensemble. Returns true if d is
an ensemble. false otherwise. Will yield a potentially misleading
False if d is anything but one of the MsPASS ensemble objects.
e.g. if d is something like a string it will correctly be returned
as False, but what happens next depends on the algorithm. Best practice
should be to precede a call to this function with a call to _input_is_valid
and trap data that is not a MsPASS data object.
"""
return isinstance(d, (TimeSeriesEnsemble, SeismogramEnsemble))
# Use of abstract base class based on:
# https://python-course.eu/oop/the-abc-of-abstract-base-classes.php
[docs]class Executioner(ABC):
"""
Abstract base class for family of python classes used for killing
mspass data objects failing to pass a particular metric.
It is made abstract to define required methods a particular instance
must create. As in any good OOP that also means subclasses can
add additional methods. This class should be used only as base
class as it has no functionality by itself.
"""
def __call__(self, d):
"""
This method can make the object callable and easier to use.
After an instance is iniated, instead of explicitly calling
kill_if_true, one can just call this object.
For example, instead of:
int_tester.kill_if_true(enscpy, apply_to_members=True)
We can just call this:
int_tester(enscpy, apply_to_members=True)
"""
self.kill_if_true(d)
[docs] @abstractmethod
def kill_if_true(self, d):
"""
This method should run a test on d that will call the kill method
on MsPASS atomic object d if the test defined by the implementation
fails. This is the main working method of this class of function
objects.
"""
pass
[docs] def edit_ensemble_members(self, ensemble):
"""
Subclasses should call this method if the input data are an
ensemble. A trick of inheritance allows the algorithm of self
to then be applied to whole ensemble. Putting this in the
base class avoids the duplication of duplicate code in all
subclasses.
"""
if ensemble.live:
for d in ensemble.member:
self.kill_if_true(d)
return ensemble
[docs] def log_kill(self, d, testname, message, severity=ErrorSeverity.Informational):
"""
This base class method is used to standardize the error logging
functionality of all Executioners. It writes a standardized
message to simplify writing of subclasses - they need only
define the testname (normally the name of the subclass) and
format a specific message to be posted.
Note most subclasses will may want to include a verbose option
(or the reciprocal silent) and only write log messages when
verbose is set true.
:param d: MsPASS data object to which elog message is to be
written.
:param testname: is the string assigned to the "algorithm" field
of the message posted to d.elog.
:param message: specialized message to post - this string is added
to an internal generic message.
:param severity: ErrorSeverity to assign to elog message
(See ErrorLogger docstring). Default is Informational
"""
if _input_is_valid(d):
kill_message = "Killed by kill_if_true method. Reason:\n"
kill_message += message
d.elog.log_error(testname, kill_message, severity)
else:
raise MsPASSError(
"Execution.log_kill method received invalid data; must be a MsPASS data object",
ErrorSeverity.Fatal,
)
[docs]class MetadataGT(Executioner):
"""
Implementation of Executioner using a greater than test of a
Metadata attribute. Both the metadata key and the threshold
for the kill test are set on creation. This implementation should
work on any value pairs for which the > operator in python works.
That is true for pairs of numeric types, for example, but will fail
if one of the pair is a string (an error anyway for any rational use of this).
It should also work for any pair of pyobjects for which operator > is defined,
although anything nonstandard should be tested carefully before using
this class for editing data with such nonstandard types. This was
mostly intended for simple numeric attributes.
"""
def __init__(self, key, value, verbose=False):
"""
One and only constructor. Sets the parameters that define this
tester.
:param key: key used for extracting a Metadata component for test
:param value: is the threshold value for the test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.value = value
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if the d[self.key] > value stored with the class.
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif self.key in d:
# because we don't have an else this logic will silently
# do nothing if key is not defined
testval = d[self.key]
if testval > self.value:
d.kill()
if self.verbose:
message = "Value associated with key={key} of {dval} is greater than test value={value}".format(
key=self.key, dval=d[self.key], value=self.value
)
self.log_kill(d, "MetadataGT", message)
else:
raise MsPASSError(
"MetadataGT received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataGE(Executioner):
"""
Implementation of Executioner using a greater than or equal test of a
Metadata attribute. Both the metadata key and the threshold
for the kill test are set on creation. This implementation should
work on any value pairs for which the >= operator in python works.
That is true for pairs of numeric types, for example, but will fail
if one of the pair is a string (an error anyway for any rational use of this).
It should also work for any pair of pyobjects for which operator >= is defined,
although anything nonstandard should be tested carefully before using
this class for editing data with such nonstandard types. This was
mostly intended for simple numeric attributes.
"""
def __init__(self, key, value, verbose=False):
"""
One and only constructor. Sets the parameters that define this
tester.
:param key: key used for extracting a Metadata component for test
:param value: is the threshold value for the test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.value = value
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if the d[self.key] >= value stored with the class.
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif self.key in d:
testval = d[self.key]
if testval >= self.value:
d.kill()
if self.verbose:
message = "Value associated with key={key} of {dval} is >= test value={value}".format(
key=self.key, dval=d[self.key], value=self.value
)
self.log_kill(d, "MetadataGE", message)
else:
raise MsPASSError(
"MetadataGE received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataLT(Executioner):
"""
Implementation of Executioner using a lessthan test of a
Metadata attribute. Both the metadata key and the threshold
for the kill test are set on creation. This implementation should
work on any value pairs for which the < operator in python works.
That is true for pairs of numeric types, for example, but will fail
if one of the pair is a string (an error anyway for any rational use of this).
It should also work for any pair of pyobjects for which operator < is defined,
although anything nonstandard should be tested carefully before using
this class for editing data with such nonstandard types. This was
mostly intended for simple numeric attributes.
"""
def __init__(self, key, value, verbose=False):
"""
One and only constructor. Sets the parameters that define this
tester.
:param key: key used for extracting a Metadata component for test
:param value: is the threshold value for the test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.value = value
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if the d[self.key] > value stored with the class.
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif self.key in d:
testval = d[self.key]
if testval < self.value:
d.kill()
if self.verbose:
message = "Value associated with key={key} of {dval} is < than test value={value}".format(
key=self.key, dval=d[self.key], value=self.value
)
self.log_kill(d, "MetadataLT", message)
else:
raise MsPASSError(
"MetadataLT received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataLE(Executioner):
"""
Implementation of Executioner using a less than or equal test of a
Metadata attribute. Both the metadata key and the threshold
for the kill test are set on creation. This implementation should
work on any value pairs for which the <= operator in python works.
That is true for pairs of numeric types, for example, but will fail
if one of the pair is a string (an error anyway for any rational use of this).
It should also work for any pair of pyobjects for which operator >= is defined,
although anything nonstandard should be tested carefully before using
this class for editing data with such nonstandard types. This was
mostly intended for simple numeric attributes.
"""
def __init__(self, key, value, verbose=False):
"""
One and only constructor. Sets the parameters that define this
tester.
:param key: key used for extracting a Metadata component for test
:param value: is the threshold value for the test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.value = value
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if the d[self.key] <= value stored with the class.
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif self.key in d:
testval = d[self.key]
if testval <= self.value:
d.kill()
if self.verbose:
message = "Value associated with key={key} of {dval} is <= to test value={value}".format(
key=self.key, dval=d[self.key], value=self.value
)
self.log_kill(d, "MetadataLT", message)
else:
raise MsPASSError(
"MetadataLE received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataEQ(Executioner):
"""
Implementation of Executioner using an equality test of a
Metadata attribute. Both the metadata key and the threshold
for the kill test are set on creation. This implementation should
work on any value pairs for which the == operator in python works.
That is true for pairs of numeric types, for example, but will fail
if one of the pair is a string (an error anyway for any rational use of this).
It should also work for any pair of pyobjects for which operator >= is defined,
although anything nonstandard should be tested carefully before using
this class for editing data with such nonstandard types. This was
mostly intended for simple numeric attributes. It can be used for
booleans even though few would write an if statement testing if two
booleans were equal.
"""
def __init__(self, key, value, verbose=False):
"""
One and only constructor. Sets the parameters that define this
tester.
:param key: key used for extracting a Metadata component for test
:param value: is the threshold value for the test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.value = value
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if the d[self.key] == value stored with the class.
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif self.key in d:
testval = d[self.key]
if testval == self.value:
d.kill()
if self.verbose:
message = "Value associated with key={key} of {dval} is equal to test value={value}".format(
key=self.key, dval=d[self.key], value=self.value
)
self.log_kill(d, "MetadataEQ", message)
else:
raise MsPASSError(
"MetadataEQ received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataNE(Executioner):
"""
Implementation of Executioner using an not equal (NE) test of a
Metadata attribute. Both the metadata key and the threshold
for the kill test are set on creation. This implementation should
work on any value pairs for which the != operator in python works.
That is true for pairs of numeric types, for example, but will fail
if one of the pair is a string (an error anyway for any rational use of this).
It should also work for any pair of pyobjects for which operator >= is defined,
although anything nonstandard should be tested carefully before using
this class for editing data with such nonstandard types. This was
mostly intended for simple numeric attributes. It can be used for
booleans even though few would write an if statement testing if two
booleans were equal.
"""
def __init__(self, key, value, verbose=False):
"""
One and only constructor. Sets the parameters that define this
tester.
:param key: key used for extracting a Metadata component for test
:param value: is the threshold value for the test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.value = value
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if the d[self.key] != value stored with the class.
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif self.key in d:
testval = d[self.key]
if testval != self.value:
d.kill()
if self.verbose:
message = "Value associated with key={key} of {dval} is != to test value={value}".format(
key=self.key, dval=d[self.key], value=self.value
)
self.log_kill(d, "MetadataNE", message)
else:
raise MsPASSError(
"MetadataNE received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataDefined(Executioner):
"""
Implementation of Executioner using an existence test. The
constructor loads only a key string. The test for the kill_if_true
method is then simply for the existence of a value associated with
the loaded key. Data will be killed if the defined key exists
in the Metadata (header).
"""
def __init__(self, key, verbose=False):
"""
One and only constructor. Sets the key used for the existence test.
:param key: key used for existence test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if self.key is defined for this datum
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif d.is_defined(self.key):
d.kill()
if self.verbose:
message = "Metadata key={key} is defined".format(key=self.key)
self.log_kill(d, "MetadataDefined", message)
else:
raise MsPASSError(
"MetadataDefined received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataUndefined(Executioner):
"""
Implementation of Executioner using an nonexistence test. The
constructor loads only a key string. The test for the kill_if_true
method is then simply for the existence of a value associated with
the loaded key. Data will be killed if the defined key does not exists
(Undefined) in the Metadata (header).
This class is a useful prefilter to apply to any algorithm that
requires a particular metadata attribute. Use FiringSquad to define
a chain of required metadata to prefilter data input to such an algorithm.
"""
def __init__(self, key, verbose=False):
"""
One and only constructor. Sets the key used for the existence test.
:param key: key used for existence test.
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if self.key is not defined for this datum
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif not d.is_defined(self.key):
d.kill()
if self.verbose:
message = "Metadata key={key} is not defined".format(key=self.key)
self.log_kill(d, "MetadataUndefined", message)
else:
raise MsPASSError(
"MetadataUndefined received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class MetadataInterval(Executioner):
"""
Implementation of Executioner based on a numeric range of values.
i.e. it tests if a metadata value is in a range defined by
an upper and lower value. Ranges have a minor complication in
handling the edge condition: should or should the test not include
the edge? Rather than write different functions for the four possible
combinations of <=, <, >=, and > that define a range we use
constructor arguments (use_lower_edge and use_upper_edge) to
turn inclusion of the boundary on and off.
In this context an interval test also has two logical alternatives.
One may want to keep data inside an interval (most common and default)
or delete data within a specified interval. That logic is controlled by
the kill_if_outside boolean
Intervals mostly make sense only for numeric types (int and float), but
can be used with strings. In reality this function should work with any
object for which the operators >, <, >=, and >= are defined but that is
adventure land if you try.
"""
def __init__(
self,
key,
lower_endpoint,
upper_endpoint,
use_lower_edge=True,
use_upper_edge=True,
kill_if_outside=True,
verbose=False,
):
"""
One and only constructor. Sets the parameters that define this
tester.
:param key: key used for extracting a Metadata component for test
:param lower_endpoint: value defining the lower bound of the range test
:param upper_endpoint: value defining the upper_bound of the range test
:parame use_lower_edge: if true (default) the lower range test uses
>= lower_endpoint. When false uses >.
:param use_upper_edge: if true (default) the upper range test uses
<= uper_endpoint. When false uses <.
:param kill_if_outside: boolean controlling logic of how test test
is applied. When true (default) data are killed when data are outside the
specified range. When false data are killed that are inside the
specified range. The default is True because that type of
test is much more common than the opposite. (e.g. retain source-receiver
distances within specified range)
:param verbose: if true informational messages will be posted to
the elog area of d. When false (default) kills are silent. That
default is intentioanl to reduce the size of elog data in large data
sets that often require extensive editing. Set true if you need
details on why data were killed or for debugging.
"""
self.key = key
self.lower_endpoint = lower_endpoint
self.upper_endpoint = upper_endpoint
self.use_lower_edge = use_lower_edge
self.use_upper_edge = use_upper_edge
self.kill_if_outside = kill_if_outside
self.verbose = verbose
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of this abstract method for this tester.
Kills d if the d[self.key] <= value stored with the class.
Returns a (potentially edited) copy of the input to allow use in
a parallel map operation.
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
elif self.key in d:
testval = d[self.key]
# These two are used to defined boolean result of lower
# and upper tests. Truth of result is and of the two
# inverting the test (inside_test false) inverts the logic (not)
upper_range_test = False
lower_range_test = False
if self.use_lower_edge:
if testval >= self.lower_endpoint:
lower_range_test = True
else:
lower_range_test = False
else:
if testval > self.lower_endpoint:
lower_range_test = True
else:
lower_range_test = False
if self.use_upper_edge:
if testval <= self.upper_endpoint:
upper_range_test = True
else:
upper_range_test = False
else:
if testval < self.upper_endpoint:
upper_range_test = True
else:
upper_range_test = False
death_sentence = lower_range_test and upper_range_test
# double negatives are a bit weird here but the logic
# requires the logic be inverted if testing outside the
# defined interval
if self.kill_if_outside:
death_sentence = not death_sentence
if death_sentence:
d.kill()
if self.verbose:
message1 = "Value associated with key={key} of {dval} failed range test\n".format(
key=self.key, dval=testval
)
message2 = "Interval range is {lower} to {upper}. ".format(
lower=self.lower_endpoint, upper=self.upper_endpoint
)
message3 = "Test booleans: lowerEQ={lower}, upperEQ={upper}, kill_inside={inside}".format(
lower=self.use_lower_edge,
upper=self.use_upper_edge,
inside=self.kill_if_outside,
)
self.log_kill(
d, "MetadataInterval", message1 + message2 + message3
)
else:
raise MsPASSError(
"MetadataInterval received invalid input data", ErrorSeverity.Fatal
)
return d
[docs]class FiringSquad(Executioner):
"""
Used to apply multiple Executioners in a single pass - hence the name
FiringSquare image; facing more than one thing that could kill you.
The implementation in kill_if_true iterates through a list of
Executioners. Once the datum is killed it is immediately returned.
If the Executions are running in verbose mode that means some tests
can shadow others. It is like a firing squad where the guns are fired
in sequence and the body is removed as soon as there is a hit. The
victim walks away only if all the guns miss.
Note the class has a += operator to allow appending additional
tests to the chain.
"""
def __init__(self, executioner_list):
"""
One and only constructor. executioner_list does not literally
have to be a list container. It can be any container that is iterable so
a list, tuple, or array can be used. Internally the contents
are copied to a python list container so this the contents of
executioner_list are treated as immutable.
The constructor will throw a MsPASSError exception if any of the
contents of executioner_list is not a child of the
Executioner class.
"""
for ex in executioner_list:
if not isinstance(ex, Executioner):
raise MsPASSError(
"FiringSquad constructor: invalid input. Expected array of Executioners",
ErrorSeverity.Fatal,
)
# to allow flexibility of the structure used for input we should
# copy the executioner_list. Further, this assure the
# result will iterate correctly and allow for append
self.executioners = list()
for ex in executioner_list:
self.executioners.append(ex)
[docs] @mspass_method_wrapper
def kill_if_true(self, d, apply_to_members=False):
"""
Implementation of base class method. In this case failure is
defined as not passing one of the set of tests loaded when
the object was created. As noted earlier the tests are performed
in the same order they were passed to the constructor of added on
with the += operator.
:param d: is a mspass data object to be checked
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
else:
for killer in self.executioners:
killer.kill_if_true(d, apply_to_members=apply_to_members)
if d.dead():
break
return d
else:
raise MsPASSError(
"FiringSquad received invalid input data", ErrorSeverity.Fatal
)
def __iadd__(self, other):
"""
Defines the += operator for the class. In that case it means a new
test is appended. Raises a MsPASSError set Fatal other is not a
subclass of Executioner.
"""
if isinstance(other, Executioner):
self.executioners.append(other)
else:
raise MsPASSError(
"FiringSquare operator += rhs is not an Executioner",
ErrorSeverity.Fatal,
)
return self
# Start base class and subclasses for header math operators
[docs]class MetadataOperator(ABC):
"""
Base class for a set of inline Metadata editors. That is, there
are many instances where Metadata attributes need to be altered
during a workflow where it is either unnecessary or inappropriate
to access the database. e.g. we have found updates in MongoDB
can be very slow if done one transaction at a time so it can
streamline processing to edit metadata on the fly. This base
class defines the API for a suite of tools for editing metadata
on the fly.
"""
[docs] @abstractmethod
def apply(self, d, apply_to_members=False, fast_mode=False):
"""
Implementations are required to implement this method. It must
accept a MsPASS data object, d, (TimeSeries, Seismogram, TimeSeriesEnsemble,
or SeimogramEnsemble) and apply the operator the implementation defines
to the Metadata of d. It should throw a MsPASSError exception if
d is not one of the expected data types. It should, on the other
hand, handle d==None gracefully and do nothing if d is None.
Ensemble handlers need to deal with an ambiguity in which metadata
the operation refers to. Both the overall ensemble container and
individual members have (normally independent) Metadata containers.
Implementations of this method should have a apply_to_members
argument as in the signature for this base class. When True
the editing is done by a loop over all members while if
False (which should be the default) the ensemble's container
should be used.
This method must return an (potentially but not guarnateed) edited
version of d. That is essential to allow this method to be used
as the function in a parallel map operator.
If a required metadata key is missing this method should do nothing
unless a verbose flag is set. In that case it should log that as
an error. Best practice for any use of operators based on this
base class is to apply a kill operator for (MetadataDefined) on
all data before running a calculator. That way you can guarantee
the data needed for all operations is valid before trying to
do calculations on on required metadata. There is some overhead
in validating metadata so all implementations should include
use of "fast_mode". When set true the safeties will be bypassed
for speed. That includes at least the two requried methods
"check_keys" and "check_operation". Implementations may add
other safties.
"""
pass
[docs] @abstractmethod
def check_keys(self, d):
"""
All implementation should implement this checker even if all it contains
is a pass. It should validate the keys are defined in data to be
handled in the apply method. An early call in apply should always
be to call this method.
"""
pass
[docs] @abstractmethod
def check_operation(self, d):
"""
All implementations should implement this method even if they choose
to ignore it. It should be used to guarantee the operator the class
defines will succeed on the datum sent to the apply method.
e.g. any standard arithmetic operations will throw a TypeError if
one of the operands is a string. This method should be used to
make the operator as bombproof as possible logging a message
to the datum rather than aborting if there is an issue. Some
classes may want to implement this as pass because it makes
no sense - e.g. setting a constant value. Those are the
exception, however, so the api dogmatically demands these
be implemented even if they do nothing.
"""
pass
[docs] def edit_ensemble_members(self, ensemble):
"""
Subclasses should call this method if the input data are an
ensemble. A trick of inheritance allows the algorithm of self
to then be applied to whole ensemble. Putting this in the
base class avoids the duplication of duplicate code in all \
subclasses.
"""
if ensemble.live:
for d in ensemble.member:
self.apply(d)
return ensemble
[docs] def log_edit(self, d, testname, message, severity=ErrorSeverity.Informational):
"""
This base class method is used to standardize the error logging
functionality of all editors. It writes a standardized
message to simplify writing of subclasses - they need only
define the testname (normally the name of the subclass) and
format a specific message to be posted.
Note most subclasses will may want to include a verbose option
(or the reciprocal silent) and only write log messages when
verbose is set true.
:param d: MsPASS data object to which elog message is to be
written.
:param testname: is the string assigned to the "algorithm" field
of the message posted to d.elog.
:param message: specialized message to post - this string is added
to an internal generic message.
:param severity: ErrorSeverity to assign to elog message
(See ErrorLogger docstring). Default is Informational
"""
if _input_is_valid(d):
edit_message = "Altered by apply method of this class. Details:\n"
edit_message += message
d.elog.log_error(testname, edit_message, severity)
else:
raise MsPASSError(
"MetadataOperator.apply method received invalid data; must be a MsPASS data object",
ErrorSeverity.Fatal,
)
[docs]class ChangeKey(MetadataOperator):
def __init__(self, oldkey, newkey, erase_old=True):
# important to validate type of keys to avoid unexpected exceptions
# when used
if isinstance(oldkey, str) and isinstance(newkey, str):
self.oldkey = oldkey
self.newkey = newkey
self.erase_old = erase_old
else:
message = (
"ChangeKey constructor usage error. ChangeKey(oldkey,newkey)\n"
+ "oldkey and newkey must be strings defining metadata keys - check types"
)
raise MsPASSError(message, ErrorSeverity.Fatal)
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False, fast_mode=False, verbose=False):
if _input_is_valid(d):
if d.dead():
return d
else:
if not fast_mode:
keysok = self.check_keys(d, apply_to_members)
if not keysok:
if verbose:
message = "key=" + self.oldkey + " is not defined"
self.log_edit(
d, "ChangeKey", message, ErrorSeverity.Invalid
)
return d
# from here on we can assume operation is ok
if _is_ensemble(d) and apply_to_members:
for m in d.member:
if m.dead():
continue
val = m[self.oldkey]
m[self.newkey] = val
if self.erase_old:
m.erase(self.oldkey)
else:
# Land here if for all atomic data and if asked
# to only modify the ensemble (global) Metadata container.
# i.e. when apply_to_member is false. Makes the logic a
# bit confusing but reduces redundant code
val = d[self.oldkey]
d[self.newkey] = val
if self.erase_old:
d.erase(self.oldkey)
return d
else:
raise MsPASSError(
"ChangeKey.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d, apply_to_members):
if _is_ensemble(d) and apply_to_members:
for m in d.member:
if m.dead():
continue
if not m.is_defined(self.oldkey):
return False
return True
else:
if d.is_defined(self.oldkey):
return True
else:
return False
[docs] def check_operation(self, d):
"""
there is no operation on a ChangeKey so this method does nothing.
Because it is an abstract base we have to have this stub.
"""
pass
[docs]class SetValue(MetadataOperator):
"""
Used to set a specified metadata key to a constant value. Note any
existing value of Metadata associated with the key defined in the
operator always be overwritten.
Example: to set the value of key = 'a' to constant 2.0
op = SetValue('a',2.0)
d = op.apply(d)
"""
def __init__(self, key, constant_value=0):
"""
Constructor for this operator.
:param key: string defining key for Metadata to be set.
:param constant_value: is the value to use for setting the
attribute linked to the key. Note we do no type checking so
beware of implicit integer conversion (e.g. use 2.0 if you mean
want a float 2.0 not 2 which python would auto cast to an int)
"""
self.key = key
self.value = constant_value
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
"""
Used to apply this operator to Metadata of a MsPASS data object.
Use of decorator adds common MsPASS arguments as call options.
:param d: datum to which operator is to be applied. d must be a
valid MsPASS data object or this method will throw a fatal MsPASSError
exception. If d is marked dead it will be silently ignored.
:param apply_to_members: when true and d is an ensemble object the
operator is applied to the members. When false the metadata for the
ensemble will be altered. This parameter is ignored for atomic
data types. Default is False.
:return: always returns a (usually) edited copy of the input d.
When the input d is dead the copy will always be unaltered.
Note the copy is a shallow copy which in python means we just return
the equivalent of a pointer to the caller. Important for efficiency
as d can be very large for some ensembles.
"""
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
d[self.key] = self.value
return d
else:
raise MsPASSError(
"SetValue.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
Useless implementation of required abstract base method. In this
case all it does is test if the stored value of key is a string.
Returns true if the key is a string and false otherwise.
"""
if isinstance(self.key, str):
return True
else:
return False
# all arithmetic operators need an output key to place result that can be the same as one of the inputs
# These are unary - operate using a constant value set on constructoin
[docs]class Add(MetadataOperator):
"""
Used to implement += operator on a specified Metadata key.
Example: to add 2 to data, d, with key='icdp' could use this
op = Add('icdp',2)
d = op.apply(d)
"""
def __init__(self, key, value_to_add=1):
self.key = key
self.value = value_to_add
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Add Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = "Required key=" + self.key + " not defined"
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The += operator fails on Metadata with key="
+ self.key
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val = d[self.key]
val += self.value
d[self.key] = val
return d
else:
raise MsPASSError(
"Add.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the key defined for the operator is defined for d.
Returns true if is defined and false if not. the method assumes
d is a valid child of Metadata so the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
testval = d[self.key]
testval += self.value
except:
return False
else:
return True
[docs]class Multiply(MetadataOperator):
"""
Used to implement *= operator on a specified Metadata key.
Example: to multiple metadata in, d, with key='Pamp' by 2.5
you could use this
op = Multiply('Pamp',2.5)
d = op.apply(d)
"""
def __init__(self, key, value_to_multiply=1):
self.key = key
self.value = value_to_multiply
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Multiply Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = "Required key=" + self.key + " not defined"
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The *= operator fails on Metadata with key="
+ self.key
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val = d[self.key]
val *= self.value
d[self.key] = val
return d
else:
raise MsPASSError(
"Multiply.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the key defined for the operator is defined for d.
Returns true if is defined and false if not. the method assumes
d is a valid child of Metadata so the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
testval = d[self.key]
testval *= self.value
except:
return False
else:
return True
[docs]class Subtract(MetadataOperator):
"""
Used to implement -= operator on a specified Metadata key.
Example: to subtract 2 from metadata, d, with key='icdp' could use this
op = Subtract('icdp',2)
d = op.apply(d)
"""
def __init__(self, key, value_to_subtract):
self.key = key
self.value = value_to_subtract
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Subtract Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = "Required key=" + self.key + " not defined"
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The -= operator fails on Metadata with key="
+ self.key
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val = d[self.key]
val -= self.value
d[self.key] = val
return d
else:
raise MsPASSError(
"Subtract.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the key defined for the operator is defined for d.
Returns true if is defined and false if not. the method assumes
d is a valid child of Metadata so the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
testval = d[self.key]
testval -= self.value
except:
return False
else:
return True
[docs]class Divide(MetadataOperator):
"""
Used to implement /= operator on a specified Metadata key.
Example: to divide metadata in, d, with key='Pamp' by 2.0
you could use this
op = Divide('Pamp',2.-)
d = op.apply(d)
"""
def __init__(self, key, value_to_divide=1):
self.key = key
self.value = value_to_divide
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Divide Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = "Required key=" + self.key + " not defined"
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The /= operator fails on Metadata with key="
+ self.key
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val = d[self.key]
val /= self.value
d[self.key] = val
return d
else:
raise MsPASSError(
"Divide.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the key defined for the operator is defined for d.
Returns true if is defined and false if not. the method assumes
d is a valid child of Metadata so the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
testval = d[self.key]
testval /= self.value
except:
return False
else:
return True
[docs]class IntegerDivide(MetadataOperator):
"""
Used to implement // operator on a specified Metadata key.
The // operator is a bit obscure but it implements the common
need to truncate a division result to an integer. This will work
on floats but the result will always be close to and integer
value as if the operation were done with integers. Note
also IntegerDivide is the complement to Mod which returns the remainder
of such a division.
Example: to apply integer division to metadata in, d, with key='icdp' by 5
you could use this
op = IntegerDivide('icdp',5)
d = op.apply(d)
"""
def __init__(self, key, value=1):
self.key = key
self.value = value
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata IntegerDivide Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = "Required key=" + self.key + " not defined"
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The // operator fails on Metadata with key="
+ self.key
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val = d[self.key]
# there is a //= operator but it is obscure in this context
val = val // self.value
d[self.key] = val
return d
else:
raise MsPASSError(
"IntegerDivide.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the key defined for the operator is defined for d.
Returns true if is defined and false if not. the method assumes
d is a valid child of Metadata so the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
testval = d[self.key]
testval = testval // self.value
except:
return False
else:
return True
[docs]class Mod(MetadataOperator):
"""
Used to implement % operator on a specified Metadata key.
The % operator is a bit obscure but it implements the common
need return the remainder of a divide operation. It is commonly
used, for example, in cmp processing where survey flag numbers can
often be converted to channel numbers for simple multichannel cable
geometries.
This operator will work any numeric type but it is most commonly used
for integer attributes.
Example: to convert the metadata associated with the key 'ichan' that
are currently counting by 1 to numbers that cycle from 0 to 23 us this:
op = Mod('ix',24)
d = op.apply(d)
"""
# note default would zero everything if used
def __init__(self, key, value=1):
self.key = key
self.value = value
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Mod Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = "Required key=" + self.key + " not defined"
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The % operator fails on Metadata with key="
+ self.key
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val = d[self.key]
# there is a //= operator but it is obscure in this context
val = val % self.value
d[self.key] = val
return d
else:
raise MsPASSError(
"Mod.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the key defined for the operator is defined for d.
Returns true if is defined and false if not. the method assumes
d is a valid child of Metadata so the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
testval = d[self.key]
testval = testval % self.value
except:
return False
else:
return True
# These are binary operators using two keys set in constructor
[docs]class Add2(MetadataOperator):
"""
Used to implement + operator that adds two Metadata attributes
together. The attributes are feteched with two keys set when
the operator is constructed. Let a be the value in a datum
associated with key2 (arg1 of constructor) and b be the
value associated with key2 (arg2 of constructor). The apply
method of this class computes a+b and sets the Metadata attribute
defined by key0 (arg0 of constructor) to that value (a+b)
i.e. d[key0] = d[key1] + d[key2]
Note key0 can be the same as either key1 or key2. The contents of the
left hand side (key0) are always set by this operator unless that
input was previously marked dead. Further key1 and key2 can be the
same although it is hard to conceive how that could be useful.
Example: to compute ix as the d['sx'] + d['chan'] use
op = Add2('ix1','sx','chan')
d = op.apply(d)
"""
def __init__(self, key0, key1, key2):
self.key0 = key0
self.key1 = key1
self.key2 = key2
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Add2 Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = (
"Required keys key1="
+ self.key1
+ " and/or key2="
+ self.key2
+ " not defined"
)
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The + operator failed with Metadata fetched with keys="
+ self.key1
+ " and "
+ self.key2
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val1 = d[self.key1]
val2 = d[self.key2]
d[self.key0] = val1 + val2
return d
else:
raise MsPASSError(
"Add2.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the keys required for the operator are defined for d.
If either required key are missing from d return False. Return
True if both are set in d. The method assumes
d is a valid child of Metadata so that the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key1) and d.is_defined(self.key2):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
val1 = d[self.key1]
val2 = d[self.key2]
testval = val1 + val2
except:
return False
else:
return True
[docs]class Multiply2(MetadataOperator):
"""
Used to implement * operator that multiplies two Metadata attributes
together. The attributes are feteched with two keys set when
the operator is constructed. Let a be the value in a datum
associated with key2 (arg1 of constructor) and b be the
value associated with key2 (arg2 of constructor). The apply
method of this class computes a*b and sets the Metadata attribute
defined by key0 (arg0 of constructor) to that value (a*b)
i.e. d[key0] = d[key1] * d[key2]
Note key0 can be the same as either key1 or key2. The contents of the
left hand side (key0) are always set by this operator unless that
input was previously marked dead. Further key1 and key2 can be the
same although it is hard to conceive how that could be useful.
Example: to compute ix as the d['sx'] * d['chan'] use
op = Multiply2('ix1','sx','chan')
d = op.apply(d)
"""
def __init__(self, key0, key1, key2):
self.key0 = key0
self.key1 = key1
self.key2 = key2
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Multiply2 Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = (
"Required keys key1="
+ self.key1
+ " and/or key2="
+ self.key2
+ " not defined"
)
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The * operator failed with Metadata fetched with keys="
+ self.key1
+ " and "
+ self.key2
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val1 = d[self.key1]
val2 = d[self.key2]
d[self.key0] = val1 * val2
return d
else:
raise MsPASSError(
"Multiply2.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the keys required for the operator are defined for d.
If either required key are missing from d return False. Return
True if both are set in d. The method assumes
d is a valid child of Metadata so that the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key1) and d.is_defined(self.key2):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
val1 = d[self.key1]
val2 = d[self.key2]
testval = val1 * val2
except:
return False
else:
return True
[docs]class Subtract2(MetadataOperator):
"""
Used to implement - operator that computes the difference of two Metadata
attributes. The attributes are feteched with two keys set when
the operator is constructed. Let a be the value in a datum
associated with key2 (arg1 of constructor) and b be the
value associated with key2 (arg2 of constructor). The apply
method of this class computes a-b and sets the Metadata attribute
defined by key0 (arg0 of constructor) to that value (a-b)
i.e. d[key0] = d[key1] - d[key2]
Note key0 can be the same as either key1 or key2. The contents of the
left hand side (key0) are always set by this operator unless that
input was previously marked dead. Further key1 and key2 can be the
same although it is hard to conceive how that could be useful.
Example: to compute ix as the d['sx'] - d['chan'] use
op = Subtract2('ix1','sx','chan')
d = op.apply(d)
"""
def __init__(self, key0, key1, key2):
self.key0 = key0
self.key1 = key1
self.key2 = key2
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Subtract2 Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = (
"Required keys key1="
+ self.key1
+ " and/or key2="
+ self.key2
+ " not defined"
)
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The - operator failed with Metadata fetched with keys="
+ self.key1
+ " and "
+ self.key2
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val1 = d[self.key1]
val2 = d[self.key2]
d[self.key0] = val1 - val2
return d
else:
raise MsPASSError(
"Subtract2.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the keys required for the operator are defined for d.
If either required key are missing from d return False. Return
True if both are set in d. The method assumes
d is a valid child of Metadata so that the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key1) and d.is_defined(self.key2):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
val1 = d[self.key1]
val2 = d[self.key2]
testval = val1 - val2
except:
return False
else:
return True
[docs]class Divide2(MetadataOperator):
"""
Used to implement / operator that divides two Metadata attributes.
The attributes are feteched with two keys set when
the operator is constructed. Let a be the value in a datum
associated with key2 (arg1 of constructor) and b be the
value associated with key2 (arg2 of constructor). The apply
method of this class computes a/b and sets the Metadata attribute
defined by key0 (arg0 of constructor) to that value (a/b)
i.e. d[key0] = d[key1] / d[key2]
Note key0 can be the same as either key1 or key2. The contents of the
left hand side (key0) are always set by this operator unless that
input was previously marked dead. Further key1 and key2 can be the
same although it is hard to conceive how that could be useful.
Example: to compute ix as the d['sx'] / d['chan'] use
op = Divide2('ix1','sx','chan')
d = op.apply(d)
"""
def __init__(self, key0, key1, key2):
self.key0 = key0
self.key1 = key1
self.key2 = key2
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Divide2 Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = (
"Required keys key1="
+ self.key1
+ " and/or key2="
+ self.key2
+ " not defined"
)
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The / operator failed with Metadata fetched with keys="
+ self.key1
+ " and "
+ self.key2
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val1 = d[self.key1]
val2 = d[self.key2]
d[self.key0] = val1 / val2
return d
else:
raise MsPASSError(
"Divide2.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the keys required for the operator are defined for d.
If either required key are missing from d return False. Return
True if both are set in d. The method assumes
d is a valid child of Metadata so that the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key1) and d.is_defined(self.key2):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
val1 = d[self.key1]
val2 = d[self.key2]
testval = val1 / val2
except:
return False
else:
return True
[docs]class IntegerDivide2(MetadataOperator):
"""
Used to implement // operator between two Metadata attributes.
The attributes are feteched with two keys set when
the operator is constructed. Let a be the value in a datum
associated with key2 (arg1 of constructor) and b be the
value associated with key2 (arg2 of constructor). The apply
method of this class computes a//b and sets the Metadata attribute
defined by key0 (arg0 of constructor) to that value (a+b)
i.e. d[key0] = d[key1] // d[key2]
Note key0 can be the same as either key1 or key2. The contents of the
left hand side (key0) are always set by this operator unless that
input was previously marked dead. Further key1 and key2 can be the
same although it is hard to conceive how that could be useful.
Example: to compute ix as the d['sx'] // d['chan'] use
op = IntegerDivide2('ix1','sx','chan')
d = op.apply(d)
"""
def __init__(self, key0, key1, key2):
self.key0 = key0
self.key1 = key1
self.key2 = key2
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata IntegerDivide2 Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = (
"Required keys key1="
+ self.key1
+ " and/or key2="
+ self.key2
+ " not defined"
)
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The // operator failed with Metadata fetched with keys="
+ self.key1
+ " and "
+ self.key2
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val1 = d[self.key1]
val2 = d[self.key2]
d[self.key0] = val1 // val2
return d
else:
raise MsPASSError(
"IntegerDivide2.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the keys required for the operator are defined for d.
If either required key are missing from d return False. Return
True if both are set in d. The method assumes
d is a valid child of Metadata so that the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key1) and d.is_defined(self.key2):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
val1 = d[self.key1]
val2 = d[self.key2]
testval = val1 // val2
except:
return False
else:
return True
[docs]class Mod2(MetadataOperator):
"""
Used to implement % operator between two Metadata attributes.
The attributes are feteched with two keys set when
the operator is constructed. Let a be the value in a datum
associated with key2 (arg1 of constructor) and b be the
value associated with key2 (arg2 of constructor). The apply
method of this class computes a+b and sets the Metadata attribute
defined by key0 (arg0 of constructor) to that value (a%b)
i.e. d[key0] = d[key1] % d[key2]
Note key0 can be the same as either key1 or key2. The contents of the
left hand side (key0) are always set by this operator unless that
input was previously marked dead. Further key1 and key2 can be the
same although it is hard to conceive how that could be useful.
Example: to compute ix as the d['sx'] % d['chan'] use
op = Mod2('ix1','sx','chan')
d = op.apply(d)
"""
def __init__(self, key0, key1, key2):
self.key0 = key0
self.key1 = key1
self.key2 = key2
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
elognametag = "Metadata Mod2 Operator"
if _input_is_valid(d):
if d.dead():
return d
else:
if _is_ensemble(d) and apply_to_members:
d = self.edit_ensemble_members(d)
else:
if not self.check_keys(d):
message = (
"Required keys key1="
+ self.key1
+ " and/or key2="
+ self.key2
+ " not defined"
)
self.log_edit(d, elognametag, message, ErrorSeverity.Invalid)
d.kill()
return d
elif not self.check_operation(d):
message = (
"The % operator failed with Metadata fetched with keys="
+ self.key1
+ " and "
+ self.key2
+ "\nAn incompatible type likely is stored with that key"
)
d.kill()
return d
val1 = d[self.key1]
val2 = d[self.key2]
d[self.key0] = val1 % val2
return d
else:
raise MsPASSError(
"Mod2.apply: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)
[docs] def check_keys(self, d):
"""
checks that the keys required for the operator are defined for d.
If either required key are missing from d return False. Return
True if both are set in d. The method assumes
d is a valid child of Metadata so that the is_defined method will
not generate an exception. that means this method should ALWAYS
be called after a test with _input_is_valid.
"""
if d.is_defined(self.key1) and d.is_defined(self.key2):
return True
else:
return False
[docs] def check_operation(self, d):
"""
This method checks to make sure the value works with the operation
required. It provides a (required by the base class) standardization
of testing for validity of the operation.
"""
# we catch any exception - think we could be explicity with a TypeError
# but this is more robust
try:
val1 = d[self.key1]
val2 = d[self.key2]
testval = val1 % val2
except:
return False
else:
return True
# Note this class was patterned closely after FiringSquad - the approach
# is identical. I did little more than edit a copy of FiringSquad to produce
# this class
[docs]class MetadataOperatorChain(MetadataOperator):
"""
Used to apply multiple a chain of arithmetic operators to derive
computed metadata attributes. Very elaborate calculations can be
done through this class by chaining appropriate atomic operators
defined elsewhere in the module (i.e. Add, Subtract, etc.).
The operation chain is defined by a python list of the atomic operators.
When the apply method of this class is called the list of operators
are applied sequentially in list order.
Note the class has a += operator to allow appending additional
operators to the chain.
"""
def __init__(self, operator_list):
"""
One and only constructor. operator_list does not literally
have to be a list container. It can be any container that is iterable so
a list, tuple, or array can be used. Internally the contents
are copied to a python list container so this the contents of
operator_list sent to the constructor are treated not unintentionally
modified.
The constructor will throw a MsPASSError exception if any of the
contents of operator_list is not a child of the
MetadataOperator base class.
"""
for ex in operator_list:
if not isinstance(ex, MetadataOperator):
raise MsPASSError(
"MetadataOperatorChain constructor: invalid input. Expected iterable container of MetadataOperator objects",
ErrorSeverity.Fatal,
)
# to allow flexibility of the structure used for input we should
# copy the operator_list. Further, this assure the
# result will iterate correctly and allow for append
self.oplist = list()
for ex in operator_list:
self.oplist.append(ex)
[docs] @mspass_method_wrapper
def apply(self, d, apply_to_members=False):
"""
Implementation of base class method. In this case failure is
defined as not passing one of the set of tests loaded when
the object was created. As noted earlier the tests are performed
in the same order they were passed to the constructor of added on
with the += operator.
:param d: is a mspass data object to be checked
"""
if _input_is_valid(d):
if d.dead():
return d
if _is_ensemble(d) and apply_to_members:
self.edit_ensemble_members(d)
else:
for op in self.oplist:
op.apply(d, apply_to_members=apply_to_members)
if d.dead():
break
return d
else:
raise MsPASSError(
"MetadataOperatorChain received invalid input data", ErrorSeverity.Fatal
)
# These two virtual methods have to be defined but they do nothing in
# this context. We depend on the atomic operators to implement these
# checks
def __iadd__(self, other):
"""
Defines the += operator for the class. In that case it means a new
operator is appended. Raises a MsPASSError set Fatal other is not a
subclass of MetadataOperator.
"""
if isinstance(other, MetadataOperator):
self.oplist.append(other)
else:
raise MsPASSError(
"MetadataOperatorChain: operator += rhs is not a child of MetadataOperator",
ErrorSeverity.Fatal,
)
return self
# other editors to implement that do not match the abstract base class model
[docs]def erase_metadata(d, keylist, apply_to_members=False):
"""
This editor clears the contents of any data associated with a list of
Metadata keys. If there is no data for that key it will silently do
nothing. If the input is an ensemble and apply_to_members is True
all the algorithm will run on the metadata of each member in a loop.
If false only the ensemble (global) metadata are handled.
:param d: must be a valid MsPASS data object. If not the function
will throw an exception. If the datum is marked dead it will
silently just return the contents.
:param keylist: python list of strings to use as keywords. Any matching
keys the metadata of d will be cleared.
:param apply_to_members: is a boolean controlling how the function
should handle ensembles. Then set true the erase algorithm will be
applied in a loop over the member vector of an input ensemble. When
False (default) only the ensemble's metadata is checked. This parameter
is ignored if the input is an atomic data type.
:return: edited copy of d
"""
if _input_is_valid(d):
if _is_ensemble(d) and apply_to_members:
for m in d.member:
for k in keylist:
# erase silently does nothing if the key is not defined so
# no error handler is needed here
m.erase(k)
else:
for k in keylist:
d.erase(k)
return d
else:
raise MsPASSError(
"erase_metadata: input is not a valid MsPASS data object",
ErrorSeverity.Fatal,
)