pyckster 26.2.3__py3-none-any.whl → 26.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyckster/__init__.py CHANGED
@@ -15,7 +15,7 @@ except ImportError:
15
15
  pass # matplotlib not available, that's fine
16
16
 
17
17
  # Define version and metadata in one place
18
- __version__ = "26.2.3"
18
+ __version__ = "26.2.4"
19
19
  __author__ = "Sylvain Pasquet"
20
20
  __email__ = "sylvain.pasquet@sorbonne-universite.fr"
21
21
  __license__ = "GPLv3"
pyckster/core.py CHANGED
@@ -1714,6 +1714,7 @@ class MainWindow(QMainWindow):
1714
1714
 
1715
1715
  # Create a QListWidget for file names and add it to the left
1716
1716
  self.fileListWidget = QListWidget()
1717
+ self.fileListWidget.setSelectionMode(QListWidget.ExtendedSelection) # Enable multi-selection
1717
1718
  self.fileListWidget.itemSelectionChanged.connect(self.onFileSelectionChanged)
1718
1719
  self.fileListWidget.setMinimumWidth(50) # Set minimum width
1719
1720
  leftLayout.addWidget(self.fileListWidget)
@@ -2262,28 +2263,36 @@ class MainWindow(QMainWindow):
2262
2263
  self.openFileAction.triggered.connect(self.openFile)
2263
2264
 
2264
2265
  # Create QAction for importing ASCII matrix
2265
- self.importAsciiAction = QAction('Import ASCII matrix...', self)
2266
+ self.importAsciiAction = QAction('Import ASCII matrix', self)
2266
2267
  self.fileMenu.addAction(self.importAsciiAction)
2267
2268
  self.importAsciiAction.triggered.connect(self.importAsciiMatrix)
2268
2269
 
2269
2270
  # Create QAction for importing ASCII archive
2270
- self.importAsciiArchiveAction = QAction('Import ASCII archive...', self)
2271
+ self.importAsciiArchiveAction = QAction('Import ASCII archive', self)
2271
2272
  self.fileMenu.addAction(self.importAsciiArchiveAction)
2272
2273
  self.importAsciiArchiveAction.triggered.connect(self.importAsciiArchive)
2273
2274
 
2274
2275
  # Add separator
2275
2276
  self.fileMenu.addSeparator()
2276
2277
 
2277
- # Create QAction for stacking shots
2278
- self.stackShotsAction = QAction('Stack shots', self)
2279
- self.fileMenu.addAction(self.stackShotsAction)
2278
+ # Create a submenu for stacking shots
2279
+ self.stackSubMenu = self.fileMenu.addMenu('Stack...')
2280
+
2281
+ # Create QAction for stacking shots with same source position
2282
+ self.stackShotsAction = QAction('shots at same position', self)
2283
+ self.stackSubMenu.addAction(self.stackShotsAction)
2280
2284
  self.stackShotsAction.triggered.connect(self.stackShots)
2281
2285
 
2286
+ # Create QAction for stacking selected shots
2287
+ self.stackSelectedShotsAction = QAction('selected shots', self)
2288
+ self.stackSubMenu.addAction(self.stackSelectedShotsAction)
2289
+ self.stackSelectedShotsAction.triggered.connect(self.stackSelectedShots)
2290
+
2282
2291
  # Add separator
2283
2292
  self.fileMenu.addSeparator()
2284
2293
 
2285
2294
  # Create a submenu for saving single files
2286
- self.saveSingleFileSubMenu = self.fileMenu.addMenu('Save current shot')
2295
+ self.saveSingleFileSubMenu = self.fileMenu.addMenu('Save current shot...')
2287
2296
 
2288
2297
  # Create QAction for saving current file in SEGY
2289
2298
  self.saveSingleFileSegyAction = QAction('in a SEGY file', self)
@@ -2301,7 +2310,7 @@ class MainWindow(QMainWindow):
2301
2310
  self.saveSingleFileAsciiAction.triggered.connect(self.saveSingleFileASCII)
2302
2311
 
2303
2312
  # Create a submenu for saving all files
2304
- self.saveFileSubMenu = self.fileMenu.addMenu('Save all shots')
2313
+ self.saveFileSubMenu = self.fileMenu.addMenu('Save all shots...')
2305
2314
 
2306
2315
  # Create QAction for saving all files in SEGY
2307
2316
  self.saveAllFilesSegyAction = QAction('in separate SEGY files', self)
@@ -2331,10 +2340,10 @@ class MainWindow(QMainWindow):
2331
2340
  # Add separator
2332
2341
  self.fileMenu.addSeparator()
2333
2342
 
2334
- # Create QAction for removing current file
2335
- self.removeShotAction = QAction('Remove current shot', self)
2343
+ # Create QAction for removing selected files
2344
+ self.removeShotAction = QAction('Remove selected shot(s)', self)
2336
2345
  self.fileMenu.addAction(self.removeShotAction)
2337
- self.removeShotAction.triggered.connect(self.removeShot)
2346
+ self.removeShotAction.triggered.connect(self.removeSelectedShots)
2338
2347
 
2339
2348
  # Create QAction for clearing the memory
2340
2349
  self.clearMemoryAction = QAction('Clear memory', self)
@@ -12876,7 +12885,17 @@ class MainWindow(QMainWindow):
12876
12885
  if show_cursor:
12877
12886
  QApplication.restoreOverrideCursor()
12878
12887
 
12888
+ def getSelectedShotIndices(self):
12889
+ """Helper function to get indices of selected shots in the file list.
12890
+ Returns a sorted list of indices (0-based)."""
12891
+ selected_items = self.fileListWidget.selectedItems()
12892
+ if not selected_items:
12893
+ return []
12894
+ selected_indices = [self.fileListWidget.row(item) for item in selected_items]
12895
+ return sorted(selected_indices)
12896
+
12879
12897
  def removeShot(self):
12898
+ """Legacy function - kept for backward compatibility. Use removeSelectedShots instead."""
12880
12899
  if self.currentIndex is not None:
12881
12900
  # Remove the current file from the lists
12882
12901
  for attr in self.attributes_to_initialize:
@@ -12900,6 +12919,59 @@ class MainWindow(QMainWindow):
12900
12919
  else:
12901
12920
  QMessageBox.information(self, "No Shots Remaining", "No shots remaining. Memory will be cleared.")
12902
12921
  self.initMemory()
12922
+
12923
+ def removeSelectedShots(self):
12924
+ """Remove all selected shots from the file list."""
12925
+ # Get selected row indices
12926
+ selected_items = self.fileListWidget.selectedItems()
12927
+ if not selected_items:
12928
+ QMessageBox.warning(self, "No Selection", "No shots selected. Please select one or more shots to remove.")
12929
+ return
12930
+
12931
+ # Get the indices of selected items
12932
+ selected_indices = [self.fileListWidget.row(item) for item in selected_items]
12933
+ selected_indices.sort(reverse=True) # Sort in reverse to remove from end first
12934
+
12935
+ # Confirm removal if multiple shots selected
12936
+ if len(selected_indices) > 1:
12937
+ reply = QMessageBox.question(self, "Confirm Removal",
12938
+ f"Are you sure you want to remove {len(selected_indices)} selected shot(s)?",
12939
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
12940
+ if reply == QMessageBox.No:
12941
+ return
12942
+
12943
+ # Remove selected shots (from end to beginning to maintain indices)
12944
+ for idx in selected_indices:
12945
+ for attr in self.attributes_to_initialize:
12946
+ getattr(self, attr).pop(idx)
12947
+
12948
+ # Update current index to the first remaining shot or 0
12949
+ if self.streams:
12950
+ # Set to the first valid index that's not removed
12951
+ remaining_count = len(self.fileNames)
12952
+ if remaining_count > 0:
12953
+ # Try to select the shot after the last removed one, or the last available
12954
+ new_index = min(selected_indices) if selected_indices else 0
12955
+ self.currentIndex = min(new_index, remaining_count - 1)
12956
+ else:
12957
+ self.currentIndex = 0
12958
+ else:
12959
+ self.currentIndex = 0
12960
+
12961
+ # Update the plot type dictionary
12962
+ self.updatePlotTypeDict()
12963
+
12964
+ # Update the file list display
12965
+ self.updateFileListDisplay()
12966
+
12967
+ # Update selected file in the list display
12968
+ if self.streams:
12969
+ self.fileListWidget.setCurrentRow(self.currentIndex)
12970
+ # Update the plot
12971
+ self.updatePlots()
12972
+ else:
12973
+ QMessageBox.information(self, "No Shots Remaining", "No shots remaining. Memory will be cleared.")
12974
+ self.initMemory()
12903
12975
 
12904
12976
  #######################################
12905
12977
  # Saving shot functions
@@ -16806,9 +16878,18 @@ class MainWindow(QMainWindow):
16806
16878
 
16807
16879
  def batchEditFFID(self):
16808
16880
  if self.streams:
16881
+ # Get selected shot indices for default values (only if 2+ shots selected)
16882
+ selected_indices = self.getSelectedShotIndices()
16883
+ if len(selected_indices) >= 2:
16884
+ first_source_default = selected_indices[0] + 1
16885
+ last_source_default = selected_indices[-1] + 1
16886
+ else:
16887
+ first_source_default = 1
16888
+ last_source_default = len(self.streams)
16889
+
16809
16890
  parameters = [
16810
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16811
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
16891
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
16892
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
16812
16893
  {'label': 'First FFID', 'initial_value': self.ffid[0], 'type': 'int'},
16813
16894
  {'label': 'Increment', 'initial_value': 1, 'type': 'int'}
16814
16895
  ]
@@ -16845,9 +16926,18 @@ class MainWindow(QMainWindow):
16845
16926
 
16846
16927
  def batchEditDelay(self):
16847
16928
  if self.streams:
16929
+ # Get selected shot indices for default values (only if 2+ shots selected)
16930
+ selected_indices = self.getSelectedShotIndices()
16931
+ if len(selected_indices) >= 2:
16932
+ first_source_default = selected_indices[0] + 1
16933
+ last_source_default = selected_indices[-1] + 1
16934
+ else:
16935
+ first_source_default = 1
16936
+ last_source_default = len(self.streams)
16937
+
16848
16938
  parameters = [
16849
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16850
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
16939
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
16940
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
16851
16941
  {'label': 'Delay (in s)', 'initial_value': self.delay[self.currentIndex], 'type': 'float'},
16852
16942
  ]
16853
16943
 
@@ -16925,9 +17015,18 @@ class MainWindow(QMainWindow):
16925
17015
 
16926
17016
  def batchEditSourcePosition(self):
16927
17017
  if self.streams:
17018
+ # Get selected shot indices for default values (only if 2+ shots selected)
17019
+ selected_indices = self.getSelectedShotIndices()
17020
+ if len(selected_indices) >= 2:
17021
+ first_source_default = selected_indices[0] + 1
17022
+ last_source_default = selected_indices[-1] + 1
17023
+ else:
17024
+ first_source_default = 1
17025
+ last_source_default = len(self.streams)
17026
+
16928
17027
  parameters = [
16929
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16930
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17028
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17029
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
16931
17030
  {'label': 'Skip every N sources', 'initial_value': 0, 'type': 'int'},
16932
17031
  {'label': 'First Source Position (in m)', 'initial_value': self.source_position[0], 'type': 'float'},
16933
17032
  {'label': 'Last Source Position (in m)', 'initial_value': '', 'type': 'float_or_empty'},
@@ -17033,9 +17132,18 @@ class MainWindow(QMainWindow):
17033
17132
 
17034
17133
  def batchEditTracePosition(self):
17035
17134
  if self.streams:
17135
+ # Get selected shot indices for default values (only if 2+ shots selected)
17136
+ selected_indices = self.getSelectedShotIndices()
17137
+ if len(selected_indices) >= 2:
17138
+ first_source_default = selected_indices[0] + 1
17139
+ last_source_default = selected_indices[-1] + 1
17140
+ else:
17141
+ first_source_default = 1
17142
+ last_source_default = len(self.streams)
17143
+
17036
17144
  parameters = [
17037
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17038
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17145
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17146
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17039
17147
  {'label': 'First Trace #', 'initial_value': 1, 'type': 'int'},
17040
17148
  {'label': 'Last Trace #', 'initial_value': len(self.trace_position[self.currentIndex]), 'type': 'int'},
17041
17149
  {'label': 'First Trace Position (in m)', 'initial_value': self.trace_position[self.currentIndex][0], 'type': 'float'},
@@ -17095,9 +17203,18 @@ class MainWindow(QMainWindow):
17095
17203
 
17096
17204
  def batchSwapTraces(self):
17097
17205
  if self.streams:
17206
+ # Get selected shot indices for default values (only if 2+ shots selected)
17207
+ selected_indices = self.getSelectedShotIndices()
17208
+ if len(selected_indices) >= 2:
17209
+ first_source_default = selected_indices[0] + 1
17210
+ last_source_default = selected_indices[-1] + 1
17211
+ else:
17212
+ first_source_default = 1
17213
+ last_source_default = len(self.streams)
17214
+
17098
17215
  parameters = [
17099
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17100
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17216
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17217
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17101
17218
  {'label': 'First Trace # to swap', 'initial_value': 1, 'type': 'int'},
17102
17219
  {'label': 'Second Trace # to swap', 'initial_value': 2, 'type': 'int'}
17103
17220
  ]
@@ -17163,9 +17280,18 @@ class MainWindow(QMainWindow):
17163
17280
 
17164
17281
  def batchRemoveTraces(self):
17165
17282
  if self.streams:
17283
+ # Get selected shot indices for default values (only if 2+ shots selected)
17284
+ selected_indices = self.getSelectedShotIndices()
17285
+ if len(selected_indices) >= 2:
17286
+ first_source_default = selected_indices[0] + 1
17287
+ last_source_default = selected_indices[-1] + 1
17288
+ else:
17289
+ first_source_default = 1
17290
+ last_source_default = len(self.streams)
17291
+
17166
17292
  parameters = [
17167
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17168
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17293
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17294
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17169
17295
  {'label': 'First Trace # to remove', 'initial_value': 1, 'type': 'int'},
17170
17296
  {'label': 'Last Trace # to remove', 'initial_value': len(self.trace_position[self.currentIndex]), 'type': 'int'}
17171
17297
  ]
@@ -17229,9 +17355,18 @@ class MainWindow(QMainWindow):
17229
17355
 
17230
17356
  def batchMoveTraces(self):
17231
17357
  if self.streams:
17358
+ # Get selected shot indices for default values (only if 2+ shots selected)
17359
+ selected_indices = self.getSelectedShotIndices()
17360
+ if len(selected_indices) >= 2:
17361
+ first_source_default = selected_indices[0] + 1
17362
+ last_source_default = selected_indices[-1] + 1
17363
+ else:
17364
+ first_source_default = 1
17365
+ last_source_default = len(self.streams)
17366
+
17232
17367
  parameters = [
17233
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17234
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17368
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17369
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17235
17370
  {'label': 'First Trace # to move', 'initial_value': 1, 'type': 'int'},
17236
17371
  {'label': 'Last Trace # to move', 'initial_value': 1, 'type': 'int'},
17237
17372
  {'label': 'New Position', 'initial_value': 1, 'type': 'int'}
@@ -17298,9 +17433,18 @@ class MainWindow(QMainWindow):
17298
17433
 
17299
17434
  def batchMuteTraces(self):
17300
17435
  if self.streams:
17436
+ # Get selected shot indices for default values (only if 2+ shots selected)
17437
+ selected_indices = self.getSelectedShotIndices()
17438
+ if len(selected_indices) >= 2:
17439
+ first_source_default = selected_indices[0] + 1
17440
+ last_source_default = selected_indices[-1] + 1
17441
+ else:
17442
+ first_source_default = 1
17443
+ last_source_default = len(self.streams)
17444
+
17301
17445
  parameters = [
17302
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17303
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17446
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17447
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17304
17448
  {'label': 'First Trace # to mute', 'initial_value': 1, 'type': 'int'},
17305
17449
  {'label': 'Last Trace # to mute', 'initial_value': len(self.trace_position[self.currentIndex]), 'type': 'int'}
17306
17450
  ]
@@ -17350,11 +17494,20 @@ class MainWindow(QMainWindow):
17350
17494
  def batchInsertMutedTraces(self):
17351
17495
  """Batch insert zero traces for a range of shots."""
17352
17496
  if self.streams:
17497
+ # Get selected shot indices for default values (only if 2+ shots selected)
17498
+ selected_indices = self.getSelectedShotIndices()
17499
+ if len(selected_indices) >= 2:
17500
+ first_source_default = selected_indices[0] + 1
17501
+ last_source_default = selected_indices[-1] + 1
17502
+ else:
17503
+ first_source_default = 1
17504
+ last_source_default = len(self.streams)
17505
+
17353
17506
  n_traces = len(self.trace_position[self.currentIndex]) if self.currentIndex < len(self.trace_position) else 1
17354
17507
 
17355
17508
  parameters = [
17356
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17357
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17509
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17510
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17358
17511
  {'label': 'Insert after Trace # (0 for beginning)', 'initial_value': n_traces, 'type': 'int'},
17359
17512
  {'label': 'Number of traces to insert', 'initial_value': 1, 'type': 'int'}
17360
17513
  ]
@@ -17407,9 +17560,18 @@ class MainWindow(QMainWindow):
17407
17560
  def batchZeroPadTraces(self):
17408
17561
  """Batch zero pad all traces for a range of shots."""
17409
17562
  if self.streams:
17563
+ # Get selected shot indices for default values (only if 2+ shots selected)
17564
+ selected_indices = self.getSelectedShotIndices()
17565
+ if len(selected_indices) >= 2:
17566
+ first_source_default = selected_indices[0] + 1
17567
+ last_source_default = selected_indices[-1] + 1
17568
+ else:
17569
+ first_source_default = 1
17570
+ last_source_default = len(self.streams)
17571
+
17410
17572
  parameters = [
17411
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17412
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17573
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17574
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17413
17575
  {'label': 'Pad at end (seconds)', 'initial_value': 0.0, 'type': 'float'}
17414
17576
  ]
17415
17577
 
@@ -17470,11 +17632,20 @@ class MainWindow(QMainWindow):
17470
17632
  def batchReverseTraces(self):
17471
17633
  """Batch reverse trace data (flip data matrix left-to-right) while keeping headers in place"""
17472
17634
  if self.streams:
17635
+ # Get selected shot indices for default values (only if 2+ shots selected)
17636
+ selected_indices = self.getSelectedShotIndices()
17637
+ if len(selected_indices) >= 2:
17638
+ first_source_default = selected_indices[0] + 1
17639
+ last_source_default = selected_indices[-1] + 1
17640
+ else:
17641
+ first_source_default = 1
17642
+ last_source_default = len(self.streams)
17643
+
17473
17644
  n_traces = len(self.trace_position[self.currentIndex]) if self.currentIndex < len(self.trace_position) else 1
17474
17645
 
17475
17646
  parameters = [
17476
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17477
- {'label': 'Last Source #', 'initial_value': len(self.streams), 'type': 'int'},
17647
+ {'label': 'First Source #', 'initial_value': first_source_default, 'type': 'int'},
17648
+ {'label': 'Last Source #', 'initial_value': last_source_default, 'type': 'int'},
17478
17649
  {'label': 'First Trace #', 'initial_value': 1, 'type': 'int'},
17479
17650
  {'label': 'Last Trace #', 'initial_value': n_traces, 'type': 'int'}
17480
17651
  ]
@@ -17838,6 +18009,203 @@ class MainWindow(QMainWindow):
17838
18009
  QMessageBox.information(self, "Stacking Complete",
17839
18010
  f"Successfully created {stacked_shot_count} stacked shot(s).")
17840
18011
 
18012
+ def stackSelectedShots(self):
18013
+ """
18014
+ Stack selected shots regardless of their source position.
18015
+ Creates a new stacked shot with traces summed at identical receiver positions.
18016
+ Uses the source position from the first selected shot.
18017
+ """
18018
+ import obspy
18019
+ from obspy import Stream
18020
+
18021
+ if not self.streams:
18022
+ QMessageBox.information(self, "No Data", "No shots loaded. Please load shot data first.")
18023
+ return
18024
+
18025
+ # Get selected shot indices
18026
+ selected_indices = self.getSelectedShotIndices()
18027
+
18028
+ if len(selected_indices) < 2:
18029
+ QMessageBox.warning(self, "Insufficient Selection",
18030
+ "Please select at least 2 shots to stack. Use Ctrl+Click to select multiple shots.")
18031
+ return
18032
+
18033
+ # Build a dictionary of receiver positions across all selected shots
18034
+ # Key: receiver position, Value: list of (shot_idx, trace_idx) tuples
18035
+ receiver_traces = {}
18036
+
18037
+ for shot_idx in selected_indices:
18038
+ for trace_idx, receiver_pos in enumerate(self.trace_position[shot_idx]):
18039
+ if receiver_pos not in receiver_traces:
18040
+ receiver_traces[receiver_pos] = []
18041
+ receiver_traces[receiver_pos].append((shot_idx, trace_idx))
18042
+
18043
+ # Get unique receiver positions (sorted)
18044
+ receiver_positions = sorted(receiver_traces.keys())
18045
+
18046
+ if not receiver_positions:
18047
+ QMessageBox.warning(self, "No Receiver Positions",
18048
+ "Could not find any receiver positions in selected shots.")
18049
+ return
18050
+
18051
+ # Use source position from first selected shot
18052
+ first_shot_idx = selected_indices[0]
18053
+ source_pos = self.source_position[first_shot_idx]
18054
+
18055
+ # Show confirmation dialog
18056
+ shot_indices_str = ', '.join(str(i + 1) for i in selected_indices)
18057
+ reply = QMessageBox.question(self, "Confirm Stacking",
18058
+ f"Stack {len(selected_indices)} selected shot(s) ({shot_indices_str})?\n\n"
18059
+ f"Source position: {source_pos}m\n"
18060
+ f"Number of unique receiver positions: {len(receiver_positions)}\n\n"
18061
+ f"Traces at the same receiver position will be summed.",
18062
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
18063
+
18064
+ if reply == QMessageBox.No:
18065
+ return
18066
+
18067
+ QApplication.setOverrideCursor(Qt.WaitCursor)
18068
+ try:
18069
+ # Create stacked stream
18070
+ stacked_stream = obspy.Stream()
18071
+
18072
+ # For each receiver position, sum all traces at that position
18073
+ for receiver_pos in receiver_positions:
18074
+ trace_list = receiver_traces[receiver_pos]
18075
+
18076
+ # Get all traces at this receiver position
18077
+ traces_to_sum = []
18078
+ for shot_idx, trace_idx in trace_list:
18079
+ traces_to_sum.append(self.streams[shot_idx][trace_idx])
18080
+
18081
+ # Sum the traces
18082
+ if traces_to_sum:
18083
+ summed_trace = traces_to_sum[0].copy()
18084
+ for trace in traces_to_sum[1:]:
18085
+ summed_trace.data += trace.data
18086
+ stacked_stream.append(summed_trace)
18087
+
18088
+ if len(stacked_stream) == 0:
18089
+ QMessageBox.warning(self, "No Traces", "Could not create stacked traces.")
18090
+ return
18091
+
18092
+ # Find the maximum existing FFID and increment
18093
+ max_ffid = max(self.ffid) if self.ffid else 0
18094
+ new_ffid = max_ffid + 1
18095
+
18096
+ # Get format from first shot
18097
+ format_to_use = self.input_format[first_shot_idx]
18098
+
18099
+ # Update FFID in headers for stacked stream
18100
+ for trace in stacked_stream:
18101
+ trace.stats[format_to_use].trace_header.original_field_record_number = new_ffid
18102
+
18103
+ # Append to parallel arrays
18104
+ stacked_shot_name = f"stacked_selected_{new_ffid}"
18105
+ self.fileNames.append(stacked_shot_name)
18106
+ self.streams.append(stacked_stream)
18107
+ self.input_format.append(format_to_use)
18108
+
18109
+ # Extract data from the stacked stream
18110
+ n_sample_stacked = len(stacked_stream[0].data)
18111
+ self.n_sample.append(n_sample_stacked)
18112
+
18113
+ # Get sample interval from first shot
18114
+ sample_interval_stacked = self.sample_interval[first_shot_idx]
18115
+ self.sample_interval.append(sample_interval_stacked)
18116
+
18117
+ # Get delay from first shot
18118
+ delay_stacked = self.delay[first_shot_idx]
18119
+ self.delay.append(delay_stacked)
18120
+
18121
+ # Generate time array
18122
+ time_stacked = np.arange(n_sample_stacked) * sample_interval_stacked + delay_stacked
18123
+ self.time.append(time_stacked)
18124
+
18125
+ # Get record length
18126
+ record_length_stacked = self.record_length[first_shot_idx]
18127
+ self.record_length.append(record_length_stacked)
18128
+
18129
+ # FFID
18130
+ self.ffid.append(new_ffid)
18131
+
18132
+ # Source position (from first selected shot)
18133
+ self.source_position.append(source_pos)
18134
+
18135
+ # Trace numbering
18136
+ shot_trace_numbers = [i + 1 for i in range(len(stacked_stream))]
18137
+ self.shot_trace_number.append(shot_trace_numbers)
18138
+
18139
+ # Trace positions (receiver positions, sorted)
18140
+ self.trace_position.append(receiver_positions)
18141
+
18142
+ # File trace numbers (sequential)
18143
+ file_trace_numbers = list(range(1, len(stacked_stream) + 1))
18144
+ self.file_trace_number.append(file_trace_numbers)
18145
+
18146
+ # Initialize unique_trace_number (will be computed later)
18147
+ self.unique_trace_number.append(None)
18148
+
18149
+ # Trace elevations (from first occurrence of each position)
18150
+ trace_elevations_stacked = []
18151
+ for receiver_pos in receiver_positions:
18152
+ # Find first occurrence of this position
18153
+ for shot_idx, trace_idx in receiver_traces[receiver_pos]:
18154
+ trace_elevations_stacked.append(self.trace_elevation[shot_idx][trace_idx])
18155
+ break
18156
+ self.trace_elevation.append(trace_elevations_stacked)
18157
+
18158
+ # Source elevation (from first shot)
18159
+ self.source_elevation.append(self.source_elevation[first_shot_idx])
18160
+
18161
+ # Offset (distance from source to each receiver)
18162
+ offsets_stacked = [abs(rp - source_pos) for rp in receiver_positions]
18163
+ self.offset.append(offsets_stacked)
18164
+
18165
+ # Initialize picks, errors, and UI items as None
18166
+ self.picks.append(None)
18167
+ self.error.append(None)
18168
+ self.pickSeismoItems.append(None)
18169
+ self.pickLayoutItems.append(None)
18170
+ self.airWaveItems.append(None)
18171
+
18172
+ # Update global trace numbering
18173
+ self._updateAllUniqueTraceNumbers()
18174
+
18175
+ # Sync headers
18176
+ self.syncHeadersToStreams(len(self.streams) - 1)
18177
+ self.headers_modified = True
18178
+
18179
+ # Update display
18180
+ self.updateFileListDisplay()
18181
+ self.updatePlots()
18182
+
18183
+ # Ask if user wants to remove the selected shots
18184
+ reply = QMessageBox.question(self, "Remove Selected Shots",
18185
+ "Remove the selected shots that were used in stacking?",
18186
+ QMessageBox.Yes | QMessageBox.No)
18187
+
18188
+ if reply == QMessageBox.Yes:
18189
+ # Remove shots in reverse order to preserve indices
18190
+ for idx in reversed(selected_indices):
18191
+ for attr in self.attributes_to_initialize:
18192
+ getattr(self, attr).pop(idx)
18193
+
18194
+ # Update display
18195
+ self.updateFileListDisplay()
18196
+ if self.streams:
18197
+ self.currentIndex = min(self.currentIndex, len(self.streams) - 1)
18198
+ self.fileListWidget.setCurrentRow(self.currentIndex)
18199
+ self.updatePlots()
18200
+
18201
+ QMessageBox.information(self, "Stacking Complete",
18202
+ f"Successfully stacked {len(selected_indices)} shot(s) into FFID {new_ffid}.\n"
18203
+ f"Source position: {source_pos}m\n"
18204
+ f"Number of traces: {len(receiver_positions)}")
18205
+
18206
+ finally:
18207
+ QApplication.restoreOverrideCursor()
18208
+
17841
18209
  #######################################
17842
18210
  # Set parameters functions
17843
18211
  #######################################
pyckster/obspy_utils.py CHANGED
@@ -2,6 +2,7 @@ import os
2
2
  import re
3
3
  import numpy as np
4
4
  import obspy
5
+ import warnings
5
6
 
6
7
  #######################################
7
8
  # Obspy functions
@@ -63,39 +64,49 @@ def read_seismic_file(seismic_file, separate_sources=False):
63
64
  stream : obspy.Stream
64
65
  The stream object containing the seismic data.
65
66
  '''
66
- # Validate .dat files are actually binary, not ASCII
67
- file_ext = os.path.splitext(seismic_file)[1].lower()
68
- if file_ext == '.dat':
69
- if not is_valid_binary_seismic_file(seismic_file):
70
- raise ValueError(
71
- f"File '{os.path.basename(seismic_file)}' appears to be an ASCII text file, "
72
- f"not a binary Seg2 seismic file. Please verify the file format."
73
- )
74
-
75
- # Determine format based on file extension
76
- format_map = {
77
- '.sgy': 'SEGY',
78
- '.seg': 'SEGY',
79
- '.segy': 'SEGY',
80
- '.su': 'SU',
81
- '.seg2': 'SEG2',
82
- '.sg2': 'SEG2',
83
- '.dat': 'SEG2', # .dat files are typically SEG2
84
- }
85
-
86
- # Get format from extension, default to None (let obspy auto-detect)
87
- file_format = format_map.get(file_ext, None)
88
-
89
- # Read the seismic file with explicit format if known
90
- if file_format:
91
- stream = obspy.read(seismic_file, format=file_format, unpack_trace_headers=True)
92
- else:
93
- stream = obspy.read(seismic_file, unpack_trace_headers=True)
67
+ # Suppress expected warnings from ObsPy's SEG2 reader
68
+ # These warnings are expected and handled by our code:
69
+ # 1. DELAY field warning - we manually extract and apply the delay
70
+ # 2. Custom header warning - we handle custom SEG2 headers appropriately
71
+ # 3. Creating trace header - expected during SEG2 to SEGY conversion
72
+ with warnings.catch_warnings():
73
+ warnings.filterwarnings('ignore', message='.*DELAY.*', category=UserWarning)
74
+ warnings.filterwarnings('ignore', message='.*custom defined SEG2 header.*', category=UserWarning)
75
+ warnings.filterwarnings('ignore', message='CREATING TRACE HEADER', category=UserWarning)
76
+
77
+ # Validate .dat files are actually binary, not ASCII
78
+ file_ext = os.path.splitext(seismic_file)[1].lower()
79
+ if file_ext == '.dat':
80
+ if not is_valid_binary_seismic_file(seismic_file):
81
+ raise ValueError(
82
+ f"File '{os.path.basename(seismic_file)}' appears to be an ASCII text file, "
83
+ f"not a binary Seg2 seismic file. Please verify the file format."
84
+ )
85
+
86
+ # Determine format based on file extension
87
+ format_map = {
88
+ '.sgy': 'SEGY',
89
+ '.seg': 'SEGY',
90
+ '.segy': 'SEGY',
91
+ '.su': 'SU',
92
+ '.seg2': 'SEG2',
93
+ '.sg2': 'SEG2',
94
+ '.dat': 'SEG2', # .dat files are typically SEG2
95
+ }
96
+
97
+ # Get format from extension, default to None (let obspy auto-detect)
98
+ file_format = format_map.get(file_ext, None)
99
+
100
+ # Read the seismic file with explicit format if known
101
+ if file_format:
102
+ stream = obspy.read(seismic_file, format=file_format, unpack_trace_headers=True)
103
+ else:
104
+ stream = obspy.read(seismic_file, unpack_trace_headers=True)
94
105
 
95
106
  input_format = check_format(stream)
96
107
 
97
108
  if input_format == 'seg2':
98
- # Preserve original SEG2 coordinate information before conversion
109
+ # Preserve original SEG2 coordinate information and delay before conversion
99
110
  original_seg2_coords = []
100
111
  for trace_index, trace in enumerate(stream):
101
112
  coord_info = {
@@ -105,13 +116,24 @@ def read_seismic_file(seismic_file, separate_sources=False):
105
116
  'source_x': 0.0,
106
117
  'source_y': 0.0,
107
118
  'source_z': 0.0,
119
+ 'delay': 0.0,
108
120
  'trace_index': trace_index
109
121
  }
110
122
 
111
- # Extract coordinates from SEG2 headers with multiple field name variations
123
+ # Extract coordinates and delay from SEG2 headers with multiple field name variations
112
124
  if hasattr(trace.stats, 'seg2'):
113
125
  seg2 = trace.stats.seg2
114
126
 
127
+ # Extract DELAY from SEG2 headers
128
+ delay_fields = ['DELAY', 'DELAY_TIME', 'RECORDING_DELAY']
129
+ for field in delay_fields:
130
+ if hasattr(seg2, field):
131
+ try:
132
+ coord_info['delay'] = float(getattr(seg2, field))
133
+ break
134
+ except (ValueError, TypeError):
135
+ continue
136
+
115
137
  # Try different field names for receiver location
116
138
  receiver_location_fields = ['RECEIVER_LOCATION', 'RECEIVER_LOCATION_X', 'RECEIVER_X', 'REC_X']
117
139
  for field in receiver_location_fields:
@@ -170,8 +192,11 @@ def read_seismic_file(seismic_file, separate_sources=False):
170
192
  else:
171
193
  ffid = 1
172
194
 
173
- stream.write('tmp.sgy',format='SEGY',data_encoding=5, byteorder='>')
174
- stream = obspy.read('tmp.sgy',unpack_trace_headers=True)
195
+ # Suppress warnings during SEG2 to SEGY conversion
196
+ with warnings.catch_warnings():
197
+ warnings.filterwarnings('ignore', message='CREATING TRACE HEADER', category=UserWarning)
198
+ stream.write('tmp.sgy',format='SEGY',data_encoding=5, byteorder='>')
199
+ stream = obspy.read('tmp.sgy',unpack_trace_headers=True)
175
200
  os.remove('tmp.sgy')
176
201
 
177
202
  # Calculate coordinate scalar for preserved coordinates
@@ -204,6 +229,8 @@ def read_seismic_file(seismic_file, separate_sources=False):
204
229
  trace.stats[input_format].trace_header.source_coordinate_x = int(coord_info['source_x'] * coordinate_scalar)
205
230
  trace.stats[input_format].trace_header.source_coordinate_y = int(coord_info['source_y'] * coordinate_scalar)
206
231
  trace.stats[input_format].trace_header.surface_elevation_at_source = int(coord_info['source_z'] * coordinate_scalar)
232
+ # Apply delay from SEG2 header (convert to milliseconds for SEGY)
233
+ trace.stats[input_format].trace_header.delay_recording_time = int(coord_info['delay'] * 1000)
207
234
 
208
235
  if separate_sources:
209
236
  stream = separate_streams(stream)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyckster
3
- Version: 26.2.3
3
+ Version: 26.2.4
4
4
  Summary: A PyQt5-based GUI for the processing and analysis of active near-surface seismic data
5
5
  Home-page: https://gitlab.in2p3.fr/metis-geophysics/pyckster
6
6
  Author: Sylvain Pasquet
@@ -1,15 +1,15 @@
1
- pyckster/__init__.py,sha256=qESM7e1M2mtkZ_qclsH4wVv39yjHCQrkPg1gO0xze7E,905
1
+ pyckster/__init__.py,sha256=hyEyaD3gWhebOlI1NcoMePXDldgUNNoe7mZ-l0lIm-Y,905
2
2
  pyckster/__main__.py,sha256=zv3AGVKorKo2tgWOEIcVnkDbp15eepSqka3IoWH_adU,406
3
3
  pyckster/auto_picking.py,sha256=fyZiOj0Ib-SB_oxsKnUszECHbOjo4JE23JVQILGYZco,12754
4
4
  pyckster/bayesian_inversion.py,sha256=kdnKOlAZ0JlYLipuFDHlwS7dU8LtI-0aMb90bYpEHhE,163523
5
- pyckster/core.py,sha256=fcrnR7L18Gwc5AiXj5aWpdqRnjPDZbyWOFSyg0FIsa8,1250792
5
+ pyckster/core.py,sha256=HDJz6UB2TaGM-i68BRgb9X2poM3CJQDiOE3Is_IwG7w,1268015
6
6
  pyckster/dispersion_stack_viewer.py,sha256=7Dh2e1tSct062D7Qh6nNrMdJcqKWcJvDIv84V8sC6C8,12645
7
7
  pyckster/inversion_app.py,sha256=ovM44oYBFsvfKxO7rjjThUhkJnLDLZZ0R6ZVp-5r66E,60676
8
8
  pyckster/inversion_manager.py,sha256=P8i1fqUJKMWkd-9PoDmNtmQuKglGKTeSuptUUA57D-8,15393
9
9
  pyckster/inversion_visualizer.py,sha256=vfKZIoJzKawbaEv29NsYYIGnWLDQCGef5bM2vY1aCBo,22135
10
10
  pyckster/ipython_console.py,sha256=tZyyoiXCjCl7ozxOj_h-YR4eGjoC4kpKe7nZ48eUAJc,9313
11
11
  pyckster/mpl_export.py,sha256=_WqPo9l9ABiSoU0ukLfm4caGV1-FKKbXjt8SoBHTR30,12346
12
- pyckster/obspy_utils.py,sha256=wm74oyLvvQmF97_ySXPZD95ya-8Aer-pLyVYlA3QWoM,35420
12
+ pyckster/obspy_utils.py,sha256=Rb0-HAZpil7LxDOMtoKqxlSYL71LVe9rAMrT0PRHXbU,37132
13
13
  pyckster/pick_io.py,sha256=r7QoRCA2zaGeZlFKuAJ86KnAH_mh_l4qGvP02Q0mwVA,36001
14
14
  pyckster/pyqtgraph_utils.py,sha256=PAeE3n_wz7skHOC5eLnkFczbie7diVH1xvuL8jtJ4T8,6049
15
15
  pyckster/surface_wave_analysis.py,sha256=97BrDA-n5AZp89NdxQ2ekZPaCErMc7v8C6GmD5KTi-4,102695
@@ -17,9 +17,9 @@ pyckster/surface_wave_profiling.py,sha256=L9KidhKmfGvVoPZjf6us3c49VB7VPB_VcsDqRx
17
17
  pyckster/sw_utils.py,sha256=-2CpQ9BkmUHaMBrNy2qXx1R-g9qPX8D9igKi_G-iRHE,13213
18
18
  pyckster/tab_factory.py,sha256=NlCIC6F8BrEu7a8BYOJJdWy5ftpX_zKDLj7SbcwBbh8,14519
19
19
  pyckster/visualization_utils.py,sha256=bgODn21NAQx1FOMPj91kdDd0szKOgUyfZ3cQlyu2PF8,47947
20
- pyckster-26.2.3.dist-info/licenses/LICENCE,sha256=-uaAIm20JrJKoMdCdn2GlFQfNU4fbsHWK3eh4kIQ_Ec,35143
21
- pyckster-26.2.3.dist-info/METADATA,sha256=lUsnI0wr-TluggRLaC0MF5rUyFfm-VMhZ0H0YK5uZk0,4567
22
- pyckster-26.2.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
- pyckster-26.2.3.dist-info/entry_points.txt,sha256=yrOQx1wHi84rbxX_ZYtYaVcK3EeuRhHRQDZRc8mB0NI,100
24
- pyckster-26.2.3.dist-info/top_level.txt,sha256=eaihhwhEmlysgdZE4HmELFdSUwlXcMv90YorkjOXujQ,9
25
- pyckster-26.2.3.dist-info/RECORD,,
20
+ pyckster-26.2.4.dist-info/licenses/LICENCE,sha256=-uaAIm20JrJKoMdCdn2GlFQfNU4fbsHWK3eh4kIQ_Ec,35143
21
+ pyckster-26.2.4.dist-info/METADATA,sha256=TKeHttAjV2XDga_kdw2glW3b3c6GMP2GczZCK2L0tVk,4567
22
+ pyckster-26.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
+ pyckster-26.2.4.dist-info/entry_points.txt,sha256=yrOQx1wHi84rbxX_ZYtYaVcK3EeuRhHRQDZRc8mB0NI,100
24
+ pyckster-26.2.4.dist-info/top_level.txt,sha256=eaihhwhEmlysgdZE4HmELFdSUwlXcMv90YorkjOXujQ,9
25
+ pyckster-26.2.4.dist-info/RECORD,,