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>)