Skip to content

Commit 4f3bc00

Browse files
committed
Add local earthquake model support using pygmm
1 parent 2e75fe9 commit 4f3bc00

File tree

4 files changed

+535
-0
lines changed

4 files changed

+535
-0
lines changed

pyincore/models/hazard/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
from pyincore.models.hazard.earthquake import Earthquake
9+
from pyincore.models.hazard.localearthquake import LocalEarthquake
910
from pyincore.models.hazard.hazard import Hazard
1011
from pyincore.models.hazard.hazarddataset import HazardDataset
1112
from pyincore.models.hazard.flood import Flood
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
2+
import pygmm
3+
import numpy as np
4+
import math
5+
6+
from pyincore.models.hazard.earthquake import Earthquake
7+
from pyincore.utils.hazardutil import HazardUtil
8+
from pyincore.models.units import Units
9+
10+
11+
class LocalEarthquake(Earthquake):
12+
13+
def __init__(self, metadata):
14+
super().__init__(metadata)
15+
16+
attenuations = metadata["attenuations"]
17+
18+
# Currently, we only support one attenuation
19+
self.attenuation = list(attenuations.keys())[0]
20+
21+
self.eq_parameters = self.get_pygmm_parameters(metadata)
22+
self.default_units = {"pga": "g", "pgv": "cm/s", "pgd": "cm", "sa": "g"}
23+
24+
def read_hazard_values(self, payload: list, hazard_service=None, **kwargs):
25+
"""Retrieve bulk earthquake hazard values either from the Hazard service or read it from local Dataset
26+
27+
Args:
28+
payload (list):
29+
hazard_service (obj): Hazard service.
30+
kwargs (dict): Keyword arguments.
31+
Returns:
32+
obj: Hazard values.
33+
34+
"""
35+
36+
print("read value from model: "+self.attenuation)
37+
model_class = getattr(pygmm, self.attenuation)
38+
# magnitude = self.magnitude
39+
eq_parameters = self.eq_parameters
40+
41+
response = []
42+
43+
# TODO shouldn't need this
44+
magnitude = eq_parameters["mag"]
45+
for req in payload:
46+
hazard_values = []
47+
for index, req_demand_type in enumerate(req["demands"]):
48+
req_demand = req_demand_type.lower()
49+
req_demand_period = 0.0
50+
51+
if "sa" in req_demand:
52+
req_demand_parts = req_demand.split(" ")
53+
req_demand_period = req_demand_parts[0]
54+
req_demand = req_demand_parts[1]
55+
56+
latitude = float(req["loc"].split(",")[0])
57+
longitude = float(req["loc"].split(",")[1])
58+
# longitude, latitude
59+
60+
local_site = [latitude, longitude]
61+
62+
rupture_distance_ergo = HazardUtil.compute_rupture_distance(magnitude, self.depth, self.mechanism,
63+
self.azimuth_angle, self.dip_angle,
64+
local_site, self.source_site,)
65+
joyner_boore_distance = HazardUtil.compute_joyner_boore_distance(magnitude, self.mechanism, self.depth,
66+
self.azimuth_angle, self.dip_angle,
67+
local_site, self.source_site)
68+
69+
z_tor = self.coseismic_rupture_depth
70+
down_dip_width = self.get_downdip_rupture_width(magnitude, self.mechanism)
71+
72+
horizontal_distance = HazardUtil.compute_horizontal_distance(joyner_boore_distance, z_tor,
73+
down_dip_width, self.dip_angle,
74+
self.azimuth_angle, rupture_distance_ergo)
75+
76+
# TODO - this should come from a shapefile of site conditions maybe?
77+
site_condition = self.default_site_condition
78+
site_vs30 = 760
79+
80+
eq_parameters["dist_rup"] = rupture_distance_ergo
81+
eq_parameters["dist_jb"] = joyner_boore_distance
82+
eq_parameters["dist_x"] = horizontal_distance
83+
84+
scenario = pygmm.Scenario(**eq_parameters)
85+
model = model_class(scenario)
86+
if req_demand == "pga":
87+
raw_hazard_value = model.pga
88+
elif req_demand == "pgv":
89+
raw_hazard_value = model.pgv
90+
elif req_demand == "pgd":
91+
raw_hazard_value = model.pgd
92+
elif req_demand == "sa":
93+
sa_values = model.spec_accels
94+
sa_index_array = np.where(model.periods == float(req_demand_period))
95+
if len(sa_index_array[0]) != 0:
96+
sa_index = sa_index_array[0][0]
97+
else:
98+
sa_index = self.find_nearest(model.periods, float(req_demand_period))
99+
100+
sa_values = model.spec_accels
101+
raw_hazard_value = sa_values[sa_index]
102+
103+
if raw_hazard_value is None:
104+
converted_hazard_value = raw_hazard_value
105+
else:
106+
converted_hazard_value = Units.convert_hazard(raw_hazard_value,
107+
original_demand_units=self.default_units[req_demand],
108+
requested_demand_units=req["units"][index])
109+
110+
hazard_values.append(converted_hazard_value)
111+
112+
req.update({"hazardValues": hazard_values})
113+
response.append(req)
114+
115+
return response
116+
117+
def find_nearest(self, period_array, period):
118+
array = np.asarray(period_array)
119+
idx = (np.abs(period_array - period)).argmin()
120+
return idx
121+
122+
def get_pygmm_parameters(self, metadata):
123+
124+
eq_parameters = metadata["eqParameters"]
125+
magnitude = float(eq_parameters["magnitude"])
126+
event_type = eq_parameters["eventType"]
127+
128+
# TODO - need to handle the multiple attenuation models case
129+
fault_type_map = eq_parameters["faultTypeMap"]
130+
mechanism = fault_type_map[self.attenuation]
131+
132+
if mechanism == "Strike-Slip":
133+
mechanism_pygmm = "SS"
134+
else:
135+
# TODO handle other cases
136+
mechanism_pygmm = mechanism
137+
138+
dip_angle = float(eq_parameters["dipAngle"])
139+
# self.azimuth_angle = eq_parameters["azimuthAngle"]
140+
141+
# TODO remove these later
142+
self.mechanism = mechanism_pygmm
143+
self.dip_angle = dip_angle
144+
145+
self.azimuth_angle = 130.0
146+
self.default_site_condition = "soil"
147+
self.default_site_vs30 = 760
148+
self.source_site = [float(eq_parameters["srcLatitude"]), float(eq_parameters["srcLongitude"])]
149+
self.depth = float(eq_parameters["depth"])
150+
self.coseismic_rupture_depth = float(eq_parameters["coseismicRuptureDepth"])
151+
152+
# TODO all the eq parameters should be mapped into a dictionary that can be passed to the pygmm Scenario so
153+
# we can map IN-CORE parameters to pygmm parameters (e.g. Ztor is depth_tor in pygmm)
154+
155+
eq_parameters = {"mag": magnitude, "site_cond": self.default_site_condition, "v_s30": self.default_site_vs30,
156+
"dip": dip_angle, "mechanism": mechanism_pygmm, "event_type": event_type,
157+
"depth_tor": 3.0, "depth_1_0": 0.0}
158+
159+
return eq_parameters
160+
161+
def get_downdip_rupture_width(self, magnitude, mechanism):
162+
# TODO this should come from a dict of coefficients for different mechanisms
163+
a = -0.76
164+
b = 0.27
165+
166+
return math.pow(10, a + b * magnitude)

pyincore/utils/hazardutil.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright (c) 2024 University of Illinois and others. All rights reserved.
2+
#
3+
# This program and the accompanying materials are made available under the
4+
# terms of the Mozilla Public License v2.0 which accompanies this distribution,
5+
# and is available at https://www.mozilla.org/en-US/MPL/2.0/
6+
7+
import logging
8+
import sys
9+
import math
10+
11+
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
12+
13+
14+
class HazardUtil:
15+
16+
@staticmethod
17+
def compute_rupture_distance(magnitude, depth, mechanism, azimuth_angle, dip_angle, local_site, source_site):
18+
original_distances = HazardUtil.compute_original_distance(local_site, source_site, depth)
19+
20+
rupture_length = HazardUtil.get_subsurface_rupture_length(magnitude, mechanism)
21+
rupture_width = HazardUtil.get_downdip_rupture_width(magnitude, mechanism)
22+
transformed_distances = HazardUtil.compute_transformed_distances(azimuth_angle, dip_angle, original_distances)
23+
24+
if math.fabs(transformed_distances[0]) <= rupture_width / 2.0 and math.fabs(transformed_distances[1]) <= rupture_length / 2.0:
25+
r_rup = math.fabs(transformed_distances[2])
26+
elif (math.fabs(transformed_distances[0]) > rupture_width / 2.0 and math.fabs(
27+
transformed_distances[1]) <= rupture_length / 2.0):
28+
r_rup = math.sqrt(transformed_distances[2] * transformed_distances[2] + math.pow((math.fabs(
29+
transformed_distances[0]) - rupture_width / 2.0), 2))
30+
elif (math.fabs(transformed_distances[0]) <= rupture_width / 2.0 and math.fabs(
31+
transformed_distances[1]) > rupture_length / 2.0):
32+
r_rup = math.sqrt(transformed_distances[2] * transformed_distances[2] + math.pow((math.fabs(
33+
transformed_distances[1]) - rupture_length / 2.0), 2))
34+
else:
35+
r_rup = math.sqrt(transformed_distances[2] * transformed_distances[2] + math.pow((math.fabs(
36+
transformed_distances[0]) - rupture_width / 2.0), 2) + math.pow((math.fabs(transformed_distances[1]) -
37+
rupture_length / 2.0), 2))
38+
39+
if r_rup < 0.0:
40+
print("r_rup is less than 0?")
41+
42+
return r_rup
43+
44+
@staticmethod
45+
def compute_joyner_boore_distance(magnitude, mechanism, depth, azimuth_angle, dip_angle, local_site, source_site):
46+
original_distances = HazardUtil.compute_original_distance(local_site, source_site, depth)
47+
rupture_length = HazardUtil.get_subsurface_rupture_length(magnitude, mechanism)
48+
rupture_width = HazardUtil.get_downdip_rupture_width(magnitude, mechanism)
49+
50+
transformed_distances = HazardUtil.compute_transformed_distances(azimuth_angle, dip_angle, original_distances)
51+
52+
dip_angle_radians = math.radians(dip_angle)
53+
distance_x = transformed_distances[0]
54+
distance_y = transformed_distances[1]
55+
distance_z = transformed_distances[2]
56+
57+
x_proj = distance_x * math.cos(dip_angle_radians) + distance_z * math.sin(dip_angle_radians)
58+
59+
if (math.fabs(x_proj) <= rupture_width * math.cos(dip_angle_radians) / 2.0 and math.fabs(distance_y) <=
60+
rupture_length / 2.0):
61+
r_jb = 0.0
62+
elif (math.fabs(x_proj) > rupture_width * math.cos(dip_angle_radians) / 2.0 and math.fabs(distance_y) <=
63+
rupture_length / 2.0):
64+
r_jb = math.fabs(x_proj) - rupture_width * math.cos(dip_angle_radians) / 2.0
65+
elif (math.fabs(x_proj) <= rupture_width * math.cos(dip_angle_radians) / 2.0 and math.fabs(distance_y) >
66+
rupture_length / 2.0):
67+
r_jb = math.fabs(distance_y) - rupture_length / 2.0
68+
else:
69+
r_jb = math.sqrt(math.pow(math.fabs(x_proj) - rupture_width * math.cos(dip_angle_radians) / 2.0,
70+
2) + math.pow(math.fabs(distance_y) - rupture_length / 2.0, 2))
71+
72+
return r_jb
73+
74+
@staticmethod
75+
def compute_horizontal_distance(joyner_boore_distance, z_tor, down_dip_width, dip_angle, azimuth_angle, r_rup):
76+
dip_angle_radians = math.radians(dip_angle)
77+
azimuth_angle_radians = math.radians(azimuth_angle)
78+
79+
if dip_angle != 90.0:
80+
if azimuth_angle >= 0 and azimuth_angle <= 180 and azimuth_angle != 90.0:
81+
if joyner_boore_distance * math.fabs(math.tan(azimuth_angle_radians)) <= down_dip_width * math.cos(
82+
dip_angle_radians):
83+
rx = joyner_boore_distance * math.tan(azimuth_angle_radians)
84+
else:
85+
rx = joyner_boore_distance * math.tan(azimuth_angle_radians) * math.cos(azimuth_angle_radians -
86+
math.asin((down_dip_width *
87+
math.cos(dip_angle_radians) *
88+
math.cos(azimuth_angle_radians) / joyner_boore_distance)))
89+
elif azimuth_angle == 90.0:
90+
if joyner_boore_distance > 0:
91+
rx = joyner_boore_distance + down_dip_width * math.cos(dip_angle_radians)
92+
else:
93+
if r_rup < z_tor * (1 / math.cos(dip_angle_radians)):
94+
rx = math.sqrt(math.pow(r_rup, 2) - math.pow(z_tor, 2))
95+
else:
96+
rx = r_rup * (1 / math.sin(azimuth_angle_radians)) - z_tor * (1 / math.tan(dip_angle_radians))
97+
else:
98+
rx = joyner_boore_distance * math.sin(azimuth_angle_radians)
99+
else:
100+
rx = joyner_boore_distance * math.sin(azimuth_angle_radians)
101+
102+
return rx
103+
104+
@staticmethod
105+
def compute_original_distance(local_site, source_site, depth):
106+
site_latitude = local_site[0]
107+
site_longitude = local_site[1]
108+
source_latitude = source_site[0]
109+
source_longitude = source_site[1]
110+
111+
original_distance = []
112+
# 6373 is approximate radius of earth
113+
original_distance.append((site_longitude - source_longitude) * math.pi * 6373.0 / 180.0)
114+
original_distance.append((site_latitude - source_latitude) * math.pi * 6373.0 / 180.0)
115+
original_distance.append(depth)
116+
117+
return original_distance
118+
119+
@staticmethod
120+
def compute_transformed_distances(azimuth_angle, dip_angle, original_distances):
121+
azimuth_angle_radians = math.radians(azimuth_angle)
122+
dip_angle_radians = math.radians(dip_angle)
123+
124+
distance_x = (math.cos(dip_angle_radians) * math.cos(azimuth_angle_radians) * original_distances[0] -
125+
math.cos(dip_angle_radians) * math.sin(azimuth_angle_radians) * original_distances[1] -
126+
math.sin(dip_angle_radians) * original_distances[2])
127+
128+
distance_y = math.sin(azimuth_angle_radians) * original_distances[0] + math.cos(
129+
azimuth_angle_radians) * original_distances[1]
130+
131+
distance_z = (math.sin(dip_angle_radians) * math.cos(azimuth_angle_radians) * original_distances[0] -
132+
math.sin(dip_angle_radians) * math.sin(azimuth_angle_radians) * original_distances[1] + math.cos(
133+
dip_angle_radians) * original_distances[2])
134+
135+
transformed_distances = [distance_x, distance_y, distance_z]
136+
137+
return transformed_distances
138+
139+
@staticmethod
140+
def get_subsurface_rupture_length(magnitude, mechanism):
141+
# TODO this should come from a dict of coefficients for different mechanisms
142+
a = -2.57
143+
b = 0.62
144+
145+
return math.pow(10, a + b * magnitude)
146+
147+
@staticmethod
148+
def get_downdip_rupture_width(magnitude, mechanism):
149+
# TODO this should come from a dict of coefficients for different mechanisms
150+
a = -0.76
151+
b = 0.27
152+
153+
return math.pow(10, a + b * magnitude)

0 commit comments

Comments
 (0)