From 945ac4539f9f961d4f03a27ba64588ac031a1ce7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:53:53 +0000 Subject: [PATCH 1/4] Initial plan From 2783f323c65d423c735b105e76100a7eb7e3a1a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:58:49 +0000 Subject: [PATCH 2/4] Fix fault dip display by retrieving value from stored data Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .../feature_details_panel.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py b/loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py index 446d6dd..0a93bff 100644 --- a/loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py +++ b/loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py @@ -578,9 +578,27 @@ def __init__(self, parent=None, *, fault=None, model_manager=None, data_manager= if fault is None: raise ValueError("Fault must be provided.") self.fault = fault - dip = normal_vector_to_strike_and_dip(fault.fault_normal_vector)[0, 1] + + # Try to get dip from stored fault data first + dip = None + if model_manager is not None and fault.name in model_manager.faults: + fault_data = model_manager.faults[fault.name].get('data') + if fault_data is not None and 'dip' in fault_data.columns and not fault_data.empty: + dip = fault_data['dip'].mean() + + # Fallback: calculate from normal vector if not found in stored data + if dip is None: + try: + dip = normal_vector_to_strike_and_dip(fault.fault_normal_vector)[0, 1] + except Exception: + dip = 90 # Default value if calculation fails pitch = 0 + if model_manager is not None and fault.name in model_manager.faults: + fault_data = model_manager.faults[fault.name].get('data') + if fault_data is not None and 'pitch' in fault_data.columns and not fault_data.empty: + pitch = fault_data['pitch'].mean() + self.fault_parameters = { 'displacement': fault.displacement, 'major_axis_length': fault.fault_major_axis, From 21b82ab27ae8dc28d8154a65fad0397cd5ce950f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:59:47 +0000 Subject: [PATCH 3/4] Add unit tests for fault dip display fix Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- tests/unit/test_fault_dip_display.py | 164 +++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/unit/test_fault_dip_display.py diff --git a/tests/unit/test_fault_dip_display.py b/tests/unit/test_fault_dip_display.py new file mode 100644 index 0000000..0a87c02 --- /dev/null +++ b/tests/unit/test_fault_dip_display.py @@ -0,0 +1,164 @@ +#! python3 + +"""Test fault dip display in the Geological Features panel. + +Usage from the repo root folder: + +.. code-block:: bash + # for whole tests + python -m unittest tests.unit.test_fault_dip_display + # for specific test + python -m unittest tests.unit.test_fault_dip_display.TestFaultDipDisplay.test_dip_from_stored_data +""" + +import unittest +from unittest.mock import MagicMock, Mock +import pandas as pd +import numpy as np + + +class TestFaultDipDisplay(unittest.TestCase): + """Test fault dip retrieval and display.""" + + def setUp(self): + """Set up mock objects for testing.""" + # Mock the fault object + self.mock_fault = Mock() + self.mock_fault.name = "TestFault" + self.mock_fault.displacement = 100 + self.mock_fault.fault_major_axis = 500 + self.mock_fault.fault_minor_axis = 300 + self.mock_fault.fault_intermediate_axis = 400 + + # Mock fault_normal_vector that would give a dip of 90 (vertical fault) + self.mock_fault.fault_normal_vector = np.array([1.0, 0.0, 0.0]) + + # Mock the model manager + self.mock_model_manager = Mock() + self.mock_model_manager.faults = {} + + def test_dip_from_stored_data(self): + """Test that dip is retrieved from stored fault data when available.""" + # Create fault data with a dip of 45 degrees + fault_data = pd.DataFrame({ + 'X': [0, 1, 2], + 'Y': [0, 1, 2], + 'Z': [0, 0, 0], + 'dip': [45, 45, 45] + }) + + self.mock_model_manager.faults['TestFault'] = {'data': fault_data} + + # Import here to avoid issues if PyQt5 is not available + try: + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + + # Create the panel - this should retrieve dip from stored data + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Check that the dip was retrieved from stored data (45 degrees) + # not from the normal vector calculation (which would be 90 degrees) + self.assertEqual(panel.fault_parameters['dip'], 45) + + except ImportError as e: + self.skipTest(f"Cannot import GUI components: {e}") + + def test_dip_fallback_to_normal_vector(self): + """Test that dip falls back to normal vector calculation when not in stored data.""" + # No stored dip data + fault_data = pd.DataFrame({ + 'X': [0, 1, 2], + 'Y': [0, 1, 2], + 'Z': [0, 0, 0] + }) + + self.mock_model_manager.faults['TestFault'] = {'data': fault_data} + + try: + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + from LoopStructural.utils import normal_vector_to_strike_and_dip + + # Calculate expected dip from normal vector + expected_dip = normal_vector_to_strike_and_dip(self.mock_fault.fault_normal_vector)[0, 1] + + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Should fall back to calculating from normal vector + self.assertEqual(panel.fault_parameters['dip'], expected_dip) + + except ImportError as e: + self.skipTest(f"Cannot import GUI components: {e}") + + def test_dip_default_when_no_data(self): + """Test that dip defaults to 90 when no fault data exists.""" + # No fault data at all + self.mock_model_manager.faults = {} + + try: + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Should use a reasonable default or calculate from normal vector + self.assertIsInstance(panel.fault_parameters['dip'], (int, float)) + self.assertGreaterEqual(panel.fault_parameters['dip'], 0) + self.assertLessEqual(panel.fault_parameters['dip'], 90) + + except ImportError as e: + self.skipTest(f"Cannot import GUI components: {e}") + + def test_pitch_from_stored_data(self): + """Test that pitch is also retrieved from stored fault data when available.""" + # Create fault data with pitch + fault_data = pd.DataFrame({ + 'X': [0, 1, 2], + 'Y': [0, 1, 2], + 'Z': [0, 0, 0], + 'dip': [45, 45, 45], + 'pitch': [30, 30, 30] + }) + + self.mock_model_manager.faults['TestFault'] = {'data': fault_data} + + try: + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Check that pitch was retrieved from stored data + self.assertEqual(panel.fault_parameters['pitch'], 30) + + except ImportError as e: + self.skipTest(f"Cannot import GUI components: {e}") + + +if __name__ == "__main__": + unittest.main() From 79a3be199211d62a8fb7ce9485083109a76f722b Mon Sep 17 00:00:00 2001 From: lachlangrose Date: Thu, 19 Feb 2026 14:13:59 +1100 Subject: [PATCH 4/4] refactor: move fault dip display tests to a new file and update structure --- tests/qgis/test_fault_dip_display.py | 147 ++++++++++++++++++++++++ tests/unit/test_fault_dip_display.py | 164 --------------------------- 2 files changed, 147 insertions(+), 164 deletions(-) create mode 100644 tests/qgis/test_fault_dip_display.py delete mode 100644 tests/unit/test_fault_dip_display.py diff --git a/tests/qgis/test_fault_dip_display.py b/tests/qgis/test_fault_dip_display.py new file mode 100644 index 0000000..3b08d43 --- /dev/null +++ b/tests/qgis/test_fault_dip_display.py @@ -0,0 +1,147 @@ +#! python3 + +"""Test fault dip display in the Geological Features panel. + +Usage from the repo root folder: + +.. code-block:: bash + # for whole tests + python -m pytest tests/qgis/test_fault_dip_display.py + # for specific test + python -m pytest tests/qgis/test_fault_dip_display.py::test_dip_from_stored_data +""" + +import unittest +from unittest.mock import MagicMock, Mock +import pandas as pd +import numpy as np + + +class TestFaultDipDisplay(unittest.TestCase): + """Test fault dip retrieval and display.""" + + def setUp(self): + """Set up mock objects for testing.""" + # Mock the fault object + self.mock_fault = Mock() + self.mock_fault.name = "TestFault" + self.mock_fault.displacement = 100 + self.mock_fault.fault_major_axis = 500 + self.mock_fault.fault_minor_axis = 300 + self.mock_fault.fault_intermediate_axis = 400 + + # Mock fault_normal_vector that would give a dip of 90 (vertical fault) + self.mock_fault.fault_normal_vector = np.array([1.0, 0.0, 0.0]) + + # Mock the model manager + self.mock_model_manager = Mock() + self.mock_model_manager.faults = {} + + def test_dip_from_stored_data(self): + """Test that dip is retrieved from stored fault data when available.""" + # Create fault data with a dip of 45 degrees + fault_data = pd.DataFrame({ + 'X': [0, 1, 2], + 'Y': [0, 1, 2], + 'Z': [0, 0, 0], + 'dip': [45, 45, 45] + }) + + self.mock_model_manager.faults['TestFault'] = {'data': fault_data} + + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + + # Create the panel - this should retrieve dip from stored data + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Check that the dip was retrieved from stored data (45 degrees) + # not from the normal vector calculation (which would be 90 degrees) + self.assertEqual(panel.fault_parameters['dip'], 45) + + def test_dip_fallback_to_normal_vector(self): + """Test that dip falls back to normal vector calculation when not in stored data.""" + # No stored dip data + fault_data = pd.DataFrame({ + 'X': [0, 1, 2], + 'Y': [0, 1, 2], + 'Z': [0, 0, 0] + }) + + self.mock_model_manager.faults['TestFault'] = {'data': fault_data} + + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + from LoopStructural.utils import normal_vector_to_strike_and_dip + + # Calculate expected dip from normal vector + expected_dip = normal_vector_to_strike_and_dip(self.mock_fault.fault_normal_vector)[0, 1] + + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Should fall back to calculating from normal vector + self.assertEqual(panel.fault_parameters['dip'], expected_dip) + + def test_dip_default_when_no_data(self): + """Test that dip defaults to 90 when no fault data exists.""" + # No fault data at all + self.mock_model_manager.faults = {} + + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Should use a reasonable default or calculate from normal vector + self.assertIsInstance(panel.fault_parameters['dip'], (int, float)) + self.assertGreaterEqual(panel.fault_parameters['dip'], 0) + self.assertLessEqual(panel.fault_parameters['dip'], 90) + + def test_pitch_from_stored_data(self): + """Test that pitch is also retrieved from stored fault data when available.""" + # Create fault data with pitch + fault_data = pd.DataFrame({ + 'X': [0, 1, 2], + 'Y': [0, 1, 2], + 'Z': [0, 0, 0], + 'dip': [45, 45, 45], + 'pitch': [30, 30, 30] + }) + + self.mock_model_manager.faults['TestFault'] = {'data': fault_data} + + from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( + FaultFeatureDetailsPanel + ) + + panel = FaultFeatureDetailsPanel( + parent=None, + fault=self.mock_fault, + model_manager=self.mock_model_manager, + data_manager=None + ) + + # Check that pitch was retrieved from stored data + self.assertEqual(panel.fault_parameters['pitch'], 30) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_fault_dip_display.py b/tests/unit/test_fault_dip_display.py deleted file mode 100644 index 0a87c02..0000000 --- a/tests/unit/test_fault_dip_display.py +++ /dev/null @@ -1,164 +0,0 @@ -#! python3 - -"""Test fault dip display in the Geological Features panel. - -Usage from the repo root folder: - -.. code-block:: bash - # for whole tests - python -m unittest tests.unit.test_fault_dip_display - # for specific test - python -m unittest tests.unit.test_fault_dip_display.TestFaultDipDisplay.test_dip_from_stored_data -""" - -import unittest -from unittest.mock import MagicMock, Mock -import pandas as pd -import numpy as np - - -class TestFaultDipDisplay(unittest.TestCase): - """Test fault dip retrieval and display.""" - - def setUp(self): - """Set up mock objects for testing.""" - # Mock the fault object - self.mock_fault = Mock() - self.mock_fault.name = "TestFault" - self.mock_fault.displacement = 100 - self.mock_fault.fault_major_axis = 500 - self.mock_fault.fault_minor_axis = 300 - self.mock_fault.fault_intermediate_axis = 400 - - # Mock fault_normal_vector that would give a dip of 90 (vertical fault) - self.mock_fault.fault_normal_vector = np.array([1.0, 0.0, 0.0]) - - # Mock the model manager - self.mock_model_manager = Mock() - self.mock_model_manager.faults = {} - - def test_dip_from_stored_data(self): - """Test that dip is retrieved from stored fault data when available.""" - # Create fault data with a dip of 45 degrees - fault_data = pd.DataFrame({ - 'X': [0, 1, 2], - 'Y': [0, 1, 2], - 'Z': [0, 0, 0], - 'dip': [45, 45, 45] - }) - - self.mock_model_manager.faults['TestFault'] = {'data': fault_data} - - # Import here to avoid issues if PyQt5 is not available - try: - from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( - FaultFeatureDetailsPanel - ) - - # Create the panel - this should retrieve dip from stored data - panel = FaultFeatureDetailsPanel( - parent=None, - fault=self.mock_fault, - model_manager=self.mock_model_manager, - data_manager=None - ) - - # Check that the dip was retrieved from stored data (45 degrees) - # not from the normal vector calculation (which would be 90 degrees) - self.assertEqual(panel.fault_parameters['dip'], 45) - - except ImportError as e: - self.skipTest(f"Cannot import GUI components: {e}") - - def test_dip_fallback_to_normal_vector(self): - """Test that dip falls back to normal vector calculation when not in stored data.""" - # No stored dip data - fault_data = pd.DataFrame({ - 'X': [0, 1, 2], - 'Y': [0, 1, 2], - 'Z': [0, 0, 0] - }) - - self.mock_model_manager.faults['TestFault'] = {'data': fault_data} - - try: - from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( - FaultFeatureDetailsPanel - ) - from LoopStructural.utils import normal_vector_to_strike_and_dip - - # Calculate expected dip from normal vector - expected_dip = normal_vector_to_strike_and_dip(self.mock_fault.fault_normal_vector)[0, 1] - - panel = FaultFeatureDetailsPanel( - parent=None, - fault=self.mock_fault, - model_manager=self.mock_model_manager, - data_manager=None - ) - - # Should fall back to calculating from normal vector - self.assertEqual(panel.fault_parameters['dip'], expected_dip) - - except ImportError as e: - self.skipTest(f"Cannot import GUI components: {e}") - - def test_dip_default_when_no_data(self): - """Test that dip defaults to 90 when no fault data exists.""" - # No fault data at all - self.mock_model_manager.faults = {} - - try: - from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( - FaultFeatureDetailsPanel - ) - - panel = FaultFeatureDetailsPanel( - parent=None, - fault=self.mock_fault, - model_manager=self.mock_model_manager, - data_manager=None - ) - - # Should use a reasonable default or calculate from normal vector - self.assertIsInstance(panel.fault_parameters['dip'], (int, float)) - self.assertGreaterEqual(panel.fault_parameters['dip'], 0) - self.assertLessEqual(panel.fault_parameters['dip'], 90) - - except ImportError as e: - self.skipTest(f"Cannot import GUI components: {e}") - - def test_pitch_from_stored_data(self): - """Test that pitch is also retrieved from stored fault data when available.""" - # Create fault data with pitch - fault_data = pd.DataFrame({ - 'X': [0, 1, 2], - 'Y': [0, 1, 2], - 'Z': [0, 0, 0], - 'dip': [45, 45, 45], - 'pitch': [30, 30, 30] - }) - - self.mock_model_manager.faults['TestFault'] = {'data': fault_data} - - try: - from loopstructural.gui.modelling.geological_model_tab.feature_details_panel import ( - FaultFeatureDetailsPanel - ) - - panel = FaultFeatureDetailsPanel( - parent=None, - fault=self.mock_fault, - model_manager=self.mock_model_manager, - data_manager=None - ) - - # Check that pitch was retrieved from stored data - self.assertEqual(panel.fault_parameters['pitch'], 30) - - except ImportError as e: - self.skipTest(f"Cannot import GUI components: {e}") - - -if __name__ == "__main__": - unittest.main()