pyckster 26.1.3__py3-none-any.whl → 26.1.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 +2 -2
- pyckster/core.py +1666 -418
- pyckster/pick_io.py +13 -3
- pyckster/sw_utils.py +10 -2
- {pyckster-26.1.3.dist-info → pyckster-26.1.4.dist-info}/METADATA +13 -5
- {pyckster-26.1.3.dist-info → pyckster-26.1.4.dist-info}/RECORD +10 -11
- pyckster/pac_inversion.py +0 -785
- {pyckster-26.1.3.dist-info → pyckster-26.1.4.dist-info}/WHEEL +0 -0
- {pyckster-26.1.3.dist-info → pyckster-26.1.4.dist-info}/entry_points.txt +0 -0
- {pyckster-26.1.3.dist-info → pyckster-26.1.4.dist-info}/licenses/LICENCE +0 -0
- {pyckster-26.1.3.dist-info → pyckster-26.1.4.dist-info}/top_level.txt +0 -0
pyckster/core.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
This script is a PyQt5 GUI for
|
|
4
|
+
This script is a PyQt5 GUI for the processing and analysis of active near-surface seismic data
|
|
5
5
|
Copyright (C) 2026 Sylvain Pasquet
|
|
6
6
|
Email: sylvain.pasquet@sorbonne-universite.fr
|
|
7
7
|
|
|
@@ -49,6 +49,7 @@ from PyQt5.QtWidgets import (
|
|
|
49
49
|
QFileDialog, QAction, QActionGroup, QLabel, QListWidget, QComboBox, QStatusBar, QScrollArea,QProgressDialog,
|
|
50
50
|
QPushButton, QDialog, QHBoxLayout, QVBoxLayout, QLineEdit, QCheckBox, QTableWidget, QTableWidgetItem, QMessageBox,
|
|
51
51
|
QDoubleSpinBox, QFrame, QFormLayout, QDialogButtonBox, QGroupBox, QSpinBox, QGraphicsRectItem, QSlider, QStyle,
|
|
52
|
+
QStackedLayout,
|
|
52
53
|
QTextEdit,
|
|
53
54
|
)
|
|
54
55
|
from PyQt5.QtCore import QLocale, pyqtSignal, Qt
|
|
@@ -1896,6 +1897,13 @@ class MainWindow(QMainWindow):
|
|
|
1896
1897
|
self.dispersionShowCrosshairCheck.toggled.connect(self.onPickingShowCrosshairChanged)
|
|
1897
1898
|
dispersionPicksLayout.addWidget(self.dispersionShowCrosshairCheck)
|
|
1898
1899
|
|
|
1900
|
+
# Show Aliasing Limit checkbox
|
|
1901
|
+
self.dispersionShowAliasingLimitCheck = QCheckBox("Show Aliasing Limit")
|
|
1902
|
+
self.dispersionShowAliasingLimitCheck.setChecked(getattr(self, 'dispersion_show_aliasing_limit', False))
|
|
1903
|
+
self.dispersionShowAliasingLimitCheck.setToolTip("Show spatial aliasing limits (energy below dispersion)")
|
|
1904
|
+
self.dispersionShowAliasingLimitCheck.toggled.connect(self.onDispersionShowAliasingLimitChanged)
|
|
1905
|
+
dispersionPicksLayout.addWidget(self.dispersionShowAliasingLimitCheck)
|
|
1906
|
+
|
|
1899
1907
|
dispersionPicksLayout.addStretch()
|
|
1900
1908
|
|
|
1901
1909
|
# Create top view selector controls panel
|
|
@@ -2971,7 +2979,8 @@ class MainWindow(QMainWindow):
|
|
|
2971
2979
|
for attr in ['topPseudosectionModeSpinBox', 'bottomPseudosectionModeSpinBox',
|
|
2972
2980
|
'topPseudosectionFreqLambdaCheck', 'bottomPseudosectionFreqLambdaCheck',
|
|
2973
2981
|
'topPseudosectionInterpCheck', 'bottomPseudosectionInterpCheck',
|
|
2974
|
-
'topPseudosectionInvertYCheck', 'bottomPseudosectionInvertYCheck'
|
|
2982
|
+
'topPseudosectionInvertYCheck', 'bottomPseudosectionInvertYCheck',
|
|
2983
|
+
'topPseudosectionUseSourceXCheck', 'bottomPseudosectionUseSourceXCheck']:
|
|
2975
2984
|
if hasattr(self, attr):
|
|
2976
2985
|
setattr(self, attr, None)
|
|
2977
2986
|
except Exception:
|
|
@@ -3473,12 +3482,31 @@ class MainWindow(QMainWindow):
|
|
|
3473
3482
|
default_colormap = getattr(self, colormap_attr, 'plasma')
|
|
3474
3483
|
colormap_combo.setCurrentText(default_colormap)
|
|
3475
3484
|
|
|
3476
|
-
#
|
|
3485
|
+
# Reverse colormap checkbox (created before connecting combo handler)
|
|
3486
|
+
reverse_cmap_check = QCheckBox("Reverse")
|
|
3487
|
+
reverse_cmap_check.setToolTip("Reverse colormap direction (use _r version)")
|
|
3488
|
+
|
|
3489
|
+
# Connect handler with reverse logic
|
|
3477
3490
|
colormap_handler = f'onTopColormapChanged' if prefix == 'top' else 'onBottomColormapChanged'
|
|
3478
|
-
|
|
3491
|
+
def on_colormap_changed(cmap_name):
|
|
3492
|
+
# Apply _r suffix if reverse is checked
|
|
3493
|
+
if reverse_cmap_check.isChecked():
|
|
3494
|
+
getattr(self, colormap_handler)(cmap_name + '_r')
|
|
3495
|
+
else:
|
|
3496
|
+
getattr(self, colormap_handler)(cmap_name)
|
|
3497
|
+
colormap_combo.currentTextChanged.connect(on_colormap_changed)
|
|
3479
3498
|
|
|
3480
3499
|
layout.addWidget(colormap_combo)
|
|
3481
3500
|
setattr(self, f'{prefix}ColormapComboBox', colormap_combo)
|
|
3501
|
+
|
|
3502
|
+
# Reverse checkbox toggle behavior
|
|
3503
|
+
def toggle_reverse_cmap(checked):
|
|
3504
|
+
current = colormap_combo.currentText()
|
|
3505
|
+
# Trigger colormap change with current selection
|
|
3506
|
+
on_colormap_changed(current)
|
|
3507
|
+
reverse_cmap_check.toggled.connect(toggle_reverse_cmap)
|
|
3508
|
+
layout.addWidget(reverse_cmap_check)
|
|
3509
|
+
setattr(self, f'{prefix}ReverseColormapCheck', reverse_cmap_check)
|
|
3482
3510
|
|
|
3483
3511
|
# Traveltimes: no additional dynamic controls beyond colormap
|
|
3484
3512
|
if view_type == "Traveltimes":
|
|
@@ -3522,6 +3550,13 @@ class MainWindow(QMainWindow):
|
|
|
3522
3550
|
invert_y_check.toggled.connect(self.onPseudoSectionInvertYToggled)
|
|
3523
3551
|
layout.addWidget(invert_y_check)
|
|
3524
3552
|
setattr(self, f'{prefix}PseudosectionInvertYCheck', invert_y_check)
|
|
3553
|
+
|
|
3554
|
+
# Use source position / FFID on X-axis
|
|
3555
|
+
use_source_x_check = QCheckBox("Use source / FFID on X")
|
|
3556
|
+
use_source_x_check.setChecked(getattr(self, 'pseudosection_use_source_x', False))
|
|
3557
|
+
use_source_x_check.toggled.connect(self.onPseudoSectionUseSourceXToggled)
|
|
3558
|
+
layout.addWidget(use_source_x_check)
|
|
3559
|
+
setattr(self, f'{prefix}PseudosectionUseSourceXCheck', use_source_x_check)
|
|
3525
3560
|
|
|
3526
3561
|
add_separator()
|
|
3527
3562
|
layout.addStretch()
|
|
@@ -3625,9 +3660,31 @@ class MainWindow(QMainWindow):
|
|
|
3625
3660
|
seismo_colormap_combo = QComboBox()
|
|
3626
3661
|
seismo_colormap_combo.addItems(['viridis', 'plasma', 'inferno', 'magma', 'cividis', 'turbo', 'hot', 'cool', 'coolwarm', 'seismic', 'RdBu', 'spring', 'summer', 'autumn', 'winter', 'gray', 'bone', 'copper', 'Blues', 'Reds', 'Greens', 'Oranges', 'Purples', 'Greys', 'jet', 'rainbow', 'pink', 'terrain', 'ocean'])
|
|
3627
3662
|
seismo_colormap_combo.setCurrentText(getattr(self, 'seismogram_colormap_str', 'Greys'))
|
|
3628
|
-
|
|
3663
|
+
|
|
3664
|
+
# Reverse colormap checkbox (created before connecting combo handler)
|
|
3665
|
+
reverse_seismo_cmap_check = QCheckBox("Reverse")
|
|
3666
|
+
reverse_seismo_cmap_check.setToolTip("Reverse colormap direction (use _r version)")
|
|
3667
|
+
|
|
3668
|
+
# Connect handler with reverse logic
|
|
3669
|
+
def on_seismo_colormap_changed(cmap_name):
|
|
3670
|
+
# Apply _r suffix if reverse is checked
|
|
3671
|
+
if reverse_seismo_cmap_check.isChecked():
|
|
3672
|
+
self.onSeismoColormapChanged(cmap_name + '_r')
|
|
3673
|
+
else:
|
|
3674
|
+
self.onSeismoColormapChanged(cmap_name)
|
|
3675
|
+
seismo_colormap_combo.currentTextChanged.connect(on_seismo_colormap_changed)
|
|
3676
|
+
|
|
3629
3677
|
layout.addWidget(seismo_colormap_combo)
|
|
3630
3678
|
setattr(self, 'seismoImageColormapCombo', seismo_colormap_combo)
|
|
3679
|
+
|
|
3680
|
+
# Reverse checkbox toggle behavior
|
|
3681
|
+
def toggle_reverse_seismo_cmap(checked):
|
|
3682
|
+
current = seismo_colormap_combo.currentText()
|
|
3683
|
+
# Trigger colormap change with current selection
|
|
3684
|
+
on_seismo_colormap_changed(current)
|
|
3685
|
+
reverse_seismo_cmap_check.toggled.connect(toggle_reverse_seismo_cmap)
|
|
3686
|
+
layout.addWidget(reverse_seismo_cmap_check)
|
|
3687
|
+
setattr(self, 'seismoReverseColormapCheck', reverse_seismo_cmap_check)
|
|
3631
3688
|
|
|
3632
3689
|
interp_check = QCheckBox("Interpolate")
|
|
3633
3690
|
interp_check.setChecked(getattr(self, 'seismo_image_interpolate', True))
|
|
@@ -3756,6 +3813,29 @@ class MainWindow(QMainWindow):
|
|
|
3756
3813
|
use_topo_check.toggled.connect(self.onUseTopographyChanged)
|
|
3757
3814
|
layout.addWidget(use_topo_check)
|
|
3758
3815
|
setattr(self, 'useTopographyCheck', use_topo_check)
|
|
3816
|
+
|
|
3817
|
+
# Enhance checkbox for better mode visibility
|
|
3818
|
+
enhance_check = QCheckBox("Enhance")
|
|
3819
|
+
enhance_check.setChecked(bool(getattr(self, 'dispersion_enhance', False)))
|
|
3820
|
+
enhance_check.setToolTip("Enhance weak modes via contrast transformation (makes weak modes more visible)")
|
|
3821
|
+
enhance_check.toggled.connect(self.onDispersionEnhanceChanged)
|
|
3822
|
+
layout.addWidget(enhance_check)
|
|
3823
|
+
setattr(self, 'dispersionEnhanceCheck', enhance_check)
|
|
3824
|
+
|
|
3825
|
+
# Saturation slider for clipping bright spots
|
|
3826
|
+
layout.addWidget(QLabel("Saturation:"))
|
|
3827
|
+
saturation_spin = QDoubleSpinBox()
|
|
3828
|
+
saturation_spin.setRange(0.5, 1.5)
|
|
3829
|
+
saturation_spin.setDecimals(2)
|
|
3830
|
+
saturation_spin.setSingleStep(0.01)
|
|
3831
|
+
saturation_spin.setValue(getattr(self, 'dispersion_saturation', 1.0))
|
|
3832
|
+
saturation_spin.setToolTip("Colormap brightness: <1 darkens, 1=normal, >1 brightens (1.25 ≈ MATLAB brighten(0.5))")
|
|
3833
|
+
saturation_spin.setMinimumWidth(60)
|
|
3834
|
+
saturation_spin.setMaximumWidth(70)
|
|
3835
|
+
saturation_spin.valueChanged.connect(self.onDispersionSaturationChanged)
|
|
3836
|
+
layout.addWidget(saturation_spin)
|
|
3837
|
+
setattr(self, 'dispersionSaturationSpin', saturation_spin)
|
|
3838
|
+
|
|
3759
3839
|
|
|
3760
3840
|
# Permute axes button (Spectrogram only)
|
|
3761
3841
|
if view_type == "Spectrogram":
|
|
@@ -3841,14 +3921,15 @@ class MainWindow(QMainWindow):
|
|
|
3841
3921
|
|
|
3842
3922
|
add_separator()
|
|
3843
3923
|
|
|
3924
|
+
# Trace selection for phase-shift using range slider
|
|
3925
|
+
trace_range_container = QHBoxLayout()
|
|
3926
|
+
|
|
3844
3927
|
batch_window_button = QPushButton("Batch windowing")
|
|
3845
3928
|
batch_window_button.setToolTip("Set fixed window parameters for all shots")
|
|
3846
3929
|
batch_window_button.clicked.connect(self.onDispersionBatchWindowing)
|
|
3847
3930
|
trace_range_container.addWidget(batch_window_button)
|
|
3848
3931
|
trace_range_container.addStretch()
|
|
3849
3932
|
|
|
3850
|
-
# Trace selection for phase-shift using range slider
|
|
3851
|
-
trace_range_container = QHBoxLayout()
|
|
3852
3933
|
trace_range_label = QLabel("Trace range:")
|
|
3853
3934
|
trace_range_container.addWidget(trace_range_label)
|
|
3854
3935
|
|
|
@@ -3870,6 +3951,32 @@ class MainWindow(QMainWindow):
|
|
|
3870
3951
|
layout.addWidget(trace_range_slider)
|
|
3871
3952
|
# Use view-specific attribute name for the slider itself (top_traceRangeSlider or bottom_traceRangeSlider)
|
|
3872
3953
|
setattr(self, f'{prefix}_traceRangeSlider', trace_range_slider)
|
|
3954
|
+
|
|
3955
|
+
add_separator()
|
|
3956
|
+
|
|
3957
|
+
# Add stack checkbox for dispersion view (after trace range slider)
|
|
3958
|
+
stack_checkbox_container = QWidget()
|
|
3959
|
+
stack_checkbox_layout = QHBoxLayout(stack_checkbox_container)
|
|
3960
|
+
stack_checkbox_layout.setContentsMargins(0, 0, 0, 0)
|
|
3961
|
+
stack_checkbox_layout.setSpacing(4)
|
|
3962
|
+
stack_checkbox_layout.addStretch()
|
|
3963
|
+
|
|
3964
|
+
# Use view-specific checkbox reference so we can keep state across top/bottom swaps
|
|
3965
|
+
stack_checkbox = QCheckBox("Stack all shots")
|
|
3966
|
+
stack_checkbox.setChecked(getattr(self, 'dispersion_stack_mode', False))
|
|
3967
|
+
stack_checkbox.setToolTip("Sum dispersion images from all shots with traces in current window range")
|
|
3968
|
+
stack_checkbox.stateChanged.connect(self.onDispersionStackToggled)
|
|
3969
|
+
stack_checkbox_layout.addWidget(stack_checkbox)
|
|
3970
|
+
setattr(self, f"{prefix}_dispersionStackCheckbox", stack_checkbox)
|
|
3971
|
+
|
|
3972
|
+
# Stacking parameters button
|
|
3973
|
+
stack_params_button = QPushButton("Stack params")
|
|
3974
|
+
stack_params_button.setToolTip("Set stacking parameters (offset limits, side preference)")
|
|
3975
|
+
stack_params_button.clicked.connect(self.onDispersionStackParamsClicked)
|
|
3976
|
+
stack_checkbox_layout.addWidget(stack_params_button)
|
|
3977
|
+
setattr(self, f"{prefix}_dispersionStackParamsButton", stack_params_button)
|
|
3978
|
+
|
|
3979
|
+
layout.addWidget(stack_checkbox_container)
|
|
3873
3980
|
|
|
3874
3981
|
# Populate trace range if we have data
|
|
3875
3982
|
if self.streams and self.currentIndex is not None:
|
|
@@ -4034,6 +4141,24 @@ class MainWindow(QMainWindow):
|
|
|
4034
4141
|
if top_type == "dispersion" or bottom_type == "dispersion":
|
|
4035
4142
|
self.updatePlots()
|
|
4036
4143
|
|
|
4144
|
+
def onDispersionEnhanceChanged(self, state):
|
|
4145
|
+
"""Handle enhance checkbox toggle for dispersion images"""
|
|
4146
|
+
self.dispersion_enhance = bool(state)
|
|
4147
|
+
# Replot dispersion if showing
|
|
4148
|
+
top_type = getattr(self, 'topPlotType', '').lower()
|
|
4149
|
+
bottom_type = getattr(self, 'bottomPlotType', '').lower()
|
|
4150
|
+
if top_type == "dispersion" or bottom_type == "dispersion":
|
|
4151
|
+
self.updatePlots()
|
|
4152
|
+
|
|
4153
|
+
def onDispersionSaturationChanged(self, value):
|
|
4154
|
+
"""Handle saturation slider change for dispersion images"""
|
|
4155
|
+
self.dispersion_saturation = float(value)
|
|
4156
|
+
# Replot dispersion if showing
|
|
4157
|
+
top_type = getattr(self, 'topPlotType', '').lower()
|
|
4158
|
+
bottom_type = getattr(self, 'bottomPlotType', '').lower()
|
|
4159
|
+
if top_type == "dispersion" or bottom_type == "dispersion":
|
|
4160
|
+
self.updatePlots()
|
|
4161
|
+
|
|
4037
4162
|
def onSpectrogramPermuteAxes(self):
|
|
4038
4163
|
"""Toggle permutation of frequency and trace axes in spectrogram"""
|
|
4039
4164
|
self.spectrogram_freq_on_y = not self.spectrogram_freq_on_y
|
|
@@ -4054,31 +4179,132 @@ class MainWindow(QMainWindow):
|
|
|
4054
4179
|
|
|
4055
4180
|
def onDispersionBatchWindowing(self):
|
|
4056
4181
|
"""Configure batch windowing parameters for all shots in dispersion view"""
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4182
|
+
class BatchWindowingDialog(QDialog):
|
|
4183
|
+
def __init__(self, parent, default_center=None):
|
|
4184
|
+
super().__init__(parent)
|
|
4185
|
+
self.setWindowTitle("Batch Windowing Parameters")
|
|
4186
|
+
self.setModal(True)
|
|
4187
|
+
|
|
4188
|
+
layout = QVBoxLayout(self)
|
|
4189
|
+
|
|
4190
|
+
# Mode selector
|
|
4191
|
+
mode_row = QHBoxLayout()
|
|
4192
|
+
mode_row.addWidget(QLabel("Mode:"))
|
|
4193
|
+
self.modeCombo = QComboBox()
|
|
4194
|
+
self.modeCombo.addItems(["Constant offset", "Constant xmid"])
|
|
4195
|
+
mode_row.addWidget(self.modeCombo)
|
|
4196
|
+
layout.addLayout(mode_row)
|
|
4197
|
+
|
|
4198
|
+
self.stack = QStackedLayout()
|
|
4199
|
+
|
|
4200
|
+
# Constant offset page (current behaviour)
|
|
4201
|
+
offset_page = QWidget()
|
|
4202
|
+
offset_form = QFormLayout(offset_page)
|
|
4203
|
+
self.offset_window_len = QSpinBox()
|
|
4204
|
+
self.offset_window_len.setRange(1, 100000)
|
|
4205
|
+
self.offset_window_len.setValue(10)
|
|
4206
|
+
offset_form.addRow("Window length (traces)", self.offset_window_len)
|
|
4207
|
+
|
|
4208
|
+
self.window_side_combo = QComboBox()
|
|
4209
|
+
self.window_side_combo.addItems(["left", "right", "best"])
|
|
4210
|
+
self.window_side_combo.setCurrentText("best")
|
|
4211
|
+
offset_form.addRow("Window side", self.window_side_combo)
|
|
4212
|
+
|
|
4213
|
+
self.prefer_side_combo = QComboBox()
|
|
4214
|
+
self.prefer_side_combo.addItems(["left", "right"])
|
|
4215
|
+
self.prefer_side_combo.setCurrentText("right")
|
|
4216
|
+
offset_form.addRow("Prefer side (when best)", self.prefer_side_combo)
|
|
4217
|
+
|
|
4218
|
+
self.offset_from_shot = QSpinBox()
|
|
4219
|
+
self.offset_from_shot.setRange(0, 100000)
|
|
4220
|
+
self.offset_from_shot.setValue(1)
|
|
4221
|
+
offset_form.addRow("Offset from shot (traces)", self.offset_from_shot)
|
|
4222
|
+
|
|
4223
|
+
self.stack.addWidget(offset_page)
|
|
4224
|
+
|
|
4225
|
+
# Constant xmid page (new behaviour)
|
|
4226
|
+
xmid_page = QWidget()
|
|
4227
|
+
xmid_form = QFormLayout(xmid_page)
|
|
4228
|
+
self.xmid_window_len = QSpinBox()
|
|
4229
|
+
self.xmid_window_len.setRange(1, 100000)
|
|
4230
|
+
self.xmid_window_len.setValue(10)
|
|
4231
|
+
xmid_form.addRow("Window length (traces)", self.xmid_window_len)
|
|
4232
|
+
|
|
4233
|
+
self.xmid_center = QDoubleSpinBox()
|
|
4234
|
+
self.xmid_center.setDecimals(2)
|
|
4235
|
+
self.xmid_center.setRange(-1e9, 1e9)
|
|
4236
|
+
if default_center is not None:
|
|
4237
|
+
self.xmid_center.setValue(float(default_center))
|
|
4238
|
+
xmid_form.addRow("Center of window (m)", self.xmid_center)
|
|
4239
|
+
|
|
4240
|
+
self.stack.addWidget(xmid_page)
|
|
4241
|
+
|
|
4242
|
+
stack_container = QWidget()
|
|
4243
|
+
stack_container.setLayout(self.stack)
|
|
4244
|
+
layout.addWidget(stack_container)
|
|
4245
|
+
|
|
4246
|
+
# Buttons
|
|
4247
|
+
buttons = QHBoxLayout()
|
|
4248
|
+
self.okButton = QPushButton("OK")
|
|
4249
|
+
self.cancelButton = QPushButton("Cancel")
|
|
4250
|
+
buttons.addWidget(self.okButton)
|
|
4251
|
+
buttons.addWidget(self.cancelButton)
|
|
4252
|
+
layout.addLayout(buttons)
|
|
4253
|
+
|
|
4254
|
+
self.modeCombo.currentIndexChanged.connect(self.stack.setCurrentIndex)
|
|
4255
|
+
self.okButton.clicked.connect(self.accept)
|
|
4256
|
+
self.cancelButton.clicked.connect(self.reject)
|
|
4257
|
+
|
|
4258
|
+
def getValues(self):
|
|
4259
|
+
mode = 'constant_offset' if self.modeCombo.currentIndex() == 0 else 'constant_xmid'
|
|
4260
|
+
if mode == 'constant_offset':
|
|
4261
|
+
return {
|
|
4262
|
+
'mode': mode,
|
|
4263
|
+
'window_length': self.offset_window_len.value(),
|
|
4264
|
+
'window_side': self.window_side_combo.currentText(),
|
|
4265
|
+
'prefer_side': self.prefer_side_combo.currentText(),
|
|
4266
|
+
'offset_from_shot': self.offset_from_shot.value(),
|
|
4267
|
+
'center_xmid': None
|
|
4268
|
+
}
|
|
4269
|
+
else:
|
|
4270
|
+
return {
|
|
4271
|
+
'mode': mode,
|
|
4272
|
+
'window_length': self.xmid_window_len.value(),
|
|
4273
|
+
'window_side': 'best',
|
|
4274
|
+
'prefer_side': 'right',
|
|
4275
|
+
'offset_from_shot': 0,
|
|
4276
|
+
'center_xmid': self.xmid_center.value()
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
# Default center: use existing xmid if available, otherwise current shot midpoint
|
|
4280
|
+
default_center = None
|
|
4281
|
+
if hasattr(self, 'currentIndex') and self.currentIndex is not None:
|
|
4282
|
+
if hasattr(self, 'xmid_values') and self.currentIndex in self.xmid_values:
|
|
4283
|
+
default_center = self.xmid_values[self.currentIndex]
|
|
4284
|
+
elif self.trace_position and self.currentIndex < len(self.trace_position):
|
|
4285
|
+
positions = self.trace_position[self.currentIndex]
|
|
4286
|
+
if positions is not None and len(positions) > 0:
|
|
4287
|
+
default_center = float(np.mean(positions))
|
|
4288
|
+
|
|
4289
|
+
dialog = BatchWindowingDialog(self, default_center=default_center)
|
|
4290
|
+
|
|
4070
4291
|
if dialog.exec_():
|
|
4071
4292
|
values = dialog.getValues()
|
|
4072
|
-
window_length = values.get('
|
|
4073
|
-
window_side = values.get('
|
|
4074
|
-
prefer_side = values.get('
|
|
4075
|
-
offset_from_shot = values.get('
|
|
4293
|
+
window_length = values.get('window_length', 10)
|
|
4294
|
+
window_side = values.get('window_side', 'best')
|
|
4295
|
+
prefer_side = values.get('prefer_side', 'right')
|
|
4296
|
+
offset_from_shot = values.get('offset_from_shot', 1)
|
|
4297
|
+
mode = values.get('mode', 'constant_offset')
|
|
4298
|
+
center_xmid = values.get('center_xmid', None)
|
|
4076
4299
|
|
|
4077
4300
|
# Apply batch windowing to all shots
|
|
4078
|
-
self._applyBatchWindowingToAllShots(window_length, window_side, offset_from_shot, prefer_side)
|
|
4301
|
+
self._applyBatchWindowingToAllShots(window_length, window_side, offset_from_shot, prefer_side, mode, center_xmid)
|
|
4079
4302
|
|
|
4080
|
-
def _applyBatchWindowingToAllShots(self, window_length, window_side, offset_from_shot, prefer_side='right'):
|
|
4081
|
-
"""Apply batch windowing parameters to all loaded shots using shot and geophone positions
|
|
4303
|
+
def _applyBatchWindowingToAllShots(self, window_length, window_side, offset_from_shot, prefer_side='right', mode='constant_offset', center_xmid=None):
|
|
4304
|
+
"""Apply batch windowing parameters to all loaded shots using shot and geophone positions.
|
|
4305
|
+
|
|
4306
|
+
mode: 'constant_offset' (legacy) or 'constant_xmid' (fixed center across shots).
|
|
4307
|
+
"""
|
|
4082
4308
|
if not self.streams:
|
|
4083
4309
|
QMessageBox.warning(self, "No Data", "No data loaded.")
|
|
4084
4310
|
return
|
|
@@ -4093,6 +4319,11 @@ class MainWindow(QMainWindow):
|
|
|
4093
4319
|
self.batch_windowing_params['window_side'] = window_side
|
|
4094
4320
|
self.batch_windowing_params['offset_from_shot'] = offset_from_shot
|
|
4095
4321
|
self.batch_windowing_params['prefer_side'] = prefer_side
|
|
4322
|
+
self.batch_windowing_params['mode'] = mode
|
|
4323
|
+
self.batch_windowing_params['center_xmid'] = center_xmid
|
|
4324
|
+
|
|
4325
|
+
applied_count = 0
|
|
4326
|
+
total_shots = len(self.streams)
|
|
4096
4327
|
|
|
4097
4328
|
# Apply to each shot
|
|
4098
4329
|
for shot_idx, stream in enumerate(self.streams):
|
|
@@ -4104,181 +4335,146 @@ class MainWindow(QMainWindow):
|
|
|
4104
4335
|
|
|
4105
4336
|
shot_pos = self.source_position[shot_idx]
|
|
4106
4337
|
trace_positions = np.array(self.trace_position[shot_idx])
|
|
4338
|
+
if trace_positions is None or len(trace_positions) == 0:
|
|
4339
|
+
continue
|
|
4107
4340
|
|
|
4108
|
-
# Calculate mean geophone spacing
|
|
4109
|
-
if len(trace_positions) > 1:
|
|
4110
|
-
sorted_positions = np.sort(trace_positions)
|
|
4111
|
-
mean_dg = np.mean(np.diff(sorted_positions))
|
|
4112
|
-
else:
|
|
4113
|
-
mean_dg = 1.0 # Default spacing if only one trace
|
|
4114
|
-
|
|
4115
|
-
# Calculate offset distance threshold
|
|
4116
|
-
offset_distance = offset_from_shot * mean_dg
|
|
4117
|
-
|
|
4118
|
-
# Calculate distances from shot (positive = right, negative = left)
|
|
4119
|
-
distances = trace_positions - shot_pos
|
|
4120
|
-
abs_distances = np.abs(distances)
|
|
4121
|
-
|
|
4122
|
-
# Sort traces by distance from shot to determine left/right
|
|
4123
|
-
sorted_indices = np.argsort(distances)
|
|
4124
|
-
sorted_distances = distances[sorted_indices]
|
|
4125
|
-
|
|
4126
|
-
# Find traces on left (negative distance) and right (positive distance)
|
|
4127
|
-
left_mask = sorted_distances < 0
|
|
4128
|
-
right_mask = sorted_distances > 0
|
|
4129
|
-
|
|
4130
|
-
left_indices = sorted_indices[left_mask]
|
|
4131
|
-
right_indices = sorted_indices[right_mask]
|
|
4132
|
-
|
|
4133
|
-
# Calculate window range based on side preference
|
|
4134
4341
|
first_trace = None
|
|
4135
4342
|
last_trace = None
|
|
4343
|
+
selected_traces = None
|
|
4136
4344
|
|
|
4137
|
-
if
|
|
4138
|
-
#
|
|
4139
|
-
if
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4345
|
+
if mode == 'constant_xmid':
|
|
4346
|
+
# Fixed center across shots; pick closest traces to requested xmid
|
|
4347
|
+
if center_xmid is None:
|
|
4348
|
+
continue
|
|
4349
|
+
if len(trace_positions) < 1:
|
|
4350
|
+
continue
|
|
4351
|
+
# Pick nearest window_length traces to the target center
|
|
4352
|
+
idx_sorted = np.argsort(np.abs(trace_positions - center_xmid))
|
|
4353
|
+
if len(idx_sorted) >= window_length:
|
|
4354
|
+
selected_traces = idx_sorted[:window_length]
|
|
4355
|
+
else:
|
|
4356
|
+
# Not enough traces → keep previous window for this shot
|
|
4357
|
+
continue
|
|
4358
|
+
first_trace = int(np.min(selected_traces))
|
|
4359
|
+
last_trace = int(np.max(selected_traces))
|
|
4360
|
+
else:
|
|
4361
|
+
# Legacy constant offset behaviour
|
|
4362
|
+
# Calculate mean geophone spacing
|
|
4363
|
+
if len(trace_positions) > 1:
|
|
4364
|
+
sorted_positions = np.sort(trace_positions)
|
|
4365
|
+
mean_dg = np.mean(np.diff(sorted_positions))
|
|
4366
|
+
else:
|
|
4367
|
+
mean_dg = 1.0 # Default spacing if only one trace
|
|
4368
|
+
|
|
4369
|
+
# Calculate offset distance threshold
|
|
4370
|
+
offset_distance = offset_from_shot * mean_dg
|
|
4371
|
+
|
|
4372
|
+
# Calculate distances from shot (positive = right, negative = left)
|
|
4373
|
+
distances = trace_positions - shot_pos
|
|
4374
|
+
abs_distances = np.abs(distances)
|
|
4375
|
+
|
|
4376
|
+
# Sort traces by distance from shot to determine left/right
|
|
4377
|
+
sorted_indices = np.argsort(distances)
|
|
4378
|
+
sorted_distances = distances[sorted_indices]
|
|
4379
|
+
|
|
4380
|
+
# Find traces on left (negative distance) and right (positive distance)
|
|
4381
|
+
left_mask = sorted_distances < 0
|
|
4382
|
+
right_mask = sorted_distances > 0
|
|
4383
|
+
|
|
4384
|
+
left_indices = sorted_indices[left_mask]
|
|
4385
|
+
right_indices = sorted_indices[right_mask]
|
|
4386
|
+
|
|
4387
|
+
if window_side == 'left':
|
|
4388
|
+
if len(left_indices) > 0:
|
|
4389
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance]
|
|
4390
|
+
if len(left_beyond_offset) > 0:
|
|
4181
4391
|
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4182
4392
|
selected_traces = left_sorted[:window_length]
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4188
|
-
|
|
4189
|
-
if len(left_beyond_offset) >= window_length:
|
|
4190
|
-
# Enough traces on the left beyond offset
|
|
4191
|
-
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4192
|
-
selected_traces = left_sorted[:window_length]
|
|
4193
|
-
first_trace = int(np.min(selected_traces))
|
|
4194
|
-
last_trace = int(np.max(selected_traces))
|
|
4195
|
-
else:
|
|
4196
|
-
# Try right side
|
|
4197
|
-
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4198
|
-
if len(right_beyond_offset) >= window_length:
|
|
4393
|
+
elif window_side == 'right':
|
|
4394
|
+
if len(right_indices) > 0:
|
|
4395
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance]
|
|
4396
|
+
if len(right_beyond_offset) > 0:
|
|
4199
4397
|
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4200
4398
|
selected_traces = right_sorted[:window_length]
|
|
4201
|
-
|
|
4202
|
-
last_trace = int(np.max(selected_traces))
|
|
4203
|
-
|
|
4204
|
-
# If still no valid range, reduce requirements (fewer than window_length traces or closer than offset)
|
|
4205
|
-
if first_trace is None or last_trace is None:
|
|
4206
|
-
# Use what's available, respecting preference
|
|
4399
|
+
elif window_side == 'best':
|
|
4207
4400
|
if prefer_side == 'right':
|
|
4208
|
-
# Try right side with any available traces beyond offset
|
|
4209
4401
|
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4210
|
-
if len(right_beyond_offset)
|
|
4402
|
+
if len(right_beyond_offset) >= window_length:
|
|
4211
4403
|
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4212
|
-
selected_traces = right_sorted
|
|
4213
|
-
first_trace = int(np.min(selected_traces))
|
|
4214
|
-
last_trace = int(np.max(selected_traces))
|
|
4404
|
+
selected_traces = right_sorted[:window_length]
|
|
4215
4405
|
else:
|
|
4216
|
-
# Try left side with any available traces beyond offset
|
|
4217
4406
|
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4218
|
-
if len(left_beyond_offset)
|
|
4407
|
+
if len(left_beyond_offset) >= window_length:
|
|
4219
4408
|
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4220
|
-
selected_traces = left_sorted
|
|
4221
|
-
first_trace = int(np.min(selected_traces))
|
|
4222
|
-
last_trace = int(np.max(selected_traces))
|
|
4223
|
-
elif len(right_indices) > 0:
|
|
4224
|
-
# No offset possible, use closest right traces
|
|
4225
|
-
right_sorted = right_indices[np.argsort(abs_distances[right_indices])]
|
|
4226
|
-
selected_traces = right_sorted[:window_length] if len(right_sorted) >= window_length else right_sorted
|
|
4227
|
-
first_trace = int(np.min(selected_traces))
|
|
4228
|
-
last_trace = int(np.max(selected_traces))
|
|
4229
|
-
elif len(left_indices) > 0:
|
|
4230
|
-
# No right traces, use closest left traces
|
|
4231
|
-
left_sorted = left_indices[np.argsort(abs_distances[left_indices])]
|
|
4232
|
-
selected_traces = left_sorted[:window_length] if len(left_sorted) >= window_length else left_sorted
|
|
4233
|
-
first_trace = int(np.min(selected_traces))
|
|
4234
|
-
last_trace = int(np.max(selected_traces))
|
|
4409
|
+
selected_traces = left_sorted[:window_length]
|
|
4235
4410
|
else:
|
|
4236
|
-
# Prefer left side
|
|
4237
|
-
# Try left side with any available traces beyond offset
|
|
4238
4411
|
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4239
|
-
if len(left_beyond_offset)
|
|
4412
|
+
if len(left_beyond_offset) >= window_length:
|
|
4240
4413
|
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4241
|
-
selected_traces = left_sorted
|
|
4242
|
-
first_trace = int(np.min(selected_traces))
|
|
4243
|
-
last_trace = int(np.max(selected_traces))
|
|
4414
|
+
selected_traces = left_sorted[:window_length]
|
|
4244
4415
|
else:
|
|
4245
|
-
|
|
4416
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4417
|
+
if len(right_beyond_offset) >= window_length:
|
|
4418
|
+
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4419
|
+
selected_traces = right_sorted[:window_length]
|
|
4420
|
+
|
|
4421
|
+
# If still no valid range, relax requirements
|
|
4422
|
+
if selected_traces is None:
|
|
4423
|
+
if prefer_side == 'right':
|
|
4246
4424
|
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4247
4425
|
if len(right_beyond_offset) > 0:
|
|
4248
4426
|
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4249
4427
|
selected_traces = right_sorted
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4428
|
+
else:
|
|
4429
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4430
|
+
if len(left_beyond_offset) > 0:
|
|
4431
|
+
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4432
|
+
selected_traces = left_sorted
|
|
4433
|
+
elif len(right_indices) > 0:
|
|
4434
|
+
right_sorted = right_indices[np.argsort(abs_distances[right_indices])]
|
|
4435
|
+
selected_traces = right_sorted[:window_length] if len(right_sorted) >= window_length else right_sorted
|
|
4436
|
+
elif len(left_indices) > 0:
|
|
4437
|
+
left_sorted = left_indices[np.argsort(abs_distances[left_indices])]
|
|
4438
|
+
selected_traces = left_sorted[:window_length] if len(left_sorted) >= window_length else left_sorted
|
|
4439
|
+
else:
|
|
4440
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4441
|
+
if len(left_beyond_offset) > 0:
|
|
4442
|
+
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4443
|
+
selected_traces = left_sorted
|
|
4444
|
+
else:
|
|
4445
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4446
|
+
if len(right_beyond_offset) > 0:
|
|
4447
|
+
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4448
|
+
selected_traces = right_sorted
|
|
4449
|
+
elif len(left_indices) > 0:
|
|
4450
|
+
left_sorted = left_indices[np.argsort(abs_distances[left_indices])]
|
|
4451
|
+
selected_traces = left_sorted[:window_length] if len(left_sorted) >= window_length else left_sorted
|
|
4452
|
+
elif len(right_indices) > 0:
|
|
4453
|
+
right_sorted = right_indices[np.argsort(abs_distances[right_indices])]
|
|
4454
|
+
selected_traces = right_sorted[:window_length] if len(right_sorted) >= window_length else right_sorted
|
|
4455
|
+
|
|
4456
|
+
if selected_traces is None:
|
|
4457
|
+
abs_distances = np.abs(distances)
|
|
4458
|
+
closest_indices = np.argsort(abs_distances)[:min(window_length, n_traces)]
|
|
4459
|
+
selected_traces = closest_indices
|
|
4460
|
+
|
|
4461
|
+
if selected_traces is not None and len(selected_traces) > 0:
|
|
4462
|
+
first_trace = int(np.min(selected_traces))
|
|
4463
|
+
last_trace = int(np.max(selected_traces))
|
|
4464
|
+
|
|
4465
|
+
# If we still don't have a valid window (e.g., constant_xmid with too few traces), keep previous
|
|
4266
4466
|
if first_trace is None or last_trace is None:
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
closest_indices = np.argsort(abs_distances)[:min(window_length, n_traces)]
|
|
4270
|
-
first_trace = int(np.min(closest_indices))
|
|
4271
|
-
last_trace = int(np.max(closest_indices))
|
|
4272
|
-
|
|
4467
|
+
continue
|
|
4468
|
+
|
|
4273
4469
|
# Ensure valid range
|
|
4274
4470
|
first_trace = max(0, min(first_trace, n_traces - 1))
|
|
4275
4471
|
last_trace = max(first_trace, min(last_trace, n_traces - 1))
|
|
4276
|
-
|
|
4472
|
+
|
|
4277
4473
|
# Store in trace range memory using shot_idx as key (consistent with onFileSelectionChanged)
|
|
4278
4474
|
file_key = shot_idx
|
|
4279
4475
|
if file_key not in self.trace_range_memory:
|
|
4280
4476
|
self.trace_range_memory[file_key] = {}
|
|
4281
|
-
|
|
4477
|
+
|
|
4282
4478
|
# Store for both views (sync them) using dictionary structure
|
|
4283
4479
|
self.trace_range_memory[file_key]['top'] = {
|
|
4284
4480
|
'first': first_trace,
|
|
@@ -4298,6 +4494,8 @@ class MainWindow(QMainWindow):
|
|
|
4298
4494
|
if not hasattr(self, 'xmid_values'):
|
|
4299
4495
|
self.xmid_values = {}
|
|
4300
4496
|
self.xmid_values[shot_idx] = center
|
|
4497
|
+
|
|
4498
|
+
applied_count += 1
|
|
4301
4499
|
|
|
4302
4500
|
# Update the current shot's range slider and attributes
|
|
4303
4501
|
if self.currentIndex is not None and self.currentIndex < len(self.streams):
|
|
@@ -4358,10 +4556,17 @@ class MainWindow(QMainWindow):
|
|
|
4358
4556
|
self._plot_target_viewbox = None
|
|
4359
4557
|
|
|
4360
4558
|
# Show confirmation message
|
|
4361
|
-
prefer_text = f", Prefer: {prefer_side}" if window_side == 'best' else ""
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4559
|
+
prefer_text = f", Prefer: {prefer_side}" if (mode == 'constant_offset' and window_side == 'best') else ""
|
|
4560
|
+
mode_text = "Constant xmid" if mode == 'constant_xmid' else "Constant offset"
|
|
4561
|
+
QMessageBox.information(
|
|
4562
|
+
self,
|
|
4563
|
+
"Batch Windowing",
|
|
4564
|
+
f"Applied batch windowing to {applied_count}/{total_shots} shots.\n"
|
|
4565
|
+
f"Mode: {mode_text}. Window: {window_length} traces"
|
|
4566
|
+
f"{', Center: %.2f m' % center_xmid if (mode == 'constant_xmid' and center_xmid is not None) else ''}"
|
|
4567
|
+
f"{', Side: ' + window_side if mode == 'constant_offset' else ''}"
|
|
4568
|
+
f"{prefer_text}"
|
|
4569
|
+
)
|
|
4365
4570
|
|
|
4366
4571
|
except Exception as e:
|
|
4367
4572
|
QMessageBox.critical(self, "Error", f"Failed to apply batch windowing: {str(e)}")
|
|
@@ -4694,6 +4899,9 @@ class MainWindow(QMainWindow):
|
|
|
4694
4899
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
4695
4900
|
return
|
|
4696
4901
|
|
|
4902
|
+
# Update dispersion window info in status bar
|
|
4903
|
+
self.updateDispersionWindowInfo()
|
|
4904
|
+
|
|
4697
4905
|
# Update the trace extent indicator and replot only for the controlled view
|
|
4698
4906
|
if controlled_view == "top":
|
|
4699
4907
|
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
@@ -4744,23 +4952,36 @@ class MainWindow(QMainWindow):
|
|
|
4744
4952
|
self.streams and self.trace_position and self.currentIndex < len(self.trace_position)):
|
|
4745
4953
|
self.dispersionWindowLabel.setText("")
|
|
4746
4954
|
return
|
|
4955
|
+
|
|
4956
|
+
# Only show info when a dispersion view is active
|
|
4957
|
+
top_dispersion = getattr(self, 'topPlotType', None) == 'dispersion'
|
|
4958
|
+
bottom_dispersion = getattr(self, 'bottomPlotType', None) == 'dispersion'
|
|
4959
|
+
if not (top_dispersion or bottom_dispersion):
|
|
4960
|
+
self.dispersionWindowLabel.setText("")
|
|
4961
|
+
return
|
|
4747
4962
|
|
|
4748
4963
|
# Determine which trace range to use - prioritize view-specific ranges
|
|
4749
4964
|
first_trace = None
|
|
4750
4965
|
last_trace = None
|
|
4751
4966
|
|
|
4752
|
-
#
|
|
4753
|
-
if
|
|
4754
|
-
first_trace = self
|
|
4755
|
-
last_trace = self
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
first_trace
|
|
4763
|
-
|
|
4967
|
+
# Prefer the trace range from the view currently showing the dispersion image
|
|
4968
|
+
if top_dispersion:
|
|
4969
|
+
first_trace = getattr(self, 'top_first_trace', None)
|
|
4970
|
+
last_trace = getattr(self, 'top_last_trace', None)
|
|
4971
|
+
if first_trace is None or last_trace is None:
|
|
4972
|
+
first_trace = getattr(self, 'bottom_first_trace', getattr(self, 'first_trace', None))
|
|
4973
|
+
last_trace = getattr(self, 'bottom_last_trace', getattr(self, 'last_trace', None))
|
|
4974
|
+
else:
|
|
4975
|
+
first_trace = getattr(self, 'bottom_first_trace', None)
|
|
4976
|
+
last_trace = getattr(self, 'bottom_last_trace', None)
|
|
4977
|
+
if first_trace is None or last_trace is None:
|
|
4978
|
+
first_trace = getattr(self, 'top_first_trace', getattr(self, 'first_trace', None))
|
|
4979
|
+
last_trace = getattr(self, 'top_last_trace', getattr(self, 'last_trace', None))
|
|
4980
|
+
# Final fallback to unified attributes if everything else is missing
|
|
4981
|
+
if first_trace is None or last_trace is None:
|
|
4982
|
+
if hasattr(self, 'first_trace') and hasattr(self, 'last_trace'):
|
|
4983
|
+
first_trace = self.first_trace
|
|
4984
|
+
last_trace = self.last_trace
|
|
4764
4985
|
|
|
4765
4986
|
# If no trace range is set, don't show anything
|
|
4766
4987
|
if first_trace is None or last_trace is None:
|
|
@@ -4792,10 +5013,16 @@ class MainWindow(QMainWindow):
|
|
|
4792
5013
|
self.xmid_values = {}
|
|
4793
5014
|
self.xmid_values[self.currentIndex] = center
|
|
4794
5015
|
|
|
5016
|
+
# Build the status message
|
|
5017
|
+
window_info = f"Disp. Window: First={pos_first:.1f}m, Last={pos_last:.1f}m, Center={center:.1f}m, Width={width:.1f}m ({n_traces_used} tr)"
|
|
5018
|
+
|
|
5019
|
+
# Append stacking info if enabled
|
|
5020
|
+
if getattr(self, 'dispersion_stack_mode', False):
|
|
5021
|
+
shot_count = getattr(self, 'dispersion_stack_shot_count', 0)
|
|
5022
|
+
window_info += f" | Stacked ({shot_count} shots)"
|
|
5023
|
+
|
|
4795
5024
|
# Update the status bar label
|
|
4796
|
-
self.dispersionWindowLabel.setText(
|
|
4797
|
-
f"Disp. Window: First={pos_first:.1f}m, Last={pos_last:.1f}m, Center={center:.1f}m, Width={width:.1f}m ({n_traces_used} tr)"
|
|
4798
|
-
)
|
|
5025
|
+
self.dispersionWindowLabel.setText(window_info)
|
|
4799
5026
|
|
|
4800
5027
|
def onTopNormPerTraceChanged(self, state):
|
|
4801
5028
|
"""Handle per-trace normalization toggle for top Spectrogram/Dispersion"""
|
|
@@ -5418,6 +5645,26 @@ class MainWindow(QMainWindow):
|
|
|
5418
5645
|
(hasattr(self, 'bottomPlotType') and self.bottomPlotType == "seismogram"):
|
|
5419
5646
|
self.updatePlots()
|
|
5420
5647
|
|
|
5648
|
+
def onDispersionShowAliasingLimitChanged(self, state):
|
|
5649
|
+
"""Handle aliasing limit display toggle for dispersion view"""
|
|
5650
|
+
self.dispersion_show_aliasing_limit = bool(state)
|
|
5651
|
+
|
|
5652
|
+
# Replot dispersion if currently displayed (set correct target widget)
|
|
5653
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
5654
|
+
if self.streams and self.currentIndex is not None:
|
|
5655
|
+
self._plot_target_widget = self.topPlotWidget
|
|
5656
|
+
self._plot_target_viewbox = self.topViewBox
|
|
5657
|
+
self.plotDispersion()
|
|
5658
|
+
self._plot_target_widget = None
|
|
5659
|
+
self._plot_target_viewbox = None
|
|
5660
|
+
elif hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
5661
|
+
if self.streams and self.currentIndex is not None:
|
|
5662
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
5663
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
5664
|
+
self.plotDispersion()
|
|
5665
|
+
self._plot_target_widget = None
|
|
5666
|
+
self._plot_target_viewbox = None
|
|
5667
|
+
|
|
5421
5668
|
def onSeismoShowDispWindowChanged(self, state):
|
|
5422
5669
|
"""Handle seismogram show dispersion window toggle"""
|
|
5423
5670
|
self.seismo_show_disp_window = bool(state)
|
|
@@ -5722,15 +5969,24 @@ class MainWindow(QMainWindow):
|
|
|
5722
5969
|
|
|
5723
5970
|
# Check if any of the dragged files are supported formats
|
|
5724
5971
|
for url in urls:
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5972
|
+
try:
|
|
5973
|
+
file_path = url.toLocalFile()
|
|
5974
|
+
if not file_path:
|
|
5975
|
+
continue
|
|
5976
|
+
# Check for seismic data formats: .su, .segy, .sgy, .sg2, .dat
|
|
5977
|
+
if file_path.lower().endswith(('.su', '.segy', '.sgy', '.sg2', '.dat', '.seg2')):
|
|
5978
|
+
accepted = True
|
|
5979
|
+
break
|
|
5980
|
+
# Check for pick files: .sgt
|
|
5981
|
+
if file_path.lower().endswith('.sgt'):
|
|
5982
|
+
accepted = True
|
|
5983
|
+
break
|
|
5984
|
+
# Check for dispersion picks: .pvc
|
|
5985
|
+
if file_path.lower().endswith('.pvc'):
|
|
5986
|
+
accepted = True
|
|
5987
|
+
break
|
|
5988
|
+
except Exception:
|
|
5989
|
+
continue
|
|
5734
5990
|
|
|
5735
5991
|
if accepted:
|
|
5736
5992
|
event.acceptProposedAction()
|
|
@@ -5745,30 +6001,44 @@ class MainWindow(QMainWindow):
|
|
|
5745
6001
|
urls = event.mimeData().urls()
|
|
5746
6002
|
seismic_files = []
|
|
5747
6003
|
pick_files = []
|
|
6004
|
+
pvc_files = []
|
|
5748
6005
|
|
|
5749
6006
|
# Categorize the dropped files
|
|
5750
6007
|
for url in urls:
|
|
5751
6008
|
file_path = url.toLocalFile()
|
|
5752
6009
|
|
|
5753
6010
|
# Seismic data files
|
|
5754
|
-
if file_path.lower().endswith(('.su', '.segy', '.sgy', '.sg2', '.dat')):
|
|
6011
|
+
if file_path.lower().endswith(('.su', '.segy', '.sgy', '.sg2', '.dat', '.seg2')):
|
|
5755
6012
|
seismic_files.append(file_path)
|
|
5756
|
-
#
|
|
6013
|
+
# Traveltime pick files
|
|
5757
6014
|
elif file_path.lower().endswith('.sgt'):
|
|
5758
6015
|
pick_files.append(file_path)
|
|
6016
|
+
# Dispersion pick files
|
|
6017
|
+
elif file_path.lower().endswith('.pvc'):
|
|
6018
|
+
pvc_files.append(file_path)
|
|
5759
6019
|
|
|
5760
6020
|
# Process seismic files
|
|
5761
6021
|
if seismic_files:
|
|
5762
|
-
|
|
5763
|
-
|
|
6022
|
+
try:
|
|
6023
|
+
self.openFile(fileNames_new=seismic_files)
|
|
6024
|
+
event.acceptProposedAction()
|
|
6025
|
+
except Exception as e:
|
|
6026
|
+
QMessageBox.critical(self, "Load Seismic Error", f"Error loading seismic files:\n{str(e)}")
|
|
5764
6027
|
|
|
5765
|
-
# Process pick files (load them after seismic files are loaded)
|
|
6028
|
+
# Process traveltime pick files (load them after seismic files are loaded)
|
|
5766
6029
|
if pick_files and self.streams:
|
|
5767
6030
|
for pick_file in pick_files:
|
|
5768
6031
|
try:
|
|
5769
6032
|
self.loadPicks(pick_file, verbose=False)
|
|
5770
6033
|
except Exception as e:
|
|
5771
6034
|
QMessageBox.warning(self, "Load Picks Error", f"Error loading picks from {pick_file}:\n{str(e)}")
|
|
6035
|
+
|
|
6036
|
+
# Process dispersion pick files
|
|
6037
|
+
if pvc_files:
|
|
6038
|
+
try:
|
|
6039
|
+
self.loadDispersionCurvesFromPvc(file_paths=pvc_files)
|
|
6040
|
+
except Exception as e:
|
|
6041
|
+
QMessageBox.warning(self, "Load Dispersion Picks Error", f"Error loading dispersion picks:\n{str(e)}")
|
|
5772
6042
|
|
|
5773
6043
|
def _get_wiggle_amplitude(self, x_pos, y_time):
|
|
5774
6044
|
"""
|
|
@@ -6168,6 +6438,129 @@ class MainWindow(QMainWindow):
|
|
|
6168
6438
|
if amp is not None:
|
|
6169
6439
|
amp_text = f", Amplitude: {amp:.4f}"
|
|
6170
6440
|
self.coordinatesLabel.setText(f"{x_label}: {x_value}, {y_label}: {y_value}{amp_text}")
|
|
6441
|
+
|
|
6442
|
+
elif self.bottomPlotType == 'pseudosection':
|
|
6443
|
+
# Pseudo-section: X = source position/FFID or window center, Y = frequency or wavelength
|
|
6444
|
+
use_source_x = getattr(self, 'pseudosection_use_source_x', False)
|
|
6445
|
+
use_wavelength = getattr(self, 'pseudosection_use_wavelength', False)
|
|
6446
|
+
|
|
6447
|
+
# Y-axis: frequency or wavelength
|
|
6448
|
+
if use_wavelength:
|
|
6449
|
+
y_label = "Wavelength"
|
|
6450
|
+
y_value = f"{view_coords.y():.1f} m"
|
|
6451
|
+
else:
|
|
6452
|
+
y_label = "Frequency"
|
|
6453
|
+
y_value = f"{view_coords.y():.1f} Hz"
|
|
6454
|
+
|
|
6455
|
+
# X-axis: depends on pseudosection_use_source_x
|
|
6456
|
+
if use_source_x:
|
|
6457
|
+
# Match layout Y-axis label (source position or FFID)
|
|
6458
|
+
plot_type_y = getattr(self, 'plotTypeY', 'source_position')
|
|
6459
|
+
if plot_type_y == 'ffid':
|
|
6460
|
+
x_label = "FFID"
|
|
6461
|
+
x_value = f"{view_coords.x():.0f}"
|
|
6462
|
+
elif plot_type_y == 'source_position':
|
|
6463
|
+
x_label = "Source Position"
|
|
6464
|
+
x_value = f"{view_coords.x():.1f} m"
|
|
6465
|
+
else:
|
|
6466
|
+
x_label = "X"
|
|
6467
|
+
x_value = f"{view_coords.x():.2f}"
|
|
6468
|
+
else:
|
|
6469
|
+
# Window center: position or trace number depending on plotTypeX
|
|
6470
|
+
plot_type_x = getattr(self, 'plotTypeX', 'trace_position')
|
|
6471
|
+
if plot_type_x == 'trace_position':
|
|
6472
|
+
x_label = "Window Center"
|
|
6473
|
+
x_value = f"{view_coords.x():.1f} m"
|
|
6474
|
+
else:
|
|
6475
|
+
# Trace number modes (shot_trace_number, file_trace_number, unique_trace_number)
|
|
6476
|
+
x_label = "Window Center"
|
|
6477
|
+
x_value = f"{view_coords.x():.1f}"
|
|
6478
|
+
|
|
6479
|
+
# Try to find nearby pick and show phase velocity
|
|
6480
|
+
phase_vel_text = ""
|
|
6481
|
+
try:
|
|
6482
|
+
if self.dispersion_curves is not None:
|
|
6483
|
+
show_interp = getattr(self, 'pseudosection_show_interpolated', False)
|
|
6484
|
+
selected_mode = getattr(self, 'pseudosection_mode', 0)
|
|
6485
|
+
|
|
6486
|
+
# Collect all pick points with their phase velocities
|
|
6487
|
+
pick_x_coords = []
|
|
6488
|
+
pick_y_coords = []
|
|
6489
|
+
pick_velocities = []
|
|
6490
|
+
|
|
6491
|
+
for shot_idx, shot_curves in enumerate(self.dispersion_curves):
|
|
6492
|
+
if shot_curves is None or 'modes' not in shot_curves:
|
|
6493
|
+
continue
|
|
6494
|
+
if selected_mode not in shot_curves['modes']:
|
|
6495
|
+
continue
|
|
6496
|
+
mode_data = shot_curves['modes'][selected_mode]
|
|
6497
|
+
|
|
6498
|
+
# Get frequencies and velocities
|
|
6499
|
+
freqs = None
|
|
6500
|
+
vels = None
|
|
6501
|
+
|
|
6502
|
+
if show_interp and 'frequencies' in mode_data:
|
|
6503
|
+
freqs = np.array(mode_data.get('frequencies', []))
|
|
6504
|
+
vels = np.array(mode_data.get('velocities', []))
|
|
6505
|
+
elif 'picked_points' in mode_data and mode_data['picked_points']:
|
|
6506
|
+
points = mode_data['picked_points']
|
|
6507
|
+
freqs = np.array([p[0] for p in points])
|
|
6508
|
+
vels = np.array([p[1] for p in points])
|
|
6509
|
+
|
|
6510
|
+
if freqs is not None and len(freqs) > 0:
|
|
6511
|
+
# Determine X coordinate for this shot
|
|
6512
|
+
if use_source_x:
|
|
6513
|
+
plot_type_y = getattr(self, 'plotTypeY', 'source_position')
|
|
6514
|
+
if plot_type_y == 'ffid' and hasattr(self, 'ffid') and shot_idx < len(self.ffid):
|
|
6515
|
+
try:
|
|
6516
|
+
x_coord = float(self.ffid[shot_idx])
|
|
6517
|
+
except Exception:
|
|
6518
|
+
continue
|
|
6519
|
+
elif shot_idx < len(self.source_position):
|
|
6520
|
+
x_coord = self.source_position[shot_idx]
|
|
6521
|
+
else:
|
|
6522
|
+
continue
|
|
6523
|
+
else:
|
|
6524
|
+
if shot_idx not in getattr(self, 'xmid_values', {}):
|
|
6525
|
+
continue
|
|
6526
|
+
x_coord = self.xmid_values[shot_idx]
|
|
6527
|
+
|
|
6528
|
+
# Add all points for this shot
|
|
6529
|
+
for freq, vel in zip(freqs, vels):
|
|
6530
|
+
if use_wavelength:
|
|
6531
|
+
wavelength = vel / freq if freq > 0 else 0
|
|
6532
|
+
pick_y_coords.append(wavelength)
|
|
6533
|
+
else:
|
|
6534
|
+
pick_y_coords.append(freq)
|
|
6535
|
+
pick_x_coords.append(x_coord)
|
|
6536
|
+
pick_velocities.append(vel)
|
|
6537
|
+
|
|
6538
|
+
# Find closest pick
|
|
6539
|
+
if len(pick_x_coords) > 0:
|
|
6540
|
+
pick_distances = []
|
|
6541
|
+
for i in range(len(pick_x_coords)):
|
|
6542
|
+
dx = pick_x_coords[i] - view_coords.x()
|
|
6543
|
+
dy = pick_y_coords[i] - view_coords.y()
|
|
6544
|
+
distance = (dx**2 + dy**2)**0.5
|
|
6545
|
+
pick_distances.append(distance)
|
|
6546
|
+
|
|
6547
|
+
min_distance = min(pick_distances)
|
|
6548
|
+
|
|
6549
|
+
# Define tolerance based on view range (about 2% of the view range)
|
|
6550
|
+
x_range = self.bottomViewBox.viewRange()[0]
|
|
6551
|
+
y_range = self.bottomViewBox.viewRange()[1]
|
|
6552
|
+
x_tolerance = abs(x_range[1] - x_range[0]) * 0.02
|
|
6553
|
+
y_tolerance = abs(y_range[1] - y_range[0]) * 0.02
|
|
6554
|
+
tolerance = (x_tolerance**2 + y_tolerance**2)**0.5
|
|
6555
|
+
|
|
6556
|
+
if min_distance < tolerance:
|
|
6557
|
+
closest_idx = pick_distances.index(min_distance)
|
|
6558
|
+
phase_vel = pick_velocities[closest_idx]
|
|
6559
|
+
phase_vel_text = f", Phase Velocity: {phase_vel:.1f} m/s"
|
|
6560
|
+
except Exception:
|
|
6561
|
+
pass
|
|
6562
|
+
|
|
6563
|
+
self.coordinatesLabel.setText(f"{x_label}: {x_value}, {y_label}: {y_value}{phase_vel_text}")
|
|
6171
6564
|
|
|
6172
6565
|
else: # 'layout' plot
|
|
6173
6566
|
# Layout plot: show pick time if available, otherwise show coordinates
|
|
@@ -6309,6 +6702,10 @@ class MainWindow(QMainWindow):
|
|
|
6309
6702
|
def initializeAttributes(self):
|
|
6310
6703
|
# Initialize the attributes
|
|
6311
6704
|
|
|
6705
|
+
# Initialize core data structure lists (MUST be first, used by openFile)
|
|
6706
|
+
self.fileNames = []
|
|
6707
|
+
self.streams = []
|
|
6708
|
+
|
|
6312
6709
|
self.currentFileName = None
|
|
6313
6710
|
self.currentIndex = None
|
|
6314
6711
|
self.streamIndex = None
|
|
@@ -6368,6 +6765,15 @@ class MainWindow(QMainWindow):
|
|
|
6368
6765
|
# Dispersion-specific normalization settings (independent from spectrogram)
|
|
6369
6766
|
self.dispersion_norm_per_trace = True
|
|
6370
6767
|
self.dispersion_norm_per_freq = True
|
|
6768
|
+
|
|
6769
|
+
# Dispersion stacking parameters (used when stacking shots)
|
|
6770
|
+
self.dispersion_stack_min_offset = 0.0 # Minimum offset to window edge (m)
|
|
6771
|
+
self.dispersion_stack_max_offset = None # Maximum offset to window edge (m); None = no limit
|
|
6772
|
+
self.dispersion_stack_side = 'Both sides' # Side preference: 'Both sides', 'Left only', 'Right only'
|
|
6773
|
+
|
|
6774
|
+
# Dispersion display enhancement parameters
|
|
6775
|
+
self.dispersion_enhance = False # Enhance weak modes using 1/(1-x) transformation
|
|
6776
|
+
self.dispersion_saturation = 1.0 # Saturation factor (0.5-1.5): 1.0=no change
|
|
6371
6777
|
|
|
6372
6778
|
# Unified analytical view parameter defaults (same for top/bottom)
|
|
6373
6779
|
self.fmin = None # Default fmin (auto -> 0 for Spectrogram/Phase-Shift)
|
|
@@ -6414,6 +6820,7 @@ class MainWindow(QMainWindow):
|
|
|
6414
6820
|
self.update_pick_flag = False
|
|
6415
6821
|
self.update_file_flag = False
|
|
6416
6822
|
self.pick_file = ""
|
|
6823
|
+
self.geophone_mapping = None # Maps (shot_idx, trace_idx) -> sgt_geophone_number for order matching
|
|
6417
6824
|
self.refrac_manager = None
|
|
6418
6825
|
self.output_format = 'SEGY'
|
|
6419
6826
|
self.headers_modified = False # Track if headers have been modified (includes trace edits)
|
|
@@ -6425,6 +6832,7 @@ class MainWindow(QMainWindow):
|
|
|
6425
6832
|
self.pseudosection_colormap_str = 'plasma' # Colormap for velocity encoding
|
|
6426
6833
|
self.pseudosection_invert_y = True # Default: Y-axis pointing downward
|
|
6427
6834
|
self.pseudosection_mode = 0 # Selected mode for pseudo-section display (M0, M1, M2, ...)
|
|
6835
|
+
self.pseudosection_use_source_x = False # False = window center (trace pos), True = source/FFID
|
|
6428
6836
|
|
|
6429
6837
|
# Dispersion curve picking data structures (per view: top/bottom)
|
|
6430
6838
|
# Format: {view: {'picked_points': [], 'picked_point_items': [], 'curve_line': None, 'current_dispersion_data': None}}
|
|
@@ -7055,6 +7463,13 @@ class MainWindow(QMainWindow):
|
|
|
7055
7463
|
freq_min = self.dispersionAutoPickFreqMinSpin.value()
|
|
7056
7464
|
freq_max = self.dispersionAutoPickFreqMaxSpin.value()
|
|
7057
7465
|
|
|
7466
|
+
# Get trace spacing for aliasing limit calculation
|
|
7467
|
+
dx = None
|
|
7468
|
+
if self.trace_position and self.currentIndex < len(self.trace_position):
|
|
7469
|
+
trace_pos = self.trace_position[self.currentIndex]
|
|
7470
|
+
if len(trace_pos) >= 2:
|
|
7471
|
+
dx = np.mean(np.abs(np.diff(trace_pos)))
|
|
7472
|
+
|
|
7058
7473
|
# Filter frequencies within range
|
|
7059
7474
|
freq_mask = (frequencies >= freq_min) & (frequencies <= freq_max)
|
|
7060
7475
|
|
|
@@ -7070,12 +7485,32 @@ class MainWindow(QMainWindow):
|
|
|
7070
7485
|
# Compute max per frequency (full velocity range) within frequency range
|
|
7071
7486
|
picked_points = []
|
|
7072
7487
|
picked_items = []
|
|
7073
|
-
for f_idx
|
|
7488
|
+
for f_idx in range(len(frequencies)):
|
|
7074
7489
|
freq = frequencies[f_idx]
|
|
7075
7490
|
# Only pick if frequency is in range
|
|
7076
7491
|
if not freq_mask[f_idx]:
|
|
7077
7492
|
continue
|
|
7493
|
+
|
|
7494
|
+
# Get dispersion slice at this frequency
|
|
7495
|
+
dispersion_slice = FV[f_idx, :]
|
|
7496
|
+
|
|
7497
|
+
# Apply aliasing limit: only consider velocities above first-order aliasing boundary
|
|
7498
|
+
# Formula: vi = f/(( f/v0) + (i/dx)), for i=1 the boundary is approximately v > f*dx
|
|
7499
|
+
if dx is not None and dx > 0:
|
|
7500
|
+
v_alias = freq * dx # First-order aliasing boundary
|
|
7501
|
+
aliasing_mask = velocities >= v_alias
|
|
7502
|
+
if not aliasing_mask.any():
|
|
7503
|
+
# No valid velocities above aliasing limit, skip this frequency
|
|
7504
|
+
continue
|
|
7505
|
+
# Mask out velocities below aliasing limit
|
|
7506
|
+
masked_slice = np.where(aliasing_mask, dispersion_slice, -np.inf)
|
|
7507
|
+
else:
|
|
7508
|
+
masked_slice = dispersion_slice
|
|
7509
|
+
|
|
7510
|
+
# Find maximum in the valid (non-aliased) region
|
|
7511
|
+
vel_idx = np.argmax(masked_slice)
|
|
7078
7512
|
vel = velocities[vel_idx]
|
|
7513
|
+
|
|
7079
7514
|
picked_points.append((freq, vel))
|
|
7080
7515
|
point_item = pqg.ScatterPlotItem(
|
|
7081
7516
|
x=[freq], y=[vel],
|
|
@@ -7093,9 +7528,28 @@ class MainWindow(QMainWindow):
|
|
|
7093
7528
|
picked_items.append(point_item)
|
|
7094
7529
|
plot_widget.addItem(point_item)
|
|
7095
7530
|
|
|
7096
|
-
# Persist
|
|
7531
|
+
# Persist to mode data
|
|
7097
7532
|
mode_data['picked_points'] = picked_points
|
|
7098
7533
|
mode_data['picked_point_items'] = picked_items
|
|
7534
|
+
|
|
7535
|
+
# Store picked points to dispersion_curves for persistence across shot switches
|
|
7536
|
+
if self.currentIndex is not None and self.currentIndex < len(self.dispersion_curves):
|
|
7537
|
+
if self.dispersion_curves[self.currentIndex] is None:
|
|
7538
|
+
self.dispersion_curves[self.currentIndex] = {'modes': {}}
|
|
7539
|
+
if 'modes' not in self.dispersion_curves[self.currentIndex]:
|
|
7540
|
+
self.dispersion_curves[self.currentIndex]['modes'] = {}
|
|
7541
|
+
|
|
7542
|
+
self.dispersion_curves[self.currentIndex]['modes'][state['current_mode']] = {
|
|
7543
|
+
'picked_points': picked_points.copy()
|
|
7544
|
+
}
|
|
7545
|
+
|
|
7546
|
+
# Sync current mode data to other view
|
|
7547
|
+
other_view = 'bottom' if view == 'top' else 'top'
|
|
7548
|
+
other_state = self.dispersion_picking_state[other_view]
|
|
7549
|
+
self._ensure_mode_exists(other_view, state['current_mode'])
|
|
7550
|
+
other_mode_data = other_state['modes'][state['current_mode']]
|
|
7551
|
+
other_mode_data['picked_points'] = picked_points.copy()
|
|
7552
|
+
other_mode_data['picked_point_items'] = [None] * len(picked_points)
|
|
7099
7553
|
|
|
7100
7554
|
def _refreshDispersionPicksDisplay(self):
|
|
7101
7555
|
"""Refresh display of all modes' picks and curves on dispersion plots"""
|
|
@@ -7470,6 +7924,13 @@ class MainWindow(QMainWindow):
|
|
|
7470
7924
|
is_auto_mode = self.dispersionSemiAutoCheckbox.isChecked()
|
|
7471
7925
|
vel_window = getattr(self, 'dispersion_auto_vel_window', 0.0)
|
|
7472
7926
|
|
|
7927
|
+
# Get trace spacing for aliasing limit
|
|
7928
|
+
dx = None
|
|
7929
|
+
if self.trace_position and self.currentIndex < len(self.trace_position):
|
|
7930
|
+
trace_pos = self.trace_position[self.currentIndex]
|
|
7931
|
+
if len(trace_pos) >= 2:
|
|
7932
|
+
dx = np.mean(np.abs(np.diff(trace_pos)))
|
|
7933
|
+
|
|
7473
7934
|
if is_auto_mode:
|
|
7474
7935
|
# Auto mode: find maximum at the clicked frequency, within optional velocity window
|
|
7475
7936
|
freq_idx = np.argmin(np.abs(frequencies - clicked_freq))
|
|
@@ -7478,15 +7939,27 @@ class MainWindow(QMainWindow):
|
|
|
7478
7939
|
# Get the dispersion values at this frequency
|
|
7479
7940
|
dispersion_slice = FV[freq_idx, :]
|
|
7480
7941
|
|
|
7942
|
+
# Apply aliasing limit: exclude velocities below first-order aliasing boundary
|
|
7943
|
+
# Formula: vi = f/((f/v0) + (i/dx)), for i=1 the boundary is approximately v > f*dx
|
|
7944
|
+
aliasing_mask = np.ones(len(velocities), dtype=bool)
|
|
7945
|
+
if dx is not None and dx > 0:
|
|
7946
|
+
v_alias = actual_freq * dx # First-order aliasing boundary
|
|
7947
|
+
aliasing_mask = velocities >= v_alias
|
|
7948
|
+
|
|
7481
7949
|
# Apply velocity window if specified (>0): search only around clicked velocity
|
|
7482
7950
|
if vel_window > 0:
|
|
7483
7951
|
vel_mask = np.abs(velocities - clicked_velocity) <= vel_window
|
|
7484
|
-
|
|
7485
|
-
|
|
7952
|
+
combined_mask = vel_mask & aliasing_mask
|
|
7953
|
+
if combined_mask.any():
|
|
7954
|
+
masked_slice = np.where(combined_mask, dispersion_slice, -np.inf)
|
|
7486
7955
|
else:
|
|
7487
7956
|
masked_slice = dispersion_slice # fallback to full range if window empty
|
|
7488
7957
|
else:
|
|
7489
|
-
|
|
7958
|
+
# Just apply aliasing mask
|
|
7959
|
+
if aliasing_mask.any():
|
|
7960
|
+
masked_slice = np.where(aliasing_mask, dispersion_slice, -np.inf)
|
|
7961
|
+
else:
|
|
7962
|
+
masked_slice = dispersion_slice
|
|
7490
7963
|
|
|
7491
7964
|
max_vel_idx = np.argmax(masked_slice)
|
|
7492
7965
|
picked_velocity = velocities[max_vel_idx]
|
|
@@ -8116,12 +8589,17 @@ class MainWindow(QMainWindow):
|
|
|
8116
8589
|
if view:
|
|
8117
8590
|
self.interpolateDispersionCurve(view)
|
|
8118
8591
|
|
|
8119
|
-
def loadDispersionCurvesFromPvc(self):
|
|
8120
|
-
"""Load dispersion curves from .pvc files and match to shots by Xmid
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8592
|
+
def loadDispersionCurvesFromPvc(self, file_paths=None):
|
|
8593
|
+
"""Load dispersion curves from .pvc files and match to shots by Xmid
|
|
8594
|
+
|
|
8595
|
+
Args:
|
|
8596
|
+
file_paths: Optional list of file paths. If None, opens file dialog.
|
|
8597
|
+
"""
|
|
8598
|
+
# Open file dialog if no paths provided
|
|
8599
|
+
if file_paths is None:
|
|
8600
|
+
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
8601
|
+
self, "Select .pvc Files", "", "PVC Files (*.pvc);;All Files (*)"
|
|
8602
|
+
)
|
|
8125
8603
|
|
|
8126
8604
|
if not file_paths:
|
|
8127
8605
|
return
|
|
@@ -8364,8 +8842,6 @@ class MainWindow(QMainWindow):
|
|
|
8364
8842
|
unmatch_label.setWordWrap(True)
|
|
8365
8843
|
unmatch_label.setStyleSheet("color: #ff6f00;")
|
|
8366
8844
|
content_layout.addWidget(unmatch_label)
|
|
8367
|
-
|
|
8368
|
-
# Debug info removed - was too verbose for normal operation
|
|
8369
8845
|
|
|
8370
8846
|
content_layout.addStretch()
|
|
8371
8847
|
scroll_area.setWidget(content_widget)
|
|
@@ -8633,6 +9109,22 @@ class MainWindow(QMainWindow):
|
|
|
8633
9109
|
|
|
8634
9110
|
def _on_dispersion_live_interpolate_changed(self, state):
|
|
8635
9111
|
"""Handler for live interpolate checkbox state change"""
|
|
9112
|
+
# Synchronize with pseudo-section interpolate checkboxes
|
|
9113
|
+
is_checked = (state == Qt.Checked)
|
|
9114
|
+
if hasattr(self, 'topPseudosectionInterpCheck'):
|
|
9115
|
+
self.topPseudosectionInterpCheck.blockSignals(True)
|
|
9116
|
+
self.topPseudosectionInterpCheck.setChecked(is_checked)
|
|
9117
|
+
self.topPseudosectionInterpCheck.blockSignals(False)
|
|
9118
|
+
if hasattr(self, 'bottomPseudosectionInterpCheck'):
|
|
9119
|
+
self.bottomPseudosectionInterpCheck.blockSignals(True)
|
|
9120
|
+
self.bottomPseudosectionInterpCheck.setChecked(is_checked)
|
|
9121
|
+
self.bottomPseudosectionInterpCheck.blockSignals(False)
|
|
9122
|
+
self.pseudosection_show_interpolated = is_checked
|
|
9123
|
+
|
|
9124
|
+
# Interpolate for pseudo-section if enabling
|
|
9125
|
+
if is_checked:
|
|
9126
|
+
self._interpolateAllShotsForPseudoSection()
|
|
9127
|
+
|
|
8636
9128
|
view = self._get_active_dispersion_view()
|
|
8637
9129
|
if view:
|
|
8638
9130
|
if state == Qt.Checked:
|
|
@@ -8684,6 +9176,86 @@ class MainWindow(QMainWindow):
|
|
|
8684
9176
|
if len(mode_data['picked_points']) >= 2:
|
|
8685
9177
|
self.interpolateDispersionCurve(view)
|
|
8686
9178
|
|
|
9179
|
+
def onDispersionStackToggled(self, state):
|
|
9180
|
+
"""Toggle stacking of dispersion images from all shots in current window range"""
|
|
9181
|
+
self.dispersion_stack_mode = bool(state)
|
|
9182
|
+
# Keep both top/bottom checkboxes in sync
|
|
9183
|
+
for attr in ("top_dispersionStackCheckbox", "bottom_dispersionStackCheckbox"):
|
|
9184
|
+
cb = getattr(self, attr, None)
|
|
9185
|
+
if cb is not None and cb.isChecked() != self.dispersion_stack_mode:
|
|
9186
|
+
cb.blockSignals(True)
|
|
9187
|
+
cb.setChecked(self.dispersion_stack_mode)
|
|
9188
|
+
cb.blockSignals(False)
|
|
9189
|
+
# Replot dispersion with stacking enabled/disabled
|
|
9190
|
+
if getattr(self, 'bottomPlotType', None) == 'dispersion':
|
|
9191
|
+
# Ensure we render into the bottom plot when it owns the dispersion view
|
|
9192
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
9193
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
9194
|
+
self.plotDispersion()
|
|
9195
|
+
self._plot_target_widget = None
|
|
9196
|
+
self._plot_target_viewbox = None
|
|
9197
|
+
if getattr(self, 'topPlotType', None) == 'dispersion':
|
|
9198
|
+
# Ensure we render into the top plot when it owns the dispersion view
|
|
9199
|
+
self._plot_target_widget = self.topPlotWidget
|
|
9200
|
+
self._plot_target_viewbox = self.topViewBox
|
|
9201
|
+
self.plotDispersion()
|
|
9202
|
+
self._plot_target_widget = None
|
|
9203
|
+
self._plot_target_viewbox = None
|
|
9204
|
+
|
|
9205
|
+
def onDispersionStackParamsClicked(self):
|
|
9206
|
+
"""Open a dialog to configure shot stacking parameters for dispersion."""
|
|
9207
|
+
# Current parameter values
|
|
9208
|
+
min_off = getattr(self, 'dispersion_stack_min_offset', 0.0)
|
|
9209
|
+
max_off = getattr(self, 'dispersion_stack_max_offset', None)
|
|
9210
|
+
side_pref = getattr(self, 'dispersion_stack_side', 'Both sides')
|
|
9211
|
+
|
|
9212
|
+
parameters = [
|
|
9213
|
+
{
|
|
9214
|
+
'label': 'Min offset from window edge (m)',
|
|
9215
|
+
'initial_value': min_off,
|
|
9216
|
+
'type': 'float'
|
|
9217
|
+
},
|
|
9218
|
+
{
|
|
9219
|
+
'label': 'Max offset from window edge (m)',
|
|
9220
|
+
'initial_value': '' if max_off is None else max_off,
|
|
9221
|
+
'type': 'float_or_empty'
|
|
9222
|
+
},
|
|
9223
|
+
{
|
|
9224
|
+
'label': 'Use shots from side',
|
|
9225
|
+
'initial_value': side_pref,
|
|
9226
|
+
'type': 'combo',
|
|
9227
|
+
'values': ['Both sides', 'Left only', 'Right only']
|
|
9228
|
+
}
|
|
9229
|
+
]
|
|
9230
|
+
|
|
9231
|
+
dialog = GenericParameterDialog(title="Stacking Parameters", parameters=parameters, add_checkbox=False, parent=self)
|
|
9232
|
+
if dialog.exec_():
|
|
9233
|
+
values = dialog.getValues()
|
|
9234
|
+
# Update internal parameters
|
|
9235
|
+
self.dispersion_stack_min_offset = values.get('Min offset from window edge (m)', 0.0) or 0.0
|
|
9236
|
+
self.dispersion_stack_max_offset = values.get('Max offset from window edge (m)', None)
|
|
9237
|
+
self.dispersion_stack_side = values.get('Use shots from side', 'Both sides') or 'Both sides'
|
|
9238
|
+
|
|
9239
|
+
# Replot dispersion in whichever view(s) currently show it
|
|
9240
|
+
if getattr(self, 'bottomPlotType', None) == 'dispersion':
|
|
9241
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
9242
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
9243
|
+
try:
|
|
9244
|
+
self.plotDispersion()
|
|
9245
|
+
except Exception:
|
|
9246
|
+
pass
|
|
9247
|
+
self._plot_target_widget = None
|
|
9248
|
+
self._plot_target_viewbox = None
|
|
9249
|
+
if getattr(self, 'topPlotType', None) == 'dispersion':
|
|
9250
|
+
self._plot_target_widget = self.topPlotWidget
|
|
9251
|
+
self._plot_target_viewbox = self.topViewBox
|
|
9252
|
+
try:
|
|
9253
|
+
self.plotDispersion()
|
|
9254
|
+
except Exception:
|
|
9255
|
+
pass
|
|
9256
|
+
self._plot_target_widget = None
|
|
9257
|
+
self._plot_target_viewbox = None
|
|
9258
|
+
|
|
8687
9259
|
def _calculateDispersionErrors(self, view, mode_data=None):
|
|
8688
9260
|
"""Calculate Lorentzian errors for dispersion picks
|
|
8689
9261
|
|
|
@@ -9332,7 +9904,10 @@ class MainWindow(QMainWindow):
|
|
|
9332
9904
|
flat_plot_data_x = [item for sublist in plot_data_x for item in sublist]
|
|
9333
9905
|
|
|
9334
9906
|
# Access the appropriate attribute - flatten picks from ALL streams (like Layout View)
|
|
9335
|
-
|
|
9907
|
+
if self.picks is not None:
|
|
9908
|
+
flat_plot_data_y = [item for sublist in self.picks for item in sublist]
|
|
9909
|
+
else:
|
|
9910
|
+
flat_plot_data_y = []
|
|
9336
9911
|
|
|
9337
9912
|
# Get unique traces and times from list of list of traces array that are not None
|
|
9338
9913
|
traces = [trace for trace in flat_plot_data_x if trace is not None]
|
|
@@ -9806,6 +10381,8 @@ class MainWindow(QMainWindow):
|
|
|
9806
10381
|
self.plotSpectrogram()
|
|
9807
10382
|
elif self.topPlotType == "dispersion":
|
|
9808
10383
|
self.plotDispersion()
|
|
10384
|
+
elif self.topPlotType == "pseudosection":
|
|
10385
|
+
self.plotPseudoSection()
|
|
9809
10386
|
|
|
9810
10387
|
# Reset target widget after plotting
|
|
9811
10388
|
self._plot_target_widget = None
|
|
@@ -9828,12 +10405,23 @@ class MainWindow(QMainWindow):
|
|
|
9828
10405
|
self.plotSpectrogram()
|
|
9829
10406
|
elif self.bottomPlotType == "dispersion":
|
|
9830
10407
|
self.plotDispersion()
|
|
10408
|
+
elif self.bottomPlotType == "pseudosection":
|
|
10409
|
+
self.plotPseudoSection()
|
|
9831
10410
|
|
|
9832
10411
|
# Reset target widget after plotting
|
|
9833
10412
|
self._plot_target_widget = None
|
|
9834
10413
|
self._plot_target_viewbox = None
|
|
9835
10414
|
|
|
9836
|
-
# Update
|
|
10415
|
+
# Update FFID/source in status bar for all views
|
|
10416
|
+
if hasattr(self, 'ffid') and self.ffid and self.currentIndex < len(self.ffid):
|
|
10417
|
+
try:
|
|
10418
|
+
ffid_val = self.ffid[self.currentIndex]
|
|
10419
|
+
src_val = self.source_position[self.currentIndex] if self.source_position else 'N/A'
|
|
10420
|
+
self.ffidLabel.setText(f'FFID: {ffid_val} | Source at {src_val} m')
|
|
10421
|
+
except Exception:
|
|
10422
|
+
pass
|
|
10423
|
+
|
|
10424
|
+
# Update dispersion window info in status bar
|
|
9837
10425
|
self.updateDispersionWindowInfo()
|
|
9838
10426
|
|
|
9839
10427
|
def navigateToPreviousFile(self):
|
|
@@ -10051,10 +10639,6 @@ class MainWindow(QMainWindow):
|
|
|
10051
10639
|
# Optionally subtract the mean to center around zero
|
|
10052
10640
|
# data_matrix = data_matrix - np.mean(data_matrix)
|
|
10053
10641
|
|
|
10054
|
-
print(f"DEBUG: Matrix shape: {data_matrix.shape}")
|
|
10055
|
-
print(f"DEBUG: Value range: {np.min(data_matrix)} to {np.max(data_matrix)}")
|
|
10056
|
-
print(f"DEBUG: Data type: {data_matrix.dtype}")
|
|
10057
|
-
|
|
10058
10642
|
# Convert to ObsPy stream
|
|
10059
10643
|
stream = self.ascii_to_obspy_stream(data_matrix, params)
|
|
10060
10644
|
|
|
@@ -16055,11 +16639,20 @@ class MainWindow(QMainWindow):
|
|
|
16055
16639
|
self.tracePositionAction.setChecked(False)
|
|
16056
16640
|
# Disable air wave checkbox when not plotting by position (but keep it checked)
|
|
16057
16641
|
if hasattr(self, 'airWaveWiggleCheck'):
|
|
16058
|
-
|
|
16642
|
+
try:
|
|
16643
|
+
self.airWaveWiggleCheck.setEnabled(False)
|
|
16644
|
+
except RuntimeError:
|
|
16645
|
+
pass
|
|
16059
16646
|
if hasattr(self, 'showAirWaveAction'):
|
|
16060
|
-
|
|
16647
|
+
try:
|
|
16648
|
+
self.showAirWaveAction.setEnabled(False)
|
|
16649
|
+
except RuntimeError:
|
|
16650
|
+
pass
|
|
16061
16651
|
if hasattr(self, 'seismoShowAirWaveCheck'):
|
|
16062
|
-
|
|
16652
|
+
try:
|
|
16653
|
+
self.seismoShowAirWaveCheck.setEnabled(False)
|
|
16654
|
+
except RuntimeError:
|
|
16655
|
+
pass
|
|
16063
16656
|
self.mean_dg = 1
|
|
16064
16657
|
self.x_label = 'Trace Number (Original Field Record)'
|
|
16065
16658
|
if self.streams:
|
|
@@ -17316,6 +17909,16 @@ class MainWindow(QMainWindow):
|
|
|
17316
17909
|
self.update_pick_flag = False
|
|
17317
17910
|
self.update_file_flag = False
|
|
17318
17911
|
|
|
17912
|
+
# Update status bar FFID/source when dispersion or pseudo-section is active (ensure it stays current when navigating shots)
|
|
17913
|
+
if (self.bottomPlotType in ('pseudosection', 'dispersion') or
|
|
17914
|
+
self.topPlotType in ('pseudosection', 'dispersion')) and self.currentIndex is not None:
|
|
17915
|
+
try:
|
|
17916
|
+
ffid_val = self.ffid[self.currentIndex] if hasattr(self, 'ffid') and self.ffid else 'N/A'
|
|
17917
|
+
src_val = self.source_position[self.currentIndex] if self.source_position else 'N/A'
|
|
17918
|
+
self.ffidLabel.setText(f'FFID: {ffid_val} | Source at {src_val} m')
|
|
17919
|
+
except Exception:
|
|
17920
|
+
pass
|
|
17921
|
+
|
|
17319
17922
|
# Draw/update trace extent indicator (only shown on seismogram when dispersion is active)
|
|
17320
17923
|
self.drawTraceExtentIndicator()
|
|
17321
17924
|
|
|
@@ -17841,76 +18444,212 @@ class MainWindow(QMainWindow):
|
|
|
17841
18444
|
first_trace = max(0, min(first_trace, n_traces - 1))
|
|
17842
18445
|
last_trace = max(first_trace + 1, min(last_trace, n_traces - 1)) # Ensure at least 2 traces
|
|
17843
18446
|
|
|
17844
|
-
#
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
|
|
17857
|
-
|
|
18447
|
+
# Check if stacking is enabled
|
|
18448
|
+
use_stacking = getattr(self, 'dispersion_stack_mode', False)
|
|
18449
|
+
|
|
18450
|
+
if use_stacking:
|
|
18451
|
+
# Stack dispersion from all shots with traces in current window range
|
|
18452
|
+
# Get common parameters
|
|
18453
|
+
vmin = getattr(self, 'vmin', 10.0)
|
|
18454
|
+
vmax = getattr(self, 'vmax', 1500.0)
|
|
18455
|
+
dv = getattr(self, 'dv', 5.0)
|
|
18456
|
+
dt_ref = self.sample_interval[self.currentIndex]
|
|
18457
|
+
nyquist = 1.0 / (2 * dt_ref)
|
|
18458
|
+
fmin_cfg = self.fmin if getattr(self, 'fmin', None) is not None else 0.0
|
|
18459
|
+
fmax_default = min(100.0, nyquist)
|
|
18460
|
+
fmax_cfg = self.fmax if getattr(self, 'fmax', None) is not None else fmax_default
|
|
18461
|
+
fmin = max(0.0, min(fmin_cfg, nyquist))
|
|
18462
|
+
fmax = max(0.0, min(fmax_cfg, nyquist))
|
|
18463
|
+
|
|
18464
|
+
stacked_FV = None
|
|
18465
|
+
fs_ref = None
|
|
18466
|
+
vs_ref = None
|
|
18467
|
+
shot_count = 0
|
|
18468
|
+
|
|
18469
|
+
for shot_idx, stream_shot in enumerate(self.streams):
|
|
18470
|
+
n_traces_shot = len(stream_shot)
|
|
18471
|
+
if n_traces_shot <= first_trace:
|
|
18472
|
+
continue
|
|
18473
|
+
|
|
18474
|
+
last_trace_actual = min(last_trace, n_traces_shot - 1)
|
|
18475
|
+
trace_indices_shot = np.arange(first_trace, last_trace_actual + 1)
|
|
18476
|
+
|
|
18477
|
+
# Exclude shots whose source lies inside the current trace window
|
|
18478
|
+
if (self.trace_position and shot_idx < len(self.trace_position) and
|
|
18479
|
+
self.source_position and shot_idx < len(self.source_position)):
|
|
18480
|
+
trace_positions_window = [self.trace_position[shot_idx][i] for i in trace_indices_shot]
|
|
18481
|
+
source_pos = self.source_position[shot_idx]
|
|
18482
|
+
window_min = min(trace_positions_window)
|
|
18483
|
+
window_max = max(trace_positions_window)
|
|
18484
|
+
|
|
18485
|
+
# Skip this shot if source is inside the window
|
|
18486
|
+
if window_min <= source_pos <= window_max:
|
|
18487
|
+
continue
|
|
18488
|
+
|
|
18489
|
+
# Apply side preference and offset constraints relative to window edges
|
|
18490
|
+
side_pref = getattr(self, 'dispersion_stack_side', 'Both sides')
|
|
18491
|
+
min_off = getattr(self, 'dispersion_stack_min_offset', 0.0) or 0.0
|
|
18492
|
+
max_off = getattr(self, 'dispersion_stack_max_offset', None)
|
|
18493
|
+
|
|
18494
|
+
# Determine which side the shot is on and its edge offset
|
|
18495
|
+
if source_pos < window_min:
|
|
18496
|
+
shot_side = 'Left only'
|
|
18497
|
+
edge_offset = window_min - source_pos
|
|
18498
|
+
elif source_pos > window_max:
|
|
18499
|
+
shot_side = 'Right only'
|
|
18500
|
+
edge_offset = source_pos - window_max
|
|
18501
|
+
else:
|
|
18502
|
+
# Already excluded above; keep for safety
|
|
18503
|
+
continue
|
|
18504
|
+
|
|
18505
|
+
# Enforce side preference
|
|
18506
|
+
if side_pref == 'Left only' and shot_side != 'Left only':
|
|
18507
|
+
continue
|
|
18508
|
+
if side_pref == 'Right only' and shot_side != 'Right only':
|
|
18509
|
+
continue
|
|
18510
|
+
|
|
18511
|
+
# Enforce offset constraints
|
|
18512
|
+
if edge_offset < min_off:
|
|
18513
|
+
continue
|
|
18514
|
+
if (max_off is not None) and (edge_offset > max_off):
|
|
18515
|
+
continue
|
|
18516
|
+
|
|
18517
|
+
try:
|
|
18518
|
+
offsets_all_shot = np.array([self.offset[shot_idx][i] for i in trace_indices_shot])
|
|
18519
|
+
distances_all_shot = np.abs(offsets_all_shot)
|
|
18520
|
+
|
|
18521
|
+
if getattr(self, 'use_topography_correction', False):
|
|
18522
|
+
trace_elev = np.array([self.trace_elevation[shot_idx][i] for i in trace_indices_shot])
|
|
18523
|
+
source_elev = self.source_elevation[shot_idx]
|
|
18524
|
+
dz = source_elev - trace_elev
|
|
18525
|
+
distances_all_shot = np.sqrt(distances_all_shot**2 + dz**2)
|
|
18526
|
+
|
|
18527
|
+
sorted_order = np.argsort(offsets_all_shot)
|
|
18528
|
+
distances_sorted = distances_all_shot[sorted_order]
|
|
18529
|
+
trace_indices_shot_sorted = trace_indices_shot[sorted_order]
|
|
18530
|
+
|
|
18531
|
+
data_shot = np.array([stream_shot[i].data for i in trace_indices_shot_sorted])
|
|
18532
|
+
dt_shot = self.sample_interval[shot_idx]
|
|
18533
|
+
|
|
18534
|
+
fs, vs, FV_shot = phase_shift(data_shot, dt_shot, distances_sorted, vmin, vmax, dv, fmax, fmin)
|
|
18535
|
+
|
|
18536
|
+
eps = 1e-12
|
|
18537
|
+
# Discard amplitudes below aliasing boundary for this shot
|
|
18538
|
+
dx_alias = None
|
|
18539
|
+
if len(distances_sorted) >= 2:
|
|
18540
|
+
dx_alias = np.mean(np.abs(np.diff(distances_sorted)))
|
|
18541
|
+
if dx_alias and dx_alias > 0:
|
|
18542
|
+
v_alias = 2.0 * fs[:, None] * dx_alias
|
|
18543
|
+
alias_mask = vs[None, :] >= v_alias
|
|
18544
|
+
FV_shot = np.where(alias_mask, FV_shot, 0.0)
|
|
18545
|
+
|
|
18546
|
+
if getattr(self, 'dispersion_norm_per_freq', False):
|
|
18547
|
+
max_per_f = np.maximum(np.max(FV_shot, axis=1, keepdims=True), eps)
|
|
18548
|
+
FV_shot = FV_shot / max_per_f
|
|
18549
|
+
|
|
18550
|
+
if stacked_FV is None:
|
|
18551
|
+
stacked_FV = FV_shot.copy()
|
|
18552
|
+
fs_ref = fs
|
|
18553
|
+
vs_ref = vs
|
|
18554
|
+
else:
|
|
18555
|
+
stacked_FV += FV_shot
|
|
18556
|
+
|
|
18557
|
+
shot_count += 1
|
|
18558
|
+
except (IndexError, ValueError, KeyError):
|
|
18559
|
+
continue
|
|
17858
18560
|
|
|
17859
|
-
|
|
17860
|
-
|
|
18561
|
+
if stacked_FV is None or shot_count == 0:
|
|
18562
|
+
QMessageBox.warning(self, "No Data", "No shots found in current window range.")
|
|
18563
|
+
return
|
|
17861
18564
|
|
|
17862
|
-
#
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
# Apply topography correction if enabled
|
|
17866
|
-
if getattr(self, 'use_topography_correction', False):
|
|
17867
|
-
# Get elevation data for selected traces and source
|
|
17868
|
-
trace_elevations = np.array([self.trace_elevation[self.currentIndex][i] for i in trace_indices])
|
|
17869
|
-
source_elevation = self.source_elevation[self.currentIndex]
|
|
18565
|
+
# Store the number of shots used in stacking for status bar display
|
|
18566
|
+
self.dispersion_stack_shot_count = shot_count
|
|
17870
18567
|
|
|
17871
|
-
|
|
17872
|
-
|
|
18568
|
+
FV = stacked_FV / shot_count
|
|
18569
|
+
fs = fs_ref
|
|
18570
|
+
vs = vs_ref
|
|
18571
|
+
else:
|
|
18572
|
+
# Original single-shot logic
|
|
18573
|
+
# Skip computation if source is within the current trace window
|
|
18574
|
+
trace_indices = np.arange(first_trace, last_trace + 1)
|
|
18575
|
+
trace_pos_all = np.array([self.trace_position[self.currentIndex][i] for i in trace_indices])
|
|
18576
|
+
source_pos = self.source_position[self.currentIndex]
|
|
18577
|
+
window_min = np.min(trace_pos_all)
|
|
18578
|
+
window_max = np.max(trace_pos_all)
|
|
18579
|
+
|
|
18580
|
+
if window_min <= source_pos <= window_max:
|
|
18581
|
+
# Source is within window, skip computation silently
|
|
18582
|
+
return
|
|
17873
18583
|
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
|
|
17894
|
-
|
|
17895
|
-
|
|
17896
|
-
|
|
17897
|
-
|
|
17898
|
-
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17904
|
-
|
|
18584
|
+
offsets_all = np.array([self.offset[self.currentIndex][i] for i in trace_indices])
|
|
18585
|
+
|
|
18586
|
+
# Compute distances (absolute distance from source)
|
|
18587
|
+
distances_all = np.abs(offsets_all)
|
|
18588
|
+
|
|
18589
|
+
# Apply topography correction if enabled
|
|
18590
|
+
if getattr(self, 'use_topography_correction', False):
|
|
18591
|
+
# Get elevation data for selected traces and source
|
|
18592
|
+
trace_elevations = np.array([self.trace_elevation[self.currentIndex][i] for i in trace_indices])
|
|
18593
|
+
source_elevation = self.source_elevation[self.currentIndex]
|
|
18594
|
+
|
|
18595
|
+
# Calculate elevation differences
|
|
18596
|
+
dz = trace_elevations - source_elevation
|
|
18597
|
+
|
|
18598
|
+
# Calculate slant distance: sqrt(horizontal_distance^2 + elevation_difference^2)
|
|
18599
|
+
distances_all = np.sqrt(distances_all**2 + dz**2)
|
|
18600
|
+
|
|
18601
|
+
# Sort traces by offset value (left to right spatially)
|
|
18602
|
+
sorted_order = np.argsort(offsets_all)
|
|
18603
|
+
trace_indices = trace_indices[sorted_order]
|
|
18604
|
+
trace_pos_sorted = trace_pos_all[sorted_order]
|
|
18605
|
+
offsets_sorted = offsets_all[sorted_order]
|
|
18606
|
+
distances_sorted = distances_all[sorted_order]
|
|
18607
|
+
|
|
18608
|
+
# Get data in sorted order
|
|
18609
|
+
data = np.array([stream[i].data for i in trace_indices])
|
|
18610
|
+
offsets = distances_sorted # Pass distances (absolute) to phase_shift, not offsets
|
|
18611
|
+
|
|
18612
|
+
# Get parameters
|
|
18613
|
+
dt = self.sample_interval[self.currentIndex]
|
|
18614
|
+
|
|
18615
|
+
# Parameters for phase-shift analysis (use UI values if provided)
|
|
18616
|
+
vmin = getattr(self, 'vmin', 10.0)
|
|
18617
|
+
vmax = getattr(self, 'vmax', 1500.0)
|
|
18618
|
+
dv = getattr(self, 'dv', 5.0)
|
|
18619
|
+
nyquist = 1.0 / (2 * dt)
|
|
18620
|
+
# Use configured defaults; if None provided by UI, fall back to safe bounds
|
|
18621
|
+
fmin_cfg = self.fmin if getattr(self, 'fmin', None) is not None else 0.0
|
|
18622
|
+
fmax_default = min(100.0, nyquist)
|
|
18623
|
+
fmax_cfg = self.fmax if getattr(self, 'fmax', None) is not None else fmax_default
|
|
18624
|
+
# Clamp to [0, Nyquist] defensively
|
|
18625
|
+
fmin = max(0.0, min(fmin_cfg, nyquist))
|
|
18626
|
+
fmax = max(0.0, min(fmax_cfg, nyquist))
|
|
17905
18627
|
# Compute phase-shift transform
|
|
17906
18628
|
fs, vs, FV = phase_shift(data, dt, offsets, vmin, vmax, dv, fmax, fmin)
|
|
17907
18629
|
|
|
18630
|
+
# Store raw FV for potential inversion transformation
|
|
18631
|
+
FV_raw = FV.copy()
|
|
18632
|
+
|
|
18633
|
+
# Discard amplitudes below aliasing boundary before normalization
|
|
18634
|
+
# v_alias = 2 * f * dx ; zero out energy below that
|
|
18635
|
+
dx_alias = None
|
|
18636
|
+
if self.trace_position and self.currentIndex < len(self.trace_position):
|
|
18637
|
+
trace_pos = self.trace_position[self.currentIndex]
|
|
18638
|
+
if len(trace_pos) >= 2:
|
|
18639
|
+
dx_alias = np.mean(np.abs(np.diff(trace_pos)))
|
|
18640
|
+
if dx_alias and dx_alias > 0:
|
|
18641
|
+
v_alias = 2.0 * fs[:, None] * dx_alias # shape (nfreq,1)
|
|
18642
|
+
alias_mask = vs[None, :] >= v_alias
|
|
18643
|
+
FV = np.where(alias_mask, FV, 0.0)
|
|
18644
|
+
|
|
17908
18645
|
# Normalize based on user selection (Dispersion only uses per-frequency normalization)
|
|
17909
18646
|
eps = 1e-12
|
|
17910
18647
|
if getattr(self, 'dispersion_norm_per_freq', False):
|
|
17911
|
-
# Per-
|
|
17912
|
-
|
|
17913
|
-
FV = FV /
|
|
18648
|
+
# Per-frequency normalization for Phase-Shift (max of 1 at each frequency)
|
|
18649
|
+
max_per_f = np.maximum(np.max(FV, axis=1, keepdims=True), eps)
|
|
18650
|
+
FV = FV / max_per_f
|
|
18651
|
+
|
|
18652
|
+
try:
|
|
17914
18653
|
|
|
17915
18654
|
# Apply frequency and velocity limits (clamp to Nyquist for safety)
|
|
17916
18655
|
fmin = getattr(self, 'fmin', None)
|
|
@@ -17944,9 +18683,57 @@ class MainWindow(QMainWindow):
|
|
|
17944
18683
|
vs = vs[vmask]
|
|
17945
18684
|
FV = FV[:, vmask]
|
|
17946
18685
|
|
|
18686
|
+
# Apply display enhancement transformations
|
|
18687
|
+
FV_display = FV.copy()
|
|
18688
|
+
|
|
18689
|
+
# Apply enhancement transformation if enabled
|
|
18690
|
+
# IMPORTANT: Work on raw unnormalized data for proper dynamic range
|
|
18691
|
+
if getattr(self, 'dispersion_enhance', False):
|
|
18692
|
+
# Use raw data for enhancement to avoid issues with normalized [0,1] range
|
|
18693
|
+
FV_for_enhance = FV_raw.copy()
|
|
18694
|
+
|
|
18695
|
+
# Apply enhancement formula: 1/(max-x) to amplify weak modes
|
|
18696
|
+
max_val = np.max(FV_for_enhance)
|
|
18697
|
+
epsilon = max_val * 0.01 # 1% of max to prevent division by zero
|
|
18698
|
+
FV_display = 1.0 / (max_val - FV_for_enhance + epsilon)
|
|
18699
|
+
|
|
18700
|
+
# Normalize to [0, 1] range
|
|
18701
|
+
vmin, vmax = np.min(FV_display), np.max(FV_display)
|
|
18702
|
+
if vmax > vmin:
|
|
18703
|
+
FV_display = (FV_display - vmin) / (vmax - vmin)
|
|
18704
|
+
else:
|
|
18705
|
+
FV_display = np.zeros_like(FV_display)
|
|
18706
|
+
|
|
17947
18707
|
# Create image item with colormap (rotated: freq on x-axis, velocity on y-axis)
|
|
17948
18708
|
colormap = getattr(self, 'dispersion_colormap_str', 'plasma')
|
|
17949
|
-
image_item = createImageItem(
|
|
18709
|
+
image_item = createImageItem(FV_display.T, fs, vs, colormap=colormap)
|
|
18710
|
+
|
|
18711
|
+
# Apply MATLAB-like brighten to the colormap instead of data (beta in [-1,1])
|
|
18712
|
+
saturation_factor = getattr(self, 'dispersion_saturation', 1.0)
|
|
18713
|
+
beta = (saturation_factor - 1.0) / 0.5 # 1.0->0, 0.5->-1 (darken), 1.5->+1 (brighten)
|
|
18714
|
+
beta = max(-1.0, min(1.0, beta))
|
|
18715
|
+
if abs(beta) > 1e-6:
|
|
18716
|
+
try:
|
|
18717
|
+
cmap_obj = image_item.getColorMap()
|
|
18718
|
+
if cmap_obj is None:
|
|
18719
|
+
cmap_obj = pqg.colormap.get(colormap, source='matplotlib')
|
|
18720
|
+
if cmap_obj is not None:
|
|
18721
|
+
lut = cmap_obj.getLookupTable(nPts=256, alpha=True)
|
|
18722
|
+
lut = lut.astype(float)
|
|
18723
|
+
if lut.max() > 1.0:
|
|
18724
|
+
lut = lut / 255.0
|
|
18725
|
+
tol = np.sqrt(np.finfo(float).eps)
|
|
18726
|
+
if beta > 0:
|
|
18727
|
+
gamma = 1.0 - min(1.0 - tol, beta)
|
|
18728
|
+
else:
|
|
18729
|
+
gamma = 1.0 / (1.0 + max(-1.0 + tol, beta))
|
|
18730
|
+
lut[:, :3] = np.clip(np.power(lut[:, :3], gamma), 0.0, 1.0)
|
|
18731
|
+
lut_out = (lut * 255).astype(np.ubyte)
|
|
18732
|
+
positions = np.linspace(0.0, 1.0, lut_out.shape[0])
|
|
18733
|
+
bright_cmap = pqg.ColorMap(positions, lut_out)
|
|
18734
|
+
image_item.setColorMap(bright_cmap)
|
|
18735
|
+
except Exception:
|
|
18736
|
+
pass
|
|
17950
18737
|
|
|
17951
18738
|
# Store image data and coordinates for amplitude lookup
|
|
17952
18739
|
self.dispersionImageData = FV.T
|
|
@@ -18009,11 +18796,18 @@ class MainWindow(QMainWindow):
|
|
|
18009
18796
|
pass
|
|
18010
18797
|
plot_widget.scene().sigMouseClicked.connect(lambda ev: self.onDispersionClick(ev, view_key))
|
|
18011
18798
|
|
|
18799
|
+
# Add aliasing limit curve if enabled
|
|
18800
|
+
if getattr(self, 'dispersion_show_aliasing_limit', False):
|
|
18801
|
+
self._plotAliasingLimit(plot_widget, fs, vs) # Spatial aliasing (below surface-wave energy)
|
|
18802
|
+
|
|
18012
18803
|
# Update error bars if show errors is enabled
|
|
18013
18804
|
self._updateDispersionErrorBars(view_key)
|
|
18014
18805
|
|
|
18015
18806
|
# Update interpolated error lines if show interpolated errors is enabled
|
|
18016
18807
|
self._updateDispersionInterpErrors(view_key)
|
|
18808
|
+
|
|
18809
|
+
# Update status bar with dispersion window info and stacking info
|
|
18810
|
+
self.updateDispersionWindowInfo()
|
|
18017
18811
|
|
|
18018
18812
|
except ValueError as e:
|
|
18019
18813
|
QMessageBox.warning(self, "Phase-Shift Error",
|
|
@@ -18022,6 +18816,44 @@ class MainWindow(QMainWindow):
|
|
|
18022
18816
|
QMessageBox.warning(self, "Computation Error",
|
|
18023
18817
|
f"An error occurred:\n{str(e)}")
|
|
18024
18818
|
|
|
18819
|
+
def _plotAliasingLimit(self, plot_widget, frequencies, velocities):
|
|
18820
|
+
"""Plot the spatial aliasing limits on the dispersion image.
|
|
18821
|
+
|
|
18822
|
+
Energy below surface-wave caused by spatial undersampling.
|
|
18823
|
+
|
|
18824
|
+
Spatial aliasing occurs when wavelength < 2*dx
|
|
18825
|
+
Phase velocity v = f*lambda, so aliasing when v < f*2*dx
|
|
18826
|
+
First-order aliasing boundary: v_alias = 2 * f * dx
|
|
18827
|
+
"""
|
|
18828
|
+
try:
|
|
18829
|
+
# Get trace spacing for the current shot
|
|
18830
|
+
if not (self.trace_position and self.currentIndex < len(self.trace_position)):
|
|
18831
|
+
return
|
|
18832
|
+
|
|
18833
|
+
trace_pos = self.trace_position[self.currentIndex]
|
|
18834
|
+
if len(trace_pos) < 2:
|
|
18835
|
+
return
|
|
18836
|
+
|
|
18837
|
+
# Calculate mean trace spacing
|
|
18838
|
+
dx = np.mean(np.abs(np.diff(trace_pos)))
|
|
18839
|
+
if dx <= 0:
|
|
18840
|
+
return
|
|
18841
|
+
|
|
18842
|
+
f_array = np.array(frequencies)
|
|
18843
|
+
v_max = np.max(velocities)
|
|
18844
|
+
v_min = np.min(velocities)
|
|
18845
|
+
|
|
18846
|
+
# Plot first-order aliasing band only (wavelength < 2*dx)
|
|
18847
|
+
v_alias_boundary = 2.0 * f_array * dx
|
|
18848
|
+
v_alias_boundary = np.clip(v_alias_boundary, v_min, v_max)
|
|
18849
|
+
pen = pqg.mkPen(color=(255, 0, 0, 200), width=2, style=Qt.DashLine)
|
|
18850
|
+
curve = pqg.PlotCurveItem(f_array, v_alias_boundary, pen=pen)
|
|
18851
|
+
plot_widget.addItem(curve)
|
|
18852
|
+
|
|
18853
|
+
except Exception as e:
|
|
18854
|
+
# Silently skip if aliasing limit cannot be computed
|
|
18855
|
+
pass
|
|
18856
|
+
|
|
18025
18857
|
def plotPseudoSection(self):
|
|
18026
18858
|
"""Plot pseudo-section: phase velocity as 2D image (xmid vs frequency/wavelength)"""
|
|
18027
18859
|
plot_widget, viewbox = self._getBottomPlotWidgets()
|
|
@@ -18033,24 +18865,82 @@ class MainWindow(QMainWindow):
|
|
|
18033
18865
|
self._safelyclearPlot(plot_widget)
|
|
18034
18866
|
self.removeLegend()
|
|
18035
18867
|
self.removeColorBar()
|
|
18868
|
+
|
|
18869
|
+
# Update status bar with current FFID and source info so it stays fresh when pseudo-section is active
|
|
18870
|
+
if self.currentIndex is not None:
|
|
18871
|
+
try:
|
|
18872
|
+
ffid_val = self.ffid[self.currentIndex] if hasattr(self, 'ffid') and self.ffid else 'N/A'
|
|
18873
|
+
src_val = self.source_position[self.currentIndex] if self.source_position else 'N/A'
|
|
18874
|
+
self.ffidLabel.setText(f'FFID: {ffid_val} | Source at {src_val} m')
|
|
18875
|
+
except Exception:
|
|
18876
|
+
pass
|
|
18036
18877
|
|
|
18037
18878
|
# Collect all shots with dispersion picks
|
|
18038
|
-
|
|
18879
|
+
xcoord_list = [] # list of (shot_idx, x_coord)
|
|
18039
18880
|
shot_freqs = [] # list of frequency arrays per shot
|
|
18040
18881
|
shot_vels = [] # list of velocity arrays per shot
|
|
18041
18882
|
|
|
18042
18883
|
use_wavelength = getattr(self, 'pseudosection_use_wavelength', False)
|
|
18043
18884
|
show_interp = getattr(self, 'pseudosection_show_interpolated', False)
|
|
18044
18885
|
selected_mode = getattr(self, 'pseudosection_mode', 0)
|
|
18886
|
+
use_source_x = getattr(self, 'pseudosection_use_source_x', False)
|
|
18045
18887
|
|
|
18046
18888
|
for shot_idx, shot_curves in enumerate(self.dispersion_curves):
|
|
18047
18889
|
if shot_curves is None or 'modes' not in shot_curves:
|
|
18048
18890
|
continue
|
|
18049
18891
|
|
|
18050
|
-
#
|
|
18051
|
-
if
|
|
18052
|
-
|
|
18053
|
-
|
|
18892
|
+
# Determine X coordinate for this shot
|
|
18893
|
+
if use_source_x:
|
|
18894
|
+
x_coord = None
|
|
18895
|
+
# Check plotTypeY to determine whether to use FFID or source position
|
|
18896
|
+
plot_type_y = getattr(self, 'plotTypeY', 'source_position')
|
|
18897
|
+
if plot_type_y == 'ffid' and hasattr(self, 'ffid') and shot_idx < len(self.ffid):
|
|
18898
|
+
# Use FFID
|
|
18899
|
+
try:
|
|
18900
|
+
x_coord = float(self.ffid[shot_idx])
|
|
18901
|
+
except Exception:
|
|
18902
|
+
x_coord = None
|
|
18903
|
+
elif shot_idx < len(self.source_position):
|
|
18904
|
+
# Use source position
|
|
18905
|
+
x_coord = self.source_position[shot_idx]
|
|
18906
|
+
elif hasattr(self, 'ffid') and shot_idx < len(self.ffid):
|
|
18907
|
+
# Fallback to FFID if source position not available
|
|
18908
|
+
try:
|
|
18909
|
+
x_coord = float(self.ffid[shot_idx])
|
|
18910
|
+
except Exception:
|
|
18911
|
+
x_coord = None
|
|
18912
|
+
if x_coord is None and shot_idx in getattr(self, 'xmid_values', {}):
|
|
18913
|
+
x_coord = self.xmid_values[shot_idx]
|
|
18914
|
+
else:
|
|
18915
|
+
if shot_idx not in self.xmid_values:
|
|
18916
|
+
continue
|
|
18917
|
+
xmid_pos = self.xmid_values[shot_idx] # Window center position in meters
|
|
18918
|
+
|
|
18919
|
+
# Determine if we're using trace numbers or positions
|
|
18920
|
+
plot_type_x = getattr(self, 'plotTypeX', 'trace_position')
|
|
18921
|
+
if plot_type_x == 'trace_position':
|
|
18922
|
+
# Use window center position directly
|
|
18923
|
+
x_coord = xmid_pos
|
|
18924
|
+
else:
|
|
18925
|
+
# Convert window center position to theoretical trace number
|
|
18926
|
+
# Interpolate from trace positions to trace numbers for this shot
|
|
18927
|
+
if shot_idx < len(self.plotTypeDict.get(plot_type_x, [])) and shot_idx < len(self.trace_position):
|
|
18928
|
+
trace_nums = self.plotTypeDict[plot_type_x][shot_idx]
|
|
18929
|
+
trace_positions = self.trace_position[shot_idx]
|
|
18930
|
+
if len(trace_nums) > 0 and len(trace_positions) > 0:
|
|
18931
|
+
# Interpolate to find trace number at window center position
|
|
18932
|
+
from scipy.interpolate import interp1d
|
|
18933
|
+
try:
|
|
18934
|
+
f = interp1d(trace_positions, trace_nums, kind='linear',
|
|
18935
|
+
bounds_error=False, fill_value='extrapolate')
|
|
18936
|
+
x_coord = float(f(xmid_pos))
|
|
18937
|
+
except Exception:
|
|
18938
|
+
# Fallback to position if interpolation fails
|
|
18939
|
+
x_coord = xmid_pos
|
|
18940
|
+
else:
|
|
18941
|
+
x_coord = xmid_pos
|
|
18942
|
+
else:
|
|
18943
|
+
x_coord = xmid_pos
|
|
18054
18944
|
|
|
18055
18945
|
# Collect selected mode picks
|
|
18056
18946
|
if selected_mode not in shot_curves['modes']:
|
|
@@ -18071,22 +18961,33 @@ class MainWindow(QMainWindow):
|
|
|
18071
18961
|
freqs = np.array([p[0] for p in points])
|
|
18072
18962
|
vels = np.array([p[1] for p in points])
|
|
18073
18963
|
|
|
18074
|
-
if freqs is not None and len(freqs) > 0:
|
|
18075
|
-
|
|
18964
|
+
if freqs is not None and len(freqs) > 0 and x_coord is not None:
|
|
18965
|
+
xcoord_list.append((shot_idx, x_coord))
|
|
18076
18966
|
shot_freqs.append(freqs)
|
|
18077
18967
|
shot_vels.append(vels)
|
|
18078
18968
|
|
|
18079
|
-
if not
|
|
18969
|
+
if not xcoord_list:
|
|
18970
|
+
# Set labels even when no data - use same logic as main plotting
|
|
18971
|
+
if use_source_x:
|
|
18972
|
+
x_label = getattr(self, 'y_label', 'Source Position (m)')
|
|
18973
|
+
else:
|
|
18974
|
+
plot_type_x = getattr(self, 'plotTypeX', 'trace_position')
|
|
18975
|
+
if plot_type_x == 'trace_position':
|
|
18976
|
+
x_label = 'Window center (m)'
|
|
18977
|
+
else:
|
|
18978
|
+
# Trace number modes
|
|
18979
|
+
x_label = 'Window center (trace no)'
|
|
18080
18980
|
plot_widget.setLabel('left', 'Frequency (Hz)' if not use_wavelength else 'Wavelength (m)')
|
|
18081
|
-
plot_widget.setLabel('
|
|
18981
|
+
plot_widget.setLabel('top', x_label)
|
|
18982
|
+
plot_widget.setLabel('bottom', '')
|
|
18082
18983
|
return
|
|
18083
18984
|
|
|
18084
|
-
# Sort by
|
|
18085
|
-
sorted_idx = np.argsort([x[1] for x in
|
|
18086
|
-
|
|
18985
|
+
# Sort by X coordinate
|
|
18986
|
+
sorted_idx = np.argsort([x[1] for x in xcoord_list])
|
|
18987
|
+
xcoord_list = [xcoord_list[i] for i in sorted_idx]
|
|
18087
18988
|
shot_freqs = [shot_freqs[i] for i in sorted_idx]
|
|
18088
18989
|
shot_vels = [shot_vels[i] for i in sorted_idx]
|
|
18089
|
-
|
|
18990
|
+
xcoords = np.array([x[1] for x in xcoord_list])
|
|
18090
18991
|
|
|
18091
18992
|
# Create 2D grid: freq/λ vs xmid
|
|
18092
18993
|
# For simplicity, create a scatter plot with colormap encoding velocity
|
|
@@ -18095,9 +18996,9 @@ class MainWindow(QMainWindow):
|
|
|
18095
18996
|
plot_data_vel = []
|
|
18096
18997
|
|
|
18097
18998
|
for i, (freqs, vels) in enumerate(zip(shot_freqs, shot_vels)):
|
|
18098
|
-
|
|
18999
|
+
x_val = xcoords[i]
|
|
18099
19000
|
for freq, vel in zip(freqs, vels):
|
|
18100
|
-
plot_data_x.append(
|
|
19001
|
+
plot_data_x.append(x_val)
|
|
18101
19002
|
if use_wavelength:
|
|
18102
19003
|
# λ = v / f
|
|
18103
19004
|
wavelength = vel / freq if freq > 0 else 0
|
|
@@ -18133,33 +19034,99 @@ class MainWindow(QMainWindow):
|
|
|
18133
19034
|
size=6, pen=None, brush=brush_list)
|
|
18134
19035
|
plot_widget.addItem(scatter)
|
|
18135
19036
|
|
|
18136
|
-
# Add vertical red lines around the current shot's
|
|
18137
|
-
if self.currentIndex is not None
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
|
|
18157
|
-
|
|
19037
|
+
# Add vertical red lines around the current shot's x position
|
|
19038
|
+
if self.currentIndex is not None:
|
|
19039
|
+
current_x = None
|
|
19040
|
+
if use_source_x:
|
|
19041
|
+
# Match the x-axis choice: use FFID when plotTypeY is ffid, else source position
|
|
19042
|
+
plot_type_y = getattr(self, 'plotTypeY', 'source_position')
|
|
19043
|
+
if plot_type_y == 'ffid' and hasattr(self, 'ffid') and self.currentIndex < len(self.ffid):
|
|
19044
|
+
try:
|
|
19045
|
+
current_x = float(self.ffid[self.currentIndex])
|
|
19046
|
+
except Exception:
|
|
19047
|
+
current_x = None
|
|
19048
|
+
elif self.currentIndex < len(self.source_position):
|
|
19049
|
+
current_x = self.source_position[self.currentIndex]
|
|
19050
|
+
elif hasattr(self, 'ffid') and self.currentIndex < len(self.ffid):
|
|
19051
|
+
try:
|
|
19052
|
+
current_x = float(self.ffid[self.currentIndex])
|
|
19053
|
+
except Exception:
|
|
19054
|
+
current_x = None
|
|
19055
|
+
if current_x is None and self.currentIndex in getattr(self, 'xmid_values', {}):
|
|
19056
|
+
current_x = self.xmid_values[self.currentIndex]
|
|
19057
|
+
elif self.currentIndex in self.xmid_values:
|
|
19058
|
+
# Use window center - convert to trace number if needed
|
|
19059
|
+
xmid_pos = self.xmid_values[self.currentIndex]
|
|
19060
|
+
plot_type_x = getattr(self, 'plotTypeX', 'trace_position')
|
|
19061
|
+
if plot_type_x == 'trace_position':
|
|
19062
|
+
current_x = xmid_pos
|
|
19063
|
+
else:
|
|
19064
|
+
# Convert window center position to theoretical trace number
|
|
19065
|
+
if self.currentIndex < len(self.plotTypeDict.get(plot_type_x, [])) and self.currentIndex < len(self.trace_position):
|
|
19066
|
+
trace_nums = self.plotTypeDict[plot_type_x][self.currentIndex]
|
|
19067
|
+
trace_positions = self.trace_position[self.currentIndex]
|
|
19068
|
+
if len(trace_nums) > 0 and len(trace_positions) > 0:
|
|
19069
|
+
from scipy.interpolate import interp1d
|
|
19070
|
+
try:
|
|
19071
|
+
f = interp1d(trace_positions, trace_nums, kind='linear',
|
|
19072
|
+
bounds_error=False, fill_value='extrapolate')
|
|
19073
|
+
current_x = float(f(xmid_pos))
|
|
19074
|
+
except Exception:
|
|
19075
|
+
current_x = xmid_pos
|
|
19076
|
+
else:
|
|
19077
|
+
current_x = xmid_pos
|
|
19078
|
+
else:
|
|
19079
|
+
current_x = xmid_pos
|
|
19080
|
+
|
|
19081
|
+
if current_x is not None:
|
|
19082
|
+
# Calculate spacing between guide lines based on axis mode
|
|
19083
|
+
if use_source_x:
|
|
19084
|
+
plot_type_y = getattr(self, 'plotTypeY', 'source_position')
|
|
19085
|
+
# Use 1 for FFID mode, otherwise calculate from actual source spacing
|
|
19086
|
+
if plot_type_y == 'ffid':
|
|
19087
|
+
mean_dx = 1.0
|
|
19088
|
+
elif plot_type_y == 'source_position' and len(self.source_position) > 1:
|
|
19089
|
+
# Use actual spacing between all source positions from the data
|
|
19090
|
+
mean_dx = np.mean(np.abs(np.diff(self.source_position)))
|
|
19091
|
+
else:
|
|
19092
|
+
mean_dx = getattr(self, 'mean_dg', 1.0)
|
|
19093
|
+
else:
|
|
19094
|
+
plot_type_x = getattr(self, 'plotTypeX', 'trace_position')
|
|
19095
|
+
# Use mean_dg for trace positions, 1 for trace number/ffid modes
|
|
19096
|
+
# Don't refine - window centers (xmid) can overlap between shots
|
|
19097
|
+
mean_dx = getattr(self, 'mean_dg', 1.0) if plot_type_x == 'trace_position' else 1.0
|
|
19098
|
+
|
|
19099
|
+
# Y-axis range for the vertical lines
|
|
19100
|
+
y_min_line, y_max_line = plot_data_y.min(), plot_data_y.max()
|
|
19101
|
+
|
|
19102
|
+
# Draw two vertical lines at x ± mean_dx/2
|
|
19103
|
+
x_line_1 = current_x - mean_dx/2
|
|
19104
|
+
x_line_2 = current_x + mean_dx/2
|
|
19105
|
+
y_line = [y_min_line, y_max_line]
|
|
19106
|
+
|
|
19107
|
+
line1 = pqg.PlotDataItem([x_line_1, x_line_1], y_line, pen=pqg.mkPen('r', width=2))
|
|
19108
|
+
line2 = pqg.PlotDataItem([x_line_2, x_line_2], y_line, pen=pqg.mkPen('r', width=2))
|
|
19109
|
+
plot_widget.addItem(line1)
|
|
19110
|
+
plot_widget.addItem(line2)
|
|
18158
19111
|
|
|
18159
19112
|
# Set axis labels
|
|
18160
19113
|
y_label = 'Wavelength (m)' if use_wavelength else 'Frequency (Hz)'
|
|
19114
|
+
# Determine X-axis label based on checkbox and current plot type
|
|
19115
|
+
if use_source_x:
|
|
19116
|
+
# Use the same label as layout view's Y-axis (depends on plotTypeY)
|
|
19117
|
+
x_label = getattr(self, 'y_label', 'Source Position (m)')
|
|
19118
|
+
else:
|
|
19119
|
+
# When not using source position, we're using window centers (xmid)
|
|
19120
|
+
plot_type_x = getattr(self, 'plotTypeX', 'trace_position')
|
|
19121
|
+
if plot_type_x == 'trace_position':
|
|
19122
|
+
# Window center as position in meters
|
|
19123
|
+
x_label = 'Window center (m)'
|
|
19124
|
+
else:
|
|
19125
|
+
# Window center as theoretical trace number
|
|
19126
|
+
x_label = 'Window center (trace no)'
|
|
18161
19127
|
plot_widget.setLabel('left', y_label)
|
|
18162
|
-
plot_widget.setLabel('
|
|
19128
|
+
plot_widget.setLabel('top', x_label)
|
|
19129
|
+
plot_widget.setLabel('bottom', '')
|
|
18163
19130
|
|
|
18164
19131
|
# Create colormap and colorbar (mirror layout view behavior)
|
|
18165
19132
|
try:
|
|
@@ -18177,43 +19144,72 @@ class MainWindow(QMainWindow):
|
|
|
18177
19144
|
plot_widget.plotItem.layout.addItem(self.colorbar_title_label, 2, 6)
|
|
18178
19145
|
|
|
18179
19146
|
plot_widget.showAxis('left')
|
|
18180
|
-
plot_widget.showAxis('bottom')
|
|
18181
19147
|
plot_widget.showAxis('top')
|
|
18182
19148
|
plot_widget.showAxis('right')
|
|
19149
|
+
plot_widget.showAxis('bottom')
|
|
18183
19150
|
plot_widget.getAxis('right').setStyle(showValues=False)
|
|
19151
|
+
plot_widget.getAxis('bottom').setLabel('')
|
|
19152
|
+
plot_widget.getAxis('top').setStyle(showValues=True)
|
|
18184
19153
|
|
|
18185
19154
|
# Apply Y-axis invert setting
|
|
18186
19155
|
invert_y = getattr(self, 'pseudosection_invert_y', True)
|
|
18187
19156
|
viewbox.invertY(invert_y)
|
|
18188
19157
|
|
|
18189
|
-
# Set limits
|
|
19158
|
+
# Set limits
|
|
18190
19159
|
if len(plot_data_x) > 0:
|
|
18191
|
-
# Get layout view x-axis range (all trace positions across all shots)
|
|
18192
|
-
self.updatePlotTypeDict()
|
|
18193
|
-
plot_data_x_layout = self.plotTypeDict.get(self.plotTypeX, [])
|
|
18194
|
-
flat_plot_data_x = [item for sublist in plot_data_x_layout for item in sublist]
|
|
18195
|
-
traces = [trace for trace in flat_plot_data_x if trace is not None]
|
|
18196
|
-
|
|
18197
19160
|
# Y-axis uses data from pseudo-section
|
|
18198
19161
|
y_min, y_max = plot_data_y.min(), plot_data_y.max()
|
|
18199
19162
|
y_margin = (y_max - y_min) * 0.05 if y_max > y_min else 1.0
|
|
18200
19163
|
|
|
18201
|
-
# X-axis
|
|
18202
|
-
if
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18209
|
-
|
|
18210
|
-
|
|
18211
|
-
|
|
19164
|
+
# X-axis range depends on checkbox state
|
|
19165
|
+
if use_source_x:
|
|
19166
|
+
# Use full range of source position / FFID coordinates (all shots, not just those with picks)
|
|
19167
|
+
plot_type_y = getattr(self, 'plotTypeY', 'source_position')
|
|
19168
|
+
if plot_type_y == 'ffid' and hasattr(self, 'ffid') and self.ffid:
|
|
19169
|
+
# Use FFID range
|
|
19170
|
+
try:
|
|
19171
|
+
ffid_values = [float(f) for f in self.ffid if f is not None]
|
|
19172
|
+
if ffid_values:
|
|
19173
|
+
x_min, x_max = min(ffid_values), max(ffid_values)
|
|
19174
|
+
else:
|
|
19175
|
+
x_min, x_max = xcoords.min(), xcoords.max()
|
|
19176
|
+
except (ValueError, TypeError):
|
|
19177
|
+
x_min, x_max = xcoords.min(), xcoords.max()
|
|
19178
|
+
elif self.source_position:
|
|
19179
|
+
# Use source position range
|
|
19180
|
+
x_min, x_max = min(self.source_position), max(self.source_position)
|
|
19181
|
+
else:
|
|
19182
|
+
# Fallback to picked shots only
|
|
19183
|
+
x_min, x_max = xcoords.min(), xcoords.max()
|
|
19184
|
+
|
|
18212
19185
|
x_margin = (x_max - x_min) * 0.05 if x_max > x_min else 1.0
|
|
18213
19186
|
viewbox.setLimits(xMin=x_min-x_margin, xMax=x_max+x_margin,
|
|
18214
19187
|
yMin=y_min-y_margin, yMax=y_max+y_margin)
|
|
18215
19188
|
viewbox.setXRange(x_min-x_margin, x_max+x_margin, padding=0)
|
|
18216
19189
|
viewbox.setYRange(y_min-y_margin, y_max+y_margin, padding=0)
|
|
19190
|
+
else:
|
|
19191
|
+
# X-axis matches layout view range (window centers based on plotTypeX)
|
|
19192
|
+
self.updatePlotTypeDict()
|
|
19193
|
+
plot_data_x_layout = self.plotTypeDict.get(self.plotTypeX, [])
|
|
19194
|
+
flat_plot_data_x = [item for sublist in plot_data_x_layout for item in sublist]
|
|
19195
|
+
traces = [trace for trace in flat_plot_data_x if trace is not None]
|
|
19196
|
+
|
|
19197
|
+
if traces:
|
|
19198
|
+
x_min_layout = min(traces) - self.mean_dg
|
|
19199
|
+
x_max_layout = max(traces) + self.mean_dg
|
|
19200
|
+
viewbox.setLimits(xMin=x_min_layout, xMax=x_max_layout,
|
|
19201
|
+
yMin=y_min-y_margin, yMax=y_max+y_margin)
|
|
19202
|
+
viewbox.setXRange(x_min_layout, x_max_layout, padding=0)
|
|
19203
|
+
viewbox.setYRange(y_min-y_margin, y_max+y_margin, padding=0)
|
|
19204
|
+
else:
|
|
19205
|
+
# Fallback to data-based range if layout info unavailable
|
|
19206
|
+
x_min, x_max = plot_data_x.min(), plot_data_x.max()
|
|
19207
|
+
x_margin = (x_max - x_min) * 0.05 if x_max > x_min else 1.0
|
|
19208
|
+
viewbox.setLimits(xMin=x_min-x_margin, xMax=x_max+x_margin,
|
|
19209
|
+
yMin=y_min-y_margin, yMax=y_max+y_margin)
|
|
19210
|
+
viewbox.setXRange(x_min-x_margin, x_max+x_margin, padding=0)
|
|
19211
|
+
viewbox.setYRange(y_min-y_margin, y_max+y_margin, padding=0)
|
|
19212
|
+
|
|
18217
19213
|
|
|
18218
19214
|
def onPseudoSectionFreqLambdaToggled(self, state):
|
|
18219
19215
|
"""Toggle between frequency and wavelength display"""
|
|
@@ -18235,6 +19231,51 @@ class MainWindow(QMainWindow):
|
|
|
18235
19231
|
def onPseudoSectionInterpToggled(self, state):
|
|
18236
19232
|
"""Toggle between picked and interpolated curve display"""
|
|
18237
19233
|
self.pseudosection_show_interpolated = bool(state)
|
|
19234
|
+
|
|
19235
|
+
# Synchronize with dispersion view interpolate checkbox
|
|
19236
|
+
if hasattr(self, 'dispersionLiveInterpolateCheckbox'):
|
|
19237
|
+
self.dispersionLiveInterpolateCheckbox.blockSignals(True)
|
|
19238
|
+
self.dispersionLiveInterpolateCheckbox.setChecked(bool(state))
|
|
19239
|
+
self.dispersionLiveInterpolateCheckbox.blockSignals(False)
|
|
19240
|
+
|
|
19241
|
+
# If enabling interpolation, interpolate all shots for pseudo-section
|
|
19242
|
+
if bool(state):
|
|
19243
|
+
self._interpolateAllShotsForPseudoSection()
|
|
19244
|
+
|
|
19245
|
+
# Also trigger dispersion interpolation for active view
|
|
19246
|
+
view = self._get_active_dispersion_view()
|
|
19247
|
+
if view:
|
|
19248
|
+
if self.dispersionShowAllModesCheckbox.isChecked():
|
|
19249
|
+
self.interpolateAllDispersionCurves(view)
|
|
19250
|
+
else:
|
|
19251
|
+
mode_data = self._get_current_mode_data(view)
|
|
19252
|
+
if len(mode_data['picked_points']) >= 2:
|
|
19253
|
+
self.interpolateDispersionCurve(view)
|
|
19254
|
+
else:
|
|
19255
|
+
# Remove interpolated curves from dispersion view when disabling
|
|
19256
|
+
view = self._get_active_dispersion_view()
|
|
19257
|
+
if view:
|
|
19258
|
+
plot_widget = self.plotWidget if view == 'top' else self.bottomPlotWidget
|
|
19259
|
+
state_dict = self.dispersion_picking_state[view]
|
|
19260
|
+
|
|
19261
|
+
for mode_num, mode_data in state_dict['modes'].items():
|
|
19262
|
+
if mode_data['curve_line'] is not None:
|
|
19263
|
+
try:
|
|
19264
|
+
plot_widget.removeItem(mode_data['curve_line'])
|
|
19265
|
+
except:
|
|
19266
|
+
pass
|
|
19267
|
+
mode_data['curve_line'] = None
|
|
19268
|
+
if mode_data['interp_error_fill'] is not None:
|
|
19269
|
+
try:
|
|
19270
|
+
items = mode_data['interp_error_fill'] if isinstance(mode_data['interp_error_fill'], list) else [mode_data['interp_error_fill']]
|
|
19271
|
+
for item in items:
|
|
19272
|
+
if item is not None:
|
|
19273
|
+
plot_widget.removeItem(item)
|
|
19274
|
+
except:
|
|
19275
|
+
pass
|
|
19276
|
+
mode_data['interp_error_fill'] = None
|
|
19277
|
+
mode_data['curve_data'] = None
|
|
19278
|
+
|
|
18238
19279
|
# Replot whichever view is showing pseudo-section
|
|
18239
19280
|
if getattr(self, 'bottomPlotType', None) == "pseudosection":
|
|
18240
19281
|
self._plot_target_widget = self.bottomPlotWidget
|
|
@@ -18283,6 +19324,89 @@ class MainWindow(QMainWindow):
|
|
|
18283
19324
|
self._plot_target_widget = None
|
|
18284
19325
|
self._plot_target_viewbox = None
|
|
18285
19326
|
|
|
19327
|
+
def onPseudoSectionUseSourceXToggled(self, state):
|
|
19328
|
+
"""Toggle use of source position/FFID for pseudo-section X-axis"""
|
|
19329
|
+
self.pseudosection_use_source_x = bool(state)
|
|
19330
|
+
if getattr(self, 'bottomPlotType', None) == "pseudosection":
|
|
19331
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
19332
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
19333
|
+
self.plotPseudoSection()
|
|
19334
|
+
self._plot_target_widget = None
|
|
19335
|
+
self._plot_target_viewbox = None
|
|
19336
|
+
if getattr(self, 'topPlotType', None) == "pseudosection":
|
|
19337
|
+
self._plot_target_widget = self.plotWidget
|
|
19338
|
+
self._plot_target_viewbox = self.viewBox
|
|
19339
|
+
self.plotPseudoSection()
|
|
19340
|
+
self._plot_target_widget = None
|
|
19341
|
+
self._plot_target_viewbox = None
|
|
19342
|
+
|
|
19343
|
+
def _interpolateAllShotsForPseudoSection(self):
|
|
19344
|
+
"""Interpolate dispersion curves for all shots that have picks.
|
|
19345
|
+
This is used when toggling interpolation in pseudo-section view."""
|
|
19346
|
+
if not self.dispersion_curves:
|
|
19347
|
+
return
|
|
19348
|
+
|
|
19349
|
+
# Get interpolation method from combo box
|
|
19350
|
+
method_text = self.dispersionInterpMethodCombo.currentText()
|
|
19351
|
+
min_points_required = {
|
|
19352
|
+
"Linear": 2,
|
|
19353
|
+
"Quadratic": 3,
|
|
19354
|
+
"Cubic": 4,
|
|
19355
|
+
"Spline": 4
|
|
19356
|
+
}
|
|
19357
|
+
min_points = min_points_required.get(method_text, 2)
|
|
19358
|
+
|
|
19359
|
+
# Process each shot
|
|
19360
|
+
for shot_idx, shot_curves in enumerate(self.dispersion_curves):
|
|
19361
|
+
if shot_curves is None or 'modes' not in shot_curves:
|
|
19362
|
+
continue
|
|
19363
|
+
|
|
19364
|
+
# Process each mode in this shot
|
|
19365
|
+
for mode_num, mode_data in shot_curves['modes'].items():
|
|
19366
|
+
if 'picked_points' not in mode_data or len(mode_data['picked_points']) < min_points:
|
|
19367
|
+
continue
|
|
19368
|
+
|
|
19369
|
+
try:
|
|
19370
|
+
# Sort points by frequency
|
|
19371
|
+
sorted_points = sorted(mode_data['picked_points'])
|
|
19372
|
+
frequencies = np.array([p[0] for p in sorted_points])
|
|
19373
|
+
velocities = np.array([p[1] for p in sorted_points])
|
|
19374
|
+
|
|
19375
|
+
# Create interpolated curve
|
|
19376
|
+
freq_min, freq_max = frequencies[0], frequencies[-1]
|
|
19377
|
+
# Use a reasonable frequency step (10 Hz or 1/100th of range)
|
|
19378
|
+
freq_range = freq_max - freq_min
|
|
19379
|
+
freq_step = min(10.0, freq_range / 100.0) if freq_range > 0 else 1.0
|
|
19380
|
+
interp_frequencies = np.arange(freq_min, freq_max + freq_step/2, freq_step/2)
|
|
19381
|
+
|
|
19382
|
+
method_map = {
|
|
19383
|
+
"Cubic": "cubic",
|
|
19384
|
+
"Linear": "linear",
|
|
19385
|
+
"Quadratic": "quadratic",
|
|
19386
|
+
"Spline": "cubic"
|
|
19387
|
+
}
|
|
19388
|
+
kind = method_map.get(method_text, "cubic")
|
|
19389
|
+
|
|
19390
|
+
# Interpolate using selected method
|
|
19391
|
+
from scipy.interpolate import interp1d, UnivariateSpline
|
|
19392
|
+
|
|
19393
|
+
if method_text == "Spline":
|
|
19394
|
+
spline = UnivariateSpline(frequencies, velocities, s=len(frequencies)*0.1, k=3)
|
|
19395
|
+
interp_velocities = spline(interp_frequencies)
|
|
19396
|
+
else:
|
|
19397
|
+
interp_func = interp1d(frequencies, velocities, kind=kind,
|
|
19398
|
+
bounds_error=False, fill_value='extrapolate')
|
|
19399
|
+
interp_velocities = interp_func(interp_frequencies)
|
|
19400
|
+
|
|
19401
|
+
# Store interpolated curve in dispersion_curves
|
|
19402
|
+
mode_data['frequencies'] = interp_frequencies.tolist()
|
|
19403
|
+
mode_data['velocities'] = interp_velocities.tolist()
|
|
19404
|
+
mode_data['interpolated'] = True
|
|
19405
|
+
|
|
19406
|
+
except Exception:
|
|
19407
|
+
# Skip this mode if interpolation fails
|
|
19408
|
+
continue
|
|
19409
|
+
|
|
18286
19410
|
#######################################
|
|
18287
19411
|
# Topo functions
|
|
18288
19412
|
#######################################
|
|
@@ -18640,7 +19764,7 @@ class MainWindow(QMainWindow):
|
|
|
18640
19764
|
def handleFreehandPick(self, drag_path, view="top"):
|
|
18641
19765
|
"""
|
|
18642
19766
|
Called when a Ctrl+left mouse drag finishes (freehand pick).
|
|
18643
|
-
Adds picks along the drag path.
|
|
19767
|
+
Adds picks along the drag path - picks ALL crossed traces (like dispersion).
|
|
18644
19768
|
|
|
18645
19769
|
Args:
|
|
18646
19770
|
drag_path: List of points from the drag operation
|
|
@@ -18668,20 +19792,75 @@ class MainWindow(QMainWindow):
|
|
|
18668
19792
|
else:
|
|
18669
19793
|
viewBox = self.viewBox
|
|
18670
19794
|
|
|
18671
|
-
|
|
19795
|
+
# Convert drag path to view coordinates
|
|
19796
|
+
path_coords = []
|
|
18672
19797
|
for pt in drag_path:
|
|
18673
19798
|
mousePoint = viewBox.mapSceneToView(pt)
|
|
18674
|
-
x
|
|
18675
|
-
|
|
19799
|
+
path_coords.append((mousePoint.x(), mousePoint.y()))
|
|
19800
|
+
|
|
19801
|
+
if len(path_coords) < 2:
|
|
19802
|
+
return
|
|
19803
|
+
|
|
19804
|
+
# Get trace position range covered by the drag
|
|
19805
|
+
x_positions = [coord[0] for coord in path_coords]
|
|
19806
|
+
min_x = min(x_positions)
|
|
19807
|
+
max_x = max(x_positions)
|
|
19808
|
+
|
|
19809
|
+
# Get all trace positions for current file
|
|
19810
|
+
plot_data_x = self.plotTypeDict.get(self.plotTypeX, [])
|
|
19811
|
+
if not plot_data_x or self.currentIndex >= len(plot_data_x):
|
|
19812
|
+
return
|
|
19813
|
+
|
|
19814
|
+
trace_positions = np.array(plot_data_x[self.currentIndex])
|
|
19815
|
+
|
|
19816
|
+
# Find all traces that fall within the crossed range
|
|
19817
|
+
trace_mask = (trace_positions >= min_x) & (trace_positions <= max_x)
|
|
19818
|
+
crossed_trace_indices = np.where(trace_mask)[0]
|
|
19819
|
+
|
|
19820
|
+
if len(crossed_trace_indices) == 0:
|
|
19821
|
+
return
|
|
19822
|
+
|
|
19823
|
+
# Sort path once for interpolation
|
|
19824
|
+
sorted_path = sorted(path_coords, key=lambda c: c[0])
|
|
19825
|
+
|
|
19826
|
+
# Map trace_idx -> time (dedup per trace index)
|
|
19827
|
+
trace_to_time = {}
|
|
19828
|
+
for trace_idx in crossed_trace_indices:
|
|
19829
|
+
trace_x = trace_positions[trace_idx]
|
|
18676
19830
|
|
|
18677
|
-
#
|
|
18678
|
-
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
|
|
19831
|
+
# Find time at this trace by linear interpolation through drag path
|
|
19832
|
+
time_value = None
|
|
19833
|
+
for i in range(len(sorted_path) - 1):
|
|
19834
|
+
x1, t1 = sorted_path[i]
|
|
19835
|
+
x2, t2 = sorted_path[i + 1]
|
|
19836
|
+
|
|
19837
|
+
if x1 <= trace_x <= x2:
|
|
19838
|
+
# Linear interpolation
|
|
19839
|
+
if x2 != x1:
|
|
19840
|
+
s = (trace_x - x1) / (x2 - x1)
|
|
19841
|
+
time_value = t1 + s * (t2 - t1)
|
|
19842
|
+
else:
|
|
19843
|
+
time_value = t1
|
|
19844
|
+
break
|
|
18682
19845
|
|
|
18683
|
-
#
|
|
18684
|
-
|
|
19846
|
+
# If trace is outside the path range, use nearest endpoint
|
|
19847
|
+
if time_value is None:
|
|
19848
|
+
if trace_x < sorted_path[0][0]:
|
|
19849
|
+
time_value = sorted_path[0][1]
|
|
19850
|
+
else:
|
|
19851
|
+
time_value = sorted_path[-1][1]
|
|
19852
|
+
|
|
19853
|
+
trace_to_time[trace_idx] = time_value
|
|
19854
|
+
|
|
19855
|
+
# Add picks for all crossed traces
|
|
19856
|
+
picked_traces = []
|
|
19857
|
+
for trace_idx in sorted(trace_to_time.keys()):
|
|
19858
|
+
time_value = trace_to_time[trace_idx]
|
|
19859
|
+
trace_x = trace_positions[trace_idx]
|
|
19860
|
+
|
|
19861
|
+
# Add pick at this trace/time
|
|
19862
|
+
self._add_pick_at(trace_x, time_value)
|
|
19863
|
+
picked_traces.append(trace_idx)
|
|
18685
19864
|
|
|
18686
19865
|
self.update_pick_flag = True
|
|
18687
19866
|
self.picks_modified = True # Mark picks as modified
|
|
@@ -18690,25 +19869,25 @@ class MainWindow(QMainWindow):
|
|
|
18690
19869
|
if view == "bottom":
|
|
18691
19870
|
# Picking in bottom seismogram - update bottom
|
|
18692
19871
|
self._plot_target_widget = self.bottomPlotWidget
|
|
18693
|
-
for trace_idx in
|
|
19872
|
+
for trace_idx in picked_traces:
|
|
18694
19873
|
self._update_seismo_pick_display(trace_idx)
|
|
18695
19874
|
self._plot_target_widget = None
|
|
18696
19875
|
# Also update top seismogram if it's showing one
|
|
18697
19876
|
if self.topPlotType == "seismogram":
|
|
18698
19877
|
self._plot_target_widget = self.plotWidget
|
|
18699
|
-
for trace_idx in
|
|
19878
|
+
for trace_idx in picked_traces:
|
|
18700
19879
|
self._update_seismo_pick_display(trace_idx)
|
|
18701
19880
|
self._plot_target_widget = None
|
|
18702
19881
|
else: # view == "top"
|
|
18703
19882
|
# Picking in top seismogram - update top
|
|
18704
19883
|
self._plot_target_widget = self.plotWidget
|
|
18705
|
-
for trace_idx in
|
|
19884
|
+
for trace_idx in picked_traces:
|
|
18706
19885
|
self._update_seismo_pick_display(trace_idx)
|
|
18707
19886
|
self._plot_target_widget = None
|
|
18708
19887
|
# Also update bottom seismogram if it's showing one
|
|
18709
19888
|
if self.bottomPlotType == "seismogram":
|
|
18710
19889
|
self._plot_target_widget = self.bottomPlotWidget
|
|
18711
|
-
for trace_idx in
|
|
19890
|
+
for trace_idx in picked_traces:
|
|
18712
19891
|
self._update_seismo_pick_display(trace_idx)
|
|
18713
19892
|
self._plot_target_widget = None
|
|
18714
19893
|
|
|
@@ -19240,7 +20419,8 @@ class MainWindow(QMainWindow):
|
|
|
19240
20419
|
self.source_position,
|
|
19241
20420
|
self.source_elevation,
|
|
19242
20421
|
self.picks,
|
|
19243
|
-
self.error
|
|
20422
|
+
self.error,
|
|
20423
|
+
geophone_mapping=self.geophone_mapping
|
|
19244
20424
|
)
|
|
19245
20425
|
|
|
19246
20426
|
self.picks_modified = False # Reset flag after successful save
|
|
@@ -19290,6 +20470,7 @@ class MainWindow(QMainWindow):
|
|
|
19290
20470
|
matched_sgt_picks: set of (source_index, geophone_index) tuples that have been matched
|
|
19291
20471
|
Returns tuple: (number of picks matched, number of sources with picks, max pick time)
|
|
19292
20472
|
"""
|
|
20473
|
+
|
|
19293
20474
|
picks_matched = 0
|
|
19294
20475
|
skipped_already_assigned = 0
|
|
19295
20476
|
skipped_no_trace_found = 0
|
|
@@ -19305,7 +20486,7 @@ class MainWindow(QMainWindow):
|
|
|
19305
20486
|
plot_data_x = self.plotTypeDict.get(self.plotTypeX, [])
|
|
19306
20487
|
|
|
19307
20488
|
# Create progress dialog for alternative matching
|
|
19308
|
-
mode_label = {"exact_x": "Exact X", "nearest_x": "Nearest X", "nearest_xz": "Nearest XZ"}
|
|
20489
|
+
mode_label = {"exact_x": "Exact X", "nearest_x": "Nearest X", "nearest_xz": "Nearest XZ", "match_order": "Order Matching"}
|
|
19309
20490
|
progress = QProgressDialog(f"Alternative matching ({mode_label.get(matching_mode, matching_mode)})...", "Cancel", 0, len(self.fileNames), self)
|
|
19310
20491
|
progress.setWindowTitle("Alternative Pick Matching")
|
|
19311
20492
|
progress.setMinimumDuration(0)
|
|
@@ -19314,7 +20495,81 @@ class MainWindow(QMainWindow):
|
|
|
19314
20495
|
progress.show()
|
|
19315
20496
|
QApplication.processEvents()
|
|
19316
20497
|
|
|
19317
|
-
#
|
|
20498
|
+
# For order-based matching, create mappings from actual data and SGT data
|
|
20499
|
+
source_order_map = None
|
|
20500
|
+
trace_order_maps = None
|
|
20501
|
+
|
|
20502
|
+
if matching_mode == "match_order":
|
|
20503
|
+
# Get all unique positions from headers (sources + traces)
|
|
20504
|
+
unique_header_positions = set()
|
|
20505
|
+
|
|
20506
|
+
for i in range(len(self.fileNames)):
|
|
20507
|
+
if self.source_position[i] is not None:
|
|
20508
|
+
unique_header_positions.add(self.source_position[i])
|
|
20509
|
+
|
|
20510
|
+
for shot_idx in range(len(self.fileNames)):
|
|
20511
|
+
if self.trace_position[shot_idx] is not None:
|
|
20512
|
+
for trace_pos in self.trace_position[shot_idx]:
|
|
20513
|
+
unique_header_positions.add(trace_pos)
|
|
20514
|
+
|
|
20515
|
+
sorted_header_positions = sorted(list(unique_header_positions))
|
|
20516
|
+
|
|
20517
|
+
# Get all station positions from SGT file
|
|
20518
|
+
sgt_station_positions = []
|
|
20519
|
+
for idx, sgt_station in enumerate(uploaded_stations):
|
|
20520
|
+
sgt_x = sgt_station[0]
|
|
20521
|
+
sgt_station_positions.append((sgt_x, idx + 1))
|
|
20522
|
+
|
|
20523
|
+
sgt_station_positions.sort(key=lambda x: x[0])
|
|
20524
|
+
sorted_sgt_positions = [pos[0] for pos in sgt_station_positions]
|
|
20525
|
+
|
|
20526
|
+
# Create position-to-index mapping for headers
|
|
20527
|
+
header_pos_map = {}
|
|
20528
|
+
|
|
20529
|
+
for shot_idx in range(len(self.fileNames)):
|
|
20530
|
+
if self.source_position[shot_idx] is not None:
|
|
20531
|
+
pos = self.source_position[shot_idx]
|
|
20532
|
+
if pos not in header_pos_map:
|
|
20533
|
+
header_pos_map[pos] = {'sources': [], 'traces': []}
|
|
20534
|
+
header_pos_map[pos]['sources'].append(shot_idx)
|
|
20535
|
+
|
|
20536
|
+
for shot_idx in range(len(self.fileNames)):
|
|
20537
|
+
if self.trace_position[shot_idx] is not None:
|
|
20538
|
+
for trace_idx, trace_pos in enumerate(self.trace_position[shot_idx]):
|
|
20539
|
+
if trace_pos not in header_pos_map:
|
|
20540
|
+
header_pos_map[trace_pos] = {'sources': [], 'traces': []}
|
|
20541
|
+
header_pos_map[trace_pos]['traces'].append((shot_idx, trace_idx))
|
|
20542
|
+
|
|
20543
|
+
# Create position-to-station-index mapping for SGT
|
|
20544
|
+
sgt_pos_to_stations = {}
|
|
20545
|
+
for sgt_x, station_idx_1based in sgt_station_positions:
|
|
20546
|
+
if sgt_x not in sgt_pos_to_stations:
|
|
20547
|
+
sgt_pos_to_stations[sgt_x] = []
|
|
20548
|
+
sgt_pos_to_stations[sgt_x].append(station_idx_1based)
|
|
20549
|
+
|
|
20550
|
+
# Match positions by sorted order
|
|
20551
|
+
source_order_map = {}
|
|
20552
|
+
trace_order_maps = {i: {} for i in range(len(self.fileNames))}
|
|
20553
|
+
|
|
20554
|
+
for order_idx in range(min(len(sorted_header_positions), len(sorted_sgt_positions))):
|
|
20555
|
+
header_pos = sorted_header_positions[order_idx]
|
|
20556
|
+
sgt_pos = sorted_sgt_positions[order_idx]
|
|
20557
|
+
|
|
20558
|
+
header_data = header_pos_map.get(header_pos, {'sources': [], 'traces': []})
|
|
20559
|
+
sgt_stations = sgt_pos_to_stations.get(sgt_pos, [])
|
|
20560
|
+
|
|
20561
|
+
# Map header sources to first SGT station at this order position
|
|
20562
|
+
if header_data['sources'] and sgt_stations:
|
|
20563
|
+
for shot_idx in header_data['sources']:
|
|
20564
|
+
source_order_map[shot_idx] = sgt_stations[0]
|
|
20565
|
+
|
|
20566
|
+
# Map header traces to SGT station(s) at this order position
|
|
20567
|
+
if header_data['traces'] and sgt_stations:
|
|
20568
|
+
for shot_idx, trace_idx in header_data['traces']:
|
|
20569
|
+
if shot_idx not in trace_order_maps:
|
|
20570
|
+
trace_order_maps[shot_idx] = {}
|
|
20571
|
+
trace_order_maps[shot_idx][trace_idx] = sgt_stations[0]
|
|
20572
|
+
|
|
19318
20573
|
for i in range(len(self.fileNames)):
|
|
19319
20574
|
# Update progress
|
|
19320
20575
|
progress.setValue(i)
|
|
@@ -19332,7 +20587,11 @@ class MainWindow(QMainWindow):
|
|
|
19332
20587
|
# Find matching source in SGT file based on mode
|
|
19333
20588
|
source_index_in_sgt = None
|
|
19334
20589
|
|
|
19335
|
-
if matching_mode == "
|
|
20590
|
+
if matching_mode == "match_order":
|
|
20591
|
+
# Match by order - use the pre-computed mapping
|
|
20592
|
+
if source_order_map is not None and i in source_order_map:
|
|
20593
|
+
source_index_in_sgt = source_order_map[i]
|
|
20594
|
+
elif matching_mode == "exact_x":
|
|
19336
20595
|
# Exact match by X only
|
|
19337
20596
|
min_source_x_dist = float('inf')
|
|
19338
20597
|
best_sgt_idx = None
|
|
@@ -19408,7 +20667,16 @@ class MainWindow(QMainWindow):
|
|
|
19408
20667
|
# Find matching trace based on mode
|
|
19409
20668
|
trace_index_in_shot = None
|
|
19410
20669
|
|
|
19411
|
-
if matching_mode == "
|
|
20670
|
+
if matching_mode == "match_order":
|
|
20671
|
+
# Match by order - use the pre-computed mapping
|
|
20672
|
+
# We need to find which trace corresponds to this SGT geophone
|
|
20673
|
+
if trace_order_maps is not None and i in trace_order_maps:
|
|
20674
|
+
# reverse lookup: find the header trace index that maps to sgt_geophone_number
|
|
20675
|
+
for header_trace_idx, sgt_geo_idx in trace_order_maps[i].items():
|
|
20676
|
+
if sgt_geo_idx == sgt_geophone_number:
|
|
20677
|
+
trace_index_in_shot = header_trace_idx
|
|
20678
|
+
break
|
|
20679
|
+
elif matching_mode == "exact_x":
|
|
19412
20680
|
# Exact match by X only
|
|
19413
20681
|
min_trace_x_dist = float('inf')
|
|
19414
20682
|
best_trace_x = None
|
|
@@ -19423,12 +20691,7 @@ class MainWindow(QMainWindow):
|
|
|
19423
20691
|
if x_dist <= tol_x:
|
|
19424
20692
|
trace_index_in_shot = trace_idx
|
|
19425
20693
|
break
|
|
19426
|
-
|
|
19427
|
-
print(
|
|
19428
|
-
f" Trace X-only exact: SGT geophone X={geophone_x:.3f}, Z={(geophone_z if geophone_z is not None else float('nan')):.3f}; "
|
|
19429
|
-
f"nearest trace X={(best_trace_x if best_trace_x is not None else float('nan')):.3f}, Z={(best_trace_z if best_trace_z is not None else float('nan')):.3f}; "
|
|
19430
|
-
f"deltaX={min_trace_x_dist:.4f} m (tol={tol_x} m)"
|
|
19431
|
-
)
|
|
20694
|
+
pass
|
|
19432
20695
|
else:
|
|
19433
20696
|
# Nearest neighbor matching
|
|
19434
20697
|
min_dist = float('inf')
|
|
@@ -19474,11 +20737,19 @@ class MainWindow(QMainWindow):
|
|
|
19474
20737
|
self.picks[i][trace_index_in_shot] = pick_time
|
|
19475
20738
|
self.error[i][trace_index_in_shot] = error
|
|
19476
20739
|
|
|
20740
|
+
# Store geophone mapping for order matching
|
|
20741
|
+
if matching_mode == "match_order":
|
|
20742
|
+
if self.geophone_mapping is None:
|
|
20743
|
+
self.geophone_mapping = {}
|
|
20744
|
+
self.geophone_mapping[(i, trace_index_in_shot)] = sgt_geophone_number
|
|
20745
|
+
|
|
19477
20746
|
# Track this SGT pick as successfully matched
|
|
19478
20747
|
matched_sgt_picks.add((source_index_in_sgt, sgt_geophone_number))
|
|
19479
20748
|
|
|
19480
20749
|
if not was_overwritten:
|
|
19481
20750
|
picks_matched += 1
|
|
20751
|
+
if verbose and matching_mode == "match_order":
|
|
20752
|
+
print(f"[ORDER MATCHING] ✓ Matched pick: Shot {i}, SGT Source {source_index_in_sgt}, Geophone {sgt_geophone_number} -> Trace {trace_index_in_shot}, Time={pick_time:.4f}s")
|
|
19482
20753
|
|
|
19483
20754
|
# Track this source as having picks and update max pick time
|
|
19484
20755
|
sources_with_picks.add(i)
|
|
@@ -19489,48 +20760,20 @@ class MainWindow(QMainWindow):
|
|
|
19489
20760
|
print(f" Matched pick: Shot {i+1}, SGT geophone #{sgt_geophone_number} (X={geophone_x:.2f}) -> Trace {trace_index_in_shot}, Time={pick_time:.3f}s")
|
|
19490
20761
|
else:
|
|
19491
20762
|
skipped_no_trace_found += 1
|
|
19492
|
-
if verbose:
|
|
19493
|
-
print(f" NO MATCH: Shot {i+1}, SGT geophone #{sgt_geophone_number} (X={geophone_x:.2f}) - could not find trace")
|
|
19494
|
-
|
|
19495
|
-
# Print summary (only when verbose)
|
|
19496
|
-
if verbose:
|
|
19497
|
-
total_attempts = picks_matched + skipped_already_assigned + skipped_no_trace_found
|
|
19498
|
-
print(f"\nMatching summary ({matching_mode}):")
|
|
19499
|
-
print(f" ✓ Successfully matched: {picks_matched} picks for {len(sources_with_picks)} sources")
|
|
19500
|
-
if skipped_already_assigned > 0:
|
|
19501
|
-
print(f" ⊗ Skipped (trace already has pick): {skipped_already_assigned}")
|
|
19502
|
-
if skipped_no_trace_found > 0:
|
|
19503
|
-
print(f" ✗ Skipped (no matching trace found): {skipped_no_trace_found}")
|
|
19504
|
-
if overwritten_count > 0:
|
|
19505
|
-
print(f" ↻ Overwritten existing picks: {overwritten_count}")
|
|
19506
|
-
|
|
19507
|
-
# Global diagnostics for exact X-only when few or no matches
|
|
19508
|
-
if verbose and matching_mode == "exact_x" and picks_matched == 0:
|
|
19509
|
-
try:
|
|
19510
|
-
sgt_xs = [st[0] for st in uploaded_stations]
|
|
19511
|
-
header_xs = []
|
|
19512
|
-
for arr in self.trace_position:
|
|
19513
|
-
if arr is not None:
|
|
19514
|
-
header_xs.extend(list(arr))
|
|
19515
|
-
if len(sgt_xs) > 0 and len(header_xs) > 0:
|
|
19516
|
-
# Compute nearest header X for each SGT X
|
|
19517
|
-
import math
|
|
19518
|
-
nearest_dxs = []
|
|
19519
|
-
# Use a simple O(N*M) for diagnostics; fast enough for typical sizes
|
|
19520
|
-
for sx in sgt_xs:
|
|
19521
|
-
md = min(abs(hx - sx) for hx in header_xs)
|
|
19522
|
-
nearest_dxs.append(md)
|
|
19523
|
-
within_tol = sum(1 for d in nearest_dxs if d <= tol_x)
|
|
19524
|
-
min_dx = min(nearest_dxs)
|
|
19525
|
-
max_dx = max(nearest_dxs)
|
|
19526
|
-
med_dx = sorted(nearest_dxs)[len(nearest_dxs)//2]
|
|
19527
|
-
print(" Exact-X diagnostics across all stations/traces:")
|
|
19528
|
-
print(f" Stations: {len(sgt_xs)}, Traces: {len(header_xs)}")
|
|
19529
|
-
print(f" Nearest deltaX stats: min={min_dx:.4f} m, median={med_dx:.4f} m, max={max_dx:.4f} m, within_tol({tol_x} m)={within_tol}")
|
|
19530
|
-
except Exception as e:
|
|
19531
|
-
if verbose:
|
|
19532
|
-
print(f" Diagnostics error: {e}")
|
|
19533
20763
|
|
|
20764
|
+
# Show final summary dialog for order matching
|
|
20765
|
+
if matching_mode == "match_order":
|
|
20766
|
+
summary_text = f"""Order-Based Pick Matching Summary
|
|
20767
|
+
|
|
20768
|
+
Source mapping entries: {len(source_order_map) if source_order_map else 0}
|
|
20769
|
+
Trace mapping entries: {len(trace_order_maps) if trace_order_maps else 0}
|
|
20770
|
+
|
|
20771
|
+
Total SGT picks processed: {len(uploaded_picks)}
|
|
20772
|
+
Successfully matched: {picks_matched}
|
|
20773
|
+
Failed to match: {skipped_no_trace_found}"""
|
|
20774
|
+
|
|
20775
|
+
QMessageBox.information(None, "Matching Complete", summary_text)
|
|
20776
|
+
|
|
19534
20777
|
# Close progress dialog
|
|
19535
20778
|
progress.setValue(len(self.fileNames))
|
|
19536
20779
|
progress.close()
|
|
@@ -19719,10 +20962,10 @@ class MainWindow(QMainWindow):
|
|
|
19719
20962
|
if 'err' in best_fields:
|
|
19720
20963
|
err_ind = best_fields.index('err')
|
|
19721
20964
|
if verbose:
|
|
19722
|
-
print(f"
|
|
20965
|
+
print(f"Detected pick columns order: {best_fields} -> indices s={s_ind}, g={g_ind}, t={t_ind}, err={err_ind}")
|
|
19723
20966
|
except Exception as e:
|
|
19724
20967
|
if verbose:
|
|
19725
|
-
print(f"
|
|
20968
|
+
print(f"Failed to apply header order, using defaults. Error: {e}")
|
|
19726
20969
|
|
|
19727
20970
|
# Read source index, trace index, pick time, pick error
|
|
19728
20971
|
uploaded_picks = []
|
|
@@ -19766,7 +21009,7 @@ class MainWindow(QMainWindow):
|
|
|
19766
21009
|
else:
|
|
19767
21010
|
print(f" s={s_idx:4d}, g={g_idx:4d} -> INVALID geophone index (outside stations list)")
|
|
19768
21011
|
except Exception as e:
|
|
19769
|
-
|
|
21012
|
+
pass
|
|
19770
21013
|
|
|
19771
21014
|
if self.currentFileName is not None:
|
|
19772
21015
|
# SGT files can be organized by shot order or by station order
|
|
@@ -20062,6 +21305,7 @@ class MainWindow(QMainWindow):
|
|
|
20062
21305
|
btn_exact_x = msg.addButton("Exact match by X only", QMessageBox.YesRole)
|
|
20063
21306
|
btn_nearest_x = msg.addButton("Nearest match by X only", QMessageBox.YesRole)
|
|
20064
21307
|
btn_nearest_xz = msg.addButton("Nearest match by X and Z", QMessageBox.YesRole)
|
|
21308
|
+
btn_match_order = msg.addButton("Match by source/geophone order", QMessageBox.YesRole)
|
|
20065
21309
|
btn_accept = msg.addButton("Accept as is", QMessageBox.AcceptRole)
|
|
20066
21310
|
btn_cancel = msg.addButton("Cancel", QMessageBox.RejectRole)
|
|
20067
21311
|
|
|
@@ -20086,10 +21330,14 @@ class MainWindow(QMainWindow):
|
|
|
20086
21330
|
matching_mode = "nearest_x"
|
|
20087
21331
|
if verbose:
|
|
20088
21332
|
print("\nAttempting nearest neighbor matching by X only...")
|
|
20089
|
-
|
|
21333
|
+
elif clicked_button == btn_nearest_xz:
|
|
20090
21334
|
matching_mode = "nearest_xz"
|
|
20091
21335
|
if verbose:
|
|
20092
21336
|
print("\nAttempting nearest neighbor matching by X and Z...")
|
|
21337
|
+
else: # btn_match_order
|
|
21338
|
+
matching_mode = "match_order"
|
|
21339
|
+
if verbose:
|
|
21340
|
+
print("\nAttempting order-based matching of source/geophone positions...")
|
|
20093
21341
|
|
|
20094
21342
|
# Perform the selected matching
|
|
20095
21343
|
picks_matched_this_round, sources_matched_this_round, max_time_this_round = self._match_sgt_picks_alternative(
|
|
@@ -20112,8 +21360,8 @@ class MainWindow(QMainWindow):
|
|
|
20112
21360
|
n_sources_total = len(sources_with_matched_picks)
|
|
20113
21361
|
|
|
20114
21362
|
if verbose:
|
|
20115
|
-
print(f"
|
|
20116
|
-
print(f"
|
|
21363
|
+
print(f"matched_sgt_picks has {len(matched_sgt_picks)} entries")
|
|
21364
|
+
print(f"prev_n_picks_total={prev_n_picks_total}, n_picks_total={n_picks_total}")
|
|
20117
21365
|
|
|
20118
21366
|
if max_time_this_round > max_picked_time:
|
|
20119
21367
|
max_picked_time = max_time_this_round
|