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())