In [38]:
from __future__ import annotations
import hvplot.pandas
import numpy as np
import holoviews as hv
import pandas as pd
import geopandas as gp
import shapely
import panel as pn
from shapely.geometry import Polygon, Point
import typing as T

# from pyposeidon.utils.cfl import parse_hgrid

VOSPOROS_SOUTH = shapely.Polygon([
    (26.77069525861703, 40.44054922299234),
    (26.563328315257657, 40.4133689586624),
    (26.115328982567448, 39.99024357789592),
    (26.394107058739323, 39.98182584129004),
    (26.785494998192448, 40.342851238592424),
    (26.77069525861703, 40.44054922299234),
])

def extract_area(x: np.ndarray, y: np.ndarray, triangles: np.ndarray, lon_range: tuple[float, float], lat_range: tuple[float, float]):
    mask = (x >= lon_range[0]) & (x <= lon_range[1]) & \
           (y >= lat_range[0]) & (y <= lat_range[1])
    node_indices = np.where(mask)[0]
    node_map = {old: new for new, old in enumerate(node_indices)}
    node_map_reverse = {new: old for new, old in enumerate(node_indices)}
    extracted_x = x[node_indices]
    extracted_y = y[node_indices]
    triangle_indices = np.array([idx for idx, tri in enumerate(triangles) if all(i in node_map for i in tri)])
    extracted_triangles = triangles[triangle_indices]
    extracted_triangles = np.array([[node_map[i] for i in tri] for tri in extracted_triangles])
    
    return extracted_x, extracted_y, extracted_triangles, node_indices, triangle_indices, node_map_reverse

# 2. Convert triangulation to GeoDataFrame with MultiPolygons
def tri_to_geopandas(points, tris, values):
    """Convert triangulation to GeoDataFrame with values for coloring"""
    polygons = [Polygon(points[tri]) for tri in tris]

    gdf = gp.GeoDataFrame({
        'A': values[tris[:,0]],
        'B': values[tris[:,1]],
        'C': values[tris[:,2]],
        'geometry': polygons
        },
        crs="EPSG:4326"  # using WGS84 for demonstration
    )    
    return gdf

def drop(nodes, elems, drop_indices):
    drop_indices = np.unique(drop_indices)
    keep_mask = np.ones(len(nodes), dtype=bool)
    keep_mask[drop_indices] = False
    nodes = nodes[keep_mask]
    old_to_new = -np.ones(len(keep_mask), dtype=int)
    old_to_new[np.where(keep_mask)[0]] = np.arange(len(nodes))
    mask_valid = np.all(np.isin(elems, np.where(keep_mask)[0]), axis=1)
    elems = elems[mask_valid]
    elems = old_to_new[elems]
    return nodes, elems

def render_gdf(gdf, case = "global"):
    if case == 'global':
        color = 'red'
    elif case == 'black sea': 
        color = "blue"
    else:
        raise ValueError("case has to be 'global' or 'black sea'!")

    if isinstance(gdf, gp.GeoDataFrame):
        return gdf.hvplot(geo=True, tiles=True, c=color, label=case)
    elif isinstance(gdf, pd.DataFrame):
        return gdf.hvplot.points(geo=True, tiles=True, c=color, line_color='k', tools=["box_select"], hover_cols = ['index'], label=case)
    

def subset_gdf(gdf, poly: shapely.Polygon):
    return gdf[gdf.geometry.within(poly)]

def polygon_to_points(gdf):
    coords_index = []
    for g in gdf.iterrows(): 
        coords_index.append([*g[1].geometry.exterior.xy, np.array([g[1].A,g[1].B,g[1].C,g[1].A])])
    pp = np.array(coords_index).transpose(0, 2, 1).reshape(-1, 3)
    return pd.DataFrame(pp, columns=['lon', 'lat', 'index'])

global mesh is taken from: https://github.com/seareport/seareport_models

Mesh v3.2 1km resolution at the coastlines

In [ ]:
file = "seareport_models/v3.2/GSHHS_f_0.01_final.gr3"
mesh_dic = parse_hgrid(file)
In [40]:
x, y, depth = mesh_dic['nodes'].T
tris = mesh_dic["elements"]

subset global mesh to vosphoros south strait

In [41]:
x_, y_, tri_, ind_, tri_sub, node_mapping = extract_area(x,y,tris, (25,45), (40,48))
gdf_ = tri_to_geopandas(np.vstack((x_, y_)).T,tri_, ind_)
global_vosphoros_gdf = subset_gdf(gdf_, VOSPOROS_SOUTH)
global_vosphoros_points = polygon_to_points(global_vosphoros_gdf)
In [42]:
points_ = render_gdf(global_vosphoros_points)
global_vosphoros_ = render_gdf(global_vosphoros_gdf)
(global_vosphoros_ * points_).opts(width = 1000, height=800)
Out[42]:

black sea mesh is taken from: https://github.com/tomsail/regional_seamsh/tree/black_sea_bathy

In [43]:
from pyposeidon.mgmsh import read_msh

black_sea_mesh = read_msh("out_blacksea_0.2.msh")
bs_pts = np.vstack((black_sea_mesh.SCHISM_hgrid_node_x, black_sea_mesh.SCHISM_hgrid_node_y)).T
bs_tri = black_sea_mesh.SCHISM_hgrid_face_nodes.data
black_sea_mesh
Info    : Reading 'out_blacksea_0.2.msh'...
Info    : 7718 entities
Info    : 48945 nodes
Info    : 97915 elements
Info    : Storing section $Projection as model attribute
2025-05-13 12:26:33,951 INFO     pyposeidon.mgmsh read_msh:76 Analyzing mesh
Info    : Done reading 'out_blacksea_0.2.msh'
2025-05-13 12:26:33,957 INFO     pyposeidon.mgmsh read_msh:99 No of Open boundaries: 6
2025-05-13 12:26:33,962 INFO     pyposeidon.mgmsh read_msh:115 No of Land boundaries: 0
2025-05-13 12:26:33,963 INFO     pyposeidon.mgmsh read_msh:131 No. of Islands: 0
2025-05-13 12:26:33,966 INFO     pyposeidon.mgmsh read_msh:192 Finalizing Dataset
/home/tomsail/miniconda3/envs/pos_test/lib/python3.11/site-packages/numpy/core/fromnumeric.py:59: FutureWarning: 'DataFrame.swapaxes' is deprecated and will be removed in a future version. Please use 'DataFrame.transpose' instead.
  return bound(*args, **kwds)
Out[43]:
<xarray.Dataset> Size: 4MB
Dimensions:                  (nSCHISM_hgrid_node: 48945,
                              nSCHISM_hgrid_face: 91753,
                              nMaxSCHISM_hgrid_face_nodes: 3, bnodes: 6162)
Coordinates:
  * bnodes                   (bnodes) int64 49kB 0 1 2 3 ... 6284 6287 6288 6289
Dimensions without coordinates: nSCHISM_hgrid_node, nSCHISM_hgrid_face,
                                nMaxSCHISM_hgrid_face_nodes
Data variables:
    SCHISM_hgrid_node_x      (nSCHISM_hgrid_node) float64 392kB 28.81 ... 29.07
    SCHISM_hgrid_node_y      (nSCHISM_hgrid_node) float64 392kB 41.3 ... 40.94
    SCHISM_hgrid_face_nodes  (nSCHISM_hgrid_face, nMaxSCHISM_hgrid_face_nodes) uint64 2MB ...
    depth                    (nSCHISM_hgrid_node) float64 392kB 0.0 0.0 ... 0.0
    node                     (bnodes) uint64 49kB 145 146 147 ... 521 522 523
    type                     (bnodes) object 49kB 'open' 'open' ... 'open'
    id                       (bnodes) int64 49kB 1 1 1 1 1 2 2 ... 5 5 5 5 6 6 6
xarray.Dataset
    • nSCHISM_hgrid_node: 48945
    • nSCHISM_hgrid_face: 91753
    • nMaxSCHISM_hgrid_face_nodes: 3
    • bnodes: 6162
    • bnodes
      (bnodes)
      int64
      0 1 2 3 4 ... 6284 6287 6288 6289
      array([   0,    1,    2, ..., 6287, 6288, 6289])
    • SCHISM_hgrid_node_x
      (nSCHISM_hgrid_node)
      float64
      28.81 29.09 29.08 ... 36.43 29.07
      array([28.81200628, 29.09112769, 29.08072026, ..., 32.1335257 ,
             36.42849426, 29.07299703])
    • SCHISM_hgrid_node_y
      (nSCHISM_hgrid_node)
      float64
      41.3 41.2 41.19 ... 41.38 40.94
      array([41.30019889, 41.19977556, 41.18745215, ..., 46.11349607,
             41.37808864, 40.94011745])
    • SCHISM_hgrid_face_nodes
      (nSCHISM_hgrid_face, nMaxSCHISM_hgrid_face_nodes)
      uint64
      28342 29896 20452 ... 45147 35470
      array([[28342, 29896, 20452],
             [16515, 48010, 27790],
             [ 3099, 27096,  6920],
             ...,
             [32317, 38805, 35448],
             [43151, 45428, 34240],
             [42714, 45147, 35470]], dtype=uint64)
    • depth
      (nSCHISM_hgrid_node)
      float64
      0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0
      array([0., 0., 0., ..., 0., 0., 0.])
    • node
      (bnodes)
      uint64
      145 146 147 148 ... 521 522 523
      array([145, 146, 147, ..., 521, 522, 523], dtype=uint64)
    • type
      (bnodes)
      object
      'open' 'open' ... 'open' 'open'
      array(['open', 'open', 'open', ..., 'open', 'open', 'open'], dtype=object)
    • id
      (bnodes)
      int64
      1 1 1 1 1 2 2 2 ... 5 5 5 5 5 6 6 6
      array([1, 1, 1, ..., 6, 6, 6])
    • bnodes
      PandasIndex
      PandasIndex(Index([   0,    1,    2,    3,    4,    5,    6,    7,    8,    9,
             ...
             6278, 6279, 6280, 6281, 6282, 6283, 6284, 6287, 6288, 6289],
            dtype='int64', name='bnodes', length=6162))
In [44]:
gdf_ = tri_to_geopandas(bs_pts, bs_tri, np.arange(len(bs_pts)))
black_sea_vosporos_gdf = subset_gdf(gdf_, VOSPOROS_SOUTH)
black_sea_vosporos_points = polygon_to_points(black_sea_vosporos_gdf)
black_sea_vosporos_ = render_gdf(black_sea_vosporos_gdf, "black sea")
In [45]:
selection = hv.streams.Selection1D(source=points_)
# Create a function to print selected points
def print_all_points(df: pd.DataFrame, indices: pd.Index[int], text_box: pn.widgets.TextAreaInput) -> T.Any:
    print("Selected indices:", indices)
    if indices:
        ind = np.unique(indices)
        list = df["index"].iloc[ind].astype(int).unique()
        value = ",\n".join(map(str, list))
    else:
        value = "No selection!"
    text_box.value = value

points_all = pn.widgets.TextAreaInput(value="", height=200, placeholder="Selected indices will appear here")  # type: ignore[no-untyped-call]
selection.add_subscriber(lambda index: print_all_points(df=global_vosphoros_points, indices=index, text_box=points_all))

plot_ = (black_sea_vosporos_ * global_vosphoros_ * points_).opts(width = 700, height=800)
layout = pn.Column(
    points_.opts(width=900, height=500),
    pn.Row(
        pn.Column("## Selected Indices:", points_all),
    ),
)    
out = pn.panel(layout).servable()
out
BokehModel(combine_events=True, render_bundle={'docs_json': {'31c4f898-81d6-43b9-9888-6681c9da68bd': {'version…
Out[45]:

we'll fix the following points and regenerate the meshing for the black sea

In [46]:
pts_ind = [6817898,
6697291,
6549074,
1714532,
1473386]
coords = mesh_dic['nodes'][pts_ind][:,:2]
gdf = gp.GeoDataFrame(
    pd.DataFrame(coords, columns=["lon", "lat"]),
    geometry=[Point(xy) for xy in coords],
    crs="EPSG:4326"
)
gdf['physical'] = "force"
gdf
gdf.to_file("points_4326.shp", driver='ESRI Shapefile')
Out[46]:
lon lat geometry physical
0 26.249899 40.010687 POINT (26.2499 40.01069) force
1 26.245730 40.021553 POINT (26.24573 40.02155) force
2 26.238869 40.034125 POINT (26.23887 40.03413) force
3 26.230883 40.045579 POINT (26.23088 40.04558) force
4 26.262392 40.001939 POINT (26.26239 40.00194) force

all these points need to be suppressed

In [47]:
global_nodes = np.vstack((x,y)).T
In [48]:
discard = [1807639,456464,4792637,2451328,39451,5503929,4015013,6409047,2134299,4386548,4967757,
4856350,7851069,1324320,3643482,7452558,512577,6384066,677206,2528912,7294189,
167240,5582097,7382856,4192983,7134380,1647070,7700285,7907239,2334291,3421735,363349,
7046390,5460790,1145947,1314347,2426245,5698280,3282119,4838108,332392,6336320,1580434,
4864137,2614067,263530,1928581,1756355,4590953,47220,4869691,3173761,7342931,551330,189622,
2312122,7482244,5405589,4401906]

nno, ell = drop(global_nodes, tris, discard)
In [49]:
llo, lla, tri_indices, node_indices, tri_sub, node_mapping = extract_area(nno[:,0],nno[:,1],ell, (25,45), (40,48))
global_black_sea_gdf = tri_to_geopandas(np.vstack((llo,lla)).T, tri_indices, node_indices)
global_vosphoros_gdf = global_black_sea_gdf[global_black_sea_gdf.geometry.within(VOSPOROS_SOUTH)]
global_vosphoros_ = render_gdf(global_vosphoros_gdf)
In [50]:
# now create points from the black sea mesh
points2_ = render_gdf(black_sea_vosporos_points, "black sea")
In [51]:
selection = hv.streams.Selection1D(source=points2_)
points_all = pn.widgets.TextAreaInput(value="", height=200, placeholder="Selected indices will appear here")  # type: ignore[no-untyped-call]
def print_all_points(df: pd.DataFrame, indices: pd.Index[int], text_box: pn.widgets.TextAreaInput) -> T.Any:
    print("Selected indices:", indices)
    if indices:
        ind = np.unique(indices)
        list = df["index"].iloc[ind].astype(int).unique()
        value = ",\n".join(map(str, list))
    else:
        value = "No selection!"
    text_box.value = value

selection.add_subscriber(lambda index: print_all_points(df=black_sea_vosporos_points, indices=index, text_box=points_all))
plot_ = (black_sea_vosporos_ * global_vosphoros_ * points_ * points2_).opts(width = 700, height=800)
layout = pn.Column(
    plot_.opts(width=900, height=500),
    pn.Row(
        pn.Column("## Selected Indices:", points_all),
    ),
)    
out = pn.panel(layout).servable()
out
BokehModel(combine_events=True, render_bundle={'docs_json': {'0914cea4-577c-4250-bbb1-18b5d9c2add2': {'version…
Out[51]:

here are the following node to drop on the back sea mesh

In [52]:
discard2 = [13598,39688,31622,28962,28963,528,529,522,28379,17111,29402,37351,23225,17568,45282,46776,
           523,46587,31245,46194,48307,524,47748,44,48416,519,518,42,520,525,521,527,526,35710,43,149, 145]

new_bs_pts, new_bs_tri = drop(bs_pts,bs_tri, discard2)
In [53]:
new_black_sea_gdf = tri_to_geopandas(new_bs_pts, new_bs_tri, np.arange(len(new_bs_pts)))
new_bs_vosphoros_gdf = new_black_sea_gdf[new_black_sea_gdf.geometry.within(VOSPOROS_SOUTH)]
new_black_sea_vosporos_points = polygon_to_points(new_bs_vosphoros_gdf)
new_points2_ = render_gdf(new_black_sea_vosporos_points, "black sea")
In [54]:
plot_ = (render_gdf(global_vosphoros_gdf) * 
         render_gdf(new_bs_vosphoros_gdf, case = "black sea") * points_ * new_points2_).opts(width = 700, height=800)
In [55]:
plot_
Out[55]:
In [56]:
stitching_map = dict([
    [512, 7732209],
    [144, 1714532],
    [143, 6549074],
    [142, 6697291],
    [37476, 3418943],
    [22941, 1473386]
])
In [57]:
import numpy as np

def stitch_meshes(global_nodes, global_elements, bs_pts, bs_tri, stitching_map):
    mapped_bs_indices = set(stitching_map.keys())
    unmapped_bs_indices = [i for i in range(len(bs_pts)) if i not in mapped_bs_indices]

    index_mapping = {}
    new_nodes = global_nodes.tolist()

    for i in unmapped_bs_indices:
        new_index = len(new_nodes)
        index_mapping[i] = new_index
        # Insert with dummy z-value (can be interpolated or set to 0)
        new_nodes.append([bs_pts[i][0], bs_pts[i][1], 0.0])

    for bs_idx, global_idx in stitching_map.items():
        index_mapping[bs_idx] = global_idx

    new_elements = []
    for tri in bs_tri:
        new_tri = [index_mapping[idx] for idx in tri]
        new_elements.append(new_tri)

    all_elements = np.vstack([global_elements, np.array(new_elements, dtype=int)])

    return np.array(new_nodes), all_elements
In [58]:
stiched_nodes, stiched_tri = stitch_meshes(mesh_dic['nodes'], mesh_dic['elements'], new_bs_pts, new_bs_tri, stitching_map)
stiched_nodes_fix, stiched_tri_fix = drop(stiched_nodes, stiched_tri, discard)
In [59]:
%matplotlib widget
In [60]:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize = (10,6))
ax.triplot(stiched_nodes_fix[:,0], stiched_nodes_fix[:,1], stiched_tri_fix, lw=0.2)
ax.set_xlim([26,30])
ax.set_ylim([39.75,41.5])
Out[60]:
[<matplotlib.lines.Line2D at 0x70581ba8a990>,
 <matplotlib.lines.Line2D at 0x70581ba8a2d0>]
Out[60]:
(26.0, 30.0)
Out[60]:
(39.75, 41.5)
Figure
No description has been provided for this image
In [61]:
import meshio

points = np.column_stack((stiched_nodes_fix[:,0], stiched_nodes_fix[:,1], np.zeros(len(stiched_nodes_fix))))  # Add z=0
cells = [("triangle", stiched_tri_fix)]
mesh = meshio.Mesh(points=points, cells=cells)
mesh.write("output_mesh.vtk")
In [62]:
from xarray_selafin.xarray_backend import SelafinAccessor
import xarray as xr

ds = xr.Dataset({
    "B": (("time", "node"), np.zeros((1,len(stiched_nodes_fix)))),
    },
    coords = {
        "x" : ("node", stiched_nodes_fix[:,0]),
        "y" : ("node", stiched_nodes_fix[:,1]),
        "time": [pd.Timestamp.now()]
    } )
ds.attrs['ikle2'] = stiched_tri_fix + 1
ds
Out[62]:
<xarray.Dataset> Size: 193MB
Dimensions:  (time: 1, node: 8039622)
Coordinates:
    x        (node) float64 64MB -1.011 -27.3 -92.7 104.2 ... 32.13 36.43 29.07
    y        (node) float64 64MB 45.49 38.64 18.62 1.046 ... 46.11 41.38 40.94
  * time     (time) datetime64[ns] 8B 2025-05-13T12:27:34.032880
Dimensions without coordinates: node
Data variables:
    B        (time, node) float64 64MB 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0
Attributes:
    ikle2:    [[3987707       1 7764558]\n [      1 4523085 7764558]\n [30905...
xarray.Dataset
    • time: 1
    • node: 8039622
    • x
      (node)
      float64
      -1.011 -27.3 -92.7 ... 36.43 29.07
      array([ -1.01094779, -27.29748857, -92.69774605, ...,  32.1335257 ,
              36.42849426,  29.07299703])
    • y
      (node)
      float64
      45.49 38.64 18.62 ... 41.38 40.94
      array([45.49251779, 38.63799004, 18.62273716, ..., 46.11349607,
             41.37808864, 40.94011745])
    • time
      (time)
      datetime64[ns]
      2025-05-13T12:27:34.032880
      array(['2025-05-13T12:27:34.032880000'], dtype='datetime64[ns]')
    • B
      (time, node)
      float64
      0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0
      array([[0., 0., 0., ..., 0., 0., 0.]])
    • time
      PandasIndex
      PandasIndex(DatetimeIndex(['2025-05-13 12:27:34.032880'], dtype='datetime64[ns]', name='time', freq=None))
  • ikle2 :
    [[3987707 1 7764558] [ 1 4523085 7764558] [3090539 1 5845218] ... [8023006 8029491 8026137] [8033836 8036112 8024929] [8033399 8035832 8026159]]
In [ ]:
ds.selafin.write('out.slf')