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
bluemesh2d/meshgen.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Stage 4: PSLG + hfun -> triangular mesh (refine / smooth / smood)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
5
|
+
|
|
6
|
+
from .dependencies import smood_dependencies
|
|
7
|
+
from .feedback import _LogWriter, _NullFeedback, _available_ram_bytes, _check
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _warn_if_mesh_too_big(node, edge, hfuns, feedback):
|
|
11
|
+
"""Estimate the refined mesh size and warn when it looks RAM-risky.
|
|
12
|
+
|
|
13
|
+
The expected triangle count is ``~(2/sqrt(3)) * integral(dA / h^2)``,
|
|
14
|
+
evaluated by sampling `hfuns` on a coarse grid over the PSLG polygons.
|
|
15
|
+
Estimation errors are fine here -- this only decides whether to warn.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
import numpy as np
|
|
19
|
+
import shapely
|
|
20
|
+
from shapely.ops import polygonize
|
|
21
|
+
|
|
22
|
+
rings = polygonize(
|
|
23
|
+
[((node[a][0], node[a][1]), (node[b][0], node[b][1]))
|
|
24
|
+
for a, b in edge])
|
|
25
|
+
area_geom = shapely.unary_union(list(rings))
|
|
26
|
+
if area_geom.is_empty:
|
|
27
|
+
return
|
|
28
|
+
xmin, ymin, xmax, ymax = area_geom.bounds
|
|
29
|
+
n = 128
|
|
30
|
+
xs = np.linspace(xmin, xmax, n)
|
|
31
|
+
ys = np.linspace(ymin, ymax, n)
|
|
32
|
+
X, Y = np.meshgrid(xs, ys)
|
|
33
|
+
xy = np.column_stack([X.ravel(), Y.ravel()])
|
|
34
|
+
inside = shapely.contains_xy(area_geom, xy[:, 0], xy[:, 1])
|
|
35
|
+
if not inside.any():
|
|
36
|
+
return
|
|
37
|
+
h = np.asarray(hfuns(xy[inside]), dtype=float)
|
|
38
|
+
cell_area = area_geom.area / inside.sum()
|
|
39
|
+
n_tria = 2.0 / np.sqrt(3.0) * cell_area * np.sum(1.0 / h ** 2)
|
|
40
|
+
if n_tria > 500_000:
|
|
41
|
+
msg = (f"The size function implies roughly {n_tria / 1e6:.1f} "
|
|
42
|
+
"million triangles; refinement may take a long time.")
|
|
43
|
+
est_bytes = 1000.0 * n_tria # ~1 kB per triangle during refine/smooth
|
|
44
|
+
avail = _available_ram_bytes()
|
|
45
|
+
if avail is not None and est_bytes > 0.5 * avail:
|
|
46
|
+
msg += (f" Estimated memory ~{est_bytes / 1e9:.1f} GB of "
|
|
47
|
+
f"~{avail / 1e9:.1f} GB available -- QGIS may become "
|
|
48
|
+
"unresponsive or crash.")
|
|
49
|
+
msg += (" Consider increasing Min element size or reducing the "
|
|
50
|
+
"domain.")
|
|
51
|
+
feedback.pushWarning(msg)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass # a failed estimate must never block the run
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _locate_fixed(vert, fixed_points, feedback, tol=1e-6):
|
|
57
|
+
"""Return indices in ``vert`` of each fixed point (nearest within tol)."""
|
|
58
|
+
import numpy as np
|
|
59
|
+
|
|
60
|
+
idx = []
|
|
61
|
+
for p in np.asarray(fixed_points, dtype=float):
|
|
62
|
+
d2 = np.sum((vert - p) ** 2, axis=1)
|
|
63
|
+
j = int(np.argmin(d2))
|
|
64
|
+
if d2[j] <= tol * tol:
|
|
65
|
+
idx.append(j)
|
|
66
|
+
else:
|
|
67
|
+
feedback.pushWarning(
|
|
68
|
+
f"Fixed point ({p[0]:.3f}, {p[1]:.3f}) not found in mesh "
|
|
69
|
+
f"(nearest node {np.sqrt(d2[j]):.3g} m away); skipped.")
|
|
70
|
+
return np.asarray(idx, dtype=int)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def mesh_pslg(node, edge, hfuns, kind="delaunay", do_smooth=True,
|
|
74
|
+
do_smood=False, smood_merge_small_links=False,
|
|
75
|
+
fixed_points=None, feedback=None):
|
|
76
|
+
"""Refine a PSLG, then optionally smooth and/or smood it.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
node : ndarray of shape (N, 2)
|
|
81
|
+
PSLG node coordinates (working CRS).
|
|
82
|
+
edge : ndarray of shape (E, 2)
|
|
83
|
+
PSLG edges (0-based node indices).
|
|
84
|
+
hfuns : callable
|
|
85
|
+
Element-size function, ``hfuns(xy) -> h``.
|
|
86
|
+
kind : {'delaunay', 'delfront'}, optional
|
|
87
|
+
Refinement scheme passed to ``refine``. Default is ``'delaunay'``.
|
|
88
|
+
do_smooth : bool, optional
|
|
89
|
+
If ``True`` (default), run non-linear mesh optimisation (``smooth``)
|
|
90
|
+
after refinement.
|
|
91
|
+
do_smood : bool, optional
|
|
92
|
+
If ``True``, additionally run orthogonalization (``smood``) after
|
|
93
|
+
smoothing. Default is ``False``.
|
|
94
|
+
smood_merge_small_links : bool, optional
|
|
95
|
+
Enable the merge step inside smood's ortho-merge cycles (pairs of
|
|
96
|
+
triangles whose circumcenters are too close are merged, then
|
|
97
|
+
re-split). Use only when the default triangle-only smood cannot
|
|
98
|
+
remove the remaining small flow links. Default is ``False``.
|
|
99
|
+
fixed_points : ndarray of shape (K, 2), optional
|
|
100
|
+
XY coordinates (working CRS) of points that must appear as mesh
|
|
101
|
+
nodes at exactly these positions: they are inserted before
|
|
102
|
+
refinement and pinned during smoothing and orthogonalization.
|
|
103
|
+
Points outside the meshed domain or coincident with boundary
|
|
104
|
+
nodes are ignored (with a warning).
|
|
105
|
+
feedback : object or None, optional
|
|
106
|
+
Feedback sink, see :func:`extract_water_polygon`.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
vert : ndarray of shape (M, 2)
|
|
111
|
+
Mesh vertex coordinates (working CRS).
|
|
112
|
+
tria : ndarray of shape (T, 3)
|
|
113
|
+
Triangle connectivity (0-based vertex indices).
|
|
114
|
+
"""
|
|
115
|
+
feedback = feedback or _NullFeedback()
|
|
116
|
+
from bluemesh2d.refine import refine
|
|
117
|
+
from bluemesh2d.smooth import smooth
|
|
118
|
+
|
|
119
|
+
kind = str(kind).lower()
|
|
120
|
+
if kind not in ("delaunay", "delfront"):
|
|
121
|
+
raise ValueError("kind must be 'delaunay' or 'delfront'")
|
|
122
|
+
|
|
123
|
+
import numpy as np
|
|
124
|
+
|
|
125
|
+
if fixed_points is not None:
|
|
126
|
+
fixed_points = np.asarray(fixed_points, dtype=float).reshape(-1, 2)
|
|
127
|
+
if fixed_points.size:
|
|
128
|
+
# drop fixed points (nearly) coincident with existing PSLG nodes:
|
|
129
|
+
# a duplicate vertex would break the triangulation
|
|
130
|
+
keep_fp = np.ones(fixed_points.shape[0], dtype=bool)
|
|
131
|
+
for i, p in enumerate(fixed_points):
|
|
132
|
+
if np.min(np.sum((node - p) ** 2, axis=1)) < 1e-6:
|
|
133
|
+
keep_fp[i] = False
|
|
134
|
+
feedback.pushWarning(
|
|
135
|
+
f"Fixed point ({p[0]:.3f}, {p[1]:.3f}) coincides with "
|
|
136
|
+
"a boundary node; skipped.")
|
|
137
|
+
fixed_points = fixed_points[keep_fp]
|
|
138
|
+
if fixed_points.size:
|
|
139
|
+
feedback.pushInfo(f"Inserting {len(fixed_points)} fixed point(s)")
|
|
140
|
+
node = np.vstack([node, fixed_points])
|
|
141
|
+
else:
|
|
142
|
+
fixed_points = None
|
|
143
|
+
|
|
144
|
+
_warn_if_mesh_too_big(node, edge, hfuns, feedback)
|
|
145
|
+
feedback.setProgress(5)
|
|
146
|
+
|
|
147
|
+
feedback.pushInfo(f"Refining mesh ({len(node)} boundary nodes, kind={kind}) ...")
|
|
148
|
+
with contextlib.redirect_stdout(_LogWriter(feedback)):
|
|
149
|
+
vert, etri, tria, tnum = refine(node, edge, [], {"kind": kind}, hfuns)
|
|
150
|
+
_check(feedback)
|
|
151
|
+
feedback.pushInfo(f"Refined: {len(vert)} nodes, {len(tria)} triangles")
|
|
152
|
+
feedback.setProgress(55)
|
|
153
|
+
|
|
154
|
+
# refine never moves input nodes, so fixed points can be re-located by
|
|
155
|
+
# coordinate after each stage (indices change with mesh compaction)
|
|
156
|
+
fixed_idx = None
|
|
157
|
+
if fixed_points is not None:
|
|
158
|
+
fixed_idx = _locate_fixed(vert, fixed_points, feedback)
|
|
159
|
+
# keep only the points actually present (e.g. outside the domain,
|
|
160
|
+
# dropped by refine) so later stages don't re-warn about them
|
|
161
|
+
fixed_points = vert[fixed_idx, :].copy()
|
|
162
|
+
if fixed_points.size == 0:
|
|
163
|
+
fixed_points = None
|
|
164
|
+
fixed_idx = None
|
|
165
|
+
|
|
166
|
+
if do_smooth:
|
|
167
|
+
feedback.pushInfo("Smoothing mesh ...")
|
|
168
|
+
with contextlib.redirect_stdout(_LogWriter(feedback)):
|
|
169
|
+
vert, etri, tria, tnum = smooth(vert, etri, tria, tnum, {}, hfuns,
|
|
170
|
+
fixed=fixed_idx)
|
|
171
|
+
_check(feedback)
|
|
172
|
+
if fixed_points is not None:
|
|
173
|
+
fixed_idx = _locate_fixed(vert, fixed_points, feedback)
|
|
174
|
+
feedback.setProgress(85)
|
|
175
|
+
|
|
176
|
+
if do_smood:
|
|
177
|
+
missing = smood_dependencies()
|
|
178
|
+
if missing:
|
|
179
|
+
raise RuntimeError(
|
|
180
|
+
"smood (orthogonalization) requires: " + ", ".join(missing)
|
|
181
|
+
+ ". Install it, or disable the smood option.")
|
|
182
|
+
feedback.pushInfo("Applying smood (orthogonalization) ...")
|
|
183
|
+
from bluemesh2d.smood import smood
|
|
184
|
+
smood_opts = {}
|
|
185
|
+
if smood_merge_small_links:
|
|
186
|
+
feedback.pushInfo("smood: small-link merging enabled")
|
|
187
|
+
smood_opts["merge_small_links"] = True
|
|
188
|
+
try:
|
|
189
|
+
with contextlib.redirect_stdout(_LogWriter(feedback)):
|
|
190
|
+
vert, etri, tria, tnum = smood(vert, etri, tria, tnum, smood_opts,
|
|
191
|
+
fixed=fixed_idx)
|
|
192
|
+
except ImportError as exc:
|
|
193
|
+
raise RuntimeError(
|
|
194
|
+
f"smood needs an optional package that is not installed: {exc}. "
|
|
195
|
+
"Install it or disable the smood option.")
|
|
196
|
+
_check(feedback)
|
|
197
|
+
feedback.pushInfo(f"After smood: {len(vert)} nodes, {len(tria)} faces")
|
|
198
|
+
|
|
199
|
+
return vert, tria
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# UGRID fill values matching bluemesh2d.geomesh_util.grd_util
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Named constants shared across the ``ortho_merge`` orthogonalization/merge pipeline.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# Earth radius (WGS84 spherical approximation), aligned with Delft's
|
|
8
|
+
# `physicalconsts`. Used for lon/lat <-> local-metric distance conversions.
|
|
9
|
+
EARTH_RADIUS = 6378137.0
|
|
10
|
+
DEG2RAD = np.pi / 180.0
|
|
11
|
+
RAD2DEG = 180.0 / np.pi
|
|
12
|
+
EARTH_RADIUS_DEG2RAD = EARTH_RADIUS * DEG2RAD
|
|
13
|
+
EARTH_RADIUS_SQ = EARTH_RADIUS * EARTH_RADIUS
|
|
14
|
+
|
|
15
|
+
# Distance-to-pole tolerance (degrees) below which pole-specific handling
|
|
16
|
+
# kicks in for spherical distance/circumcenter computations.
|
|
17
|
+
DTOL_POLE = 1.0e-6
|
|
18
|
+
|
|
19
|
+
# Default "small flow link" threshold (Delft3D-FM convention): an internal
|
|
20
|
+
# edge is flagged as a small flow link when the distance between the two
|
|
21
|
+
# adjacent triangle circumcenters is below
|
|
22
|
+
# `0.9 * threshold * 0.5 * (sqrt(area1) + sqrt(area2))`.
|
|
23
|
+
DEFAULT_SMALLLINK_THRESHOLD = 0.11
|
|
24
|
+
|
|
25
|
+
# Base amplitude of the per-edge orthogonalization displacement inside
|
|
26
|
+
# `apply_combined_ortho_smoother_to_zone` (conservative, to avoid overshoot).
|
|
27
|
+
DEFAULT_ORTHO_ALPHA = 0.025
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Small, dependency-free geometric/topological helpers shared across the
|
|
3
|
+
``ortho_merge`` modules.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_edges_from_tria(tria: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
14
|
+
"""
|
|
15
|
+
Build (edge_nodes, edge_faces) from 0-based triangles.
|
|
16
|
+
|
|
17
|
+
Edges are numbered by first occurrence in face-then-edge traversal order,
|
|
18
|
+
each edge as (min_node, max_node); an edge's two faces are in traversal
|
|
19
|
+
order (right face = -1 for boundary), with any further faces of a
|
|
20
|
+
non-manifold edge ignored.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
tria : (T,3) int array, 0-based.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
edge_nodes : (E,2) int64
|
|
29
|
+
edge_faces : (E,2) int64 (right face = -1 for boundary)
|
|
30
|
+
"""
|
|
31
|
+
tria = np.asarray(tria, dtype=np.int64)
|
|
32
|
+
if tria.ndim != 2 or tria.shape[1] != 3:
|
|
33
|
+
raise ValueError("tria must be an array of shape (T,3) with 0-based indices")
|
|
34
|
+
|
|
35
|
+
n_faces = tria.shape[0]
|
|
36
|
+
if n_faces == 0:
|
|
37
|
+
return np.zeros((0, 2), dtype=np.int64), np.zeros((0, 2), dtype=np.int64)
|
|
38
|
+
# Half-edges in traversal order: (a,b), (b,c), (c,a) per face.
|
|
39
|
+
he_a = tria[:, [0, 1, 2]].ravel()
|
|
40
|
+
he_b = tria[:, [1, 2, 0]].ravel()
|
|
41
|
+
he_f = np.repeat(np.arange(n_faces, dtype=np.int64), 3)
|
|
42
|
+
lo = np.minimum(he_a, he_b)
|
|
43
|
+
hi = np.maximum(he_a, he_b)
|
|
44
|
+
key = lo * np.int64(max(int(tria.max(initial=-1)) + 2, 1)) + hi
|
|
45
|
+
|
|
46
|
+
_, first_pos, inverse = np.unique(key, return_index=True, return_inverse=True)
|
|
47
|
+
insertion = np.argsort(first_pos, kind="stable")
|
|
48
|
+
rank = np.empty(insertion.size, dtype=np.int64)
|
|
49
|
+
rank[insertion] = np.arange(insertion.size)
|
|
50
|
+
edge_id = rank[inverse]
|
|
51
|
+
|
|
52
|
+
edge_nodes = np.column_stack([lo[first_pos][insertion], hi[first_pos][insertion]])
|
|
53
|
+
edge_faces = np.full((edge_nodes.shape[0], 2), -1, dtype=np.int64)
|
|
54
|
+
pos = np.argsort(edge_id, kind="stable")
|
|
55
|
+
sid = edge_id[pos]
|
|
56
|
+
is_first = np.r_[True, sid[1:] != sid[:-1]]
|
|
57
|
+
starts = np.flatnonzero(is_first)
|
|
58
|
+
edge_faces[sid[starts], 0] = he_f[pos[starts]]
|
|
59
|
+
seconds = starts + 1
|
|
60
|
+
seconds = seconds[seconds < sid.size]
|
|
61
|
+
seconds = seconds[~is_first[seconds]]
|
|
62
|
+
edge_faces[sid[seconds], 1] = he_f[pos[seconds]]
|
|
63
|
+
|
|
64
|
+
return edge_nodes, edge_faces
|