diff --git a/docs/index.md b/docs/index.md index 9e1818a..6c8ea8a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,7 +15,11 @@ caption: Usage maxdepth: 1 --- -Installation +usage/installation +usage/3d-modeling +usage/interface +usage/tutorials +usage/faq ``` ```{toctree} diff --git a/docs/static/bounding_box_widget.png b/docs/static/bounding_box_widget.png new file mode 100644 index 0000000..24774f7 Binary files /dev/null and b/docs/static/bounding_box_widget.png differ diff --git a/docs/static/dem_widget.png b/docs/static/dem_widget.png new file mode 100644 index 0000000..5006757 Binary files /dev/null and b/docs/static/dem_widget.png differ diff --git a/docs/static/fault_layers.png b/docs/static/fault_layers.png new file mode 100644 index 0000000..22071bc Binary files /dev/null and b/docs/static/fault_layers.png differ diff --git a/docs/static/fault_topology_hamersley.png b/docs/static/fault_topology_hamersley.png new file mode 100644 index 0000000..57e46ed Binary files /dev/null and b/docs/static/fault_topology_hamersley.png differ diff --git a/docs/static/model-setup.png b/docs/static/model-setup.png new file mode 100644 index 0000000..bfe1d56 Binary files /dev/null and b/docs/static/model-setup.png differ diff --git a/docs/static/src/fault_topology_hamersley.svg b/docs/static/src/fault_topology_hamersley.svg new file mode 100644 index 0000000..dc71f3c --- /dev/null +++ b/docs/static/src/fault_topology_hamersley.svg @@ -0,0 +1,1332 @@ + + + +A. Map showing fault labelsB. Fault abutting relationships diff --git a/docs/static/stratigraphic_column_01.png b/docs/static/stratigraphic_column_01.png new file mode 100644 index 0000000..e960323 Binary files /dev/null and b/docs/static/stratigraphic_column_01.png differ diff --git a/docs/static/stratigraphic_column_02.png b/docs/static/stratigraphic_column_02.png new file mode 100644 index 0000000..cdc7b36 Binary files /dev/null and b/docs/static/stratigraphic_column_02.png differ diff --git a/docs/static/stratigraphic_column_03.png b/docs/static/stratigraphic_column_03.png new file mode 100644 index 0000000..b458d36 Binary files /dev/null and b/docs/static/stratigraphic_column_03.png differ diff --git a/docs/static/stratigraphic_column_04.png b/docs/static/stratigraphic_column_04.png new file mode 100644 index 0000000..a6ebbd8 Binary files /dev/null and b/docs/static/stratigraphic_column_04.png differ diff --git a/docs/static/stratigraphic_layer.png b/docs/static/stratigraphic_layer.png new file mode 100644 index 0000000..3c706b9 Binary files /dev/null and b/docs/static/stratigraphic_layer.png differ diff --git a/docs/usage/3d-modeling.md b/docs/usage/3d-modeling.md new file mode 100644 index 0000000..2dda489 --- /dev/null +++ b/docs/usage/3d-modeling.md @@ -0,0 +1,30 @@ +# 3D Modelling Background + +### Background on 3D Modeling + +A **3D model** is a mathematical representation of a three-dimensional object. In the context of geology, 3D models are used to visualize and analyze the subsurface structure of the Earth. These models are constructed by integrating various types of geological data, such as borehole logs, geological maps, geophysical surveys, and structural measurements. The goal is to create a coherent representation of the subsurface that can be used for exploration, resource management, and scientific research. + + + +#### Implicit Modeling + +Implicit modeling is a modern approach to constructing 3D geological models. Unlike traditional methods that rely on explicit surfaces and manual digitization, implicit modeling uses mathematical functions to represent geological features. These functions are defined over the entire model space and allow for the automatic generation of surfaces, such as stratigraphic boundaries and faults. + +Key advantages of implicit modeling include: +- **Efficiency**: Models can be constructed quickly, even with large datasets. +- **Flexibility**: Implicit methods can handle complex geometries and data uncertainties. +- **Automation**: The process is less reliant on manual interpretation, reducing subjectivity. + +Implicit modeling has become widely used, enabling geoscientists to create detailed and accurate representations of the Earth's subsurface. + +## LoopStructural +LoopStructural is an open-source Python library designed for implicit geological modeling. It provides tools for creating 3D geological models based on various types of input data, including borehole data, surface data, and structural measurements. LoopStructural provides both the implicit modelling algorithms and parameterisation of geological objects. + +### Stratigraphic Modelling +In LoopStructural stratigraphic surfaces can be modelled using implicit functions. The function is approximated to fit observations of the surface for example the location of contacts, the orientation of the surface at the location of a contact. Combined with a stratigraphic column which defines the order of the contacts and any unconformable relationships between them, LoopStructural can interpolate a function which approximates the geometry of the surface. + +### Fault modelling +Faults are modelled in LoopStructural by building three implicit functions defining the fault surface, fault slip vector and the fault extent. Combined with a parametric representation of the fault displacement within these coordinates a kinematic model. + + + diff --git a/docs/usage/faq.md b/docs/usage/faq.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/usage/install/development.md b/docs/usage/install/development.md new file mode 100644 index 0000000..b41bba0 --- /dev/null +++ b/docs/usage/install/development.md @@ -0,0 +1,34 @@ +## Development version + +If you want to install the development version of LoopStructural plugin, you can clone the repository and install the dependencies using `pip`. +1. Clone the repository: + ```bash + git clone https://github.com/Loop3d/plugin_loopstructural.git + ``` +2. Navigate to the cloned directory: + ```bash + cd plugin_loopstructural + ``` +3. Install the required dependencies: + ```bash + pip install -r loopstructural/requirements.txt + ``` +4. To add the plugin to QGIS make a symbolic link between the plugin location and the QGIS plugin folder: + + **linux** + ```bash + ln -s $(pwd)/loopstructural ~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/LoopStructural + ``` +**macOS** + +```bash +ln -s $(pwd)/loopstructural ~/Library/Application\ Support/QGIS/QGIS3/profiles/default/python/plugins/LoopStructural +``` + +**Windows** + +```bash +mklink /D "%APPDATA%\QGIS\QGIS3\profiles\default\python\plugins\LoopStructural" "%cd%\loopstructural" +``` +5. Restart QGIS to load the plugin. +6. Install plugin autoreload diff --git a/docs/usage/install/linux.md b/docs/usage/install/linux.md new file mode 100644 index 0000000..cdca991 --- /dev/null +++ b/docs/usage/install/linux.md @@ -0,0 +1,43 @@ +# Linux + +## Installation Instructions + +### Step 1: Install QGIS +1. Open a terminal on your Linux system. +2. Add the QGIS repository to your system: + ```bash + sudo add-apt-repository ppa:ubuntugis/ubuntugis-unstable + sudo add-apt-repository ppa:qgis/qgis-stable + ``` +3. Update your package list: + ```bash + sudo apt update + ``` +4. Install QGIS: + ```bash + sudo apt install qgis python3-qgis + ``` + + + +### Step 2: Install Dependencies Using pip +1. Ensure you have `pip` installed. If not, install it using: + ```bash + sudo apt install python3-pip + ``` +2. Use `pip` to install the required dependencies for the plugin. Navigate to the directory containing the `requirements` files and run: + ```bash + pip install -e requirements.txt + ``` + +### Step 3: Install the Plugin via QGIS Plugin Manager +1. Open QGIS on your system. +2. Navigate to the **Plugins** menu and select **Manage and Install Plugins**. +3. In the Plugin Manager, search for the plugin by name (e.g., `LoopStructural`). +4. Click **Install Plugin** to download and install it. + +### Step 4: Verify Installation +1. Restart QGIS if necessary. +2. Confirm that the plugin is available under the **Plugins** menu. + +You are now ready to use the plugin on your Linux system! \ No newline at end of file diff --git a/docs/usage/install/macosx.md b/docs/usage/install/macosx.md new file mode 100644 index 0000000..ae36307 --- /dev/null +++ b/docs/usage/install/macosx.md @@ -0,0 +1,36 @@ +# MacOS + +## Installation Instructions Using Homebrew or MacPorts + +### Step 1: Install QGIS +1. Open a terminal on your Mac. +2. Install Homebrew if you haven't already: + ```bash + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + ``` +3. Use Homebrew to install QGIS: + ```bash + brew install qgis + ``` + +### Step 2: Install Dependencies Using pip +1. Ensure you have `pip` installed. If not, install it using: + ```bash + sudo easy_install pip + ``` +2. Use `pip` to install the required dependencies for the plugin. Navigate to the directory containing the `requirements` files and run: + ```bash + pip install -r requirements.txt + ``` + +### Step 3: Install the Plugin via QGIS Plugin Manager +1. Open QGIS on your system. +2. Navigate to the **Plugins** menu and select **Manage and Install Plugins**. +3. In the Plugin Manager, search for the plugin by name (e.g., `LoopStructural`). +4. Click **Install Plugin** to download and install it. + +### Step 4: Verify Installation +1. Restart QGIS if necessary. +2. Confirm that the plugin is available under the **Plugins** menu. + +You are now ready to use the plugin on your MacOS system! \ No newline at end of file diff --git a/docs/usage/install/qpip.md b/docs/usage/install/qpip.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/usage/install/windows.md b/docs/usage/install/windows.md new file mode 100644 index 0000000..aa7c3f9 --- /dev/null +++ b/docs/usage/install/windows.md @@ -0,0 +1,33 @@ +# Installation Instructions for Windows + +## Step 1: Install QGIS +1. Visit the [QGIS official website](https://qgis.org/en/site/forusers/download.html). +2. Download the latest QGIS installer for Windows. +3. Run the installer and follow the on-screen instructions to complete the installation. + +## Step 2: Open the OSGeo4W Shell +1. After installing QGIS, locate the **OSGeo4W Shell**. You can find it in the Start Menu under the QGIS folder. +2. Open the **OSGeo4W Shell**. This shell provides a command-line environment for managing QGIS and its dependencies. + +## Step 3: Install Dependencies Using `pip` +1. In the **OSGeo4W Shell**, ensure that Python is available by typing: + ```bash + python --version + ``` + This should display the Python version bundled with QGIS. +2. Use `pip` to install the required dependencies. Run the following command: + ```bash + pip install LoopStructural pyvista pyvistaqt meshio geoh5py + ``` + +## Step 4: Install the Plugin Using the QGIS Plugin Manager +1. Open QGIS. +2. Navigate to the **Plugins** menu and select **Manage and Install Plugins**. +3. In the Plugin Manager, search for the plugin by name (e.g., "LoopStructural"). +4. Click **Install** to download and install the plugin. +5. Once installed, the plugin will be available in the QGIS interface. + +## Step 5: Verify Installation +1. Restart QGIS to ensure all changes take effect. +2. Check the **Plugins** menu or toolbar for the installed plugin. +3. Open the plugin and verify that it is functioning as expected. diff --git a/docs/usage/installation.md b/docs/usage/installation.md index 832d4c1..6bb5882 100644 --- a/docs/usage/installation.md +++ b/docs/usage/installation.md @@ -4,16 +4,24 @@ This plugin is published on the official QGIS plugins repository: . -## Beta versions released +LoopStructural plugin requires the installation of `LoopStructural, loopsolver, pyvista, pyvistaqt and pyqtgraph`. Optionally meshio and geoh5py can also be installed for exporting surfaces/models into different formats. -Enable experimental extensions in the QGIS plugins manager settings panel. +To install these dependencies you can follow the instructions below for your operating system. -## Earlier development version +### Using QPIP +You can also use the experimental QGIS plugin QPIP which is developed by OPENGIS.ch that manages the Python dependencies for your QGIS environment and keeps the dependencies up to date. -If you define yourself as early adopter or a tester and can't wait for the release, the plugin is automatically packaged for each commit to main, so you can use this address as repository URL in your QGIS extensions manager settings: -```url -https://github.com/Loop3d/loopstructural-qgis//plugins.xml +---- + +```{toctree} +:caption: Installation +:maxdepth: 1 + +install/windows +install/linux +install/macosx + ``` -Be careful, this version can be unstable. + diff --git a/docs/usage/interface.md b/docs/usage/interface.md new file mode 100644 index 0000000..88bbd81 --- /dev/null +++ b/docs/usage/interface.md @@ -0,0 +1,51 @@ +# Plugin interface + +## Selecting Layers +The LoopStructural plugin interfaces with QGIS to define the model input data and parameters. + +### Bounding box +The bounding box defines the spatial extent of the model and can be either specified manually or automatically by calculating the extent from a selected layer or the current view. Not that the bounding box currently has to be axis aligned, meaning that the bounding box is defined by the minimum and maximum x, y and z coordinates. + + +![Bounding Box](../static/bounding_box_widget.png) +### Elevation data +The elevation data is used to define the height of the input data. If a digital elevation model (DEM) is available, the height for all data points will be extracted from the layer. Otherwise a constant elevation can be used. + +If the points being modelled contain a Z coordinate, this can be used instead of the DEM or constant elevation and is selected on a per layer basis. + +![DEM](../static/dem_widget.png) +### Fault layers +The faults trace layer is usually a line layer that contains the trace of the fault. The fault trace is used to define the location of the fault in the model. Optional attributes can be used to further constrain the model: +- **fault name** the name of the fault to be used in the model, if this is left blank the feature ID will be used instead. +- **Dip** the dip of the fault, if this is left blank the fault will be assumed to be vertical. +- **Displacement** - The maximum displacement magnitude of the fault. If this is not specified, a default value will be used. +- **Pitch** - defines the pitch of the fault slip vector in the fault surface. If this is left blank a vertical slip vector is assumed and projected onto the fault surface. + +![Fault Layer](../static/fault_layers.png) +### Stratigraphy +Two layers can be used to constrain the stratigraphy of the model: +1. Basal contacts - this layer defines the basal contacts of the stratigraphy. The layer should contain a line layer with the contact traces. The attributes can be used to define the name of the contact. +2. Structural data - this layer defines the structural data that is used to constrain the model. The layer should contain a point layer with the structural data. The attributes can be used to define the orientation of the data, such as dip and dip direction. + +![Stratigraphic Layer](../static/stratigraphic_layer.png) + +## Stratigraphic Column +The stratigraphic column defines the order of the contacts and any unconformable relationships between them. The column is defined by a list of units - these units are ordered from oldest at the bottom to youngest at the top. Unconformities can be inserted between units to define an unconformable relationship. The thicknesses define the true thickness of each unit and are used to parameterise the interpolation. The unit names should match the names of the contacts in the basal contacts layer. Units without basal contacts can be included in the stratigraphic column but will not be constrained by any data. + +The stratigraphic column can be initialised from the basal contacts layer by clicking the "Initialise from Layer" button. This will create a column with the contacts in the order they are found in the layer. The column can then be edited to add unconformities or change the order of the units. To change the order of units simply drag the units in the list. To add an unconformity, click the "Add Unconformity" button and drag the unconformity the location in the column. + + +![Stratigraphic Column](../static/stratigraphic_column_04.png) + + +## Fault topology relationships + +The fault-fault relationship table defines the interaction between faults in the model. This is used to define abutting relationships and where one fault is faulted by another fault. The fault table is updated whenever the faults layer or fault name field is changed. For each fault the columns indicate whether the fault is abutting to another fault. By clicking the cell the relationship can be toggled between no relationship (white background), abutting (red) and faulted (green). + +![Fault Topology](../static/fault_topology_hamersley.png) + +## Model parameters +Once the layers have been selected, stratigraphic column defined and the fault topology relationships set, the LoopStructural model can be initialised. + +Initialise model will create a LoopStructural model with all of the geological features in the model. For each feature in the model the number of interpolation elements (degrees of freedom), the weighting of the regularisation, contact points and orientation weight can be changed. +![Model Parameters](../static/model-setup.png) diff --git a/docs/usage/tutorials.md b/docs/usage/tutorials.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ffb1fad --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,109 @@ +import os +import pickle +import importlib +import pytest + +from qgis.core import QgsApplication, QgsProcessingContext, QgsProcessingFeedback + + +@pytest.fixture(scope="session", autouse=True) +def qgis_app(): + """Start a headless QGIS application for tests. + + Requires QGIS_PREFIX_PATH to be set in the environment (pointing to the QGIS install). + """ + prefix = os.environ.get("QGIS_PREFIX_PATH") + if not prefix: + raise RuntimeError( + "QGIS_PREFIX_PATH environment variable must be set to your QGIS install path for tests to run." + ) + + app = QgsApplication([], False) + app.setPrefixPath(prefix, True) + app.initQgis() + + yield app + + app.exitQgis() + + +@pytest.fixture +def qgis_context(): + """Return a fresh processing context.""" + return QgsProcessingContext() + + +@pytest.fixture +def feedback(): + """Return a simple QgsProcessingFeedback instance for algorithms.""" + return QgsProcessingFeedback() + + +@pytest.fixture +def ensure_loopstructural(monkeypatch): + """If the real LoopStructural classes are not available, inject minimal fakes into + the algorithm module so tests can run without the external dependency. + + The algorithm module imports FaultTopology and FaultRelationshipType at import-time + and uses those module-level names; this fixture patches the algorithm module + attributes when they are missing. + """ + mod_name = "loopstructural.processing.algorithms.modelling.add_fault_topology" + mod = importlib.import_module(mod_name) + + if getattr(mod, "FaultTopology", None) is not None and getattr(mod, "FaultRelationshipType", None) is not None: + # real dependency present; nothing to do + return + + class _FakeFaultTopology: + def __init__(self, strat_col=None): + self.strat_col = strat_col + self.faults = set() + # store relationships in a dict for simple inspection + self._rels = {} + + def add_fault(self, name): + self.faults.add(name) + + def update_fault_relationship(self, a, b, rel): + self._rels[(a, b)] = rel + + def __repr__(self): + return f"FakeFaultTopology(faults={sorted(self.faults)})" + + class _FakeFaultRelationshipType: + ABUTTING = 1 + + monkeypatch.setattr(mod, "FaultTopology", _FakeFaultTopology, raising=False) + monkeypatch.setattr(mod, "FaultRelationshipType", _FakeFaultRelationshipType, raising=False) + + return + + +@pytest.fixture +def simple_model_pickle(tmp_path): + """Create and return a path to a simple pickled model object suitable for tests. + + The returned object has a `features` attribute with simple feature-like objects + exposing a `name` attribute so the algorithm can find faults. + """ + class DummyFeature: + def __init__(self, name): + self.name = name + + def __repr__(self): + return f"DummyFeature({self.name})" + + class DummyModel: + def __init__(self, names=("fault1", "fault2")): + self.features = [DummyFeature(n) for n in names] + + def __repr__(self): + return f"DummyModel(features={self.features})" + + model = DummyModel() + path = tmp_path / "model.pkl" + with open(path, "wb") as fh: + pickle.dump(model, fh) + + return str(path)