Compare commits

..

8 Commits

Author SHA1 Message Date
ymlesni 046aa63f1f Merge pull request 'Replaced usage of removed variable' (!25) from July2026-cmap-fix into master
Reviewed-on: #25
2026-07-22 19:03:33 +02:00
ymlesni 120ddb703d Replaced usage of removed variable 2026-07-22 18:57:50 +02:00
ymlesni c2247ee602 Merge pull request 'custom AOI feature, changes to plotting and activity rate calculation' (!24) from July2026 into master
Reviewed-on: #24
2026-07-22 12:43:46 +02:00
ftong 5325c07404 Update src/seismic_hazard_forecasting.py
AOI feature makes exclude_low_fxy redundant as we now have way to save time by selecting small area to forecast
2026-07-08 17:16:21 +02:00
ftong b59cf0b5d1 Update src/seismic_hazard_forecasting.py
cleanup package imports
2026-07-08 17:13:30 +02:00
ftong 2ff3f3c808 Update src/seismic_hazard_forecasting.py
use dataframe total_bounds as the bounding box of SVG file
2026-07-08 16:42:22 +02:00
ftong a56f18dde0 Update src/seismic_hazard_forecasting.py
change IMT plotting method
2026-07-08 16:12:39 +02:00
ftong ab19294be5 Update src/seismic_hazard_forecasting.py
use geopandas for coordinate conversion before Location PDF estimation
2026-07-07 16:40:00 +02:00
+124 -245
View File
@@ -12,10 +12,6 @@ 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,
@@ -164,7 +160,7 @@ def apply_beast(act_rate):
tseg_minlength=2, mcmc_chains=10, tseg_minlength=2, mcmc_chains=10,
mcmc_thin=mcmc_th, mcmc_seed=10) mcmc_thin=mcmc_th, mcmc_seed=10)
# User-driven ncp selection # User-driven ncp selection
if ncp_choice == 'median': if ncp_choice == 'median':
ncp = beast_result.trend.ncp_median ncp = beast_result.trend.ncp_median
if np.isnan(ncp) or ncp == 0: if np.isnan(ncp) or ncp == 0:
@@ -188,7 +184,7 @@ def apply_beast(act_rate):
ncp = int(ncp) ncp = int(ncp)
# Filter NaNs first — BEAST fills unused cp slots with nan # Filter NaNs first — BEAST fills unused cp slots with nan
valid_cps = beast_result.trend.cp[~np.isnan(beast_result.trend.cp)] # 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
@@ -310,26 +306,23 @@ 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, read_csv from igfash.io import read_mat_cat, read_mat_m, read_mat_mc, read_mat_pdf
from igfash.window import win_CTL, win_CNE from igfash.window import 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, hellinger_dist, cols_to_rows from igfash.compute import get_cdf
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')
@@ -349,9 +342,6 @@ 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:])
@@ -362,7 +352,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')}")
@@ -471,69 +461,66 @@ verbose: {verbose}")
time, mag, lat, lon, depth = read_mat_cat(catalog_file) time, mag, lat, lon, depth = read_mat_cat(catalog_file)
# convert to UTM # Create a GeoDataFrame for the catalog data (initially WGS84 EPSG:4326)
u = utm.from_latlon(lat, lon) catalog_gdf = gpd.GeoDataFrame(
x = u[0] {'depth': depth, 'time': time},
y = u[1] geometry=gpd.points_from_xy(lon, lat),
utm_zone_number = u[2] crs="EPSG:4326"
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]
# 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
# Create an AOI GeoDataFrame and project it to the same UTM CRS
aoi_gdf_utm = gpd.GeoDataFrame(
geometry=gpd.points_from_xy(AOI_lon, AOI_lat),
crs="EPSG:4326"
).to_crs(utm_crs)
# 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
grid_x_min = (x_min // grid_dim) * grid_dim
grid_x_max = ((x_max + grid_dim - 1) // grid_dim) * grid_dim
grid_y_min = (y_min // grid_dim) * grid_dim
grid_y_max = ((y_max + grid_dim - 1) // grid_dim) * grid_dim
# define corners of grid based on global dataset # expand extent until it is square
x_min = x.min() ext_w, ext_h = grid_x_max - grid_x_min, grid_y_max - grid_y_min
y_min = y.min() delta = abs(ext_w - ext_h) / 2
x_max = x.max()
y_max = y.max()
grid_x_max = int(ceil(x_max / grid_dim) * grid_dim)
grid_x_min = int(floor(x_min / 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)
# rectangular grid
nx = int((grid_x_max - grid_x_min) / grid_dim) + 1
ny = int((grid_y_max - grid_y_min) / grid_dim) + 1
# ensure a square grid is used
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 # Shift the shorter axis outward symmetrically
x_range = np.linspace(grid_x_min, grid_x_max, nx) 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)
y_range = np.linspace(grid_y_min, grid_y_max, ny) 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)
logger.debug(f"Grid X range: {x_range}, Y range: {y_range}")
# 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)
cells = shapely.box(X, Y, X + grid_dim, Y + grid_dim)
grid_gdf_utm = gpd.GeoDataFrame(geometry=cells.flatten(), crs=utm_crs)
grid_gdf_latlon = grid_gdf_utm.to_crs("EPSG:4326")
logger.debug(f"Grid extent in UTM XY {grid_gdf_utm.total_bounds}")
logger.debug(f"Grid extent in lat lon {grid_gdf_latlon.total_bounds}")
t_windowed = time t_windowed = time
r_windowed = [[x, y]] r_windowed = [[x, y]]
@@ -556,7 +543,8 @@ 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)
corner = kale.corner(xy_kale_km, quantiles=[0.025, 0.16, 0.50, 0.84, 0.975], cmap='hot') corner = kale.corner(xy_kale_km, quantiles=[0.025, 0.16, 0.50, 0.84, 0.975], cmap='hot')
@@ -712,74 +700,41 @@ verbose: {verbose}")
m_cdf = get_cdf(m_pdf) m_cdf = get_cdf(m_pdf)
fxy = xy_kde[0] centroids_utm = grid_gdf_utm.geometry.centroid.values #extract the centroid of each cell
logger.debug(f"Normalization check; sum of all f(x,y) values = {np.sum(fxy)}") num_points = len(grid_gdf_utm)
xx, yy = np.meshgrid(x_range, y_range) # grid points distances = np.array([shapely.distance(centroids_utm[i], centroids_utm) for i in range(num_points)]) #compute distance between every grid point
grid_gdf_latlon['distance_matrix'] = [distances[i] for i in range(num_points)] #store the distance matrix in the GDF
# 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))
rx_lat = np.zeros(nx * ny)
rx_lon = np.zeros(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)
# 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
#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)
# Select only cells of the grid that are inside the AOI
if use_AOI: if use_AOI:
# Filter out receivers outside the AOI; Find indices where values are OUTSIDE the AOI centroids_latlon = grid_gdf_latlon.geometry.centroid
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)
# AOI grid extent
AOI_rx_x = x_rx[indices_filtered]
AOI_rx_y = y_rx[indices_filtered]
AOI_rx_lat, AOI_rx_lon = utm.to_latlon(AOI_rx_x, AOI_rx_y, utm_zone_number, utm_zone_letter) # Mark grid cells that are within the AOI using vectorized boundary checks
grid_gdf_latlon['AOI'] = (
logger.debug(f"Receiver UTM X range: {AOI_rx_x.min()} to {AOI_rx_x.max()}") (centroids_latlon.x >= AOI_lon[0]) & (centroids_latlon.x <= AOI_lon[1]) &
logger.debug(f"Receiver UTM Y range: {AOI_rx_y.min()} to {AOI_rx_y.max()}") (centroids_latlon.y >= AOI_lat[0]) & (centroids_latlon.y <= AOI_lat[1])
logger.debug(f"Receiver lat range: {AOI_rx_lat.min()} to {AOI_rx_lat.max()}") )
logger.debug(f"Receiver lon range: {AOI_rx_lon.min()} to {AOI_rx_lon.max()}")
else: else:
indices_filtered = indices 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()
centroids_sel = grid_gdf_latlon.loc[grid_gdf_latlon['AOI']].centroid
fr = fxy.flatten() loc_pdf = grid_gdf_latlon['location_PDF'].to_numpy() # extract the previously created location PDF from the GDF
# convert distances from m to km because openquake ground motion models take input distances in kilometres
#distances_sel = distances_sel/1000.0
# 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(distances_sel)} 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 = indices_filtered iter = range(0,len(distances_sel))
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}")
@@ -789,7 +744,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)(rx_lat[i], rx_lon[i], distances[i].flatten(), fr, p, lambdas, imls = [dask.delayed(compute_IMT_exceedance)(centroids_sel.iloc[i].y, centroids_sel.iloc[i].x, distances_sel[i].flatten(), loc_pdf, 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]
@@ -798,9 +753,9 @@ verbose: {verbose}")
else: else:
iml_grid_raw = [] iml_grid_raw = []
iter = indices_filtered iter = range(0,len(distances_sel))
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:
@@ -808,7 +763,7 @@ verbose: {verbose}")
iml = [] iml = []
for i in iter: for i in iter:
iml_i = compute_IMT_exceedance(rx_lat[i], rx_lon[i], distances[i].flatten(), fr, p, lambdas, forecast_len, 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,
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)
@@ -818,127 +773,51 @@ 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)
# create list of one empty list for each imt
iml_grid = [[] for _ in range(len(products))] # final ground motion grids
iml_grid_prep = iml_grid.copy() # temp ground motion grids
#if use_AOI or exclude_low_fxy:
for j in range(0, len(products)):
# 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)
# 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
if use_AOI:
# Update grid extents
grid_x_min = AOI_rx_x.min()
grid_x_max = AOI_rx_x.max()
grid_y_min = AOI_rx_y.min()
grid_y_max = AOI_rx_y.max()
grid_lat_min = AOI_rx_lat.min()
grid_lat_max = AOI_rx_lat.max()
grid_lon_min = AOI_rx_lon.min()
grid_lon_max = AOI_rx_lon.max()
for j in range(0, len(products)): for j, imt in enumerate(products): #generate image overlay for each IMT product
logger.debug(f"{products[j]} values: {iml_grid_raw[j]}")
grid_gdf_latlon.loc[grid_gdf_latlon['AOI'], imt] = iml_grid_raw[j] # insert computed imt grid into GDF
if use_AOI: grid_gdf_latlon_clean = grid_gdf_latlon.dropna(subset=[imt]) # remove null values from grid
# trim grid to remove all nan values
x_plot = grid_gdf_latlon_clean.geometry.centroid.x.values
# Create a boolean mask of non-NaN values y_plot = grid_gdf_latlon_clean.geometry.centroid.y.values
# ~np.isnan() returns True for values and False for NaNs z_plot = grid_gdf_latlon_clean[imt].values
nan_mask = ~np.isnan(iml_grid_prep[j])
vmin = np.nanmin(z_plot)
# Identify valid rows and columns vmax = np.nanmax(z_plot)
# .any(axis=1) checks each row; .any(axis=0) checks each column
row_mask = nan_mask.any(axis=1)
col_mask = nan_mask.any(axis=0)
# Extract the sub-array ---
# np.ix_ creates an open mesh from multiple boolean arrays so they can be broadcast together
iml_grid_prep[j] = iml_grid_prep[j][np.ix_(row_mask, col_mask)]
vmin = np.nanmin(iml_grid_prep[j]) # Generate Image Overlay
vmax = np.nanmax(iml_grid_prep[j]) fig, ax = plt.subplots()
contour = ax.tricontourf(
#iml_grid[j] = np.reshape(iml_grid_prep[j], (nx, ny)).astype(dtype=np.float64) # this reduces values to 8 decimal places x_plot,
#iml_grid_tmp = np.nan_to_num(iml_grid[j]) # change nans to zeroes y_plot,
z_plot,
# upscale the grid, trim, and interpolate if there are at least 10 grid values with range greater than 0.1 levels=200, #linear scale
#if np.count_nonzero(iml_grid_tmp) >= 10 and vmax-vmin > 0.1: cmap="YlOrRd",
# up_factor = 1 )
# iml_grid_hd = resize(iml_grid_tmp, (up_factor * len(iml_grid_tmp), up_factor * len(iml_grid_tmp)), ax.set_aspect('equal') # keep geographic coordinates from stretching
# mode='reflect', anti_aliasing=False) ax.set_axis_off()
# trim_thresh = vmin fig.patch.set_visible(False); ax.patch.set_visible(False)
# 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, ax = plt.subplots(layout=None)
ax.margins(0) # clear any data margins
ax.imshow(iml_grid_hd, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax, interpolation='bilinear', aspect='auto')
ax.axis('off')
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
# 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, transparent=True) plt.savefig(overlay_filename, pad_inches=0, bbox_inches="tight", 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, (grid_lat_min, grid_lon_min, grid_lat_max, grid_lon_max)))) map(float, (south, west, north, east))))
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)
@@ -952,17 +831,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 = products[j] colorbar_title = imt
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="YlOrRd_r",
extent=[0, 1, vmin, vmax_hd]) # Note: extent order is different for vertical extent=[0, 1, vmin, vmax]) # 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_hd, num_ticks) tick_positions = np.linspace(vmin, vmax, 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)