ncftools 0.6.2__tar.gz → 0.9.0__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.6.2
3
+ Version: 0.9.0
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
@@ -30,6 +30,8 @@ pip install -e .
30
30
  - **nc2shp**: Convert a UGRID-compliant NetCDF mesh file to ESRI Shapefiles
31
31
  - **transzone1**: Build a transition zone from triangle mesh faces and select all intersecting faces
32
32
  - **transzone2**: Extract the core transition zone — faces fully within the shrunk zone
33
+ - **setncrain**: Point a D-Flow FM model (`.ext` / `.mdu`) at a NetCDF rainfall forcing file
34
+ - **rnxml**: Rename `dimr.xml` to `dimr_config.xml`
33
35
 
34
36
  ## Usage
35
37
 
@@ -56,10 +58,13 @@ meshinfo -h
56
58
 
57
59
  ### nc2shp
58
60
 
59
- Convert a NetCDF mesh file to ESRI Shapefiles. Outputs `{stem}_faces.shp` and `{stem}_dissolved.shp` in the output directory.
61
+ Convert a NetCDF mesh file to ESRI Shapefiles. Outputs `{stem}_faces.shp` in the output
62
+ directory. Pass `-d`/`--dissolve` to additionally write `{stem}_dissolved.shp`, a single
63
+ polygon dissolved from all mesh faces (slower on large meshes).
60
64
 
61
65
  ```bash
62
66
  nc2shp -i FlowFM_net.nc
67
+ nc2shp -i mesh.nc -d
63
68
  nc2shp -i mesh.nc -o output --crs EPSG:4326
64
69
  nc2shp -i mesh.nc -q
65
70
  ```
@@ -89,6 +94,51 @@ transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_extend.shp -o
89
94
  transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_extend.shp -q
90
95
  ```
91
96
 
97
+ ### setncrain
98
+
99
+ Point a D-Flow FM model at a NetCDF rainfall forcing file.
100
+
101
+ Every `[Meteo]` block in the `.ext` file is rewritten to
102
+ `quantity=rainfall`, `forcingFile=<your .nc>`, `forcingFileType=netcdf`.
103
+ If the `.ext` file does not exist it is created from a built-in template and
104
+ registered in the `.mdu` as `ExtForceFileNew`.
105
+
106
+ The NetCDF time axis is read and the model times in the `.mdu` are set to match it
107
+ (expressed in the model's `Tunit`):
108
+
109
+ | Key | Value |
110
+ | --- | --- |
111
+ | `RefDate` | midnight of the first time stamp |
112
+ | `TStart` | offset of the first time stamp from `RefDate` |
113
+ | `TStop` | offset of the last time stamp from `RefDate` |
114
+
115
+ The `forcingFile` path is written relative to the `.ext` file (D-Flow FM resolves it
116
+ that way); use `--as-given` to write it exactly as typed. `.bak` copies of the
117
+ modified files are written unless `--no-backup` is given.
118
+
119
+ ```bash
120
+ setncrain -i May28_Event.nc
121
+ setncrain -i data/May28_Event.nc --ext dflowfm/FM_model_bnd.ext
122
+ setncrain -i May28_Event.nc --mdu dflowfm/FM_model.mdu --no-time
123
+ setncrain -i May28_Event.nc --no-backup --as-given
124
+ setncrain -i May28_Event.nc -q
125
+ ```
126
+
127
+ ### rnxml
128
+
129
+ Rename `dimr.xml` to `dimr_config.xml`, the name the DIMR runner expects. The file
130
+ stays in its folder and its contents are not touched.
131
+
132
+ If `dimr_config.xml` already exists the rename is refused; pass `--force` to
133
+ overwrite it (a `.bak` copy of the old target is kept unless `--no-backup`).
134
+
135
+ ```bash
136
+ rnxml
137
+ rnxml -i model/dimr.xml
138
+ rnxml -i dimr.xml -o dimr_config.xml --force
139
+ rnxml -i dimr.xml -q
140
+ ```
141
+
92
142
  ## Python API
93
143
 
94
144
  ```python
@@ -14,6 +14,8 @@ pip install -e .
14
14
  - **nc2shp**: Convert a UGRID-compliant NetCDF mesh file to ESRI Shapefiles
15
15
  - **transzone1**: Build a transition zone from triangle mesh faces and select all intersecting faces
16
16
  - **transzone2**: Extract the core transition zone — faces fully within the shrunk zone
17
+ - **setncrain**: Point a D-Flow FM model (`.ext` / `.mdu`) at a NetCDF rainfall forcing file
18
+ - **rnxml**: Rename `dimr.xml` to `dimr_config.xml`
17
19
 
18
20
  ## Usage
19
21
 
@@ -40,10 +42,13 @@ meshinfo -h
40
42
 
41
43
  ### nc2shp
42
44
 
43
- Convert a NetCDF mesh file to ESRI Shapefiles. Outputs `{stem}_faces.shp` and `{stem}_dissolved.shp` in the output directory.
45
+ Convert a NetCDF mesh file to ESRI Shapefiles. Outputs `{stem}_faces.shp` in the output
46
+ directory. Pass `-d`/`--dissolve` to additionally write `{stem}_dissolved.shp`, a single
47
+ polygon dissolved from all mesh faces (slower on large meshes).
44
48
 
45
49
  ```bash
46
50
  nc2shp -i FlowFM_net.nc
51
+ nc2shp -i mesh.nc -d
47
52
  nc2shp -i mesh.nc -o output --crs EPSG:4326
48
53
  nc2shp -i mesh.nc -q
49
54
  ```
@@ -73,6 +78,51 @@ transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_extend.shp -o
73
78
  transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_extend.shp -q
74
79
  ```
75
80
 
81
+ ### setncrain
82
+
83
+ Point a D-Flow FM model at a NetCDF rainfall forcing file.
84
+
85
+ Every `[Meteo]` block in the `.ext` file is rewritten to
86
+ `quantity=rainfall`, `forcingFile=<your .nc>`, `forcingFileType=netcdf`.
87
+ If the `.ext` file does not exist it is created from a built-in template and
88
+ registered in the `.mdu` as `ExtForceFileNew`.
89
+
90
+ The NetCDF time axis is read and the model times in the `.mdu` are set to match it
91
+ (expressed in the model's `Tunit`):
92
+
93
+ | Key | Value |
94
+ | --- | --- |
95
+ | `RefDate` | midnight of the first time stamp |
96
+ | `TStart` | offset of the first time stamp from `RefDate` |
97
+ | `TStop` | offset of the last time stamp from `RefDate` |
98
+
99
+ The `forcingFile` path is written relative to the `.ext` file (D-Flow FM resolves it
100
+ that way); use `--as-given` to write it exactly as typed. `.bak` copies of the
101
+ modified files are written unless `--no-backup` is given.
102
+
103
+ ```bash
104
+ setncrain -i May28_Event.nc
105
+ setncrain -i data/May28_Event.nc --ext dflowfm/FM_model_bnd.ext
106
+ setncrain -i May28_Event.nc --mdu dflowfm/FM_model.mdu --no-time
107
+ setncrain -i May28_Event.nc --no-backup --as-given
108
+ setncrain -i May28_Event.nc -q
109
+ ```
110
+
111
+ ### rnxml
112
+
113
+ Rename `dimr.xml` to `dimr_config.xml`, the name the DIMR runner expects. The file
114
+ stays in its folder and its contents are not touched.
115
+
116
+ If `dimr_config.xml` already exists the rename is refused; pass `--force` to
117
+ overwrite it (a `.bak` copy of the old target is kept unless `--no-backup`).
118
+
119
+ ```bash
120
+ rnxml
121
+ rnxml -i model/dimr.xml
122
+ rnxml -i dimr.xml -o dimr_config.xml --force
123
+ rnxml -i dimr.xml -q
124
+ ```
125
+
76
126
  ## Python API
77
127
 
78
128
  ```python
@@ -2,13 +2,15 @@
2
2
  NCFTOOLS - A collection of tools for working with NetCDF files.
3
3
  """
4
4
 
5
- __version__ = '0.6.2'
5
+ __version__ = '0.9.0'
6
6
 
7
7
  __all__ = [
8
8
  'meshinfo',
9
9
  'nc2shp',
10
10
  'transzone1',
11
11
  'transzone2',
12
+ 'setncrain',
13
+ 'rnxml',
12
14
  'describe',
13
15
  ]
14
16
 
@@ -16,5 +18,7 @@ from . import meshinfo
16
18
  from . import nc2shp
17
19
  from . import transzone1
18
20
  from . import transzone2
21
+ from . import setncrain
22
+ from . import rnxml
19
23
  from . import describe
20
24
  from . import cli
@@ -21,12 +21,14 @@ TOOL_DESCRIPTIONS = {
21
21
  'nc2shp': """
22
22
  Convert a NetCDF mesh file to ESRI Shapefiles.
23
23
 
24
- Reads a UGRID-compliant NetCDF mesh file and writes two shapefiles:
24
+ Reads a UGRID-compliant NetCDF mesh file and writes:
25
25
  {stem}_faces.shp one polygon per mesh face
26
26
  {stem}_dissolved.shp single dissolved polygon of the entire mesh
27
+ (only written with -d/--dissolve)
27
28
 
28
29
  Examples:
29
30
  nc2shp -i FlowFM_net.nc
31
+ nc2shp -i mesh.nc -d # also write the dissolved polygon
30
32
  nc2shp -i mesh.nc -o output --crs EPSG:4326
31
33
  nc2shp -i mesh.nc -q
32
34
  """,
@@ -58,6 +60,43 @@ TOOL_DESCRIPTIONS = {
58
60
  transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_faces.shp -o SHP_TRANS
59
61
  transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_faces.shp -q
60
62
  """,
63
+ 'setncrain': """
64
+ Point a D-Flow FM model at a NetCDF rainfall forcing file.
65
+
66
+ Rewrites every [Meteo] block of the .ext file to use the given NetCDF
67
+ file as rainfall forcing:
68
+ quantity=rainfall, forcingFile=<*.nc>, forcingFileType=netcdf
69
+ If the .ext file is missing it is created from a built-in template and
70
+ registered in the .mdu as ExtForceFileNew.
71
+
72
+ The NetCDF time axis is read and the model times in the .mdu are set
73
+ to match it (in the model's Tunit):
74
+ RefDate midnight of the first time stamp
75
+ TStart offset of the first time stamp from RefDate
76
+ TStop offset of the last time stamp from RefDate
77
+
78
+ Examples:
79
+ setncrain -i May28_Event.nc
80
+ setncrain -i data/May28_Event.nc --ext dflowfm/FM_model_bnd.ext
81
+ setncrain -i May28_Event.nc --no-time
82
+ setncrain -i May28_Event.nc --no-backup --as-given
83
+ """,
84
+ 'rnxml': """
85
+ Rename dimr.xml to dimr_config.xml.
86
+
87
+ D-HYDRO / Delft3D FM writes its DIMR control file as dimr.xml, while
88
+ the DIMR runner expects dimr_config.xml. This tool renames the file in
89
+ place, leaving its contents untouched.
90
+
91
+ If the target name already exists the rename is refused unless --force
92
+ is given, in which case a .bak copy of the old target is kept.
93
+
94
+ Examples:
95
+ rnxml
96
+ rnxml -i model/dimr.xml
97
+ rnxml -i dimr.xml -o dimr_config.xml --force
98
+ rnxml -i dimr.xml -q
99
+ """,
61
100
  }
62
101
 
63
102
 
@@ -2,9 +2,10 @@
2
2
  """
3
3
  nc2shp - Convert NetCDF mesh faces to ESRI Shapefiles
4
4
 
5
- Reads a UGRID-compliant NetCDF mesh file and writes two shapefiles:
5
+ Reads a UGRID-compliant NetCDF mesh file and writes:
6
6
  {stem}_faces.shp - one polygon per mesh face
7
7
  {stem}_dissolved.shp - single dissolved polygon of the entire mesh
8
+ (only with --dissolve)
8
9
  """
9
10
 
10
11
  import argparse
@@ -149,25 +150,30 @@ def dissolve_geodataframe(gdf, quiet=False):
149
150
  return dissolved
150
151
 
151
152
 
152
- def mesh_to_shp(input_file, output_dir="SHP_NC", crs="EPSG:3826", quiet=False):
153
+ def mesh_to_shp(input_file, output_dir="SHP_NC", crs="EPSG:3826", quiet=False,
154
+ dissolve=False):
153
155
  """
154
- Convert a NetCDF mesh file to face and dissolved shapefiles.
156
+ Convert a NetCDF mesh file to face and (optionally) dissolved shapefiles.
155
157
 
156
158
  Args:
157
159
  input_file (str): Path to the input NetCDF file.
158
160
  output_dir (str): Directory for output shapefiles.
159
161
  crs (str): CRS for output shapefiles.
160
162
  quiet (bool): Suppress non-error output.
163
+ dissolve (bool): Also write a single dissolved polygon shapefile.
161
164
 
162
165
  Returns:
163
- tuple[str, str]: Paths to (faces_shp, dissolved_shp).
166
+ tuple[str, str | None]: Paths to (faces_shp, dissolved_shp). The second
167
+ element is None when dissolve is False.
164
168
  """
165
169
  _print("=== NetCDF Mesh to Shapefile Converter ===", quiet)
166
170
 
167
171
  os.makedirs(output_dir, exist_ok=True)
168
172
  stem = os.path.splitext(os.path.basename(input_file))[0]
169
173
  out_faces = os.path.join(output_dir, f"{stem}_faces.shp")
170
- out_dissolved = os.path.join(output_dir, f"{stem}_dissolved.shp")
174
+ out_dissolved = (
175
+ os.path.join(output_dir, f"{stem}_dissolved.shp") if dissolve else None
176
+ )
171
177
 
172
178
  face_x, face_y, node_x, node_y, face_nodes, var_names = read_mesh_netcdf(
173
179
  input_file, quiet
@@ -181,9 +187,10 @@ def mesh_to_shp(input_file, output_dir="SHP_NC", crs="EPSG:3826", quiet=False):
181
187
  _print(f"Saving faces shapefile: {out_faces}", quiet)
182
188
  gdf.to_file(out_faces)
183
189
 
184
- dissolved = dissolve_geodataframe(gdf, quiet)
185
- _print(f"Saving dissolved shapefile: {out_dissolved}", quiet)
186
- dissolved.to_file(out_dissolved)
190
+ if dissolve:
191
+ dissolved = dissolve_geodataframe(gdf, quiet)
192
+ _print(f"Saving dissolved shapefile: {out_dissolved}", quiet)
193
+ dissolved.to_file(out_dissolved)
187
194
 
188
195
  if not quiet:
189
196
  bounds = gdf.total_bounds
@@ -191,7 +198,8 @@ def mesh_to_shp(input_file, output_dir="SHP_NC", crs="EPSG:3826", quiet=False):
191
198
  print("\n=== SUMMARY ===")
192
199
  print(f" Input: {input_file}")
193
200
  print(f" Faces: {out_faces}")
194
- print(f" Dissolved: {out_dissolved}")
201
+ if dissolve:
202
+ print(f" Dissolved: {out_dissolved}")
195
203
  print(f" Faces processed: {len(face_x):,} Valid: {len(polygons):,}")
196
204
  print(f" CRS: {gdf.crs}")
197
205
  for t, n in counts.items():
@@ -218,6 +226,7 @@ def main():
218
226
  epilog="""
219
227
  Examples:
220
228
  nc2shp -i FlowFM_net.nc
229
+ nc2shp -i mesh.nc -d
221
230
  nc2shp -i mesh.nc -o output --crs EPSG:4326
222
231
  nc2shp -i mesh.nc -q
223
232
  """,
@@ -245,6 +254,12 @@ Examples:
245
254
  default='EPSG:3826',
246
255
  help='Coordinate reference system (default: EPSG:3826)',
247
256
  )
257
+ parser.add_argument(
258
+ '-d', '--dissolve',
259
+ action='store_true',
260
+ help='Also write {stem}_dissolved.shp, a single polygon dissolved '
261
+ 'from all mesh faces (slower on large meshes)',
262
+ )
248
263
  parser.add_argument(
249
264
  '-q', '--quiet',
250
265
  action='store_true',
@@ -259,11 +274,12 @@ Examples:
259
274
 
260
275
  try:
261
276
  faces, dissolved = mesh_to_shp(
262
- args.input, args.output_dir, args.crs, args.quiet
277
+ args.input, args.output_dir, args.crs, args.quiet, args.dissolve
263
278
  )
264
279
  if args.quiet:
265
280
  print(faces)
266
- print(dissolved)
281
+ if dissolved:
282
+ print(dissolved)
267
283
  except Exception as e:
268
284
  print(f"Error: {e}", file=sys.stderr)
269
285
  sys.exit(1)
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ rnxml - Rename dimr.xml to dimr_config.xml
4
+
5
+ D-HYDRO / Delft3D FM writes its DIMR control file as dimr.xml, while the DIMR
6
+ runner expects dimr_config.xml. This tool renames the file in place.
7
+
8
+ The file contents are not touched. If the target name already exists the
9
+ rename is refused unless --force is given, in which case a .bak copy of the
10
+ existing target is kept (unless --no-backup).
11
+ """
12
+
13
+ import argparse
14
+ import importlib.metadata
15
+ import os
16
+ import shutil
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ DEFAULT_INPUT = 'dimr.xml'
21
+ DEFAULT_OUTPUT_NAME = 'dimr_config.xml'
22
+
23
+
24
+ def _print(msg, quiet=False):
25
+ if not quiet:
26
+ print(msg)
27
+
28
+
29
+ def rename_xml(
30
+ input_file=DEFAULT_INPUT,
31
+ output_name=DEFAULT_OUTPUT_NAME,
32
+ force=False,
33
+ backup=True,
34
+ quiet=False,
35
+ ):
36
+ """
37
+ Rename an XML file, keeping it in the same directory.
38
+
39
+ Args:
40
+ input_file (str): Path to the file to rename (default: dimr.xml).
41
+ output_name (str): New file name, without a directory part
42
+ (default: dimr_config.xml).
43
+ force (bool): Overwrite the target if it already exists.
44
+ backup (bool): When overwriting, keep a .bak copy of the old target.
45
+ quiet (bool): Suppress non-error output.
46
+
47
+ Returns:
48
+ Path: Path of the renamed file.
49
+
50
+ Raises:
51
+ FileNotFoundError: The input file does not exist.
52
+ ValueError: output_name contains a directory part.
53
+ FileExistsError: The target exists and force is False.
54
+ """
55
+ src = Path(input_file)
56
+ if not src.is_file():
57
+ raise FileNotFoundError(f"file not found: {src}")
58
+
59
+ if os.path.dirname(output_name):
60
+ raise ValueError(
61
+ f"--output-name must be a file name, not a path: {output_name}"
62
+ )
63
+
64
+ dst = src.parent / output_name
65
+
66
+ if src.resolve() == dst.resolve():
67
+ _print(f"{src} is already named {output_name} - nothing to do.", quiet)
68
+ return dst
69
+
70
+ if dst.exists():
71
+ if not force:
72
+ raise FileExistsError(
73
+ f"{dst} already exists - use --force to overwrite it"
74
+ )
75
+ if backup:
76
+ bak = dst.with_suffix(dst.suffix + '.bak')
77
+ shutil.copy2(dst, bak)
78
+ _print(f"Backup written: {bak}", quiet)
79
+
80
+ os.replace(src, dst)
81
+ _print(f"Renamed: {src} -> {dst}", quiet)
82
+ return dst
83
+
84
+
85
+ def main():
86
+ parser = argparse.ArgumentParser(
87
+ prog='rnxml',
88
+ description='Rename dimr.xml to dimr_config.xml',
89
+ formatter_class=argparse.RawDescriptionHelpFormatter,
90
+ epilog="""
91
+ Examples:
92
+ rnxml
93
+ rnxml -i dimr.xml
94
+ rnxml -i model/dimr.xml
95
+ rnxml -i dimr.xml -o dimr_config.xml --force
96
+ rnxml -i dimr.xml -q
97
+ """,
98
+ )
99
+
100
+ parser.add_argument(
101
+ '-v', '--version',
102
+ action='version',
103
+ version=f'%(prog)s {importlib.metadata.version("ncftools")}',
104
+ )
105
+ parser.add_argument(
106
+ '-i', '--input',
107
+ default=DEFAULT_INPUT,
108
+ metavar='FILE',
109
+ help=f'File to rename (default: {DEFAULT_INPUT})',
110
+ )
111
+ parser.add_argument(
112
+ '-o', '--output-name',
113
+ default=DEFAULT_OUTPUT_NAME,
114
+ metavar='NAME',
115
+ help=f'New file name, kept in the same folder (default: {DEFAULT_OUTPUT_NAME})',
116
+ )
117
+ parser.add_argument(
118
+ '--force',
119
+ action='store_true',
120
+ help='Overwrite the target file if it already exists',
121
+ )
122
+ parser.add_argument(
123
+ '--no-backup',
124
+ action='store_true',
125
+ help='Do not keep a .bak copy of an overwritten target',
126
+ )
127
+ parser.add_argument(
128
+ '-q', '--quiet',
129
+ action='store_true',
130
+ help='Suppress non-error output',
131
+ )
132
+
133
+ args = parser.parse_args()
134
+
135
+ try:
136
+ dst = rename_xml(
137
+ args.input,
138
+ output_name=args.output_name,
139
+ force=args.force,
140
+ backup=not args.no_backup,
141
+ quiet=args.quiet,
142
+ )
143
+ if args.quiet:
144
+ print(dst)
145
+ except Exception as e:
146
+ print(f"Error: {e}", file=sys.stderr)
147
+ sys.exit(1)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ setncrain - Point a D-Flow FM model at a NetCDF rainfall forcing file
4
+
5
+ In every [Meteo] block of the .ext file:
6
+ quantity -> rainfall
7
+ forcingFile -> <user specified *.nc>
8
+ forcingFileType -> netcdf
9
+
10
+ If the .ext file does not exist it is created from the hard-coded TEMPLATE
11
+ below, already pointing at the given NetCDF file, and ExtForceFileNew in the
12
+ model definition file (*.mdu) is set to it.
13
+
14
+ The NetCDF time axis is read and the model times in the .mdu are adjusted to it:
15
+ RefDate -> the date of the first time stamp (midnight)
16
+ TStart -> offset of the first time stamp from RefDate (in Tunit)
17
+ TStop -> offset of the last time stamp from RefDate (in Tunit)
18
+ """
19
+
20
+ import argparse
21
+ import importlib.metadata
22
+ import os
23
+ import re
24
+ import shutil
25
+ import sys
26
+ from datetime import datetime, timedelta
27
+ from pathlib import Path
28
+ from typing import Optional
29
+
30
+ REPLACEMENTS = {
31
+ "quantity": "rainfall",
32
+ "forcingfiletype": "netcdf",
33
+ # forcingFile is filled in at runtime
34
+ }
35
+
36
+ # Hard-coded .ext contents, used when the .ext file is missing.
37
+ # Edit here to change the generated file; {forcing_file} is substituted.
38
+ TEMPLATE = """[General]
39
+ fileVersion=2.02
40
+ fileType=extForce
41
+
42
+ [Meteo]
43
+ quantity=rainfall
44
+ forcingFile={forcing_file}
45
+ forcingFileType=netcdf
46
+ interpolationMethod=linearSpaceTime
47
+ operand=O
48
+
49
+ """
50
+
51
+ CF_UNITS = {
52
+ "second": 1.0, "seconds": 1.0, "sec": 1.0, "secs": 1.0, "s": 1.0,
53
+ "minute": 60.0, "minutes": 60.0, "min": 60.0, "mins": 60.0,
54
+ "hour": 3600.0, "hours": 3600.0, "hr": 3600.0, "hrs": 3600.0, "h": 3600.0,
55
+ "day": 86400.0, "days": 86400.0, "d": 86400.0,
56
+ }
57
+ # TStart/TStop are expressed in the model's Tunit
58
+ TUNIT_SECONDS = {"S": 1.0, "M": 60.0, "H": 3600.0, "D": 86400.0}
59
+
60
+
61
+ def _print(msg, quiet=False):
62
+ if not quiet:
63
+ print(msg)
64
+
65
+
66
+ def create(ext_path, nc_file, quiet=False):
67
+ """
68
+ Create a new .ext file from the hard-coded TEMPLATE, pointing at nc_file.
69
+
70
+ Args:
71
+ ext_path (Path): Path of the .ext file to write.
72
+ nc_file (str): forcingFile value to write into the [Meteo] block.
73
+ quiet (bool): Suppress non-error output.
74
+ """
75
+ ext_path = Path(ext_path)
76
+ ext_path.parent.mkdir(parents=True, exist_ok=True)
77
+ # D-Flow FM input files conventionally use CRLF
78
+ with ext_path.open("w", newline="\r\n") as f:
79
+ f.write(TEMPLATE.format(forcing_file=nc_file))
80
+ _print(f"Created: {ext_path}", quiet)
81
+ _print(
82
+ f" quantity=rainfall, forcingFile={nc_file}, forcingFileType=netcdf",
83
+ quiet,
84
+ )
85
+
86
+
87
+ def find_mdu(ext_path) -> Optional[Path]:
88
+ """Locate the .mdu next to the .ext file (FM_model.mdu wins if several)."""
89
+ ext_path = Path(ext_path)
90
+ folder = ext_path.parent if str(ext_path.parent) else Path(".")
91
+ mdus = sorted(folder.glob("*.mdu"))
92
+ if not mdus:
93
+ return None
94
+ for m in mdus:
95
+ if m.stem.lower() == "fm_model":
96
+ return m
97
+ return mdus[0]
98
+
99
+
100
+ def read_mdu_value(mdu_path, key) -> Optional[str]:
101
+ """Return the value of a single .mdu key, or None if it is absent."""
102
+ with Path(mdu_path).open("r", newline="") as f:
103
+ for line in f:
104
+ name, sep, rest = line.partition("=")
105
+ if sep and name.strip().lower() == key.lower():
106
+ return rest.partition("#")[0].strip()
107
+ return None
108
+
109
+
110
+ def update_mdu(mdu_path, settings, backup=True, quiet=False):
111
+ """
112
+ Set the given key=value pairs in the .mdu, preserving layout and comments.
113
+
114
+ Args:
115
+ mdu_path (Path): Path to the model definition file.
116
+ settings (dict): Key/value pairs to apply.
117
+ backup (bool): Write a .bak copy before overwriting.
118
+ quiet (bool): Suppress non-error output.
119
+
120
+ Returns:
121
+ list[str]: Human-readable description of each change made.
122
+ """
123
+ mdu_path = Path(mdu_path)
124
+ wanted = {k.lower(): (k, str(v)) for k, v in settings.items()}
125
+
126
+ with mdu_path.open("r", newline="") as f:
127
+ lines = f.read().splitlines(keepends=True)
128
+
129
+ out, changed, seen = [], [], set()
130
+ for line in lines:
131
+ body = line.rstrip("\r\n")
132
+ eol = line[len(body):]
133
+ key, sep, rest = body.partition("=")
134
+ name = key.strip().lower()
135
+
136
+ if sep and name in wanted:
137
+ seen.add(name)
138
+ label, value = wanted[name]
139
+ val_part, hash_sep, comment = rest.partition("#")
140
+ old = val_part.strip()
141
+ if old != value:
142
+ # keep the original column of the trailing comment
143
+ lead = val_part[: len(val_part) - len(val_part.lstrip())] if old else " "
144
+ new_val = (lead + value).ljust(len(val_part))
145
+ if hash_sep and not new_val.endswith(" "):
146
+ new_val += " "
147
+ line = f"{key}={new_val}{hash_sep}{comment}" + eol
148
+ changed.append(f"{label} = {old or '<empty>'} -> {value}")
149
+
150
+ out.append(line)
151
+
152
+ for name, (label, value) in wanted.items():
153
+ if name not in seen:
154
+ print(
155
+ f"! {label} not found in {mdu_path} - set it to {value} manually.",
156
+ file=sys.stderr,
157
+ )
158
+
159
+ if not changed:
160
+ _print(f"{mdu_path}: already up to date.", quiet)
161
+ return changed
162
+
163
+ if backup:
164
+ bak = mdu_path.with_suffix(mdu_path.suffix + ".bak")
165
+ shutil.copy2(mdu_path, bak)
166
+ _print(f"Backup written: {bak}", quiet)
167
+
168
+ with mdu_path.open("w", newline="") as f:
169
+ f.write("".join(out))
170
+ _print(f"Updated: {mdu_path}", quiet)
171
+ for c in changed:
172
+ _print(f" {c}", quiet)
173
+ return changed
174
+
175
+
176
+ def rewrite(ext_path, nc_file, backup=True, quiet=False):
177
+ """
178
+ Rewrite the [Meteo] blocks of an existing .ext file for NetCDF rainfall.
179
+
180
+ Args:
181
+ ext_path (Path): Path to the existing .ext file.
182
+ nc_file (str): forcingFile value to write.
183
+ backup (bool): Write a .bak copy before overwriting.
184
+ quiet (bool): Suppress non-error output.
185
+
186
+ Returns:
187
+ list[str]: Human-readable description of each change made.
188
+ """
189
+ ext_path = Path(ext_path)
190
+ if not ext_path.is_file():
191
+ raise FileNotFoundError(f"ext file not found: {ext_path}")
192
+
193
+ # newline='' keeps original CRLF/LF line endings intact
194
+ with ext_path.open("r", newline="") as f:
195
+ raw = f.read()
196
+
197
+ lines = raw.splitlines(keepends=True)
198
+ out = []
199
+ in_meteo = False
200
+ changed = []
201
+
202
+ for line in lines:
203
+ stripped = line.strip()
204
+ body, eol = line.rstrip("\r\n"), line[len(line.rstrip("\r\n")):]
205
+
206
+ if stripped.startswith("["):
207
+ in_meteo = stripped.lower() == "[meteo]"
208
+ out.append(line)
209
+ continue
210
+
211
+ if in_meteo and "=" in body:
212
+ key, _, old = body.partition("=")
213
+ # split off any inline comment so it is preserved
214
+ old_val, sep, comment = old.partition("#")
215
+ name = key.strip().lower()
216
+
217
+ new_val = None
218
+ if name == "forcingfile":
219
+ new_val = nc_file
220
+ elif name in REPLACEMENTS:
221
+ new_val = REPLACEMENTS[name]
222
+
223
+ if new_val is not None:
224
+ # keep original spacing style around '=' and the column of any
225
+ # trailing comment
226
+ lead = old_val[: len(old_val) - len(old_val.lstrip())]
227
+ padded = lead + new_val
228
+ if sep: # an inline comment follows: keep its column
229
+ padded = padded.ljust(len(old_val))
230
+ if not padded.endswith(" "):
231
+ padded += " "
232
+ body = f"{key}={padded}{sep}{comment}"
233
+ changed.append(f"{key.strip()} = {old_val.strip()} -> {new_val}")
234
+ line = body + eol
235
+
236
+ out.append(line)
237
+
238
+ if not changed:
239
+ _print("No [Meteo] keys matched - file left unchanged.", quiet)
240
+ return changed
241
+
242
+ if backup:
243
+ bak = ext_path.with_suffix(ext_path.suffix + ".bak")
244
+ shutil.copy2(ext_path, bak)
245
+ _print(f"Backup written: {bak}", quiet)
246
+
247
+ with ext_path.open("w", newline="") as f:
248
+ f.write("".join(out))
249
+
250
+ _print(f"Updated: {ext_path}", quiet)
251
+ for c in changed:
252
+ _print(f" {c}", quiet)
253
+ return changed
254
+
255
+
256
+ def _open_time_var(nc_path):
257
+ """Return (values, units_string) of the time coordinate of a NetCDF file."""
258
+
259
+ def pick(names, get_attr):
260
+ for cand in ("time", "TIME", "Time", "t"):
261
+ if cand in names:
262
+ return cand
263
+ for n in names: # fall back to any CF time-like variable
264
+ units = (get_attr(n, "units") or "").lower()
265
+ if " since " in units or (get_attr(n, "axis") or "") == "T":
266
+ return n
267
+ return None
268
+
269
+ try:
270
+ import netCDF4 # the usual reader in a Delft3D FM python environment
271
+ except ImportError:
272
+ pass
273
+ else:
274
+ with netCDF4.Dataset(str(nc_path)) as ds:
275
+ def get(n, a):
276
+ return getattr(ds.variables[n], a, None)
277
+
278
+ name = pick(list(ds.variables), get)
279
+ if name is None:
280
+ raise ValueError(f"no time variable found in {nc_path}")
281
+ v = ds.variables[name]
282
+ return [float(x) for x in v[:].ravel()], str(getattr(v, "units", ""))
283
+
284
+ try:
285
+ import xarray as xr # second choice
286
+ except ImportError:
287
+ pass
288
+ else:
289
+ with xr.open_dataset(str(nc_path), decode_times=False) as ds:
290
+ def get(n, a):
291
+ return ds[n].attrs.get(a)
292
+
293
+ name = pick(list(ds.variables), get)
294
+ if name is None:
295
+ raise ValueError(f"no time variable found in {nc_path}")
296
+ v = ds[name]
297
+ return [float(x) for x in v.values.ravel()], str(v.attrs.get("units", ""))
298
+
299
+ try:
300
+ from scipy.io import netcdf_file # last resort, NetCDF3 only
301
+ except ImportError as exc:
302
+ raise ImportError(
303
+ "reading the NetCDF file needs one of netCDF4, xarray or scipy - "
304
+ "install with: pip install netCDF4"
305
+ ) from exc
306
+
307
+ with netcdf_file(str(nc_path), "r", mmap=False) as ds:
308
+ def get(n, a):
309
+ val = getattr(ds.variables[n], a, None)
310
+ return val.decode() if isinstance(val, bytes) else val
311
+
312
+ name = pick(list(ds.variables), get)
313
+ if name is None:
314
+ raise ValueError(f"no time variable found in {nc_path}")
315
+ v = ds.variables[name]
316
+ units = getattr(v, "units", b"")
317
+ return (
318
+ [float(x) for x in v[:].ravel()],
319
+ units.decode() if isinstance(units, bytes) else str(units),
320
+ )
321
+
322
+
323
+ def parse_cf_units(units):
324
+ """'minutes since 1970-01-01 00:00:00.0 +0000' -> (scale in s, epoch)."""
325
+ unit, _, epoch_txt = units.partition(" since ")
326
+ scale = CF_UNITS.get(unit.strip().lower())
327
+ if scale is None:
328
+ raise ValueError(f"unsupported time unit in '{units}'")
329
+
330
+ txt = epoch_txt.strip()
331
+ # drop the time zone, then normalise an ISO 'T' separator to a space
332
+ txt = re.sub(r"\s*(Z|UTC|GMT)\s*$", "", txt) # ... 00:00:00 UTC
333
+ txt = re.sub(r"\s+[+-]\d{2}:?\d{2}\s*$", "", txt) # ... 00:00:00 +0000
334
+ txt = re.sub(r"(?<=\d)T(?=\d)", " ", txt).strip()
335
+ for fmt in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
336
+ try:
337
+ return scale, datetime.strptime(txt, fmt)
338
+ except ValueError:
339
+ continue
340
+ raise ValueError(f"cannot parse the reference date in '{units}'")
341
+
342
+
343
+ def nc_time_range(nc_path):
344
+ """First and last time stamp in the NetCDF file, as datetimes."""
345
+ values, units = _open_time_var(nc_path)
346
+ if not values:
347
+ raise ValueError(f"the time variable in {nc_path} is empty")
348
+ scale, epoch = parse_cf_units(units)
349
+ lo, hi = min(values), max(values)
350
+ return epoch + timedelta(seconds=lo * scale), epoch + timedelta(seconds=hi * scale)
351
+
352
+
353
+ def _fmt(seconds, tunit):
354
+ v = seconds / TUNIT_SECONDS.get(tunit.upper(), 1.0)
355
+ return str(int(round(v))) if abs(v - round(v)) < 1e-6 else f"{v:.6g}"
356
+
357
+
358
+ def time_settings(nc_path, tunit="S", quiet=False):
359
+ """
360
+ RefDate / TStart / TStop derived from the NetCDF time axis.
361
+
362
+ RefDate is midnight of the first time stamp; TStart and TStop are offsets
363
+ from that midnight, expressed in the model's Tunit.
364
+
365
+ Args:
366
+ nc_path (Path): Path to the NetCDF forcing file.
367
+ tunit (str): Model time unit, one of S, M, H, D.
368
+ quiet (bool): Suppress non-error output.
369
+
370
+ Returns:
371
+ dict: {'RefDate': ..., 'TStart': ..., 'TStop': ...}
372
+ """
373
+ t0, t1 = nc_time_range(nc_path)
374
+ ref = t0.replace(hour=0, minute=0, second=0, microsecond=0)
375
+ _print(
376
+ f"NetCDF time span: {t0:%Y-%m-%d %H:%M:%S} -> {t1:%Y-%m-%d %H:%M:%S}",
377
+ quiet,
378
+ )
379
+ return {
380
+ "RefDate": ref.strftime("%Y%m%d"),
381
+ "TStart": _fmt((t0 - ref).total_seconds(), tunit),
382
+ "TStop": _fmt((t1 - ref).total_seconds(), tunit),
383
+ }
384
+
385
+
386
+ def resolve_forcing_path(nc_file, ext_path):
387
+ """D-Flow FM resolves forcingFile relative to the .ext file, so rewrite a
388
+ path given relative to the cwd (or an absolute one) into that form."""
389
+ nc = Path(nc_file)
390
+ if not nc.exists():
391
+ return nc_file # nothing to resolve against; write it through as given
392
+ try:
393
+ rel = os.path.relpath(nc.resolve(), Path(ext_path).resolve().parent)
394
+ except ValueError: # different drive on Windows -> keep the absolute path
395
+ return str(nc.resolve())
396
+ return rel.replace(os.sep, "/")
397
+
398
+
399
+ def set_nc_rainfall(
400
+ nc_file,
401
+ ext_file="dflowfm/FM_model_bnd.ext",
402
+ mdu_file=None,
403
+ update_time=True,
404
+ as_given=False,
405
+ backup=True,
406
+ quiet=False,
407
+ ):
408
+ """
409
+ Point a D-Flow FM model at a NetCDF rainfall forcing file.
410
+
411
+ Args:
412
+ nc_file (str): Path to the NetCDF forcing file.
413
+ ext_file (str): Path to the .ext file (created if missing).
414
+ mdu_file (str): Model definition file; defaults to the *.mdu next to
415
+ the .ext file.
416
+ update_time (bool): Also set RefDate/TStart/TStop from the NetCDF
417
+ time axis.
418
+ as_given (bool): Write the forcingFile path exactly as typed instead
419
+ of making it relative to the .ext file.
420
+ backup (bool): Write .bak copies before overwriting files.
421
+ quiet (bool): Suppress non-error output.
422
+
423
+ Returns:
424
+ tuple[Path, Path | None]: Paths to the (.ext, .mdu) touched; the .mdu
425
+ is None when no model definition file could be found.
426
+ """
427
+ nc_path = Path(nc_file)
428
+ ext_path = Path(ext_file)
429
+ forcing = nc_file if as_given else resolve_forcing_path(nc_file, ext_path)
430
+
431
+ created = not ext_path.is_file()
432
+ if created:
433
+ _print(f"{ext_path} not found - creating it.", quiet)
434
+ create(ext_path, forcing, quiet=quiet)
435
+ else:
436
+ rewrite(ext_path, forcing, backup=backup, quiet=quiet)
437
+
438
+ # ---- model definition file -------------------------------------------
439
+ settings = {}
440
+ if created:
441
+ # a freshly created .ext has to be registered in the .mdu
442
+ settings["ExtForceFileNew"] = ext_path.name
443
+
444
+ mdu_path = Path(mdu_file) if mdu_file else find_mdu(ext_path)
445
+ if mdu_path is None:
446
+ print(
447
+ f"! No *.mdu found in {ext_path.parent} - model times not updated.",
448
+ file=sys.stderr,
449
+ )
450
+ return ext_path, None
451
+ if not mdu_path.is_file():
452
+ print(
453
+ f"! mdu not found: {mdu_path} - model times not updated.",
454
+ file=sys.stderr,
455
+ )
456
+ return ext_path, None
457
+
458
+ if update_time:
459
+ if not nc_path.is_file():
460
+ print(
461
+ f"! {nc_path} not found - RefDate/TStart/TStop left unchanged.",
462
+ file=sys.stderr,
463
+ )
464
+ else:
465
+ tunit = (read_mdu_value(mdu_path, "Tunit") or "S").upper() or "S"
466
+ try:
467
+ settings.update(time_settings(nc_path, tunit, quiet=quiet))
468
+ except Exception as exc: # unreadable file, odd time axis, ...
469
+ print(f"! could not read times from {nc_path}: {exc}", file=sys.stderr)
470
+ print(" RefDate/TStart/TStop left unchanged.", file=sys.stderr)
471
+
472
+ if settings:
473
+ update_mdu(mdu_path, settings, backup=backup, quiet=quiet)
474
+
475
+ return ext_path, mdu_path
476
+
477
+
478
+ def main():
479
+ parser = argparse.ArgumentParser(
480
+ prog='setncrain',
481
+ description='Point a D-Flow FM .ext/.mdu pair at a NetCDF rainfall forcing file',
482
+ formatter_class=argparse.RawDescriptionHelpFormatter,
483
+ epilog="""
484
+ Examples:
485
+ setncrain -i May28_Event.nc
486
+ setncrain -i C:/data/rain/May28_Event.nc
487
+ setncrain -i May28_Event.nc --ext dflowfm/FM_model_bnd.ext
488
+ setncrain -i May28_Event.nc --no-backup --as-given
489
+ """,
490
+ )
491
+
492
+ parser.add_argument(
493
+ '-v', '--version',
494
+ action='version',
495
+ version=f'%(prog)s {importlib.metadata.version("ncftools")}',
496
+ )
497
+ parser.add_argument(
498
+ '-i', '--input',
499
+ required=True,
500
+ metavar='FILE',
501
+ help='Path to the NetCDF rainfall forcing file (*.nc)',
502
+ )
503
+ parser.add_argument(
504
+ '--ext',
505
+ default='dflowfm/FM_model_bnd.ext',
506
+ metavar='FILE',
507
+ help='Path to the .ext file (default: dflowfm/FM_model_bnd.ext)',
508
+ )
509
+ parser.add_argument(
510
+ '--mdu',
511
+ metavar='FILE',
512
+ help='Model definition file to update (default: the *.mdu next to the .ext)',
513
+ )
514
+ parser.add_argument(
515
+ '--no-time',
516
+ action='store_true',
517
+ help='Do not touch RefDate/TStart/TStop in the .mdu',
518
+ )
519
+ parser.add_argument(
520
+ '--as-given',
521
+ action='store_true',
522
+ help='Write the path exactly as typed instead of making it relative to the .ext file',
523
+ )
524
+ parser.add_argument(
525
+ '--no-backup',
526
+ action='store_true',
527
+ help='Do not write .bak copies',
528
+ )
529
+ parser.add_argument(
530
+ '-q', '--quiet',
531
+ action='store_true',
532
+ help='Suppress non-error output',
533
+ )
534
+
535
+ args = parser.parse_args()
536
+
537
+ if not args.input.lower().endswith('.nc'):
538
+ print("Error: forcing file must be a *.nc file", file=sys.stderr)
539
+ sys.exit(1)
540
+
541
+ try:
542
+ ext_path, mdu_path = set_nc_rainfall(
543
+ args.input,
544
+ ext_file=args.ext,
545
+ mdu_file=args.mdu,
546
+ update_time=not args.no_time,
547
+ as_given=args.as_given,
548
+ backup=not args.no_backup,
549
+ quiet=args.quiet,
550
+ )
551
+ if args.quiet:
552
+ print(ext_path)
553
+ if mdu_path is not None:
554
+ print(mdu_path)
555
+ except Exception as e:
556
+ print(f"Error: {e}", file=sys.stderr)
557
+ sys.exit(1)
558
+
559
+
560
+ if __name__ == "__main__":
561
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncftools
3
- Version: 0.6.2
3
+ Version: 0.9.0
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
@@ -30,6 +30,8 @@ pip install -e .
30
30
  - **nc2shp**: Convert a UGRID-compliant NetCDF mesh file to ESRI Shapefiles
31
31
  - **transzone1**: Build a transition zone from triangle mesh faces and select all intersecting faces
32
32
  - **transzone2**: Extract the core transition zone — faces fully within the shrunk zone
33
+ - **setncrain**: Point a D-Flow FM model (`.ext` / `.mdu`) at a NetCDF rainfall forcing file
34
+ - **rnxml**: Rename `dimr.xml` to `dimr_config.xml`
33
35
 
34
36
  ## Usage
35
37
 
@@ -56,10 +58,13 @@ meshinfo -h
56
58
 
57
59
  ### nc2shp
58
60
 
59
- Convert a NetCDF mesh file to ESRI Shapefiles. Outputs `{stem}_faces.shp` and `{stem}_dissolved.shp` in the output directory.
61
+ Convert a NetCDF mesh file to ESRI Shapefiles. Outputs `{stem}_faces.shp` in the output
62
+ directory. Pass `-d`/`--dissolve` to additionally write `{stem}_dissolved.shp`, a single
63
+ polygon dissolved from all mesh faces (slower on large meshes).
60
64
 
61
65
  ```bash
62
66
  nc2shp -i FlowFM_net.nc
67
+ nc2shp -i mesh.nc -d
63
68
  nc2shp -i mesh.nc -o output --crs EPSG:4326
64
69
  nc2shp -i mesh.nc -q
65
70
  ```
@@ -89,6 +94,51 @@ transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_extend.shp -o
89
94
  transzone2 -i SHP_NC/FlowFM_net_faces.shp -z SHP_TRANS/trans_zone_extend.shp -q
90
95
  ```
91
96
 
97
+ ### setncrain
98
+
99
+ Point a D-Flow FM model at a NetCDF rainfall forcing file.
100
+
101
+ Every `[Meteo]` block in the `.ext` file is rewritten to
102
+ `quantity=rainfall`, `forcingFile=<your .nc>`, `forcingFileType=netcdf`.
103
+ If the `.ext` file does not exist it is created from a built-in template and
104
+ registered in the `.mdu` as `ExtForceFileNew`.
105
+
106
+ The NetCDF time axis is read and the model times in the `.mdu` are set to match it
107
+ (expressed in the model's `Tunit`):
108
+
109
+ | Key | Value |
110
+ | --- | --- |
111
+ | `RefDate` | midnight of the first time stamp |
112
+ | `TStart` | offset of the first time stamp from `RefDate` |
113
+ | `TStop` | offset of the last time stamp from `RefDate` |
114
+
115
+ The `forcingFile` path is written relative to the `.ext` file (D-Flow FM resolves it
116
+ that way); use `--as-given` to write it exactly as typed. `.bak` copies of the
117
+ modified files are written unless `--no-backup` is given.
118
+
119
+ ```bash
120
+ setncrain -i May28_Event.nc
121
+ setncrain -i data/May28_Event.nc --ext dflowfm/FM_model_bnd.ext
122
+ setncrain -i May28_Event.nc --mdu dflowfm/FM_model.mdu --no-time
123
+ setncrain -i May28_Event.nc --no-backup --as-given
124
+ setncrain -i May28_Event.nc -q
125
+ ```
126
+
127
+ ### rnxml
128
+
129
+ Rename `dimr.xml` to `dimr_config.xml`, the name the DIMR runner expects. The file
130
+ stays in its folder and its contents are not touched.
131
+
132
+ If `dimr_config.xml` already exists the rename is refused; pass `--force` to
133
+ overwrite it (a `.bak` copy of the old target is kept unless `--no-backup`).
134
+
135
+ ```bash
136
+ rnxml
137
+ rnxml -i model/dimr.xml
138
+ rnxml -i dimr.xml -o dimr_config.xml --force
139
+ rnxml -i dimr.xml -q
140
+ ```
141
+
92
142
  ## Python API
93
143
 
94
144
  ```python
@@ -7,6 +7,8 @@ ncftools/cli.py
7
7
  ncftools/describe.py
8
8
  ncftools/meshinfo.py
9
9
  ncftools/nc2shp.py
10
+ ncftools/rnxml.py
11
+ ncftools/setncrain.py
10
12
  ncftools/transzone1.py
11
13
  ncftools/transzone2.py
12
14
  ncftools.egg-info/PKG-INFO
@@ -3,5 +3,7 @@ meshinfo = ncftools.meshinfo:main
3
3
  nc2shp = ncftools.nc2shp:main
4
4
  ncftools = ncftools.cli:main
5
5
  ncftools-info = ncftools.describe:main
6
+ rnxml = ncftools.rnxml:main
7
+ setncrain = ncftools.setncrain:main
6
8
  transzone1 = ncftools.transzone1:main
7
9
  transzone2 = ncftools.transzone2:main
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ncftools"
7
- version = "0.6.2"
7
+ version = "0.9.0"
8
8
  authors = [
9
9
  { name = "aaronchh", email = "aaronhsu219@gmail.com" },
10
10
  ]
@@ -36,3 +36,5 @@ meshinfo = "ncftools.meshinfo:main"
36
36
  nc2shp = "ncftools.nc2shp:main"
37
37
  transzone1 = "ncftools.transzone1:main"
38
38
  transzone2 = "ncftools.transzone2:main"
39
+ setncrain = "ncftools.setncrain:main"
40
+ rnxml = "ncftools.rnxml:main"
File without changes
File without changes
File without changes
File without changes
File without changes