pyckster 26.1.5__py3-none-any.whl → 26.2.1__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.
- pyckster/__init__.py +1 -1
- pyckster/core.py +2633 -1367
- pyckster/obspy_utils.py +39 -0
- pyckster/pick_io.py +528 -3
- {pyckster-26.1.5.dist-info → pyckster-26.2.1.dist-info}/METADATA +1 -1
- {pyckster-26.1.5.dist-info → pyckster-26.2.1.dist-info}/RECORD +10 -10
- {pyckster-26.1.5.dist-info → pyckster-26.2.1.dist-info}/WHEEL +0 -0
- {pyckster-26.1.5.dist-info → pyckster-26.2.1.dist-info}/entry_points.txt +0 -0
- {pyckster-26.1.5.dist-info → pyckster-26.2.1.dist-info}/licenses/LICENCE +0 -0
- {pyckster-26.1.5.dist-info → pyckster-26.2.1.dist-info}/top_level.txt +0 -0
pyckster/core.py
CHANGED
|
@@ -80,6 +80,7 @@ from .obspy_utils import (
|
|
|
80
80
|
remove_trace,
|
|
81
81
|
move_trace,
|
|
82
82
|
mute_trace,
|
|
83
|
+
zero_pad_trace,
|
|
83
84
|
swap_header_format,
|
|
84
85
|
merge_streams,
|
|
85
86
|
get_max_decimals,
|
|
@@ -874,7 +875,7 @@ class TraceSelector(QDialog):
|
|
|
874
875
|
self.layout.addWidget(self.createFormItem("Trace Position (m):", self.tracePositionLineEdit))
|
|
875
876
|
|
|
876
877
|
# If trace_positions provided, initialize with the first trace position
|
|
877
|
-
if trace_positions and len(trace_positions) > 0:
|
|
878
|
+
if trace_positions is not None and len(trace_positions) > 0:
|
|
878
879
|
self.tracePositionLineEdit.setText(str(trace_positions[0]))
|
|
879
880
|
|
|
880
881
|
# Add a checkbox to apply the changes to all shots
|
|
@@ -1249,6 +1250,154 @@ def find_icon_path():
|
|
|
1249
1250
|
# Auto Pick Dialog
|
|
1250
1251
|
#######################################
|
|
1251
1252
|
|
|
1253
|
+
class AcquisitionParametersDialog(QDialog):
|
|
1254
|
+
"""Dialog to display acquisition parameters summary"""
|
|
1255
|
+
def __init__(self, params, parent=None):
|
|
1256
|
+
super().__init__(parent)
|
|
1257
|
+
self.setWindowTitle("Acquisition Parameters")
|
|
1258
|
+
self.setMinimumWidth(500)
|
|
1259
|
+
self.setMinimumHeight(400)
|
|
1260
|
+
self.resize(600, 500) # Set initial size
|
|
1261
|
+
|
|
1262
|
+
# Enable resizing and maximize button
|
|
1263
|
+
self.setWindowFlags(Qt.Window | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint)
|
|
1264
|
+
|
|
1265
|
+
layout = QVBoxLayout(self)
|
|
1266
|
+
|
|
1267
|
+
# Create a QTextEdit to display the parameters
|
|
1268
|
+
self.textEdit = QTextEdit()
|
|
1269
|
+
self.textEdit.setReadOnly(True)
|
|
1270
|
+
self.textEdit.setFont(QFont("Courier", 10))
|
|
1271
|
+
|
|
1272
|
+
# Format the parameters text
|
|
1273
|
+
text = self.formatParameters(params)
|
|
1274
|
+
self.textEdit.setPlainText(text)
|
|
1275
|
+
|
|
1276
|
+
layout.addWidget(self.textEdit)
|
|
1277
|
+
|
|
1278
|
+
# Add OK button
|
|
1279
|
+
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
1280
|
+
buttonBox.accepted.connect(self.accept)
|
|
1281
|
+
layout.addWidget(buttonBox)
|
|
1282
|
+
|
|
1283
|
+
def formatParameters(self, params):
|
|
1284
|
+
"""Format the parameters into a readable text"""
|
|
1285
|
+
lines = []
|
|
1286
|
+
lines.append("=" * 60)
|
|
1287
|
+
lines.append("ACQUISITION PARAMETERS SUMMARY")
|
|
1288
|
+
lines.append("=" * 60)
|
|
1289
|
+
lines.append("")
|
|
1290
|
+
|
|
1291
|
+
if 'error' in params:
|
|
1292
|
+
lines.append(f"Error: {params['error']}")
|
|
1293
|
+
return "\n".join(lines)
|
|
1294
|
+
|
|
1295
|
+
# Roll-along detection FIRST
|
|
1296
|
+
lines.append("ACQUISITION TYPE:")
|
|
1297
|
+
lines.append("-" * 60)
|
|
1298
|
+
if params.get('roll_along_detected'):
|
|
1299
|
+
lines.append(f" Roll-along acquisition: YES")
|
|
1300
|
+
if params.get('roll_along_segments') is not None:
|
|
1301
|
+
lines.append(f" Number of segments: {params['roll_along_segments']}")
|
|
1302
|
+
else:
|
|
1303
|
+
lines.append(f" Roll-along acquisition: NO (standard acquisition)")
|
|
1304
|
+
|
|
1305
|
+
# Shot positioning
|
|
1306
|
+
if params.get('shot_positioning'):
|
|
1307
|
+
lines.append(f" Shot positioning: {params['shot_positioning']}")
|
|
1308
|
+
lines.append("")
|
|
1309
|
+
|
|
1310
|
+
# Geophones/Traces section
|
|
1311
|
+
lines.append("GEOPHONES/TRACES:")
|
|
1312
|
+
lines.append("-" * 60)
|
|
1313
|
+
if params.get('total_unique_traces') is not None:
|
|
1314
|
+
lines.append(f" Total number of traces: {params['total_unique_traces']}")
|
|
1315
|
+
else:
|
|
1316
|
+
lines.append(f" Number of traces/shot: {params.get('n_traces', 'N/A')}")
|
|
1317
|
+
if params.get('trace_spacing') is not None:
|
|
1318
|
+
lines.append(f" Trace spacing: {params['trace_spacing']:.2f} m")
|
|
1319
|
+
else:
|
|
1320
|
+
lines.append(f" Trace spacing: N/A")
|
|
1321
|
+
if params.get('total_trace_extent') is not None:
|
|
1322
|
+
lines.append(f" Total trace extent: {params['total_trace_extent']:.2f} m")
|
|
1323
|
+
else:
|
|
1324
|
+
lines.append(f" Trace extent: N/A")
|
|
1325
|
+
if params.get('first_trace_pos') is not None:
|
|
1326
|
+
lines.append(f" First trace position: {params['first_trace_pos']:.2f} m")
|
|
1327
|
+
else:
|
|
1328
|
+
lines.append(f" First trace position: N/A")
|
|
1329
|
+
if params.get('last_trace_pos') is not None:
|
|
1330
|
+
lines.append(f" Last trace position: {params['last_trace_pos']:.2f} m")
|
|
1331
|
+
else:
|
|
1332
|
+
lines.append(f" Last trace position: N/A")
|
|
1333
|
+
|
|
1334
|
+
# Add roll-along segment details if available
|
|
1335
|
+
if params.get('roll_along_detected') and params.get('roll_along_segment_details'):
|
|
1336
|
+
lines.append("")
|
|
1337
|
+
lines.append(" Roll-along segment details:")
|
|
1338
|
+
for i, seg_detail in enumerate(params['roll_along_segment_details'], 1):
|
|
1339
|
+
lines.append(f" Segment {i}: {seg_detail}")
|
|
1340
|
+
lines.append("")
|
|
1341
|
+
|
|
1342
|
+
# Sources/Shots section
|
|
1343
|
+
lines.append("SOURCES/SHOTS:")
|
|
1344
|
+
lines.append("-" * 60)
|
|
1345
|
+
lines.append(f" Total number of shots: {params.get('n_shots', 'N/A')}")
|
|
1346
|
+
if params.get('source_spacing') is not None:
|
|
1347
|
+
spacing_label = " Source spacing (median): " if params.get('roll_along_detected') else " Source spacing: "
|
|
1348
|
+
lines.append(f"{spacing_label}{params['source_spacing']:.2f} m")
|
|
1349
|
+
else:
|
|
1350
|
+
lines.append(f" Source spacing: N/A")
|
|
1351
|
+
if params.get('total_source_extent') is not None:
|
|
1352
|
+
lines.append(f" Total source extent: {params['total_source_extent']:.2f} m")
|
|
1353
|
+
else:
|
|
1354
|
+
lines.append(f" Source extent: N/A")
|
|
1355
|
+
if params.get('first_shot_pos') is not None:
|
|
1356
|
+
lines.append(f" First shot position: {params['first_shot_pos']:.2f} m")
|
|
1357
|
+
else:
|
|
1358
|
+
lines.append(f" First shot position: N/A")
|
|
1359
|
+
if params.get('last_shot_pos') is not None:
|
|
1360
|
+
lines.append(f" Last shot position: {params['last_shot_pos']:.2f} m")
|
|
1361
|
+
else:
|
|
1362
|
+
lines.append(f" Last shot position: N/A")
|
|
1363
|
+
if params.get('min_offset') is not None:
|
|
1364
|
+
lines.append(f" Minimum offset: {abs(params['min_offset']):.2f} m")
|
|
1365
|
+
else:
|
|
1366
|
+
lines.append(f" Minimum offset: N/A")
|
|
1367
|
+
if params.get('max_offset') is not None:
|
|
1368
|
+
lines.append(f" Maximum offset: {abs(params['max_offset']):.2f} m")
|
|
1369
|
+
else:
|
|
1370
|
+
lines.append(f" Maximum offset: N/A")
|
|
1371
|
+
|
|
1372
|
+
# Add roll-along shot segment details if available
|
|
1373
|
+
if params.get('roll_along_detected') and params.get('roll_along_shot_segment_details'):
|
|
1374
|
+
lines.append("")
|
|
1375
|
+
lines.append(" Roll-along segment details:")
|
|
1376
|
+
for i, seg_detail in enumerate(params['roll_along_shot_segment_details'], 1):
|
|
1377
|
+
lines.append(f" Segment {i}: {seg_detail}")
|
|
1378
|
+
lines.append("")
|
|
1379
|
+
|
|
1380
|
+
# Timing section
|
|
1381
|
+
lines.append("TIMING:")
|
|
1382
|
+
lines.append("-" * 60)
|
|
1383
|
+
if params.get('sample_interval') is not None:
|
|
1384
|
+
lines.append(f" Sampling interval: {params['sample_interval']*1000:.3f} ms")
|
|
1385
|
+
else:
|
|
1386
|
+
lines.append(f" Sampling interval: N/A")
|
|
1387
|
+
if params.get('record_length') is not None:
|
|
1388
|
+
lines.append(f" Recording time: {params['record_length']*1000:.2f} ms")
|
|
1389
|
+
else:
|
|
1390
|
+
lines.append(f" Recording time: N/A")
|
|
1391
|
+
if params.get('delay') is not None:
|
|
1392
|
+
lines.append(f" Delay time: {params['delay']*1000:.3f} ms")
|
|
1393
|
+
else:
|
|
1394
|
+
lines.append(f" Delay time: N/A")
|
|
1395
|
+
lines.append("")
|
|
1396
|
+
lines.append("=" * 60)
|
|
1397
|
+
|
|
1398
|
+
return "\n".join(lines)
|
|
1399
|
+
|
|
1400
|
+
|
|
1252
1401
|
class AutoPickDialog(QDialog):
|
|
1253
1402
|
"""Dialog for entering frequency range for auto pick operation"""
|
|
1254
1403
|
def __init__(self, parent=None, fmin_default=0.0, fmax_default=200.0):
|
|
@@ -1493,6 +1642,12 @@ class MainWindow(QMainWindow):
|
|
|
1493
1642
|
self.fileListWidget.setMinimumWidth(50) # Set minimum width
|
|
1494
1643
|
leftLayout.addWidget(self.fileListWidget)
|
|
1495
1644
|
|
|
1645
|
+
# Add Acquisition parameters button
|
|
1646
|
+
self.acqParamsButton = QPushButton("Acquisition parameters")
|
|
1647
|
+
self.acqParamsButton.clicked.connect(self.showAcquisitionParameters)
|
|
1648
|
+
self.acqParamsButton.setToolTip("Show acquisition parameters summary")
|
|
1649
|
+
leftLayout.addWidget(self.acqParamsButton)
|
|
1650
|
+
|
|
1496
1651
|
# Add separator line
|
|
1497
1652
|
separator = QFrame()
|
|
1498
1653
|
separator.setFrameShape(QFrame.HLine)
|
|
@@ -1506,7 +1661,7 @@ class MainWindow(QMainWindow):
|
|
|
1506
1661
|
leftLayout.addWidget(QLabel("Plot traces by:"))
|
|
1507
1662
|
self.tracesCombo = QComboBox()
|
|
1508
1663
|
self.tracesCombo.addItems(["Original trace no", "Trace in shot", "Trace in survey", "Position"])
|
|
1509
|
-
self.tracesCombo.setCurrentText("
|
|
1664
|
+
self.tracesCombo.setCurrentText("Trace in shot")
|
|
1510
1665
|
self.tracesCombo.currentTextChanged.connect(self._onTracesComboChanged)
|
|
1511
1666
|
leftLayout.addWidget(self.tracesCombo)
|
|
1512
1667
|
|
|
@@ -2144,6 +2299,16 @@ class MainWindow(QMainWindow):
|
|
|
2144
2299
|
self.editTraceSubMenu.addAction(self.reverseTracesAction)
|
|
2145
2300
|
self.reverseTracesAction.triggered.connect(self.reverseTraces)
|
|
2146
2301
|
|
|
2302
|
+
# Create a QAction for inserting zero traces
|
|
2303
|
+
self.insertMutedTracesAction = QAction('Insert zero traces', self)
|
|
2304
|
+
self.editTraceSubMenu.addAction(self.insertMutedTracesAction)
|
|
2305
|
+
self.insertMutedTracesAction.triggered.connect(self.insertMutedTraces)
|
|
2306
|
+
|
|
2307
|
+
# Create a QAction for zero padding traces
|
|
2308
|
+
self.zeroPadTraceAction = QAction('Zero pad traces', self)
|
|
2309
|
+
self.editTraceSubMenu.addAction(self.zeroPadTraceAction)
|
|
2310
|
+
self.zeroPadTraceAction.triggered.connect(self.zeroPadTrace)
|
|
2311
|
+
|
|
2147
2312
|
# Create a submenu for batch editing traces
|
|
2148
2313
|
self.batchEditTraceSubMenu = self.headerMenu.addMenu('Batch edit traces')
|
|
2149
2314
|
|
|
@@ -2172,6 +2337,16 @@ class MainWindow(QMainWindow):
|
|
|
2172
2337
|
self.batchEditTraceSubMenu.addAction(self.batchReverseTracesAction)
|
|
2173
2338
|
self.batchReverseTracesAction.triggered.connect(self.batchReverseTraces)
|
|
2174
2339
|
|
|
2340
|
+
# Create a QAction for batch inserting zero traces
|
|
2341
|
+
self.batchInsertMutedTracesAction = QAction('Batch insert zero traces', self)
|
|
2342
|
+
self.batchEditTraceSubMenu.addAction(self.batchInsertMutedTracesAction)
|
|
2343
|
+
self.batchInsertMutedTracesAction.triggered.connect(self.batchInsertMutedTraces)
|
|
2344
|
+
|
|
2345
|
+
# Create a QAction for batch zero padding traces
|
|
2346
|
+
self.batchZeroPadTracesAction = QAction('Batch zero pad traces', self)
|
|
2347
|
+
self.batchEditTraceSubMenu.addAction(self.batchZeroPadTracesAction)
|
|
2348
|
+
self.batchZeroPadTracesAction.triggered.connect(self.batchZeroPadTraces)
|
|
2349
|
+
|
|
2175
2350
|
# Add separator
|
|
2176
2351
|
self.headerMenu.addSeparator()
|
|
2177
2352
|
|
|
@@ -2945,107 +3120,111 @@ class MainWindow(QMainWindow):
|
|
|
2945
3120
|
|
|
2946
3121
|
def _process_bottom_view_change(self, view_type):
|
|
2947
3122
|
"""Process bottom view change - handles plotting logic"""
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
self
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3123
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
3124
|
+
try:
|
|
3125
|
+
# Prevent redundant control visibility updates if we're in a swap
|
|
3126
|
+
should_update_visibility = not getattr(self, '_in_view_swap', False)
|
|
3127
|
+
|
|
3128
|
+
# Map view names to internal types
|
|
3129
|
+
view_mapping = {
|
|
3130
|
+
"Seismogram": "seismogram",
|
|
3131
|
+
"Layout": "layout",
|
|
3132
|
+
"Traveltimes": "traveltime",
|
|
3133
|
+
"Topography": "topo",
|
|
3134
|
+
"Spectrogram": "spectrogram",
|
|
3135
|
+
"Dispersion": "dispersion",
|
|
3136
|
+
"Pseudo-section": "pseudosection"
|
|
3137
|
+
}
|
|
3138
|
+
new_plot_type = view_mapping.get(view_type, "layout")
|
|
3139
|
+
|
|
3140
|
+
# Only rebuild dynamic controls if the view type has actually changed
|
|
3141
|
+
# This prevents unnecessary widget destruction/creation and flickering
|
|
3142
|
+
if getattr(self, '_last_bottom_plot_type', None) != new_plot_type:
|
|
3143
|
+
self.buildBottomControlsForView(view_type)
|
|
3144
|
+
self._last_bottom_plot_type = new_plot_type
|
|
3145
|
+
|
|
3146
|
+
# Scroll bottom controls scroll area to the beginning (left)
|
|
3147
|
+
if hasattr(self, 'bottomControlsScrollArea'):
|
|
3148
|
+
self.bottomControlsScrollArea.horizontalScrollBar().setValue(0)
|
|
3149
|
+
|
|
3150
|
+
# Track bottom plot type for control visibility
|
|
3151
|
+
self.bottomPlotType = new_plot_type
|
|
3152
|
+
|
|
3153
|
+
# Update trace window checkbox state based on view combination
|
|
3154
|
+
self._updateTraceWindowCheckboxState()
|
|
2978
3155
|
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
3156
|
+
# Populate trace range dropdowns if this is Dispersion view
|
|
3157
|
+
if view_type == "Dispersion":
|
|
3158
|
+
# Only enable if we have a stream loaded
|
|
3159
|
+
if self.streams and self.currentIndex is not None:
|
|
3160
|
+
self.populateTraceRangeComboboxes()
|
|
3161
|
+
else:
|
|
3162
|
+
if hasattr(self, 'traceRangeSlider') and self.traceRangeSlider is not None:
|
|
3163
|
+
try:
|
|
3164
|
+
self.traceRangeSlider.disableControls()
|
|
3165
|
+
except RuntimeError:
|
|
3166
|
+
pass # Widget may have been deleted during UI rebuild
|
|
2984
3167
|
else:
|
|
3168
|
+
# Disable trace range slider for other view types
|
|
2985
3169
|
if hasattr(self, 'traceRangeSlider') and self.traceRangeSlider is not None:
|
|
2986
3170
|
try:
|
|
2987
3171
|
self.traceRangeSlider.disableControls()
|
|
2988
3172
|
except RuntimeError:
|
|
2989
3173
|
pass # Widget may have been deleted during UI rebuild
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
self.
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
self.
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
self.updateTitle()
|
|
3045
|
-
|
|
3046
|
-
# Update control visibility based on current view types (only if not in a swap)
|
|
3047
|
-
if should_update_visibility:
|
|
3048
|
-
self._updateControlVisibility()
|
|
3174
|
+
|
|
3175
|
+
if view_type == "Seismogram":
|
|
3176
|
+
# Plot seismogram to bottom widget
|
|
3177
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3178
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3179
|
+
self.updatePlots()
|
|
3180
|
+
elif view_type == "Layout":
|
|
3181
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3182
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3183
|
+
self.plotLayout()
|
|
3184
|
+
self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
|
|
3185
|
+
elif view_type == "Traveltimes":
|
|
3186
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3187
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3188
|
+
self.plotTravelTime()
|
|
3189
|
+
self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
|
|
3190
|
+
elif view_type == "Topography":
|
|
3191
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3192
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3193
|
+
self.plotTopo()
|
|
3194
|
+
self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
|
|
3195
|
+
elif view_type == "Spectrogram":
|
|
3196
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3197
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3198
|
+
self.plotSpectrogram()
|
|
3199
|
+
self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
|
|
3200
|
+
elif view_type == "Dispersion":
|
|
3201
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3202
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3203
|
+
self.plotDispersion()
|
|
3204
|
+
elif view_type == "Pseudo-section":
|
|
3205
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3206
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3207
|
+
self.plotPseudoSection()
|
|
3208
|
+
self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
|
|
3209
|
+
|
|
3210
|
+
# Reset target widget after plotting
|
|
3211
|
+
self._plot_target_widget = None
|
|
3212
|
+
self._plot_target_viewbox = None
|
|
3213
|
+
|
|
3214
|
+
# Draw/update trace extent indicator after view change
|
|
3215
|
+
self.drawTraceExtentIndicator()
|
|
3216
|
+
|
|
3217
|
+
# Reset view to proper default range after switching
|
|
3218
|
+
self.resetBottomView()
|
|
3219
|
+
|
|
3220
|
+
# Update title to show on correct plot widget based on current view types
|
|
3221
|
+
self.updateTitle()
|
|
3222
|
+
|
|
3223
|
+
# Update control visibility based on current view types (only if not in a swap)
|
|
3224
|
+
if should_update_visibility:
|
|
3225
|
+
self._updateControlVisibility()
|
|
3226
|
+
finally:
|
|
3227
|
+
QApplication.restoreOverrideCursor()
|
|
3049
3228
|
|
|
3050
3229
|
def _updateSeismoMenuActionsForMode(self):
|
|
3051
3230
|
"""Update menu action availability based on seismogram display mode.
|
|
@@ -3854,76 +4033,92 @@ class MainWindow(QMainWindow):
|
|
|
3854
4033
|
self.spectrogram_norm_per_trace = bool(state)
|
|
3855
4034
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3856
4035
|
return
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
self
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
self
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
4036
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
4037
|
+
try:
|
|
4038
|
+
# Replot both top and bottom if they use spectrogram
|
|
4039
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "spectrogram":
|
|
4040
|
+
self._plot_target_widget = self.plotWidget
|
|
4041
|
+
self._plot_target_viewbox = self.viewBox
|
|
4042
|
+
self.plotSpectrogram()
|
|
4043
|
+
self._plot_target_widget = None
|
|
4044
|
+
self._plot_target_viewbox = None
|
|
4045
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "spectrogram":
|
|
4046
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4047
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4048
|
+
self.plotSpectrogram()
|
|
4049
|
+
self._plot_target_widget = None
|
|
4050
|
+
self._plot_target_viewbox = None
|
|
4051
|
+
finally:
|
|
4052
|
+
QApplication.restoreOverrideCursor()
|
|
3870
4053
|
|
|
3871
4054
|
def onNormPerTraceChanged_Dispersion(self, state):
|
|
3872
4055
|
"""Handle per-trace normalization toggle for Dispersion view"""
|
|
3873
4056
|
self.dispersion_norm_per_trace = bool(state)
|
|
3874
4057
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3875
4058
|
return
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
self
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
self
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4059
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
4060
|
+
try:
|
|
4061
|
+
# Replot both top and bottom if they use dispersion
|
|
4062
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
4063
|
+
self._plot_target_widget = self.plotWidget
|
|
4064
|
+
self._plot_target_viewbox = self.viewBox
|
|
4065
|
+
self.plotDispersion()
|
|
4066
|
+
self._plot_target_widget = None
|
|
4067
|
+
self._plot_target_viewbox = None
|
|
4068
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
4069
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4070
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4071
|
+
self.plotDispersion()
|
|
4072
|
+
self._plot_target_widget = None
|
|
4073
|
+
self._plot_target_viewbox = None
|
|
4074
|
+
finally:
|
|
4075
|
+
QApplication.restoreOverrideCursor()
|
|
3889
4076
|
|
|
3890
4077
|
def onNormPerFreqChanged_Spectrogram(self, state):
|
|
3891
4078
|
"""Handle per-freq normalization toggle for Spectrogram view"""
|
|
3892
4079
|
self.spectrogram_norm_per_freq = bool(state)
|
|
3893
4080
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3894
4081
|
return
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
self
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
self
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
4082
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
4083
|
+
try:
|
|
4084
|
+
# Replot both top and bottom if they use spectrogram
|
|
4085
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "spectrogram":
|
|
4086
|
+
self._plot_target_widget = self.plotWidget
|
|
4087
|
+
self._plot_target_viewbox = self.viewBox
|
|
4088
|
+
self.plotSpectrogram()
|
|
4089
|
+
self._plot_target_widget = None
|
|
4090
|
+
self._plot_target_viewbox = None
|
|
4091
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "spectrogram":
|
|
4092
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4093
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4094
|
+
self.plotSpectrogram()
|
|
4095
|
+
self._plot_target_widget = None
|
|
4096
|
+
self._plot_target_viewbox = None
|
|
4097
|
+
finally:
|
|
4098
|
+
QApplication.restoreOverrideCursor()
|
|
3908
4099
|
|
|
3909
4100
|
def onNormPerFreqChanged_Dispersion(self, state):
|
|
3910
4101
|
"""Handle per-freq normalization toggle for Dispersion view"""
|
|
3911
4102
|
self.dispersion_norm_per_freq = bool(state)
|
|
3912
4103
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3913
4104
|
return
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
self
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
self
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4105
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
4106
|
+
try:
|
|
4107
|
+
# Replot both top and bottom if they use dispersion
|
|
4108
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
4109
|
+
self._plot_target_widget = self.plotWidget
|
|
4110
|
+
self._plot_target_viewbox = self.viewBox
|
|
4111
|
+
self.plotDispersion()
|
|
4112
|
+
self._plot_target_widget = None
|
|
4113
|
+
self._plot_target_viewbox = None
|
|
4114
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
4115
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4116
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4117
|
+
self.plotDispersion()
|
|
4118
|
+
self._plot_target_widget = None
|
|
4119
|
+
self._plot_target_viewbox = None
|
|
4120
|
+
finally:
|
|
4121
|
+
QApplication.restoreOverrideCursor()
|
|
3927
4122
|
|
|
3928
4123
|
def onDispersionSaturationChanged(self, value):
|
|
3929
4124
|
"""Handle saturation slider change for dispersion images"""
|
|
@@ -5138,7 +5333,11 @@ class MainWindow(QMainWindow):
|
|
|
5138
5333
|
# Replot if either top or bottom shows seismogram (updatePlots() handles both)
|
|
5139
5334
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5140
5335
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5141
|
-
|
|
5336
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5337
|
+
try:
|
|
5338
|
+
self.updatePlots()
|
|
5339
|
+
finally:
|
|
5340
|
+
QApplication.restoreOverrideCursor()
|
|
5142
5341
|
|
|
5143
5342
|
def onSeismoGainChanged(self, value):
|
|
5144
5343
|
"""Handle seismogram gain change - unified"""
|
|
@@ -5148,7 +5347,11 @@ class MainWindow(QMainWindow):
|
|
|
5148
5347
|
# Replot if either top or bottom shows seismogram (updatePlots() handles both)
|
|
5149
5348
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5150
5349
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5151
|
-
|
|
5350
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5351
|
+
try:
|
|
5352
|
+
self.updatePlots()
|
|
5353
|
+
finally:
|
|
5354
|
+
QApplication.restoreOverrideCursor()
|
|
5152
5355
|
|
|
5153
5356
|
def onSeismoFillModeChanged(self, mode_text):
|
|
5154
5357
|
"""Handle seismogram fill mode change (Pos/Neg/None) - unified"""
|
|
@@ -5174,7 +5377,11 @@ class MainWindow(QMainWindow):
|
|
|
5174
5377
|
# Replot if either top or bottom shows seismogram (updatePlots() handles both)
|
|
5175
5378
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5176
5379
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5177
|
-
|
|
5380
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5381
|
+
try:
|
|
5382
|
+
self.updatePlots()
|
|
5383
|
+
finally:
|
|
5384
|
+
QApplication.restoreOverrideCursor()
|
|
5178
5385
|
|
|
5179
5386
|
def _calculateAmplitudeRatio(self):
|
|
5180
5387
|
"""Calculate the amplitude ratio (max/min) across all loaded traces"""
|
|
@@ -5252,8 +5459,12 @@ class MainWindow(QMainWindow):
|
|
|
5252
5459
|
# Replot if either top or bottom shows seismogram (updatePlots() handles both)
|
|
5253
5460
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5254
5461
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5255
|
-
|
|
5256
|
-
|
|
5462
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5463
|
+
try:
|
|
5464
|
+
self.updatePlots()
|
|
5465
|
+
finally:
|
|
5466
|
+
QApplication.restoreOverrideCursor()
|
|
5467
|
+
|
|
5257
5468
|
def onSeismoClipChanged(self, state):
|
|
5258
5469
|
"""Handle seismogram clip toggle - unified"""
|
|
5259
5470
|
self.seismo_clip = bool(state)
|
|
@@ -5263,7 +5474,11 @@ class MainWindow(QMainWindow):
|
|
|
5263
5474
|
# Replot if either top or bottom shows seismogram (updatePlots() handles both)
|
|
5264
5475
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5265
5476
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5266
|
-
|
|
5477
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5478
|
+
try:
|
|
5479
|
+
self.updatePlots()
|
|
5480
|
+
finally:
|
|
5481
|
+
QApplication.restoreOverrideCursor()
|
|
5267
5482
|
|
|
5268
5483
|
def onSeismoOverlapChanged(self, display_value):
|
|
5269
5484
|
"""Handle seismogram overlap multiplicator change
|
|
@@ -5287,7 +5502,11 @@ class MainWindow(QMainWindow):
|
|
|
5287
5502
|
# Replot if either top or bottom shows seismogram
|
|
5288
5503
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5289
5504
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5290
|
-
|
|
5505
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5506
|
+
try:
|
|
5507
|
+
self.updatePlots()
|
|
5508
|
+
finally:
|
|
5509
|
+
QApplication.restoreOverrideCursor()
|
|
5291
5510
|
|
|
5292
5511
|
def onSeismoReversePolarityChanged(self, state):
|
|
5293
5512
|
"""Handle seismogram reverse polarity toggle - unified"""
|
|
@@ -5326,7 +5545,11 @@ class MainWindow(QMainWindow):
|
|
|
5326
5545
|
# Replot if either top or bottom shows seismogram (updatePlots() handles both)
|
|
5327
5546
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5328
5547
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5329
|
-
|
|
5548
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5549
|
+
try:
|
|
5550
|
+
self.updatePlots()
|
|
5551
|
+
finally:
|
|
5552
|
+
QApplication.restoreOverrideCursor()
|
|
5330
5553
|
|
|
5331
5554
|
def onSeismoMaxTimeChanged(self, value):
|
|
5332
5555
|
"""Handle seismogram max time change - unified"""
|
|
@@ -5336,7 +5559,11 @@ class MainWindow(QMainWindow):
|
|
|
5336
5559
|
# Replot if either top or bottom shows seismogram (updatePlots() handles both)
|
|
5337
5560
|
if (hasattr(self, 'topPlotType') and self.topPlotType == "seismogram") or \
|
|
5338
5561
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5339
|
-
|
|
5562
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
5563
|
+
try:
|
|
5564
|
+
self.updatePlots()
|
|
5565
|
+
finally:
|
|
5566
|
+
QApplication.restoreOverrideCursor()
|
|
5340
5567
|
|
|
5341
5568
|
def onPickingShowCrosshairChanged(self, state):
|
|
5342
5569
|
"""Handle picking crosshair toggle - unified for seismogram and dispersion views"""
|
|
@@ -6489,7 +6716,7 @@ class MainWindow(QMainWindow):
|
|
|
6489
6716
|
self.col = 'k'
|
|
6490
6717
|
self.fill_brush = (0, 0, 0, 150)
|
|
6491
6718
|
self._batch_loading = False # Flag to prevent plotting during batch loading
|
|
6492
|
-
self.plotTypeX = '
|
|
6719
|
+
self.plotTypeX = 'file_trace_number' # Plot type for X axis
|
|
6493
6720
|
self.plotTypeY = 'ffid' # Plot type for Y axis
|
|
6494
6721
|
self.t_label = 'Time (s)'
|
|
6495
6722
|
self.relativeError = 0.05
|
|
@@ -7055,6 +7282,250 @@ class MainWindow(QMainWindow):
|
|
|
7055
7282
|
|
|
7056
7283
|
self.updatePlotTypeDict()
|
|
7057
7284
|
|
|
7285
|
+
def showAcquisitionParameters(self):
|
|
7286
|
+
"""Display a dialog with acquisition parameters summary"""
|
|
7287
|
+
if not self.streams or len(self.streams) == 0:
|
|
7288
|
+
QMessageBox.information(self, "No Data", "No seismic files loaded.")
|
|
7289
|
+
return
|
|
7290
|
+
|
|
7291
|
+
# Calculate parameters from all loaded streams
|
|
7292
|
+
params = self.calculateAcquisitionParameters()
|
|
7293
|
+
|
|
7294
|
+
# Show the dialog
|
|
7295
|
+
dialog = AcquisitionParametersDialog(params, self)
|
|
7296
|
+
dialog.exec_()
|
|
7297
|
+
|
|
7298
|
+
def calculateAcquisitionParameters(self):
|
|
7299
|
+
"""Calculate acquisition parameters from loaded data"""
|
|
7300
|
+
params = {}
|
|
7301
|
+
|
|
7302
|
+
try:
|
|
7303
|
+
# Number of shots
|
|
7304
|
+
params['n_shots'] = len(self.streams)
|
|
7305
|
+
|
|
7306
|
+
# Get parameters from current or first file
|
|
7307
|
+
idx = self.currentIndex if self.currentIndex is not None else 0
|
|
7308
|
+
|
|
7309
|
+
if idx >= len(self.streams):
|
|
7310
|
+
params['error'] = "Invalid file index"
|
|
7311
|
+
return params
|
|
7312
|
+
|
|
7313
|
+
# Number of traces (from current/first shot)
|
|
7314
|
+
params['n_traces'] = len(self.streams[idx])
|
|
7315
|
+
|
|
7316
|
+
# Collect all unique trace positions across all shots for total extent
|
|
7317
|
+
all_trace_positions = set()
|
|
7318
|
+
if hasattr(self, 'trace_position') and len(self.trace_position) > 0:
|
|
7319
|
+
for trace_pos_list in self.trace_position:
|
|
7320
|
+
if trace_pos_list is not None:
|
|
7321
|
+
all_trace_positions.update(trace_pos_list)
|
|
7322
|
+
|
|
7323
|
+
# Total unique traces and overall extent
|
|
7324
|
+
if len(all_trace_positions) > 0:
|
|
7325
|
+
params['total_unique_traces'] = len(all_trace_positions)
|
|
7326
|
+
params['total_trace_extent'] = max(all_trace_positions) - min(all_trace_positions)
|
|
7327
|
+
params['first_trace_pos'] = min(all_trace_positions)
|
|
7328
|
+
params['last_trace_pos'] = max(all_trace_positions)
|
|
7329
|
+
else:
|
|
7330
|
+
params['total_unique_traces'] = None
|
|
7331
|
+
params['total_trace_extent'] = None
|
|
7332
|
+
params['first_trace_pos'] = None
|
|
7333
|
+
params['last_trace_pos'] = None
|
|
7334
|
+
|
|
7335
|
+
# Trace spacing (from current/first shot)
|
|
7336
|
+
if hasattr(self, 'trace_position') and idx < len(self.trace_position):
|
|
7337
|
+
trace_pos = self.trace_position[idx]
|
|
7338
|
+
if trace_pos is not None and len(trace_pos) > 1:
|
|
7339
|
+
# Calculate spacing using median for robustness
|
|
7340
|
+
spacings = [trace_pos[i+1] - trace_pos[i] for i in range(len(trace_pos)-1)]
|
|
7341
|
+
params['trace_spacing'] = np.median(spacings)
|
|
7342
|
+
elif trace_pos is not None and len(trace_pos) == 1:
|
|
7343
|
+
params['trace_spacing'] = None
|
|
7344
|
+
else:
|
|
7345
|
+
params['trace_spacing'] = None
|
|
7346
|
+
else:
|
|
7347
|
+
params['trace_spacing'] = None
|
|
7348
|
+
|
|
7349
|
+
# Source spacing and extent
|
|
7350
|
+
if hasattr(self, 'source_position') and len(self.source_position) > 1:
|
|
7351
|
+
source_positions = self.source_position
|
|
7352
|
+
if len(source_positions) > 1:
|
|
7353
|
+
# Calculate spacing using MEDIAN to handle roll-along gaps
|
|
7354
|
+
spacings = [source_positions[i+1] - source_positions[i] for i in range(len(source_positions)-1)]
|
|
7355
|
+
params['source_spacing'] = np.median(np.abs(spacings))
|
|
7356
|
+
# Total extent from all shots
|
|
7357
|
+
params['total_source_extent'] = max(source_positions) - min(source_positions)
|
|
7358
|
+
params['first_shot_pos'] = min(source_positions)
|
|
7359
|
+
params['last_shot_pos'] = max(source_positions)
|
|
7360
|
+
else:
|
|
7361
|
+
params['source_spacing'] = None
|
|
7362
|
+
params['total_source_extent'] = None
|
|
7363
|
+
params['first_shot_pos'] = source_positions[0] if len(source_positions) > 0 else None
|
|
7364
|
+
params['last_shot_pos'] = source_positions[0] if len(source_positions) > 0 else None
|
|
7365
|
+
else:
|
|
7366
|
+
params['source_spacing'] = None
|
|
7367
|
+
params['total_source_extent'] = None
|
|
7368
|
+
params['first_shot_pos'] = None
|
|
7369
|
+
params['last_shot_pos'] = None
|
|
7370
|
+
|
|
7371
|
+
# Detect shot positioning relative to traces
|
|
7372
|
+
params['shot_positioning'] = None
|
|
7373
|
+
if (hasattr(self, 'source_position') and hasattr(self, 'trace_position') and
|
|
7374
|
+
len(self.source_position) > 0 and len(self.trace_position) > 0):
|
|
7375
|
+
try:
|
|
7376
|
+
# Collect all unique trace positions across all shots
|
|
7377
|
+
all_trace_positions = set()
|
|
7378
|
+
for trace_pos_list in self.trace_position:
|
|
7379
|
+
if trace_pos_list is not None:
|
|
7380
|
+
all_trace_positions.update(trace_pos_list)
|
|
7381
|
+
|
|
7382
|
+
# Check each source position against trace positions
|
|
7383
|
+
tolerance = 0.1 # 10 cm tolerance for considering positions equal
|
|
7384
|
+
at_trace_count = 0
|
|
7385
|
+
between_trace_count = 0
|
|
7386
|
+
|
|
7387
|
+
for src_pos in self.source_position:
|
|
7388
|
+
is_at_trace = any(abs(src_pos - trace_pos) < tolerance for trace_pos in all_trace_positions)
|
|
7389
|
+
if is_at_trace:
|
|
7390
|
+
at_trace_count += 1
|
|
7391
|
+
else:
|
|
7392
|
+
between_trace_count += 1
|
|
7393
|
+
|
|
7394
|
+
# Determine positioning type
|
|
7395
|
+
if at_trace_count > 0 and between_trace_count == 0:
|
|
7396
|
+
params['shot_positioning'] = "At trace positions (coincident)"
|
|
7397
|
+
elif between_trace_count > 0 and at_trace_count == 0:
|
|
7398
|
+
params['shot_positioning'] = "Between traces (offset)"
|
|
7399
|
+
elif at_trace_count > 0 and between_trace_count > 0:
|
|
7400
|
+
params['shot_positioning'] = f"Mixed ({at_trace_count} at traces, {between_trace_count} between)"
|
|
7401
|
+
else:
|
|
7402
|
+
params['shot_positioning'] = "Unknown"
|
|
7403
|
+
except Exception:
|
|
7404
|
+
params['shot_positioning'] = "Unknown"
|
|
7405
|
+
|
|
7406
|
+
# Min/Max offset
|
|
7407
|
+
if hasattr(self, 'offset') and len(self.offset) > 0:
|
|
7408
|
+
all_offsets = []
|
|
7409
|
+
for offset_list in self.offset:
|
|
7410
|
+
if offset_list is not None and len(offset_list) > 0:
|
|
7411
|
+
all_offsets.extend(offset_list)
|
|
7412
|
+
if len(all_offsets) > 0:
|
|
7413
|
+
# Use absolute values for offset ranges
|
|
7414
|
+
abs_offsets = [abs(offset) for offset in all_offsets]
|
|
7415
|
+
params['min_offset'] = min(abs_offsets)
|
|
7416
|
+
params['max_offset'] = max(abs_offsets)
|
|
7417
|
+
else:
|
|
7418
|
+
params['min_offset'] = None
|
|
7419
|
+
params['max_offset'] = None
|
|
7420
|
+
else:
|
|
7421
|
+
params['min_offset'] = None
|
|
7422
|
+
params['max_offset'] = None
|
|
7423
|
+
|
|
7424
|
+
# Roll-along detection
|
|
7425
|
+
params['roll_along_detected'] = False
|
|
7426
|
+
params['roll_along_segments'] = None
|
|
7427
|
+
params['roll_along_segment_details'] = None
|
|
7428
|
+
params['roll_along_shot_segment_details'] = None
|
|
7429
|
+
|
|
7430
|
+
if (hasattr(self, 'source_position') and hasattr(self, 'trace_position') and
|
|
7431
|
+
len(self.source_position) > 1 and len(self.trace_position) > 1):
|
|
7432
|
+
try:
|
|
7433
|
+
# Detect roll-along by checking if trace positions change while
|
|
7434
|
+
# some shots might be at similar positions
|
|
7435
|
+
# Group shots by similar trace array positions
|
|
7436
|
+
segments = []
|
|
7437
|
+
current_segment = [0]
|
|
7438
|
+
|
|
7439
|
+
for i in range(1, len(self.trace_position)):
|
|
7440
|
+
if self.trace_position[i] is not None and self.trace_position[i-1] is not None:
|
|
7441
|
+
# Check if trace positions have changed significantly
|
|
7442
|
+
prev_min = min(self.trace_position[i-1])
|
|
7443
|
+
prev_max = max(self.trace_position[i-1])
|
|
7444
|
+
curr_min = min(self.trace_position[i])
|
|
7445
|
+
curr_max = max(self.trace_position[i])
|
|
7446
|
+
|
|
7447
|
+
# If the trace array has shifted (different min/max positions)
|
|
7448
|
+
# This indicates a new segment in roll-along
|
|
7449
|
+
if abs(curr_min - prev_min) > 0.1 or abs(curr_max - prev_max) > 0.1:
|
|
7450
|
+
segments.append(current_segment)
|
|
7451
|
+
current_segment = [i]
|
|
7452
|
+
else:
|
|
7453
|
+
current_segment.append(i)
|
|
7454
|
+
else:
|
|
7455
|
+
current_segment.append(i)
|
|
7456
|
+
|
|
7457
|
+
# Add last segment
|
|
7458
|
+
if current_segment:
|
|
7459
|
+
segments.append(current_segment)
|
|
7460
|
+
|
|
7461
|
+
# Roll-along is detected if we have multiple segments
|
|
7462
|
+
if len(segments) > 1:
|
|
7463
|
+
params['roll_along_detected'] = True
|
|
7464
|
+
params['roll_along_segments'] = len(segments)
|
|
7465
|
+
|
|
7466
|
+
# Get detailed info for each segment (traces)
|
|
7467
|
+
segment_details = []
|
|
7468
|
+
for seg_idx, segment in enumerate(segments):
|
|
7469
|
+
# Get trace positions for this segment
|
|
7470
|
+
seg_trace_pos = self.trace_position[segment[0]]
|
|
7471
|
+
if seg_trace_pos is not None:
|
|
7472
|
+
seg_unique_traces = set()
|
|
7473
|
+
for shot_idx in segment:
|
|
7474
|
+
if self.trace_position[shot_idx] is not None:
|
|
7475
|
+
seg_unique_traces.update(self.trace_position[shot_idx])
|
|
7476
|
+
|
|
7477
|
+
seg_min = min(seg_unique_traces)
|
|
7478
|
+
seg_max = max(seg_unique_traces)
|
|
7479
|
+
seg_extent = seg_max - seg_min
|
|
7480
|
+
seg_n_traces = len(seg_unique_traces)
|
|
7481
|
+
|
|
7482
|
+
detail = f"{seg_n_traces} traces, {seg_min:.0f} to {seg_max:.0f} m (extent: {seg_extent:.0f} m)"
|
|
7483
|
+
segment_details.append(detail)
|
|
7484
|
+
|
|
7485
|
+
params['roll_along_segment_details'] = segment_details
|
|
7486
|
+
|
|
7487
|
+
# Get detailed info for each segment (shots)
|
|
7488
|
+
shot_segment_details = []
|
|
7489
|
+
for seg_idx, segment in enumerate(segments):
|
|
7490
|
+
# Get shot positions for this segment
|
|
7491
|
+
seg_shot_positions = [self.source_position[i] for i in segment if i < len(self.source_position)]
|
|
7492
|
+
if seg_shot_positions:
|
|
7493
|
+
seg_min_shot = min(seg_shot_positions)
|
|
7494
|
+
seg_max_shot = max(seg_shot_positions)
|
|
7495
|
+
seg_extent_shot = seg_max_shot - seg_min_shot
|
|
7496
|
+
seg_n_shots = len(seg_shot_positions)
|
|
7497
|
+
|
|
7498
|
+
detail = f"{seg_n_shots} shots, {seg_min_shot:.0f} to {seg_max_shot:.0f} m (extent: {seg_extent_shot:.0f} m)"
|
|
7499
|
+
shot_segment_details.append(detail)
|
|
7500
|
+
|
|
7501
|
+
params['roll_along_shot_segment_details'] = shot_segment_details
|
|
7502
|
+
except Exception:
|
|
7503
|
+
# If roll-along detection fails, just mark as not detected
|
|
7504
|
+
pass
|
|
7505
|
+
|
|
7506
|
+
# Sampling interval
|
|
7507
|
+
if hasattr(self, 'sample_interval') and idx < len(self.sample_interval):
|
|
7508
|
+
params['sample_interval'] = self.sample_interval[idx]
|
|
7509
|
+
else:
|
|
7510
|
+
params['sample_interval'] = None
|
|
7511
|
+
|
|
7512
|
+
# Recording time
|
|
7513
|
+
if hasattr(self, 'record_length') and idx < len(self.record_length):
|
|
7514
|
+
params['record_length'] = self.record_length[idx]
|
|
7515
|
+
else:
|
|
7516
|
+
params['record_length'] = None
|
|
7517
|
+
|
|
7518
|
+
# Delay time
|
|
7519
|
+
if hasattr(self, 'delay') and idx < len(self.delay):
|
|
7520
|
+
params['delay'] = self.delay[idx]
|
|
7521
|
+
else:
|
|
7522
|
+
params['delay'] = None
|
|
7523
|
+
|
|
7524
|
+
except Exception as e:
|
|
7525
|
+
params['error'] = f"Error calculating parameters: {str(e)}"
|
|
7526
|
+
|
|
7527
|
+
return params
|
|
7528
|
+
|
|
7058
7529
|
def clearMemory(self):
|
|
7059
7530
|
# Clear memory (reset the application)
|
|
7060
7531
|
|
|
@@ -10746,187 +11217,191 @@ class MainWindow(QMainWindow):
|
|
|
10746
11217
|
# Set the file update flag to True
|
|
10747
11218
|
self.update_file_flag = True
|
|
10748
11219
|
|
|
10749
|
-
|
|
10750
|
-
|
|
11220
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
11221
|
+
try:
|
|
11222
|
+
# Get the current row index directly (this handles duplicates properly)
|
|
11223
|
+
selected_row = self.fileListWidget.currentRow()
|
|
10751
11224
|
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
# Clear dispersion picking state for this new shot (without calling loadStream)
|
|
10758
|
-
# This ensures old picks don't show when switching shots
|
|
10759
|
-
for view in ['top', 'bottom']:
|
|
10760
|
-
state = self.dispersion_picking_state[view]
|
|
10761
|
-
plot_widget = self.plotWidget if view == 'top' else self.bottomPlotWidget
|
|
11225
|
+
# If a valid row is selected
|
|
11226
|
+
if selected_row >= 0 and selected_row < len(self.fileNames):
|
|
11227
|
+
self.currentFileName = self.fileNames[selected_row] # Set the current file name
|
|
11228
|
+
self.currentIndex = selected_row # Set the current index
|
|
10762
11229
|
|
|
10763
|
-
#
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
11230
|
+
# Clear dispersion picking state for this new shot (without calling loadStream)
|
|
11231
|
+
# This ensures old picks don't show when switching shots
|
|
11232
|
+
for view in ['top', 'bottom']:
|
|
11233
|
+
state = self.dispersion_picking_state[view]
|
|
11234
|
+
plot_widget = self.plotWidget if view == 'top' else self.bottomPlotWidget
|
|
11235
|
+
|
|
11236
|
+
# Remove all mode items from the plot
|
|
11237
|
+
for mode_num, mode_data in state['modes'].items():
|
|
11238
|
+
# Remove picked point items
|
|
11239
|
+
for item in mode_data['picked_point_items']:
|
|
11240
|
+
if item is not None:
|
|
11241
|
+
try:
|
|
11242
|
+
plot_widget.removeItem(item)
|
|
11243
|
+
except:
|
|
11244
|
+
pass
|
|
11245
|
+
|
|
11246
|
+
# Remove curve line
|
|
11247
|
+
if mode_data['curve_line'] is not None:
|
|
10768
11248
|
try:
|
|
10769
|
-
plot_widget.removeItem(
|
|
11249
|
+
plot_widget.removeItem(mode_data['curve_line'])
|
|
10770
11250
|
except:
|
|
10771
11251
|
pass
|
|
10772
11252
|
|
|
10773
|
-
#
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
# Reset modes but keep current mode from spinbox
|
|
10781
|
-
current_mode = self.dispersionModeSpinBox.value()
|
|
10782
|
-
state['modes'] = {}
|
|
10783
|
-
self._ensure_mode_exists(view, current_mode)
|
|
10784
|
-
state['current_mode'] = current_mode
|
|
10785
|
-
state['current_dispersion_data'] = None
|
|
10786
|
-
|
|
10787
|
-
# Now restore picks from storage if this shot has them
|
|
10788
|
-
if self.currentIndex < len(self.dispersion_curves) and self.dispersion_curves[self.currentIndex] is not None:
|
|
10789
|
-
curve_data = self.dispersion_curves[self.currentIndex]
|
|
10790
|
-
|
|
10791
|
-
# Get current mode from spinbox
|
|
10792
|
-
current_mode = self.dispersionModeSpinBox.value()
|
|
11253
|
+
# Reset modes but keep current mode from spinbox
|
|
11254
|
+
current_mode = self.dispersionModeSpinBox.value()
|
|
11255
|
+
state['modes'] = {}
|
|
11256
|
+
self._ensure_mode_exists(view, current_mode)
|
|
11257
|
+
state['current_mode'] = current_mode
|
|
11258
|
+
state['current_dispersion_data'] = None
|
|
10793
11259
|
|
|
10794
|
-
#
|
|
10795
|
-
if
|
|
10796
|
-
|
|
10797
|
-
for view in ['top', 'bottom']:
|
|
10798
|
-
state = self.dispersion_picking_state[view]
|
|
10799
|
-
state['modes'] = {}
|
|
10800
|
-
for mode_num, mode_curve_data in curve_data['modes'].items():
|
|
10801
|
-
# Avoid auto-creating interpolated lines for pvc-file imports
|
|
10802
|
-
curve_data_to_store = None
|
|
10803
|
-
if mode_curve_data.get('interpolated', False):
|
|
10804
|
-
curve_data_to_store = mode_curve_data.copy()
|
|
10805
|
-
elif mode_curve_data.get('source') not in ('pvc_file',):
|
|
10806
|
-
curve_data_to_store = mode_curve_data.copy()
|
|
10807
|
-
|
|
10808
|
-
state['modes'][mode_num] = {
|
|
10809
|
-
'picked_points': mode_curve_data.get('picked_points', []).copy(),
|
|
10810
|
-
'picked_point_items': [None] * len(mode_curve_data.get('picked_points', [])),
|
|
10811
|
-
'error_bar_items': [],
|
|
10812
|
-
'curve_line': None,
|
|
10813
|
-
'interp_error_fill': None,
|
|
10814
|
-
'curve_data': curve_data_to_store
|
|
10815
|
-
}
|
|
10816
|
-
# Ensure current mode exists (from spinbox)
|
|
10817
|
-
if current_mode not in state['modes']:
|
|
10818
|
-
self._ensure_mode_exists(view, current_mode)
|
|
10819
|
-
state['current_mode'] = current_mode
|
|
10820
|
-
else:
|
|
10821
|
-
# Old single-mode format - convert to mode 0
|
|
10822
|
-
for view in ['top', 'bottom']:
|
|
10823
|
-
state = self.dispersion_picking_state[view]
|
|
10824
|
-
state['modes'] = {
|
|
10825
|
-
0: {
|
|
10826
|
-
'picked_points': curve_data.get('picked_points', []).copy(),
|
|
10827
|
-
'picked_point_items': [None] * len(curve_data.get('picked_points', [])),
|
|
10828
|
-
'error_bar_items': [],
|
|
10829
|
-
'curve_line': None,
|
|
10830
|
-
'interp_error_fill': None,
|
|
10831
|
-
'curve_data': curve_data.copy()
|
|
10832
|
-
}
|
|
10833
|
-
}
|
|
10834
|
-
# Ensure current mode exists
|
|
10835
|
-
if current_mode not in state['modes']:
|
|
10836
|
-
self._ensure_mode_exists(view, current_mode)
|
|
10837
|
-
state['current_mode'] = current_mode
|
|
10838
|
-
|
|
10839
|
-
# Clear current_dispersion_data to avoid stale references
|
|
10840
|
-
self.dispersion_picking_state['top']['current_dispersion_data'] = None
|
|
10841
|
-
self.dispersion_picking_state['bottom']['current_dispersion_data'] = None
|
|
10842
|
-
|
|
10843
|
-
# Plot the selected file
|
|
10844
|
-
# Ensure plotting parameters are initialized for this file
|
|
10845
|
-
if (self.currentIndex < len(self.shot_trace_number) and
|
|
10846
|
-
self.shot_trace_number[self.currentIndex] is None):
|
|
10847
|
-
# This file's parameters haven't been initialized yet
|
|
10848
|
-
# This can happen with ASCII imports or corrupted file loading
|
|
10849
|
-
try:
|
|
10850
|
-
self.getPlotParameters()
|
|
10851
|
-
self.updatePlotTypeDict()
|
|
10852
|
-
except:
|
|
10853
|
-
# If getPlotParameters fails (e.g., for ASCII data), set up basic defaults
|
|
10854
|
-
if self.currentIndex < len(self.streams) and self.streams[self.currentIndex]:
|
|
10855
|
-
num_traces = len(self.streams[self.currentIndex])
|
|
10856
|
-
self.shot_trace_number[self.currentIndex] = list(range(1, num_traces + 1))
|
|
10857
|
-
self.file_trace_number[self.currentIndex] = np.arange(1, num_traces + 1)
|
|
10858
|
-
self.trace_position[self.currentIndex] = list(range(num_traces))
|
|
10859
|
-
self.offset[self.currentIndex] = [i * 1.0 for i in range(num_traces)] # Default 1m spacing
|
|
10860
|
-
self.updatePlotTypeDict()
|
|
10861
|
-
|
|
10862
|
-
# Restore trace ranges from memory if available
|
|
10863
|
-
self._restoreTraceRangesFromMemory()
|
|
10864
|
-
|
|
10865
|
-
# Update title and plots directly without triggering full view change handlers
|
|
10866
|
-
# This prevents unnecessary control rebuilds when just changing shots
|
|
10867
|
-
self.updateTitle()
|
|
10868
|
-
|
|
10869
|
-
# Plot current shot in the existing views (no view type change)
|
|
10870
|
-
if self.streams and self.currentIndex is not None:
|
|
10871
|
-
# Update top view
|
|
10872
|
-
if hasattr(self, 'topPlotType'):
|
|
10873
|
-
self._plot_target_widget = self.plotWidget
|
|
10874
|
-
self._plot_target_viewbox = self.viewBox
|
|
11260
|
+
# Now restore picks from storage if this shot has them
|
|
11261
|
+
if self.currentIndex < len(self.dispersion_curves) and self.dispersion_curves[self.currentIndex] is not None:
|
|
11262
|
+
curve_data = self.dispersion_curves[self.currentIndex]
|
|
10875
11263
|
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
elif self.topPlotType == "layout":
|
|
10879
|
-
self.plotLayout()
|
|
10880
|
-
elif self.topPlotType == "traveltime":
|
|
10881
|
-
self.plotTravelTime()
|
|
10882
|
-
elif self.topPlotType == "topo":
|
|
10883
|
-
self.plotTopo()
|
|
10884
|
-
elif self.topPlotType == "spectrogram":
|
|
10885
|
-
self.plotSpectrogram()
|
|
10886
|
-
elif self.topPlotType == "dispersion":
|
|
10887
|
-
self.plotDispersion()
|
|
10888
|
-
elif self.topPlotType == "pseudosection":
|
|
10889
|
-
self.plotPseudoSection()
|
|
10890
|
-
|
|
10891
|
-
# Reset target widget after plotting
|
|
10892
|
-
self._plot_target_widget = None
|
|
10893
|
-
self._plot_target_viewbox = None
|
|
10894
|
-
|
|
10895
|
-
# Update bottom view
|
|
10896
|
-
if hasattr(self, 'bottomPlotType'):
|
|
10897
|
-
self._plot_target_widget = self.bottomPlotWidget
|
|
10898
|
-
self._plot_target_viewbox = self.bottomViewBox
|
|
11264
|
+
# Get current mode from spinbox
|
|
11265
|
+
current_mode = self.dispersionModeSpinBox.value()
|
|
10899
11266
|
|
|
10900
|
-
if
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
11267
|
+
# Check if data is in new multi-mode format or old single-mode format
|
|
11268
|
+
if 'modes' in curve_data:
|
|
11269
|
+
# New multi-mode format
|
|
11270
|
+
for view in ['top', 'bottom']:
|
|
11271
|
+
state = self.dispersion_picking_state[view]
|
|
11272
|
+
state['modes'] = {}
|
|
11273
|
+
for mode_num, mode_curve_data in curve_data['modes'].items():
|
|
11274
|
+
# Avoid auto-creating interpolated lines for pvc-file imports
|
|
11275
|
+
curve_data_to_store = None
|
|
11276
|
+
if mode_curve_data.get('interpolated', False):
|
|
11277
|
+
curve_data_to_store = mode_curve_data.copy()
|
|
11278
|
+
elif mode_curve_data.get('source') not in ('pvc_file',):
|
|
11279
|
+
curve_data_to_store = mode_curve_data.copy()
|
|
11280
|
+
|
|
11281
|
+
state['modes'][mode_num] = {
|
|
11282
|
+
'picked_points': mode_curve_data.get('picked_points', []).copy(),
|
|
11283
|
+
'picked_point_items': [None] * len(mode_curve_data.get('picked_points', [])),
|
|
11284
|
+
'error_bar_items': [],
|
|
11285
|
+
'curve_line': None,
|
|
11286
|
+
'interp_error_fill': None,
|
|
11287
|
+
'curve_data': curve_data_to_store
|
|
11288
|
+
}
|
|
11289
|
+
# Ensure current mode exists (from spinbox)
|
|
11290
|
+
if current_mode not in state['modes']:
|
|
11291
|
+
self._ensure_mode_exists(view, current_mode)
|
|
11292
|
+
state['current_mode'] = current_mode
|
|
11293
|
+
else:
|
|
11294
|
+
# Old single-mode format - convert to mode 0
|
|
11295
|
+
for view in ['top', 'bottom']:
|
|
11296
|
+
state = self.dispersion_picking_state[view]
|
|
11297
|
+
state['modes'] = {
|
|
11298
|
+
0: {
|
|
11299
|
+
'picked_points': curve_data.get('picked_points', []).copy(),
|
|
11300
|
+
'picked_point_items': [None] * len(curve_data.get('picked_points', [])),
|
|
11301
|
+
'error_bar_items': [],
|
|
11302
|
+
'curve_line': None,
|
|
11303
|
+
'interp_error_fill': None,
|
|
11304
|
+
'curve_data': curve_data.copy()
|
|
11305
|
+
}
|
|
11306
|
+
}
|
|
11307
|
+
# Ensure current mode exists
|
|
11308
|
+
if current_mode not in state['modes']:
|
|
11309
|
+
self._ensure_mode_exists(view, current_mode)
|
|
11310
|
+
state['current_mode'] = current_mode
|
|
10914
11311
|
|
|
10915
|
-
#
|
|
10916
|
-
self.
|
|
10917
|
-
self.
|
|
10918
|
-
|
|
10919
|
-
#
|
|
10920
|
-
|
|
11312
|
+
# Clear current_dispersion_data to avoid stale references
|
|
11313
|
+
self.dispersion_picking_state['top']['current_dispersion_data'] = None
|
|
11314
|
+
self.dispersion_picking_state['bottom']['current_dispersion_data'] = None
|
|
11315
|
+
|
|
11316
|
+
# Plot the selected file
|
|
11317
|
+
# Ensure plotting parameters are initialized for this file
|
|
11318
|
+
if (self.currentIndex < len(self.shot_trace_number) and
|
|
11319
|
+
self.shot_trace_number[self.currentIndex] is None):
|
|
11320
|
+
# This file's parameters haven't been initialized yet
|
|
11321
|
+
# This can happen with ASCII imports or corrupted file loading
|
|
10921
11322
|
try:
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
11323
|
+
self.getPlotParameters()
|
|
11324
|
+
self.updatePlotTypeDict()
|
|
11325
|
+
except:
|
|
11326
|
+
# If getPlotParameters fails (e.g., for ASCII data), set up basic defaults
|
|
11327
|
+
if self.currentIndex < len(self.streams) and self.streams[self.currentIndex]:
|
|
11328
|
+
num_traces = len(self.streams[self.currentIndex])
|
|
11329
|
+
self.shot_trace_number[self.currentIndex] = list(range(1, num_traces + 1))
|
|
11330
|
+
self.file_trace_number[self.currentIndex] = np.arange(1, num_traces + 1)
|
|
11331
|
+
self.trace_position[self.currentIndex] = list(range(num_traces))
|
|
11332
|
+
self.offset[self.currentIndex] = [i * 1.0 for i in range(num_traces)] # Default 1m spacing
|
|
11333
|
+
self.updatePlotTypeDict()
|
|
11334
|
+
|
|
11335
|
+
# Restore trace ranges from memory if available
|
|
11336
|
+
self._restoreTraceRangesFromMemory()
|
|
11337
|
+
|
|
11338
|
+
# Update title and plots directly without triggering full view change handlers
|
|
11339
|
+
# This prevents unnecessary control rebuilds when just changing shots
|
|
11340
|
+
self.updateTitle()
|
|
10927
11341
|
|
|
10928
|
-
#
|
|
10929
|
-
self.
|
|
11342
|
+
# Plot current shot in the existing views (no view type change)
|
|
11343
|
+
if self.streams and self.currentIndex is not None:
|
|
11344
|
+
# Update top view
|
|
11345
|
+
if hasattr(self, 'topPlotType'):
|
|
11346
|
+
self._plot_target_widget = self.plotWidget
|
|
11347
|
+
self._plot_target_viewbox = self.viewBox
|
|
11348
|
+
|
|
11349
|
+
if self.topPlotType == "seismogram":
|
|
11350
|
+
self.updatePlots()
|
|
11351
|
+
elif self.topPlotType == "layout":
|
|
11352
|
+
self.plotLayout()
|
|
11353
|
+
elif self.topPlotType == "traveltime":
|
|
11354
|
+
self.plotTravelTime()
|
|
11355
|
+
elif self.topPlotType == "topo":
|
|
11356
|
+
self.plotTopo()
|
|
11357
|
+
elif self.topPlotType == "spectrogram":
|
|
11358
|
+
self.plotSpectrogram()
|
|
11359
|
+
elif self.topPlotType == "dispersion":
|
|
11360
|
+
self.plotDispersion()
|
|
11361
|
+
elif self.topPlotType == "pseudosection":
|
|
11362
|
+
self.plotPseudoSection()
|
|
11363
|
+
|
|
11364
|
+
# Reset target widget after plotting
|
|
11365
|
+
self._plot_target_widget = None
|
|
11366
|
+
self._plot_target_viewbox = None
|
|
11367
|
+
|
|
11368
|
+
# Update bottom view
|
|
11369
|
+
if hasattr(self, 'bottomPlotType'):
|
|
11370
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
11371
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
11372
|
+
|
|
11373
|
+
if self.bottomPlotType == "seismogram":
|
|
11374
|
+
self.updatePlots()
|
|
11375
|
+
elif self.bottomPlotType == "layout":
|
|
11376
|
+
self.plotLayout()
|
|
11377
|
+
elif self.bottomPlotType == "traveltime":
|
|
11378
|
+
self.plotTravelTime()
|
|
11379
|
+
elif self.bottomPlotType == "topo":
|
|
11380
|
+
self.plotTopo()
|
|
11381
|
+
elif self.bottomPlotType == "spectrogram":
|
|
11382
|
+
self.plotSpectrogram()
|
|
11383
|
+
elif self.bottomPlotType == "dispersion":
|
|
11384
|
+
self.plotDispersion()
|
|
11385
|
+
elif self.bottomPlotType == "pseudosection":
|
|
11386
|
+
self.plotPseudoSection()
|
|
11387
|
+
|
|
11388
|
+
# Reset target widget after plotting
|
|
11389
|
+
self._plot_target_widget = None
|
|
11390
|
+
self._plot_target_viewbox = None
|
|
11391
|
+
|
|
11392
|
+
# Update FFID/source in status bar for all views
|
|
11393
|
+
if hasattr(self, 'ffid') and self.ffid and self.currentIndex < len(self.ffid):
|
|
11394
|
+
try:
|
|
11395
|
+
ffid_val = self.ffid[self.currentIndex]
|
|
11396
|
+
src_val = self.source_position[self.currentIndex] if self.source_position else 'N/A'
|
|
11397
|
+
self.ffidLabel.setText(f'FFID: {ffid_val} | Source at {src_val} m')
|
|
11398
|
+
except Exception:
|
|
11399
|
+
pass
|
|
11400
|
+
|
|
11401
|
+
# Update dispersion window info in status bar
|
|
11402
|
+
self.updateDispersionWindowInfo()
|
|
11403
|
+
finally:
|
|
11404
|
+
QApplication.restoreOverrideCursor()
|
|
10930
11405
|
|
|
10931
11406
|
def navigateToPreviousFile(self):
|
|
10932
11407
|
"""Navigate to the previous file in the list"""
|
|
@@ -11608,21 +12083,26 @@ class MainWindow(QMainWindow):
|
|
|
11608
12083
|
# Clear batch loading flag and trigger initial plot if this was a batch load
|
|
11609
12084
|
if hasattr(self, '_batch_loading') and self._batch_loading:
|
|
11610
12085
|
self._batch_loading = False
|
|
11611
|
-
#
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
self.
|
|
11624
|
-
|
|
11625
|
-
|
|
12086
|
+
# Show wait cursor while rendering the initial wiggle/image display
|
|
12087
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
12088
|
+
try:
|
|
12089
|
+
# Now trigger the plot for the first loaded file, respecting current view selections
|
|
12090
|
+
if counter_stream > 0 and firstNewFile:
|
|
12091
|
+
self.currentFileName = firstNewFile
|
|
12092
|
+
self.currentIndex = self.fileNames.index(firstNewFile)
|
|
12093
|
+
# Ensure plot parameters are initialized for this file
|
|
12094
|
+
self.getPlotParameters()
|
|
12095
|
+
# Make sure plotTypeX is correctly set for the first file
|
|
12096
|
+
# Call autoSelectPlotTypes explicitly to ensure it's based on current file's data
|
|
12097
|
+
self.autoSelectPlotTypes()
|
|
12098
|
+
self.fileListWidget.setCurrentRow(self.currentIndex)
|
|
12099
|
+
# Trigger plots based on currently selected views (not hardcoded seismogram)
|
|
12100
|
+
if hasattr(self, 'topViewComboBox'):
|
|
12101
|
+
self.onTopViewChanged(self.topViewComboBox.currentText())
|
|
12102
|
+
if hasattr(self, 'bottomViewComboBox'):
|
|
12103
|
+
self.onBottomViewChanged(self.bottomViewComboBox.currentText())
|
|
12104
|
+
finally:
|
|
12105
|
+
QApplication.restoreOverrideCursor()
|
|
11626
12106
|
|
|
11627
12107
|
# Clean up potential batch loading flag
|
|
11628
12108
|
if hasattr(self, '_potential_batch_loading'):
|
|
@@ -11717,97 +12197,105 @@ class MainWindow(QMainWindow):
|
|
|
11717
12197
|
self.stream = read_seismic_file(self.currentFileName, separate_sources=True)
|
|
11718
12198
|
|
|
11719
12199
|
def loadStream(self):
|
|
11720
|
-
#
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
12200
|
+
# Skip wait cursor if batch loading is active (progress dialog already shown)
|
|
12201
|
+
show_cursor = not getattr(self, '_batch_loading', False)
|
|
12202
|
+
if show_cursor:
|
|
12203
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
12204
|
+
try:
|
|
12205
|
+
# Ensure all lists are properly sized for the current index
|
|
12206
|
+
for attr in self.attributes_to_initialize:
|
|
12207
|
+
attr_list = getattr(self, attr)
|
|
12208
|
+
while len(attr_list) <= self.currentIndex:
|
|
12209
|
+
if attr == 'airWaveItems':
|
|
12210
|
+
attr_list.append([None, None, None])
|
|
12211
|
+
else:
|
|
12212
|
+
attr_list.append(None)
|
|
11728
12213
|
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
12214
|
+
self.streams[self.currentIndex] = self.stream[self.streamIndex]
|
|
12215
|
+
self.input_format[self.currentIndex] = check_format(self.streams[self.currentIndex])
|
|
12216
|
+
self.getPlotParameters()
|
|
11732
12217
|
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
# Remove all mode items from the plot
|
|
11740
|
-
for mode_num, mode_data in state['modes'].items():
|
|
11741
|
-
# Remove picked point items
|
|
11742
|
-
for item in mode_data['picked_point_items']:
|
|
11743
|
-
if item is not None:
|
|
11744
|
-
try:
|
|
11745
|
-
plot_widget.removeItem(item)
|
|
11746
|
-
except:
|
|
11747
|
-
pass
|
|
11748
|
-
|
|
11749
|
-
# Remove curve line
|
|
11750
|
-
if mode_data['curve_line'] is not None:
|
|
11751
|
-
try:
|
|
11752
|
-
plot_widget.removeItem(mode_data['curve_line'])
|
|
11753
|
-
except:
|
|
11754
|
-
pass
|
|
12218
|
+
# Always update dispersion picking states when loading a shot
|
|
12219
|
+
# First, clear any old items from both plot widgets
|
|
12220
|
+
for view in ['top', 'bottom']:
|
|
12221
|
+
state = self.dispersion_picking_state[view]
|
|
12222
|
+
plot_widget = self.plotWidget if view == 'top' else self.bottomPlotWidget
|
|
11755
12223
|
|
|
11756
|
-
# Remove
|
|
11757
|
-
for
|
|
11758
|
-
|
|
12224
|
+
# Remove all mode items from the plot
|
|
12225
|
+
for mode_num, mode_data in state['modes'].items():
|
|
12226
|
+
# Remove picked point items
|
|
12227
|
+
for item in mode_data['picked_point_items']:
|
|
12228
|
+
if item is not None:
|
|
12229
|
+
try:
|
|
12230
|
+
plot_widget.removeItem(item)
|
|
12231
|
+
except:
|
|
12232
|
+
pass
|
|
12233
|
+
|
|
12234
|
+
# Remove curve line
|
|
12235
|
+
if mode_data['curve_line'] is not None:
|
|
11759
12236
|
try:
|
|
11760
|
-
plot_widget.removeItem(
|
|
12237
|
+
plot_widget.removeItem(mode_data['curve_line'])
|
|
11761
12238
|
except:
|
|
11762
12239
|
pass
|
|
12240
|
+
|
|
12241
|
+
# Remove error bars
|
|
12242
|
+
for item in mode_data.get('error_bar_items', []):
|
|
12243
|
+
if item is not None:
|
|
12244
|
+
try:
|
|
12245
|
+
plot_widget.removeItem(item)
|
|
12246
|
+
except:
|
|
12247
|
+
pass
|
|
12248
|
+
|
|
12249
|
+
# Remove interpolated error fill
|
|
12250
|
+
if mode_data.get('interp_error_fill') is not None:
|
|
12251
|
+
interp_items = mode_data['interp_error_fill']
|
|
12252
|
+
if isinstance(interp_items, list):
|
|
12253
|
+
for item in interp_items:
|
|
12254
|
+
if item is not None:
|
|
12255
|
+
try:
|
|
12256
|
+
plot_widget.removeItem(item)
|
|
12257
|
+
except:
|
|
12258
|
+
pass
|
|
11763
12259
|
|
|
11764
|
-
#
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
# CLEAR THE STATE DATA (reset to fundamental mode only)
|
|
11776
|
-
state['modes'] = {
|
|
11777
|
-
0: {
|
|
11778
|
-
'picked_points': [],
|
|
11779
|
-
'picked_point_items': [],
|
|
11780
|
-
'error_bar_items': [],
|
|
11781
|
-
'curve_line': None,
|
|
11782
|
-
'interp_error_fill': None,
|
|
11783
|
-
'curve_data': None
|
|
12260
|
+
# CLEAR THE STATE DATA (reset to fundamental mode only)
|
|
12261
|
+
state['modes'] = {
|
|
12262
|
+
0: {
|
|
12263
|
+
'picked_points': [],
|
|
12264
|
+
'picked_point_items': [],
|
|
12265
|
+
'error_bar_items': [],
|
|
12266
|
+
'curve_line': None,
|
|
12267
|
+
'interp_error_fill': None,
|
|
12268
|
+
'curve_data': None
|
|
12269
|
+
}
|
|
11784
12270
|
}
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11803
|
-
|
|
11804
|
-
self.
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
12271
|
+
state['current_mode'] = 0
|
|
12272
|
+
state['current_dispersion_data'] = None
|
|
12273
|
+
|
|
12274
|
+
# Now check if this shot has stored dispersion data and restore it
|
|
12275
|
+
self._restoreDispersionPicksFromStorage()
|
|
12276
|
+
|
|
12277
|
+
# If it is the first time the file is loaded, update the sources and traces lists
|
|
12278
|
+
if self.picks[self.currentIndex] is None:
|
|
12279
|
+
# Initialize picks for the current file with a list of nans of the same length as the traces
|
|
12280
|
+
self.picks[self.currentIndex] = [np.nan] * len(self.trace_position[self.currentIndex])
|
|
12281
|
+
# Intialize errors for the current file with a list of nans of the same length as the traces
|
|
12282
|
+
self.error[self.currentIndex] = [np.nan] * len(self.trace_position[self.currentIndex])
|
|
12283
|
+
# Initialize the scatter items for the current file with list of empty lists of the same length as the traces
|
|
12284
|
+
self.pickSeismoItems[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
|
|
12285
|
+
# Separate collections for top and bottom seismogram plots
|
|
12286
|
+
if self.currentIndex >= len(self.pickSeismoItems_top):
|
|
12287
|
+
self.pickSeismoItems_top.extend([None] * (self.currentIndex - len(self.pickSeismoItems_top) + 1))
|
|
12288
|
+
if self.currentIndex >= len(self.pickSeismoItems_bottom):
|
|
12289
|
+
self.pickSeismoItems_bottom.extend([None] * (self.currentIndex - len(self.pickSeismoItems_bottom) + 1))
|
|
12290
|
+
self.pickSeismoItems_top[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
|
|
12291
|
+
self.pickSeismoItems_bottom[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
|
|
12292
|
+
# Initialize the scatter items for the current file with list of empty lists of the same length as the traces
|
|
12293
|
+
self.pickLayoutItems[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
|
|
12294
|
+
# Initialize dispersion curves for the current file (one curve per shot)
|
|
12295
|
+
self.dispersion_curves[self.currentIndex] = None
|
|
12296
|
+
finally:
|
|
12297
|
+
if show_cursor:
|
|
12298
|
+
QApplication.restoreOverrideCursor()
|
|
11811
12299
|
|
|
11812
12300
|
def removeShot(self):
|
|
11813
12301
|
if self.currentIndex is not None:
|
|
@@ -11860,15 +12348,19 @@ class MainWindow(QMainWindow):
|
|
|
11860
12348
|
format.upper() + " files (*." + format.lower() + ")")
|
|
11861
12349
|
|
|
11862
12350
|
if savePath:
|
|
11863
|
-
|
|
12351
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
12352
|
+
try:
|
|
12353
|
+
stream = self.streams[self.currentIndex]
|
|
11864
12354
|
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
12355
|
+
stream = swap_header_format(stream, format)
|
|
12356
|
+
|
|
12357
|
+
# Save the stream as a SEGY or SU file
|
|
12358
|
+
stream.write(savePath, format=format,
|
|
12359
|
+
data_encoding=5, byteorder='>')
|
|
12360
|
+
print(f"Saved single shot ({format.upper()}): {savePath}")
|
|
12361
|
+
QMessageBox.information(self, "File Saved", f"File saved as: {savePath}")
|
|
12362
|
+
finally:
|
|
12363
|
+
QApplication.restoreOverrideCursor()
|
|
11872
12364
|
|
|
11873
12365
|
def saveAllFiles(self, single=False):
|
|
11874
12366
|
# Save all shots as SEGY or SU files
|
|
@@ -11883,48 +12375,51 @@ class MainWindow(QMainWindow):
|
|
|
11883
12375
|
saveDir = QFileDialog.getExistingDirectory(self, f"Select folder to save individual {self.output_format.upper()} files with modified headers")
|
|
11884
12376
|
|
|
11885
12377
|
if saveDir:
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
progress = QProgressDialog("Saving files...", "Cancel", 0, len(self.streams), self)
|
|
11890
|
-
progress.setWindowTitle(f"Saving Files") # Explicitly set the window title
|
|
11891
|
-
progress.setMinimumDuration(0) # Show immediately
|
|
11892
|
-
progress.setWindowModality(QtCore.Qt.WindowModal)
|
|
11893
|
-
progress.setValue(0)
|
|
11894
|
-
progress.show()
|
|
11895
|
-
QApplication.processEvents()
|
|
11896
|
-
|
|
11897
|
-
for i, stream in enumerate(self.streams):
|
|
12378
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
12379
|
+
try:
|
|
12380
|
+
# Start progress dialog if there are multiple files
|
|
11898
12381
|
if len(self.streams) > 1:
|
|
11899
|
-
|
|
11900
|
-
progress.
|
|
12382
|
+
progress = QProgressDialog("Saving files...", "Cancel", 0, len(self.streams), self)
|
|
12383
|
+
progress.setWindowTitle(f"Saving Files") # Explicitly set the window title
|
|
12384
|
+
progress.setMinimumDuration(0) # Show immediately
|
|
12385
|
+
progress.setWindowModality(QtCore.Qt.WindowModal)
|
|
12386
|
+
progress.setValue(0)
|
|
12387
|
+
progress.show()
|
|
11901
12388
|
QApplication.processEvents()
|
|
11902
12389
|
|
|
11903
|
-
stream
|
|
11904
|
-
|
|
12390
|
+
for i, stream in enumerate(self.streams):
|
|
12391
|
+
if len(self.streams) > 1:
|
|
12392
|
+
# Update the progress dialog
|
|
12393
|
+
progress.setValue(i)
|
|
12394
|
+
QApplication.processEvents()
|
|
11905
12395
|
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
if str(self.ffid[i]) in os.path.basename(self.fileNames[i]):
|
|
11909
|
-
defaultSavePath = os.path.splitext(self.fileNames[i])[0] + '.' + self.output_format.lower()
|
|
11910
|
-
else:
|
|
11911
|
-
defaultSavePath = os.path.splitext(self.fileNames[i])[0] + '_' + str(self.ffid[i]) + '.' + self.output_format.lower()
|
|
12396
|
+
stream = swap_header_format(stream, self.output_format)
|
|
12397
|
+
self.input_format[i] = check_format(stream)
|
|
11912
12398
|
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
print(f"Saved shot {i+1}/{len(self.streams)} ({self.output_format.upper()}): {savePath}")
|
|
12399
|
+
# Default file name is fileNames[i] without extension _ffid.format
|
|
12400
|
+
# check if ffid is in the original file name
|
|
12401
|
+
if str(self.ffid[i]) in os.path.basename(self.fileNames[i]):
|
|
12402
|
+
defaultSavePath = os.path.splitext(self.fileNames[i])[0] + '.' + self.output_format.lower()
|
|
12403
|
+
else:
|
|
12404
|
+
defaultSavePath = os.path.splitext(self.fileNames[i])[0] + '_' + str(self.ffid[i]) + '.' + self.output_format.lower()
|
|
11920
12405
|
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
12406
|
+
savePath = os.path.join(saveDir, os.path.basename(defaultSavePath))
|
|
12407
|
+
# Make sure we're not overwriting an existing file
|
|
12408
|
+
# if os.path.exists(savePath):
|
|
12409
|
+
savePath = savePath.replace('.' + self.output_format.lower(), '_updated.' + self.output_format.lower())
|
|
12410
|
+
stream.write(savePath, format=self.output_format,
|
|
12411
|
+
data_encoding=5, byteorder='>')
|
|
12412
|
+
print(f"Saved shot {i+1}/{len(self.streams)} ({self.output_format.upper()}): {savePath}")
|
|
12413
|
+
|
|
12414
|
+
if len(self.streams) > 1:
|
|
12415
|
+
# Ensure the progress dialog closes
|
|
12416
|
+
progress.setValue(len(self.streams))
|
|
12417
|
+
QMessageBox.information(self, "Files Saved", f"{len(self.streams)} files saved successfully in: {saveDir}")
|
|
12418
|
+
|
|
12419
|
+
# Reset headers modified flag after successful save
|
|
12420
|
+
self.headers_modified = False
|
|
12421
|
+
finally:
|
|
12422
|
+
QApplication.restoreOverrideCursor()
|
|
11928
12423
|
else:
|
|
11929
12424
|
# Ask for single filename
|
|
11930
12425
|
savePath, _ = QFileDialog.getSaveFileName(self, "Save as " + self.output_format.upper() + " file",
|
|
@@ -11932,21 +12427,25 @@ class MainWindow(QMainWindow):
|
|
|
11932
12427
|
self.output_format.upper() + " files (*." + self.output_format.lower() + ")")
|
|
11933
12428
|
|
|
11934
12429
|
if savePath:
|
|
11935
|
-
|
|
11936
|
-
|
|
12430
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
12431
|
+
try:
|
|
12432
|
+
# Merge all streams into a single stream
|
|
12433
|
+
merged_stream = merge_streams(self.streams)
|
|
11937
12434
|
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
12435
|
+
merged_stream = swap_header_format(merged_stream, self.output_format)
|
|
12436
|
+
for i, stream in enumerate(self.streams):
|
|
12437
|
+
self.input_format[i] = check_format(stream)
|
|
11941
12438
|
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
12439
|
+
# Save the merged stream as a SEGY or SU file
|
|
12440
|
+
merged_stream.write(savePath, format=self.output_format,
|
|
12441
|
+
data_encoding=5, byteorder='>')
|
|
12442
|
+
print(f"Saved all shots merged into single file ({self.output_format.upper()}): {savePath}")
|
|
12443
|
+
QMessageBox.information(self, "File Saved", f"File saved as: {savePath}")
|
|
12444
|
+
|
|
12445
|
+
# Reset headers modified flag after successful save
|
|
12446
|
+
self.headers_modified = False
|
|
12447
|
+
finally:
|
|
12448
|
+
QApplication.restoreOverrideCursor()
|
|
11950
12449
|
|
|
11951
12450
|
|
|
11952
12451
|
def saveSingleFileSEGY(self):
|
|
@@ -12203,12 +12702,12 @@ class MainWindow(QMainWindow):
|
|
|
12203
12702
|
self.sourcesCombo.setCurrentText("Position")
|
|
12204
12703
|
else:
|
|
12205
12704
|
# Use trace number and FFID plotting (default)
|
|
12206
|
-
self.plotTypeX = '
|
|
12705
|
+
self.plotTypeX = 'file_trace_number'
|
|
12207
12706
|
self.plotTypeY = 'ffid'
|
|
12208
12707
|
# Update menu actions to reflect the change
|
|
12209
|
-
if hasattr(self, '
|
|
12210
|
-
self.
|
|
12211
|
-
self.
|
|
12708
|
+
if hasattr(self, 'fileTraceNumberAction'):
|
|
12709
|
+
self.fileTraceNumberAction.setChecked(True)
|
|
12710
|
+
self.shotTraceNumberAction.setChecked(False)
|
|
12212
12711
|
self.uniqueTraceNumberAction.setChecked(False)
|
|
12213
12712
|
self.tracePositionAction.setChecked(False)
|
|
12214
12713
|
if hasattr(self, 'ffidAction'):
|
|
@@ -12217,7 +12716,7 @@ class MainWindow(QMainWindow):
|
|
|
12217
12716
|
self.offsetAction.setChecked(False)
|
|
12218
12717
|
# Update comboboxes to reflect the change
|
|
12219
12718
|
if hasattr(self, 'tracesCombo'):
|
|
12220
|
-
self.tracesCombo.setCurrentText("
|
|
12719
|
+
self.tracesCombo.setCurrentText("Trace in shot")
|
|
12221
12720
|
if hasattr(self, 'sourcesCombo'):
|
|
12222
12721
|
self.sourcesCombo.setCurrentText("FFID")
|
|
12223
12722
|
|
|
@@ -12444,18 +12943,22 @@ class MainWindow(QMainWindow):
|
|
|
12444
12943
|
)
|
|
12445
12944
|
|
|
12446
12945
|
if dialog.exec_():
|
|
12447
|
-
|
|
12946
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
12947
|
+
try:
|
|
12948
|
+
values = dialog.getValues()
|
|
12448
12949
|
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12950
|
+
ffid = values['FFID']
|
|
12951
|
+
self.ffid[self.currentIndex] = int(ffid)
|
|
12952
|
+
# Sync headers to obspy streams immediately
|
|
12953
|
+
self.syncHeadersToStreams(self.currentIndex)
|
|
12954
|
+
self.headers_modified = True # Mark headers as modified
|
|
12955
|
+
QMessageBox.information(self, "FFID Updated", f"FFID set to {ffid} for file {os.path.basename(self.currentFileName)}")
|
|
12455
12956
|
|
|
12456
|
-
|
|
12457
|
-
|
|
12458
|
-
|
|
12957
|
+
self.updateTitle()
|
|
12958
|
+
self.updateFileListDisplay()
|
|
12959
|
+
self.updatePlots()
|
|
12960
|
+
finally:
|
|
12961
|
+
QApplication.restoreOverrideCursor()
|
|
12459
12962
|
|
|
12460
12963
|
|
|
12461
12964
|
def editDelay(self):
|
|
@@ -12473,38 +12976,42 @@ class MainWindow(QMainWindow):
|
|
|
12473
12976
|
)
|
|
12474
12977
|
|
|
12475
12978
|
if dialog.exec_():
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12979
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
12980
|
+
try:
|
|
12981
|
+
values = dialog.getValues()
|
|
12982
|
+
apply_to_all = dialog.isChecked()
|
|
12983
|
+
delay = values['Delay (in s)']
|
|
12984
|
+
diff_delay = delay - self.delay[self.currentIndex]
|
|
12985
|
+
|
|
12986
|
+
if apply_to_all:
|
|
12987
|
+
self.delay = [delay] * len(self.delay) # Apply the delay to all files
|
|
12988
|
+
self.time = [np.arange(n_sample) * sample_interval + delay for n_sample, sample_interval in zip(self.n_sample, self.sample_interval)]
|
|
12989
|
+
# Update the picks with the new delay for files containing picks
|
|
12990
|
+
for i, picks in enumerate(self.picks):
|
|
12991
|
+
if picks is not None:
|
|
12992
|
+
self.picks[i] = [pick + diff_delay for pick in picks]
|
|
12993
|
+
# Sync headers to obspy streams immediately for all files
|
|
12994
|
+
for i in range(len(self.streams)):
|
|
12995
|
+
self.syncHeadersToStreams(i)
|
|
12996
|
+
self.headers_modified = True # Mark headers as modified
|
|
12997
|
+
QMessageBox.information(self, "Delay Updated", f"Delay set to {delay} s for all files")
|
|
12493
12998
|
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12999
|
+
else:
|
|
13000
|
+
self.delay[self.currentIndex] = delay
|
|
13001
|
+
self.time[self.currentIndex] = np.arange(self.n_sample[self.currentIndex]) * self.sample_interval[self.currentIndex] + self.delay[self.currentIndex]
|
|
13002
|
+
if self.picks is not None:
|
|
13003
|
+
self.picks[self.currentIndex] = [pick + diff_delay for pick in self.picks[self.currentIndex]]
|
|
13004
|
+
# Sync headers to obspy streams immediately
|
|
13005
|
+
self.syncHeadersToStreams(self.currentIndex)
|
|
13006
|
+
self.headers_modified = True # Mark headers as modified
|
|
13007
|
+
QMessageBox.information(self, "Delay Updated", f"Delay set to {delay} s for file {os.path.basename(self.currentFileName)}")
|
|
13008
|
+
# Mark headers/traces as modified
|
|
13009
|
+
self.headers_modified = True
|
|
13010
|
+
self.updatePlots()
|
|
13011
|
+
self.updateFileListDisplay()
|
|
13012
|
+
self.updatePlots()
|
|
13013
|
+
finally:
|
|
13014
|
+
QApplication.restoreOverrideCursor()
|
|
12508
13015
|
|
|
12509
13016
|
def editSampleInterval(self):
|
|
12510
13017
|
if self.streams:
|
|
@@ -12557,21 +13064,24 @@ class MainWindow(QMainWindow):
|
|
|
12557
13064
|
)
|
|
12558
13065
|
|
|
12559
13066
|
if dialog.exec_():
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
13067
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
13068
|
+
try:
|
|
13069
|
+
values = dialog.getValues()
|
|
13070
|
+
source_position = values['Source Position (in m)']
|
|
13071
|
+
self.source_position[self.currentIndex] = source_position
|
|
13072
|
+
self.offset[self.currentIndex] = self.trace_position[self.currentIndex] - source_position
|
|
13073
|
+
# Sync headers to obspy streams immediately
|
|
13074
|
+
self.syncHeadersToStreams(self.currentIndex)
|
|
13075
|
+
self.headers_modified = True # Mark headers as modified
|
|
13076
|
+
|
|
13077
|
+
# Mark headers/traces as modified
|
|
13078
|
+
self.headers_modified = True
|
|
13079
|
+
self.updateMeanSpacing()
|
|
13080
|
+
self.updatePlots()
|
|
13081
|
+
self.updateFileListDisplay()
|
|
13082
|
+
self.updatePlots()
|
|
13083
|
+
finally:
|
|
13084
|
+
QApplication.restoreOverrideCursor()
|
|
12575
13085
|
|
|
12576
13086
|
def editTracePosition(self):
|
|
12577
13087
|
if self.streams:
|
|
@@ -12580,28 +13090,44 @@ class MainWindow(QMainWindow):
|
|
|
12580
13090
|
|
|
12581
13091
|
dialog = TraceSelector(trace_numbers, trace_positions, parent=self,title="Edit Trace Position",show_position=True)
|
|
12582
13092
|
if dialog.exec_():
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
13093
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
13094
|
+
try:
|
|
13095
|
+
selected_index, new_position, apply_to_all = dialog.getValues()
|
|
13096
|
+
if apply_to_all:
|
|
13097
|
+
# Set trace position for all files
|
|
13098
|
+
for i in range(len(self.streams)):
|
|
13099
|
+
self.trace_position[i][selected_index] = new_position
|
|
13100
|
+
self.offset[i][selected_index] = new_position - self.source_position[i]
|
|
13101
|
+
|
|
13102
|
+
# Reorder traces by position to maintain spatial order
|
|
13103
|
+
self._reorderTracesByPosition(i)
|
|
13104
|
+
|
|
13105
|
+
# Sync headers to obspy streams immediately
|
|
13106
|
+
self.syncHeadersToStreams(i)
|
|
13107
|
+
self.headers_modified = True # Mark headers as modified
|
|
13108
|
+
|
|
13109
|
+
# Recalculate unique trace numbers for all files at once
|
|
13110
|
+
self._updateAllUniqueTraceNumbers()
|
|
13111
|
+
else:
|
|
13112
|
+
self.trace_position[self.currentIndex][selected_index] = new_position
|
|
13113
|
+
self.offset[self.currentIndex][selected_index] = new_position - self.source_position[self.currentIndex]
|
|
13114
|
+
|
|
13115
|
+
# Reorder traces by position to maintain spatial order
|
|
13116
|
+
self._reorderTracesByPosition(self.currentIndex)
|
|
13117
|
+
|
|
12589
13118
|
# Sync headers to obspy streams immediately
|
|
12590
|
-
self.syncHeadersToStreams(
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
self.offset[self.currentIndex][selected_index] = new_position - self.source_position[self.currentIndex]
|
|
12596
|
-
# Sync headers to obspy streams immediately
|
|
12597
|
-
self.syncHeadersToStreams(self.currentIndex)
|
|
12598
|
-
self.headers_modified = True # Mark headers as modified
|
|
12599
|
-
QMessageBox.information(self, "Trace Position Updated", f"Trace position for trace #{trace_numbers[selected_index]} set to {new_position} m for file {os.path.basename(self.currentFileName)}")
|
|
13119
|
+
self.syncHeadersToStreams(self.currentIndex)
|
|
13120
|
+
self.headers_modified = True # Mark headers as modified
|
|
13121
|
+
|
|
13122
|
+
# Recalculate unique trace numbers for all files at once
|
|
13123
|
+
self._updateAllUniqueTraceNumbers()
|
|
12600
13124
|
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
13125
|
+
self.updateMeanSpacing()
|
|
13126
|
+
self.updatePlots()
|
|
13127
|
+
self.updateFileListDisplay()
|
|
13128
|
+
self.updatePlots()
|
|
13129
|
+
finally:
|
|
13130
|
+
QApplication.restoreOverrideCursor()
|
|
12605
13131
|
|
|
12606
13132
|
def autoPickSTA_LTA(self):
|
|
12607
13133
|
"""Automatic STA/LTA picker. Shows a parameter dialog, then applies the detector to the current shot or all shots."""
|
|
@@ -14675,6 +15201,51 @@ class MainWindow(QMainWindow):
|
|
|
14675
15201
|
QMessageBox.warning(self, "Optimization Error", f"Optimization failed: {str(e)}")
|
|
14676
15202
|
return None
|
|
14677
15203
|
|
|
15204
|
+
def _reorderTracesByPosition(self, file_idx):
|
|
15205
|
+
"""Reorder traces in a file based on their spatial positions (ascending order).
|
|
15206
|
+
This maintains consistency between trace order and spatial layout.
|
|
15207
|
+
|
|
15208
|
+
Args:
|
|
15209
|
+
file_idx: Index of the file to reorder
|
|
15210
|
+
"""
|
|
15211
|
+
if file_idx >= len(self.trace_position):
|
|
15212
|
+
return
|
|
15213
|
+
|
|
15214
|
+
# Get the current positions and create a sorted index array
|
|
15215
|
+
positions = np.array(self.trace_position[file_idx])
|
|
15216
|
+
sorted_indices = np.argsort(positions)
|
|
15217
|
+
|
|
15218
|
+
# Check if already sorted (no reordering needed)
|
|
15219
|
+
if np.array_equal(sorted_indices, np.arange(len(sorted_indices))):
|
|
15220
|
+
return # Already in correct order
|
|
15221
|
+
|
|
15222
|
+
# Reorder the stream traces
|
|
15223
|
+
input_format = self.input_format[file_idx]
|
|
15224
|
+
original_traces = [self.streams[file_idx][i] for i in sorted_indices]
|
|
15225
|
+
self.streams[file_idx].traces = original_traces
|
|
15226
|
+
|
|
15227
|
+
# Reorder all associated arrays (ensure they're numpy arrays first)
|
|
15228
|
+
self.trace_position[file_idx] = positions[sorted_indices]
|
|
15229
|
+
self.trace_elevation[file_idx] = np.array(self.trace_elevation[file_idx])[sorted_indices]
|
|
15230
|
+
self.offset[file_idx] = np.array(self.offset[file_idx])[sorted_indices]
|
|
15231
|
+
self.shot_trace_number[file_idx] = np.array(self.shot_trace_number[file_idx])[sorted_indices]
|
|
15232
|
+
self.file_trace_number[file_idx] = np.array(self.file_trace_number[file_idx])[sorted_indices]
|
|
15233
|
+
|
|
15234
|
+
# Reorder picks and errors
|
|
15235
|
+
if self.picks[file_idx] is not None:
|
|
15236
|
+
self.picks[file_idx] = np.array(self.picks[file_idx])[sorted_indices]
|
|
15237
|
+
if self.error[file_idx] is not None:
|
|
15238
|
+
self.error[file_idx] = np.array(self.error[file_idx])[sorted_indices]
|
|
15239
|
+
|
|
15240
|
+
# Reorder pick items (handle as numpy array for consistent indexing)
|
|
15241
|
+
if self.pickSeismoItems[file_idx] is not None:
|
|
15242
|
+
self.pickSeismoItems[file_idx] = np.array(self.pickSeismoItems[file_idx], dtype=object)[sorted_indices]
|
|
15243
|
+
if self.pickLayoutItems[file_idx] is not None:
|
|
15244
|
+
self.pickLayoutItems[file_idx] = np.array(self.pickLayoutItems[file_idx], dtype=object)[sorted_indices]
|
|
15245
|
+
|
|
15246
|
+
# Update file_trace_number to be sequential (1, 2, 3...)
|
|
15247
|
+
self._updateSequentialTraceNumbers(file_idx)
|
|
15248
|
+
|
|
14678
15249
|
def _updateSequentialTraceNumbers(self, file_index):
|
|
14679
15250
|
"""Update file_trace_number to be sequential (1, 2, 3...) after any trace modification.
|
|
14680
15251
|
This keeps trace numbering relative regardless of which traces have been removed or reordered.
|
|
@@ -14750,17 +15321,53 @@ class MainWindow(QMainWindow):
|
|
|
14750
15321
|
)
|
|
14751
15322
|
|
|
14752
15323
|
if dialog.exec_():
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
15324
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
15325
|
+
try:
|
|
15326
|
+
values = dialog.getValues()
|
|
15327
|
+
first_trace = values['First Trace #'] # 1-based user input
|
|
15328
|
+
second_trace = values['Second Trace #'] # 1-based user input
|
|
15329
|
+
|
|
15330
|
+
# Convert to 0-based indices
|
|
15331
|
+
first_idx = first_trace - 1
|
|
15332
|
+
second_idx = second_trace - 1
|
|
15333
|
+
|
|
15334
|
+
# Swap the traces
|
|
15335
|
+
if dialog.isChecked():
|
|
15336
|
+
for i in range(len(self.streams)):
|
|
15337
|
+
n_traces = len(self.streams[i])
|
|
15338
|
+
# Validate indices
|
|
15339
|
+
if first_idx < n_traces and second_idx < n_traces and first_idx != second_idx:
|
|
15340
|
+
self.streams[i] = swap_traces(self.streams[i], first_trace, second_trace)
|
|
15341
|
+
|
|
15342
|
+
# Swap picks and errors (they follow their traces)
|
|
15343
|
+
if self.picks[i] is not None:
|
|
15344
|
+
self.picks[i][first_idx], self.picks[i][second_idx] = \
|
|
15345
|
+
self.picks[i][second_idx], self.picks[i][first_idx]
|
|
15346
|
+
if self.error[i] is not None:
|
|
15347
|
+
self.error[i][first_idx], self.error[i][second_idx] = \
|
|
15348
|
+
self.error[i][second_idx], self.error[i][first_idx]
|
|
15349
|
+
|
|
15350
|
+
# Swap pick items
|
|
15351
|
+
if self.pickSeismoItems[i] is not None:
|
|
15352
|
+
self.pickSeismoItems[i][first_idx], self.pickSeismoItems[i][second_idx] = \
|
|
15353
|
+
self.pickSeismoItems[i][second_idx], self.pickSeismoItems[i][first_idx]
|
|
15354
|
+
if self.pickLayoutItems[i] is not None:
|
|
15355
|
+
self.pickLayoutItems[i][first_idx], self.pickLayoutItems[i][second_idx] = \
|
|
15356
|
+
self.pickLayoutItems[i][second_idx], self.pickLayoutItems[i][first_idx]
|
|
15357
|
+
|
|
15358
|
+
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
15359
|
+
shot_trace_number = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
15360
|
+
for trace in self.streams[i]]
|
|
15361
|
+
self.shot_trace_number[i] = shot_trace_number
|
|
15362
|
+
|
|
15363
|
+
# Update file_trace_number to be sequential and recalculate unique_trace_number
|
|
15364
|
+
self._updateSequentialTraceNumbers(i)
|
|
15365
|
+
# Sync headers to obspy streams immediately
|
|
15366
|
+
self.syncHeadersToStreams(i)
|
|
15367
|
+
|
|
15368
|
+
self.headers_modified = True # Mark headers as modified
|
|
15369
|
+
else:
|
|
15370
|
+
i = self.currentIndex
|
|
14764
15371
|
n_traces = len(self.streams[i])
|
|
14765
15372
|
# Validate indices
|
|
14766
15373
|
if first_idx < n_traces and second_idx < n_traces and first_idx != second_idx:
|
|
@@ -14791,64 +15398,57 @@ class MainWindow(QMainWindow):
|
|
|
14791
15398
|
self._updateSequentialTraceNumbers(i)
|
|
14792
15399
|
# Sync headers to obspy streams immediately
|
|
14793
15400
|
self.syncHeadersToStreams(i)
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
i = self.currentIndex
|
|
14799
|
-
n_traces = len(self.streams[i])
|
|
14800
|
-
# Validate indices
|
|
14801
|
-
if first_idx < n_traces and second_idx < n_traces and first_idx != second_idx:
|
|
14802
|
-
self.streams[i] = swap_traces(self.streams[i], first_trace, second_trace)
|
|
14803
|
-
|
|
14804
|
-
# Swap picks and errors (they follow their traces)
|
|
14805
|
-
if self.picks[i] is not None:
|
|
14806
|
-
self.picks[i][first_idx], self.picks[i][second_idx] = \
|
|
14807
|
-
self.picks[i][second_idx], self.picks[i][first_idx]
|
|
14808
|
-
if self.error[i] is not None:
|
|
14809
|
-
self.error[i][first_idx], self.error[i][second_idx] = \
|
|
14810
|
-
self.error[i][second_idx], self.error[i][first_idx]
|
|
14811
|
-
|
|
14812
|
-
# Swap pick items
|
|
14813
|
-
if self.pickSeismoItems[i] is not None:
|
|
14814
|
-
self.pickSeismoItems[i][first_idx], self.pickSeismoItems[i][second_idx] = \
|
|
14815
|
-
self.pickSeismoItems[i][second_idx], self.pickSeismoItems[i][first_idx]
|
|
14816
|
-
if self.pickLayoutItems[i] is not None:
|
|
14817
|
-
self.pickLayoutItems[i][first_idx], self.pickLayoutItems[i][second_idx] = \
|
|
14818
|
-
self.pickLayoutItems[i][second_idx], self.pickLayoutItems[i][first_idx]
|
|
14819
|
-
|
|
14820
|
-
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
14821
|
-
shot_trace_number = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
14822
|
-
for trace in self.streams[i]]
|
|
14823
|
-
self.shot_trace_number[i] = shot_trace_number
|
|
14824
|
-
|
|
14825
|
-
# Update file_trace_number to be sequential and recalculate unique_trace_number
|
|
14826
|
-
self._updateSequentialTraceNumbers(i)
|
|
14827
|
-
# Sync headers to obspy streams immediately
|
|
14828
|
-
self.syncHeadersToStreams(i)
|
|
14829
|
-
|
|
14830
|
-
QMessageBox.information(self, "Traces Swapped", f"Traces {first_trace} and {second_trace} swapped for file {os.path.basename(self.currentFileName)}")
|
|
14831
|
-
self.headers_modified = True # Mark headers as modified
|
|
14832
|
-
else:
|
|
14833
|
-
QMessageBox.warning(self, "Invalid Trace Numbers", f"Invalid trace indices: {first_trace}, {second_trace}")
|
|
15401
|
+
|
|
15402
|
+
self.headers_modified = True # Mark headers as modified
|
|
15403
|
+
else:
|
|
15404
|
+
QMessageBox.warning(self, "Invalid Trace Numbers", f"Invalid trace indices: {first_trace}, {second_trace}")
|
|
14834
15405
|
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
15406
|
+
self.updatePlots()
|
|
15407
|
+
self.updateFileListDisplay()
|
|
15408
|
+
self.updatePlots()
|
|
15409
|
+
finally:
|
|
15410
|
+
QApplication.restoreOverrideCursor()
|
|
14838
15411
|
|
|
14839
15412
|
def removeTrace(self):
|
|
14840
15413
|
if self.streams:
|
|
14841
15414
|
trace_numbers = self.shot_trace_number[self.currentIndex]
|
|
14842
15415
|
dialog = TraceSelector(trace_numbers, trace_positions=None, parent=self, title="Remove Trace", show_position=False)
|
|
14843
15416
|
if dialog.exec_():
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
15417
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
15418
|
+
try:
|
|
15419
|
+
selected_index, apply_to_all = dialog.getValues()
|
|
15420
|
+
# selected_index is the array index (0-based) directly from TraceSelector
|
|
15421
|
+
trace_display_number = trace_numbers[selected_index]
|
|
15422
|
+
|
|
15423
|
+
if apply_to_all:
|
|
15424
|
+
# Remove the trace for all files
|
|
15425
|
+
for i in range(len(self.streams)):
|
|
15426
|
+
if selected_index < len(self.streams[i]):
|
|
15427
|
+
self.streams[i] = remove_trace(self.streams[i], trace_display_number)
|
|
15428
|
+
# Delete by index (directly remove from position)
|
|
15429
|
+
self.offset[i] = np.delete(self.offset[i], selected_index)
|
|
15430
|
+
self.trace_position[i] = np.delete(self.trace_position[i], selected_index)
|
|
15431
|
+
self.trace_elevation[i] = np.delete(self.trace_elevation[i], selected_index)
|
|
15432
|
+
self.shot_trace_number[i] = np.delete(self.shot_trace_number[i], selected_index)
|
|
15433
|
+
self.file_trace_number[i] = np.delete(self.file_trace_number[i], selected_index)
|
|
15434
|
+
self.picks[i] = np.delete(self.picks[i], selected_index)
|
|
15435
|
+
self.error[i] = np.delete(self.error[i], selected_index)
|
|
15436
|
+
self.pickSeismoItems[i] = np.delete(self.pickSeismoItems[i], selected_index)
|
|
15437
|
+
self.pickLayoutItems[i] = np.delete(self.pickLayoutItems[i], selected_index)
|
|
15438
|
+
|
|
15439
|
+
# Rebuild shot_trace_number from updated obspy headers
|
|
15440
|
+
self.shot_trace_number[i] = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
15441
|
+
for trace in self.streams[i]]
|
|
15442
|
+
# Update file_trace_number to be sequential (1, 2, 3...)
|
|
15443
|
+
self._updateSequentialTraceNumbers(i)
|
|
15444
|
+
# Sync headers to obspy streams immediately
|
|
15445
|
+
self.syncHeadersToStreams(i)
|
|
15446
|
+
|
|
15447
|
+
self.headers_modified = True # Mark headers as modified
|
|
15448
|
+
else:
|
|
15449
|
+
# Remove the trace for the current file only
|
|
15450
|
+
if selected_index < len(self.streams[self.currentIndex]):
|
|
15451
|
+
i = self.currentIndex
|
|
14852
15452
|
self.streams[i] = remove_trace(self.streams[i], trace_display_number)
|
|
14853
15453
|
# Delete by index (directly remove from position)
|
|
14854
15454
|
self.offset[i] = np.delete(self.offset[i], selected_index)
|
|
@@ -14868,39 +15468,169 @@ class MainWindow(QMainWindow):
|
|
|
14868
15468
|
self._updateSequentialTraceNumbers(i)
|
|
14869
15469
|
# Sync headers to obspy streams immediately
|
|
14870
15470
|
self.syncHeadersToStreams(i)
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
15471
|
+
|
|
15472
|
+
self.updateMeanSpacing()
|
|
15473
|
+
self.updatePlots()
|
|
15474
|
+
self.updateFileListDisplay()
|
|
15475
|
+
self.updatePlots()
|
|
15476
|
+
finally:
|
|
15477
|
+
QApplication.restoreOverrideCursor()
|
|
15478
|
+
|
|
15479
|
+
def insertMutedTraces(self):
|
|
15480
|
+
"""Insert one or more zero traces after a specified trace number."""
|
|
15481
|
+
if self.streams:
|
|
15482
|
+
n_traces = len(self.streams[self.currentIndex])
|
|
15483
|
+
|
|
15484
|
+
parameters = [
|
|
15485
|
+
{'label': 'Insert after Trace # (0 for beginning)', 'initial_value': n_traces, 'type': 'int'},
|
|
15486
|
+
{'label': 'Number of traces to insert', 'initial_value': 1, 'type': 'int'}
|
|
15487
|
+
]
|
|
15488
|
+
|
|
15489
|
+
dialog = GenericParameterDialog(
|
|
15490
|
+
title="Insert Zero Traces",
|
|
15491
|
+
parameters=parameters,
|
|
15492
|
+
add_checkbox=True,
|
|
15493
|
+
checkbox_text="Apply to all shots",
|
|
15494
|
+
parent=self
|
|
15495
|
+
)
|
|
15496
|
+
|
|
15497
|
+
if dialog.exec_():
|
|
15498
|
+
values = dialog.getValues()
|
|
15499
|
+
insert_after = values['Insert after Trace # (0 for beginning)']
|
|
15500
|
+
num_to_insert = values['Number of traces to insert']
|
|
15501
|
+
apply_to_all = dialog.isChecked()
|
|
15502
|
+
|
|
15503
|
+
# Validate inputs
|
|
15504
|
+
if num_to_insert < 1:
|
|
15505
|
+
QMessageBox.warning(self, "Invalid Input", "Number of traces must be at least 1")
|
|
15506
|
+
return
|
|
15507
|
+
|
|
15508
|
+
# Show wait cursor while processing
|
|
15509
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
15510
|
+
try:
|
|
15511
|
+
if apply_to_all:
|
|
15512
|
+
# Insert traces for all files
|
|
15513
|
+
for i in range(len(self.streams)):
|
|
15514
|
+
n_traces_i = len(self.streams[i])
|
|
15515
|
+
if insert_after > n_traces_i:
|
|
15516
|
+
continue # Skip if position is beyond trace count
|
|
15517
|
+
|
|
15518
|
+
self._insertMutedTracesInFile(i, insert_after, num_to_insert)
|
|
15519
|
+
# Sync headers to obspy streams immediately
|
|
15520
|
+
self.syncHeadersToStreams(i)
|
|
15521
|
+
|
|
15522
|
+
self.headers_modified = True
|
|
15523
|
+
else:
|
|
15524
|
+
# Insert traces for current file only
|
|
14877
15525
|
i = self.currentIndex
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
self.shot_trace_number[i] = np.delete(self.shot_trace_number[i], selected_index)
|
|
14884
|
-
self.file_trace_number[i] = np.delete(self.file_trace_number[i], selected_index)
|
|
14885
|
-
self.picks[i] = np.delete(self.picks[i], selected_index)
|
|
14886
|
-
self.error[i] = np.delete(self.error[i], selected_index)
|
|
14887
|
-
self.pickSeismoItems[i] = np.delete(self.pickSeismoItems[i], selected_index)
|
|
14888
|
-
self.pickLayoutItems[i] = np.delete(self.pickLayoutItems[i], selected_index)
|
|
15526
|
+
n_traces_i = len(self.streams[i])
|
|
15527
|
+
if insert_after > n_traces_i:
|
|
15528
|
+
QMessageBox.warning(self, "Invalid Position",
|
|
15529
|
+
f"Insert position ({insert_after}) exceeds trace count ({n_traces_i})")
|
|
15530
|
+
return
|
|
14889
15531
|
|
|
14890
|
-
|
|
14891
|
-
self.shot_trace_number[i] = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
14892
|
-
for trace in self.streams[i]]
|
|
14893
|
-
# Update file_trace_number to be sequential (1, 2, 3...)
|
|
14894
|
-
self._updateSequentialTraceNumbers(i)
|
|
15532
|
+
self._insertMutedTracesInFile(i, insert_after, num_to_insert)
|
|
14895
15533
|
# Sync headers to obspy streams immediately
|
|
14896
15534
|
self.syncHeadersToStreams(i)
|
|
14897
15535
|
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
15536
|
+
self.headers_modified = True
|
|
15537
|
+
|
|
15538
|
+
# Recalculate unique trace numbers for all files at once
|
|
15539
|
+
self._updateAllUniqueTraceNumbers()
|
|
15540
|
+
self.updateMeanSpacing()
|
|
15541
|
+
self.updatePlotTypeDict()
|
|
15542
|
+
self.updatePlots()
|
|
15543
|
+
self.updateFileListDisplay()
|
|
15544
|
+
self.updatePlots()
|
|
15545
|
+
finally:
|
|
15546
|
+
# Always restore cursor even if there's an error
|
|
15547
|
+
QApplication.restoreOverrideCursor()
|
|
15548
|
+
|
|
15549
|
+
def _insertMutedTracesInFile(self, file_idx, insert_after, num_to_insert):
|
|
15550
|
+
"""Helper function to insert zero traces in a specific file.
|
|
15551
|
+
|
|
15552
|
+
Args:
|
|
15553
|
+
file_idx: Index of the file
|
|
15554
|
+
insert_after: Insert after this trace number (0 = beginning, 1-based)
|
|
15555
|
+
num_to_insert: Number of zero traces to insert
|
|
15556
|
+
"""
|
|
15557
|
+
from obspy import Trace, Stream
|
|
15558
|
+
import copy
|
|
15559
|
+
|
|
15560
|
+
# Determine insert position (0-based index)
|
|
15561
|
+
insert_pos = insert_after # If insert_after=0, insert at position 0; if insert_after=1, insert at position 1
|
|
15562
|
+
|
|
15563
|
+
# Get a reference trace to copy metadata from (use first trace or the one before insert position)
|
|
15564
|
+
if len(self.streams[file_idx]) > 0:
|
|
15565
|
+
if insert_after > 0 and insert_after <= len(self.streams[file_idx]):
|
|
15566
|
+
ref_trace = self.streams[file_idx][insert_after - 1]
|
|
15567
|
+
else:
|
|
15568
|
+
ref_trace = self.streams[file_idx][0]
|
|
15569
|
+
else:
|
|
15570
|
+
QMessageBox.warning(self, "No Traces", "Cannot insert traces into empty stream")
|
|
15571
|
+
return
|
|
15572
|
+
|
|
15573
|
+
input_format = self.input_format[file_idx]
|
|
15574
|
+
|
|
15575
|
+
# Find the next available trace number
|
|
15576
|
+
existing_trace_numbers = [trace.stats[input_format].trace_header.trace_number_within_the_original_field_record
|
|
15577
|
+
for trace in self.streams[file_idx]]
|
|
15578
|
+
max_trace_number = max(existing_trace_numbers) if existing_trace_numbers else 0
|
|
15579
|
+
|
|
15580
|
+
# Create the zero traces
|
|
15581
|
+
new_traces = []
|
|
15582
|
+
for offset in range(num_to_insert):
|
|
15583
|
+
# Create a copy of the reference trace
|
|
15584
|
+
new_trace = ref_trace.copy()
|
|
15585
|
+
|
|
15586
|
+
# Zero out the data
|
|
15587
|
+
new_trace.data = np.zeros_like(new_trace.data)
|
|
15588
|
+
|
|
15589
|
+
# Assign a new trace number
|
|
15590
|
+
new_trace_number = max_trace_number + offset + 1
|
|
15591
|
+
new_trace.stats[input_format].trace_header.trace_number_within_the_original_field_record = new_trace_number
|
|
15592
|
+
|
|
15593
|
+
# Set position and elevation to 0
|
|
15594
|
+
if input_format == 'segy':
|
|
15595
|
+
new_trace.stats.segy.trace_header.group_coordinate_x = 0
|
|
15596
|
+
new_trace.stats.segy.trace_header.group_coordinate_y = 0
|
|
15597
|
+
elif input_format == 'su':
|
|
15598
|
+
new_trace.stats.su.trace_header.group_coordinate_x = 0
|
|
15599
|
+
new_trace.stats.su.trace_header.group_coordinate_y = 0
|
|
15600
|
+
|
|
15601
|
+
new_traces.append(new_trace)
|
|
15602
|
+
|
|
15603
|
+
# Rebuild the stream with traces inserted at the correct position
|
|
15604
|
+
new_stream = Stream()
|
|
15605
|
+
old_stream_list = list(self.streams[file_idx]) # Convert to list
|
|
15606
|
+
new_stream_list = old_stream_list[:insert_pos] + new_traces + old_stream_list[insert_pos:]
|
|
15607
|
+
for trace in new_stream_list:
|
|
15608
|
+
new_stream.append(trace)
|
|
15609
|
+
self.streams[file_idx] = new_stream
|
|
15610
|
+
|
|
15611
|
+
# Insert into associated arrays - insert all at the same position repeatedly
|
|
15612
|
+
# This works because each insert shifts subsequent elements right
|
|
15613
|
+
for offset in range(num_to_insert):
|
|
15614
|
+
new_trace_number = max_trace_number + offset + 1
|
|
15615
|
+
|
|
15616
|
+
# Always insert at insert_pos - numpy.insert shifts elements right automatically
|
|
15617
|
+
self.trace_position[file_idx] = np.insert(self.trace_position[file_idx], insert_pos, 0.0)
|
|
15618
|
+
self.trace_elevation[file_idx] = np.insert(self.trace_elevation[file_idx], insert_pos, 0.0)
|
|
15619
|
+
self.offset[file_idx] = np.insert(self.offset[file_idx], insert_pos,
|
|
15620
|
+
0.0 - self.source_position[file_idx])
|
|
15621
|
+
self.shot_trace_number[file_idx] = np.insert(self.shot_trace_number[file_idx], insert_pos, new_trace_number)
|
|
15622
|
+
self.file_trace_number[file_idx] = np.insert(self.file_trace_number[file_idx], insert_pos, insert_pos + 1)
|
|
15623
|
+
|
|
15624
|
+
# Insert NaN for picks and errors
|
|
15625
|
+
self.picks[file_idx] = np.insert(self.picks[file_idx], insert_pos, np.nan)
|
|
15626
|
+
self.error[file_idx] = np.insert(self.error[file_idx], insert_pos, np.nan)
|
|
15627
|
+
|
|
15628
|
+
# Insert None for pick items
|
|
15629
|
+
self.pickSeismoItems[file_idx] = np.insert(self.pickSeismoItems[file_idx], insert_pos, None)
|
|
15630
|
+
self.pickLayoutItems[file_idx] = np.insert(self.pickLayoutItems[file_idx], insert_pos, None)
|
|
15631
|
+
|
|
15632
|
+
# Update file_trace_number to be sequential after all insertions
|
|
15633
|
+
self._updateSequentialTraceNumbers(file_idx)
|
|
14904
15634
|
|
|
14905
15635
|
def moveTrace(self):
|
|
14906
15636
|
if self.streams:
|
|
@@ -14908,12 +15638,13 @@ class MainWindow(QMainWindow):
|
|
|
14908
15638
|
max_position = n_traces
|
|
14909
15639
|
|
|
14910
15640
|
parameters = [
|
|
14911
|
-
{'label': 'Trace #
|
|
15641
|
+
{'label': 'First Trace # to move', 'initial_value': 1, 'type': 'int'},
|
|
15642
|
+
{'label': 'Last Trace # to move', 'initial_value': 1, 'type': 'int'},
|
|
14912
15643
|
{'label': 'New Position (1 to ' + str(max_position) + ')', 'initial_value': 1, 'type': 'int'}
|
|
14913
15644
|
]
|
|
14914
15645
|
|
|
14915
15646
|
dialog = GenericParameterDialog(
|
|
14916
|
-
title="Move Trace",
|
|
15647
|
+
title="Move Trace(s)",
|
|
14917
15648
|
parameters=parameters,
|
|
14918
15649
|
add_checkbox=True,
|
|
14919
15650
|
checkbox_text="Apply to all shots",
|
|
@@ -14922,100 +15653,72 @@ class MainWindow(QMainWindow):
|
|
|
14922
15653
|
|
|
14923
15654
|
if dialog.exec_():
|
|
14924
15655
|
values = dialog.getValues()
|
|
14925
|
-
|
|
15656
|
+
first_trace_input = values['First Trace # to move'] # 1-based user input
|
|
15657
|
+
last_trace_input = values['Last Trace # to move'] # 1-based user input
|
|
14926
15658
|
new_position_input = values['New Position (1 to ' + str(max_position) + ')'] # 1-based user input
|
|
14927
15659
|
|
|
15660
|
+
# Validate input range
|
|
15661
|
+
if first_trace_input > last_trace_input:
|
|
15662
|
+
QMessageBox.warning(self, "Invalid Range", "First trace must be less than or equal to last trace")
|
|
15663
|
+
return
|
|
15664
|
+
|
|
14928
15665
|
# Convert to 0-based indices
|
|
14929
|
-
|
|
15666
|
+
first_trace_idx = first_trace_input - 1
|
|
15667
|
+
last_trace_idx = last_trace_input - 1
|
|
14930
15668
|
new_idx = new_position_input - 1
|
|
15669
|
+
num_traces_to_move = last_trace_idx - first_trace_idx + 1
|
|
14931
15670
|
|
|
14932
15671
|
if dialog.isChecked():
|
|
14933
15672
|
for i in range(len(self.streams)):
|
|
14934
15673
|
n_traces_i = len(self.streams[i])
|
|
14935
|
-
if
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
#
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
if self.picks[i] is not None and len(self.picks[i]) > trace_idx:
|
|
14952
|
-
pick_val = self.picks[i][trace_idx]
|
|
14953
|
-
self.picks[i] = np.insert(np.delete(self.picks[i], trace_idx), new_idx, pick_val)
|
|
14954
|
-
if self.error[i] is not None and len(self.error[i]) > trace_idx:
|
|
14955
|
-
error_val = self.error[i][trace_idx]
|
|
14956
|
-
self.error[i] = np.insert(np.delete(self.error[i], trace_idx), new_idx, error_val)
|
|
14957
|
-
if self.pickSeismoItems[i] is not None and len(self.pickSeismoItems[i]) > trace_idx:
|
|
14958
|
-
pick_seismo_val = self.pickSeismoItems[i][trace_idx]
|
|
14959
|
-
self.pickSeismoItems[i] = np.insert(np.delete(self.pickSeismoItems[i], trace_idx), new_idx, pick_seismo_val)
|
|
14960
|
-
if self.pickLayoutItems[i] is not None and len(self.pickLayoutItems[i]) > trace_idx:
|
|
14961
|
-
pick_layout_val = self.pickLayoutItems[i][trace_idx]
|
|
14962
|
-
self.pickLayoutItems[i] = np.insert(np.delete(self.pickLayoutItems[i], trace_idx), new_idx, pick_layout_val)
|
|
14963
|
-
|
|
14964
|
-
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
14965
|
-
shot_trace_number = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
14966
|
-
for trace in self.streams[i]]
|
|
14967
|
-
self.shot_trace_number[i] = shot_trace_number
|
|
15674
|
+
if first_trace_idx < n_traces_i and last_trace_idx < n_traces_i and new_idx < n_traces_i:
|
|
15675
|
+
# Move traces one by one - determine order to avoid index shifting issues
|
|
15676
|
+
# If moving to earlier position, move from first to last
|
|
15677
|
+
# If moving to later position, move from last to first
|
|
15678
|
+
if new_idx < first_trace_idx:
|
|
15679
|
+
# Moving to earlier position - move from first to last
|
|
15680
|
+
for offset in range(num_traces_to_move):
|
|
15681
|
+
current_trace_num = first_trace_input + offset
|
|
15682
|
+
current_new_pos = new_idx + offset
|
|
15683
|
+
self._moveSingleTrace(i, current_trace_num, current_new_pos)
|
|
15684
|
+
else:
|
|
15685
|
+
# Moving to later position - move from last to first
|
|
15686
|
+
for offset in range(num_traces_to_move - 1, -1, -1):
|
|
15687
|
+
current_trace_num = first_trace_input + offset
|
|
15688
|
+
current_new_pos = new_idx + offset
|
|
15689
|
+
self._moveSingleTrace(i, current_trace_num, current_new_pos)
|
|
14968
15690
|
|
|
14969
|
-
# Update
|
|
15691
|
+
# Update file_trace_number to be sequential
|
|
14970
15692
|
self._updateSequentialTraceNumbers(i)
|
|
14971
15693
|
# Sync headers to obspy streams immediately
|
|
14972
15694
|
self.syncHeadersToStreams(i)
|
|
14973
15695
|
|
|
14974
|
-
QMessageBox.information(self, "Trace Moved", f"Trace #{trace_num_input} moved to position {new_position_input} for all files")
|
|
14975
15696
|
self.headers_modified = True # Mark headers as modified
|
|
14976
15697
|
else:
|
|
14977
15698
|
i = self.currentIndex
|
|
14978
15699
|
n_traces_i = len(self.streams[i])
|
|
14979
|
-
if
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
#
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
if self.picks[i] is not None and len(self.picks[i]) > trace_idx:
|
|
14996
|
-
pick_val = self.picks[i][trace_idx]
|
|
14997
|
-
self.picks[i] = np.insert(np.delete(self.picks[i], trace_idx), new_idx, pick_val)
|
|
14998
|
-
if self.error[i] is not None and len(self.error[i]) > trace_idx:
|
|
14999
|
-
error_val = self.error[i][trace_idx]
|
|
15000
|
-
self.error[i] = np.insert(np.delete(self.error[i], trace_idx), new_idx, error_val)
|
|
15001
|
-
if self.pickSeismoItems[i] is not None and len(self.pickSeismoItems[i]) > trace_idx:
|
|
15002
|
-
pick_seismo_val = self.pickSeismoItems[i][trace_idx]
|
|
15003
|
-
self.pickSeismoItems[i] = np.insert(np.delete(self.pickSeismoItems[i], trace_idx), new_idx, pick_seismo_val)
|
|
15004
|
-
if self.pickLayoutItems[i] is not None and len(self.pickLayoutItems[i]) > trace_idx:
|
|
15005
|
-
pick_layout_val = self.pickLayoutItems[i][trace_idx]
|
|
15006
|
-
self.pickLayoutItems[i] = np.insert(np.delete(self.pickLayoutItems[i], trace_idx), new_idx, pick_layout_val)
|
|
15007
|
-
|
|
15008
|
-
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
15009
|
-
shot_trace_number = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
15010
|
-
for trace in self.streams[i]]
|
|
15011
|
-
self.shot_trace_number[i] = shot_trace_number
|
|
15700
|
+
if first_trace_idx < n_traces_i and last_trace_idx < n_traces_i and new_idx < n_traces_i:
|
|
15701
|
+
# Move traces one by one - determine order to avoid index shifting issues
|
|
15702
|
+
# If moving to earlier position, move from first to last
|
|
15703
|
+
# If moving to later position, move from last to first
|
|
15704
|
+
if new_idx < first_trace_idx:
|
|
15705
|
+
# Moving to earlier position - move from first to last
|
|
15706
|
+
for offset in range(num_traces_to_move):
|
|
15707
|
+
current_trace_num = first_trace_input + offset
|
|
15708
|
+
current_new_pos = new_idx + offset
|
|
15709
|
+
self._moveSingleTrace(i, current_trace_num, current_new_pos)
|
|
15710
|
+
else:
|
|
15711
|
+
# Moving to later position - move from last to first
|
|
15712
|
+
for offset in range(num_traces_to_move - 1, -1, -1):
|
|
15713
|
+
current_trace_num = first_trace_input + offset
|
|
15714
|
+
current_new_pos = new_idx + offset
|
|
15715
|
+
self._moveSingleTrace(i, current_trace_num, current_new_pos)
|
|
15012
15716
|
|
|
15013
|
-
# Update
|
|
15717
|
+
# Update file_trace_number to be sequential
|
|
15014
15718
|
self._updateSequentialTraceNumbers(i)
|
|
15015
15719
|
# Sync headers to obspy streams immediately
|
|
15016
15720
|
self.syncHeadersToStreams(i)
|
|
15017
15721
|
|
|
15018
|
-
QMessageBox.information(self, "Trace Moved", f"Trace #{trace_num_input} moved to position {new_position_input} for file {os.path.basename(self.currentFileName)}")
|
|
15019
15722
|
self.headers_modified = True # Mark headers as modified
|
|
15020
15723
|
else:
|
|
15021
15724
|
QMessageBox.warning(self, "Invalid Trace Numbers", f"Invalid trace indices or positions")
|
|
@@ -15024,6 +15727,49 @@ class MainWindow(QMainWindow):
|
|
|
15024
15727
|
self.updateFileListDisplay()
|
|
15025
15728
|
self.updatePlots()
|
|
15026
15729
|
|
|
15730
|
+
def _moveSingleTrace(self, file_idx, trace_num, new_position):
|
|
15731
|
+
"""Helper function to move a single trace within a file.
|
|
15732
|
+
|
|
15733
|
+
Args:
|
|
15734
|
+
file_idx: Index of the file
|
|
15735
|
+
trace_num: Trace number to move (1-based)
|
|
15736
|
+
new_position: New position index (0-based)
|
|
15737
|
+
"""
|
|
15738
|
+
trace_idx = trace_num - 1 # Convert to 0-based
|
|
15739
|
+
|
|
15740
|
+
self.streams[file_idx] = move_trace(self.streams[file_idx], trace_num, new_position)
|
|
15741
|
+
|
|
15742
|
+
# Move all associated data arrays EXCEPT trace_position (spatial position stays with array index)
|
|
15743
|
+
for attr in [self.trace_elevation[file_idx], self.offset[file_idx],
|
|
15744
|
+
self.file_trace_number[file_idx]]:
|
|
15745
|
+
if len(attr) > trace_idx:
|
|
15746
|
+
attr_val = attr[trace_idx]
|
|
15747
|
+
attr[:] = np.insert(np.delete(attr, trace_idx), new_position, attr_val)
|
|
15748
|
+
|
|
15749
|
+
# Move shot_trace_number array
|
|
15750
|
+
if len(self.shot_trace_number[file_idx]) > trace_idx:
|
|
15751
|
+
shot_val = self.shot_trace_number[file_idx][trace_idx]
|
|
15752
|
+
self.shot_trace_number[file_idx] = np.insert(np.delete(self.shot_trace_number[file_idx], trace_idx), new_position, shot_val)
|
|
15753
|
+
|
|
15754
|
+
# Move picks and errors
|
|
15755
|
+
if self.picks[file_idx] is not None and len(self.picks[file_idx]) > trace_idx:
|
|
15756
|
+
pick_val = self.picks[file_idx][trace_idx]
|
|
15757
|
+
self.picks[file_idx] = np.insert(np.delete(self.picks[file_idx], trace_idx), new_position, pick_val)
|
|
15758
|
+
if self.error[file_idx] is not None and len(self.error[file_idx]) > trace_idx:
|
|
15759
|
+
error_val = self.error[file_idx][trace_idx]
|
|
15760
|
+
self.error[file_idx] = np.insert(np.delete(self.error[file_idx], trace_idx), new_position, error_val)
|
|
15761
|
+
if self.pickSeismoItems[file_idx] is not None and len(self.pickSeismoItems[file_idx]) > trace_idx:
|
|
15762
|
+
pick_seismo_val = self.pickSeismoItems[file_idx][trace_idx]
|
|
15763
|
+
self.pickSeismoItems[file_idx] = np.insert(np.delete(self.pickSeismoItems[file_idx], trace_idx), new_position, pick_seismo_val)
|
|
15764
|
+
if self.pickLayoutItems[file_idx] is not None and len(self.pickLayoutItems[file_idx]) > trace_idx:
|
|
15765
|
+
pick_layout_val = self.pickLayoutItems[file_idx][trace_idx]
|
|
15766
|
+
self.pickLayoutItems[file_idx] = np.insert(np.delete(self.pickLayoutItems[file_idx], trace_idx), new_position, pick_layout_val)
|
|
15767
|
+
|
|
15768
|
+
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
15769
|
+
shot_trace_number = [trace.stats[self.input_format[file_idx]].trace_header.trace_number_within_the_original_field_record
|
|
15770
|
+
for trace in self.streams[file_idx]]
|
|
15771
|
+
self.shot_trace_number[file_idx] = shot_trace_number
|
|
15772
|
+
|
|
15027
15773
|
def muteTrace(self):
|
|
15028
15774
|
if self.streams:
|
|
15029
15775
|
trace_numbers = self.shot_trace_number[self.currentIndex]
|
|
@@ -15046,7 +15792,6 @@ class MainWindow(QMainWindow):
|
|
|
15046
15792
|
# Sync headers to obspy streams immediately
|
|
15047
15793
|
self.syncHeadersToStreams(i)
|
|
15048
15794
|
self.headers_modified = True
|
|
15049
|
-
QMessageBox.information(self, "Trace Muted", f"Trace #{trace_numbers[selected_index]} muted for all files")
|
|
15050
15795
|
else:
|
|
15051
15796
|
selected_indices = np.where(np.array(trace_numbers) == selected_trace)[0]
|
|
15052
15797
|
if selected_indices.size == 0:
|
|
@@ -15058,7 +15803,6 @@ class MainWindow(QMainWindow):
|
|
|
15058
15803
|
# Sync headers to obspy streams immediately
|
|
15059
15804
|
self.syncHeadersToStreams(self.currentIndex)
|
|
15060
15805
|
self.headers_modified = True
|
|
15061
|
-
QMessageBox.information(self, "Trace Muted", f"Trace #{trace_numbers[selected_index]} muted for file {os.path.basename(self.currentFileName)}")
|
|
15062
15806
|
self.updatePlots()
|
|
15063
15807
|
self.updateFileListDisplay()
|
|
15064
15808
|
self.updatePlots()
|
|
@@ -15066,99 +15810,221 @@ class MainWindow(QMainWindow):
|
|
|
15066
15810
|
def reverseTraces(self):
|
|
15067
15811
|
"""Reverse trace data (flip data matrix left-to-right)"""
|
|
15068
15812
|
if self.streams:
|
|
15813
|
+
n_traces = len(self.streams[self.currentIndex])
|
|
15814
|
+
|
|
15815
|
+
parameters = [
|
|
15816
|
+
{'label': 'First Trace #', 'initial_value': 1, 'type': 'int'},
|
|
15817
|
+
{'label': 'Last Trace #', 'initial_value': n_traces, 'type': 'int'}
|
|
15818
|
+
]
|
|
15819
|
+
|
|
15069
15820
|
dialog = GenericParameterDialog(
|
|
15070
15821
|
title="Reverse Trace Data",
|
|
15071
|
-
parameters=
|
|
15822
|
+
parameters=parameters,
|
|
15072
15823
|
add_checkbox=True,
|
|
15073
15824
|
checkbox_text="Apply to all shots",
|
|
15074
15825
|
parent=self
|
|
15075
15826
|
)
|
|
15076
15827
|
|
|
15077
15828
|
if dialog.exec_():
|
|
15829
|
+
values = dialog.getValues()
|
|
15830
|
+
first_trace = values['First Trace #'] - 1 # Convert to 0-based
|
|
15831
|
+
last_trace = values['Last Trace #'] - 1 # Convert to 0-based
|
|
15832
|
+
|
|
15833
|
+
# Validate input range
|
|
15834
|
+
if first_trace > last_trace:
|
|
15835
|
+
QMessageBox.warning(self, "Invalid Range", "First trace must be less than or equal to last trace")
|
|
15836
|
+
return
|
|
15837
|
+
|
|
15078
15838
|
if dialog.isChecked():
|
|
15079
15839
|
# Reverse trace data for all files
|
|
15080
15840
|
for i in range(len(self.streams)):
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
self.shot_trace_number[i] = self.shot_trace_number[i][::-1]
|
|
15086
|
-
|
|
15087
|
-
# Reverse picks and errors (they follow the data)
|
|
15088
|
-
if self.picks[i] is not None:
|
|
15089
|
-
self.picks[i] = self.picks[i][::-1]
|
|
15090
|
-
if self.error[i] is not None:
|
|
15091
|
-
self.error[i] = self.error[i][::-1]
|
|
15841
|
+
n_traces_i = len(self.streams[i])
|
|
15842
|
+
# Validate range for this file
|
|
15843
|
+
if first_trace >= n_traces_i or last_trace >= n_traces_i:
|
|
15844
|
+
continue # Skip files that don't have enough traces
|
|
15092
15845
|
|
|
15093
|
-
|
|
15094
|
-
if self.pickSeismoItems[i] is not None:
|
|
15095
|
-
self.pickSeismoItems[i] = self.pickSeismoItems[i][::-1]
|
|
15096
|
-
if self.pickLayoutItems[i] is not None:
|
|
15097
|
-
self.pickLayoutItems[i] = self.pickLayoutItems[i][::-1]
|
|
15846
|
+
self._reverseTraceSubset(i, first_trace, last_trace)
|
|
15098
15847
|
|
|
15099
|
-
QMessageBox.information(self, "Trace Data Reversed", "Trace data reversed for all files")
|
|
15100
15848
|
self.headers_modified = True
|
|
15101
15849
|
else:
|
|
15102
15850
|
i = self.currentIndex
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
# Reverse picks and errors (they follow the data)
|
|
15110
|
-
if self.picks[i] is not None:
|
|
15111
|
-
self.picks[i] = self.picks[i][::-1]
|
|
15112
|
-
if self.error[i] is not None:
|
|
15113
|
-
self.error[i] = self.error[i][::-1]
|
|
15851
|
+
n_traces_i = len(self.streams[i])
|
|
15852
|
+
# Validate range for this file
|
|
15853
|
+
if first_trace >= n_traces_i or last_trace >= n_traces_i:
|
|
15854
|
+
QMessageBox.warning(self, "Invalid Range", f"Trace range exceeds available traces ({n_traces_i})")
|
|
15855
|
+
return
|
|
15114
15856
|
|
|
15115
|
-
|
|
15116
|
-
if self.pickSeismoItems[i] is not None:
|
|
15117
|
-
self.pickSeismoItems[i] = self.pickSeismoItems[i][::-1]
|
|
15118
|
-
if self.pickLayoutItems[i] is not None:
|
|
15119
|
-
self.pickLayoutItems[i] = self.pickLayoutItems[i][::-1]
|
|
15857
|
+
self._reverseTraceSubset(i, first_trace, last_trace)
|
|
15120
15858
|
|
|
15121
|
-
QMessageBox.information(self, "Trace Data Reversed", f"Trace data reversed for file {os.path.basename(self.currentFileName)}")
|
|
15122
15859
|
self.headers_modified = True
|
|
15123
15860
|
|
|
15124
15861
|
self.updatePlots()
|
|
15125
15862
|
self.updateFileListDisplay()
|
|
15126
15863
|
self.updatePlots()
|
|
15127
|
-
|
|
15128
|
-
def
|
|
15864
|
+
|
|
15865
|
+
def _reverseTraceSubset(self, file_idx, first_trace, last_trace):
|
|
15866
|
+
"""Helper function to reverse a subset of traces within a file.
|
|
15867
|
+
|
|
15868
|
+
Args:
|
|
15869
|
+
file_idx: Index of the file
|
|
15870
|
+
first_trace: First trace index to reverse (0-based, inclusive)
|
|
15871
|
+
last_trace: Last trace index to reverse (0-based, inclusive)
|
|
15872
|
+
"""
|
|
15873
|
+
# Extract the subset of traces to reverse
|
|
15874
|
+
subset_size = last_trace - first_trace + 1
|
|
15875
|
+
|
|
15876
|
+
if subset_size <= 1:
|
|
15877
|
+
return # Nothing to reverse
|
|
15878
|
+
|
|
15879
|
+
# Reverse the stream data for the subset
|
|
15880
|
+
input_format = self.input_format[file_idx]
|
|
15881
|
+
|
|
15882
|
+
# Extract data arrays and trace numbers for the subset
|
|
15883
|
+
data_arrays = [self.streams[file_idx][i].data.copy() for i in range(first_trace, last_trace + 1)]
|
|
15884
|
+
trace_numbers = [self.streams[file_idx][i].stats[input_format].trace_header.trace_number_within_the_original_field_record
|
|
15885
|
+
for i in range(first_trace, last_trace + 1)]
|
|
15886
|
+
|
|
15887
|
+
# Reverse them
|
|
15888
|
+
data_arrays = data_arrays[::-1]
|
|
15889
|
+
trace_numbers = trace_numbers[::-1]
|
|
15890
|
+
|
|
15891
|
+
# Assign back to the stream
|
|
15892
|
+
for idx, i in enumerate(range(first_trace, last_trace + 1)):
|
|
15893
|
+
self.streams[file_idx][i].data = data_arrays[idx]
|
|
15894
|
+
self.streams[file_idx][i].stats[input_format].trace_header.trace_number_within_the_original_field_record = trace_numbers[idx]
|
|
15895
|
+
|
|
15896
|
+
# Reverse the corresponding portions of shot_trace_number
|
|
15897
|
+
self.shot_trace_number[file_idx][first_trace:last_trace+1] = self.shot_trace_number[file_idx][first_trace:last_trace+1][::-1]
|
|
15898
|
+
|
|
15899
|
+
# Reverse the corresponding portions of picks and errors (they follow the data)
|
|
15900
|
+
if self.picks[file_idx] is not None:
|
|
15901
|
+
self.picks[file_idx][first_trace:last_trace+1] = self.picks[file_idx][first_trace:last_trace+1][::-1]
|
|
15902
|
+
if self.error[file_idx] is not None:
|
|
15903
|
+
self.error[file_idx][first_trace:last_trace+1] = self.error[file_idx][first_trace:last_trace+1][::-1]
|
|
15904
|
+
|
|
15905
|
+
# Reverse the corresponding portions of pick items (they follow the data)
|
|
15906
|
+
if self.pickSeismoItems[file_idx] is not None:
|
|
15907
|
+
self.pickSeismoItems[file_idx][first_trace:last_trace+1] = self.pickSeismoItems[file_idx][first_trace:last_trace+1][::-1]
|
|
15908
|
+
if self.pickLayoutItems[file_idx] is not None:
|
|
15909
|
+
self.pickLayoutItems[file_idx][first_trace:last_trace+1] = self.pickLayoutItems[file_idx][first_trace:last_trace+1][::-1]
|
|
15910
|
+
|
|
15911
|
+
def zeroPadTrace(self):
|
|
15912
|
+
"""Zero pad all traces in a shot by adding zeros at the end of the trace data."""
|
|
15129
15913
|
if self.streams:
|
|
15130
15914
|
parameters = [
|
|
15131
|
-
|
|
15132
|
-
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
|
|
15133
|
-
{'label': 'First FFID', 'initial_value': self.ffid[0], 'type': 'int'},
|
|
15134
|
-
{'label': 'Increment', 'initial_value': 1, 'type': 'int'}
|
|
15915
|
+
{'label': 'Pad at end (seconds)', 'initial_value': 0.0, 'type': 'float'}
|
|
15135
15916
|
]
|
|
15136
|
-
|
|
15917
|
+
|
|
15137
15918
|
dialog = GenericParameterDialog(
|
|
15138
|
-
title="
|
|
15919
|
+
title="Zero Pad Shot",
|
|
15139
15920
|
parameters=parameters,
|
|
15140
|
-
add_checkbox=
|
|
15921
|
+
add_checkbox=True,
|
|
15922
|
+
checkbox_text="Apply to all shots",
|
|
15141
15923
|
parent=self
|
|
15142
15924
|
)
|
|
15143
|
-
|
|
15925
|
+
|
|
15144
15926
|
if dialog.exec_():
|
|
15145
15927
|
values = dialog.getValues()
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
|
|
15161
|
-
|
|
15928
|
+
pad_end_seconds = values['Pad at end (seconds)']
|
|
15929
|
+
apply_to_all = dialog.isChecked()
|
|
15930
|
+
|
|
15931
|
+
# Validate inputs
|
|
15932
|
+
if pad_end_seconds < 0:
|
|
15933
|
+
QMessageBox.warning(self, "Invalid Input", "Padding value must be non-negative")
|
|
15934
|
+
return
|
|
15935
|
+
|
|
15936
|
+
if pad_end_seconds == 0:
|
|
15937
|
+
QMessageBox.warning(self, "Invalid Input", "Padding value must be greater than 0")
|
|
15938
|
+
return
|
|
15939
|
+
|
|
15940
|
+
# Show wait cursor while processing
|
|
15941
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
15942
|
+
try:
|
|
15943
|
+
if apply_to_all:
|
|
15944
|
+
# Pad all traces for all files
|
|
15945
|
+
for i in range(len(self.streams)):
|
|
15946
|
+
# Convert seconds to samples
|
|
15947
|
+
pad_end = int(pad_end_seconds / self.sample_interval[i])
|
|
15948
|
+
|
|
15949
|
+
# Pad all traces in this shot
|
|
15950
|
+
for trace_num in self.shot_trace_number[i]:
|
|
15951
|
+
self.streams[i] = zero_pad_trace(self.streams[i], trace_num, 0, pad_end)
|
|
15952
|
+
|
|
15953
|
+
# Update n_sample and time array after padding
|
|
15954
|
+
if len(self.streams[i]) > 0:
|
|
15955
|
+
self.n_sample[i] = len(self.streams[i][0].data)
|
|
15956
|
+
self.time[i] = np.arange(self.n_sample[i]) * self.sample_interval[i] + self.delay[i]
|
|
15957
|
+
# Sync headers to obspy streams immediately
|
|
15958
|
+
self.syncHeadersToStreams(i)
|
|
15959
|
+
|
|
15960
|
+
self.headers_modified = True
|
|
15961
|
+
QMessageBox.information(self, "Zero Padding Applied",
|
|
15962
|
+
f"All traces padded with {pad_end_seconds} seconds at end for all shots")
|
|
15963
|
+
else:
|
|
15964
|
+
# Pad all traces for current file only
|
|
15965
|
+
# Convert seconds to samples
|
|
15966
|
+
pad_end = int(pad_end_seconds / self.sample_interval[self.currentIndex])
|
|
15967
|
+
|
|
15968
|
+
# Pad all traces in this shot
|
|
15969
|
+
for trace_num in self.shot_trace_number[self.currentIndex]:
|
|
15970
|
+
self.streams[self.currentIndex] = zero_pad_trace(self.streams[self.currentIndex], trace_num, 0, pad_end)
|
|
15971
|
+
|
|
15972
|
+
# Update n_sample and time array after padding
|
|
15973
|
+
if len(self.streams[self.currentIndex]) > 0:
|
|
15974
|
+
self.n_sample[self.currentIndex] = len(self.streams[self.currentIndex][0].data)
|
|
15975
|
+
self.time[self.currentIndex] = np.arange(self.n_sample[self.currentIndex]) * self.sample_interval[self.currentIndex] + self.delay[self.currentIndex]
|
|
15976
|
+
# Sync headers to obspy streams immediately
|
|
15977
|
+
self.syncHeadersToStreams(self.currentIndex)
|
|
15978
|
+
|
|
15979
|
+
self.headers_modified = True
|
|
15980
|
+
QMessageBox.information(self, "Zero Padding Applied",
|
|
15981
|
+
f"All traces padded with {pad_end_seconds} seconds at end")
|
|
15982
|
+
|
|
15983
|
+
self.updatePlots()
|
|
15984
|
+
self.updateFileListDisplay()
|
|
15985
|
+
self.updatePlots()
|
|
15986
|
+
finally:
|
|
15987
|
+
# Always restore cursor even if there's an error
|
|
15988
|
+
QApplication.restoreOverrideCursor()
|
|
15989
|
+
|
|
15990
|
+
def batchEditFFID(self):
|
|
15991
|
+
if self.streams:
|
|
15992
|
+
parameters = [
|
|
15993
|
+
{'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
|
|
15994
|
+
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
|
|
15995
|
+
{'label': 'First FFID', 'initial_value': self.ffid[0], 'type': 'int'},
|
|
15996
|
+
{'label': 'Increment', 'initial_value': 1, 'type': 'int'}
|
|
15997
|
+
]
|
|
15998
|
+
|
|
15999
|
+
dialog = GenericParameterDialog(
|
|
16000
|
+
title="Batch Edit FFID",
|
|
16001
|
+
parameters=parameters,
|
|
16002
|
+
add_checkbox=False,
|
|
16003
|
+
parent=self
|
|
16004
|
+
)
|
|
16005
|
+
|
|
16006
|
+
if dialog.exec_():
|
|
16007
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16008
|
+
try:
|
|
16009
|
+
values = dialog.getValues()
|
|
16010
|
+
first_shot = values['First Source #'] - 1
|
|
16011
|
+
last_shot = values['Last Source #'] - 1
|
|
16012
|
+
first_ffid = values['First FFID']
|
|
16013
|
+
increment = values['Increment']
|
|
16014
|
+
|
|
16015
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16016
|
+
self.ffid[i] = first_ffid + (i - first_shot) * increment
|
|
16017
|
+
# Sync headers to obspy streams immediately for all files
|
|
16018
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16019
|
+
self.syncHeadersToStreams(i)
|
|
16020
|
+
self.headers_modified = True # Mark headers as modified
|
|
16021
|
+
QMessageBox.information(self, "FFIDs Updated", f"FFIDs set from {first_ffid} to {self.ffid[last_shot]} with increment {increment} for shots {first_shot+1} to {last_shot+1}")
|
|
16022
|
+
|
|
16023
|
+
self.updateTitle()
|
|
16024
|
+
self.updateFileListDisplay()
|
|
16025
|
+
self.updatePlots()
|
|
16026
|
+
finally:
|
|
16027
|
+
QApplication.restoreOverrideCursor()
|
|
15162
16028
|
|
|
15163
16029
|
def batchEditDelay(self):
|
|
15164
16030
|
if self.streams:
|
|
@@ -15176,27 +16042,31 @@ class MainWindow(QMainWindow):
|
|
|
15176
16042
|
)
|
|
15177
16043
|
|
|
15178
16044
|
if dialog.exec_():
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
self.
|
|
15191
|
-
|
|
15192
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
16045
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16046
|
+
try:
|
|
16047
|
+
values = dialog.getValues()
|
|
16048
|
+
first_shot = values['First Source #'] - 1
|
|
16049
|
+
last_shot = values['Last Source #'] - 1
|
|
16050
|
+
delay = values['Delay (in s)']
|
|
16051
|
+
|
|
16052
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16053
|
+
# Calculate delay difference for each file individually
|
|
16054
|
+
diff_delay = delay - self.delay[i]
|
|
16055
|
+
self.delay[i] = delay
|
|
16056
|
+
self.time[i] = np.arange(self.n_sample[i]) * self.sample_interval[i] + self.delay[i]
|
|
16057
|
+
if self.picks[i] is not None:
|
|
16058
|
+
self.picks[i] = [pick + diff_delay for pick in self.picks[i]]
|
|
16059
|
+
# Sync headers to obspy streams immediately for all files
|
|
16060
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16061
|
+
self.syncHeadersToStreams(i)
|
|
16062
|
+
self.headers_modified = True # Mark headers as modified
|
|
16063
|
+
QMessageBox.information(self, "Delays Updated", f"Delays set to {delay} s for shots {first_shot+1} to {last_shot+1}")
|
|
15196
16064
|
|
|
15197
|
-
|
|
15198
|
-
|
|
15199
|
-
|
|
16065
|
+
self.updatePlots()
|
|
16066
|
+
self.updateFileListDisplay()
|
|
16067
|
+
self.updatePlots()
|
|
16068
|
+
finally:
|
|
16069
|
+
QApplication.restoreOverrideCursor()
|
|
15200
16070
|
|
|
15201
16071
|
def batchEditSampleInterval(self):
|
|
15202
16072
|
if self.streams:
|
|
@@ -15214,30 +16084,34 @@ class MainWindow(QMainWindow):
|
|
|
15214
16084
|
)
|
|
15215
16085
|
|
|
15216
16086
|
if dialog.exec_():
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
16087
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16088
|
+
try:
|
|
16089
|
+
values = dialog.getValues()
|
|
16090
|
+
first_shot = values['First Source #'] - 1
|
|
16091
|
+
last_shot = values['Last Source #'] - 1
|
|
16092
|
+
sample_interval = values['Sample Interval (in s)']
|
|
16093
|
+
|
|
16094
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16095
|
+
self.sample_interval[i] = sample_interval
|
|
16096
|
+
self.time[i] = np.arange(self.n_sample[i]) * self.sample_interval[i] + self.delay[i]
|
|
16097
|
+
# Sync headers to obspy streams immediately for all files
|
|
16098
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16099
|
+
self.syncHeadersToStreams(i)
|
|
16100
|
+
self.headers_modified = True
|
|
16101
|
+
QMessageBox.information(self, "Sample Interval Updated", f"Sample intervals set to {sample_interval} s for shots {first_shot+1} to {last_shot+1}")
|
|
15230
16102
|
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
16103
|
+
self.updatePlots()
|
|
16104
|
+
self.updateFileListDisplay()
|
|
16105
|
+
self.updatePlots()
|
|
16106
|
+
finally:
|
|
16107
|
+
QApplication.restoreOverrideCursor()
|
|
15234
16108
|
|
|
15235
16109
|
def batchEditSourcePosition(self):
|
|
15236
16110
|
if self.streams:
|
|
15237
16111
|
parameters = [
|
|
15238
16112
|
{'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
|
|
15239
|
-
{'label': 'Skip every N sources', 'initial_value': 0, 'type': 'int'},
|
|
15240
16113
|
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
|
|
16114
|
+
{'label': 'Skip every N sources', 'initial_value': 0, 'type': 'int'},
|
|
15241
16115
|
{'label': 'First Source Position (in m)', 'initial_value': self.source_position[0], 'type': 'float'},
|
|
15242
16116
|
{'label': 'Last Source Position (in m)', 'initial_value': '', 'type': 'float_or_empty'},
|
|
15243
16117
|
{'label': 'Spacing (in m)', 'initial_value': np.mean(np.diff(self.source_position)), 'type': 'float'},
|
|
@@ -15252,84 +16126,93 @@ class MainWindow(QMainWindow):
|
|
|
15252
16126
|
)
|
|
15253
16127
|
|
|
15254
16128
|
if dialog.exec_():
|
|
15255
|
-
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
# Check if we have a last source position
|
|
15272
|
-
if last_source_position is None or last_source_position == '':
|
|
15273
|
-
# No looping: generate positions until end of shot list
|
|
15274
|
-
# Calculate number of modified shots considering the step interval
|
|
15275
|
-
num_modified_shots = (num_shots + step - 1) // step # Ceiling division
|
|
15276
|
-
num_unique_pos = (num_modified_shots + num_stacks - 1) // num_stacks # Ceiling division
|
|
15277
|
-
unique_positions = np.round(np.arange(num_unique_pos) * spacing + first_source_position, self.rounding)
|
|
15278
|
-
|
|
15279
|
-
# Repeat each position according to stack count
|
|
15280
|
-
source_pos = []
|
|
15281
|
-
for pos_idx in range(num_unique_pos):
|
|
15282
|
-
for _ in range(num_stacks):
|
|
15283
|
-
if len(source_pos) < num_modified_shots:
|
|
15284
|
-
source_pos.append(unique_positions[pos_idx])
|
|
15285
|
-
mode_info = f"from {first_source_position} m to end with spacing {spacing} m"
|
|
15286
|
-
else:
|
|
15287
|
-
# Looping mode: generate positions and loop between first and last
|
|
15288
|
-
num_steps = int(np.round((last_source_position - first_source_position) / spacing)) + 1
|
|
15289
|
-
unique_positions = np.round(np.arange(num_steps) * spacing + first_source_position, self.rounding)
|
|
16129
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16130
|
+
try:
|
|
16131
|
+
values = dialog.getValues()
|
|
16132
|
+
first_shot = values['First Source #'] - 1
|
|
16133
|
+
last_shot_value = values['Last Source #']
|
|
16134
|
+
# Handle None or empty value - default to last source in file
|
|
16135
|
+
if last_shot_value is None:
|
|
16136
|
+
last_shot = len(self.streams) - 1
|
|
16137
|
+
else:
|
|
16138
|
+
last_shot = last_shot_value - 1
|
|
16139
|
+
first_source_position = values['First Source Position (in m)']
|
|
16140
|
+
last_source_position = values['Last Source Position (in m)']
|
|
16141
|
+
spacing = values['Spacing (in m)']
|
|
16142
|
+
skip_every = max(0, values['Skip every N sources']) # 0 = no skipping
|
|
16143
|
+
num_stacks = max(1, values['Number of stacks']) # Ensure at least 1
|
|
15290
16144
|
|
|
15291
|
-
|
|
15292
|
-
num_modified_shots = (num_shots + step - 1) // step # Ceiling division
|
|
16145
|
+
num_shots = min(last_shot + 1, len(self.streams)) - first_shot
|
|
15293
16146
|
|
|
15294
|
-
#
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
16147
|
+
# Calculate the step interval based on skip value
|
|
16148
|
+
# If skip_every is 0, step is 1 (modify every source)
|
|
16149
|
+
# If skip_every is 3, step is 4 (modify every 4th source, skipping 3 in between)
|
|
16150
|
+
step = skip_every + 1 if skip_every > 0 else 1
|
|
15298
16151
|
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
16152
|
+
# Check if we have a last source position
|
|
16153
|
+
if last_source_position is None or last_source_position == '':
|
|
16154
|
+
# No looping: generate positions until end of shot list
|
|
16155
|
+
# Calculate number of modified shots considering the step interval
|
|
16156
|
+
num_modified_shots = (num_shots + step - 1) // step # Ceiling division
|
|
16157
|
+
num_unique_pos = (num_modified_shots + num_stacks - 1) // num_stacks # Ceiling division
|
|
16158
|
+
unique_positions = np.round(np.arange(num_unique_pos) * spacing + first_source_position, self.rounding)
|
|
16159
|
+
|
|
16160
|
+
# Repeat each position according to stack count
|
|
16161
|
+
source_pos = []
|
|
16162
|
+
for pos_idx in range(num_unique_pos):
|
|
16163
|
+
for _ in range(num_stacks):
|
|
16164
|
+
if len(source_pos) < num_modified_shots:
|
|
16165
|
+
source_pos.append(unique_positions[pos_idx])
|
|
16166
|
+
mode_info = f"from {first_source_position} m to end with spacing {spacing} m"
|
|
16167
|
+
else:
|
|
16168
|
+
# Looping mode: generate positions and loop between first and last
|
|
16169
|
+
num_steps = int(np.round((last_source_position - first_source_position) / spacing)) + 1
|
|
16170
|
+
unique_positions = np.round(np.arange(num_steps) * spacing + first_source_position, self.rounding)
|
|
15302
16171
|
|
|
15303
|
-
#
|
|
15304
|
-
|
|
15305
|
-
|
|
15306
|
-
|
|
16172
|
+
# Calculate number of modified shots considering the step interval
|
|
16173
|
+
num_modified_shots = (num_shots + step - 1) // step # Ceiling division
|
|
16174
|
+
|
|
16175
|
+
# Repeat each position according to stack count, looping through all positions
|
|
16176
|
+
source_pos = []
|
|
16177
|
+
pos_cycle_idx = 0
|
|
16178
|
+
stacks_count = 0
|
|
16179
|
+
|
|
16180
|
+
while len(source_pos) < num_modified_shots:
|
|
16181
|
+
source_pos.append(unique_positions[pos_cycle_idx % len(unique_positions)])
|
|
16182
|
+
stacks_count += 1
|
|
16183
|
+
|
|
16184
|
+
# Move to next position after each stack
|
|
16185
|
+
if stacks_count >= num_stacks:
|
|
16186
|
+
pos_cycle_idx += 1
|
|
16187
|
+
stacks_count = 0
|
|
16188
|
+
|
|
16189
|
+
mode_info = f"looping from {first_source_position} m to {last_source_position} m with spacing {spacing} m"
|
|
16190
|
+
|
|
16191
|
+
# Apply source positions only to shots at step intervals
|
|
16192
|
+
source_pos_idx = 0
|
|
16193
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16194
|
+
shot_offset = i - first_shot
|
|
16195
|
+
# Only modify if shot offset is at a step interval (0, step, 2*step, etc.)
|
|
16196
|
+
if shot_offset % step == 0 and source_pos_idx < len(source_pos):
|
|
16197
|
+
self.source_position[i] = source_pos[source_pos_idx]
|
|
16198
|
+
source_pos_idx += 1
|
|
16199
|
+
for j in range(len(self.trace_position[i])):
|
|
16200
|
+
self.offset[i][j] = np.round(self.trace_position[i][j] - self.source_position[i], self.rounding)
|
|
16201
|
+
# Sync headers to obspy streams immediately for all files
|
|
16202
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16203
|
+
self.syncHeadersToStreams(i)
|
|
16204
|
+
self.headers_modified = True # Mark headers as modified
|
|
15307
16205
|
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
source_pos_idx = 0
|
|
15312
|
-
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
15313
|
-
shot_offset = i - first_shot
|
|
15314
|
-
# Only modify if shot offset is at a step interval (0, step, 2*step, etc.)
|
|
15315
|
-
if shot_offset % step == 0 and source_pos_idx < len(source_pos):
|
|
15316
|
-
self.source_position[i] = source_pos[source_pos_idx]
|
|
15317
|
-
source_pos_idx += 1
|
|
15318
|
-
for j in range(len(self.trace_position[i])):
|
|
15319
|
-
self.offset[i][j] = np.round(self.trace_position[i][j] - self.source_position[i], self.rounding)
|
|
15320
|
-
# Sync headers to obspy streams immediately for all files
|
|
15321
|
-
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
15322
|
-
self.syncHeadersToStreams(i)
|
|
15323
|
-
self.headers_modified = True # Mark headers as modified
|
|
15324
|
-
|
|
15325
|
-
stack_info = f" with {num_stacks} stacks per position" if num_stacks > 1 else ""
|
|
15326
|
-
skip_info = f" (skipping every {skip_every} sources)" if skip_every > 0 else ""
|
|
15327
|
-
QMessageBox.information(self, "Source Positions Updated", f"Source positions set {mode_info}{stack_info}{skip_info} for shots {first_shot+1} to {last_shot}")
|
|
16206
|
+
stack_info = f" with {num_stacks} stacks per position" if num_stacks > 1 else ""
|
|
16207
|
+
skip_info = f" (skipping every {skip_every} sources)" if skip_every > 0 else ""
|
|
16208
|
+
QMessageBox.information(self, "Source Positions Updated", f"Source positions set {mode_info}{stack_info}{skip_info} for shots {first_shot+1} to {last_shot+1}")
|
|
15328
16209
|
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
16210
|
+
self.updateMeanSpacing()
|
|
16211
|
+
self.updatePlots()
|
|
16212
|
+
self.updateFileListDisplay()
|
|
16213
|
+
self.updatePlots()
|
|
16214
|
+
finally:
|
|
16215
|
+
QApplication.restoreOverrideCursor()
|
|
15333
16216
|
|
|
15334
16217
|
def batchEditTracePosition(self):
|
|
15335
16218
|
if self.streams:
|
|
@@ -15350,40 +16233,48 @@ class MainWindow(QMainWindow):
|
|
|
15350
16233
|
)
|
|
15351
16234
|
|
|
15352
16235
|
if dialog.exec_():
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
|
|
15372
|
-
|
|
15373
|
-
|
|
15374
|
-
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
|
|
16236
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16237
|
+
try:
|
|
16238
|
+
values = dialog.getValues()
|
|
16239
|
+
first_shot = values['First Source #'] - 1
|
|
16240
|
+
last_shot = values['Last Source #'] - 1
|
|
16241
|
+
first_trace = values['First Trace #'] - 1
|
|
16242
|
+
last_trace = values['Last Trace #'] - 1
|
|
16243
|
+
first_trace_position = values['First Trace Position (in m)']
|
|
16244
|
+
spacing = values['Spacing (in m)']
|
|
16245
|
+
|
|
16246
|
+
# Create trace positions for the range of traces (0-based indexing for the array)
|
|
16247
|
+
num_traces = last_trace - first_trace + 1
|
|
16248
|
+
trace_pos = np.round(np.arange(num_traces) * spacing + first_trace_position, self.rounding)
|
|
16249
|
+
|
|
16250
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16251
|
+
for j in range(first_trace, last_trace + 1):
|
|
16252
|
+
# Use j - first_trace to get the correct index in trace_pos array
|
|
16253
|
+
trace_idx = j - first_trace
|
|
16254
|
+
self.trace_position[i][j] = trace_pos[trace_idx]
|
|
16255
|
+
self.offset[i][j] = np.round(self.trace_position[i][j] - self.source_position[i], self.rounding)
|
|
16256
|
+
|
|
16257
|
+
# Reorder traces by position to maintain spatial order
|
|
16258
|
+
self._reorderTracesByPosition(i)
|
|
16259
|
+
|
|
16260
|
+
# Sync headers to obspy streams immediately for all files
|
|
16261
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16262
|
+
self.syncHeadersToStreams(i)
|
|
16263
|
+
self.headers_modified = True # Mark headers as modified
|
|
16264
|
+
QMessageBox.information(
|
|
16265
|
+
self,
|
|
16266
|
+
"Trace Positions Updated",
|
|
16267
|
+
f"Trace positions set from {first_trace_position} m with spacing {spacing} m for traces {first_trace+1} to {last_trace+1} for shots {first_shot+1} to {last_shot+1} (traces reordered)"
|
|
16268
|
+
)
|
|
15380
16269
|
|
|
15381
|
-
|
|
15382
|
-
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
|
|
16270
|
+
# Recalculate unique trace numbers for all files at once
|
|
16271
|
+
self._updateAllUniqueTraceNumbers()
|
|
16272
|
+
self.updateMeanSpacing()
|
|
16273
|
+
self.updatePlots()
|
|
16274
|
+
self.updateFileListDisplay()
|
|
16275
|
+
self.updatePlots()
|
|
16276
|
+
finally:
|
|
16277
|
+
QApplication.restoreOverrideCursor()
|
|
15387
16278
|
|
|
15388
16279
|
def batchSwapTraces(self):
|
|
15389
16280
|
if self.streams:
|
|
@@ -15402,53 +16293,56 @@ class MainWindow(QMainWindow):
|
|
|
15402
16293
|
)
|
|
15403
16294
|
|
|
15404
16295
|
if dialog.exec_():
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
15412
|
-
n_traces = len(self.streams[i])
|
|
15413
|
-
# Validate indices
|
|
15414
|
-
if first_trace <= n_traces and second_trace <= n_traces and first_trace != second_trace:
|
|
15415
|
-
self.streams[i] = swap_traces(self.streams[i], first_trace, second_trace)
|
|
15416
|
-
|
|
15417
|
-
# Swap picks and errors (they follow their traces)
|
|
15418
|
-
first_idx = first_trace - 1
|
|
15419
|
-
second_idx = second_trace - 1
|
|
15420
|
-
if self.picks[i] is not None:
|
|
15421
|
-
self.picks[i][first_idx], self.picks[i][second_idx] = \
|
|
15422
|
-
self.picks[i][second_idx], self.picks[i][first_idx]
|
|
15423
|
-
if self.error[i] is not None:
|
|
15424
|
-
self.error[i][first_idx], self.error[i][second_idx] = \
|
|
15425
|
-
self.error[i][second_idx], self.error[i][first_idx]
|
|
15426
|
-
|
|
15427
|
-
# Swap pick items
|
|
15428
|
-
if self.pickSeismoItems[i] is not None:
|
|
15429
|
-
self.pickSeismoItems[i][first_idx], self.pickSeismoItems[i][second_idx] = \
|
|
15430
|
-
self.pickSeismoItems[i][second_idx], self.pickSeismoItems[i][first_idx]
|
|
15431
|
-
if self.pickLayoutItems[i] is not None:
|
|
15432
|
-
self.pickLayoutItems[i][first_idx], self.pickLayoutItems[i][second_idx] = \
|
|
15433
|
-
self.pickLayoutItems[i][second_idx], self.pickLayoutItems[i][first_idx]
|
|
15434
|
-
|
|
15435
|
-
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
15436
|
-
shot_trace_number = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
15437
|
-
for trace in self.streams[i]]
|
|
15438
|
-
self.shot_trace_number[i] = shot_trace_number
|
|
15439
|
-
|
|
15440
|
-
# Update file_trace_number to be sequential and recalculate unique_trace_number
|
|
15441
|
-
self._updateSequentialTraceNumbers(i)
|
|
15442
|
-
# Sync headers to obspy streams immediately
|
|
15443
|
-
self.syncHeadersToStreams(i)
|
|
15444
|
-
self.headers_modified = True
|
|
15445
|
-
QMessageBox.information(self, "Traces Swapped", f"Traces {first_trace} and {second_trace} swapped for all files")
|
|
16296
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16297
|
+
try:
|
|
16298
|
+
values = dialog.getValues()
|
|
16299
|
+
first_shot = values['First Source #'] - 1
|
|
16300
|
+
last_shot = values['Last Source #'] - 1
|
|
16301
|
+
first_trace = values['First Trace # to swap']
|
|
16302
|
+
second_trace = values['Second Trace # to swap']
|
|
15446
16303
|
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
16304
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16305
|
+
n_traces = len(self.streams[i])
|
|
16306
|
+
# Validate indices
|
|
16307
|
+
if first_trace <= n_traces and second_trace <= n_traces and first_trace != second_trace:
|
|
16308
|
+
self.streams[i] = swap_traces(self.streams[i], first_trace, second_trace)
|
|
16309
|
+
|
|
16310
|
+
# Swap picks and errors (they follow their traces)
|
|
16311
|
+
first_idx = first_trace - 1
|
|
16312
|
+
second_idx = second_trace - 1
|
|
16313
|
+
if self.picks[i] is not None:
|
|
16314
|
+
self.picks[i][first_idx], self.picks[i][second_idx] = \
|
|
16315
|
+
self.picks[i][second_idx], self.picks[i][first_idx]
|
|
16316
|
+
if self.error[i] is not None:
|
|
16317
|
+
self.error[i][first_idx], self.error[i][second_idx] = \
|
|
16318
|
+
self.error[i][second_idx], self.error[i][first_idx]
|
|
16319
|
+
|
|
16320
|
+
# Swap pick items
|
|
16321
|
+
if self.pickSeismoItems[i] is not None:
|
|
16322
|
+
self.pickSeismoItems[i][first_idx], self.pickSeismoItems[i][second_idx] = \
|
|
16323
|
+
self.pickSeismoItems[i][second_idx], self.pickSeismoItems[i][first_idx]
|
|
16324
|
+
if self.pickLayoutItems[i] is not None:
|
|
16325
|
+
self.pickLayoutItems[i][first_idx], self.pickLayoutItems[i][second_idx] = \
|
|
16326
|
+
self.pickLayoutItems[i][second_idx], self.pickLayoutItems[i][first_idx]
|
|
16327
|
+
|
|
16328
|
+
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
16329
|
+
shot_trace_number = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
16330
|
+
for trace in self.streams[i]]
|
|
16331
|
+
self.shot_trace_number[i] = shot_trace_number
|
|
16332
|
+
|
|
16333
|
+
# Update file_trace_number to be sequential and recalculate unique_trace_number
|
|
16334
|
+
self._updateSequentialTraceNumbers(i)
|
|
16335
|
+
# Sync headers to obspy streams immediately
|
|
16336
|
+
self.syncHeadersToStreams(i)
|
|
16337
|
+
self.headers_modified = True
|
|
16338
|
+
|
|
16339
|
+
# Recalculate unique trace numbers for all files at once
|
|
16340
|
+
self._updateAllUniqueTraceNumbers()
|
|
16341
|
+
# Update file list and refresh plots
|
|
16342
|
+
self.updateFileListDisplay()
|
|
16343
|
+
self.updatePlots()
|
|
16344
|
+
finally:
|
|
16345
|
+
QApplication.restoreOverrideCursor()
|
|
15452
16346
|
|
|
15453
16347
|
def batchRemoveTraces(self):
|
|
15454
16348
|
if self.streams:
|
|
@@ -15467,53 +16361,54 @@ class MainWindow(QMainWindow):
|
|
|
15467
16361
|
)
|
|
15468
16362
|
|
|
15469
16363
|
if dialog.exec_():
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
15474
|
-
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
|
|
15491
|
-
|
|
15492
|
-
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
15500
|
-
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
# Update file_trace_number to be sequential after all removals
|
|
15505
|
-
self._updateSequentialTraceNumbers(i)
|
|
15506
|
-
# Sync headers to obspy streams immediately after all removals for this file
|
|
15507
|
-
self.syncHeadersToStreams(i)
|
|
15508
|
-
self.headers_modified = True
|
|
16364
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16365
|
+
try:
|
|
16366
|
+
values = dialog.getValues()
|
|
16367
|
+
first_shot = values['First Source #'] - 1 # Convert to 0-based index
|
|
16368
|
+
last_shot = values['Last Source #'] - 1 # Convert to 0-based index
|
|
16369
|
+
first_trace = values['First Trace # to remove']
|
|
16370
|
+
last_trace = values['Last Trace # to remove']
|
|
16371
|
+
|
|
16372
|
+
# Process shots from first_shot to last_shot (inclusive)
|
|
16373
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16374
|
+
# Process traces in reverse order to avoid index shifting issues
|
|
16375
|
+
# Delete from last to first so indices don't shift for remaining deletions
|
|
16376
|
+
for trace_num in range(last_trace, first_trace-1, -1):
|
|
16377
|
+
# Find the index of this trace number in the current file
|
|
16378
|
+
trace_numbers = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
16379
|
+
for trace in self.streams[i]]
|
|
16380
|
+
# Look for the trace that matches this number
|
|
16381
|
+
found_indices = np.where(np.array(trace_numbers) == trace_num)[0]
|
|
16382
|
+
if found_indices.size == 0:
|
|
16383
|
+
continue # Trace not found, skip
|
|
16384
|
+
|
|
16385
|
+
selected_index = found_indices[0]
|
|
16386
|
+
# Remove the trace from the stream
|
|
16387
|
+
self.streams[i] = remove_trace(self.streams[i], trace_num)
|
|
16388
|
+
# Remove from all associated arrays
|
|
16389
|
+
self.offset[i] = np.delete(self.offset[i], selected_index)
|
|
16390
|
+
self.trace_position[i] = np.delete(self.trace_position[i], selected_index)
|
|
16391
|
+
self.trace_elevation[i] = np.delete(self.trace_elevation[i], selected_index)
|
|
16392
|
+
self.shot_trace_number[i] = np.delete(self.shot_trace_number[i], selected_index)
|
|
16393
|
+
self.file_trace_number[i] = np.delete(self.file_trace_number[i], selected_index)
|
|
16394
|
+
self.picks[i] = np.delete(self.picks[i], selected_index)
|
|
16395
|
+
self.error[i] = np.delete(self.error[i], selected_index)
|
|
16396
|
+
self.pickSeismoItems[i] = np.delete(self.pickSeismoItems[i], selected_index)
|
|
16397
|
+
self.pickLayoutItems[i] = np.delete(self.pickLayoutItems[i], selected_index)
|
|
15509
16398
|
|
|
15510
|
-
|
|
16399
|
+
# Update file_trace_number to be sequential after all removals
|
|
16400
|
+
self._updateSequentialTraceNumbers(i)
|
|
16401
|
+
# Sync headers to obspy streams immediately after all removals for this file
|
|
16402
|
+
self.syncHeadersToStreams(i)
|
|
16403
|
+
self.headers_modified = True
|
|
15511
16404
|
|
|
15512
|
-
|
|
15513
|
-
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
16405
|
+
# Recalculate unique trace numbers for all files at once
|
|
16406
|
+
self._updateAllUniqueTraceNumbers()
|
|
16407
|
+
self.updateMeanSpacing()
|
|
16408
|
+
self.updateFileListDisplay()
|
|
16409
|
+
self.updatePlots()
|
|
16410
|
+
finally:
|
|
16411
|
+
QApplication.restoreOverrideCursor()
|
|
15517
16412
|
|
|
15518
16413
|
def batchMoveTraces(self):
|
|
15519
16414
|
if self.streams:
|
|
@@ -15521,6 +16416,7 @@ class MainWindow(QMainWindow):
|
|
|
15521
16416
|
{'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
|
|
15522
16417
|
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
|
|
15523
16418
|
{'label': 'First Trace # to move', 'initial_value': 1, 'type': 'int'},
|
|
16419
|
+
{'label': 'Last Trace # to move', 'initial_value': 1, 'type': 'int'},
|
|
15524
16420
|
{'label': 'New Position', 'initial_value': 1, 'type': 'int'}
|
|
15525
16421
|
]
|
|
15526
16422
|
|
|
@@ -15532,62 +16428,56 @@ class MainWindow(QMainWindow):
|
|
|
15532
16428
|
)
|
|
15533
16429
|
|
|
15534
16430
|
if dialog.exec_():
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
# Move elements in associated arrays
|
|
15544
|
-
trace_idx = first_trace - 1 # Convert to 0-based indexing
|
|
15545
|
-
if trace_idx < len(self.trace_position[i]) and new_position < len(self.trace_position[i]):
|
|
15546
|
-
# Move elements in associated arrays using numpy operations
|
|
15547
|
-
# Store the elements to move
|
|
15548
|
-
trace_pos = self.trace_position[i][trace_idx]
|
|
15549
|
-
trace_elev = self.trace_elevation[i][trace_idx]
|
|
15550
|
-
offset_val = self.offset[i][trace_idx]
|
|
15551
|
-
shot_trace_num = self.shot_trace_number[i][trace_idx]
|
|
15552
|
-
file_trace_num = self.file_trace_number[i][trace_idx]
|
|
15553
|
-
pick = self.picks[i][trace_idx] if self.picks[i] is not None else None
|
|
15554
|
-
error = self.error[i][trace_idx] if self.error[i] is not None else None
|
|
15555
|
-
pick_seismo = self.pickSeismoItems[i][trace_idx] if self.pickSeismoItems[i] is not None else None
|
|
15556
|
-
pick_layout = self.pickLayoutItems[i][trace_idx] if self.pickLayoutItems[i] is not None else None
|
|
15557
|
-
|
|
15558
|
-
# Remove from current position and insert at new position
|
|
15559
|
-
# NOTE: trace_position stays in place - spatial positions don't move with traces
|
|
15560
|
-
self.trace_elevation[i] = np.insert(np.delete(self.trace_elevation[i], trace_idx), new_position, trace_elev)
|
|
15561
|
-
self.offset[i] = np.insert(np.delete(self.offset[i], trace_idx), new_position, offset_val)
|
|
15562
|
-
self.shot_trace_number[i] = np.insert(np.delete(self.shot_trace_number[i], trace_idx), new_position, shot_trace_num)
|
|
15563
|
-
self.file_trace_number[i] = np.insert(np.delete(self.file_trace_number[i], trace_idx), new_position, file_trace_num)
|
|
15564
|
-
if pick is not None:
|
|
15565
|
-
self.picks[i] = np.insert(np.delete(self.picks[i], trace_idx), new_position, pick)
|
|
15566
|
-
if error is not None:
|
|
15567
|
-
self.error[i] = np.insert(np.delete(self.error[i], trace_idx), new_position, error)
|
|
15568
|
-
if pick_seismo is not None:
|
|
15569
|
-
self.pickSeismoItems[i] = np.insert(np.delete(self.pickSeismoItems[i], trace_idx), new_position, pick_seismo)
|
|
15570
|
-
if pick_layout is not None:
|
|
15571
|
-
self.pickLayoutItems[i] = np.insert(np.delete(self.pickLayoutItems[i], trace_idx), new_position, pick_layout)
|
|
15572
|
-
|
|
15573
|
-
# Rebuild shot_trace_number from updated obspy headers (follows its associated trace)
|
|
15574
|
-
shot_trace_number = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
|
|
15575
|
-
for trace in self.streams[i]]
|
|
15576
|
-
self.shot_trace_number[i] = shot_trace_number
|
|
15577
|
-
|
|
15578
|
-
# Update file_trace_number to be sequential
|
|
15579
|
-
self._updateSequentialTraceNumbers(i)
|
|
15580
|
-
# Sync headers to obspy streams immediately
|
|
15581
|
-
self.syncHeadersToStreams(i)
|
|
15582
|
-
self.headers_modified = True
|
|
16431
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16432
|
+
try:
|
|
16433
|
+
values = dialog.getValues()
|
|
16434
|
+
first_shot = values['First Source #'] - 1
|
|
16435
|
+
last_shot = values['Last Source #'] - 1
|
|
16436
|
+
first_trace = values['First Trace # to move']
|
|
16437
|
+
last_trace = values['Last Trace # to move']
|
|
16438
|
+
new_position = values['New Position'] - 1
|
|
15583
16439
|
|
|
15584
|
-
|
|
16440
|
+
# Validate input range
|
|
16441
|
+
if first_trace > last_trace:
|
|
16442
|
+
QMessageBox.warning(self, "Invalid Range", "First trace must be less than or equal to last trace")
|
|
16443
|
+
return
|
|
16444
|
+
|
|
16445
|
+
first_trace_idx = first_trace - 1
|
|
16446
|
+
last_trace_idx = last_trace - 1
|
|
16447
|
+
num_traces_to_move = last_trace_idx - first_trace_idx + 1
|
|
15585
16448
|
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
16449
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16450
|
+
n_traces_i = len(self.streams[i])
|
|
16451
|
+
if first_trace_idx < n_traces_i and last_trace_idx < n_traces_i and new_position < n_traces_i:
|
|
16452
|
+
# Move traces one by one - determine order to avoid index shifting issues
|
|
16453
|
+
# If moving to earlier position, move from first to last
|
|
16454
|
+
# If moving to later position, move from last to first
|
|
16455
|
+
if new_position < first_trace_idx:
|
|
16456
|
+
# Moving to earlier position - move from first to last
|
|
16457
|
+
for offset in range(num_traces_to_move):
|
|
16458
|
+
current_trace_num = first_trace + offset
|
|
16459
|
+
current_new_pos = new_position + offset
|
|
16460
|
+
self._moveSingleTrace(i, current_trace_num, current_new_pos)
|
|
16461
|
+
else:
|
|
16462
|
+
# Moving to later position - move from last to first
|
|
16463
|
+
for offset in range(num_traces_to_move - 1, -1, -1):
|
|
16464
|
+
current_trace_num = first_trace + offset
|
|
16465
|
+
current_new_pos = new_position + offset
|
|
16466
|
+
self._moveSingleTrace(i, current_trace_num, current_new_pos)
|
|
16467
|
+
|
|
16468
|
+
# Update file_trace_number to be sequential
|
|
16469
|
+
self._updateSequentialTraceNumbers(i)
|
|
16470
|
+
# Sync headers to obspy streams immediately
|
|
16471
|
+
self.syncHeadersToStreams(i)
|
|
16472
|
+
self.headers_modified = True
|
|
16473
|
+
|
|
16474
|
+
# Recalculate unique trace numbers for all files at once
|
|
16475
|
+
self._updateAllUniqueTraceNumbers()
|
|
16476
|
+
self.updatePlots()
|
|
16477
|
+
self.updateFileListDisplay()
|
|
16478
|
+
self.updatePlots()
|
|
16479
|
+
finally:
|
|
16480
|
+
QApplication.restoreOverrideCursor()
|
|
15591
16481
|
|
|
15592
16482
|
def batchMuteTraces(self):
|
|
15593
16483
|
if self.streams:
|
|
@@ -15605,45 +16495,171 @@ class MainWindow(QMainWindow):
|
|
|
15605
16495
|
parent=self
|
|
15606
16496
|
)
|
|
15607
16497
|
|
|
16498
|
+
if dialog.exec_():
|
|
16499
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16500
|
+
try:
|
|
16501
|
+
values = dialog.getValues()
|
|
16502
|
+
first_shot = values['First Source #'] - 1
|
|
16503
|
+
last_shot = values['Last Source #'] - 1
|
|
16504
|
+
first_trace = values['First Trace # to mute']
|
|
16505
|
+
last_trace = values['Last Trace # to mute']
|
|
16506
|
+
|
|
16507
|
+
# Adjust last_trace for loop (inclusive range)
|
|
16508
|
+
last_trace_loop = last_trace + 1
|
|
16509
|
+
|
|
16510
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16511
|
+
# Process traces in reverse order for consistency, though muting doesn't shift indices
|
|
16512
|
+
for j in range(last_trace_loop-1, first_trace-1, -1):
|
|
16513
|
+
trace_numbers = self.shot_trace_number[i]
|
|
16514
|
+
selected_indices = np.where(np.array(trace_numbers) == j)[0]
|
|
16515
|
+
if selected_indices.size == 0:
|
|
16516
|
+
QMessageBox.information(self, "Trace Not Found", f"Trace #{j} not found in file {os.path.basename(self.fileNames[i])}")
|
|
16517
|
+
continue
|
|
16518
|
+
|
|
16519
|
+
# Mute the trace for the current file
|
|
16520
|
+
self.streams[i] = mute_trace(self.streams[i], j)
|
|
16521
|
+
# Sync headers to obspy streams immediately after all mutes for this file
|
|
16522
|
+
self.syncHeadersToStreams(i)
|
|
16523
|
+
|
|
16524
|
+
# Mark headers/traces as modified so the unsaved-changes warning appears
|
|
16525
|
+
self.headers_modified = True
|
|
16526
|
+
|
|
16527
|
+
self.updatePlots()
|
|
16528
|
+
self.updateFileListDisplay()
|
|
16529
|
+
self.updatePlots()
|
|
16530
|
+
finally:
|
|
16531
|
+
QApplication.restoreOverrideCursor()
|
|
16532
|
+
|
|
16533
|
+
def batchInsertMutedTraces(self):
|
|
16534
|
+
"""Batch insert zero traces for a range of shots."""
|
|
16535
|
+
if self.streams:
|
|
16536
|
+
n_traces = len(self.trace_position[self.currentIndex]) if self.currentIndex < len(self.trace_position) else 1
|
|
16537
|
+
|
|
16538
|
+
parameters = [
|
|
16539
|
+
{'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
|
|
16540
|
+
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
|
|
16541
|
+
{'label': 'Insert after Trace # (0 for beginning)', 'initial_value': n_traces, 'type': 'int'},
|
|
16542
|
+
{'label': 'Number of traces to insert', 'initial_value': 1, 'type': 'int'}
|
|
16543
|
+
]
|
|
16544
|
+
|
|
16545
|
+
dialog = GenericParameterDialog(
|
|
16546
|
+
title="Batch Insert Zero Traces",
|
|
16547
|
+
parameters=parameters,
|
|
16548
|
+
add_checkbox=False,
|
|
16549
|
+
parent=self
|
|
16550
|
+
)
|
|
16551
|
+
|
|
15608
16552
|
if dialog.exec_():
|
|
15609
16553
|
values = dialog.getValues()
|
|
15610
16554
|
first_shot = values['First Source #'] - 1
|
|
15611
|
-
last_shot = values['Last Source #']
|
|
15612
|
-
|
|
15613
|
-
|
|
16555
|
+
last_shot = values['Last Source #'] - 1
|
|
16556
|
+
insert_after = values['Insert after Trace # (0 for beginning)']
|
|
16557
|
+
num_to_insert = values['Number of traces to insert']
|
|
15614
16558
|
|
|
15615
|
-
#
|
|
15616
|
-
|
|
15617
|
-
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15622
|
-
|
|
15623
|
-
|
|
15624
|
-
|
|
15625
|
-
|
|
16559
|
+
# Validate inputs
|
|
16560
|
+
if num_to_insert < 1:
|
|
16561
|
+
QMessageBox.warning(self, "Invalid Input", "Number of traces must be at least 1")
|
|
16562
|
+
return
|
|
16563
|
+
|
|
16564
|
+
# Show wait cursor while processing
|
|
16565
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16566
|
+
try:
|
|
16567
|
+
# Insert traces for specified range of shots
|
|
16568
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16569
|
+
n_traces_i = len(self.streams[i])
|
|
16570
|
+
if insert_after > n_traces_i:
|
|
16571
|
+
continue # Skip if position is beyond trace count
|
|
16572
|
+
|
|
16573
|
+
self._insertMutedTracesInFile(i, insert_after, num_to_insert)
|
|
16574
|
+
# Sync headers to obspy streams immediately
|
|
16575
|
+
self.syncHeadersToStreams(i)
|
|
16576
|
+
|
|
16577
|
+
self.headers_modified = True
|
|
16578
|
+
|
|
16579
|
+
# Recalculate unique trace numbers for all files at once
|
|
16580
|
+
self._updateAllUniqueTraceNumbers()
|
|
16581
|
+
self.updateMeanSpacing()
|
|
16582
|
+
self.updatePlotTypeDict()
|
|
16583
|
+
self.updatePlots()
|
|
16584
|
+
self.updateFileListDisplay()
|
|
16585
|
+
self.updatePlots()
|
|
16586
|
+
finally:
|
|
16587
|
+
# Always restore cursor even if there's an error
|
|
16588
|
+
QApplication.restoreOverrideCursor()
|
|
15626
16589
|
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
16590
|
+
def batchZeroPadTraces(self):
|
|
16591
|
+
"""Batch zero pad all traces for a range of shots."""
|
|
16592
|
+
if self.streams:
|
|
16593
|
+
parameters = [
|
|
16594
|
+
{'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
|
|
16595
|
+
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
|
|
16596
|
+
{'label': 'Pad at end (seconds)', 'initial_value': 0.0, 'type': 'float'}
|
|
16597
|
+
]
|
|
16598
|
+
|
|
16599
|
+
dialog = GenericParameterDialog(
|
|
16600
|
+
title="Batch Zero Pad Shots",
|
|
16601
|
+
parameters=parameters,
|
|
16602
|
+
add_checkbox=False,
|
|
16603
|
+
parent=self
|
|
16604
|
+
)
|
|
16605
|
+
|
|
16606
|
+
if dialog.exec_():
|
|
16607
|
+
values = dialog.getValues()
|
|
16608
|
+
first_shot = values['First Source #'] - 1
|
|
16609
|
+
last_shot = values['Last Source #'] - 1
|
|
16610
|
+
pad_end_seconds = values['Pad at end (seconds)']
|
|
15631
16611
|
|
|
15632
|
-
#
|
|
15633
|
-
|
|
16612
|
+
# Validate inputs
|
|
16613
|
+
if pad_end_seconds < 0:
|
|
16614
|
+
QMessageBox.warning(self, "Invalid Input", "Padding value must be non-negative")
|
|
16615
|
+
return
|
|
15634
16616
|
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
|
|
16617
|
+
if pad_end_seconds == 0:
|
|
16618
|
+
QMessageBox.warning(self, "Invalid Input", "Padding value must be greater than 0")
|
|
16619
|
+
return
|
|
16620
|
+
|
|
16621
|
+
# Show wait cursor while processing
|
|
16622
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16623
|
+
try:
|
|
16624
|
+
# Pad all traces for specified range of shots
|
|
16625
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16626
|
+
# Convert seconds to samples for this file
|
|
16627
|
+
pad_end = int(pad_end_seconds / self.sample_interval[i])
|
|
16628
|
+
|
|
16629
|
+
# Process all traces in this shot
|
|
16630
|
+
for trace_num in self.shot_trace_number[i]:
|
|
16631
|
+
self.streams[i] = zero_pad_trace(self.streams[i], trace_num, 0, pad_end)
|
|
16632
|
+
|
|
16633
|
+
# Update n_sample and time array after padding
|
|
16634
|
+
if len(self.streams[i]) > 0:
|
|
16635
|
+
self.n_sample[i] = len(self.streams[i][0].data)
|
|
16636
|
+
self.time[i] = np.arange(self.n_sample[i]) * self.sample_interval[i] + self.delay[i]
|
|
16637
|
+
# Sync headers to obspy streams immediately after all padding for this file
|
|
16638
|
+
self.syncHeadersToStreams(i)
|
|
16639
|
+
|
|
16640
|
+
# Mark headers/traces as modified so the unsaved-changes warning appears
|
|
16641
|
+
self.headers_modified = True
|
|
16642
|
+
|
|
16643
|
+
QMessageBox.information(self, "Zero Padding Applied",
|
|
16644
|
+
f"All traces padded with {pad_end_seconds} seconds at end for shots {first_shot+1} to {last_shot+1}")
|
|
16645
|
+
|
|
16646
|
+
self.updatePlots()
|
|
16647
|
+
self.updateFileListDisplay()
|
|
16648
|
+
self.updatePlots()
|
|
16649
|
+
finally:
|
|
16650
|
+
# Always restore cursor even if there's an error
|
|
16651
|
+
QApplication.restoreOverrideCursor()
|
|
15640
16652
|
|
|
15641
16653
|
def batchReverseTraces(self):
|
|
15642
16654
|
"""Batch reverse trace data (flip data matrix left-to-right) while keeping headers in place"""
|
|
15643
16655
|
if self.streams:
|
|
16656
|
+
n_traces = len(self.trace_position[self.currentIndex]) if self.currentIndex < len(self.trace_position) else 1
|
|
16657
|
+
|
|
15644
16658
|
parameters = [
|
|
15645
16659
|
{'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
|
|
15646
|
-
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'}
|
|
16660
|
+
{'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
|
|
16661
|
+
{'label': 'First Trace #', 'initial_value': 1, 'type': 'int'},
|
|
16662
|
+
{'label': 'Last Trace #', 'initial_value': n_traces, 'type': 'int'}
|
|
15647
16663
|
]
|
|
15648
16664
|
|
|
15649
16665
|
dialog = GenericParameterDialog(
|
|
@@ -15654,35 +16670,36 @@ class MainWindow(QMainWindow):
|
|
|
15654
16670
|
)
|
|
15655
16671
|
|
|
15656
16672
|
if dialog.exec_():
|
|
15657
|
-
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
# Reverse shot_trace_number to match the flipped trace numbers in headers
|
|
15666
|
-
self.shot_trace_number[i] = self.shot_trace_number[i][::-1]
|
|
16673
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
16674
|
+
try:
|
|
16675
|
+
values = dialog.getValues()
|
|
16676
|
+
first_shot = values['First Source #'] - 1
|
|
16677
|
+
last_shot = values['Last Source #'] - 1
|
|
16678
|
+
first_trace = values['First Trace #'] - 1 # Convert to 0-based
|
|
16679
|
+
last_trace = values['Last Trace #'] - 1 # Convert to 0-based
|
|
15667
16680
|
|
|
15668
|
-
#
|
|
15669
|
-
if
|
|
15670
|
-
self
|
|
15671
|
-
|
|
15672
|
-
|
|
16681
|
+
# Validate input range
|
|
16682
|
+
if first_trace > last_trace:
|
|
16683
|
+
QMessageBox.warning(self, "Invalid Range", "First trace must be less than or equal to last trace")
|
|
16684
|
+
return
|
|
16685
|
+
|
|
16686
|
+
for i in range(first_shot, min(last_shot + 1, len(self.streams))):
|
|
16687
|
+
n_traces_i = len(self.streams[i])
|
|
16688
|
+
# Validate range for this file
|
|
16689
|
+
if first_trace >= n_traces_i or last_trace >= n_traces_i:
|
|
16690
|
+
continue # Skip files that don't have enough traces
|
|
16691
|
+
|
|
16692
|
+
self._reverseTraceSubset(i, first_trace, last_trace)
|
|
15673
16693
|
|
|
15674
|
-
|
|
15675
|
-
if
|
|
15676
|
-
|
|
15677
|
-
if self.pickLayoutItems[i] is not None:
|
|
15678
|
-
self.pickLayoutItems[i] = self.pickLayoutItems[i][::-1]
|
|
15679
|
-
|
|
15680
|
-
self.headers_modified = True
|
|
15681
|
-
QMessageBox.information(self, "Trace Data Reversed", f"Trace data reversed for shots {first_shot+1} to {last_shot} (headers unchanged)")
|
|
16694
|
+
self.headers_modified = True
|
|
16695
|
+
trace_range = f"Traces #{first_trace+1}-{last_trace+1}" if first_trace != last_trace else f"Trace #{first_trace+1}"
|
|
16696
|
+
QMessageBox.information(self, "Trace Data Reversed", f"{trace_range} reversed for shots {first_shot+1} to {last_shot+1} (headers unchanged)")
|
|
15682
16697
|
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
|
|
16698
|
+
self.updatePlots()
|
|
16699
|
+
self.updateFileListDisplay()
|
|
16700
|
+
self.updatePlots()
|
|
16701
|
+
finally:
|
|
16702
|
+
QApplication.restoreOverrideCursor()
|
|
15686
16703
|
|
|
15687
16704
|
def stackShots(self):
|
|
15688
16705
|
"""
|
|
@@ -16837,123 +17854,143 @@ class MainWindow(QMainWindow):
|
|
|
16837
17854
|
# Top widget plot functions - mirror bottom plot functions but use self.plotWidget instead
|
|
16838
17855
|
def plotLayoutInTopWidget(self):
|
|
16839
17856
|
"""Plot layout view in top widget"""
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
|
|
16848
|
-
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
|
|
16855
|
-
|
|
16856
|
-
|
|
16857
|
-
|
|
16858
|
-
|
|
16859
|
-
|
|
16860
|
-
|
|
17857
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
17858
|
+
try:
|
|
17859
|
+
self.topPlotType = 'layout'
|
|
17860
|
+
self.plotWidget.clear()
|
|
17861
|
+
# Re-add crosshair lines after clear to the viewbox so they stay above images
|
|
17862
|
+
self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
|
|
17863
|
+
self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
|
|
17864
|
+
self.crosshair_vline.setZValue(1000)
|
|
17865
|
+
self.crosshair_hline.setZValue(1000)
|
|
17866
|
+
# Set visibility based on picking_show_crosshair and plot type
|
|
17867
|
+
show_crosshair = (
|
|
17868
|
+
getattr(self, 'picking_show_crosshair', False)
|
|
17869
|
+
and self.topPlotType in ('seismogram', 'dispersion')
|
|
17870
|
+
)
|
|
17871
|
+
self.crosshair_vline.setVisible(show_crosshair)
|
|
17872
|
+
self.crosshair_hline.setVisible(show_crosshair)
|
|
17873
|
+
# Set target widget for plotLayout to use top widget
|
|
17874
|
+
self._plot_target_widget = self.plotWidget
|
|
17875
|
+
self._plot_target_viewbox = self.viewBox
|
|
17876
|
+
self.plotLayout()
|
|
17877
|
+
# Reset target widget after plotting
|
|
17878
|
+
self._plot_target_widget = None
|
|
17879
|
+
self._plot_target_viewbox = None
|
|
17880
|
+
finally:
|
|
17881
|
+
QApplication.restoreOverrideCursor()
|
|
16861
17882
|
|
|
16862
17883
|
def plotTravelTimeInTopWidget(self):
|
|
16863
17884
|
"""Plot travel times view in top widget"""
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
17885
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
17886
|
+
try:
|
|
17887
|
+
self.topPlotType = 'traveltime'
|
|
17888
|
+
self.plotWidget.clear()
|
|
17889
|
+
# Re-add crosshair lines after clear to the viewbox so they stay above images
|
|
17890
|
+
self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
|
|
17891
|
+
self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
|
|
17892
|
+
self.crosshair_vline.setZValue(1000)
|
|
17893
|
+
self.crosshair_hline.setZValue(1000)
|
|
17894
|
+
# Set visibility based on picking_show_crosshair and plot type
|
|
17895
|
+
show_crosshair = (
|
|
17896
|
+
getattr(self, 'picking_show_crosshair', False)
|
|
17897
|
+
and self.topPlotType in ('seismogram', 'dispersion')
|
|
17898
|
+
)
|
|
17899
|
+
self.crosshair_vline.setVisible(show_crosshair)
|
|
17900
|
+
self.crosshair_hline.setVisible(show_crosshair)
|
|
17901
|
+
# Set target widget for plotTravelTime to use top widget
|
|
17902
|
+
self._plot_target_widget = self.plotWidget
|
|
17903
|
+
self._plot_target_viewbox = self.viewBox
|
|
17904
|
+
self.plotTravelTime()
|
|
17905
|
+
# Reset target widget after plotting
|
|
17906
|
+
self._plot_target_widget = None
|
|
17907
|
+
self._plot_target_viewbox = None
|
|
17908
|
+
finally:
|
|
17909
|
+
QApplication.restoreOverrideCursor()
|
|
16885
17910
|
|
|
16886
17911
|
def plotTopoInTopWidget(self):
|
|
16887
17912
|
"""Plot topography view in top widget"""
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
16895
|
-
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
|
|
16900
|
-
|
|
16901
|
-
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
|
|
16908
|
-
|
|
17913
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
17914
|
+
try:
|
|
17915
|
+
self.topPlotType = 'topo'
|
|
17916
|
+
self.plotWidget.clear()
|
|
17917
|
+
# Re-add crosshair lines after clear to the viewbox so they stay above images
|
|
17918
|
+
self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
|
|
17919
|
+
self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
|
|
17920
|
+
self.crosshair_vline.setZValue(1000)
|
|
17921
|
+
self.crosshair_hline.setZValue(1000)
|
|
17922
|
+
# Set visibility based on picking_show_crosshair and plot type
|
|
17923
|
+
show_crosshair = (
|
|
17924
|
+
getattr(self, 'picking_show_crosshair', False)
|
|
17925
|
+
and self.topPlotType in ('seismogram', 'dispersion')
|
|
17926
|
+
)
|
|
17927
|
+
self.crosshair_vline.setVisible(show_crosshair)
|
|
17928
|
+
self.crosshair_hline.setVisible(show_crosshair)
|
|
17929
|
+
# Set target widget for plotTopo to use top widget
|
|
17930
|
+
self._plot_target_widget = self.plotWidget
|
|
17931
|
+
self._plot_target_viewbox = self.viewBox
|
|
17932
|
+
self.plotTopo()
|
|
17933
|
+
# Reset target widget after plotting
|
|
17934
|
+
self._plot_target_widget = None
|
|
17935
|
+
self._plot_target_viewbox = None
|
|
17936
|
+
finally:
|
|
17937
|
+
QApplication.restoreOverrideCursor()
|
|
16909
17938
|
|
|
16910
17939
|
def plotSpectrogramInTopWidget(self):
|
|
16911
17940
|
"""Plot spectrogram view in top widget"""
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
16920
|
-
|
|
16921
|
-
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
|
|
16927
|
-
|
|
16928
|
-
|
|
16929
|
-
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
17941
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
17942
|
+
try:
|
|
17943
|
+
self.topPlotType = 'spectrogram'
|
|
17944
|
+
self.plotWidget.clear()
|
|
17945
|
+
# Re-add crosshair lines after clear to the viewbox so they stay above images
|
|
17946
|
+
self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
|
|
17947
|
+
self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
|
|
17948
|
+
self.crosshair_vline.setZValue(1000)
|
|
17949
|
+
self.crosshair_hline.setZValue(1000)
|
|
17950
|
+
# Set visibility based on picking_show_crosshair and plot type
|
|
17951
|
+
show_crosshair = (
|
|
17952
|
+
getattr(self, 'picking_show_crosshair', False)
|
|
17953
|
+
and self.topPlotType in ('seismogram', 'dispersion')
|
|
17954
|
+
)
|
|
17955
|
+
self.crosshair_vline.setVisible(show_crosshair)
|
|
17956
|
+
self.crosshair_hline.setVisible(show_crosshair)
|
|
17957
|
+
# Set target widget for plotSpectrogram to use top widget
|
|
17958
|
+
self._plot_target_widget = self.plotWidget
|
|
17959
|
+
self._plot_target_viewbox = self.viewBox
|
|
17960
|
+
self.plotSpectrogram()
|
|
17961
|
+
# Reset target widget after plotting
|
|
17962
|
+
self._plot_target_widget = None
|
|
17963
|
+
self._plot_target_viewbox = None
|
|
17964
|
+
finally:
|
|
17965
|
+
QApplication.restoreOverrideCursor()
|
|
16933
17966
|
|
|
16934
17967
|
def plotDispersionInTopWidget(self):
|
|
16935
17968
|
"""Plot dispersion/phase-shift view in top widget"""
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
16951
|
-
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
17969
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
17970
|
+
try:
|
|
17971
|
+
self.topPlotType = 'dispersion'
|
|
17972
|
+
self.plotWidget.clear()
|
|
17973
|
+
# Re-add crosshair lines after clear to the viewbox so they stay above images
|
|
17974
|
+
self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
|
|
17975
|
+
self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
|
|
17976
|
+
self.crosshair_vline.setZValue(1000)
|
|
17977
|
+
self.crosshair_hline.setZValue(1000)
|
|
17978
|
+
# Set visibility based on picking_show_crosshair and plot type
|
|
17979
|
+
show_crosshair = (
|
|
17980
|
+
getattr(self, 'picking_show_crosshair', False)
|
|
17981
|
+
and self.topPlotType in ('seismogram', 'dispersion')
|
|
17982
|
+
)
|
|
17983
|
+
self.crosshair_vline.setVisible(show_crosshair)
|
|
17984
|
+
self.crosshair_hline.setVisible(show_crosshair)
|
|
17985
|
+
# Set target widget for plotDispersion to use top widget
|
|
17986
|
+
self._plot_target_widget = self.plotWidget
|
|
17987
|
+
self._plot_target_viewbox = self.viewBox
|
|
17988
|
+
self.plotDispersion()
|
|
17989
|
+
# Reset target widget after plotting
|
|
17990
|
+
self._plot_target_widget = None
|
|
17991
|
+
self._plot_target_viewbox = None
|
|
17992
|
+
finally:
|
|
17993
|
+
QApplication.restoreOverrideCursor()
|
|
16957
17994
|
|
|
16958
17995
|
def setTracePositionPlot(self):
|
|
16959
17996
|
self.plotTypeX = 'trace_position'
|
|
@@ -19735,139 +20772,159 @@ class MainWindow(QMainWindow):
|
|
|
19735
20772
|
|
|
19736
20773
|
def onPseudoSectionFreqLambdaToggled(self, state):
|
|
19737
20774
|
"""Toggle between frequency and wavelength display"""
|
|
19738
|
-
|
|
19739
|
-
|
|
19740
|
-
|
|
19741
|
-
|
|
19742
|
-
self
|
|
19743
|
-
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
|
|
19747
|
-
|
|
19748
|
-
self
|
|
19749
|
-
|
|
19750
|
-
|
|
19751
|
-
|
|
20775
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
20776
|
+
try:
|
|
20777
|
+
self.pseudosection_use_wavelength = bool(state)
|
|
20778
|
+
# Replot whichever view is showing pseudo-section
|
|
20779
|
+
if getattr(self, 'bottomPlotType', None) == "pseudosection":
|
|
20780
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
20781
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
20782
|
+
self.plotPseudoSection()
|
|
20783
|
+
self._plot_target_widget = None
|
|
20784
|
+
self._plot_target_viewbox = None
|
|
20785
|
+
if getattr(self, 'topPlotType', None) == "pseudosection":
|
|
20786
|
+
self._plot_target_widget = self.plotWidget
|
|
20787
|
+
self._plot_target_viewbox = self.viewBox
|
|
20788
|
+
self.plotPseudoSection()
|
|
20789
|
+
self._plot_target_widget = None
|
|
20790
|
+
self._plot_target_viewbox = None
|
|
20791
|
+
finally:
|
|
20792
|
+
QApplication.restoreOverrideCursor()
|
|
19752
20793
|
|
|
19753
20794
|
def onPseudoSectionInterpToggled(self, state):
|
|
19754
20795
|
"""Toggle between picked and interpolated curve display in pseudo-section"""
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
if hasattr(self, 'dispersionLiveInterpolateCheckbox'):
|
|
19760
|
-
self.dispersionLiveInterpolateCheckbox.blockSignals(True)
|
|
19761
|
-
self.dispersionLiveInterpolateCheckbox.setChecked(is_checked)
|
|
19762
|
-
self.dispersionLiveInterpolateCheckbox.blockSignals(False)
|
|
19763
|
-
|
|
19764
|
-
if is_checked:
|
|
19765
|
-
# ENABLE INTERPOLATION: Compute for all shots and both views
|
|
19766
|
-
|
|
19767
|
-
# 1. Interpolate all shots for pseudo-section display
|
|
19768
|
-
self._interpolateAllShotsForPseudoSection()
|
|
19769
|
-
|
|
19770
|
-
# 2. Interpolate current dispersion view for all modes
|
|
19771
|
-
view = self._get_active_dispersion_view()
|
|
19772
|
-
if view:
|
|
19773
|
-
if self.dispersionShowAllModesCheckbox.isChecked():
|
|
19774
|
-
# Interpolate all modes
|
|
19775
|
-
self.interpolateAllDispersionCurves(view)
|
|
19776
|
-
else:
|
|
19777
|
-
# Interpolate only current mode
|
|
19778
|
-
mode_data = self._get_current_mode_data(view)
|
|
19779
|
-
if len(mode_data['picked_points']) >= 2:
|
|
19780
|
-
self.interpolateDispersionCurve(view)
|
|
20796
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
20797
|
+
try:
|
|
20798
|
+
is_checked = bool(state)
|
|
20799
|
+
self.pseudosection_show_interpolated = is_checked
|
|
19781
20800
|
|
|
19782
|
-
#
|
|
19783
|
-
self
|
|
19784
|
-
|
|
19785
|
-
|
|
20801
|
+
# Synchronize with dispersion view interpolate checkbox (block signals to avoid recursion)
|
|
20802
|
+
if hasattr(self, 'dispersionLiveInterpolateCheckbox'):
|
|
20803
|
+
self.dispersionLiveInterpolateCheckbox.blockSignals(True)
|
|
20804
|
+
self.dispersionLiveInterpolateCheckbox.setChecked(is_checked)
|
|
20805
|
+
self.dispersionLiveInterpolateCheckbox.blockSignals(False)
|
|
19786
20806
|
|
|
19787
|
-
|
|
19788
|
-
|
|
19789
|
-
if view:
|
|
19790
|
-
plot_widget = self.plotWidget if view == 'top' else self.bottomPlotWidget
|
|
19791
|
-
state_dict = self.dispersion_picking_state[view]
|
|
20807
|
+
if is_checked:
|
|
20808
|
+
# ENABLE INTERPOLATION: Compute for all shots and both views
|
|
19792
20809
|
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
20810
|
+
# 1. Interpolate all shots for pseudo-section display
|
|
20811
|
+
self._interpolateAllShotsForPseudoSection()
|
|
20812
|
+
|
|
20813
|
+
# 2. Interpolate current dispersion view for all modes
|
|
20814
|
+
view = self._get_active_dispersion_view()
|
|
20815
|
+
if view:
|
|
20816
|
+
if self.dispersionShowAllModesCheckbox.isChecked():
|
|
20817
|
+
# Interpolate all modes
|
|
20818
|
+
self.interpolateAllDispersionCurves(view)
|
|
20819
|
+
else:
|
|
20820
|
+
# Interpolate only current mode
|
|
20821
|
+
mode_data = self._get_current_mode_data(view)
|
|
20822
|
+
if len(mode_data['picked_points']) >= 2:
|
|
20823
|
+
self.interpolateDispersionCurve(view)
|
|
20824
|
+
|
|
20825
|
+
# 3. Update pseudo-section display
|
|
20826
|
+
self._updatePseudoSectionIfVisible()
|
|
20827
|
+
else:
|
|
20828
|
+
# DISABLE INTERPOLATION: Remove curves and errors from both views
|
|
20829
|
+
|
|
20830
|
+
# 1. Remove from dispersion view
|
|
20831
|
+
view = self._get_active_dispersion_view()
|
|
20832
|
+
if view:
|
|
20833
|
+
plot_widget = self.plotWidget if view == 'top' else self.bottomPlotWidget
|
|
20834
|
+
state_dict = self.dispersion_picking_state[view]
|
|
19801
20835
|
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
20836
|
+
for mode_num, mode_data in state_dict['modes'].items():
|
|
20837
|
+
# Remove interpolated curve
|
|
20838
|
+
if mode_data['curve_line'] is not None:
|
|
20839
|
+
try:
|
|
20840
|
+
plot_widget.removeItem(mode_data['curve_line'])
|
|
20841
|
+
except:
|
|
20842
|
+
pass
|
|
20843
|
+
mode_data['curve_line'] = None
|
|
20844
|
+
|
|
20845
|
+
# Remove interpolated error fill (can be single item or list)
|
|
20846
|
+
if mode_data['interp_error_fill'] is not None:
|
|
20847
|
+
try:
|
|
20848
|
+
items = mode_data['interp_error_fill'] if isinstance(mode_data['interp_error_fill'], list) else [mode_data['interp_error_fill']]
|
|
20849
|
+
for item in items:
|
|
20850
|
+
if item is not None:
|
|
20851
|
+
plot_widget.removeItem(item)
|
|
20852
|
+
except:
|
|
20853
|
+
pass
|
|
20854
|
+
mode_data['interp_error_fill'] = None
|
|
20855
|
+
|
|
20856
|
+
# Clear curve_data but keep picked_points
|
|
20857
|
+
mode_data['curve_data'] = None
|
|
19812
20858
|
|
|
19813
|
-
#
|
|
19814
|
-
|
|
20859
|
+
# Refresh to update display (will redraw picked errors if Show Errors is checked)
|
|
20860
|
+
self._refreshDispersionPicksDisplay()
|
|
19815
20861
|
|
|
19816
|
-
#
|
|
19817
|
-
self.
|
|
19818
|
-
|
|
19819
|
-
|
|
19820
|
-
self._updatePseudoSectionIfVisible()
|
|
20862
|
+
# 2. Update pseudo-section display (will revert to picked points)
|
|
20863
|
+
self._updatePseudoSectionIfVisible()
|
|
20864
|
+
finally:
|
|
20865
|
+
QApplication.restoreOverrideCursor()
|
|
19821
20866
|
|
|
19822
20867
|
def onPseudoSectionInvertYToggled(self, state):
|
|
19823
20868
|
"""Toggle Y-axis inversion for pseudo-section"""
|
|
19824
|
-
|
|
19825
|
-
|
|
19826
|
-
|
|
19827
|
-
|
|
19828
|
-
self
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
self
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
20869
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
20870
|
+
try:
|
|
20871
|
+
self.pseudosection_invert_y = bool(state)
|
|
20872
|
+
# Replot whichever view is showing pseudo-section
|
|
20873
|
+
if getattr(self, 'bottomPlotType', None) == "pseudosection":
|
|
20874
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
20875
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
20876
|
+
self.plotPseudoSection()
|
|
20877
|
+
self._plot_target_widget = None
|
|
20878
|
+
self._plot_target_viewbox = None
|
|
20879
|
+
if getattr(self, 'topPlotType', None) == "pseudosection":
|
|
20880
|
+
self._plot_target_widget = self.plotWidget
|
|
20881
|
+
self._plot_target_viewbox = self.viewBox
|
|
20882
|
+
self.plotPseudoSection()
|
|
20883
|
+
self._plot_target_widget = None
|
|
20884
|
+
self._plot_target_viewbox = None
|
|
20885
|
+
finally:
|
|
20886
|
+
QApplication.restoreOverrideCursor()
|
|
19838
20887
|
|
|
19839
20888
|
def onPseudoSectionModeChanged(self, value):
|
|
19840
20889
|
"""Handle pseudo-section mode spinbox change"""
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19844
|
-
|
|
19845
|
-
self
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
self
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
20890
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
20891
|
+
try:
|
|
20892
|
+
self.pseudosection_mode = int(value)
|
|
20893
|
+
# Replot whichever view is showing pseudo-section
|
|
20894
|
+
if getattr(self, 'bottomPlotType', None) == "pseudosection":
|
|
20895
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
20896
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
20897
|
+
self.plotPseudoSection()
|
|
20898
|
+
self._plot_target_widget = None
|
|
20899
|
+
self._plot_target_viewbox = None
|
|
20900
|
+
if getattr(self, 'topPlotType', None) == "pseudosection":
|
|
20901
|
+
self._plot_target_widget = self.plotWidget
|
|
20902
|
+
self._plot_target_viewbox = self.viewBox
|
|
20903
|
+
self.plotPseudoSection()
|
|
20904
|
+
self._plot_target_widget = None
|
|
20905
|
+
self._plot_target_viewbox = None
|
|
20906
|
+
finally:
|
|
20907
|
+
QApplication.restoreOverrideCursor()
|
|
19855
20908
|
|
|
19856
20909
|
def onPseudoSectionUseSourceXToggled(self, state):
|
|
19857
20910
|
"""Toggle use of source position/FFID for pseudo-section X-axis"""
|
|
19858
|
-
|
|
19859
|
-
|
|
19860
|
-
self.
|
|
19861
|
-
self
|
|
19862
|
-
|
|
19863
|
-
|
|
19864
|
-
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
self
|
|
19868
|
-
|
|
19869
|
-
|
|
19870
|
-
|
|
20911
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
20912
|
+
try:
|
|
20913
|
+
self.pseudosection_use_source_x = bool(state)
|
|
20914
|
+
if getattr(self, 'bottomPlotType', None) == "pseudosection":
|
|
20915
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
20916
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
20917
|
+
self.plotPseudoSection()
|
|
20918
|
+
self._plot_target_widget = None
|
|
20919
|
+
self._plot_target_viewbox = None
|
|
20920
|
+
if getattr(self, 'topPlotType', None) == "pseudosection":
|
|
20921
|
+
self._plot_target_widget = self.plotWidget
|
|
20922
|
+
self._plot_target_viewbox = self.viewBox
|
|
20923
|
+
self.plotPseudoSection()
|
|
20924
|
+
self._plot_target_widget = None
|
|
20925
|
+
self._plot_target_viewbox = None
|
|
20926
|
+
finally:
|
|
20927
|
+
QApplication.restoreOverrideCursor()
|
|
19871
20928
|
|
|
19872
20929
|
def _interpolateAllShotsForPseudoSection(self):
|
|
19873
20930
|
"""Legacy function - now uses unified interpolation.
|
|
@@ -19978,23 +21035,27 @@ class MainWindow(QMainWindow):
|
|
|
19978
21035
|
|
|
19979
21036
|
# Only update display if click actually changed currentIndex
|
|
19980
21037
|
if self.streams and self.currentIndex is not None:
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
21038
|
+
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
21039
|
+
try:
|
|
21040
|
+
# Restore trace ranges for the new shot from memory
|
|
21041
|
+
self._restoreTraceRangesFromMemory()
|
|
21042
|
+
|
|
21043
|
+
# Set flag to update file display
|
|
21044
|
+
self.update_file_flag = True
|
|
19986
21045
|
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
21046
|
+
# Temporarily disconnect the signal to prevent conflicts
|
|
21047
|
+
self.fileListWidget.itemSelectionChanged.disconnect(self.onFileSelectionChanged)
|
|
21048
|
+
|
|
21049
|
+
# Update file list display to sync the selection
|
|
21050
|
+
self.updateFileListDisplay()
|
|
21051
|
+
|
|
21052
|
+
# Reconnect the signal
|
|
21053
|
+
self.fileListWidget.itemSelectionChanged.connect(self.onFileSelectionChanged)
|
|
19995
21054
|
|
|
19996
|
-
|
|
19997
|
-
|
|
21055
|
+
# Batch the updates to improve performance
|
|
21056
|
+
self.updatePlots()
|
|
21057
|
+
finally:
|
|
21058
|
+
QApplication.restoreOverrideCursor()
|
|
19998
21059
|
|
|
19999
21060
|
def _layoutViewClick(self, event, viewbox):
|
|
20000
21061
|
"""
|
|
@@ -21265,8 +22326,191 @@ Failed to match: {skipped_no_trace_found}"""
|
|
|
21265
22326
|
|
|
21266
22327
|
return picks_matched, len(sources_with_picks), max_pick_time
|
|
21267
22328
|
|
|
22329
|
+
def _loadPicksFromLST(self, lst_file, verbose=False):
|
|
22330
|
+
"""
|
|
22331
|
+
Load picks from a Rayfract .LST file.
|
|
22332
|
+
|
|
22333
|
+
Parameters
|
|
22334
|
+
----------
|
|
22335
|
+
lst_file : str
|
|
22336
|
+
Path to .LST file
|
|
22337
|
+
verbose : bool
|
|
22338
|
+
Print debug information
|
|
22339
|
+
|
|
22340
|
+
Returns
|
|
22341
|
+
-------
|
|
22342
|
+
tuple
|
|
22343
|
+
(n_picks_in_file, n_matched_picks)
|
|
22344
|
+
"""
|
|
22345
|
+
from .pick_io import read_lst_file, match_lst_picks_to_geometry
|
|
22346
|
+
|
|
22347
|
+
try:
|
|
22348
|
+
# Read LST file
|
|
22349
|
+
lst_data = read_lst_file(lst_file, verbose=verbose)
|
|
22350
|
+
|
|
22351
|
+
if lst_data['n_picks'] == 0:
|
|
22352
|
+
QMessageBox.warning(self, "No Picks", f"No valid picks found in {os.path.basename(lst_file)}")
|
|
22353
|
+
return 0, 0
|
|
22354
|
+
|
|
22355
|
+
# Calculate position scaling factor
|
|
22356
|
+
# Compare median geophone spacing in LST to mean_dg in seismic data
|
|
22357
|
+
position_scale = 1.0
|
|
22358
|
+
|
|
22359
|
+
# Calculate median spacing from LST positions
|
|
22360
|
+
all_positions = [pos for _, _, pos, _ in lst_data['picks']]
|
|
22361
|
+
if len(all_positions) > 1:
|
|
22362
|
+
unique_positions = sorted(set(all_positions))
|
|
22363
|
+
if len(unique_positions) > 1:
|
|
22364
|
+
spacings = [unique_positions[i+1] - unique_positions[i]
|
|
22365
|
+
for i in range(len(unique_positions)-1)]
|
|
22366
|
+
median_spacing_lst = np.median(spacings)
|
|
22367
|
+
|
|
22368
|
+
# Use self.mean_dg from seismic data
|
|
22369
|
+
if hasattr(self, 'mean_dg') and self.mean_dg is not None and self.mean_dg > 0:
|
|
22370
|
+
if median_spacing_lst > 0:
|
|
22371
|
+
position_scale = self.mean_dg / median_spacing_lst
|
|
22372
|
+
print(f"\nDetected position scaling:")
|
|
22373
|
+
print(f" Median LST spacing: {median_spacing_lst:.2f}")
|
|
22374
|
+
print(f" Seismic mean_dg: {self.mean_dg:.2f}")
|
|
22375
|
+
print(f" Scaling factor: {position_scale:.2f}")
|
|
22376
|
+
|
|
22377
|
+
# Match picks to current geometry
|
|
22378
|
+
matched = match_lst_picks_to_geometry(
|
|
22379
|
+
lst_data,
|
|
22380
|
+
self.trace_position,
|
|
22381
|
+
self.source_position,
|
|
22382
|
+
shot_number_offset=0, # Could be made configurable via dialog
|
|
22383
|
+
position_tolerance=0.1, # Could be made configurable via dialog
|
|
22384
|
+
position_scale=position_scale,
|
|
22385
|
+
verbose=True # Always print diagnostic info for now
|
|
22386
|
+
)
|
|
22387
|
+
|
|
22388
|
+
# Apply matched picks
|
|
22389
|
+
for i, (pick_list, error_list) in enumerate(zip(matched['picks'], matched['errors'])):
|
|
22390
|
+
if self.picks[i] is None:
|
|
22391
|
+
self.picks[i] = [np.nan] * len(pick_list)
|
|
22392
|
+
if self.error[i] is None:
|
|
22393
|
+
self.error[i] = [0.001] * len(error_list)
|
|
22394
|
+
|
|
22395
|
+
for j, (pick, error) in enumerate(zip(pick_list, error_list)):
|
|
22396
|
+
if not np.isnan(pick):
|
|
22397
|
+
self.picks[i][j] = pick
|
|
22398
|
+
self.error[i][j] = error
|
|
22399
|
+
|
|
22400
|
+
# Mark picks as modified
|
|
22401
|
+
self.picks_modified = True
|
|
22402
|
+
|
|
22403
|
+
# Trigger pick update
|
|
22404
|
+
self.update_pick_flag = True
|
|
22405
|
+
|
|
22406
|
+
# Count sources with picks
|
|
22407
|
+
sources_with_picks = sum(1 for pick_list in matched['picks']
|
|
22408
|
+
if any(not np.isnan(p) for p in pick_list))
|
|
22409
|
+
|
|
22410
|
+
if matched['n_matched'] == 0:
|
|
22411
|
+
QMessageBox.warning(self, "No Matches",
|
|
22412
|
+
f"Could not match any picks from {os.path.basename(lst_file)} to current geometry.\n\n"
|
|
22413
|
+
f"File contains {matched['n_total']} picks but none matched.")
|
|
22414
|
+
|
|
22415
|
+
return lst_data['n_picks'], matched['n_matched']
|
|
22416
|
+
|
|
22417
|
+
except Exception as e:
|
|
22418
|
+
QMessageBox.critical(self, "Error Loading LST File",
|
|
22419
|
+
f"Error reading {os.path.basename(lst_file)}:\n\n{str(e)}")
|
|
22420
|
+
if verbose:
|
|
22421
|
+
import traceback
|
|
22422
|
+
traceback.print_exc()
|
|
22423
|
+
return 0, 0
|
|
22424
|
+
|
|
22425
|
+
def _loadPicksFromVS(self, vs_file, verbose=False):
|
|
22426
|
+
"""
|
|
22427
|
+
Load picks from a SeisImager/PickWin .vs file.
|
|
22428
|
+
|
|
22429
|
+
Parameters
|
|
22430
|
+
----------
|
|
22431
|
+
vs_file : str
|
|
22432
|
+
Path to .vs file
|
|
22433
|
+
verbose : bool
|
|
22434
|
+
Print debug information
|
|
22435
|
+
|
|
22436
|
+
Returns
|
|
22437
|
+
-------
|
|
22438
|
+
tuple
|
|
22439
|
+
(n_picks_in_file, n_matched_picks)
|
|
22440
|
+
"""
|
|
22441
|
+
from .pick_io import read_vs_file, match_vs_picks_to_geometry
|
|
22442
|
+
|
|
22443
|
+
try:
|
|
22444
|
+
# Read VS file
|
|
22445
|
+
vs_data = read_vs_file(vs_file, verbose=verbose)
|
|
22446
|
+
|
|
22447
|
+
if vs_data['n_picks'] == 0:
|
|
22448
|
+
QMessageBox.warning(self, "No Picks", f"No valid picks found in {os.path.basename(vs_file)}")
|
|
22449
|
+
return 0, 0
|
|
22450
|
+
|
|
22451
|
+
# Calculate position scaling factor
|
|
22452
|
+
# Compare VS spacing to mean_dg in seismic data
|
|
22453
|
+
position_scale = 1.0
|
|
22454
|
+
median_spacing_vs = vs_data.get('spacing', 1.0)
|
|
22455
|
+
|
|
22456
|
+
# Use self.mean_dg from seismic data
|
|
22457
|
+
if hasattr(self, 'mean_dg') and self.mean_dg is not None and self.mean_dg > 0:
|
|
22458
|
+
if median_spacing_vs > 0:
|
|
22459
|
+
position_scale = self.mean_dg / median_spacing_vs
|
|
22460
|
+
print(f"\nDetected position scaling:")
|
|
22461
|
+
print(f" VS spacing: {median_spacing_vs:.2f}")
|
|
22462
|
+
print(f" Seismic mean_dg: {self.mean_dg:.2f}")
|
|
22463
|
+
print(f" Scaling factor: {position_scale:.2f}")
|
|
22464
|
+
|
|
22465
|
+
# Match picks to current geometry
|
|
22466
|
+
matched = match_vs_picks_to_geometry(
|
|
22467
|
+
vs_data,
|
|
22468
|
+
self.trace_position,
|
|
22469
|
+
self.source_position,
|
|
22470
|
+
offset_tolerance=0.5, # Could be made configurable via dialog
|
|
22471
|
+
position_scale=position_scale,
|
|
22472
|
+
verbose=True # Always print diagnostic info for now
|
|
22473
|
+
)
|
|
22474
|
+
|
|
22475
|
+
# Apply matched picks
|
|
22476
|
+
for i, (pick_list, error_list) in enumerate(zip(matched['picks'], matched['errors'])):
|
|
22477
|
+
if self.picks[i] is None:
|
|
22478
|
+
self.picks[i] = [np.nan] * len(pick_list)
|
|
22479
|
+
if self.error[i] is None:
|
|
22480
|
+
self.error[i] = [0.001] * len(error_list)
|
|
22481
|
+
|
|
22482
|
+
for j, (pick, error) in enumerate(zip(pick_list, error_list)):
|
|
22483
|
+
if not np.isnan(pick):
|
|
22484
|
+
self.picks[i][j] = pick
|
|
22485
|
+
self.error[i][j] = error
|
|
22486
|
+
|
|
22487
|
+
# Mark picks as modified
|
|
22488
|
+
self.picks_modified = True
|
|
22489
|
+
|
|
22490
|
+
# Trigger pick update
|
|
22491
|
+
self.update_pick_flag = True
|
|
22492
|
+
|
|
22493
|
+
# Count sources with picks
|
|
22494
|
+
sources_with_picks = sum(1 for pick_list in matched['picks']
|
|
22495
|
+
if any(not np.isnan(p) for p in pick_list))
|
|
22496
|
+
|
|
22497
|
+
if matched['n_matched'] == 0:
|
|
22498
|
+
QMessageBox.warning(self, "No Matches",
|
|
22499
|
+
f"Could not match any picks from {os.path.basename(vs_file)} to current geometry.\n\n"
|
|
22500
|
+
f"File contains {matched['n_total']} picks but none matched.")
|
|
22501
|
+
|
|
22502
|
+
return vs_data['n_picks'], matched['n_matched']
|
|
22503
|
+
|
|
22504
|
+
except Exception as e:
|
|
22505
|
+
QMessageBox.critical(self, "Error Loading VS File",
|
|
22506
|
+
f"Error reading {os.path.basename(vs_file)}:\n\n{str(e)}")
|
|
22507
|
+
if verbose:
|
|
22508
|
+
import traceback
|
|
22509
|
+
traceback.print_exc()
|
|
22510
|
+
return 0, 0
|
|
22511
|
+
|
|
21268
22512
|
def loadPicks(self, fname=None, verbose=False):
|
|
21269
|
-
# Load picks from
|
|
22513
|
+
# Load picks from .sgt (PyGimli), .LST (Rayfract), or .vs (SeisImager/PickWin) files
|
|
21270
22514
|
|
|
21271
22515
|
# Check if seismic data is loaded first
|
|
21272
22516
|
if not self.streams:
|
|
@@ -21276,7 +22520,7 @@ Failed to match: {skipped_no_trace_found}"""
|
|
|
21276
22520
|
# The first argument returned is the filename(s) and path(s)
|
|
21277
22521
|
if fname is None or not fname:
|
|
21278
22522
|
fnames, _ = QFileDialog.getOpenFileNames(
|
|
21279
|
-
self, 'Open file(s)', filter='
|
|
22523
|
+
self, 'Open file(s)', filter='Pick files (*.sgt *.LST *.lst *.vs);;SGT files (*.sgt);;LST files (*.LST *.lst);;VS files (*.vs)')
|
|
21280
22524
|
else:
|
|
21281
22525
|
# If fname is provided, ensure it's a list for consistent processing
|
|
21282
22526
|
if isinstance(fname, str):
|
|
@@ -21287,7 +22531,7 @@ Failed to match: {skipped_no_trace_found}"""
|
|
|
21287
22531
|
if fnames:
|
|
21288
22532
|
# Initialize counters for all files
|
|
21289
22533
|
total_picks_loaded = 0
|
|
21290
|
-
total_picks_in_files = 0 # Track total picks across all
|
|
22534
|
+
total_picks_in_files = 0 # Track total picks across all files
|
|
21291
22535
|
total_sources_loaded = 0
|
|
21292
22536
|
overall_max_picked_time = 0
|
|
21293
22537
|
|
|
@@ -21296,6 +22540,28 @@ Failed to match: {skipped_no_trace_found}"""
|
|
|
21296
22540
|
if verbose:
|
|
21297
22541
|
print(f"\nProcessing file {file_idx + 1}/{len(fnames)}: {fname}")
|
|
21298
22542
|
|
|
22543
|
+
# Detect file type by extension
|
|
22544
|
+
file_ext = fname.lower().split('.')[-1]
|
|
22545
|
+
|
|
22546
|
+
if file_ext == 'lst':
|
|
22547
|
+
# Load LST (Rayfract) file
|
|
22548
|
+
n_picks_file, n_matched = self._loadPicksFromLST(fname, verbose=verbose)
|
|
22549
|
+
total_picks_in_files += n_picks_file
|
|
22550
|
+
total_picks_loaded += n_matched
|
|
22551
|
+
if n_matched > 0:
|
|
22552
|
+
total_sources_loaded += 1
|
|
22553
|
+
continue # Skip to next file
|
|
22554
|
+
|
|
22555
|
+
if file_ext == 'vs':
|
|
22556
|
+
# Load VS (SeisImager/PickWin) file
|
|
22557
|
+
n_picks_file, n_matched = self._loadPicksFromVS(fname, verbose=verbose)
|
|
22558
|
+
total_picks_in_files += n_picks_file
|
|
22559
|
+
total_picks_loaded += n_matched
|
|
22560
|
+
if n_matched > 0:
|
|
22561
|
+
total_sources_loaded += 1
|
|
22562
|
+
continue # Skip to next file
|
|
22563
|
+
|
|
22564
|
+
# Default: Load SGT (PyGimli) file
|
|
21299
22565
|
with open(fname, 'r') as f:
|
|
21300
22566
|
# Read number of stations
|
|
21301
22567
|
n_stations = int(f.readline().split('#')[0].strip())
|