source_modelling.scripts.plot_srf
Plot multi-segment rupture with slip.
1"""Plot multi-segment rupture with slip.""" 2 3from pathlib import Path 4from typing import Annotated, Optional 5 6import numpy as np 7import typer 8 9from pygmt_helper import plotting 10from qcore import coordinates 11from source_modelling import srf 12 13app = typer.Typer() 14 15 16@app.command(help="Plot multi-segment rupture with slip.") 17def plot_srf( 18 srf_ffp: Annotated[ 19 Path, typer.Argument(help="Path to SRF file to plot.", exists=True) 20 ], 21 output_ffp: Annotated[ 22 Path, typer.Argument(help="Output plot image.", dir_okay=False) 23 ], 24 dpi: Annotated[ 25 float, typer.Option(help="Plot output DPI (higher is better)") 26 ] = 300, 27 title: Annotated[Optional[str], typer.Option(help="Plot title to use")] = None, 28 levels: Annotated[ 29 float, 30 typer.Option( 31 help="Plot time as contours of every LEVELS seconds", metavar="LEVELS" 32 ), 33 ] = 1, 34 realisation_ffp: Annotated[ 35 Optional[Path], 36 typer.Option( 37 help="Path to realisation, used to mark jump points.", exists=True 38 ), 39 ] = None, 40 latitude_pad: Annotated[ 41 float, typer.Option(help="Latitude padding to apply (degrees)") 42 ] = 0, 43 longitude_pad: Annotated[ 44 float, typer.Option(help="longitude padding to apply (degrees)") 45 ] = 0, 46 annotations: Annotated[bool, typer.Option(help="Label contours")] = True, 47) -> None: 48 """Plot multi-segment rupture with slip. 49 50 Parameters 51 ---------- 52 srf_ffp : Path 53 Path to SRF file to plot. 54 output_ffp : Path 55 Output plot image. 56 dpi : float 57 Plot output DPI (higher is better). 58 title : Optional[str] 59 Plot title to use 60 levels : float 61 Plot time as contours of every `levels` seconds. 62 realisation_ffp : Optional[Path] 63 Path to realisation, used to mark jump points. 64 latitude_pad : float 65 Latitude padding to apply (degrees). 66 longitude_pad : float 67 Longitude padding to apply (degrees). 68 annotations : bool 69 Label contours. 70 """ 71 srf_data = srf.read_srf(srf_ffp) 72 73 region = ( 74 srf_data.points["lon"].min() - longitude_pad, 75 srf_data.points["lon"].max() + longitude_pad, 76 srf_data.points["lat"].min() - latitude_pad, 77 srf_data.points["lat"].max() + latitude_pad, 78 ) 79 80 # Compute slip limits 81 slip_quantile = srf_data.points["slip"].quantile(0.98) 82 slip_cb_max = max(int(np.round(slip_quantile, -1)), 10) 83 cmap_limits = (0, slip_cb_max, slip_cb_max / 10) 84 85 fig = plotting.gen_region_fig(title, region=region, map_data=None) 86 87 for (_, segment), segment_points in zip( 88 srf_data.header.iterrows(), srf_data.segments 89 ): 90 nstk = segment["nstk"] 91 ndip = segment["ndip"] 92 93 # Create standard slip heatmap. 94 cur_grid = plotting.create_grid( 95 segment_points, 96 "slip", 97 grid_spacing="5e/5e", 98 region=( 99 segment_points["lon"].min(), 100 segment_points["lon"].max(), 101 segment_points["lat"].min(), 102 segment_points["lat"].max(), 103 ), 104 set_water_to_nan=False, 105 ) 106 107 plotting.plot_grid( 108 fig, 109 cur_grid, 110 "hot", 111 cmap_limits, 112 ("white", "black"), 113 transparency=0, 114 reverse_cmap=True, 115 plot_contours=False, 116 cb_label="slip", 117 continuous_cmap=True, 118 ) 119 120 # Plot time contours 121 time_grid = plotting.create_grid( 122 segment_points, 123 "tinit", 124 grid_spacing="5e/5e", 125 region=( 126 segment_points["lon"].min(), 127 segment_points["lon"].max(), 128 segment_points["lat"].min(), 129 segment_points["lat"].max(), 130 ), 131 set_water_to_nan=False, 132 ) 133 fig.grdcontour(levels=1, grid=time_grid, pen="0.1p") 134 135 # Plot bounds of the current segment. 136 corners = segment_points.iloc[[0, nstk - 1, -1, (ndip - 1) * nstk]] 137 fig.plot( 138 x=corners["lon"].iloc[list(range(len(corners))) + [0]].to_list(), 139 y=corners["lat"].iloc[list(range(len(corners))) + [0]].to_list(), 140 pen="0.5p,black,-", 141 ) 142 fig.plot( 143 x=corners["lon"].iloc[:2].to_list(), 144 y=corners["lat"].iloc[:2].to_list(), 145 pen="0.8p,black", 146 ) 147 148 if not annotations: 149 continue 150 151 # Custom annotation implementation. Rough algorithm is: 152 # 1. Compute the number of whole second increments in tinit 153 tinit_max = int(np.round(segment_points["tinit"].max())) 154 tinit_min = int(np.round(segment_points["tinit"].min())) 155 # 2. Provided we have a non-trivial number of increments 156 if tinit_max - tinit_min >= 1: 157 # 3. Iterate over every second between the two and 158 for j in range(tinit_min, tinit_max): 159 # 4. Find the segment point with the closest tinit value to the current second. 160 min_delta = (segment_points["tinit"] - j).abs().min() 161 # 5. Provided that this point has a tinit closest enough to the current second. 162 if min_delta < 0.1: 163 # 6. Get that point and 164 closest_point = segment_points[ 165 (segment_points["tinit"] - j).abs() == min_delta 166 ].iloc[0] 167 # Add an annotation label to the plot. 168 fig.text( 169 text=f"{j}", 170 x=closest_point["lon"], 171 y=closest_point["lat"], 172 font="5p", 173 fill="white", 174 ) 175 176 # Plot the hypocentre. 177 hypocentre = srf_data.points[ 178 srf_data.points["tinit"] == srf_data.points["tinit"].min() 179 ].iloc[0] 180 fig.plot( 181 x=hypocentre["lon"], 182 y=hypocentre["lat"], 183 style="a0.4c", 184 pen="1p,black", 185 fill="white", 186 ) 187 188 # If we are supplied a JSON realisation, we can add labels for jump points. 189 if realisation_ffp: # pragma: no cover 190 # NOTE: this import is here because the workflow is, as yet, 191 # not ready to be installed along-side source modelling. 192 from workflow.realisations import RupturePropagationConfig, SourceConfig 193 194 rupture_propagation_config = RupturePropagationConfig.read_from_realisation( 195 realisation_ffp 196 ) 197 source_config = SourceConfig.read_from_realisation(realisation_ffp) 198 for fault_name, jump_point in rupture_propagation_config.jump_points.items(): 199 parent_name = rupture_propagation_config.rupture_causality_tree[fault_name] 200 if not parent_name: 201 continue 202 source = source_config.source_geometries[fault_name] 203 parent = source_config.source_geometries[parent_name] 204 205 # Ruptures jump from_point --> to_point 206 from_point = parent.fault_coordinates_to_wgs_depth_coordinates( 207 jump_point.from_point 208 ) 209 210 # Find the closest point to the theoretical jump point (so we can lookup the time). 211 closest_from_point_distance_idx = ( 212 coordinates.distance_between_wgs_depth_coordinates( 213 srf_data.points[["lat", "lon", "dep"]].to_numpy() 214 * np.array([1, 1, 1000]), 215 from_point, 216 ) 217 ).argmin() 218 srf_jump_point = srf_data.points.iloc[closest_from_point_distance_idx] 219 220 to_point = source.fault_coordinates_to_wgs_depth_coordinates( 221 jump_point.to_point 222 )[:2] 223 224 fig.plot( 225 x=from_point[1], 226 y=from_point[0], 227 style="t0.4c", 228 pen="1p,black", 229 fill="white", 230 ) 231 fig.text( 232 x=srf_jump_point["lon"], 233 y=srf_jump_point["lat"] - 0.01, 234 font="5p", 235 fill="white", 236 text=f"t_jump = {srf_jump_point['tinit']:.2f}", 237 ) 238 fig.plot( 239 x=to_point[1], 240 y=to_point[0], 241 style="i0.4c", 242 pen="1p,black", 243 fill="white", 244 ) 245 246 fig.savefig( 247 output_ffp, 248 dpi=dpi, 249 anti_alias=True, 250 ) 251 252 253if __name__ == "__main__": 254 app()
app =
<typer.main.Typer object>
@app.command(help='Plot multi-segment rupture with slip.')
def
plot_srf( srf_ffp: Annotated[pathlib.Path, <typer.models.ArgumentInfo object>], output_ffp: Annotated[pathlib.Path, <typer.models.ArgumentInfo object>], dpi: Annotated[float, <typer.models.OptionInfo object>] = 300, title: Annotated[Optional[str], <typer.models.OptionInfo object>] = None, levels: Annotated[float, <typer.models.OptionInfo object>] = 1, realisation_ffp: Annotated[Optional[pathlib.Path], <typer.models.OptionInfo object>] = None, latitude_pad: Annotated[float, <typer.models.OptionInfo object>] = 0, longitude_pad: Annotated[float, <typer.models.OptionInfo object>] = 0, annotations: Annotated[bool, <typer.models.OptionInfo object>] = True) -> None:
17@app.command(help="Plot multi-segment rupture with slip.") 18def plot_srf( 19 srf_ffp: Annotated[ 20 Path, typer.Argument(help="Path to SRF file to plot.", exists=True) 21 ], 22 output_ffp: Annotated[ 23 Path, typer.Argument(help="Output plot image.", dir_okay=False) 24 ], 25 dpi: Annotated[ 26 float, typer.Option(help="Plot output DPI (higher is better)") 27 ] = 300, 28 title: Annotated[Optional[str], typer.Option(help="Plot title to use")] = None, 29 levels: Annotated[ 30 float, 31 typer.Option( 32 help="Plot time as contours of every LEVELS seconds", metavar="LEVELS" 33 ), 34 ] = 1, 35 realisation_ffp: Annotated[ 36 Optional[Path], 37 typer.Option( 38 help="Path to realisation, used to mark jump points.", exists=True 39 ), 40 ] = None, 41 latitude_pad: Annotated[ 42 float, typer.Option(help="Latitude padding to apply (degrees)") 43 ] = 0, 44 longitude_pad: Annotated[ 45 float, typer.Option(help="longitude padding to apply (degrees)") 46 ] = 0, 47 annotations: Annotated[bool, typer.Option(help="Label contours")] = True, 48) -> None: 49 """Plot multi-segment rupture with slip. 50 51 Parameters 52 ---------- 53 srf_ffp : Path 54 Path to SRF file to plot. 55 output_ffp : Path 56 Output plot image. 57 dpi : float 58 Plot output DPI (higher is better). 59 title : Optional[str] 60 Plot title to use 61 levels : float 62 Plot time as contours of every `levels` seconds. 63 realisation_ffp : Optional[Path] 64 Path to realisation, used to mark jump points. 65 latitude_pad : float 66 Latitude padding to apply (degrees). 67 longitude_pad : float 68 Longitude padding to apply (degrees). 69 annotations : bool 70 Label contours. 71 """ 72 srf_data = srf.read_srf(srf_ffp) 73 74 region = ( 75 srf_data.points["lon"].min() - longitude_pad, 76 srf_data.points["lon"].max() + longitude_pad, 77 srf_data.points["lat"].min() - latitude_pad, 78 srf_data.points["lat"].max() + latitude_pad, 79 ) 80 81 # Compute slip limits 82 slip_quantile = srf_data.points["slip"].quantile(0.98) 83 slip_cb_max = max(int(np.round(slip_quantile, -1)), 10) 84 cmap_limits = (0, slip_cb_max, slip_cb_max / 10) 85 86 fig = plotting.gen_region_fig(title, region=region, map_data=None) 87 88 for (_, segment), segment_points in zip( 89 srf_data.header.iterrows(), srf_data.segments 90 ): 91 nstk = segment["nstk"] 92 ndip = segment["ndip"] 93 94 # Create standard slip heatmap. 95 cur_grid = plotting.create_grid( 96 segment_points, 97 "slip", 98 grid_spacing="5e/5e", 99 region=( 100 segment_points["lon"].min(), 101 segment_points["lon"].max(), 102 segment_points["lat"].min(), 103 segment_points["lat"].max(), 104 ), 105 set_water_to_nan=False, 106 ) 107 108 plotting.plot_grid( 109 fig, 110 cur_grid, 111 "hot", 112 cmap_limits, 113 ("white", "black"), 114 transparency=0, 115 reverse_cmap=True, 116 plot_contours=False, 117 cb_label="slip", 118 continuous_cmap=True, 119 ) 120 121 # Plot time contours 122 time_grid = plotting.create_grid( 123 segment_points, 124 "tinit", 125 grid_spacing="5e/5e", 126 region=( 127 segment_points["lon"].min(), 128 segment_points["lon"].max(), 129 segment_points["lat"].min(), 130 segment_points["lat"].max(), 131 ), 132 set_water_to_nan=False, 133 ) 134 fig.grdcontour(levels=1, grid=time_grid, pen="0.1p") 135 136 # Plot bounds of the current segment. 137 corners = segment_points.iloc[[0, nstk - 1, -1, (ndip - 1) * nstk]] 138 fig.plot( 139 x=corners["lon"].iloc[list(range(len(corners))) + [0]].to_list(), 140 y=corners["lat"].iloc[list(range(len(corners))) + [0]].to_list(), 141 pen="0.5p,black,-", 142 ) 143 fig.plot( 144 x=corners["lon"].iloc[:2].to_list(), 145 y=corners["lat"].iloc[:2].to_list(), 146 pen="0.8p,black", 147 ) 148 149 if not annotations: 150 continue 151 152 # Custom annotation implementation. Rough algorithm is: 153 # 1. Compute the number of whole second increments in tinit 154 tinit_max = int(np.round(segment_points["tinit"].max())) 155 tinit_min = int(np.round(segment_points["tinit"].min())) 156 # 2. Provided we have a non-trivial number of increments 157 if tinit_max - tinit_min >= 1: 158 # 3. Iterate over every second between the two and 159 for j in range(tinit_min, tinit_max): 160 # 4. Find the segment point with the closest tinit value to the current second. 161 min_delta = (segment_points["tinit"] - j).abs().min() 162 # 5. Provided that this point has a tinit closest enough to the current second. 163 if min_delta < 0.1: 164 # 6. Get that point and 165 closest_point = segment_points[ 166 (segment_points["tinit"] - j).abs() == min_delta 167 ].iloc[0] 168 # Add an annotation label to the plot. 169 fig.text( 170 text=f"{j}", 171 x=closest_point["lon"], 172 y=closest_point["lat"], 173 font="5p", 174 fill="white", 175 ) 176 177 # Plot the hypocentre. 178 hypocentre = srf_data.points[ 179 srf_data.points["tinit"] == srf_data.points["tinit"].min() 180 ].iloc[0] 181 fig.plot( 182 x=hypocentre["lon"], 183 y=hypocentre["lat"], 184 style="a0.4c", 185 pen="1p,black", 186 fill="white", 187 ) 188 189 # If we are supplied a JSON realisation, we can add labels for jump points. 190 if realisation_ffp: # pragma: no cover 191 # NOTE: this import is here because the workflow is, as yet, 192 # not ready to be installed along-side source modelling. 193 from workflow.realisations import RupturePropagationConfig, SourceConfig 194 195 rupture_propagation_config = RupturePropagationConfig.read_from_realisation( 196 realisation_ffp 197 ) 198 source_config = SourceConfig.read_from_realisation(realisation_ffp) 199 for fault_name, jump_point in rupture_propagation_config.jump_points.items(): 200 parent_name = rupture_propagation_config.rupture_causality_tree[fault_name] 201 if not parent_name: 202 continue 203 source = source_config.source_geometries[fault_name] 204 parent = source_config.source_geometries[parent_name] 205 206 # Ruptures jump from_point --> to_point 207 from_point = parent.fault_coordinates_to_wgs_depth_coordinates( 208 jump_point.from_point 209 ) 210 211 # Find the closest point to the theoretical jump point (so we can lookup the time). 212 closest_from_point_distance_idx = ( 213 coordinates.distance_between_wgs_depth_coordinates( 214 srf_data.points[["lat", "lon", "dep"]].to_numpy() 215 * np.array([1, 1, 1000]), 216 from_point, 217 ) 218 ).argmin() 219 srf_jump_point = srf_data.points.iloc[closest_from_point_distance_idx] 220 221 to_point = source.fault_coordinates_to_wgs_depth_coordinates( 222 jump_point.to_point 223 )[:2] 224 225 fig.plot( 226 x=from_point[1], 227 y=from_point[0], 228 style="t0.4c", 229 pen="1p,black", 230 fill="white", 231 ) 232 fig.text( 233 x=srf_jump_point["lon"], 234 y=srf_jump_point["lat"] - 0.01, 235 font="5p", 236 fill="white", 237 text=f"t_jump = {srf_jump_point['tinit']:.2f}", 238 ) 239 fig.plot( 240 x=to_point[1], 241 y=to_point[0], 242 style="i0.4c", 243 pen="1p,black", 244 fill="white", 245 ) 246 247 fig.savefig( 248 output_ffp, 249 dpi=dpi, 250 anti_alias=True, 251 )
Plot multi-segment rupture with slip.
Parameters
- srf_ffp (Path): Path to SRF file to plot.
- output_ffp (Path): Output plot image.
- dpi (float): Plot output DPI (higher is better).
- title (Optional[str]): Plot title to use
- levels (float):
Plot time as contours of every
levelsseconds. - realisation_ffp (Optional[Path]): Path to realisation, used to mark jump points.
- latitude_pad (float): Latitude padding to apply (degrees).
- longitude_pad (float): Longitude padding to apply (degrees).
- annotations (bool): Label contours.