{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Miscellaneous Utilities\n",
"The follow demonstrates some miscellaneous utilities included in ObsPlus.\n",
"\n",
"## Geodetics\n",
"It is often necessary to calculate geometric parameters (distance, azimuth, etc.) for pairs of entities in two different groups. For example, distance from each event in a catalog to each receiver in an inventory. ObsPlus provides a simple class for completing this task."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:15.377555Z",
"iopub.status.busy": "2026-03-20T08:39:15.377386Z",
"iopub.status.idle": "2026-03-20T08:39:17.165811Z",
"shell.execute_reply": "2026-03-20T08:39:17.164944Z"
}
},
"outputs": [],
"source": [
"# Load the catalog and inventory from the crandall dataset\n",
"import obsplus\n",
"from obsplus.utils.geodetics import SpatialCalculator\n",
"\n",
"crandall = obsplus.load_dataset(\"crandall_test\")\n",
"cat = crandall.event_client.get_events()\n",
"inv = crandall.station_client.get_stations()"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.167816Z",
"iopub.status.busy": "2026-03-20T08:39:17.167614Z",
"iopub.status.idle": "2026-03-20T08:39:17.170460Z",
"shell.execute_reply": "2026-03-20T08:39:17.169776Z"
}
},
"outputs": [],
"source": [
"# init a SpatialCalculator instance (defaults to Earth's params)\n",
"spatial_calc = SpatialCalculator()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.172109Z",
"iopub.status.busy": "2026-03-20T08:39:17.171945Z",
"iopub.status.idle": "2026-03-20T08:39:17.299214Z",
"shell.execute_reply": "2026-03-20T08:39:17.298205Z"
}
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/runner/micromamba/envs/test/lib/python3.13/site-packages/obspy/taup/taup_geo.py:105: UserWarning: Assuming spherical planet when calculating epicentral distance. Install the Python module 'geographiclib' to solve this.\n",
" warnings.warn(msg)\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" | \n",
" distance_m | \n",
" azimuth | \n",
" back_azimuth | \n",
" distance_degrees | \n",
" vertical_distance_m | \n",
"
\n",
" \n",
" | id1 | \n",
" id2 | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | smi:local/248925 | \n",
" TA.P17A..BHE | \n",
" 41321.405319 | \n",
" 268.566729 | \n",
" 88.261617 | \n",
" 0.371196 | \n",
" 6307.0 | \n",
"
\n",
" \n",
" | smi:local/248883 | \n",
" TA.P17A..BHE | \n",
" 41577.826856 | \n",
" 268.653991 | \n",
" 88.346971 | \n",
" 0.373500 | \n",
" 5867.0 | \n",
"
\n",
" \n",
" | smi:local/248882 | \n",
" TA.P17A..BHE | \n",
" 41788.158988 | \n",
" 268.905967 | \n",
" 88.597352 | \n",
" 0.375389 | \n",
" 3477.0 | \n",
"
\n",
" \n",
" | smi:local/248891 | \n",
" TA.P17A..BHE | \n",
" 41690.761121 | \n",
" 268.246283 | \n",
" 87.938508 | \n",
" 0.374514 | \n",
" 4927.0 | \n",
"
\n",
" \n",
" | smi:local/248887 | \n",
" TA.P17A..BHE | \n",
" 40909.197252 | \n",
" 268.518109 | \n",
" 88.216048 | \n",
" 0.367494 | \n",
" 5847.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" distance_m azimuth back_azimuth \\\n",
"id1 id2 \n",
"smi:local/248925 TA.P17A..BHE 41321.405319 268.566729 88.261617 \n",
"smi:local/248883 TA.P17A..BHE 41577.826856 268.653991 88.346971 \n",
"smi:local/248882 TA.P17A..BHE 41788.158988 268.905967 88.597352 \n",
"smi:local/248891 TA.P17A..BHE 41690.761121 268.246283 87.938508 \n",
"smi:local/248887 TA.P17A..BHE 40909.197252 268.518109 88.216048 \n",
"\n",
" distance_degrees vertical_distance_m \n",
"id1 id2 \n",
"smi:local/248925 TA.P17A..BHE 0.371196 6307.0 \n",
"smi:local/248883 TA.P17A..BHE 0.373500 5867.0 \n",
"smi:local/248882 TA.P17A..BHE 0.375389 3477.0 \n",
"smi:local/248891 TA.P17A..BHE 0.374514 4927.0 \n",
"smi:local/248887 TA.P17A..BHE 0.367494 5847.0 "
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# create distance dataframe\n",
"df = spatial_calc(entity_1=cat, entity_2=inv)\n",
"df.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Since a multi-index is used it provides a fairly intuitive way to look up particular event-channel pairs using a tuple of (event_id, seed_id) in conjunction with the `.loc` DataFrame attribute like so:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.329176Z",
"iopub.status.busy": "2026-03-20T08:39:17.328918Z",
"iopub.status.idle": "2026-03-20T08:39:17.332320Z",
"shell.execute_reply": "2026-03-20T08:39:17.331426Z"
}
},
"outputs": [],
"source": [
"event_id = str(cat[0].resource_id)\n",
"seed_id = \"UU.MPU..HHZ\"\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.335391Z",
"iopub.status.busy": "2026-03-20T08:39:17.335212Z",
"iopub.status.idle": "2026-03-20T08:39:17.337813Z",
"shell.execute_reply": "2026-03-20T08:39:17.337155Z"
}
},
"outputs": [],
"source": [
"# or just get a particular parameter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The distances can be converted to km and the distrtibutions can be described."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.339929Z",
"iopub.status.busy": "2026-03-20T08:39:17.339756Z",
"iopub.status.idle": "2026-03-20T08:39:17.358075Z",
"shell.execute_reply": "2026-03-20T08:39:17.357261Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" azimuth | \n",
" back_azimuth | \n",
" distance_degrees | \n",
" distance_km | \n",
" vertical_distance_km | \n",
"
\n",
" \n",
" \n",
" \n",
" | count | \n",
" 408.00 | \n",
" 408.00 | \n",
" 408.00 | \n",
" 408.00 | \n",
" 408.00 | \n",
"
\n",
" \n",
" | mean | \n",
" 201.06 | \n",
" 211.69 | \n",
" 0.82 | \n",
" 91.24 | \n",
" 5.22 | \n",
"
\n",
" \n",
" | std | \n",
" 104.37 | \n",
" 98.06 | \n",
" 0.33 | \n",
" 36.55 | \n",
" 1.85 | \n",
"
\n",
" \n",
" | min | \n",
" 9.16 | \n",
" 48.81 | \n",
" 0.16 | \n",
" 18.26 | \n",
" 1.70 | \n",
"
\n",
" \n",
" | 25% | \n",
" 126.47 | \n",
" 122.45 | \n",
" 0.63 | \n",
" 70.46 | \n",
" 3.69 | \n",
"
\n",
" \n",
" | 50% | \n",
" 163.87 | \n",
" 189.79 | \n",
" 0.82 | \n",
" 90.85 | \n",
" 5.50 | \n",
"
\n",
" \n",
" | 75% | \n",
" 293.44 | \n",
" 307.27 | \n",
" 1.12 | \n",
" 124.36 | \n",
" 6.25 | \n",
"
\n",
" \n",
" | max | \n",
" 358.12 | \n",
" 344.77 | \n",
" 1.29 | \n",
" 144.00 | \n",
" 9.31 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" azimuth back_azimuth distance_degrees distance_km \\\n",
"count 408.00 408.00 408.00 408.00 \n",
"mean 201.06 211.69 0.82 91.24 \n",
"std 104.37 98.06 0.33 36.55 \n",
"min 9.16 48.81 0.16 18.26 \n",
"25% 126.47 122.45 0.63 70.46 \n",
"50% 163.87 189.79 0.82 90.85 \n",
"75% 293.44 307.27 1.12 124.36 \n",
"max 358.12 344.77 1.29 144.00 \n",
"\n",
" vertical_distance_km \n",
"count 408.00 \n",
"mean 5.22 \n",
"std 1.85 \n",
"min 1.70 \n",
"25% 3.69 \n",
"50% 5.50 \n",
"75% 6.25 \n",
"max 9.31 "
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Convert add km columns and delete m columns\n",
"m_columns = [x for x in df.columns if x.endswith(\"_m\")]\n",
"km_columns = [x.replace(\"_m\", \"_km\") for x in m_columns]\n",
"\n",
"df_km = df.assign(**{x: df[y] / 1000.0 for x, y in zip(km_columns, m_columns)}).drop(\n",
" columns=m_columns\n",
")\n",
"\n",
"# Calculate stats for source reseiver distances\n",
"df_km.describe().round(decimals=2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Time\n",
"\n",
"Working with dates/times can be frustrating, especially since (as of 2020) ObsPy, numpy, and pandas all use slightly different methods for working with time. ObsPlus provides some utilities to make things a little easier."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.360032Z",
"iopub.status.busy": "2026-03-20T08:39:17.359850Z",
"iopub.status.idle": "2026-03-20T08:39:17.362752Z",
"shell.execute_reply": "2026-03-20T08:39:17.361963Z"
}
},
"outputs": [],
"source": [
"\n",
"import numpy as np\n",
"import obspy\n",
"\n",
"import obsplus\n",
"from obsplus.utils import to_datetime64, to_timedelta64, to_utc"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Note:** All ObsPlus datafames use numpy/pandas datatypes. "
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.364450Z",
"iopub.status.busy": "2026-03-20T08:39:17.364280Z",
"iopub.status.idle": "2026-03-20T08:39:17.412822Z",
"shell.execute_reply": "2026-03-20T08:39:17.411883Z"
}
},
"outputs": [],
"source": [
"df = obsplus.events_to_df(obspy.read_events())"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.414682Z",
"iopub.status.busy": "2026-03-20T08:39:17.414489Z",
"iopub.status.idle": "2026-03-20T08:39:17.418933Z",
"shell.execute_reply": "2026-03-20T08:39:17.418241Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"0 2012-04-04 14:21:42.300\n",
"1 2012-04-04 14:18:37.000\n",
"2 2012-04-04 14:08:46.000\n",
"Name: time, dtype: datetime64[ns]"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df[\"time\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Numpy and ObsPy time differences"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One difference between numpy's `datatime64` and ObsPy's `UTCDateTime` is how offsets are applied. For ObsPy, numbers are simply taken as seconds, but numpy requires explicitly using `timedeta64` instances."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.420928Z",
"iopub.status.busy": "2026-03-20T08:39:17.420764Z",
"iopub.status.idle": "2026-03-20T08:39:17.424199Z",
"shell.execute_reply": "2026-03-20T08:39:17.423397Z"
}
},
"outputs": [],
"source": [
"time_str = \"2020-01-03T11:00:00\"\n",
"utc = obspy.UTCDateTime(time_str)\n",
"dt64 = np.datetime64(time_str, \"ns\")"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.425826Z",
"iopub.status.busy": "2026-03-20T08:39:17.425662Z",
"iopub.status.idle": "2026-03-20T08:39:17.428415Z",
"shell.execute_reply": "2026-03-20T08:39:17.427573Z"
}
},
"outputs": [],
"source": [
"# add 1/2 second to UTCDateTime\n",
"utc2 = utc + 0.5"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.429898Z",
"iopub.status.busy": "2026-03-20T08:39:17.429742Z",
"iopub.status.idle": "2026-03-20T08:39:17.432457Z",
"shell.execute_reply": "2026-03-20T08:39:17.431815Z"
}
},
"outputs": [],
"source": [
"# however doing the same thing with datetime64 raises a TypeError\n",
"try:\n",
" dt64 + 0.5\n",
"except TypeError:\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.434243Z",
"iopub.status.busy": "2026-03-20T08:39:17.434067Z",
"iopub.status.idle": "2026-03-20T08:39:17.436693Z",
"shell.execute_reply": "2026-03-20T08:39:17.436070Z"
}
},
"outputs": [],
"source": [
"# so you need to use a timedelta64\n",
"dt64_2 = dt64 + np.timedelta64(500, \"ms\")"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.438499Z",
"iopub.status.busy": "2026-03-20T08:39:17.438321Z",
"iopub.status.idle": "2026-03-20T08:39:17.443025Z",
"shell.execute_reply": "2026-03-20T08:39:17.442293Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"0 2012-04-04 14:21:42.800\n",
"1 2012-04-04 14:18:37.500\n",
"2 2012-04-04 14:08:46.500\n",
"Name: time, dtype: datetime64[ns]"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# This is, of course, also the case with datetime64 columns\n",
"df[\"time\"] + np.timedelta64(500, \"ms\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ObsPlus provides some common converters between ObsPy and Pandas/Numpy time objects."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Converting between ObsPy and Numpy time datatypes\n",
"Conversion between ObsPy `UTCDateTime` and numpy `datetime64` objects when needed."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.444801Z",
"iopub.status.busy": "2026-03-20T08:39:17.444620Z",
"iopub.status.idle": "2026-03-20T08:39:17.447945Z",
"shell.execute_reply": "2026-03-20T08:39:17.447278Z"
}
},
"outputs": [],
"source": [
"# Convert a time column to an array of ObsPy UTCDateTime objects\n",
"utc_array = to_utc(df[\"time\"])"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.449883Z",
"iopub.status.busy": "2026-03-20T08:39:17.449702Z",
"iopub.status.idle": "2026-03-20T08:39:17.453011Z",
"shell.execute_reply": "2026-03-20T08:39:17.452225Z"
}
},
"outputs": [],
"source": [
"# Convert back to datetime64\n",
"dt64_array = to_datetime64(utc_array)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"**Note**: `datetime64` arrays are much more efficient in terms of memory usage and computational efficiency than arrays of `UTCDateTime` *objects*.\n",
"\n",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you prefer not to manually define `timedelta64` to perform offsets, `to_timedelta64` simply converts a real number to an offset in seconds."
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"execution": {
"iopub.execute_input": "2026-03-20T08:39:17.454675Z",
"iopub.status.busy": "2026-03-20T08:39:17.454513Z",
"iopub.status.idle": "2026-03-20T08:39:17.458434Z",
"shell.execute_reply": "2026-03-20T08:39:17.457640Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"np.timedelta64(3255000000,'ns')"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"to_timedelta64(3.255)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.12"
}
},
"nbformat": 4,
"nbformat_minor": 4
}