diff --git a/pyproject.toml b/pyproject.toml index 4367d56..837a6f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ plot-mw-contributions = "visualisation.sources.plot_mw_contributions:app" plot-slip-rise-rake = "visualisation.sources.plot_slip_rise_rake:app" plot-srf-distribution = "visualisation.sources.plot_srf_distribution:app" plot-1d-velocity-model = "visualisation.plot_1d_velocity_model:app" +plot-rupture-path = "visualisation.plot_rupture_path:app" plot-stoch = "visualisation.sources.plot_stoch:app" plot-ts = "visualisation.plot_ts:app" diff --git a/tests/test_plots.py b/tests/test_plots.py index 4a7bbac..1b98960 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -4,9 +4,7 @@ import diffimg import pytest -from visualisation import plot_1d_velocity_model, realisation - -####### Ancestor +from visualisation import plot_1d_velocity_model, plot_rupture_path, realisation from visualisation.sources import ( plot_mw_contributions, plot_rakes, @@ -21,10 +19,9 @@ TEST_DATA_DIR = Path(__file__).parent PLOT_IMAGE_DIRECTORY = Path("wiki/images") +STATIONS_FFP = TEST_DATA_DIR / "realisation" / "stations.ll" DEFAULT_IMAGE_DIFF_TOLERANCE = 0.05 -STATIONS_FFP = Path("tests") / "realisation" / "stations.ll" - @pytest.fixture(scope="module") def plot_image_dir() -> Path: @@ -78,6 +75,12 @@ def output_image_path(tmp_path: Path) -> Path: return tmp_path / "output.png" +@pytest.fixture(scope="module") +def realisation_base_file() -> Path: + """Path to the realisation JSON file.""" + return TEST_DATA_DIR / "realisation" / "realisation.json" + + def assert_images_match( generated_path: Path, expected_path: Path, @@ -297,7 +300,7 @@ def test_plot_velocity_model( ), ], ) -def test_realisation_plotting( +def test_realisation_plot( plot_image_dir: Path, output_image_path: Path, domain_realisation_ffp: Path, @@ -347,3 +350,15 @@ def test_plot_stoch( ) assert_images_match(output_image_path, expected_image_file) + + +def test_plot_rupture_path( + velocity_model_plot_file: Path, + output_image_path: Path, + plot_image_dir: Path, +): + expected_image_file = plot_image_dir / "alpine_hope_1_path.png" + plot_rupture_path.plot_rupture_path_to_file( + velocity_model_plot_file, output_image_path + ) + assert_images_match(output_image_path, expected_image_file) diff --git a/visualisation/plot_rupture_path.py b/visualisation/plot_rupture_path.py new file mode 100644 index 0000000..c1a08f2 --- /dev/null +++ b/visualisation/plot_rupture_path.py @@ -0,0 +1,157 @@ +"""Plot rupture propagation paths for realisations.""" + +from pathlib import Path +from typing import Annotated + +import pygmt +import typer + +from pygmt_helper import plotting +from qcore import cli +from visualisation import realisation, utils +from workflow.realisations import RupturePropagationConfig, SourceConfig + +app = typer.Typer() + + +def plot_rupture_path( + fig: pygmt.Figure, + source_config: SourceConfig, + rup_prop_config: RupturePropagationConfig, +): + """Plot a rupture path. + + The rupture is plotted as a series of directed arrows between two + faults, from the parent fault to a subsequent fault. + + Parameters + ---------- + fig : pygmt.Figure + The pygmt figure to plot on. + source_config : SourceConfig + The definition of the sources. + rup_prop_config : RupturePropagationConfig + The rupture propagation containing the rupture path. + + Examples + -------- + >>> # Create a figure and plot a rupture path + >>> import pygmt + >>> from workflow.realisations import RupturePropagationConfig, SourceConfig + >>> + >>> # Load configurations from files + >>> source_config = SourceConfig.read_from_realisation("path/to/realisation.json") + >>> rup_prop_config = RupturePropagationConfig.read_from_realisation("path/to/realisation.json") + >>> + >>> # Create a PyGMT figure with appropriate region + >>> region = utils.bounding_region_for( + ... [fault.geometry for fault in source_config.source_geometries.values()], + ... latitude_pad=0.5, + ... longitude_pad=0.5 + ... ) + >>> fig = plotting.gen_region_fig("Rupture Path", region, projection="M15c") + >>> + >>> # Plot the rupture path on the figure + >>> plot_rupture_path(fig, source_config, rup_prop_config) + >>> + >>> # Display or save the figure + >>> fig.show() + >>> # Or save to file + >>> fig.savefig("rupture_path.png") + """ + realisation.plot_sources(fig, source_config, fill="white") + + for fault_name, parent_name in rup_prop_config.rupture_causality_tree.items(): + if not parent_name: + continue + + fault = source_config.source_geometries[fault_name] + parent = source_config.source_geometries[parent_name] + parent_point = utils.polygon_nztm_to_pygmt( + parent.geometry + ).representative_point() + fault_point = utils.polygon_nztm_to_pygmt(fault.geometry).representative_point() + data_for_plot = [[parent_point.x, parent_point.y, fault_point.x, fault_point.y]] + + fig.plot( + data=data_for_plot, + style="=0.3c+ea45+s", + pen="0.5p,black", + fill="black", + ) + + initial_fault = source_config.source_geometries[rup_prop_config.initial_fault] + hypocentre = initial_fault.fault_coordinates_to_wgs_depth_coordinates( + rup_prop_config.hypocentre + ) + fig.plot( + x=hypocentre[1], + y=hypocentre[0], + style="a0.3c", + pen="0.3p,black", + fill="gold", + ) + + +@cli.from_docstring(app) +def plot_rupture_path_to_file( + realisation_ffp: Annotated[Path, typer.Argument(dir_okay=False)], + output_ffp: Annotated[Path, typer.Argument(dir_okay=False, writable=True)], + title: Annotated[str | None, typer.Option()] = None, + latitude_pad: Annotated[float, typer.Option(min=0)] = 0, + longitude_pad: Annotated[float, typer.Option(min=0)] = 0, + width: Annotated[float, typer.Option(min=0)] = 17, + subtitle: Annotated[str | None, typer.Option()] = None, +): + """Plot a rupture path from a realisation to a file. + + Parameters + ---------- + realisation_ffp : Path + The realisation to plot. + output_ffp : Path + The output image path. + title : str, optional + The title of the plot. + latitude_pad : float + The latitude padding to apply (in degrees). + longitude_pad : float + The longitude padding to apply (in degrees). + width : float + The width of the plot (in cm). + subtitle : str, optional + A plot subtitle. + + Examples + -------- + >>> from pathlib import Path + >>> + >>> # Plot a rupture path from a realisation file to a PNG + >>> plot_rupture_path_to_file( + ... realisation_ffp=Path("realisations/alpine_fault_M7.8.json"), + ... output_ffp=Path("outputs/alpine_rupture.png"), + ... title="Alpine Fault Rupture Simulation", + ... latitude_pad=0.3, + ... longitude_pad=0.3, + ... width=20, + ... subtitle="Northward propagation scenario" + ... ) + >>> + >>> # Minimal usage with default parameters + >>> plot_rupture_path_to_file( + ... Path("realisations/hikurangi_M8.0.json"), + ... Path("outputs/hikurangi_rupture.png") + ... ) + """ + source_config = SourceConfig.read_from_realisation(realisation_ffp) + rup_prop_config = RupturePropagationConfig.read_from_realisation(realisation_ffp) + region = utils.bounding_region_for( + [fault.geometry for fault in source_config.source_geometries.values()], + latitude_pad=latitude_pad, + longitude_pad=longitude_pad, + ) + fig = plotting.gen_region_fig( + title, region, projection=f"M{width}c", subtitle=subtitle + ) + plot_rupture_path(fig, source_config, rup_prop_config) + fig.savefig(output_ffp) diff --git a/wiki/Domains.md b/wiki/Realisations.md similarity index 55% rename from wiki/Domains.md rename to wiki/Realisations.md index e49b733..429e201 100644 --- a/wiki/Domains.md +++ b/wiki/Realisations.md @@ -131,3 +131,44 @@ $ plot-domain alpine_base_1.json alpine_base_1_with_options.png --latitude-pad 0 triangles), and a legend indicating the number of stations in the domain will be added. See an example station file in [the QuakeCoRE dropbox](https://www.dropbox.com/scl/fi/bb852b1f0rly6cfvs9p9b/geoNet_stats-2023-06-28.ll?rlkey=3y8k5qviy52nbksfdkm1n92yh&st=92bvmzph&dl=0). ![Domain plot showing red station triangles and a legend](images/alpine_base_1_stations.png) +# Plotting Realisation Rupture Paths +The `plot-rupture-path` CLI tool plots a realisation rupture path *prior* to generating an SRF (See [the sources plotting tools](Sources.md) to work with SRFs instead). This can be useful for debugging the rupture propagation process. + +You can find the help text for this tool with `plot-rupture-path --help` + +``` + Usage: plot-rupture-path [OPTIONS] REALISATION_FFP OUTPUT_FFP + + Plot a rupture path from a realisation to a file. + +╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ * realisation_ffp FILE The realisation to plot. [default: None] [required] │ +│ * output_ffp FILE The output image path. [default: None] [required] │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ --title TEXT The title of the plot. [default: None] │ +│ --latitude-pad FLOAT RANGE [x>=0] The latitude padding too apply (in degrees). [default: 0] │ +│ --longitude-pad FLOAT RANGE [x>=0] The longitude padding to apply (in degrees). [default: 0] │ +│ --width FLOAT RANGE [x>=0] The width of the plot (in cm). [default: 17] │ +│ --subtitle TEXT A plot subtitle. [default: None] │ +│ --install-completion Install completion for the current shell. │ +│ --show-completion Show completion for the current shell, to copy it or customize the installation. │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +``` + +Replace `REALISATION_FFP` with the path to your realisation file (e.g., +`realisation.json`) and `OUTPUT_PLOT_FFP` with the desired name for your +output image (e.g., `rupture_path.png`). + +This basic command will create a map plot showing: + +1. The earthquake source geometry (fault plane(s), typically polygons with a thin black line and white fill). +2. The planned path the rupture will take through the faults, indicated by arrows. + +> [!NOTE] +> The arrows do not indicate the precise location that the rupture will jump between faults. They only indicate the order the faults will rupture in. + +![Basic plot showing source geometry and a rupture path](images/alpine_hope_1_rupture_path.png) + +The options `--title`, `--latitude-pad`, `--longitude-pad`, `--width` and `--subtitle` behave as they do for plotting realisation domains. diff --git a/wiki/images/alpine_hope_1_path.png b/wiki/images/alpine_hope_1_path.png new file mode 100644 index 0000000..34078e9 Binary files /dev/null and b/wiki/images/alpine_hope_1_path.png differ