Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ARISTOTLE: plot shakemap #10112

Merged
merged 44 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e170a52
Download shakemap image from the usgs if available
ptormene Oct 22, 2024
49d16bd
Merge remote-tracking branch 'origin/master' into shakemap
ptormene Oct 22, 2024
fcffd9b
Fix aristotle webui tests, including intensity_map in the expected ru…
ptormene Oct 22, 2024
edadf33
Fix another test including intensity_map in the expected keys
ptormene Oct 22, 2024
a93fd57
Display also the pga image
ptormene Oct 23, 2024
c932911
Display images (intensity map and pga) in a single row
ptormene Oct 23, 2024
289ae97
Hide also the pga map when there is an error downloading it
ptormene Oct 23, 2024
4da09c7
Minor improvements
ptormene Oct 23, 2024
a1d74f3
Change some labels [ci skip]
ptormene Oct 23, 2024
a0c3199
WIP: plotting rupture after retrieving rupture data from usgs
ptormene Oct 23, 2024
2d51b7e
Use non-interactive backend to plot the rupture
ptormene Oct 24, 2024
b0e61ad
Do not download/display intensity map and pga map from the usgs
ptormene Oct 24, 2024
ab1b061
Merge remote-tracking branch 'origin/master' into plotrup
ptormene Oct 28, 2024
3e440e2
Merge remote-tracking branch 'origin/master' into plotrup
ptormene Oct 29, 2024
a70919a
Some renaming for consistency
ptormene Oct 30, 2024
a5e594f
Improve plot_rupture, also adding populated places if present in mosa…
ptormene Oct 30, 2024
87b7a2e
Add a note about another possible fallback strategy in case of missin…
ptormene Oct 30, 2024
820f926
Add a parameter to include/exclude populated places
ptormene Oct 30, 2024
58ace37
Make lon_field, lat_field and label_field parametric
ptormene Oct 30, 2024
95d8981
Remove intensity_map and pga from expected keys
ptormene Oct 30, 2024
64f8ea1
Remove an unused import; add an expected key
ptormene Oct 31, 2024
2c5d672
When pressing 'Retrieve rupture data', display avg gmf for mmi and pg…
ptormene Oct 31, 2024
c95b8dd
Minor rename
ptormene Oct 31, 2024
edac784
Some adjustments to the shakemap plots to make them look and fit bett…
ptormene Oct 31, 2024
80dbab1
Fix expected keys in test_aristotle_mode
ptormene Oct 31, 2024
b7f29d8
Fix expected keys for another test
ptormene Oct 31, 2024
9065447
Comment out the import of temporarily unused plot_rupture in views.py
ptormene Oct 31, 2024
f43092e
Merge remote-tracking branch 'origin/master' into plotshakemap
ptormene Nov 4, 2024
7a1504d
Fix circular import and manage the use and deletion of rupdic['shakem…
ptormene Nov 4, 2024
69d8017
Minor refactoring of download_rupture_dict
ptormene Nov 4, 2024
e10c659
Add a couple of notes and revert a change in the text of the 'Retriev…
ptormene Nov 4, 2024
3d2714f
Merge remote-tracking branch 'origin/master' into plotshakemap
ptormene Nov 5, 2024
f724794
If stationlist_url does not contain the usgs_id, try to build the url…
ptormene Nov 5, 2024
ef39301
Revert "If stationlist_url does not contain the usgs_id, try to build…
ptormene Nov 5, 2024
4bb83d2
Merge remote-tracking branch 'origin/master' into plotrup
ptormene Nov 6, 2024
6179d20
Merge remote-tracking branch 'origin/plotrup' into plotshakemap
ptormene Nov 6, 2024
1e6d1dc
Add a docstring to plt_to_base64
ptormene Nov 6, 2024
ec9bd6f
Merge remote-tracking branch 'origin/master' into plotrup
ptormene Nov 6, 2024
1360a27
Merge branch 'plotrup' into plotshakemap
ptormene Nov 6, 2024
31ab5f1
Merge remote-tracking branch 'origin/master' into plotrup
ptormene Nov 7, 2024
ac31674
Merge branch 'plotrup' into plotshakemap
ptormene Nov 7, 2024
5a74817
In the MMI and PGA plots of the ShakeMap, also plot the boundaries of…
ptormene Nov 7, 2024
2617f32
Improve several comments/docstrings [ci skip]
ptormene Nov 7, 2024
76a79ef
Merge remote-tracking branch 'origin/master' into plotshakemap
ptormene Nov 14, 2024
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
5 changes: 5 additions & 0 deletions openquake/calculators/getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ def get_ebrupture(dstore, rup_id): # used in show rupture
return get_ebr(rec, geom, trt)


def get_rupture_from_dstore(dstore, rup_id=0):
ebr = get_ebrupture(dstore, rup_id)
return ebr.rupture


# this is never called directly; get_rupture_getters is used instead
class RuptureGetter(object):
"""
Expand Down
150 changes: 122 additions & 28 deletions openquake/calculators/postproc/plots.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
#
# Copyright (C) 2024, GEM Foundation
#
#
# OpenQuake is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
#
# OpenQuake is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
#
# You should have received a copy of the GNU Affero General Public License
# along with OpenQuake. If not, see <http://www.gnu.org/licenses/>.

import io
import os
import base64
import numpy
from shapely.geometry import MultiPolygon
from openquake.commonlib import readinput, datastore
from openquake.hmtk.plotting.patch import PolygonPatch
from openquake.calculators.getters import get_ebrupture


def import_plt():
Expand All @@ -32,7 +33,7 @@ def import_plt():
return plt


def add_borders(ax, read_df=readinput.read_countries_df, buffer=0):
def add_borders(ax, read_df=readinput.read_countries_df, buffer=0, alpha=0.1):
plt = import_plt()
polys = read_df(buffer)['geom']
cm = plt.get_cmap('RdBu')
Expand All @@ -41,9 +42,27 @@ def add_borders(ax, read_df=readinput.read_countries_df, buffer=0):
colour = cm(1. * idx / num_colours)
if isinstance(poly, MultiPolygon):
for onepoly in poly.geoms:
ax.add_patch(PolygonPatch(onepoly, fc=colour, alpha=0.1))
ax.add_patch(PolygonPatch(onepoly, fc=colour, alpha=alpha))
else:
ax.add_patch(PolygonPatch(poly, fc=colour, alpha=0.1))
ax.add_patch(PolygonPatch(poly, fc=colour, alpha=alpha))
return ax


def add_populated_places(ax, xlim, ylim, read_df=readinput.read_populated_places_df,
lon_field='longitude', lat_field='latitude',
label_field='name'):
data = read_df(lon_field, lat_field, label_field)
if data is None:
return ax
data = data[(data[lon_field] >= xlim[0]) & (data[lon_field] <= xlim[1])
& (data[lat_field] >= ylim[0]) & (data[lat_field] <= ylim[1])]
if len(data) == 0:
return ax
ax.scatter(data[lon_field], data[lat_field], label="Populated places",
s=2, color='black', alpha=0.5)
for _, row in data.iterrows():
ax.text(row[lon_field], row[lat_field], row[label_field], fontsize=7,
ha='right', alpha=0.5)
return ax


Expand All @@ -59,6 +78,64 @@ def get_country_iso_codes(calc_id, assetcol):
return id_0_str


def plt_to_base64(plt):
"""
The base64 string can be passed to a Django template and embedded
directly in HTML, without having to save the image to disk
"""
bio = io.BytesIO()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we are using base64? Is it required by the javascript side? Please add a comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a docstring explaining it

plt.savefig(bio, format='png', bbox_inches='tight')
bio.seek(0)
img_base64 = base64.b64encode(bio.getvalue()).decode('utf-8')
return img_base64


def plot_shakemap(shakemap_array, imt, backend=None, figsize=(10, 10),
with_populated_places=False, return_base64=False,
rupture=None):
plt = import_plt()
if backend is not None:
# we may need to use a non-interactive backend
import matplotlib
matplotlib.use(backend)
_fig, ax = plt.subplots(figsize=figsize)
ax.set_aspect('equal')
ax.grid(True)
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
title = 'Avg GMF for %s' % imt
ax.set_title(title)
gmf = shakemap_array['val'][imt]
markersize = 5
coll = ax.scatter(shakemap_array['lon'], shakemap_array['lat'], c=gmf,
cmap='jet', s=markersize)
plt.colorbar(coll)
ax = add_borders(ax, alpha=0.2)
BUF_ANGLE = 1
min_x = shakemap_array['lon'].min()
max_x = shakemap_array['lon'].max()
min_y = shakemap_array['lat'].min()
max_y = shakemap_array['lat'].max()
if rupture is not None:
ax, rup_min_x, rup_min_y, rup_max_x, rup_max_y = add_rupture(
ax, rupture, hypo_alpha=0.8, hypo_markersize=8, surf_alpha=0.9,
surf_facecolor='none', surf_linestyle='--')
min_x = min(min_x, rup_min_x)
max_x = max(max_x, rup_max_x)
min_y = min(min_y, rup_min_y)
max_y = max(max_y, rup_max_y)
xlim = (min_x - BUF_ANGLE, max_x + BUF_ANGLE)
ylim = (min_y - BUF_ANGLE, max_y + BUF_ANGLE)
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)
if with_populated_places:
ax = add_populated_places(ax, xlim, ylim)
if return_base64:
return plt_to_base64(plt)
else:
return plt


def plot_avg_gmf(ex, imt):
plt = import_plt()
_fig, ax = plt.subplots(figsize=(10, 10))
Expand Down Expand Up @@ -94,49 +171,68 @@ def plot_avg_gmf(ex, imt):
return plt


def add_surface(ax, surface, label):
ax.fill(*surface.get_surface_boundaries(), alpha=.5, edgecolor='grey',
label=label)
def add_surface(ax, surface, label, alpha=0.5, facecolor=None, linestyle='-'):
fill_params = {
'alpha': alpha,
'edgecolor': 'grey',
'label': label
}
if facecolor is not None:
fill_params['facecolor'] = facecolor
ax.fill(*surface.get_surface_boundaries(), **fill_params)
return surface.get_bounding_box()


def add_rupture(ax, dstore, rup_id=0):
ebr = get_ebrupture(dstore, rup_id)
rup = ebr.rupture
def add_rupture(ax, rup, hypo_alpha=0.5, hypo_markersize=8, surf_alpha=0.5,
surf_facecolor=None, surf_linestyle='-'):
if hasattr(rup.surface, 'surfaces'):
min_x = 180
max_x = -180
min_y = 90
max_y = -90
for surf_idx, surface in enumerate(rup.surface.surfaces):
min_x_, max_x_, max_y_, min_y_ = add_surface(
ax, surface, 'Surface %d' % surf_idx)
ax, surface, 'Surface %d' % surf_idx, alpha=surf_alpha,
facecolor=surf_facecolor, linestyle=surf_linestyle)
min_x = min(min_x, min_x_)
max_x = max(max_x, max_x_)
min_y = min(min_y, min_y_)
max_y = max(max_y, max_y_)
else:
min_x, max_x, max_y, min_y = add_surface(ax, rup.surface, 'Surface')
min_x, max_x, max_y, min_y = add_surface(
ax, rup.surface, 'Surface', alpha=surf_alpha, facecolor=surf_facecolor,
linestyle=surf_linestyle)
ax.plot(rup.hypocenter.x, rup.hypocenter.y, marker='*',
color='orange', label='Hypocenter', alpha=.5,
color='orange', label='Hypocenter', alpha=hypo_alpha,
linestyle='', markersize=8)
return ax, min_x, min_y, max_x, max_y


def plot_rupture(dstore):
def plot_rupture(rup, backend=None, figsize=(10, 10),
with_populated_places=False, return_base64=False):
# NB: matplotlib is imported inside since it is a costly import
plt = import_plt()
_fig, ax = plt.subplots(figsize=(10, 10))
if backend is not None:
# we may need to use a non-interactive backend
import matplotlib
matplotlib.use(backend)
_fig, ax = plt.subplots(figsize=figsize)
ax.set_aspect('equal')
ax.grid(True)
# assuming there is only 1 rupture, so rup_id=0
ax, min_x, min_y, max_x, max_y = add_rupture(ax, dstore, rup_id=0)
ax, min_x, min_y, max_x, max_y = add_rupture(ax, rup)
ax = add_borders(ax)
BUF_ANGLE = 4
ax.set_xlim(min_x - BUF_ANGLE, max_x + BUF_ANGLE)
ax.set_ylim(min_y - BUF_ANGLE, max_y + BUF_ANGLE)
BUF_ANGLE = 1
xlim = (min_x - BUF_ANGLE, max_x + BUF_ANGLE)
ylim = (min_y - BUF_ANGLE, max_y + BUF_ANGLE)
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)
if with_populated_places:
ax = add_populated_places(ax, xlim, ylim)
ax.legend()
return plt
if return_base64:
return plt_to_base64(plt)
else:
return plt


def add_surface_3d(ax, surface, label):
Expand All @@ -147,13 +243,11 @@ def add_surface_3d(ax, surface, label):
ax.plot_surface(lon_grid, lat_grid, depth_grid, alpha=0.5, label=label)


def plot_rupture_3d(dstore):
def plot_rupture_3d(rup):
# NB: matplotlib is imported inside since it is a costly import
plt = import_plt()
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ebr = get_ebrupture(dstore, rup_id=0)
rup = ebr.rupture
if hasattr(rup.surface, 'surfaces'):
for surf_idx, surface in enumerate(rup.surface.surfaces):
add_surface_3d(ax, surface, 'Surface %d' % surf_idx)
Expand Down
8 changes: 6 additions & 2 deletions openquake/commands/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from openquake.hazardlib.geo.utils import PolygonPlotter
from openquake.hazardlib.contexts import Effect, get_effect_by_mag
from openquake.hazardlib.calc.filters import getdefault, IntegrationDistance
from openquake.calculators.getters import get_rupture_from_dstore
from openquake.calculators.extract import (
Extractor, WebExtractor, clusterize)
from openquake.calculators.postproc.plots import (
Expand Down Expand Up @@ -58,6 +59,7 @@ def make_figure_magdist(extractors, what):
ax.legend()
return plt


def make_figure_hcurves(extractors, what):
"""
$ oq plot "hcurves?kind=mean&imt=PGA&site_id=0"
Expand Down Expand Up @@ -1042,7 +1044,8 @@ def make_figure_rupture(extractors, what):
"""
[ex] = extractors
dstore = ex.dstore
return plot_rupture(dstore)
rup = get_rupture_from_dstore(dstore, rup_id=0)
return plot_rupture(rup)


def make_figure_rupture_3d(extractors, what):
Expand All @@ -1051,7 +1054,8 @@ def make_figure_rupture_3d(extractors, what):
"""
[ex] = extractors
dstore = ex.dstore
return plot_rupture_3d(dstore)
rup = get_rupture_from_dstore(dstore, rup_id=0)
return plot_rupture_3d(rup)


def plot_wkt(wkt_string):
Expand Down
5 changes: 3 additions & 2 deletions openquake/commands/plot_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import logging
from openquake.commonlib import datastore
from openquake.hazardlib.geo.utils import cross_idl, get_bbox
from openquake.calculators.getters import get_rupture_from_dstore
from openquake.calculators.postproc.plots import (
add_borders, get_assetcol, get_country_iso_codes)
from openquake.calculators.postproc.plots import add_rupture
Expand Down Expand Up @@ -83,8 +84,8 @@ def main(calc_id: int = -1, site_model=False,
print('rupture(%s, %s), dist=%s' % (lon, lat, dist))
if os.environ.get('OQ_APPLICATION_MODE') == 'ARISTOTLE':
# assuming there is only 1 rupture, so rup_id=0
ax, _min_x, _min_y, _max_x, _max_y = add_rupture(
ax, dstore, rup_id=0)
rup = get_rupture_from_dstore(dstore, rup_id=0)
ax, _min_x, _min_y, _max_x, _max_y = add_rupture(ax, rup)
else:
p.scatter(xlon, xlat, marker='*', color='orange',
label='hypocenter', alpha=.5)
Expand Down
3 changes: 3 additions & 0 deletions openquake/commands/tests/independence_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@


class IndependenceTestCase(unittest.TestCase):
def test_hazardlib(self):
assert_independent('openquake.hazardlib', 'openquake.calculators')

def test_risklib(self):
assert_independent('openquake.risklib', 'openquake.commonlib')
assert_independent('openquake.risklib', 'openquake.calculators')
Expand Down
29 changes: 29 additions & 0 deletions openquake/commonlib/readinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -1662,6 +1662,20 @@ def read_geometries(fname, code, buffer=0):
return pandas.DataFrame(dict(code=codes, geom=geoms))


@functools.lru_cache()
def read_populated_places(fname, lon_name, lat_name, label_name):
"""
Reading coordinates and names of populated places from a CSV file

:returns: a Pandas DataFrame
"""
data = pandas.read_csv(fname)
expected_colnames_set = {lon_name, lat_name, label_name}
if not expected_colnames_set.issubset(data.columns):
raise ValueError(f"CSV file must contain {expected_colnames_set} columns.")
return data


def read_mosaic_df(buffer):
"""
:returns: a DataFrame of geometries for the mosaic models
Expand All @@ -1680,6 +1694,21 @@ def read_countries_df(buffer=0.1):
return read_geometries(fname, 'shapeGroup', buffer)


def read_populated_places_df(lon_field='longitude', lat_field='latitude',
label_field='name'):
"""
Reading from a 'worldcities.csv' file in the mosaic_dir, if present, or returning
None otherwise

:returns: a DataFrame of coordinates and names of populated places
"""
mosaic_dir = config.directory.mosaic_dir
fname = os.path.join(mosaic_dir, 'worldcities.csv')
if not os.path.isfile(fname):
return
return read_populated_places(fname, lon_field, lat_field, label_field)


def read_source_models(fnames, hdf5path='', **converterparams):
"""
:param fnames: a list of source model files
Expand Down
2 changes: 2 additions & 0 deletions openquake/engine/aristotle.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def get_aristotle_params(arist):
inputs = {'exposure': [arist.exposure_hdf5],
'job_ini': '<in-memory>'}
rupdic = get_rupture_dict(arist.rupture_dict, arist.ignore_shakemap)
if 'shakemap_array' in rupdic:
del rupdic['shakemap_array']
if arist.station_data_file is None:
# NOTE: giving precedence to the station_data_file uploaded via form
try:
Expand Down
Loading
Loading