Skip to content
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1c01499
Add to aristotle level 2 interface radio buttons to select the workfl…
ptormene Dec 17, 2024
9cba9bf
Merge remote-tracking branch 'origin/master' into level2
ptormene Dec 19, 2024
643479e
Replace the use_shakemap checkbox with the logic related to the corre…
ptormene Dec 20, 2024
1e67abc
Move rupture-related form fields where they can be edited in case the…
ptormene Dec 20, 2024
4351021
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 7, 2025
8da0b8c
Rename use_pnt_src_from_usgs to use_pnt_rup_from_usgs
ptormene Jan 7, 2025
cdb4134
While retrieving rupture data, disable the approach selector
ptormene Jan 7, 2025
af36555
If the approach is to provide rupture params, enable editing dip and …
ptormene Jan 7, 2025
4550f51
Extract function require_usgs_id
ptormene Jan 7, 2025
43a1cce
Fix workflow in which the user uploads a rupture model
ptormene Jan 7, 2025
88936b3
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 8, 2025
694b280
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 8, 2025
8f179fc
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 9, 2025
8f807a3
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 13, 2025
a0140aa
Fix workflow for approach provide_rup_params
ptormene Jan 13, 2025
3344631
With the approach providing rupture parameters, don't show the usgs_i…
ptormene Jan 13, 2025
1591ded
Use a map to associate each approach with the text of the 'retrieve d…
ptormene Jan 13, 2025
2c0dcf3
Communicate the selected approach to the backend
ptormene Jan 13, 2025
041ceec
Fix shakemap tests
ptormene Jan 14, 2025
36a59f9
Fix test_aristotle_mode
ptormene Jan 14, 2025
31184c1
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 15, 2025
2c56f33
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 15, 2025
3dd521b
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 20, 2025
cfefb6f
Generalize _get_preferred_shakemap -> _get_preferred_item and use it …
ptormene Jan 21, 2025
7cfff81
Improve workflow 'build_rup_from_usgs' allowing the user to select on…
ptormene Jan 21, 2025
e55e344
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 22, 2025
067136d
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 22, 2025
eec9250
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 23, 2025
b8cd1c6
Add the possibility to load rupdic from origin and use it for approac…
ptormene Jan 23, 2025
7c850c6
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 27, 2025
7f19aae
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 28, 2025
8a7340d
Fix style of aristotle approach radio buttons
ptormene Jan 28, 2025
3cb5a72
Remove a couple of empty lines
ptormene Jan 28, 2025
8bb6925
Add the selector for the magnitude scaling relationship (TODO: make i…
ptormene Jan 28, 2025
4dd6171
Use get_available_magnitude_scalerel instead of get_available_scalerel
ptormene Jan 28, 2025
2489526
Fix getting the selected magnitude scaling relationship if it exists
ptormene Jan 28, 2025
233a6dd
Add a test checking that get_rup_dic also returns the nodal planes an…
ptormene Jan 29, 2025
3d5b1c8
Add some draft code to get coordinates from shakemap_polygon.txt when…
ptormene Jan 29, 2025
ce3c82b
Do not attempt to get rupdic twice in case of approach use_finite_rup…
ptormene Jan 29, 2025
44207c7
Use the selected MSR in the calculation
ptormene Jan 30, 2025
c629c5c
Add approach and msr arguments to aristotle.py main_cmd and arist uti…
ptormene Jan 30, 2025
7634e2b
Extract function _get_rup_dic_from_xml
ptormene Jan 30, 2025
c4560c2
Fixed tests
Jan 31, 2025
94532b3
Cleanup
Jan 31, 2025
eb51099
Fix workflow provide_rup (the user uploads a rupture), adding suffix …
ptormene Jan 31, 2025
55c580f
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 31, 2025
5f2cf92
Merge remote-tracking branch 'origin/master' into level2
ptormene Jan 31, 2025
fe80735
Merge remote-tracking branch 'origin/master' into level2
ptormene Feb 5, 2025
7da21e9
Merge remote-tracking branch 'origin/master' into level2
ptormene Feb 5, 2025
9c1d019
Fix check on msr_selector (that was breaking the workflow using the s…
ptormene Feb 5, 2025
f07d3c3
Improve workflow for use_pnt_rup_from_usgs; do not download stations …
ptormene Feb 5, 2025
2c47bf5
Initialize err as a dict instead of None
ptormene Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 107 additions & 26 deletions openquake/hazardlib/shakemap/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from openquake.baselib.node import node_from_xml
from openquake.hazardlib import nrml, sourceconverter
from openquake.hazardlib.source.rupture import get_multiplanar, is_matrix
from openquake.hazardlib.scalerel import get_available_magnitude_scalerel

NOT_FOUND = 'No file with extension \'.%s\' file found'
US_GOV = 'https://earthquake.usgs.gov'
Expand Down Expand Up @@ -71,6 +72,7 @@
}
REQUIRED_IMTS = {'PGA', 'PSA03', 'PSA10'}


@dataclass
class User:
level: int = 0
Expand Down Expand Up @@ -257,6 +259,8 @@ def utc_to_local_time(utc_timestamp, lon, lat):
utc_time = utc_zone.localize(utc_time)
local_zone = pytz.timezone(timezone_str)
local_timestamp = utc_time.astimezone(local_zone)
# NOTE: we may keep microseconds and change the regex validating the format
Comment thread
micheles marked this conversation as resolved.
Outdated
local_timestamp = local_timestamp.replace(microsecond=0)
return local_timestamp


Expand Down Expand Up @@ -402,18 +406,19 @@ def usgs_to_ecd_format(stations, exclude_imts=()):
return df_seismic_non_null


def _get_preferred_shakemap(shakemaps):
preferred_weights = [shakemap['preferredWeight'] for shakemap in shakemaps]
def _get_preferred_item(items):
# items can be for instance shakemaps, moment tensors or finite faults
preferred_weights = [item['preferredWeight'] for item in items]
preferred_idxs = [idx for idx, val in enumerate(preferred_weights)
if val == max(preferred_weights)]
preferred_shakemaps = [shakemaps[idx] for idx in preferred_idxs]
if len(preferred_shakemaps) > 1:
update_times = [shakemap['updateTime'] for shakemap in preferred_shakemaps]
preferred_items = [items[idx] for idx in preferred_idxs]
if len(preferred_items) > 1:
update_times = [item['updateTime'] for item in preferred_items]
latest_idx = update_times.index(max(update_times))
shakemap = preferred_shakemaps[latest_idx]
item = preferred_items[latest_idx]
else:
shakemap = preferred_shakemaps[0]
return shakemap
item = preferred_items[0]
return item


def download_station_data_file(usgs_id, contents, user):
Expand Down Expand Up @@ -472,16 +477,17 @@ def load_rupdic_from_finite_fault(usgs_id, mag, products):
"""
Extract the finite fault properties from products.
NB: if the finite-fault list contains multiple elements we take the
first one.
preferred one.
"""
logging.info('Getting finite-fault properties')
try:
ff = products['finite-fault']
except KeyError:
if 'finite-fault' not in products:
# e.g. us6000phrk
# FIXME: not tested
raise MissingLink('There is no finite-fault info for %s' % usgs_id)
if isinstance(ff, list):
ff = ff[0]
err_msg = f'There is no finite-fault info for {usgs_id}'
err = {"status": "failed", "error_msg": err_msg}
return None, err
ffs = products['finite-fault']
ff = _get_preferred_item(ffs)
p = ff['properties']
lon = float(p['longitude'])
lat = float(p['latitude'])
Expand All @@ -494,7 +500,40 @@ def load_rupdic_from_finite_fault(usgs_id, mag, products):
'require_dip_strike': True,
'pga_map_png': None, 'mmi_map_png': None,
'usgs_id': usgs_id, 'rupture_file': None}
return rupdic
return rupdic, None


def load_rupdic_from_origin(usgs_id, products):
"""
Extract the origin properties from products.
NB: if the origin list contains multiple elements we take the
preferred one.
"""
# TODO: we may try to unify this with the very similar load_rupdic_from_finite_fault
logging.info('Getting origin properties')
if 'origin' not in products:
# FIXME: not tested
err_msg = f'There is no origin info for {usgs_id}'
err = {"status": "failed", "error_msg": err_msg}
return None, err
origins = products['origin']
origin = _get_preferred_item(origins)
p = origin['properties']
mag = float(p['magnitude'])
lon = float(p['longitude'])
lat = float(p['latitude'])
dep = float(p['depth'])
rake = 0.
utc_time = p['eventtime']
local_time = utc_to_local_time(utc_time, lon, lat)
time_event = local_time_to_time_event(local_time)
rupdic = {'lon': lon, 'lat': lat, 'dep': dep,
'mag': mag, 'rake': rake,
'local_timestamp': str(local_time), 'time_event': time_event,
'require_dip_strike': True,
'pga_map_png': None, 'mmi_map_png': None,
'usgs_id': usgs_id, 'rupture_file': None}
return rupdic, None


# NB: not used right now
Expand Down Expand Up @@ -638,7 +677,7 @@ def _contents_properties_shakemap(usgs_id, user, use_shakemap, monitor):
properties = js['properties']

# NB: currently we cannot find a case with missing shakemap
shakemap = _get_preferred_shakemap(properties['products']['shakemap'])
shakemap = _get_preferred_item(properties['products']['shakemap'])
contents = shakemap['contents']

if (user.level == 1 or use_shakemap) and 'download/grid.xml' in contents:
Expand All @@ -657,15 +696,36 @@ def _contents_properties_shakemap(usgs_id, user, use_shakemap, monitor):
return contents, properties, shakemap_array, err


def get_rup_dic(usgs_id, user=User(), use_shakemap=False,
rupture_file=None, station_data_file=None,
monitor=performance.Monitor()):
def _get_nodal_planes(properties):
err = None
Comment thread
micheles marked this conversation as resolved.
Outdated
if 'moment-tensor' not in properties['products']:
err = 'Unable to retrieve information about the nodal solution'
return None, err
moment_tensor = _get_preferred_item(properties['products']['moment-tensor'])
props = moment_tensor['properties']
nodal_planes = {}
for key, value in props.items():
if key.startswith('nodal-plane-'):
parts = key.split('-')
plane = f'NP{parts[2]}'
attr = parts[3] # Get the attribute (i.e. 'dip', 'rake' or 'strike')
if plane not in nodal_planes:
nodal_planes[plane] = {}
nodal_planes[plane][attr] = float(value)
return nodal_planes, err


def get_rup_dic(dic, user=User(), approach='use_shakemap_from_usgs',
use_shakemap=False, rupture_file=None,
station_data_file=None, monitor=performance.Monitor()):
"""
If the rupture_file is None, download a rupture from the USGS site given
the ShakeMap ID, else build the rupture locally with the given usgs_id.

:param usgs_id: ShakeMap ID
:param dic: dictionary with ShakeMap ID and other parameters
:param user: User instance
:param approach: the workflow selected by the user
(default: 'use_shakemap_from_usgs')
:param use_shakemap: download the ShakeMap only if True
:param rupture_file: None
:param station_data_file: None
Expand All @@ -674,6 +734,15 @@ def get_rup_dic(usgs_id, user=User(), use_shakemap=False,
rupdic = {}
rup_data = {}
err = {}
usgs_id = dic['usgs_id']
if approach == 'provide_rup_params':
rupdic = dic.copy()
rupdic.update(rupture_file=rupture_file,
station_data_file=station_data_file,
require_dip_strike=True)
rup = None
return rup, rupdic, err

if rupture_file and rupture_file.endswith('.xml'):
[rup_node] = nrml.read(os.path.join(user.testdir, rupture_file)
if user.testdir else rupture_file)
Expand Down Expand Up @@ -704,18 +773,30 @@ def get_rup_dic(usgs_id, user=User(), use_shakemap=False,
usgs_id, user, use_shakemap, monitor)
if err:
return None, None, err

if 'download/rupture.json' not in contents:
if approach in ['use_pnt_rup_from_usgs', 'build_rup_from_usgs']:
rupdic, err = load_rupdic_from_origin(usgs_id, properties['products'])
if err:
return None, None, err
elif ('download/rupture.json' not in contents
or approach == 'use_finite_rup_from_usgs'):
# happens for us6000f65h in parsers_test
rupdic = load_rupdic_from_finite_fault(
rupdic, err = load_rupdic_from_finite_fault(
usgs_id, properties['mag'], properties['products'])
if not rupdic:
if err:
return None, None, err

if approach == 'build_rup_from_usgs':
rupdic['nodal_planes'], err = _get_nodal_planes(properties)
rupdic['msrs'] = list(get_available_magnitude_scalerel())
if err:
return None, rupdic, err

if not rupdic or approach == 'use_finite_rup_from_usgs':
if not rup_data:
with monitor('Downloading rupture json'):
rup_data, rupture_file = download_rupture_data(
usgs_id, contents, user)
rupdic = convert_rup_data(rup_data, usgs_id, rupture_file, shakemap)

if user.level == 2 and not station_data_file:
with monitor('Downloading stations'):
rupdic['station_data_file'], rupdic['station_data_issue'] = (
Expand Down
11 changes: 8 additions & 3 deletions openquake/hazardlib/shakemap/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def get_oqparams(self, usgs_id, mosaic_models, trts, use_shakemap):
'station_data_file_from_usgs': 'Station data from USGS',
'station_data_file': 'Station data CSV',
'maximum_distance_stations': 'Maximum distance of stations (km)',
'nodal_plane': 'Nodal plane',
'msr': 'Magnitude scaling relationship',
}

ARISTOTLE_FORM_PLACEHOLDERS = {
Expand All @@ -154,6 +156,8 @@ def get_oqparams(self, usgs_id, mosaic_models, trts, use_shakemap):
'station_data_file_from_usgs': '',
'station_data_file': 'Station data CSV',
'maximum_distance_stations': 'float ≥ 0',
'nodal_plane': '',
'msr': '',
}

validators = {
Expand Down Expand Up @@ -256,14 +260,15 @@ def aristotle_validate(POST, user, rupture_file=None, station_data_file=None,
if err:
return None, dic, params, err

# NOTE: in level 1 interface there is no checkbox and the ShakeMap has to be used.
# in level 2 interface the checkbox is unchecked by default
# NOTE: in level 1 interface the ShakeMap has to be used.
# in level 2 interface it depends from the selected approach
use_shakemap = user.level == 1
if 'use_shakemap' in POST:
use_shakemap = POST['use_shakemap'] == 'true'
approach = POST['approach']

rup, rupdic, err = get_rup_dic(
dic['usgs_id'], user, use_shakemap, rupture_file, station_data_file, monitor)
dic, user, approach, use_shakemap, rupture_file, station_data_file, monitor)
if err:
return None, None, None, err
# round floats
Expand Down
27 changes: 20 additions & 7 deletions openquake/hazardlib/tests/shakemap/parsers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,25 @@ def setUp(cls):
def test_1(self):
# wrong usgs_id
_rup, _rupdic, err = get_rup_dic(
'usp0001cc', User(level=2, testdir=''), use_shakemap=True)
{'usgs_id': 'usp0001cc'}, User(level=2, testdir=''),
'use_shakemap_from_usgs', use_shakemap=True)
self.assertIn('Unable to download from https://earthquake.usgs.gov/fdsnws/'
'event/1/query?eventid=usp0001cc&', err['error_msg'])

def test_2(self):
_rup, dic, _err = get_rup_dic('usp0001ccb', user=user, use_shakemap=True)
_rup, dic, _err = get_rup_dic(
{'usgs_id': 'usp0001ccb'}, user=user, approach='use_shakemap_from_usgs',
use_shakemap=True)
self.assertIsNotNone(dic['shakemap_array'])
_rup, dic, _err = get_rup_dic('usp0001ccb', user=user, use_shakemap=False)
_rup, dic, _err = get_rup_dic(
{'usgs_id': 'usp0001ccb'}, user=user, approach='use_shakemap_from_usgs',
use_shakemap=False)
self.assertIsNone(dic['shakemap_array'])

def test_3(self):
_rup, dic, _err = get_rup_dic('us6000f65h', user=user, use_shakemap=True)
_rup, dic, _err = get_rup_dic(
{'usgs_id': 'us6000f65h'}, user=user, approach='use_shakemap_from_usgs',
use_shakemap=True)
self.assertEqual(dic['lon'], -73.475)
self.assertEqual(dic['lat'], 18.408)
self.assertEqual(dic['dep'], 10.0)
Expand All @@ -65,22 +72,28 @@ def test_3(self):

def test_4(self):
# point_rup
_rup, dic, _err = get_rup_dic('us6000jllz', user=user, use_shakemap=True)
_rup, dic, _err = get_rup_dic(
{'usgs_id': 'us6000jllz'}, user=user, approach='use_shakemap_from_usgs',
use_shakemap=True)
self.assertEqual(dic['lon'], 37.0143)
self.assertEqual(dic['lat'], 37.2256)
self.assertEqual(dic['dep'], 10.)
self.assertEqual(dic['require_dip_strike'], True)

def test_5(self):
# 12 vertices instead of 4 in rupture.json
rup, dic, _err = get_rup_dic('us20002926', user=user, use_shakemap=True)
rup, dic, _err = get_rup_dic(
{'usgs_id': 'us20002926'}, user=user, approach='use_shakemap_from_usgs',
use_shakemap=True)
self.assertIsNone(rup)
self.assertEqual(dic['require_dip_strike'], True)
self.assertEqual(dic['rupture_issue'],
'Unable to convert the rupture from the USGS format')

def test_6(self):
rup, dic, _err = get_rup_dic('usp0001ccb', user=user, use_shakemap=True)
rup, dic, _err = get_rup_dic(
{'usgs_id': 'usp0001ccb'}, user=user, approach='use_shakemap_from_usgs',
use_shakemap=True)
self.assertEqual(rup.mag, 6.7)
self.assertEqual(dic['require_dip_strike'], False)
self.assertEqual(dic['station_data_issue'],
Expand Down
9 changes: 5 additions & 4 deletions openquake/hazardlib/tests/shakemap/validate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ def setUp(cls):

def test_1(self):
# no rupture, yes stations
POST = {'usgs_id': 'us6000jllz'}
POST = {'usgs_id': 'us6000jllz', 'approach': 'use_shakemap_from_usgs'}
_rup, rupdic, _params, err = aristotle_validate(POST, user)
self.assertEqual(rupdic['require_dip_strike'], True)
self.assertIn('stations', rupdic['station_data_file'])
self.assertEqual(err, {})

def test_2(self):
POST = {'usgs_id': 'us7000n05d'}
POST = {'usgs_id': 'us7000n05d', 'approach': 'use_shakemap_from_usgs'}
_rup, rupdic, _params, err = aristotle_validate(POST, user)
self.assertEqual(rupdic['rupture_from_usgs'], False)
self.assertEqual(rupdic['require_dip_strike'], True)
Expand Down Expand Up @@ -81,7 +81,8 @@ def test_3(self):
'time_event': 'day',
'trt': 'active shallow crust normal',
'truncation_level': '3',
'usgs_id': 'FromFile'}
'usgs_id': 'FromFile',
'approach': 'provide_rup'}

for stations in (None, 'stationlist_seismic.csv'):
_rup, rupdic, params, err = aristotle_validate(
Expand Down Expand Up @@ -126,7 +127,7 @@ def test_3(self):

def test_4(self):
# for us7000n7n8 the stations.json does not contain stations
POST = {'usgs_id': 'us7000n7n8'}
POST = {'usgs_id': 'us7000n7n8', 'approach': 'use_shakemap_from_usgs'}
_rup, rupdic, _oqparams, err = aristotle_validate(POST, user)
self.assertEqual(rupdic['require_dip_strike'], False)
self.assertEqual(rupdic['mag'], 7.0)
Expand Down
1 change: 0 additions & 1 deletion openquake/hazardlib/valid.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,4 +1507,3 @@ def fragmentno(src):
return -1
fragment = fragments[1].split('!')[0] # strip !b16
return int(fragment)

13 changes: 13 additions & 0 deletions openquake/server/static/css/engine.css
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,16 @@ table#aristotle-losses th td {
.hidden_except_submit label {
display: none;
}

.hidden {
display: none;
}

#aristotle_approach {
display: block; /* each label in its own row */
margin-bottom: 20px; /* Space between radio options */
}

input[type="radio"] {
margin-right: 5px; /* Space between radio button and label text */
}
Loading