qgis-tem-loader

qgis plugin for loading TEM geophysical inversion XYZ files as 3D objects
git clone git://src.adamsgaard.dk/qgis-tem-loader # fast
git clone https://src.adamsgaard.dk/qgis-tem-loader.git # slow
Log | Files | Refs | README | LICENSE Back to index

README.md (8432B)


      1 # TEM Loader
      2 
      3 A QGIS 3.40+ plugin for loading Time-Domain Electromagnetic (TEM) geophysical inversion XYZ files as styled 3D vector layers.
      4 
      5 ## Overview
      6 
      7 TEM Loader parses XYZ inversion model files and loads them into QGIS as three layer types:
      8 
      9 - **Points** — surface terrain positions with survey line labels
     10 - **DOI** — depth-of-investigation points at depth
     11 - **Layers** — resistivity model layers as vertical 3D line segments
     12 
     13 Each file gets its own layer group. Layers are styled with pre-built QML styles and support the QGIS Elevation Profile tool for profile visualization.
     14 
     15 ## Requirements
     16 
     17 - QGIS 3.40.0 or later (up to 4.99)
     18 - Python 3 (bundled with QGIS)
     19 
     20 ## Installation
     21 
     22 1. Download `tem_loader.zip` from the [releases page](https://gitlab.com/qgeomodel/qgis-tem-loader/-/releases) or build it yourself (see below).
     23 2. In QGIS: **Plugins > Manage and Install Plugins > Install from ZIP**.
     24 3. Select the zip file and click **Install Plugin**.
     25 
     26 ## Usage
     27 
     28 ### Load TEM XYZ files
     29 
     30 1. Open a QGIS project.
     31 2. Go to **Plugins > TEM Loader > Load TEM XYZ files…**.
     32 3. Select one or more `.xyz` inversion files.
     33 4. In the import options dialog, optionally enable **Adjust vertical position to digital elevation model** and choose a raster layer from the current project. When enabled, surface elevations are sampled from band 1 of the selected DEM; points outside the DEM keep their source elevation and emit a console warning.
     34 5. If the file metadata declares an EPSG code, imported layers use that CRS; otherwise the loader falls back to the project CRS, then to EPSG:4326.
     35 6. A GeoPackage (`.gpkg`) is written beside each source file with `points`, optional `doi`, and `layers` layers, and the corresponding layers are added to the project with `points` above `doi` above `layers`.
     36 7. Invalid or malformed `.xyz` inputs are rejected with explicit validation errors, and when multiple files are selected the loader continues with the remaining files while reporting the failing input filename.
     37 
     38 ### 3D interpolation
     39 
     40 After loading one or more TEM datasets, the plugin can interpolate their resistivity model into a regular 3D voxel grid:
     41 
     42 1. Load at least one dataset with **Load TEM XYZ files…** so that `layers` (LineStringZ) and optionally `doi` (PointZ) layers are present in the project.
     43 2. Go to **Plugins > TEM Loader > 3D interpolation…**.
     44 3. Select the input `layers` from the list (multi-select). Select any `doi` layers to enable per-sounding DOI clipping.
     45 4. Set the 3D grid extent (X, Y, Z) and cell size. The cell counts are derived from the extent and cell size and shown as a live `nx × ny × nz = N cells` preview that updates as you edit. Use **Use extent of selected input layers** to fill X/Y from the layer bounding box.
     46 5. Adjust IDW parameters (power, log10 mode, horizontal and vertical search radii, and min/max neighbors) and optionally disable DOI clipping. The horizontal and vertical search radii define an anisotropic search ellipsoid: a wide horizontal reach bridges sparse soundings while a tight vertical radius preserves the layering of the resistivity models. Leave **Horizontal search radius** at **Auto** to derive it from the median sounding spacing, **Vertical search radius** at **Auto** to derive it from the median layer thickness, or **Max neighbors** at **Unlimited** to weight every sample in range.
     47 6. Click **OK** and choose an output `.gpkg` path.
     48 7. The resulting `voxels` PointZ layer is added under an **Interpolation** group, styled with the resistivity color ramp, and visible as colored cubes in the 3D Map View.
     49 
     50 The interpolation is pure Python (no external dependencies). For large grids a progress bar is shown and can be cancelled. The grid is capped at 5 000 000 cells to prevent accidental out-of-memory runs; a warning is shown when the cap is exceeded.
     51 
     52 ### Mean resistivity maps
     53 
     54 Mean resistivity maps (Danish: middelmodstandskort) show the mean resistivity of the TEM models within fixed elevation intervals, one GeoTIFF per interval.
     55 
     56 1. Load at least one dataset with **Load TEM XYZ files…** so that `layers` (LineStringZ) and optionally `doi` (PointZ) layers are present in the project.
     57 2. Go to **Plugins > TEM Loader > Mean resistivity maps…**.
     58 3. Select the input `layers` and any `doi` layers (layers named `doi` are preselected).
     59 4. Set the top and bottom elevation and the interval thickness (default 10 m), or use **Use elevation range of selected input layers** to fill the range from the data, snapped to the interval thickness.
     60 5. Set the cell size (default 50 m), the search radius (**Auto** derives it as three times the median sounding spacing), the IDW power, the minimum number of neighbors, and the minimum interval coverage (default 50 %).
     61 6. Click **OK**. One GeoTIFF per interval is written to `<name>_mean_resistivity/` next to the input GeoPackage, as `mean_resistivity_<top>_<bottom>.tif`, and added to a **Mean resistivity** group, top interval first. Only the top map is visible initially.
     62 
     63 Method:
     64 
     65 - For each sounding, the mean resistivity in an interval is the thickness-weighted mean of log10(resistivity) over the parts of the model layers inside the interval. Only the model above the DOI is used: layers are clipped to the DOI at import by default, and selected `doi` layers clip them again.
     66 - A sounding contributes to an interval only when its model covers at least the minimum interval coverage of the interval thickness.
     67 - The per-sounding values are gridded with inverse-distance weighting in log10 space. Grid cells with fewer than the minimum number of soundings within the search radius are left blank (nodata).
     68 - Rasters are single-band Float32 GeoTIFFs in ohm-m with nodata `-9999`, in the CRS of the first input layer, styled with the same resistivity color classes as the `layers` layer. Intervals without data produce no map.
     69 
     70 ## XYZ File Format
     71 
     72 Input files are whitespace-delimited with optional `/`-prefixed metadata lines. The loader currently supports these export variants:
     73 
     74 - **TEMImage** exports with `Line`, `StationNo`, `Z`, `DOI`, `DataResidual`, `NumLayers`, `Res_*`, and `Thick_*`
     75 - **Aarhus Workbench** exports with `LINE_NO`, `ELEVATION`, `RECORD`, `RESDATA`, `RHO_*`, `THK_*`, `DEP_TOP_*`, `DEP_BOT_*`, and `DOI_STANDARD`
     76 - **ATEM SCI Workbench** wide exports with `FID`, `LINE`, `E`, `N`, `DEM`, `RESI1`, `DOI`, `ResNNN`, `ResNNN_doi`, and `ElevNNN`
     77 
     78 For Aarhus Workbench files, the plugin normalizes fields into the existing output schema by:
     79 
     80 - accepting either `X` / `Y` or `UTMX` / `UTMY` as source coordinate headers
     81 - deriving `StationNo` from `LINE_NO` and `RECORD` as `LINE_RECORD`, for example `1_00001`
     82 - mapping `DOI_STANDARD` to the output `DOI` field
     83 - using `ELEVATION` as the surface Z value
     84 - using `DEP_TOP_*` and `DEP_BOT_*` when building layer geometry
     85 
     86 For ATEM SCI Workbench wide files, the plugin normalizes fields by:
     87 
     88 - mapping `E` / `N` to output coordinates
     89 - using `DEM` as the surface Z value
     90 - mapping `RESI1` to `DataResidual`
     91 - deriving `StationNo` from `LINE` and `FID` as `LINE_FID`, for example `100101_00001`
     92 - mapping `DOI` as a depth below `DEM`
     93 - using `ResNNN` for model resistivity and `ElevNNN` as layer-top elevations
     94 - inferring a finite bottom elevation for the final layer so all model resistivity columns render as 3D segments
     95 
     96 Legacy TEMImage files with an extra leading index or project column are handled automatically.
     97 
     98 ## Development
     99 
    100 ```sh
    101 make lint      # run flake8
    102 make test      # run the unittest suite
    103 make verify    # run lint and tests
    104 make package   # build tem_loader.zip for installation
    105 make clean     # remove the zip
    106 ```
    107 
    108 `core.py` (XYZ parsing and CSV helper output) has no QGIS dependency and is fully unit-testable. `interpolate.py` and `mean_resistivity.py` depend only on numpy. `geotiff.py` uses the GDAL Python bindings that ship with QGIS; its tests are skipped when GDAL is not installed. `tem_loader.py` (GeoPackage writing and layer loading) requires a running QGIS instance and is verified with mocked unit tests plus manual QGIS checks.
    109 
    110 ## References
    111 
    112 - Test data from Salling from Madsen et al 2026:
    113   - publication DOI: <https://doi.org/10.1002/gdj3.70067>
    114   - data DOI: <https://doi.org/10.5281/zenodo.16894038>
    115 
    116 ## Repository
    117 
    118 - Source: <https://gitlab.com/qgeomodel/qgis-tem-loader>
    119 - Issues: <https://gitlab.com/qgeomodel/qgis-tem-loader/issues>
    120 - Author: Anders Damsgaard (<andam@geus.dk>)