24 Commits

Author SHA1 Message Date
910d933467 Merge branch 'master' into ftong-patch-cython 2025-07-10 08:55:13 +02:00
e06f4a5a05 edit error message for clarity 2025-07-09 15:23:42 +02:00
2906af6918 improve error message when user provides magnitude file incompatible with ground motion model 2025-07-09 15:09:44 +02:00
dd84829b6d define m_max when magnitude distribution estimation is not enabled; take from user's m_file 2025-07-09 13:27:44 +02:00
5b25b93090 convert custom activity rate from list to numpy array of type 'double' to satisfy cython input 2025-07-09 13:15:04 +02:00
9fd7de6124 Merge pull request 'Update src/seismic_hazard_forecasting.py' (!18) from ftong-patch-mc into master
Reviewed-on: #18
Reviewed-by: asia <asia@noreply.example.org>
2025-07-09 12:05:59 +02:00
d56aaeef39 Update src/seismic_hazard_forecasting.py 2025-07-09 11:25:55 +02:00
b4ef228a03 Update src/seismic_hazard_forecasting.py 2025-07-09 11:08:04 +02:00
fc2e8b53c7 Merge pull request 'July2025updates' (!17) from July2025updates into master
Reviewed-on: #17
Reviewed-by: asia <asia@noreply.example.org>
2025-07-08 14:49:31 +02:00
664ab1025b catch time_win_duration values that are too large 2025-07-07 16:42:57 +02:00
36378f4d6c Update src/seismic_hazard_forecasting.py 2025-07-07 16:36:40 +02:00
1244655a68 Update src/seismic_hazard_forecasting.py 2025-07-07 15:07:33 +02:00
70c08f47ab add info msg about m_max 2025-07-07 14:49:35 +02:00
60ae1c96cd raise exception for null magnitude values 2025-07-07 14:43:42 +02:00
1ea6c85ab2 Update src/seismic_hazard_forecasting.py 2025-07-07 13:45:17 +02:00
84440152eb Update src/seismic_hazard_forecasting.py 2025-07-07 13:40:02 +02:00
a941939493 remove processing of depth as it is not used 2025-07-07 13:25:05 +02:00
f4d2cfc3cd add check that m_max is not too large for the ground motion model 2025-07-02 15:43:05 +02:00
bd1ad26693 change default m_max from 3 to 1 mag unit above max mag in catalog 2025-07-02 15:33:42 +02:00
bc4d57a5ab catch null result from ground motion forecasting 2025-07-02 14:27:33 +02:00
921de3f423 fix - activity rate figure no longer cut off at the bottom 2025-07-01 16:24:59 +02:00
b287715f44 use distances in km instead of m 2025-07-01 16:09:24 +02:00
eb14b7d235 update package version logging, remove cpu tracking 2025-07-01 16:06:11 +02:00
3ffcf2c4db Added licence 2025-06-23 14:51:08 +02:00
2 changed files with 69 additions and 40 deletions

13
LICENCE.txt Normal file
View File

@@ -0,0 +1,13 @@
Copyright 2025 Institute of Geophysics, Polish Academy of Sciences
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -17,7 +17,7 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
then the program looks for a label of 'Mw' for magnitude in the catalog then the program looks for a label of 'Mw' for magnitude in the catalog
mc: The magnitude of completeness (Mc) of the catalog mc: The magnitude of completeness (Mc) of the catalog
m_max:M_max. The magnitude distribution is estimated for the range from Mc to M_max. If no value is provided, m_max:M_max. The magnitude distribution is estimated for the range from Mc to M_max. If no value is provided,
then the program sets M_max to be 3 magnitude units above the maximum magnitude value in the catalog. then the program sets M_max to be 1 magnitude units above the maximum magnitude value in the catalog.
m_kde_method: The kernel density estimator to use. m_kde_method: The kernel density estimator to use.
xy_select: If True, perform an estimation of the magnitude distribution using KDE with the chosen KDE method xy_select: If True, perform an estimation of the magnitude distribution using KDE with the chosen KDE method
grid_dim: The grid cell size (in metres) of the final ground motion product map. A smaller cell size will grid_dim: The grid cell size (in metres) of the final ground motion product map. A smaller cell size will
@@ -45,22 +45,19 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
""" """
import sys import sys
from importlib.metadata import version
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 from math import ceil, floor, isnan
import numpy as np import numpy as np
import scipy
import obspy
import dask import dask
from dask.diagnostics import ProgressBar # use Dask progress bar from dask.diagnostics import ProgressBar # use Dask progress bar
import kalepy as kale import kalepy as kale
import utm import utm
from skimage.transform import resize from skimage.transform import resize
import psutil
import openquake.engine
import igfash import igfash
from igfash.io import read_mat_cat, read_mat_m, read_mat_pdf, read_csv from igfash.io import read_mat_cat, read_mat_m, read_mat_mc, read_mat_pdf, read_csv
from igfash.window import win_CTL, 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
@@ -84,6 +81,7 @@ def main(catalog_file, mc_file, pdf_file, m_file, m_select, mag_label, mc, m_max
m_range = [None] m_range = [None]
else: else:
m_range = read_mat_m(m_file) m_range = read_mat_m(m_file)
m_max = m_range[-1] # take m_max from the m_file
if verbose: if verbose:
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@@ -105,25 +103,13 @@ verbose: {verbose}")
# 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 {np.__version__}") logger.debug(f"Numpy version {version('numpy')}")
logger.debug(f"Scipy version {scipy.__version__}") logger.debug(f"Scipy version {version('scipy')}")
logger.debug(f"Obspy version {obspy.__version__}") logger.debug(f"Obspy version {version('obspy')}")
logger.debug(f"Openquake version {openquake.engine.__version__}") logger.debug(f"Openquake version {version('openquake.engine')}")
logger.debug(f"Igfash version {igfash.__version__}") logger.debug(f"Igfash version {version('igfash')}")
logger.debug(f"Rbeast version {version('rbeast')}")
# print number of cpu cores available logger.debug(f"Dask version {version('dask')}")
ncpu = psutil.cpu_count(logical=False)
logger.debug(f"Number of cpu cores available: {ncpu}")
for process in psutil.process_iter():
with process.oneshot():
# cpu = process.cpu_percent()
cpu = process.cpu_percent() / ncpu
if cpu > 1:
logger.debug(f"{process.name()}, {cpu}")
logger.debug(f"BASELINE CPU LOAD% {psutil.cpu_percent(interval=None, percpu=True)}")
dask.config.set(scheduler='processes') dask.config.set(scheduler='processes')
@@ -145,12 +131,18 @@ verbose: {verbose}")
time, mag, lat, lon, depth = read_mat_cat(catalog_file, mag_label=mag_label, catalog_label='Catalog') time, mag, lat, lon, depth = read_mat_cat(catalog_file, mag_label=mag_label, catalog_label='Catalog')
# check for null magnitude values
m_null_idx = np.where(np.isnan(mag))[0]
if len(m_null_idx) > 0:
msg = f"There are null values in the magnitude column of the catalog at indices {m_null_idx}"
logger.error(msg)
raise Exception(msg)
if mc != None: if mc != None:
logger.info("Mc value provided by user") logger.info("Mc value provided by user")
trim_to_mc = True trim_to_mc = True
elif mc_file != None: elif mc_file != None:
logger.info("Mc estimation output file provided; selecting largest Mc from the list") logger.info("Mc estimation output file provided; selecting largest Mc from the list")
mc = read_mc(mc_file) mc = read_mat_mc(mc_file)
trim_to_mc = True trim_to_mc = True
else: else:
logger.info("No Mc provided; using all magnitudes from the catalog") logger.info("No Mc provided; using all magnitudes from the catalog")
@@ -166,9 +158,10 @@ verbose: {verbose}")
lat = np.delete(lat, indices) lat = np.delete(lat, indices)
lon = np.delete(lon, indices) lon = np.delete(lon, indices)
# if user does not provide a m_max, set m_max to 3 magnitude units above max magnitude in catalog # if user does not provide a m_max, set m_max to 1 magnitude unit above max magnitude in catalog
if m_max == None: if m_max == None:
m_max = mag.max() + 3.0 m_max = mag.max() + 1.0
logger.info(f"No m_max was given. Therefore m_max is automatically set to: {m_max}")
start = timer() start = timer()
@@ -233,8 +226,6 @@ verbose: {verbose}")
y_min = y.min() y_min = y.min()
x_max = x.max() x_max = x.max()
y_max = y.max() y_max = y.max()
z_min = depth.min()
z_max = depth.max()
grid_x_max = int(ceil(x_max / grid_dim) * grid_dim) grid_x_max = int(ceil(x_max / grid_dim) * grid_dim)
grid_x_min = int(floor(x_min / grid_dim) * grid_dim) grid_x_min = int(floor(x_min / grid_dim) * grid_dim)
@@ -330,8 +321,8 @@ verbose: {verbose}")
lambdas = [None] lambdas = [None]
if custom_rate != None and forecast_select: if custom_rate != None and forecast_select:
logger.info(f"Using activity rate specified by user: {custom_rate} per {time_unit}") logger.info(f"Using activity rate specified by user: {custom_rate} per {time_unit}")
lambdas = [custom_rate] lambdas = np.array([custom_rate], dtype='d')
lambdas_perc = [1] lambdas_perc = np.array([1], dtype='d')
elif rate_select: elif rate_select:
logger.info(f"Activity rate modeling selected") logger.info(f"Activity rate modeling selected")
@@ -349,6 +340,12 @@ verbose: {verbose}")
elif time_unit == 'years': elif time_unit == 'years':
multiplicator = 1 / 365 multiplicator = 1 / 365
# Raise an exception when time_win_duration from the user is too large relative to the catalog
if time_win_duration/multiplicator > 0.5*(time[-1] - time[0]):
msg = "Activity rate estimation time window must be less than half the catalog length. Use a shorter time window."
logger.error(msg)
raise Exception(msg)
# Selects dates in datenum format and procceeds to forecast value # Selects dates in datenum format and procceeds to forecast value
start_date = datenum_data[-1] - (2 * time_win_duration / multiplicator) 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]] dates_calc = [date for date in datenum_data if start_date <= date <= datenum_data[-1]]
@@ -365,7 +362,7 @@ verbose: {verbose}")
act_rate, bin_counts, bin_edges, out, pprs, rt, idx, u_e = calc_bins(np.array(datenum_data), time_unit, 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, time_win_duration, dates_calc,
rate_forecast, rate_unc_high, rate_unc_low, rate_forecast, rate_unc_high, rate_unc_low,
multiplicator, quiet=True) multiplicator, quiet=True, figsize=(14,9))
# Assign probabilities # Assign probabilities
lambdas, lambdas_perc = lambda_probs(act_rate, dates_calc, bin_edges) lambdas, lambdas_perc = lambda_probs(act_rate, dates_calc, bin_edges)
@@ -377,18 +374,29 @@ verbose: {verbose}")
if forecast_select: if forecast_select:
products = products_string.split() products = products_string.split()
logger.info( logger.info(f"Ground motion forecasting selected with ground motion model {model} and IMT products {products_string}")
f"Ground motion forecasting selected with ground motion model {model} and IMT products {products_string}")
# validate m_max against the grond motion model
models_anthro_limited = ['Lasocki2013', 'Atkinson2015', 'ConvertitoEtAl2012Geysers'] # these models require that m_max<=4.5
if m_max > 4.5 and model in models_anthro_limited:
if m_file is None:
msg = f"The selected ground motion model {model} is only valid for magnitudes up to 4.5. Please select a lower maximum magnitude."
else:
msg = f"The selected ground motion model {model} is only valid for magnitudes up to 4.5, but the provided magnitude file includes values up to {m_max}. Please adjust the magnitude range in the file accordingly."
logger.error(msg)
raise Exception(msg)
if not xy_select: if not xy_select:
msg = "Event location distribution modeling was not selected; cannot continue..." msg = "Event location distribution modeling was not selected; cannot continue..."
logger.error(msg) logger.error(msg)
raise Exception(msg) raise Exception(msg)
elif m_pdf[0] == None:
if m_pdf[0] == None:
msg = "Magnitude distribution modeling was not selected and magnitude PDF file was not provided; cannot continue..." msg = "Magnitude distribution modeling was not selected and magnitude PDF file was not provided; cannot continue..."
logger.error(msg) logger.error(msg)
raise Exception(msg) raise Exception(msg)
elif lambdas[0] == None:
if lambdas[0] == 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)
@@ -426,7 +434,10 @@ verbose: {verbose}")
rx_lat[i], rx_lon[i] = utm.to_latlon(x_rx[i], y_rx[i], utm_zone_number, 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 utm_zone_letter) # get receiver location as lat,lon
# experimental - compute ground motion only at grid points that have minimum probability density of thresh_fxy # 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: if exclude_low_fxy:
indices = list(np.where(fxy.flatten() > thresh_fxy)[0]) indices = list(np.where(fxy.flatten() > thresh_fxy)[0])
else: else:
@@ -455,7 +466,7 @@ verbose: {verbose}")
imls = [dask.delayed(compute_IMT_exceedance)(rx_lat[i], rx_lon[i], distances[i].flatten(), fr, 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=2.0, rx_label=i, log_level=logging.DEBUG, imt=imt, IMT_min=0.0, IMT_max=2.0, rx_label=i,
rtol=0.1, use_cython=False) for i in iter] rtol=0.1, use_cython=True) for i in iter]
iml = dask.compute(*imls) iml = dask.compute(*imls)
iml_grid_raw.append(list(iml)) iml_grid_raw.append(list(iml))
@@ -467,7 +478,7 @@ verbose: {verbose}")
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(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 = 2.0, rx_label = i, rtol = 0.1, use_cython=False) IMT_max = 2.0, rx_label = i, rtol = 0.1, use_cython=True)
iml.append(iml_i) iml.append(iml_i)
logger.info(f"Estimated {imt} at rx {i} is {iml_i}") logger.info(f"Estimated {imt} at rx {i} is {iml_i}")
iml_grid_raw.append(iml) iml_grid_raw.append(iml)
@@ -475,6 +486,11 @@ 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")
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)
raise Exception(msg)
# create list of one empty list for each imt # create list of one empty list for each imt
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