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.
- bluemesh2d/_version.py +24 -0
- bluemesh2d/aabb_tree/findball.py +121 -0
- bluemesh2d/aabb_tree/findtria.py +299 -0
- bluemesh2d/aabb_tree/maketree.py +147 -0
- bluemesh2d/aabb_tree/mapvert.py +72 -0
- bluemesh2d/aabb_tree/queryset.py +83 -0
- bluemesh2d/aabb_tree/scantree.py +124 -0
- bluemesh2d/dependencies.py +83 -0
- bluemesh2d/feedback.py +142 -0
- bluemesh2d/geom_util/boundary_util.py +216 -0
- bluemesh2d/geom_util/getiso.py +194 -0
- bluemesh2d/geom_util/poly_util.py +795 -0
- bluemesh2d/geom_util/proj_util.py +250 -0
- bluemesh2d/geomesh_util/border_util.py +283 -0
- bluemesh2d/geomesh_util/depth_field.py +333 -0
- bluemesh2d/geomesh_util/grd_util.py +1000 -0
- bluemesh2d/geomesh_util/interpolation_mesh.py +318 -0
- bluemesh2d/geomesh_util/merge_circumcenters.py +268 -0
- bluemesh2d/geomesh_util/water_polygon.py +406 -0
- bluemesh2d/hfun_util/build_hfun.py +589 -0
- bluemesh2d/hfun_util/hfun_dispersion.py +96 -0
- bluemesh2d/hfun_util/lfshfn.py +110 -0
- bluemesh2d/hfun_util/limhfn.py +65 -0
- bluemesh2d/hfun_util/make_constant_hfun.py +32 -0
- bluemesh2d/hfun_util/make_depth_hfun.py +51 -0
- bluemesh2d/hfun_util/smooth_and_precomput.py +188 -0
- bluemesh2d/hfun_util/trihfn.py +84 -0
- bluemesh2d/hjac_util/limgrad.py +105 -0
- bluemesh2d/mesh_ball/cdtbal1.py +38 -0
- bluemesh2d/mesh_ball/cdtbal2.py +104 -0
- bluemesh2d/mesh_ball/inv_2x2.py +61 -0
- bluemesh2d/mesh_ball/inv_3x3.py +72 -0
- bluemesh2d/mesh_ball/pwrbal2.py +134 -0
- bluemesh2d/mesh_ball/tribal2.py +27 -0
- bluemesh2d/mesh_cost/relhfn.py +62 -0
- bluemesh2d/mesh_cost/triang.py +59 -0
- bluemesh2d/mesh_cost/triarea.py +51 -0
- bluemesh2d/mesh_cost/trideg.py +44 -0
- bluemesh2d/mesh_cost/triscr.py +43 -0
- bluemesh2d/mesh_file/bnd_util.py +664 -0
- bluemesh2d/mesh_file/loadmsh.py +165 -0
- bluemesh2d/mesh_file/ugrid.py +308 -0
- bluemesh2d/mesh_util/cfmtri.py +118 -0
- bluemesh2d/mesh_util/deltri.py +131 -0
- bluemesh2d/mesh_util/idxtri.py +53 -0
- bluemesh2d/mesh_util/isfeat.py +90 -0
- bluemesh2d/mesh_util/minlen.py +46 -0
- bluemesh2d/mesh_util/setset.py +49 -0
- bluemesh2d/mesh_util/tricon.py +90 -0
- bluemesh2d/mesh_util/tridiv.py +187 -0
- bluemesh2d/meshgen.py +202 -0
- bluemesh2d/ortho_merge/constants.py +27 -0
- bluemesh2d/ortho_merge/geometry.py +64 -0
- bluemesh2d/ortho_merge/ortho_merge_iter.py +812 -0
- bluemesh2d/ortho_merge/orthogonalize.py +2989 -0
- bluemesh2d/pipeline.py +277 -0
- bluemesh2d/poly_data/airfoil.msh +958 -0
- bluemesh2d/poly_data/channel.msh +212 -0
- bluemesh2d/poly_data/islands.msh +13819 -0
- bluemesh2d/poly_data/lake.msh +612 -0
- bluemesh2d/poly_data/river.msh +690 -0
- bluemesh2d/poly_test/inpoly.py +105 -0
- bluemesh2d/poly_test/inpoly_mat.py +104 -0
- bluemesh2d/refine.py +972 -0
- bluemesh2d/smood.py +623 -0
- bluemesh2d/smooth.py +522 -0
- bluemesh2d/tricost.py +426 -0
- bluemesh2d/tridemo.py +723 -0
- bluemesh2d/triread.py +53 -0
- bluemesh2d-0.1.1.dev0.dist-info/METADATA +139 -0
- bluemesh2d-0.1.1.dev0.dist-info/RECORD +74 -0
- bluemesh2d-0.1.1.dev0.dist-info/WHEEL +5 -0
- bluemesh2d-0.1.1.dev0.dist-info/licenses/LICENSE +674 -0
- 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
|