Skip to content

Commit 4633ae4

Browse files
mjrenomjreno
authored andcommitted
support NCF encodings and grid mappings
1 parent 65a4df8 commit 4633ae4

File tree

6 files changed

+206
-35
lines changed

6 files changed

+206
-35
lines changed

flopy/discretization/grid.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1288,14 +1288,15 @@ def write_shapefile(self, filename="grid.shp", crs=None, prjfile=None, **kwargs)
12881288
)
12891289
return
12901290

1291-
def dataset(self, modeltime=None, mesh=None):
1291+
def dataset(self, modeltime=None, mesh=None, encoding=None):
12921292
"""
12931293
Method to generate baseline xarray dataset
12941294
12951295
Parameters
12961296
----------
12971297
modeltime : FloPy ModelTime object
12981298
mesh : mesh type
1299+
encoding : variable encoding dictionary
12991300
13001301
Returns
13011302
-------

flopy/discretization/structuredgrid.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,11 +1770,12 @@ def get_plottable_layer_array(self, a, layer):
17701770
assert plotarray.shape == required_shape, msg
17711771
return plotarray
17721772

1773-
def dataset(self, modeltime=None, mesh=None):
1773+
def dataset(self, modeltime=None, mesh=None, encoding=None):
17741774
"""
17751775
modeltime : FloPy ModelTime object
17761776
mesh : mesh type
17771777
valid mesh types are "layered" or None
1778+
encoding : variable encoding dictionary
17781779
"""
17791780
from ..utils import import_optional_dependency
17801781

@@ -1787,11 +1788,11 @@ def dataset(self, modeltime=None, mesh=None):
17871788
ds.attrs["modflow_grid"] = "STRUCTURED"
17881789

17891790
if mesh and mesh.upper() == "LAYERED":
1790-
return self._layered_mesh_dataset(ds, modeltime)
1791+
return self._layered_mesh_dataset(ds, modeltime, encoding)
17911792
elif mesh is None:
1792-
return self._structured_dataset(ds, modeltime)
1793+
return self._structured_dataset(ds, modeltime, encoding)
17931794

1794-
def _layered_mesh_dataset(self, ds, modeltime=None):
1795+
def _layered_mesh_dataset(self, ds, modeltime=None, encoding=None):
17951796
FILLNA_INT32 = np.int32(-2147483647)
17961797
FILLNA_DBL = 9.96920996838687e36
17971798
lenunits = {0: "m", 1: "ft", 2: "m", 3: "m"}
@@ -1894,9 +1895,25 @@ def _layered_mesh_dataset(self, ds, modeltime=None):
18941895
ds["mesh_face_nodes"].attrs["_FillValue"] = FILLNA_INT32
18951896
ds["mesh_face_nodes"].attrs["start_index"] = np.int32(1)
18961897

1898+
if encoding is not None and "wkt" in encoding and encoding["wkt"] is not None:
1899+
ds = ds.assign({"projection": ([], np.int32(1))})
1900+
# wkt override to existing crs
1901+
ds["projection"].attrs["wkt"] = encoding["wkt"]
1902+
ds["mesh_node_x"].attrs["grid_mapping"] = "projection"
1903+
ds["mesh_node_y"].attrs["grid_mapping"] = "projection"
1904+
ds["mesh_face_x"].attrs["grid_mapping"] = "projection"
1905+
ds["mesh_face_y"].attrs["grid_mapping"] = "projection"
1906+
elif self.crs is not None:
1907+
ds = ds.assign({"projection": ([], np.int32(1))})
1908+
ds["projection"].attrs["wkt"] = self.crs.to_wkt()
1909+
ds["mesh_node_x"].attrs["grid_mapping"] = "projection"
1910+
ds["mesh_node_y"].attrs["grid_mapping"] = "projection"
1911+
ds["mesh_face_x"].attrs["grid_mapping"] = "projection"
1912+
ds["mesh_face_y"].attrs["grid_mapping"] = "projection"
1913+
18971914
return ds
18981915

1899-
def _structured_dataset(self, ds, modeltime=None):
1916+
def _structured_dataset(self, ds, modeltime=None, encoding=None):
19001917
lenunits = {0: "m", 1: "ft", 2: "m", 3: "m"}
19011918

19021919
x = self.xoffset + self.xycenters[0]
@@ -1953,6 +1970,18 @@ def _structured_dataset(self, ds, modeltime=None):
19531970
ds["x"].attrs["long_name"] = "Easting"
19541971
ds["x"].attrs["bounds"] = "x_bnds"
19551972

1973+
if encoding is not None and "wkt" in encoding and encoding["wkt"] is not None:
1974+
ds = ds.assign({"projection": ([], np.int32(1))})
1975+
# wkt override to existing crs
1976+
ds["projection"].attrs["crs_wkt"] = encoding["wkt"]
1977+
ds["x"].attrs["grid_mapping"] = "projection"
1978+
ds["y"].attrs["grid_mapping"] = "projection"
1979+
elif self.crs is not None:
1980+
ds = ds.assign({"projection": ([], np.int32(1))})
1981+
ds["projection"].attrs["crs_wkt"] = self.crs.to_wkt()
1982+
ds["x"].attrs["grid_mapping"] = "projection"
1983+
ds["y"].attrs["grid_mapping"] = "projection"
1984+
19561985
return ds
19571986

19581987
def _set_structured_iverts(self):

flopy/discretization/vertexgrid.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,12 +600,13 @@ def get_plottable_layer_array(self, a, layer):
600600
assert plotarray.shape == required_shape, msg
601601
return plotarray
602602

603-
def dataset(self, modeltime=None, mesh=None):
603+
def dataset(self, modeltime=None, mesh=None, encoding=None):
604604
"""
605605
modeltime : FloPy ModelTime object
606606
mesh : mesh type
607607
valid mesh types are "layered" or None
608608
VertexGrid objects only support layered mesh
609+
encoding : variable encoding dictionary
609610
"""
610611
from ..utils import import_optional_dependency
611612

@@ -717,6 +718,22 @@ def dataset(self, modeltime=None, mesh=None):
717718
ds["mesh_face_nodes"].attrs["_FillValue"] = FILLNA_INT32
718719
ds["mesh_face_nodes"].attrs["start_index"] = np.int32(1)
719720

721+
if encoding is not None and "wkt" in encoding and encoding["wkt"] is not None:
722+
ds = ds.assign({"projection": ([], np.int32(1))})
723+
# wkt override to existing crs
724+
ds["projection"].attrs["wkt"] = encoding["wkt"]
725+
ds["mesh_node_x"].attrs["grid_mapping"] = "projection"
726+
ds["mesh_node_y"].attrs["grid_mapping"] = "projection"
727+
ds["mesh_face_x"].attrs["grid_mapping"] = "projection"
728+
ds["mesh_face_y"].attrs["grid_mapping"] = "projection"
729+
elif self.crs is not None:
730+
ds = ds.assign({"projection": ([], np.int32(1))})
731+
ds["projection"].attrs["wkt"] = self.crs.to_wkt()
732+
ds["mesh_node_x"].attrs["grid_mapping"] = "projection"
733+
ds["mesh_node_y"].attrs["grid_mapping"] = "projection"
734+
ds["mesh_face_x"].attrs["grid_mapping"] = "projection"
735+
ds["mesh_face_y"].attrs["grid_mapping"] = "projection"
736+
720737
return ds
721738

722739
# initialize grid from a grb file

flopy/mf6/mfmodel.py

Lines changed: 135 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,7 +1313,6 @@ def write(
13131313
ext_file_action=ExtFileAction.copy_relative_paths,
13141314
netcdf=None,
13151315
):
1316-
from ..version import __version__
13171316
"""
13181317
Writes out model's package files.
13191318
@@ -1359,7 +1358,10 @@ def write(
13591358
if write_netcdf:
13601359
# set data storage to write ascii for netcdf
13611360
pp._set_netcdf_storage()
1362-
1361+
if pp.package_type.startswith("dis"):
1362+
crs = pp.crs.get_data()
1363+
if crs is not None and self.modelgrid.crs is None:
1364+
self.modelgrid.crs = crs[0][1]
13631365
if (
13641366
self.simulation_data.verbosity_level.value
13651367
>= VerbosityLevel.normal.value
@@ -1372,35 +1374,11 @@ def write(
13721374

13731375
# write netcdf file
13741376
if write_netcdf and netcdf.lower() != "nofile":
1375-
mesh = netcdf
1376-
if mesh.upper() == "STRUCTURED":
1377-
mesh = None
1378-
1379-
ds = self.modelgrid.dataset(
1380-
modeltime=self.modeltime,
1381-
mesh=mesh,
1382-
)
1383-
1384-
nc_info = self.netcdf_info(mesh=mesh)
1385-
nc_info["attrs"]["title"] = f"{self.name.upper()} input"
1386-
nc_info["attrs"]["source"] = f"flopy {__version__}"
1387-
# :history = "first created 2025/8/21 9:46:2.909" ;
1388-
# :Conventions = "CF-1.11 UGRID-1.0" ;
1389-
ds = self.update_dataset(ds, netcdf_info=nc_info, mesh=mesh)
1390-
1391-
# write dataset to netcdf
1392-
fname = self.name_file.nc_filerecord.get_data()[0][0]
1393-
ds.to_netcdf(
1394-
os.path.join(self.model_ws, fname),
1395-
format="NETCDF4",
1396-
engine="netcdf4"
1397-
)
1398-
1377+
self._write_netcdf(mesh=netcdf)
13991378
if nc_fname is not None:
14001379
self.name_file.nc_filerecord = None
14011380

14021381

1403-
14041382
def get_grid_type(self):
14051383
"""
14061384
Return the type of grid used by model 'model_name' in simulation
@@ -2316,12 +2294,142 @@ def update_dataset(self, dataset, netcdf_info=None, mesh=None, update_data=True)
23162294
else:
23172295
nc_info = netcdf_info
23182296

2297+
if (
2298+
self.simulation.simulation_data.verbosity_level.value
2299+
>= VerbosityLevel.normal.value
2300+
):
2301+
print(f" updating model dataset...")
2302+
23192303
for a in nc_info["attrs"]:
23202304
dataset.attrs[a] = nc_info["attrs"][a]
23212305

23222306
# add all packages and update data
23232307
for p in self.packagelist:
23242308
# add package var to dataset
2309+
if (
2310+
self.simulation.simulation_data.verbosity_level.value
2311+
>= VerbosityLevel.normal.value
2312+
):
2313+
print(f" updating dataset for package {p._get_pname()}...")
23252314
dataset = p.update_dataset(dataset, mesh=mesh, update_data=update_data)
23262315

23272316
return dataset
2317+
2318+
def _write_netcdf(self, mesh=None):
2319+
import datetime
2320+
2321+
from ..version import __version__
2322+
if mesh is not None and mesh.upper() == "STRUCTURED":
2323+
mesh = None
2324+
2325+
encode = {}
2326+
for pp in self.packagelist:
2327+
if pp.package_type == "ncf":
2328+
encode["shuffle"] = pp.shuffle.get_data()
2329+
encode["deflate"] = pp.deflate.get_data()
2330+
encode["chunk_time"] = pp.chunk_time.get_data()
2331+
encode["chunk_face"] = pp.chunk_face.get_data()
2332+
encode["chunk_x"] = pp.chunk_x.get_data()
2333+
encode["chunk_y"] = pp.chunk_y.get_data()
2334+
encode["chunk_z"] = pp.chunk_z.get_data()
2335+
wkt = pp.wkt.get_data()
2336+
if wkt is not None:
2337+
wkt = wkt[0][1]
2338+
encode["wkt"] = wkt
2339+
2340+
if (
2341+
self.simulation.simulation_data.verbosity_level.value
2342+
>= VerbosityLevel.normal.value
2343+
):
2344+
print(f" creating model dataset...")
2345+
2346+
ds = self.modelgrid.dataset(
2347+
modeltime=self.modeltime,
2348+
mesh=mesh,
2349+
encoding=encode,
2350+
)
2351+
2352+
dt = datetime.datetime.now()
2353+
timestamp = dt.strftime("%m/%d/%Y %H:%M:%S")
2354+
2355+
nc_info = self.netcdf_info(mesh=mesh)
2356+
nc_info["attrs"]["title"] = f"{self.name.upper()} input"
2357+
nc_info["attrs"]["source"] = f"flopy {__version__}"
2358+
nc_info["attrs"]["history"] = f"first created {timestamp}"
2359+
if mesh is None:
2360+
nc_info["attrs"]["Conventions"] = "CF-1.11"
2361+
elif mesh.upper() is "LAYERED":
2362+
nc_info["attrs"]["Conventions"] = "CF-1.11 UGRID-1.0"
2363+
2364+
ds = self.update_dataset(
2365+
ds,
2366+
netcdf_info=nc_info,
2367+
mesh=mesh,
2368+
)
2369+
2370+
chunk = False
2371+
chunk_t = False
2372+
if mesh is None:
2373+
if (
2374+
"chunk_x" in encode
2375+
and encode["chunk_x"] is not None
2376+
and "chunk_y" in encode
2377+
and encode["chunk_y"] is not None
2378+
and "chunk_z" in encode
2379+
and encode["chunk_z"] is not None
2380+
):
2381+
chunk = True
2382+
elif mesh.upper() == "LAYERED":
2383+
if "chunk_face" in encode and encode["chunk_face"] is not None:
2384+
chunk = True
2385+
if "chunk_time" in encode and encode["chunk_time"] is not None:
2386+
chunk_t = True
2387+
2388+
base_encode = {}
2389+
if "deflate" in encode and encode["deflate"] is not None:
2390+
base_encode["zlib"] = True
2391+
base_encode["complevel"] = encode["deflate"]
2392+
if "shuffle" in encode and encode["deflate"] is not None:
2393+
base_encode["shuffle"] = True
2394+
2395+
encoding = {}
2396+
chunk_dims = {'time', 'nmesh_face', 'z', 'y', 'x'}
2397+
for varname, da in ds.data_vars.items():
2398+
dims = ds.data_vars[varname].dims
2399+
codes = dict(base_encode)
2400+
if (
2401+
not set(dims).issubset(chunk_dims)
2402+
or not chunk or not chunk_t
2403+
):
2404+
encoding[varname] = codes
2405+
continue
2406+
chunksizes = []
2407+
if "time" in dims:
2408+
chunksizes.append(encode["chunk_time"])
2409+
if mesh is None:
2410+
if "z" in dims:
2411+
chunksizes.append(encode["chunk_z"])
2412+
if "y" in dims:
2413+
chunksizes.append(encode["chunk_y"])
2414+
if "x" in dims:
2415+
chunksizes.append(encode["chunk_x"])
2416+
elif mesh.upper() == "LAYERED" and "nmesh_face" in dims:
2417+
chunksizes.append(encode["chunk_face"])
2418+
if len(chunksizes) > 0:
2419+
codes["chunksizes"] = chunksizes
2420+
encoding[varname] = codes
2421+
2422+
fname = self.name_file.nc_filerecord.get_data()[0][0]
2423+
2424+
if (
2425+
self.simulation.simulation_data.verbosity_level.value
2426+
>= VerbosityLevel.normal.value
2427+
):
2428+
print(f" writing NetCDF file {fname}...")
2429+
# write dataset to netcdf
2430+
ds.to_netcdf(
2431+
os.path.join(self.model_ws, fname),
2432+
format="NETCDF4",
2433+
engine="netcdf4",
2434+
encoding=encoding,
2435+
)

flopy/mf6/mfpackage.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3760,6 +3760,8 @@ def _data_shape(shape):
37603760

37613761
return dims_l
37623762

3763+
projection = "projection" in dataset.data_vars
3764+
37633765
last_path = ''
37643766
pitem = None
37653767
pdata = None
@@ -3774,6 +3776,15 @@ def _data_shape(shape):
37743776
dataset = dataset.assign(var_d)
37753777
for a in nc_info[v]["attrs"]:
37763778
dataset[varname].attrs[a] = nc_info[v]["attrs"][a]
3779+
if projection:
3780+
dims = dataset[varname].dims
3781+
if "nmesh_face" in dims or "nmesh_node" in dims:
3782+
dataset[varname].attrs["grid_mapping"] = "projection"
3783+
dataset[varname].attrs["coordinates"] = "mesh_face_x mesh_face_y"
3784+
elif mesh is None and len(dims) > 1:
3785+
# TODO don't set if lon / lat?
3786+
dataset[varname].attrs["grid_mapping"] = "projection"
3787+
dataset[varname].attrs["coordinates"] = "x y"
37773788

37783789
if update_data:
37793790
path = nc_info[v]["attrs"]["modflow_input"]

flopy/utils/datautil.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ def reset_delimiter_used():
305305

306306
@staticmethod
307307
def split_data_line(line, external_file=False, delimiter_conf_length=15):
308+
no_split_keys = ['crs', 'wkt']
308309
if PyListUtil.line_num > delimiter_conf_length and PyListUtil.consistent_delim:
309310
# consistent delimiter has been found. continue using that
310311
# delimiter without doing further checks
@@ -358,7 +359,11 @@ def split_data_line(line, external_file=False, delimiter_conf_length=15):
358359
max_split_size = len(max_split_list)
359360
max_split_type = "combo"
360361

361-
if max_split_type is not None and max_split_size > 1:
362+
if (
363+
max_split_type is not None
364+
and max_split_size > 1
365+
and clean_line[0].lower() not in no_split_keys
366+
):
362367
clean_line = max_split_list
363368
if PyListUtil.line_num == 0:
364369
PyListUtil.delimiter_used = max_split_type

0 commit comments

Comments
 (0)