"""
Pydantic schema for ObsPlus event model.
ObsPlus Event Model is a superset of, and compatible with, ObsPy's Event
model.
"""
from datetime import datetime
from typing import Optional, List, Union
from uuid import uuid4
from typing_extensions import Literal, Annotated
import obspy.core.event as ev
from obspy import UTCDateTime
from pydantic import (
model_validator,
ConfigDict,
BaseModel,
PlainValidator,
Field,
)
from obsplus.constants import NSLC
# ----- Type Literals (enum like)
data_used_wave_type = Literal[
"P waves", "body waves", "surface waves", "mantle waves", "combined", "unknown"
]
AmplitudeCategory = Literal["point", "mean", "duration", "period", "integral", "other"]
AmplitudeUnit = Literal["m", "s", "m/s", "m/(s*s)", "m*s", "dimensionless", "other"]
DataUsedWaveType = Literal[
"P waves", "body waves", "surface waves", "mantle waves", "combined", "unknown"
]
EvaluationMode = Literal["manual", "automatic"]
EvaluationStatus = Literal["preliminary", "confirmed", "reviewed", "final", "rejected"]
EventDescriptionType = Literal[
"felt report",
"Flinn-Engdahl region",
"local time",
"tectonic summary",
"nearest cities",
"earthquake name",
"region name",
]
EventType = Literal[
"not existing",
"not reported",
"earthquake",
"anthropogenic event",
"collapse",
"cavity collapse",
"mine collapse",
"building collapse",
"explosion",
"accidental explosion",
"chemical explosion",
"controlled explosion",
"experimental explosion",
"industrial explosion",
"mining explosion",
"quarry blast",
"road cut",
"blasting levee",
"nuclear explosion",
"induced or triggered event",
"rock burst",
"reservoir loading",
"fluid injection",
"fluid extraction",
"crash",
"plane crash",
"train crash",
"boat crash",
"other event",
"atmospheric event",
"sonic boom",
"sonic blast",
"acoustic noise",
"thunder",
"avalanche",
"snow avalanche",
"debris avalanche",
"hydroacoustic event",
"ice quake",
"slide",
"landslide",
"rockslide",
"meteorite",
"volcanic eruption",
]
EventTypeCertainty = Literal["known", "suspected"]
MTInversionType = Literal["general", "zero trace", "double couple"]
MomentTensorCategory = Literal["teleseismic", "regional"]
OriginDepthType = Literal[
"from location",
"from moment tensor inversion",
"from modeling of broad-band P waveforms",
"constrained by depth phases",
"constrained by direct phases",
"constrained by depth and direct phases",
"operator assigned",
"other",
]
OriginType = Literal[
"hypocenter",
"centroid",
"amplitude",
"macroseismic",
"rupture start",
"rupture end",
]
OriginUncertaintyDescription = Literal[
"horizontal uncertainty", "uncertainty ellipse", "confidence ellipsoid"
]
PickOnset = Literal["emergent", "impulsive", "questionable"]
PickPolarity = Literal["positive", "negative", "undecidable"]
SourceTimeFunctionType = Literal["box car", "triangle", "trapezoid", "unknown"]
def _to_datetime(dt: Union[datetime, UTCDateTime]) -> datetime:
"""Convert object to datatime."""
return UTCDateTime(dt).datetime
UTCDateTimeFormat = Annotated[UTCDateTime, PlainValidator(_to_datetime)]
# ----- Type Models
class _ObsPyModel(BaseModel):
model_config = ConfigDict(
validate_assignment=True,
arbitrary_types_allowed=True,
from_attributes=True,
extra="ignore",
)
# extra: Optional[AttribDictType] = None
@staticmethod
def _convert_to_obspy(value):
"""Convert an object to obspy or return value."""
if hasattr(value, "to_obspy"):
return value.to_obspy()
return value
def to_obspy(self):
"""Convert to obspy objects."""
name = self.__class__.__name__
cls = getattr(ev, name)
# Note: converting to a dict is deprecated, but we don't want
# to model dump because that is recursive, so we use this
# ugly hack to just get all attributes
out = {}
for i in self.model_fields:
val = getattr(self, i)
if isinstance(val, (list, tuple)):
out[i] = [self._convert_to_obspy(x) for x in val]
else:
out[i] = self._convert_to_obspy(val)
return cls(**out)
[docs]
class ResourceIdentifier(_ObsPyModel):
"""Resource ID"""
id: str = Field(default_factory=lambda: str(uuid4()))
class _ModelWithResourceID(_ObsPyModel):
"""A model which has a resource ID"""
resource_id: ResourceIdentifier = Field(
default_factory=lambda: ResourceIdentifier()
)
[docs]
class QuantityError(_ObsPyModel):
"""Quantity Error"""
uncertainty: Optional[float] = None
lower_uncertainty: Optional[float] = None
upper_uncertainty: Optional[float] = None
confidence_level: Optional[float] = None
[docs]
class CreationInfo(_ObsPyModel):
"""Creation info"""
agency_id: Optional[str] = None
agency_uri: Optional[ResourceIdentifier] = None
author: Optional[str] = None
author_uri: Optional[ResourceIdentifier] = None
creation_time: Optional[UTCDateTimeFormat] = None
version: Optional[str] = None
[docs]
class TimeWindow(_ObsPyModel):
"""Time Window"""
begin: Optional[float] = None
end: Optional[float] = None
reference: Optional[UTCDateTimeFormat] = None
[docs]
class CompositeTime(_ObsPyModel):
"""Composite Time"""
year: Optional[int] = None
year_errors: Optional[QuantityError] = None
month: Optional[int] = None
month_errors: Optional[QuantityError] = None
day: Optional[int] = None
day_errors: Optional[QuantityError] = None
hour: Optional[int] = None
hour_errors: Optional[QuantityError] = None
minute: Optional[int] = None
minute_errors: Optional[QuantityError] = None
second: Optional[float] = None
second_errors: Optional[QuantityError] = None
[docs]
class ConfidenceEllipsoid(_ObsPyModel):
"""Confidence Ellipsoid"""
semi_major_axis_length: Optional[float] = None
semi_minor_axis_length: Optional[float] = None
semi_intermediate_axis_length: Optional[float] = None
major_axis_plunge: Optional[float] = None
major_axis_azimuth: Optional[float] = None
major_axis_rotation: Optional[float] = None
[docs]
class DataUsed(_ObsPyModel):
"""Data Used"""
wave_type: Optional[DataUsedWaveType] = None
station_count: Optional[int] = None
component_count: Optional[int] = None
shortest_period: Optional[float] = None
longest_period: Optional[float] = None
# --- Magnitude classes
[docs]
class StationMagnitude(_ModelWithResourceID):
"""Station Magnitude."""
origin_id: Optional[ResourceIdentifier] = None
mag: Optional[float] = None
mag_errors: Optional[QuantityError] = None
station_magnitude_type: Optional[str] = None
amplitude_id: Optional[ResourceIdentifier] = None
method_id: Optional[ResourceIdentifier] = None
waveform_id: Optional[WaveformStreamID] = None
creation_info: Optional[CreationInfo] = None
comments: List[Comment] = []
[docs]
class StationMagnitudeContribution(_ObsPyModel):
"""Station Magnitude Contribution"""
station_magnitude_id: Optional[ResourceIdentifier] = None
residual: Optional[float] = None
weight: Optional[float] = None
[docs]
class Amplitude(_ModelWithResourceID):
"""Amplitude"""
generic_amplitude: Optional[float] = None
generic_amplitude_errors: Optional[QuantityError] = None
type: Optional[str] = None
category: Optional[AmplitudeCategory] = None
unit: Optional[AmplitudeUnit] = None
method_id: Optional[ResourceIdentifier] = None
period: Optional[float] = None
period_errors: Optional[QuantityError] = None
snr: Optional[float] = None
time_window: Optional[TimeWindow] = None
pick_id: Optional[ResourceIdentifier] = None
waveform_id: Optional[WaveformStreamID] = None
filter_id: Optional[ResourceIdentifier] = None
scaling_time: Optional[UTCDateTimeFormat] = None
scaling_time_errors: Optional[QuantityError] = None
magnitude_hint: Optional[str] = None
evaluation_mode: Optional[EvaluationMode] = None
evaluation_status: Optional[EvaluationStatus] = None
creation_info: Optional[CreationInfo] = None
comments: List[Comment] = []
# --- Origin classes
[docs]
class OriginUncertainty(_ObsPyModel):
"""Origin Uncertainty"""
horizontal_uncertainty: Optional[float] = None
min_horizontal_uncertainty: Optional[float] = None
max_horizontal_uncertainty: Optional[float] = None
azimuth_max_horizontal_uncertainty: Optional[float] = None
confidence_ellipsoid: Optional[ConfidenceEllipsoid] = None
preferred_description: Optional[OriginUncertaintyDescription] = None
confidence_level: Optional[float] = None
[docs]
class OriginQuality(_ObsPyModel):
"""Origin Quality"""
associated_phase_count: Optional[int] = None
used_phase_count: Optional[int] = None
associated_station_count: Optional[int] = None
used_station_count: Optional[int] = None
depth_phase_count: Optional[int] = None
standard_error: Optional[float] = None
azimuthal_gap: Optional[float] = None
secondary_azimuthal_gap: Optional[float] = None
ground_truth_level: Optional[str] = None
minimum_distance: Optional[float] = None
maximum_distance: Optional[float] = None
median_distance: Optional[float] = None
[docs]
class Pick(_ModelWithResourceID):
"""Pick"""
time: Optional[UTCDateTimeFormat] = None
time_errors: Optional[QuantityError] = None
waveform_id: Optional[WaveformStreamID] = None
filter_id: Optional[ResourceIdentifier] = None
method_id: Optional[ResourceIdentifier] = None
horizontal_slowness: Optional[float] = None
horizontal_slowness_errors: Optional[QuantityError] = None
backazimuth: Optional[float] = None
backazimuth_errors: Optional[QuantityError] = None
slowness_method_id: Optional[ResourceIdentifier] = None
onset: Optional[PickOnset] = None
phase_hint: Optional[str] = None
polarity: Optional[PickPolarity] = None
evaluation_mode: Optional[EvaluationMode] = None
evaluation_status: Optional[EvaluationStatus] = None
creation_info: Optional[CreationInfo] = None
comments: List[Comment] = []
[docs]
class Arrival(_ModelWithResourceID):
"""Arrival"""
pick_id: Optional[ResourceIdentifier] = None
phase: Optional[str] = None
time_correction: Optional[float] = None
azimuth: Optional[float] = None
distance: Optional[float] = None
takeoff_angle: Optional[float] = None
takeoff_angle_errors: Optional[QuantityError] = None
time_residual: Optional[float] = None
horizontal_slowness_residual: Optional[float] = None
backazimuth_residual: Optional[float] = None
time_weight: Optional[float] = None
horizontal_slowness_weight: Optional[float] = None
backazimuth_weight: Optional[float] = None
earth_model_id: Optional[ResourceIdentifier] = None
creation_info: Optional[CreationInfo] = None
comments: List[Comment] = []
[docs]
class Origin(_ModelWithResourceID):
"""Origin"""
time: UTCDateTimeFormat
time_errors: Optional[QuantityError] = None
longitude: Optional[float] = None
longitude_errors: Optional[QuantityError] = None
latitude: Optional[float] = None
latitude_errors: Optional[QuantityError] = None
depth: Optional[float] = None
depth_errors: Optional[QuantityError] = None
depth_type: Optional[OriginDepthType] = None
time_fixed: Optional[bool] = None
epicenter_fixed: Optional[bool] = None
reference_system_id: Optional[ResourceIdentifier] = None
method_id: Optional[ResourceIdentifier] = None
earth_model_id: Optional[ResourceIdentifier] = None
quality: Optional[OriginQuality] = None
origin_type: Optional[OriginType] = None
origin_uncertainty: Optional[OriginUncertainty] = None
region: Optional[str] = None
evaluation_mode: Optional[EvaluationMode] = None
evaluation_status: Optional[EvaluationStatus] = None
creation_info: Optional[CreationInfo] = None
comments: List[Comment] = []
arrivals: List[Arrival] = []
composite_times: List[CompositeTime] = []
[docs]
class Magnitude(_ModelWithResourceID):
"""Magnitude"""
mag: Optional[float] = None
mag_errors: Optional[QuantityError] = None
magnitude_type: Optional[str] = None
origin_id: Optional[ResourceIdentifier] = None
method_id: Optional[ResourceIdentifier] = None
station_count: Optional[int] = None
azimuthal_gap: Optional[float] = None
evaluation_mode: Optional[EvaluationMode] = None
evaluation_status: Optional[EvaluationStatus] = None
creation_info: Optional[CreationInfo] = None
comments: List[Comment] = []
station_magnitude_contributions: List[StationMagnitudeContribution] = []
# --- Source objects
[docs]
class Axis(_ObsPyModel):
"""Axis"""
azimuth: Optional[float] = None
plunge: Optional[float] = None
length: Optional[float] = None
[docs]
class NodalPlane(_ObsPyModel):
"""Nodal Plane"""
strike: Optional[float] = None
dip: Optional[float] = None
rake: Optional[float] = None
[docs]
class NodalPlanes(_ObsPyModel):
"""Nodal Planes"""
nodal_plane_1: Optional[NodalPlane] = None
nodal_plane_2: Optional[NodalPlane] = None
preferred_plane: Optional[int] = None
[docs]
class PrincipalAxes(_ObsPyModel):
"""Principal Axes"""
t_axis: Optional[Axis] = None
p_axis: Optional[Axis] = None
n_axis: Optional[Axis] = None
[docs]
class Tensor(_ObsPyModel):
"""Tensor"""
m_rr: Optional[float] = None
m_rr_errors: Optional[QuantityError] = None
m_tt: Optional[float] = None
m_tt_errors: Optional[QuantityError] = None
m_pp: Optional[float] = None
m_pp_errors: Optional[QuantityError] = None
m_rt: Optional[float] = None
m_rt_errors: Optional[QuantityError] = None
m_rp: Optional[float] = None
m_rp_errors: Optional[QuantityError] = None
m_tp: Optional[float] = None
m_tp_errors: Optional[QuantityError] = None
[docs]
class SourceTimeFunction(_ObsPyModel):
"""Source Time Function"""
type: Optional[SourceTimeFunctionType] = None
duration: Optional[float] = None
rise_time: Optional[float] = None
decay_time: Optional[float] = None
[docs]
class MomentTensor(_ModelWithResourceID):
"""Moment Tensor"""
derived_origin_id: Optional[ResourceIdentifier] = None
moment_magnitude_id: Optional[ResourceIdentifier] = None
scalar_moment: Optional[float] = None
scalar_moment_errors: Optional[QuantityError] = None
tensor: Optional[Tensor] = None
variance: Optional[float] = None
variance_reduction: Optional[float] = None
double_couple: Optional[float] = None
clvd: Optional[float] = None
iso: Optional[float] = None
greens_function_id: Optional[float] = None
filter_id: Optional[ResourceIdentifier] = None
source_time_function: Optional[SourceTimeFunction] = None
data_used: Optional[List[DataUsed]] = None
method_id: Optional[ResourceIdentifier] = None
category: Optional[MomentTensorCategory] = None
inversion_type: Optional[MTInversionType] = None
creation_info: Optional[CreationInfo] = None
[docs]
class FocalMechanism(_ModelWithResourceID):
"""Focal Mechanism"""
triggering_origin_id: Optional[ResourceIdentifier] = None
nodal_planes: Optional[NodalPlanes] = None
principal_axes: Optional[PrincipalAxes] = None
azimuthal_gap: Optional[float] = None
station_polarity_count: Optional[int] = None
misfit: Optional[float] = None
station_distribution_ratio: Optional[float] = None
method_id: Optional[ResourceIdentifier] = None
evaluation_mode: Optional[EvaluationMode] = None
evaluation_status: Optional[EvaluationStatus] = None
moment_tensor: Optional[MomentTensor] = None
creation_info: Optional[CreationInfo] = None
waveform_id: List[WaveformStreamID] = []
comments: List[Comment] = []
# --- Event definitions
[docs]
class EventDescription(_ObsPyModel):
"""Event Description"""
text: Optional[str] = None
type: Optional[EventDescriptionType] = None
[docs]
class Event(_ModelWithResourceID):
"""Event"""
event_type: Optional[EventType] = None
event_type_certainty: Optional[EventTypeCertainty] = None
creation_info: Optional[CreationInfo] = None
preferred_origin_id: Optional[ResourceIdentifier] = None
preferred_magnitude_id: Optional[ResourceIdentifier] = None
preferred_focal_mechanism_id: Optional[ResourceIdentifier] = None
event_descriptions: List[EventDescription] = []
comments: List[Comment] = []
picks: List[Pick] = []
amplitudes: List[Amplitude] = []
focal_mechanisms: List[FocalMechanism] = []
origins: List[Origin] = []
magnitudes: List[Magnitude] = []
station_magnitudes: List[StationMagnitude] = []
[docs]
def to_obspy(self):
"""convert the catalog to obspy form"""
out = super().to_obspy()
out.scope_resource_ids()
return out
[docs]
class Catalog(_ModelWithResourceID):
"""A collection of events."""
events: List[Event] = []
description: Optional[str] = None
comments: Optional[List[Comment]] = None
creation_info: Optional[CreationInfo] = None