ncftools 0.9.2__tar.gz → 0.9.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncftools
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: A collection of tools for working with NetCDF files
5
5
  Author-email: aaronchh <aaronhsu219@gmail.com>
6
6
  License-Expression: MIT
@@ -2,7 +2,7 @@
2
2
  NCFTOOLS - A collection of tools for working with NetCDF files.
3
3
  """
4
4
 
5
- __version__ = '0.9.2'
5
+ __version__ = '0.9.3'
6
6
 
7
7
  __all__ = [
8
8
  'meshinfo',
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ meshinfo - Display mesh information from FlowFM NetCDF files
4
+ Uses netCDF4 Python bindings
5
+ """
6
+
7
+ import argparse
8
+ import os
9
+ import sys
10
+
11
+ import netCDF4 as nc
12
+ import numpy as np
13
+
14
+
15
+ MAX_CELL_NODES = 6 # D-Flow FM only treats closed polygons up to 6 nodes as cells
16
+
17
+
18
+ def find_net_cells(x, y, links):
19
+ """
20
+ Derive the 2D cells of an old-format net file from its node-link graph.
21
+
22
+ Traces every face of the planar graph by walking half-edges with the face
23
+ kept on the left. Counter-clockwise faces of at most MAX_CELL_NODES nodes
24
+ are cells; the outer boundary and unmeshed holes are not.
25
+
26
+ Args:
27
+ x, y (ndarray): Node coordinates
28
+ links (ndarray): (nLinks, 2) zero-based node indices
29
+
30
+ Returns:
31
+ ndarray: Number of nodes of each cell
32
+ """
33
+ n_links = len(links)
34
+ if n_links == 0:
35
+ return np.array([], dtype=int)
36
+
37
+ src = np.concatenate([links[:, 0], links[:, 1]])
38
+ dst = np.concatenate([links[:, 1], links[:, 0]])
39
+ twin = np.concatenate([np.arange(n_links, 2 * n_links), np.arange(n_links)])
40
+
41
+ # Sort half-edges around each origin node by direction (counter-clockwise)
42
+ angle = np.arctan2(y[dst] - y[src], x[dst] - x[src])
43
+ order = np.lexsort((angle, src))
44
+ rank = np.empty_like(order)
45
+ rank[order] = np.arange(len(order))
46
+ degree = np.bincount(src, minlength=len(x))
47
+ start = np.concatenate([[0], np.cumsum(degree)[:-1]])
48
+
49
+ # Next half-edge in the face: the one preceding the twin around the end node
50
+ s = start[dst]
51
+ nxt = order[s + (rank[twin] - s - 1) % degree[dst]]
52
+
53
+ # Label each face cycle by its smallest half-edge index (pointer jumping)
54
+ label = np.arange(len(nxt))
55
+ jump = nxt
56
+ for _ in range(int(np.ceil(np.log2(len(nxt)))) + 1):
57
+ label = np.minimum(label, label[jump])
58
+ jump = jump[jump]
59
+
60
+ n_nodes = np.bincount(label, minlength=len(nxt))
61
+ area2 = np.bincount(label, weights=x[src] * y[dst] - x[dst] * y[src],
62
+ minlength=len(nxt))
63
+ is_cell = (n_nodes >= 3) & (n_nodes <= MAX_CELL_NODES) & (area2 > 0)
64
+ return n_nodes[is_cell]
65
+
66
+
67
+ def _read_ugrid(dataset):
68
+ """Read counts and coordinates from a UGRID (Mesh2d_*) file."""
69
+ info = {
70
+ 'nodes': dataset.dimensions['Mesh2d_nNodes'].size,
71
+ 'faces': dataset.dimensions['Mesh2d_nFaces'].size,
72
+ 'edges': dataset.dimensions['Mesh2d_nEdges'].size,
73
+ 'face_sizes': None,
74
+ 'x': None,
75
+ 'y': None,
76
+ }
77
+
78
+ if 'Mesh2d_face_nodes' in dataset.variables:
79
+ face_nodes = dataset.variables['Mesh2d_face_nodes'][:]
80
+ fill_value = dataset.variables['Mesh2d_face_nodes']._FillValue
81
+ info['face_sizes'] = np.sum(face_nodes != fill_value, axis=1)
82
+
83
+ if 'Mesh2d_node_x' in dataset.variables and 'Mesh2d_node_y' in dataset.variables:
84
+ info['x'] = dataset.variables['Mesh2d_node_x'][:]
85
+ info['y'] = dataset.variables['Mesh2d_node_y'][:]
86
+
87
+ return info
88
+
89
+
90
+ def _read_net(dataset):
91
+ """Read counts and coordinates from an old-format (NetNode/NetLink) net file."""
92
+ x = np.asarray(dataset.variables['NetNode_x'][:], dtype=float)
93
+ y = np.asarray(dataset.variables['NetNode_y'][:], dtype=float)
94
+ links = np.asarray(dataset.variables['NetLink'][:], dtype=np.int64)
95
+ start_index = getattr(dataset.variables['NetLink'], 'start_index', 1)
96
+ face_sizes = find_net_cells(x, y, links - start_index)
97
+
98
+ return {
99
+ 'nodes': len(x),
100
+ 'faces': len(face_sizes),
101
+ 'edges': len(links),
102
+ 'face_sizes': face_sizes,
103
+ 'x': x,
104
+ 'y': y,
105
+ }
106
+
107
+
108
+ def show_mesh_info(nc_file):
109
+ """
110
+ Display mesh information from a FlowFM NetCDF file.
111
+
112
+ Supports both UGRID map/net files (Mesh2d_* variables) and old-format
113
+ net files (NetNode_x/NetNode_y/NetLink), whose cells are derived from
114
+ the links.
115
+
116
+ Args:
117
+ nc_file (str): Path to the FlowFM NetCDF mesh file
118
+ """
119
+ with nc.Dataset(nc_file, 'r') as dataset:
120
+ if 'Mesh2d_nNodes' in dataset.dimensions:
121
+ info = _read_ugrid(dataset)
122
+ derived = False
123
+ elif all(v in dataset.variables for v in ('NetNode_x', 'NetNode_y', 'NetLink')):
124
+ info = _read_net(dataset)
125
+ derived = True
126
+ else:
127
+ raise ValueError('no Mesh2d_* or NetNode/NetLink variables found')
128
+
129
+ print(f"FlowFM Mesh Information from: {nc_file}")
130
+ print("=" * 60)
131
+
132
+ nodes, faces, edges = info['nodes'], info['faces'], info['edges']
133
+ suffix = " (derived from NetLink)" if derived else ""
134
+
135
+ print(f"Number of mesh nodes: {nodes:,}")
136
+ print(f"Number of mesh faces: {faces:,}{suffix}")
137
+ print(f"Number of mesh edges: {edges:,}")
138
+
139
+ if info['face_sizes'] is not None:
140
+ valid_nodes = info['face_sizes']
141
+
142
+ triangles = int(np.sum(valid_nodes == 3))
143
+ quads = int(np.sum(valid_nodes == 4))
144
+ others = int(np.sum(valid_nodes > 4))
145
+
146
+ print("\nElement Types:")
147
+ print(f" Triangular elements: {triangles:,}")
148
+ print(f" Quadrilateral elements: {quads:,}")
149
+ if others:
150
+ print(f" 5-6 sided elements: {others:,}")
151
+ print(f" Total elements: {triangles + quads + others:,}")
152
+
153
+ if info['x'] is not None and len(info['x']):
154
+ x_coords, y_coords = info['x'], info['y']
155
+
156
+ print("\nSpatial extent:")
157
+ print(f" X range: {np.min(x_coords):.1f} to {np.max(x_coords):.1f}")
158
+ print(f" Y range: {np.min(y_coords):.1f} to {np.max(y_coords):.1f}")
159
+
160
+
161
+ def main():
162
+ parser = argparse.ArgumentParser(
163
+ prog='meshinfo',
164
+ description='Display mesh information from FlowFM NetCDF files',
165
+ formatter_class=argparse.RawDescriptionHelpFormatter,
166
+ epilog="""
167
+ Examples:
168
+ meshinfo # Use default FlowFM_net.nc
169
+ meshinfo grid.nc # Use any FlowFM mesh file
170
+ meshinfo -i grid.nc # Same, with the -i flag
171
+ meshinfo -h # Show this help message
172
+
173
+ The tool displays:
174
+ - Number of nodes, faces, and edges
175
+ - Element type distribution (triangles vs quadrilaterals)
176
+ - Spatial extent (X and Y coordinate ranges)
177
+ """,
178
+ )
179
+
180
+ parser.add_argument(
181
+ 'file',
182
+ nargs='?',
183
+ metavar='FILE',
184
+ help='Path to the FlowFM NetCDF mesh file (default: FlowFM_net.nc)',
185
+ )
186
+ parser.add_argument(
187
+ '-i', '--input',
188
+ metavar='FILE',
189
+ help='Same as FILE (kept for backward compatibility)',
190
+ )
191
+
192
+ args = parser.parse_args()
193
+
194
+ if args.file and args.input:
195
+ parser.error('give the mesh file either as FILE or with -i, not both')
196
+ args.input = args.file or args.input or 'FlowFM_net.nc'
197
+
198
+ if not os.path.isfile(args.input):
199
+ print(f"Error: File '{args.input}' not found.")
200
+ print(f"Current directory: {os.getcwd()}")
201
+ print("Please check the file path and try again.")
202
+ sys.exit(1)
203
+
204
+ try:
205
+ show_mesh_info(args.input)
206
+ except Exception as e:
207
+ print(f"Error reading NetCDF file: {e}")
208
+ sys.exit(1)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncftools
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: A collection of tools for working with NetCDF files
5
5
  Author-email: aaronchh <aaronhsu219@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ncftools"
7
- version = "0.9.2"
7
+ version = "0.9.3"
8
8
  authors = [
9
9
  { name = "aaronchh", email = "aaronhsu219@gmail.com" },
10
10
  ]
@@ -1,110 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- meshinfo - Display mesh information from FlowFM NetCDF files
4
- Uses netCDF4 Python bindings
5
- """
6
-
7
- import argparse
8
- import os
9
- import sys
10
-
11
- import netCDF4 as nc
12
- import numpy as np
13
-
14
-
15
- def show_mesh_info(nc_file):
16
- """
17
- Display mesh information from a FlowFM NetCDF file.
18
-
19
- Args:
20
- nc_file (str): Path to the FlowFM NetCDF mesh file
21
- """
22
- dataset = nc.Dataset(nc_file, 'r')
23
-
24
- print(f"FlowFM Mesh Information from: {nc_file}")
25
- print("=" * 60)
26
-
27
- nodes = dataset.dimensions['Mesh2d_nNodes'].size
28
- faces = dataset.dimensions['Mesh2d_nFaces'].size
29
- edges = dataset.dimensions['Mesh2d_nEdges'].size
30
-
31
- print(f"Number of mesh nodes: {nodes:,}")
32
- print(f"Number of mesh faces: {faces:,}")
33
- print(f"Number of mesh edges: {edges:,}")
34
-
35
- if 'Mesh2d_face_nodes' in dataset.variables:
36
- face_nodes = dataset.variables['Mesh2d_face_nodes'][:]
37
- fill_value = dataset.variables['Mesh2d_face_nodes']._FillValue
38
- valid_nodes = np.sum(face_nodes != fill_value, axis=1)
39
-
40
- triangles = int(np.sum(valid_nodes == 3))
41
- quads = int(np.sum(valid_nodes == 4))
42
-
43
- print("\nElement Types:")
44
- print(f" Triangular elements: {triangles:,}")
45
- print(f" Quadrilateral elements: {quads:,}")
46
- print(f" Total elements: {triangles + quads:,}")
47
-
48
- if 'Mesh2d_node_x' in dataset.variables and 'Mesh2d_node_y' in dataset.variables:
49
- x_coords = dataset.variables['Mesh2d_node_x'][:]
50
- y_coords = dataset.variables['Mesh2d_node_y'][:]
51
-
52
- print("\nSpatial extent:")
53
- print(f" X range: {np.min(x_coords):.1f} to {np.max(x_coords):.1f}")
54
- print(f" Y range: {np.min(y_coords):.1f} to {np.max(y_coords):.1f}")
55
-
56
- dataset.close()
57
-
58
-
59
- def main():
60
- parser = argparse.ArgumentParser(
61
- prog='meshinfo',
62
- description='Display mesh information from FlowFM NetCDF files',
63
- formatter_class=argparse.RawDescriptionHelpFormatter,
64
- epilog="""
65
- Examples:
66
- meshinfo # Use default FlowFM_net.nc
67
- meshinfo grid.nc # Use any FlowFM mesh file
68
- meshinfo -i grid.nc # Same, with the -i flag
69
- meshinfo -h # Show this help message
70
-
71
- The tool displays:
72
- - Number of nodes, faces, and edges
73
- - Element type distribution (triangles vs quadrilaterals)
74
- - Spatial extent (X and Y coordinate ranges)
75
- """,
76
- )
77
-
78
- parser.add_argument(
79
- 'file',
80
- nargs='?',
81
- metavar='FILE',
82
- help='Path to the FlowFM NetCDF mesh file (default: FlowFM_net.nc)',
83
- )
84
- parser.add_argument(
85
- '-i', '--input',
86
- metavar='FILE',
87
- help='Same as FILE (kept for backward compatibility)',
88
- )
89
-
90
- args = parser.parse_args()
91
-
92
- if args.file and args.input:
93
- parser.error('give the mesh file either as FILE or with -i, not both')
94
- args.input = args.file or args.input or 'FlowFM_net.nc'
95
-
96
- if not os.path.isfile(args.input):
97
- print(f"Error: File '{args.input}' not found.")
98
- print(f"Current directory: {os.getcwd()}")
99
- print("Please check the file path and try again.")
100
- sys.exit(1)
101
-
102
- try:
103
- show_mesh_info(args.input)
104
- except Exception as e:
105
- print(f"Error reading NetCDF file: {e}")
106
- sys.exit(1)
107
-
108
-
109
- if __name__ == "__main__":
110
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes