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 +1 -1
- pyckster/core.py +603 -48
- pyckster/obspy_utils.py +59 -32
- {pyckster-26.2.2.dist-info → pyckster-26.2.4.dist-info}/METADATA +1 -1
- {pyckster-26.2.2.dist-info → pyckster-26.2.4.dist-info}/RECORD +9 -9
- {pyckster-26.2.2.dist-info → pyckster-26.2.4.dist-info}/WHEEL +0 -0
- {pyckster-26.2.2.dist-info → pyckster-26.2.4.dist-info}/entry_points.txt +0 -0
- {pyckster-26.2.2.dist-info → pyckster-26.2.4.dist-info}/licenses/LICENCE +0 -0
- {pyckster-26.2.2.dist-info → pyckster-26.2.4.dist-info}/top_level.txt +0 -0
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.
|
|
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
|
|
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
|
|
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
|
|
2278
|
-
self.
|
|
2279
|
-
|
|
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
|
|
2335
|
-
self.removeShotAction = QAction('Remove
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
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
|
|
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\
|
|
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
|
-
|
|
12132
|
-
|
|
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':
|
|
16624
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
16663
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
16743
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
16851
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
16913
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
16981
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
17047
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
17116
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
17170
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
17225
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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':
|
|
17290
|
-
{'label': 'Last Source #', 'initial_value':
|
|
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
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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,15 +1,15 @@
|
|
|
1
|
-
pyckster/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
21
|
-
pyckster-26.2.
|
|
22
|
-
pyckster-26.2.
|
|
23
|
-
pyckster-26.2.
|
|
24
|
-
pyckster-26.2.
|
|
25
|
-
pyckster-26.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|