lsdtt3 utils contents
This document explains what belongs in src/utils/, what does not belong in
src/utils/, and how the utility-layer statistics and comparison functions are
intended to be used.
Purpose of src/utils/
The utils layer is the home for:
- generic mathematical routines
- generic numerical / statistical routines
- lightweight containers or helper types
- metadata-only geometry helpers
- functions that operate on already-valid flat data
- code that does not depend on GDAL / OGR
- code that is not tied to a specific backend implementation
In short, utils should contain things that are reusable across many parts of
lsdtt3, and which could also make sense in another numerical C++ project.
Good examples are:
- descriptive statistics
- histograms, PDFs, CDFs
- binning utilities
- distribution-comparison tests
- circular-data comparison tools
- simple array/container helpers
- pure raster-grid geometry helpers
What should not go into utils
utils is not the place for:
- GDAL / OGR I/O
- CRS parsing or coordinate transformation through GDAL
- raster file loading / writing
- backend factories
- nodata masking rules tied to raster objects
- CPU-specific raster traversal
- GPU-specific kernels
- flow-routing construction
- moving-window extraction over raster objects
- basin extraction from flow-routing objects
Those belong elsewhere:
src/core/for shared infrastructure, GDAL/OGR-aware helpers, I/O, and backend-neutral interfacessrc/cpu/for concrete CPU implementationssrc/gpu/for concrete GPU implementations
The valid-data contract
A core design rule in lsdtt3 is:
utilsroutines should assume the input data are already valid.
This means the utility layer should not do:
- nodata filtering
- mask checking
- raster flattening
- moving-window extraction
- basin extraction
- NaN screening tied to raster semantics
Instead, those tasks should happen before data reach utils.
Why this rule exists
This keeps utils:
- simple
- reusable
- backend-independent
- easy to test
- free from raster-specific policy decisions
It also avoids mixing two distinct concerns:
- What are the valid values?
- What statistics or tests should be computed on those values?
Where nodata handling should happen
Nodata checks and validity filtering belong in the code that owns the data structure or the backend-specific extraction logic.
Typical examples:
-
whole-raster statistics
The raster object or a CPU/GPU raster helper should flatten valid pixels and pass those values toutils. -
moving-window statistics
The moving-window extraction code should collect valid values inside the window and pass those values toutils. -
basin statistics
The basin extraction code should use flow-routing relationships (for example, vias_vector) to collect valid basin values and pass those values toutils.
In practice
That means functions like these conceptually belong outside utils:
flatten_to_valid(...)window_to_valid(...)extract_valid_basin_values(...)
These are naturally CPU/GPU-side or object-side tasks.
Current contents of src/utils/
The utils layer contains the following headers. The sections below describe the long-standing core modules in detail; the rest are summarised in the table.
| Header | Purpose |
|---|---|
lsdtt_array2d |
Lightweight 2D array over a contiguous std::vector |
lsdtt_binary_array2d_tracing |
Boundary tracing on binary images (Gonzalez, Woods & Eddins pp. 557–560); returns row/col in the input array's coordinate system |
lsdtt_distributions |
Parametric distribution fitting for valid flat numeric samples |
lsdtt_histogram |
Histogram, PDF/CDF and binning utilities |
lsdtt_image_filters |
Generic 2-D image-filter primitives on flat row-major arrays |
lsdtt_linear_algebra |
Lightweight linear algebra utilities |
lsdtt_natural_breaks |
Jenks natural-breaks classification (lsdtt_jenks_options) |
lsdtt_numeric_utils |
Numeric helpers, including inclusive start, start + step, … ranges |
lsdtt_polygon_geometry |
Header-only planar polygon geometry |
lsdtt_random |
Fully specified random numbers (splitmix64) |
lsdtt_raster_geometry |
Header-only raster geometry from metadata alone |
lsdtt_regression |
Linear, orthogonal and polynomial regression for valid paired samples |
lsdtt_signal1d |
1-D profile signal processing: moving average, LOWESS, derivative |
lsdtt_splines |
Parametric spline fitting for valid ordered planar data |
lsdtt_statistical_tests |
Nonparametric two-sample tests and distribution distances |
lsdtt_statistics |
Basic 1-D descriptive statistics |
lsdtt_string_utils |
Header-only string helpers shared by CLIs and library |
lsdtt_thread_utils |
Thread-count resolution |
lsdtt_image_filters is worth singling out: it holds the native Otsu,
Canny, Hough and adaptive-threshold primitives that replaced OpenCV. Where a
primitive stands in for an OpenCV routine it reproduces that routine's rounding
rules, and names the routine it mirrors in its comment, so raster-level
operations built on it match the previous output exactly. Its inputs are dense
images — every element is data, with no nodata or mask handling; the raster
layer in src/cpu/ decides what nodata becomes before calling in.
lsdtt_array2d.hpp
A lightweight 2D array wrapper over a contiguous std::vector.
This is a good utils component because it is:
- generic
- reusable
- independent of GDAL
- not backend-specific
lsdtt_raster_geometry.hpp
Header-only raster geometry helpers that work only with raster metadata and grid geometry.
These functions are appropriate for utils because they are:
- metadata-only
- pure geometry helpers
- free of file I/O
- free of reprojection logic
lsdtt_statistics.hpp/.cpp
Descriptive statistics for valid flat numeric samples.
Typical outputs include:
- mean
- median
- percentiles
- variance
- standard deviation
- mean absolute deviation
- median absolute deviation
- interquartile range
These functions assume the caller has already supplied valid values.
lsdtt_histogram.hpp/.cpp
Histogram, PDF, CDF, and binning utilities for valid flat numeric samples.
This module includes:
- equal-width histograms
- log-binned histograms
- natural-breaks binning
- PDF and CDF generation
- binning raw values
- descriptive statistics within bins
lsdtt_statistical_tests.hpp/.cpp
Nonparametric distribution-comparison tools for valid flat numeric samples.
This module is intended for comparing two datasets without assuming normality.
What kind of functions belong in utils
As a rule of thumb, a function belongs in utils if all of the following are
true:
- it works on plain numeric values, vectors, iterator ranges, or simple helper containers
- it does not need GDAL / OGR
- it does not need raster file handles
- it does not need backend selection
- it does not need to know about nodata policy
- it does not care whether the caller is CPU or GPU code
Examples:
compute_descriptive_stats(...)compute_histogram(...)compute_pdf(...)compute_cdf(...)compute_kolmogorov_smirnov_two_sample_test(...)compute_wasserstein_distance(...)
What kind of functions should stay outside utils
A function should probably not go into utils if it needs to know about:
- raster masks
- nodata sentinels
- CRS
- GDAL vector layers
- moving-window boundary handling on a raster object
- flow-routing object internals
- device memory or CUDA kernels
Examples that should stay outside utils:
- “flatten this raster to valid values”
- “extract valid values inside this moving window”
- “extract basin values from this outlet pixel”
- “transform longitude/latitude to raster CRS”
- “load this GeoTIFF and write output COG”
Statistical philosophy in utils
Most lsdtt3 terrain datasets are not normally distributed, so the statistics
and comparison tools in utils should be designed with that in mind.
The emphasis is therefore on:
- robust descriptive statistics
- empirical distribution tools
- nonparametric tests
- transport/distance measures
- circular-data comparisons
Statistical tests and distances in utils
This section explains the main comparison tools and when to use them.
Kolmogorov–Smirnov two-sample test
What it measures
The Kolmogorov–Smirnov (KS) test compares two empirical cumulative distribution functions and finds the maximum absolute difference between them.
Use it when
- you want a general nonparametric comparison of two 1D distributions
- you want a simple, widely known test
- you care about the largest mismatch anywhere in the CDF
Strengths
- simple interpretation
- nonparametric
- no normality assumption
- works directly on continuous 1D data
Limitations
- less sensitive to tail differences than Anderson–Darling
- based on the largest single discrepancy, not overall mismatch
- not the right tool for circular data
Good use cases
- comparing elevation distributions between two basins
- comparing slope distributions between two terrain classes
- checking whether two extracted raster subsets differ overall
Kuiper test
What it measures
The Kuiper test is a circular-data analogue of KS. It uses the sum of the largest positive and negative differences between two circular empirical CDFs.
Use it when
- your data are angular or circular
- values wrap around a period
- you want a circular test that is not biased by where the origin is chosen
Strengths
- suitable for circular variables
- sensitive across the whole circle
- invariant to circular origin choice
Limitations
- test statistic is unitless
- less directly interpretable than a transport distance
- for axial/orientation data you must use the axial version, not the ordinary directional version
Good use cases
- comparing aspect distributions
- comparing flow-direction or bearing distributions
- comparing orientation distributions after converting them appropriately
Directional versus axial circular data
This distinction is extremely important.
Directional / bearing data
Use directional tests when absolute direction matters.
Examples: - aspect as a bearing - downslope azimuth - wind direction - channel azimuth
In directional data:
10°and190°are different0°and360°are equivalent because of periodic wrap
Axial / orientation data
Use axial tests when facing does not matter, only orientation.
Examples: - line orientation - ridge orientation - fracture orientation - any undirected axis
In axial data:
10°and190°are the same orientation- the effective period is
180°rather than360°
Rule
- for bearings: use directional Kuiper and directional circular Wasserstein
- for orientations: use axial Kuiper and axial circular Wasserstein
Wasserstein distance
What it measures
The Wasserstein distance measures how much “transport” is needed to move one distribution into the other.
It is often called an earth-mover distance.
Use it when
- you want to know how different two distributions are
- you want a result in the same units as the data
- you care about effect size, not only a hypothesis test
Strengths
- directly interpretable
- reported in data units
- very useful as a “distance” rather than just a test statistic
Limitations
- not itself a p-value-based test
- standard 1D Wasserstein does not work correctly for circular wrap-around
Good use cases
- comparing two basin elevation distributions in metres
- comparing channel steepness distributions
- asking “how different are these datasets, in the units I care about?”
Circular Wasserstein distance
What it measures
Circular Wasserstein is the circle-aware counterpart of ordinary Wasserstein. It measures transport on a circle, respecting the fact that angular data wrap.
Use it when
- your data are periodic or circular
- you want an angular distance
- you want to quantify difference in angular units such as degrees or radians
Strengths
- interpretable in angular units
- much better than linear Wasserstein for circular data
- complements Kuiper nicely
Good use cases
- comparing aspect distributions
- comparing bearing distributions
- comparing orientation distributions (with the axial version)
Rotation-invariant circular Wasserstein
What it measures
This is a circular Wasserstein distance that allows one global rotation before comparing the two distributions.
Use it when
- you care about distribution shape on the circle
- absolute phase / absolute orientation is not important
- one distribution may simply be rotated relative to the other
Strengths
- separates shape difference from simple angular offset
- useful when circular origin or absolute rotation is arbitrary
Limitations
- answers a different question from ordinary circular Wasserstein
- should not be used if absolute bearing itself is scientifically important
Good use cases
- comparing directional shapes up to rotation
- comparing orientation distributions where absolute reference direction is not the main interest
Cramér–von Mises two-sample test
What it measures
Cramér–von Mises compares two empirical distributions using the overall integrated difference between their empirical CDFs.
Use it when
- you care about overall distribution mismatch, not just the single largest gap
- you want a smoother whole-distribution comparison than KS
Strengths
- sensitive to broad distributional differences
- often complements KS well
Limitations
- less familiar to many users than KS
- interpretation of the statistic is less intuitive than Wasserstein
Good use cases
- comparing overall basin elevation distributions
- comparing overall slope or curvature distributions
Anderson–Darling two-sample test
What it measures
Anderson–Darling is similar in spirit to Cramér–von Mises, but gives more weight to the tails of the distribution.
Use it when
- tail behaviour matters
- extremes are scientifically important
- you care about low-end or high-end differences more strongly
Strengths
- more sensitive than KS to differences in extremes
- particularly useful when tails matter
Limitations
- not as directly interpretable as a distance
- like all such tests, p-values need caution for spatially dependent data
Good use cases
- comparing relief extremes
- comparing high-slope tails
- comparing extreme curvature or roughness behaviour
Energy distance
What it measures
Energy distance is a nonparametric distance between two distributions.
Use it when
- you want a distance-style summary of whole-distribution difference
- you want something complementary to Wasserstein and KS
Strengths
- nonparametric
- zero only when distributions match
- captures overall distributional difference
Limitations
- less directly interpretable in domain units than Wasserstein
- best thought of as a distance measure rather than a simple hypothesis test
Good use cases
- comparing two terrain-property distributions when you want a general distribution-distance measure
- complementing KS or Wasserstein in summary tables
Which test or distance should I use?
If I want a simple general comparison
Use:
- Kolmogorov–Smirnov
If I want to compare overall distribution shape
Use:
- Cramér–von Mises
- energy distance
If I care about tails or extremes
Use:
- Anderson–Darling
If I want an interpretable distance in the same units as the data
Use:
- Wasserstein distance
If my data are circular and absolute bearing matters
Use:
- Kuiper
- circular Wasserstein
If my data are circular and only orientation matters
Use:
- axial Kuiper
- axial circular Wasserstein
If I care about circular distribution shape but not absolute phase
Use:
- rotation-invariant circular Wasserstein
- or its axial counterpart for orientation data
Important caution for raster and basin data
Many standard statistical tests assume observations are approximately independent.
Raster-derived values, windowed values, and basin pixel values are often:
- spatially autocorrelated
- topographically correlated
- not independent in the classical statistical sense
Because of that:
- test statistics are often still useful
- distances are often very useful
- p-values should be interpreted cautiously unless dependence has been handled
In many terrain-analysis workflows, the most useful outputs are often:
- the test statistic
- the effect size or distance
- clear figures of the compared distributions
rather than a p-value alone.
Recommended development rule for utils
When adding a new utility function, ask:
Could this function operate on a vector of valid values from any source, without needing to know about raster masks, nodata values, CRS, or a backend?
If the answer is yes, it probably belongs in utils.
If the answer is no, it probably belongs in:
corecpu- or
gpu
depending on what it depends on.
Summary
src/utils/ should contain:
- generic numerical functions
- generic statistics
- generic histogram/binning utilities
- nonparametric distribution comparison tools
- circular-data comparison tools
- lightweight reusable helper types
src/utils/ should not contain:
- nodata filtering
- raster flattening
- CRS / GDAL logic
- backend-specific traversal
- CPU/GPU-specific extraction logic
The main rule is:
code in
utilsassumes the data are already valid.
That rule keeps utils clean, reusable, and consistent with the overall
architecture of lsdtt3.