pyckster 26.2.2__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.2"
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)
@@ -11663,7 +11672,7 @@ class MainWindow(QMainWindow):
11663
11672
  self.skip_header_edit = QSpinBox()
11664
11673
  self.skip_header_edit.setRange(0, 1000)
11665
11674
  self.skip_header_edit.setValue(0)
11666
- self.skip_header_edit.setToolTip("Number of header lines to skip in the ASCII file")
11675
+ self.skip_header_edit.setToolTip("Number of lines to skip from the beginning of the file (counts all lines including comments)")
11667
11676
  data_layout.addRow("Skip header lines:", self.skip_header_edit)
11668
11677
 
11669
11678
  self.delimiter_edit = QLineEdit()
@@ -11738,7 +11747,24 @@ class MainWindow(QMainWindow):
11738
11747
  successful_imports = 0
11739
11748
  failed_imports = []
11740
11749
 
11741
- for file_path in params['file_paths']:
11750
+ # Create progress dialog for batch import
11751
+ progress = None
11752
+ if len(params['file_paths']) > 1:
11753
+ progress = QProgressDialog("Importing ASCII matrices...", "Cancel", 0, len(params['file_paths']), self)
11754
+ progress.setWindowTitle("Importing ASCII Matrices")
11755
+ progress.setMinimumDuration(0)
11756
+ progress.setWindowModality(QtCore.Qt.WindowModal)
11757
+ progress.setValue(0)
11758
+ progress.show()
11759
+ QApplication.processEvents()
11760
+
11761
+ for idx, file_path in enumerate(params['file_paths']):
11762
+ if progress:
11763
+ progress.setValue(idx)
11764
+ if progress.wasCanceled():
11765
+ break
11766
+ progress.setLabelText(f"Importing file {idx+1} of {len(params['file_paths'])}...")
11767
+ QApplication.processEvents()
11742
11768
  try:
11743
11769
  # Load the ASCII matrix
11744
11770
  QApplication.processEvents()
@@ -11760,13 +11786,15 @@ class MainWindow(QMainWindow):
11760
11786
  delimiter = None # Let numpy handle it
11761
11787
 
11762
11788
  # Load the data with skiprows parameter
11789
+ # Set comments=None to disable automatic comment skipping (numpy default is comments='#')
11790
+ # This ensures skiprows counts ALL lines as documented
11763
11791
  try:
11764
- data_matrix = np.loadtxt(file_path, delimiter=delimiter, skiprows=params['skip_header'])
11792
+ data_matrix = np.loadtxt(file_path, delimiter=delimiter, skiprows=params['skip_header'], comments=None)
11765
11793
  except ValueError:
11766
11794
  # Try with different delimiters
11767
11795
  for delim in [None, ',', '\t', ' ']:
11768
11796
  try:
11769
- data_matrix = np.loadtxt(file_path, delimiter=delim, skiprows=params['skip_header'])
11797
+ data_matrix = np.loadtxt(file_path, delimiter=delim, skiprows=params['skip_header'], comments=None)
11770
11798
  break
11771
11799
  except ValueError:
11772
11800
  continue
@@ -11781,6 +11809,53 @@ class MainWindow(QMainWindow):
11781
11809
  if data_matrix.ndim == 1:
11782
11810
  data_matrix = data_matrix.reshape(-1, 1)
11783
11811
 
11812
+ # Auto-detect orientation: number of traces should typically be less than number of time samples
11813
+ # This helps prevent memory overload from incorrect orientation
11814
+ n_rows, n_cols = data_matrix.shape
11815
+ if n_cols > n_rows:
11816
+ # More columns than rows suggests wrong orientation
11817
+ # This would mean more traces than time samples, which is unusual
11818
+
11819
+ # Check if we should use "yes to all" from previous choice
11820
+ if not hasattr(self, '_transpose_yes_to_all'):
11821
+ self._transpose_yes_to_all = False
11822
+
11823
+ if self._transpose_yes_to_all:
11824
+ # Automatically transpose based on previous "yes to all" choice
11825
+ data_matrix = data_matrix.T
11826
+ params['transpose'] = not params['transpose']
11827
+ else:
11828
+ # Ask user
11829
+ msg = QMessageBox()
11830
+ msg.setIcon(QMessageBox.Warning)
11831
+ msg.setWindowTitle("Potential Orientation Issue")
11832
+ msg.setText(
11833
+ f"The matrix has {n_cols} columns and {n_rows} rows.\n\n"
11834
+ f"This would result in {n_cols} traces with {n_rows} time samples each, "
11835
+ f"which is unusual (typically, traces < time samples).\n\n"
11836
+ f"Do you want to transpose the matrix?\n"
11837
+ f"(This would give {n_rows} traces with {n_cols} samples each)"
11838
+ )
11839
+ msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
11840
+ msg.setDefaultButton(QMessageBox.Yes)
11841
+
11842
+ # Add "Yes to All" checkbox for batch imports
11843
+ yes_to_all_checkbox = None
11844
+ if len(params['file_paths']) > 1:
11845
+ yes_to_all_checkbox = QCheckBox("Apply to all remaining files")
11846
+ msg.setCheckBox(yes_to_all_checkbox)
11847
+
11848
+ result = msg.exec_()
11849
+
11850
+ # Check if "Yes to All" was selected
11851
+ if yes_to_all_checkbox and yes_to_all_checkbox.isChecked():
11852
+ self._transpose_yes_to_all = True
11853
+
11854
+ if result == QMessageBox.Yes:
11855
+ data_matrix = data_matrix.T
11856
+ # Update the effective transpose state for display later
11857
+ params['transpose'] = not params['transpose']
11858
+
11784
11859
  # Validate the data matrix
11785
11860
  if data_matrix.size == 0:
11786
11861
  raise ValueError("The loaded matrix is empty")
@@ -11850,6 +11925,10 @@ class MainWindow(QMainWindow):
11850
11925
  except Exception as e:
11851
11926
  failed_imports.append((os.path.basename(file_path), str(e)))
11852
11927
 
11928
+ # Close progress dialog
11929
+ if progress:
11930
+ progress.setValue(len(params['file_paths']))
11931
+
11853
11932
  # After processing all files, update UI and show results
11854
11933
  if successful_imports > 0:
11855
11934
  # Select the last imported file
@@ -11870,9 +11949,57 @@ class MainWindow(QMainWindow):
11870
11949
  if len(failed_imports) > 5:
11871
11950
  error_msg += f"\n\n... and {len(failed_imports) - 5} more"
11872
11951
  QMessageBox.warning(self, "Import Complete with Errors", error_msg)
11873
- else:
11874
- QMessageBox.information(self, "Import Successful",
11875
- f"Successfully imported {successful_imports} ASCII file(s)")
11952
+ elif successful_imports > 0:
11953
+ # Calculate total traces and samples for successful imports
11954
+ total_traces = 0
11955
+ total_samples = 0
11956
+ # Track different configurations
11957
+ config_counts = {} # (n_traces, n_samples): count
11958
+
11959
+ for i in range(len(self.streams) - successful_imports, len(self.streams)):
11960
+ if i >= 0 and i < len(self.trace_position) and self.trace_position[i] is not None:
11961
+ n_traces = len(self.trace_position[i])
11962
+ total_traces += n_traces
11963
+ else:
11964
+ n_traces = 0
11965
+
11966
+ if i >= 0 and i < len(self.n_sample) and self.n_sample[i] is not None:
11967
+ n_samples = self.n_sample[i]
11968
+ total_samples += n_samples
11969
+ else:
11970
+ n_samples = 0
11971
+
11972
+ # Track configuration
11973
+ config = (n_traces, n_samples)
11974
+ config_counts[config] = config_counts.get(config, 0) + 1
11975
+
11976
+ # Show detailed information
11977
+ if successful_imports == 1:
11978
+ # Single file - show detailed info
11979
+ idx = len(self.streams) - 1
11980
+ n_traces = len(self.trace_position[idx]) if idx < len(self.trace_position) and self.trace_position[idx] else 0
11981
+ n_samp = self.n_sample[idx] if idx < len(self.n_sample) and self.n_sample[idx] else 0
11982
+ QMessageBox.information(
11983
+ self, "Import Successful",
11984
+ f"Successfully imported ASCII matrix\n\n"
11985
+ f"Traces: {n_traces}\n"
11986
+ f"Samples per trace: {n_samp}\n"
11987
+ f"Orientation: {'Transposed (traces were in rows)' if params['transpose'] else 'Standard (traces in columns)'}"
11988
+ )
11989
+ else:
11990
+ # Multiple files - show configuration breakdown
11991
+ summary_msg = f"Successfully imported {successful_imports} ASCII file(s)\n\n"
11992
+
11993
+ # Show configuration breakdown
11994
+ if len(config_counts) > 0:
11995
+ for (n_traces, n_samples), count in sorted(config_counts.items()):
11996
+ summary_msg += f"• {count} file(s): {n_traces} traces × {n_samples} samples\n"
11997
+
11998
+ QMessageBox.information(self, "Import Successful", summary_msg.rstrip())
11999
+
12000
+ # Reset transpose yes-to-all flag after batch import completes
12001
+ if hasattr(self, '_transpose_yes_to_all'):
12002
+ delattr(self, '_transpose_yes_to_all')
11876
12003
 
11877
12004
  def ascii_to_obspy_stream(self, data_matrix, params):
11878
12005
  """Convert ASCII matrix to ObsPy Stream object"""
@@ -11939,7 +12066,24 @@ class MainWindow(QMainWindow):
11939
12066
  successful_imports = 0
11940
12067
  failed_imports = []
11941
12068
 
11942
- for archive_path in archive_paths:
12069
+ # Create progress dialog for batch archive import
12070
+ progress = None
12071
+ if len(archive_paths) > 1:
12072
+ progress = QProgressDialog("Importing archives...", "Cancel", 0, len(archive_paths), self)
12073
+ progress.setWindowTitle("Importing Archives")
12074
+ progress.setMinimumDuration(0)
12075
+ progress.setWindowModality(QtCore.Qt.WindowModal)
12076
+ progress.setValue(0)
12077
+ progress.show()
12078
+ QApplication.processEvents()
12079
+
12080
+ for idx, archive_path in enumerate(archive_paths):
12081
+ if progress:
12082
+ progress.setValue(idx)
12083
+ if progress.wasCanceled():
12084
+ break
12085
+ progress.setLabelText(f"Importing archive {idx+1} of {len(archive_paths)}...")
12086
+ QApplication.processEvents()
11943
12087
  try:
11944
12088
  QApplication.setOverrideCursor(Qt.WaitCursor)
11945
12089
 
@@ -12101,6 +12245,10 @@ class MainWindow(QMainWindow):
12101
12245
  finally:
12102
12246
  QApplication.restoreOverrideCursor()
12103
12247
 
12248
+ # Close progress dialog
12249
+ if progress:
12250
+ progress.setValue(len(archive_paths))
12251
+
12104
12252
  # After processing all files, update UI and show results
12105
12253
  if successful_imports > 0:
12106
12254
  # Initialize plot labels if they don't exist
@@ -12119,19 +12267,70 @@ class MainWindow(QMainWindow):
12119
12267
  # Plot the new data
12120
12268
  self.updatePlots()
12121
12269
 
12270
+ # Calculate total traces and samples for successful imports
12271
+ total_traces = 0
12272
+ total_samples = 0
12273
+ # Track different configurations
12274
+ config_counts = {} # (n_traces, n_samples): count
12275
+
12276
+ if successful_imports > 0:
12277
+ # Count traces and samples from the newly imported files
12278
+ # (last successful_imports files in the lists)
12279
+ for i in range(len(self.streams) - successful_imports, len(self.streams)):
12280
+ if i >= 0 and i < len(self.trace_position) and self.trace_position[i] is not None:
12281
+ n_traces = len(self.trace_position[i])
12282
+ total_traces += n_traces
12283
+ else:
12284
+ n_traces = 0
12285
+
12286
+ if i >= 0 and i < len(self.n_sample) and self.n_sample[i] is not None:
12287
+ n_samples = self.n_sample[i]
12288
+ total_samples += n_samples * (len(self.trace_position[i]) if self.trace_position[i] else 0)
12289
+ else:
12290
+ n_samples = 0
12291
+
12292
+ # Track configuration
12293
+ config = (n_traces, n_samples)
12294
+ config_counts[config] = config_counts.get(config, 0) + 1
12295
+
12122
12296
  # Show import summary
12123
12297
  if failed_imports:
12124
- error_msg = f"Successfully imported {successful_imports} archive(s).\n\nFailed imports ({len(failed_imports)}):\n"
12298
+ error_msg = f"Successfully imported {successful_imports} archive(s).\n\n"
12299
+
12300
+ # Show configuration breakdown
12301
+ if len(config_counts) > 0:
12302
+ for (n_traces, n_samples), count in sorted(config_counts.items()):
12303
+ error_msg += f"• {count} archive(s): {n_traces} traces × {n_samples} samples\n"
12304
+
12305
+ error_msg += "\n"
12306
+ error_msg += f"Failed imports ({len(failed_imports)}):\n"
12125
12307
  for fname, error in failed_imports[:5]: # Show first 5 errors
12126
12308
  error_msg += f"\n• {fname}: {error}"
12127
12309
  if len(failed_imports) > 5:
12128
12310
  error_msg += f"\n\n... and {len(failed_imports) - 5} more"
12129
12311
  QMessageBox.warning(self, "Import Complete with Errors", error_msg)
12130
12312
  elif successful_imports > 0:
12131
- QMessageBox.information(
12132
- self, "Import Successful",
12133
- f"Successfully imported {successful_imports} ASCII archive(s)"
12134
- )
12313
+ if len(archive_paths) > 1:
12314
+ # Batch import summary
12315
+ summary_msg = f"Successfully imported {successful_imports} ASCII archive(s)\n\n"
12316
+
12317
+ # Show configuration breakdown
12318
+ if len(config_counts) > 0:
12319
+ for (n_traces, n_samples), count in sorted(config_counts.items()):
12320
+ summary_msg += f"• {count} archive(s): {n_traces} traces × {n_samples} samples\n"
12321
+
12322
+ QMessageBox.information(self, "Import Successful", summary_msg.rstrip())
12323
+ else:
12324
+ # Single archive - detailed info
12325
+ idx = len(self.streams) - 1
12326
+ n_traces = len(self.trace_position[idx]) if idx < len(self.trace_position) and self.trace_position[idx] else 0
12327
+ n_samp = self.n_sample[idx] if idx < len(self.n_sample) and self.n_sample[idx] else 0
12328
+ QMessageBox.information(
12329
+ self, "Import Successful",
12330
+ f"Imported {os.path.basename(archive_paths[0])}\n\n"
12331
+ f"Traces: {n_traces}\n"
12332
+ f"Samples per trace: {n_samp}"
12333
+ )
12135
12334
 
12136
12335
  #######################################
12137
12336
  # File loading and processing functions
@@ -12461,9 +12660,6 @@ class MainWindow(QMainWindow):
12461
12660
  # Close progress dialog
12462
12661
  progress.setValue(len(fileNames_new))
12463
12662
 
12464
- # Success - dialogs removed for smoother workflow
12465
- # Files are now visible in the file list
12466
-
12467
12663
  self.sortFiles() # Sort the files based on the file names
12468
12664
  self.updateFileListDisplay() # Update the file list display
12469
12665
  self.sortFileList() # Sort the file list widget
@@ -12689,7 +12885,17 @@ class MainWindow(QMainWindow):
12689
12885
  if show_cursor:
12690
12886
  QApplication.restoreOverrideCursor()
12691
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
+
12692
12897
  def removeShot(self):
12898
+ """Legacy function - kept for backward compatibility. Use removeSelectedShots instead."""
12693
12899
  if self.currentIndex is not None:
12694
12900
  # Remove the current file from the lists
12695
12901
  for attr in self.attributes_to_initialize:
@@ -12713,6 +12919,59 @@ class MainWindow(QMainWindow):
12713
12919
  else:
12714
12920
  QMessageBox.information(self, "No Shots Remaining", "No shots remaining. Memory will be cleared.")
12715
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()
12716
12975
 
12717
12976
  #######################################
12718
12977
  # Saving shot functions
@@ -16619,9 +16878,18 @@ class MainWindow(QMainWindow):
16619
16878
 
16620
16879
  def batchEditFFID(self):
16621
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
+
16622
16890
  parameters = [
16623
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16624
- {'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'},
16625
16893
  {'label': 'First FFID', 'initial_value': self.ffid[0], 'type': 'int'},
16626
16894
  {'label': 'Increment', 'initial_value': 1, 'type': 'int'}
16627
16895
  ]
@@ -16658,9 +16926,18 @@ class MainWindow(QMainWindow):
16658
16926
 
16659
16927
  def batchEditDelay(self):
16660
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
+
16661
16938
  parameters = [
16662
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16663
- {'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'},
16664
16941
  {'label': 'Delay (in s)', 'initial_value': self.delay[self.currentIndex], 'type': 'float'},
16665
16942
  ]
16666
16943
 
@@ -16738,9 +17015,18 @@ class MainWindow(QMainWindow):
16738
17015
 
16739
17016
  def batchEditSourcePosition(self):
16740
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
+
16741
17027
  parameters = [
16742
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16743
- {'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'},
16744
17030
  {'label': 'Skip every N sources', 'initial_value': 0, 'type': 'int'},
16745
17031
  {'label': 'First Source Position (in m)', 'initial_value': self.source_position[0], 'type': 'float'},
16746
17032
  {'label': 'Last Source Position (in m)', 'initial_value': '', 'type': 'float_or_empty'},
@@ -16846,9 +17132,18 @@ class MainWindow(QMainWindow):
16846
17132
 
16847
17133
  def batchEditTracePosition(self):
16848
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
+
16849
17144
  parameters = [
16850
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16851
- {'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'},
16852
17147
  {'label': 'First Trace #', 'initial_value': 1, 'type': 'int'},
16853
17148
  {'label': 'Last Trace #', 'initial_value': len(self.trace_position[self.currentIndex]), 'type': 'int'},
16854
17149
  {'label': 'First Trace Position (in m)', 'initial_value': self.trace_position[self.currentIndex][0], 'type': 'float'},
@@ -16908,9 +17203,18 @@ class MainWindow(QMainWindow):
16908
17203
 
16909
17204
  def batchSwapTraces(self):
16910
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
+
16911
17215
  parameters = [
16912
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16913
- {'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'},
16914
17218
  {'label': 'First Trace # to swap', 'initial_value': 1, 'type': 'int'},
16915
17219
  {'label': 'Second Trace # to swap', 'initial_value': 2, 'type': 'int'}
16916
17220
  ]
@@ -16976,9 +17280,18 @@ class MainWindow(QMainWindow):
16976
17280
 
16977
17281
  def batchRemoveTraces(self):
16978
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
+
16979
17292
  parameters = [
16980
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
16981
- {'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'},
16982
17295
  {'label': 'First Trace # to remove', 'initial_value': 1, 'type': 'int'},
16983
17296
  {'label': 'Last Trace # to remove', 'initial_value': len(self.trace_position[self.currentIndex]), 'type': 'int'}
16984
17297
  ]
@@ -17042,9 +17355,18 @@ class MainWindow(QMainWindow):
17042
17355
 
17043
17356
  def batchMoveTraces(self):
17044
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
+
17045
17367
  parameters = [
17046
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17047
- {'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'},
17048
17370
  {'label': 'First Trace # to move', 'initial_value': 1, 'type': 'int'},
17049
17371
  {'label': 'Last Trace # to move', 'initial_value': 1, 'type': 'int'},
17050
17372
  {'label': 'New Position', 'initial_value': 1, 'type': 'int'}
@@ -17111,9 +17433,18 @@ class MainWindow(QMainWindow):
17111
17433
 
17112
17434
  def batchMuteTraces(self):
17113
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
+
17114
17445
  parameters = [
17115
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17116
- {'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'},
17117
17448
  {'label': 'First Trace # to mute', 'initial_value': 1, 'type': 'int'},
17118
17449
  {'label': 'Last Trace # to mute', 'initial_value': len(self.trace_position[self.currentIndex]), 'type': 'int'}
17119
17450
  ]
@@ -17163,11 +17494,20 @@ class MainWindow(QMainWindow):
17163
17494
  def batchInsertMutedTraces(self):
17164
17495
  """Batch insert zero traces for a range of shots."""
17165
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
+
17166
17506
  n_traces = len(self.trace_position[self.currentIndex]) if self.currentIndex < len(self.trace_position) else 1
17167
17507
 
17168
17508
  parameters = [
17169
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17170
- {'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'},
17171
17511
  {'label': 'Insert after Trace # (0 for beginning)', 'initial_value': n_traces, 'type': 'int'},
17172
17512
  {'label': 'Number of traces to insert', 'initial_value': 1, 'type': 'int'}
17173
17513
  ]
@@ -17220,9 +17560,18 @@ class MainWindow(QMainWindow):
17220
17560
  def batchZeroPadTraces(self):
17221
17561
  """Batch zero pad all traces for a range of shots."""
17222
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
+
17223
17572
  parameters = [
17224
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17225
- {'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'},
17226
17575
  {'label': 'Pad at end (seconds)', 'initial_value': 0.0, 'type': 'float'}
17227
17576
  ]
17228
17577
 
@@ -17283,11 +17632,20 @@ class MainWindow(QMainWindow):
17283
17632
  def batchReverseTraces(self):
17284
17633
  """Batch reverse trace data (flip data matrix left-to-right) while keeping headers in place"""
17285
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
+
17286
17644
  n_traces = len(self.trace_position[self.currentIndex]) if self.currentIndex < len(self.trace_position) else 1
17287
17645
 
17288
17646
  parameters = [
17289
- {'label': 'First Source #', 'initial_value': 1, 'type': 'int'},
17290
- {'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'},
17291
17649
  {'label': 'First Trace #', 'initial_value': 1, 'type': 'int'},
17292
17650
  {'label': 'Last Trace #', 'initial_value': n_traces, 'type': 'int'}
17293
17651
  ]
@@ -17651,6 +18009,203 @@ class MainWindow(QMainWindow):
17651
18009
  QMessageBox.information(self, "Stacking Complete",
17652
18010
  f"Successfully created {stacked_shot_count} stacked shot(s).")
17653
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
+
17654
18209
  #######################################
17655
18210
  # Set parameters functions
17656
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.2
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=LO70RUeDFgSj8VdFhawOny6CVOPFksmbZ18sxkVpTBg,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=0D3vDbpaToJ-RuNvpL7C4uGPLlrQaPvxb2xBuOTFxsI,1240456
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.2.dist-info/licenses/LICENCE,sha256=-uaAIm20JrJKoMdCdn2GlFQfNU4fbsHWK3eh4kIQ_Ec,35143
21
- pyckster-26.2.2.dist-info/METADATA,sha256=mxaCepOXltujMuxOYLLnG-n4nehgEGZDfd38G8h-O8c,4567
22
- pyckster-26.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
- pyckster-26.2.2.dist-info/entry_points.txt,sha256=yrOQx1wHi84rbxX_ZYtYaVcK3EeuRhHRQDZRc8mB0NI,100
24
- pyckster-26.2.2.dist-info/top_level.txt,sha256=eaihhwhEmlysgdZE4HmELFdSUwlXcMv90YorkjOXujQ,9
25
- pyckster-26.2.2.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,,