bluemesh2d 0.1.1.dev0__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.
Files changed (74) hide show
  1. bluemesh2d/_version.py +24 -0
  2. bluemesh2d/aabb_tree/findball.py +121 -0
  3. bluemesh2d/aabb_tree/findtria.py +299 -0
  4. bluemesh2d/aabb_tree/maketree.py +147 -0
  5. bluemesh2d/aabb_tree/mapvert.py +72 -0
  6. bluemesh2d/aabb_tree/queryset.py +83 -0
  7. bluemesh2d/aabb_tree/scantree.py +124 -0
  8. bluemesh2d/dependencies.py +83 -0
  9. bluemesh2d/feedback.py +142 -0
  10. bluemesh2d/geom_util/boundary_util.py +216 -0
  11. bluemesh2d/geom_util/getiso.py +194 -0
  12. bluemesh2d/geom_util/poly_util.py +795 -0
  13. bluemesh2d/geom_util/proj_util.py +250 -0
  14. bluemesh2d/geomesh_util/border_util.py +283 -0
  15. bluemesh2d/geomesh_util/depth_field.py +333 -0
  16. bluemesh2d/geomesh_util/grd_util.py +1000 -0
  17. bluemesh2d/geomesh_util/interpolation_mesh.py +318 -0
  18. bluemesh2d/geomesh_util/merge_circumcenters.py +268 -0
  19. bluemesh2d/geomesh_util/water_polygon.py +406 -0
  20. bluemesh2d/hfun_util/build_hfun.py +589 -0
  21. bluemesh2d/hfun_util/hfun_dispersion.py +96 -0
  22. bluemesh2d/hfun_util/lfshfn.py +110 -0
  23. bluemesh2d/hfun_util/limhfn.py +65 -0
  24. bluemesh2d/hfun_util/make_constant_hfun.py +32 -0
  25. bluemesh2d/hfun_util/make_depth_hfun.py +51 -0
  26. bluemesh2d/hfun_util/smooth_and_precomput.py +188 -0
  27. bluemesh2d/hfun_util/trihfn.py +84 -0
  28. bluemesh2d/hjac_util/limgrad.py +105 -0
  29. bluemesh2d/mesh_ball/cdtbal1.py +38 -0
  30. bluemesh2d/mesh_ball/cdtbal2.py +104 -0
  31. bluemesh2d/mesh_ball/inv_2x2.py +61 -0
  32. bluemesh2d/mesh_ball/inv_3x3.py +72 -0
  33. bluemesh2d/mesh_ball/pwrbal2.py +134 -0
  34. bluemesh2d/mesh_ball/tribal2.py +27 -0
  35. bluemesh2d/mesh_cost/relhfn.py +62 -0
  36. bluemesh2d/mesh_cost/triang.py +59 -0
  37. bluemesh2d/mesh_cost/triarea.py +51 -0
  38. bluemesh2d/mesh_cost/trideg.py +44 -0
  39. bluemesh2d/mesh_cost/triscr.py +43 -0
  40. bluemesh2d/mesh_file/bnd_util.py +664 -0
  41. bluemesh2d/mesh_file/loadmsh.py +165 -0
  42. bluemesh2d/mesh_file/ugrid.py +308 -0
  43. bluemesh2d/mesh_util/cfmtri.py +118 -0
  44. bluemesh2d/mesh_util/deltri.py +131 -0
  45. bluemesh2d/mesh_util/idxtri.py +53 -0
  46. bluemesh2d/mesh_util/isfeat.py +90 -0
  47. bluemesh2d/mesh_util/minlen.py +46 -0
  48. bluemesh2d/mesh_util/setset.py +49 -0
  49. bluemesh2d/mesh_util/tricon.py +90 -0
  50. bluemesh2d/mesh_util/tridiv.py +187 -0
  51. bluemesh2d/meshgen.py +202 -0
  52. bluemesh2d/ortho_merge/constants.py +27 -0
  53. bluemesh2d/ortho_merge/geometry.py +64 -0
  54. bluemesh2d/ortho_merge/ortho_merge_iter.py +812 -0
  55. bluemesh2d/ortho_merge/orthogonalize.py +2989 -0
  56. bluemesh2d/pipeline.py +277 -0
  57. bluemesh2d/poly_data/airfoil.msh +958 -0
  58. bluemesh2d/poly_data/channel.msh +212 -0
  59. bluemesh2d/poly_data/islands.msh +13819 -0
  60. bluemesh2d/poly_data/lake.msh +612 -0
  61. bluemesh2d/poly_data/river.msh +690 -0
  62. bluemesh2d/poly_test/inpoly.py +105 -0
  63. bluemesh2d/poly_test/inpoly_mat.py +104 -0
  64. bluemesh2d/refine.py +972 -0
  65. bluemesh2d/smood.py +623 -0
  66. bluemesh2d/smooth.py +522 -0
  67. bluemesh2d/tricost.py +426 -0
  68. bluemesh2d/tridemo.py +723 -0
  69. bluemesh2d/triread.py +53 -0
  70. bluemesh2d-0.1.1.dev0.dist-info/METADATA +139 -0
  71. bluemesh2d-0.1.1.dev0.dist-info/RECORD +74 -0
  72. bluemesh2d-0.1.1.dev0.dist-info/WHEEL +5 -0
  73. bluemesh2d-0.1.1.dev0.dist-info/licenses/LICENSE +674 -0
  74. bluemesh2d-0.1.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,333 @@
1
+ import numpy as np
2
+ import pyproj
3
+ import rasterio
4
+ from rasterio.transform import rowcol
5
+ from rasterio.warp import transform_bounds
6
+ from scipy.interpolate import (
7
+ LinearNDInterpolator,
8
+ NearestNDInterpolator,
9
+ RegularGridInterpolator,
10
+ )
11
+
12
+
13
+ def _check_method(method):
14
+ """Validate the interpolation ``method`` shared by all depth-field readers."""
15
+ if method not in ("linear", "nearest"):
16
+ raise ValueError("method must be 'linear' or 'nearest'")
17
+ return method
18
+
19
+ def depth_field_from_dat(x, y, z, input_crs, output_crs, method="linear"):
20
+ """Build a callable depth field from scattered x-y-z points.
21
+
22
+ Parameters
23
+ ----------
24
+ x, y, z : ndarray of shape (N,)
25
+ Point coordinates and depth/elevation values.
26
+ input_crs : str or pyproj.CRS
27
+ CRS of the input coordinates.
28
+ output_crs : str or pyproj.CRS
29
+ CRS in which the returned depth field is queried.
30
+ method : {'linear', 'nearest'}, optional
31
+ Interpolation method. Default is ``'linear'``.
32
+
33
+ Returns
34
+ -------
35
+ depth_field : callable
36
+ ``depth_field(xy)`` returns interpolated depth values (m) for ``xy`` of
37
+ shape ``(M, 2)`` in ``output_crs``. Exposes ``.bounds`` in
38
+ ``output_crs``.
39
+ """
40
+
41
+ _check_method(method)
42
+
43
+ # Clean invalid values
44
+ mask = np.isfinite(x) & np.isfinite(y) & np.isfinite(z)
45
+ x, y, z = x[mask], y[mask], z[mask]
46
+
47
+ # Create interpolator
48
+ if method == "linear":
49
+ interp = LinearNDInterpolator(list(zip(x, y)), z, fill_value=np.nan)
50
+ else: # nearest
51
+ interp = NearestNDInterpolator(list(zip(x, y)), z)
52
+
53
+ input_crs = pyproj.CRS.from_user_input(input_crs)
54
+ output_crs = pyproj.CRS.from_user_input(output_crs)
55
+ transformer = pyproj.Transformer.from_crs(output_crs, input_crs, always_xy=True)
56
+
57
+ # Closure function
58
+ def depth_field(xy):
59
+ """Evaluate interpolated depth at query coordinates."""
60
+ xs, ys = xy[:, 0], xy[:, 1]
61
+ xs, ys = transformer.transform(xs, ys)
62
+ depth = -interp(xs, ys)
63
+ depth[np.isnan(depth)] = 0.0
64
+ return np.asarray(depth, dtype=float)
65
+
66
+ # Expose the data extent in the query (output) CRS, so callers can size a
67
+ # sampling grid without a separate domain (same contract as the other readers).
68
+ depth_field.bounds = transform_bounds(
69
+ input_crs, output_crs, float(x.min()), float(y.min()), float(x.max()), float(y.max())
70
+ )
71
+
72
+ return depth_field
73
+
74
+
75
+ def _cell_center_offset(dataset):
76
+ """Half-cell offset (in pixels) to reach cell centres from the transform.
77
+
78
+ The rasterio/GDAL affine is corner-referenced, but a raster value's
79
+ location depends on the GeoTIFF ``GTRasterTypeGeoKey`` -- surfaced as the
80
+ GDAL/rasterio ``AREA_OR_POINT`` tag:
81
+
82
+ - ``Area`` (default, and what a missing tag means): the value represents
83
+ the whole cell, so its location is the cell *centre* -> offset 0.5.
84
+ - ``Point``: the value is a point sample at the grid node the transform
85
+ already indexes -> no offset.
86
+
87
+ Callers add this to grid-building indices, or subtract it when converting
88
+ world coordinates to fractional indices, so sampling lands on the true
89
+ data location for either convention.
90
+ """
91
+ try:
92
+ tag = str(dataset.tags().get("AREA_OR_POINT", "Area")).strip().lower()
93
+ except Exception:
94
+ tag = "area"
95
+ return 0.0 if tag.startswith("point") else 0.5
96
+
97
+
98
+ def _read_window_decimated(dataset, bbox, max_cells):
99
+ """Read band 1 clipped to ``bbox`` (raster CRS) and decimated to fit.
100
+
101
+ Only the requested window is read (never the whole raster), down-sampled
102
+ on read when still larger than ``max_cells`` -- so an oversized bathymetry
103
+ raster is turned into a depth field without materialising it in memory.
104
+
105
+ Returns ``(band, transform, step)`` where ``transform`` is the affine of
106
+ the decimated window (maps local col/row -> raster-CRS coordinates).
107
+ """
108
+ from rasterio.transform import Affine
109
+ from rasterio.windows import Window
110
+
111
+ W, H = dataset.width, dataset.height
112
+ if bbox is not None:
113
+ xmin, ymin, xmax, ymax = bbox
114
+ inv = ~dataset.transform
115
+ cols, rows = [], []
116
+ for x in (xmin, xmax):
117
+ for y in (ymin, ymax):
118
+ c, r = inv * (x, y)
119
+ cols.append(c)
120
+ rows.append(r)
121
+ col_off = max(0, int(np.floor(min(cols))) - 2)
122
+ row_off = max(0, int(np.floor(min(rows))) - 2)
123
+ col_end = min(W, int(np.ceil(max(cols))) + 2)
124
+ row_end = min(H, int(np.ceil(max(rows))) + 2)
125
+ if col_end <= col_off or row_end <= row_off:
126
+ raise RuntimeError(
127
+ "The domain does not overlap the bathymetry raster.")
128
+ else:
129
+ col_off, row_off, col_end, row_end = 0, 0, W, H
130
+
131
+ win = Window(col_off, row_off, col_end - col_off, row_end - row_off)
132
+ win_w, win_h = int(win.width), int(win.height)
133
+ step = 1
134
+ if max_cells and win_w * win_h > max_cells:
135
+ step = int(np.ceil((win_w * win_h / float(max_cells)) ** 0.5))
136
+ out_w = max(1, win_w // step)
137
+ out_h = max(1, win_h // step)
138
+
139
+ band = dataset.read(1, window=win, out_shape=(out_h, out_w))
140
+ t = dataset.window_transform(win) * Affine.scale(win_w / out_w, win_h / out_h)
141
+ return band, t, step
142
+
143
+
144
+ def depth_field_from_tif(tiff_path, output_crs, raster_crs=None, method="linear",
145
+ bbox=None, max_cells=None, invert_z=False,
146
+ nodata_value=None):
147
+ """Build a callable depth field from a bathymetry GeoTIFF.
148
+
149
+ Parameters
150
+ ----------
151
+ tiff_path : str
152
+ Path to the bathymetry GeoTIFF file.
153
+ output_crs : str or pyproj.CRS
154
+ CRS of coordinates passed to the returned depth field.
155
+ raster_crs : str or pyproj.CRS, optional
156
+ CRS of the raster. If ``None``, taken from the GeoTIFF metadata.
157
+ method : {'linear', 'nearest'}, optional
158
+ ``'linear'`` for bilinear interpolation; ``'nearest'`` for nearest pixel.
159
+ Default is ``'linear'``.
160
+ bbox : tuple or None, optional
161
+ ``(xmin, ymin, xmax, ymax)`` in the *raster* CRS. When given, only that
162
+ window of the raster is read (much less memory on large rasters). The
163
+ depth field returns 0 outside it, so the window must cover the full
164
+ query extent. Default ``None`` (whole raster).
165
+ max_cells : int or None, optional
166
+ If the window still exceeds this many cells it is down-sampled on read.
167
+ Default ``None`` (no decimation).
168
+ invert_z : bool, optional
169
+ The raster is assumed to store elevation (positive up), so depth is
170
+ ``-value``. Set ``True`` when the raster already stores depth
171
+ (positive down) or has an inverted Z sign, giving depth ``+value``.
172
+ Default ``False``.
173
+ nodata_value : float or None, optional
174
+ Elevation (positive up) assigned to nodata / non-finite pixels.
175
+ ``None`` (default) uses 0 (sea level -> depth 0).
176
+
177
+ Returns
178
+ -------
179
+ depth_field : callable
180
+ ``depth_field(xy)`` returns depth (m) for ``xy`` of shape ``(N, 2)`` in
181
+ ``output_crs``. Exposes ``.bounds`` in ``output_crs``.
182
+ """
183
+
184
+ _check_method(method)
185
+
186
+ from bluemesh2d.geom_util.proj_util import (
187
+ bundled_raster_data_env, require_georeferenced)
188
+ with bundled_raster_data_env(), rasterio.open(tiff_path) as dataset:
189
+ if raster_crs is None:
190
+ require_georeferenced(dataset)
191
+ raster_crs = dataset.crs
192
+ nodata = dataset.nodata
193
+ center_off = _cell_center_offset(dataset) # 0.5 Area / 0.0 Point
194
+ band, transform, _ = _read_window_decimated(dataset, bbox, max_cells)
195
+ # window bounds in raster CRS (from the decimated transform)
196
+ x0, y0 = transform * (0, 0)
197
+ x1, y1 = transform * (band.shape[1], band.shape[0])
198
+ rxmin, rxmax = sorted((x0, x1))
199
+ rymin, rymax = sorted((y0, y1))
200
+
201
+ output_crs = pyproj.CRS.from_user_input(output_crs)
202
+ raster_crs = pyproj.CRS.from_user_input(raster_crs) if raster_crs else output_crs
203
+ if raster_crs != output_crs:
204
+ transformer = pyproj.Transformer.from_crs(
205
+ output_crs, raster_crs, always_xy=True
206
+ )
207
+ else:
208
+ transformer = None
209
+
210
+ # Depth from elevation (-value), or +value when the Z sign is inverted.
211
+ # nodata / non-finite pixels are replaced by 0 (sea level -> depth 0), so
212
+ # a NaN can never propagate into a nodata hole in the hfun raster.
213
+ sign = 1.0 if invert_z else -1.0
214
+ depth_grid = np.asarray(band, dtype=np.float64) * sign
215
+ bad = ~np.isfinite(depth_grid)
216
+ if nodata is not None:
217
+ bad |= (np.asarray(band) == nodata)
218
+ if bad.any():
219
+ nv = 0.0 if nodata_value is None else float(nodata_value)
220
+ depth_grid[bad] = sign * nv
221
+
222
+ if method == "linear":
223
+ # Build interpolator in pixel (row, col) space
224
+ inv_transform = ~transform
225
+ rows = np.arange(band.shape[0])
226
+ cols = np.arange(band.shape[1])
227
+ interp = RegularGridInterpolator(
228
+ (rows, cols),
229
+ depth_grid,
230
+ method="linear",
231
+ bounds_error=False,
232
+ fill_value=np.nan,
233
+ )
234
+
235
+ def depth_field(xy):
236
+ xs, ys = xy[:, 0], xy[:, 1]
237
+ if transformer is not None:
238
+ xs, ys = transformer.transform(xs, ys)
239
+ xs, ys = np.asarray(xs), np.asarray(ys)
240
+
241
+ if method == "nearest":
242
+ rows, cols = rowcol(transform, xs, ys)
243
+ rows = np.clip(rows, 0, band.shape[0] - 1)
244
+ cols = np.clip(cols, 0, band.shape[1] - 1)
245
+ depth = depth_grid[rows, cols]
246
+ else:
247
+ # method == "linear": continuous (col, row) from inverse transform
248
+ col_row = np.column_stack(inv_transform * (xs, ys))
249
+ # RegularGridInterpolator expects (row, col) for array [rows, cols];
250
+ # subtract the half-cell offset (Area rasters) because the transform
251
+ # is corner-referenced while grid values sit at cell centres --
252
+ # avoids a half-cell shift. Point rasters use offset 0.
253
+ row_col = col_row[:, [1, 0]] - center_off
254
+ depth = interp(row_col)
255
+ # queries outside the read window return NaN; treat as depth 0 so the
256
+ # size function never produces nodata
257
+ depth = np.asarray(depth, dtype=np.float64)
258
+ depth[~np.isfinite(depth)] = 0.0
259
+ return depth
260
+
261
+ # Expose the (windowed) raster extent in the query (output) CRS as
262
+ # (xmin, ymin, xmax, ymax), so callers can size a sampling grid.
263
+ if raster_crs != output_crs:
264
+ depth_field.bounds = transform_bounds(
265
+ raster_crs, output_crs, rxmin, rymin, rxmax, rymax)
266
+ else:
267
+ depth_field.bounds = (rxmin, rymin, rxmax, rymax)
268
+
269
+ return depth_field
270
+
271
+
272
+ def depth_field_from_xr(ds, input_crs, output_crs, x_name='lon', y_name='lat',
273
+ z_name="elevation", method="linear"):
274
+ """Build a callable depth field from an xarray bathymetry dataset.
275
+
276
+ Parameters
277
+ ----------
278
+ ds : xarray.Dataset
279
+ Bathymetry dataset (e.g. GEBCO subset).
280
+ input_crs : str or pyproj.CRS
281
+ CRS of the dataset coordinates.
282
+ output_crs : str or pyproj.CRS
283
+ CRS in which the depth field is queried.
284
+ x_name, y_name, z_name : str, optional
285
+ Names of longitude, latitude, and elevation variables in ``ds``.
286
+ method : {'linear', 'nearest'}, optional
287
+ Grid interpolation method. Default is ``'linear'``.
288
+
289
+ Returns
290
+ -------
291
+ depth_field : callable
292
+ ``depth_field(xy)`` returns depth values (m) for ``xy`` of shape
293
+ ``(N, 2)`` in ``output_crs``. Exposes ``.bounds`` in ``output_crs``.
294
+ """
295
+
296
+ _check_method(method)
297
+
298
+ # Extract lon/lat grid and data (z assumed [lat, lon])
299
+ lon = np.asarray(ds[x_name].values, dtype=float)
300
+ lat = np.asarray(ds[y_name].values, dtype=float)
301
+ z = np.asarray(ds[z_name].values, dtype=float)
302
+ if z.ndim != 2:
303
+ raise ValueError(f"z must be 2D, got shape {z.shape}")
304
+
305
+ # RegularGridInterpolator needs strictly increasing axes; flip if needed
306
+ if lat[0] > lat[-1]:
307
+ lat, z = lat[::-1], z[::-1, :]
308
+ if lon[0] > lon[-1]:
309
+ lon, z = lon[::-1], z[:, ::-1]
310
+
311
+ interp = RegularGridInterpolator(
312
+ (lat, lon), z, method=method, bounds_error=False, fill_value=np.nan
313
+ )
314
+
315
+ input_crs = pyproj.CRS.from_user_input(input_crs)
316
+ output_crs = pyproj.CRS.from_user_input(output_crs)
317
+ to_ds = pyproj.Transformer.from_crs(output_crs, input_crs, always_xy=True)
318
+
319
+ def depth_field(xy):
320
+ """Evaluate interpolated depth at query coordinates."""
321
+ xs, ys = xy[:, 0], xy[:, 1]
322
+ lon_q, lat_q = to_ds.transform(xs, ys)
323
+ # depth = -elevation; interpolator expects (lat, lon) order
324
+ depth = -interp(np.column_stack([np.asarray(lat_q), np.asarray(lon_q)]))
325
+ return np.asarray(depth, dtype=np.float64)
326
+
327
+ # Expose the data extent in the query (output) CRS (same contract as the others).
328
+ depth_field.bounds = transform_bounds(
329
+ input_crs, output_crs, float(lon.min()), float(lat.min()),
330
+ float(lon.max()), float(lat.max())
331
+ )
332
+
333
+ return depth_field