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
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