Anisotropy Theory and Implementation¶
1. What Is Geometric Anisotropy?¶
Spatial correlation in groundwater systems is often directionally dependent. For example, a glacial aquifer may have a much longer correlation length along the direction of ice flow than perpendicular to it. This is called geometric anisotropy: the shape of the variogram (specifically its range) varies with direction, but the sill and nugget remain constant.
Formally, the variogram in direction θ has range a(θ), and the anisotropy is described by:
- Major axis direction (
angle_major): the direction of maximum spatial correlation (longest range) - Major range (
range): the variogram range along the major axis - Minor range:
ratio × range, the variogram range along the perpendicular (minor) axis - Anisotropy ratio (
ratio):minor_range / major_range, constrained to(0, 1]
A ratio of 1.0 means the field is isotropic (equal correlation in all directions). A ratio of 0.5 means the minor-axis range is half the major-axis range.
2. The Pre-Transformation Approach¶
UK_SSPA v2 handles geometric anisotropy via coordinate pre-transformation rather than using PyKrige's internal anisotropy parameters. The approach is:
- Transform the input coordinates from raw space to an isotropic model space using an affine transformation
- Run isotropic kriging in model space (PyKrige's internal anisotropy is disabled)
- Invert the transformation when converting model-space results back to raw space
This approach has several advantages: - It is mathematically equivalent to anisotropic kriging - It allows the same transformation to be applied consistently to drift terms, control points, and prediction grids - It avoids numerical issues that can arise from PyKrige's internal anisotropy handling when combined with specified drift
Critical contract: PyKrige's internal anisotropy parameters (
anisotropy_scaling,anisotropy_angle) are explicitly set to their identity values (scaling=1, angle=0) after pre-transformation. Seekriging.pyfor the variogram clone withanisotropy_enabled=False.
3. The Transformation Formula¶
The full transformation from raw coordinates X to model coordinates X′ is:
X′ = S ⊙ ((X − center) @ R)
Where:
- center = centroid of the observation data [mean(x), mean(y)]
- R = 2D rotation matrix (built from the internally-converted arithmetic angle)
- S = element-wise scaling vector [1, 1/ratio]
- ⊙ = element-wise multiplication
- @ = matrix multiplication (coordinates as row vectors times rotation matrix)
This is implemented in transform.py via apply_transform().
Angle Input and Internal Conversion¶
The user specifies angle_major as an azimuth (clockwise from North), matching the KT3D SETROT convention. Internally, the code converts to an arithmetic angle before building the rotation matrix:
alpha = 90 - azimuth (internal conversion)
Rotation Matrix¶
For the internally-converted angle α (in radians, arithmetic convention):
R = | cos(α) -sin(α) |
| sin(α) cos(α) |
The forward transform applies coordinates as row vectors: coords @ R, which rotates the coordinate system so the major axis aligns with the X-axis.
Scaling Vector¶
S = [1.0, 1/ratio]
The X-axis (major axis after rotation) is left unchanged. The Y-axis (minor axis after rotation) is stretched by 1/ratio to equalize the correlation lengths.
4. Step-by-Step Transformation¶
Given observation coordinates (x, y), angle_major (degrees, azimuth CW from North), and ratio:
Step 1 — Translate to Centroid¶
x_center = mean(x)
y_center = mean(y)
x_c = x - x_center
y_c = y - y_center
This centers the data so that rotation is about the data centroid, not the coordinate origin.
Step 2 — Convert Azimuth to Arithmetic (Internal)¶
alpha = 90 - angle_major # internal conversion
theta = radians(alpha)
This converts the user-facing azimuth angle to the arithmetic angle used by the rotation matrix.
Step 3 — Rotate Using coords @ R¶
R = [[cos(theta), -sin(theta)],
[sin(theta), cos(theta)]]
# Row-vector multiplication: coords @ R
x_r = cos(theta) * x_c + sin(theta) * y_c
y_r = -sin(theta) * x_c + cos(theta) * y_c
After this step, the major axis of correlation is aligned with the positive X-axis in the rotated frame.
Step 4 — Scale the Minor Axis¶
x_prime = x_r * 1.0 # major axis unchanged
y_prime = y_r * (1/ratio) # minor axis stretched
After scaling, the correlation structure is isotropic in model space: the effective range is major_range in all directions.
5. Angle Convention — Explicit Statement¶
angle_majoruses the azimuth convention: Clockwise (CW) from the positive Y-axis (North). This matches the KT3D SETROT convention.
angle_major (azimuth) |
Arithmetic (internal) | Major axis direction |
|---|---|---|
| 0° | 90° | North (positive Y-axis) |
| 30° | 60° | N30E |
| 45° | 45° | Northeast |
| 90° | 0° | East (positive X-axis) |
| 180° | −90° / 270° | South (negative Y-axis) |
| 270° | 180° | West (negative X-axis) |
Examples:
- angle_major = 0° → the major axis of spatial correlation points North; internally converted to 90° arithmetic
- angle_major = 30° → the major axis points N30E; internally converted to 60° arithmetic
- angle_major = 90° → the major axis points East; internally converted to 0° arithmetic
Internal conversion:
alpha = 90 − azimuth. The standard rotation matrix is then built usingalpha. Seedocs/glossary.mdfor the full conversion table.
6. Diagram: Before and After Transformation¶
The following illustrates a point cloud with angle_major = 60° (azimuth, N60E direction), ratio = 0.5:
RAW SPACE MODEL SPACE
(major axis at 60° azimuth = N60E) (major axis aligned with X-axis)
Y Y'
| . . |
| . . . . | . .
| . . . . . . | . . .
| . . . . . | . . .
| . . . | . .
+------------- X +------------- X'
Elongated at N60E direction Elongated along X-axis
(major range >> minor range) (isotropic after Y-stretch)
Transformation steps applied:
1. Translate: subtract centroid → data centered at origin
2. Convert: azimuth 60° → arithmetic 30° (internal)
3. Rotate using coords @ R → major axis now points along +X
4. Scale Y by 1/0.5 = 2 → minor axis stretched to match major axis length
After step 4, the point cloud appears circular (isotropic) in model space, and standard isotropic kriging applies.
7. Inverse Transform¶
To convert model-space coordinates back to raw space (e.g., for output or visualization), the inverse transformation is applied via invert_transform_coords():
X = (X′ / S) @ R^T + center
Step-by-step:
# 1. Un-scale
x_us = x_prime / 1.0
y_us = y_prime / (1/ratio) # = y_prime * ratio
# 2. Un-rotate (transpose of R = inverse for orthogonal matrix)
# Using coords @ R^T
x_ur = cos(theta) * x_us - sin(theta) * y_us
y_ur = sin(theta) * x_us + cos(theta) * y_us
# 3. Un-translate
x = x_ur + x_center
y = y_ur + y_center
The inverse transform is used in predict_on_grid() to convert grid coordinates from raw space to model space before prediction, and is also available for converting model-space outputs back to raw space.
8. Why PyKrige's Internal Anisotropy Is Disabled¶
PyKrige supports anisotropy natively via anisotropy_scaling and anisotropy_angle parameters. When UK_SSPA v2 uses pre-transformation, PyKrige's internal anisotropy must be disabled to avoid double-application:
| Scenario | Result |
|---|---|
| Pre-transform applied + PyKrige anisotropy enabled | Double anisotropy — incorrect, over-corrected |
| Pre-transform applied + PyKrige anisotropy disabled | Correct — isotropic kriging in model space |
| No pre-transform + PyKrige anisotropy enabled | Equivalent (but drift terms would need separate handling) |
In practice, the variogram object is cloned with anisotropy_enabled=False before being passed to PyKrige. This clone preserves all other variogram parameters (sill, range, nugget, model type) but sets anisotropy_scaling=1 and anisotropy_angle=0.
9. The apply_anisotropy Toggle for Linesink Drift¶
The drift_terms.linesink_river configuration supports an apply_anisotropy flag:
"linesink_river": {
"use": true,
"apply_anisotropy": true
}
apply_anisotropy |
Behavior |
|---|---|
true (default) |
Linesink segment geometry is transformed to model space before computing the AEM potential field. The drift term is computed in model space, consistent with the kriging system. |
false |
Linesink geometry remains in raw space for AEM computation. The resulting potential field is computed in raw coordinates, then used as a drift column in the (model-space) kriging system. |
Physical interpretation of apply_anisotropy: false:
Use this when the river's hydraulic influence is better represented in raw geographic space — for example, when the river geometry itself defines the anisotropy direction and you do not want the AEM potential to be distorted by the coordinate transformation. This is an advanced option; the default (true) is appropriate for most cases.
Coordinate space contract: When
apply_anisotropy=false, the AEM drift columns are computed using raw coordinates, but polynomial drift columns and the kriging covariance structure are still computed in model space. The drift matrix columns are concatenated regardless, so the kriging system remains internally consistent — only the geometric interpretation of the AEM term differs.
10. Implementation Reference¶
| Function | File | Purpose |
|---|---|---|
get_transform_params() |
transform.py |
Computes center, R, S from data coordinates and variogram parameters; converts azimuth to arithmetic internally |
apply_transform() |
transform.py |
Applies forward transform: raw → model space via coords @ R then scale |
invert_transform_coords() |
transform.py |
Applies inverse transform: model → raw space |
variogram.clone() |
variogram.py |
Creates a copy with anisotropy_enabled=False for passing to PyKrige |
predict_on_grid() |
kriging.py |
Transforms grid coordinates to model space before prediction |
See Also¶
docs/glossary.md— Angle convention, coordinate spaces, key terminologydocs/api/transform.md— Full API reference fortransform.pydocs/theory/polynomial-drift.md— How drift terms are computed in model spacedocs/validation/vv_transform_roundtrip.py— V&V: roundtrip accuracy of forward + inverse transformdocs/validation/vv_anisotropy_consistency.py— V&V: equivalence of pre-transform approach vs PyKrige internal anisotropy