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,250 @@
1
+ import numpy as np
2
+ import pyproj
3
+ from shapely.ops import transform
4
+
5
+
6
+ def get_utm_crs_from_crs(crs):
7
+ """Return a UTM CRS suited to the given geographic CRS.
8
+
9
+ Parameters
10
+ ----------
11
+ crs : pyproj.CRS
12
+ Input coordinate reference system.
13
+
14
+ Returns
15
+ -------
16
+ pyproj.CRS
17
+ UTM CRS for the origin of ``crs``, or ``crs`` unchanged if already projected.
18
+ """
19
+ if crs.is_projected:
20
+ return crs
21
+ transformer = pyproj.Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
22
+ lon0, lat0 = transformer.transform(0, 0)
23
+ utm_zone = int((lon0 + 180) / 6) + 1
24
+ epsg_code = 326 if lat0 >= 0 else 327
25
+ return pyproj.CRS.from_epsg(epsg_code * 100 + utm_zone)
26
+
27
+ def get_local_utm_crs(crs, x=None, y=None, bbox=None):
28
+ """Return a local Transverse Mercator CRS centered on the data.
29
+
30
+ If ``crs`` is already projected, it is returned unchanged. Otherwise the
31
+ data center is converted to WGS84 and a Transverse Mercator projection is
32
+ built with that central meridian and origin latitude (units in metres).
33
+
34
+ Parameters
35
+ ----------
36
+ crs : pyproj.CRS
37
+ CRS of the input data (geographic or projected).
38
+ x, y : array-like, optional
39
+ Point coordinates (same size). Ignored if bbox is provided.
40
+ bbox : tuple, optional
41
+ (xmin, ymin, xmax, ymax) in the input CRS.
42
+ Used if (x, y) are not provided.
43
+
44
+ Returns
45
+ -------
46
+ pyproj.CRS
47
+ Projected CRS in meters, centered on the data's area.
48
+ """
49
+ crs = pyproj.CRS.from_user_input(crs)
50
+ if crs.is_projected:
51
+ return crs
52
+
53
+ # Compute the center in the source CRS
54
+ if bbox is not None:
55
+ xmin, ymin, xmax, ymax = bbox
56
+ x_center = (xmin + xmax) / 2.0
57
+ y_center = (ymin + ymax) / 2.0
58
+ elif x is not None and y is not None:
59
+ x_center = np.nanmean(np.asarray(x))
60
+ y_center = np.nanmean(np.asarray(y))
61
+ else:
62
+ raise ValueError("Provide either (x, y) or bbox.")
63
+
64
+ # Convert the center to WGS84
65
+ transformer = pyproj.Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
66
+ lon_center, lat_center = transformer.transform(x_center, y_center)
67
+
68
+ # Local Transverse Mercator: central meridian = lon_center, origin latitude = lat_center
69
+ # k=1 at the central meridian, units in meters
70
+ wkt = (
71
+ f'PROJCS["UTM local",'
72
+ f'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],'
73
+ f'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
74
+ f'PROJECTION["Transverse_Mercator"],'
75
+ f'PARAMETER["latitude_of_origin",{lat_center}],'
76
+ f'PARAMETER["central_meridian",{lon_center}],'
77
+ f'PARAMETER["scale_factor",1],'
78
+ f'PARAMETER["false_easting",0],'
79
+ f'PARAMETER["false_northing",0],'
80
+ f'UNIT["metre",1]]'
81
+ )
82
+ return pyproj.CRS.from_wkt(wkt)
83
+
84
+ def get_proj_crs_from_ll(lon0, lat0):
85
+ """Create a local Transverse Mercator CRS centered on given coordinates.
86
+
87
+ Designed to match Delft3D-FM cartesian distance calculations when
88
+ ``jsferic=0``: Transverse Mercator with scale factor ``k=1.0`` at the
89
+ central meridian, units in metres.
90
+
91
+ Parameters
92
+ ----------
93
+ lon0 : float
94
+ Longitude of the projection center (degrees).
95
+ lat0 : float
96
+ Latitude of the projection center (degrees).
97
+
98
+ Returns
99
+ -------
100
+ pyproj.CRS
101
+ Local Transverse Mercator CRS in metres, centered on ``(lon0, lat0)``.
102
+
103
+ Notes
104
+ -----
105
+ Uses ``k=1.0`` for local accuracy rather than the UTM standard ``k=0.9996``.
106
+ """
107
+
108
+ # Use k=1.0 for better local accuracy (vs k=0.9996 for UTM)
109
+ # This matches Delft3D's local plane projection behavior
110
+ proj_local = pyproj.CRS.from_proj4(
111
+ f"+proj=tmerc +lat_0={lat0} +lon_0={lon0} +k=1.0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
112
+ )
113
+ return proj_local
114
+
115
+
116
+ def reproject_node(node, crs_from, crs_to):
117
+ """Reproject 2D vertex coordinates from one CRS to another.
118
+
119
+ Parameters
120
+ ----------
121
+ node : ndarray of shape (N, 2)
122
+ Array of vertex coordinates to be reprojected.
123
+ crs_from : pyproj.CRS
124
+ Source coordinate reference system.
125
+ crs_to : pyproj.CRS
126
+ Target coordinate reference system.
127
+
128
+ Returns
129
+ -------
130
+ ndarray of shape (N, 2)
131
+ Reprojected vertex coordinates.
132
+ """
133
+
134
+ node = np.asarray(node)
135
+ transformer = pyproj.Transformer.from_crs(crs_from, crs_to, always_xy=True)
136
+ x2, y2 = transformer.transform(node[:, 0], node[:, 1])
137
+ return np.column_stack((x2, y2))
138
+
139
+
140
+ def reproject_geometry(geom, crs_from, crs_to):
141
+ """Reproject a Shapely geometry or coordinate array between CRSs.
142
+
143
+ Parameters
144
+ ----------
145
+ geom : ndarray of shape (N, 2) or shapely geometry
146
+ Coordinates or geometry to reproject.
147
+ crs_from : pyproj.CRS or str
148
+ Source coordinate reference system.
149
+ crs_to : pyproj.CRS or str
150
+ Target coordinate reference system.
151
+
152
+ Returns
153
+ -------
154
+ ndarray or shapely geometry
155
+ Reprojected geometry or coordinate array (same type as input).
156
+ """
157
+
158
+ transformer = pyproj.Transformer.from_crs(crs_from, crs_to, always_xy=True)
159
+ return transform(transformer.transform, geom).buffer(0)
160
+
161
+
162
+ import contextlib as _contextlib
163
+
164
+
165
+ @_contextlib.contextmanager
166
+ def bundled_raster_data_env():
167
+ """Point rasterio's bundled GDAL/PROJ at their own data files.
168
+
169
+ Hosts like QGIS export ``GDAL_DATA``/``PROJ_LIB`` for *their* libraries;
170
+ a pip rasterio wheel (which bundles its own GDAL and PROJ) would then
171
+ read foreign -- usually newer -- data files and fail with obscure errors
172
+ (``proj.db DATABASE.LAYOUT`` mismatches and the like). This context uses
173
+ rasterio's own configuration channels only (``rasterio.Env`` and its
174
+ private PROJ search path), so the process environment -- and therefore
175
+ the host's pyproj/PROJ -- is never touched. A no-op when rasterio is not
176
+ a wheel (Linux system packages have no bundled data dirs).
177
+ """
178
+ import os
179
+
180
+ try:
181
+ import rasterio
182
+ base = os.path.dirname(rasterio.__file__)
183
+ except Exception:
184
+ yield
185
+ return
186
+
187
+ gdal_data = os.path.join(base, "gdal_data")
188
+ proj_data = os.path.join(base, "proj_data")
189
+ if os.path.isdir(proj_data):
190
+ try: # binds rasterio's own PROJ (only) to its own database
191
+ from rasterio._env import set_proj_data_search_path
192
+ set_proj_data_search_path(proj_data)
193
+ except Exception:
194
+ pass
195
+ if os.path.isdir(gdal_data):
196
+ with rasterio.Env(GDAL_DATA=gdal_data):
197
+ yield
198
+ return
199
+ yield
200
+
201
+
202
+ def require_georeferenced(dataset, what="bathymetry raster"):
203
+ """Raise a clear error when a raster has no CRS or no geotransform.
204
+
205
+ Without these the pipeline can't place the coastline / mesh in the world:
206
+ a missing CRS otherwise crashes deep in a PROJ call, and a missing
207
+ geotransform (rasterio substitutes the identity affine) would silently
208
+ produce geometry in pixel coordinates. Fail early with an actionable
209
+ message instead.
210
+ """
211
+ if dataset.crs is None:
212
+ raise ValueError(
213
+ f"The {what} has no coordinate reference system (CRS). Assign one "
214
+ "(in QGIS: right-click the layer -> Properties -> Source -> Set "
215
+ "CRS, or `gdal_edit.py -a_srs EPSG:xxxx file.tif`) before meshing.")
216
+ if dataset.transform.is_identity:
217
+ raise ValueError(
218
+ f"The {what} is not georeferenced: it has no geotransform, so map "
219
+ "and pixel coordinates are the same. Georeference it (assign a "
220
+ "geotransform or a world file) before meshing.")
221
+
222
+
223
+ def _raster_crs(src):
224
+ """Build a pyproj CRS from an open rasterio dataset, robustly.
225
+
226
+ Prefer the EPSG code (a single, well-trodden PROJ database lookup) over
227
+ parsing the full WKT: the WKT parser resolves the datum against the PROJ
228
+ database and has been observed to hard-crash (access violation) in some
229
+ QGIS/PROJ builds. Falls back to WKT only when there is no EPSG code.
230
+
231
+ Parameters
232
+ ----------
233
+ src : rasterio.io.DatasetReader
234
+ Open rasterio dataset.
235
+
236
+ Returns
237
+ -------
238
+ crs : pyproj.CRS
239
+ CRS of ``src``.
240
+ """
241
+ import pyproj
242
+
243
+ try:
244
+ epsg = src.crs.to_epsg()
245
+ except Exception:
246
+ epsg = None
247
+ if epsg:
248
+ return pyproj.CRS.from_epsg(epsg)
249
+ return pyproj.CRS.from_wkt(src.crs.to_wkt())
250
+
@@ -0,0 +1,283 @@
1
+ import numpy as np
2
+ from shapely.geometry import LinearRing, Point, Polygon
3
+
4
+
5
+ def read_poly_from_dat(dat_path, delimiter=None):
6
+ """Read polygon contours from a ``.dat`` file into PSLG node/edge arrays.
7
+
8
+ Contours are separated by ``NaN NaN`` rows. Each contour is closed and
9
+ concatenated into global node and edge arrays.
10
+
11
+ Parameters
12
+ ----------
13
+ dat_path : str
14
+ Path to the ``.dat`` file.
15
+ delimiter : str, optional
16
+ Delimiter for :func:`numpy.loadtxt`. Default is auto-detected.
17
+
18
+ Returns
19
+ -------
20
+ node : ndarray of shape (N, 2)
21
+ Vertex coordinates ``(x, y)``.
22
+ edge : ndarray of shape (M, 2), dtype int
23
+ Edge connectivity (0-based vertex indices).
24
+ """
25
+
26
+ # Load file
27
+ p0 = np.loadtxt(dat_path, delimiter=delimiter)
28
+ if p0.shape[1] < 2:
29
+ raise ValueError("The .dat file must contain at least two columns: x y")
30
+
31
+ # Find NaN separators
32
+ isnan = np.isnan(p0[:, 0])
33
+ s = np.where(isnan)[0]
34
+ s = np.concatenate(([0], s, [len(p0)]))
35
+
36
+ node = []
37
+ edge = []
38
+ cont = 0
39
+
40
+ # Loop over polygons
41
+ for i in range(len(s) - 1):
42
+ p = p0[s[i] : s[i + 1], :]
43
+ p = p[~np.isnan(p[:, 0])] # remove NaN rows
44
+ if len(p) == 0:
45
+ continue
46
+
47
+ n = len(p)
48
+ # Close the polygon by connecting last point to first
49
+ c = np.column_stack([np.arange(0, n), np.arange(1, n + 1)])
50
+ c[-1, 1] = 0 # last edge closes to first
51
+
52
+ # Apply offset to edge indices
53
+ c = c + cont
54
+
55
+ # Append
56
+ node.append(p)
57
+ edge.append(c)
58
+
59
+ cont += n # offset for next polygon
60
+
61
+ # Concatenate all nodes and edges
62
+ node = np.vstack(node)
63
+ edge = np.vstack(edge).astype(int)
64
+
65
+ return node, edge
66
+
67
+
68
+ def _split_edges_at_discontinuity(edges):
69
+ """Split an edge list wherever consecutive rows share no vertex."""
70
+ if edges is None or edges.size == 0:
71
+ return []
72
+ edges = np.asarray(edges, dtype=int)
73
+ n = len(edges)
74
+ if n == 1:
75
+ return [edges]
76
+
77
+ chunks = []
78
+ start = 0
79
+
80
+ for i in range(1, n):
81
+ a, b = int(edges[i, 0]), int(edges[i, 1])
82
+ a_prev, b_prev = int(edges[i - 1, 0]), int(edges[i - 1, 1])
83
+ # Check if current edge shares a vertex with previous
84
+ shared = (a == a_prev or a == b_prev or b == a_prev or b == b_prev)
85
+
86
+ if not shared:
87
+ # Discontinuity: cut here
88
+ chunks.append(edges[start:i])
89
+ start = i
90
+
91
+ # Add the last chunk
92
+ chunks.append(edges[start:])
93
+ return chunks
94
+
95
+
96
+ def _ordered_edges_to_chains(edges):
97
+ """Order edges into chains, then split at discontinuities."""
98
+ if edges is None or edges.size == 0:
99
+ return []
100
+ edges = np.asarray(edges, dtype=int)
101
+ n = len(edges)
102
+
103
+ # Build adjacency
104
+ adj = {}
105
+ for i in range(n):
106
+ u, v = int(edges[i, 0]), int(edges[i, 1])
107
+ if u not in adj:
108
+ adj[u] = []
109
+ adj[u].append((i, v))
110
+ if v not in adj:
111
+ adj[v] = []
112
+ adj[v].append((i, u))
113
+
114
+ used = set()
115
+ ordered_chains = []
116
+
117
+ def extend_forward(last_v, lst, used_set):
118
+ while True:
119
+ cands = [(ei, other) for ei, other in adj[last_v] if ei not in used_set]
120
+ if not cands:
121
+ break
122
+ ei, other = cands[0]
123
+ lst.append((int(edges[ei, 0]), int(edges[ei, 1])))
124
+ used_set.add(ei)
125
+ last_v = other
126
+
127
+ def extend_backward(first_v, lst, used_set):
128
+ while True:
129
+ cands = [(ei, other) for ei, other in adj[first_v] if ei not in used_set]
130
+ if not cands:
131
+ break
132
+ ei, other = cands[0]
133
+ lst.insert(0, (int(edges[ei, 0]), int(edges[ei, 1])))
134
+ used_set.add(ei)
135
+ first_v = other
136
+
137
+ # Build chains per connected component
138
+ for start_i in range(n):
139
+ if start_i in used:
140
+ continue
141
+ a0, b0 = int(edges[start_i, 0]), int(edges[start_i, 1])
142
+ chain = [(a0, b0)]
143
+ used.add(start_i)
144
+ extend_forward(b0, chain, used)
145
+ extend_backward(a0, chain, used)
146
+ ordered_chains.append(np.array(chain, dtype=int))
147
+
148
+ if not ordered_chains:
149
+ return []
150
+
151
+ # Concatenate all chains into one ordered array
152
+ flat = np.vstack(ordered_chains)
153
+
154
+ # Split at discontinuities (consecutive rows without shared vertex)
155
+ return _split_edges_at_discontinuity(flat)
156
+
157
+
158
+ def _chain_edges_to_nodelist(edge_arr):
159
+ """Ordered (m, 2) edges -> 1D array of node indices."""
160
+ if edge_arr is None or len(edge_arr) == 0:
161
+ return np.array([], dtype=int)
162
+ edge_arr = np.asarray(edge_arr, dtype=int)
163
+ out = [int(edge_arr[0, 0]), int(edge_arr[0, 1])]
164
+ for i in range(1, len(edge_arr)):
165
+ a, b = int(edge_arr[i, 0]), int(edge_arr[i, 1])
166
+ if a == out[-1]:
167
+ out.append(b)
168
+ elif b == out[-1]:
169
+ out.append(a)
170
+ elif a == out[0]:
171
+ out.insert(0, b)
172
+ elif b == out[0]:
173
+ out.insert(0, a)
174
+ else:
175
+ out.append(a)
176
+ out.append(b)
177
+ return np.array(out, dtype=int)
178
+
179
+
180
+ def identify_boundary(vert, tria, z, zlim=0.0, Manual_open_boundary=None):
181
+ """Classify open and land boundaries from a triangulated mesh.
182
+
183
+ Boundary edges are the mesh edges adjacent to only one triangle. They are
184
+ tagged open when mean nodal elevation exceeds ``zlim`` or the edge midpoint
185
+ lies inside ``Manual_open_boundary``. Contours are ordered and split at
186
+ discontinuities.
187
+
188
+ Parameters
189
+ ----------
190
+ vert : ndarray of shape (N, 2)
191
+ Node coordinates ``(x, y)``.
192
+ tria : ndarray of shape (M, 3)
193
+ Triangle connectivity (0-based node indices).
194
+ z : ndarray of shape (N,)
195
+ Nodal elevation values.
196
+ zlim : float, optional
197
+ Elevation threshold; edges with mean elevation above this are open.
198
+ Default is 0.0.
199
+ Manual_open_boundary : shapely.geometry.Polygon, optional
200
+ Edges whose midpoint lies inside this polygon are classified as open.
201
+
202
+ Returns
203
+ -------
204
+ dict
205
+ Dictionary with keys:
206
+
207
+ - ``edge_tag`` : ndarray of shape (K, 3), ``(node1, node2, tag)``
208
+ (tag 1 = open, 2 = land)
209
+ - ``edge_open`` : ndarray of shape (L, 2), flat open-boundary edges
210
+ - ``edge_land`` : ndarray of shape (P, 2), flat land-boundary edges
211
+ - ``open_contours`` : list of 1D node-index arrays (one per contour)
212
+ - ``land_contours`` : list of 1D node-index arrays (one per contour)
213
+ """
214
+ edges = np.vstack([tria[:, [0, 1]], tria[:, [1, 2]], tria[:, [2, 0]]])
215
+ edges = np.sort(edges, axis=1)
216
+ edges_sorted, counts = np.unique(edges, axis=0, return_counts=True)
217
+ edge_free = edges_sorted[counts == 1]
218
+ if edge_free.size == 0:
219
+ return {
220
+ "edge_tag": np.empty((0, 3), dtype=int),
221
+ "edge_open": np.empty((0, 2), dtype=int),
222
+ "edge_land": np.empty((0, 2), dtype=int),
223
+ "open_contours": [],
224
+ "land_contours": [],
225
+ }
226
+
227
+ edge_open_list = []
228
+ edge_land_list = []
229
+ for (a, b) in edge_free:
230
+ zmean = 0.5 * (z[a] + z[b])
231
+ mid = (vert[a] + vert[b]) / 2.0
232
+ in_manual = (
233
+ Manual_open_boundary.contains(Point(mid))
234
+ if Manual_open_boundary is not None
235
+ else False
236
+ )
237
+ if zmean > zlim or in_manual:
238
+ edge_open_list.append([a, b])
239
+ else:
240
+ edge_land_list.append([a, b])
241
+
242
+ edge_open = (
243
+ np.array(edge_open_list, dtype=int)
244
+ if edge_open_list
245
+ else np.empty((0, 2), dtype=int)
246
+ )
247
+ edge_land = (
248
+ np.array(edge_land_list, dtype=int)
249
+ if edge_land_list
250
+ else np.empty((0, 2), dtype=int)
251
+ )
252
+
253
+ open_chains = _ordered_edges_to_chains(edge_open)
254
+ land_chains = _ordered_edges_to_chains(edge_land)
255
+
256
+ open_contours = [_chain_edges_to_nodelist(c) for c in open_chains]
257
+ land_contours = [_chain_edges_to_nodelist(c) for c in land_chains]
258
+
259
+ edge_open_flat = (
260
+ np.vstack(open_chains) if open_chains else np.empty((0, 2), dtype=int)
261
+ )
262
+ edge_land_flat = (
263
+ np.vstack(land_chains) if land_chains else np.empty((0, 2), dtype=int)
264
+ )
265
+
266
+ tag_open = np.ones((edge_open_flat.shape[0], 1), dtype=int)
267
+ tag_land = np.full((edge_land_flat.shape[0], 1), 2, dtype=int)
268
+ edge_tag_parts = []
269
+ if edge_open_flat.shape[0] > 0:
270
+ edge_tag_parts.append(np.hstack([edge_open_flat, tag_open]))
271
+ if edge_land_flat.shape[0] > 0:
272
+ edge_tag_parts.append(np.hstack([edge_land_flat, tag_land]))
273
+ edge_tag = (
274
+ np.vstack(edge_tag_parts) if edge_tag_parts else np.empty((0, 3), dtype=int)
275
+ )
276
+
277
+ return {
278
+ "edge_tag": edge_tag,
279
+ "edge_open": edge_open_flat,
280
+ "edge_land": edge_land_flat,
281
+ "open_contours": open_contours,
282
+ "land_contours": land_contours,
283
+ }