From f314feccc630dbce2f6c476fcb4add8dee62486f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 19 Jun 2025 17:13:31 +1200 Subject: [PATCH 01/16] fix plot ts memory consumption --- visualisation/plot_ts.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index 447592a..78b01ef 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -311,11 +311,21 @@ def waveform_coordinates(nztm_corners: np.ndarray, nx: int, ny: int) -> np.ndarr ) return coords_nztm[::-1, :, :] # Reverse order to (x, y) for NZTM +def tslice_get(xyts_file: XYTSFile, index: int, downsample: int = 1) -> np.ndarray: + if downsample > 1: + frame_data = xyts_file.data[index, :, ::downsample, ::downsample] + else: + frame_data = xyts_file.data[index] # shape: (3, ny, nx) + accum = np.zeros(frame_data.shape[1:], dtype=np.float32) + for i in range(3): + np.add(accum, frame_data[i] ** 2, out=accum) + np.sqrt(accum, out=accum) + return accum def render_single_frame( frame_index: int, dt: float, - ground_motion_magnitude: np.ndarray, + xyts_file_path: Path, max_motion: float, cmap: str, source_config: SourceConfig, @@ -375,6 +385,7 @@ def render_single_frame( str The filename of the saved frame. """ + xyts_file = XYTSFile(xyts_file_path) # Create a new figure for this frame cm = 1 / 2.54 fig = plt.figure(figsize=(width * cm, height * cm)) @@ -422,9 +433,10 @@ def render_single_frame( ) # Add the actual data for this frame - current_data = ground_motion_magnitude[frame_index, :, :] + + current_data = tslice_get(xyts_file, frame_index) pcm = ax.pcolormesh( - xr, + xr[0], yr, apply_cmap_with_alpha(current_data, 0, max_motion, cmap=cmap), cmap=cmap, @@ -480,6 +492,7 @@ def animate_low_frequency_mpl_nztm( scale: Annotated[str, typer.Option()] = "10m", shading: Annotated[str, typer.Option()] = "gouraud", frame_count: Annotated[int | None, typer.Option()] = None, + frame_start: Annotated[int, typer.Option()] = 0, width: Annotated[float, typer.Option()] = 30.0, height: Annotated[float, typer.Option()] = 30.0, dpi: Annotated[int, typer.Option()] = 150, @@ -540,8 +553,6 @@ def animate_low_frequency_mpl_nztm( source_config = SourceConfig.read_from_realisation(realisation_ffp) xyts_file = XYTSFile(xyts_ffp) - ground_motion_magnitude = np.linalg.norm(xyts_file.data, axis=1) - nztm_corners = xyts_nztm_corners(xyts_file) map_extent_nztm = map_extents(nztm_corners, padding) @@ -564,7 +575,7 @@ def animate_low_frequency_mpl_nztm( render_frame = functools.partial( render_single_frame, dt=xyts_file.dt, - ground_motion_magnitude=ground_motion_magnitude, + xyts_file_path = xyts_ffp.resolve(), max_motion=max_motion, cmap=cmap, source_config=source_config, @@ -586,11 +597,11 @@ def animate_low_frequency_mpl_nztm( render_frame(0) - with mp.Pool() as pool: + with mp.Pool(4) as pool: # Render all frames in parallel _ = list( tqdm.tqdm( - pool.imap(render_frame, range(1, frame_count)), + pool.imap(render_frame, range(frame_start, frame_start + frame_count)), total=frame_count, unit="frame", desc="Rendering frames", From dc2b06edec7b96dc2ffa06d6445e654e0d37ba1d Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 19 Jun 2025 17:13:59 +1200 Subject: [PATCH 02/16] use all cores --- visualisation/plot_ts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index 78b01ef..7354ad6 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -597,7 +597,7 @@ def animate_low_frequency_mpl_nztm( render_frame(0) - with mp.Pool(4) as pool: + with mp.Pool() as pool: # Render all frames in parallel _ = list( tqdm.tqdm( From 883de987da2d27660bc158e14c12581bfcabc33d Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 19 Jun 2025 17:24:42 +1200 Subject: [PATCH 03/16] downsampling --- visualisation/plot_ts.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index 7354ad6..a2940e1 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -340,6 +340,7 @@ def render_single_frame( width: float, height: float, dpi: int, + downsample ) -> str: """Render a single frame of the animation. @@ -436,9 +437,9 @@ def render_single_frame( current_data = tslice_get(xyts_file, frame_index) pcm = ax.pcolormesh( - xr[0], - yr, - apply_cmap_with_alpha(current_data, 0, max_motion, cmap=cmap), + xr[0, ::downsample], + yr[::downsample, ::downsample], + apply_cmap_with_alpha(current_data[::downsample, ::downsample], 0, max_motion, cmap=cmap), cmap=cmap, vmin=0, vmax=max_motion, @@ -501,6 +502,7 @@ def animate_low_frequency_mpl_nztm( zoom: Annotated[float, typer.Option()] = 1, simple_map: Annotated[bool, typer.Option()] = False, map_quality: Annotated[int, typer.Option()] = 4, + downsample: int = 1 ) -> None: """Render low-frequency output as a 2D video of ground motions. @@ -590,6 +592,7 @@ def animate_low_frequency_mpl_nztm( width=width, height=height, dpi=dpi, + downsample=downsample ) # warm the OSM cache to speed up rendering by rendering the first frame From cd031ffc551dee7c9ab00b874814ba1b8ad27394 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 19 Jun 2025 17:39:09 +1200 Subject: [PATCH 04/16] swap axes?? --- visualisation/plot_ts.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index a2940e1..b651153 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -437,15 +437,16 @@ def render_single_frame( current_data = tslice_get(xyts_file, frame_index) pcm = ax.pcolormesh( - xr[0, ::downsample], yr[::downsample, ::downsample], - apply_cmap_with_alpha(current_data[::downsample, ::downsample], 0, max_motion, cmap=cmap), + xr[::downsample, ::downsample], + current_data[::downsample, ::downsample], cmap=cmap, vmin=0, vmax=max_motion, shading="gouraud", zorder=3, - rasterized=True, + transform=NZTM_CRS, + ) # Add time text From ebc1621128a8ca6b77f538d42c16a782ea1e0668 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 19 Jun 2025 17:43:08 +1200 Subject: [PATCH 05/16] bring back alpha --- visualisation/plot_ts.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index b651153..7a29545 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -439,14 +439,10 @@ def render_single_frame( pcm = ax.pcolormesh( yr[::downsample, ::downsample], xr[::downsample, ::downsample], - current_data[::downsample, ::downsample], - cmap=cmap, - vmin=0, - vmax=max_motion, + apply_cmap_with_alpha(current_data[::downsample, ::downsample], 0, max_motion), shading="gouraud", zorder=3, transform=NZTM_CRS, - ) # Add time text From f5432279bdf71e52b21599f6a0dd27142d1f9763 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 11:44:25 +1200 Subject: [PATCH 06/16] more efficient timeslice extraction --- visualisation/plot_ts.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index 7a29545..b12eb8b 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -312,15 +312,28 @@ def waveform_coordinates(nztm_corners: np.ndarray, nx: int, ny: int) -> np.ndarr return coords_nztm[::-1, :, :] # Reverse order to (x, y) for NZTM def tslice_get(xyts_file: XYTSFile, index: int, downsample: int = 1) -> np.ndarray: + """Retreive a single timeslice from an xyts file with downsampling + + Parameters + ---------- + xyts_file : XYTSFile + The xyts file to retreive from. + index : int + The timeslice index to read from. + downsample : int + If greater than 1, downsample the array in strides of `downsample` in + the x and y direction. + + Returns + ------- + array of float32 + An array of shape (ny, nx) containing the downsampled frame data for `index`. + """ if downsample > 1: frame_data = xyts_file.data[index, :, ::downsample, ::downsample] else: frame_data = xyts_file.data[index] # shape: (3, ny, nx) - accum = np.zeros(frame_data.shape[1:], dtype=np.float32) - for i in range(3): - np.add(accum, frame_data[i] ** 2, out=accum) - np.sqrt(accum, out=accum) - return accum + return np.linalg.norm(frame_data, axis=0) def render_single_frame( frame_index: int, From 8a91f090ecd155c4a1bb78d3a4f7a33f3b53f0c2 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 11:44:52 +1200 Subject: [PATCH 07/16] fix cmap, shading and colourbar --- visualisation/plot_ts.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index b12eb8b..12d9213 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -339,13 +339,14 @@ def render_single_frame( frame_index: int, dt: float, xyts_file_path: Path, - max_motion: float, - cmap: str, source_config: SourceConfig, nztm_corners: np.ndarray, map_extent_nztm: tuple[float, float, float, float], xr: np.ndarray, yr: np.ndarray, + max_motion: float, + cmap: str, + shading: str, simple_map: bool, scale: str, map_quality: int, @@ -353,7 +354,7 @@ def render_single_frame( width: float, height: float, dpi: int, - downsample + downsample: int ) -> str: """Render a single frame of the animation. @@ -452,8 +453,9 @@ def render_single_frame( pcm = ax.pcolormesh( yr[::downsample, ::downsample], xr[::downsample, ::downsample], - apply_cmap_with_alpha(current_data[::downsample, ::downsample], 0, max_motion), - shading="gouraud", + apply_cmap_with_alpha(current_data[::downsample, ::downsample], 0, max_motion, cmap=cmap), + cmap=cmap, vmin=0, vmax=max_motion, + shading=shading, zorder=3, transform=NZTM_CRS, ) @@ -478,7 +480,7 @@ def render_single_frame( plt.tight_layout(rect=[0.05, 0.05, 0.95, 0.95]) cbar = fig.colorbar( - pcm, ax=ax, orientation="vertical", pad=0.02, aspect=30, shrink=0.8 + pcm, ax=ax, orientation="vertical", pad=0.02, aspect=30, shrink=0.8, ) cbar.set_label("Ground Motion (cm/s)") @@ -587,6 +589,7 @@ def animate_low_frequency_mpl_nztm( render_frame = functools.partial( render_single_frame, dt=xyts_file.dt, + shading=shading, xyts_file_path = xyts_ffp.resolve(), max_motion=max_motion, cmap=cmap, From 604dcbe9ee64aa158a5d3419aacbd2658a89928f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 11:56:52 +1200 Subject: [PATCH 08/16] docs(plot-ts): fix numpy doc issues --- visualisation/plot_ts.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index 12d9213..c9c9b9d 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -364,12 +364,6 @@ def render_single_frame( The index of the frame to render. dt : float The time step of the simulation. - ground_motion_magnitude : np.ndarray - The ground motion magnitude data. - max_motion : float - The maximum ground motion value for color scaling. - cmap : str - The colormap to use for the animation. source_config : SourceConfig The source configuration object. nztm_corners : np.ndarray @@ -380,6 +374,12 @@ def render_single_frame( The x coordinates of the gridpoints in NZTM coordinates. yr : np.ndarray The y coordinates of the gridpoints in NZTM coordinates. + max_motion : float + The maximum ground motion value for color scaling. + cmap : str + The colormap to use for the animation. + shading : str + The shading to apply to the colourmap. simple_map : bool If True, disable OpenStreetMap background and use a simple map. scale : str @@ -394,6 +394,10 @@ def render_single_frame( The height of the figure in cm. dpi : int The DPI for the figure. + downsample : int, optional + If greater than 1, downsample the timeslice array in strides of + `downsample` in the x and y direction. Provides a speedup for large + domains. Returns ------- @@ -493,7 +497,7 @@ def render_single_frame( @cli.from_docstring(app, name="xyts") -def animate_low_frequency_mpl_nztm( +def animate_low_frequency( realisation_ffp: Annotated[Path, typer.Argument(exists=True, dir_okay=False)], xyts_ffp: Annotated[Path, typer.Argument(exists=True, dir_okay=False)], output_mp4: Annotated[ @@ -556,6 +560,10 @@ def animate_low_frequency_mpl_nztm( map_quality : int, optional The quality of the map, by default 4. Has no effect if using a simple map. Lower values have lower quality but render faster. + downsample : int, optional + If greater than 1, downsample the timeslice array in strides of + `downsample` in the x and y direction. Provides a speedup for large + domains. """ ffmpeg = shutil.which("ffmpeg") if not ffmpeg: From a0f2de5a6e088cd1e64ced3bb8fa9699adedacc6 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 11:57:18 +1200 Subject: [PATCH 09/16] Update visualisation/plot_ts.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- visualisation/plot_ts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index c9c9b9d..10d5a9f 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -312,12 +312,12 @@ def waveform_coordinates(nztm_corners: np.ndarray, nx: int, ny: int) -> np.ndarr return coords_nztm[::-1, :, :] # Reverse order to (x, y) for NZTM def tslice_get(xyts_file: XYTSFile, index: int, downsample: int = 1) -> np.ndarray: - """Retreive a single timeslice from an xyts file with downsampling + """Retrieve a single timeslice from an xyts file with downsampling Parameters ---------- xyts_file : XYTSFile - The xyts file to retreive from. + The xyts file to retrieve from. index : int The timeslice index to read from. downsample : int From 2144bbb7455c10410a4b16a00dd8f6e18ee743d1 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 11:58:05 +1200 Subject: [PATCH 10/16] Update visualisation/plot_ts.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- visualisation/plot_ts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index 10d5a9f..e94974c 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -518,7 +518,7 @@ def animate_low_frequency( zoom: Annotated[float, typer.Option()] = 1, simple_map: Annotated[bool, typer.Option()] = False, map_quality: Annotated[int, typer.Option()] = 4, - downsample: int = 1 + downsample: Annotated[int, typer.Option()] = 1 ) -> None: """Render low-frequency output as a 2D video of ground motions. From c5d16c17cc454bc2f67a6b2b6389ee34115326e9 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 11:58:54 +1200 Subject: [PATCH 11/16] use downsampling in `tslice_get` --- visualisation/plot_ts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index e94974c..acda76a 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -453,11 +453,11 @@ def render_single_frame( # Add the actual data for this frame - current_data = tslice_get(xyts_file, frame_index) + current_data = tslice_get(xyts_file, frame_index, downsample=downsample) pcm = ax.pcolormesh( yr[::downsample, ::downsample], xr[::downsample, ::downsample], - apply_cmap_with_alpha(current_data[::downsample, ::downsample], 0, max_motion, cmap=cmap), + apply_cmap_with_alpha(current_data, 0, max_motion, cmap=cmap), cmap=cmap, vmin=0, vmax=max_motion, shading=shading, zorder=3, From 87759d2f32d49715901793ec696dd1724a68c1e2 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 12:21:01 +1200 Subject: [PATCH 12/16] use raw bytes output --- requirements.txt | 1 + visualisation/plot_ts.py | 130 +++++++++++++++++++-------------------- 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/requirements.txt b/requirements.txt index a70d53b..562b5db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ diffimg cartopy matplotlib numpy +ffmpeg pygmt pygmt_helper @ git+https://github.com/ucgmsim/pygmt_helper.git source_modelling @ git+https://github.com/ucgmsim/source_modelling.git diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index acda76a..210882f 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -2,6 +2,7 @@ import functools import multiprocessing as mp +import io import os import shutil import subprocess @@ -19,6 +20,7 @@ import matplotlib.colors as mcolors import matplotlib.pyplot as plt import numpy as np +import ffmpeg import shapely import tqdm import typer @@ -355,7 +357,7 @@ def render_single_frame( height: float, dpi: int, downsample: int -) -> str: +) -> bytes: """Render a single frame of the animation. Parameters @@ -401,8 +403,8 @@ def render_single_frame( Returns ------- - str - The filename of the saved frame. + bytes + The raw frame output for the frame index """ xyts_file = XYTSFile(xyts_file_path) # Create a new figure for this frame @@ -489,12 +491,9 @@ def render_single_frame( cbar.set_label("Ground Motion (cm/s)") # Save the frame to a file - frame_filename = f"frame_{frame_index:04d}.png" - plt.savefig(frame_filename, dpi=dpi) - plt.close(fig) - - return frame_filename - + with io.BytesIO() as io_buf: + fig.savefig(io_buf, format='raw', dpi=dpi) + return io_buf.getvalue() @cli.from_docstring(app, name="xyts") def animate_low_frequency( @@ -565,8 +564,8 @@ def animate_low_frequency( `downsample` in the x and y direction. Provides a speedup for large domains. """ - ffmpeg = shutil.which("ffmpeg") - if not ffmpeg: + have_ffmpeg = shutil.which("ffmpeg") + if not have_ffmpeg: print( "You must have ffmpeg installed. See https://ffmpeg.org/download.html.", ) @@ -593,67 +592,64 @@ def animate_low_frequency( frame_count = frame_count or xyts_file.nt xr, yr = waveform_coordinates(nztm_corners, xyts_file.nx, xyts_file.ny) - with tempfile.TemporaryDirectory() as temp_dir: - render_frame = functools.partial( - render_single_frame, - dt=xyts_file.dt, - shading=shading, - xyts_file_path = xyts_ffp.resolve(), - max_motion=max_motion, - cmap=cmap, - source_config=source_config, - nztm_corners=nztm_corners, - map_extent_nztm=map_extent_nztm, - xr=xr, - yr=yr, - simple_map=simple_map, - scale=scale, - map_quality=map_quality, - title=title, - width=width, - height=height, - dpi=dpi, - downsample=downsample - ) + render_frame = functools.partial( + render_single_frame, + dt=xyts_file.dt, + shading=shading, + xyts_file_path = xyts_ffp.resolve(), + max_motion=max_motion, + cmap=cmap, + source_config=source_config, + nztm_corners=nztm_corners, + map_extent_nztm=map_extent_nztm, + xr=xr, + yr=yr, + simple_map=simple_map, + scale=scale, + map_quality=map_quality, + title=title, + width=width, + height=height, + dpi=dpi, + downsample=downsample + ) - # warm the OSM cache to speed up rendering by rendering the first frame - os.chdir(temp_dir) + # warm the OSM cache to speed up rendering by rendering the first frame - render_frame(0) + frames = [render_frame(0)] - with mp.Pool() as pool: - # Render all frames in parallel - _ = list( - tqdm.tqdm( - pool.imap(render_frame, range(frame_start, frame_start + frame_count)), - total=frame_count, - unit="frame", - desc="Rendering frames", - initial=1, - ) + with mp.Pool() as pool: + # Render all frames in parallel + frames.extend( + tqdm.tqdm( + pool.imap(render_frame, range(frame_start, frame_start + frame_count)), + total=frame_count, + unit="frame", + desc="Rendering frames", + initial=1, ) + ) + cm = 1/2.54 + width_px = int(width * cm * dpi) + height_px = int(height * cm * dpi) + # Use ffmpeg to combine frames into video + process = ( + ffmpeg + .input('pipe:0', format='rawvideo', pix_fmt='rgba', s=f'{width_px}x{height_px}') + .output(str(output_mp4), pix_fmt='yuv420p', r=fps, vcodec='libx264', crf=23, vf='pad=ceil(iw/2)*2:ceil(ih/2)*2') + .overwrite_output() + .run_async(pipe_stdin=True) + ) + + # Write the raw video data to FFmpeg's stdin + for frame in frames: + process.stdin.write(frame) + + process.stdin.close() + + # Wait for FFmpeg to finish + process.wait() - # Use ffmpeg to combine frames into video - - ffmpeg_cmd = [ - ffmpeg, - "-y", # Overwrite output file if it exists - "-framerate", - str(fps), - "-i", - "frame_%04d.png", - "-c:v", - "libx264", - "-vf", - "pad=ceil(iw/2)*2:ceil(ih/2)*2", - "-pix_fmt", - "yuv420p", - "-crf", - "23", # Quality setting (lower is better) - str(output_mp4), - ] - - subprocess.run(ffmpeg_cmd, check=True) def non_zero_data_points( From b14558b3f9bad66b73aa9659494dae22a5814ef6 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 12:25:35 +1200 Subject: [PATCH 13/16] close figure when finished frame rendering --- visualisation/plot_ts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index 210882f..e3b4586 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -493,8 +493,10 @@ def render_single_frame( # Save the frame to a file with io.BytesIO() as io_buf: fig.savefig(io_buf, format='raw', dpi=dpi) + fig.close() return io_buf.getvalue() + @cli.from_docstring(app, name="xyts") def animate_low_frequency( realisation_ffp: Annotated[Path, typer.Argument(exists=True, dir_okay=False)], From 401d12f7b82c59cd0bda7aea820a627d8541e66f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 23 Jun 2025 12:26:37 +1200 Subject: [PATCH 14/16] close matplotlib figure properly --- visualisation/plot_ts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index e3b4586..e629db7 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -493,7 +493,7 @@ def render_single_frame( # Save the frame to a file with io.BytesIO() as io_buf: fig.savefig(io_buf, format='raw', dpi=dpi) - fig.close() + plt.close(fig) return io_buf.getvalue() From 66fd4160ab21046af16e127a0d7384e3f174210d Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Tue, 24 Jun 2025 09:49:53 +1200 Subject: [PATCH 15/16] docs(plot-ts): document all parameters --- visualisation/plot_ts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index e629db7..e3f5877 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -366,6 +366,8 @@ def render_single_frame( The index of the frame to render. dt : float The time step of the simulation. + xyts_file_path : Path + The path to the XYTS file. source_config : SourceConfig The source configuration object. nztm_corners : np.ndarray @@ -543,6 +545,8 @@ def animate_low_frequency( The shading method for `plt.pcolormesh`, by default "gouraud". frame_count : int | None, optional The number of frames to display in the animation, by default None (uses all frames). + frame_start : int, optional + The frame to start the animation on. Defaults to zero. width : float, optional The width of the figure in cm, by default 30. height : float, optional From 29e0c64a767876dee3b713e93c753b82122899d0 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Tue, 24 Jun 2025 09:58:14 +1200 Subject: [PATCH 16/16] fix(plot-ts): ruff fixes --- .gitignore | 10 +++++++++ requirements.txt | 2 +- visualisation/plot_ts.py | 47 +++++++++++++++++++++++++--------------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index ad4a1f1..0aa3ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,13 @@ poetry.toml pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python + +# Devenv +.devenv* +devenv.local.nix +devenv.lock +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/requirements.txt b/requirements.txt index 562b5db..5287e84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ diffimg cartopy matplotlib numpy -ffmpeg +ffmpeg-python pygmt pygmt_helper @ git+https://github.com/ucgmsim/pygmt_helper.git source_modelling @ git+https://github.com/ucgmsim/source_modelling.git diff --git a/visualisation/plot_ts.py b/visualisation/plot_ts.py index e3f5877..d919845 100644 --- a/visualisation/plot_ts.py +++ b/visualisation/plot_ts.py @@ -1,12 +1,9 @@ """Create simulation video of surface ground motion levels.""" import functools -import multiprocessing as mp import io -import os +import multiprocessing as mp import shutil -import subprocess -import tempfile from pathlib import Path from typing import Annotated @@ -17,10 +14,10 @@ matplotlib.use("Agg") +import ffmpeg import matplotlib.colors as mcolors import matplotlib.pyplot as plt import numpy as np -import ffmpeg import shapely import tqdm import typer @@ -313,6 +310,7 @@ def waveform_coordinates(nztm_corners: np.ndarray, nx: int, ny: int) -> np.ndarr ) return coords_nztm[::-1, :, :] # Reverse order to (x, y) for NZTM + def tslice_get(xyts_file: XYTSFile, index: int, downsample: int = 1) -> np.ndarray: """Retrieve a single timeslice from an xyts file with downsampling @@ -337,6 +335,7 @@ def tslice_get(xyts_file: XYTSFile, index: int, downsample: int = 1) -> np.ndarr frame_data = xyts_file.data[index] # shape: (3, ny, nx) return np.linalg.norm(frame_data, axis=0) + def render_single_frame( frame_index: int, dt: float, @@ -356,7 +355,7 @@ def render_single_frame( width: float, height: float, dpi: int, - downsample: int + downsample: int, ) -> bytes: """Render a single frame of the animation. @@ -462,7 +461,9 @@ def render_single_frame( yr[::downsample, ::downsample], xr[::downsample, ::downsample], apply_cmap_with_alpha(current_data, 0, max_motion, cmap=cmap), - cmap=cmap, vmin=0, vmax=max_motion, + cmap=cmap, + vmin=0, + vmax=max_motion, shading=shading, zorder=3, transform=NZTM_CRS, @@ -488,13 +489,18 @@ def render_single_frame( plt.tight_layout(rect=[0.05, 0.05, 0.95, 0.95]) cbar = fig.colorbar( - pcm, ax=ax, orientation="vertical", pad=0.02, aspect=30, shrink=0.8, + pcm, + ax=ax, + orientation="vertical", + pad=0.02, + aspect=30, + shrink=0.8, ) cbar.set_label("Ground Motion (cm/s)") # Save the frame to a file with io.BytesIO() as io_buf: - fig.savefig(io_buf, format='raw', dpi=dpi) + fig.savefig(io_buf, format="raw", dpi=dpi) plt.close(fig) return io_buf.getvalue() @@ -521,7 +527,7 @@ def animate_low_frequency( zoom: Annotated[float, typer.Option()] = 1, simple_map: Annotated[bool, typer.Option()] = False, map_quality: Annotated[int, typer.Option()] = 4, - downsample: Annotated[int, typer.Option()] = 1 + downsample: Annotated[int, typer.Option()] = 1, ) -> None: """Render low-frequency output as a 2D video of ground motions. @@ -602,7 +608,7 @@ def animate_low_frequency( render_single_frame, dt=xyts_file.dt, shading=shading, - xyts_file_path = xyts_ffp.resolve(), + xyts_file_path=xyts_ffp.resolve(), max_motion=max_motion, cmap=cmap, source_config=source_config, @@ -617,7 +623,7 @@ def animate_low_frequency( width=width, height=height, dpi=dpi, - downsample=downsample + downsample=downsample, ) # warm the OSM cache to speed up rendering by rendering the first frame @@ -635,14 +641,22 @@ def animate_low_frequency( initial=1, ) ) - cm = 1/2.54 + cm = 1 / 2.54 width_px = int(width * cm * dpi) height_px = int(height * cm * dpi) # Use ffmpeg to combine frames into video process = ( - ffmpeg - .input('pipe:0', format='rawvideo', pix_fmt='rgba', s=f'{width_px}x{height_px}') - .output(str(output_mp4), pix_fmt='yuv420p', r=fps, vcodec='libx264', crf=23, vf='pad=ceil(iw/2)*2:ceil(ih/2)*2') + ffmpeg.input( + "pipe:0", format="rawvideo", pix_fmt="rgba", s=f"{width_px}x{height_px}" + ) + .output( + str(output_mp4), + pix_fmt="yuv420p", + r=fps, + vcodec="libx264", + crf=23, + vf="pad=ceil(iw/2)*2:ceil(ih/2)*2", + ) .overwrite_output() .run_async(pipe_stdin=True) ) @@ -657,7 +671,6 @@ def animate_low_frequency( process.wait() - def non_zero_data_points( x: np.ndarray, y: np.ndarray, z: np.ndarray ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: