z7py 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
z7py/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """z7py — Z7 (IGEO7/ISEA7H) discrete global grid indexing for Python.
2
+
3
+ Bit-packed 64-bit Z7 indices with Numba-accelerated neighbour traversal,
4
+ parent/child navigation and monotonic range arithmetic. Pure NumPy/Numba:
5
+ no DGGRID binary and no geospatial stack required.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from .z7 import (
11
+ # Resolution stats
12
+ RESOLUTION_STATS,
13
+ get_resolution_stats,
14
+ get_num_cells,
15
+ get_cell_area_m2,
16
+ get_cell_area_km2,
17
+ get_cls_m,
18
+ get_cls_km,
19
+ find_resolution_by_value,
20
+ find_resolution_by_cls_m,
21
+ find_resolution_by_area_m2,
22
+ find_resolution_by_num_cells,
23
+ # Hex / string / int conversion
24
+ decode_z7hex_index,
25
+ encode_z7hex_index,
26
+ z7hex_to_z7string,
27
+ z7hex_to_z7int,
28
+ z7int_to_z7hex,
29
+ decode_z7int,
30
+ encode_z7int,
31
+ get_z7hex_resolution,
32
+ get_z7hex_local_pos,
33
+ get_z7string_resolution,
34
+ get_z7string_local_pos,
35
+ z7string_to_index,
36
+ index_to_z7string,
37
+ # Index ops (operate on raw uint64)
38
+ get_base_cell,
39
+ get_digit,
40
+ get_digits,
41
+ get_resolution,
42
+ get_parent,
43
+ # Neighbour tables / helpers
44
+ get_base_cell_neighbours,
45
+ get_base_cell_neighbour,
46
+ get_neighbours,
47
+ get_neighbour,
48
+ first_non_zero,
49
+ neighbour_addition_cw,
50
+ neighbour_addition_ccw,
51
+ neighbour_addition_ccw_mod,
52
+ z7_to_monotonic_int,
53
+ monotonic_int_to_z7,
54
+ )
55
+
56
+ from .latitudes import (
57
+ geodetic_to_authalic,
58
+ authalic_to_geodetic,
59
+ WGS84_A,
60
+ WGS84_F,
61
+ )
z7py/latitudes.py ADDED
@@ -0,0 +1,152 @@
1
+ """
2
+ Authalic ↔ Geodetic latitude conversion for WGS84 ellipsoid.
3
+ Translated from z7jl/AuthalicLatitude.jl.
4
+ """
5
+
6
+ import numpy as np
7
+ import numba as nb
8
+
9
+ # The order of the series is a global constant
10
+ POLYNOMIAL_ORDER = 6
11
+
12
+ # WGS84 ellipsoid parameters
13
+ WGS84_A = 6378137.0 # Semi-major axis (meters)
14
+ WGS84_F = 1.0 / 298.257223563 # Flattening
15
+
16
+ # Geodetic to authalic: Coefficients for converting ϕ to ξ.
17
+ # Eq. A19 in Karney (2022)
18
+ # These are the same as in the Julia code, 6x6 matrix.
19
+ AUTHALIC_FWD = np.array([
20
+ [-4/3, -4/45, 88/315, 538/4725, 20824/467775, -44732/2837835],
21
+ [0, 34/45, 8/105, -2482/14175, -37192/467775, -12467764/212837625],
22
+ [0, 0, -1532/2835, -898/14175, 54968/467775, 100320856/1915538625],
23
+ [0, 0, 0, 6007/14175, 24496/467775, -5884124/70945875],
24
+ [0, 0, 0, 0, -23356/66825, -839792/19348875],
25
+ [0, 0, 0, 0, 0, 570284222/1915538625]
26
+ ], dtype=np.float64)
27
+
28
+ # Authalic to geodetic: Coefficients for converting ξ to ϕ.
29
+ # Eq. A20 in Karney (2022)
30
+ AUTHALIC_INV = np.array([
31
+ [4/3, 4/45, -16/35, -2582/14175, 60136/467775, 28112932/212837625],
32
+ [0, 46/45, 152/945, -11966/14175, -21016/51975, 251310128/638512875],
33
+ [0, 0, 3044/2835, 3802/14175, -94388/66825, -8797648/10945935],
34
+ [0, 0, 0, 6059/4725, 41072/93555, -1472637812/638512875],
35
+ [0, 0, 0, 0, 768272/467775, 455935736/638512875],
36
+ [0, 0, 0, 0, 0, 4210684958/1915538625]
37
+ ], dtype=np.float64)
38
+
39
+ # Coefficients for expansion of the normalized meridian arc unit in terms
40
+ # of *n²*, the square of the third flattening.
41
+ # See [Karney 2010](crate::Bibliography::Kar10) eq. (29)
42
+ MERIDIAN_ARC_COEFFICIENTS = np.array([
43
+ 1.0,
44
+ 1.0/4.0,
45
+ 1.0/64.0,
46
+ 1.0/256.0,
47
+ 25.0/16384.0,
48
+ 49.0/65536.0,
49
+ 441.0/1048576.0
50
+ ], dtype=np.float64)
51
+
52
+ @nb.njit(cache=True)
53
+ def horner(arg, coeffs):
54
+ """
55
+ Evaluate a polynomial Σ cᵢ ⋅ xⁱ using the efficient Horner's scheme.
56
+ `coeffs` should be ordered from the lowest power (c₀, c₁, ...) to the highest.
57
+ """
58
+ val = coeffs[-1]
59
+ for i in range(len(coeffs) - 2, -1, -1):
60
+ val = val * arg + coeffs[i]
61
+ return val
62
+
63
+ @nb.njit(cache=True)
64
+ def fourier_sin(angle, coeffs):
65
+ """
66
+ Evaluate a Fourier sine series: Σ cᵢ * sin(i * angle).
67
+ """
68
+ val = 0.0
69
+ for i in range(len(coeffs)):
70
+ val += coeffs[i] * np.sin((i + 1) * angle)
71
+ return val
72
+
73
+ @nb.njit(cache=True)
74
+ def third_flattening(f):
75
+ """
76
+ The third flattening, n = f / (2 - f).
77
+ """
78
+ return f / (2.0 - f)
79
+
80
+ @nb.njit(cache=True)
81
+ def normalized_meridian_arc_unit(f):
82
+ """
83
+ The Normalized Meridian Arc Unit, Qn.
84
+ """
85
+ n = third_flattening(f)
86
+ return horner(n * n, MERIDIAN_ARC_COEFFICIENTS) / (1.0 + n)
87
+
88
+ @nb.njit(cache=True)
89
+ def compute_fourier_coeffs(f, poly_fwd, poly_inv):
90
+ """
91
+ Compute Fourier coefficients by evaluating their corresponding Taylor polynomials.
92
+ """
93
+ n = third_flattening(f)
94
+ fwd = np.empty(POLYNOMIAL_ORDER, dtype=np.float64)
95
+ inv = np.empty(POLYNOMIAL_ORDER, dtype=np.float64)
96
+ for i in range(POLYNOMIAL_ORDER):
97
+ fwd[i] = n * horner(n, poly_fwd[i])
98
+ inv[i] = n * horner(n, poly_inv[i])
99
+
100
+ # etc[0] in Julia corresponds to normalized_meridian_arc_unit
101
+ qn = normalized_meridian_arc_unit(f)
102
+ return fwd, inv, qn
103
+
104
+ @nb.njit(cache=True)
105
+ def _geodetic_to_authalic_rad(phi_rad, fwd_coeffs):
106
+ return phi_rad + fourier_sin(2.0 * phi_rad, fwd_coeffs)
107
+
108
+ @nb.njit(cache=True)
109
+ def _authalic_to_geodetic_rad(xi_rad, inv_coeffs):
110
+ return xi_rad + fourier_sin(2.0 * xi_rad, inv_coeffs)
111
+
112
+ # Precomputed coefficients for WGS84 for performance
113
+ WGS84_FWD, WGS84_INV, WGS84_QN = compute_fourier_coeffs(WGS84_F, AUTHALIC_FWD, AUTHALIC_INV)
114
+
115
+ @nb.njit(cache=True)
116
+ def geodetic_to_authalic(phi, degrees=True):
117
+ """
118
+ Convert geodetic latitude (ϕ) to authalic latitude (ξ) for WGS84.
119
+ """
120
+ phi_rad = np.radians(phi) if degrees else phi
121
+ xi_rad = _geodetic_to_authalic_rad(phi_rad, WGS84_FWD)
122
+ return np.degrees(xi_rad) if degrees else xi_rad
123
+
124
+ @nb.njit(cache=True)
125
+ def authalic_to_geodetic(xi, degrees=True):
126
+ """
127
+ Convert authalic latitude (ξ) to geodetic latitude (ϕ) for WGS84.
128
+ """
129
+ xi_rad = np.radians(xi) if degrees else xi
130
+ phi_rad = _authalic_to_geodetic_rad(xi_rad, WGS84_INV)
131
+ return np.degrees(phi_rad) if degrees else phi_rad
132
+
133
+ # Generic versions that take ellipsoid parameters
134
+ @nb.njit(cache=True)
135
+ def geodetic_to_authalic_custom(phi, f, poly_fwd, degrees=True):
136
+ phi_rad = np.radians(phi) if degrees else phi
137
+ n = third_flattening(f)
138
+ fwd = np.empty(POLYNOMIAL_ORDER, dtype=np.float64)
139
+ for i in range(POLYNOMIAL_ORDER):
140
+ fwd[i] = n * horner(n, poly_fwd[i])
141
+ xi_rad = _geodetic_to_authalic_rad(phi_rad, fwd)
142
+ return np.degrees(xi_rad) if degrees else xi_rad
143
+
144
+ @nb.njit(cache=True)
145
+ def authalic_to_geodetic_custom(xi, f, poly_inv, degrees=True):
146
+ xi_rad = np.radians(xi) if degrees else xi
147
+ n = third_flattening(f)
148
+ inv = np.empty(POLYNOMIAL_ORDER, dtype=np.float64)
149
+ for i in range(POLYNOMIAL_ORDER):
150
+ inv[i] = n * horner(n, poly_inv[i])
151
+ phi_rad = _authalic_to_geodetic_rad(xi_rad, inv)
152
+ return np.degrees(phi_rad) if degrees else phi_rad