diff --git a/openquake/calculators/base.py b/openquake/calculators/base.py index c0996025f76a..a67b009d4436 100644 --- a/openquake/calculators/base.py +++ b/openquake/calculators/base.py @@ -37,7 +37,7 @@ import configparser import collections -from openquake.commands.plot_assets import main as plot_assets +from openquake.commands.plot_assets_webmercator import main as plot_assets from openquake.baselib import general, hdf5, config from openquake.baselib import parallel from openquake.baselib.performance import Monitor, idx_start_stop diff --git a/openquake/calculators/getters.py b/openquake/calculators/getters.py index abdcdc62ce9b..9c920c814eda 100644 --- a/openquake/calculators/getters.py +++ b/openquake/calculators/getters.py @@ -51,7 +51,7 @@ def build_stat_curve(hcurve, imtls, stat, weights, wget, use_rates=False): assert len(poes) == len(weights), (len(poes), len(weights)) L = imtls.size array = numpy.zeros((L, 1)) - + if weights.shape[1] > 1: # IMT-dependent weights # this is slower since the arrays are shorter for imt in imtls: @@ -246,7 +246,7 @@ def build(cls, dstore): dic = collections.defaultdict(lambda: ZeroGetter(mgetter.L, mgetter.R)) for sid in rates: dic[sid] = cls(sid, rates[sid], mgetter.trt_rlzs, mgetter.R) - return dic + return dic def __init__(self, sid, rates, trt_rlzs, R): self.sid = sid diff --git a/openquake/calculators/postproc/aelo_plots.py b/openquake/calculators/postproc/aelo_plots.py index 7a424efd1866..435c17054fca 100644 --- a/openquake/calculators/postproc/aelo_plots.py +++ b/openquake/calculators/postproc/aelo_plots.py @@ -21,12 +21,11 @@ import json import matplotlib as mpl from scipy import interpolate -from openquake.commonlib import readinput from openquake.hazardlib.calc.mean_rates import to_rates from openquake.hazardlib.imt import from_string from openquake.calculators.extract import get_info from openquake.calculators.postproc.plots import ( - add_borders, adjust_limits, auto_limits) + adjust_limits, auto_limits, add_basemap) from PIL import Image ASCE_version = 'ASCE7-22' @@ -555,7 +554,7 @@ def plot_sites(dstore, update_dstore=False): if len(sites) == 1: markersize = 30 marker = 'x' - padding = 20 + padding = 0.005 elif len(sites) < 50: markersize = 1 marker = 'o' @@ -570,7 +569,9 @@ def plot_sites(dstore, update_dstore=False): padding = 0 plt.scatter(lons, lats, c='black', marker=marker, s=markersize) xlim, ylim = auto_limits(ax) - add_borders(ax, readinput.read_countries_df, buffer=0.) + x_min, x_max = xlim + y_min, y_max = ylim + add_basemap(ax, x_min, y_min, x_max, y_max) adjust_limits(ax, xlim, ylim, padding=padding) if update_dstore: bio = io.BytesIO() diff --git a/openquake/calculators/postproc/plots.py b/openquake/calculators/postproc/plots.py index 4f9f6c8d7861..7acea9c67dc6 100644 --- a/openquake/calculators/postproc/plots.py +++ b/openquake/calculators/postproc/plots.py @@ -20,6 +20,8 @@ import os import base64 import numpy +import contextily +from pyproj import Transformer from shapely.geometry import MultiPolygon from openquake.commonlib import readinput, datastore from openquake.hmtk.plotting.patch import PolygonPatch @@ -33,6 +35,22 @@ def import_plt(): return plt +def add_basemap(ax, x_min, y_min, x_max, y_max, + source=contextily.providers.CartoDB.Positron): + # NOTE: another interesting option: + # source = contextily.providers.TopPlusOpen.Grey + img, extent = contextily.bounds2img(x_min, y_min, x_max, y_max, source=source) + ax.imshow(img, extent=extent, interpolation='bilinear', alpha=1) + ax.text( + 0.01, 0.01, # Position: Bottom-left corner (normalized coordinates) + source['attribution'], + transform=ax.transAxes, # Place text relative to axes + fontsize=8, color="black", alpha=0.5, + ha="left", # Horizontal alignment + va="bottom", # Vertical alignment + ) + + def auto_limits(ax): # Set the plot to display all contents and return the limits determined # automatically @@ -127,22 +145,28 @@ def plot_shakemap(shakemap_array, imt, backend=None, figsize=(10, 10), matplotlib.use(backend) _fig, ax = plt.subplots(figsize=figsize) ax.set_aspect('equal') - ax.grid(True) + # 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) + markersize = 0.005 + transformer = Transformer.from_crs('EPSG:4326', 'EPSG:3857', always_xy=True) + x_webmercator, y_webmercator = transformer.transform( + shakemap_array['lon'], shakemap_array['lat']) + coll = ax.scatter(x_webmercator, y_webmercator, c=gmf, cmap='jet', s=markersize, + alpha=0.4) plt.colorbar(coll) if rupture is not None: - add_rupture(ax, rupture, hypo_alpha=0.8, hypo_markersize=8, surf_alpha=0.9, - surf_facecolor='none', surf_linestyle='--') + add_rupture_webmercator( + ax, rupture, hypo_alpha=0.8, hypo_markersize=8, surf_alpha=1, + surf_facecolor='none', surf_linestyle='--') xlim, ylim = auto_limits(ax) - add_borders(ax, alpha=0.2) - adjust_limits(ax, xlim, ylim) + x_min, x_max = xlim + y_min, y_max = ylim + add_basemap(ax, x_min, y_min, x_max, y_max) + adjust_limits(ax, xlim, ylim, padding=0.005) if with_cities: add_cities(ax, xlim, ylim) if return_base64: @@ -155,10 +179,9 @@ def plot_avg_gmf(ex, imt): plt = import_plt() _fig, ax = plt.subplots(figsize=(10, 10)) ax.set_aspect('equal') - ax.grid(True) + # ax.grid(True) ax.set_xlabel('Lon') ax.set_ylabel('Lat') - title = 'Avg GMF for %s' % imt assetcol = get_assetcol(ex.calc_id) if assetcol is not None: @@ -166,17 +189,19 @@ def plot_avg_gmf(ex, imt): if country_iso_codes is not None: title += ' (Countries: %s)' % country_iso_codes ax.set_title(title) - avg_gmf = ex.get('avg_gmf?imt=%s' % imt) gmf = avg_gmf[imt] markersize = 5 - coll = ax.scatter(avg_gmf['lons'], avg_gmf['lats'], c=gmf, cmap='jet', - s=markersize) - plt.colorbar(coll) - + transformer = Transformer.from_crs('EPSG:4326', 'EPSG:3857', always_xy=True) + x_webmercator, y_webmercator = transformer.transform( + avg_gmf['lons'], avg_gmf['lats']) + coll = ax.scatter(x_webmercator, y_webmercator, c=gmf, cmap='jet', s=markersize) + plt.colorbar(coll, ax=ax) xlim, ylim = auto_limits(ax) - add_borders(ax) - adjust_limits(ax, xlim, ylim) + x_min, x_max = xlim + y_min, y_max = ylim + add_basemap(ax, x_min, y_min, x_max, y_max) + adjust_limits(ax, xlim, ylim, padding=1E5) return plt @@ -191,6 +216,21 @@ def add_surface(ax, surface, label, alpha=0.5, facecolor=None, linestyle='-'): ax.fill(*surface.get_surface_boundaries(), **fill_params) +def add_surface_webmercator( + ax, surface, label, alpha=0.5, facecolor=None, linestyle='-'): + transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True) + lon, lat = surface.get_surface_boundaries() + x, y = transformer.transform(lon, lat) # Transform lat/lon to Web Mercator + fill_params = { + 'alpha': alpha, + 'edgecolor': 'grey', + 'label': label + } + if facecolor is not None: + fill_params['facecolor'] = facecolor + ax.fill(x, y, **fill_params) + + 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'): @@ -205,6 +245,25 @@ def add_rupture(ax, rup, hypo_alpha=0.5, hypo_markersize=8, surf_alpha=0.5, linestyle='', markersize=8) +def add_rupture_webmercator( + ax, rup, hypo_alpha=0.5, hypo_markersize=8, surf_alpha=0.5, + surf_facecolor=None, surf_linestyle='-'): + transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True) + if hasattr(rup.surface, 'surfaces'): + for surf_idx, surface in enumerate(rup.surface.surfaces): + add_surface_webmercator( + ax, surface, 'Surface %d' % surf_idx, alpha=surf_alpha, + facecolor=surf_facecolor, linestyle=surf_linestyle) + else: + add_surface_webmercator( + ax, rup.surface, 'Surface', alpha=surf_alpha, + facecolor=surf_facecolor, linestyle=surf_linestyle) + hypo_x, hypo_y = transformer.transform(rup.hypocenter.longitude, + rup.hypocenter.latitude) + ax.plot(hypo_x, hypo_y, marker='*', color='orange', label='Hypocenter', + alpha=hypo_alpha, linestyle='', markersize=hypo_markersize) + + def plot_rupture(rup, backend=None, figsize=(10, 10), with_cities=False, with_borders=True, return_base64=False): # NB: matplotlib is imported inside since it is a costly import @@ -235,6 +294,31 @@ def plot_rupture(rup, backend=None, figsize=(10, 10), return plt +def plot_rupture_webmercator(rup, backend=None, figsize=(10, 10), return_base64=False): + # NB: matplotlib is imported inside since it is a costly import + 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) + add_rupture_webmercator( + ax, rup, hypo_alpha=0.8, hypo_markersize=8, surf_alpha=0.3, + surf_linestyle='--') + xlim, ylim = auto_limits(ax) + x_min, x_max = xlim + y_min, y_max = ylim + add_basemap(ax, x_min, y_min, x_max, y_max, + source=contextily.providers.TopPlusOpen.Color) + adjust_limits(ax, xlim, ylim, padding=1E5) + if return_base64: + return plt_to_base64(plt) + else: + return plt + + def add_surface_3d(ax, surface, label): lon, lat, depth = surface.get_surface_boundaries_3d() lon_grid = numpy.array([[lon[0], lon[1]], [lon[3], lon[2]]]) diff --git a/openquake/commands/plot.py b/openquake/commands/plot.py index b9de075b01d9..8575c4ebc5a0 100644 --- a/openquake/commands/plot.py +++ b/openquake/commands/plot.py @@ -39,8 +39,8 @@ from openquake.calculators.extract import ( Extractor, WebExtractor, clusterize) from openquake.calculators.postproc.plots import ( - plot_avg_gmf, import_plt, add_borders, plot_rupture, plot_rupture_3d, - adjust_limits, auto_limits) + plot_avg_gmf, import_plt, add_borders, plot_rupture, plot_rupture_webmercator, + plot_rupture_3d, adjust_limits, auto_limits) from openquake.calculators.postproc.aelo_plots import ( plot_mean_hcurves_rtgm, plot_disagg_by_src, plot_governing_mce_asce_7_16, plot_mce_spectra, plot_governing_mce, @@ -1125,6 +1125,16 @@ def make_figure_build_rupture(extractors, what): return plot_rupture(rup, with_borders=with_borders) +def make_figure_rupture_webmercator(extractors, what): + """ + $ oq plot "rupture_webmercator?" + """ + [ex] = extractors + dstore = ex.dstore + rup = get_ebrupture(dstore, rup_id=0) + return plot_rupture_webmercator(rup) + + def make_figure_rupture_3d(extractors, what): """ $ oq plot "rupture_3d?" diff --git a/openquake/commands/plot_assets_webmercator.py b/openquake/commands/plot_assets_webmercator.py new file mode 100644 index 000000000000..debd9c2e6b0b --- /dev/null +++ b/openquake/commands/plot_assets_webmercator.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (C) 2017-2023 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 . + +import os +import numpy +import shapely +import logging +from openquake.commonlib import datastore +from openquake.hazardlib.geo.utils import cross_idl # , get_bbox +from openquake.calculators.getters import get_ebrupture +from openquake.calculators.postproc.plots import ( + get_assetcol, get_country_iso_codes, add_rupture_webmercator, adjust_limits, + auto_limits, add_basemap) + + +def main(calc_id: int = -1, site_model=False, + save_to=None, *, show=True, assets_only=False): + """ + Plot the sites and the assets + """ + + # NB: matplotlib is imported inside since it is a costly import + import matplotlib.pyplot as p + from pyproj import Transformer + from openquake.hmtk.plotting.patch import PolygonPatch + + dstore = datastore.read(calc_id) + oq = dstore['oqparam'] + try: + region = oq.region + except KeyError: + region = None + sitecol = dstore['sitecol'] + assetcol = get_assetcol(calc_id) + _fig, ax = p.subplots(figsize=(10, 10)) + transformer = Transformer.from_crs('EPSG:4326', 'EPSG:3857', always_xy=True) + if region: + region_geom = shapely.wkt.loads(region) + region_proj_geom = shapely.ops.transform(transformer.transform, region_geom) + pp = PolygonPatch(region_proj_geom, alpha=0.1) + ax.add_patch(pp) + ax.set_aspect('equal') + # ax.grid(True) + # markersize = 0.005 + # if assets_only: + # markersize_site_model = markersize_assets = 5 + # else: + # markersize_site_model = markersize_sitecol = markersize_assets = 18 + # markersize_discarded = markersize_assets + if site_model and 'site_model' in dstore: + sm = dstore['site_model'] + sm_lons, sm_lats = sm['lon'], sm['lat'] + if len(sm_lons) > 1 and cross_idl(*sm_lons): + sm_lons %= 360 + x_webmercator, y_webmercator = transformer.transform(sm_lons, sm_lats) + p.scatter(x_webmercator, y_webmercator, marker='.', color='orange', + label='site model') # , s=markersize_site_model) + # p.scatter(sitecol.complete.lons, sitecol.complete.lats, marker='.', + # color='gray', label='grid') + x_assetcol_wm, y_assetcol_wm = transformer.transform( + assetcol['lon'], assetcol['lat']) + p.scatter(x_assetcol_wm, y_assetcol_wm, marker='.', color='green', + label='assets') # , s=markersize_assets) + if not assets_only: + x_webmercator, y_webmercator = transformer.transform( + sitecol.lons, sitecol.lats) + p.scatter(x_webmercator, y_webmercator, marker='+', color='black', + label='sites') # , s=markersize_sitecol) + if 'discarded' in dstore: + disc = numpy.unique(dstore['discarded']['lon', 'lat']) + x_webmercator, y_webmercator = transformer.transform( + disc['lon'], disc['lat']) + p.scatter(x_webmercator, y_webmercator, marker='x', color='red', + label='discarded') # , s=markersize_discarded) + if oq.rupture_xml or oq.rupture_dict: + rec = dstore['ruptures'][0] + lon, lat, _dep = rec['hypo'] + xlon, xlat = [lon], [lat] + x_webmercator, y_webmercator = transformer.transform(xlon, xlat) + dist = sitecol.get_cdist(rec) + 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 + rup = get_ebrupture(dstore, rup_id=0) + add_rupture_webmercator( + ax, rup, hypo_alpha=0.8, hypo_markersize=8, surf_alpha=0.3, + surf_linestyle='--') + else: + p.scatter(x_webmercator, y_webmercator, marker='*', color='orange', + label='hypocenter', alpha=.5) + else: + xlon, xlat = [], [] + + if region: + x_min, y_min, x_max, y_max = region_proj_geom.bounds + xlim = (x_min, x_max) + ylim = (y_min, y_max) + else: + xlim, ylim = auto_limits(ax) + x_min, x_max = xlim + y_min, y_max = ylim + add_basemap(ax, x_min, y_min, x_max, y_max) + adjust_limits(ax, xlim, ylim, padding=1E5) + + country_iso_codes = get_country_iso_codes(calc_id, assetcol) + if country_iso_codes is not None: + # NOTE: use following lines to add custom items without changing title + # ax.plot([], [], ' ', label=country_iso_codes) + # ax.legend() + title = 'Countries: %s' % country_iso_codes + ax.legend(title=title) + else: + ax.legend() + + if save_to: + p.savefig(save_to, alpha=True, dpi=300) + logging.info(f'Plot saved to {save_to}') + if show: + p.show() + return p + + +main.calc_id = 'a computation id' +main.site_model = 'plot the site model too' +main.save_to = 'save the plot to this filename' +main.show = 'show the plot' +main.assets_only = 'display assets only (without sites and discarded)' diff --git a/openquake/server/views.py b/openquake/server/views.py index a9ea28cfa556..39e6c929cd86 100644 --- a/openquake/server/views.py +++ b/openquake/server/views.py @@ -60,8 +60,8 @@ from openquake.calculators.export import ( export, AGGRISK_FIELD_DESCRIPTION, EXPOSURE_FIELD_DESCRIPTION) from openquake.calculators.extract import extract as _extract +from openquake.calculators.postproc.plots import plot_shakemap, plot_rupture_webmercator from openquake.calculators.postproc.compute_rtgm import notification_dtype -from openquake.calculators.postproc.plots import plot_shakemap, plot_rupture from openquake.engine import __version__ as oqversion from openquake.engine.export import core from openquake.engine import engine, aelo, impact @@ -936,8 +936,8 @@ def impact_get_rupture_data(request): with_cities=False, return_base64=True, rupture=rup) del rupdic['shakemap_array'] elif rup is not None: - img_base64 = plot_rupture(rup, backend='Agg', figsize=(8, 8), - return_base64=True) + img_base64 = plot_rupture_webmercator(rup, backend='Agg', figsize=(8, 8), + return_base64=True) rupdic['rupture_png'] = img_base64 if request.user.level < 2 and 'warning_msg' in rupdic: # we don't want to show the warning to level 1 users diff --git a/requirements-py311-linux64.txt b/requirements-py311-linux64.txt index cec8d91aa63f..d7be5e0f3a0c 100644 --- a/requirements-py311-linux64.txt +++ b/requirements-py311-linux64.txt @@ -78,3 +78,4 @@ https://wheelhouse.openquake.org/v3/py/zipp-3.17.0-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/pluggy-1.5.0-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/pytest-8.3.3-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/django_appconf-1.0.6-py3-none-any.whl +contextily==1.6.2 # FIXME: replace with the url to our wheelhouse diff --git a/requirements-py312-linux64.txt b/requirements-py312-linux64.txt index 0ea464002602..9aa86e20c154 100644 --- a/requirements-py312-linux64.txt +++ b/requirements-py312-linux64.txt @@ -78,3 +78,4 @@ https://wheelhouse.openquake.org/v3/py/zipp-3.17.0-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/pluggy-1.5.0-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/pytest-8.3.3-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/django_appconf-1.0.6-py3-none-any.whl +contextily==1.6.2 # FIXME: replace with the url to our wheelhouse diff --git a/requirements-py313-linux64.txt b/requirements-py313-linux64.txt index b296b0be6681..33477f8fa578 100644 --- a/requirements-py313-linux64.txt +++ b/requirements-py313-linux64.txt @@ -66,3 +66,4 @@ https://wheelhouse.openquake.org/v3/py/zipp-3.17.0-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/pluggy-1.5.0-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/pytest-8.3.3-py3-none-any.whl https://wheelhouse.openquake.org/v3/py/django_appconf-1.0.6-py3-none-any.whl +contextily==1.6.2 # FIXME: replace with the url to our wheelhouse