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,589 @@
1
+ """Stage 2: element-size (hfun) rasters -- build, save and load."""
2
+ from __future__ import annotations
3
+
4
+
5
+ from ..feedback import (
6
+ _NullFeedback,
7
+ _available_ram_bytes,
8
+ _check,
9
+ _warn_if_ram_risk,
10
+ )
11
+ from ..geom_util.proj_util import _raster_crs, bundled_raster_data_env
12
+
13
+
14
+ def _compile_custom_hfun(code):
15
+ """Compile user Python into a callable size function.
16
+
17
+ `code` is either a single expression using ``d`` (depth, m, >= 0), ``x``,
18
+ ``y`` (UTM metres) and ``np`` -- e.g. ``np.sqrt(9.81 * d) * 30`` -- or a
19
+ block that defines ``def hfun(d, x, y): ...``.
20
+
21
+ Parameters
22
+ ----------
23
+ code : str
24
+ Python expression or function definition, see above.
25
+
26
+ Returns
27
+ -------
28
+ f : callable
29
+ ``f(d, x, y) -> h``, arrays of element size in metres.
30
+ """
31
+ import numpy as np
32
+
33
+ env = {"np": np}
34
+ if "def hfun" in code:
35
+ exec(compile(code, "<custom hfun>", "exec"), env)
36
+ if "hfun" not in env or not callable(env["hfun"]):
37
+ raise ValueError("Custom code must define `def hfun(d, x, y):`")
38
+ return env["hfun"]
39
+ expr = compile(code, "<custom hfun>", "eval")
40
+
41
+ def f(d, x, y):
42
+ return eval(expr, env, {"d": d, "x": x, "y": y, "depth": d})
43
+
44
+ return f
45
+
46
+
47
+ def _make_depth_hfun(depth_field, method="polynomial",
48
+ a=0.14, b=28.0,
49
+ wave_period=12.0, cells_per_wavelength=20, zmin=1.0,
50
+ custom_code=None, h_const=1000.0,
51
+ hmin=100.0, hmax=10000.0, detail=None, detail_hmin=None,
52
+ slope_ncells=None, slope_step=500.0, slope_hmin=None):
53
+ """Build a depth-based mesh-size function, one of three sizing laws.
54
+
55
+ All methods are floored at `hmin` (`detail_hmin` inside the `detail`
56
+ polygon) and capped at `hmax`.
57
+
58
+ Parameters
59
+ ----------
60
+ depth_field : callable
61
+ ``depth_field(xy) -> d``, depth (m) at query points ``xy`` of shape
62
+ ``(N, 2)``. May expose a ``.bounds`` attribute, propagated to the
63
+ returned function.
64
+ method : {'polynomial', 'wavelength', 'custom', 'constant'}, optional
65
+ Sizing law. ``'polynomial'``: ``h = a*d**2 + b*d``.
66
+ ``'wavelength'``: ``h = L(T, d) / N`` (Hunt-1979 dispersion, see
67
+ ``hfun_wavenumhunt``). ``'custom'``: user Python, see
68
+ :func:`_compile_custom_hfun`. ``'constant'``: ``h = h_const``
69
+ everywhere (see ``bluemesh2d.hfun_util.make_constant_hfun``).
70
+ Default is ``'polynomial'``.
71
+ a, b : float, optional
72
+ Coefficients for the ``'polynomial'`` method. Defaults are 0.14 and
73
+ 28.0.
74
+ wave_period : float, optional
75
+ Wave period ``T`` (s) for the ``'wavelength'`` method. Default is
76
+ 12.0.
77
+ cells_per_wavelength : int, optional
78
+ Cells per wavelength ``N`` for the ``'wavelength'`` method. Default
79
+ is 20.
80
+ zmin : float, optional
81
+ Minimum depth (m) used in the dispersion relation for the
82
+ ``'wavelength'`` method. Default is 1.0.
83
+ custom_code : str or None, optional
84
+ Python code for the ``'custom'`` method, see
85
+ :func:`_compile_custom_hfun`. Required when ``method='custom'``.
86
+ h_const : float, optional
87
+ Element size (m) for the ``'constant'`` method. Default is 1000.0.
88
+ hmin, hmax : float, optional
89
+ Element-size floor and cap (m). Defaults are 100.0 and 10000.0.
90
+ detail : shapely.geometry.base.BaseGeometry or None, optional
91
+ Detail-region polygon; inside it the floor is `detail_hmin` instead
92
+ of `hmin`. Default is ``None``.
93
+ detail_hmin : float or None, optional
94
+ Element-size floor (m) inside `detail`. Default is ``None``.
95
+ slope_ncells : float or None, optional
96
+ If given, the size is also limited by the bathymetric-slope term
97
+ ``h_slope = 2*pi*d / (slope_ncells * |grad d|)``, refining the mesh
98
+ where the bathymetry is steep (shelf break) with ~`slope_ncells`
99
+ cells across the slope feature. ``None`` (default) disables it.
100
+ slope_step : float, optional
101
+ Finite-difference step (m) used to estimate the depth gradient;
102
+ use roughly the bathymetry raster resolution. Default is 500.0.
103
+ slope_hmin : float or None, optional
104
+ Independent floor (m) for the slope term: the slope refinement
105
+ never asks for cells smaller than this, regardless of `hmin`.
106
+ ``None`` (default) leaves the slope term floored at `hmin` only
107
+ (via the final clip).
108
+
109
+ Returns
110
+ -------
111
+ hfun : callable
112
+ ``hfun(test) -> h``, element size (m) at query points ``test`` of
113
+ shape ``(N, 2)``. Carries a ``.bounds`` attribute when `depth_field`
114
+ has one.
115
+ """
116
+ import numpy as np
117
+ import shapely
118
+
119
+ if method not in ("polynomial", "wavelength", "custom", "constant"):
120
+ raise ValueError(
121
+ "method must be 'polynomial', 'wavelength', 'custom' or 'constant'")
122
+ if method == "custom" and not custom_code:
123
+ raise ValueError("method='custom' needs `custom_code`")
124
+ custom = _compile_custom_hfun(custom_code) if method == "custom" else None
125
+ if method == "constant":
126
+ from bluemesh2d.hfun_util.make_constant_hfun import make_constant_hfun
127
+ const_fun = make_constant_hfun(h_const)
128
+
129
+ detail_mask = None
130
+ if detail is not None and detail_hmin is not None:
131
+ def detail_mask(xy):
132
+ return shapely.contains_xy(detail, xy[:, 0], xy[:, 1])
133
+
134
+ def grad_mag(xy):
135
+ e = float(slope_step)
136
+ dzdx = (np.asarray(depth_field(xy + [e, 0.0]), dtype=float).reshape(-1)
137
+ - np.asarray(depth_field(xy - [e, 0.0]), dtype=float).reshape(-1)) / (2 * e)
138
+ dzdy = (np.asarray(depth_field(xy + [0.0, e]), dtype=float).reshape(-1)
139
+ - np.asarray(depth_field(xy - [0.0, e]), dtype=float).reshape(-1)) / (2 * e)
140
+ return np.hypot(dzdx, dzdy)
141
+
142
+ def hfun(test):
143
+ xy = np.atleast_2d(np.asarray(test, dtype=float))
144
+ d = np.asarray(depth_field(xy), dtype=float).reshape(-1)
145
+ d = np.where(d < 0, 0.0, d)
146
+ if method == "constant":
147
+ values = const_fun(xy)
148
+ elif method == "polynomial":
149
+ values = a * d ** 2 + b * d
150
+ elif method == "wavelength":
151
+ from bluemesh2d.hfun_util.hfun_dispersion import hfun_wavenumhunt
152
+ values = hfun_wavenumhunt(xy, d, wave_period,
153
+ cells_per_wavelength, zmin, hmin)
154
+ else:
155
+ values = np.asarray(custom(d, xy[:, 0], xy[:, 1]),
156
+ dtype=float).reshape(-1)
157
+ values = np.broadcast_to(values, d.shape).copy()
158
+ if slope_ncells is not None:
159
+ g = grad_mag(xy)
160
+ h_slope = 2.0 * np.pi * d / (slope_ncells * np.maximum(g, 1e-12))
161
+ if slope_hmin is not None:
162
+ h_slope = np.maximum(h_slope, slope_hmin)
163
+ values = np.minimum(values, h_slope)
164
+ lo = np.full(xy.shape[0], hmin, dtype=float)
165
+ if detail_mask is not None:
166
+ lo[detail_mask(xy)] = detail_hmin
167
+ # safety net: a non-finite size (e.g. a NaN leaking from raster
168
+ # nodata) must never reach the output -> fall back to the cap
169
+ values = np.where(np.isfinite(values), values, hmax)
170
+ return np.clip(values, lo, hmax)
171
+
172
+ if hasattr(depth_field, "bounds"):
173
+ hfun.bounds = depth_field.bounds
174
+ return hfun
175
+
176
+
177
+ def build_hfun_raster(raster_path, out_path, method="polynomial",
178
+ a=0.14, b=28.0,
179
+ wave_period=12.0, cells_per_wavelength=20, zmin=1.0,
180
+ custom_code=None, h_const=1000.0,
181
+ hmin=100.0, hmax=10000.0,
182
+ detail_geom=None, detail_hmin=None,
183
+ domain_geom=None,
184
+ slope_ncells=None, slope_step=None, slope_hmin=None,
185
+ max_gradient=0.1, cell_size=None,
186
+ extent_buffer=None, invert_z=False, nodata_value=None,
187
+ feedback=None):
188
+ """Build the gradient-limited element-size field and save it as a GeoTIFF.
189
+
190
+ `domain_geom` (e.g. the stage-1 water polygon) restricts hfun computation
191
+ to that polygon's extent instead of the whole raster -- both the
192
+ gradient-limiting grid and the output raster are limited to it, which is
193
+ much faster when only a small part of a large raster is meshed. The
194
+ output raster is in the working CRS; its pixel values are element sizes
195
+ in metres.
196
+
197
+ Parameters
198
+ ----------
199
+ raster_path : str
200
+ Path to the bathymetry raster (elevation, positive up).
201
+ out_path : str
202
+ Output GeoTIFF path.
203
+ method : {'polynomial', 'wavelength', 'custom', 'constant'}, optional
204
+ Sizing law, see :func:`_make_depth_hfun`. Default is
205
+ ``'polynomial'``.
206
+ a, b : float, optional
207
+ Coefficients for the ``'polynomial'`` method. Defaults are 0.14 and
208
+ 28.0.
209
+ wave_period : float, optional
210
+ Wave period ``T`` (s) for the ``'wavelength'`` method. Default is
211
+ 12.0.
212
+ cells_per_wavelength : int, optional
213
+ Cells per wavelength ``N`` for the ``'wavelength'`` method. Default
214
+ is 20.
215
+ zmin : float, optional
216
+ Minimum depth (m) for the ``'wavelength'`` method. Default is 1.0.
217
+ custom_code : str or None, optional
218
+ Python code for the ``'custom'`` method, see
219
+ :func:`_compile_custom_hfun`. Default is ``None``.
220
+ hmin, hmax : float, optional
221
+ Element-size floor and cap (m). Defaults are 100.0 and 10000.0.
222
+ detail_geom : shapely.geometry.base.BaseGeometry or None, optional
223
+ Detail-region polygon in the *raster* CRS. Default is ``None``.
224
+ detail_hmin : float or None, optional
225
+ Element-size floor (m) inside `detail_geom`. Default is ``None``.
226
+ domain_geom : shapely.geometry.base.BaseGeometry or None, optional
227
+ Domain polygon in the *raster* CRS restricting the computed extent
228
+ (e.g. the stage-1 water polygon). Default is ``None`` (whole raster).
229
+ slope_ncells : float or None, optional
230
+ Bathymetric-slope refinement, see :func:`_make_depth_hfun`.
231
+ ``None`` (default) disables it.
232
+ slope_step : float or None, optional
233
+ Finite-difference step (m) for the depth gradient; ``None``
234
+ (default) uses the raster pixel size in the working CRS.
235
+ slope_hmin : float or None, optional
236
+ Independent floor (m) for the slope term, see
237
+ :func:`_make_depth_hfun`. Default is ``None``.
238
+ max_gradient : float, optional
239
+ Maximum allowed size gradient (m/m), see ``smooth_precomput_hfun``.
240
+ Default is 0.1.
241
+ cell_size : float or None, optional
242
+ Sampling-grid resolution (m); auto-derived when ``None``. Default is
243
+ ``None``.
244
+ extent_buffer : float or None, optional
245
+ Buffer (m) padding the computed extent. ``None`` or a negative value
246
+ uses the automatic value
247
+ ``min((hmax - hmin)/max_gradient, 0.25*max(width, height))`` -- the
248
+ gradient-limiting influence radius. Default is ``None``.
249
+ feedback : object or None, optional
250
+ Feedback sink, see :func:`extract_water_polygon`.
251
+
252
+ Returns
253
+ -------
254
+ out_path : str
255
+ Path to the written GeoTIFF (same as the input `out_path`).
256
+ utm_crs : pyproj.CRS
257
+ Working CRS the output raster is written in.
258
+ """
259
+ feedback = feedback or _NullFeedback()
260
+ import numpy as np
261
+ import pyproj
262
+ import rasterio
263
+ from rasterio.transform import from_origin
264
+
265
+ from bluemesh2d.geom_util.proj_util import get_local_utm_crs, reproject_geometry
266
+ from bluemesh2d.geomesh_util.depth_field import depth_field_from_tif
267
+ from bluemesh2d.hfun_util.smooth_and_precomput import smooth_precomput_hfun
268
+
269
+ with bundled_raster_data_env(), rasterio.open(raster_path) as src:
270
+ raster_crs = _raster_crs(src)
271
+ w, h = src.width, src.height
272
+ lon = (src.transform * (np.arange(w), np.zeros(w)))[0]
273
+ lat = (src.transform * (np.zeros(h), np.arange(h)))[1]
274
+ utm_crs = get_local_utm_crs(raster_crs, lon, lat)
275
+
276
+ def _to_utm(geom):
277
+ return geom if utm_crs == raster_crs else \
278
+ reproject_geometry(geom, raster_crs, utm_crs)
279
+
280
+ # Read only the sampling window of the bathymetry (domain extent + the
281
+ # gradient-limiting margin), decimated when huge, so an oversized raster
282
+ # is never read in full. Without a domain, the whole raster is read but
283
+ # still decimated to a RAM-based cell budget.
284
+ avail = _available_ram_bytes() or 4_000_000_000
285
+ max_cells = int(min(120_000_000, max(4_000_000, 0.15 * avail / 16.0)))
286
+ raster_bbox = None
287
+ if domain_geom is not None:
288
+ dxmin, dymin, dxmax, dymax = _to_utm(domain_geom).bounds
289
+ ddw, ddh = dxmax - dxmin, dymax - dymin
290
+ cs = cell_size if cell_size is not None \
291
+ else max(max(ddw, ddh) / 1200.0, hmin / 2.0)
292
+ if extent_buffer is None or extent_buffer < 0:
293
+ mg = min((hmax - hmin) / max_gradient, 0.25 * max(ddw, ddh))
294
+ else:
295
+ mg = float(extent_buffer)
296
+ sbox = (dxmin - mg, dymin - mg, dxmax + mg, dymax + mg)
297
+ if utm_crs == raster_crs:
298
+ raster_bbox = sbox
299
+ else:
300
+ from rasterio.warp import transform_bounds as _tb
301
+ raster_bbox = _tb(utm_crs, raster_crs, *sbox)
302
+
303
+ feedback.pushInfo(f"Building depth-based size function ({method}) ...")
304
+ depth_field = depth_field_from_tif(raster_path, output_crs=utm_crs,
305
+ bbox=raster_bbox, max_cells=max_cells,
306
+ invert_z=invert_z,
307
+ nodata_value=nodata_value)
308
+
309
+ detail_u = _to_utm(detail_geom) if detail_geom is not None else None
310
+
311
+ if slope_ncells is not None and slope_step is None:
312
+ # raster pixel size in metres (working CRS), measured at the centre
313
+ cx = 0.5 * (lon[0] + lon[-1])
314
+ cy = 0.5 * (lat[0] + lat[-1])
315
+ px = abs(lon[1] - lon[0]) if len(lon) > 1 else abs(lat[1] - lat[0])
316
+ tr = pyproj.Transformer.from_crs(raster_crs, utm_crs, always_xy=True)
317
+ x0, y0 = tr.transform(cx, cy)
318
+ x1, y1 = tr.transform(cx + px, cy)
319
+ slope_step = float(np.hypot(x1 - x0, y1 - y0))
320
+ feedback.pushInfo(
321
+ f"Slope refinement on (N={slope_ncells:g}, "
322
+ f"gradient step {slope_step:.0f} m from the raster resolution).")
323
+
324
+ hfun = _make_depth_hfun(depth_field, method=method, a=a, b=b,
325
+ wave_period=wave_period,
326
+ cells_per_wavelength=cells_per_wavelength,
327
+ zmin=zmin, custom_code=custom_code,
328
+ h_const=h_const,
329
+ hmin=hmin, hmax=hmax,
330
+ detail=detail_u,
331
+ detail_hmin=(detail_hmin if detail_u is not None else None),
332
+ slope_ncells=slope_ncells,
333
+ slope_step=(slope_step if slope_step else 500.0),
334
+ slope_hmin=slope_hmin)
335
+ _check(feedback)
336
+
337
+ # region to compute hfun over: the domain (water) polygon if given, else
338
+ # the whole raster extent
339
+ if domain_geom is not None:
340
+ region = tuple(_to_utm(domain_geom).bounds)
341
+ feedback.pushInfo("Limiting hfun to the domain polygon extent.")
342
+ else:
343
+ region = tuple(depth_field.bounds)
344
+
345
+ xmin, ymin, xmax, ymax = region
346
+ dw, dh = xmax - xmin, ymax - ymin
347
+ if cell_size is None:
348
+ cell_size = max(max(dw, dh) / 1200.0, hmin / 2.0)
349
+ # pad by the gradient-limiting influence radius so boundary queries near the
350
+ # domain edge stay inside the raster (or by the user-set extent buffer)
351
+ if extent_buffer is None or extent_buffer < 0:
352
+ margin = min((hmax - hmin) / max_gradient, 0.25 * max(dw, dh))
353
+ else:
354
+ margin = float(extent_buffer)
355
+ xs = np.arange(xmin - margin, xmax + margin + cell_size, cell_size)
356
+ ys = np.arange(ymax + margin, ymin - margin - cell_size, -cell_size) # top->down
357
+ # the gradient-limiting grid and the output raster are both ~this size;
358
+ # ~40 bytes/cell covers the working float64 arrays
359
+ _warn_if_ram_risk(
360
+ feedback, 40.0 * len(xs) * len(ys),
361
+ f"The element-size grid ({len(ys)} x {len(xs)} cells "
362
+ f"@ {cell_size:.0f} m)",
363
+ hint="Increase Min element size, reduce the domain / extent buffer, "
364
+ "or provide a water polygon to limit the computed area.")
365
+ feedback.setProgress(10)
366
+
367
+ feedback.pushInfo("Gradient-limiting the size function (this can take a moment) ...")
368
+ hfuns = smooth_precomput_hfun(hfun, domain=region, max_gradient=max_gradient,
369
+ cell_size=cell_size, plot=False)
370
+ _check(feedback)
371
+ feedback.setProgress(55)
372
+
373
+ # ------------------------------- sample onto a regular UTM grid and save
374
+ feedback.pushInfo(f"Sampling size raster {len(ys)} x {len(xs)} @ {cell_size:.0f} m ...")
375
+ H = np.empty((len(ys), len(xs)), dtype=np.float32)
376
+ # sample in row blocks: bounded peak memory, cancellable, real progress
377
+ rows = max(1, int(2_000_000 // max(len(xs), 1)))
378
+ for i0 in range(0, len(ys), rows):
379
+ i1 = min(i0 + rows, len(ys))
380
+ Xc, Yc = np.meshgrid(xs, ys[i0:i1])
381
+ H[i0:i1] = np.asarray(
382
+ hfuns(np.column_stack([Xc.ravel(), Yc.ravel()])),
383
+ dtype=np.float32).reshape(i1 - i0, len(xs))
384
+ _check(feedback)
385
+ feedback.setProgress(55 + 40.0 * i1 / len(ys))
386
+
387
+ transform = from_origin(xs[0] - cell_size / 2, ys[0] + cell_size / 2,
388
+ cell_size, cell_size)
389
+ with bundled_raster_data_env(), rasterio.open(
390
+ out_path, "w", driver="GTiff", height=H.shape[0], width=H.shape[1],
391
+ count=1, dtype="float32", crs=utm_crs.to_wkt(), transform=transform,
392
+ compress="deflate",
393
+ ) as dst:
394
+ dst.write(H, 1)
395
+ feedback.pushInfo(f"Size raster written -> {out_path}")
396
+ return out_path, utm_crs
397
+
398
+
399
+ def build_hfun_constant_raster(water_geom, out_path, h_domain,
400
+ detail_geom=None, h_detail=None,
401
+ max_gradient=0.1, extent_buffer=None,
402
+ layer_crs=None, feedback=None):
403
+ """Uniform element-size raster from the water polygon (no bathymetry).
404
+
405
+ The size is `h_domain` everywhere, `h_detail` inside `detail_geom`,
406
+ gradient-limited so the transition between the two respects
407
+ `max_gradient`. The computed extent is the water polygon's bounds plus
408
+ a buffer. The output GeoTIFF is in the working CRS (the layer CRS when
409
+ already projected, a local UTM otherwise), like
410
+ :func:`build_hfun_raster`.
411
+
412
+ Parameters
413
+ ----------
414
+ water_geom : shapely.geometry.base.BaseGeometry
415
+ Water polygon (stage 1) in `layer_crs`; defines the computed extent.
416
+ out_path : str
417
+ Output GeoTIFF path.
418
+ h_domain : float
419
+ Element size (m) over the domain.
420
+ detail_geom : shapely.geometry.base.BaseGeometry or None, optional
421
+ Detail-region polygon in `layer_crs`. Default is ``None``.
422
+ h_detail : float or None, optional
423
+ Element size (m) inside `detail_geom`. Default is ``None`` (ignored).
424
+ max_gradient : float, optional
425
+ Maximum allowed size gradient (m/m). Default is 0.1.
426
+ extent_buffer : float or None, optional
427
+ Buffer (m) padding the computed extent; ``None`` or negative uses
428
+ the automatic gradient-influence radius. Default is ``None``.
429
+ layer_crs : pyproj.CRS or None, optional
430
+ CRS of the input geometries. ``None`` assumes an already-metric CRS.
431
+ feedback : object or None, optional
432
+ Feedback sink, see :func:`extract_water_polygon`.
433
+
434
+ Returns
435
+ -------
436
+ out_path : str
437
+ Path to the written GeoTIFF.
438
+ utm_crs : pyproj.CRS
439
+ Working CRS the output raster is written in.
440
+ """
441
+ feedback = feedback or _NullFeedback()
442
+ import numpy as np
443
+ import pyproj
444
+ import rasterio
445
+ import shapely
446
+ from rasterio.transform import from_origin
447
+
448
+ from bluemesh2d.geom_util.proj_util import get_local_utm_crs, reproject_geometry
449
+ from bluemesh2d.hfun_util.make_constant_hfun import make_constant_hfun
450
+ from bluemesh2d.hfun_util.smooth_and_precomput import smooth_precomput_hfun
451
+
452
+ h_domain = float(h_domain)
453
+ if not np.isfinite(h_domain) or h_domain <= 0:
454
+ raise ValueError("The domain element size must be > 0.")
455
+ use_detail = (detail_geom is not None and h_detail is not None
456
+ and float(h_detail) > 0)
457
+ h_detail = float(h_detail) if use_detail else h_domain
458
+
459
+ if layer_crs is None:
460
+ layer_crs = pyproj.CRS.from_epsg(3857) # assume metric
461
+ xmin0, ymin0, xmax0, ymax0 = water_geom.bounds
462
+ utm_crs = get_local_utm_crs(pyproj.CRS(layer_crs),
463
+ np.array([xmin0, xmax0]),
464
+ np.array([ymin0, ymax0]))
465
+ if pyproj.CRS(layer_crs) != utm_crs:
466
+ water_u = reproject_geometry(water_geom, layer_crs, utm_crs)
467
+ detail_u = (reproject_geometry(detail_geom, layer_crs, utm_crs)
468
+ if use_detail else None)
469
+ else:
470
+ water_u = water_geom
471
+ detail_u = detail_geom if use_detail else None
472
+
473
+ base = make_constant_hfun(h_domain, bounds=water_u.bounds)
474
+ if use_detail:
475
+ def hfun(xy):
476
+ xy = np.atleast_2d(np.asarray(xy, dtype=float))
477
+ v = base(xy)
478
+ v[shapely.contains_xy(detail_u, xy[:, 0], xy[:, 1])] = h_detail
479
+ return v
480
+ hfun.bounds = water_u.bounds
481
+ else:
482
+ hfun = base
483
+
484
+ hmin = min(h_domain, h_detail)
485
+ hmax = max(h_domain, h_detail)
486
+ region = tuple(water_u.bounds)
487
+ xmin, ymin, xmax, ymax = region
488
+ dw, dh = xmax - xmin, ymax - ymin
489
+ cell_size = max(max(dw, dh) / 1200.0, hmin / 2.0)
490
+ if extent_buffer is None or extent_buffer < 0:
491
+ margin = min((hmax - hmin) / max_gradient, 0.25 * max(dw, dh))
492
+ margin = max(margin, 2.0 * cell_size)
493
+ else:
494
+ margin = float(extent_buffer)
495
+ xs = np.arange(xmin - margin, xmax + margin + cell_size, cell_size)
496
+ ys = np.arange(ymax + margin, ymin - margin - cell_size, -cell_size)
497
+ _warn_if_ram_risk(
498
+ feedback, 40.0 * len(xs) * len(ys),
499
+ f"The element-size grid ({len(ys)} x {len(xs)} cells "
500
+ f"@ {cell_size:.0f} m)",
501
+ hint="Increase the element sizes or reduce the extent buffer.")
502
+ feedback.setProgress(10)
503
+
504
+ if hmax > hmin:
505
+ feedback.pushInfo(
506
+ "Gradient-limiting the size function (this can take a moment) ...")
507
+ hfuns = smooth_precomput_hfun(hfun, domain=region,
508
+ max_gradient=max_gradient,
509
+ cell_size=cell_size, plot=False)
510
+ else:
511
+ feedback.pushInfo("Uniform size: no gradient limiting needed.")
512
+ hfuns = hfun
513
+ _check(feedback)
514
+ feedback.setProgress(55)
515
+
516
+ feedback.pushInfo(
517
+ f"Sampling size raster {len(ys)} x {len(xs)} @ {cell_size:.0f} m ...")
518
+ H = np.empty((len(ys), len(xs)), dtype=np.float32)
519
+ rows = max(1, int(2_000_000 // max(len(xs), 1)))
520
+ for i0 in range(0, len(ys), rows):
521
+ i1 = min(i0 + rows, len(ys))
522
+ Xc, Yc = np.meshgrid(xs, ys[i0:i1])
523
+ H[i0:i1] = np.asarray(
524
+ hfuns(np.column_stack([Xc.ravel(), Yc.ravel()])),
525
+ dtype=np.float32).reshape(i1 - i0, len(xs))
526
+ _check(feedback)
527
+ feedback.setProgress(55 + 40.0 * i1 / len(ys))
528
+
529
+ transform = from_origin(xs[0] - cell_size / 2, ys[0] + cell_size / 2,
530
+ cell_size, cell_size)
531
+ with bundled_raster_data_env(), rasterio.open(
532
+ out_path, "w", driver="GTiff", height=H.shape[0], width=H.shape[1],
533
+ count=1, dtype="float32", crs=utm_crs.to_wkt(), transform=transform,
534
+ compress="deflate",
535
+ ) as dst:
536
+ dst.write(H, 1)
537
+ feedback.pushInfo(f"Size raster written -> {out_path}")
538
+ return out_path, utm_crs
539
+
540
+
541
+ def load_hfun_raster(hfun_path):
542
+ """Load a size raster (stage 2 output) into a fast callable size function.
543
+
544
+ Parameters
545
+ ----------
546
+ hfun_path : str
547
+ Path to the GeoTIFF written by :func:`build_hfun_raster`.
548
+
549
+ Returns
550
+ -------
551
+ hfuns : callable
552
+ ``hfuns(xy) -> h``, element size (m) at query points ``xy`` of shape
553
+ ``(N, 2)``, linearly interpolated from the raster. Exposes
554
+ ``.bounds`` (raster bounds) and ``.crs_wkt`` (WKT of the raster's
555
+ working CRS, or ``None``).
556
+ """
557
+ import numpy as np
558
+ import rasterio
559
+ from scipy.interpolate import RegularGridInterpolator
560
+
561
+ with bundled_raster_data_env(), rasterio.open(hfun_path) as src:
562
+ H = src.read(1).astype(float)
563
+ t = src.transform
564
+ xs = t.c + t.a * (np.arange(src.width) + 0.5)
565
+ ys = t.f + t.e * (np.arange(src.height) + 0.5)
566
+ bounds = tuple(src.bounds)
567
+ crs_wkt = src.crs.to_wkt() if src.crs else None
568
+
569
+ if ys[0] > ys[-1]: # RegularGridInterpolator needs increasing axes
570
+ ys, H = ys[::-1], H[::-1, :]
571
+ interp = RegularGridInterpolator((ys, xs), H, method="linear",
572
+ bounds_error=False, fill_value=None)
573
+ h_lo, h_hi = float(H.min()), float(H.max())
574
+
575
+ def hfuns(xy):
576
+ xy = np.atleast_2d(np.asarray(xy, dtype=float))
577
+ h = interp(np.column_stack([xy[:, 1], xy[:, 0]]))
578
+ np.clip(h, h_lo, h_hi, out=h)
579
+ return h
580
+
581
+ hfuns.bounds = bounds
582
+ hfuns.crs_wkt = crs_wkt # the working (UTM) CRS the grid lives in
583
+ return hfuns
584
+
585
+
586
+ # ===========================================================================
587
+ # Stage 3: polygon + hfun -> resampled boundary + PSLG
588
+ # ===========================================================================
589
+
@@ -0,0 +1,96 @@
1
+ import numpy as np
2
+
3
+
4
+ def hfun_wavenumhunt(test, depth_field, T, N, zmin, hmin):
5
+ """Build a mesh-size function from local wavelength.
6
+
7
+ Parameters
8
+ ----------
9
+ test : ndarray of shape (N, 2)
10
+ Coordinates where the mesh size is evaluated.
11
+ depth_field : callable or array_like
12
+ Water depth at each query point; a scalar or length-``N`` array.
13
+ T : float
14
+ Wave period in seconds.
15
+ N : int
16
+ Target number of cells per wavelength.
17
+ zmin : float
18
+ Minimum water depth used in the dispersion relation.
19
+ hmin : float
20
+ Minimum allowed mesh size.
21
+
22
+ Returns
23
+ -------
24
+ hfun : ndarray of shape (N,)
25
+ Target cell size at each query point.
26
+ """
27
+ # Determine local depth
28
+ if callable(depth_field):
29
+ h = depth_field(test)
30
+ else:
31
+ h = np.asarray(depth_field)
32
+ if h.size == 1:
33
+ h = np.full(test.shape[0], h)
34
+ elif h.shape[0] != test.shape[0]:
35
+ raise ValueError("depth_field must match number of test points")
36
+
37
+ # Compute wavelength
38
+ h = np.maximum(h, zmin)
39
+ L, _ = wavenumhunt(T, h)
40
+
41
+ # Mesh size proportional to wavelength
42
+ hfun = L / N
43
+
44
+ # Enforce minimum mesh size
45
+ hfun = np.maximum(hfun, hmin)
46
+
47
+ return hfun
48
+
49
+
50
+ def wavenumhunt(T, h):
51
+ """Compute wavelength and wavenumber via the Hunt (1979) approximation.
52
+
53
+ Parameters
54
+ ----------
55
+ T : float or array_like
56
+ Wave period in seconds.
57
+ h : float or array_like
58
+ Water depth in metres.
59
+
60
+ Returns
61
+ -------
62
+ L : float or ndarray
63
+ Wavelength in metres.
64
+ k : float or ndarray
65
+ Wavenumber in 1/m.
66
+ """
67
+ D = np.array(
68
+ [
69
+ 0.6666666666,
70
+ 0.3555555555,
71
+ 0.1608465608,
72
+ 0.0632098765,
73
+ 0.0217540484,
74
+ 0.0065407983,
75
+ ]
76
+ )
77
+
78
+ L0 = (9.81 * T**2) / (2 * np.pi)
79
+ k0 = 2 * np.pi / L0
80
+ k0h = k0 * h
81
+
82
+ # Approximation of kh following Hunt (1979)
83
+ poly = (
84
+ D[0] * k0h**1
85
+ + D[1] * k0h**2
86
+ + D[2] * k0h**3
87
+ + D[3] * k0h**4
88
+ + D[4] * k0h**5
89
+ + D[5] * k0h**6
90
+ )
91
+ kh = k0h * np.sqrt(1 + (k0h * (1 + poly)) ** -1)
92
+
93
+ k = kh / h
94
+ L = 2 * np.pi / k
95
+
96
+ return L, k