Compare commits

..

4 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
+198 -127
View File
@@ -12,6 +12,10 @@ ncp_choice = 'default'
tcp_max = 5 tcp_max = 5
torder_min = 0 torder_min = 0
torder_max = 1 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, def plot_results(act_rate, bin_edges, bin_edges_dt, rt, boundaries,
bin_dur, unit, multiplicator, bin_dur, unit, multiplicator,
@@ -148,13 +152,13 @@ def apply_beast(act_rate):
prob : A list of probabilities and change points extracted from BEAST results. prob : A list of probabilities and change points extracted from BEAST results.
""" """
#mirror_len = int(np.ceil(0.20 * len(act_rate))) mirror_len = int(np.ceil(0.20 * len(act_rate)))
#left_mirror = act_rate[:mirror_len][::-1] left_mirror = act_rate[:mirror_len][::-1]
#right_mirror = act_rate[-mirror_len:][::-1] right_mirror = act_rate[-mirror_len:][::-1]
#act_rate_mirrored = np.concatenate([left_mirror, act_rate, right_mirror]) act_rate_mirrored = np.concatenate([left_mirror, act_rate, right_mirror])
mcmc_th = int(np.clip(np.ceil(len(act_rate) / 100), 2, 15)) mcmc_th = int(np.clip(np.ceil(len(act_rate) / 100), 2, 15))
beast_result = rb.beast(act_rate, period=0, beast_result = rb.beast(act_rate_mirrored, period=0,
tcp_minmax=[0, tcp_max], tcp_minmax=[0, tcp_max],
torder_minmax=[torder_min, torder_max], torder_minmax=[torder_min, torder_max],
tseg_minlength=2, mcmc_chains=10, tseg_minlength=2, mcmc_chains=10,
@@ -183,20 +187,11 @@ def apply_beast(act_rate):
return beast_result, np.array([]) return beast_result, np.array([])
ncp = int(ncp) ncp = int(ncp)
# Filter NaNs first — BEAST fills unused cp slots with nan
# valid_cps = beast_result.trend.cp[~np.isnan(beast_result.trend.cp)]
cps = beast_result.trend.cp[:ncp] cps = beast_result.trend.cp[:ncp]
# Discard mirrored zone changepoints and correct indices # Discard mirrored zone changepoints and correct indices
#valid_mask = (cps > mirror_len) & (cps <= mirror_len + len(act_rate)) valid_mask = (cps > mirror_len) & (cps <= mirror_len + len(act_rate))
#cps = cps[valid_mask] - mirror_len cps = cps[valid_mask] - mirror_len
# Discard changepoints too close to the start or end (artifacts of mirroring).
# bins_after_cp / bins_before_cp set the minimum buffer bins required at each end.
#bins_before_cp = 2
#bins_after_cp = 2
#if len(cps) > 0:
# cps = cps[(cps >= bins_before_cp) & (cps <= len(act_rate) - bins_after_cp)]
return beast_result, np.sort(cps) return beast_result, np.sort(cps)
@@ -258,6 +253,7 @@ def bins_and_beast(dates, unit, bin_dur, multiplicator):
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.
@@ -306,23 +302,26 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
import logging import logging
from base_logger import getDefaultLogger from base_logger import getDefaultLogger
from timeit import default_timer as timer from timeit import default_timer as timer
from math import ceil, floor, isnan
import numpy as np import numpy as np
import dask import dask
import kalepy as kale import kalepy as kale
import utm
from skimage.transform import resize
import igfash import igfash
from igfash.io import read_mat_cat, read_mat_m, read_mat_mc, read_mat_pdf from igfash.io import read_mat_cat, read_mat_m, read_mat_mc, read_mat_pdf, read_csv
from igfash.window import win_CNE from igfash.window import win_CTL, win_CNE
import igfash.kde as kde import igfash.kde as kde
from igfash.gm import compute_IMT_exceedance from igfash.gm import compute_IMT_exceedance
from igfash.compute import get_cdf from igfash.compute import get_cdf, hellinger_dist, cols_to_rows
from igfash.rate import lambda_probs, calc_bins, bootstrap_forecast_rolling
from igfash.mc import estimate_mc
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator from matplotlib.ticker import MultipleLocator
from matplotlib.contour import ContourSet from matplotlib.contour import ContourSet
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import json import json
import multiprocessing as mp import multiprocessing as mp
import geopandas as gpd
import shapely
logger = getDefaultLogger('igfash') logger = getDefaultLogger('igfash')
@@ -342,6 +341,9 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
else: else:
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
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_lat = np.array(AOI_extent[:2])
AOI_lon = np.array(AOI_extent[2:]) AOI_lon = np.array(AOI_extent[2:])
@@ -352,7 +354,7 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
xy_select: {xy_select}\n grid_dim: {grid_dim}\n xy_win_method: {xy_win_method}\n rate_select: {rate_select}\n time_win_duration: {time_win_duration}\n \ xy_select: {xy_select}\n grid_dim: {grid_dim}\n xy_win_method: {xy_win_method}\n rate_select: {rate_select}\n time_win_duration: {time_win_duration}\n \
forecast_select: {forecast_select}\n custom_rate: {custom_rate}\n forecast_len: {forecast_len}\n time_unit: {time_unit}\n model: {model}\n products: {products_string}\n \ forecast_select: {forecast_select}\n custom_rate: {custom_rate}\n forecast_len: {forecast_len}\n time_unit: {time_unit}\n model: {model}\n products: {products_string}\n \
verbose: {verbose}") verbose: {verbose}")
logger.debug(f"Area of interest selected by user - Latitude: {AOI_lat}, Longitude: {AOI_lon}")
# print key package version numbers # print key package version numbers
logger.debug(f"Python version {sys.version}") logger.debug(f"Python version {sys.version}")
logger.debug(f"Numpy version {version('numpy')}") logger.debug(f"Numpy version {version('numpy')}")
@@ -461,65 +463,68 @@ verbose: {verbose}")
time, mag, lat, lon, depth = read_mat_cat(catalog_file) time, mag, lat, lon, depth = read_mat_cat(catalog_file)
# Create a GeoDataFrame for the catalog data (initially WGS84 EPSG:4326) # convert to UTM
catalog_gdf = gpd.GeoDataFrame( u = utm.from_latlon(lat, lon)
{'depth': depth, 'time': time}, x = u[0]
geometry=gpd.points_from_xy(lon, lat), y = u[1]
crs="EPSG:4326" utm_zone_number = u[2]
) utm_zone_letter = u[3]
logger.debug(f"Latitude / Longitude coordinates correspond to UTM zone {utm_zone_number}{utm_zone_letter}")
utm_crs = catalog_gdf.estimate_utm_crs() # Find the UTM EPSG code for this location
catalog_gdf_utm = catalog_gdf.to_crs(utm_crs) # Convert the catalog entirely to UTM meters
logger.debug(f"Latitude / Longitude event coordinates converted to UTM zone {utm_crs}")
# Extract event coordinates directly from the vector geometry
x = catalog_gdf_utm.geometry.x.values
y = catalog_gdf_utm.geometry.y.values
# Handle Area of Interest (AOI)
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
u_AOI = utm.from_latlon(AOI_lat, AOI_lon)
x_AOI = u_AOI[0]
y_AOI = u_AOI[1]
# Create an AOI GeoDataFrame and project it to the same UTM CRS # make sure grid contains the user's AOI
aoi_gdf_utm = gpd.GeoDataFrame( x_min = np.concatenate((x, x_AOI)).min()
geometry=gpd.points_from_xy(AOI_lon, AOI_lat), y_min = np.concatenate((y, y_AOI)).min()
crs="EPSG:4326" x_max = np.concatenate((x, x_AOI)).max()
).to_crs(utm_crs) 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
# Combine dataframes to extract the collective bounding box limits
combined_gdf = gpd.GeoDataFrame(geometry=gpd.pd.concat([catalog_gdf_utm.geometry, aoi_gdf_utm.geometry]))
x_min, y_min, x_max, y_max = combined_gdf.total_bounds
else: else:
use_AOI = False use_AOI = False
x_min, y_min, x_max, y_max = catalog_gdf_utm.total_bounds
# round up grid dimensions # define corners of grid based on global dataset
grid_x_min = (x_min // grid_dim) * grid_dim x_min = x.min()
grid_x_max = ((x_max + grid_dim - 1) // grid_dim) * grid_dim y_min = y.min()
grid_y_min = (y_min // grid_dim) * grid_dim x_max = x.max()
grid_y_max = ((y_max + grid_dim - 1) // grid_dim) * grid_dim y_max = y.max()
# expand extent until it is square grid_x_max = int(ceil(x_max / grid_dim) * grid_dim)
ext_w, ext_h = grid_x_max - grid_x_min, grid_y_max - grid_y_min grid_x_min = int(floor(x_min / grid_dim) * grid_dim)
delta = abs(ext_w - ext_h) / 2 grid_y_max = int(ceil(y_max / grid_dim) * grid_dim)
grid_y_min = int(floor(y_min / grid_dim) * grid_dim)
# Shift the shorter axis outward symmetrically
grid_x_min, grid_x_max = (grid_x_min - delta, grid_x_max + delta) if ext_w < ext_h else (grid_x_min, grid_x_max)
grid_y_min, grid_y_max = (grid_y_min - delta, grid_y_max + delta) if ext_h < ext_w else (grid_y_min, grid_y_max)
# make grid points
x_range = np.arange(grid_x_min, grid_x_max + grid_dim, grid_dim)
nx = len(x_range)
y_range = np.arange(grid_y_min, grid_y_max + grid_dim, grid_dim)
ny = len(y_range)
X, Y = np.meshgrid(x_range, y_range) # rectangular grid
cells = shapely.box(X, Y, X + grid_dim, Y + grid_dim) nx = int((grid_x_max - grid_x_min) / grid_dim) + 1
grid_gdf_utm = gpd.GeoDataFrame(geometry=cells.flatten(), crs=utm_crs) ny = int((grid_y_max - grid_y_min) / grid_dim) + 1
grid_gdf_latlon = grid_gdf_utm.to_crs("EPSG:4326")
logger.debug(f"Grid extent in UTM XY {grid_gdf_utm.total_bounds}") # ensure a square grid is used
logger.debug(f"Grid extent in lat lon {grid_gdf_latlon.total_bounds}") if nx > ny: # enlarge y dimension to match x
ny = nx
grid_y_max = int(grid_y_min + (ny - 1) * grid_dim)
else: # enlarge x dimension to match y
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 t_windowed = time
r_windowed = [[x, y]] r_windowed = [[x, y]]
@@ -543,7 +548,6 @@ verbose: {verbose}")
xy_kale = output_kale[0] xy_kale = output_kale[0]
xy_kde = output_kde[0] xy_kde = output_kde[0]
grid_gdf_latlon['location_PDF'] = xy_kde[0].flatten() # insert location PDF as a column of the latlon GDF
# plot location PDF # plot location PDF
xy_kale_km = type(xy_kale)(xy_kale.dataset / 1000) xy_kale_km = type(xy_kale)(xy_kale.dataset / 1000)
@@ -700,41 +704,63 @@ verbose: {verbose}")
m_cdf = get_cdf(m_pdf) m_cdf = get_cdf(m_pdf)
centroids_utm = grid_gdf_utm.geometry.centroid.values #extract the centroid of each cell fxy = xy_kde[0]
num_points = len(grid_gdf_utm) logger.debug(f"Normalization check; sum of all f(x,y) values = {np.sum(fxy)}")
distances = np.array([shapely.distance(centroids_utm[i], centroids_utm) for i in range(num_points)]) #compute distance between every grid point xx, yy = np.meshgrid(x_range, y_range) # grid points
grid_gdf_latlon['distance_matrix'] = [distances[i] for i in range(num_points)] #store the distance matrix in the GDF
# Select only cells of the grid that are inside the AOI # set every grid point to be a receiver
if use_AOI: grid_shape = xx.shape
centroids_latlon = grid_gdf_latlon.geometry.centroid x_rx = xx.flatten()
y_rx = yy.flatten()
# Mark grid cells that are within the AOI using vectorized boundary checks num_points = x_rx.size
grid_gdf_latlon['AOI'] = ( distances = np.zeros(shape=(num_points, grid_shape[0], grid_shape[1]))
(centroids_latlon.x >= AOI_lon[0]) & (centroids_latlon.x <= AOI_lon[1]) &
(centroids_latlon.y >= AOI_lat[0]) & (centroids_latlon.y <= AOI_lat[1])
)
else:
grid_gdf_latlon['AOI']=True #set entire grid to be the area of interest
distances_sel = grid_gdf_latlon.loc[grid_gdf_latlon['AOI']]['distance_matrix'].to_numpy() # compute distance matrix for each receiver
centroids_sel = grid_gdf_latlon.loc[grid_gdf_latlon['AOI']].centroid #distances = np.zeros(shape=(nx * ny, nx, ny))
rx_lat = np.zeros(nx * ny)
rx_lon = np.zeros(nx * ny)
loc_pdf = grid_gdf_latlon['location_PDF'].to_numpy() # extract the previously created location PDF from the GDF 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)
# create context object for receiver and append to list
rx_lat[i], rx_lon[i] = utm.to_latlon(x_rx[i], y_rx[i], utm_zone_number,
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_sel = distances_sel/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 = 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 # For each receiver compute estimated ground motion values
logger.info(f"Estimating ground motion intensity at {len(distances_sel)} grid points...") logger.info(f"Estimating ground motion intensity at {len(indices_filtered)} grid points...")
start = timer()
use_pp = True use_pp = True
start = timer()
if use_pp: # use dask parallel computing if use_pp: # use dask parallel computing
mp.set_start_method("fork", force=True) mp.set_start_method("fork", force=True)
iter = range(0,len(distances_sel)) iter = indices_filtered
iml_grid_raw = [] # raw ground motion grids iml_grid_raw = [] # raw ground motion grids
for imt in products: for imt in products:
logger.info(f"Estimating {imt}") logger.info(f"Estimating {imt}")
@@ -744,7 +770,7 @@ verbose: {verbose}")
else: else:
IMT_max = 2.0 # search interval max for acceleration (g) IMT_max = 2.0 # search interval max for acceleration (g)
imls = [dask.delayed(compute_IMT_exceedance)(centroids_sel.iloc[i].y, centroids_sel.iloc[i].x, distances_sel[i].flatten(), loc_pdf, p, lambdas, 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, forecast_len, lambdas_perc, m_range, m_pdf, m_cdf, model,
log_level=logging.DEBUG, imt=imt, IMT_min=0.0, IMT_max=IMT_max, 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] rtol=0.1, use_cython=True) for i in iter]
@@ -753,9 +779,9 @@ verbose: {verbose}")
else: else:
iml_grid_raw = [] iml_grid_raw = []
iter = range(0,len(distances_sel)) iter = indices_filtered
for imt in products: for imt in products:
if imt == "PGV": if imt == "PGV":
IMT_max = 200 # search interval max for velocity (cm/s) IMT_max = 200 # search interval max for velocity (cm/s)
else: else:
@@ -763,7 +789,7 @@ verbose: {verbose}")
iml = [] iml = []
for i in iter: for i in iter:
iml_i = compute_IMT_exceedance(centroids_sel.iloc[i].y, centroids_sel.iloc[i].x, distances_sel[i].flatten(), loc_pdf, p, lambdas, forecast_len, 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, lambdas_perc, m_range, m_pdf, m_cdf, model, imt=imt, IMT_min = 0.0,
IMT_max = IMT_max, 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) iml.append(iml_i)
@@ -773,51 +799,96 @@ 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)
raise Exception(msg) raise Exception(msg)
for j, imt in enumerate(products): #generate image overlay for each IMT product # create list of one empty list for each imt
logger.debug(f"{products[j]} values: {iml_grid_raw[j]}") iml_grid = [[] for _ in range(len(products))] # final ground motion grids
grid_gdf_latlon.loc[grid_gdf_latlon['AOI'], imt] = iml_grid_raw[j] # insert computed imt grid into GDF iml_grid_prep = iml_grid.copy() # temp ground motion grids
grid_gdf_latlon_clean = grid_gdf_latlon.dropna(subset=[imt]) # remove null values from grid #if use_AOI or exclude_low_fxy:
x_plot = grid_gdf_latlon_clean.geometry.centroid.x.values for j in range(0, len(products)):
y_plot = grid_gdf_latlon_clean.geometry.centroid.y.values # Reassemble the grid cleanly using the original shape
z_plot = grid_gdf_latlon_clean[imt].values # Initialize a flat array filled entirely with NaNs
iml_grid_flat = np.full(num_points, np.nan, dtype=np.float64)
vmin = np.nanmin(z_plot) # Assign the computed values to their exact original 1D index positions
vmax = np.nanmax(z_plot) iml_grid_flat[indices_filtered] = iml_grid_raw[j]
# Generate Image Overlay # Reshape back using the exact shape of your original xx/yy grids
fig, ax = plt.subplots() iml_grid_prep[j] = iml_grid_flat.reshape(grid_shape)
contour = ax.tricontourf(
x_plot,
y_plot, #for i in indices:
z_plot, # if i in indices_filtered:
levels=200, #linear scale # for j in range(0, len(products)):
cmap="YlOrRd", # iml_grid_prep[j].append(iml_grid_raw[j].pop(0))
) # else:
ax.set_aspect('equal') # keep geographic coordinates from stretching # list(map(lambda lst: lst.append(np.nan),
ax.set_axis_off() # iml_grid_prep)) # use np.nan to indicate grid point excluded
fig.patch.set_visible(False); ax.patch.set_visible(False) #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 = 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])]
# Create an image from the grid
cmap_name = 'YlOrRd'
cmap = plt.get_cmap(cmap_name)
fig, ax = plt.subplots(figsize=(6, 6))
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" overlay_filename = f"overlay_{j}.svg"
plt.savefig(overlay_filename, pad_inches=0, bbox_inches="tight", transparent=True) plt.savefig(overlay_filename, pad_inches=0, transparent=True)
plt.close(fig) plt.close(fig)
# set image map extent in geographic coordinates
north = grid_gdf_latlon[grid_gdf_latlon['AOI']].total_bounds[3]
south = grid_gdf_latlon[grid_gdf_latlon['AOI']].total_bounds[1]
east = grid_gdf_latlon[grid_gdf_latlon['AOI']].total_bounds[2]
west = grid_gdf_latlon[grid_gdf_latlon['AOI']].total_bounds[0]
# Embed geographic bounding box into the SVG # Embed geographic bounding box into the SVG
map_bounds = dict(zip(("south", "west", "north", "east"), map_bounds = dict(zip(("south", "west", "north", "east"),
map(float, (south, west, north, east)))) map(float, (grid_lat_min, grid_lon_min, grid_lat_max, grid_lon_max))))
tree = ET.parse(overlay_filename) tree = ET.parse(overlay_filename)
tree.getroot().set("data-map-bounds", json.dumps(map_bounds)) tree.getroot().set("data-map-bounds", json.dumps(map_bounds))
tree.write(overlay_filename, encoding="utf-8", xml_declaration=True) tree.write(overlay_filename, encoding="utf-8", xml_declaration=True)
@@ -831,17 +902,17 @@ verbose: {verbose}")
gradient = np.vstack((gradient, gradient)).T gradient = np.vstack((gradient, gradient)).T
gradient = np.tile(gradient, (1, width)) gradient = np.tile(gradient, (1, width))
colorbar_title = imt colorbar_title = products[j]
if "PGA" in colorbar_title or "SA" in colorbar_title: if "PGA" in colorbar_title or "SA" in colorbar_title:
colorbar_title = colorbar_title + " (g)" colorbar_title = colorbar_title + " (g)"
fig, ax = plt.subplots(figsize=((width + 40) / 100.0, (height + 20) / 100.0), fig, ax = plt.subplots(figsize=((width + 40) / 100.0, (height + 20) / 100.0),
dpi=100) # Increase fig size for labels dpi=100) # Increase fig size for labels
ax.imshow(gradient, aspect='auto', cmap=cmap.reversed(), ax.imshow(gradient, aspect='auto', cmap=cmap.reversed(),
extent=[0, 1, vmin, vmax]) # Note: extent order is different for vertical extent=[0, 1, vmin, vmax_hd]) # Note: extent order is different for vertical
ax.set_xticks([]) # Remove x-ticks for vertical colorbar ax.set_xticks([]) # Remove x-ticks for vertical colorbar
num_ticks = 11 # Show more ticks num_ticks = 11 # Show more ticks
tick_positions = np.linspace(vmin, vmax, num_ticks) tick_positions = np.linspace(vmin, vmax_hd, num_ticks)
ax.set_yticks(tick_positions) ax.set_yticks(tick_positions)
ax.set_yticklabels([f"{tick:.2f}" for tick in tick_positions]) # format tick labels ax.set_yticklabels([f"{tick:.2f}" for tick in tick_positions]) # format tick labels
ax.set_title(colorbar_title, loc='right', pad=15) ax.set_title(colorbar_title, loc='right', pad=15)