#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Standalone Dispersion Curve Inversion Application

PyQt window for loading .pvc files, running multi-mode surface wave inversions,
and visualizing results.
"""

import sys
import os
import numpy as np
import warnings
from scipy.interpolate import interp1d

# Set locale for consistent decimal formatting
import locale
os.environ['LC_NUMERIC'] = 'C'
try:
    locale.setlocale(locale.LC_NUMERIC, 'C')
except locale.Error:
    pass

from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
    QPushButton, QLabel, QFileDialog, QGroupBox, QFormLayout, QComboBox,
    QDoubleSpinBox, QSpinBox, QCheckBox, QMessageBox, QSplitter, QSizePolicy,
    QScrollArea, QTreeWidget, QTreeWidgetItem, QProgressBar, QTableWidget,
    QTableWidgetItem, QRadioButton, QButtonGroup, QTextEdit, QToolButton, QGridLayout, QLayout, QFrame
)
from PyQt5.QtCore import Qt, QLocale, QThread, pyqtSignal, QRect, QPoint, QSize
from PyQt5.QtGui import QFont

# Configure matplotlib
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar

# Import project modules
try:
    from .sw_utils import read_pvc_file, write_pvc_file, resample_dispersion_curve, lorentzian_error
    from .dc_inversion_manager import run_batch_inversion, run_multimode_sw_inversion
    from .dc_inversion_visualization_utils import DCInversionVisualizations
    from .core import GenericParameterDialog
except ImportError:
    from sw_utils import read_pvc_file, write_pvc_file, resample_dispersion_curve, lorentzian_error
    from dc_inversion_manager import run_batch_inversion, run_multimode_sw_inversion
    from dc_inversion_visualization_utils import DCInversionVisualizations
    from core import GenericParameterDialog

# Set default locale
QLocale.setDefault(QLocale(QLocale.English, QLocale.UnitedStates))


class FlowLayout(QLayout):
    """Layout that arranges widgets in a flow, wrapping to next line as needed"""
    def __init__(self, parent=None, margin=0, spacing=-1):
        super(FlowLayout, self).__init__(parent)
        if parent is not None:
            self.setContentsMargins(margin, margin, margin, margin)
        self.setSpacing(spacing)
        self.itemList = []

    def __del__(self):
        item = self.takeAt(0)
        while item:
            item = self.takeAt(0)

    def addItem(self, item):
        self.itemList.append(item)

    def count(self):
        return len(self.itemList)

    def itemAt(self, index):
        if 0 <= index < len(self.itemList):
            return self.itemList[index]
        return None

    def takeAt(self, index):
        if 0 <= index < len(self.itemList):
            return self.itemList.pop(index)
        return None

    def expandingDirections(self):
        return Qt.Orientations(Qt.Orientation(0))

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        height = self._doLayout(QRect(0, 0, width, 0), True)
        return height

    def setGeometry(self, rect):
        super(FlowLayout, self).setGeometry(rect)
        self._doLayout(rect, False)

    def sizeHint(self):
        return self.minimumSize()

    def minimumSize(self):
        size = QSize()
        for item in self.itemList:
            size = size.expandedTo(item.minimumSize())
        margin = self.contentsMargins().left()
        size += QSize(2 * margin, 2 * margin)
        return size

    def _doLayout(self, rect, testOnly):
        x = rect.x()
        y = rect.y()
        lineHeight = 0

        for item in self.itemList:
            wid = item.widget()
            spaceX = self.spacing() + wid.style().layoutSpacing(
                QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
            )
            spaceY = self.spacing() + wid.style().layoutSpacing(
                QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
            )
            nextX = x + item.sizeHint().width() + spaceX
            if nextX - spaceX > rect.right() and lineHeight > 0:
                x = rect.x()
                y = y + lineHeight + spaceY
                nextX = x + item.sizeHint().width() + spaceX
                lineHeight = 0

            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            x = nextX
            lineHeight = max(lineHeight, item.sizeHint().height())

        return y + lineHeight - rect.y()


class CollapsibleBox(QWidget):
    """
    A custom collapsible widget with an arrow button to expand/collapse content.
    """
    def __init__(self, title="", parent=None):
        super(CollapsibleBox, self).__init__(parent)

        self.toggle_button = QToolButton(text=title, parent=self)
        self.toggle_button.setCheckable(True)
        self.toggle_button.setChecked(True)  # Start expanded by default
        self.toggle_button.setStyleSheet("QToolButton { border: none; font-weight: bold; }")
        self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.toggle_button.setArrowType(Qt.DownArrow)
        self.toggle_button.pressed.connect(self.on_pressed)

        self.content_area = QWidget(parent=self)
        self.content_area.setVisible(True)  # Visible by default

        main_layout = QVBoxLayout(self)
        main_layout.setSpacing(0)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addWidget(self.toggle_button)
        main_layout.addWidget(self.content_area)

    def on_pressed(self):
        checked = self.toggle_button.isChecked()
        self.toggle_button.setArrowType(Qt.DownArrow if not checked else Qt.RightArrow)
        self.content_area.setVisible(not checked)

    def setContentLayout(self, layout):
        self.content_area.setLayout(layout)
        
    def setChecked(self, checked):
        self.toggle_button.setChecked(checked)
        # Manually trigger the state change logic
        self.toggle_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
        self.content_area.setVisible(checked)


class PositionProgressWidget(QWidget):
    """Progress indicator for single position inversion"""
    
    def __init__(self, xmid, parent=None):
        super().__init__(parent)
        self.xmid = xmid
        
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 2, 0, 2)
        
        # Position label
        self.label = QLabel(f"Position {xmid:.1f}m:")
        self.label.setMinimumWidth(120)
        layout.addWidget(self.label)
        
        # Progress bar
        self.progress = QProgressBar()
        self.progress.setMaximumWidth(200)
        layout.addWidget(self.progress)
        
        # Status label
        self.status_label = QLabel("Queued")
        layout.addWidget(self.status_label)
        
        layout.addStretch()
    
    def update_status(self, status, progress=0, result=None):
        """Update status: 'queued', 'running', 'completed', 'failed'"""
        self.progress.setValue(int(progress))
        
        if status == 'queued':
            self.status_label.setText("⏳ Queued")
            self.status_label.setStyleSheet("color: gray;")
        elif status == 'running':
            self.status_label.setText(f"🔄 Running... {progress}%")
            self.status_label.setStyleSheet("color: blue;")
        elif status == 'completed':
            chi2 = result.get('total_chi2', 0) if result else 0
            self.status_label.setText(f"✅ Done (χ²={chi2:.2f})")
            self.status_label.setStyleSheet("color: green; font-weight: bold;")
            self.progress.setValue(100)
        elif status == 'failed':
            error_msg = result if isinstance(result, str) else "Unknown error"
            self.status_label.setText(f"❌ Failed: {error_msg[:30]}...")
            self.status_label.setStyleSheet("color: red;")
            self.progress.setValue(0)


class BatchProgressWidget(QWidget):
    """Widget to track progress of batch inversions"""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.position_widgets = {}
        self.initUI()
    
    def initUI(self):
        layout = QVBoxLayout(self)
        
        # Overall progress
        self.overall_label = QLabel("Overall Progress:")
        layout.addWidget(self.overall_label)
        
        self.overall_progress = QProgressBar()
        layout.addWidget(self.overall_progress)
        
        # Scroll area for per-position progress
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setMaximumHeight(200)
        
        self.positions_container = QWidget()
        self.positions_layout = QVBoxLayout(self.positions_container)
        scroll.setWidget(self.positions_container)
        layout.addWidget(scroll)
    
    def initialize_positions(self, xmid_list):
        """Setup progress bars for each position"""
        # Clear existing
        for widget in self.position_widgets.values():
            widget.deleteLater()
        self.position_widgets.clear()
        
        # Create new
        for xmid in xmid_list:
            widget = PositionProgressWidget(xmid)
            self.positions_layout.addWidget(widget)
            self.position_widgets[xmid] = widget
    
    def update_position(self, xmid, status, progress=0, result=None):
        """Update individual position progress"""
        if xmid in self.position_widgets:
            self.position_widgets[xmid].update_status(status, progress, result)
    
    def update_overall(self, completed, total):
        """Update overall progress"""
        if total > 0:
            percentage = int(100 * completed / total)
            self.overall_progress.setValue(percentage)
            self.overall_label.setText(
                f"Overall Progress: {completed}/{total} positions ({percentage}%)"
            )


class InversionWorkerThread(QThread):
    """Thread for running batch inversions without blocking GUI"""
    
    progress_signal = pyqtSignal(float, int, int, str, object)
    finished_signal = pyqtSignal(dict)
    
    def __init__(self, positions_data, params, parallel=True, n_workers=None):
        super().__init__()
        self.positions_data = positions_data
        self.params = params
        self.parallel = parallel
        self.n_workers = n_workers
        self._is_cancelled = False
    
    def run(self):
        """Execute batch inversion in background"""
        try:
            results = run_batch_inversion(
                self.positions_data,
                self.params,
                progress_callback=self.emit_progress,
                parallel=self.parallel,
                n_workers=self.n_workers
            )
            self.finished_signal.emit(results)
        except Exception as e:
            import traceback
            self.finished_signal.emit({
                'error': str(e),
                'traceback': traceback.format_exc()
            })
    
    def emit_progress(self, xmid, idx, total, status, result):
        """Callback from run_batch_inversion"""
        progress = int(100 * idx / total) if total > 0 else 0
        self.progress_signal.emit(xmid, idx, total, status, result)
    
    def cancel(self):
        """Request cancellation"""
        self._is_cancelled = True
        self.terminate()


class StandaloneDispersionInversionApp(QMainWindow):
    """Main application window for dispersion curve inversion"""
    
    def __init__(self, dc_data=None, parent=None):
        super().__init__(parent)
        
        self.setWindowTitle("Dispersion Curve Inversion Tool")
        
        # Set window flags to ensure maximize button is available
        from PyQt5.QtCore import Qt
        self.setWindowFlags(Qt.Window | Qt.WindowMaximizeButtonHint | 
                           Qt.WindowMinimizeButtonHint | Qt.WindowCloseButtonHint)
        
        self.resize(1400, 800)
        self.showMaximized()
        
        # Detect standalone vs integrated mode
        self.standalone_mode = (dc_data is None and parent is None)
        
        # Data storage
        self.positions_data = {}
        self.inversion_results = {}
        self.current_position_xmid = None
        self.inversion_thread = None
        
        # Topography data
        self.topo_positions = np.array([0, 1])
        self.topo_elevations = np.array([0, 0])
        self.has_topography = False
        
        # Topography import parameters
        self.column_x = 0
        self.column_z = 1
        self.delimiter = '\t'
        self.skiprows = 0
        self.usecols = None
        self.rounding = 3
        
        # Enable drag and drop
        self.setAcceptDrops(True)
        
        # Initialize UI
        self.initUI()
        
        # Load data if provided
        if dc_data:
            self.load_data_from_pyckster(dc_data)
    
    def apply_tight_layout(self, figure):
        """Safely apply tight_layout with warning suppression"""
        try:
            with warnings.catch_warnings():
                warnings.filterwarnings('ignore', category=UserWarning, message='.*Tight layout.*')
                figure.tight_layout()
        except:
            pass
    
    def initUI(self):
        """Initialize user interface"""
        # Set locale
        locale = QLocale(QLocale.C)
        QLocale.setDefault(locale)
        
        # Central widget
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        main_layout = QHBoxLayout(central_widget)
        
        # Create splitter for left panel and right visualization
        splitter = QSplitter(Qt.Horizontal)
        splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        
        # LEFT PANEL
        left_panel = self.create_left_panel()
        splitter.addWidget(left_panel)
        
        # RIGHT PANEL (tabs)
        right_panel = self.create_right_panel()
        splitter.addWidget(right_panel)
        
        # Set initial splitter sizes
        splitter.setSizes([400, 1000])
        
        main_layout.addWidget(splitter)
    
    def importTopography(self):
        """Import topography from file with parameter dialog"""
        # First, select the file
        fname, _ = QFileDialog.getOpenFileName(
            self, 'Open Topography File', 
            filter='Topography files (*.xyz *.csv *.txt *.dat);;All files (*.*)'
        )
        
        if not fname:
            return
        
        # Show parameter dialog (like main Pyckster app)
        parameters = [
            {'label': 'X Column #', 'initial_value': self.column_x + 1, 'type': 'int'},
            {'label': 'Z Column #', 'initial_value': self.column_z + 1, 'type': 'int'},
            {'label': 'Delimiter', 'initial_value': self.delimiter, 'type': 'str'},
            {'label': 'Number of rows to skip (0 to read all lines)', 'initial_value': self.skiprows, 'type': 'int'},
            {'label': 'List of columns to import (within brackets)', 'initial_value': self.usecols, 'type': 'list'}
        ]
        
        dialog = GenericParameterDialog(
            title="Set Topography Parameters",
            parameters=parameters,
            parent=self
        )
        
        if not dialog.exec_():
            return
        
        # Get parameters from dialog
        values = dialog.getValues()
        self.column_x = values['X Column #'] - 1
        self.column_z = values['Z Column #'] - 1
        self.delimiter = values['Delimiter']
        self.skiprows = values['Number of rows to skip (0 to read all lines)']
        self.usecols = values['List of columns to import (within brackets)']
        
        if self.usecols == []:
            self.usecols = None
        else:
            # Check if 0 is in the list
            if 0 not in self.usecols:
                # Remove 1 from each column number
                self.usecols = [col - 1 for col in self.usecols]
        
        try:
            # Load the file with user-specified parameters
            data = np.loadtxt(fname, delimiter=self.delimiter, 
                            skiprows=self.skiprows, usecols=self.usecols)
            
            self.topo_positions = data[:, self.column_x]
            self.topo_elevations = np.round(data[:, self.column_z], self.rounding)
            self.has_topography = True
            
            # Update positions data with elevations if positions exist
            if self.positions_data:
                self.updatePositionElevations()
            
            # Refresh plots to show topography
            self.plotModelsTab()
            
            msg = f"Topography loaded from: {fname}\\nPositions: {len(self.topo_positions)}"
            QMessageBox.information(self, "Topography Loaded", msg)
        
        except Exception as e:
            msg = f"Error loading topography:\\n{e}"
            QMessageBox.critical(self, "Topography Load Error", msg)
    
    def resetTopography(self):
        """Reset topography to flat (elevation = 0)"""
        self.topo_positions = np.array([0, 1])
        self.topo_elevations = np.array([0, 0])
        self.has_topography = False
        
        # Remove elevations from positions data
        for xmid in self.positions_data.keys():
            if 'elevation' in self.positions_data[xmid]:
                self.positions_data[xmid]['elevation'] = 0.0
        
        # Refresh plots to remove topography
        self.plotModelsTab()
        
        QMessageBox.information(self, "Topography Reset", "Topography reset to flat (elevation = 0)")
    
    def updatePositionElevations(self):
        """Interpolate elevations at each xmid position"""
        if not self.has_topography or len(self.topo_positions) < 2:
            return
        
        # Create interpolation function
        f = interp1d(self.topo_positions, self.topo_elevations, 
                    fill_value="extrapolate", kind='linear')
        
        # Update each position
        for xmid in self.positions_data.keys():
            elevation = float(np.round(f(xmid), self.rounding))
            self.positions_data[xmid]['elevation'] = elevation
    
    def create_left_panel(self):
        """Create left control panel with scroll area"""
        # Create scroll area
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setMinimumWidth(300)
        scroll.setMaximumWidth(400)
        
        # Create panel content
        panel = QWidget()
        
        layout = QVBoxLayout(panel)
        
        # Load and topography buttons (only in standalone mode)
        if self.standalone_mode:
            load_btn = QPushButton("Load PVC Files")
            load_btn.clicked.connect(self.loadPvcFiles)
            layout.addWidget(load_btn)
            
            # Topography buttons in horizontal layout
            topo_layout = QHBoxLayout()
            topo_import_btn = QPushButton("Import Topography")
            topo_import_btn.clicked.connect(self.importTopography)
            topo_reset_btn = QPushButton("Reset Topography")
            topo_reset_btn.clicked.connect(self.resetTopography)
            topo_layout.addWidget(topo_import_btn)
            topo_layout.addWidget(topo_reset_btn)
            layout.addLayout(topo_layout)
        
        # Create splitter for resizable position tree section
        splitter = QSplitter(Qt.Vertical)
        splitter.setChildrenCollapsible(False)
        
        # Top part of splitter: Position tree with its controls
        top_widget = QWidget()
        top_layout = QVBoxLayout(top_widget)
        top_layout.setContentsMargins(0, 0, 0, 0)
        
        top_layout.addWidget(QLabel("Positions and Modes:"))
        self.position_tree = QTreeWidget()
        self.position_tree.setHeaderLabels(["Position / Mode", "N Points", "Freq Range"])
        self.position_tree.itemChanged.connect(self.onTreeItemChanged)
        self.position_tree.itemDoubleClicked.connect(self.onTreeItemDoubleClicked)
        self.position_tree.setMinimumHeight(150)
        top_layout.addWidget(self.position_tree)
        
        # Quick selection buttons
        btn_layout = QHBoxLayout()
        select_all_pos = QPushButton("Select All Positions")
        select_all_pos.clicked.connect(lambda: self.selectAllPositions(True))
        deselect_all_pos = QPushButton("Deselect All")
        deselect_all_pos.clicked.connect(lambda: self.selectAllPositions(False))
        btn_layout.addWidget(select_all_pos)
        btn_layout.addWidget(deselect_all_pos)
        top_layout.addLayout(btn_layout)
        
        splitter.addWidget(top_widget)
        
        # Bottom part of splitter: Everything else
        bottom_widget = QWidget()
        bottom_layout = QVBoxLayout(bottom_widget)
        bottom_layout.setContentsMargins(0, 0, 0, 0)
        
        # Mode selection checkboxes (dynamically created)
        bottom_layout.addWidget(QLabel("Mode Selection:"))
        self.mode_checks_widget = QWidget()
        self.mode_checks_layout = FlowLayout(self.mode_checks_widget)
        self.mode_checks_layout.setContentsMargins(0, 0, 0, 0)
        bottom_layout.addWidget(self.mode_checks_widget)
        
        # Summary label
        self.summary_label = QLabel("No data loaded")
        bottom_layout.addWidget(self.summary_label)
        
        # Error parameters section (for 2-column PVC files)
        error_params_group = self.create_error_params_group()
        bottom_layout.addWidget(error_params_group)
        
        # Resample section
        resample_group = self.create_resample_group()
        bottom_layout.addWidget(resample_group)
        
        # Inversion parameters
        params_group = self.create_parameters_group()
        bottom_layout.addWidget(params_group)
        
        # Starting model definition
        starting_model_group = self.create_starting_model_group()
        bottom_layout.addWidget(starting_model_group)
        
        # Parallel processing options
        parallel_group = self.create_parallel_group()
        bottom_layout.addWidget(parallel_group)
        
        # Run controls
        run_group = self.create_run_group()
        bottom_layout.addWidget(run_group)
        
        # Progress widget (initially hidden)
        self.progress_widget = BatchProgressWidget()
        self.progress_widget.setVisible(False)
        bottom_layout.addWidget(self.progress_widget)
        
        bottom_layout.addStretch()
        
        splitter.addWidget(bottom_widget)
        
        # Set initial splitter sizes (70% top, 30% bottom)
        splitter.setSizes([700, 300])
        
        layout.addWidget(splitter)
        
        # Set panel as scroll area widget
        scroll.setWidget(panel)
        
        return scroll
    
    def create_error_params_group(self):
        """Create error calculation parameters group for Lorentzian error computation"""
        box = CollapsibleBox("Error Parameters (Lorentzian)")
        
        layout = QFormLayout()
        
        # Trace spacing (dx)
        self.error_dx_spin = QDoubleSpinBox()
        self.error_dx_spin.setRange(0.1, 100.0)
        self.error_dx_spin.setValue(1.0)
        self.error_dx_spin.setSuffix(" m")
        self.error_dx_spin.setDecimals(2)
        self.error_dx_spin.setToolTip("Trace/geophone spacing in meters")
        layout.addRow("Trace Spacing (dx):", self.error_dx_spin)
        
        # Window size (Nx)
        self.error_nx_spin = QSpinBox()
        self.error_nx_spin.setRange(1, 1000)
        self.error_nx_spin.setValue(48)
        self.error_nx_spin.setToolTip("Number of traces in window (Nx)")
        layout.addRow("Window Size (Nx):", self.error_nx_spin)
        
        # Lorentzian parameter (a)
        self.error_a_spin = QDoubleSpinBox()
        self.error_a_spin.setRange(0.1, 2.0)
        self.error_a_spin.setValue(0.5)
        self.error_a_spin.setSingleStep(0.1)
        self.error_a_spin.setDecimals(2)
        self.error_a_spin.setToolTip("Lorentzian shape parameter (a)")
        layout.addRow("Shape Parameter (a):", self.error_a_spin)
        
        # Error scale factor
        self.error_scale_spin = QDoubleSpinBox()
        self.error_scale_spin.setRange(0.1, 10.0)
        self.error_scale_spin.setValue(1.0)
        self.error_scale_spin.setSingleStep(0.1)
        self.error_scale_spin.setDecimals(2)
        self.error_scale_spin.setToolTip("Scale factor for error bars (1.0 = 100%)")
        layout.addRow("Error Scale:", self.error_scale_spin)
        
        # Info label
        info_label = QLabel("Automatically used for 2-column PVC files.\nCan override existing errors with 'Recalculate' button.")
        info_label.setStyleSheet("color: #666; font-style: italic; font-size: 10px;")
        info_label.setWordWrap(True)
        layout.addRow(info_label)
        
        # Button to recalculate errors
        recalc_btn = QPushButton("Recalculate Errors")
        recalc_btn.setToolTip("Recalculate Lorentzian errors for ALL curves using current parameters")
        recalc_btn.clicked.connect(self.recalculateErrors)
        layout.addRow(recalc_btn)
        
        box.setContentLayout(layout)
        return box
    
    def create_resample_group(self):
        """Create resampling controls group"""
        box = CollapsibleBox("Resample Settings")
        
        layout = QFormLayout()
        
        self.domain_combo = QComboBox()
        self.domain_combo.addItems(["Frequency", "Wavelength"])
        layout.addRow("Domain:", self.domain_combo)
        
        self.min_value_spin = QDoubleSpinBox()
        self.min_value_spin.setRange(0, 10000)
        self.min_value_spin.setValue(5)
        layout.addRow("Min:", self.min_value_spin)
        
        self.max_value_spin = QDoubleSpinBox()
        self.max_value_spin.setRange(0, 10000)
        self.max_value_spin.setValue(60)
        layout.addRow("Max:", self.max_value_spin)
        
        self.n_samples_spin = QSpinBox()
        self.n_samples_spin.setRange(5, 200)
        self.n_samples_spin.setValue(30)
        layout.addRow("N Samples:", self.n_samples_spin)
        
        self.spacing_combo = QComboBox()
        self.spacing_combo.addItems(["Linear", "Logarithmic"])
        layout.addRow("Spacing:", self.spacing_combo)
        
        # Buttons in horizontal layout
        buttons_layout = QHBoxLayout()
        resample_btn = QPushButton("Resample All")
        resample_btn.clicked.connect(self.resampleSelected)
        buttons_layout.addWidget(resample_btn)
        
        revert_btn = QPushButton("Revert to Original")
        revert_btn.clicked.connect(self.revertToOriginal)
        buttons_layout.addWidget(revert_btn)
        
        layout.addRow(buttons_layout)
        
        box.setContentLayout(layout)
        return box
    
    def create_parameters_group(self):
        """Create inversion parameters group"""
        box = CollapsibleBox("Inversion Parameters")
        
        layout = QFormLayout()
        
        self.n_layers_spin = QSpinBox()
        self.n_layers_spin.setRange(3, 20)
        self.n_layers_spin.setValue(10)
        self.n_layers_spin.valueChanged.connect(self.onLayersCountChanged)
        layout.addRow("Layers:", self.n_layers_spin)
        
        self.max_depth_spin = QDoubleSpinBox()
        self.max_depth_spin.setRange(5, 200)
        self.max_depth_spin.setValue(30)
        self.max_depth_spin.setSuffix(" m")
        self.max_depth_spin.valueChanged.connect(self.onLayersCountChanged)
        layout.addRow("Max Depth:", self.max_depth_spin)
        
        self.thickness_combo = QComboBox()
        self.thickness_combo.addItems(["Equal", "Logarithmic"])
        self.thickness_combo.currentTextChanged.connect(self.onInversionParamChanged)
        layout.addRow("Thickness:", self.thickness_combo)
        
        self.nu_spin = QDoubleSpinBox()
        self.nu_spin.setRange(0.1, 0.49)
        self.nu_spin.setValue(0.25)
        self.nu_spin.setSingleStep(0.05)
        self.nu_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Poisson's ratio (ν):", self.nu_spin)
        
        self.rho_spin = QDoubleSpinBox()
        self.rho_spin.setRange(1000, 3000)
        self.rho_spin.setValue(2000)
        self.rho_spin.setSuffix(" kg/m³")
        self.rho_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Density (ρ):", self.rho_spin)
        
        self.lambda_spin = QDoubleSpinBox()
        self.lambda_spin.setRange(1, 10000)
        self.lambda_spin.setValue(100)
        self.lambda_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Lambda (λ):", self.lambda_spin)
        
        self.lambda_factor_spin = QDoubleSpinBox()
        self.lambda_factor_spin.setRange(0.1, 1.0)
        self.lambda_factor_spin.setValue(0.8)
        self.lambda_factor_spin.setSingleStep(0.1)
        self.lambda_factor_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Lambda factor:", self.lambda_factor_spin)
        
        self.max_iter_spin = QSpinBox()
        self.max_iter_spin.setRange(1, 100)
        self.max_iter_spin.setValue(20)
        self.max_iter_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Max iterations:", self.max_iter_spin)
        
        self.algorithm_combo = QComboBox()
        self.algorithm_combo.addItems(["dunkin", "fast-delta"])
        self.algorithm_combo.setCurrentText("dunkin")
        self.algorithm_combo.currentTextChanged.connect(self.onInversionParamChanged)
        layout.addRow("Algorithm:", self.algorithm_combo)
        
        box.setContentLayout(layout)
        return box
    
    def create_starting_model_group(self):
        """Create starting model definition group"""
        box = CollapsibleBox("Starting Model Definition")
        
        main_layout = QVBoxLayout()
        
        # Model type selection
        type_layout = QHBoxLayout()
        type_layout.addWidget(QLabel("Model type:"))
        self.starting_model_combo = QComboBox()
        self.starting_model_combo.addItems(["Wavelength-based", "Generic gradient", "Custom layers"])
        self.starting_model_combo.setCurrentText("Wavelength-based")
        self.starting_model_combo.currentIndexChanged.connect(self.onStartingModelTypeChanged)
        type_layout.addWidget(self.starting_model_combo)
        type_layout.addStretch()
        main_layout.addLayout(type_layout)
        
        # Container for parameters (will be swapped based on model type)
        self.starting_model_params_widget = QWidget()
        self.starting_model_params_layout = QVBoxLayout(self.starting_model_params_widget)
        self.starting_model_params_layout.setContentsMargins(0, 5, 0, 0)
        main_layout.addWidget(self.starting_model_params_widget)
        
        # Create parameter widgets for each model type
        self.wavelength_params_widget = self.create_wavelength_params()
        self.gradient_params_widget = self.create_gradient_params()
        self.custom_params_widget = self.create_custom_params()
        
        # Add all to layout (initially show wavelength-based)
        self.starting_model_params_layout.addWidget(self.wavelength_params_widget)
        self.starting_model_params_layout.addWidget(self.gradient_params_widget)
        self.starting_model_params_layout.addWidget(self.custom_params_widget)
        
        self.wavelength_params_widget.setVisible(True)
        self.gradient_params_widget.setVisible(False)
        self.custom_params_widget.setVisible(False)
        
        box.setContentLayout(main_layout)
        return box
    
    def create_wavelength_params(self):
        """Create wavelength-based parameter widgets"""
        widget = QWidget()
        layout = QFormLayout(widget)
        layout.setContentsMargins(0, 0, 0, 0)
        
        info_label = QLabel("Wavelength-based parameters:")
        info_label.setStyleSheet("font-weight: bold; font-size: 9pt;")
        layout.addRow(info_label)
        
        self.vs_vr_ratio_spin = QDoubleSpinBox()
        self.vs_vr_ratio_spin.setRange(0.5, 1.0)
        self.vs_vr_ratio_spin.setValue(0.92)
        self.vs_vr_ratio_spin.setSingleStep(0.01)
        self.vs_vr_ratio_spin.setDecimals(2)
        self.vs_vr_ratio_spin.setToolTip("Ratio VR/VS: phase velocity divided by shear wave velocity (typically 0.92 for M0)")
        self.vs_vr_ratio_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("VR/VS ratio:", self.vs_vr_ratio_spin)
        
        self.depth_factor_spin = QDoubleSpinBox()
        self.depth_factor_spin.setRange(2.0, 5.0)
        self.depth_factor_spin.setValue(3.5)
        self.depth_factor_spin.setSingleStep(0.1)
        self.depth_factor_spin.setDecimals(1)
        self.depth_factor_spin.setToolTip("Investigation depth = wavelength / depth_factor")
        self.depth_factor_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Depth factor:", self.depth_factor_spin)
        
        return widget
    
    def create_gradient_params(self):
        """Create generic gradient parameter widgets"""
        widget = QWidget()
        layout = QFormLayout(widget)
        layout.setContentsMargins(0, 0, 0, 0)
        
        info_label = QLabel("Generic gradient parameters:")
        info_label.setStyleSheet("font-weight: bold; font-size: 9pt;")
        layout.addRow(info_label)
        
        self.gradient_vs_top_spin = QDoubleSpinBox()
        self.gradient_vs_top_spin.setRange(50, 2000)
        self.gradient_vs_top_spin.setValue(200)
        self.gradient_vs_top_spin.setSuffix(" m/s")
        self.gradient_vs_top_spin.setToolTip("Vs at surface")
        self.gradient_vs_top_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Top Vs:", self.gradient_vs_top_spin)
        
        self.gradient_vs_bottom_spin = QDoubleSpinBox()
        self.gradient_vs_bottom_spin.setRange(100, 3000)
        self.gradient_vs_bottom_spin.setValue(800)
        self.gradient_vs_bottom_spin.setSuffix(" m/s")
        self.gradient_vs_bottom_spin.setToolTip("Vs at max depth")
        self.gradient_vs_bottom_spin.valueChanged.connect(self.onInversionParamChanged)
        layout.addRow("Bottom Vs:", self.gradient_vs_bottom_spin)
        
        return widget
    
    def create_custom_params(self):
        """Create custom layers parameter widgets"""
        widget = QWidget()
        layout = QVBoxLayout(widget)
        layout.setContentsMargins(0, 0, 0, 0)
        
        info_label = QLabel("Custom layers:")
        info_label.setStyleSheet("font-weight: bold; font-size: 9pt;")
        layout.addWidget(info_label)
        
        note_label = QLabel("Define properties for each layer. Number of layers is set in Inversion Parameters.")
        note_label.setStyleSheet("font-size: 8pt; color: #666;")
        note_label.setWordWrap(True)
        layout.addWidget(note_label)
        
        # Create table for layer parameters
        self.custom_layers_table = QTableWidget()
        self.custom_layers_table.setColumnCount(5)
        self.custom_layers_table.setHorizontalHeaderLabels(["Layer", "Thickness (m)", "Vp (m/s)", "Vs (m/s)", "ρ (kg/m³)"])
        self.custom_layers_table.setMaximumHeight(300)
        self.custom_layers_table.setMinimumHeight(150)
        
        # Initialize with current n_layers value
        self.updateCustomLayersTable()
        
        layout.addWidget(self.custom_layers_table)
        
        return widget
    
    def onStartingModelTypeChanged(self, index):
        """Handle starting model type change"""
        if not self.warnAboutInversionReset():
            # User cancelled, revert the change
            # Note: We can't easily revert without triggering this again, so just return
            pass
        
        model_type = self.starting_model_combo.currentText()
        
        # Hide all parameter widgets
        self.wavelength_params_widget.setVisible(False)
        self.gradient_params_widget.setVisible(False)
        self.custom_params_widget.setVisible(False)
        
        # Show selected widget
        if model_type == "Wavelength-based":
            self.wavelength_params_widget.setVisible(True)
        elif model_type == "Generic gradient":
            self.gradient_params_widget.setVisible(True)
        elif model_type == "Custom layers":
            self.custom_params_widget.setVisible(True)
            # Update table in case n_layers changed
            self.updateCustomLayersTable()
    
    def warnAboutInversionReset(self):
        """Warn user if inversion results exist and will be invalidated"""
        if not self.inversion_results:
            return True  # No results, proceed without warning
        
        reply = QMessageBox.warning(
            self, 
            "Warning: Existing Inversion Results",
            "Changing inversion parameters or starting model will invalidate all current inversion results.\n\n"
            "Do you want to continue?",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            # Clear inversion results
            self.inversion_results = {}
            return True
        else:
            return False
    
    def onInversionParamChanged(self):
        """Handle any inversion parameter change"""
        self.warnAboutInversionReset()
    
    def onLayersCountChanged(self, value):
        """Handle change in number of layers"""
        if not self.warnAboutInversionReset():
            # User cancelled, revert the change
            # Note: Since we can't easily revert the spin box value without triggering this again,
            # we just return. The warning is sufficient.
            pass
        # Update custom layers table if that tab is visible
        if hasattr(self, 'custom_layers_table'):
            self.updateCustomLayersTable()
    
    def updateCustomLayersTable(self):
        """Update custom layers table based on n_layers value"""
        if not hasattr(self, 'custom_layers_table'):
            return
        
        n_layers = self.n_layers_spin.value()
        max_depth = self.max_depth_spin.value()
        
        # Store current values if table already has data
        old_values = {}
        if self.custom_layers_table.rowCount() > 0:
            for row in range(self.custom_layers_table.rowCount()):
                layer_num = row + 1
                thickness_item = self.custom_layers_table.item(row, 1)
                vp_item = self.custom_layers_table.item(row, 2)
                vs_item = self.custom_layers_table.item(row, 3)
                rho_item = self.custom_layers_table.item(row, 4)
                
                if thickness_item and vp_item and vs_item and rho_item:
                    try:
                        old_values[layer_num] = {
                            'thickness': float(thickness_item.text()),
                            'vp': float(vp_item.text()),
                            'vs': float(vs_item.text()),
                            'rho': float(rho_item.text())
                        }
                    except ValueError:
                        pass
        
        # Set table rows
        self.custom_layers_table.setRowCount(n_layers)
        
        # Default thickness: equal layers
        default_thickness = max_depth / n_layers
        
        # Populate table
        for i in range(n_layers):
            layer_num = i + 1
            
            # Layer number (read-only)
            layer_item = QTableWidgetItem(str(layer_num))
            layer_item.setFlags(layer_item.flags() & ~Qt.ItemIsEditable)
            self.custom_layers_table.setItem(i, 0, layer_item)
            
            # Get old values or use defaults
            if layer_num in old_values:
                thickness = old_values[layer_num]['thickness']
                vp = old_values[layer_num]['vp']
                vs = old_values[layer_num]['vs']
                rho = old_values[layer_num]['rho']
            else:
                thickness = default_thickness
                # Default gradient: 200-800 m/s for Vs
                vs = 200 + (600 * i / (n_layers - 1)) if n_layers > 1 else 400
                vp = vs * 1.7  # Default Vp/Vs ratio
                rho = 2000  # Default density
            
            # Thickness
            thickness_item = QTableWidgetItem(f"{thickness:.2f}")
            self.custom_layers_table.setItem(i, 1, thickness_item)
            
            # Vp
            vp_item = QTableWidgetItem(f"{vp:.0f}")
            self.custom_layers_table.setItem(i, 2, vp_item)
            
            # Vs
            vs_item = QTableWidgetItem(f"{vs:.0f}")
            self.custom_layers_table.setItem(i, 3, vs_item)
            
            # Rho
            rho_item = QTableWidgetItem(f"{rho:.0f}")
            self.custom_layers_table.setItem(i, 4, rho_item)
        
        # Resize columns to content
        self.custom_layers_table.resizeColumnsToContents()
    
    def create_parallel_group(self):
        """Create parallel processing controls"""
        box = CollapsibleBox("Parallel Processing")
        
        layout = QFormLayout()
        
        self.parallel_check = QCheckBox("Enable parallel processing")
        self.parallel_check.setChecked(True)
        layout.addRow(self.parallel_check)
        
        self.n_workers_spin = QSpinBox()
        self.n_workers_spin.setRange(1, 16)
        import multiprocessing as mp
        self.n_workers_spin.setValue(max(1, mp.cpu_count() - 1))
        layout.addRow("Workers:", self.n_workers_spin)
        
        box.setContentLayout(layout)
        return box
    
    def create_run_group(self):
        """Create run controls"""
        box = CollapsibleBox("Run Inversion")
        
        layout = QVBoxLayout()
        
        # Single vs batch selection
        self.single_radio = QRadioButton("Current position only")
        self.batch_radio = QRadioButton("All selected positions")
        self.batch_radio.setChecked(True)
        
        radio_group = QButtonGroup()
        radio_group.addButton(self.single_radio)
        radio_group.addButton(self.batch_radio)
        
        layout.addWidget(self.single_radio)
        layout.addWidget(self.batch_radio)
        
        # Run button
        self.run_button = QPushButton("Run Inversion")
        self.run_button.clicked.connect(self.runInversion)
        self.run_button.setStyleSheet("QPushButton { font-weight: bold; padding: 10px; }")
        layout.addWidget(self.run_button)
        
        # Export button
        export_btn = QPushButton("Export Results")
        export_btn.clicked.connect(self.exportResults)
        layout.addWidget(export_btn)
        
        box.setContentLayout(layout)
        return box
    
    def create_right_panel(self):
        """Create right panel with tabs"""
        self.tab_widget = QTabWidget()
        
        # Tab 1: Overview (4 panels)
        self.overview_tab = self.create_overview_tab()
        self.tab_widget.addTab(self.overview_tab, "Overview")
        
        # Tab 2: Models (4 panels)
        self.models_tab = self.create_models_tab()
        self.tab_widget.addTab(self.models_tab, "Models")
        
        # Tab 3: Data Fit (4 panels)
        self.datafit_tab = self.create_datafit_tab()
        self.tab_widget.addTab(self.datafit_tab, "Data Fit")
        
        # Tab 4: Pseudo-sections (4 panels)
        self.pseudosections_tab = self.create_pseudosections_tab()
        self.tab_widget.addTab(self.pseudosections_tab, "Pseudo-sections")
        
        #  Results table tab
        self.results_tab = self.create_results_tab()
        self.tab_widget.addTab(self.results_tab, "Results") 
        
        # Connect tab change handler to refresh controls
        self.tab_widget.currentChanged.connect(self.onTabChanged)
        
        # Trigger initial tab setup
        self.onTabChanged(0)  # Initialize overview tab controls
        
        return self.tab_widget
    
    def onTabChanged(self, index):
        """Refresh controls when tab changes"""
        # Get panels for current tab
        panels = None
        if index == 0 and hasattr(self, 'overview_panels'):
            panels = self.overview_panels
        elif index == 1 and hasattr(self, 'models_panels'):
            panels = self.models_panels
        elif index == 2 and hasattr(self, 'datafit_panels'):
            panels = self.datafit_panels
        elif index == 3 and hasattr(self, 'pseudosections_panels'):
            panels = self.pseudosections_panels
        
        # Refresh controls for each panel in the current tab
        if panels:
            for panel in panels:
                view_name = panel.get('current_view')
                if view_name:
                    self.update_control_visibility(panel, view_name)
    
    def create_multi_panel_tab(self, titles):
        """Create a 2x2 grid of plots using splitters"""
        tab = QWidget()
        layout = QVBoxLayout(tab)
        
        # Define all available views across all tabs
        all_views = [
            "Dispersion Curves",
            "Observed vs Calculated",
            "Pseudo-section",
            "1D Models",
            "2D Models",
            "Histogram",
            "Misfit"
        ]
        
        # Create 2x2 grid using splitters
        vsplitter = QSplitter(Qt.Vertical)
        hsplitter1 = QSplitter(Qt.Horizontal)
        hsplitter2 = QSplitter(Qt.Horizontal)
        
        # Create 4 plot panels
        panels = []
        for i, title in enumerate(titles):
            container = QWidget()
            container.setMinimumSize(200, 200)
            container_layout = QVBoxLayout(container)
            container_layout.setContentsMargins(5, 5, 5, 5)
            
            # Header with dropdown selector and toolbar
            header_layout = QHBoxLayout()
            
            # View selector dropdown
            view_label = QLabel("View:")
            view_label.setStyleSheet("font-weight: bold;")
            header_layout.addWidget(view_label)
            
            view_combo = QComboBox()
            view_combo.addItems(all_views)
            view_combo.setCurrentText(title)
            view_combo.setMinimumWidth(200)
            view_combo.currentTextChanged.connect(lambda text, panel_idx=i: self.onViewChanged(panel_idx, text))
            header_layout.addWidget(view_combo)
            
            # Create figure
            fig = Figure(figsize=(5, 4))
            canvas = FigureCanvas(fig)
            canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
            toolbar = NavigationToolbar(canvas, container)
            header_layout.addStretch()
            header_layout.addWidget(toolbar)
            
            container_layout.addLayout(header_layout)
            
            # Create a placeholder for dynamic controls (will be populated later)
            controls_placeholder = QWidget()
            controls_placeholder.setMaximumHeight(0)  # Hidden by default
            controls_placeholder.setVisible(False)
            container_layout.addWidget(controls_placeholder)
            
            container_layout.addWidget(canvas)
            
            panels.append({
                'container': container, 
                'figure': fig, 
                'canvas': canvas,
                'view_combo': view_combo,
                'current_view': title,
                'controls_placeholder': controls_placeholder
            })
        
        # Add to splitters
        hsplitter1.addWidget(panels[0]['container'])
        hsplitter1.addWidget(panels[1]['container'])
        hsplitter2.addWidget(panels[2]['container'])
        hsplitter2.addWidget(panels[3]['container'])
        vsplitter.addWidget(hsplitter1)
        vsplitter.addWidget(hsplitter2)
        
        # Set equal sizes
        hsplitter1.setSizes([500, 500])
        hsplitter2.setSizes([500, 500])
        vsplitter.setSizes([500, 500])
        
        layout.addWidget(vsplitter)
        
        tab.panels = panels
        return tab
    
    def onViewChanged(self, panel_idx, view_name):
        """Handle view dropdown change - plot the selected view in the specified panel"""
        # Determine which tab owns this panel
        panel = None
        tab_name = None
        
        if hasattr(self, 'overview_panels') and panel_idx < len(self.overview_panels):
            if self.overview_panels[panel_idx].get('view_combo') and \
               self.overview_panels[panel_idx]['view_combo'].currentText() == view_name:
                panel = self.overview_panels[panel_idx]
                tab_name = 'overview'
        
        if panel is None and hasattr(self, 'models_panels') and panel_idx < len(self.models_panels):
            if self.models_panels[panel_idx].get('view_combo') and \
               self.models_panels[panel_idx]['view_combo'].currentText() == view_name:
                panel = self.models_panels[panel_idx]
                tab_name = 'models'
        
        if panel is None and hasattr(self, 'datafit_panels') and panel_idx < len(self.datafit_panels):
            if self.datafit_panels[panel_idx].get('view_combo') and \
               self.datafit_panels[panel_idx]['view_combo'].currentText() == view_name:
                panel = self.datafit_panels[panel_idx]
                tab_name = 'datafit'
        
        if panel is None and hasattr(self, 'pseudosections_panels') and panel_idx < len(self.pseudosections_panels):
            if self.pseudosections_panels[panel_idx].get('view_combo') and \
               self.pseudosections_panels[panel_idx]['view_combo'].currentText() == view_name:
                panel = self.pseudosections_panels[panel_idx]
                tab_name = 'pseudosections'
        
        if panel is None:
            return
        
        # Update the current view for this panel
        panel['current_view'] = view_name
        
        # Update control visibility for this view
        self.update_control_visibility(panel, view_name)
        
        # Clear and plot the selected view
        self.plot_single_view(panel, view_name)
    
    def update_control_visibility(self, panel, view_name):
        """Show/hide appropriate controls based on the selected view"""
        # Map views to their required control types
        view_to_controls = {
            "Dispersion Curves": "dispersion",
            "Observed vs Calculated": "obsvcalc",
            "Pseudo-section": "pseudo",
            "1D Models": "1dmodels",
            "2D Models": "2dmodels",
            "Histogram": "histogram",
        }
        
        required_control = view_to_controls.get(view_name, None)
        
        # Show the appropriate controls
        if required_control and 'controls_placeholder' in panel:
            self.set_panel_controls(panel, required_control)
        elif 'controls_placeholder' in panel:
            # Hide controls if no controls needed for this view
            placeholder = panel['controls_placeholder']
            
            # Clear any widgets from the layout
            layout = placeholder.layout()
            if layout:
                while layout.count():
                    item = layout.takeAt(0)
                    widget = item.widget()
                    if widget:
                        widget.setParent(None)
            
            # Clear the control type and hide
            panel['current_control_type'] = None
            placeholder.setVisible(False)
            placeholder.setMaximumHeight(0)
    
    def set_panel_controls(self, panel, control_type):
        """Set the appropriate control widget for a panel"""
        placeholder = panel.get('controls_placeholder')
        if not placeholder:
            return
        
        # Check if this panel already has the correct control type
        if panel.get('current_control_type') == control_type:
            # Restore values if they exist
            self._restore_panel_control_values(panel, control_type)
            return  # Already showing the right controls
        
        # Get or reuse layout
        layout = placeholder.layout()
        if layout is None:
            layout = QVBoxLayout(placeholder)
            layout.setContentsMargins(0, 0, 0, 0)
            layout.setSpacing(0)
        else:
            # Clear existing widgets from layout
            while layout.count():
                item = layout.takeAt(0)
                widget = item.widget()
                if widget:
                    widget.deleteLater()
        
        # Update the current control type
        panel['current_control_type'] = control_type
        
        # Create a new control widget for this panel
        control_widget = None
        
        if control_type == "dispersion":
            control_widget = self._create_dispersion_controls(panel)
        elif control_type == "pseudo":
            control_widget = self._create_pseudosection_controls(panel)
        elif control_type == "histogram":
            control_widget = self._create_histogram_controls(panel)
        elif control_type == "1dmodels":
            control_widget = self._create_1dmodels_controls(panel)
        elif control_type == "2dmodels":
            control_widget = self._create_2dmodels_controls(panel)
        elif control_type == "obsvcalc":
            control_widget = self._create_obsvcalc_controls(panel)
        
        if control_widget:
            # Store reference to control widget in panel
            panel['control_widget'] = control_widget
            # Add to this panel's layout
            layout.addWidget(control_widget)
            placeholder.setVisible(True)
            placeholder.setMaximumHeight(60)
            placeholder.setMinimumHeight(50)
            
            # Restore panel-specific control values
            self._restore_panel_control_values(panel, control_type)
        else:
            placeholder.setVisible(False)
            placeholder.setMaximumHeight(0)
    
    def _save_panel_control_values(self, panel, control_type):
        """Save current control values to the panel"""
        if not panel:
            return
        
        if 'control_values' not in panel:
            panel['control_values'] = {}
        
        if control_type == "pseudo":
            if 'pseudo_mode_combo' in panel:
                panel['control_values']['pseudo_mode'] = panel['pseudo_mode_combo'].currentText()
                panel['control_values']['pseudo_datatype'] = panel['pseudo_datatype_combo'].currentText()
                panel['control_values']['pseudo_yaxis'] = panel['pseudo_yaxis_combo'].currentText()
                # Velocity controls
                if 'pseudo_vel_min_spin' in panel:
                    panel['control_values']['pseudo_vel_min'] = panel['pseudo_vel_min_spin'].value()
                    panel['control_values']['pseudo_vel_max'] = panel['pseudo_vel_max_spin'].value()
                # Residual controls
                if 'pseudo_res_range_spin' in panel:
                    panel['control_values']['pseudo_res_range'] = panel['pseudo_res_range_spin'].value()
        elif control_type == "mesh":
            if 'mesh_cmap_combo' in panel:
                panel['control_values']['mesh_cmap'] = panel['mesh_cmap_combo'].currentText()
                panel['control_values']['mesh_show_mesh'] = panel['mesh_show_mesh_check'].isChecked()
                panel['control_values']['mesh_vs_min'] = panel['mesh_vs_min_spin'].value()
                panel['control_values']['mesh_vs_max'] = panel['mesh_vs_max_spin'].value()
                panel['control_values']['mesh_aspect'] = panel['mesh_aspect_spin'].value()
        elif control_type == "dispersion":
            if 'disp_show_combo' in panel:
                panel['control_values']['disp_show'] = panel['disp_show_combo'].currentText()
                panel['control_values']['disp_mode_filter'] = panel['disp_mode_filter_combo'].currentText()
                panel['control_values']['disp_colorby'] = panel['disp_colorby_combo'].currentText()
                panel['control_values']['disp_show_errors'] = panel['disp_show_errors_check'].isChecked()
                panel['control_values']['disp_xaxis'] = panel['disp_xaxis_combo'].currentText()
                panel['control_values']['disp_cmap'] = panel['disp_cmap_combo'].currentText()
        elif control_type == "histogram":
            if 'hist_bins_spin' in panel:
                panel['control_values']['hist_datatype'] = panel['hist_datatype_combo'].currentText()
                panel['control_values']['hist_bins'] = panel['hist_bins_spin'].value()
                panel['control_values']['hist_modes'] = panel['hist_modes_combo'].currentText()
                panel['control_values']['hist_split'] = panel['hist_split_check'].isChecked()
                panel['control_values']['hist_split_type'] = panel['hist_split_type_combo'].currentText()
                panel['control_values']['hist_split_step'] = panel['hist_split_step_spin'].value()
        elif control_type == "1dmodels":
            if 'models_1d_type_combo' in panel:
                panel['control_values']['models_1d_type'] = panel['models_1d_type_combo'].currentText()
                panel['control_values']['models_1d_display'] = panel['models_1d_display_combo'].currentText()
                panel['control_values']['models_1d_colorby'] = panel['models_1d_colorby_combo'].currentText()
                panel['control_values']['models_1d_plotstyle'] = panel['models_1d_plotstyle_combo'].currentText()
        elif control_type == "2dmodels":
            if 'models_2d_type_combo' in panel:
                panel['control_values']['models_2d_type'] = panel['models_2d_type_combo'].currentText()
                panel['control_values']['models_2d_show_mesh'] = panel['models_2d_show_mesh_check'].isChecked()
                panel['control_values']['models_2d_aspect'] = panel['models_2d_aspect_spin'].value()
                # Velocity controls
                if 'vel_cmap_combo' in panel:
                    panel['control_values']['vel_cmap'] = panel['vel_cmap_combo'].currentText()
                    panel['control_values']['vel_vs_min'] = panel['vel_vs_min_spin'].value()
                    panel['control_values']['vel_vs_max'] = panel['vel_vs_max_spin'].value()
                # Residual controls
                if 'res_cmap_combo' in panel:
                    panel['control_values']['res_cmap'] = panel['res_cmap_combo'].currentText()
                    panel['control_values']['res_range'] = panel['res_range_spin'].value()
        elif control_type == "obsvcalc":
            if 'obsvcalc_colorby_combo' in panel:
                panel['control_values']['obsvcalc_colorby'] = panel['obsvcalc_colorby_combo'].currentText()
                panel['control_values']['obsvcalc_cmap'] = panel['obsvcalc_cmap_combo'].currentText()
                # Residual range control
                if 'obsvcalc_res_range_spin' in panel:
                    panel['control_values']['obsvcalc_res_range'] = panel['obsvcalc_res_range_spin'].value()
    
    def _restore_panel_control_values(self, panel, control_type):
        """Restore control values from the panel"""
        if not panel or 'control_values' not in panel:
            return
        
        values = panel['control_values']
        
        if control_type == "pseudo" and 'pseudo_mode' in values:
            if 'pseudo_mode_combo' in panel:
                panel['pseudo_mode_combo'].blockSignals(True)
                panel['pseudo_datatype_combo'].blockSignals(True)
                panel['pseudo_yaxis_combo'].blockSignals(True)
                panel['pseudo_mode_combo'].setCurrentText(values['pseudo_mode'])
                panel['pseudo_datatype_combo'].setCurrentText(values['pseudo_datatype'])
                if 'pseudo_yaxis' in values:
                    panel['pseudo_yaxis_combo'].setCurrentText(values['pseudo_yaxis'])
                panel['pseudo_mode_combo'].blockSignals(False)
                panel['pseudo_datatype_combo'].blockSignals(False)
                panel['pseudo_yaxis_combo'].blockSignals(False)
                
                # Velocity controls
                if 'pseudo_vel_min' in values and 'pseudo_vel_min_spin' in panel:
                    panel['pseudo_vel_min_spin'].blockSignals(True)
                    panel['pseudo_vel_max_spin'].blockSignals(True)
                    panel['pseudo_vel_min_spin'].setValue(values['pseudo_vel_min'])
                    panel['pseudo_vel_max_spin'].setValue(values['pseudo_vel_max'])
                    panel['pseudo_vel_min_spin'].blockSignals(False)
                    panel['pseudo_vel_max_spin'].blockSignals(False)
                
                # Residual controls
                if 'pseudo_res_range' in values and 'pseudo_res_range_spin' in panel:
                    panel['pseudo_res_range_spin'].blockSignals(True)
                    panel['pseudo_res_range_spin'].setValue(values['pseudo_res_range'])
                    panel['pseudo_res_range_spin'].blockSignals(False)
                
                # Update control visibility
                is_difference = 'Diff' in values['pseudo_datatype']
                self._toggle_pseudo_controls(panel, is_difference)
        elif control_type == "mesh" and 'mesh_cmap' in values:
            if 'mesh_cmap_combo' in panel:
                panel['mesh_cmap_combo'].blockSignals(True)
                panel['mesh_show_mesh_check'].blockSignals(True)
                panel['mesh_vs_min_spin'].blockSignals(True)
                panel['mesh_vs_max_spin'].blockSignals(True)
                panel['mesh_aspect_spin'].blockSignals(True)
                
                panel['mesh_cmap_combo'].setCurrentText(values['mesh_cmap'])
                panel['mesh_show_mesh_check'].setChecked(values['mesh_show_mesh'])
                panel['mesh_vs_min_spin'].setValue(values['mesh_vs_min'])
                panel['mesh_vs_max_spin'].setValue(values['mesh_vs_max'])
                panel['mesh_aspect_spin'].setValue(values['mesh_aspect'])
                
                panel['mesh_cmap_combo'].blockSignals(False)
                panel['mesh_show_mesh_check'].blockSignals(False)
                panel['mesh_vs_min_spin'].blockSignals(False)
                panel['mesh_vs_max_spin'].blockSignals(False)
                panel['mesh_aspect_spin'].blockSignals(False)
        elif control_type == "dispersion" and 'disp_show' in values:
            if 'disp_show_combo' in panel:
                panel['disp_show_combo'].blockSignals(True)
                panel['disp_mode_filter_combo'].blockSignals(True)
                panel['disp_colorby_combo'].blockSignals(True)
                panel['disp_show_errors_check'].blockSignals(True)
                panel['disp_xaxis_combo'].blockSignals(True)
                panel['disp_cmap_combo'].blockSignals(True)
                
                panel['disp_show_combo'].setCurrentText(values['disp_show'])
                panel['disp_mode_filter_combo'].setCurrentText(values['disp_mode_filter'])
                panel['disp_colorby_combo'].setCurrentText(values['disp_colorby'])
                panel['disp_show_errors_check'].setChecked(values['disp_show_errors'])
                panel['disp_xaxis_combo'].setCurrentText(values['disp_xaxis'])
                panel['disp_cmap_combo'].setCurrentText(values['disp_cmap'])
                
                panel['disp_show_combo'].blockSignals(False)
                panel['disp_mode_filter_combo'].blockSignals(False)
                panel['disp_colorby_combo'].blockSignals(False)
                panel['disp_show_errors_check'].blockSignals(False)
                panel['disp_xaxis_combo'].blockSignals(False)
                panel['disp_cmap_combo'].blockSignals(False)
        elif control_type == "histogram" and 'hist_bins' in values:
            if 'hist_bins_spin' in panel:
                panel['hist_datatype_combo'].blockSignals(True)
                panel['hist_bins_spin'].blockSignals(True)
                panel['hist_modes_combo'].blockSignals(True)
                panel['hist_split_check'].blockSignals(True)
                panel['hist_split_type_combo'].blockSignals(True)
                panel['hist_split_step_spin'].blockSignals(True)
                
                if 'hist_datatype' in values:
                    panel['hist_datatype_combo'].setCurrentText(values['hist_datatype'])
                panel['hist_bins_spin'].setValue(values['hist_bins'])
                panel['hist_modes_combo'].setCurrentText(values['hist_modes'])
                if 'hist_split' in values:
                    panel['hist_split_check'].setChecked(values['hist_split'])
                if 'hist_split_type' in values:
                    panel['hist_split_type_combo'].setCurrentText(values['hist_split_type'])
                if 'hist_split_step' in values:
                    panel['hist_split_step_spin'].setValue(values['hist_split_step'])
                
                panel['hist_datatype_combo'].blockSignals(False)
                panel['hist_bins_spin'].blockSignals(False)
                panel['hist_modes_combo'].blockSignals(False)
                panel['hist_split_check'].blockSignals(False)
                panel['hist_split_type_combo'].blockSignals(False)
                panel['hist_split_step_spin'].blockSignals(False)
        elif control_type == "1dmodels" and 'models_1d_type' in values:
            if 'models_1d_type_combo' in panel:
                panel['models_1d_type_combo'].blockSignals(True)
                panel['models_1d_display_combo'].blockSignals(True)
                panel['models_1d_colorby_combo'].blockSignals(True)
                panel['models_1d_plotstyle_combo'].blockSignals(True)
                panel['models_1d_type_combo'].setCurrentText(values['models_1d_type'])
                panel['models_1d_display_combo'].setCurrentText(values['models_1d_display'])
                panel['models_1d_colorby_combo'].setCurrentText(values['models_1d_colorby'])
                if 'models_1d_plotstyle' in values:
                    panel['models_1d_plotstyle_combo'].setCurrentText(values['models_1d_plotstyle'])
                panel['models_1d_type_combo'].blockSignals(False)
                panel['models_1d_display_combo'].blockSignals(False)
                panel['models_1d_colorby_combo'].blockSignals(False)
                panel['models_1d_plotstyle_combo'].blockSignals(False)
        elif control_type == "2dmodels" and 'models_2d_type' in values:
            if 'models_2d_type_combo' in panel:
                panel['models_2d_type_combo'].blockSignals(True)
                panel['models_2d_show_mesh_check'].blockSignals(True)
                panel['models_2d_aspect_spin'].blockSignals(True)
                
                panel['models_2d_type_combo'].setCurrentText(values['models_2d_type'])
                panel['models_2d_show_mesh_check'].setChecked(values['models_2d_show_mesh'])
                panel['models_2d_aspect_spin'].setValue(values['models_2d_aspect'])
                
                # Velocity controls
                if 'vel_cmap' in values and 'vel_cmap_combo' in panel:
                    panel['vel_cmap_combo'].blockSignals(True)
                    panel['vel_vs_min_spin'].blockSignals(True)
                    panel['vel_vs_max_spin'].blockSignals(True)
                    
                    panel['vel_cmap_combo'].setCurrentText(values['vel_cmap'])
                    panel['vel_vs_min_spin'].setValue(values['vel_vs_min'])
                    panel['vel_vs_max_spin'].setValue(values['vel_vs_max'])
                    
                    panel['vel_cmap_combo'].blockSignals(False)
                    panel['vel_vs_min_spin'].blockSignals(False)
                    panel['vel_vs_max_spin'].blockSignals(False)
                
                # Residual controls
                if 'res_cmap' in values and 'res_cmap_combo' in panel:
                    panel['res_cmap_combo'].blockSignals(True)
                    panel['res_range_spin'].blockSignals(True)
                    
                    panel['res_cmap_combo'].setCurrentText(values['res_cmap'])
                    panel['res_range_spin'].setValue(values['res_range'])
                    
                    panel['res_cmap_combo'].blockSignals(False)
                    panel['res_range_spin'].blockSignals(False)
                
                panel['models_2d_type_combo'].blockSignals(False)
                panel['models_2d_show_mesh_check'].blockSignals(False)
                panel['models_2d_aspect_spin'].blockSignals(False)
                
                # Update control visibility based on model type
                is_difference = 'Difference' in values['models_2d_type']
                self._toggle_2d_model_controls(panel, is_difference)
        elif control_type == "obsvcalc" and 'obsvcalc_colorby' in values:
            if 'obsvcalc_colorby_combo' in panel:
                panel['obsvcalc_colorby_combo'].blockSignals(True)
                panel['obsvcalc_cmap_combo'].blockSignals(True)
                
                panel['obsvcalc_colorby_combo'].setCurrentText(values['obsvcalc_colorby'])
                panel['obsvcalc_cmap_combo'].setCurrentText(values['obsvcalc_cmap'])
                
                # Residual range control
                if 'obsvcalc_res_range' in values and 'obsvcalc_res_range_spin' in panel:
                    panel['obsvcalc_res_range_spin'].blockSignals(True)
                    panel['obsvcalc_res_range_spin'].setValue(values['obsvcalc_res_range'])
                    panel['obsvcalc_res_range_spin'].blockSignals(False)
                
                panel['obsvcalc_colorby_combo'].blockSignals(False)
                panel['obsvcalc_cmap_combo'].blockSignals(False)
                
                # Update control visibility
                is_residual = 'Residual' in values['obsvcalc_colorby']
                self._toggle_obsvcalc_controls(panel, is_residual)
    
    def plot_single_view(self, panel, view_name):
        """Plot a specific view in the given panel"""
        panel['figure'].clear()
        ax = panel['figure'].add_subplot(111)
        
        # Helper function to get control value from panel or widget
        def get_panel_value(panel, key, widget_getter, default):
            if 'control_values' in panel and key in panel['control_values']:
                return panel['control_values'][key]
            elif hasattr(self, widget_getter):
                widget = getattr(self, widget_getter)
                if hasattr(widget, 'currentText'):
                    return widget.currentText()
                elif hasattr(widget, 'isChecked'):
                    return widget.isChecked()
                elif hasattr(widget, 'value'):
                    return widget.value()
            return default
        
        # Get control values for each view type
        if view_name == "Dispersion Curves":
            # Get control values from panel
            show_all = get_panel_value(panel, 'disp_show', 'disp_show_combo', 'All') == "All"
            mode_filter = get_panel_value(panel, 'disp_mode_filter', 'disp_mode_filter_combo', 'All')
            color_by = get_panel_value(panel, 'disp_colorby', 'disp_colorby_combo', 'Mode').lower()
            show_errors = get_panel_value(panel, 'disp_show_errors', 'disp_show_errors_check', True)
            x_axis = get_panel_value(panel, 'disp_xaxis', 'disp_xaxis_combo', 'Frequency').lower()
            cmap = get_panel_value(panel, 'disp_cmap', 'disp_cmap_combo', 'viridis')
            
            DCInversionVisualizations.plot_dispersion_data(
                ax, self.positions_data,
                selected_only=not show_all,
                mode_filter=mode_filter,
                color_by=color_by,
                show_errors=show_errors,
                x_axis=x_axis,
                cmap=cmap
            )
        
        elif view_name == "2D Models":
            # Get model type (Starting/Final/Difference)
            model_type = get_panel_value(panel, 'models_2d_type', 'models_2d_type_combo', 'Starting')
            show_mesh = get_panel_value(panel, 'models_2d_show_mesh', 'models_2d_show_mesh_check', False)
            aspect = get_panel_value(panel, 'models_2d_aspect', 'models_2d_aspect_spin', 1.0)
            
            is_difference = 'Difference' in model_type
            
            if is_difference:
                # Get residual controls
                cmap = get_panel_value(panel, 'res_cmap', 'res_cmap_combo', 'RdBu_r')
                res_range = get_panel_value(panel, 'res_range', 'res_range_spin', 0.0)
                
                if res_range == 0.0:
                    vs_min = None
                    vs_max = None
                else:
                    vs_min = -res_range
                    vs_max = res_range
            else:
                # Get velocity controls
                cmap = get_panel_value(panel, 'vel_cmap', 'vel_cmap_combo', 'viridis')
                vs_min = get_panel_value(panel, 'vel_vs_min', 'vel_vs_min_spin', 0.0)
                vs_max = get_panel_value(panel, 'vel_vs_max', 'vel_vs_max_spin', 0.0)
                
                if vs_min == 0.0 and vs_max == 0.0:
                    vs_min = None
                    vs_max = None
                elif vs_min == 0.0:
                    vs_min = None
                elif vs_max == 0.0:
                    vs_max = None
            
            if model_type == "Starting":
                self.plot_starting_2d_model(ax, cmap=cmap, show_mesh=show_mesh,
                                           vmin=vs_min, vmax=vs_max, aspect=aspect)
            elif model_type == "Final":
                if self.inversion_results:
                    self.plot_2d_vs_model(ax, cmap=cmap, show_mesh=show_mesh,
                                         vmin=vs_min, vmax=vs_max, aspect=aspect)
                else:
                    ax.text(0.5, 0.5, 'Run inversion first', ha='center', va='center',
                           transform=ax.transAxes)
            elif model_type == "Difference (Absolute)":
                if self.inversion_results:
                    self.plot_2d_model_difference(ax, cmap=cmap, show_mesh=show_mesh,
                                                 vmin=vs_min, vmax=vs_max, aspect=aspect, relative=False)
                else:
                    ax.text(0.5, 0.5, 'Run inversion first', ha='center', va='center',
                           transform=ax.transAxes)
            else:  # Difference (Relative)
                if self.inversion_results:
                    self.plot_2d_model_difference(ax, cmap=cmap, show_mesh=show_mesh,
                                                 vmin=vs_min, vmax=vs_max, aspect=aspect, relative=True)
                else:
                    ax.text(0.5, 0.5, 'Run inversion first', ha='center', va='center',
                           transform=ax.transAxes)
        
        elif view_name == "Pseudo-section":
            # Get mode and data type from controls
            mode_name = get_panel_value(panel, 'pseudo_mode', 'pseudo_mode_combo', 'M0')
            data_type = get_panel_value(panel, 'pseudo_datatype', 'pseudo_datatype_combo', 'Observed')
            y_axis = get_panel_value(panel, 'pseudo_yaxis', 'pseudo_yaxis_combo', 'Frequency')
            
            is_difference = 'Diff' in data_type
            
            if is_difference:
                # Get residual controls
                res_range = get_panel_value(panel, 'pseudo_res_range', 'pseudo_res_range_spin', 0.0)
                if res_range == 0.0:
                    vmin = None
                    vmax = None
                else:
                    vmin = -res_range
                    vmax = res_range
            else:
                # Get velocity controls
                vmin = get_panel_value(panel, 'pseudo_vel_min', 'pseudo_vel_min_spin', 0.0)
                vmax = get_panel_value(panel, 'pseudo_vel_max', 'pseudo_vel_max_spin', 0.0)
                
                if vmin == 0.0 and vmax == 0.0:
                    vmin = None
                    vmax = None
                elif vmin == 0.0:
                    vmin = None
                elif vmax == 0.0:
                    vmax = None
            
            # Map data type to plotting function
            if data_type == "Difference":
                self.plot_pseudosection_difference(ax, mode_name=mode_name, relative=False, 
                                                  vmin=vmin, vmax=vmax, y_axis=y_axis)
            elif data_type == "Relative Diff (%)":
                self.plot_pseudosection_difference(ax, mode_name=mode_name, relative=True, 
                                                  vmin=vmin, vmax=vmax, y_axis=y_axis)
            else:
                # Observed or Computed
                plot_data_type = 'observed' if data_type == "Observed" else 'computed'
                self.plot_pseudosection(ax, mode_name=mode_name, data_type=plot_data_type,
                                      vmin=vmin, vmax=vmax, y_axis=y_axis)
        
        elif view_name == "Histogram":
            # Get histogram type (Phase Velocity or Residual)
            data_type = get_panel_value(panel, 'hist_datatype', 'hist_datatype_combo', 'Phase Velocity')
            bins = get_panel_value(panel, 'hist_bins', 'hist_bins_spin', 30)
            mode_filter = get_panel_value(panel, 'hist_modes', 'hist_modes_combo', 'All')
            split_by = get_panel_value(panel, 'hist_split', 'hist_split_check', False)
            split_type = get_panel_value(panel, 'hist_split_type', 'hist_split_type_combo', 'Frequency')
            split_step = get_panel_value(panel, 'hist_split_step', 'hist_split_step_spin', 5.0)
            
            if data_type == "Phase Velocity":
                if split_by:
                    self.plot_velocity_histogram_split(ax, bins=bins, mode_filter=mode_filter,
                                                       split_type=split_type, split_step=split_step)
                else:
                    self.plot_velocity_histogram(ax, bins=bins, mode_filter=mode_filter)
            elif data_type == "Residual (Absolute)":
                if split_by:
                    self.plot_residual_histogram_split(ax, bins=bins, relative=False,
                                                       split_type=split_type, split_step=split_step)
                else:
                    self.plot_residual_histogram(ax, bins=bins, relative=False)
            else:  # Residual (Relative)
                if split_by:
                    self.plot_residual_histogram_split(ax, bins=bins, relative=True,
                                                       split_type=split_type, split_step=split_step)
                else:
                    self.plot_residual_histogram(ax, bins=bins, relative=True)
        
        elif view_name == "1D Models":
            # Get model type (Starting/Final)
            model_type = get_panel_value(panel, 'models_1d_type', 'models_1d_type_combo', 'Starting')
            display_type = get_panel_value(panel, 'models_1d_display', 'models_1d_display_combo', 'All')
            color_by = get_panel_value(panel, 'models_1d_colorby', 'models_1d_colorby_combo', 'Position')
            plot_style = get_panel_value(panel, 'models_1d_plotstyle', 'models_1d_plotstyle_combo', 'Steps')
            
            if model_type == "Starting":
                self.plot_starting_1d_models(ax, show_all=(display_type == "All"), 
                                            color_by=color_by.lower(), plot_style=plot_style.lower())
            elif model_type == "Final":
                if self.inversion_results:
                    self.plot_final_1d_models(ax, show_all=(display_type == "All"),
                                             color_by=color_by.lower(), plot_style=plot_style.lower())
                else:
                    ax.text(0.5, 0.5, 'Run inversion first', ha='center', va='center',
                           transform=ax.transAxes)
        
        elif view_name == "Misfit":
            if self.inversion_results:
                self.plot_misfit_summary(ax)
            else:
                ax.text(0.5, 0.5, 'Run inversion first', ha='center', va='center',
                       transform=ax.transAxes)
        
        elif view_name == "Observed vs Calculated":
            # Get color by option
            color_by = get_panel_value(panel, 'obsvcalc_colorby', 'obsvcalc_colorby_combo', 'Observed')
            cmap = get_panel_value(panel, 'obsvcalc_cmap', 'obsvcalc_cmap_combo', 'viridis')
            res_range = get_panel_value(panel, 'obsvcalc_res_range', 'obsvcalc_res_range_spin', 0.0)
            
            if self.inversion_results:
                self.plot_obs_vs_calc_correlation(ax, color_by=color_by, cmap=cmap,
                                                 res_range=res_range)
            else:
                ax.text(0.5, 0.5, 'Run inversion first', ha='center', va='center',
                       transform=ax.transAxes)
        
        else:
            ax.text(0.5, 0.5, f'View: {view_name}\n(Not yet implemented)', 
                   ha='center', va='center', transform=ax.transAxes)
        
        self.apply_tight_layout(panel['figure'])
        panel['canvas'].draw()
    
    def create_overview_tab(self):
        """Tab 1: Overview - Dispersion curves, 2D model, Pseudo-section, Histogram"""
        titles = ["Dispersion Curves", "2D Models", 
                  "Pseudo-section", "Histogram"]
        tab = self.create_multi_panel_tab(titles)
        self.overview_panels = tab.panels
        return tab
    
    def _create_dispersion_controls(self, panel):
        """Create dispersion control widgets and return scroll area"""
        # Create controls widget
        controls_widget = QWidget()
        controls_widget.setStyleSheet("""
            QWidget {
                background: transparent;
            }
            QComboBox {
                background-color: white;
                color: black;
            }
            QComboBox QAbstractItemView {
                background-color: white;
                color: black;
            }
            QSpinBox, QDoubleSpinBox {
                background-color: white;
                color: black;
            }
            QCheckBox {
                spacing: 5px;
            }
            QCheckBox::indicator {
                width: 13px;
                height: 13px;
                border: 1px solid #999;
            }
            QCheckBox::indicator:unchecked {
                background-color: white;
            }
        """)
        controls_layout = QHBoxLayout(controls_widget)
        controls_layout.setContentsMargins(5, 5, 5, 5)
        controls_layout.setSpacing(10)
        
        # Xmid selection dropdown (Selected/All)
        xmid_label = QLabel("Xmids:")
        disp_show_combo = QComboBox()
        disp_show_combo.addItems(["All", "Selected"])
        disp_show_combo.setCurrentText("All")  # Default to All
        disp_show_combo.currentTextChanged.connect(lambda: self.onDispersionControlChanged(panel))
        
        # Mode filter dropdown (will be populated when data is loaded)
        mode_label = QLabel("Modes:")
        disp_mode_filter_combo = QComboBox()
        disp_mode_filter_combo.addItems(["All"])  # Will be updated when data loads
        disp_mode_filter_combo.setCurrentText("All")
        disp_mode_filter_combo.currentTextChanged.connect(lambda: self.onDispersionControlChanged(panel))
        
        # Color by dropdown (Mode/Position)
        colorby_label = QLabel("Color by:")
        disp_colorby_combo = QComboBox()
        disp_colorby_combo.addItems(["Mode", "Position"])
        disp_colorby_combo.setCurrentText("Mode")  # Default to Mode
        disp_colorby_combo.currentTextChanged.connect(lambda: self.onDispersionControlChanged(panel))
        
        # Show errors checkbox
        disp_show_errors_check = QCheckBox("Show errors")
        disp_show_errors_check.setChecked(True)
        disp_show_errors_check.stateChanged.connect(lambda: self.onDispersionControlChanged(panel))
        
        # X-axis dropdown (Frequency/Wavelength)
        xaxis_label = QLabel("X-axis:")
        disp_xaxis_combo = QComboBox()
        disp_xaxis_combo.addItems(["Frequency", "Wavelength"])
        disp_xaxis_combo.currentTextChanged.connect(lambda: self.onDispersionControlChanged(panel))
        
        # Colormap selection (for position coloring)
        cmap_label = QLabel("Cmap:")
        disp_cmap_combo = QComboBox()
        disp_cmap_combo.addItems(["viridis", "plasma", "inferno", "magma", "cividis", "turbo", "jet", "rainbow"])
        disp_cmap_combo.setCurrentText("viridis")  # Default colormap
        disp_cmap_combo.currentTextChanged.connect(lambda: self.onDispersionControlChanged(panel))
        
        # Add widgets to layout
        controls_layout.addWidget(xmid_label)
        controls_layout.addWidget(disp_show_combo)
        controls_layout.addWidget(mode_label)
        controls_layout.addWidget(disp_mode_filter_combo)
        controls_layout.addWidget(colorby_label)
        controls_layout.addWidget(disp_colorby_combo)
        controls_layout.addWidget(disp_show_errors_check)
        controls_layout.addWidget(xaxis_label)
        controls_layout.addWidget(disp_xaxis_combo)
        controls_layout.addWidget(cmap_label)
        controls_layout.addWidget(disp_cmap_combo)
        controls_layout.addStretch()
        
        # Store widget references in panel for later access
        panel['disp_show_combo'] = disp_show_combo
        panel['disp_mode_filter_combo'] = disp_mode_filter_combo
        panel['disp_colorby_combo'] = disp_colorby_combo
        panel['disp_show_errors_check'] = disp_show_errors_check
        panel['disp_xaxis_combo'] = disp_xaxis_combo
        panel['disp_cmap_combo'] = disp_cmap_combo
        
        # Wrap controls in scroll area for horizontal scrolling
        scroll_area = QScrollArea()
        scroll_area.setWidget(controls_widget)
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.setMaximumHeight(50)
        scroll_area.setMinimumHeight(50)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        # Set viewport to transparent
        palette = scroll_area.viewport().palette()
        palette.setColor(scroll_area.viewport().backgroundRole(), Qt.transparent)
        scroll_area.viewport().setPalette(palette)
        # Apply thin scrollbar styling
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: none;
                background: transparent;
                padding: 0px;
                margin: 0px;
            }
            QScrollArea > QWidget > QWidget {
                background: transparent;
            }
            QScrollBar:horizontal {
                height: 5px;
                background: #f0f0f0;
                border: none;
                margin: 0px;
            }
            QScrollBar::handle:horizontal {
                background: #c0c0c0;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
                width: 0px;
            }
        """)
        
        return scroll_area
    
    def _create_mesh_controls(self, panel):
        """Create mesh control widgets and return scroll area"""
        # Create controls widget
        controls_widget = QWidget()
        controls_widget.setStyleSheet("""
            QWidget {
                background: transparent;
            }
            QComboBox {
                background-color: white;
                color: black;
            }
            QComboBox QAbstractItemView {
                background-color: white;
                color: black;
            }
            QSpinBox, QDoubleSpinBox {
                background-color: white;
                color: black;
            }
            QCheckBox {
                spacing: 5px;
            }
            QCheckBox::indicator {
                width: 13px;
                height: 13px;
                border: 1px solid #999;
            }
            QCheckBox::indicator:unchecked {
                background-color: white;
            }
        """)
        controls_layout = QHBoxLayout(controls_widget)
        controls_layout.setContentsMargins(5, 5, 5, 5)
        controls_layout.setSpacing(10)
        
        # Colormap selection dropdown
        cmap_label = QLabel("Cmap:")
        mesh_cmap_combo = QComboBox()
        mesh_cmap_combo.addItems(["viridis", "plasma", "inferno", "magma", 
                                       "cividis", "turbo", "jet", "rainbow", 
                                       "seismic", "RdYlBu_r", "Spectral_r"])
        mesh_cmap_combo.setCurrentText("viridis")
        mesh_cmap_combo.currentTextChanged.connect(lambda: self.onMeshControlChanged(panel))
        
        # Show mesh checkbox
        mesh_show_mesh_check = QCheckBox("Show mesh")
        mesh_show_mesh_check.setChecked(False)  # Default off
        mesh_show_mesh_check.stateChanged.connect(lambda: self.onMeshControlChanged(panel))
        
        # Min Vs spinbox
        vs_min_label = QLabel("Min Vs:")
        mesh_vs_min_spin = QDoubleSpinBox()
        mesh_vs_min_spin.setRange(0.0, 10000.0)
        mesh_vs_min_spin.setValue(0.0)  # Will be auto-set based on data
        mesh_vs_min_spin.setSingleStep(50.0)
        mesh_vs_min_spin.setSuffix(" m/s")
        mesh_vs_min_spin.setSpecialValueText("Auto")
        mesh_vs_min_spin.valueChanged.connect(lambda: self.onMeshControlChanged(panel))
        
        # Max Vs spinbox
        vs_max_label = QLabel("Max Vs:")
        mesh_vs_max_spin = QDoubleSpinBox()
        mesh_vs_max_spin.setRange(0.0, 10000.0)
        mesh_vs_max_spin.setValue(0.0)  # Will be auto-set based on data
        mesh_vs_max_spin.setSingleStep(50.0)
        mesh_vs_max_spin.setSuffix(" m/s")
        mesh_vs_max_spin.setSpecialValueText("Auto")
        mesh_vs_max_spin.valueChanged.connect(lambda: self.onMeshControlChanged(panel))
        
        # Aspect ratio spinbox
        aspect_label = QLabel("Aspect:")
        mesh_aspect_spin = QDoubleSpinBox()
        mesh_aspect_spin.setRange(0.1, 10.0)
        mesh_aspect_spin.setValue(1.0)  # Default 1:1
        mesh_aspect_spin.setSingleStep(0.1)
        mesh_aspect_spin.setDecimals(1)
        mesh_aspect_spin.valueChanged.connect(lambda: self.onMeshControlChanged(panel))
        
        # Add widgets to layout
        controls_layout.addWidget(cmap_label)
        controls_layout.addWidget(mesh_cmap_combo)
        controls_layout.addWidget(mesh_show_mesh_check)
        controls_layout.addWidget(vs_min_label)
        controls_layout.addWidget(mesh_vs_min_spin)
        controls_layout.addWidget(vs_max_label)
        controls_layout.addWidget(mesh_vs_max_spin)
        controls_layout.addWidget(aspect_label)
        controls_layout.addWidget(mesh_aspect_spin)
        controls_layout.addStretch()
        
        # Store widget references in panel for later access
        panel['mesh_cmap_combo'] = mesh_cmap_combo
        panel['mesh_show_mesh_check'] = mesh_show_mesh_check
        panel['mesh_vs_min_spin'] = mesh_vs_min_spin
        panel['mesh_vs_max_spin'] = mesh_vs_max_spin
        panel['mesh_aspect_spin'] = mesh_aspect_spin
        
        # Wrap controls in scroll area for horizontal scrolling
        scroll_area = QScrollArea()
        scroll_area.setWidget(controls_widget)
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.setMaximumHeight(50)
        scroll_area.setMinimumHeight(50)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        # Set viewport to transparent
        palette = scroll_area.viewport().palette()
        palette.setColor(scroll_area.viewport().backgroundRole(), Qt.transparent)
        scroll_area.viewport().setPalette(palette)
        # Apply thin scrollbar styling (same as other controls)
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: none;
                background: transparent;
                padding: 0px;
                margin: 0px;
            }
            QScrollArea > QWidget > QWidget {
                background: transparent;
            }
            QScrollBar:horizontal {
                height: 5px;
                background: #f0f0f0;
                border: none;
                margin: 0px;
            }
            QScrollBar::handle:horizontal {
                background: #c0c0c0;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
                width: 0px;
            }
        """)
        
        return scroll_area
    
    def _create_pseudosection_controls(self, panel):
        """Create pseudosection control widgets and return scroll area"""
        # Create controls widget
        controls_widget = QWidget()
        controls_widget.setStyleSheet("""
            QWidget {
                background: transparent;
            }
            QComboBox {
                background-color: white;
                color: black;
            }
            QComboBox QAbstractItemView {
                background-color: white;
                color: black;
            }
            QSpinBox, QDoubleSpinBox {
                background-color: white;
                color: black;
            }
        """)
        controls_layout = QHBoxLayout(controls_widget)
        controls_layout.setContentsMargins(5, 5, 5, 5)
        controls_layout.setSpacing(10)
        
        # Mode selection dropdown
        mode_label = QLabel("Mode:")
        pseudo_mode_combo = QComboBox()
        # Will be populated when data is loaded
        pseudo_mode_combo.addItems(["M0", "M1", "M2", "M3", "M4"])
        pseudo_mode_combo.currentTextChanged.connect(lambda: self.onPseudosectionControlChanged(panel))
        
        # Data type dropdown
        datatype_label = QLabel("Data:")
        pseudo_datatype_combo = QComboBox()
        pseudo_datatype_combo.addItems(["Observed", "Computed", "Difference", "Relative Diff (%)"])
        pseudo_datatype_combo.currentTextChanged.connect(lambda: self.onPseudosectionControlChanged(panel))
        
        # Y-axis type dropdown
        yaxis_label = QLabel("Y-axis:")
        pseudo_yaxis_combo = QComboBox()
        pseudo_yaxis_combo.addItems(["Frequency", "Wavelength"])
        pseudo_yaxis_combo.currentTextChanged.connect(lambda: self.onPseudosectionControlChanged(panel))
        
        # --- Velocity controls (for Observed/Computed) ---
        vel_min_label = QLabel("Min Vr:")
        pseudo_vel_min_spin = QDoubleSpinBox()
        pseudo_vel_min_spin.setRange(0.0, 10000.0)
        pseudo_vel_min_spin.setValue(0.0)
        pseudo_vel_min_spin.setSingleStep(50.0)
        pseudo_vel_min_spin.setSuffix(" m/s")
        pseudo_vel_min_spin.setSpecialValueText("Auto")
        pseudo_vel_min_spin.valueChanged.connect(lambda: self.onPseudosectionControlChanged(panel))
        
        vel_max_label = QLabel("Max Vr:")
        pseudo_vel_max_spin = QDoubleSpinBox()
        pseudo_vel_max_spin.setRange(0.0, 10000.0)
        pseudo_vel_max_spin.setValue(0.0)
        pseudo_vel_max_spin.setSingleStep(50.0)
        pseudo_vel_max_spin.setSuffix(" m/s")
        pseudo_vel_max_spin.setSpecialValueText("Auto")
        pseudo_vel_max_spin.valueChanged.connect(lambda: self.onPseudosectionControlChanged(panel))
        
        # --- Residual controls (for Difference) ---
        res_range_label = QLabel("±Range:")
        pseudo_res_range_spin = QDoubleSpinBox()
        pseudo_res_range_spin.setRange(0.0, 10000.0)
        pseudo_res_range_spin.setValue(0.0)
        pseudo_res_range_spin.setSingleStep(50.0)
        pseudo_res_range_spin.setSuffix(" m/s")
        pseudo_res_range_spin.setSpecialValueText("Auto")
        pseudo_res_range_spin.valueChanged.connect(lambda: self.onPseudosectionControlChanged(panel))
        
        # Add widgets to layout
        controls_layout.addWidget(mode_label)
        controls_layout.addWidget(pseudo_mode_combo)
        controls_layout.addWidget(datatype_label)
        controls_layout.addWidget(pseudo_datatype_combo)
        controls_layout.addWidget(yaxis_label)
        controls_layout.addWidget(pseudo_yaxis_combo)
        # Velocity controls
        controls_layout.addWidget(vel_min_label)
        controls_layout.addWidget(pseudo_vel_min_spin)
        controls_layout.addWidget(vel_max_label)
        controls_layout.addWidget(pseudo_vel_max_spin)
        # Residual controls
        controls_layout.addWidget(res_range_label)
        controls_layout.addWidget(pseudo_res_range_spin)
        controls_layout.addStretch()
        
        # Store widget references in panel for later access
        panel['pseudo_mode_combo'] = pseudo_mode_combo
        panel['pseudo_datatype_combo'] = pseudo_datatype_combo
        panel['pseudo_yaxis_combo'] = pseudo_yaxis_combo
        # Velocity widgets
        panel['pseudo_vel_min_label'] = vel_min_label
        panel['pseudo_vel_min_spin'] = pseudo_vel_min_spin
        panel['pseudo_vel_max_label'] = vel_max_label
        panel['pseudo_vel_max_spin'] = pseudo_vel_max_spin
        # Residual widgets
        panel['pseudo_res_range_label'] = res_range_label
        panel['pseudo_res_range_spin'] = pseudo_res_range_spin
        panel['_prev_pseudo_datatype'] = 'Observed'  # Initialize tracking
        
        # Initially show velocity controls
        self._toggle_pseudo_controls(panel, is_difference=False)
        
        # Wrap controls in scroll area for horizontal scrolling
        scroll_area = QScrollArea()
        scroll_area.setWidget(controls_widget)
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.setMaximumHeight(50)
        scroll_area.setMinimumHeight(50)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        # Set viewport to transparent
        palette = scroll_area.viewport().palette()
        palette.setColor(scroll_area.viewport().backgroundRole(), Qt.transparent)
        scroll_area.viewport().setPalette(palette)
        # Apply thin scrollbar styling
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: none;
                background: transparent;
                padding: 0px;
                margin: 0px;
            }
            QScrollArea > QWidget > QWidget {
                background: transparent;
            }
            QScrollBar:horizontal {
                height: 5px;
                background: #f0f0f0;
                border: none;
                margin: 0px;
            }
            QScrollBar::handle:horizontal {
                background: #c0c0c0;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
                width: 0px;
            }
        """)
        
        return scroll_area
    
    def _create_histogram_controls(self, panel):
        """Create histogram control widgets and return scroll area"""
        # Create controls widget
        controls_widget = QWidget()
        controls_widget.setStyleSheet("""
            QWidget {
                background: transparent;
            }
            QComboBox {
                background-color: white;
                color: black;
            }
            QComboBox QAbstractItemView {
                background-color: white;
                color: black;
            }
            QSpinBox {
                background-color: white;
                color: black;
            }
            QDoubleSpinBox {
                background-color: white;
                color: black;
            }
            QCheckBox {
                spacing: 5px;
            }
        """)
        controls_layout = QHBoxLayout(controls_widget)
        controls_layout.setContentsMargins(5, 5, 5, 5)
        controls_layout.setSpacing(10)
        
        # Data type dropdown (Phase Velocity or Residual)
        datatype_label = QLabel("Data:")
        hist_datatype_combo = QComboBox()
        hist_datatype_combo.addItems(["Phase Velocity", "Residual (Absolute)", "Residual (Relative)"])
        hist_datatype_combo.currentTextChanged.connect(lambda: self.onHistogramControlChanged(panel))
        
        # Bins spinbox
        bins_label = QLabel("Bins:")
        hist_bins_spin = QSpinBox()
        hist_bins_spin.setRange(10, 100)
        hist_bins_spin.setValue(30)
        hist_bins_spin.setSingleStep(5)
        hist_bins_spin.valueChanged.connect(lambda: self.onHistogramControlChanged(panel))
        
        # Mode selection
        modes_label = QLabel("Modes:")
        hist_modes_combo = QComboBox()
        hist_modes_combo.addItems(["All", "M0 only", "M1 only", "M2 only"])
        hist_modes_combo.currentTextChanged.connect(lambda: self.onHistogramControlChanged(panel))
        
        # Split by frequency/wavelength
        split_check = QCheckBox("Split by:")
        split_check.setChecked(False)
        split_check.stateChanged.connect(lambda: self.onHistogramControlChanged(panel))
        
        split_type_combo = QComboBox()
        split_type_combo.addItems(["Frequency", "Wavelength"])
        split_type_combo.currentTextChanged.connect(lambda: self.onHistogramControlChanged(panel))
        
        split_step_label = QLabel("Step:")
        split_step_spin = QDoubleSpinBox()
        split_step_spin.setRange(1.0, 1000.0)
        split_step_spin.setValue(5.0)
        split_step_spin.setSingleStep(1.0)
        split_step_spin.setDecimals(1)
        split_step_spin.valueChanged.connect(lambda: self.onHistogramControlChanged(panel))
        
        # Add widgets to layout
        controls_layout.addWidget(datatype_label)
        controls_layout.addWidget(hist_datatype_combo)
        controls_layout.addWidget(bins_label)
        controls_layout.addWidget(hist_bins_spin)
        controls_layout.addWidget(modes_label)
        controls_layout.addWidget(hist_modes_combo)
        controls_layout.addWidget(split_check)
        controls_layout.addWidget(split_type_combo)
        controls_layout.addWidget(split_step_label)
        controls_layout.addWidget(split_step_spin)
        controls_layout.addStretch()
        
        # Store widget references in panel for later access
        panel['hist_datatype_combo'] = hist_datatype_combo
        panel['hist_bins_spin'] = hist_bins_spin
        panel['hist_modes_combo'] = hist_modes_combo
        panel['hist_split_check'] = split_check
        panel['hist_split_type_combo'] = split_type_combo
        panel['hist_split_step_spin'] = split_step_spin
        
        # Wrap controls in scroll area for horizontal scrolling
        scroll_area = QScrollArea()
        scroll_area.setWidget(controls_widget)
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.setMaximumHeight(50)
        scroll_area.setMinimumHeight(50)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        # Set viewport to transparent
        palette = scroll_area.viewport().palette()
        palette.setColor(scroll_area.viewport().backgroundRole(), Qt.transparent)
        scroll_area.viewport().setPalette(palette)
        # Apply thin scrollbar styling
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: none;
                background: transparent;
                padding: 0px;
                margin: 0px;
            }
            QScrollArea > QWidget > QWidget {
                background: transparent;
            }
            QScrollBar:horizontal {
                height: 5px;
                background: #f0f0f0;
                border: none;
                margin: 0px;
            }
            QScrollBar::handle:horizontal {
                background: #c0c0c0;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
                width: 0px;
            }
        """)
        
        return scroll_area
    
    def _create_1dmodels_controls(self, panel):
        """Create 1D models control widgets and return scroll area"""
        # Create controls widget
        controls_widget = QWidget()
        controls_widget.setStyleSheet("""
            QWidget {
                background: transparent;
            }
            QComboBox {
                background-color: white;
                color: black;
            }
            QComboBox QAbstractItemView {
                background-color: white;
                color: black;
            }
        """)
        controls_layout = QHBoxLayout(controls_widget)
        controls_layout.setContentsMargins(5, 5, 5, 5)
        controls_layout.setSpacing(10)
        
        # Model type dropdown (Starting/Final)
        modeltype_label = QLabel("Model:")
        models_1d_type_combo = QComboBox()
        models_1d_type_combo.addItems(["Starting", "Final"])
        models_1d_type_combo.currentTextChanged.connect(lambda: self.on1DModelsControlChanged(panel))
        
        # Display mode dropdown (All/Selected)
        display_label = QLabel("Show:")
        models_1d_display_combo = QComboBox()
        models_1d_display_combo.addItems(["All", "Selected"])
        models_1d_display_combo.currentTextChanged.connect(lambda: self.on1DModelsControlChanged(panel))
        
        # Color by dropdown
        colorby_label = QLabel("Color by:")
        models_1d_colorby_combo = QComboBox()
        models_1d_colorby_combo.addItems(["Position", "Misfit"])
        models_1d_colorby_combo.currentTextChanged.connect(lambda: self.on1DModelsControlChanged(panel))
        
        # Plot style dropdown
        plotstyle_label = QLabel("Style:")
        models_1d_plotstyle_combo = QComboBox()
        models_1d_plotstyle_combo.addItems(["Steps", "Curves"])
        models_1d_plotstyle_combo.currentTextChanged.connect(lambda: self.on1DModelsControlChanged(panel))
        
        # Add widgets to layout
        controls_layout.addWidget(modeltype_label)
        controls_layout.addWidget(models_1d_type_combo)
        controls_layout.addWidget(display_label)
        controls_layout.addWidget(models_1d_display_combo)
        controls_layout.addWidget(colorby_label)
        controls_layout.addWidget(models_1d_colorby_combo)
        controls_layout.addWidget(plotstyle_label)
        controls_layout.addWidget(models_1d_plotstyle_combo)
        controls_layout.addStretch()
        
        # Store widget references in panel for later access
        panel['models_1d_type_combo'] = models_1d_type_combo
        panel['models_1d_display_combo'] = models_1d_display_combo
        panel['models_1d_colorby_combo'] = models_1d_colorby_combo
        panel['models_1d_plotstyle_combo'] = models_1d_plotstyle_combo
        
        # Wrap controls in scroll area
        scroll_area = QScrollArea()
        scroll_area.setWidget(controls_widget)
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.setMaximumHeight(50)
        scroll_area.setMinimumHeight(50)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        palette = scroll_area.viewport().palette()
        palette.setColor(scroll_area.viewport().backgroundRole(), Qt.transparent)
        scroll_area.viewport().setPalette(palette)
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: none;
                background: transparent;
                padding: 0px;
                margin: 0px;
            }
            QScrollArea > QWidget > QWidget {
                background: transparent;
            }
            QScrollBar:horizontal {
                height: 5px;
                background: #f0f0f0;
                border: none;
                margin: 0px;
            }
            QScrollBar::handle:horizontal {
                background: #c0c0c0;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
                width: 0px;
            }
        """)
        
        return scroll_area
    
    def _create_2dmodels_controls(self, panel):
        """Create 2D models control widgets and return scroll area"""
        # Create controls widget
        controls_widget = QWidget()
        controls_widget.setStyleSheet("""
            QWidget {
                background: transparent;
            }
            QComboBox {
                background-color: white;
                color: black;
            }
            QComboBox QAbstractItemView {
                background-color: white;
                color: black;
            }
            QSpinBox, QDoubleSpinBox {
                background-color: white;
                color: black;
            }
            QCheckBox {
                spacing: 5px;
            }
            QCheckBox::indicator {
                width: 13px;
                height: 13px;
                border: 1px solid #999;
            }
            QCheckBox::indicator:unchecked {
                background-color: white;
            }
        """)
        controls_layout = QHBoxLayout(controls_widget)
        controls_layout.setContentsMargins(5, 5, 5, 5)
        controls_layout.setSpacing(10)
        
        # Model type dropdown (Starting/Final/Difference)
        modeltype_label = QLabel("Model:")
        models_2d_type_combo = QComboBox()
        models_2d_type_combo.addItems(["Starting", "Final", "Difference (Absolute)", "Difference (Relative)"])
        models_2d_type_combo.currentTextChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        # --- Velocity controls (for Starting/Final) ---
        vel_cmap_label = QLabel("Cmap:")
        vel_cmap_combo = QComboBox()
        vel_cmap_combo.addItems(["viridis", "plasma", "inferno", "magma", 
                                 "cividis", "turbo", "jet", "rainbow"])
        vel_cmap_combo.setCurrentText("viridis")
        vel_cmap_combo.currentTextChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        vel_min_label = QLabel("Min Vs:")
        vel_vs_min_spin = QDoubleSpinBox()
        vel_vs_min_spin.setRange(0.0, 10000.0)
        vel_vs_min_spin.setValue(0.0)
        vel_vs_min_spin.setSingleStep(50.0)
        vel_vs_min_spin.setSuffix(" m/s")
        vel_vs_min_spin.setSpecialValueText("Auto")
        vel_vs_min_spin.valueChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        vel_max_label = QLabel("Max Vs:")
        vel_vs_max_spin = QDoubleSpinBox()
        vel_vs_max_spin.setRange(0.0, 10000.0)
        vel_vs_max_spin.setValue(0.0)
        vel_vs_max_spin.setSingleStep(50.0)
        vel_vs_max_spin.setSuffix(" m/s")
        vel_vs_max_spin.setSpecialValueText("Auto")
        vel_vs_max_spin.valueChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        # --- Residual controls (for Difference) ---
        res_cmap_label = QLabel("Cmap:")
        res_cmap_combo = QComboBox()
        res_cmap_combo.addItems(["RdBu_r", "seismic", "coolwarm", "RdYlBu_r", "Spectral_r", "bwr"])
        res_cmap_combo.setCurrentText("RdBu_r")
        res_cmap_combo.currentTextChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        res_range_label = QLabel("±Range:")
        res_range_spin = QDoubleSpinBox()
        res_range_spin.setRange(0.0, 10000.0)
        res_range_spin.setValue(0.0)
        res_range_spin.setSingleStep(50.0)
        res_range_spin.setSuffix(" m/s")
        res_range_spin.setSpecialValueText("Auto")
        res_range_spin.valueChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        # --- Common controls ---
        models_2d_show_mesh_check = QCheckBox("Show mesh")
        models_2d_show_mesh_check.setChecked(False)
        models_2d_show_mesh_check.stateChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        aspect_label = QLabel("Aspect:")
        models_2d_aspect_spin = QDoubleSpinBox()
        models_2d_aspect_spin.setRange(0.1, 10.0)
        models_2d_aspect_spin.setValue(1.0)
        models_2d_aspect_spin.setSingleStep(0.1)
        models_2d_aspect_spin.setDecimals(1)
        models_2d_aspect_spin.valueChanged.connect(lambda: self.on2DModelsControlChanged(panel))
        
        # Add widgets to layout
        controls_layout.addWidget(modeltype_label)
        controls_layout.addWidget(models_2d_type_combo)
        # Velocity controls
        controls_layout.addWidget(vel_cmap_label)
        controls_layout.addWidget(vel_cmap_combo)
        controls_layout.addWidget(vel_min_label)
        controls_layout.addWidget(vel_vs_min_spin)
        controls_layout.addWidget(vel_max_label)
        controls_layout.addWidget(vel_vs_max_spin)
        # Residual controls
        controls_layout.addWidget(res_cmap_label)
        controls_layout.addWidget(res_cmap_combo)
        controls_layout.addWidget(res_range_label)
        controls_layout.addWidget(res_range_spin)
        # Common controls
        controls_layout.addWidget(models_2d_show_mesh_check)
        controls_layout.addWidget(aspect_label)
        controls_layout.addWidget(models_2d_aspect_spin)
        controls_layout.addStretch()
        
        # Store widget references in panel for later access
        panel['models_2d_type_combo'] = models_2d_type_combo
        # Velocity widgets
        panel['vel_cmap_label'] = vel_cmap_label
        panel['vel_cmap_combo'] = vel_cmap_combo
        panel['vel_min_label'] = vel_min_label
        panel['vel_vs_min_spin'] = vel_vs_min_spin
        panel['vel_max_label'] = vel_max_label
        panel['vel_vs_max_spin'] = vel_vs_max_spin
        # Residual widgets
        panel['res_cmap_label'] = res_cmap_label
        panel['res_cmap_combo'] = res_cmap_combo
        panel['res_range_label'] = res_range_label
        panel['res_range_spin'] = res_range_spin
        # Common widgets
        panel['models_2d_show_mesh_check'] = models_2d_show_mesh_check
        panel['models_2d_aspect_spin'] = models_2d_aspect_spin
        panel['_prev_model_type'] = 'Starting'  # Initialize model type tracking
        
        # Initially show velocity controls
        self._toggle_2d_model_controls(panel, is_difference=False)
        
        # Wrap controls in scroll area
        scroll_area = QScrollArea()
        scroll_area.setWidget(controls_widget)
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.setMaximumHeight(50)
        scroll_area.setMinimumHeight(50)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        palette = scroll_area.viewport().palette()
        palette.setColor(scroll_area.viewport().backgroundRole(), Qt.transparent)
        scroll_area.viewport().setPalette(palette)
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: none;
                background: transparent;
                padding: 0px;
                margin: 0px;
            }
            QScrollArea > QWidget > QWidget {
                background: transparent;
            }
            QScrollBar:horizontal {
                height: 5px;
                background: #f0f0f0;
                border: none;
                margin: 0px;
            }
            QScrollBar::handle:horizontal {
                background: #c0c0c0;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
                width: 0px;
            }
        """)
        
        return scroll_area
    
    def _create_obsvcalc_controls(self, panel):
        """Create observed vs calculated control widgets and return scroll area"""
        # Create controls widget
        controls_widget = QWidget()
        controls_widget.setStyleSheet("""
            QWidget {
                background: transparent;
            }
            QComboBox {
                background-color: white;
                color: black;
            }
            QComboBox QAbstractItemView {
                background-color: white;
                color: black;
            }
        """)
        controls_layout = QHBoxLayout(controls_widget)
        controls_layout.setContentsMargins(5, 5, 5, 5)
        controls_layout.setSpacing(10)
        
        # Color by dropdown (Observed, Calculated, Residual)
        colorby_label = QLabel("Color by:")
        obsvcalc_colorby_combo = QComboBox()
        obsvcalc_colorby_combo.addItems(["Observed", "Calculated", "Residual (Absolute)", "Residual (Relative)"])
        obsvcalc_colorby_combo.setCurrentText("Residual (Absolute)")
        obsvcalc_colorby_combo.currentTextChanged.connect(lambda: self.onObsVsCalcControlChanged(panel))
        
        # Colormap selection
        cmap_label = QLabel("Cmap:")
        obsvcalc_cmap_combo = QComboBox()
        obsvcalc_cmap_combo.addItems(["viridis", "plasma", "inferno", "magma", 
                                      "cividis", "turbo", "jet", "rainbow", 
                                      "seismic", "RdBu_r"])
        obsvcalc_cmap_combo.setCurrentText("RdBu_r")
        obsvcalc_cmap_combo.currentTextChanged.connect(lambda: self.onObsVsCalcControlChanged(panel))
        
        # Residual range control (only visible when showing residuals)
        res_range_label = QLabel("±Range:")
        obsvcalc_res_range_spin = QDoubleSpinBox()
        obsvcalc_res_range_spin.setRange(0.0, 10000.0)
        obsvcalc_res_range_spin.setValue(0.0)
        obsvcalc_res_range_spin.setSingleStep(50.0)
        obsvcalc_res_range_spin.setSuffix(" m/s")
        obsvcalc_res_range_spin.setSpecialValueText("Auto")
        obsvcalc_res_range_spin.valueChanged.connect(lambda: self.onObsVsCalcControlChanged(panel))
        
        # Add widgets to layout
        controls_layout.addWidget(colorby_label)
        controls_layout.addWidget(obsvcalc_colorby_combo)
        controls_layout.addWidget(cmap_label)
        controls_layout.addWidget(obsvcalc_cmap_combo)
        controls_layout.addWidget(res_range_label)
        controls_layout.addWidget(obsvcalc_res_range_spin)
        controls_layout.addStretch()
        
        # Store widget references in panel for later access
        panel['obsvcalc_colorby_combo'] = obsvcalc_colorby_combo
        panel['obsvcalc_cmap_combo'] = obsvcalc_cmap_combo
        panel['obsvcalc_res_range_label'] = res_range_label
        panel['obsvcalc_res_range_spin'] = obsvcalc_res_range_spin
        
        # Wrap controls in scroll area
        scroll_area = QScrollArea()
        scroll_area.setWidget(controls_widget)
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        scroll_area.setMaximumHeight(50)
        scroll_area.setMinimumHeight(50)
        scroll_area.setFrameShape(QFrame.NoFrame)
        scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        palette = scroll_area.viewport().palette()
        palette.setColor(scroll_area.viewport().backgroundRole(), Qt.transparent)
        scroll_area.viewport().setPalette(palette)
        scroll_area.setStyleSheet("""
            QScrollArea {
                border: none;
                background: transparent;
                padding: 0px;
                margin: 0px;
            }
            QScrollArea > QWidget > QWidget {
                background: transparent;
            }
            QScrollBar:horizontal {
                height: 5px;
                background: #f0f0f0;
                border: none;
                margin: 0px;
            }
            QScrollBar::handle:horizontal {
                background: #c0c0c0;
                border-radius: 4px;
                min-width: 20px;
            }
            QScrollBar::handle:horizontal:hover {
                background: #a0a0a0;
            }
            QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
                width: 0px;
            }
        """)
        
        # Initialize visibility based on default color by (Residual Absolute)
        self._toggle_obsvcalc_controls(panel, True)
        
        return scroll_area
    
    def onDispersionControlChanged(self, panel):
        """Update the panel with dispersion controls"""
        # Save current control values to this panel
        self._save_panel_control_values(panel, 'dispersion')
        # Redraw only this panel
        view_name = panel.get('current_view')
        if view_name:
            self.plot_single_view(panel, view_name)
    
    def onMeshControlChanged(self, panel):
        """Update the panel with mesh controls"""
        # Save current control values to this panel
        self._save_panel_control_values(panel, 'mesh')
        # Redraw only this panel
        view_name = panel.get('current_view')
        if view_name:
            self.plot_single_view(panel, view_name)
    
    def _toggle_pseudo_controls(self, panel, is_difference):
        """Show/hide appropriate controls based on data type"""
        # Velocity controls
        vel_widgets = ['pseudo_vel_min_label', 'pseudo_vel_min_spin', 
                      'pseudo_vel_max_label', 'pseudo_vel_max_spin']
        # Residual controls
        res_widgets = ['pseudo_res_range_label', 'pseudo_res_range_spin']
        
        for widget_name in vel_widgets:
            widget = panel.get(widget_name)
            if widget:
                widget.setVisible(not is_difference)
        
        for widget_name in res_widgets:
            widget = panel.get(widget_name)
            if widget:
                widget.setVisible(is_difference)
        
        # Update suffix based on data type
        if is_difference:
            datatype_combo = panel.get('pseudo_datatype_combo')
            if datatype_combo:
                current_datatype = datatype_combo.currentText()
                res_range_spin = panel.get('pseudo_res_range_spin')
                if res_range_spin:
                    if 'Relative' in current_datatype:
                        res_range_spin.setSuffix(' %')
                    else:
                        res_range_spin.setSuffix(' m/s')
    
    def onPseudosectionControlChanged(self, panel):
        """Update the panel with pseudo-section controls"""
        # Toggle controls visibility when switching between data and residual views
        datatype_combo = panel.get('pseudo_datatype_combo')
        
        if datatype_combo:
            current_datatype = datatype_combo.currentText()
            previous_datatype = panel.get('_prev_pseudo_datatype', 'Observed')
            
            # Check if we're switching between data and residual views
            is_difference_now = 'Diff' in current_datatype
            was_difference_before = 'Diff' in previous_datatype
            
            if is_difference_now != was_difference_before:
                # Show/hide appropriate controls
                self._toggle_pseudo_controls(panel, is_difference_now)
            
            # Store current type for next comparison
            panel['_prev_pseudo_datatype'] = current_datatype
        
        # Save current control values to this panel
        self._save_panel_control_values(panel, 'pseudo')
        # Redraw only this panel
        view_name = panel.get('current_view')
        if view_name:
            self.plot_single_view(panel, view_name)
    
    def onHistogramControlChanged(self, panel):
        """Update the panel with histogram controls"""
        # Save current control values to this panel
        self._save_panel_control_values(panel, 'histogram')
        # Redraw only this panel
        view_name = panel.get('current_view')
        if view_name:
            self.plot_single_view(panel, view_name)
    
    def on1DModelsControlChanged(self, panel):
        """Update the panel with 1D models controls"""
        # Save current control values to this panel
        self._save_panel_control_values(panel, '1dmodels')
        # Redraw only this panel
        view_name = panel.get('current_view')
        if view_name:
            self.plot_single_view(panel, view_name)
    
    def _toggle_2d_model_controls(self, panel, is_difference):
        """Show/hide appropriate controls based on model type"""
        # Velocity controls
        vel_widgets = ['vel_cmap_label', 'vel_cmap_combo', 'vel_min_label', 
                      'vel_vs_min_spin', 'vel_max_label', 'vel_vs_max_spin']
        # Residual controls
        res_widgets = ['res_cmap_label', 'res_cmap_combo', 'res_range_label', 'res_range_spin']
        
        for widget_name in vel_widgets:
            widget = panel.get(widget_name)
            if widget:
                widget.setVisible(not is_difference)
        
        for widget_name in res_widgets:
            widget = panel.get(widget_name)
            if widget:
                widget.setVisible(is_difference)
        
        # Update suffix based on model type
        if is_difference:
            model_type_combo = panel.get('models_2d_type_combo')
            if model_type_combo:
                current_model_type = model_type_combo.currentText()
                res_range_spin = panel.get('res_range_spin')
                if res_range_spin:
                    if 'Relative' in current_model_type:
                        res_range_spin.setSuffix(' %')
                    else:
                        res_range_spin.setSuffix(' m/s')
    
    def on2DModelsControlChanged(self, panel):
        """Update the panel with 2D models controls"""
        # Toggle controls visibility when switching between data and residual views
        model_type_combo = panel.get('models_2d_type_combo')
        
        if model_type_combo:
            current_model_type = model_type_combo.currentText()
            previous_model_type = panel.get('_prev_model_type', 'Starting')
            
            # Check if we're switching between data and residual views
            is_difference_now = 'Difference' in current_model_type
            was_difference_before = 'Difference' in previous_model_type
            
            if is_difference_now != was_difference_before:
                # Show/hide appropriate controls
                self._toggle_2d_model_controls(panel, is_difference_now)
            
            # Store current type for next comparison
            panel['_prev_model_type'] = current_model_type
        
        # Save current control values to this panel
        self._save_panel_control_values(panel, '2dmodels')
        # Redraw only this panel
        view_name = panel.get('current_view')
        if view_name:
            self.plot_single_view(panel, view_name)
    
    def _toggle_obsvcalc_controls(self, panel, is_residual):
        """Show/hide residual range controls based on color by selection"""
        res_widgets = ['obsvcalc_res_range_label', 'obsvcalc_res_range_spin']
        
        for widget_name in res_widgets:
            widget = panel.get(widget_name)
            if widget:
                widget.setVisible(is_residual)
        
        # Update suffix based on color by selection
        if is_residual:
            colorby_combo = panel.get('obsvcalc_colorby_combo')
            if colorby_combo:
                current_colorby = colorby_combo.currentText()
                res_range_spin = panel.get('obsvcalc_res_range_spin')
                if res_range_spin:
                    if 'Relative' in current_colorby:
                        res_range_spin.setSuffix(' %')
                    else:
                        res_range_spin.setSuffix(' m/s')
    
    def onObsVsCalcControlChanged(self, panel):
        """Update the panel with observed vs calculated controls"""
        # Toggle controls visibility when switching between data and residual views
        colorby_combo = panel.get('obsvcalc_colorby_combo')
        
        if colorby_combo:
            current_colorby = colorby_combo.currentText()
            previous_colorby = panel.get('_prev_obsvcalc_colorby', 'Observed')
            
            # Check if we're switching between data and residual views
            is_residual_now = 'Residual' in current_colorby
            was_residual_before = 'Residual' in previous_colorby
            
            if is_residual_now != was_residual_before:
                # Show/hide appropriate controls
                self._toggle_obsvcalc_controls(panel, is_residual_now)
            
            # Store current type for next comparison
            panel['_prev_obsvcalc_colorby'] = current_colorby
        
        # Save current control values to this panel
        self._save_panel_control_values(panel, 'obsvcalc')
        # Redraw only this panel
        view_name = panel.get('current_view')
        if view_name:
            self.plot_single_view(panel, view_name)
    
    def create_models_tab(self):
        """Tab 2: Models - 1D and 2D models with toggles"""
        titles = ["1D Models", "2D Models",
                  "1D Models", "2D Models"]
        tab = self.create_multi_panel_tab(titles)
        self.models_panels = tab.panels
        return tab
    
    def create_datafit_tab(self):
        """Tab 3: Data Fit - Dispersion curves, Obs vs Calc, Pseudo-section, Histogram"""
        titles = ["Dispersion Curves", "Observed vs Calculated",
                  "Pseudo-section", "Histogram"]
        tab = self.create_multi_panel_tab(titles)
        self.datafit_panels = tab.panels
        return tab
    
    def create_pseudosections_tab(self):
        """Tab 4: Pseudo-sections - Multiple pseudo-section views for comparison"""
        titles = ["Pseudo-section", "Pseudo-section",
                  "Pseudo-section", "Pseudo-section"]
        tab = self.create_multi_panel_tab(titles)
        self.pseudosections_panels = tab.panels
        return tab
    

    
    def create_results_tab(self):
        """Create batch results tab"""
        widget = QWidget()
        layout = QVBoxLayout(widget)
        
        # Summary table
        self.results_table = QTableWidget()
        self.results_table.setColumnCount(2)
        self.results_table.setHorizontalHeaderLabels(["Parameter", "Value"])
        layout.addWidget(self.results_table)
        
        # Batch comparison plot
        self.results_figure = Figure(figsize=(12, 4))
        self.results_canvas = FigureCanvas(self.results_figure)
        toolbar = NavigationToolbar(self.results_canvas, widget)
        
        layout.addWidget(toolbar)
        layout.addWidget(self.results_canvas)
        
        return widget
    
    def loadPvcFiles(self):
        """Load PVC files from file dialog"""
        file_paths, _ = QFileDialog.getOpenFileNames(
            self, "Select PVC Files", "", "PVC Files (*.pvc);;All Files (*)"
        )
        
        if not file_paths:
            return
        
        self.loadPvcFilesFromPaths(file_paths)
    
    def loadPvcFilesFromPaths(self, file_paths):
        """Load PVC files from given paths"""
        for file_path in file_paths:
            try:
                pvc_data = read_pvc_file(file_path)
                
                xmid = pvc_data['xmid']
                if xmid is None:
                    xmid = len(self.positions_data)  # Use index if no xmid
                
                mode = pvc_data['mode']
                
                # Calculate Lorentzian errors if not present in PVC file (2-column legacy format)
                errors_computed = False
                if pvc_data['errors'] is None:
                    errors_array = self.calculateLorentzianErrors(
                        pvc_data['velocities'], 
                        pvc_data['frequencies']
                    )
                    pvc_data['errors'] = errors_array.tolist()
                    errors_computed = True
                
                # Initialize position if needed
                if xmid not in self.positions_data:
                    self.positions_data[xmid] = {
                        'xmid': xmid,
                        'modes': {},
                        'selected_for_inversion': True
                    }
                
                # Add mode
                self.positions_data[xmid]['modes'][mode] = {
                    'filename': pvc_data['filename'],
                    'original': {
                        'frequencies': pvc_data['frequencies'],
                        'velocities': pvc_data['velocities'],
                        'errors': pvc_data['errors']
                    },
                    'errors_computed': errors_computed,  # Track if errors were computed
                    'selected_for_inversion': True
                }
                
            except Exception as e:
                QMessageBox.warning(self, "Load Error", 
                    f"Failed to load {file_path}:\n{str(e)}")
        
        self.updatePositionTree()
        self.updateModeButtons()
        self.updateModeFilterDropdown()
        self.updateSummary()
        # Plot overview tab to show loaded data
        self.plotOverviewTab()
    
    def load_data_from_pyckster(self, dc_data):
        """Load data passed from main Pyckster app"""
        self.positions_data = dc_data
        
        # Extract elevation data if available
        elevations_found = False
        for xmid, pos_data in self.positions_data.items():
            if 'elevation' in pos_data:
                elevations_found = True
                break
        
        if elevations_found:
            # Build topography arrays from positions with elevations
            xmids_sorted = sorted(self.positions_data.keys())
            self.topo_positions = np.array(xmids_sorted)
            self.topo_elevations = np.array([
                self.positions_data[x].get('elevation', 0.0) 
                for x in xmids_sorted
            ])
            self.has_topography = True
            print(f"Loaded topography from parent: {len(xmids_sorted)} positions, "
                  f"elevation range: {self.topo_elevations.min():.1f} to {self.topo_elevations.max():.1f} m")
        
        self.updatePositionTree()
        self.updateModeButtons()
        self.updateModeFilterDropdown()
        self.updateSummary()
        # Plot overview tab to show loaded data
        self.plotOverviewTab()
    
    def calculateLorentzianErrors(self, velocities, frequencies):
        """Calculate Lorentzian errors using current UI parameters
        
        Args:
            velocities: array-like of velocities (m/s)
            frequencies: array-like of frequencies (Hz)
            
        Returns:
            numpy array of errors (m/s)
        """
        import numpy as np
        
        velocities = np.asarray(velocities, dtype=float)
        frequencies = np.asarray(frequencies, dtype=float)
        
        # Get parameters from UI
        dx = self.error_dx_spin.value()
        Nx = self.error_nx_spin.value()
        a = self.error_a_spin.value()
        error_scale = self.error_scale_spin.value()
        
        # Calculate Lorentzian errors
        errors = lorentzian_error(velocities, frequencies, dx, Nx, a=a)
        
        # Apply scale factor
        errors = errors * error_scale
        
        return errors
    
    def recalculateErrors(self):
        """Recalculate Lorentzian errors for all curves using current parameters"""
        count = 0
        
        for xmid, pos_data in self.positions_data.items():
            for mode_name, mode_data in pos_data.get('modes', {}).items():
                # Recalculate errors for ALL curves (not just computed ones)
                original = mode_data['original']
                new_errors = self.calculateLorentzianErrors(
                    original['velocities'],
                    original['frequencies']
                )
                original['errors'] = new_errors.tolist()
                # Mark as computed so user knows these are Lorentzian errors
                mode_data['errors_computed'] = True
                count += 1
                
                # Also update resampled data if it exists
                if 'resampled' in mode_data:
                    resampled = mode_data['resampled']
                    new_errors_resampled = self.calculateLorentzianErrors(
                        resampled['velocities'],
                        resampled['frequencies']
                    )
                    resampled['errors'] = new_errors_resampled.tolist()
        
        if count > 0:
            QMessageBox.information(self, "Errors Recalculated", 
                f"Recalculated Lorentzian errors for {count} curve(s) using current parameters:\n"
                f"  dx = {self.error_dx_spin.value():.2f} m\n"
                f"  Nx = {self.error_nx_spin.value()}\n"
                f"  a = {self.error_a_spin.value():.2f}\n"
                f"  Error Scale = {self.error_scale_spin.value():.2f}")
            # Refresh plots
            self.plotOverviewTab()
        else:
            QMessageBox.information(self, "No Curves", "No curves loaded to recalculate errors.")
    
    def updatePositionTree(self):
        """Update position tree widget"""
        self.position_tree.clear()
        
        for xmid in sorted(self.positions_data.keys()):
            pos_data = self.positions_data[xmid]
            
            # Position item
            pos_item = QTreeWidgetItem([f"{xmid:.1f} m", "", ""])
            pos_item.setFlags(pos_item.flags() | Qt.ItemIsUserCheckable)
            pos_item.setCheckState(0, Qt.Checked if pos_data.get('selected_for_inversion', True) else Qt.Unchecked)
            pos_item.setData(0, Qt.UserRole, ('position', xmid))
            
            # Mode items
            for mode_name in sorted(pos_data.get('modes', {}).keys()):
                mode_data = pos_data['modes'][mode_name]
                data = mode_data.get('resampled', mode_data.get('original'))
                
                if data:
                    frequencies = np.asarray(data['frequencies'])
                    n_points = len(frequencies)
                    freq_min = np.min(frequencies)
                    freq_max = np.max(frequencies)
                    
                    mode_item = QTreeWidgetItem([
                        mode_name,
                        str(n_points),
                        f"{freq_min:.1f}-{freq_max:.1f} Hz"
                    ])
                    mode_item.setFlags(mode_item.flags() | Qt.ItemIsUserCheckable)
                    mode_item.setCheckState(0, Qt.Checked if mode_data.get('selected_for_inversion', True) else Qt.Unchecked)
                    mode_item.setData(0, Qt.UserRole, ('mode', xmid, mode_name))
                    
                    pos_item.addChild(mode_item)
            
            self.position_tree.addTopLevelItem(pos_item)
            pos_item.setExpanded(True)
    
    def onTreeItemChanged(self, item, column):
        """Handle tree item check state change"""
        data = item.data(0, Qt.UserRole)
        if data is None:
            return
        
        check_state = (item.checkState(0) == Qt.Checked)
        
        if data[0] == 'position':
            xmid = data[1]
            self.positions_data[xmid]['selected_for_inversion'] = check_state
            # Block signals to avoid cascading updates when setting children
            self.position_tree.blockSignals(True)
            for i in range(item.childCount()):
                child = item.child(i)
                child.setCheckState(0, Qt.Checked if check_state else Qt.Unchecked)
                # Also update data for child modes
                child_data = child.data(0, Qt.UserRole)
                if child_data and child_data[0] == 'mode':
                    mode_name = child_data[2]
                    self.positions_data[xmid]['modes'][mode_name]['selected_for_inversion'] = check_state
            self.position_tree.blockSignals(False)
        
        elif data[0] == 'mode':
            xmid = data[1]
            mode_name = data[2]
            self.positions_data[xmid]['modes'][mode_name]['selected_for_inversion'] = check_state
        
        self.updateSummary()
        # Fast update: only update dispersion curve plot
        self.updateDispersionPlot()
    
    def updateCurrentTab(self):
        """Update plots in the currently visible tab"""
        if not self.positions_data:
            return
        
        if not hasattr(self, 'tab_widget'):
            return
        
        current_index = self.tab_widget.currentIndex()
        
        # Tab 0: Overview
        if current_index == 0:
            self.plotOverviewTab()
        # Tab 1: Models
        elif current_index == 1:
            self.plotModelsTab()
        # Tab 2: Data Fit
        elif current_index == 2:
            self.plotDataFitTab()
        # Tab 3: Pseudo-sections
        elif current_index == 3:
            self.plotPseudosectionsTab()
        # Tab 4: Results (no automatic update needed)
    
    def updateDispersionPlot(self):
        """Update only the dispersion curves panel (fast update for selection changes)"""
        if not self.positions_data:
            return
        
        if not hasattr(self, 'overview_panels'):
            return
        
        # Only update Panel 0 of Overview tab (dispersion curves)
        # Use current control settings
        if hasattr(self, 'disp_show_combo'):
            show_all = self.disp_show_combo.currentText() == "All"
            mode_filter = self.disp_mode_filter_combo.currentText()
            color_by = self.disp_colorby_combo.currentText().lower()
            show_errors = self.disp_show_errors_check.isChecked()
            x_axis = self.disp_xaxis_combo.currentText().lower()
            cmap = self.disp_cmap_combo.currentText()
        else:
            show_all = True
            mode_filter = "All"
            color_by = 'mode'
            show_errors = True
            x_axis = 'frequency'
            cmap = 'viridis'
        
        self.overview_panels[0]['figure'].clear()
        ax0 = self.overview_panels[0]['figure'].add_subplot(111)
        DCInversionVisualizations.plot_dispersion_data(
            ax0, self.positions_data,
            selected_only=not show_all,
            mode_filter=mode_filter,
            color_by=color_by,
            show_errors=show_errors,
            x_axis=x_axis,
            cmap=cmap
        )
        self.apply_tight_layout(self.overview_panels[0]['figure'])
        self.overview_panels[0]['canvas'].draw()
    
    def onTreeItemDoubleClicked(self, item, column):
        """Handle double-click on tree item"""
        data = item.data(0, Qt.UserRole)
        if data and data[0] == 'position':
            xmid = data[1]
            self.current_position_xmid = xmid
            # Switch to Data Fit tab if inversion results exist
            if self.inversion_results:
                self.tab_widget.setCurrentIndex(2)  # Data Fit tab
                self.plotDataFitTab()
    
    def selectAllPositions(self, select):
        """Select or deselect all positions"""
        # Block signals to avoid multiple updates
        self.position_tree.blockSignals(True)
        for i in range(self.position_tree.topLevelItemCount()):
            item = self.position_tree.topLevelItem(i)
            item.setCheckState(0, Qt.Checked if select else Qt.Unchecked)
            # Update data directly since signals are blocked
            data = item.data(0, Qt.UserRole)
            if data and data[0] == 'position':
                xmid = data[1]
                self.positions_data[xmid]['selected_for_inversion'] = select
                # Also update all children
                for j in range(item.childCount()):
                    child = item.child(j)
                    child.setCheckState(0, Qt.Checked if select else Qt.Unchecked)
                    child_data = child.data(0, Qt.UserRole)
                    if child_data and child_data[0] == 'mode':
                        mode_name = child_data[2]
                        self.positions_data[xmid]['modes'][mode_name]['selected_for_inversion'] = select
        self.position_tree.blockSignals(False)
        self.updateSummary()
        self.updateCurrentTab()
    
    def selectMode(self, mode_name, select):
        """Select or deselect all instances of a specific mode"""
        # Block signals to avoid multiple updates
        self.position_tree.blockSignals(True)
        for i in range(self.position_tree.topLevelItemCount()):
            pos_item = self.position_tree.topLevelItem(i)
            for j in range(pos_item.childCount()):
                mode_item = pos_item.child(j)
                data = mode_item.data(0, Qt.UserRole)
                if data and data[0] == 'mode' and data[2] == mode_name:
                    mode_item.setCheckState(0, Qt.Checked if select else Qt.Unchecked)
                    # Update data directly since signals are blocked
                    xmid = data[1]
                    self.positions_data[xmid]['modes'][mode_name]['selected_for_inversion'] = select
        self.position_tree.blockSignals(False)
        self.updateSummary()
        self.updateCurrentTab()
    
    def updateModeButtons(self):
        """Update mode selection checkboxes based on available modes"""
        # Clear existing checkboxes
        while self.mode_checks_layout.count():
            child = self.mode_checks_layout.takeAt(0)
            if child.widget():
                child.widget().deleteLater()
        
        if not self.positions_data:
            return
        
        # Collect all unique modes
        all_modes = set()
        for pos_data in self.positions_data.values():
            for mode_name in pos_data.get('modes', {}).keys():
                all_modes.add(mode_name)
        
        if not all_modes:
            return
        
        # Create checkboxes for each mode in flow layout (auto-wraps)
        sorted_modes = sorted(all_modes, key=lambda x: int(x.replace('M', '')))
        for mode_name in sorted_modes:
            checkbox = QCheckBox(f" {mode_name}")
            checkbox.setChecked(True)  # All modes selected by default
            checkbox.stateChanged.connect(
                lambda state, m=mode_name: self.selectMode(m, state == Qt.Checked)
            )
            self.mode_checks_layout.addWidget(checkbox)
    
    def updateModeFilterDropdown(self):
        """Update mode filter dropdown based on available modes"""
        if not hasattr(self, 'disp_mode_filter_combo'):
            return
        
        if not self.positions_data:
            return
        
        # Collect all unique modes
        all_modes = set()
        for pos_data in self.positions_data.values():
            for mode_name in pos_data.get('modes', {}).keys():
                all_modes.add(mode_name)
        
        if not all_modes:
            return
        
        # Store current selection
        current_selection = self.disp_mode_filter_combo.currentText()
        
        # Clear and repopulate
        self.disp_mode_filter_combo.clear()
        sorted_modes = sorted(all_modes, key=lambda x: int(x.replace('M', '')))
        self.disp_mode_filter_combo.addItems(["All"] + sorted_modes)
        
        # Restore selection if still valid
        if current_selection in ["All"] + sorted_modes:
            self.disp_mode_filter_combo.setCurrentText(current_selection)
        else:
            self.disp_mode_filter_combo.setCurrentText("All")
    
    def updateSummary(self):
        """Update summary label"""
        n_positions = len([p for p in self.positions_data.values() 
                          if p.get('selected_for_inversion', True)])
        n_total_positions = len(self.positions_data)
        
        n_curves = sum(len([m for m in p.get('modes', {}).values() 
                           if m.get('selected_for_inversion', True)])
                      for p in self.positions_data.values()
                      if p.get('selected_for_inversion', True))
        
        self.summary_label.setText(
            f"Selected: {n_positions}/{n_total_positions} positions, {n_curves} curves"
        )
        
        # Update max mode in pseudo-section spinbox
        if hasattr(self, 'pseudo_mode_spin'):
            max_mode = 0
            for pos_data in self.positions_data.values():
                for mode_name in pos_data.get('modes', {}).keys():
                    try:
                        mode_num = int(mode_name.replace('M', ''))
                        max_mode = max(max_mode, mode_num)
                    except:
                        pass
            self.pseudo_mode_spin.setMaximum(max(max_mode, 2))
    
    def resampleSelected(self):
        """Resample all curves"""
        # Warn if inversion results exist
        if self.inversion_results:
            reply = QMessageBox.warning(
                self, 
                "Warning: Existing Inversion Results",
                "Changing the resampling will invalidate all current inversion results.\n\n"
                "Do you want to continue?",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No
            )
            if reply == QMessageBox.No:
                return
            
            # Clear inversion results
            self.inversion_results = {}
        
        domain = 'wavelength' if self.domain_combo.currentText() == "Wavelength" else 'frequency'
        spacing = 'log' if self.spacing_combo.currentText() == "Logarithmic" else 'linear'
        min_val = self.min_value_spin.value()
        max_val = self.max_value_spin.value()
        n_samples = self.n_samples_spin.value()
        
        count = 0
        for pos_data in self.positions_data.values():
            for mode_data in pos_data.get('modes', {}).values():
                original = mode_data['original']
                
                try:
                    freq_new, vel_new, err_new = resample_dispersion_curve(
                        original['frequencies'],
                        original['velocities'],
                        original.get('errors'),
                        domain=domain,
                        min_value=min_val,
                        max_value=max_val,
                        n_samples=n_samples,
                        spacing=spacing
                    )
                    
                    mode_data['resampled'] = {
                        'frequencies': freq_new,
                        'velocities': vel_new,
                        'errors': err_new,
                        'domain': domain,
                        'spacing': spacing
                    }
                    count += 1
                    
                except Exception as e:
                    print(f"Resampling error: {e}")
        
        QMessageBox.information(self, "Resample Complete", 
            f"Resampled {count} curves")
        
        self.updatePositionTree()
        self.updateCurrentTab()
    
    def revertToOriginal(self):
        """Revert all curves to original data (remove resampling)"""
        # Warn if inversion results exist
        if self.inversion_results:
            reply = QMessageBox.warning(
                self, 
                "Warning: Existing Inversion Results",
                "Reverting to original data will invalidate all current inversion results.\n\n"
                "Do you want to continue?",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No
            )
            if reply == QMessageBox.No:
                return
            
            # Clear inversion results
            self.inversion_results = {}
        
        count = 0
        for pos_data in self.positions_data.values():
            for mode_data in pos_data.get('modes', {}).values():
                # Remove resampled data if it exists
                if 'resampled' in mode_data:
                    del mode_data['resampled']
                    count += 1
        
        QMessageBox.information(self, "Revert Complete", 
            f"Reverted {count} curves to original data")
        
        self.updatePositionTree()
        self.updateCurrentTab()
    
    def collectInversionParams(self):
        """Collect inversion parameters from UI"""
        params = {
            'n_layers': self.n_layers_spin.value(),
            'max_depth': self.max_depth_spin.value(),
            'thickness_strategy': self.thickness_combo.currentText().lower(),
            'nu': self.nu_spin.value(),
            'rho': self.rho_spin.value(),
            'lambda': self.lambda_spin.value(),
            'lambda_factor': self.lambda_factor_spin.value(),
            'max_iter': self.max_iter_spin.value(),
            'algorithm': self.algorithm_combo.currentText(),
            'gradient_start': True,
            'use_reference_model': True,
            'target_chi2': 1.5,
            'verbose': False
        }
        
        # Add starting model parameters based on type
        model_type = self.starting_model_combo.currentText()
        params['starting_model_type'] = model_type
        
        if model_type == "Wavelength-based":
            params['vs_vr_ratio'] = self.vs_vr_ratio_spin.value()
            params['depth_factor'] = self.depth_factor_spin.value()
        elif model_type == "Generic gradient":
            params['gradient_vs_top'] = self.gradient_vs_top_spin.value()
            params['gradient_vs_bottom'] = self.gradient_vs_bottom_spin.value()
        elif model_type == "Custom layers":
            # Gather custom layer data from table
            custom_layers = []
            for row in range(self.custom_layers_table.rowCount()):
                try:
                    thickness = float(self.custom_layers_table.item(row, 1).text())
                    vp = float(self.custom_layers_table.item(row, 2).text())
                    vs = float(self.custom_layers_table.item(row, 3).text())
                    rho = float(self.custom_layers_table.item(row, 4).text())
                    
                    custom_layers.append({
                        'thickness': thickness,
                        'vp': vp,
                        'vs': vs,
                        'rho': rho
                    })
                except (ValueError, AttributeError) as e:
                    QMessageBox.warning(self, "Invalid Layer Data",
                        f"Invalid data in layer {row + 1}. Please check all values are valid numbers.")
                    return None
            
            params['custom_layers'] = custom_layers
        
        return params
    
    def runInversion(self):
        """Run inversion for selected positions"""
        # Determine which positions to invert
        if self.single_radio.isChecked():
            if self.current_position_xmid is None:
                QMessageBox.warning(self, "No Selection", 
                    "Please double-click a position to select it")
                return
            selected_xmids = [self.current_position_xmid]
        else:  # Batch mode
            selected_xmids = [
                xmid for xmid, data in self.positions_data.items()
                if data.get('selected_for_inversion', True)
            ]
        
        if not selected_xmids:
            QMessageBox.warning(self, "No Selection", "No positions selected")
            return
        
        # Show progress widget
        self.progress_widget.initialize_positions(selected_xmids)
        self.progress_widget.setVisible(True)
        
        # Collect parameters
        params = self.collectInversionParams()
        if params is None:
            # Error already shown in collectInversionParams
            return
        
        # Filter positions_data to selected
        selected_data = {
            xmid: self.positions_data[xmid]
            for xmid in selected_xmids
        }
        
        # Check parallelization settings
        parallel = self.parallel_check.isChecked() and len(selected_xmids) > 1
        n_workers = self.n_workers_spin.value() if parallel else None
        
        # Create worker thread
        self.inversion_thread = InversionWorkerThread(
            selected_data, params, parallel, n_workers
        )
        self.inversion_thread.progress_signal.connect(self.onInversionProgress)
        self.inversion_thread.finished_signal.connect(self.onInversionFinished)
        self.inversion_thread.start()
        
        # Disable run button
        self.run_button.setEnabled(False)
        self.run_button.setText("Running...")
    
    def onInversionProgress(self, xmid, idx, total, status, result):
        """Handle progress updates from worker thread"""
        self.progress_widget.update_position(xmid, status, 0, result)
        self.progress_widget.update_overall(idx, total)
    
    def onInversionFinished(self, results):
        """Handle completion of all inversions"""
        # Store results
        self.inversion_results = results
        
        # Re-enable run button
        self.run_button.setEnabled(True)
        self.run_button.setText("Run Inversion")
        
        # Check for errors
        if 'error' in results and 'traceback' in results:
            QMessageBox.critical(self, "Inversion Error",
                f"Inversion failed:\n{results['error']}")
            return
        
        n_success = sum(1 for r in results.values() if r.get('success', False))
        n_total = len(results)
        
        if n_success == n_total:
            QMessageBox.information(self, "Success",
                f"All {n_total} inversions completed successfully!")
        else:
            # Collect error messages from failed inversions
            failed_msgs = []
            for xmid, result in results.items():
                if not result.get('success', False):
                    error_msg = result.get('error', 'Unknown error')
                    failed_msgs.append(f"Xmid {xmid:.1f}m: {error_msg}")
            
            error_details = "\n\n".join(failed_msgs[:5])  # Show first 5 errors
            if len(failed_msgs) > 5:
                error_details += f"\n\n... and {len(failed_msgs) - 5} more errors"
            
            QMessageBox.warning(self, "Partial Success",
                f"{n_success}/{n_total} inversions succeeded.\n"
                f"{n_total - n_success} failed.\n\n"
                f"Error details:\n{error_details}")
        
        # Visualize results
        self.visualizeResults()
    
    def visualizeResults(self):
        """Visualize inversion results"""
        if not self.inversion_results:
            return
        
        # Update all tabs with results
        self.plotOverviewTab()
        self.plotModelsTab()
        self.plotDataFitTab()
        self.plotPseudosectionsTab()
        self.updateResultsTable()
        
        # Switch to Overview tab
        self.tab_widget.setCurrentIndex(0)
    
    def plotOverviewTab(self):
        """Plot Overview tab - respects user-selected views in dropdowns"""
        for i, panel in enumerate(self.overview_panels):
            view_name = panel.get('current_view', panel['view_combo'].currentText())
            self.plot_single_view(panel, view_name)
    
    def plotModelsTab(self):
        """Plot Models tab - respects user-selected views in dropdowns"""
        for i, panel in enumerate(self.models_panels):
            view_name = panel.get('current_view', panel['view_combo'].currentText())
            self.plot_single_view(panel, view_name)
    
    def plotDataFitTab(self):
        """Plot Data Fit tab - respects user-selected views in dropdowns"""
        for i, panel in enumerate(self.datafit_panels):
            view_name = panel.get('current_view', panel['view_combo'].currentText())
            self.plot_single_view(panel, view_name)
    
    def plotPseudosectionsTab(self):
        """Plot Pseudo-sections tab - respects user-selected views in dropdowns"""
        for i, panel in enumerate(self.pseudosections_panels):
            view_name = panel.get('current_view', panel['view_combo'].currentText())
            self.plot_single_view(panel, view_name)
    
    # Helper plotting methods for individual panels
    def plot_2d_vs_model(self, ax, cmap='viridis', show_mesh=False, vmin=None, vmax=None, aspect=1.0):
        """Plot 2D Vs model from all 1D inversions using interpolated grid with topography
        
        Parameters:
        -----------
        ax : matplotlib axis
        cmap : str
            Colormap name
        show_mesh : bool
            Whether to show mesh lines
        vmin : float or None
            Minimum velocity for color scale (None for auto)
        vmax : float or None
            Maximum velocity for color scale (None for auto)
        aspect : float
            Aspect ratio (1.0 = equal, >1 = vertical exaggeration)
        """
        if not self.inversion_results:
            return
        
        # Collect all inverted models
        xmids = []
        vs_models = []
        thicknesses = []
        elevations = []
        max_depth_below_surface = 0
        
        for xmid in sorted(self.inversion_results.keys()):
            result = self.inversion_results[xmid]
            if not result.get('success', False):
                continue
            
            xmids.append(xmid)
            vs_models.append(result['vs_model'])
            thicknesses.append(result['thickness'])
            
            # Get elevation for this position
            elevation = self.positions_data.get(xmid, {}).get('elevation', 0.0)
            elevations.append(elevation)
            
            depth = np.cumsum(result['thickness'])
            if len(depth) > 0:
                max_depth_below_surface = max(max_depth_below_surface, depth[-1])
        
        if len(xmids) == 0:
            ax.text(0.5, 0.5, 'No successful inversions', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Create depth grid (below surface)
        depth_grid = np.linspace(0, max_depth_below_surface, 100)
        
        # Convert to arrays
        xmids = np.array(xmids)
        elevations = np.array(elevations)
        
        # Interpolate Vs at each position
        vs_2d = np.zeros((len(depth_grid), len(xmids)))
        
        for i, (vs_model, thickness) in enumerate(zip(vs_models, thicknesses)):
            # Create depth array for this position
            layer_bottoms = np.hstack([0, np.cumsum(thickness)])
            
            # Interpolate Vs to common depth grid
            for j, d in enumerate(depth_grid):
                # Find which layer this depth is in
                layer_idx = np.searchsorted(layer_bottoms[:-1], d, side='right') - 1
                layer_idx = max(0, min(layer_idx, len(vs_model) - 1))
                vs_2d[j, i] = vs_model[layer_idx]
        
        # Create meshgrid for plotting
        # Y coordinate = elevation - depth_below_surface
        X, D = np.meshgrid(xmids, depth_grid)
        E = np.tile(elevations, (len(depth_grid), 1))  # Broadcast elevations to match depth grid
        Y = E - D  # Absolute elevation = surface elevation - depth
        
        # Plot as pcolormesh with control parameters
        im = ax.pcolormesh(X, Y, vs_2d, cmap=cmap, shading='nearest', vmin=vmin, vmax=vmax)
        
        # Show mesh if requested
        if show_mesh:
            ax.pcolormesh(X, Y, vs_2d, cmap=cmap, shading='nearest', 
                         vmin=vmin, vmax=vmax, edgecolors='k', linewidth=0.5, alpha=0.8)
        
        # Plot topography line if available
        if self.has_topography:
            ax.plot(xmids, elevations, 'k-', linewidth=2, label='Topography')
        
        ax.set_xlabel('Position (m)')
        ax.set_ylabel('Elevation (m)')
        
        # Set aspect ratio
        ax.set_aspect(aspect)
        
        # Add colorbar
        cbar = ax.figure.colorbar(im, ax=ax)
        cbar.set_label('Vs (m/s)')
        
        if self.has_topography:
            ax.legend(loc='upper right')
    
    def plot_2d_model_difference(self, ax, cmap='RdBu_r', show_mesh=False, vmin=None, vmax=None, aspect=1.0, relative=False):
        """Plot difference between final and starting 2D Vs models
        
        Parameters:
        -----------
        ax : matplotlib axis
        cmap : str
            Colormap name (default RdBu_r for diverging colormap)
        show_mesh : bool
            Whether to show mesh lines
        vmin : float or None
            Minimum value for color scale (None for auto symmetric)
        vmax : float or None
            Maximum value for color scale (None for auto symmetric)
        aspect : float
            Aspect ratio (1.0 = equal, >1 = vertical exaggeration)
        relative : bool
            If True, plot relative difference (%), otherwise absolute (m/s)
        """
        if not self.inversion_results:
            ax.text(0.5, 0.5, 'Run inversion first', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Collect all models
        xmids = []
        vs_final_models = []
        vs_start_models = []
        thicknesses = []
        elevations = []
        max_depth_below_surface = 0
        
        for xmid in sorted(self.inversion_results.keys()):
            result = self.inversion_results[xmid]
            if not result.get('success', False):
                continue
            
            if 'vs_start' not in result:
                continue
            
            xmids.append(xmid)
            vs_final_models.append(result['vs_model'])
            vs_start_models.append(result['vs_start'])
            thicknesses.append(result['thickness'])
            
            # Get elevation for this position
            elevation = self.positions_data.get(xmid, {}).get('elevation', 0.0)
            elevations.append(elevation)
            
            depth = np.cumsum(result['thickness'])
            if len(depth) > 0:
                max_depth_below_surface = max(max_depth_below_surface, depth[-1])
        
        if len(xmids) == 0:
            ax.text(0.5, 0.5, 'No successful inversions', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Create depth grid (below surface)
        depth_grid = np.linspace(0, max_depth_below_surface, 100)
        
        # Convert to arrays
        xmids = np.array(xmids)
        elevations = np.array(elevations)
        
        # Interpolate Vs at each position for both models
        vs_final_2d = np.zeros((len(depth_grid), len(xmids)))
        vs_start_2d = np.zeros((len(depth_grid), len(xmids)))
        
        for i, (vs_final, vs_start, thickness) in enumerate(zip(vs_final_models, vs_start_models, thicknesses)):
            # Create depth array for this position
            layer_bottoms = np.hstack([0, np.cumsum(thickness)])
            
            # Interpolate Vs to common depth grid
            for j, d in enumerate(depth_grid):
                # Find which layer this depth is in
                layer_idx = np.searchsorted(layer_bottoms[:-1], d, side='right') - 1
                layer_idx = max(0, min(layer_idx, len(vs_final) - 1))
                vs_final_2d[j, i] = vs_final[layer_idx]
                vs_start_2d[j, i] = vs_start[layer_idx]
        
        # Calculate difference
        if relative:
            # Relative difference in percentage
            diff_2d = 100.0 * (vs_final_2d - vs_start_2d) / vs_start_2d
        else:
            # Absolute difference in m/s
            diff_2d = vs_final_2d - vs_start_2d
        
        # Create meshgrid for plotting
        X, D = np.meshgrid(xmids, depth_grid)
        E = np.tile(elevations, (len(depth_grid), 1))
        Y = E - D  # Absolute elevation
        
        # Auto-scale to symmetric range if not specified
        if vmin is None and vmax is None:
            max_abs = np.abs(diff_2d).max()
            vmin, vmax = -max_abs, max_abs
        elif vmin is None:
            vmin = -vmax
        elif vmax is None:
            vmax = -vmin
        
        # Plot as pcolormesh
        im = ax.pcolormesh(X, Y, diff_2d, cmap=cmap, shading='nearest', vmin=vmin, vmax=vmax)
        
        # Show mesh if requested
        if show_mesh:
            ax.pcolormesh(X, Y, diff_2d, cmap=cmap, shading='nearest', 
                         vmin=vmin, vmax=vmax, edgecolors='k', linewidth=0.5, alpha=0.8)
        
        # Plot topography line if available
        if self.has_topography:
            ax.plot(xmids, elevations, 'k-', linewidth=2, label='Topography')
        
        ax.set_xlabel('Position (m)')
        ax.set_ylabel('Elevation (m)')
        
        if relative:
            cbar_label = 'Relative Difference (%)'
        else:
            cbar_label = 'Difference (m/s)'
        
        # Set aspect ratio
        ax.set_aspect(aspect)
        
        # Add colorbar
        cbar = ax.figure.colorbar(im, ax=ax)
        cbar.set_label(cbar_label)
        
        if self.has_topography:
            ax.legend(loc='upper right')
   
    def plot_pseudosection(self, ax, mode_name='M0', data_type='observed', vmin=None, vmax=None, y_axis='Frequency'):
        """Plot pseudo-section for a specific mode"""
        xcoords = []
        all_freqs = []
        all_vels = []
        
        for xmid in sorted(self.positions_data.keys()):
            pos_data = self.positions_data[xmid]
            if mode_name not in pos_data.get('modes', {}):
                continue
            
            mode_data = pos_data['modes'][mode_name]
            
            if data_type == 'observed':
                data = mode_data.get('resampled') or mode_data.get('original')
            elif data_type == 'computed' and self.inversion_results:
                # Get computed response
                if xmid not in self.inversion_results:
                    continue
                result = self.inversion_results[xmid]
                if not result.get('success', False) or mode_name not in result.get('mode_fits', {}):
                    continue
                fit = result['mode_fits'][mode_name]
                data_orig = mode_data.get('resampled') or mode_data.get('original')
                data = {
                    'frequencies': data_orig.get('frequencies', []),
                    'velocities': fit['calculated']
                }
            else:
                continue
            
            if data is None:
                continue
            
            freq = np.asarray(data.get('frequencies', []))
            vel = np.asarray(data.get('velocities', []))
            
            if len(freq) == 0:
                continue
            
            xcoords.extend([xmid] * len(freq))
            all_freqs.extend(freq)
            all_vels.extend(vel)
        
        if not xcoords:
            ax.text(0.5, 0.5, f'No data for {mode_name}', ha='center', va='center', transform=ax.transAxes)
            return
        
        xcoords = np.array(xcoords)
        all_freqs = np.array(all_freqs)
        all_vels = np.array(all_vels)
        
        # Calculate y-axis values (frequency or wavelength)
        if y_axis == 'Wavelength':
            # Wavelength = Velocity / Frequency
            y_values = all_vels / all_freqs
            ylabel = 'Wavelength (m)'
        else:
            y_values = all_freqs
            ylabel = 'Frequency (Hz)'
        
        scatter = ax.scatter(xcoords, y_values, c=all_vels, s=30, cmap='plasma', alpha=0.7,
                            vmin=vmin, vmax=vmax)
        ax.set_xlabel('Xmid Position (m)')
        ax.set_ylabel(ylabel)
        ax.grid(True, alpha=0.3)
        ax.invert_yaxis()  # Invert y-axis to point downward
        ax.figure.colorbar(scatter, ax=ax, label='Phase Velocity (m/s)')
    
    def plot_pseudosection_difference(self, ax, mode_name='M0', relative=False, vmin=None, vmax=None, y_axis='Frequency'):
        """Plot difference between observed and computed pseudo-sections"""
        xcoords = []
        all_freqs = []
        all_vels = []  # Store observed velocities for wavelength calculation
        all_diff = []
        
        for xmid in sorted(self.positions_data.keys()):
            if xmid not in self.inversion_results:
                continue
            
            result = self.inversion_results[xmid]
            if not result.get('success', False) or mode_name not in result.get('mode_fits', {}):
                continue
            
            pos_data = self.positions_data[xmid]
            if mode_name not in pos_data.get('modes', {}):
                continue
            
            mode_data = pos_data['modes'][mode_name]
            data_orig = mode_data.get('resampled') or mode_data.get('original')
            
            fit = result['mode_fits'][mode_name]
            freq = np.asarray(data_orig.get('frequencies', []))
            obs = np.asarray(data_orig.get('velocities', []))
            calc = fit['calculated']
            
            if len(freq) == 0:
                continue
            
            if relative:
                diff = 100 * (calc - obs) / obs  # Relative difference in %
            else:
                diff = calc - obs  # Absolute difference
            
            xcoords.extend([xmid] * len(freq))
            all_freqs.extend(freq)
            all_vels.extend(obs)
            all_diff.extend(diff)
        
        if not xcoords:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        xcoords = np.array(xcoords)
        all_freqs = np.array(all_freqs)
        all_vels = np.array(all_vels)
        all_diff = np.array(all_diff)
        
        # Use provided vmin/vmax or auto-calculate symmetric range
        if vmin is None and vmax is None:
            max_abs = np.abs(all_diff).max()
            vmin = -max_abs
            vmax = max_abs
        
        # Calculate y-axis values (frequency or wavelength)
        if y_axis == 'Wavelength':
            # Wavelength = Velocity / Frequency (using observed velocities)
            y_values = all_vels / all_freqs
            ylabel = 'Wavelength (m)'
        else:
            y_values = all_freqs
            ylabel = 'Frequency (Hz)'
        
        scatter = ax.scatter(xcoords, y_values, c=all_diff, s=30, cmap='RdBu_r', alpha=0.7,
                            vmin=vmin, vmax=vmax)
        ax.set_xlabel('Xmid Position (m)')
        ax.set_ylabel(ylabel)
        if relative:
            ax.figure.colorbar(scatter, ax=ax, label='Relative Diff (%)')
        else:
            ax.figure.colorbar(scatter, ax=ax, label='Diff (m/s)')
        ax.grid(True, alpha=0.3)
        ax.invert_yaxis()  # Invert y-axis to point downward
    
    def plot_topography_only(self, ax):
        """Plot topography line only (before inversion is run)"""
        if not self.has_topography or len(self.positions_data) == 0:
            ax.text(0.5, 0.5, 'Load topography and data', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Get xmid positions and their elevations
        xmids = []
        elevations = []
        
        for xmid in sorted(self.positions_data.keys()):
            elevation = self.positions_data.get(xmid, {}).get('elevation', 0.0)
            xmids.append(xmid)
            elevations.append(elevation)
        
        xmids = np.array(xmids)
        elevations = np.array(elevations)
        
        # Plot topography line
        ax.plot(xmids, elevations, 'k-', linewidth=2, marker='o', 
               markersize=4, label='Topography')
        
        ax.set_xlabel('Position (m)')
        ax.set_ylabel('Elevation (m)')
        ax.grid(True, alpha=0.3)
        ax.legend(loc='best')
    
    def plot_velocity_histogram(self, ax, bins=30, mode_filter="All"):
        """Plot histogram of phase velocities with optional mode filtering"""
        all_vels = []
        
        for xmid, pos_data in self.positions_data.items():
            for mode_name, mode_data in pos_data.get('modes', {}).items():
                # Apply mode filter
                if mode_filter != "All":
                    if mode_filter == "M0 only" and mode_name != "M0":
                        continue
                    elif mode_filter == "M1 only" and mode_name != "M1":
                        continue
                    elif mode_filter == "M2 only" and mode_name != "M2":
                        continue
                
                data = mode_data.get('resampled') or mode_data.get('original')
                if data:
                    vel = np.asarray(data.get('velocities', []))
                    all_vels.extend(vel)
        
        if not all_vels:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        all_vels = np.array(all_vels)
        ax.hist(all_vels, bins=bins, alpha=0.7, edgecolor='black')
        
        # Add statistical markers
        mean_val = np.mean(all_vels)
        median_val = np.median(all_vels)
        std_val = np.std(all_vels)
        p5 = np.percentile(all_vels, 5)
        p95 = np.percentile(all_vels, 95)
        
        ax.axvline(mean_val, color='red', linestyle='-', linewidth=2, label=f'Mean: {mean_val:.1f}')
        ax.axvline(median_val, color='blue', linestyle='--', linewidth=2, label=f'Median: {median_val:.1f}')
        ax.axvline(mean_val - std_val, color='orange', linestyle=':', linewidth=1.5, 
                  label=f'Mean±Std: [{mean_val-std_val:.1f}, {mean_val+std_val:.1f}]')
        ax.axvline(mean_val + std_val, color='orange', linestyle=':', linewidth=1.5)
        ax.axvline(p5, color='green', linestyle='-.', linewidth=1.5, 
                  label=f'5th/95th: [{p5:.1f}, {p95:.1f}]')
        ax.axvline(p95, color='green', linestyle='-.', linewidth=1.5)
        
        ax.set_xlabel('Phase Velocity (m/s)')
        ax.set_ylabel('Count')
        ax.grid(True, alpha=0.3, axis='y')
        ax.legend(loc='upper right', fontsize=8)
    
    def plot_velocity_histogram_split(self, ax, bins=30, mode_filter="All", 
                                      split_type="Frequency", split_step=5.0):
        """Plot velocity histogram split by frequency or wavelength ranges"""
        # Collect data with frequencies
        data_points = []  # (velocity, frequency, wavelength)
        
        for xmid, pos_data in self.positions_data.items():
            for mode_name, mode_data in pos_data.get('modes', {}).items():
                # Apply mode filter
                if mode_filter != "All":
                    if mode_filter == "M0 only" and mode_name != "M0":
                        continue
                    elif mode_filter == "M1 only" and mode_name != "M1":
                        continue
                    elif mode_filter == "M2 only" and mode_name != "M2":
                        continue
                
                data = mode_data.get('resampled') or mode_data.get('original')
                if data:
                    freqs = np.asarray(data.get('frequencies', []))
                    vels = np.asarray(data.get('velocities', []))
                    
                    if len(freqs) == len(vels):
                        wavelengths = vels / freqs
                        for v, f, w in zip(vels, freqs, wavelengths):
                            data_points.append((v, f, w))
        
        if not data_points:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        # Convert to numpy arrays
        all_vels = np.array([d[0] for d in data_points])
        all_freqs = np.array([d[1] for d in data_points])
        all_wavelengths = np.array([d[2] for d in data_points])
        
        # Determine splitting parameter
        if split_type == "Wavelength":
            split_values = all_wavelengths
            split_label = "Wavelength (m)"
        else:
            split_values = all_freqs
            split_label = "Frequency (Hz)"
        
        # Create ranges
        min_val = np.floor(split_values.min() / split_step) * split_step
        max_val = np.ceil(split_values.max() / split_step) * split_step
        ranges = np.arange(min_val, max_val + split_step, split_step)
        
        # Calculate common bins for all velocity data
        vel_min = all_vels.min()
        vel_max = all_vels.max()
        common_bins = np.linspace(vel_min, vel_max, bins + 1)
        bin_centers = (common_bins[:-1] + common_bins[1:]) / 2
        
        # Calculate histogram for each range
        n_ranges = len(ranges) - 1
        colors = plt.cm.tab10(np.linspace(0, 1, min(n_ranges, 10)))
        
        # Width of each bar group
        bar_width = (common_bins[1] - common_bins[0]) / (n_ranges + 1)
        
        for i in range(n_ranges):
            range_min = ranges[i]
            range_max = ranges[i + 1]
            
            # Filter data in this range
            mask = (split_values >= range_min) & (split_values < range_max)
            range_vels = all_vels[mask]
            
            if len(range_vels) > 0:
                # Calculate histogram
                counts, _ = np.histogram(range_vels, bins=common_bins)
                
                # Offset position for this range
                offset = (i - n_ranges/2 + 0.5) * bar_width
                positions = bin_centers + offset
                
                range_label = f'{range_min:.1f}-{range_max:.1f}'
                ax.bar(positions, counts, width=bar_width * 0.9, alpha=0.8,
                      edgecolor='black', linewidth=0.5,
                      color=colors[i % len(colors)], label=range_label)
        
        ax.set_xlabel('Phase Velocity (m/s)')
        ax.set_ylabel('Count')
        ax.grid(True, alpha=0.3, axis='y')
        ax.legend(title=split_label, loc='upper right', fontsize=7, ncol=2)
    
    def plot_starting_1d_models(self, ax, show_all=True, color_by='position', plot_style='steps'):
        """Plot starting 1D Vs models
        
        Parameters:
        -----------
        ax : matplotlib axis
        show_all : bool
            If True, show all models. If False, show only selected
        color_by : str
            'position' or 'misfit' - how to color themodels
        plot_style : str
            'steps' or 'curves' - plotting style
        """
        if not self.inversion_results:
            ax.text(0.5, 0.5, 'Run inversion first', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Get sorted xmid values for color normalization
        xmid_list = sorted(self.inversion_results.keys())
        xmid_min = min(xmid_list)
        xmid_max = max(xmid_list)
        
        # Create colormap normalization
        from matplotlib.colors import Normalize
        from matplotlib.cm import ScalarMappable
        
        if color_by == 'misfit':
            # Color by chi-squared values
            chi2_values = [self.inversion_results[xmid].get('total_chi2', 0) 
                          for xmid in xmid_list if self.inversion_results[xmid].get('success', False)]
            chi2_min = min(chi2_values) if chi2_values else 0
            chi2_max = max(chi2_values) if chi2_values else 1
            norm = Normalize(vmin=chi2_min, vmax=chi2_max)
            cmap = plt.cm.RdYlGn_r  # Red for high misfit, green for low
            sm = ScalarMappable(norm=norm, cmap=cmap)
            colorbar_label = 'χ²'
        else:
            # Color by position
            norm = Normalize(vmin=xmid_min, vmax=xmid_max)
            cmap = plt.cm.viridis
            sm = ScalarMappable(norm=norm, cmap=cmap)
            colorbar_label = 'Position (m)'
        
        for xmid in xmid_list:
            result = self.inversion_results[xmid]
            if not result.get('success', False):
                continue
            
            # Filter by selection if show_all is False
            if not show_all:
                # Check if this xmid is selected (you may need to implement selection tracking)
                pass  # For now, show all
            
            if 'vs_start' not in result:
                continue
            
            thickness = result['thickness']
            vs_start = result['vs_start']
            
            # Create depth array
            depth = np.hstack([0, np.cumsum(thickness)])
            
            label = f'Xmid={xmid:.1f}m'
            
            # Determine color based on color_by parameter
            if color_by == 'misfit':
                chi2 = result.get('total_chi2', 0)
                color = cmap(norm(chi2))
            else:
                color = cmap(norm(xmid))
            
            # Number of layers is one less than depth boundaries
            n_layers = len(depth) - 1
            
            if plot_style == 'steps':
                # Create step plot with explicit interleaving
                vs_plot = np.repeat(vs_start[:n_layers], 2)  # Each velocity appears twice
                depth_plot = np.empty(2 * n_layers)  # Pre-allocate
                depth_plot[0::2] = depth[:-1]  # Top of each layer
                depth_plot[1::2] = depth[1:]   # Bottom of each layer
                ax.plot(vs_plot, depth_plot, label=label, color=color, linewidth=1.5, alpha=0.7)
            else:  # curves
                # Smooth curves: plot at layer centers
                depth_centers = depth[:-1] + thickness / 2
                ax.plot(vs_start[:n_layers], depth_centers, '-', label=label, color=color, 
                       linewidth=1.5, alpha=0.7)
        
        ax.set_xlabel('Vs (m/s)')
        ax.set_ylabel('Depth (m)')
        ax.invert_yaxis()
        ax.grid(True, alpha=0.3)
        
        # Add colorbar
        cbar = ax.figure.colorbar(sm, ax=ax, label=colorbar_label)
        
        # Show legend only if few positions
        if len(xmid_list) <= 10:
            ax.legend(fontsize=8, loc='best')
    
    def plot_starting_2d_model(self, ax, cmap='viridis', show_mesh=False, vmin=None, vmax=None, aspect=1.0):
        """Plot starting 2D Vs model with topography
        
        Parameters:
        -----------
        ax : matplotlib axis
        cmap : str
            Colormap name
        show_mesh : bool
            Whether to show mesh lines
        vmin : float or None
            Minimum velocity for color scale (None for auto)
        vmax : float or None
            Maximum velocity for color scale (None for auto)
        aspect : float
            Aspect ratio (1.0 = equal, >1 = vertical exaggeration)
        """
        if not self.inversion_results:
            ax.text(0.5, 0.5, 'Run inversion first', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Collect all starting models
        xmids = []
        vs_models = []
        thicknesses = []
        elevations = []
        max_depth_below_surface = 0
        
        for xmid in sorted(self.inversion_results.keys()):
            result = self.inversion_results[xmid]
            if not result.get('success', False):
                continue
            if 'vs_start' not in result:
                continue
            
            xmids.append(xmid)
            vs_models.append(result['vs_start'])
            thicknesses.append(result['thickness'])
            
            # Get elevation for this position
            elevation = self.positions_data.get(xmid, {}).get('elevation', 0.0)
            elevations.append(elevation)
            
            depth = np.cumsum(result['thickness'])
            if len(depth) > 0:
                max_depth_below_surface = max(max_depth_below_surface, depth[-1])
        
        if len(xmids) == 0:
            ax.text(0.5, 0.5, 'No starting models available', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Create depth grid (below surface)
        depth_grid = np.linspace(0, max_depth_below_surface, 100)
        
        # Convert to arrays
        xmids = np.array(xmids)
        elevations = np.array(elevations)
        
        # Interpolate Vs at each position
        vs_2d = np.zeros((len(depth_grid), len(xmids)))
        
        for i, (vs_model, thickness) in enumerate(zip(vs_models, thicknesses)):
            # Create depth array for this position
            layer_bottoms = np.hstack([0, np.cumsum(thickness)])
            
            # Interpolate Vs to common depth grid
            for j, d in enumerate(depth_grid):
                # Find which layer this depth is in
                layer_idx = np.searchsorted(layer_bottoms[:-1], d, side='right') - 1
                layer_idx = max(0, min(layer_idx, len(vs_model) - 1))
                vs_2d[j, i] = vs_model[layer_idx]
        
        # Create meshgrid for plotting
        # Y coordinate = elevation - depth_below_surface
        X, D = np.meshgrid(xmids, depth_grid)
        E = np.tile(elevations, (len(depth_grid), 1))  # Broadcast elevations to match depth grid
        Y = E - D  # Absolute elevation = surface elevation - depth
        
        # Plot as pcolormesh with control parameters
        im = ax.pcolormesh(X, Y, vs_2d, cmap=cmap, shading='nearest', vmin=vmin, vmax=vmax)
        
        # Show mesh if requested
        if show_mesh:
            ax.pcolormesh(X, Y, vs_2d, cmap=cmap, shading='nearest', 
                         vmin=vmin, vmax=vmax, edgecolors='k', linewidth=0.5, alpha=0.8)
        
        # Plot topography line if available
        if self.has_topography:
            ax.plot(xmids, elevations, 'k-', linewidth=2, label='Topography')
        
        ax.set_xlabel('Position (m)')
        ax.set_ylabel('Elevation (m)')
        
        # Set aspect ratio
        ax.set_aspect(aspect)
        
        # Add colorbar
        cbar = ax.figure.colorbar(im, ax=ax)
        cbar.set_label('Vs (m/s)')
        
        if self.has_topography:
            ax.legend(loc='upper right')
    
    def plot_final_1d_models(self, ax, show_all=True, color_by='position', plot_style='steps'):
        """Plot final 1D Vs profiles
        
        Parameters:
        -----------
        ax : matplotlib axis
        show_all : bool
            If True, show all models. If False, show only selected
        color_by : str
            'position' or 'misfit' - how to color the models
        plot_style : str
            'steps' or 'curves' - plotting style
        """
        # Get sorted xmid values for color normalization
        xmid_list = sorted(self.inversion_results.keys())
        xmid_min = min(xmid_list)
        xmid_max = max(xmid_list)
        
        # Create colormap normalization
        from matplotlib.colors import Normalize
        from matplotlib.cm import ScalarMappable
        
        if color_by == 'misfit':
            # Color by chi-squared values
            chi2_values = [self.inversion_results[xmid].get('total_chi2', 0) 
                          for xmid in xmid_list if self.inversion_results[xmid].get('success', False)]
            chi2_min = min(chi2_values) if chi2_values else 0
            chi2_max = max(chi2_values) if chi2_values else 1
            norm = Normalize(vmin=chi2_min, vmax=chi2_max)
            cmap = plt.cm.RdYlGn_r  # Red for high misfit, green for low
            sm = ScalarMappable(norm=norm, cmap=cmap)
            colorbar_label = 'χ²'
        else:
            # Color by position
            norm = Normalize(vmin=xmid_min, vmax=xmid_max)
            cmap = plt.cm.viridis
            sm = ScalarMappable(norm=norm, cmap=cmap)
            colorbar_label = 'Position (m)'
        
        for xmid in xmid_list:
            result = self.inversion_results[xmid]
            if not result.get('success', False):
                continue
            
            # Filter by selection if show_all is False
            if not show_all:
                # Check if this xmid is selected (you may need to implement selection tracking)
                pass  # For now, show all
            
            thickness = result['thickness']
            vs_model = result['vs_model']
            
            # Create depth array
            depth = np.hstack([0, np.cumsum(thickness)])
            
            label = f'Xmid={xmid:.1f}m'
            
            # Determine color based on color_by parameter
            if color_by == 'misfit':
                chi2 = result.get('total_chi2', 0)
                color = cmap(norm(chi2))
            else:
                color = cmap(norm(xmid))
            
            # Number of layers is one less than depth boundaries
            n_layers = len(depth) - 1
            
            if plot_style == 'steps':
                # Create step plot with explicit interleaving
                vs_plot = np.repeat(vs_model[:n_layers], 2)  # Each velocity appears twice
                depth_plot = np.empty(2 * n_layers)  # Pre-allocate
                depth_plot[0::2] = depth[:-1]  # Top of each layer
                depth_plot[1::2] = depth[1:]   # Bottom of each layer
                ax.plot(vs_plot, depth_plot, label=label, color=color, alpha=0.7)
            else:  # curves
                # Smooth curves: plot at layer centers
                depth_centers = depth[:-1] + thickness / 2
                ax.plot(vs_model[:n_layers], depth_centers, '-', label=label, color=color, alpha=0.7, 
                       linewidth=1.5)
        
        ax.invert_yaxis()
        ax.set_xlabel('Vs (m/s)')
        ax.set_ylabel('Depth (m)')
        ax.grid(True, alpha=0.3)
        
        # Add colorbar
        cbar = ax.figure.colorbar(sm, ax=ax, label=colorbar_label)
        
        # Show legend only if few positions
        if len(xmid_list) <= 10:
            ax.legend(fontsize=8, loc='best')
        if len(xmid_list) <= 10:
            ax.legend(fontsize=8, loc='best')
    
    def plot_misfit_summary(self, ax):
        """Plot RMS and Chi-squared values vs position
        
        Parameters:
        -----------
        ax : matplotlib axis
        """
        if not self.inversion_results:
            ax.text(0.5, 0.5, 'No inversion results', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Collect misfit data from results
        xmids = []
        rms_values = []
        rrms_values = []
        chi2_values = []
        
        for xmid in sorted(self.inversion_results.keys()):
            result = self.inversion_results[xmid]
            if result.get('success', False):
                xmids.append(xmid)
                rms_values.append(result.get('total_rms', 0))
                rrms_values.append(result.get('rrms', 0))
                chi2_values.append(result.get('total_chi2', 0))
        
        if not xmids:
            ax.text(0.5, 0.5, 'No successful inversions', 
                   ha='center', va='center', transform=ax.transAxes)
            return
        
        # Create twin axis for chi-squared
        ax2 = ax.twinx()
        
        # Plot RMS values
        line1 = ax.plot(xmids, rms_values, 'o-', color='#1f77b4', 
                       linewidth=2, markersize=6, label='RMS (m/s)')
        
        # Plot Chi-squared values on right axis
        line2 = ax2.plot(xmids, chi2_values, 's-', color='#d62728', 
                        linewidth=2, markersize=6, label='χ²')
        
        # Set labels
        ax.set_xlabel('Position (m)')
        ax.set_ylabel('RMS (m/s)', color='#1f77b4')
        ax2.set_ylabel('χ²', color='#d62728')
        
        # Color the y-axis labels
        ax.tick_params(axis='y', labelcolor='#1f77b4')
        ax2.tick_params(axis='y', labelcolor='#d62728')
        
        # Grid
        ax.grid(True, alpha=0.3)
        
        # Combine legends
        lines = line1 + line2
        labels = [l.get_label() for l in lines]
        ax.legend(lines, labels, loc='upper right', fontsize=9)
    
    def plot_picked_and_computed_dc(self, ax):
        """Plot picked dispersion curves with computed curves overlaid"""
        colors = {'M0': '#1f77b4', 'M1': '#ff7f0e', 'M2': '#2ca02c'}
        
        for xmid in sorted(self.positions_data.keys()):
            pos_data = self.positions_data[xmid]
            result = self.inversion_results.get(xmid)
            
            for mode_name, mode_data in pos_data.get('modes', {}).items():
                data_orig = mode_data.get('resampled') or mode_data.get('original')
                if not data_orig:
                    continue
                
                freq = np.asarray(data_orig.get('frequencies', []))
                vel_obs = np.asarray(data_orig.get('velocities', []))
                
                color = colors.get(mode_name, 'gray')
                ax.plot(freq, vel_obs, 'o', color=color, label=f'{mode_name} Picked (x={xmid:.1f}m)', 
                       markersize=4, alpha=0.6)
                
                if result and result.get('success', False) and mode_name in result.get('mode_fits', {}):
                    fit = result['mode_fits'][mode_name]
                    vel_calc = fit['calculated']
                    ax.plot(freq, vel_calc, '-', color=color, linewidth=2, alpha=0.9)
        
        ax.set_xlabel('Frequency (Hz)')
        ax.set_ylabel('Phase Velocity (m/s)')
        ax.grid(True, alpha=0.3)
        # ax.legend(fontsize=7, loc='best')  # Legend removed for clarity
    
    def plot_obs_vs_sim(self, ax):
        """Plot observed vs simulated velocities colored by residuals"""
        all_obs = []
        all_sim = []
        all_resid = []
        
        for xmid, result in self.inversion_results.items():
            if not result.get('success', False):
                continue
            
            for mode_name, fit in result.get('mode_fits', {}).items():
                all_obs.extend(fit['observed'])
                all_sim.extend(fit['calculated'])
                all_resid.extend(fit['residuals'])
        
        if not all_obs:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        all_obs = np.array(all_obs)
        all_sim = np.array(all_sim)
        all_resid = np.array(all_resid)
        
        # Color by residuals using same colormap as residuals plot
        vmax = np.abs(all_resid).max()
        scatter = ax.scatter(all_obs, all_sim, c=all_resid, s=20, alpha=0.6,
                            cmap='RdBu_r', vmin=-vmax, vmax=vmax)
        
        # Add 1:1 line
        lims = [min(all_obs.min(), all_sim.min()), max(all_obs.max(), all_sim.max())]
        ax.plot(lims, lims, 'k--', alpha=0.7, linewidth=1.5, label='1:1 line')
        
        ax.set_xlabel('Observed Velocity (m/s)')
        ax.set_ylabel('Simulated Velocity (m/s)')
        ax.grid(True, alpha=0.3)
        ax.legend()
        ax.set_aspect('equal', adjustable='box')
        ax.figure.colorbar(scatter, ax=ax, label='Residual (m/s)')
    
    def plot_obs_vs_calc_correlation(self, ax, color_by='Observed', cmap='viridis', 
                                     res_range=0.0):
        """Plot observed vs calculated velocities with customizable coloring
        
        Parameters:
        -----------
        ax : matplotlib axis
        color_by : str
            'Observed', 'Calculated', 'Residual (Absolute)', or 'Residual (Relative)'
        cmap : str
            Colormap name
        res_range : float
            Symmetric residual range (0.0 = auto, uses ±max(abs(residual)))
        """
        all_obs = []
        all_calc = []
        all_resid = []
        all_resid_rel = []
        
        for xmid, result in self.inversion_results.items():
            if not result.get('success', False):
                continue
            
            for mode_name, fit in result.get('mode_fits', {}).items():
                obs = np.array(fit['observed'])
                calc = np.array(fit['calculated'])
                resid = np.array(fit['residuals'])
                resid_rel = 100.0 * resid / obs if len(obs) > 0 else np.array([])
                
                all_obs.extend(obs)
                all_calc.extend(calc)
                all_resid.extend(resid)
                all_resid_rel.extend(resid_rel)
        
        if not all_obs:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        all_obs = np.array(all_obs)
        all_calc = np.array(all_calc)
        all_resid = np.array(all_resid)
        all_resid_rel = np.array(all_resid_rel)
        
        # Determine color values based on color_by
        if color_by == 'Observed':
            color_values = all_obs
            cmap_use = cmap
            label = 'Observed Velocity (m/s)'
            vmin, vmax = None, None
        elif color_by == 'Calculated':
            color_values = all_calc
            cmap_use = cmap
            label = 'Calculated Velocity (m/s)'
            vmin, vmax = None, None
        elif color_by == 'Residual (Absolute)':
            color_values = all_resid
            cmap_use = cmap
            label = 'Residual (m/s)'
            # Use custom range if provided, otherwise use symmetric max(abs)
            if res_range != 0.0:
                vmin, vmax = -res_range, res_range
            else:
                vmax_abs = np.abs(all_resid).max()
                vmin, vmax = -vmax_abs, vmax_abs
        else:  # Residual (Relative)
            color_values = all_resid_rel
            cmap_use = cmap
            label = 'Residual (%)'
            # Use custom range if provided, otherwise use symmetric max(abs)
            if res_range != 0.0:
                vmin, vmax = -res_range, res_range
            else:
                vmax_abs = np.abs(all_resid_rel).max()
                vmin, vmax = -vmax_abs, vmax_abs
        
        # Create scatter plot
        scatter = ax.scatter(all_obs, all_calc, c=color_values, s=20, alpha=0.6,
                            cmap=cmap_use, vmin=vmin, vmax=vmax)
        
        # Add 1:1 line
        lims = [min(all_obs.min(), all_calc.min()), max(all_obs.max(), all_calc.max())]
        ax.plot(lims, lims, 'k--', alpha=0.7, linewidth=1.5, label='1:1 line')
        
        ax.set_xlabel('Observed Velocity (m/s)')
        ax.set_ylabel('Calculated Velocity (m/s)')
        ax.grid(True, alpha=0.3)
        ax.legend()
        ax.set_aspect('equal', adjustable='box')
        ax.figure.colorbar(scatter, ax=ax, label=label)
    
    def plot_residuals(self, ax):
        """Plot residuals using same colormap as pseudo-section"""
        all_freq = []
        all_resid = []
        
        for xmid, result in self.inversion_results.items():
            if not result.get('success', False):
                continue
            
            pos_data = self.positions_data[xmid]
            for mode_name, fit in result.get('mode_fits', {}).items():
                mode_data = pos_data.get('modes', {}).get(mode_name, {})
                data_orig = mode_data.get('resampled') or mode_data.get('original')
                if data_orig:
                    freq = np.asarray(data_orig.get('frequencies', []))
                    all_freq.extend(freq)
                    all_resid.extend(fit['residuals'])
        
        if not all_freq:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        all_freq = np.array(all_freq)
        all_resid = np.array(all_resid)
        
        # Use same colormap as pseudo-section (RdBu_r) with symmetric limits
        # Use absolute residuals (same as pseudo-section with relative=False)
        vmax = np.abs(all_resid).max()
        scatter = ax.scatter(all_freq, all_resid, c=all_resid, s=20, alpha=0.6,
                            cmap='RdBu_r', vmin=-vmax, vmax=vmax)
        ax.axhline(y=0, color='black', linestyle='--', alpha=0.7, linewidth=1)
        ax.set_xlabel('Frequency (Hz)')
        ax.set_ylabel('Residual (m/s)')
        ax.grid(True, alpha=0.3)
        ax.figure.colorbar(scatter, ax=ax, label='Residual (m/s)')
    
    def plot_residual_histogram(self, ax, bins=30, relative=False):
        """Plot histogram of residuals
        
        Parameters:
        -----------
        ax : matplotlib axis
        bins : int
            Number of histogram bins
        relative : bool
            If True, plot relative residuals (%), otherwise absolute (m/s)
        """
        all_resid = []
        
        for xmid, result in self.inversion_results.items():
            if not result.get('success', False):
                continue
            
            for mode_name, fit in result.get('mode_fits', {}).items():
                if relative:
                    # Calculate relative residuals as percentage
                    obs = np.array(fit['observed'])
                    resid = np.array(fit['residuals'])
                    rel_resid = 100.0 * resid / obs
                    all_resid.extend(rel_resid)
                else:
                    all_resid.extend(fit['residuals'])
        
        if not all_resid:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        all_resid = np.array(all_resid)
        ax.hist(all_resid, bins=bins, alpha=0.7, edgecolor='black')
        
        # Add statistical markers
        mean_val = np.mean(all_resid)
        median_val = np.median(all_resid)
        std_val = np.std(all_resid)
        p5 = np.percentile(all_resid, 5)
        p95 = np.percentile(all_resid, 95)
        
        ax.axvline(x=0, color='black', linestyle='-', linewidth=1, alpha=0.5, label='Zero')
        ax.axvline(mean_val, color='red', linestyle='-', linewidth=2, label=f'Mean: {mean_val:.2f}')
        ax.axvline(median_val, color='blue', linestyle='--', linewidth=2, label=f'Median: {median_val:.2f}')
        ax.axvline(mean_val - std_val, color='orange', linestyle=':', linewidth=1.5,
                  label=f'Mean±Std: [{mean_val-std_val:.2f}, {mean_val+std_val:.2f}]')
        ax.axvline(mean_val + std_val, color='orange', linestyle=':', linewidth=1.5)
        ax.axvline(p5, color='green', linestyle='-.', linewidth=1.5,
                  label=f'5th/95th: [{p5:.2f}, {p95:.2f}]')
        ax.axvline(p95, color='green', linestyle='-.', linewidth=1.5)
        
        if relative:
            ax.set_xlabel('Residual (%)')
        else:
            ax.set_xlabel('Residual (m/s)')
        
        ax.set_ylabel('Count')
        ax.grid(True, alpha=0.3, axis='y')
        ax.legend(loc='upper right', fontsize=8)
    
    def plot_residual_histogram_split(self, ax, bins=30, relative=False, split_type="Frequency", split_step=5.0):
        """Plot residual histogram split by frequency or wavelength ranges
        
        Parameters:
        -----------
        ax : matplotlib axis
        bins : int
            Number of histogram bins
        relative : bool
            If True, plot relative residuals (%), otherwise absolute (m/s)
        split_type : str
            "Frequency" or "Wavelength"
        split_step : float
            Step size for creating ranges
        """
        all_resid = []
        all_freqs = []
        all_vels = []
        
        for xmid, result in self.inversion_results.items():
            if not result.get('success', False):
                continue
            
            pos_data = self.positions_data[xmid]
            for mode_name, fit in result.get('mode_fits', {}).items():
                mode_data = pos_data.get('modes', {}).get(mode_name, {})
                data_orig = mode_data.get('resampled') or mode_data.get('original')
                if data_orig:
                    frequencies = np.asarray(data_orig.get('frequencies', []))
                    obs = np.array(fit['observed'])
                    resid = np.array(fit['residuals'])
                    
                    if relative:
                        # Calculate relative residuals as percentage
                        rel_resid = 100.0 * resid / obs
                        all_resid.extend(rel_resid)
                    else:
                        all_resid.extend(resid)
                    
                    all_freqs.extend(frequencies)
                    all_vels.extend(obs)
        
        if not all_resid:
            ax.text(0.5, 0.5, 'No data', ha='center', va='center', transform=ax.transAxes)
            return
        
        all_resid = np.array(all_resid)
        all_freqs = np.array(all_freqs)
        all_vels = np.array(all_vels)
        all_wavelengths = all_vels / all_freqs
        
        # Determine splitting parameter
        if split_type == "Wavelength":
            split_values = all_wavelengths
            split_label = "Wavelength (m)"
        else:
            split_values = all_freqs
            split_label = "Frequency (Hz)"
        
        # Create ranges
        min_val = np.floor(split_values.min() / split_step) * split_step
        max_val = np.ceil(split_values.max() / split_step) * split_step
        ranges = np.arange(min_val, max_val + split_step, split_step)
        
        # Calculate common bins for all residual data
        resid_min = all_resid.min()
        resid_max = all_resid.max()
        common_bins = np.linspace(resid_min, resid_max, bins + 1)
        bin_centers = (common_bins[:-1] + common_bins[1:]) / 2
        
        # Calculate histogram for each range
        n_ranges = len(ranges) - 1
        colors = plt.cm.tab10(np.linspace(0, 1, min(n_ranges, 10)))
        
        # Width of each bar group
        bar_width = (common_bins[1] - common_bins[0]) / (n_ranges + 1)
        
        for i in range(n_ranges):
            range_min = ranges[i]
            range_max = ranges[i + 1]
            
            # Filter data in this range
            mask = (split_values >= range_min) & (split_values < range_max)
            range_resid = all_resid[mask]
            
            if len(range_resid) > 0:
                # Calculate histogram
                counts, _ = np.histogram(range_resid, bins=common_bins)
                
                # Offset position for this range
                offset = (i - n_ranges/2 + 0.5) * bar_width
                positions = bin_centers + offset
                
                range_label = f'{range_min:.1f}-{range_max:.1f}'
                ax.bar(positions, counts, width=bar_width * 0.9, alpha=0.8,
                      edgecolor='black', linewidth=0.5,
                      color=colors[i % len(colors)], label=range_label)
        
        # Add zero line
        ax.axvline(x=0, color='black', linestyle='-', linewidth=1.5, alpha=0.5)
        
        if relative:
            ax.set_xlabel('Residual (%)')
        else:
            ax.set_xlabel('Residual (m/s)')
        
        ax.set_ylabel('Count')
        ax.grid(True, alpha=0.3, axis='y')
        ax.legend(title=split_label, loc='upper right', fontsize=7, ncol=2)
    
    def updateResultsTable(self):
        """Update results table with batch inversion summary"""
        if not self.inversion_results:
            return
        
        # Clear existing
        self.results_table.setRowCount(0)
        self.results_table.setColumnCount(10)
        self.results_table.setHorizontalHeaderLabels([
            "Xmid (m)", "Status", "RMS (m/s)", "RRMS (%)", "Chi²",
            "N Modes", "N Data", "Min Vs", "Max Vs", "Depth (m)"
        ])
        
        # Add rows for each position
        for i, (xmid, result) in enumerate(sorted(self.inversion_results.items())):
            self.results_table.insertRow(i)
            
            self.results_table.setItem(i, 0, QTableWidgetItem(f"{xmid:.1f}"))
            
            if result.get('success', False):
                self.results_table.setItem(i, 1, QTableWidgetItem("Success"))
                self.results_table.setItem(i, 2, QTableWidgetItem(f"{result['total_rms']:.3f}"))
                self.results_table.setItem(i, 3, QTableWidgetItem(f"{result['rrms']:.2f}"))
                self.results_table.setItem(i, 4, QTableWidgetItem(f"{result['total_chi2']:.3f}"))
                self.results_table.setItem(i, 5, QTableWidgetItem(str(len(result['mode_fits']))))
                self.results_table.setItem(i, 6, QTableWidgetItem(str(sum(fit['n'] for fit in result['mode_fits'].values()))))
                self.results_table.setItem(i, 7, QTableWidgetItem(f"{np.min(result['vs_model']):.1f}"))
                self.results_table.setItem(i, 8, QTableWidgetItem(f"{np.max(result['vs_model']):.1f}"))
                self.results_table.setItem(i, 9, QTableWidgetItem(f"{np.sum(result['thickness']):.1f}"))
            else:
                self.results_table.setItem(i, 1, QTableWidgetItem("Failed"))
                error = result.get('error', 'Unknown error')
                self.results_table.setItem(i, 2, QTableWidgetItem(error[:50]))
        
        self.results_table.resizeColumnsToContents()
        
        # Also update the batch comparison plots
        self.plotBatchResults()
    
    def plotBatchResults(self):
        """Plot batch comparison results"""
        DCInversionVisualizations.plot_batch_results(
            self.results_figure, self.inversion_results
        )
        
        self.results_canvas.draw()
    
    def exportResults(self):
        """Export inversion results"""
        if not self.inversion_results:
            QMessageBox.warning(self, "No Results", "No inversion results to export")
            return
        
        # Select directory
        directory = QFileDialog.getExistingDirectory(self, "Select Export Directory")
        if not directory:
            return
        
        try:
            for xmid, result in self.inversion_results.items():
                if not result.get('success', False):
                    continue
                
                # Export Vs profile
                vs_file = os.path.join(directory, f"vs_profile_{xmid:.1f}m.txt")
                with open(vs_file, 'w') as f:
                    f.write("# Depth(m)\tVs(m/s)\tVp(m/s)\n")
                    depths = np.concatenate([[0], np.cumsum(result['thickness'])])
                    for i, (d, vs, vp) in enumerate(zip(depths, result['vs_model'], result['vp_model'])):
                        f.write(f"{d:.2f}\t{vs:.2f}\t{vp:.2f}\n")
                
                # Export fitted curves
                for mode_name, fit_data in result['mode_fits'].items():
                    # Get frequencies from position data
                    pos_data = self.positions_data.get(xmid, {})
                    if mode_name in pos_data.get('modes', {}):
                        mode_data = pos_data['modes'][mode_name]
                        data = mode_data.get('resampled', mode_data.get('original'))
                        if data:
                            freq = np.asarray(data['frequencies'])
                            
                            fit_file = os.path.join(directory, f"fit_{xmid:.1f}m_{mode_name}.txt")
                            with open(fit_file, 'w') as f:
                                f.write("# Frequency(Hz)\tObserved(m/s)\tCalculated(m/s)\tResidual(m/s)\n")
                                for fr, obs, calc, res in zip(freq, fit_data['observed'], 
                                                             fit_data['calculated'], fit_data['residuals']):
                                    f.write(f"{fr:.6f}\t{obs:.6f}\t{calc:.6f}\t{res:.6f}\n")
            
            QMessageBox.information(self, "Export Complete",
                f"Results exported to {directory}")
        
        except Exception as e:
            QMessageBox.critical(self, "Export Error", f"Export failed:\n{str(e)}")
    
    def dragEnterEvent(self, event):
        """Handle drag enter"""
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
    
    def dropEvent(self, event):
        """Handle drop event for PVC files"""
        if event.mimeData().hasUrls():
            urls = event.mimeData().urls()
            pvc_files = [url.toLocalFile() for url in urls 
                        if url.toLocalFile().lower().endswith('.pvc')]
            
            if pvc_files:
                self.loadPvcFilesFromPaths(pvc_files)
                event.acceptProposedAction()


def launch_inversion_app(dc_data=None, parent_window=None):
    """
    Launch dispersion inversion app
    
    Args:
        dc_data: dict from main app OR None for standalone
        parent_window: MainWindow instance (optional)
    """
    app = QApplication.instance()
    if app is None:
        app = QApplication(sys.argv)
    
    window = StandaloneDispersionInversionApp(dc_data, parent_window)
    window.show()
    
    if parent_window is None:
        sys.exit(app.exec_())


if __name__ == '__main__':
    launch_inversion_app()
