pyckster 26.1.1__tar.gz → 26.1.2__tar.gz
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-26.1.1 → pyckster-26.1.2}/CHANGES.md +2 -0
- {pyckster-26.1.1/pyckster.egg-info → pyckster-26.1.2}/PKG-INFO +1 -1
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/__init__.py +1 -1
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/core.py +593 -112
- {pyckster-26.1.1 → pyckster-26.1.2/pyckster.egg-info}/PKG-INFO +1 -1
- {pyckster-26.1.1 → pyckster-26.1.2}/LICENCE +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/MANIFEST.in +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/README.md +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/images/pyckster.png +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/images/pyckster.svg +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/images/screenshot_01.png +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/__main__.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/auto_picking.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/bayesian_inversion.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/inversion_app.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/inversion_manager.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/inversion_visualizer.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/ipython_console.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/mpl_export.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/obspy_utils.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/pac_inversion.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/pick_io.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/pyqtgraph_utils.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/surface_wave_analysis.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/surface_wave_profiling.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/sw_utils.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/tab_factory.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster/visualization_utils.py +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster.egg-info/SOURCES.txt +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster.egg-info/dependency_links.txt +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster.egg-info/entry_points.txt +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster.egg-info/not-zip-safe +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster.egg-info/requires.txt +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/pyckster.egg-info/top_level.txt +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/setup.cfg +0 -0
- {pyckster-26.1.1 → pyckster-26.1.2}/setup.py +0 -0
|
@@ -15,7 +15,7 @@ except ImportError:
|
|
|
15
15
|
pass # matplotlib not available, that's fine
|
|
16
16
|
|
|
17
17
|
# Define version and metadata in one place
|
|
18
|
-
__version__ = "26.1.
|
|
18
|
+
__version__ = "26.1.2"
|
|
19
19
|
__author__ = "Sylvain Pasquet"
|
|
20
20
|
__email__ = "sylvain.pasquet@sorbonne-universite.fr"
|
|
21
21
|
__license__ = "GPLv3"
|
|
@@ -709,9 +709,9 @@ class GenericParameterDialog(QDialog):
|
|
|
709
709
|
combo = QComboBox(self)
|
|
710
710
|
# Fill the combo box with the provided choices (expect the parameter to include a key 'values')
|
|
711
711
|
for choice in param.get('values', []):
|
|
712
|
-
combo.addItem(
|
|
712
|
+
combo.addItem(choice)
|
|
713
713
|
# Set current value if provided
|
|
714
|
-
combo.setCurrentText(
|
|
714
|
+
combo.setCurrentText(initial_value)
|
|
715
715
|
self.lineEdits[label_text] = (combo, param_type)
|
|
716
716
|
self.layout.addLayout(self.createFormItem(label_text, combo))
|
|
717
717
|
elif param_type == 'slider':
|
|
@@ -851,9 +851,6 @@ class GenericParameterDialog(QDialog):
|
|
|
851
851
|
elif param_type == 'combo':
|
|
852
852
|
# For dropdown lists, get the current text
|
|
853
853
|
value = lineEdit.currentText()
|
|
854
|
-
# Optionally remove surrounding quotes if present
|
|
855
|
-
if value.startswith("'") and value.endswith("'"):
|
|
856
|
-
value = value[1:-1]
|
|
857
854
|
values[label] = value
|
|
858
855
|
elif param_type == 'slider':
|
|
859
856
|
# For sliders, get the scaled value
|
|
@@ -3587,29 +3584,42 @@ class MainWindow(QMainWindow):
|
|
|
3587
3584
|
|
|
3588
3585
|
# Analytical views: Normalization toggles (Spectrogram, Dispersion)
|
|
3589
3586
|
if view_type in ("Spectrogram", "Dispersion"):
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
norm_per_trace_handler = 'onNormPerTraceChanged'
|
|
3593
|
-
norm_per_freq_handler = 'onNormPerFreqChanged'
|
|
3594
|
-
|
|
3595
|
-
norm_per_trace_check = QCheckBox()
|
|
3596
|
-
norm_per_trace_check.setChecked(bool(getattr(self, norm_per_trace_attr, True)))
|
|
3597
|
-
norm_per_trace_check.toggled.connect(getattr(self, norm_per_trace_handler))
|
|
3587
|
+
# Use view-specific normalization attributes
|
|
3588
|
+
view_prefix = view_type.lower()
|
|
3598
3589
|
|
|
3599
3590
|
if view_type == "Spectrogram":
|
|
3591
|
+
# Spectrogram: both per-trace and per-freq normalization options
|
|
3592
|
+
norm_per_trace_attr = f'{view_prefix}_norm_per_trace'
|
|
3593
|
+
norm_per_freq_attr = f'{view_prefix}_norm_per_freq'
|
|
3594
|
+
norm_per_trace_handler = f'onNormPerTraceChanged_{view_type}'
|
|
3595
|
+
norm_per_freq_handler = f'onNormPerFreqChanged_{view_type}'
|
|
3596
|
+
|
|
3597
|
+
norm_per_trace_check = QCheckBox()
|
|
3598
|
+
norm_per_trace_check.setChecked(bool(getattr(self, norm_per_trace_attr, True)))
|
|
3599
|
+
norm_per_trace_check.toggled.connect(getattr(self, norm_per_trace_handler))
|
|
3600
3600
|
norm_per_trace_check.setText("Norm. per trace")
|
|
3601
|
+
|
|
3601
3602
|
norm_per_freq_check = QCheckBox()
|
|
3602
3603
|
norm_per_freq_check.setChecked(bool(getattr(self, norm_per_freq_attr, False)))
|
|
3603
3604
|
norm_per_freq_check.toggled.connect(getattr(self, norm_per_freq_handler))
|
|
3604
3605
|
norm_per_freq_check.setText("Norm. per freq")
|
|
3606
|
+
|
|
3605
3607
|
layout.addWidget(norm_per_trace_check)
|
|
3606
3608
|
layout.addWidget(norm_per_freq_check)
|
|
3607
|
-
setattr(self, '
|
|
3608
|
-
setattr(self, '
|
|
3609
|
+
setattr(self, f'{prefix}NormPerTraceCheck_spectrogram', norm_per_trace_check)
|
|
3610
|
+
setattr(self, f'{prefix}NormPerFreqCheck_spectrogram', norm_per_freq_check)
|
|
3609
3611
|
else:
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3612
|
+
# Dispersion: only per-freq normalization option
|
|
3613
|
+
norm_per_freq_attr = f'{view_prefix}_norm_per_freq'
|
|
3614
|
+
norm_per_freq_handler = f'onNormPerFreqChanged_{view_type}'
|
|
3615
|
+
|
|
3616
|
+
norm_per_freq_check = QCheckBox()
|
|
3617
|
+
norm_per_freq_check.setChecked(bool(getattr(self, norm_per_freq_attr, False)))
|
|
3618
|
+
norm_per_freq_check.toggled.connect(getattr(self, norm_per_freq_handler))
|
|
3619
|
+
norm_per_freq_check.setText("Norm.")
|
|
3620
|
+
|
|
3621
|
+
layout.addWidget(norm_per_freq_check)
|
|
3622
|
+
setattr(self, f'{prefix}NormPerFreqCheck_dispersion', norm_per_freq_check)
|
|
3613
3623
|
|
|
3614
3624
|
# Topography correction checkbox (Dispersion only)
|
|
3615
3625
|
if view_type == "Dispersion":
|
|
@@ -3619,6 +3629,14 @@ class MainWindow(QMainWindow):
|
|
|
3619
3629
|
layout.addWidget(use_topo_check)
|
|
3620
3630
|
setattr(self, 'useTopographyCheck', use_topo_check)
|
|
3621
3631
|
|
|
3632
|
+
# Permute axes button (Spectrogram only)
|
|
3633
|
+
if view_type == "Spectrogram":
|
|
3634
|
+
permute_button = QPushButton("Permute axes")
|
|
3635
|
+
permute_button.setToolTip("Swap frequency and trace axes")
|
|
3636
|
+
permute_button.clicked.connect(self.onSpectrogramPermuteAxes)
|
|
3637
|
+
layout.addWidget(permute_button)
|
|
3638
|
+
setattr(self, 'spectrogramPermuteButton', permute_button)
|
|
3639
|
+
|
|
3622
3640
|
add_separator()
|
|
3623
3641
|
|
|
3624
3642
|
# Frequency controls (Spectrogram, Dispersion)
|
|
@@ -3636,7 +3654,9 @@ class MainWindow(QMainWindow):
|
|
|
3636
3654
|
fmin_spin.setMaximumWidth(70)
|
|
3637
3655
|
fmin_spin.valueChanged.connect(getattr(self, fmin_handler))
|
|
3638
3656
|
layout.addWidget(fmin_spin)
|
|
3657
|
+
# Store both generic and view-specific names
|
|
3639
3658
|
setattr(self, 'fminSpin', fmin_spin)
|
|
3659
|
+
setattr(self, f'{prefix}_fminSpin', fmin_spin)
|
|
3640
3660
|
|
|
3641
3661
|
fmax_spin = QDoubleSpinBox()
|
|
3642
3662
|
fmax_spin.setRange(0.0, 1e4)
|
|
@@ -3647,7 +3667,9 @@ class MainWindow(QMainWindow):
|
|
|
3647
3667
|
fmax_spin.setMaximumWidth(70)
|
|
3648
3668
|
fmax_spin.valueChanged.connect(getattr(self, fmax_handler))
|
|
3649
3669
|
layout.addWidget(fmax_spin)
|
|
3670
|
+
# Store both generic and view-specific names
|
|
3650
3671
|
setattr(self, 'fmaxSpin', fmax_spin)
|
|
3672
|
+
setattr(self, f'{prefix}_fmaxSpin', fmax_spin)
|
|
3651
3673
|
|
|
3652
3674
|
add_separator()
|
|
3653
3675
|
|
|
@@ -3692,7 +3714,21 @@ class MainWindow(QMainWindow):
|
|
|
3692
3714
|
add_separator()
|
|
3693
3715
|
|
|
3694
3716
|
# Trace selection for phase-shift using range slider
|
|
3695
|
-
|
|
3717
|
+
trace_range_container = QHBoxLayout()
|
|
3718
|
+
trace_range_label = QLabel("Trace range:")
|
|
3719
|
+
trace_range_container.addWidget(trace_range_label)
|
|
3720
|
+
|
|
3721
|
+
batch_window_button = QPushButton("Batch windowing")
|
|
3722
|
+
batch_window_button.setToolTip("Set fixed window parameters for all shots")
|
|
3723
|
+
batch_window_button.clicked.connect(self.onDispersionBatchWindowing)
|
|
3724
|
+
trace_range_container.addWidget(batch_window_button)
|
|
3725
|
+
trace_range_container.addStretch()
|
|
3726
|
+
|
|
3727
|
+
# Add the horizontal layout as a widget to the main layout
|
|
3728
|
+
trace_range_widget = QWidget()
|
|
3729
|
+
trace_range_widget.setLayout(trace_range_container)
|
|
3730
|
+
layout.addWidget(trace_range_widget)
|
|
3731
|
+
|
|
3696
3732
|
trace_range_slider = RangeSlider()
|
|
3697
3733
|
# Store which view this slider controls
|
|
3698
3734
|
trace_range_slider.controlled_view = prefix # "top" or "bottom"
|
|
@@ -3770,45 +3806,50 @@ class MainWindow(QMainWindow):
|
|
|
3770
3806
|
if hasattr(self, 'topDynamicControlsLayout'):
|
|
3771
3807
|
self._clear_layout(self.topDynamicControlsLayout)
|
|
3772
3808
|
|
|
3773
|
-
def
|
|
3774
|
-
"""Handle per-trace normalization toggle
|
|
3775
|
-
self.
|
|
3809
|
+
def onNormPerTraceChanged_Spectrogram(self, state):
|
|
3810
|
+
"""Handle per-trace normalization toggle for Spectrogram view"""
|
|
3811
|
+
self.spectrogram_norm_per_trace = bool(state)
|
|
3776
3812
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3777
3813
|
return
|
|
3778
|
-
# Replot both top and bottom if they use
|
|
3779
|
-
if hasattr(self, 'topPlotType'):
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
self._plot_target_viewbox = None
|
|
3792
|
-
if hasattr(self, 'bottomPlotType'):
|
|
3793
|
-
if self.bottomPlotType == "spectrogram":
|
|
3794
|
-
self._plot_target_widget = self.bottomPlotWidget
|
|
3795
|
-
self._plot_target_viewbox = self.bottomViewBox
|
|
3796
|
-
self.plotSpectrogram()
|
|
3797
|
-
self._plot_target_widget = None
|
|
3798
|
-
self._plot_target_viewbox = None
|
|
3799
|
-
elif self.bottomPlotType == "dispersion":
|
|
3800
|
-
self._plot_target_widget = self.bottomPlotWidget
|
|
3801
|
-
self._plot_target_viewbox = self.bottomViewBox
|
|
3802
|
-
self.plotDispersion()
|
|
3803
|
-
self._plot_target_widget = None
|
|
3804
|
-
self._plot_target_viewbox = None
|
|
3814
|
+
# Replot both top and bottom if they use spectrogram
|
|
3815
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "spectrogram":
|
|
3816
|
+
self._plot_target_widget = self.plotWidget
|
|
3817
|
+
self._plot_target_viewbox = self.viewBox
|
|
3818
|
+
self.plotSpectrogram()
|
|
3819
|
+
self._plot_target_widget = None
|
|
3820
|
+
self._plot_target_viewbox = None
|
|
3821
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "spectrogram":
|
|
3822
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3823
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3824
|
+
self.plotSpectrogram()
|
|
3825
|
+
self._plot_target_widget = None
|
|
3826
|
+
self._plot_target_viewbox = None
|
|
3805
3827
|
|
|
3806
|
-
def
|
|
3807
|
-
"""Handle per-
|
|
3808
|
-
self.
|
|
3828
|
+
def onNormPerTraceChanged_Dispersion(self, state):
|
|
3829
|
+
"""Handle per-trace normalization toggle for Dispersion view"""
|
|
3830
|
+
self.dispersion_norm_per_trace = bool(state)
|
|
3831
|
+
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3832
|
+
return
|
|
3833
|
+
# Replot both top and bottom if they use dispersion
|
|
3834
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
3835
|
+
self._plot_target_widget = self.plotWidget
|
|
3836
|
+
self._plot_target_viewbox = self.viewBox
|
|
3837
|
+
self.plotDispersion()
|
|
3838
|
+
self._plot_target_widget = None
|
|
3839
|
+
self._plot_target_viewbox = None
|
|
3840
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
3841
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3842
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3843
|
+
self.plotDispersion()
|
|
3844
|
+
self._plot_target_widget = None
|
|
3845
|
+
self._plot_target_viewbox = None
|
|
3846
|
+
|
|
3847
|
+
def onNormPerFreqChanged_Spectrogram(self, state):
|
|
3848
|
+
"""Handle per-freq normalization toggle for Spectrogram view"""
|
|
3849
|
+
self.spectrogram_norm_per_freq = bool(state)
|
|
3809
3850
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3810
3851
|
return
|
|
3811
|
-
# Replot both top and bottom if they use
|
|
3852
|
+
# Replot both top and bottom if they use spectrogram
|
|
3812
3853
|
if hasattr(self, 'topPlotType') and self.topPlotType == "spectrogram":
|
|
3813
3854
|
self._plot_target_widget = self.plotWidget
|
|
3814
3855
|
self._plot_target_viewbox = self.viewBox
|
|
@@ -3821,7 +3862,39 @@ class MainWindow(QMainWindow):
|
|
|
3821
3862
|
self.plotSpectrogram()
|
|
3822
3863
|
self._plot_target_widget = None
|
|
3823
3864
|
self._plot_target_viewbox = None
|
|
3824
|
-
|
|
3865
|
+
|
|
3866
|
+
def onNormPerFreqChanged_Dispersion(self, state):
|
|
3867
|
+
"""Handle per-freq normalization toggle for Dispersion view"""
|
|
3868
|
+
self.dispersion_norm_per_freq = bool(state)
|
|
3869
|
+
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3870
|
+
return
|
|
3871
|
+
# Replot both top and bottom if they use dispersion
|
|
3872
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
3873
|
+
self._plot_target_widget = self.plotWidget
|
|
3874
|
+
self._plot_target_viewbox = self.viewBox
|
|
3875
|
+
self.plotDispersion()
|
|
3876
|
+
self._plot_target_widget = None
|
|
3877
|
+
self._plot_target_viewbox = None
|
|
3878
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
3879
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3880
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3881
|
+
self.plotDispersion()
|
|
3882
|
+
self._plot_target_widget = None
|
|
3883
|
+
self._plot_target_viewbox = None
|
|
3884
|
+
|
|
3885
|
+
# Deprecated methods kept for backward compatibility
|
|
3886
|
+
def onNormPerTraceChanged(self, state):
|
|
3887
|
+
"""Deprecated: use view-specific handlers instead"""
|
|
3888
|
+
self.norm_per_trace = bool(state)
|
|
3889
|
+
# Delegate to spectrogram handler for backward compatibility
|
|
3890
|
+
self.onNormPerTraceChanged_Spectrogram(state)
|
|
3891
|
+
|
|
3892
|
+
def onNormPerFreqChanged(self, state):
|
|
3893
|
+
"""Deprecated: use view-specific handlers instead"""
|
|
3894
|
+
self.norm_per_freq = bool(state)
|
|
3895
|
+
# Delegate to spectrogram handler for backward compatibility
|
|
3896
|
+
self.onNormPerFreqChanged_Spectrogram(state)
|
|
3897
|
+
|
|
3825
3898
|
def onUseTopographyChanged(self, state):
|
|
3826
3899
|
"""Handle topography correction checkbox toggle for phase-shift analysis"""
|
|
3827
3900
|
self.use_topography_correction = bool(state)
|
|
@@ -3831,50 +3904,406 @@ class MainWindow(QMainWindow):
|
|
|
3831
3904
|
if top_type == "dispersion" or bottom_type == "dispersion":
|
|
3832
3905
|
self.updatePlots()
|
|
3833
3906
|
|
|
3907
|
+
def onSpectrogramPermuteAxes(self):
|
|
3908
|
+
"""Toggle permutation of frequency and trace axes in spectrogram"""
|
|
3909
|
+
self.spectrogram_freq_on_y = not self.spectrogram_freq_on_y
|
|
3910
|
+
|
|
3911
|
+
# Replot both top and bottom if they use spectrogram
|
|
3912
|
+
if hasattr(self, 'topPlotType') and self.topPlotType == "spectrogram":
|
|
3913
|
+
self._plot_target_widget = self.plotWidget
|
|
3914
|
+
self._plot_target_viewbox = self.viewBox
|
|
3915
|
+
self.plotSpectrogram()
|
|
3916
|
+
self._plot_target_widget = None
|
|
3917
|
+
self._plot_target_viewbox = None
|
|
3918
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "spectrogram":
|
|
3919
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
3920
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
3921
|
+
self.plotSpectrogram()
|
|
3922
|
+
self._plot_target_widget = None
|
|
3923
|
+
self._plot_target_viewbox = None
|
|
3924
|
+
|
|
3925
|
+
def onDispersionBatchWindowing(self):
|
|
3926
|
+
"""Configure batch windowing parameters for all shots in dispersion view"""
|
|
3927
|
+
parameters = [
|
|
3928
|
+
{'label': 'Window length (traces)', 'initial_value': 10, 'type': 'int'},
|
|
3929
|
+
{'label': 'Window side', 'initial_value': 'best', 'type': 'combo', 'values': ['left', 'right', 'best']},
|
|
3930
|
+
{'label': 'Prefer side (when best)', 'initial_value': 'right', 'type': 'combo', 'values': ['left', 'right']},
|
|
3931
|
+
{'label': 'Offset from shot (traces)', 'initial_value': 1, 'type': 'int'}
|
|
3932
|
+
]
|
|
3933
|
+
|
|
3934
|
+
dialog = GenericParameterDialog(
|
|
3935
|
+
title="Batch Windowing Parameters",
|
|
3936
|
+
parameters=parameters,
|
|
3937
|
+
parent=self
|
|
3938
|
+
)
|
|
3939
|
+
|
|
3940
|
+
if dialog.exec_():
|
|
3941
|
+
values = dialog.getValues()
|
|
3942
|
+
window_length = values.get('Window length (traces)', 10)
|
|
3943
|
+
window_side = values.get('Window side', 'best')
|
|
3944
|
+
prefer_side = values.get('Prefer side (when best)', 'right')
|
|
3945
|
+
offset_from_shot = values.get('Offset from shot (traces)', 1)
|
|
3946
|
+
|
|
3947
|
+
# Apply batch windowing to all shots
|
|
3948
|
+
self._applyBatchWindowingToAllShots(window_length, window_side, offset_from_shot, prefer_side)
|
|
3949
|
+
|
|
3950
|
+
def _applyBatchWindowingToAllShots(self, window_length, window_side, offset_from_shot, prefer_side='right'):
|
|
3951
|
+
"""Apply batch windowing parameters to all loaded shots using shot and geophone positions"""
|
|
3952
|
+
if not self.streams:
|
|
3953
|
+
QMessageBox.warning(self, "No Data", "No data loaded.")
|
|
3954
|
+
return
|
|
3955
|
+
|
|
3956
|
+
try:
|
|
3957
|
+
# Initialize batch windowing memory if not exists
|
|
3958
|
+
if not hasattr(self, 'batch_windowing_params'):
|
|
3959
|
+
self.batch_windowing_params = {}
|
|
3960
|
+
|
|
3961
|
+
# Store parameters
|
|
3962
|
+
self.batch_windowing_params['window_length'] = window_length
|
|
3963
|
+
self.batch_windowing_params['window_side'] = window_side
|
|
3964
|
+
self.batch_windowing_params['offset_from_shot'] = offset_from_shot
|
|
3965
|
+
self.batch_windowing_params['prefer_side'] = prefer_side
|
|
3966
|
+
|
|
3967
|
+
# Apply to each shot
|
|
3968
|
+
for shot_idx, stream in enumerate(self.streams):
|
|
3969
|
+
n_traces = len(stream)
|
|
3970
|
+
|
|
3971
|
+
# Get shot position and trace positions
|
|
3972
|
+
if shot_idx >= len(self.source_position) or shot_idx >= len(self.trace_position):
|
|
3973
|
+
continue
|
|
3974
|
+
|
|
3975
|
+
shot_pos = self.source_position[shot_idx]
|
|
3976
|
+
trace_positions = np.array(self.trace_position[shot_idx])
|
|
3977
|
+
|
|
3978
|
+
# Calculate mean geophone spacing
|
|
3979
|
+
if len(trace_positions) > 1:
|
|
3980
|
+
sorted_positions = np.sort(trace_positions)
|
|
3981
|
+
mean_dg = np.mean(np.diff(sorted_positions))
|
|
3982
|
+
else:
|
|
3983
|
+
mean_dg = 1.0 # Default spacing if only one trace
|
|
3984
|
+
|
|
3985
|
+
# Calculate offset distance threshold
|
|
3986
|
+
offset_distance = offset_from_shot * mean_dg
|
|
3987
|
+
|
|
3988
|
+
# Calculate distances from shot (positive = right, negative = left)
|
|
3989
|
+
distances = trace_positions - shot_pos
|
|
3990
|
+
abs_distances = np.abs(distances)
|
|
3991
|
+
|
|
3992
|
+
# Sort traces by distance from shot to determine left/right
|
|
3993
|
+
sorted_indices = np.argsort(distances)
|
|
3994
|
+
sorted_distances = distances[sorted_indices]
|
|
3995
|
+
|
|
3996
|
+
# Find traces on left (negative distance) and right (positive distance)
|
|
3997
|
+
left_mask = sorted_distances < 0
|
|
3998
|
+
right_mask = sorted_distances > 0
|
|
3999
|
+
|
|
4000
|
+
left_indices = sorted_indices[left_mask]
|
|
4001
|
+
right_indices = sorted_indices[right_mask]
|
|
4002
|
+
|
|
4003
|
+
# Calculate window range based on side preference
|
|
4004
|
+
first_trace = None
|
|
4005
|
+
last_trace = None
|
|
4006
|
+
|
|
4007
|
+
if window_side == 'left':
|
|
4008
|
+
# Use traces to the left of shot, beyond offset_distance
|
|
4009
|
+
if len(left_indices) > 0:
|
|
4010
|
+
# Filter left traces that are beyond the offset distance
|
|
4011
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance]
|
|
4012
|
+
|
|
4013
|
+
if len(left_beyond_offset) > 0:
|
|
4014
|
+
# Sort by absolute distance (closest to shot first, among those beyond offset)
|
|
4015
|
+
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4016
|
+
# Take window_length traces
|
|
4017
|
+
selected_traces = left_sorted[:window_length]
|
|
4018
|
+
first_trace = int(np.min(selected_traces))
|
|
4019
|
+
last_trace = int(np.max(selected_traces))
|
|
4020
|
+
|
|
4021
|
+
elif window_side == 'right':
|
|
4022
|
+
# Use traces to the right of shot, beyond offset_distance
|
|
4023
|
+
if len(right_indices) > 0:
|
|
4024
|
+
# Filter right traces that are beyond the offset distance
|
|
4025
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance]
|
|
4026
|
+
|
|
4027
|
+
if len(right_beyond_offset) > 0:
|
|
4028
|
+
# Sort by distance (closest to shot first, among those beyond offset)
|
|
4029
|
+
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4030
|
+
# Take window_length traces
|
|
4031
|
+
selected_traces = right_sorted[:window_length]
|
|
4032
|
+
first_trace = int(np.min(selected_traces))
|
|
4033
|
+
last_trace = int(np.max(selected_traces))
|
|
4034
|
+
|
|
4035
|
+
elif window_side == 'best':
|
|
4036
|
+
# Try preferred side first, fallback to other side if not enough traces
|
|
4037
|
+
if prefer_side == 'right':
|
|
4038
|
+
# Prefer right side - check if enough traces beyond offset_distance
|
|
4039
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4040
|
+
|
|
4041
|
+
if len(right_beyond_offset) >= window_length:
|
|
4042
|
+
# Enough traces on the right beyond offset
|
|
4043
|
+
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4044
|
+
selected_traces = right_sorted[:window_length]
|
|
4045
|
+
first_trace = int(np.min(selected_traces))
|
|
4046
|
+
last_trace = int(np.max(selected_traces))
|
|
4047
|
+
else:
|
|
4048
|
+
# Try left side
|
|
4049
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4050
|
+
if len(left_beyond_offset) >= window_length:
|
|
4051
|
+
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4052
|
+
selected_traces = left_sorted[:window_length]
|
|
4053
|
+
first_trace = int(np.min(selected_traces))
|
|
4054
|
+
last_trace = int(np.max(selected_traces))
|
|
4055
|
+
else:
|
|
4056
|
+
# Prefer left side - check if enough traces beyond offset_distance
|
|
4057
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4058
|
+
|
|
4059
|
+
if len(left_beyond_offset) >= window_length:
|
|
4060
|
+
# Enough traces on the left beyond offset
|
|
4061
|
+
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4062
|
+
selected_traces = left_sorted[:window_length]
|
|
4063
|
+
first_trace = int(np.min(selected_traces))
|
|
4064
|
+
last_trace = int(np.max(selected_traces))
|
|
4065
|
+
else:
|
|
4066
|
+
# Try right side
|
|
4067
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4068
|
+
if len(right_beyond_offset) >= window_length:
|
|
4069
|
+
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4070
|
+
selected_traces = right_sorted[:window_length]
|
|
4071
|
+
first_trace = int(np.min(selected_traces))
|
|
4072
|
+
last_trace = int(np.max(selected_traces))
|
|
4073
|
+
|
|
4074
|
+
# If still no valid range, reduce requirements (fewer than window_length traces or closer than offset)
|
|
4075
|
+
if first_trace is None or last_trace is None:
|
|
4076
|
+
# Use what's available, respecting preference
|
|
4077
|
+
if prefer_side == 'right':
|
|
4078
|
+
# Try right side with any available traces beyond offset
|
|
4079
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4080
|
+
if len(right_beyond_offset) > 0:
|
|
4081
|
+
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4082
|
+
selected_traces = right_sorted
|
|
4083
|
+
first_trace = int(np.min(selected_traces))
|
|
4084
|
+
last_trace = int(np.max(selected_traces))
|
|
4085
|
+
else:
|
|
4086
|
+
# Try left side with any available traces beyond offset
|
|
4087
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4088
|
+
if len(left_beyond_offset) > 0:
|
|
4089
|
+
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4090
|
+
selected_traces = left_sorted
|
|
4091
|
+
first_trace = int(np.min(selected_traces))
|
|
4092
|
+
last_trace = int(np.max(selected_traces))
|
|
4093
|
+
elif len(right_indices) > 0:
|
|
4094
|
+
# No offset possible, use closest right traces
|
|
4095
|
+
right_sorted = right_indices[np.argsort(abs_distances[right_indices])]
|
|
4096
|
+
selected_traces = right_sorted[:window_length] if len(right_sorted) >= window_length else right_sorted
|
|
4097
|
+
first_trace = int(np.min(selected_traces))
|
|
4098
|
+
last_trace = int(np.max(selected_traces))
|
|
4099
|
+
elif len(left_indices) > 0:
|
|
4100
|
+
# No right traces, use closest left traces
|
|
4101
|
+
left_sorted = left_indices[np.argsort(abs_distances[left_indices])]
|
|
4102
|
+
selected_traces = left_sorted[:window_length] if len(left_sorted) >= window_length else left_sorted
|
|
4103
|
+
first_trace = int(np.min(selected_traces))
|
|
4104
|
+
last_trace = int(np.max(selected_traces))
|
|
4105
|
+
else:
|
|
4106
|
+
# Prefer left side
|
|
4107
|
+
# Try left side with any available traces beyond offset
|
|
4108
|
+
left_beyond_offset = left_indices[abs_distances[left_indices] >= offset_distance] if len(left_indices) > 0 else np.array([])
|
|
4109
|
+
if len(left_beyond_offset) > 0:
|
|
4110
|
+
left_sorted = left_beyond_offset[np.argsort(abs_distances[left_beyond_offset])]
|
|
4111
|
+
selected_traces = left_sorted
|
|
4112
|
+
first_trace = int(np.min(selected_traces))
|
|
4113
|
+
last_trace = int(np.max(selected_traces))
|
|
4114
|
+
else:
|
|
4115
|
+
# Try right side with any available traces beyond offset
|
|
4116
|
+
right_beyond_offset = right_indices[abs_distances[right_indices] >= offset_distance] if len(right_indices) > 0 else np.array([])
|
|
4117
|
+
if len(right_beyond_offset) > 0:
|
|
4118
|
+
right_sorted = right_beyond_offset[np.argsort(abs_distances[right_beyond_offset])]
|
|
4119
|
+
selected_traces = right_sorted
|
|
4120
|
+
first_trace = int(np.min(selected_traces))
|
|
4121
|
+
last_trace = int(np.max(selected_traces))
|
|
4122
|
+
elif len(left_indices) > 0:
|
|
4123
|
+
# No offset possible, use closest left traces
|
|
4124
|
+
left_sorted = left_indices[np.argsort(abs_distances[left_indices])]
|
|
4125
|
+
selected_traces = left_sorted[:window_length] if len(left_sorted) >= window_length else left_sorted
|
|
4126
|
+
first_trace = int(np.min(selected_traces))
|
|
4127
|
+
last_trace = int(np.max(selected_traces))
|
|
4128
|
+
elif len(right_indices) > 0:
|
|
4129
|
+
# No left traces, use closest right traces
|
|
4130
|
+
right_sorted = right_indices[np.argsort(abs_distances[right_indices])]
|
|
4131
|
+
selected_traces = right_sorted[:window_length] if len(right_sorted) >= window_length else right_sorted
|
|
4132
|
+
first_trace = int(np.min(selected_traces))
|
|
4133
|
+
last_trace = int(np.max(selected_traces))
|
|
4134
|
+
|
|
4135
|
+
# Final fallback if still no valid range (shouldn't reach here)
|
|
4136
|
+
if first_trace is None or last_trace is None:
|
|
4137
|
+
# Use closest traces to shot position
|
|
4138
|
+
abs_distances = np.abs(distances)
|
|
4139
|
+
closest_indices = np.argsort(abs_distances)[:min(window_length, n_traces)]
|
|
4140
|
+
first_trace = int(np.min(closest_indices))
|
|
4141
|
+
last_trace = int(np.max(closest_indices))
|
|
4142
|
+
|
|
4143
|
+
# Ensure valid range
|
|
4144
|
+
first_trace = max(0, min(first_trace, n_traces - 1))
|
|
4145
|
+
last_trace = max(first_trace, min(last_trace, n_traces - 1))
|
|
4146
|
+
|
|
4147
|
+
# Store in trace range memory using shot_idx as key (consistent with onFileSelectionChanged)
|
|
4148
|
+
file_key = shot_idx
|
|
4149
|
+
if file_key not in self.trace_range_memory:
|
|
4150
|
+
self.trace_range_memory[file_key] = {}
|
|
4151
|
+
|
|
4152
|
+
# Store for both views (sync them) using dictionary structure
|
|
4153
|
+
self.trace_range_memory[file_key]['top'] = {
|
|
4154
|
+
'first': first_trace,
|
|
4155
|
+
'last': last_trace,
|
|
4156
|
+
'n_traces': n_traces
|
|
4157
|
+
}
|
|
4158
|
+
self.trace_range_memory[file_key]['bottom'] = {
|
|
4159
|
+
'first': first_trace,
|
|
4160
|
+
'last': last_trace,
|
|
4161
|
+
'n_traces': n_traces
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
# Update the current shot's range slider and attributes
|
|
4165
|
+
if self.currentIndex is not None and self.currentIndex < len(self.streams):
|
|
4166
|
+
file_key = self.currentIndex
|
|
4167
|
+
|
|
4168
|
+
if file_key in self.trace_range_memory and 'bottom' in self.trace_range_memory[file_key]:
|
|
4169
|
+
stored_range = self.trace_range_memory[file_key]['bottom']
|
|
4170
|
+
first_trace = stored_range['first']
|
|
4171
|
+
last_trace = stored_range['last']
|
|
4172
|
+
n_traces = stored_range['n_traces']
|
|
4173
|
+
|
|
4174
|
+
# Update view-specific attributes
|
|
4175
|
+
self.bottom_first_trace = first_trace
|
|
4176
|
+
self.bottom_last_trace = last_trace
|
|
4177
|
+
self.top_first_trace = first_trace
|
|
4178
|
+
self.top_last_trace = last_trace
|
|
4179
|
+
|
|
4180
|
+
# Update the slider widget with error handling
|
|
4181
|
+
try:
|
|
4182
|
+
if hasattr(self, 'bottom_traceRangeSlider') and self.bottom_traceRangeSlider is not None:
|
|
4183
|
+
# Check if widget is still valid (not deleted)
|
|
4184
|
+
try:
|
|
4185
|
+
self.bottom_traceRangeSlider.setRange(first_trace, last_trace, n_traces - 1)
|
|
4186
|
+
except RuntimeError:
|
|
4187
|
+
# Widget was deleted, skip update
|
|
4188
|
+
pass
|
|
4189
|
+
except Exception:
|
|
4190
|
+
pass
|
|
4191
|
+
|
|
4192
|
+
try:
|
|
4193
|
+
if hasattr(self, 'top_traceRangeSlider') and self.top_traceRangeSlider is not None:
|
|
4194
|
+
# Check if widget is still valid (not deleted)
|
|
4195
|
+
try:
|
|
4196
|
+
self.top_traceRangeSlider.setRange(first_trace, last_trace, n_traces - 1)
|
|
4197
|
+
except RuntimeError:
|
|
4198
|
+
# Widget was deleted, skip update
|
|
4199
|
+
pass
|
|
4200
|
+
except Exception:
|
|
4201
|
+
pass
|
|
4202
|
+
|
|
4203
|
+
# Redraw dispersion if active
|
|
4204
|
+
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
4205
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4206
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4207
|
+
self.drawTraceExtentIndicator()
|
|
4208
|
+
self.plotDispersion()
|
|
4209
|
+
self._plot_target_widget = None
|
|
4210
|
+
self._plot_target_viewbox = None
|
|
4211
|
+
elif hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
4212
|
+
self._plot_target_widget = self.topPlotWidget
|
|
4213
|
+
self._plot_target_viewbox = self.topViewBox
|
|
4214
|
+
self.drawTraceExtentIndicator()
|
|
4215
|
+
self.plotDispersion()
|
|
4216
|
+
self._plot_target_widget = None
|
|
4217
|
+
self._plot_target_viewbox = None
|
|
4218
|
+
|
|
4219
|
+
# Show confirmation message
|
|
4220
|
+
prefer_text = f", Prefer: {prefer_side}" if window_side == 'best' else ""
|
|
4221
|
+
QMessageBox.information(self, "Batch Windowing",
|
|
4222
|
+
f"Applied batch windowing to {len(self.streams)} shots.\n"
|
|
4223
|
+
f"Window: {window_length} traces, Side: {window_side}{prefer_text}")
|
|
4224
|
+
|
|
4225
|
+
except Exception as e:
|
|
4226
|
+
QMessageBox.critical(self, "Error", f"Failed to apply batch windowing: {str(e)}")
|
|
4227
|
+
|
|
3834
4228
|
def onFreqRangeChanged(self, _=None):
|
|
3835
4229
|
"""Handle frequency range change (fmin/fmax) - unified"""
|
|
3836
4230
|
# Guard against calls when no file is loaded
|
|
3837
4231
|
if not (hasattr(self, 'currentIndex') and self.currentIndex is not None and self.streams):
|
|
3838
4232
|
return
|
|
3839
4233
|
|
|
4234
|
+
# Detect which spinbox sent the signal
|
|
4235
|
+
sender_spin = self.sender()
|
|
4236
|
+
|
|
3840
4237
|
# Get current values from spinboxes
|
|
3841
4238
|
try:
|
|
3842
|
-
|
|
3843
|
-
|
|
4239
|
+
# Use the sender spinbox value if available, otherwise use the generic one
|
|
4240
|
+
if sender_spin and hasattr(sender_spin, 'value'):
|
|
4241
|
+
# Sender is one of the spinboxes, but we still need both fmin and fmax
|
|
4242
|
+
# Find the corresponding pair (if sender is fmin spinbox, get both fmin and fmax)
|
|
4243
|
+
fmin_val = None
|
|
4244
|
+
fmax_val = None
|
|
4245
|
+
|
|
4246
|
+
# Check which spinbox sent the signal
|
|
4247
|
+
for prefix in ['top', 'bottom']:
|
|
4248
|
+
fmin_attr = f'{prefix}_fminSpin'
|
|
4249
|
+
fmax_attr = f'{prefix}_fmaxSpin'
|
|
4250
|
+
|
|
4251
|
+
if hasattr(self, fmin_attr) and getattr(self, fmin_attr) is sender_spin:
|
|
4252
|
+
# Sender is the fmin spinbox for this prefix
|
|
4253
|
+
fmin_val = sender_spin.value()
|
|
4254
|
+
if hasattr(self, fmax_attr):
|
|
4255
|
+
fmax_val = getattr(self, fmax_attr).value()
|
|
4256
|
+
break
|
|
4257
|
+
elif hasattr(self, fmax_attr) and getattr(self, fmax_attr) is sender_spin:
|
|
4258
|
+
# Sender is the fmax spinbox for this prefix
|
|
4259
|
+
fmax_val = sender_spin.value()
|
|
4260
|
+
if hasattr(self, fmin_attr):
|
|
4261
|
+
fmin_val = getattr(self, fmin_attr).value()
|
|
4262
|
+
break
|
|
4263
|
+
|
|
4264
|
+
# Fallback if sender not identified
|
|
4265
|
+
if fmin_val is None or fmax_val is None:
|
|
4266
|
+
fmin_val = self.fminSpin.value()
|
|
4267
|
+
fmax_val = self.fmaxSpin.value()
|
|
4268
|
+
else:
|
|
4269
|
+
fmin_val = self.fminSpin.value()
|
|
4270
|
+
fmax_val = self.fmaxSpin.value()
|
|
3844
4271
|
except (RuntimeError, AttributeError):
|
|
3845
4272
|
return
|
|
3846
4273
|
|
|
3847
|
-
#
|
|
3848
|
-
nyquist = None
|
|
3849
|
-
try:
|
|
3850
|
-
if self.currentIndex is not None:
|
|
3851
|
-
dt = self.sample_interval[self.currentIndex]
|
|
3852
|
-
if dt and dt > 0:
|
|
3853
|
-
nyquist = 1.0 / (2.0 * dt)
|
|
3854
|
-
except Exception:
|
|
3855
|
-
nyquist = None
|
|
3856
|
-
|
|
3857
|
-
if nyquist is not None:
|
|
3858
|
-
# Clamp to [0, nyquist]
|
|
3859
|
-
fmin_val = max(0.0, min(fmin_val, nyquist))
|
|
3860
|
-
fmax_val = max(0.0, min(fmax_val, nyquist))
|
|
3861
|
-
try:
|
|
3862
|
-
self.fminSpin.blockSignals(True)
|
|
3863
|
-
self.fmaxSpin.blockSignals(True)
|
|
3864
|
-
self.fminSpin.setValue(fmin_val)
|
|
3865
|
-
self.fmaxSpin.setValue(fmax_val)
|
|
3866
|
-
finally:
|
|
3867
|
-
self.fminSpin.blockSignals(False)
|
|
3868
|
-
self.fmaxSpin.blockSignals(False)
|
|
3869
|
-
|
|
4274
|
+
# Store the raw values (allow user to set any frequency)
|
|
3870
4275
|
self.fmin = fmin_val if fmin_val > 0 else None
|
|
3871
4276
|
self.fmax = fmax_val if fmax_val > 0 else None
|
|
3872
4277
|
|
|
3873
|
-
# Guard against invalid ranges
|
|
4278
|
+
# Guard against invalid ranges (fmin >= fmax is only checked for actual values)
|
|
3874
4279
|
if (self.fmin is not None and self.fmax is not None and self.fmin >= self.fmax):
|
|
3875
4280
|
return
|
|
3876
4281
|
|
|
3877
|
-
#
|
|
4282
|
+
# Sync all view-specific spinboxes to the same value (except the sender)
|
|
4283
|
+
for prefix in ['top', 'bottom']:
|
|
4284
|
+
fmin_attr = f'{prefix}_fminSpin'
|
|
4285
|
+
fmax_attr = f'{prefix}_fmaxSpin'
|
|
4286
|
+
if hasattr(self, fmin_attr):
|
|
4287
|
+
try:
|
|
4288
|
+
spin = getattr(self, fmin_attr)
|
|
4289
|
+
if spin is not None and spin is not sender_spin and spin.value() != fmin_val:
|
|
4290
|
+
spin.blockSignals(True)
|
|
4291
|
+
spin.setValue(fmin_val)
|
|
4292
|
+
spin.blockSignals(False)
|
|
4293
|
+
except RuntimeError:
|
|
4294
|
+
pass
|
|
4295
|
+
if hasattr(self, fmax_attr):
|
|
4296
|
+
try:
|
|
4297
|
+
spin = getattr(self, fmax_attr)
|
|
4298
|
+
if spin is not None and spin is not sender_spin and spin.value() != fmax_val:
|
|
4299
|
+
spin.blockSignals(True)
|
|
4300
|
+
spin.setValue(fmax_val)
|
|
4301
|
+
spin.blockSignals(False)
|
|
4302
|
+
except RuntimeError:
|
|
4303
|
+
pass
|
|
4304
|
+
|
|
4305
|
+
# Always replot if this is a dispersion or spectrogram view
|
|
4306
|
+
# The plotting functions will handle Nyquist clamping for their specific data
|
|
3878
4307
|
if hasattr(self, 'topPlotType'):
|
|
3879
4308
|
if self.topPlotType == "spectrogram":
|
|
3880
4309
|
self._plot_target_widget = self.plotWidget
|
|
@@ -3978,10 +4407,10 @@ class MainWindow(QMainWindow):
|
|
|
3978
4407
|
|
|
3979
4408
|
# Update the trace extent indicator and replot only for top view
|
|
3980
4409
|
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
3981
|
-
self.drawTraceExtentIndicator()
|
|
3982
4410
|
# Set target to top widget for this plot
|
|
3983
4411
|
self._plot_target_widget = self.topPlotWidget
|
|
3984
4412
|
self._plot_target_viewbox = self.topViewBox
|
|
4413
|
+
self.drawTraceExtentIndicator()
|
|
3985
4414
|
self.plotDispersion()
|
|
3986
4415
|
self._plot_target_widget = None
|
|
3987
4416
|
self._plot_target_viewbox = None
|
|
@@ -4030,6 +4459,9 @@ class MainWindow(QMainWindow):
|
|
|
4030
4459
|
|
|
4031
4460
|
# Update the trace extent indicator and replot only for bottom view
|
|
4032
4461
|
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
4462
|
+
# Set target to bottom widget for this plot
|
|
4463
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4464
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4033
4465
|
self.drawTraceExtentIndicator()
|
|
4034
4466
|
self.plotDispersion()
|
|
4035
4467
|
|
|
@@ -4124,33 +4556,42 @@ class MainWindow(QMainWindow):
|
|
|
4124
4556
|
# Update the trace extent indicator and replot only for the controlled view
|
|
4125
4557
|
if controlled_view == "top":
|
|
4126
4558
|
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
4127
|
-
if not suppress_indicator_redraw:
|
|
4128
|
-
self.drawTraceExtentIndicator()
|
|
4129
4559
|
# Set target to top widget for this plot
|
|
4130
4560
|
self._plot_target_widget = self.topPlotWidget
|
|
4131
4561
|
self._plot_target_viewbox = self.topViewBox
|
|
4562
|
+
if not suppress_indicator_redraw:
|
|
4563
|
+
self.drawTraceExtentIndicator()
|
|
4132
4564
|
self.plotDispersion()
|
|
4133
4565
|
self._plot_target_widget = None
|
|
4134
4566
|
self._plot_target_viewbox = None
|
|
4135
4567
|
elif controlled_view == "bottom":
|
|
4136
4568
|
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
4569
|
+
# Set target to bottom widget for this plot
|
|
4570
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4571
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4137
4572
|
if not suppress_indicator_redraw:
|
|
4138
4573
|
self.drawTraceExtentIndicator()
|
|
4139
4574
|
self.plotDispersion()
|
|
4575
|
+
self._plot_target_widget = None
|
|
4576
|
+
self._plot_target_viewbox = None
|
|
4140
4577
|
else:
|
|
4141
4578
|
# Fallback: update both if we're not sure
|
|
4142
4579
|
if hasattr(self, 'topPlotType') and self.topPlotType == "dispersion":
|
|
4143
|
-
if not suppress_indicator_redraw:
|
|
4144
|
-
self.drawTraceExtentIndicator()
|
|
4145
4580
|
self._plot_target_widget = self.topPlotWidget
|
|
4146
4581
|
self._plot_target_viewbox = self.topViewBox
|
|
4582
|
+
if not suppress_indicator_redraw:
|
|
4583
|
+
self.drawTraceExtentIndicator()
|
|
4147
4584
|
self.plotDispersion()
|
|
4148
4585
|
self._plot_target_widget = None
|
|
4149
4586
|
self._plot_target_viewbox = None
|
|
4150
4587
|
if hasattr(self, 'bottomPlotType') and self.bottomPlotType == "dispersion":
|
|
4588
|
+
self._plot_target_widget = self.bottomPlotWidget
|
|
4589
|
+
self._plot_target_viewbox = self.bottomViewBox
|
|
4151
4590
|
if not suppress_indicator_redraw:
|
|
4152
4591
|
self.drawTraceExtentIndicator()
|
|
4153
4592
|
self.plotDispersion()
|
|
4593
|
+
self._plot_target_widget = None
|
|
4594
|
+
self._plot_target_viewbox = None
|
|
4154
4595
|
|
|
4155
4596
|
def updateDispersionWindowInfo(self):
|
|
4156
4597
|
"""Update the status bar with dispersion window center and width information"""
|
|
@@ -5749,9 +6190,16 @@ class MainWindow(QMainWindow):
|
|
|
5749
6190
|
self.spectrogram_colormap_str = 'plasma'
|
|
5750
6191
|
self.dispersion_colormap_str = 'plasma'
|
|
5751
6192
|
|
|
5752
|
-
#
|
|
5753
|
-
self.
|
|
5754
|
-
|
|
6193
|
+
# Spectrogram axis permutation (freq on x or y axis)
|
|
6194
|
+
self.spectrogram_freq_on_y = True # True: freq on y-axis, False: freq on x-axis
|
|
6195
|
+
|
|
6196
|
+
# Spectrogram-specific normalization settings (independent from dispersion)
|
|
6197
|
+
self.spectrogram_norm_per_trace = True
|
|
6198
|
+
self.spectrogram_norm_per_freq = False
|
|
6199
|
+
|
|
6200
|
+
# Dispersion-specific normalization settings (independent from spectrogram)
|
|
6201
|
+
self.dispersion_norm_per_trace = True
|
|
6202
|
+
self.dispersion_norm_per_freq = True
|
|
5755
6203
|
|
|
5756
6204
|
# Unified analytical view parameter defaults (same for top/bottom)
|
|
5757
6205
|
self.fmin = None # Default fmin (auto -> 0 for Spectrogram/Phase-Shift)
|
|
@@ -16351,11 +16799,11 @@ class MainWindow(QMainWindow):
|
|
|
16351
16799
|
|
|
16352
16800
|
# Normalize FFT data based on user selection
|
|
16353
16801
|
eps = 1e-12
|
|
16354
|
-
if getattr(self, '
|
|
16802
|
+
if getattr(self, 'spectrogram_norm_per_trace', True):
|
|
16355
16803
|
# Per-trace normalization for spectrogram
|
|
16356
16804
|
max_per_trace = np.maximum(np.max(fft_data, axis=1, keepdims=True), eps)
|
|
16357
16805
|
fft_data = fft_data / max_per_trace
|
|
16358
|
-
if getattr(self, '
|
|
16806
|
+
if getattr(self, 'spectrogram_norm_per_freq', False):
|
|
16359
16807
|
# Per-frequency normalization
|
|
16360
16808
|
max_per_freq = np.maximum(np.max(fft_data, axis=0, keepdims=True), eps)
|
|
16361
16809
|
fft_data = fft_data / max_per_freq
|
|
@@ -16382,21 +16830,43 @@ class MainWindow(QMainWindow):
|
|
|
16382
16830
|
freqs = freqs[fmask]
|
|
16383
16831
|
fft_data = fft_data[:, fmask]
|
|
16384
16832
|
|
|
16833
|
+
# Check if axes should be permuted
|
|
16834
|
+
freq_on_y = getattr(self, 'spectrogram_freq_on_y', True)
|
|
16835
|
+
|
|
16385
16836
|
# Create image item with colormap
|
|
16386
16837
|
colormap = getattr(self, 'spectrogram_colormap_str', 'plasma')
|
|
16387
|
-
|
|
16388
|
-
|
|
16389
|
-
|
|
16390
|
-
|
|
16391
|
-
|
|
16392
|
-
|
|
16838
|
+
|
|
16839
|
+
if freq_on_y:
|
|
16840
|
+
# Original: frequency on y-axis, traces on x-axis
|
|
16841
|
+
image_item = createImageItem(fft_data.T, x, freqs, colormap=colormap)
|
|
16842
|
+
# Store image data and coordinates for amplitude lookup
|
|
16843
|
+
self.spectrogramImageData = fft_data.T
|
|
16844
|
+
self.spectrogramImageX = x
|
|
16845
|
+
self.spectrogramImageFreqs = freqs
|
|
16846
|
+
|
|
16847
|
+
# Set axis labels
|
|
16848
|
+
x_label = self.x_label
|
|
16849
|
+
y_label = 'Frequency (Hz)'
|
|
16850
|
+
invert_y = True # Frequency increases downward
|
|
16851
|
+
else:
|
|
16852
|
+
# Permuted: traces on y-axis, frequency on x-axis
|
|
16853
|
+
image_item = createImageItem(fft_data, freqs, x, colormap=colormap)
|
|
16854
|
+
# Store image data and coordinates for amplitude lookup
|
|
16855
|
+
self.spectrogramImageData = fft_data
|
|
16856
|
+
self.spectrogramImageX = freqs
|
|
16857
|
+
self.spectrogramImageFreqs = x
|
|
16858
|
+
|
|
16859
|
+
# Set axis labels
|
|
16860
|
+
x_label = 'Frequency (Hz)'
|
|
16861
|
+
y_label = self.x_label
|
|
16862
|
+
invert_y = False # Traces increase downward normally
|
|
16393
16863
|
|
|
16394
16864
|
# Add image to plot
|
|
16395
16865
|
plot_widget.addItem(image_item)
|
|
16396
16866
|
|
|
16397
16867
|
# Set axis labels
|
|
16398
|
-
plot_widget.setLabel('left',
|
|
16399
|
-
plot_widget.setLabel('top',
|
|
16868
|
+
plot_widget.setLabel('left', y_label)
|
|
16869
|
+
plot_widget.setLabel('top', x_label)
|
|
16400
16870
|
plot_widget.setLabel('bottom', '')
|
|
16401
16871
|
plot_widget.showAxis('top')
|
|
16402
16872
|
plot_widget.showAxis('bottom')
|
|
@@ -16405,16 +16875,31 @@ class MainWindow(QMainWindow):
|
|
|
16405
16875
|
plot_widget.getAxis('right').setStyle(showValues=False)
|
|
16406
16876
|
plot_widget.getAxis('right').setLabel('')
|
|
16407
16877
|
|
|
16408
|
-
#
|
|
16409
|
-
if
|
|
16410
|
-
viewbox.invertY
|
|
16878
|
+
# Invert Y-axis if needed
|
|
16879
|
+
if invert_y:
|
|
16880
|
+
if not viewbox.state.get('invertY', False):
|
|
16881
|
+
viewbox.invertY(True)
|
|
16882
|
+
else:
|
|
16883
|
+
if viewbox.state.get('invertY', False):
|
|
16884
|
+
viewbox.invertY(False)
|
|
16411
16885
|
|
|
16412
16886
|
# Set explicit ranges for consistent tick label placement and prevent zooming out beyond data range
|
|
16887
|
+
if freq_on_y:
|
|
16888
|
+
x_range_min = min(x) - self.mean_dg
|
|
16889
|
+
x_range_max = max(x) + self.mean_dg
|
|
16890
|
+
y_range_min = min(freqs)
|
|
16891
|
+
y_range_max = max(freqs)
|
|
16892
|
+
else:
|
|
16893
|
+
x_range_min = min(freqs)
|
|
16894
|
+
x_range_max = max(freqs)
|
|
16895
|
+
y_range_min = min(x)
|
|
16896
|
+
y_range_max = max(x)
|
|
16897
|
+
|
|
16413
16898
|
if len(x) > 0 and len(freqs) > 0:
|
|
16414
|
-
viewbox.setLimits(xMin=
|
|
16415
|
-
|
|
16416
|
-
viewbox.setXRange(
|
|
16417
|
-
viewbox.setYRange(
|
|
16899
|
+
viewbox.setLimits(xMin=x_range_min, xMax=x_range_max,
|
|
16900
|
+
yMin=y_range_min, yMax=y_range_max)
|
|
16901
|
+
viewbox.setXRange(x_range_min, x_range_max, padding=0)
|
|
16902
|
+
viewbox.setYRange(y_range_min, y_range_max, padding=0)
|
|
16418
16903
|
else:
|
|
16419
16904
|
viewbox.setLimits(xMin=None, xMax=None, yMin=None, yMax=None)
|
|
16420
16905
|
|
|
@@ -16540,13 +17025,9 @@ class MainWindow(QMainWindow):
|
|
|
16540
17025
|
# Compute phase-shift transform
|
|
16541
17026
|
fs, vs, FV = phase_shift(data, dt, offsets, vmin, vmax, dv, fmax, fmin)
|
|
16542
17027
|
|
|
16543
|
-
# Normalize based on user selection
|
|
17028
|
+
# Normalize based on user selection (Dispersion only uses per-frequency normalization)
|
|
16544
17029
|
eps = 1e-12
|
|
16545
|
-
if getattr(self, '
|
|
16546
|
-
# Per-frequency normalization for Phase-Shift (normalize each frequency independently)
|
|
16547
|
-
max_per_freq = np.maximum(np.max(FV, axis=1, keepdims=True), eps)
|
|
16548
|
-
FV = FV / max_per_freq
|
|
16549
|
-
if getattr(self, 'norm_per_freq', False):
|
|
17030
|
+
if getattr(self, 'dispersion_norm_per_freq', False):
|
|
16550
17031
|
# Per-velocity normalization for Phase-Shift (normalize each velocity independently)
|
|
16551
17032
|
max_per_v = np.maximum(np.max(FV, axis=0, keepdims=True), eps)
|
|
16552
17033
|
FV = FV / max_per_v
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|