RSSeismicScripting 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. rsseismic/Application.py +196 -0
  2. rsseismic/ApplicationManager.py +111 -0
  3. rsseismic/BedrockLayer.py +29 -0
  4. rsseismic/MeanProfile.py +169 -0
  5. rsseismic/MeanProfileSoilLayer.py +119 -0
  6. rsseismic/Model.py +204 -0
  7. rsseismic/Motions.py +140 -0
  8. rsseismic/ProfileGenerationSettings.py +29 -0
  9. rsseismic/Profiles.py +186 -0
  10. rsseismic/ProjectSettings.py +46 -0
  11. rsseismic/ReferenceCurve.py +73 -0
  12. rsseismic/Results.py +419 -0
  13. rsseismic/SoilLayer.py +34 -0
  14. rsseismic/SoilLayerCurve.py +124 -0
  15. rsseismic/SoilLayerOperations.py +120 -0
  16. rsseismic/__init__.py +79 -0
  17. rsseismic/_application_paths.py +74 -0
  18. rsseismic/_batch_compute.py +420 -0
  19. rsseismic/_client.py +81 -0
  20. rsseismic/_proto_converters.py +345 -0
  21. rsseismic/_proxyObject.py +11 -0
  22. rsseismic/_result_bulk_dataframes.py +202 -0
  23. rsseismic/_result_dataframes.py +77 -0
  24. rsseismic/_rpc_helpers.py +55 -0
  25. rsseismic/enums/__init__.py +23 -0
  26. rsseismic/enums/_generated.py +120 -0
  27. rsseismic/enums/fitting_procedure.py +15 -0
  28. rsseismic/exceptions.py +11 -0
  29. rsseismic/generatedFiles/ApplicationService_pb2.py +46 -0
  30. rsseismic/generatedFiles/ApplicationService_pb2.pyi +67 -0
  31. rsseismic/generatedFiles/ApplicationService_pb2_grpc.py +274 -0
  32. rsseismic/generatedFiles/BedrockLayerDataService_pb2.py +27 -0
  33. rsseismic/generatedFiles/BedrockLayerDataService_pb2.pyi +5 -0
  34. rsseismic/generatedFiles/BedrockLayerDataService_pb2_grpc.py +231 -0
  35. rsseismic/generatedFiles/CommonMessages_pb2.py +50 -0
  36. rsseismic/generatedFiles/CommonMessages_pb2.pyi +113 -0
  37. rsseismic/generatedFiles/CommonMessages_pb2_grpc.py +29 -0
  38. rsseismic/generatedFiles/MeanProfileService_pb2.py +62 -0
  39. rsseismic/generatedFiles/MeanProfileService_pb2.pyi +176 -0
  40. rsseismic/generatedFiles/MeanProfileService_pb2_grpc.py +458 -0
  41. rsseismic/generatedFiles/MeanProfileSoilLayerDataService_pb2.py +45 -0
  42. rsseismic/generatedFiles/MeanProfileSoilLayerDataService_pb2.pyi +87 -0
  43. rsseismic/generatedFiles/MeanProfileSoilLayerDataService_pb2_grpc.py +360 -0
  44. rsseismic/generatedFiles/ModelService_pb2.py +54 -0
  45. rsseismic/generatedFiles/ModelService_pb2.pyi +122 -0
  46. rsseismic/generatedFiles/ModelService_pb2_grpc.py +360 -0
  47. rsseismic/generatedFiles/MotionService_pb2.py +78 -0
  48. rsseismic/generatedFiles/MotionService_pb2.pyi +224 -0
  49. rsseismic/generatedFiles/MotionService_pb2_grpc.py +575 -0
  50. rsseismic/generatedFiles/ProfileGenerationSettingsService_pb2.py +27 -0
  51. rsseismic/generatedFiles/ProfileGenerationSettingsService_pb2.pyi +5 -0
  52. rsseismic/generatedFiles/ProfileGenerationSettingsService_pb2_grpc.py +145 -0
  53. rsseismic/generatedFiles/ProfileService_pb2.py +76 -0
  54. rsseismic/generatedFiles/ProfileService_pb2.pyi +246 -0
  55. rsseismic/generatedFiles/ProfileService_pb2_grpc.py +532 -0
  56. rsseismic/generatedFiles/ProjectSettingsDataService_pb2.py +35 -0
  57. rsseismic/generatedFiles/ProjectSettingsDataService_pb2.pyi +40 -0
  58. rsseismic/generatedFiles/ProjectSettingsDataService_pb2_grpc.py +232 -0
  59. rsseismic/generatedFiles/ReferenceCurveDataService_pb2.py +41 -0
  60. rsseismic/generatedFiles/ReferenceCurveDataService_pb2.pyi +73 -0
  61. rsseismic/generatedFiles/ReferenceCurveDataService_pb2_grpc.py +281 -0
  62. rsseismic/generatedFiles/ResultsService_pb2.py +82 -0
  63. rsseismic/generatedFiles/ResultsService_pb2.pyi +373 -0
  64. rsseismic/generatedFiles/ResultsService_pb2_grpc.py +489 -0
  65. rsseismic/generatedFiles/SoilLayerCurveDataService_pb2.py +52 -0
  66. rsseismic/generatedFiles/SoilLayerCurveDataService_pb2.pyi +150 -0
  67. rsseismic/generatedFiles/SoilLayerCurveDataService_pb2_grpc.py +280 -0
  68. rsseismic/generatedFiles/SoilLayerDataService_pb2.py +27 -0
  69. rsseismic/generatedFiles/SoilLayerDataService_pb2.pyi +5 -0
  70. rsseismic/generatedFiles/SoilLayerDataService_pb2_grpc.py +231 -0
  71. rsseismic/generatedFiles/SoilLayerService_pb2.py +46 -0
  72. rsseismic/generatedFiles/SoilLayerService_pb2.pyi +122 -0
  73. rsseismic/generatedFiles/SoilLayerService_pb2_grpc.py +317 -0
  74. rsseismic/generatedFiles/__init__.py +4 -0
  75. rsseismic/properties/_PropertyAccessor.py +151 -0
  76. rsseismic/properties/__init__.py +6 -0
  77. rsseismic/properties/bedrockLayer/__init__.py +1 -0
  78. rsseismic/properties/bedrockLayer/_generated.py +127 -0
  79. rsseismic/properties/meanProfileSoilLayer/__init__.py +1 -0
  80. rsseismic/properties/meanProfileSoilLayer/_generated.py +87 -0
  81. rsseismic/properties/profileGenerationSettings/__init__.py +1 -0
  82. rsseismic/properties/profileGenerationSettings/_generated.py +195 -0
  83. rsseismic/properties/projectSettings/__init__.py +1 -0
  84. rsseismic/properties/projectSettings/_generated.py +270 -0
  85. rsseismic/properties/referenceCurve/__init__.py +1 -0
  86. rsseismic/properties/referenceCurve/_generated.py +264 -0
  87. rsseismic/properties/soilLayer/__init__.py +1 -0
  88. rsseismic/properties/soilLayer/_generated.py +523 -0
  89. rsseismic/types.py +311 -0
  90. rsseismicscripting-3.0.0.dist-info/METADATA +88 -0
  91. rsseismicscripting-3.0.0.dist-info/RECORD +94 -0
  92. rsseismicscripting-3.0.0.dist-info/WHEEL +5 -0
  93. rsseismicscripting-3.0.0.dist-info/licenses/License.txt +20 -0
  94. rsseismicscripting-3.0.0.dist-info/top_level.txt +1 -0
rsseismic/__init__.py ADDED
@@ -0,0 +1,79 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("Rocscience.RSSeismic")
4
+ logger.setLevel(logging.INFO)
5
+
6
+ if not logger.handlers:
7
+ handler = logging.StreamHandler()
8
+ handler.setLevel(logging.INFO)
9
+ handler.setFormatter(
10
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
11
+ )
12
+ logger.addHandler(handler)
13
+
14
+ from rsseismic._batch_compute import BatchComputeJobResult, BatchComputeResult
15
+ from rsseismic.Application import RSSeismicApplication
16
+ from rsseismic.Model import Model, RunComputeResult
17
+ from rsseismic.Results import Results
18
+ from rsseismic.types import (
19
+ ActiveProfile,
20
+ AppendLayersResult,
21
+ BedrockLayerSummary,
22
+ DeleteLayerResult,
23
+ DeleteProfileResult,
24
+ DiscretePoint,
25
+ DynamicsPoint,
26
+ GqHFittingLimits,
27
+ InsertLayerResult,
28
+ MotionInfo,
29
+ MotionPairInfo,
30
+ MoveLayerResult,
31
+ MeanProfileAppendLayersResult,
32
+ MeanProfileDeleteLayerResult,
33
+ MeanProfileInsertLayerResult,
34
+ MeanProfileMoveLayerResult,
35
+ MeanProfileSummary,
36
+ ProfileSummary,
37
+ ResultDatabaseInfo,
38
+ ResultLayerSummary,
39
+ ResultMotionSummary,
40
+ ResultMotionType,
41
+ ResultSeriesKind,
42
+ SoilLayerSummary,
43
+ UnitSystem,
44
+ )
45
+
46
+ __all__ = [
47
+ "RSSeismicApplication",
48
+ "BatchComputeJobResult",
49
+ "BatchComputeResult",
50
+ "Model",
51
+ "RunComputeResult",
52
+ "Results",
53
+ "DynamicsPoint",
54
+ "DiscretePoint",
55
+ "ProfileSummary",
56
+ "ActiveProfile",
57
+ "SoilLayerSummary",
58
+ "BedrockLayerSummary",
59
+ "DeleteProfileResult",
60
+ "MotionInfo",
61
+ "MotionPairInfo",
62
+ "ResultDatabaseInfo",
63
+ "ResultLayerSummary",
64
+ "ResultMotionSummary",
65
+ "ResultMotionType",
66
+ "ResultSeriesKind",
67
+ "InsertLayerResult",
68
+ "AppendLayersResult",
69
+ "DeleteLayerResult",
70
+ "MoveLayerResult",
71
+ "MeanProfileSummary",
72
+ "MeanProfileInsertLayerResult",
73
+ "MeanProfileAppendLayersResult",
74
+ "MeanProfileDeleteLayerResult",
75
+ "MeanProfileMoveLayerResult",
76
+ "GqHFittingLimits",
77
+ "UnitSystem",
78
+ "logger",
79
+ ]
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+
6
+
7
+ def getInstalledExecutablePath() -> str:
8
+ """
9
+ Return the path to RSSeismic.exe from the Rocscience registry Install value.
10
+
11
+ Raises:
12
+ RuntimeError: on non-Windows or when no installation is found
13
+ """
14
+ if sys.platform != "win32":
15
+ raise RuntimeError(
16
+ "Automatic RSSeismic install lookup is only supported on Windows. "
17
+ "Pass overridePathToExecutable to startApplication()."
18
+ )
19
+
20
+ import winreg
21
+
22
+ candidates: list[tuple[str, str]] = []
23
+ for root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
24
+ try:
25
+ rocscience_key = winreg.OpenKey(root, r"SOFTWARE\Rocscience")
26
+ except OSError:
27
+ continue
28
+
29
+ index = 0
30
+ while True:
31
+ try:
32
+ subkey_name = winreg.EnumKey(rocscience_key, index)
33
+ except OSError:
34
+ break
35
+ index += 1
36
+
37
+ if "rsseismic" not in subkey_name.casefold():
38
+ continue
39
+
40
+ try:
41
+ product_key = winreg.OpenKey(rocscience_key, subkey_name)
42
+ install_path, _ = winreg.QueryValueEx(product_key, "Install")
43
+ candidates.append((subkey_name, str(install_path)))
44
+ except OSError:
45
+ continue
46
+
47
+ if not candidates:
48
+ raise RuntimeError(
49
+ "Could not find an RSSeismic installation in the registry. "
50
+ "Pass overridePathToExecutable to startApplication()."
51
+ )
52
+
53
+ candidates.sort(key=lambda item: item[0], reverse=True)
54
+ for _, install_path in candidates:
55
+ executable = _executableFromInstallPath(install_path)
56
+ if executable is not None:
57
+ return executable
58
+
59
+ raise RuntimeError(
60
+ "Found RSSeismic registry entries but RSSeismic.exe was not present. "
61
+ "Pass overridePathToExecutable to startApplication()."
62
+ )
63
+
64
+
65
+ def _executableFromInstallPath(install_path: str) -> str | None:
66
+ normalized = install_path.rstrip("\\/")
67
+ if normalized.casefold().endswith(".exe") and os.path.isfile(normalized):
68
+ return normalized
69
+
70
+ candidate = os.path.join(normalized, "RSSeismic.exe")
71
+ if os.path.isfile(candidate):
72
+ return candidate
73
+
74
+ return None
@@ -0,0 +1,420 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import threading
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING
9
+
10
+ from rsseismic.Model import Model, RunComputeResult
11
+
12
+ if TYPE_CHECKING:
13
+ from rsseismic.Application import RSSeismicApplication
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class BatchComputeJobResult:
18
+ """Outcome of compute for one project in a batch."""
19
+
20
+ file_path: str
21
+ success: bool
22
+ partial_success: bool
23
+ error_message: str
24
+ motion_names: tuple[str, ...] = ()
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class BatchComputeResult:
29
+ """Aggregate outcome of runBatchCompute."""
30
+
31
+ results: list[BatchComputeJobResult]
32
+ requested_parallel_degree: int
33
+ effective_parallel_degree: int
34
+
35
+ @property
36
+ def ok(self) -> bool:
37
+ """True when every job completed with full success (no partial failures)."""
38
+ return all(
39
+ job.success and not job.partial_success for job in self.results
40
+ )
41
+
42
+
43
+ def resolve_parallel_degree(
44
+ requested: int,
45
+ job_count: int,
46
+ max_concurrent_analyses: int | None = None,
47
+ ) -> int:
48
+ """
49
+ Resolve cross-project concurrency for a batch.
50
+
51
+ Caps requested parallelism by job count and by logical CPU count divided by
52
+ the effective within-project engine cap (from RSSeismic preferences when available).
53
+ """
54
+ if not isinstance(requested, int):
55
+ raise TypeError("parallel_degree must be an int.")
56
+ if requested < 1:
57
+ raise ValueError("parallel_degree must be >= 1.")
58
+ if job_count < 1:
59
+ return 1
60
+
61
+ logical_cores = max(1, os.cpu_count() or 1)
62
+ if max_concurrent_analyses is not None and max_concurrent_analyses >= 1:
63
+ engines_per_project = max_concurrent_analyses
64
+ else:
65
+ engines_per_project = max(1, logical_cores // 2)
66
+
67
+ project_cap = max(1, logical_cores // engines_per_project)
68
+
69
+ return max(
70
+ 1,
71
+ min(requested, job_count, project_cap),
72
+ )
73
+
74
+
75
+ def _read_max_concurrent_analyses(app: RSSeismicApplication) -> int | None:
76
+ get_value = getattr(app, "getMaxConcurrentAnalyses", None)
77
+ if get_value is None:
78
+ return None
79
+
80
+ try:
81
+ value = int(get_value())
82
+ except Exception:
83
+ return None
84
+
85
+ if value < 1:
86
+ return None
87
+
88
+ return value
89
+
90
+
91
+ def _configure_max_concurrent_analyses_for_batch(
92
+ app: RSSeismicApplication,
93
+ max_concurrent_analyses: int | None,
94
+ ) -> tuple[int | None, int | None]:
95
+ """
96
+ Apply optional maxConcurrentAnalyses override for a batch.
97
+
98
+ Returns (value for parallel_degree cap, original preference to restore).
99
+ """
100
+ if max_concurrent_analyses is None:
101
+ return _read_max_concurrent_analyses(app), None
102
+
103
+ set_value = getattr(app, "setMaxConcurrentAnalyses", None)
104
+ get_value = getattr(app, "getMaxConcurrentAnalyses", None)
105
+ if set_value is None or get_value is None:
106
+ return _read_max_concurrent_analyses(app), None
107
+
108
+ original = int(get_value())
109
+ set_value(max_concurrent_analyses)
110
+ return int(get_value()), original
111
+
112
+
113
+ def _restore_max_concurrent_analyses(
114
+ app: RSSeismicApplication,
115
+ original: int | None,
116
+ ) -> None:
117
+ if original is None:
118
+ return
119
+
120
+ set_value = getattr(app, "setMaxConcurrentAnalyses", None)
121
+ if set_value is not None:
122
+ set_value(original)
123
+
124
+
125
+ def normalize_batch_file_paths(
126
+ file_paths: list[str],
127
+ logger: logging.Logger | None = None,
128
+ ) -> list[str]:
129
+ """Return absolute, de-duplicated paths (case-insensitive on Windows)."""
130
+ seen: set[str] = set()
131
+ unique: list[str] = []
132
+
133
+ for raw_path in file_paths:
134
+ path = os.path.abspath(raw_path.strip())
135
+ key = os.path.normcase(path)
136
+ if key in seen:
137
+ if logger is not None:
138
+ logger.warning("Duplicate path omitted from batch: %s", path)
139
+ continue
140
+ seen.add(key)
141
+ unique.append(path)
142
+
143
+ return unique
144
+
145
+
146
+ def normalize_motion_names(
147
+ motion_names: list[str] | None,
148
+ logger: logging.Logger | None = None,
149
+ ) -> list[str] | None:
150
+ """Return de-duplicated motion names (case-insensitive), preserving first occurrence."""
151
+ if motion_names is None:
152
+ return None
153
+
154
+ seen: set[str] = set()
155
+ unique: list[str] = []
156
+
157
+ for raw_name in motion_names:
158
+ name = raw_name.strip()
159
+ if not name:
160
+ if logger is not None:
161
+ logger.warning("Empty motion name omitted from batch.")
162
+ continue
163
+
164
+ key = name.casefold()
165
+ if key in seen:
166
+ if logger is not None:
167
+ logger.warning("Duplicate motion name omitted from batch: %s", name)
168
+ continue
169
+
170
+ seen.add(key)
171
+ unique.append(name)
172
+
173
+ return unique
174
+
175
+
176
+ def resolve_motion_names(model: Model, motion_names: list[str] | None) -> list[str]:
177
+ """
178
+ Resolve motion selection for one batch job.
179
+
180
+ When motion_names is provided, every name must exist after refreshMotionsList.
181
+ When omitted, uses current project selection or the first available motion.
182
+ """
183
+ model.Motions.refreshMotionsList()
184
+ motions = model.Motions.listMotions()
185
+ if not motions:
186
+ raise RuntimeError(
187
+ "No motions available. Configure motion directories in RSSeismic "
188
+ "Preferences or ensure the project has motions."
189
+ )
190
+
191
+ available = {motion.name for motion in motions}
192
+
193
+ if motion_names:
194
+ missing = [name for name in motion_names if name not in available]
195
+ if missing:
196
+ raise RuntimeError(
197
+ "Motions not found in project: " + ", ".join(missing)
198
+ )
199
+ return list(motion_names)
200
+
201
+ selected = [motion.name for motion in motions if motion.isSelected]
202
+ if selected:
203
+ return selected
204
+
205
+ return [motions[0].name]
206
+
207
+
208
+ def _run_single_batch_job(
209
+ app: RSSeismicApplication,
210
+ file_path: str,
211
+ motion_names: list[str] | None,
212
+ *,
213
+ timeout: float | None,
214
+ close_save_project: bool,
215
+ ) -> BatchComputeJobResult:
216
+ model: Model | None = None
217
+ try:
218
+ if not os.path.isfile(file_path):
219
+ return BatchComputeJobResult(
220
+ file_path=file_path,
221
+ success=False,
222
+ partial_success=False,
223
+ error_message=f"Project file not found: {file_path}",
224
+ )
225
+
226
+ model = app.openFile(file_path)
227
+ names = resolve_motion_names(model, motion_names)
228
+ model.Motions.setMotionSelection(names, selectOnlyListed=True)
229
+
230
+ compute_result: RunComputeResult = model.runCompute(timeout=timeout)
231
+ return BatchComputeJobResult(
232
+ file_path=file_path,
233
+ success=compute_result.success,
234
+ partial_success=compute_result.partialSuccess,
235
+ error_message=compute_result.errorMessage,
236
+ motion_names=tuple(names),
237
+ )
238
+ except Exception as ex:
239
+ return BatchComputeJobResult(
240
+ file_path=file_path,
241
+ success=False,
242
+ partial_success=False,
243
+ error_message=str(ex),
244
+ )
245
+ finally:
246
+ if model is not None:
247
+ model.close(saveProject=close_save_project)
248
+
249
+
250
+ def _job_failed(job: BatchComputeJobResult) -> bool:
251
+ return not job.success or job.partial_success
252
+
253
+
254
+ def run_batch_compute(
255
+ app: RSSeismicApplication,
256
+ file_paths: list[str],
257
+ motion_names: list[str] | None = None,
258
+ *,
259
+ parallel_degree: int = 1,
260
+ max_concurrent_analyses: int | None = None,
261
+ timeout: float | None = None,
262
+ close_save_project: bool = False,
263
+ continue_on_error: bool = True,
264
+ logger: logging.Logger | None = None,
265
+ ) -> BatchComputeResult:
266
+ """Run open → motion selection → compute → close for each project path."""
267
+ batch_logger = logger or logging.getLogger("Rocscience.RSSeismic")
268
+ normalized_paths = normalize_batch_file_paths(file_paths, batch_logger)
269
+ normalized_motion_names = normalize_motion_names(motion_names, batch_logger)
270
+
271
+ cap_max_concurrent_analyses, restore_max_concurrent_analyses = (
272
+ _configure_max_concurrent_analyses_for_batch(app, max_concurrent_analyses)
273
+ )
274
+ requested_parallel_degree = parallel_degree
275
+ effective_parallel_degree = resolve_parallel_degree(
276
+ parallel_degree,
277
+ len(normalized_paths),
278
+ cap_max_concurrent_analyses,
279
+ )
280
+
281
+ if effective_parallel_degree < requested_parallel_degree:
282
+ if cap_max_concurrent_analyses is not None:
283
+ batch_logger.warning(
284
+ "parallel_degree %s capped to %s (job_count=%s, maxConcurrentAnalyses=%s).",
285
+ requested_parallel_degree,
286
+ effective_parallel_degree,
287
+ len(normalized_paths),
288
+ cap_max_concurrent_analyses,
289
+ )
290
+ else:
291
+ batch_logger.warning(
292
+ "parallel_degree %s capped to %s (job_count=%s; maxConcurrentAnalyses unavailable).",
293
+ requested_parallel_degree,
294
+ effective_parallel_degree,
295
+ len(normalized_paths),
296
+ )
297
+
298
+ try:
299
+ if not normalized_paths:
300
+ return BatchComputeResult(
301
+ results=[],
302
+ requested_parallel_degree=requested_parallel_degree,
303
+ effective_parallel_degree=effective_parallel_degree,
304
+ )
305
+
306
+ if effective_parallel_degree == 1:
307
+ results = _run_batch_sequential(
308
+ app,
309
+ normalized_paths,
310
+ normalized_motion_names,
311
+ timeout=timeout,
312
+ close_save_project=close_save_project,
313
+ continue_on_error=continue_on_error,
314
+ stop_event=None,
315
+ )
316
+ else:
317
+ results = _run_batch_parallel(
318
+ app,
319
+ normalized_paths,
320
+ normalized_motion_names,
321
+ max_workers=effective_parallel_degree,
322
+ timeout=timeout,
323
+ close_save_project=close_save_project,
324
+ continue_on_error=continue_on_error,
325
+ )
326
+
327
+ return BatchComputeResult(
328
+ results=results,
329
+ requested_parallel_degree=requested_parallel_degree,
330
+ effective_parallel_degree=effective_parallel_degree,
331
+ )
332
+ finally:
333
+ _restore_max_concurrent_analyses(app, restore_max_concurrent_analyses)
334
+
335
+
336
+ def _run_batch_sequential(
337
+ app: RSSeismicApplication,
338
+ file_paths: list[str],
339
+ motion_names: list[str] | None,
340
+ *,
341
+ timeout: float | None,
342
+ close_save_project: bool,
343
+ continue_on_error: bool,
344
+ stop_event: threading.Event | None,
345
+ ) -> list[BatchComputeJobResult]:
346
+ results: list[BatchComputeJobResult] = []
347
+
348
+ for file_path in file_paths:
349
+ if stop_event is not None and stop_event.is_set():
350
+ results.append(
351
+ BatchComputeJobResult(
352
+ file_path=file_path,
353
+ success=False,
354
+ partial_success=False,
355
+ error_message="Batch aborted after a prior failure.",
356
+ )
357
+ )
358
+ continue
359
+
360
+ job_result = _run_single_batch_job(
361
+ app,
362
+ file_path,
363
+ motion_names,
364
+ timeout=timeout,
365
+ close_save_project=close_save_project,
366
+ )
367
+ results.append(job_result)
368
+
369
+ if not continue_on_error and _job_failed(job_result):
370
+ if stop_event is not None:
371
+ stop_event.set()
372
+ break
373
+
374
+ return results
375
+
376
+
377
+ def _run_batch_parallel(
378
+ app: RSSeismicApplication,
379
+ file_paths: list[str],
380
+ motion_names: list[str] | None,
381
+ *,
382
+ max_workers: int,
383
+ timeout: float | None,
384
+ close_save_project: bool,
385
+ continue_on_error: bool,
386
+ ) -> list[BatchComputeJobResult]:
387
+ stop_event = threading.Event()
388
+ results: list[BatchComputeJobResult | None] = [None] * len(file_paths)
389
+
390
+ def worker(index: int, file_path: str) -> None:
391
+ if stop_event.is_set():
392
+ results[index] = BatchComputeJobResult(
393
+ file_path=file_path,
394
+ success=False,
395
+ partial_success=False,
396
+ error_message="Batch aborted after a prior failure.",
397
+ )
398
+ return
399
+
400
+ job_result = _run_single_batch_job(
401
+ app,
402
+ file_path,
403
+ motion_names,
404
+ timeout=timeout,
405
+ close_save_project=close_save_project,
406
+ )
407
+ results[index] = job_result
408
+
409
+ if not continue_on_error and _job_failed(job_result):
410
+ stop_event.set()
411
+
412
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
413
+ futures = [
414
+ executor.submit(worker, index, file_path)
415
+ for index, file_path in enumerate(file_paths)
416
+ ]
417
+ for future in futures:
418
+ future.result()
419
+
420
+ return [result for result in results if result is not None]
rsseismic/_client.py ADDED
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import logging
5
+ import os
6
+
7
+ import grpc
8
+
9
+ from rsseismic.generatedFiles import ApplicationService_pb2 as ApplicationService_pb2
10
+ from rsseismic.generatedFiles import ApplicationService_pb2_grpc as ApplicationService_pb2_grpc
11
+
12
+ # Product default when RSSeismic scripting server dialog opens (see App.DefaultScriptingPort in RSSeismic).
13
+ DEFAULT_SCRIPTING_PORT = int(os.environ.get("RSSEISMIC_SCRIPTING_PORT", "60058"))
14
+
15
+ _GRPC_MAX_MESSAGE_BYTES = 1024 * 1024 * 1024
16
+
17
+
18
+ def _grpc_channel_options() -> list[tuple[str, int]]:
19
+ return [
20
+ ("grpc.max_receive_message_length", _GRPC_MAX_MESSAGE_BYTES),
21
+ ("grpc.max_send_message_length", _GRPC_MAX_MESSAGE_BYTES),
22
+ ]
23
+
24
+
25
+ class Client:
26
+ """gRPC client for a running RSSeismic scripting server."""
27
+
28
+ def __init__(self, port: int = DEFAULT_SCRIPTING_PORT, host: str = "localhost"):
29
+ self.host = host
30
+ self.port = port
31
+ self.logger = logging.getLogger("Rocscience.RSSeismic")
32
+ self._establishConnection()
33
+ atexit.register(self.closeConnection)
34
+
35
+ @property
36
+ def channel(self) -> grpc.Channel:
37
+ if self._channel is None:
38
+ raise RuntimeError("gRPC channel is closed.")
39
+ return self._channel
40
+
41
+ def _establishConnection(self) -> None:
42
+ self._channel = grpc.insecure_channel(
43
+ f"{self.host}:{self.port}",
44
+ options=_grpc_channel_options(),
45
+ )
46
+
47
+ def closeConnection(self) -> None:
48
+ if getattr(self, "_channel", None) is not None:
49
+ self._channel.close()
50
+ self._channel = None
51
+
52
+ def ping(self) -> None:
53
+ stub = ApplicationService_pb2_grpc.ApplicationServiceStub(self.channel)
54
+ self.callFunction(stub.Ping, ApplicationService_pb2.PingRequest())
55
+
56
+ def callFunction(self, function, request, timeout: float | None = None):
57
+ try:
58
+ if timeout is not None:
59
+ response, call = function.with_call(request, timeout=timeout)
60
+ else:
61
+ response, call = function.with_call(request)
62
+ self._logTrailingMetadata(call)
63
+ return response
64
+ except grpc.RpcError:
65
+ self.logger.exception("An exception was raised from RSSeismic.")
66
+ raise
67
+
68
+ def _logTrailingMetadata(self, call) -> None:
69
+ if call is None:
70
+ return
71
+
72
+ for key, value in call.trailing_metadata():
73
+ if isinstance(value, bytes):
74
+ value = value.decode("utf-8")
75
+
76
+ if key == "warning-bin":
77
+ self.logger.warning(value)
78
+ elif key == "info-bin":
79
+ self.logger.info(value)
80
+ elif key == "debug-bin":
81
+ self.logger.debug(value)