Skip to content

Architecture

TanagerSpec is not just a bag of functions — it is a deliberately designed system. The guiding idea is that once you learn one part, the rest feels familiar. This page explains the design philosophy, walks through the package architecture, and traces how data and control flow from a raw file to a finished product.

Design philosophy

One object owns the scene

You load a scene once into a single TanagerSpec instance, and everything — preprocessing, plotting, analysis, export — is discoverable on that one object. There is no juggling of helper functions, format converters, and global state. The scene is the API.

from tanagerspec import TanagerSpec

scene = TanagerSpec.from_file("scene.h5")
# from here on, your whole analysis is `scene.<something>`

Accessor namespaces that read like English

Capabilities are grouped into three intuitive namespaces, so the code says what it does:

scene.plot.*          →  see it          (rgb, bands_gallery, pixel_spectra, hunt_pixels, …)
scene.analysis.*      →  compute on it   (calculate_index, dim_reduction, clustering, classify_scene, …)
scene.convert_to.*    →  take it out     (geotiff, envi_bil, xarray)

This separation of concerns keeps the surface tidy and makes the package tab-discoverable — type scene.plot. in a notebook and the right tools are right there. For the complete list of methods and their parameters, see the API Reference.

A consistent grammar across every method

Methods follow the same predictable shape: analysis options first, presentation controls second.

  • Analysis optionspreset, n_components, index_name, mask_threshold, …
  • Presentation controlsplot=, save_png=, save_geotiff=

Learn one method and you can guess the rest. Want a figure? plot=True. Want it saved? save_png="out.png". Want a GIS layer? save_geotiff="out.tif". The same RGB preset system (true_color, false_color_nir, false_color_swir, false_color_urban) is reused everywhere — in scene.plot.rgb, ROI tools, and index context maps alike.

Composable by design — data flows as plain arrays

Index maps, PCA components, cluster labels, and classification maps are all just 2-D / 3-D NumPy arrays. Because the package speaks a common currency, the same tools work on any result. Comparing an NDVI map to a custom index, or to a PCA component, uses the exact same calls you already know (for example scene.analysis.compare_layers).

Stateful and self-aware

A TanagerSpec object tracks its own state. Calling scene.info() reports the current product type, cube shape, band count, dropped bands, and whether the cube has been denoised. Key operations such as preprocess() and denoise() work in place to save memory, and denoise() is idempotent — it refuses to run twice on the same cube. These safeguards help you avoid accidentally repeating work or losing track of your data.

GIS-native by default

Every map is one argument away from your GIS. Clustering and classification accept save_geotiff=, and scene.convert_to bridges to GeoTIFF, ENVI-BIL, and xarray/NetCDF, so results drop straight into QGIS, ArcGIS, ENVI, or any xarray workflow.

Package architecture

The diagram below is the class-level architecture of tanagerspec. It shows the domain models, the loader, the central scene object, and the three namespaces.

TanagerSpec package architecture

Reading the diagram from the data inward:

  • Domain models (frozen dataclasses, core/models.py)CubeMetadata holds per-band metadata (wavelengths, FWHM, units, and the good-band mask); SpectralCube bundles the NumPy data array with its CubeMetadata; and LoadedProduct is the complete output of a load: the SpectralCube, per-pixel masks, an optional grid_info, the detected product_type, and the source_path. HDFEOSGridInfo (io/tanager_grid.py) carries the CRS and affine transform for orthorectified products. These models are frozen to prevent accidental shared-state mutation; updates go through helpers such as CubeMetadata.with_good_wavelengths().

  • TanagerLoader (io/) — opens the HDF5-EOS file, introspects shape and dtype, detects the product type, and creates a LoadedProduct. It is the only sensor-specific component; everything downstream consumes the standardized models.

  • TanagerSpec (the scene object) — wraps exactly one LoadedProduct. It exposes the mutable spectral cube as dataset, surfaces metadata through properties (wavelengths, good_bands, crs, transform), and tracks processing state (dropped_bands, bands_dropped, is_denoised). Its core methods — info(), preprocess(), drop_bands(), denoise(), build_spectral_library() — act directly on the cube.

  • The three namespacesPlotting (scene.plot), Analysis (scene.analysis), and HDF5Converters (scene.convert_to) are attached to the scene at construction. They are thin facades: each validates inputs and forwards to backend functions in viz/, analysis/, and converters/.

  • TanagerSpecChild (base class, core/child_base.py) — every namespace inherits from it. It holds a reference to the parent scene and re-reads the parent's current dataset, wavelengths, good_bands, masks, and grid_info on each call. This is why processing methods can mutate the parent in place and the namespaces immediately see the latest state, with no arguments to thread through.

  • Module-level utilities (utils/)download_scene(), inspect_hdf(), and IndexCatalog are re-exported at the top level of tanagerspec and can be used without constructing a scene.

Data and control flow

A typical analysis moves through the system like this:

flowchart TD
    hdf["HDF5-EOS file"] --> loader["TanagerLoader (io)"]
    loader -->|creates| product["LoadedProduct (core.models)"]
    product --> scene["TanagerSpec"]
    scene --> process["preprocess / drop_bands / denoise (process) — in place"]
    scene --> plot["scene.plot.*"]
    scene --> analysis["scene.analysis.*"]
    scene --> convert["scene.convert_to.*"]
    plot --> viz["viz/ backends"]
    analysis --> analysisBackends["analysis/ backends (exploration, indices, unsupervised, classification)"]
    convert --> converters["converters/ (GeoTIFF, ENVI-BIL, xarray)"]
  1. TanagerSpec.from_file(path) asks TanagerLoader to read the file and return a frozen LoadedProduct.
  2. The scene wraps that product and exposes the cube as a mutable NumPy array.
  3. Optional in-place processing (preprocess, drop_bands, denoise) updates the cube and the good-band mask on the scene.
  4. Visualization, analysis, and export are invoked through the namespaces, which read the scene's current state via TanagerSpecChild and delegate to the backend modules.

For the contributor's view of which file holds each of these pieces — and where to add new ones — continue to Source Layout. For the in-memory cube contract and xarray export conventions, see Conventions.