Compare commits

...

42 Commits

Author SHA1 Message Date
ftong 00c0ca4c3a Update src/seismic_hazard_forecasting.py 2026-06-24 12:46:06 +02:00
ftong 008b3f7d21 Update src/seismic_hazard_forecasting.py
try distances/1000
2026-06-24 12:27:25 +02:00
ftong 2b6cd52131 Update src/seismic_hazard_forecasting.py 2026-06-24 11:39:03 +02:00
ftong 7e550f36f9 Update src/shf_wrapper.py
update wrapper
2026-06-23 17:16:33 +02:00
ftong bb70ebbe24 Update src/seismic_hazard_forecasting.py
stretch image to exact borders of svg file
2026-06-23 14:33:55 +02:00
ftong 73fd54cc76 Update src/seismic_hazard_forecasting.py
add more logging
2026-06-23 13:09:39 +02:00
ftong 72dc427a0e Update src/seismic_hazard_forecasting.py 2026-06-23 10:56:54 +02:00
ftong 3576b9eb96 Update src/seismic_hazard_forecasting.py
activate test zone AOI
2026-06-23 03:58:33 +02:00
ftong 92b6eb4880 Update src/seismic_hazard_forecasting.py 2026-06-23 03:21:53 +02:00
ftong 13b463a15a Update src/seismic_hazard_forecasting.py
try distance in metres
2026-06-23 02:53:44 +02:00
ftong 62e551ff6a Update src/seismic_hazard_forecasting.py 2026-06-23 02:52:42 +02:00
ftong fa192fd7a0 Update src/seismic_hazard_forecasting.py 2026-06-23 02:20:53 +02:00
ftong e3d7fca55c Update src/seismic_hazard_forecasting.py 2026-06-23 02:05:27 +02:00
ftong c7f1dfa548 Update src/seismic_hazard_forecasting.py 2026-06-23 00:40:16 +02:00
ftong 105a36893a Update src/seismic_hazard_forecasting.py 2026-06-23 00:04:34 +02:00
ftong d5e5435d83 Update src/seismic_hazard_forecasting.py 2026-06-22 23:36:33 +02:00
ftong 144f7e1fcd Update src/seismic_hazard_forecasting.py
remove events below mc for activity rate
2026-06-22 23:04:07 +02:00
ftong 337457d49c Update src/seismic_hazard_forecasting.py 2026-06-22 22:58:10 +02:00
ftong 3d4f9138d7 Update src/seismic_hazard_forecasting.py
fix attempt 2
2026-06-22 19:23:13 +02:00
ftong 0ef41d72f6 Update src/seismic_hazard_forecasting.py
fix orientation issue attempt 1
2026-06-22 19:06:19 +02:00
ftong b7ee8d52ab Update src/seismic_hazard_forecasting.py
log message about AOI activation and fix orientation if AOI selected
2026-06-22 14:08:10 +02:00
ftong 89f3f70c62 Update src/seismic_hazard_forecasting.py 2026-06-22 12:28:23 +02:00
ftong 9f78e5681c Update src/seismic_hazard_forecasting.py
use upscale=1 while testing
2026-06-19 18:07:55 +02:00
ftong 9a0b9444a3 Update src/seismic_hazard_forecasting.py 2026-06-19 17:01:55 +02:00
ftong 3ea88e0eb4 Update src/seismic_hazard_forecasting.py
fix array size of lambdas and lambdas_perc
2026-06-19 16:05:37 +02:00
ftong 504b553b9a Update src/seismic_hazard_forecasting.py
make lambdas at least 1d
2026-06-19 15:59:50 +02:00
ftong 18e3fd0ca3 Update src/seismic_hazard_forecasting.py 2026-06-19 15:54:49 +02:00
ftong 59955cf085 Update src/seismic_hazard_forecasting.py 2026-06-19 15:52:18 +02:00
ftong 0a09a2dc00 Update src/seismic_hazard_forecasting.py 2026-06-19 15:50:50 +02:00
ftong e7a344bca3 Update src/seismic_hazard_forecasting.py
remove "percentage" in activity rate csv
2026-06-19 15:45:23 +02:00
ftong 01ec215124 Update src/seismic_hazard_forecasting.py
missing from matplotlib.ticker import MultipleLocator
2026-06-19 15:41:33 +02:00
ftong f50a315ed7 Update src/seismic_hazard_forecasting.py
import matplotlib
2026-06-19 15:39:41 +02:00
ftong 037863e975 Update src/seismic_hazard_forecasting.py
temporary global variables since GUI not ready
2026-06-19 15:38:35 +02:00
ftong fb00131997 Update src/seismic_hazard_forecasting.py 2026-06-19 15:33:43 +02:00
ftong 41a900c366 Update src/seismic_hazard_forecasting.py
import numpy
2026-06-19 15:09:47 +02:00
ftong 4212037a78 Update src/seismic_hazard_forecasting.py
fix time_win_duration name
2026-06-19 15:08:17 +02:00
ftong a00d4d6f52 Update src/seismic_hazard_forecasting.py
fix missing call to bin_and_beast
2026-06-19 15:02:14 +02:00
ftong 09cf4b0e6a Update src/seismic_hazard_forecasting.py
fix mag_label
2026-06-19 14:54:20 +02:00
ftong 3b797e41cb Update src/seismic_hazard_forecasting.py
fix syntax error
2026-06-19 14:40:44 +02:00
ftong b51e5b9f43 Update src/seismic_hazard_forecasting.py
correct mag_data variable name
2026-06-19 14:38:00 +02:00
ftong bb3184a126 Update src/seismic_hazard_forecasting.py
correct activity rate to be per time unit before fed to forecasting
2026-06-19 14:36:49 +02:00
ftong 46ff6b8a6c Update src/seismic_hazard_forecasting.py
use new activity rate forecast method
2026-06-19 14:29:44 +02:00
2 changed files with 373 additions and 77 deletions
+369 -77
View File
@@ -1,8 +1,259 @@
# -*- coding: utf-8 -*- # -*- 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, 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, 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, AOI_extent, 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. Python application that reads an earthquake catalog and performs seismic hazard forecasting.
@@ -222,7 +473,7 @@ verbose: {verbose}")
if (None not in AOI_lat) and (None not in AOI_lon): if (None not in AOI_lat) and (None not in AOI_lon):
use_AOI = True 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 #convert AOI to UTM
u_AOI = utm.from_latlon(AOI_lat, AOI_lon) u_AOI = utm.from_latlon(AOI_lat, AOI_lon)
x_AOI = u_AOI[0] x_AOI = u_AOI[0]
@@ -250,8 +501,7 @@ verbose: {verbose}")
grid_y_max = int(ceil(y_max / grid_dim) * grid_dim) grid_y_max = int(ceil(y_max / grid_dim) * grid_dim)
grid_y_min = int(floor(y_min / 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 # rectangular grid
nx = int((grid_x_max - grid_x_min) / grid_dim) + 1 nx = int((grid_x_max - grid_x_min) / grid_dim) + 1
@@ -266,10 +516,16 @@ verbose: {verbose}")
nx = ny nx = ny
grid_x_max = int(grid_x_min + (nx - 1) * grid_dim) 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 # new x and y range
x_range = np.linspace(grid_x_min, grid_x_max, nx) x_range = np.linspace(grid_x_min, grid_x_max, nx)
y_range = np.linspace(grid_y_min, grid_y_max, ny) 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 t_windowed = time
r_windowed = [[x, y]] r_windowed = [[x, y]]
@@ -344,11 +600,14 @@ verbose: {verbose}")
elif rate_select: elif rate_select:
logger.info(f"Activity rate modeling selected") logger.info(f"Activity rate modeling selected")
datenum_data, mag_data, lat_dummy, lon_dummy, depth_dummy = read_mat_cat(catalog_file, mag_label=mag_label, output_datenum=True)
time, mag_dummy, lat_dummy, lon_dummy, depth_dummy = read_mat_cat(catalog_file, output_datenum=True) if trim_to_mc:
indices = np.argwhere(mag_data < mc)
datenum_data = time # REMEMBER THE DECIMAL DENOTES DAYS mag_data = np.delete(mag_data, indices)
datenum_data = np.delete(datenum_data, indices)
if time_unit == 'hours': if time_unit == 'hours':
multiplicator = 24 multiplicator = 24
elif time_unit == 'days': elif time_unit == 'days':
@@ -364,32 +623,46 @@ verbose: {verbose}")
logger.error(msg) logger.error(msg)
raise Exception(msg) raise Exception(msg)
# Selects dates in datenum format and procceeds to forecast value #-----------data are sorted in case they were not-----------------
start_date = datenum_data[-1] - (2 * time_win_duration / multiplicator) sorted_pairs = sorted(zip(datenum_data, mag_data), key=lambda x: x[0])
dates_calc = [date for date in datenum_data if start_date <= date <= datenum_data[-1]] datenum_data, mag_data = map(list, zip(*sorted_pairs))
forecasts, bca_conf95, rate_mean = bootstrap_forecast_rolling(dates_calc, multiplicator)
# FINAL VALUES OF RATE AND ITS UNCERTAINTY IN THE 5-95 PERCENTILE #-------split the data into bins and apply BEAST for changepoint detection--------------------
unc_bca05 = [ci.low for ci in bca_conf95]; act_rate, bin_counts, bin_edges, bin_edges_dt, out, cps, rt, boundaries, bin_dur, time_unit = bins_and_beast(
unc_bca95 = [ci.high for ci in bca_conf95] np.array(datenum_data), time_unit, time_win_duration, multiplicator)
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]
# 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,
rate_forecast, rate_unc_high, rate_unc_low,
multiplicator, quiet=True, figsize=(14,9))
# Assign probabilities
lambdas, lambdas_perc = lambda_probs(act_rate, dates_calc, bin_edges)
lambdas = np.array(lambdas, dtype='d')
lambdas_perc = np.array(lambdas_perc, dtype='d')
# print("Forecasted activity rates: ", lambdas, "events per", time_unit[:-1]) #------Forecasted rate is taken from BEAST or is equal to last value if no changepoints detected-----
logger.info(f"Forecasted activity rates: {lambdas} events per {time_unit} with percentages {lambdas_perc}") if len(cps) > 0:
np.savetxt('activity_rate.csv', np.vstack((lambdas, lambdas_perc)).T, header="lambda, percentage", 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,
datenum_data, mag_data)
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("------------------------------------------------------")
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') delimiter=',', fmt='%1.4f')
if forecast_select: if forecast_select:
@@ -416,7 +689,7 @@ verbose: {verbose}")
logger.error(msg) logger.error(msg)
raise Exception(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..." msg = "Activity rate modeling was not selected and custom activity rate was not provided; cannot continue..."
logger.error(msg) logger.error(msg)
raise Exception(msg) raise Exception(msg)
@@ -434,18 +707,22 @@ verbose: {verbose}")
fxy = xy_kde[0] fxy = xy_kde[0]
logger.debug(f"Normalization check; sum of all f(x,y) values = {np.sum(fxy)}") 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 # set every grid point to be a receiver
grid_shape = xx.shape
x_rx = xx.flatten() x_rx = xx.flatten()
y_rx = yy.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 # 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_lat = np.zeros(nx * ny)
rx_lon = 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 # Compute the squared distances directly using NumPy's vectorized operations
squared_distances = (xx - x_rx[i]) ** 2 + (yy - y_rx[i]) ** 2 squared_distances = (xx - x_rx[i]) ** 2 + (yy - y_rx[i]) ** 2
distances[i] = np.sqrt(squared_distances) distances[i] = np.sqrt(squared_distances)
@@ -455,13 +732,13 @@ verbose: {verbose}")
utm_zone_letter) # get receiver location as lat,lon 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 # 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 # compute ground motion only at grid points that have minimum probability density of thresh_fxy
if exclude_low_fxy: if exclude_low_fxy:
indices = list(np.where(fxy.flatten() > thresh_fxy)[0]) indices = list(np.where(fxy.flatten() > thresh_fxy)[0])
else: else:
indices = range(0, len(distances)) indices = np.arange(num_points)
if use_AOI: if use_AOI:
# Filter out receivers outside the AOI; Find indices where values are OUTSIDE the AOI # Filter out receivers outside the AOI; Find indices where values are OUTSIDE the AOI
@@ -477,8 +754,6 @@ verbose: {verbose}")
# For each receiver compute estimated ground motion values # For each receiver compute estimated ground motion values
logger.info(f"Estimating ground motion intensity at {len(indices_filtered)} grid points...") logger.info(f"Estimating ground motion intensity at {len(indices_filtered)} grid points...")
PGA = np.zeros(shape=(nx * ny))
start = timer() start = timer()
use_pp = True use_pp = True
@@ -524,6 +799,8 @@ verbose: {verbose}")
end = timer() end = timer()
logger.info(f"Ground motion exceedance computation time: {round(end - start, 1)} seconds") 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(): if np.isnan(iml_grid_raw).all():
msg = "No valid ground motion intensity measures were forecasted. Try a different ground motion model." msg = "No valid ground motion intensity measures were forecasted. Try a different ground motion model."
logger.error(msg) logger.error(msg)
@@ -533,65 +810,80 @@ verbose: {verbose}")
iml_grid = [[] for _ in range(len(products))] # final ground motion grids iml_grid = [[] for _ in range(len(products))] # final ground motion grids
iml_grid_prep = iml_grid.copy() # temp ground motion grids iml_grid_prep = iml_grid.copy() # temp ground motion grids
if use_AOI: #if use_AOI or exclude_low_fxy:
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)): for j in range(0, len(products)):
vmin = min(x for x in iml_grid_prep[j] if x is not np.nan) # Reassemble the grid cleanly using the original shape
vmax = max(x for x in iml_grid_prep[j] if x is not np.nan) # Initialize a flat array filled entirely with NaNs
iml_grid[j] = np.reshape(iml_grid_prep[j], (nx, ny)).astype( iml_grid_flat = np.full(num_points, np.nan, dtype=np.float64)
dtype=np.float64) # this reduces values to 8 decimal places
iml_grid_tmp = np.nan_to_num(iml_grid[j]) # change nans to zeroes # Assign the computed values to their exact original 1D index positions
iml_grid_flat[indices_filtered] = iml_grid_raw[j]
# 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 # 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: #if np.count_nonzero(iml_grid_tmp) >= 10 and vmax-vmin > 0.1:
up_factor = 4 # up_factor = 1
iml_grid_hd = resize(iml_grid_tmp, (up_factor * len(iml_grid_tmp), up_factor * len(iml_grid_tmp)), # iml_grid_hd = resize(iml_grid_tmp, (up_factor * len(iml_grid_tmp), up_factor * len(iml_grid_tmp)),
mode='reflect', anti_aliasing=False) # mode='reflect', anti_aliasing=False)
trim_thresh = vmin # trim_thresh = vmin
iml_grid_hd[iml_grid_hd < trim_thresh] = np.nan # iml_grid_hd[iml_grid_hd < trim_thresh] = np.nan
else: #else:
iml_grid_hd = iml_grid_tmp #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_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)) #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)) vmax_hd = np.nanmax(iml_grid_hd)
# generate image overlay # generate image overlay
north, south = lat.max(), lat.min() # Latitude range north, south = grid_lat_max, grid_lat_min # Latitude range
east, west = lon.max(), lon.min() # Longitude range east, west = grid_lon_max, grid_lon_min # Longitude range
bounds = [[south, west], [north, east]] bounds = [[south, west], [north, east]]
map_center = [np.mean([north, south]), np.mean([east, west])] map_center = [np.mean([north, south]), np.mean([east, west])]
# Create an image from the grid # Create an image from the grid
cmap_name = 'YlOrRd' cmap_name = 'YlOrRd'
cmap = plt.get_cmap(cmap_name) cmap = plt.get_cmap(cmap_name)
fig, ax = plt.subplots(figsize=(6, 6)) fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(iml_grid_hd, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax, interpolation='bilinear') 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') ax.axis('off')
# Save the figure # Save the figure
fig.canvas.draw() fig.canvas.draw()
overlay_filename = f"overlay_{j}.svg" 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) plt.close(fig)
# Embed geographic bounding box into the SVG # Embed geographic bounding box into the SVG
+4
View File
@@ -31,6 +31,9 @@ def main(argv):
return False return False
else: else:
raise argparse.ArgumentTypeError("Boolean value expected.") raise argparse.ArgumentTypeError("Boolean value expected.")
def float_or_none(v):
return None if v.lower() == "none" else float(v)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -55,6 +58,7 @@ def main(argv):
parser.add_argument("--time_unit", type=str) parser.add_argument("--time_unit", type=str)
parser.add_argument("--model", type=str) parser.add_argument("--model", type=str)
parser.add_argument("--products_string", type=str) parser.add_argument("--products_string", type=str)
parser.add_argument("--AOI_extent", nargs=4, type=float_or_none, default=[None] * 4, required=False)
parser.add_argument("--verbose", type=str2bool) parser.add_argument("--verbose", type=str2bool)
args = parser.parse_args() args = parser.parse_args()