|
|
|
@@ -1,9 +1,260 @@
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
from eqdist.rate import datenum_to_datetime
|
|
|
|
|
import Rbeast as rb;
|
|
|
|
|
from scipy.stats import bootstrap
|
|
|
|
|
from matplotlib.dates import DateFormatter, AutoDateLocator
|
|
|
|
|
from matplotlib.ticker import MultipleLocator
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
global ncp_choice, tcp_max, torder_min, torder_max
|
|
|
|
|
ncp_choice = 'default'
|
|
|
|
|
tcp_max = 5
|
|
|
|
|
torder_min = 0
|
|
|
|
|
torder_max = 1
|
|
|
|
|
#AOI_lat = np.array([51.48, 51.54])
|
|
|
|
|
#AOI_lon = np.array([16.15, 16.24])
|
|
|
|
|
#AOI_lat = np.array([None, None])
|
|
|
|
|
#AOI_lon = np.array([None, None])
|
|
|
|
|
|
|
|
|
|
def plot_results(act_rate, bin_edges, bin_edges_dt, rt, boundaries,
|
|
|
|
|
bin_dur, unit, multiplicator,
|
|
|
|
|
rate_forecast, rate_unc_high, rate_unc_low,
|
|
|
|
|
datenum_data, mag_data):
|
|
|
|
|
|
|
|
|
|
end_date = bin_edges[-1]
|
|
|
|
|
|
|
|
|
|
fig, ax = plt.subplots(figsize=(14, 6))
|
|
|
|
|
ax.plot(bin_edges_dt[1:], act_rate, '-o', linewidth=2.5, markersize=6.5, label='Activity rate')
|
|
|
|
|
|
|
|
|
|
if rate_forecast is not None:
|
|
|
|
|
next_date = end_date + (bin_dur / multiplicator)
|
|
|
|
|
ax.plot(datenum_to_datetime(next_date), rate_forecast,
|
|
|
|
|
'ro', label='Forecasted Rate', markersize=6.5)
|
|
|
|
|
ax.plot([bin_edges_dt[-1], datenum_to_datetime(next_date)], [act_rate[-1], rate_forecast], 'r-', linewidth=2.5)
|
|
|
|
|
ax.vlines(datenum_to_datetime(next_date), rate_unc_low, rate_unc_high, colors='r',
|
|
|
|
|
linewidth=2, label='Bootstrap uncertainty')
|
|
|
|
|
|
|
|
|
|
ax.xaxis.set_major_locator(AutoDateLocator())
|
|
|
|
|
ax.xaxis.set_major_formatter(DateFormatter('%d-%b-%Y'))
|
|
|
|
|
plt.xticks(rotation=45)
|
|
|
|
|
plt.title(f'Activity rate (Time Unit: {unit}, Bin Duration: {bin_dur} {unit})',fontsize=18)
|
|
|
|
|
# plt.title(f'Activity rate (Bin Duration: {bin_dur} {unit})',fontsize=18)
|
|
|
|
|
plt.xlabel('Time (Bin Center Date)', fontsize=16)
|
|
|
|
|
ax.set_ylabel('Activity rate per selected time period',fontsize=16)
|
|
|
|
|
plt.grid(True)
|
|
|
|
|
|
|
|
|
|
if len(rt) > 0:
|
|
|
|
|
for i in range(len(rt)):
|
|
|
|
|
ax.plot(bin_edges_dt[1:][boundaries[i]:boundaries[i+1]],
|
|
|
|
|
[rt[i]] * (boundaries[i+1] - boundaries[i]),
|
|
|
|
|
linewidth=2, label=f'Rate period {i+1}')
|
|
|
|
|
|
|
|
|
|
# ---- Magnitude scatter on right y-axis ----
|
|
|
|
|
ax2 = ax.twinx()
|
|
|
|
|
event_dates = [datenum_to_datetime(d) for d in datenum_data]
|
|
|
|
|
|
|
|
|
|
#-------------extract magnitude bins from data---------------------
|
|
|
|
|
mags = np.array(mag_data)
|
|
|
|
|
min_mag = mags.min()
|
|
|
|
|
max_mag = mags.max()
|
|
|
|
|
|
|
|
|
|
low_thresh = int(np.floor(min_mag))
|
|
|
|
|
high_thresh = int(np.floor(max_mag))
|
|
|
|
|
|
|
|
|
|
thresholds = list(range(low_thresh, high_thresh + 1))
|
|
|
|
|
|
|
|
|
|
base_size = 15
|
|
|
|
|
size_step = 35
|
|
|
|
|
|
|
|
|
|
bins_def = []
|
|
|
|
|
for idx, t in enumerate(thresholds):
|
|
|
|
|
low = t
|
|
|
|
|
if idx < len(thresholds) - 1:
|
|
|
|
|
high = thresholds[idx + 1]
|
|
|
|
|
label = f'{low:.1f} \u2264 M < {high:.1f}'
|
|
|
|
|
else:
|
|
|
|
|
high = np.inf
|
|
|
|
|
label = f'M \u2265 {low:.1f}'
|
|
|
|
|
size = base_size + idx * size_step
|
|
|
|
|
bins_def.append((low, high, size, label))
|
|
|
|
|
|
|
|
|
|
for low, high, size, label in bins_def:
|
|
|
|
|
mask = (mags >= low) & (mags < high)
|
|
|
|
|
if np.any(mask):
|
|
|
|
|
sel_dates = [d for d, m in zip(event_dates, mask) if m]
|
|
|
|
|
sel_mags = mags[mask]
|
|
|
|
|
ax2.scatter(sel_dates, sel_mags, s=size,
|
|
|
|
|
facecolor='purple', edgecolor='black',
|
|
|
|
|
alpha=0.15, linewidth=1, label=label)
|
|
|
|
|
|
|
|
|
|
ax2.set_ylabel('Magnitude', color='purple',fontsize=16)
|
|
|
|
|
ax2.yaxis.set_major_locator(MultipleLocator(0.5))
|
|
|
|
|
ax2.yaxis.set_minor_locator(MultipleLocator(0.1))
|
|
|
|
|
ax2.spines['right'].set_color('purple')
|
|
|
|
|
ax2.tick_params(axis='y', colors='purple')
|
|
|
|
|
|
|
|
|
|
h1, l1 = ax.get_legend_handles_labels()
|
|
|
|
|
h2, l2 = ax2.get_legend_handles_labels()
|
|
|
|
|
handles = h1 + h2
|
|
|
|
|
labels = l1 + l2
|
|
|
|
|
n_legend = len(handles)
|
|
|
|
|
|
|
|
|
|
ncols = max(1, int(np.ceil(n_legend / 5))) # ~5 entries per column
|
|
|
|
|
|
|
|
|
|
#-------add 20% headroom above the data to make space for legend------
|
|
|
|
|
ymin, ymax = ax.get_ylim()
|
|
|
|
|
ax.set_ylim(ymin, ymax * 1.20)
|
|
|
|
|
|
|
|
|
|
ax.legend(handles, labels, loc='best',
|
|
|
|
|
ncol=ncols, borderaxespad=0,framealpha=0.7)
|
|
|
|
|
|
|
|
|
|
ax.set_zorder(ax2.get_zorder() + 1) # put scatter plot behind the line plot
|
|
|
|
|
ax.patch.set_visible(False)
|
|
|
|
|
|
|
|
|
|
fig.tight_layout()
|
|
|
|
|
plt.savefig("activity_rate.png", dpi=600)
|
|
|
|
|
plt.show()
|
|
|
|
|
|
|
|
|
|
def bootstrap_forecast(data):
|
|
|
|
|
|
|
|
|
|
window_data=data
|
|
|
|
|
|
|
|
|
|
if len(window_data) >= 5:
|
|
|
|
|
res95 = bootstrap((window_data,), np.mean, confidence_level=0.95,
|
|
|
|
|
method='BCa', n_resamples=1000)
|
|
|
|
|
else:
|
|
|
|
|
res95 = bootstrap((window_data,), np.mean, confidence_level=0.95,
|
|
|
|
|
method='BCa', n_resamples=int(len(window_data) ** len(window_data)))
|
|
|
|
|
|
|
|
|
|
forecast = np.mean(res95.bootstrap_distribution)
|
|
|
|
|
bca_conf95 = res95.confidence_interval
|
|
|
|
|
return forecast, bca_conf95
|
|
|
|
|
|
|
|
|
|
def calc_rates(act_rate, cps):
|
|
|
|
|
"""
|
|
|
|
|
Calculates mean activity rates between changepoints.
|
|
|
|
|
cps : sorted array of changepoint indices into act_rate
|
|
|
|
|
Returns rt (list of rates) and segment boundaries
|
|
|
|
|
"""
|
|
|
|
|
boundaries = [0] + list(cps.astype(int)) + [len(act_rate)]
|
|
|
|
|
rt = [np.mean(act_rate[boundaries[i]:boundaries[i+1]])
|
|
|
|
|
for i in range(len(boundaries)-1)]
|
|
|
|
|
return rt, boundaries
|
|
|
|
|
|
|
|
|
|
def apply_beast(act_rate):
|
|
|
|
|
"""
|
|
|
|
|
Applies BEAST to the smmothed rate data using different smoothing windows.
|
|
|
|
|
Input
|
|
|
|
|
act_rate : The activity rate data array to smooth and apply BEAST.
|
|
|
|
|
Output
|
|
|
|
|
out : A list of BEAST results for each smoothed rate array.
|
|
|
|
|
prob : A list of probabilities and change points extracted from BEAST results.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
mirror_len = int(np.ceil(0.20 * len(act_rate)))
|
|
|
|
|
left_mirror = act_rate[:mirror_len][::-1]
|
|
|
|
|
right_mirror = act_rate[-mirror_len:][::-1]
|
|
|
|
|
act_rate_mirrored = np.concatenate([left_mirror, act_rate, right_mirror])
|
|
|
|
|
|
|
|
|
|
mcmc_th = int(np.clip(np.ceil(len(act_rate) / 100), 2, 15))
|
|
|
|
|
beast_result = rb.beast(act_rate_mirrored, period=0,
|
|
|
|
|
tcp_minmax=[0, tcp_max],
|
|
|
|
|
torder_minmax=[torder_min, torder_max],
|
|
|
|
|
tseg_minlength=2, mcmc_chains=10,
|
|
|
|
|
mcmc_thin=mcmc_th, mcmc_seed=10)
|
|
|
|
|
|
|
|
|
|
# User-driven ncp selection
|
|
|
|
|
if ncp_choice == 'median':
|
|
|
|
|
ncp = beast_result.trend.ncp_median
|
|
|
|
|
if np.isnan(ncp) or ncp == 0:
|
|
|
|
|
return beast_result, np.array([])
|
|
|
|
|
elif ncp_choice == 'mode':
|
|
|
|
|
ncp = beast_result.trend.ncp_mode
|
|
|
|
|
if np.isnan(ncp) or ncp == 0:
|
|
|
|
|
return beast_result, np.array([])
|
|
|
|
|
elif ncp_choice == 'pct90':
|
|
|
|
|
ncp = beast_result.trend.ncp_pct90
|
|
|
|
|
if np.isnan(ncp) or ncp == 0:
|
|
|
|
|
return beast_result, np.array([])
|
|
|
|
|
else: # default: median with mode and pct90 fallback
|
|
|
|
|
ncp = beast_result.trend.ncp_median
|
|
|
|
|
if np.isnan(ncp) or ncp == 0:
|
|
|
|
|
ncp = beast_result.trend.mode
|
|
|
|
|
if np.isnan(ncp) or ncp == 0:
|
|
|
|
|
ncp = beast_result.trend.ncp_pct90
|
|
|
|
|
if np.isnan(ncp) or ncp == 0:
|
|
|
|
|
return beast_result, np.array([])
|
|
|
|
|
|
|
|
|
|
ncp = int(ncp)
|
|
|
|
|
cps = beast_result.trend.cp[:ncp]
|
|
|
|
|
|
|
|
|
|
# Discard mirrored zone changepoints and correct indices
|
|
|
|
|
valid_mask = (cps > mirror_len) & (cps <= mirror_len + len(act_rate))
|
|
|
|
|
cps = cps[valid_mask] - mirror_len
|
|
|
|
|
|
|
|
|
|
return beast_result, np.sort(cps)
|
|
|
|
|
|
|
|
|
|
def bins_and_beast(dates, unit, bin_dur, multiplicator):
|
|
|
|
|
start_date = dates.min()
|
|
|
|
|
end_date = dates.max()
|
|
|
|
|
|
|
|
|
|
valid_units = ['hours', 'days']
|
|
|
|
|
if unit not in valid_units:
|
|
|
|
|
unit = 'days'
|
|
|
|
|
bin_dur = 15
|
|
|
|
|
if (end_date - start_date) < 15 and unit == 'days':
|
|
|
|
|
unit = 'hours'
|
|
|
|
|
bin_dur = 12
|
|
|
|
|
|
|
|
|
|
bin_edges = [end_date]
|
|
|
|
|
while bin_edges[-1] > start_date:
|
|
|
|
|
bin_edges.append(bin_edges[-1] - (bin_dur / multiplicator))
|
|
|
|
|
bin_edges = bin_edges[::-1]
|
|
|
|
|
|
|
|
|
|
#-------Drop first bin or keep it if >80% of set duration------
|
|
|
|
|
first_width_days = bin_edges[1] - start_date
|
|
|
|
|
first_width_units = first_width_days * multiplicator
|
|
|
|
|
|
|
|
|
|
if first_width_units >= 0.8 * bin_dur:
|
|
|
|
|
bin_edges[0] = start_date # edge of first bin is at data start
|
|
|
|
|
else:
|
|
|
|
|
bin_edges = bin_edges[1:] # drop bin 0 (and its events)
|
|
|
|
|
|
|
|
|
|
#------------Error if remaining bins are fewer than 2------------
|
|
|
|
|
if len(bin_edges) < 2:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Not enough data to form at least one full bin of duration "
|
|
|
|
|
f"{bin_dur} {unit}(s) after dropping the partial first bin "
|
|
|
|
|
f"({first_width_units:.2f} {unit}(s), below the 80% threshold). "
|
|
|
|
|
f"Try a shorter bin_dur or check your input data range."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
bin_edges_dt = [datenum_to_datetime(d) for d in bin_edges]
|
|
|
|
|
bin_counts, _ = np.histogram(dates, bins=bin_edges)
|
|
|
|
|
|
|
|
|
|
act_rate = [count / ((bin_edges[i + 1] - bin_edges[i]) * multiplicator / bin_dur)
|
|
|
|
|
for i, count in enumerate(bin_counts)]
|
|
|
|
|
|
|
|
|
|
out, cps = apply_beast(act_rate)
|
|
|
|
|
|
|
|
|
|
if len(cps) > 0:
|
|
|
|
|
rt, boundaries = calc_rates(act_rate, cps)
|
|
|
|
|
print(f'Changepoints detected at bins: {cps}')
|
|
|
|
|
else:
|
|
|
|
|
rt = []
|
|
|
|
|
boundaries = []
|
|
|
|
|
print('-----------------------------------------------------')
|
|
|
|
|
print('No changepoints detected by BEAST (Zhao et al., 2019)')
|
|
|
|
|
print('-----------------------------------------------------')
|
|
|
|
|
|
|
|
|
|
return act_rate, bin_counts, bin_edges, bin_edges_dt, out, cps, rt, boundaries, bin_dur, unit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max,
|
|
|
|
|
m_kde_method, xy_select, grid_dim, xy_win_method, rate_select, time_win_duration,
|
|
|
|
|
forecast_select, custom_rate, forecast_len, time_unit, model, products_string, verbose):
|
|
|
|
|
# forecast_select, custom_rate, forecast_len, time_unit, model, products_string, verbose):
|
|
|
|
|
forecast_select, custom_rate, forecast_len, time_unit, AOI_extent, model, products_string, verbose):
|
|
|
|
|
"""
|
|
|
|
|
Python application that reads an earthquake catalog and performs seismic hazard forecasting.
|
|
|
|
|
Arguments:
|
|
|
|
@@ -33,6 +284,8 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
|
|
|
|
|
forecasting.
|
|
|
|
|
forecast_len: Length of the forecast for seismic hazard assessment.
|
|
|
|
|
time_unit: Times units for the inputs Time Window Duration, Custom Activity Rate, and Forecast Length.
|
|
|
|
|
AOI_extent: The forecast geographical area of interest specified as a latitude and longitude range in decimal degrees
|
|
|
|
|
in the form [lat_min, lat_max, lon_min, lon_max].
|
|
|
|
|
model: Select from the following ground motion models available. Other models in the Openquake library are
|
|
|
|
|
available but have not yet been tested.
|
|
|
|
|
products_string: The ground motion intensity types to output. Use a space between names to select more than
|
|
|
|
@@ -52,7 +305,6 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
|
|
|
|
|
from math import ceil, floor, isnan
|
|
|
|
|
import numpy as np
|
|
|
|
|
import dask
|
|
|
|
|
from dask.diagnostics import ProgressBar # use Dask progress bar
|
|
|
|
|
import kalepy as kale
|
|
|
|
|
import utm
|
|
|
|
|
from skimage.transform import resize
|
|
|
|
@@ -69,6 +321,7 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
|
|
|
|
|
from matplotlib.contour import ContourSet
|
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
import json
|
|
|
|
|
import multiprocessing as mp
|
|
|
|
|
|
|
|
|
|
logger = getDefaultLogger('igfash')
|
|
|
|
|
|
|
|
|
@@ -88,11 +341,12 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
|
|
|
|
|
else:
|
|
|
|
|
logger.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
|
|
# temporary hard-coded configuration
|
|
|
|
|
# exclude_low_fxy = False
|
|
|
|
|
exclude_low_fxy = True
|
|
|
|
|
exclude_low_fxy = False # skip low probability areas of the map
|
|
|
|
|
thresh_fxy = 1e-3 # minimum fxy value (location PDF) needed to do PGA estimation (to skip low probability areas); also should scale according to number of grid points
|
|
|
|
|
|
|
|
|
|
AOI_lat = np.array(AOI_extent[:2])
|
|
|
|
|
AOI_lon = np.array(AOI_extent[2:])
|
|
|
|
|
|
|
|
|
|
# log user selections
|
|
|
|
|
logger.debug(f"User input files\n Catalog: {catalog_file}\n Mc: {mc_file}\n Mag_PDF: {pdf_file}\n Mag: {m_file}")
|
|
|
|
|
logger.debug(
|
|
|
|
@@ -125,10 +379,6 @@ verbose: {verbose}")
|
|
|
|
|
logger.info("No magnitude label of catalog specified, therefore try Mw by default")
|
|
|
|
|
mag_label = 'Mw'
|
|
|
|
|
|
|
|
|
|
# if cat_label == None:
|
|
|
|
|
# print("No magnitude label of catalog specified, therefore try 'Catalog' by default")
|
|
|
|
|
# cat_label='Catalog'
|
|
|
|
|
|
|
|
|
|
time, mag, lat, lon, depth = read_mat_cat(catalog_file, mag_label=mag_label, catalog_label='Catalog')
|
|
|
|
|
|
|
|
|
|
# check for null magnitude values
|
|
|
|
@@ -221,6 +471,25 @@ verbose: {verbose}")
|
|
|
|
|
utm_zone_letter = u[3]
|
|
|
|
|
logger.debug(f"Latitude / Longitude coordinates correspond to UTM zone {utm_zone_number}{utm_zone_letter}")
|
|
|
|
|
|
|
|
|
|
if (None not in AOI_lat) and (None not in AOI_lon):
|
|
|
|
|
use_AOI = True
|
|
|
|
|
logger.info(f"Area of Interest (AOI) selected with latitutde range {AOI_lat} and longitude range {AOI_lon}")
|
|
|
|
|
#convert AOI to UTM
|
|
|
|
|
u_AOI = utm.from_latlon(AOI_lat, AOI_lon)
|
|
|
|
|
x_AOI = u_AOI[0]
|
|
|
|
|
y_AOI = u_AOI[1]
|
|
|
|
|
|
|
|
|
|
# make sure grid contains the user's AOI
|
|
|
|
|
x_min = np.concatenate((x, x_AOI)).min()
|
|
|
|
|
y_min = np.concatenate((y, y_AOI)).min()
|
|
|
|
|
x_max = np.concatenate((x, x_AOI)).max()
|
|
|
|
|
y_max = np.concatenate((y, y_AOI)).max()
|
|
|
|
|
|
|
|
|
|
exclude_low_fxy = False # don't exclude any points because we need to analyze all grid points in the AOI
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
use_AOI = False
|
|
|
|
|
|
|
|
|
|
# define corners of grid based on global dataset
|
|
|
|
|
x_min = x.min()
|
|
|
|
|
y_min = y.min()
|
|
|
|
@@ -232,8 +501,7 @@ verbose: {verbose}")
|
|
|
|
|
grid_y_max = int(ceil(y_max / grid_dim) * grid_dim)
|
|
|
|
|
grid_y_min = int(floor(y_min / grid_dim) * grid_dim)
|
|
|
|
|
|
|
|
|
|
grid_lat_max, grid_lon_max = utm.to_latlon(grid_x_max, grid_y_max, utm_zone_number, utm_zone_letter)
|
|
|
|
|
grid_lat_min, grid_lon_min = utm.to_latlon(grid_x_min, grid_y_min, utm_zone_number, utm_zone_letter)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# rectangular grid
|
|
|
|
|
nx = int((grid_x_max - grid_x_min) / grid_dim) + 1
|
|
|
|
@@ -248,17 +516,23 @@ verbose: {verbose}")
|
|
|
|
|
nx = ny
|
|
|
|
|
grid_x_max = int(grid_x_min + (nx - 1) * grid_dim)
|
|
|
|
|
|
|
|
|
|
# update grid extent in lat/lon
|
|
|
|
|
grid_lat_max, grid_lon_max = utm.to_latlon(grid_x_max, grid_y_max, utm_zone_number, utm_zone_letter)
|
|
|
|
|
grid_lat_min, grid_lon_min = utm.to_latlon(grid_x_min, grid_y_min, utm_zone_number, utm_zone_letter)
|
|
|
|
|
|
|
|
|
|
# new x and y range
|
|
|
|
|
x_range = np.linspace(grid_x_min, grid_x_max, nx)
|
|
|
|
|
y_range = np.linspace(grid_y_min, grid_y_max, ny)
|
|
|
|
|
|
|
|
|
|
logger.debug(f"Grid X range: {x_range}, Y range: {y_range}")
|
|
|
|
|
|
|
|
|
|
t_windowed = time
|
|
|
|
|
r_windowed = [[x, y]]
|
|
|
|
|
|
|
|
|
|
# %% compute KDE and extract PDF
|
|
|
|
|
start = timer()
|
|
|
|
|
|
|
|
|
|
if xy_win_method == "TW":
|
|
|
|
|
if xy_win_method:
|
|
|
|
|
logger.info("Time weighting function selected")
|
|
|
|
|
|
|
|
|
|
x_weights = np.linspace(0, 15, len(t_windowed))
|
|
|
|
@@ -319,7 +593,7 @@ verbose: {verbose}")
|
|
|
|
|
|
|
|
|
|
# run activity rate modeling
|
|
|
|
|
lambdas = [None]
|
|
|
|
|
if custom_rate != None and forecast_select:
|
|
|
|
|
if custom_rate != None and forecast_select and not rate_select:
|
|
|
|
|
logger.info(f"Using activity rate specified by user: {custom_rate} per {time_unit}")
|
|
|
|
|
lambdas = np.array([custom_rate], dtype='d')
|
|
|
|
|
lambdas_perc = np.array([1], dtype='d')
|
|
|
|
@@ -327,9 +601,12 @@ verbose: {verbose}")
|
|
|
|
|
elif rate_select:
|
|
|
|
|
logger.info(f"Activity rate modeling selected")
|
|
|
|
|
|
|
|
|
|
time, mag_dummy, lat_dummy, lon_dummy, depth_dummy = read_mat_cat(catalog_file, output_datenum=True)
|
|
|
|
|
datenum_data, mag_data, lat_dummy, lon_dummy, depth_dummy = read_mat_cat(catalog_file, mag_label=mag_label, output_datenum=True)
|
|
|
|
|
|
|
|
|
|
datenum_data = time # REMEMBER THE DECIMAL DENOTES DAYS
|
|
|
|
|
if trim_to_mc:
|
|
|
|
|
indices = np.argwhere(mag_data < mc)
|
|
|
|
|
mag_data = np.delete(mag_data, indices)
|
|
|
|
|
datenum_data = np.delete(datenum_data, indices)
|
|
|
|
|
|
|
|
|
|
if time_unit == 'hours':
|
|
|
|
|
multiplicator = 24
|
|
|
|
@@ -346,30 +623,46 @@ verbose: {verbose}")
|
|
|
|
|
logger.error(msg)
|
|
|
|
|
raise Exception(msg)
|
|
|
|
|
|
|
|
|
|
# Selects dates in datenum format and procceeds to forecast value
|
|
|
|
|
start_date = datenum_data[-1] - (2 * time_win_duration / multiplicator)
|
|
|
|
|
dates_calc = [date for date in datenum_data if start_date <= date <= datenum_data[-1]]
|
|
|
|
|
forecasts, bca_conf95, rate_mean = bootstrap_forecast_rolling(dates_calc, multiplicator)
|
|
|
|
|
#-----------data are sorted in case they were not-----------------
|
|
|
|
|
sorted_pairs = sorted(zip(datenum_data, mag_data), key=lambda x: x[0])
|
|
|
|
|
datenum_data, mag_data = map(list, zip(*sorted_pairs))
|
|
|
|
|
|
|
|
|
|
# FINAL VALUES OF RATE AND ITS UNCERTAINTY IN THE 5-95 PERCENTILE
|
|
|
|
|
unc_bca05 = [ci.low for ci in bca_conf95];
|
|
|
|
|
unc_bca95 = [ci.high for ci in bca_conf95]
|
|
|
|
|
rate_unc_high = multiplicator / np.array(unc_bca05);
|
|
|
|
|
rate_unc_low = multiplicator / np.array(unc_bca95);
|
|
|
|
|
rate_forecast = multiplicator / np.median(forecasts) # [per time unit]
|
|
|
|
|
#-------split the data into bins and apply BEAST for changepoint detection--------------------
|
|
|
|
|
act_rate, bin_counts, bin_edges, bin_edges_dt, out, cps, rt, boundaries, bin_dur, time_unit = bins_and_beast(
|
|
|
|
|
np.array(datenum_data), time_unit, time_win_duration, multiplicator)
|
|
|
|
|
|
|
|
|
|
# Plot of forecasted activity rate with previous binned activity rate
|
|
|
|
|
act_rate, bin_counts, bin_edges, out, pprs, rt, idx, u_e = calc_bins(np.array(datenum_data), time_unit,
|
|
|
|
|
time_win_duration, dates_calc,
|
|
|
|
|
#------Forecasted rate is taken from BEAST or is equal to last value if no changepoints detected-----
|
|
|
|
|
if len(cps) > 0:
|
|
|
|
|
rate_forecast = rt[-1]
|
|
|
|
|
last_cp_bin = int(cps[-1])
|
|
|
|
|
else:
|
|
|
|
|
rate_forecast = act_rate[-1]
|
|
|
|
|
last_cp_bin = len(act_rate) - 1
|
|
|
|
|
|
|
|
|
|
last_cp_datenum = bin_edges[last_cp_bin]
|
|
|
|
|
dates_calc = [date for date in datenum_data if last_cp_datenum <= date <= datenum_data[-1]]
|
|
|
|
|
interevent_times = np.diff(dates_calc)
|
|
|
|
|
|
|
|
|
|
#------------Use BCa for uncertainty intervals-----------------
|
|
|
|
|
forecast, bca_conf95 = bootstrap_forecast(interevent_times)
|
|
|
|
|
rate_unc_high = bin_dur / (bca_conf95.low * multiplicator)
|
|
|
|
|
rate_unc_low = bin_dur / (bca_conf95.high * multiplicator)
|
|
|
|
|
|
|
|
|
|
#----------------------Plot------------------------------------
|
|
|
|
|
plot_results(act_rate, bin_edges, bin_edges_dt, rt, boundaries,
|
|
|
|
|
bin_dur, time_unit, multiplicator,
|
|
|
|
|
rate_forecast, rate_unc_high, rate_unc_low,
|
|
|
|
|
multiplicator, quiet=True, figsize=(14,9))
|
|
|
|
|
datenum_data, mag_data)
|
|
|
|
|
|
|
|
|
|
# Assign probabilities
|
|
|
|
|
lambdas, lambdas_perc = lambda_probs(act_rate, dates_calc, bin_edges)
|
|
|
|
|
logger.info("\n----------------- Forecast Summary -----------------")
|
|
|
|
|
logger.info(f"Forecasted activity rate (next {bin_dur} {time_unit}(s)): {rate_forecast:.4f}")
|
|
|
|
|
logger.info(f"95% BCa confidence interval: [{rate_unc_low:.4f}, {rate_unc_high:.4f}]")
|
|
|
|
|
logger.info("------------------------------------------------------")
|
|
|
|
|
|
|
|
|
|
# print("Forecasted activity rates: ", lambdas, "events per", time_unit[:-1])
|
|
|
|
|
logger.info(f"Forecasted activity rates: {lambdas} events per {time_unit} with percentages {lambdas_perc}")
|
|
|
|
|
np.savetxt('activity_rate.csv', np.vstack((lambdas, lambdas_perc)).T, header="lambda, percentage",
|
|
|
|
|
lambdas = np.array([rate_forecast/bin_dur], dtype='d')
|
|
|
|
|
lambdas_perc = np.array([1], dtype='d')
|
|
|
|
|
|
|
|
|
|
np.savetxt('activity_rate.csv', lambdas, header=f"Activity Rate (Events per {time_unit[:-1]})",
|
|
|
|
|
delimiter=',', fmt='%1.4f')
|
|
|
|
|
|
|
|
|
|
if forecast_select:
|
|
|
|
@@ -396,7 +689,7 @@ verbose: {verbose}")
|
|
|
|
|
logger.error(msg)
|
|
|
|
|
raise Exception(msg)
|
|
|
|
|
|
|
|
|
|
if lambdas[0] == None:
|
|
|
|
|
if lambdas == None:
|
|
|
|
|
msg = "Activity rate modeling was not selected and custom activity rate was not provided; cannot continue..."
|
|
|
|
|
logger.error(msg)
|
|
|
|
|
raise Exception(msg)
|
|
|
|
@@ -414,18 +707,22 @@ verbose: {verbose}")
|
|
|
|
|
fxy = xy_kde[0]
|
|
|
|
|
logger.debug(f"Normalization check; sum of all f(x,y) values = {np.sum(fxy)}")
|
|
|
|
|
|
|
|
|
|
xx, yy = np.meshgrid(x_range, y_range, indexing='ij') # grid points
|
|
|
|
|
xx, yy = np.meshgrid(x_range, y_range) # grid points
|
|
|
|
|
|
|
|
|
|
# set every grid point to be a receiver
|
|
|
|
|
grid_shape = xx.shape
|
|
|
|
|
x_rx = xx.flatten()
|
|
|
|
|
y_rx = yy.flatten()
|
|
|
|
|
|
|
|
|
|
num_points = x_rx.size
|
|
|
|
|
distances = np.zeros(shape=(num_points, grid_shape[0], grid_shape[1]))
|
|
|
|
|
|
|
|
|
|
# compute distance matrix for each receiver
|
|
|
|
|
distances = np.zeros(shape=(nx * ny, nx, ny))
|
|
|
|
|
#distances = np.zeros(shape=(nx * ny, nx, ny))
|
|
|
|
|
rx_lat = np.zeros(nx * ny)
|
|
|
|
|
rx_lon = np.zeros(nx * ny)
|
|
|
|
|
|
|
|
|
|
for i in range(nx * ny):
|
|
|
|
|
for i in range(num_points):
|
|
|
|
|
# Compute the squared distances directly using NumPy's vectorized operations
|
|
|
|
|
squared_distances = (xx - x_rx[i]) ** 2 + (yy - y_rx[i]) ** 2
|
|
|
|
|
distances[i] = np.sqrt(squared_distances)
|
|
|
|
@@ -435,50 +732,66 @@ verbose: {verbose}")
|
|
|
|
|
utm_zone_letter) # get receiver location as lat,lon
|
|
|
|
|
|
|
|
|
|
# convert distances from m to km because openquake ground motion models take input distances in kilometres
|
|
|
|
|
distances = distances/1000.0
|
|
|
|
|
#distances = distances/1000.0
|
|
|
|
|
|
|
|
|
|
# compute ground motion only at grid points that have minimum probability density of thresh_fxy
|
|
|
|
|
if exclude_low_fxy:
|
|
|
|
|
indices = list(np.where(fxy.flatten() > thresh_fxy)[0])
|
|
|
|
|
else:
|
|
|
|
|
indices = range(0, len(distances))
|
|
|
|
|
indices = np.arange(num_points)
|
|
|
|
|
|
|
|
|
|
if use_AOI:
|
|
|
|
|
# Filter out receivers outside the AOI; Find indices where values are OUTSIDE the AOI
|
|
|
|
|
indices_outside_x = np.where((x_rx < x_AOI[0]) | (x_rx > x_AOI[1]))[0]
|
|
|
|
|
indices_outside_y = np.where((y_rx < y_AOI[0]) | (y_rx > y_AOI[1]))[0]
|
|
|
|
|
indices_outside_AOI = np.unique(np.concatenate((indices_outside_x, indices_outside_y)))
|
|
|
|
|
indices_filtered = np.setdiff1d(indices, indices_outside_AOI)
|
|
|
|
|
else:
|
|
|
|
|
indices_filtered = indices
|
|
|
|
|
|
|
|
|
|
fr = fxy.flatten()
|
|
|
|
|
|
|
|
|
|
# For each receiver compute estimated ground motion values
|
|
|
|
|
logger.info(f"Estimating ground motion intensity at {len(indices)} grid points...")
|
|
|
|
|
|
|
|
|
|
PGA = np.zeros(shape=(nx * ny))
|
|
|
|
|
logger.info(f"Estimating ground motion intensity at {len(indices_filtered)} grid points...")
|
|
|
|
|
|
|
|
|
|
start = timer()
|
|
|
|
|
|
|
|
|
|
use_pp = False
|
|
|
|
|
use_pp = True
|
|
|
|
|
|
|
|
|
|
if use_pp: # use dask parallel computing
|
|
|
|
|
pbar = ProgressBar()
|
|
|
|
|
pbar.register()
|
|
|
|
|
# iter = range(0,len(distances))
|
|
|
|
|
iter = indices
|
|
|
|
|
mp.set_start_method("fork", force=True)
|
|
|
|
|
iter = indices_filtered
|
|
|
|
|
iml_grid_raw = [] # raw ground motion grids
|
|
|
|
|
for imt in products:
|
|
|
|
|
logger.info(f"Estimating {imt}")
|
|
|
|
|
|
|
|
|
|
if imt == "PGV":
|
|
|
|
|
IMT_max = 200 # search interval max for velocity (cm/s)
|
|
|
|
|
else:
|
|
|
|
|
IMT_max = 2.0 # search interval max for acceleration (g)
|
|
|
|
|
|
|
|
|
|
imls = [dask.delayed(compute_IMT_exceedance)(rx_lat[i], rx_lon[i], distances[i].flatten(), fr, p, lambdas,
|
|
|
|
|
forecast_len, lambdas_perc, m_range, m_pdf, m_cdf, model,
|
|
|
|
|
log_level=logging.DEBUG, imt=imt, IMT_min=0.0, IMT_max=2.0, rx_label=i,
|
|
|
|
|
log_level=logging.DEBUG, imt=imt, IMT_min=0.0, IMT_max=IMT_max, rx_label=i,
|
|
|
|
|
rtol=0.1, use_cython=True) for i in iter]
|
|
|
|
|
iml = dask.compute(*imls)
|
|
|
|
|
iml_grid_raw.append(list(iml))
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
iml_grid_raw = []
|
|
|
|
|
iter = indices
|
|
|
|
|
iter = indices_filtered
|
|
|
|
|
for imt in products:
|
|
|
|
|
|
|
|
|
|
if imt == "PGV":
|
|
|
|
|
IMT_max = 200 # search interval max for velocity (cm/s)
|
|
|
|
|
else:
|
|
|
|
|
IMT_max = 2.0 # search interval max for acceleration (g)
|
|
|
|
|
|
|
|
|
|
iml = []
|
|
|
|
|
for i in iter:
|
|
|
|
|
iml_i = compute_IMT_exceedance(rx_lat[i], rx_lon[i], distances[i].flatten(), fr, p, lambdas, forecast_len,
|
|
|
|
|
lambdas_perc, m_range, m_pdf, m_cdf, model, imt=imt, IMT_min = 0.0,
|
|
|
|
|
IMT_max = 2.0, rx_label = i, rtol = 0.1, use_cython=True)
|
|
|
|
|
IMT_max = IMT_max, rx_label = i, rtol = 0.1, use_cython=True)
|
|
|
|
|
iml.append(iml_i)
|
|
|
|
|
logger.info(f"Estimated {imt} at rx {i} is {iml_i}")
|
|
|
|
|
iml_grid_raw.append(iml)
|
|
|
|
@@ -486,6 +799,8 @@ verbose: {verbose}")
|
|
|
|
|
end = timer()
|
|
|
|
|
logger.info(f"Ground motion exceedance computation time: {round(end - start, 1)} seconds")
|
|
|
|
|
|
|
|
|
|
logger.debug(f"IMT values: {iml_grid_raw[0]}")
|
|
|
|
|
|
|
|
|
|
if np.isnan(iml_grid_raw).all():
|
|
|
|
|
msg = "No valid ground motion intensity measures were forecasted. Try a different ground motion model."
|
|
|
|
|
logger.error(msg)
|
|
|
|
@@ -495,39 +810,64 @@ verbose: {verbose}")
|
|
|
|
|
iml_grid = [[] for _ in range(len(products))] # final ground motion grids
|
|
|
|
|
iml_grid_prep = iml_grid.copy() # temp ground motion grids
|
|
|
|
|
|
|
|
|
|
if exclude_low_fxy:
|
|
|
|
|
for i in range(0, len(distances)):
|
|
|
|
|
if i in indices:
|
|
|
|
|
for j in range(0, len(products)):
|
|
|
|
|
iml_grid_prep[j].append(iml_grid_raw[j].pop(0))
|
|
|
|
|
else:
|
|
|
|
|
list(map(lambda lst: lst.append(np.nan),
|
|
|
|
|
iml_grid_prep)) # use np.nan to indicate grid point excluded
|
|
|
|
|
else:
|
|
|
|
|
iml_grid_prep = iml_grid_raw
|
|
|
|
|
#if use_AOI or exclude_low_fxy:
|
|
|
|
|
|
|
|
|
|
for j in range(0, len(products)):
|
|
|
|
|
vmin = min(x for x in iml_grid_prep[j] if x is not np.nan)
|
|
|
|
|
vmax = max(x for x in iml_grid_prep[j] if x is not np.nan)
|
|
|
|
|
iml_grid[j] = np.reshape(iml_grid_prep[j], (nx, ny)).astype(
|
|
|
|
|
dtype=np.float64) # this reduces values to 8 decimal places
|
|
|
|
|
iml_grid_tmp = np.nan_to_num(iml_grid[j]) # change nans to zeroes
|
|
|
|
|
# Reassemble the grid cleanly using the original shape
|
|
|
|
|
# Initialize a flat array filled entirely with NaNs
|
|
|
|
|
iml_grid_flat = np.full(num_points, np.nan, dtype=np.float64)
|
|
|
|
|
|
|
|
|
|
# upscale the grid
|
|
|
|
|
up_factor = 4
|
|
|
|
|
iml_grid_hd = resize(iml_grid_tmp, (up_factor * len(iml_grid_tmp), up_factor * len(iml_grid_tmp)),
|
|
|
|
|
mode='reflect', anti_aliasing=False)
|
|
|
|
|
iml_grid_hd[iml_grid_hd == 0.0] = np.nan # change zeroes back to nan
|
|
|
|
|
# Assign the computed values to their exact original 1D index positions
|
|
|
|
|
iml_grid_flat[indices_filtered] = iml_grid_raw[j]
|
|
|
|
|
|
|
|
|
|
# trim edges so the grid is not so blocky
|
|
|
|
|
vmin_hd = min(x for x in iml_grid_hd.flatten() if not isnan(x))
|
|
|
|
|
vmax_hd = max(x for x in iml_grid_hd.flatten() if not isnan(x))
|
|
|
|
|
trim_thresh = vmin
|
|
|
|
|
iml_grid_hd[iml_grid_hd < trim_thresh] = np.nan
|
|
|
|
|
# Reshape back using the exact shape of your original xx/yy grids
|
|
|
|
|
iml_grid_prep[j] = iml_grid_flat.reshape(grid_shape)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#for i in indices:
|
|
|
|
|
# if i in indices_filtered:
|
|
|
|
|
# for j in range(0, len(products)):
|
|
|
|
|
# iml_grid_prep[j].append(iml_grid_raw[j].pop(0))
|
|
|
|
|
# else:
|
|
|
|
|
# list(map(lambda lst: lst.append(np.nan),
|
|
|
|
|
# iml_grid_prep)) # use np.nan to indicate grid point excluded
|
|
|
|
|
#elif exclude_low_fxy:
|
|
|
|
|
# for i in range(0, len(distances)):
|
|
|
|
|
# if i in indices:
|
|
|
|
|
# for j in range(0, len(products)):
|
|
|
|
|
# iml_grid_prep[j].append(iml_grid_raw[j].pop(0))
|
|
|
|
|
# else:
|
|
|
|
|
# list(map(lambda lst: lst.append(np.nan),
|
|
|
|
|
# iml_grid_prep)) # use np.nan to indicate grid point excluded
|
|
|
|
|
#else:
|
|
|
|
|
# iml_grid_prep = iml_grid_raw
|
|
|
|
|
|
|
|
|
|
for j in range(0, len(products)):
|
|
|
|
|
vmin = np.nanmin(iml_grid_prep[j])
|
|
|
|
|
vmax = np.nanmax(iml_grid_prep[j])
|
|
|
|
|
|
|
|
|
|
#iml_grid[j] = np.reshape(iml_grid_prep[j], (nx, ny)).astype(dtype=np.float64) # this reduces values to 8 decimal places
|
|
|
|
|
#iml_grid_tmp = np.nan_to_num(iml_grid[j]) # change nans to zeroes
|
|
|
|
|
|
|
|
|
|
# upscale the grid, trim, and interpolate if there are at least 10 grid values with range greater than 0.1
|
|
|
|
|
#if np.count_nonzero(iml_grid_tmp) >= 10 and vmax-vmin > 0.1:
|
|
|
|
|
# up_factor = 1
|
|
|
|
|
# iml_grid_hd = resize(iml_grid_tmp, (up_factor * len(iml_grid_tmp), up_factor * len(iml_grid_tmp)),
|
|
|
|
|
# mode='reflect', anti_aliasing=False)
|
|
|
|
|
# trim_thresh = vmin
|
|
|
|
|
# iml_grid_hd[iml_grid_hd < trim_thresh] = np.nan
|
|
|
|
|
#else:
|
|
|
|
|
#iml_grid_hd = iml_grid_tmp
|
|
|
|
|
|
|
|
|
|
#iml_grid_hd[iml_grid_hd == 0.0] = np.nan # change zeroes back to nan
|
|
|
|
|
|
|
|
|
|
iml_grid_hd = iml_grid_prep[j]
|
|
|
|
|
#vmin_hd = min(x for x in iml_grid_hd.flatten() if not isnan(x))
|
|
|
|
|
vmax_hd = np.nanmax(iml_grid_hd)
|
|
|
|
|
|
|
|
|
|
# generate image overlay
|
|
|
|
|
north, south = lat.max(), lat.min() # Latitude range
|
|
|
|
|
east, west = lon.max(), lon.min() # Longitude range
|
|
|
|
|
north, south = grid_lat_max, grid_lat_min # Latitude range
|
|
|
|
|
east, west = grid_lon_max, grid_lon_min # Longitude range
|
|
|
|
|
bounds = [[south, west], [north, east]]
|
|
|
|
|
|
|
|
|
|
map_center = [np.mean([north, south]), np.mean([east, west])]
|
|
|
|
@@ -536,13 +876,14 @@ verbose: {verbose}")
|
|
|
|
|
cmap_name = 'YlOrRd'
|
|
|
|
|
cmap = plt.get_cmap(cmap_name)
|
|
|
|
|
fig, ax = plt.subplots(figsize=(6, 6))
|
|
|
|
|
ax.imshow(iml_grid_hd, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
|
|
|
fig.add_axes([0, 0, 1, 1])
|
|
|
|
|
ax.imshow(iml_grid_hd, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax, interpolation='bilinear', aspect='auto')
|
|
|
|
|
ax.axis('off')
|
|
|
|
|
|
|
|
|
|
# Save the figure
|
|
|
|
|
fig.canvas.draw()
|
|
|
|
|
overlay_filename = f"overlay_{j}.svg"
|
|
|
|
|
plt.savefig(overlay_filename, bbox_inches="tight", pad_inches=0, transparent=True)
|
|
|
|
|
plt.savefig(overlay_filename, pad_inches=0, transparent=True)
|
|
|
|
|
plt.close(fig)
|
|
|
|
|
|
|
|
|
|
# Embed geographic bounding box into the SVG
|
|
|
|
|