ncftools 0.4.0__tar.gz → 0.5.2__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.4.0
3
+ Version: 0.5.2
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
@@ -11,6 +11,8 @@ Requires-Python: >=3.8
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: numpy>=1.20.0
13
13
  Requires-Dist: netCDF4>=1.6.0
14
+ Requires-Dist: geopandas>=0.12.0
15
+ Requires-Dist: shapely>=1.8.0
14
16
 
15
17
  # NCFTOOLS
16
18
 
@@ -31,6 +33,9 @@ pip install -e .
31
33
  ### Command-line
32
34
 
33
35
  ```bash
36
+ # Check installed version
37
+ meshinfo --version
38
+
34
39
  # List all available commands
35
40
  ncftools-info
36
41
 
@@ -17,6 +17,9 @@ pip install -e .
17
17
  ### Command-line
18
18
 
19
19
  ```bash
20
+ # Check installed version
21
+ meshinfo --version
22
+
20
23
  # List all available commands
21
24
  ncftools-info
22
25
 
@@ -2,13 +2,15 @@
2
2
  NCFTOOLS - A collection of tools for working with NetCDF files.
3
3
  """
4
4
 
5
- __version__ = '0.4.0'
5
+ __version__ = '0.5.2'
6
6
 
7
7
  __all__ = [
8
8
  'meshinfo',
9
+ 'nc2shp',
9
10
  'describe',
10
11
  ]
11
12
 
12
13
  from . import meshinfo
14
+ from . import nc2shp
13
15
  from . import describe
14
16
  from . import cli
@@ -18,6 +18,18 @@ TOOL_DESCRIPTIONS = {
18
18
  meshinfo -i FlowFM_net.nc # Display mesh info for a given file
19
19
  meshinfo -i grid.nc # Any FlowFM mesh NetCDF file
20
20
  """,
21
+ 'nc2shp': """
22
+ Convert a NetCDF mesh file to ESRI Shapefiles.
23
+
24
+ Reads a UGRID-compliant NetCDF mesh file and writes two shapefiles:
25
+ {stem}_faces.shp one polygon per mesh face
26
+ {stem}_dissolved.shp single dissolved polygon of the entire mesh
27
+
28
+ Examples:
29
+ nc2shp -i FlowFM_net.nc
30
+ nc2shp -i mesh.nc -o output --crs EPSG:4326
31
+ nc2shp -i mesh.nc -q
32
+ """,
21
33
  }
22
34
 
23
35
 
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ nc2shp - Convert NetCDF mesh faces to ESRI Shapefiles
4
+
5
+ Reads a UGRID-compliant NetCDF mesh file and writes two shapefiles:
6
+ {stem}_faces.shp - one polygon per mesh face
7
+ {stem}_dissolved.shp - single dissolved polygon of the entire mesh
8
+ """
9
+
10
+ import argparse
11
+ import importlib.metadata
12
+ import os
13
+ import sys
14
+
15
+ import netCDF4 as nc
16
+ import numpy as np
17
+ import geopandas as gpd
18
+ from shapely.geometry import Polygon
19
+ from shapely.ops import unary_union
20
+
21
+
22
+ def read_mesh_netcdf(file_path, quiet=False):
23
+ """
24
+ Read mesh face coordinates from a UGRID NetCDF file.
25
+
26
+ Returns:
27
+ tuple: (face_x_coords, face_y_coords, node_x, node_y, face_nodes, var_names)
28
+ """
29
+ _print(f"Reading NetCDF file: {file_path}", quiet)
30
+
31
+ dataset = nc.Dataset(file_path, 'r')
32
+
33
+ var_names = {}
34
+ for var_name in dataset.variables:
35
+ vl = var_name.lower()
36
+ if 'node_x' in vl:
37
+ var_names['node_x'] = var_name
38
+ elif 'node_y' in vl:
39
+ var_names['node_y'] = var_name
40
+ elif 'face_nodes' in vl:
41
+ var_names['face_nodes'] = var_name
42
+ elif 'face_x_bnd' in vl:
43
+ var_names['face_x_bnd'] = var_name
44
+ elif 'face_y_bnd' in vl:
45
+ var_names['face_y_bnd'] = var_name
46
+
47
+ required = ['node_x', 'node_y', 'face_nodes', 'face_x_bnd', 'face_y_bnd']
48
+ missing = [v for v in required if v not in var_names]
49
+ if missing:
50
+ dataset.close()
51
+ raise ValueError(
52
+ f"Missing required variables: {missing}. "
53
+ f"Available: {list(dataset.variables.keys())}"
54
+ )
55
+
56
+ node_x = dataset.variables[var_names['node_x']][:]
57
+ node_y = dataset.variables[var_names['node_y']][:]
58
+ face_nodes = dataset.variables[var_names['face_nodes']][:]
59
+ face_x = dataset.variables[var_names['face_x_bnd']][:]
60
+ face_y = dataset.variables[var_names['face_y_bnd']][:]
61
+ dataset.close()
62
+
63
+ if not quiet:
64
+ print(f" Nodes: {len(node_x):,} Faces: {len(face_x):,} "
65
+ f"Max nodes/face: {face_x.shape[1]}")
66
+
67
+ return face_x, face_y, node_x, node_y, face_nodes, var_names
68
+
69
+
70
+ def create_polygons(face_x, face_y, quiet=False):
71
+ """
72
+ Build Shapely Polygon objects from mesh face coordinate arrays.
73
+
74
+ Returns:
75
+ list[Polygon]
76
+ """
77
+ _print("Creating polygons from mesh faces...", quiet)
78
+
79
+ polygons = []
80
+ n_invalid = 0
81
+ tri = quad = 0
82
+
83
+ for i in range(len(face_x)):
84
+ coords = [
85
+ (float(face_x[i, j]), float(face_y[i, j]))
86
+ for j in range(face_x.shape[1])
87
+ if (not np.ma.is_masked(face_x[i, j])
88
+ and not np.ma.is_masked(face_y[i, j])
89
+ and abs(face_x[i, j]) < 1e30
90
+ and abs(face_y[i, j]) < 1e30)
91
+ ]
92
+
93
+ if len(coords) < 3:
94
+ n_invalid += 1
95
+ continue
96
+
97
+ if coords[0] != coords[-1]:
98
+ coords.append(coords[0])
99
+
100
+ try:
101
+ poly = Polygon(coords)
102
+ if not poly.is_valid or poly.is_empty:
103
+ poly = poly.buffer(0)
104
+ if poly.is_valid and not poly.is_empty:
105
+ polygons.append(poly)
106
+ n = len(coords) - 1 # exclude closing duplicate
107
+ if n == 3:
108
+ tri += 1
109
+ elif n == 4:
110
+ quad += 1
111
+ else:
112
+ n_invalid += 1
113
+ except Exception as e:
114
+ n_invalid += 1
115
+ if not quiet:
116
+ print(f" Warning: face {i} skipped: {e}")
117
+
118
+ if not quiet:
119
+ print(f" Valid polygons: {len(polygons)} "
120
+ f"(triangles: {tri}, quads: {quad}, skipped: {n_invalid})")
121
+
122
+ return polygons
123
+
124
+
125
+ def create_geodataframe(polygons, crs="EPSG:3826"):
126
+ """Build a GeoDataFrame from a list of Shapely polygons."""
127
+ data = {
128
+ 'face_id': range(len(polygons)),
129
+ 'area': [p.area for p in polygons],
130
+ 'type': [
131
+ 'triangle' if len(list(p.exterior.coords)) == 4
132
+ else 'quadrilateral' if len(list(p.exterior.coords)) == 5
133
+ else 'other'
134
+ for p in polygons
135
+ ],
136
+ }
137
+ return gpd.GeoDataFrame(data, geometry=polygons, crs=crs)
138
+
139
+
140
+ def dissolve_geodataframe(gdf, quiet=False):
141
+ """Dissolve all polygons into a single geometry."""
142
+ _print("Dissolving polygons...", quiet)
143
+ geom = unary_union(gdf.geometry.tolist())
144
+ dissolved = gpd.GeoDataFrame(
145
+ {'id': [1], 'total_area': [geom.area], 'count': [len(gdf)]},
146
+ geometry=[geom],
147
+ crs=gdf.crs,
148
+ )
149
+ _print(f" Total area: {geom.area:.2f} sq units", quiet)
150
+ return dissolved
151
+
152
+
153
+ def mesh_to_shp(input_file, output_dir="SHP_NC", crs="EPSG:3826", quiet=False):
154
+ """
155
+ Convert a NetCDF mesh file to face and dissolved shapefiles.
156
+
157
+ Args:
158
+ input_file (str): Path to the input NetCDF file.
159
+ output_dir (str): Directory for output shapefiles.
160
+ crs (str): CRS for output shapefiles.
161
+ quiet (bool): Suppress non-error output.
162
+
163
+ Returns:
164
+ tuple[str, str]: Paths to (faces_shp, dissolved_shp).
165
+ """
166
+ _print("=== NetCDF Mesh to Shapefile Converter ===", quiet)
167
+
168
+ os.makedirs(output_dir, exist_ok=True)
169
+ stem = os.path.splitext(os.path.basename(input_file))[0]
170
+ out_faces = os.path.join(output_dir, f"{stem}_faces.shp")
171
+ out_dissolved = os.path.join(output_dir, f"{stem}_dissolved.shp")
172
+
173
+ face_x, face_y, node_x, node_y, face_nodes, var_names = read_mesh_netcdf(
174
+ input_file, quiet
175
+ )
176
+ polygons = create_polygons(face_x, face_y, quiet)
177
+
178
+ if not polygons:
179
+ raise RuntimeError("No valid polygons were created from the mesh.")
180
+
181
+ gdf = create_geodataframe(polygons, crs)
182
+ _print(f"Saving faces shapefile: {out_faces}", quiet)
183
+ gdf.to_file(out_faces)
184
+
185
+ dissolved = dissolve_geodataframe(gdf, quiet)
186
+ _print(f"Saving dissolved shapefile: {out_dissolved}", quiet)
187
+ dissolved.to_file(out_dissolved)
188
+
189
+ if not quiet:
190
+ bounds = gdf.total_bounds
191
+ counts = gdf['type'].value_counts()
192
+ print("\n=== SUMMARY ===")
193
+ print(f" Input: {input_file}")
194
+ print(f" Faces: {out_faces}")
195
+ print(f" Dissolved: {out_dissolved}")
196
+ print(f" Faces processed: {len(face_x):,} Valid: {len(polygons):,}")
197
+ print(f" CRS: {gdf.crs}")
198
+ for t, n in counts.items():
199
+ print(f" {t}: {n} ({n/len(gdf)*100:.1f}%)")
200
+ print(f" Area — min: {gdf.geometry.area.min():.2f} "
201
+ f"max: {gdf.geometry.area.max():.2f} "
202
+ f"mean: {gdf.geometry.area.mean():.2f}")
203
+ print(f" X: {bounds[0]:.2f} to {bounds[2]:.2f}")
204
+ print(f" Y: {bounds[1]:.2f} to {bounds[3]:.2f}")
205
+
206
+ return out_faces, out_dissolved
207
+
208
+
209
+ def _print(msg, quiet=False):
210
+ if not quiet:
211
+ print(msg)
212
+
213
+
214
+ def main():
215
+ parser = argparse.ArgumentParser(
216
+ prog='nc2shp',
217
+ description='Convert a NetCDF mesh file to ESRI Shapefiles',
218
+ formatter_class=argparse.RawDescriptionHelpFormatter,
219
+ epilog="""
220
+ Examples:
221
+ nc2shp -i FlowFM_net.nc
222
+ nc2shp -i mesh.nc -o output --crs EPSG:4326
223
+ nc2shp -i mesh.nc -q
224
+ """,
225
+ )
226
+
227
+ parser.add_argument(
228
+ '-v', '--version',
229
+ action='version',
230
+ version=f'%(prog)s {importlib.metadata.version("ncftools")}',
231
+ )
232
+ parser.add_argument(
233
+ '-i', '--input',
234
+ required=True,
235
+ metavar='FILE',
236
+ help='Path to the NetCDF mesh file',
237
+ )
238
+ parser.add_argument(
239
+ '-o', '--output-dir',
240
+ default='SHP_NC',
241
+ metavar='DIR',
242
+ help='Output directory for shapefiles (default: SHP_NC)',
243
+ )
244
+ parser.add_argument(
245
+ '--crs',
246
+ default='EPSG:3826',
247
+ help='Coordinate reference system (default: EPSG:3826)',
248
+ )
249
+ parser.add_argument(
250
+ '-q', '--quiet',
251
+ action='store_true',
252
+ help='Suppress non-error output',
253
+ )
254
+
255
+ args = parser.parse_args()
256
+
257
+ if not os.path.isfile(args.input):
258
+ print(f"Error: File '{args.input}' not found.", file=sys.stderr)
259
+ sys.exit(1)
260
+
261
+ try:
262
+ faces, dissolved = mesh_to_shp(
263
+ args.input, args.output_dir, args.crs, args.quiet
264
+ )
265
+ if args.quiet:
266
+ print(faces)
267
+ print(dissolved)
268
+ except Exception as e:
269
+ print(f"Error: {e}", file=sys.stderr)
270
+ sys.exit(1)
271
+
272
+
273
+ if __name__ == "__main__":
274
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncftools
3
- Version: 0.4.0
3
+ Version: 0.5.2
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
@@ -11,6 +11,8 @@ Requires-Python: >=3.8
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: numpy>=1.20.0
13
13
  Requires-Dist: netCDF4>=1.6.0
14
+ Requires-Dist: geopandas>=0.12.0
15
+ Requires-Dist: shapely>=1.8.0
14
16
 
15
17
  # NCFTOOLS
16
18
 
@@ -31,6 +33,9 @@ pip install -e .
31
33
  ### Command-line
32
34
 
33
35
  ```bash
36
+ # Check installed version
37
+ meshinfo --version
38
+
34
39
  # List all available commands
35
40
  ncftools-info
36
41
 
@@ -6,6 +6,7 @@ ncftools/__init__.py
6
6
  ncftools/cli.py
7
7
  ncftools/describe.py
8
8
  ncftools/meshinfo.py
9
+ ncftools/nc2shp.py
9
10
  ncftools.egg-info/PKG-INFO
10
11
  ncftools.egg-info/SOURCES.txt
11
12
  ncftools.egg-info/dependency_links.txt
@@ -1,4 +1,5 @@
1
1
  [console_scripts]
2
2
  meshinfo = ncftools.meshinfo:main
3
+ nc2shp = ncftools.nc2shp:main
3
4
  ncftools = ncftools.cli:main
4
5
  ncftools-info = ncftools.describe:main
@@ -0,0 +1,4 @@
1
+ numpy>=1.20.0
2
+ netCDF4>=1.6.0
3
+ geopandas>=0.12.0
4
+ shapely>=1.8.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ncftools"
7
- version = "0.4.0"
7
+ version = "0.5.2"
8
8
  authors = [
9
9
  { name = "aaronchh", email = "aaronhsu219@gmail.com" },
10
10
  ]
@@ -15,6 +15,8 @@ requires-python = ">=3.8"
15
15
  dependencies = [
16
16
  "numpy>=1.20.0",
17
17
  "netCDF4>=1.6.0",
18
+ "geopandas>=0.12.0",
19
+ "shapely>=1.8.0",
18
20
  ]
19
21
  classifiers = [
20
22
  "Programming Language :: Python :: 3",
@@ -31,3 +33,4 @@ Homepage = "https://github.com/AaronOET/ncftools"
31
33
  ncftools-info = "ncftools.describe:main"
32
34
  ncftools = "ncftools.cli:main"
33
35
  meshinfo = "ncftools.meshinfo:main"
36
+ nc2shp = "ncftools.nc2shp:main"
@@ -1,2 +0,0 @@
1
- numpy>=1.20.0
2
- netCDF4>=1.6.0
File without changes
File without changes
File without changes
File without changes
File without changes