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: differentiate interfaces for level 0, 1 and 2 #10157

Open
wants to merge 55 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 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
25fdb76
Move aelo and aristotle forms to separate files to be included depend…
ptormene Nov 4, 2024
0101396
Draft of aristotle forms for levels 0, 1 and 2, to be better refactored
ptormene Nov 5, 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
31c3e60
Merge remote-tracking branch 'origin/master' into import-templates
ptormene Nov 14, 2024
e82df46
Merge branch 'plotshakemap' into import-templates
ptormene Nov 14, 2024
701ac22
Render the aristotle form differently depending from a INTERFACE_LEVE…
ptormene Nov 15, 2024
af19074
Disable the button to run an aristotle calculation until the rupture …
ptormene Nov 15, 2024
caf68ef
Hide rupture-related form fields from level 1 interface
ptormene Nov 15, 2024
7656344
Re-arrange shakemaps in one row
ptormene Nov 15, 2024
268417e
Fix a label after button reset; use interface level 2 by default
ptormene Nov 18, 2024
a3475df
Merge remote-tracking branch 'origin/master' into import-templates
ptormene Nov 18, 2024
0329fd2
Add a adjust_limits function to set xlim and ylim making the plot loo…
ptormene Nov 18, 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
166 changes: 135 additions & 31 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,20 @@ def import_plt():
return plt


def add_borders(ax, read_df=readinput.read_countries_df, buffer=0):
def adjust_limits(x_min, x_max, y_min, y_max, padding=0.5):
# Make the plot display all items with some margin, looking square
x_min, x_max = x_min - padding, x_max + padding
y_min, y_max = y_min - padding, y_max + padding
x_range = x_max - x_min
y_range = y_max - y_min
max_range = max(x_range, y_range)
x_center = (x_min + x_max) / 2
y_center = (y_min + y_max) / 2
xlim = x_center - max_range / 2, x_center + max_range / 2
ylim = y_center - max_range / 2, y_center + max_range / 2
return xlim, ylim

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 +55,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 +91,62 @@ 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()
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)
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, ylim = adjust_limits(min_x, max_x, min_y, max_y)
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 @@ -88,55 +176,73 @@ def plot_avg_gmf(ex, imt):
maxx = avg_gmf['lons'].max()
miny = avg_gmf['lats'].min()
maxy = avg_gmf['lats'].max()
w, h = maxx - minx, maxy - miny
ax.set_xlim(minx - 0.2 * w, maxx + 0.2 * w)
ax.set_ylim(miny - 0.2 * h, maxy + 0.2 * h)

xlim, ylim = adjust_limits(minx, maxx, miny, maxy)
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)
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)
xlim, ylim = adjust_limits(min_x, max_x, min_y, max_y)
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 +253,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
18 changes: 11 additions & 7 deletions openquake/commands/plot_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
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
add_borders, get_assetcol, get_country_iso_codes, add_rupture, adjust_limits)


def main(calc_id: int = -1, site_model=False,
Expand Down Expand Up @@ -83,8 +83,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, _minx, _miny, _maxx, _maxy = add_rupture(ax, rup)
else:
p.scatter(xlon, xlat, marker='*', color='orange',
label='hypocenter', alpha=.5)
Expand All @@ -98,9 +98,13 @@ def main(calc_id: int = -1, site_model=False,
else:
minx, miny, maxx, maxy = get_bbox(
assetcol['lon'], assetcol['lat'], xlon, xlat)
BUF_ANGLE = 1
ax.set_xlim(minx - BUF_ANGLE, maxx + BUF_ANGLE)
ax.set_ylim(miny - BUF_ANGLE, maxy + BUF_ANGLE)
minx = min(minx, _minx)
maxx = max(maxx, _maxx)
miny = min(miny, _miny)
maxy = max(maxy, _maxy)
xlim, ylim = adjust_limits(minx, maxx, miny, maxy)
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)

country_iso_codes = get_country_iso_codes(calc_id, assetcol)
if country_iso_codes is not None:
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
Loading
Loading