Plot computed results#

Load profile and layer results as pandas DataFrames and plot them with matplotlib.

  1"""
  2Plot computed profile and layer results using pandas DataFrames from model.Results.
  3
  4Requires RSSeismic with scripting server connected and a .rsseismicfile with motions.
  5matplotlib is installed with RSSeismicScripting.
  6
  7  python examples/example_plot_results_v1.py "E:\\path\\to\\project.rsseismicfile"
  8  python examples/example_plot_results_v1.py --port 60058 --motion ChiChi project.rsseismicfile
  9  python examples/example_plot_results_v1.py --no-compute project.rsseismicfile
 10  python examples/example_plot_results_v1.py --output-dir ./plots project.rsseismicfile
 11"""
 12
 13from __future__ import annotations
 14
 15import argparse
 16import os
 17import sys
 18
 19from rsseismic import RSSeismicApplication, logger, ResultSeriesKind
 20from rsseismic._client import DEFAULT_SCRIPTING_PORT
 21from rsseismic.exceptions import ScriptingError
 22
 23
 24def _select_motion_names(model, requested_names: list[str]) -> list[str]:
 25    model.Motions.refreshMotionsList()
 26    motions = model.Motions.listMotions()
 27    if not motions:
 28        raise RuntimeError(
 29            "No motions available. Configure motion directories in RSSeismic Preferences "
 30            "or ensure the project has motions."
 31        )
 32
 33    if requested_names:
 34        return requested_names
 35
 36    selected = [motion.name for motion in motions if motion.isSelected]
 37    if selected:
 38        return selected
 39
 40    return [motions[0].name]
 41
 42
 43def _require_matplotlib():
 44    try:
 45        import matplotlib.pyplot as plt
 46    except ImportError as exc:
 47        raise RuntimeError(
 48            "matplotlib is required for this example. Reinstall RSSeismicScripting "
 49            "(pip install --force-reinstall RSSeismicScripting) or run: pip install matplotlib"
 50        ) from exc
 51    return plt
 52
 53
 54def _save_or_show(plt, fig, output_dir: str | None, filename: str) -> None:
 55    if output_dir:
 56        os.makedirs(output_dir, exist_ok=True)
 57        output_path = os.path.join(output_dir, filename)
 58        fig.savefig(output_path, dpi=150)
 59        print(f"Saved plot: {output_path}")
 60    else:
 61        plt.show()
 62
 63
 64def plot_results(
 65    project_path: str,
 66    port: int = DEFAULT_SCRIPTING_PORT,
 67    motion_names: list[str] | None = None,
 68    run_compute: bool = True,
 69    output_dir: str | None = None,
 70) -> None:
 71    project_path = os.path.abspath(project_path.strip())
 72    if not os.path.isfile(project_path):
 73        raise FileNotFoundError(f"Project file not found: {project_path}")
 74
 75    plt = _require_matplotlib()
 76    app = RSSeismicApplication(port=port)
 77    model = None
 78
 79    try:
 80        app.ping()
 81        model = app.openFile(project_path)
 82
 83        profile_name = model.Profiles.getActiveProfile().profileName
 84        names_to_select = _select_motion_names(model, motion_names or [])
 85        model.Motions.setMotionSelection(names_to_select, selectOnlyListed=True)
 86        model.save()
 87
 88        if run_compute:
 89            result = model.runCompute()
 90            if not result.success:
 91                raise RuntimeError(result.errorMessage or "RunCompute failed.")
 92            if result.partialSuccess:
 93                logger.warning("Some profile/motion pairs failed:\n%s", result.errorMessage)
 94
 95        motion_name = names_to_select[0]
 96        entry = model.Results.getResultDatabasePath(profile_name, motion_name)
 97        deconv = model.Profiles.getBedrockLayerProxy(profile_name).Data.getBoolValue("Deconvolution")
 98        has_db3 = entry.resultDatabaseExists or entry.complementaryResultDatabaseExists
 99
100        layers = model.Profiles.listActiveSoilLayers()
101        if not layers:
102            raise RuntimeError("Active profile has no soil layers.")
103
104        layer_id = layers[0].layerID
105        layer_label = layers[0].name or f"layer {layers[0].index1Based}"
106
107        fig, axes = plt.subplots(1, 2, figsize=(10, 5))
108        fig.suptitle(f"{profile_name}{motion_name}")
109
110        if not deconv and has_db3:
111            max_strain_df = model.Results.getResultSeriesDataFrame(
112                profile_name,
113                motion_name,
114                ResultSeriesKind.MAX_STRAIN,
115            )
116            profile_df = model.Results.getProfileResultsDataFrame(profile_name, motion_name)
117
118            axes[0].plot(
119                max_strain_df["maxStrain"],
120                max_strain_df["depth"],
121                marker="o",
122                linewidth=1,
123            )
124            axes[0].set_xlabel("max strain")
125            axes[0].set_ylabel("depth")
126            axes[0].set_title("Max strain profile")
127            axes[0].grid(True, alpha=0.3)
128            axes[0].invert_yaxis()
129
130            print(f"Profile rows: {len(profile_df)}")
131            print(f"Max-strain series rows: {len(max_strain_df)}")
132            print(f"Profile columns: {list(profile_df.columns)}")
133        else:
134            reason = "deconvolution profile" if deconv else "no db3 result database"
135            logger.info("Skipping profile-level results (%s).", reason)
136            axes[0].set_title(f"Profile results unavailable ({reason})")
137            axes[0].axis("off")
138
139        can_read_layer = (
140            entry.deconvolutionBedrockMotionExists
141            or any(layer.layerMotionExists for layer in entry.deconvolutionLayerFiles)
142            if deconv
143            else has_db3
144        )
145        if can_read_layer:
146            try:
147                time_history_df = model.Results.getLayerTimeHistoryDataFrame(
148                    profile_name,
149                    motion_name,
150                    layer_id,
151                )
152            except ScriptingError as ex:
153                logger.warning(
154                    "Skipping layer time-history plot for %s (%s): %s",
155                    layer_label,
156                    layer_id,
157                    ex,
158                )
159                axes[1].set_title("Layer time history (unavailable)")
160                axes[1].axis("off")
161            else:
162                axes[1].plot(time_history_df["time"], time_history_df["accel"], linewidth=0.8)
163                axes[1].set_xlabel("time")
164                axes[1].set_ylabel("acceleration")
165                axes[1].set_title(f"Acceleration — {layer_label}")
166                axes[1].grid(True, alpha=0.3)
167        else:
168            logger.info("Skipping layer time history (no layer motion/db3 output).")
169            axes[1].set_title("Layer time history (not available)")
170            axes[1].axis("off")
171
172        fig.tight_layout()
173        _save_or_show(
174            plt,
175            fig,
176            output_dir,
177            f"{profile_name}_{motion_name}_results.png".replace(" ", "_"),
178        )
179
180        strain_motion_name = None
181        for name in names_to_select:
182            strain_entry = model.Results.getResultDatabasePath(profile_name, name)
183            if strain_entry.strainConvergenceExists:
184                strain_motion_name = name
185                break
186
187        if strain_motion_name is not None:
188            try:
189                strain_df = model.Results.getStrainConvergenceDataFrame(
190                    profile_name,
191                    strain_motion_name,
192                )
193            except ScriptingError as ex:
194                logger.warning(
195                    "Strain convergence query failed for motion %r: %s",
196                    strain_motion_name,
197                    ex,
198                )
199            else:
200                fig2, ax2 = plt.subplots(figsize=(8, 4))
201                fig2.suptitle(f"{profile_name}{strain_motion_name} (strain convergence)")
202                layer_columns = [col for col in strain_df.columns if col.startswith("strainLayer")]
203                if layer_columns:
204                    for column in layer_columns:
205                        ax2.plot(strain_df["iteration"], strain_df[column], marker="o", label=column)
206                    ax2.legend(loc="best", fontsize=8)
207                ax2.set_xlabel("iteration")
208                ax2.set_ylabel("strain")
209                ax2.grid(True, alpha=0.3)
210                fig2.tight_layout()
211                _save_or_show(
212                    plt,
213                    fig2,
214                    output_dir,
215                    f"{profile_name}_{strain_motion_name}_strain_convergence.png".replace(" ", "_"),
216                )
217                print(f"Strain convergence rows: {len(strain_df)}")
218        else:
219            logger.info(
220                "Skipping strain convergence (no strain.txt for selected motions on profile %s).",
221                profile_name,
222            )
223
224        displacement_animation = model.ProjectSettings.Data.getBoolValue("displacementAnimation")
225        if displacement_animation and not deconv and entry.resultDatabaseExists:
226            try:
227                disp_df = model.Results.getRealTimeDisplacementDataFrame(
228                    profile_name,
229                    motion_name,
230                )
231            except ScriptingError as ex:
232                logger.warning("Real-time displacement query failed: %s", ex)
233            else:
234                disp_columns = [col for col in disp_df.columns if col.startswith("dispLayer")]
235                if disp_columns:
236                    fig3, ax3 = plt.subplots(figsize=(8, 4))
237                    fig3.suptitle(f"{profile_name}{motion_name} (real-time displacement)")
238                    ax3.plot(disp_df["time"], disp_df[disp_columns[0]], linewidth=0.8)
239                    ax3.set_xlabel("time")
240                    ax3.set_ylabel(disp_columns[0])
241                    ax3.set_title(f"{disp_columns[0]} vs time")
242                    ax3.grid(True, alpha=0.3)
243                    fig3.tight_layout()
244                    _save_or_show(
245                        plt,
246                        fig3,
247                        output_dir,
248                        f"{profile_name}_{motion_name}_displacement.png".replace(" ", "_"),
249                    )
250                    print(f"Real-time displacement rows: {len(disp_df)}")
251        else:
252            logger.info(
253                "Skipping real-time displacement (requires displacementAnimation, standard db3, non-deconv)."
254            )
255    finally:
256        if model is not None:
257            model.close(saveProject=False)
258        app.close()
259
260
261def parse_args() -> argparse.Namespace:
262    parser = argparse.ArgumentParser(
263        description="Compute (optional) and plot RSSeismic result DataFrames."
264    )
265    parser.add_argument(
266        "project",
267        help="Absolute path to a .rsseismicfile",
268    )
269    parser.add_argument(
270        "--port",
271        "-p",
272        type=int,
273        default=int(os.environ.get("RSSEISMIC_SCRIPTING_PORT", DEFAULT_SCRIPTING_PORT)),
274        help=f"gRPC port (default: {DEFAULT_SCRIPTING_PORT}, or RSSEISMIC_SCRIPTING_PORT env)",
275    )
276    parser.add_argument(
277        "--motion",
278        "-m",
279        action="append",
280        default=[],
281        metavar="NAME",
282        help="Motion name to select for compute (repeatable). Default: already-selected, else first motion.",
283    )
284    parser.add_argument(
285        "--no-compute",
286        action="store_true",
287        help="Skip RunCompute and read existing result databases",
288    )
289    parser.add_argument(
290        "--output-dir",
291        "-o",
292        default="",
293        help="Save PNG to this directory instead of opening an interactive window",
294    )
295    return parser.parse_args()
296
297
298def main() -> int:
299    args = parse_args()
300    try:
301        plot_results(
302            args.project,
303            port=args.port,
304            motion_names=args.motion,
305            run_compute=not args.no_compute,
306            output_dir=args.output_dir or None,
307        )
308    except Exception as ex:
309        print(f"Plot workflow failed: {ex}", file=sys.stderr)
310        return 1
311
312    return 0
313
314
315if __name__ == "__main__":
316    raise SystemExit(main())