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.
- rsseismic/Application.py +196 -0
- rsseismic/ApplicationManager.py +111 -0
- rsseismic/BedrockLayer.py +29 -0
- rsseismic/MeanProfile.py +169 -0
- rsseismic/MeanProfileSoilLayer.py +119 -0
- rsseismic/Model.py +204 -0
- rsseismic/Motions.py +140 -0
- rsseismic/ProfileGenerationSettings.py +29 -0
- rsseismic/Profiles.py +186 -0
- rsseismic/ProjectSettings.py +46 -0
- rsseismic/ReferenceCurve.py +73 -0
- rsseismic/Results.py +419 -0
- rsseismic/SoilLayer.py +34 -0
- rsseismic/SoilLayerCurve.py +124 -0
- rsseismic/SoilLayerOperations.py +120 -0
- rsseismic/__init__.py +79 -0
- rsseismic/_application_paths.py +74 -0
- rsseismic/_batch_compute.py +420 -0
- rsseismic/_client.py +81 -0
- rsseismic/_proto_converters.py +345 -0
- rsseismic/_proxyObject.py +11 -0
- rsseismic/_result_bulk_dataframes.py +202 -0
- rsseismic/_result_dataframes.py +77 -0
- rsseismic/_rpc_helpers.py +55 -0
- rsseismic/enums/__init__.py +23 -0
- rsseismic/enums/_generated.py +120 -0
- rsseismic/enums/fitting_procedure.py +15 -0
- rsseismic/exceptions.py +11 -0
- rsseismic/generatedFiles/ApplicationService_pb2.py +46 -0
- rsseismic/generatedFiles/ApplicationService_pb2.pyi +67 -0
- rsseismic/generatedFiles/ApplicationService_pb2_grpc.py +274 -0
- rsseismic/generatedFiles/BedrockLayerDataService_pb2.py +27 -0
- rsseismic/generatedFiles/BedrockLayerDataService_pb2.pyi +5 -0
- rsseismic/generatedFiles/BedrockLayerDataService_pb2_grpc.py +231 -0
- rsseismic/generatedFiles/CommonMessages_pb2.py +50 -0
- rsseismic/generatedFiles/CommonMessages_pb2.pyi +113 -0
- rsseismic/generatedFiles/CommonMessages_pb2_grpc.py +29 -0
- rsseismic/generatedFiles/MeanProfileService_pb2.py +62 -0
- rsseismic/generatedFiles/MeanProfileService_pb2.pyi +176 -0
- rsseismic/generatedFiles/MeanProfileService_pb2_grpc.py +458 -0
- rsseismic/generatedFiles/MeanProfileSoilLayerDataService_pb2.py +45 -0
- rsseismic/generatedFiles/MeanProfileSoilLayerDataService_pb2.pyi +87 -0
- rsseismic/generatedFiles/MeanProfileSoilLayerDataService_pb2_grpc.py +360 -0
- rsseismic/generatedFiles/ModelService_pb2.py +54 -0
- rsseismic/generatedFiles/ModelService_pb2.pyi +122 -0
- rsseismic/generatedFiles/ModelService_pb2_grpc.py +360 -0
- rsseismic/generatedFiles/MotionService_pb2.py +78 -0
- rsseismic/generatedFiles/MotionService_pb2.pyi +224 -0
- rsseismic/generatedFiles/MotionService_pb2_grpc.py +575 -0
- rsseismic/generatedFiles/ProfileGenerationSettingsService_pb2.py +27 -0
- rsseismic/generatedFiles/ProfileGenerationSettingsService_pb2.pyi +5 -0
- rsseismic/generatedFiles/ProfileGenerationSettingsService_pb2_grpc.py +145 -0
- rsseismic/generatedFiles/ProfileService_pb2.py +76 -0
- rsseismic/generatedFiles/ProfileService_pb2.pyi +246 -0
- rsseismic/generatedFiles/ProfileService_pb2_grpc.py +532 -0
- rsseismic/generatedFiles/ProjectSettingsDataService_pb2.py +35 -0
- rsseismic/generatedFiles/ProjectSettingsDataService_pb2.pyi +40 -0
- rsseismic/generatedFiles/ProjectSettingsDataService_pb2_grpc.py +232 -0
- rsseismic/generatedFiles/ReferenceCurveDataService_pb2.py +41 -0
- rsseismic/generatedFiles/ReferenceCurveDataService_pb2.pyi +73 -0
- rsseismic/generatedFiles/ReferenceCurveDataService_pb2_grpc.py +281 -0
- rsseismic/generatedFiles/ResultsService_pb2.py +82 -0
- rsseismic/generatedFiles/ResultsService_pb2.pyi +373 -0
- rsseismic/generatedFiles/ResultsService_pb2_grpc.py +489 -0
- rsseismic/generatedFiles/SoilLayerCurveDataService_pb2.py +52 -0
- rsseismic/generatedFiles/SoilLayerCurveDataService_pb2.pyi +150 -0
- rsseismic/generatedFiles/SoilLayerCurveDataService_pb2_grpc.py +280 -0
- rsseismic/generatedFiles/SoilLayerDataService_pb2.py +27 -0
- rsseismic/generatedFiles/SoilLayerDataService_pb2.pyi +5 -0
- rsseismic/generatedFiles/SoilLayerDataService_pb2_grpc.py +231 -0
- rsseismic/generatedFiles/SoilLayerService_pb2.py +46 -0
- rsseismic/generatedFiles/SoilLayerService_pb2.pyi +122 -0
- rsseismic/generatedFiles/SoilLayerService_pb2_grpc.py +317 -0
- rsseismic/generatedFiles/__init__.py +4 -0
- rsseismic/properties/_PropertyAccessor.py +151 -0
- rsseismic/properties/__init__.py +6 -0
- rsseismic/properties/bedrockLayer/__init__.py +1 -0
- rsseismic/properties/bedrockLayer/_generated.py +127 -0
- rsseismic/properties/meanProfileSoilLayer/__init__.py +1 -0
- rsseismic/properties/meanProfileSoilLayer/_generated.py +87 -0
- rsseismic/properties/profileGenerationSettings/__init__.py +1 -0
- rsseismic/properties/profileGenerationSettings/_generated.py +195 -0
- rsseismic/properties/projectSettings/__init__.py +1 -0
- rsseismic/properties/projectSettings/_generated.py +270 -0
- rsseismic/properties/referenceCurve/__init__.py +1 -0
- rsseismic/properties/referenceCurve/_generated.py +264 -0
- rsseismic/properties/soilLayer/__init__.py +1 -0
- rsseismic/properties/soilLayer/_generated.py +523 -0
- rsseismic/types.py +311 -0
- rsseismicscripting-3.0.0.dist-info/METADATA +88 -0
- rsseismicscripting-3.0.0.dist-info/RECORD +94 -0
- rsseismicscripting-3.0.0.dist-info/WHEEL +5 -0
- rsseismicscripting-3.0.0.dist-info/licenses/License.txt +20 -0
- 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)
|