Skip to main content

tsg-xr: A tool for loading TSG datasets into Xarray

The file format associated with The Spectral Geologist™ (and specifically Hylogger™ datasets which have been processed with the software) consists of an ensemble of files:

  • Binary data files containing spectra, high resolutoin imagery and profilometer data
  • Configuration files (principally text, similar in format to TOML)
  • Low resolution core imagery exports (hole overview, per-tray imagery; as JPEG images with associated markup)

tsg-xr heavily leverages the filereader of pytsg to provide access to these data, and presents data in an Xarray format to condense the otherwise complex arrangement. Here pytsg provides an efficient interface to the binary components of the TSG file format, and tsg-xr is largely just arranging this into a condensed data structure which allows easier subseqent use (and serialization to indexable formats, e.g. Zarr).

Usage

tsg-xr is intended to be used to read directories containing ensembles of TSG files; to do so just point the load_tsg funnction at the appropriate directory:

from tsgxr import load_tsg

DT : xarray.DataTree = load_tsg("./Hylogger_Hole_42")

Key array-based data can be accessed directly from this xarray.Datatree object:

DT['NIR/Spectra']: xarray.Dataset
DT['NIR/Products']: xarray.Dataset
DT['TIR/Spectra']: xarray.Dataset
DT['TIR/Products']: xarray.Dataset
DT['Image']: xarray.Dataset
DT['Lidar']: xarray.Dataset

For example, to extract and plot the first metre of core imagery (note here the 'Image' key is repeated twice due to restrictions on the structure):

import matplotlib.pyplot as plt 

image : xarray.DataArray = DT["Image"].ds.Image
DT["Image"].ds.Image.sel(depth=slice(0, 1)).plot.imshow(yincrease=False)
plt.gca().set(aspect="equal"); # fix the aspect ratio

Similarly, to plot the spectra from a specific interval (e.g. 9.2 to 9.3m here) against wavelength, you can provide a slice to the xarray.DataArray.sel method:

spectra : xarray.DataArray = DT["NIR/Spectra"].ds["Spectra"]

spectra.sel(depth=slice(9.2, 9.3)).plot.line(
    x="wavelength", add_legend=False, color="k", alpha=0.5
)

Scalars and other spectral features are also available; spectral feature (centre, depth, width) data is grouped for brevity:

products: xarray.Dataset = DT["NIR"]["Products"].ds

products.Centres
products.Depths
products.Widths
products["Grp1 sTSAS"]
...
products["Min1 sTSAS"]
...
products["Wt1 sTSAS"]
...

Configuration related to integer-encoding of sample data is also included in the dataset attributes:

products.attrs

Installation

The tsg-xr pacakge can be installed standalone into your local environment using pip, or you can create an environment with related dependencies using uv (useful for a development scenario, or if you're only using the tool for a singular project).

Option 1: Standalone Installation

The package is can be installed from PyPI using pip with:

pip install tsgxr

The package is also directly installable from GitHub using pip with:

pip install git+https://github.com/CSIRO-GeoscienceAnalytics/tsg-xr

Option 2: Setup an Environment

This repository is set up to use uv for environment management, and a uv.lock file is included in this repository. After cloning this repository and navigating to this directory, uv venv followed by ./.venv\Scripts\activate and uv sync should create a .venv virtual environment in which to run tsg-xr code. Environments can be activated in terminal environments with .\.venv\Scripts\activate (on Windows).

Data Structure Overview

An example of the data structure used is given below, for the STAVELY_17 hole available from the NVCL (note that the choice of index coordinate - between depth and sample - will affect some of this structure's orientation):

DT : xarray.DataTree = load_tsg(
    "./07e4dcac-5216-44a6-9a6b-0c4c1f7ce7d", index_coord="depth", lazy=True, chunks=512
)
<xarray.DataTree>
Group: /
├── Group: /NIR
│   ├── Group: /NIR/Spectra
│   │       Dimensions:           (depth: 21350, wavelength: 531)
│   │       Coordinates:
│   │         * depth             (depth) float64 171kB 0.004111 0.004112 ... 156.0 156.0
│   │           sample            (depth) int64 171kB 1 4 2 5 0 ... 23290 23269 23251 23294
│   │           tray              (depth) int64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           section           (depth) int64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           section-part      (depth) int64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           section-position  (depth) float64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           hole              (depth) object 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │         * wavelength        (wavelength) float64 4kB 380.0 384.0 ... 2.496e+03 2.5e+03
│   │       Data variables:
│   │           Spectra           (depth, wavelength) float32 45MB dask.array<chunksize=(508, 512), meta=np.ndarray>
│   └── Group: /NIR/Products
│           Dimensions:           (sample: 23375)
│           Coordinates:
│             * sample            (sample) int64 187kB 0 1 2 3 4 ... 23371 23372 23373 23374
│               tray              (sample) int64 187kB 1 1 1 1 1 1 1 ... 50 50 50 50 50 50
│               section           (sample) int64 187kB 1 1 1 1 1 1 1 1 1 ... 4 4 4 4 4 4 4 4
│               section-part      (sample) int64 187kB 1 2 3 4 5 6 ... 121 122 123 124 125
│               depth             (sample) float64 187kB 0.004117 0.004111 ... 156.0 156.0
│               section-position  (sample) float64 187kB 6.264 14.26 22.26 ... 988.0 996.0
│               hole              (sample) object 187kB 'STAVELY_17' ... 'STAVELY_17'
├── Group: /TIR
│   ├── Group: /TIR/Spectra
│   │       Dimensions:           (depth: 21350, wavelength: 341)
│   │       Coordinates:
│   │         * depth             (depth) float64 171kB 0.004111 0.004112 ... 156.0 156.0
│   │           sample            (depth) int64 171kB 1 4 2 5 0 ... 23290 23269 23251 23294
│   │           tray              (depth) int64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           section           (depth) int64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           section-part      (depth) int64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           section-position  (depth) float64 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │           hole              (depth) object 171kB dask.array<chunksize=(508,), meta=np.ndarray>
│   │         * wavelength        (wavelength) float64 3kB 6e+03 6.025e+03 ... 1.45e+04
│   │       Data variables:
│   │           Spectra           (depth, wavelength) float32 29MB dask.array<chunksize=(508, 341), meta=np.ndarray>
│   └── Group: /TIR/Products
│           Dimensions:           (sample: 23375)
│           Coordinates:
│             * sample            (sample) int64 187kB 0 1 2 3 4 ... 23371 23372 23373 23374
│               tray              (sample) int64 187kB 1 1 1 1 1 1 1 ... 50 50 50 50 50 50
│               section           (sample) int64 187kB 1 1 1 1 1 1 1 1 1 ... 4 4 4 4 4 4 4 4
│               section-part      (sample) int64 187kB 1 2 3 4 5 6 ... 121 122 123 124 125
│               depth             (sample) float64 187kB 0.004117 0.004111 ... 156.0 156.0
│               section-position  (sample) float64 187kB 6.264 14.26 22.26 ... 988.0 996.0
│               hole              (sample) object 187kB 'STAVELY_17' ... 'STAVELY_17'
├── Group: /Lidar
│       Dimensions:           (depth: 21350, sample: 23375)
│       Coordinates:
│           depth             (sample) float64 187kB 0.004117 0.004111 ... 156.0 156.0
│         * sample            (sample) int64 187kB 0 1 2 3 4 ... 23371 23372 23373 23374
│           tray              (sample) int64 187kB 1 1 1 1 1 1 1 ... 50 50 50 50 50 50
│           section           (sample) int64 187kB 1 1 1 1 1 1 1 1 1 ... 4 4 4 4 4 4 4 4
│           section-part      (sample) int64 187kB 1 2 3 4 5 6 ... 121 122 123 124 125
│           section-position  (sample) float64 187kB 6.264 14.26 22.26 ... 988.0 996.0
│           hole              (sample) object 187kB 'STAVELY_17' ... 'STAVELY_17'
│       Data variables:
│           Lidar             (depth) float32 85kB 92.44 75.37 75.98 ... 58.43 1.3
└── Group: /Image
        Dimensions:  (depth: 2898500, width: 926, channel: 3, x: 2898500)
        Coordinates:
          * depth    (depth) float32 12MB 0.004111 0.004177 0.004243 ... 156.0 156.0
          * width    (width) float64 7kB -0.03054 -0.03047 -0.0304 ... 0.03047 0.03054
          * channel  (channel) int64 24B 0 1 2
            section  (x) int32 12MB dask.array<chunksize=(512,), meta=np.ndarray>
            tray     (x) int32 12MB dask.array<chunksize=(512,), meta=np.ndarray>
        Dimensions without coordinates: x
        Data variables:
            Image    (depth, width, channel) uint8 8GB dask.array<chunksize=(511, 512, 3), meta=np.ndarray>

Performance Overview

Some rough performance numbers are given below, comparing tsg-xr and pytsg for the STAVELY_17 hylogger dataset (07e4dcac-5216-44a6-9a6b-0c4c1f7ce7d in NVCL shown above, CRAS is 296MB and it has NIR and TIR spectral data totalling 155MB); these were performed with Python 3.13 on Windows using an i7-13850HX (2.10 GHz) reading from a Gen4 NVME.

Note that tsg-xr is not necessarilly faster in loading data (it uses pytsg for some of the basic loading steps and data classes), but it provides a more formatted/annotated data structure, translation of coordinates, and nodata values. Further, the main benefits of lazy loading are for true color imagery, and principally for memory usage where you're not planning to load the whole dataset (at least at once), including where the imagery is of greater size than availabile RAM.

Loading the whole dataset without an image:

> %timeit load_tsg(hyloggerdir, image=False, lazy=False) # tsg-xr
3.04 s ± 158 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

> %timeit load_tsg(hyloggerdir, image=False, lazy=True) # lazy tsg-xr
2.65 s ± 144 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

> %timeit read_package(hyloggerdir, read_cras_file=False) # pytsg
429 ms ± 36.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Loading the dataset with an image:

%timeit load_tsg(hyloggerdir, image=True) # tsg-xr
7.06 s ± 955 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit load_tsg(hyloggerdir, image=True, lazy=True) # lazy tsg-xr
2.98 s ± 372 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit read_package(hyloggerdir, read_cras_file=True) # pytsg
19.5 s ± 9.41 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

Reading a TSG spectral dataset:

> %timeit xarray.open_dataset(tsgfile, engine="tsg") # tsg-xr
1 s ± 23.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

> %timeit  xarray.open_dataset(tsgfile, engine="lazytsg") # lazy tsg-xr
303 ms ± 7.91 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

> %timeit pytsg.parse_tsg.read_tsg_bip_pair(tsgfile, bipfile, "NIR",) # pytsg
192 ms ± 6.42 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Command Line Interface

Converting TSG files to Zarr

A minimal command line interface exists for converstion of TSG files to Zarr archives. Generally, if you're using uv, you would use uv run to do this; otherwise you can directly use the tsgxr entry point where the respective environment is activated.

A selection of configuration options are avialable from the commandline, which can be found under the help menu:

uv run tsgxr tsg2zarr --help

Basic usage is as follows, where <Path> refers to either i) an individual TSG scalars file (.tsg), ii) a Hylogger TSG directory, or iii) a directory containing multiple Hylogger TSG directories (multiple datasets can be converted simultaneously):

uv run tsgxr tsg2zarr <Path>

Outputs are by default added to the Hylogger TSG directories themselves, but can be optionally collated into a separate directory; outputs will use the hole name extracted from the TSG dataset and be specific to the spectra specified (NIR or TIR):

uv run tsgxr tsg2zarr <Path> --output_dir "./collated_zarr_archives/"

Note that by default, this will create zipped Zarr archives. These can be directly opened in e.g. Xarray.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tsgxr-3.0.1.tar.gz (197.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tsgxr-3.0.1-py3-none-any.whl (17.4 kB view details)

Uploaded Python 3

File details

Details for the file tsgxr-3.0.1.tar.gz.

File metadata

  • Download URL: tsgxr-3.0.1.tar.gz
  • Upload date:
  • Size: 197.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for tsgxr-3.0.1.tar.gz
Algorithm Hash digest
SHA256 345b060d80021f4ebbf36d06d87f303899693b4f48e3ceb9609a9ebd18f10413
MD5 097fc775b408520740093461da14d213
BLAKE2b-256 ec8ab297d4bc8ef29aaa2ed1c83f9bbda4f03f2101ce44bf209d2d781c7cc655

See more details on using hashes here.

File details

Details for the file tsgxr-3.0.1-py3-none-any.whl.

File metadata

  • Download URL: tsgxr-3.0.1-py3-none-any.whl
  • Upload date:
  • Size: 17.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for tsgxr-3.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5b18feecf2420ec6e76b42a2207f11a43186c23031c8ee6a1391e8a505916584
MD5 cbc203f8df5327fb035225491d25561e
BLAKE2b-256 a6eaa8874b1dc832bdee9aab9d7decc4dca8f9a716e53691894d057d9b2f54d6

See more details on using hashes here.

Release history Release notifications | RSS feed

3.2.1

2 files

3.2.0

2 files

3.1.1

2 files

This release

3.0.1 This release

2 files

0.2.5

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page