interpies-lite 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.
@@ -0,0 +1,24 @@
1
+ """
2
+ interpies_lite
3
+ ==============
4
+
5
+ A from-scratch, dependency-light re-implementation of the key features of
6
+ the original ``interpies`` library: loading geophysical grids, applying
7
+ potential-field transforms (derivatives, ISVD, upward continuation,
8
+ edge-enhancement filters like tilt / HGM / TDR / local wavenumber) and
9
+ plotting with hillshading.
10
+
11
+ Example
12
+ -------
13
+ >>> import interpies_lite as ip
14
+ >>> g = ip.open("data/brtpgrd.gxf")
15
+ >>> g.info()
16
+ >>> g.detrend().tilt().show()
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from interpies_lite.grid import Grid
21
+ from interpies_lite.io import open_rasterio as open
22
+
23
+ __version__ = "0.1.0"
24
+ __all__ = ["Grid", "open", "open_rasterio"]
@@ -0,0 +1,58 @@
1
+ """
2
+ interpies_lite.colors
3
+ =====================
4
+
5
+ Minimal built-in colormaps: Geosoft clra-style, parula, and access to
6
+ matplotlib built-ins. Avoids shipping the 35 KB lookup tables of the
7
+ original library; perceptually uniform maps from matplotlib (viridis,
8
+ plasma, inferno, magma, cividis) are recommended.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import matplotlib.pyplot as plt
13
+ from matplotlib.colors import LinearSegmentedColormap
14
+
15
+ # Geosoft "clra" approximate ramp: blue -> cyan -> green -> yellow -> red
16
+ _CLRA_NODES = [
17
+ (0.00, (0.00, 0.00, 0.50)),
18
+ (0.15, (0.00, 0.00, 1.00)),
19
+ (0.35, (0.00, 0.75, 1.00)),
20
+ (0.50, (0.00, 1.00, 0.50)),
21
+ (0.65, (0.75, 1.00, 0.00)),
22
+ (0.85, (1.00, 0.75, 0.00)),
23
+ (1.00, (0.50, 0.00, 0.00)),
24
+ ]
25
+
26
+ # MATLAB parula key stops
27
+ _PARULA_NODES = [
28
+ (0.00, (0.208, 0.166, 0.529)),
29
+ (0.25, (0.00, 0.387, 0.882)),
30
+ (0.50, (0.00, 0.68, 0.6)),
31
+ (0.75, (0.65, 0.82, 0.25)),
32
+ (1.00, (0.98, 0.90, 0.13)),
33
+ ]
34
+
35
+
36
+ def _make(name: str, nodes) -> LinearSegmentedColormap:
37
+ return LinearSegmentedColormap.from_list(name, nodes)
38
+
39
+
40
+ # register builtins
41
+ _BUILTINS = {
42
+ "geosoft": _make("geosoft", _CLRA_NODES),
43
+ "clra": _make("clra", _CLRA_NODES),
44
+ "parula": _make("parula", _PARULA_NODES),
45
+ }
46
+
47
+
48
+ def load_cmap(name: str = "geosoft"):
49
+ """Return a colormap by name; falls back to matplotlib built-ins."""
50
+ if name in _BUILTINS:
51
+ return _BUILTINS[name]
52
+ if name in plt.colormaps():
53
+ return plt.get_cmap(name)
54
+ raise ValueError(f"Unknown colormap: {name}")
55
+
56
+
57
+ def list_cmaps() -> list[str]:
58
+ return sorted(list(_BUILTINS.keys()) + [c for c in plt.colormaps() if not c.endswith("_r")])
@@ -0,0 +1,162 @@
1
+ """
2
+ interpies_lite.graphics
3
+ =======================
4
+
5
+ Plotting: hillshaded 2D grids with configurable colormap normalisation,
6
+ contours and colorbar.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Literal
11
+
12
+ import numpy as np
13
+ import matplotlib.pyplot as plt
14
+ from matplotlib.colors import Normalize, ListedColormap, BoundaryNorm
15
+
16
+ from interpies_lite import colors as icolors
17
+ from interpies_lite import transforms as T
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # normalisation helpers
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def _auto_norm(data: np.ndarray, mode: str = "equalize") -> Normalize:
25
+ finite = data[np.isfinite(data)]
26
+ if mode == "equalize":
27
+ # histogram-equalise the lookup: resample colors at quantiles
28
+ lo, hi = np.percentile(finite, [2, 98])
29
+ return Normalize(vmin=lo, vmax=hi)
30
+ if mode == "autolevels":
31
+ lo, hi = np.percentile(finite, [2, 98])
32
+ return Normalize(vmin=lo, vmax=hi)
33
+ if mode == "linear":
34
+ return Normalize(vmin=finite.min(), vmax=finite.max())
35
+ raise ValueError(mode)
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # hillshade
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def _hillshade(
43
+ data: np.ndarray,
44
+ cellsize: float,
45
+ zf: float = 10.0,
46
+ azdeg: float = 45.0,
47
+ altdeg: float = 45.0,
48
+ ) -> np.ndarray:
49
+ """Simple hillshade; returns values in [0, 1]."""
50
+ az = np.radians(360.0 - azdeg)
51
+ alt = np.radians(altdeg)
52
+ dy, dx = np.gradient(np.nan_to_num(data) * zf, cellsize, cellsize)
53
+ slope = np.pi / 2.0 - np.arctan(np.sqrt(dx**2 + dy**2))
54
+ aspect = np.arctan2(-dx, dy)
55
+ shaded = np.sin(alt) * np.sin(slope) + np.cos(alt) * np.cos(slope) * np.cos(az - aspect)
56
+ return np.clip(shaded, 0, 1)
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # main show function
61
+ # ---------------------------------------------------------------------------
62
+
63
+ def show_grid(
64
+ grid,
65
+ ax=None,
66
+ cmap: str = "geosoft",
67
+ cmap_norm: Literal["equalize", "autolevels", "linear"] = "equalize",
68
+ hs: bool = True,
69
+ zf: float = 10.0,
70
+ azdeg: float = 45.0,
71
+ altdeg: float = 45.0,
72
+ hs_contrast: float = 1.5,
73
+ cmap_brightness: float = 1.0,
74
+ alpha: float = 0.7,
75
+ contours: bool | int = False,
76
+ colorbar: bool = True,
77
+ cb_contours: bool = False,
78
+ std_range: float = 1.0,
79
+ figsize: tuple[float, float] = (8, 8),
80
+ title: str | None = None,
81
+ **kwargs,
82
+ ):
83
+ """Display a Grid with hillshading and optional contours.
84
+
85
+ Parameters mirror the original interpies ``Grid.show()``.
86
+ """
87
+ data = grid.data
88
+ extent = grid.extent
89
+ cmap_obj = icolors.load_cmap(cmap)
90
+
91
+ # optional brightness tweak
92
+ if cmap_brightness != 1.0:
93
+ arr = cmap_obj(np.linspace(0, 1, 256))
94
+ arr[:, :3] = np.clip(arr[:, :3] * cmap_brightness, 0, 1)
95
+ cmap_obj = ListedColormap(arr)
96
+
97
+ norm = _auto_norm(data, cmap_norm)
98
+
99
+ if ax is None:
100
+ _, ax = plt.subplots(figsize=figsize)
101
+
102
+ # colour layer
103
+ im = ax.imshow(
104
+ np.ma.masked_invalid(data),
105
+ extent=extent,
106
+ origin="upper",
107
+ cmap=cmap_obj,
108
+ norm=norm,
109
+ alpha=alpha if hs else 1.0,
110
+ **kwargs,
111
+ )
112
+
113
+ # hillshade overlay
114
+ if hs:
115
+ shade = _hillshade(data, grid.cellsize, zf=zf, azdeg=azdeg, altdeg=altdeg)
116
+ shade = np.clip((shade - 0.5) * hs_contrast + 0.5, 0, 1)
117
+ ax.imshow(
118
+ shade,
119
+ extent=extent,
120
+ origin="upper",
121
+ cmap="gray",
122
+ alpha=1.0 - alpha,
123
+ zorder=im.get_zorder() - 0.1,
124
+ )
125
+
126
+ # contours
127
+ if contours:
128
+ n_levels = 32 if contours is True else int(contours)
129
+ cs = ax.contour(
130
+ np.ma.masked_invalid(data),
131
+ extent=extent,
132
+ origin="upper",
133
+ levels=n_levels,
134
+ colors="k",
135
+ linewidths=0.4,
136
+ alpha=0.6,
137
+ )
138
+ if cb_contours:
139
+ # draw contour level ticks on the colorbar later
140
+ pass
141
+
142
+ if colorbar:
143
+ cb = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
144
+ cb.set_label(grid.name)
145
+
146
+ ax.set_title(title or grid.name)
147
+ ax.set_xlabel("Easting")
148
+ ax.set_ylabel("Northing")
149
+ return ax
150
+
151
+
152
+ def save_grid_image(grid, output_file: str, dpi: int = 150, **kwargs) -> None:
153
+ """Render the grid without title/colorbar/annotations and save to file."""
154
+ kwargs.setdefault("colorbar", False)
155
+ kwargs.setdefault("title", "")
156
+ ax = show_grid(grid, **kwargs)
157
+ ax.set_xlabel("")
158
+ ax.set_ylabel("")
159
+ ax.set_xticks([])
160
+ ax.set_yticks([])
161
+ ax.figure.savefig(output_file, dpi=dpi, bbox_inches="tight", pad_inches=0)
162
+ plt.close(ax.figure)
interpies_lite/grid.py ADDED
@@ -0,0 +1,318 @@
1
+ """
2
+ interpies_lite.grid
3
+ ===================
4
+
5
+ The :class:`Grid` object: a 2D array with georeferencing, plus all the
6
+ potential-field methods (derivatives, enhancements, continuation) that make
7
+ interpies useful.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+
17
+ from interpies_lite import transforms as T
18
+
19
+
20
+ @dataclass
21
+ class Grid:
22
+ """A single-band georeferenced raster.
23
+
24
+ Attributes
25
+ ----------
26
+ data : 2D float array (nodata -> NaN)
27
+ transform : 6-tuple or affine.Affine
28
+ Affine mapping (row, col) -> (x, y).
29
+ crs : str or None
30
+ Coordinate reference system (EPSG:xxxx string or PROJ.4).
31
+ name, filename : str
32
+ nodata : float or None
33
+ """
34
+
35
+ data: np.ndarray
36
+ transform: Any = None
37
+ crs: str | None = None
38
+ name: str = "Unknown"
39
+ filename: str = "Unknown"
40
+ nodata: float | None = None
41
+
42
+ # ---------- post-init derived attributes ----------
43
+ def __post_init__(self) -> None:
44
+ self.data = np.asarray(self.data, dtype=float)
45
+ if self.nodata is not None:
46
+ self.data[self.data == self.nodata] = np.nan
47
+ if self.transform is None:
48
+ # default: origin (0,0), 100 m cells
49
+ self.transform = (100.0, 0.0, 0.0, 0.0, -100.0, 0.0)
50
+
51
+ # ---------- derived properties ----------
52
+ @property
53
+ def nrows(self) -> int:
54
+ return self.data.shape[0]
55
+
56
+ @property
57
+ def ncols(self) -> int:
58
+ return self.data.shape[1]
59
+
60
+ @property
61
+ def cellsize(self) -> float:
62
+ return abs(float(self.transform[0]))
63
+
64
+ @property
65
+ def y_cellsize(self) -> float:
66
+ return abs(float(self.transform[4]))
67
+
68
+ @property
69
+ def x_origin(self) -> float:
70
+ """X of upper-left corner."""
71
+ return float(self.transform[2])
72
+
73
+ @property
74
+ def y_origin(self) -> float:
75
+ """Y of upper-left corner."""
76
+ return float(self.transform[5])
77
+
78
+ @property
79
+ def extent(self) -> list[float]:
80
+ """matplotlib-style extent: [left, right, bottom, top]."""
81
+ left = self.x_origin
82
+ right = left + self.ncols * self.cellsize
83
+ top = self.y_origin
84
+ bottom = top - self.nrows * self.y_cellsize
85
+ return [left, right, bottom, top]
86
+
87
+ @property
88
+ def mask(self) -> np.ndarray:
89
+ return ~np.isfinite(self.data)
90
+
91
+ # ---------- constructors ----------
92
+ @classmethod
93
+ def from_array(
94
+ cls,
95
+ array: np.ndarray,
96
+ west: float = 0.0,
97
+ north: float = 0.0,
98
+ cellsize: float = 100.0,
99
+ y_cellsize: float | None = None,
100
+ crs: str | None = None,
101
+ name: str = "Unknown",
102
+ ) -> "Grid":
103
+ ys = y_cellsize if y_cellsize is not None else cellsize
104
+ transform = (cellsize, 0.0, west, 0.0, -ys, north)
105
+ return cls(data=np.asarray(array), transform=transform, crs=crs, name=name)
106
+
107
+ @classmethod
108
+ def from_dataset(cls, dataset, band: int = 1, name: str | None = None) -> "Grid":
109
+ """Build a Grid from an open rasterio dataset."""
110
+ data = dataset.read(band).astype(float)
111
+ crs = dataset.crs.to_string() if dataset.crs is not None else None
112
+ return cls(
113
+ data=data,
114
+ transform=dataset.transform,
115
+ crs=crs,
116
+ name=name or os.path.splitext(os.path.basename(dataset.name))[0],
117
+ filename=dataset.name,
118
+ nodata=dataset.nodata,
119
+ )
120
+
121
+ # ---------- IO ----------
122
+ def save(self, output_file: str) -> None:
123
+ """Write as GeoTIFF."""
124
+ try:
125
+ import rasterio
126
+ except ImportError as e:
127
+ raise RuntimeError(
128
+ "rasterio is required for save(); install with `pip install rasterio`"
129
+ ) from e
130
+ profile = {
131
+ "driver": "GTiff",
132
+ "height": self.nrows,
133
+ "width": self.ncols,
134
+ "count": 1,
135
+ "dtype": "float64",
136
+ "crs": self.crs,
137
+ "transform": self.transform,
138
+ }
139
+ with rasterio.open(output_file, "w", **profile) as dst:
140
+ dst.write(np.nan_to_num(self.data), 1)
141
+
142
+ # ---------- geometry / preprocessing ----------
143
+ def _new(self, data: np.ndarray, name: str | None = None, **kw) -> "Grid":
144
+ base = dict(transform=self.transform, crs=self.crs, filename=self.filename, nodata=self.nodata)
145
+ base.update(kw)
146
+ return Grid(data=data, name=name or self.name, **base)
147
+
148
+ def clip(self, xmin: float, xmax: float, ymin: float, ymax: float) -> "Grid":
149
+ left, right, bottom, top = self.extent
150
+ c1 = max(0, int(np.floor((xmin - left) / self.cellsize)))
151
+ c2 = min(self.ncols, int(np.ceil((xmax - left) / self.cellsize)))
152
+ r1 = max(0, int(np.floor((top - ymax) / self.y_cellsize)))
153
+ r2 = min(self.nrows, int(np.ceil((top - ymin) / self.y_cellsize)))
154
+ sub = self.data[r1:r2, c1:c2]
155
+ new_left = left + c1 * self.cellsize
156
+ new_top = top - r1 * self.y_cellsize
157
+ new_transform = (self.transform[0], 0.0, new_left, 0.0, self.transform[4], new_top)
158
+ return self._new(sub, name=f"{self.name}_clip", transform=new_transform)
159
+
160
+ def resample(self, sampling: int = 2) -> "Grid":
161
+ sub = T.simple_resample(self.data, sampling)
162
+ new_cs = self.cellsize * sampling
163
+ new_ycs = self.y_cellsize * sampling
164
+ new_transform = (new_cs, 0.0, self.x_origin, 0.0, -new_ycs, self.y_origin)
165
+ return self._new(sub, name=f"{self.name}_res{sampling}", transform=new_transform)
166
+
167
+ def scale(self, factor: float) -> "Grid":
168
+ return self._new(self.data * factor, name=f"{self.name}_scaled")
169
+
170
+ def detrend(self, degree: int = 1, sampling: int = 4) -> "Grid":
171
+ sub = T.simple_resample(np.nan_to_num(self.data), sampling)
172
+ ny, nx = sub.shape
173
+ ys = np.linspace(self.y_origin, self.y_origin - ny * self.y_cellsize * sampling, ny)
174
+ xs = np.linspace(self.x_origin, self.x_origin + nx * self.cellsize * sampling, nx)
175
+ XX, YY = np.meshgrid(xs, ys)
176
+ trend_sub = T.find_trend(np.c_[XX.ravel(), YY.ravel()], sub.ravel(), degree=degree)
177
+ # resize trend back to original shape
178
+ from scipy.ndimage import zoom
179
+ zy = self.nrows / ny
180
+ zx = self.ncols / nx
181
+ trend_full = zoom(trend_sub.reshape(ny, nx), (zy, zx), order=1)
182
+ return self._new(self.data - trend_full, name=f"{self.name}_detrend{degree}")
183
+
184
+ def fill_nodata(self) -> "Grid":
185
+ return self._new(T.fill_nodata(self.data), name=f"{self.name}_filled")
186
+
187
+ def smooth(self, method: str = "SG", deg: int = 3, win: int = 5, sigma: float = 1.0) -> "Grid":
188
+ if method.upper() == "SG":
189
+ out = T.savgol_smooth(self.data, deg=deg, win=win)
190
+ else:
191
+ out = T.gauss(self.data, sigma=sigma)
192
+ return self._new(out, name=f"{self.name}_smooth")
193
+
194
+ def laplacian(self) -> "Grid":
195
+ return self._new(T.laplacian(self.data, self.cellsize), name=f"{self.name}_lap")
196
+
197
+ def info(self) -> None:
198
+ s = T.stats(self.data)
199
+ print(f"Grid: {self.name}")
200
+ print(f" file : {self.filename}")
201
+ print(f" shape : {self.nrows} rows x {self.ncols} cols")
202
+ print(f" cellsize : {self.cellsize} x {self.y_cellsize}")
203
+ print(f" extent : {self.extent}")
204
+ print(f" crs : {self.crs}")
205
+ print(f" min/max : {s['min']:.3f} / {s['max']:.3f}")
206
+ print(f" mean/std : {s['mean']:.3f} +/- {s['std']:.3f}")
207
+
208
+ # ---------- derivatives ----------
209
+ def dx(self, method: str = "SG", deg: int = 3, win: int = 5, fs_tap: int = 5) -> "Grid":
210
+ if method.upper() == "FS":
211
+ out = T.fs_deriv(self.data, self.cellsize, "dx", tap=fs_tap)
212
+ elif method.lower() == "fourier":
213
+ out = T.fourier_transform(self.data, self.cellsize, trans="dx", order=1)
214
+ else:
215
+ out = T.savgol_deriv(self.data, self.cellsize, "dx", deg=deg, win=win)
216
+ return self._new(out, name=f"{self.name}_dx")
217
+
218
+ def dy(self, method: str = "SG", deg: int = 3, win: int = 5, fs_tap: int = 5) -> "Grid":
219
+ if method.upper() == "FS":
220
+ out = T.fs_deriv(self.data, self.cellsize, "dy", tap=fs_tap)
221
+ elif method.lower() == "fourier":
222
+ out = T.fourier_transform(self.data, self.cellsize, trans="dy", order=1)
223
+ else:
224
+ out = T.savgol_deriv(self.data, self.cellsize, "dy", deg=deg, win=win)
225
+ return self._new(out, name=f"{self.name}_dy")
226
+
227
+ def dx2(self, method: str = "SG", deg: int = 4, win: int = 5, fs_tap: int = 5) -> "Grid":
228
+ return self.dx(method=method, deg=deg, win=win, fs_tap=fs_tap).dx(method=method, deg=deg, win=win, fs_tap=fs_tap)
229
+
230
+ def dy2(self, method: str = "SG", deg: int = 4, win: int = 5, fs_tap: int = 5) -> "Grid":
231
+ return self.dy(method=method, deg=deg, win=win, fs_tap=fs_tap).dy(method=method, deg=deg, win=win, fs_tap=fs_tap)
232
+
233
+ def dxdy(self, method: str = "SG", deg: int = 3, win: int = 5, fs_tap: int = 5) -> "Grid":
234
+ return self.dx(method=method, deg=deg, win=win, fs_tap=fs_tap).dy(method=method, deg=deg, win=win, fs_tap=fs_tap)
235
+
236
+ def dz(self, method: str = "fourier", order: int = 1, **kw) -> "Grid":
237
+ if method.lower() == "fourier":
238
+ out = T.fourier_transform(self.data, self.cellsize, trans="dz", order=order, **kw)
239
+ else:
240
+ out = T.isvd(self.data, self.cellsize, method=method, order=order, **kw)
241
+ return self._new(out, name=f"{self.name}_dz{order}")
242
+
243
+ def isvd(self, method: str = "SG", order: int = 1, deg: int = 3, win: int = 5, fs_tap: int = 5) -> "Grid":
244
+ out = T.isvd(self.data, self.cellsize, method=method, order=order, deg=deg, win=win, fs_tap=fs_tap)
245
+ return self._new(out, name=f"{self.name}_dz{order}")
246
+
247
+ def vi(self, order: int = 1, eps: float = 1e-6) -> "Grid":
248
+ out = T.fourier_transform(self.data, self.cellsize, trans="vi", order=order, eps=eps)
249
+ return self._new(out, name=f"{self.name}_vi{order}")
250
+
251
+ # ---------- enhancement transforms ----------
252
+ def hgm(self, method: str = "SG", deg: int = 3, win: int = 5, fs_tap: int = 5) -> "Grid":
253
+ gx = self.dx(method=method, deg=deg, win=win, fs_tap=fs_tap).data
254
+ gy = self.dy(method=method, deg=deg, win=win, fs_tap=fs_tap).data
255
+ return self._new(np.sqrt(gx * gx + gy * gy), name=f"{self.name}_HGM")
256
+
257
+ def tilt(self, hgm_method: str = "SG", dz_method: str = "isvd",
258
+ deg: int = 3, win: int = 5, fs_tap: int = 5, alpha: float = 1.0) -> "Grid":
259
+ hgm_grid = self.hgm(method=hgm_method, deg=deg, win=win, fs_tap=fs_tap)
260
+ if dz_method.lower() == "isvd":
261
+ dz_grid = self.isvd(method=hgm_method, order=1, deg=deg, win=win, fs_tap=fs_tap)
262
+ else:
263
+ dz_grid = self.dz(method="fourier", order=1)
264
+ out = np.arctan(alpha * dz_grid.data / hgm_grid.data) * 180.0 / np.pi
265
+ return self._new(out, name=f"{self.name}_tilt")
266
+
267
+ def tg(self, method: str = "SG", dz_method: str = "isvd",
268
+ deg: int = 3, win: int = 5, fs_tap: int = 5) -> "Grid":
269
+ gx = self.dx(method=method, deg=deg, win=win, fs_tap=fs_tap).data
270
+ gy = self.dy(method=method, deg=deg, win=win, fs_tap=fs_tap).data
271
+ if dz_method.lower() == "isvd":
272
+ gz = self.isvd(method=method, order=1, deg=deg, win=win, fs_tap=fs_tap).data
273
+ else:
274
+ gz = self.dz(method="fourier", order=1).data
275
+ return self._new(np.sqrt(gx**2 + gy**2 + gz**2), name=f"{self.name}_TG")
276
+
277
+ def lw(self, method: str = "SG", dz_method: str = "isvd",
278
+ deg: int = 4, win: int = 5, fs_tap: int = 5) -> "Grid":
279
+ gx = self.dx(method=method, deg=deg, win=win, fs_tap=fs_tap).data
280
+ gy = self.dy(method=method, deg=deg, win=win, fs_tap=fs_tap).data
281
+ if dz_method.lower() == "isvd":
282
+ gz = self.isvd(method=method, order=1, deg=deg, win=win, fs_tap=fs_tap).data
283
+ gzz = self.isvd(method=method, order=2, deg=deg, win=win, fs_tap=fs_tap).data
284
+ else:
285
+ gz = self.dz(method="fourier", order=1).data
286
+ gzz = self.dz(method="fourier", order=2).data
287
+ # d/dx(dz) via a follow-up dx on gz
288
+ gzx = T.savgol_deriv(gz, self.cellsize, "dx", deg=deg, win=win)
289
+ gzy = T.savgol_deriv(gz, self.cellsize, "dy", deg=deg, win=win)
290
+ num = gzx * gx + gzy * gy + gzz * gz
291
+ den = gx * gx + gy * gy + gz * gz
292
+ return self._new(num / den, name=f"{self.name}_LW")
293
+
294
+ def tahg(self, method: str = "SG", dz_method: str = "isvd",
295
+ deg: int = 4, win: int = 5, fs_tap: int = 5, alpha: float = 1.0) -> "Grid":
296
+ hgm_grid = self.hgm(method=method, deg=deg, win=win, fs_tap=fs_tap)
297
+ out = hgm_grid.tilt(hgm_method=method, dz_method=dz_method,
298
+ deg=deg, win=win, fs_tap=fs_tap, alpha=alpha)
299
+ out.name = f"{self.name}_TAHG"
300
+ return out
301
+
302
+ # ---------- continuation / filtering ----------
303
+ def up(self, z: float = 500.0) -> "Grid":
304
+ out = T.fourier_transform(self.data, self.cellsize, trans="upcont", z=z)
305
+ return self._new(out, name=f"{self.name}_UC{z}")
306
+
307
+ def hp_filter_uc(self, z: float = 5000.0) -> "Grid":
308
+ regional = T.fourier_transform(self.data, self.cellsize, trans="upcont", z=z)
309
+ return self._new(self.data - regional, name=f"{self.name}_HPUC{z}")
310
+
311
+ # ---------- display ----------
312
+ def show(self, **kwargs):
313
+ from interpies_lite.graphics import show_grid
314
+ return show_grid(self, **kwargs)
315
+
316
+ def save_image(self, output_file: str, **kwargs) -> None:
317
+ from interpies_lite.graphics import save_grid_image
318
+ save_grid_image(self, output_file, **kwargs)
interpies_lite/io.py ADDED
@@ -0,0 +1,20 @@
1
+ """
2
+ interpies_lite.io
3
+ =================
4
+
5
+ Thin wrapper around rasterio for opening single-band rasters.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from interpies_lite.grid import Grid
10
+
11
+
12
+ def open_rasterio(path: str, band: int = 1, name: str | None = None) -> Grid:
13
+ try:
14
+ import rasterio
15
+ except ImportError as e:
16
+ raise RuntimeError(
17
+ "rasterio is required to open rasters. Install with `pip install rasterio`."
18
+ ) from e
19
+ with rasterio.open(path) as ds:
20
+ return Grid.from_dataset(ds, band=band, name=name)
@@ -0,0 +1,64 @@
1
+ """Smoke test: build a synthetic grid and run every transform end-to-end."""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+
7
+ # make the package importable when running from the repo root
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ from interpies_lite import Grid
11
+ import interpies_lite as ip
12
+
13
+
14
+ def make_synthetic(n=120):
15
+ x = np.linspace(-5000, 5000, n)
16
+ y = np.linspace(-5000, 5000, n)
17
+ XX, YY = np.meshgrid(x, y)
18
+ # two Gaussian anomalies + a linear trend + noise
19
+ g1 = 80 * np.exp(-((XX - 1500) ** 2 + (YY - 800) ** 2) / (2 * 800**2))
20
+ g2 = -50 * np.exp(-((XX + 1800) ** 2 + (YY + 1200) ** 2) / (2 * 1000**2))
21
+ trend = 0.002 * XX + 0.001 * YY
22
+ rng = np.random.default_rng(0)
23
+ data = g1 + g2 + trend + rng.normal(0, 0.5, size=XX.shape)
24
+ return Grid.from_array(data, west=0, north=10000, cellsize=100, crs="EPSG:32633", name="synth")
25
+
26
+
27
+ def main():
28
+ g = make_synthetic()
29
+ print("== grid.info() ==")
30
+ g.info()
31
+
32
+ print("\n== derivatives ==")
33
+ for m in [g.dx(), g.dy(), g.dx2(), g.dy2(), g.dxdy(), g.dz(method="fourier"), g.isvd()]:
34
+ print(f" {m.name:20s} range=({np.nanmin(m.data):8.2f}, {np.nanmax(m.data):8.2f})")
35
+
36
+ print("\n== enhancement transforms ==")
37
+ for m in [g.hgm(), g.tilt(), g.tg(), g.lw(), g.tahg()]:
38
+ print(f" {m.name:20s} range=({np.nanmin(m.data):8.2f}, {np.nanmax(m.data):8.2f})")
39
+
40
+ print("\n== continuation / filtering ==")
41
+ for m in [g.up(z=500), g.hp_filter_uc(z=2000), g.smooth(), g.laplacian(), g.vi()]:
42
+ print(f" {m.name:20s} range=({np.nanmin(m.data):8.2f}, {np.nanmax(m.data):8.2f})")
43
+
44
+ print("\n== preprocessing ==")
45
+ g2 = g.detrend(degree=1).clip(1000, 8000, -8000, -1000).resample(2)
46
+ print(f" clipped/resampled shape: {g2.data.shape}, extent={g2.extent}")
47
+
48
+ print("\n== plotting (headless) ==")
49
+ import matplotlib
50
+ matplotlib.use("Agg")
51
+ import matplotlib.pyplot as plt
52
+ fig, axes = plt.subplots(2, 3, figsize=(15, 9))
53
+ for ax, m in zip(axes.ravel(), [g, g.hgm(), g.tilt(), g.tg(), g.up(500), g.hp_filter_uc(2000)]):
54
+ m.show(ax=ax, hs=True, colorbar=False)
55
+ out = Path(__file__).parent / "smoke_test_output.png"
56
+ fig.savefig(out, dpi=100, bbox_inches="tight")
57
+ print(f" saved -> {out}")
58
+ plt.close(fig)
59
+
60
+ print("\nALL OK")
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
@@ -0,0 +1,27 @@
1
+ """
2
+ interpies_lite.spatial
3
+ ======================
4
+
5
+ Lightweight coordinate helpers. Uses pyproj when available; falls back to
6
+ a no-op with a warning.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+
13
+ def project_points(points, src_srs: str = "EPSG:4326", dst_srs: str = "EPSG:32633"):
14
+ """Reproject an (N, 2) array of points from src_srs to dst_srs."""
15
+ try:
16
+ from pyproj import Transformer
17
+ except ImportError:
18
+ raise RuntimeError("pyproj is required: pip install pyproj")
19
+ t = Transformer.from_crs(src_srs, dst_srs, always_xy=True)
20
+ pts = np.asarray(points, dtype=float)
21
+ x, y = t.transform(pts[:, 0], pts[:, 1])
22
+ return np.column_stack([x, y])
23
+
24
+
25
+ def grid_extent(west: float, north: float, cellsize: float, nrows: int, ncols: float):
26
+ """Return matplotlib-style [left, right, bottom, top]."""
27
+ return [west, west + ncols * cellsize, north - nrows * cellsize, north]
@@ -0,0 +1,227 @@
1
+ """
2
+ interpies_lite.transforms
3
+ ==========================
4
+
5
+ Pure-numpy/scipy implementations of potential-field transforms:
6
+ derivatives (Savitzky-Golay / finite-sample / Fourier), vertical derivative
7
+ via the ISVD (integrated second vertical derivative) method, upward
8
+ continuation, vertical integration, detrending and padding helpers.
9
+
10
+ No rasterio / GDAL / obspy dependency here.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Literal
15
+
16
+ import numpy as np
17
+ from numpy.typing import ArrayLike
18
+ from scipy import ndimage as ndi
19
+ from scipy import signal
20
+ from sklearn.linear_model import LinearRegression
21
+ from sklearn.pipeline import Pipeline
22
+ from sklearn.preprocessing import PolynomialFeatures
23
+
24
+
25
+ def replace_edges(data: np.ndarray, ncells: int = 1) -> np.ndarray:
26
+ out = data.copy()
27
+ for i in range(ncells):
28
+ out[i, :] = out[ncells, :]
29
+ out[-1 - i, :] = out[-1 - ncells, :]
30
+ out[:, i] = out[:, ncells]
31
+ out[:, -1 - i] = out[:, -1 - ncells]
32
+ return out
33
+
34
+
35
+ def fill_nodata(data: np.ndarray, invalid: np.ndarray | None = None) -> np.ndarray:
36
+ if invalid is None:
37
+ invalid = ~np.isfinite(data)
38
+ if not invalid.any():
39
+ return data.copy()
40
+ return ndi.generic_filter(
41
+ np.where(invalid, np.nan, data),
42
+ lambda v: np.nanmean(v),
43
+ size=3,
44
+ mode="nearest",
45
+ )
46
+
47
+
48
+ def simple_resample(data: np.ndarray, sampling: int = 2) -> np.ndarray:
49
+ return data[::sampling, ::sampling]
50
+
51
+
52
+ def stats(data: np.ndarray) -> dict:
53
+ arr = data[np.isfinite(data)]
54
+ return {
55
+ "min": float(np.min(arr)),
56
+ "max": float(np.max(arr)),
57
+ "mean": float(np.mean(arr)),
58
+ "std": float(np.std(arr)),
59
+ "median": float(np.median(arr)),
60
+ "p02": float(np.percentile(arr, 2)),
61
+ "p98": float(np.percentile(arr, 98)),
62
+ }
63
+
64
+
65
+ def find_trend(point_coords: ArrayLike, data: ArrayLike, degree: int = 1, return_model: bool = False):
66
+ X = np.asarray(point_coords)
67
+ y = np.asarray(data).ravel()
68
+ mask = np.isfinite(y)
69
+ model = Pipeline([("poly", PolynomialFeatures(degree=degree)), ("reg", LinearRegression())])
70
+ model.fit(X[mask], y[mask])
71
+ if return_model:
72
+ return model
73
+ return model.predict(X).reshape(np.asarray(data).shape)
74
+
75
+
76
+ def savgol2d(degree: int, window_size: int) -> np.ndarray:
77
+ if window_size % 2 == 0:
78
+ raise ValueError("window_size must be odd")
79
+ half = window_size // 2
80
+ x = np.arange(-half, half + 1)
81
+ order = np.arange(degree + 1)
82
+ V = x[:, None] ** order[None, :]
83
+ coef_0 = np.linalg.pinv(V)[0]
84
+ kernel = np.outer(coef_0, coef_0)
85
+ return kernel / kernel.sum()
86
+
87
+
88
+ def savgol_smooth(data: np.ndarray, deg: int = 3, win: int = 5, do_edges: bool = False) -> np.ndarray:
89
+ kernel = savgol2d(deg, win)
90
+ out = signal.convolve2d(np.nan_to_num(data), kernel, mode="same", boundary="symm")
91
+ if do_edges:
92
+ out = replace_edges(out, ncells=win // 2)
93
+ return out
94
+
95
+
96
+ def savgol_deriv(
97
+ data: np.ndarray,
98
+ cellsize: float,
99
+ direction: Literal["dx", "dy"] = "dx",
100
+ deg: int = 3,
101
+ win: int = 5,
102
+ do_edges: bool = True,
103
+ ) -> np.ndarray:
104
+ if win % 2 == 0:
105
+ raise ValueError("win must be odd")
106
+ c_dx = signal.savgol_coeffs(win, deg, deriv=1, use="dot")
107
+ c_smooth = signal.savgol_coeffs(win, deg, deriv=0, use="dot")
108
+ if direction == "dx":
109
+ kx, ky = c_dx.reshape(1, -1), c_smooth.reshape(-1, 1)
110
+ else:
111
+ kx, ky = c_smooth.reshape(1, -1), c_dx.reshape(-1, 1)
112
+ kernel = ky @ kx
113
+ out = signal.convolve2d(np.nan_to_num(data), kernel, mode="same", boundary="symm") / cellsize
114
+ if do_edges:
115
+ out = replace_edges(out, ncells=win // 2)
116
+ return out
117
+
118
+
119
+ def fs_coefficients(tap: int = 5) -> np.ndarray:
120
+ x = np.arange(-tap, tap + 1, dtype=float)
121
+ A = np.vstack([np.ones_like(x), x]).T
122
+ return np.linalg.pinv(A)[1]
123
+
124
+
125
+ def fs_deriv(data: np.ndarray, cellsize: float, direction: Literal["dx", "dy"] = "dx", tap: int = 5) -> np.ndarray:
126
+ c = fs_coefficients(tap)
127
+ kernel = c.reshape(1, -1) if direction == "dx" else c.reshape(-1, 1)
128
+ return signal.convolve2d(np.nan_to_num(data), kernel, mode="same", boundary="symm") / cellsize
129
+
130
+
131
+ def next_pow2(x: int) -> int:
132
+ p = 1
133
+ while p < x:
134
+ p <<= 1
135
+ return p
136
+
137
+
138
+ def getk(nx: int, ny: int, dx: float, dy: float):
139
+ fx = np.fft.fftfreq(nx, d=dx)
140
+ fy = np.fft.fftfreq(ny, d=dy)
141
+ kx, ky = np.meshgrid(2 * np.pi * fx, 2 * np.pi * fy)
142
+ return kx, ky, np.sqrt(kx**2 + ky**2)
143
+
144
+
145
+ def pad_full(data: np.ndarray, mode: str = "reflect") -> np.ndarray:
146
+ ny, nx = data.shape
147
+ return np.pad(data, ((ny // 2, ny // 2), (nx // 2, nx // 2)), mode=mode)
148
+
149
+
150
+ def unpad_full(data: np.ndarray, nrows: int, ncols: int) -> np.ndarray:
151
+ return data[nrows // 2:nrows // 2 + nrows, ncols // 2:ncols // 2 + ncols]
152
+
153
+
154
+ def fourier_transform(
155
+ data: np.ndarray,
156
+ cellsize: float,
157
+ trans: Literal["dx", "dy", "dxdy", "dz", "vi", "upcont"] = "dx",
158
+ order: int = 1,
159
+ z: float = 0.0,
160
+ eps: float = 1e-6,
161
+ pad: bool = True,
162
+ ) -> np.ndarray:
163
+ work = pad_full(data) if pad else data.copy()
164
+ ny, nx = work.shape
165
+ kx, ky, k = getk(nx, ny, cellsize, cellsize)
166
+ F = np.fft.fft2(np.nan_to_num(work))
167
+
168
+ if trans == "dx":
169
+ factor = (1j * kx) ** order
170
+ elif trans == "dy":
171
+ factor = (1j * ky) ** order
172
+ elif trans == "dxdy":
173
+ factor = (1j * kx) ** order * (1j * ky) ** order
174
+ elif trans == "dz":
175
+ factor = k**order
176
+ elif trans == "vi":
177
+ k_safe = k.copy()
178
+ k_safe[k_safe == 0] = eps
179
+ factor = k_safe ** (-order)
180
+ elif trans == "upcont":
181
+ factor = np.exp(-z * k)
182
+ else:
183
+ raise ValueError(f"Unknown transform: {trans}")
184
+
185
+ out = np.real(np.fft.ifft2(F * factor))
186
+ if pad:
187
+ out = unpad_full(out, data.shape[0], data.shape[1])
188
+ return out
189
+
190
+
191
+ def isvd(
192
+ data: np.ndarray,
193
+ cellsize: float,
194
+ method: Literal["SG", "FS", "fourier"] = "SG",
195
+ order: int = 1,
196
+ deg: int = 4,
197
+ win: int = 5,
198
+ fs_tap: int = 5,
199
+ do_edges: bool = True,
200
+ **kwargs,
201
+ ) -> np.ndarray:
202
+ if method.upper() == "SG":
203
+ dx2 = savgol_deriv(data, cellsize, "dx", deg=deg, win=win, do_edges=do_edges)
204
+ dx2 = savgol_deriv(dx2, cellsize, "dx", deg=deg, win=win, do_edges=do_edges)
205
+ dy2 = savgol_deriv(data, cellsize, "dy", deg=deg, win=win, do_edges=do_edges)
206
+ dy2 = savgol_deriv(dy2, cellsize, "dy", deg=deg, win=win, do_edges=do_edges)
207
+ elif method.upper() == "FS":
208
+ dx2 = fs_deriv(fs_deriv(data, cellsize, "dx", tap=fs_tap), cellsize, "dx", tap=fs_tap)
209
+ dy2 = fs_deriv(fs_deriv(data, cellsize, "dy", tap=fs_tap), cellsize, "dy", tap=fs_tap)
210
+ else:
211
+ dx2 = fourier_transform(data, cellsize, trans="dx", order=2, **kwargs)
212
+ dy2 = fourier_transform(data, cellsize, trans="dy", order=2, **kwargs)
213
+
214
+ dz2 = -(dx2 + dy2)
215
+ if order == 2:
216
+ return dz2
217
+ if order == 1:
218
+ return fourier_transform(dz2, cellsize, trans="vi", order=1, pad=True)
219
+ raise NotImplementedError(f"ISVD order={order} not implemented")
220
+
221
+
222
+ def gauss(data: np.ndarray, sigma: float = 1.0) -> np.ndarray:
223
+ return ndi.gaussian_filter(np.nan_to_num(data), sigma=sigma)
224
+
225
+
226
+ def laplacian(data: np.ndarray, cellsize: float) -> np.ndarray:
227
+ return ndi.laplace(np.nan_to_num(data)) / (cellsize**2)
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.5
2
+ Name: interpies-lite
3
+ Version: 0.1.0
4
+ Summary: Lightweight reimplementation of interpies: potential-field grid transforms and plotting (derivatives, ISVD, upward continuation, edge enhancement).
5
+ Project-URL: Homepage, https://github.com/xyt556/interpies-lite
6
+ Project-URL: Repository, https://github.com/xyt556/interpies-lite
7
+ Project-URL: Issues, https://github.com/xyt556/interpies-lite/issues
8
+ Author-email: Yantao XI <xyt556@gmail.com>
9
+ License: BSD 3-Clause License
10
+
11
+ Copyright (c) 2026, interpies-lite contributors
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without
15
+ modification, are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice, this
18
+ list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ 3. Neither the name of the copyright holder nor the names of its
25
+ contributors may be used to endorse or promote products derived from
26
+ this software without specific prior written permission.
27
+
28
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
32
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
34
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
36
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
+ License-File: LICENSE
39
+ Keywords: derivatives,edge-detection,geophysics,gravity,isvd,magnetics,potential-field,raster,remote-sensing,tilt,upward-continuation
40
+ Classifier: Development Status :: 4 - Beta
41
+ Classifier: Intended Audience :: Science/Research
42
+ Classifier: License :: OSI Approved :: BSD License
43
+ Classifier: Operating System :: OS Independent
44
+ Classifier: Programming Language :: Python :: 3
45
+ Classifier: Programming Language :: Python :: 3.10
46
+ Classifier: Programming Language :: Python :: 3.11
47
+ Classifier: Programming Language :: Python :: 3.12
48
+ Classifier: Topic :: Scientific/Engineering :: GIS
49
+ Classifier: Topic :: Scientific/Engineering :: Physics
50
+ Requires-Python: >=3.10
51
+ Requires-Dist: matplotlib>=3.7
52
+ Requires-Dist: numpy>=1.24
53
+ Requires-Dist: scikit-learn>=1.2
54
+ Requires-Dist: scipy>=1.10
55
+ Provides-Extra: dev
56
+ Requires-Dist: build>=1.2; extra == 'dev'
57
+ Requires-Dist: pytest>=8.0; extra == 'dev'
58
+ Requires-Dist: twine>=5.0; extra == 'dev'
59
+ Provides-Extra: geo
60
+ Requires-Dist: pyproj>=3.5; extra == 'geo'
61
+ Requires-Dist: rasterio>=1.3; extra == 'geo'
62
+ Description-Content-Type: text/markdown
63
+
64
+ # interpies_lite
65
+
66
+ 对原 `interpies` 库的轻量重实现。目标:解决原库依赖钉死(`gdal==3.8.4`)、久未更新的问题,保留核心功能与 API 习惯。
67
+
68
+ ## 与原库的差异
69
+
70
+ | 维度 | 原 interpies | interpies_lite |
71
+ | --- | --- | --- |
72
+ | 栅格读写 | rasterio + 钉死 gdal==3.8.4 | rasterio>=1.3(可选,未装时只能用 `Grid.from_array`) |
73
+ | SEGY 模块 | obspy 依赖 | 砍掉 |
74
+ | 色表数据 | 35 KB 硬编码 LUT | 内置 Geosoft clra / parula 近似色表,其他直接用 matplotlib 色表 |
75
+ | Grid 实现 | 普通类 + 手写属性 | `@dataclass` + property,类型注解完整 |
76
+ | 依赖 | numpy/scipy/matplotlib/rasterio/gdal/scikit-image/scikit-learn | numpy/scipy/matplotlib/scikit-learn(rasterio/pyproj 可选) |
77
+ | Python | >=3.10 | >=3.10 |
78
+
79
+ ## 模块
80
+
81
+ ```
82
+ interpies_lite/
83
+ ├── __init__.py # open() / Grid
84
+ ├── grid.py # Grid 类:IO、几何、预处理、全部位场方法
85
+ ├── transforms.py # 纯算法:导数、ISVD、傅里叶变换、延拓、平滑
86
+ ├── graphics.py # show / save_image:晕渲、色图、等值线
87
+ ├── colors.py # 内置色表
88
+ ├── spatial.py # pyproj 点投影(可选)
89
+ └── io.py # rasterio 封装
90
+ ```
91
+
92
+ ## 快速开始
93
+
94
+ ```python
95
+ import interpies_lite as ip
96
+
97
+ # 从文件加载(需要 rasterio)
98
+ g = ip.open("data/brtpgrd.gxf")
99
+ g.info()
100
+
101
+ # 从 numpy 数组构造(无任何重依赖)
102
+ import numpy as np
103
+ data = np.random.randn(100, 100)
104
+ g = ip.Grid.from_array(data, west=0, north=0, cellsize=100, crs="EPSG:32633")
105
+
106
+ # 链式调用:去趋势 → 平滑 → 边界识别
107
+ out = g.detrend(degree=1).smooth(win=5).tilt()
108
+ out.show()
109
+
110
+ # 可用变换(全部返回新 Grid):
111
+ # dx / dy / dx2 / dy2 / dxdy / dz / isvd / vi
112
+ # hgm / tilt / tg / lw / tahg
113
+ # up(z) / hp_filter_uc(z) / laplacian
114
+ # clip / resample / scale / detrend / fill_nodata / smooth
115
+ ```
116
+
117
+ ## 安装
118
+
119
+ 本包暂未发布到 PyPI,可通过以下任一方式安装:
120
+
121
+ ### 方式 1:从 GitHub 直接安装(推荐)
122
+
123
+ 公开仓库,无需下载,pip 自动拉取最新代码:
124
+
125
+ ```bash
126
+ pip install git+https://github.com/xyt556/interpies-lite.git
127
+ ```
128
+
129
+ 指定版本/分支:
130
+
131
+ ```bash
132
+ pip install git+https://github.com/xyt556/interpies-lite.git@main
133
+ ```
134
+
135
+ ### 方式 2:从本地目录安装
136
+
137
+ ```bash
138
+ cd path/to/interpies_lite
139
+ pip install .
140
+ ```
141
+
142
+ ### 方式 3:从 wheel 文件安装
143
+
144
+ ```bash
145
+ pip install dist/interpies_lite-0.1.0-py3-none-any.whl
146
+ ```
147
+
148
+ ### 方式 4:开发模式(可编辑,改代码即时生效)
149
+
150
+ ```bash
151
+ cd path/to/interpies_lite
152
+ pip install -e .
153
+ ```
154
+
155
+ ### 可选依赖(读写 GeoTIFF/GXF)
156
+
157
+ ```bash
158
+ pip install "interpies-lite[geo]"
159
+ # 等价于
160
+ pip install rasterio pyproj
161
+ ```
162
+
163
+ 核心依赖(必装):numpy、scipy、matplotlib、scikit-learn。
164
+
165
+ ## 示例
166
+
167
+ 仓库自带两个 notebook:
168
+
169
+ | Notebook | 数据 | 内容 |
170
+ | --- | --- | --- |
171
+ | `notebooks/interpies_lite_demo.ipynb` | 合成数据 | 快速上手,无需 rasterio,开箱即跑 |
172
+ | `notebooks/interpies_lite_brtpgrd_demo.ipynb` | `data/brtpgrd.gxf`(真实航磁) | 课堂版完整教程,含算法原理与公式 |
173
+
174
+ 克隆仓库后:
175
+
176
+ ```bash
177
+ cd interpies_lite
178
+ pip install -e .[geo]
179
+ jupyter notebook notebooks/
180
+ ```
181
+
182
+ `data/brtpgrd.gxf` 为 Colorado 地区航磁数据(707×629,50 m,NAD27/UTM13N),来自原 interpies 项目示例。
183
+
184
+ ## 冒烟测试
185
+
186
+ ```bash
187
+ python smoke_test.py
188
+ # 在合成网格上跑通全部变换,输出 smoke_test_output.png
189
+ ```
@@ -0,0 +1,12 @@
1
+ interpies_lite/__init__.py,sha256=2Dfz8DgJjVgB2w6yLMQh5MWSDEtZFjfUla52z3klrjo,671
2
+ interpies_lite/colors.py,sha256=Q8AY5CWIHJ8NXNVbB7lT3a93guaqqpVOHyzhxFrrTzE,1665
3
+ interpies_lite/graphics.py,sha256=UTXzzSdpRtuPyfSW-l_Ot_rFlTCwUVOlEiM54TrTflo,4844
4
+ interpies_lite/grid.py,sha256=aqRN0RqKwyZKdjxlBBMbaN4w7TD3b6pAiw19l2KvuaA,13727
5
+ interpies_lite/io.py,sha256=EAmkaXC9MX9gI17PCFCRMNKP4tluSUu9xcCDzRkI3ZE,551
6
+ interpies_lite/smoke_test.py,sha256=RFXtdAwOUOkizQIKoLkKQ0XtYhhJvmWwxEpKWRbHgVk,2348
7
+ interpies_lite/spatial.py,sha256=6uF3Kz59MgJfePfYjWaIwMrycHMaH6iI2BbSUwmPkRo,920
8
+ interpies_lite/transforms.py,sha256=oxGqJtgj-GwXZlchPNHOH4xxqfymH-LxnpArd1omM5Q,7325
9
+ interpies_lite-0.1.0.dist-info/METADATA,sha256=7Bl3MUNqbGLD7bOYTdp9nwwLduXgQ0EpkRQ2GQBSszg,6830
10
+ interpies_lite-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
11
+ interpies_lite-0.1.0.dist-info/licenses/LICENSE,sha256=KvHyl_BBFqqqQRqfXJ3SuUL09PYyJzTmS2Nqt8CMCPk,1535
12
+ interpies_lite-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, interpies-lite contributors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.