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/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("Original trace no")
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
- # Prevent redundant control visibility updates if we're in a swap
2949
- should_update_visibility = not getattr(self, '_in_view_swap', False)
2950
-
2951
- # Map view names to internal types
2952
- view_mapping = {
2953
- "Seismogram": "seismogram",
2954
- "Layout": "layout",
2955
- "Traveltimes": "traveltime",
2956
- "Topography": "topo",
2957
- "Spectrogram": "spectrogram",
2958
- "Dispersion": "dispersion",
2959
- "Pseudo-section": "pseudosection"
2960
- }
2961
- new_plot_type = view_mapping.get(view_type, "layout")
2962
-
2963
- # Only rebuild dynamic controls if the view type has actually changed
2964
- # This prevents unnecessary widget destruction/creation and flickering
2965
- if getattr(self, '_last_bottom_plot_type', None) != new_plot_type:
2966
- self.buildBottomControlsForView(view_type)
2967
- self._last_bottom_plot_type = new_plot_type
2968
-
2969
- # Scroll bottom controls scroll area to the beginning (left)
2970
- if hasattr(self, 'bottomControlsScrollArea'):
2971
- self.bottomControlsScrollArea.horizontalScrollBar().setValue(0)
2972
-
2973
- # Track bottom plot type for control visibility
2974
- self.bottomPlotType = new_plot_type
2975
-
2976
- # Update trace window checkbox state based on view combination
2977
- self._updateTraceWindowCheckboxState()
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
- # Populate trace range dropdowns if this is Dispersion view
2980
- if view_type == "Dispersion":
2981
- # Only enable if we have a stream loaded
2982
- if self.streams and self.currentIndex is not None:
2983
- self.populateTraceRangeComboboxes()
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
- else:
2991
- # Disable trace range slider for other view types
2992
- if hasattr(self, 'traceRangeSlider') and self.traceRangeSlider is not None:
2993
- try:
2994
- self.traceRangeSlider.disableControls()
2995
- except RuntimeError:
2996
- pass # Widget may have been deleted during UI rebuild
2997
-
2998
- if view_type == "Seismogram":
2999
- # Plot seismogram to bottom widget
3000
- self._plot_target_widget = self.bottomPlotWidget
3001
- self._plot_target_viewbox = self.bottomViewBox
3002
- self.updatePlots()
3003
- elif view_type == "Layout":
3004
- self._plot_target_widget = self.bottomPlotWidget
3005
- self._plot_target_viewbox = self.bottomViewBox
3006
- self.plotLayout()
3007
- self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
3008
- elif view_type == "Traveltimes":
3009
- self._plot_target_widget = self.bottomPlotWidget
3010
- self._plot_target_viewbox = self.bottomViewBox
3011
- self.plotTravelTime()
3012
- self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
3013
- elif view_type == "Topography":
3014
- self._plot_target_widget = self.bottomPlotWidget
3015
- self._plot_target_viewbox = self.bottomViewBox
3016
- self.plotTopo()
3017
- self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
3018
- elif view_type == "Spectrogram":
3019
- self._plot_target_widget = self.bottomPlotWidget
3020
- self._plot_target_viewbox = self.bottomViewBox
3021
- self.plotSpectrogram()
3022
- self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
3023
- elif view_type == "Dispersion":
3024
- self._plot_target_widget = self.bottomPlotWidget
3025
- self._plot_target_viewbox = self.bottomViewBox
3026
- self.plotDispersion()
3027
- elif view_type == "Pseudo-section":
3028
- self._plot_target_widget = self.bottomPlotWidget
3029
- self._plot_target_viewbox = self.bottomViewBox
3030
- self.plotPseudoSection()
3031
- self._restoreSceneClickSignals(self.bottomPlotWidget, "bottom")
3032
-
3033
- # Reset target widget after plotting
3034
- self._plot_target_widget = None
3035
- self._plot_target_viewbox = None
3036
-
3037
- # Draw/update trace extent indicator after view change
3038
- self.drawTraceExtentIndicator()
3039
-
3040
- # Reset view to proper default range after switching
3041
- self.resetBottomView()
3042
-
3043
- # Update title to show on correct plot widget based on current view types
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
- # Replot both top and bottom if they use spectrogram
3858
- if hasattr(self, 'topPlotType') and self.topPlotType == "spectrogram":
3859
- self._plot_target_widget = self.plotWidget
3860
- self._plot_target_viewbox = self.viewBox
3861
- self.plotSpectrogram()
3862
- self._plot_target_widget = None
3863
- self._plot_target_viewbox = None
3864
- if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "spectrogram":
3865
- self._plot_target_widget = self.bottomPlotWidget
3866
- self._plot_target_viewbox = self.bottomViewBox
3867
- self.plotSpectrogram()
3868
- self._plot_target_widget = None
3869
- self._plot_target_viewbox = None
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
- # Replot both top and bottom if they use dispersion
3877
- if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
3878
- self._plot_target_widget = self.plotWidget
3879
- self._plot_target_viewbox = self.viewBox
3880
- self.plotDispersion()
3881
- self._plot_target_widget = None
3882
- self._plot_target_viewbox = None
3883
- if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
3884
- self._plot_target_widget = self.bottomPlotWidget
3885
- self._plot_target_viewbox = self.bottomViewBox
3886
- self.plotDispersion()
3887
- self._plot_target_widget = None
3888
- self._plot_target_viewbox = None
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
- # Replot both top and bottom if they use spectrogram
3896
- if hasattr(self, 'topPlotType') and self.topPlotType == "spectrogram":
3897
- self._plot_target_widget = self.plotWidget
3898
- self._plot_target_viewbox = self.viewBox
3899
- self.plotSpectrogram()
3900
- self._plot_target_widget = None
3901
- self._plot_target_viewbox = None
3902
- if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "spectrogram":
3903
- self._plot_target_widget = self.bottomPlotWidget
3904
- self._plot_target_viewbox = self.bottomViewBox
3905
- self.plotSpectrogram()
3906
- self._plot_target_widget = None
3907
- self._plot_target_viewbox = None
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
- # Replot both top and bottom if they use dispersion
3915
- if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
3916
- self._plot_target_widget = self.plotWidget
3917
- self._plot_target_viewbox = self.viewBox
3918
- self.plotDispersion()
3919
- self._plot_target_widget = None
3920
- self._plot_target_viewbox = None
3921
- if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
3922
- self._plot_target_widget = self.bottomPlotWidget
3923
- self._plot_target_viewbox = self.bottomViewBox
3924
- self.plotDispersion()
3925
- self._plot_target_widget = None
3926
- self._plot_target_viewbox = None
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
- self.updatePlots()
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
- self.updatePlots()
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
- self.updatePlots()
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
- self.updatePlots()
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
- self.updatePlots()
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
- self.updatePlots()
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
- self.updatePlots()
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
- self.updatePlots()
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 = 'shot_trace_number' # Plot type for X axis
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
- # Get the current row index directly (this handles duplicates properly)
10750
- selected_row = self.fileListWidget.currentRow()
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
- # If a valid row is selected
10753
- if selected_row >= 0 and selected_row < len(self.fileNames):
10754
- self.currentFileName = self.fileNames[selected_row] # Set the current file name
10755
- self.currentIndex = selected_row # Set the current index
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
- # Remove all mode items from the plot
10764
- for mode_num, mode_data in state['modes'].items():
10765
- # Remove picked point items
10766
- for item in mode_data['picked_point_items']:
10767
- if item is not None:
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(item)
11249
+ plot_widget.removeItem(mode_data['curve_line'])
10770
11250
  except:
10771
11251
  pass
10772
11252
 
10773
- # Remove curve line
10774
- if mode_data['curve_line'] is not None:
10775
- try:
10776
- plot_widget.removeItem(mode_data['curve_line'])
10777
- except:
10778
- pass
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
- # Check if data is in new multi-mode format or old single-mode format
10795
- if 'modes' in curve_data:
10796
- # New multi-mode format
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
- if self.topPlotType == "seismogram":
10877
- self.updatePlots()
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 self.bottomPlotType == "seismogram":
10901
- self.updatePlots()
10902
- elif self.bottomPlotType == "layout":
10903
- self.plotLayout()
10904
- elif self.bottomPlotType == "traveltime":
10905
- self.plotTravelTime()
10906
- elif self.bottomPlotType == "topo":
10907
- self.plotTopo()
10908
- elif self.bottomPlotType == "spectrogram":
10909
- self.plotSpectrogram()
10910
- elif self.bottomPlotType == "dispersion":
10911
- self.plotDispersion()
10912
- elif self.bottomPlotType == "pseudosection":
10913
- self.plotPseudoSection()
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
- # Reset target widget after plotting
10916
- self._plot_target_widget = None
10917
- self._plot_target_viewbox = None
10918
-
10919
- # Update FFID/source in status bar for all views
10920
- if hasattr(self, 'ffid') and self.ffid and self.currentIndex < len(self.ffid):
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
- ffid_val = self.ffid[self.currentIndex]
10923
- src_val = self.source_position[self.currentIndex] if self.source_position else 'N/A'
10924
- self.ffidLabel.setText(f'FFID: {ffid_val} | Source at {src_val} m')
10925
- except Exception:
10926
- pass
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
- # Update dispersion window info in status bar
10929
- self.updateDispersionWindowInfo()
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
- # Now trigger the plot for the first loaded file, respecting current view selections
11612
- if counter_stream > 0 and firstNewFile:
11613
- self.currentFileName = firstNewFile
11614
- self.currentIndex = self.fileNames.index(firstNewFile)
11615
- # Ensure plot parameters are initialized for this file
11616
- self.getPlotParameters()
11617
- # Make sure plotTypeX is correctly set for the first file
11618
- # Call autoSelectPlotTypes explicitly to ensure it's based on current file's data
11619
- self.autoSelectPlotTypes()
11620
- self.fileListWidget.setCurrentRow(self.currentIndex)
11621
- # Trigger plots based on currently selected views (not hardcoded seismogram)
11622
- if hasattr(self, 'topViewComboBox'):
11623
- self.onTopViewChanged(self.topViewComboBox.currentText())
11624
- if hasattr(self, 'bottomViewComboBox'):
11625
- self.onBottomViewChanged(self.bottomViewComboBox.currentText())
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
- # Ensure all lists are properly sized for the current index
11721
- for attr in self.attributes_to_initialize:
11722
- attr_list = getattr(self, attr)
11723
- while len(attr_list) <= self.currentIndex:
11724
- if attr == 'airWaveItems':
11725
- attr_list.append([None, None, None])
11726
- else:
11727
- attr_list.append(None)
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
- self.streams[self.currentIndex] = self.stream[self.streamIndex]
11730
- self.input_format[self.currentIndex] = check_format(self.streams[self.currentIndex])
11731
- self.getPlotParameters()
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
- # Always update dispersion picking states when loading a shot
11734
- # First, clear any old items from both plot widgets
11735
- for view in ['top', 'bottom']:
11736
- state = self.dispersion_picking_state[view]
11737
- plot_widget = self.plotWidget if view == 'top' else self.bottomPlotWidget
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 error bars
11757
- for item in mode_data.get('error_bar_items', []):
11758
- if item is not None:
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(item)
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
- # Remove interpolated error fill
11765
- if mode_data.get('interp_error_fill') is not None:
11766
- interp_items = mode_data['interp_error_fill']
11767
- if isinstance(interp_items, list):
11768
- for item in interp_items:
11769
- if item is not None:
11770
- try:
11771
- plot_widget.removeItem(item)
11772
- except:
11773
- pass
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
- state['current_mode'] = 0
11787
- state['current_dispersion_data'] = None
11788
-
11789
- # Now check if this shot has stored dispersion data and restore it
11790
- self._restoreDispersionPicksFromStorage()
11791
-
11792
- # If it is the first time the file is loaded, update the sources and traces lists
11793
- if self.picks[self.currentIndex] is None:
11794
- # Initialize picks for the current file with a list of nans of the same length as the traces
11795
- self.picks[self.currentIndex] = [np.nan] * len(self.trace_position[self.currentIndex])
11796
- # Intialize errors for the current file with a list of nans of the same length as the traces
11797
- self.error[self.currentIndex] = [np.nan] * len(self.trace_position[self.currentIndex])
11798
- # Initialize the scatter items for the current file with list of empty lists of the same length as the traces
11799
- self.pickSeismoItems[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
11800
- # Separate collections for top and bottom seismogram plots
11801
- if self.currentIndex >= len(self.pickSeismoItems_top):
11802
- self.pickSeismoItems_top.extend([None] * (self.currentIndex - len(self.pickSeismoItems_top) + 1))
11803
- if self.currentIndex >= len(self.pickSeismoItems_bottom):
11804
- self.pickSeismoItems_bottom.extend([None] * (self.currentIndex - len(self.pickSeismoItems_bottom) + 1))
11805
- self.pickSeismoItems_top[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
11806
- self.pickSeismoItems_bottom[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
11807
- # Initialize the scatter items for the current file with list of empty lists of the same length as the traces
11808
- self.pickLayoutItems[self.currentIndex] = [None] * len(self.trace_position[self.currentIndex])
11809
- # Initialize dispersion curves for the current file (one curve per shot)
11810
- self.dispersion_curves[self.currentIndex] = None
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
- stream = self.streams[self.currentIndex]
12351
+ QApplication.setOverrideCursor(Qt.WaitCursor)
12352
+ try:
12353
+ stream = self.streams[self.currentIndex]
11864
12354
 
11865
- stream = swap_header_format(stream, format)
11866
-
11867
- # Save the stream as a SEGY or SU file
11868
- stream.write(savePath, format=format,
11869
- data_encoding=5, byteorder='>')
11870
- print(f"Saved single shot ({format.upper()}): {savePath}")
11871
- QMessageBox.information(self, "File Saved", f"File saved as: {savePath}")
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
- # Start progress dialog if there are multiple files
11888
- if len(self.streams) > 1:
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
- # Update the progress dialog
11900
- progress.setValue(i)
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 = swap_header_format(stream, self.output_format)
11904
- self.input_format[i] = check_format(stream)
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
- # Default file name is fileNames[i] without extension _ffid.format
11907
- # check if ffid is in the original file name
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
- savePath = os.path.join(saveDir, os.path.basename(defaultSavePath))
11914
- # Make sure we're not overwriting an existing file
11915
- # if os.path.exists(savePath):
11916
- savePath = savePath.replace('.' + self.output_format.lower(), '_updated.' + self.output_format.lower())
11917
- stream.write(savePath, format=self.output_format,
11918
- data_encoding=5, byteorder='>')
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
- if len(self.streams) > 1:
11922
- # Ensure the progress dialog closes
11923
- progress.setValue(len(self.streams))
11924
- QMessageBox.information(self, "Files Saved", f"{len(self.streams)} files saved successfully in: {saveDir}")
11925
-
11926
- # Reset headers modified flag after successful save
11927
- self.headers_modified = False
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
- # Merge all streams into a single stream
11936
- merged_stream = merge_streams(self.streams)
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
- merged_stream = swap_header_format(merged_stream, self.output_format)
11939
- for i, stream in enumerate(self.streams):
11940
- self.input_format[i] = check_format(stream)
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
- # Save the merged stream as a SEGY or SU file
11943
- merged_stream.write(savePath, format=self.output_format,
11944
- data_encoding=5, byteorder='>')
11945
- print(f"Saved all shots merged into single file ({self.output_format.upper()}): {savePath}")
11946
- QMessageBox.information(self, "File Saved", f"File saved as: {savePath}")
11947
-
11948
- # Reset headers modified flag after successful save
11949
- self.headers_modified = False
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 = 'shot_trace_number'
12705
+ self.plotTypeX = 'file_trace_number'
12207
12706
  self.plotTypeY = 'ffid'
12208
12707
  # Update menu actions to reflect the change
12209
- if hasattr(self, 'shotTraceNumberAction'):
12210
- self.shotTraceNumberAction.setChecked(True)
12211
- self.fileTraceNumberAction.setChecked(False)
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("Original trace no")
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
- values = dialog.getValues()
12946
+ QApplication.setOverrideCursor(Qt.WaitCursor)
12947
+ try:
12948
+ values = dialog.getValues()
12448
12949
 
12449
- ffid = values['FFID']
12450
- self.ffid[self.currentIndex] = int(ffid)
12451
- # Sync headers to obspy streams immediately
12452
- self.syncHeadersToStreams(self.currentIndex)
12453
- self.headers_modified = True # Mark headers as modified
12454
- QMessageBox.information(self, "FFID Updated", f"FFID set to {ffid} for file {os.path.basename(self.currentFileName)}")
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
- self.updateTitle()
12457
- self.updateFileListDisplay()
12458
- self.updatePlots()
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
- values = dialog.getValues()
12477
- apply_to_all = dialog.isChecked()
12478
- delay = values['Delay (in s)']
12479
- diff_delay = delay - self.delay[self.currentIndex]
12480
-
12481
- if apply_to_all:
12482
- self.delay = [delay] * len(self.delay) # Apply the delay to all files
12483
- self.time = [np.arange(n_sample) * sample_interval + delay for n_sample, sample_interval in zip(self.n_sample, self.sample_interval)]
12484
- # Update the picks with the new delay for files containing picks
12485
- for i, picks in enumerate(self.picks):
12486
- if picks is not None:
12487
- self.picks[i] = [pick + diff_delay for pick in picks]
12488
- # Sync headers to obspy streams immediately for all files
12489
- for i in range(len(self.streams)):
12490
- self.syncHeadersToStreams(i)
12491
- self.headers_modified = True # Mark headers as modified
12492
- QMessageBox.information(self, "Delay Updated", f"Delay set to {delay} s for all files")
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
- else:
12495
- self.delay[self.currentIndex] = delay
12496
- self.time[self.currentIndex] = np.arange(self.n_sample[self.currentIndex]) * self.sample_interval[self.currentIndex] + self.delay[self.currentIndex]
12497
- if self.picks is not None:
12498
- self.picks[self.currentIndex] = [pick + diff_delay for pick in self.picks[self.currentIndex]]
12499
- # Sync headers to obspy streams immediately
12500
- self.syncHeadersToStreams(self.currentIndex)
12501
- self.headers_modified = True # Mark headers as modified
12502
- QMessageBox.information(self, "Delay Updated", f"Delay set to {delay} s for file {os.path.basename(self.currentFileName)}")
12503
- # Mark headers/traces as modified
12504
- self.headers_modified = True
12505
- self.updatePlots()
12506
- self.updateFileListDisplay()
12507
- self.updatePlots()
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
- values = dialog.getValues()
12561
- source_position = values['Source Position (in m)']
12562
- self.source_position[self.currentIndex] = source_position
12563
- self.offset[self.currentIndex] = self.trace_position[self.currentIndex] - source_position
12564
- # Sync headers to obspy streams immediately
12565
- self.syncHeadersToStreams(self.currentIndex)
12566
- self.headers_modified = True # Mark headers as modified
12567
- QMessageBox.information(self, "Source Position Updated", f"Source position set to {source_position} m for file {os.path.basename(self.currentFileName)}")
12568
-
12569
- # Mark headers/traces as modified
12570
- self.headers_modified = True
12571
- self.updateMeanSpacing()
12572
- self.updatePlots()
12573
- self.updateFileListDisplay()
12574
- self.updatePlots()
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
- selected_index, new_position, apply_to_all = dialog.getValues()
12584
- if apply_to_all:
12585
- # Set trace position for all files
12586
- for i in range(len(self.streams)):
12587
- self.trace_position[i][selected_index] = new_position
12588
- self.offset[i][selected_index] = new_position - self.source_position[i]
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(i)
12591
- self.headers_modified = True # Mark headers as modified
12592
- QMessageBox.information(self, "Trace Position Updated", f"Trace position for trace #{trace_numbers[selected_index]} set to {new_position} m for all files")
12593
- else:
12594
- self.trace_position[self.currentIndex][selected_index] = new_position
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
- self.updateMeanSpacing()
12602
- self.updatePlots()
12603
- self.updateFileListDisplay()
12604
- self.updatePlots()
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
- values = dialog.getValues()
14754
- first_trace = values['First Trace #'] # 1-based user input
14755
- second_trace = values['Second Trace #'] # 1-based user input
14756
-
14757
- # Convert to 0-based indices
14758
- first_idx = first_trace - 1
14759
- second_idx = second_trace - 1
14760
-
14761
- # Swap the traces
14762
- if dialog.isChecked():
14763
- for i in range(len(self.streams)):
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
- QMessageBox.information(self, "Traces Swapped", f"Traces {first_trace} and {second_trace} swapped for all files")
14796
- self.headers_modified = True # Mark headers as modified
14797
- else:
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
- self.updatePlots()
14836
- self.updateFileListDisplay()
14837
- self.updatePlots()
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
- selected_index, apply_to_all = dialog.getValues()
14845
- # selected_index is the array index (0-based) directly from TraceSelector
14846
- trace_display_number = trace_numbers[selected_index]
14847
-
14848
- if apply_to_all:
14849
- # Remove the trace for all files
14850
- for i in range(len(self.streams)):
14851
- if selected_index < len(self.streams[i]):
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
- QMessageBox.information(self, "Trace Removed", f"Trace #{trace_display_number} removed for all files")
14873
- self.headers_modified = True # Mark headers as modified
14874
- else:
14875
- # Remove the trace for the current file only
14876
- if selected_index < len(self.streams[self.currentIndex]):
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
- self.streams[i] = remove_trace(self.streams[i], trace_display_number)
14879
- # Delete by index (directly remove from position)
14880
- self.offset[i] = np.delete(self.offset[i], selected_index)
14881
- self.trace_position[i] = np.delete(self.trace_position[i], selected_index)
14882
- self.trace_elevation[i] = np.delete(self.trace_elevation[i], selected_index)
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
- # Rebuild shot_trace_number from updated obspy headers
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
- QMessageBox.information(self, "Trace Removed", f"Trace #{trace_display_number} removed for file {os.path.basename(self.currentFileName)}")
14899
-
14900
- self.updateMeanSpacing()
14901
- self.updatePlots()
14902
- self.updateFileListDisplay()
14903
- self.updatePlots()
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 # (relative)', 'initial_value': 1, 'type': 'int'},
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
- trace_num_input = values['Trace # (relative)'] # 1-based user input
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
- trace_idx = trace_num_input - 1
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 trace_idx < n_traces_i and new_idx < n_traces_i and trace_idx != new_idx:
14936
- self.streams[i] = move_trace(self.streams[i], trace_num_input, new_idx)
14937
-
14938
- # Move all associated data arrays EXCEPT trace_position (spatial position stays with array index)
14939
- for attr in [self.trace_elevation[i], self.offset[i],
14940
- self.file_trace_number[i]]:
14941
- if len(attr) > trace_idx:
14942
- attr_val = attr[trace_idx]
14943
- attr[:] = np.insert(np.delete(attr, trace_idx), new_idx, attr_val)
14944
-
14945
- # Move shot_trace_number array
14946
- if len(self.shot_trace_number[i]) > trace_idx:
14947
- shot_val = self.shot_trace_number[i][trace_idx]
14948
- self.shot_trace_number[i] = np.insert(np.delete(self.shot_trace_number[i], trace_idx), new_idx, shot_val)
14949
-
14950
- # Move picks and errors
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 shot_trace_number to be sequential
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 trace_idx < n_traces_i and new_idx < n_traces_i and trace_idx != new_idx:
14980
- self.streams[i] = move_trace(self.streams[i], trace_num_input, new_idx)
14981
-
14982
- # Move all associated data arrays EXCEPT trace_position (spatial position stays with array index)
14983
- for attr in [self.trace_elevation[i], self.offset[i],
14984
- self.file_trace_number[i]]:
14985
- if len(attr) > trace_idx:
14986
- attr_val = attr[trace_idx]
14987
- attr[:] = np.insert(np.delete(attr, trace_idx), new_idx, attr_val)
14988
-
14989
- # Move shot_trace_number array
14990
- if len(self.shot_trace_number[i]) > trace_idx:
14991
- shot_val = self.shot_trace_number[i][trace_idx]
14992
- self.shot_trace_number[i] = np.insert(np.delete(self.shot_trace_number[i], trace_idx), new_idx, shot_val)
14993
-
14994
- # Move picks and errors
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 shot_trace_number to be sequential
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
- # Reverse the stream data and trace numbers (positions stay in place)
15082
- self.streams[i] = reverse_traces(self.streams[i])
15083
-
15084
- # Reverse shot_trace_number to match the flipped trace numbers in headers
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
- # Reverse pick items (they follow the data)
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
- # Reverse the stream data and trace numbers (positions stay in place)
15104
- self.streams[i] = reverse_traces(self.streams[i])
15105
-
15106
- # Reverse shot_trace_number to match the flipped trace numbers in headers
15107
- self.shot_trace_number[i] = self.shot_trace_number[i][::-1]
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
- # Reverse pick items (they follow the data)
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 batchEditFFID(self):
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
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
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="Batch Edit FFID",
15919
+ title="Zero Pad Shot",
15139
15920
  parameters=parameters,
15140
- add_checkbox=False,
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
- first_shot = values['First Source #'] - 1
15147
- last_shot = values['Last Source #']
15148
- first_ffid = values['First FFID']
15149
- increment = values['Increment']
15150
-
15151
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15152
- self.ffid[i] = first_ffid + (i - first_shot) * increment
15153
- # Sync headers to obspy streams immediately for all files
15154
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15155
- self.syncHeadersToStreams(i)
15156
- self.headers_modified = True # Mark headers as modified
15157
- QMessageBox.information(self, "FFIDs Updated", f"FFIDs set from {first_ffid} to {self.ffid[last_shot-1]} with increment {increment} for shots {first_shot+1} to {last_shot}")
15158
-
15159
- self.updateTitle()
15160
- self.updateFileListDisplay()
15161
- self.updatePlots()
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
- values = dialog.getValues()
15180
- first_shot = values['First Source #'] - 1
15181
- last_shot = values['Last Source #']
15182
- delay = values['Delay (in s)']
15183
-
15184
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15185
- # Calculate delay difference for each file individually
15186
- diff_delay = delay - self.delay[i]
15187
- self.delay[i] = delay
15188
- self.time[i] = np.arange(self.n_sample[i]) * self.sample_interval[i] + self.delay[i]
15189
- if self.picks[i] is not None:
15190
- self.picks[i] = [pick + diff_delay for pick in self.picks[i]]
15191
- # Sync headers to obspy streams immediately for all files
15192
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15193
- self.syncHeadersToStreams(i)
15194
- self.headers_modified = True # Mark headers as modified
15195
- QMessageBox.information(self, "Delays Updated", f"Delays set to {delay} s for shots {first_shot+1} to {last_shot}")
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
- self.updatePlots()
15198
- self.updateFileListDisplay()
15199
- self.updatePlots()
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
- values = dialog.getValues()
15218
- first_shot = values['First Source #'] - 1
15219
- last_shot = values['Last Source #']
15220
- sample_interval = values['Sample Interval (in s)']
15221
-
15222
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15223
- self.sample_interval[i] = sample_interval
15224
- self.time[i] = np.arange(self.n_sample[i]) * self.sample_interval[i] + self.delay[i]
15225
- # Sync headers to obspy streams immediately for all files
15226
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15227
- self.syncHeadersToStreams(i)
15228
- self.headers_modified = True
15229
- QMessageBox.information(self, "Sample Interval Updated", f"Sample intervals set to {sample_interval} s for shots {first_shot+1} to {last_shot}")
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
- self.updatePlots()
15232
- self.updateFileListDisplay()
15233
- self.updatePlots()
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
- values = dialog.getValues()
15256
- first_shot = values['First Source #'] - 1
15257
- last_shot = values['Last Source #']
15258
- first_source_position = values['First Source Position (in m)']
15259
- last_source_position = values['Last Source Position (in m)']
15260
- spacing = values['Spacing (in m)']
15261
- skip_every = max(0, values['Skip every N sources']) # 0 = no skipping
15262
- num_stacks = max(1, values['Number of stacks']) # Ensure at least 1
15263
-
15264
- num_shots = min(last_shot + 1, len(self.streams)) - first_shot
15265
-
15266
- # Calculate the step interval based on skip value
15267
- # If skip_every is 0, step is 1 (modify every source)
15268
- # If skip_every is 3, step is 4 (modify every 4th source, skipping 3 in between)
15269
- step = skip_every + 1 if skip_every > 0 else 1
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
- # Calculate number of modified shots considering the step interval
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
- # Repeat each position according to stack count, looping through all positions
15295
- source_pos = []
15296
- pos_cycle_idx = 0
15297
- stacks_count = 0
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
- while len(source_pos) < num_modified_shots:
15300
- source_pos.append(unique_positions[pos_cycle_idx % len(unique_positions)])
15301
- stacks_count += 1
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
- # Move to next position after each stack
15304
- if stacks_count >= num_stacks:
15305
- pos_cycle_idx += 1
15306
- stacks_count = 0
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
- mode_info = f"looping from {first_source_position} m to {last_source_position} m with spacing {spacing} m"
15309
-
15310
- # Apply source positions only to shots at step intervals
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
- self.updateMeanSpacing()
15330
- self.updatePlots()
15331
- self.updateFileListDisplay()
15332
- self.updatePlots()
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
- values = dialog.getValues()
15354
- first_shot = values['First Source #'] - 1
15355
- last_shot = values['Last Source #']
15356
- first_trace = values['First Trace #'] - 1
15357
- last_trace = values['Last Trace #']
15358
- first_trace_position = values['First Trace Position (in m)']
15359
- spacing = values['Spacing (in m)']
15360
-
15361
- # Create trace positions for the range of traces (0-based indexing for the array)
15362
- num_traces = last_trace - first_trace
15363
- trace_pos = np.round(np.arange(num_traces) * spacing + first_trace_position, self.rounding)
15364
-
15365
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15366
- for j in range(first_trace, last_trace):
15367
- # Use j - first_trace to get the correct index in trace_pos array
15368
- trace_idx = j - first_trace
15369
- self.trace_position[i][j] = trace_pos[trace_idx]
15370
- self.offset[i][j] = np.round(self.trace_position[i][j] - self.source_position[i], self.rounding)
15371
- # Sync headers to obspy streams immediately for all files
15372
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15373
- self.syncHeadersToStreams(i)
15374
- self.headers_modified = True # Mark headers as modified
15375
- QMessageBox.information(
15376
- self,
15377
- "Trace Positions Updated",
15378
- f"Trace positions set from {first_trace_position} m with spacing {spacing} m for traces {first_trace+1} to {last_trace} for shots {first_shot+1} to {last_shot}"
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
- # Recalculate unique trace numbers for all files at once
15382
- self._updateAllUniqueTraceNumbers()
15383
- self.updateMeanSpacing()
15384
- self.updatePlots()
15385
- self.updateFileListDisplay()
15386
- self.updatePlots()
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
- values = dialog.getValues()
15406
- first_shot = values['First Source #'] - 1
15407
- last_shot = values['Last Source #']
15408
- first_trace = values['First Trace # to swap']
15409
- second_trace = values['Second Trace # to swap']
15410
-
15411
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
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
- # Recalculate unique trace numbers for all files at once
15448
- self._updateAllUniqueTraceNumbers()
15449
- # Update file list and refresh plots
15450
- self.updateFileListDisplay()
15451
- self.updatePlots()
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
- values = dialog.getValues()
15471
- first_shot = values['First Source #'] - 1 # Convert to 0-based index
15472
- last_shot = values['Last Source #'] # This is 1-based user input for the LAST shot to include
15473
- first_trace = values['First Trace # to remove']
15474
- last_trace = values['Last Trace # to remove']
15475
-
15476
- # Process shots from first_shot to last_shot (inclusive)
15477
- # last_shot is 1-based user input, so we convert it to exclusive for range: last_shot becomes last_shot+1
15478
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15479
- # Process traces in reverse order to avoid index shifting issues
15480
- # Delete from last to first so indices don't shift for remaining deletions
15481
- for trace_num in range(last_trace, first_trace-1, -1):
15482
- # Find the index of this trace number in the current file
15483
- trace_numbers = [trace.stats[self.input_format[i]].trace_header.trace_number_within_the_original_field_record
15484
- for trace in self.streams[i]]
15485
- # Look for the trace that matches this number
15486
- found_indices = np.where(np.array(trace_numbers) == trace_num)[0]
15487
- if found_indices.size == 0:
15488
- continue # Trace not found, skip
15489
-
15490
- selected_index = found_indices[0]
15491
- # Remove the trace from the stream
15492
- self.streams[i] = remove_trace(self.streams[i], trace_num)
15493
- # Remove from all associated arrays
15494
- self.offset[i] = np.delete(self.offset[i], selected_index)
15495
- self.trace_position[i] = np.delete(self.trace_position[i], selected_index)
15496
- self.trace_elevation[i] = np.delete(self.trace_elevation[i], selected_index)
15497
- self.shot_trace_number[i] = np.delete(self.shot_trace_number[i], selected_index)
15498
- self.file_trace_number[i] = np.delete(self.file_trace_number[i], selected_index)
15499
- self.picks[i] = np.delete(self.picks[i], selected_index)
15500
- self.error[i] = np.delete(self.error[i], selected_index)
15501
- self.pickSeismoItems[i] = np.delete(self.pickSeismoItems[i], selected_index)
15502
- self.pickLayoutItems[i] = np.delete(self.pickLayoutItems[i], selected_index)
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
- QMessageBox.information(self, "Traces Removed", f"Traces {first_trace} to {last_trace} removed for all files")
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
- # Recalculate unique trace numbers for all files at once
15513
- self._updateAllUniqueTraceNumbers()
15514
- self.updateMeanSpacing()
15515
- self.updateFileListDisplay()
15516
- self.updatePlots()
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
- values = dialog.getValues()
15536
- first_shot = values['First Source #'] - 1
15537
- last_shot = values['Last Source #']
15538
- first_trace = values['First Trace # to move']
15539
- new_position = values['New Position'] - 1
15540
-
15541
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15542
- self.streams[i] = move_trace(self.streams[i], first_trace, new_position)
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
- QMessageBox.information(self, "Trace Moved", f"Trace #{first_trace} moved to position {new_position + 1} for all files")
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
- # Recalculate unique trace numbers for all files at once
15587
- self._updateAllUniqueTraceNumbers()
15588
- self.updatePlots()
15589
- self.updateFileListDisplay()
15590
- self.updatePlots()
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
- first_trace = values['First Trace # to mute']
15613
- last_trace = values['Last Trace # to mute']
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
- # Adjust last_trace for loop (inclusive range)
15616
- last_trace_loop = last_trace + 1
15617
-
15618
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15619
- # Process traces in reverse order for consistency, though muting doesn't shift indices
15620
- for j in range(last_trace_loop-1, first_trace-1, -1):
15621
- trace_numbers = self.shot_trace_number[i]
15622
- selected_indices = np.where(np.array(trace_numbers) == j)[0]
15623
- if selected_indices.size == 0:
15624
- QMessageBox.information(self, "Trace Not Found", f"Trace #{j} not found in file {os.path.basename(self.fileNames[i])}")
15625
- continue
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
- # Mute the trace for the current file
15628
- self.streams[i] = mute_trace(self.streams[i], j)
15629
- # Sync headers to obspy streams immediately after all mutes for this file
15630
- self.syncHeadersToStreams(i)
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
- # Mark headers/traces as modified so the unsaved-changes warning appears
15633
- self.headers_modified = True
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
- QMessageBox.information(self, "Traces Muted", f"Traces {first_trace} to {last_trace} muted for all files")
15636
-
15637
- self.updatePlots()
15638
- self.updateFileListDisplay()
15639
- self.updatePlots()
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
- values = dialog.getValues()
15658
- first_shot = values['First Source #'] - 1
15659
- last_shot = values['Last Source #']
15660
-
15661
- for i in range(first_shot, min(last_shot + 1, len(self.streams))):
15662
- # Reverse the stream data and trace numbers (positions stay in place)
15663
- self.streams[i] = reverse_traces(self.streams[i])
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
- # Reverse picks and errors (they follow the data)
15669
- if self.picks[i] is not None:
15670
- self.picks[i] = self.picks[i][::-1]
15671
- if self.error[i] is not None:
15672
- self.error[i] = self.error[i][::-1]
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
- # Reverse pick items (they follow the data)
15675
- if self.pickSeismoItems[i] is not None:
15676
- self.pickSeismoItems[i] = self.pickSeismoItems[i][::-1]
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
- self.updatePlots()
15684
- self.updateFileListDisplay()
15685
- self.updatePlots()
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
- self.topPlotType = 'layout'
16841
- self.plotWidget.clear()
16842
- # Re-add crosshair lines after clear to the viewbox so they stay above images
16843
- self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
16844
- self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
16845
- self.crosshair_vline.setZValue(1000)
16846
- self.crosshair_hline.setZValue(1000)
16847
- # Set visibility based on picking_show_crosshair and plot type
16848
- show_crosshair = (
16849
- getattr(self, 'picking_show_crosshair', False)
16850
- and self.topPlotType in ('seismogram', 'dispersion')
16851
- )
16852
- self.crosshair_vline.setVisible(show_crosshair)
16853
- self.crosshair_hline.setVisible(show_crosshair)
16854
- # Set target widget for plotLayout to use top widget
16855
- self._plot_target_widget = self.plotWidget
16856
- self._plot_target_viewbox = self.viewBox
16857
- self.plotLayout()
16858
- # Reset target widget after plotting
16859
- self._plot_target_widget = None
16860
- self._plot_target_viewbox = None
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
- self.topPlotType = 'traveltime'
16865
- self.plotWidget.clear()
16866
- # Re-add crosshair lines after clear to the viewbox so they stay above images
16867
- self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
16868
- self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
16869
- self.crosshair_vline.setZValue(1000)
16870
- self.crosshair_hline.setZValue(1000)
16871
- # Set visibility based on picking_show_crosshair and plot type
16872
- show_crosshair = (
16873
- getattr(self, 'picking_show_crosshair', False)
16874
- and self.topPlotType in ('seismogram', 'dispersion')
16875
- )
16876
- self.crosshair_vline.setVisible(show_crosshair)
16877
- self.crosshair_hline.setVisible(show_crosshair)
16878
- # Set target widget for plotTravelTime to use top widget
16879
- self._plot_target_widget = self.plotWidget
16880
- self._plot_target_viewbox = self.viewBox
16881
- self.plotTravelTime()
16882
- # Reset target widget after plotting
16883
- self._plot_target_widget = None
16884
- self._plot_target_viewbox = None
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
- self.topPlotType = 'topo'
16889
- self.plotWidget.clear()
16890
- # Re-add crosshair lines after clear to the viewbox so they stay above images
16891
- self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
16892
- self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
16893
- self.crosshair_vline.setZValue(1000)
16894
- self.crosshair_hline.setZValue(1000)
16895
- # Set visibility based on picking_show_crosshair and plot type
16896
- show_crosshair = (
16897
- getattr(self, 'picking_show_crosshair', False)
16898
- and self.topPlotType in ('seismogram', 'dispersion')
16899
- )
16900
- self.crosshair_vline.setVisible(show_crosshair)
16901
- self.crosshair_hline.setVisible(show_crosshair)
16902
- # Set target widget for plotTopo to use top widget
16903
- self._plot_target_widget = self.plotWidget
16904
- self._plot_target_viewbox = self.viewBox
16905
- self.plotTopo()
16906
- # Reset target widget after plotting
16907
- self._plot_target_widget = None
16908
- self._plot_target_viewbox = None
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
- self.topPlotType = 'spectrogram'
16913
- self.plotWidget.clear()
16914
- # Re-add crosshair lines after clear to the viewbox so they stay above images
16915
- self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
16916
- self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
16917
- self.crosshair_vline.setZValue(1000)
16918
- self.crosshair_hline.setZValue(1000)
16919
- # Set visibility based on picking_show_crosshair and plot type
16920
- show_crosshair = (
16921
- getattr(self, 'picking_show_crosshair', False)
16922
- and self.topPlotType in ('seismogram', 'dispersion')
16923
- )
16924
- self.crosshair_vline.setVisible(show_crosshair)
16925
- self.crosshair_hline.setVisible(show_crosshair)
16926
- # Set target widget for plotSpectrogram to use top widget
16927
- self._plot_target_widget = self.plotWidget
16928
- self._plot_target_viewbox = self.viewBox
16929
- self.plotSpectrogram()
16930
- # Reset target widget after plotting
16931
- self._plot_target_widget = None
16932
- self._plot_target_viewbox = None
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
- self.topPlotType = 'dispersion'
16937
- self.plotWidget.clear()
16938
- # Re-add crosshair lines after clear to the viewbox so they stay above images
16939
- self.viewBox.addItem(self.crosshair_vline, ignoreBounds=True)
16940
- self.viewBox.addItem(self.crosshair_hline, ignoreBounds=True)
16941
- self.crosshair_vline.setZValue(1000)
16942
- self.crosshair_hline.setZValue(1000)
16943
- # Set visibility based on picking_show_crosshair and plot type
16944
- show_crosshair = (
16945
- getattr(self, 'picking_show_crosshair', False)
16946
- and self.topPlotType in ('seismogram', 'dispersion')
16947
- )
16948
- self.crosshair_vline.setVisible(show_crosshair)
16949
- self.crosshair_hline.setVisible(show_crosshair)
16950
- # Set target widget for plotDispersion to use top widget
16951
- self._plot_target_widget = self.plotWidget
16952
- self._plot_target_viewbox = self.viewBox
16953
- self.plotDispersion()
16954
- # Reset target widget after plotting
16955
- self._plot_target_widget = None
16956
- self._plot_target_viewbox = None
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
- self.pseudosection_use_wavelength = bool(state)
19739
- # Replot whichever view is showing pseudo-section
19740
- if getattr(self, 'bottomPlotType', None) == "pseudosection":
19741
- self._plot_target_widget = self.bottomPlotWidget
19742
- self._plot_target_viewbox = self.bottomViewBox
19743
- self.plotPseudoSection()
19744
- self._plot_target_widget = None
19745
- self._plot_target_viewbox = None
19746
- if getattr(self, 'topPlotType', None) == "pseudosection":
19747
- self._plot_target_widget = self.plotWidget
19748
- self._plot_target_viewbox = self.viewBox
19749
- self.plotPseudoSection()
19750
- self._plot_target_widget = None
19751
- self._plot_target_viewbox = None
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
- is_checked = bool(state)
19756
- self.pseudosection_show_interpolated = is_checked
19757
-
19758
- # Synchronize with dispersion view interpolate checkbox (block signals to avoid recursion)
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
- # 3. Update pseudo-section display
19783
- self._updatePseudoSectionIfVisible()
19784
- else:
19785
- # DISABLE INTERPOLATION: Remove curves and errors from both views
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
- # 1. Remove from dispersion view
19788
- view = self._get_active_dispersion_view()
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
- for mode_num, mode_data in state_dict['modes'].items():
19794
- # Remove interpolated curve
19795
- if mode_data['curve_line'] is not None:
19796
- try:
19797
- plot_widget.removeItem(mode_data['curve_line'])
19798
- except:
19799
- pass
19800
- mode_data['curve_line'] = None
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
- # Remove interpolated error fill (can be single item or list)
19803
- if mode_data['interp_error_fill'] is not None:
19804
- try:
19805
- items = mode_data['interp_error_fill'] if isinstance(mode_data['interp_error_fill'], list) else [mode_data['interp_error_fill']]
19806
- for item in items:
19807
- if item is not None:
19808
- plot_widget.removeItem(item)
19809
- except:
19810
- pass
19811
- mode_data['interp_error_fill'] = None
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
- # Clear curve_data but keep picked_points
19814
- mode_data['curve_data'] = None
20859
+ # Refresh to update display (will redraw picked errors if Show Errors is checked)
20860
+ self._refreshDispersionPicksDisplay()
19815
20861
 
19816
- # Refresh to update display (will redraw picked errors if Show Errors is checked)
19817
- self._refreshDispersionPicksDisplay()
19818
-
19819
- # 2. Update pseudo-section display (will revert to picked points)
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
- self.pseudosection_invert_y = bool(state)
19825
- # Replot whichever view is showing pseudo-section
19826
- if getattr(self, 'bottomPlotType', None) == "pseudosection":
19827
- self._plot_target_widget = self.bottomPlotWidget
19828
- self._plot_target_viewbox = self.bottomViewBox
19829
- self.plotPseudoSection()
19830
- self._plot_target_widget = None
19831
- self._plot_target_viewbox = None
19832
- if getattr(self, 'topPlotType', None) == "pseudosection":
19833
- self._plot_target_widget = self.plotWidget
19834
- self._plot_target_viewbox = self.viewBox
19835
- self.plotPseudoSection()
19836
- self._plot_target_widget = None
19837
- self._plot_target_viewbox = None
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
- self.pseudosection_mode = int(value)
19842
- # Replot whichever view is showing pseudo-section
19843
- if getattr(self, 'bottomPlotType', None) == "pseudosection":
19844
- self._plot_target_widget = self.bottomPlotWidget
19845
- self._plot_target_viewbox = self.bottomViewBox
19846
- self.plotPseudoSection()
19847
- self._plot_target_widget = None
19848
- self._plot_target_viewbox = None
19849
- if getattr(self, 'topPlotType', None) == "pseudosection":
19850
- self._plot_target_widget = self.plotWidget
19851
- self._plot_target_viewbox = self.viewBox
19852
- self.plotPseudoSection()
19853
- self._plot_target_widget = None
19854
- self._plot_target_viewbox = None
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
- self.pseudosection_use_source_x = bool(state)
19859
- if getattr(self, 'bottomPlotType', None) == "pseudosection":
19860
- self._plot_target_widget = self.bottomPlotWidget
19861
- self._plot_target_viewbox = self.bottomViewBox
19862
- self.plotPseudoSection()
19863
- self._plot_target_widget = None
19864
- self._plot_target_viewbox = None
19865
- if getattr(self, 'topPlotType', None) == "pseudosection":
19866
- self._plot_target_widget = self.plotWidget
19867
- self._plot_target_viewbox = self.viewBox
19868
- self.plotPseudoSection()
19869
- self._plot_target_widget = None
19870
- self._plot_target_viewbox = None
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
- # Restore trace ranges for the new shot from memory
19982
- self._restoreTraceRangesFromMemory()
19983
-
19984
- # Set flag to update file display
19985
- self.update_file_flag = True
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
- # Temporarily disconnect the signal to prevent conflicts
19988
- self.fileListWidget.itemSelectionChanged.disconnect(self.onFileSelectionChanged)
19989
-
19990
- # Update file list display to sync the selection
19991
- self.updateFileListDisplay()
19992
-
19993
- # Reconnect the signal
19994
- self.fileListWidget.itemSelectionChanged.connect(self.onFileSelectionChanged)
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
- # Batch the updates to improve performance
19997
- self.updatePlots()
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 single or multiple pygimli .sgt files
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='Source-Geophone-Time file (*.sgt)')
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 SGT files
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())