From 1bfce6110295f7e13a5e84576870c399c3de5839 Mon Sep 17 00:00:00 2001 From: bekozi Date: Mon, 23 Mar 2020 10:27:40 -0500 Subject: [PATCH 01/12] WIP: Test failing in ESMPy with missing "srcFile" argument --- src/ocgis/messages.py | 1 + src/ocgis/ocli.py | 4 +++- src/ocgis/regrid/base.py | 4 ++++ src/ocgis/spatial/grid_chunker.py | 1 + src/ocgis/test/base.py | 3 +++ src/ocgis/test/test_ocgis/test_ocli.py | 1 + .../test_spatial/test_grid_chunker.py | 17 ++++++++++++++--- 7 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/ocgis/messages.py b/src/ocgis/messages.py index 9a36b31f6..580ba1825 100644 --- a/src/ocgis/messages.py +++ b/src/ocgis/messages.py @@ -1,3 +1,4 @@ M1 = 'No dimensioned variables found. This typically means no target variables in the dataset have space and/or time dimensions associated with them. Consider using a dimension map if file metadata is non-standard. Overloading "variable" is also an option to avoid dimension checking.' M3 = 'Output path exists "{0}" and must be removed before proceeding. Set "overwrite" argument or env.OVERWRITE to True to overwrite.' M4 = """A level subset was requested but the target dataset does not have a level dimension. The dataset's alias is: {0}""" +M5 = "The ESMF FileMode constant value. BASIC (the default) only writes the factor index list and weight factor variables. WITHAUX adds auxiliary variables and additional file metadata to the output weight file." \ No newline at end of file diff --git a/src/ocgis/ocli.py b/src/ocgis/ocli.py index 18dc9ec35..28d726c31 100644 --- a/src/ocgis/ocli.py +++ b/src/ocgis/ocli.py @@ -13,6 +13,7 @@ from ocgis.base import grid_abstraction_scope, raise_if_empty from ocgis.constants import DriverKey, Topology, GridChunkerConstants, DecompositionType from ocgis.driver.nc_ugrid import DriverNetcdfUGRID +from ocgis.messages import M5 from ocgis.spatial.grid_chunker import GridChunker from ocgis.spatial.spatial_subset import SpatialSubsetOperation from ocgis.util.logging_ocgis import ocgis_lh @@ -91,9 +92,10 @@ def ocli(): @click.option('--verbose/--not_verbose', default=False, help='If True, log to standard out using verbosity level.') @click.option('--loglvl', default="INFO", help='Verbosity level for standard out logging. Default is ' '"INFO". See Python logging level docs for additional values: https://docs.python.org/3/howto/logging.html') +@click.option('--weightfilemode', default="BASIC", help=M5) def chunked_rwg(source, destination, weight, nchunks_dst, merge, esmf_src_type, esmf_dst_type, genweights, esmf_regrid_method, spatial_subset, src_resolution, dst_resolution, buffer_distance, wd, persist, - eager, ignore_degenerate, data_variables, spatial_subset_path, verbose, loglvl): + eager, ignore_degenerate, data_variables, spatial_subset_path, verbose, loglvl, weightfilemode): if verbose: ocgis_lh.configure(to_stream=True, level=getattr(logging, loglvl)) ocgis_lh(msg="Starting Chunked Regrid Weight Generation", level=logging.INFO, logger=CRWG_LOG) diff --git a/src/ocgis/regrid/base.py b/src/ocgis/regrid/base.py index 4b7be56fd..18a23e1e7 100644 --- a/src/ocgis/regrid/base.py +++ b/src/ocgis/regrid/base.py @@ -869,6 +869,10 @@ def update_esmf_kwargs(target): except KeyError: raise ValueError('Chunked regridding does not support "{}".'.format(unmapped_action)) + if 'filemode' not in target: + target['filemode'] = "BASIC" + target['filemode'] = getattr(ESMF.FileMode, target['filemode']) + # Never create a route handle for weight generation only. target['create_rh'] = False diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index 1b91889ba..814958da4 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -88,6 +88,7 @@ class GridChunker(AbstractOcgisObject): ``'regrid_method'`` ``'CONSERVE'`` ``'CONSERVE'``, ``'BILINEAR'``, ``'PATCH'``, ``'NEAREST_STOD'`` ``'unmapped_action'`` ``'IGNORE'`` ``'IGNORE'``, ``'ERROR'`` ``'ignore_degenerate'`` ``False`` ``True``/``False`` + ``'filemode'`` ``'BASIC'`` ``'BASIC'``, ``'WITHAUX'`` ======================= ============== =============================================================== :param bool use_spatial_decomp: If ``True``, use a spatial decomposition as opposed to an index-based decomposition diff --git a/src/ocgis/test/base.py b/src/ocgis/test/base.py index e245c9f3c..9d5b3fd85 100644 --- a/src/ocgis/test/base.py +++ b/src/ocgis/test/base.py @@ -428,6 +428,9 @@ def assertWeightFilesEquivalent(self, src_filename, dst_filename): nwf = RequestDataset(dst_filename).get() gwf = RequestDataset(src_filename).get() + + self.assertEqual(nwf.keys(), gwf.keys()) + nwf_row = nwf['row'].get_value() gwf_row = gwf['row'].get_value() self.assertAsSetEqual(nwf_row, gwf_row) diff --git a/src/ocgis/test/test_ocgis/test_ocli.py b/src/ocgis/test/test_ocgis/test_ocli.py index 0fe2f0d07..be15a9f9d 100644 --- a/src/ocgis/test/test_ocgis/test_ocli.py +++ b/src/ocgis/test/test_ocgis/test_ocli.py @@ -45,6 +45,7 @@ def fixture_flags_good(self): poss.spatial_subset = ['__exclude__', '__include__'] poss.not_eager = ['__exclude__', '__include__'] poss.ignore_degenerate = ['__exclude__', '__include__'] + poss.weightfilemode = ['__exclude__', 'basic', 'withaux'] return poss diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 0eb2e32d4..e0e52ecb9 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -313,10 +313,13 @@ def test_system_splitting_unstructured_with_weights(self): self.run_system_splitting_unstructured(True) # subprocess.check_call(['tree', self.current_dir_output]) - @attr('esmf') - def test_create_merged_weight_file(self): + def run_create_merged_weight_file(self, filemode): import ESMF + if filemode is None: + filemode = "BASIC" + esmf_filemode = getattr(ESMF.FileMode, filemode) + path_src = self.get_temporary_file_path('src.nc') path_dst = self.get_temporary_file_path('dst.nc') @@ -328,6 +331,7 @@ def test_create_merged_weight_file(self): # Split source and destination grids --------------------------------------------------------------------------- + esmf_kwargs = {"filemode": filemode} gs = GridChunker(src_grid, dst_grid, (2, 2), check_contains=False, allow_masked=True, paths=self.fixture_paths, genweights=True) gs.write_chunks() @@ -346,12 +350,19 @@ def test_create_merged_weight_file(self): srcfield = ESMF.Field(grid=srcgrid) dstfield = ESMF.Field(grid=dstgrid) _ = ESMF.Regrid(srcfield=srcfield, dstfield=dstfield, filename=global_weights_filename, - regrid_method=ESMF.RegridMethod.CONSERVE) + regrid_method=ESMF.RegridMethod.CONSERVE, filemode=esmf_filemode) # Test merged and global weight files are equivalent ----------------------------------------------------------- self.assertWeightFilesEquivalent(global_weights_filename, merged_weight_filename) + @attr('esmf') + def test_create_merged_weight_file(self): + for filemode in [None, "WITHAUX"]: + self.run_create_merged_weight_file(filemode) + self.tearDown() + os.mkdir(self.current_dir_output) + @attr('esmf') def test_create_merged_weight_file_unstructured(self): import ESMF From 09c6738e7d9f3da600757b4c18ad9b1707ac3593 Mon Sep 17 00:00:00 2001 From: bekozi Date: Mon, 23 Mar 2020 10:55:20 -0500 Subject: [PATCH 02/12] WIP: Test now fails in ocgis since additional weight file variables not merged --- src/ocgis/test/base.py | 4 ++-- src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ocgis/test/base.py b/src/ocgis/test/base.py index 9d5b3fd85..fcb8a61ea 100644 --- a/src/ocgis/test/base.py +++ b/src/ocgis/test/base.py @@ -426,8 +426,8 @@ def assertWarns(self, warning, meth): def assertWeightFilesEquivalent(self, src_filename, dst_filename): """Assert weight files are equivalent.""" - nwf = RequestDataset(dst_filename).get() - gwf = RequestDataset(src_filename).get() + nwf = RequestDataset(dst_filename, driver="netcdf").get() + gwf = RequestDataset(src_filename, driver="netcdf").get() self.assertEqual(nwf.keys(), gwf.keys()) diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index e0e52ecb9..98989b790 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -350,7 +350,9 @@ def run_create_merged_weight_file(self, filemode): srcfield = ESMF.Field(grid=srcgrid) dstfield = ESMF.Field(grid=dstgrid) _ = ESMF.Regrid(srcfield=srcfield, dstfield=dstfield, filename=global_weights_filename, - regrid_method=ESMF.RegridMethod.CONSERVE, filemode=esmf_filemode) + regrid_method=ESMF.RegridMethod.CONSERVE, filemode=esmf_filemode, src_file=path_src, + dst_file=path_dst, src_file_type=ESMF.FileFormat.GRIDSPEC, + dst_file_type=ESMF.FileFormat.GRIDSPEC) # Test merged and global weight files are equivalent ----------------------------------------------------------- From a3801863a77d1c90f918f31dc9583c8d8d73eebe Mon Sep 17 00:00:00 2001 From: bekozi Date: Mon, 23 Mar 2020 11:30:48 -0500 Subject: [PATCH 03/12] WIP: Chunked weight files have auxiliary variables --- src/ocgis/spatial/grid_chunker.py | 8 ++++++++ .../test_ocgis/test_spatial/test_grid_chunker.py | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index 814958da4..7691ac3d2 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -808,6 +808,8 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr assert dst_path is not None from ocgis.regrid.base import create_esmf_field, create_esmf_regrid + import ESMF + if src_grid is None: src_grid = self.src_grid if dst_grid is None: @@ -819,6 +821,12 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr dstfield, dstgrid = create_esmf_field(dst_path, dst_grid, self.esmf_kwargs) regrid = None + # If auxiliary weight file variables are being written, update the ESMF arguments with some additional metadata. + if self.esmf_kwargs.get('filemode', None) == ESMF.FileMode.WITHAUX: + self.esmf_kwargs['src_file'] = src_path + self.esmf_kwargs['dst_file'] = dst_path + self.esmf_kwargs['src_file_type'] = self.src_grid.driver.get_esmf_fileformat() + self.esmf_kwargs['dst_file_type'] = self.dst_grid.driver.get_esmf_fileformat() try: ocgis_lh(msg="creating esmf regrid...", logger=_LOCAL_LOGGER, level=logging.DEBUG) regrid = create_esmf_regrid(srcfield=srcfield, dstfield=dstfield, filename=wgt_path, **self.esmf_kwargs) diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 98989b790..a9d031e72 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -333,12 +333,20 @@ def run_create_merged_weight_file(self, filemode): esmf_kwargs = {"filemode": filemode} gs = GridChunker(src_grid, dst_grid, (2, 2), check_contains=False, allow_masked=True, paths=self.fixture_paths, - genweights=True) + genweights=True, esmf_kwargs=esmf_kwargs) gs.write_chunks() + if filemode == "WITHAUX": + weightfile = self.get_temporary_file_path('esmf_weights_1.nc') + rd = RequestDataset(weightfile, driver='netcdf') + field = rd.create_field() + self.assertGreater(len(field.keys()), 3) + # Merge weight files ------------------------------------------------------------------------------------------- merged_weight_filename = self.get_temporary_file_path('merged_weights.nc') + if filemode == "WITHAUX": #tdk:rm + print(self.current_dir_output) #tdk:rm gs.create_merged_weight_file(merged_weight_filename) # Generate a global weight file using ESMF --------------------------------------------------------------------- @@ -360,7 +368,9 @@ def run_create_merged_weight_file(self, filemode): @attr('esmf') def test_create_merged_weight_file(self): - for filemode in [None, "WITHAUX"]: + # poss = [None, "WITHAUX"] #tdk:uncomm + poss = ["WITHAUX"] + for filemode in poss: self.run_create_merged_weight_file(filemode) self.tearDown() os.mkdir(self.current_dir_output) From 19798c9bb7871583e323e06f3b14bb4609e53aa0 Mon Sep 17 00:00:00 2001 From: bekozi Date: Mon, 23 Mar 2020 11:37:23 -0500 Subject: [PATCH 04/12] WIP: Chunked weight files have auxiliary variables --- src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index a9d031e72..0a568b22a 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -345,8 +345,6 @@ def run_create_merged_weight_file(self, filemode): # Merge weight files ------------------------------------------------------------------------------------------- merged_weight_filename = self.get_temporary_file_path('merged_weights.nc') - if filemode == "WITHAUX": #tdk:rm - print(self.current_dir_output) #tdk:rm gs.create_merged_weight_file(merged_weight_filename) # Generate a global weight file using ESMF --------------------------------------------------------------------- From 9fa6a9a7dfa5364430457f2f6d3e8535c4de7d27 Mon Sep 17 00:00:00 2001 From: bekozi Date: Mon, 23 Mar 2020 11:41:50 -0500 Subject: [PATCH 05/12] WIP: Chunked weight files have auxiliary variables --- src/ocgis/spatial/grid_chunker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index 7691ac3d2..1616ba5b3 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -353,7 +353,7 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): raise IOError(wfn) else: continue - wdata = RequestDataset(wfn).get() + wdata = RequestDataset(wfn, driver='netcdf').get() for wvn in wf_varnames: odata = wdata[wvn].get_value() try: From ab5e99cf0c733a66097dfd5f1b9a4c3b6b9e941a Mon Sep 17 00:00:00 2001 From: bekozi Date: Mon, 23 Mar 2020 16:15:43 -0500 Subject: [PATCH 06/12] WIP: Before modifying merged weight file --- src/ocgis/regrid/base.py | 4 -- src/ocgis/spatial/grid_chunker.py | 37 ++++++++++++++----- .../test_spatial/test_grid_chunker.py | 5 +-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/ocgis/regrid/base.py b/src/ocgis/regrid/base.py index 18a23e1e7..4b7be56fd 100644 --- a/src/ocgis/regrid/base.py +++ b/src/ocgis/regrid/base.py @@ -869,10 +869,6 @@ def update_esmf_kwargs(target): except KeyError: raise ValueError('Chunked regridding does not support "{}".'.format(unmapped_action)) - if 'filemode' not in target: - target['filemode'] = "BASIC" - target['filemode'] = getattr(ESMF.FileMode, target['filemode']) - # Never create a route handle for weight generation only. target['create_rh'] = False diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index 1616ba5b3..7c047df58 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -23,6 +23,7 @@ _LOCAL_LOGGER = "grid_chunker" + class GridChunker(AbstractOcgisObject): """ Splits source and destination grids into separate netCDF files. "Source" is intended to mean the source data for a @@ -306,7 +307,7 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): raise ValueError("'create_merged_weight_file' does not work in parallel") index_filename = self.create_full_path_from_template('index_file') - ifile = RequestDataset(uri=index_filename).get() + ifile = RequestDataset(uri=index_filename, driver='netcdf').get() ifile.load() ifc = GridChunkerConstants.IndexFile gidx = ifile[ifc.NAME_INDEX_VARIABLE].attrs @@ -326,7 +327,7 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): raise IOError(wfn) else: continue - curr_dimsize = RequestDataset(wfn).get().dimensions['n_s'].size + curr_dimsize = RequestDataset(wfn, driver='netcdf').get().dimensions['n_s'].size # ESMF writes the weight file, but it may be empty if there are no generated weights. if curr_dimsize is not None: n_s_size += curr_dimsize @@ -339,7 +340,7 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): for w, wd in zip(wf_varnames, wf_dtypes): var = Variable(name=w, dimensions=dim, dtype=wd) vc.add_variable(var) - vc.write(merged_weight_filename) + vc.write(merged_weight_filename, opened=None) # Transfer weights to the merged file. sidx = 0 @@ -731,7 +732,7 @@ def write_chunks(self): vm.barrier() ocgis_lh(logger=_LOCAL_LOGGER, msg='write_chunks:writing esmf weights: {}'.format(wgt_path), level=logging.DEBUG) - self.write_esmf_weights(src_path, dst_path, wgt_path, src_grid=sub_src, dst_grid=sub_dst) + self.write_esmf_weights(src_path, dst_path, wgt_path, src_grid=sub_src, dst_grid=sub_dst, filemode=None) vm.barrier() # Global shapes require a VM global scope to collect. @@ -791,7 +792,7 @@ def write_chunks(self): vm.barrier() - def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_grid=None): + def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_grid=None, filemode=None): """ Write ESMF regridding weights for a source and destination filename combination. @@ -803,6 +804,8 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr :param dst_grid: If provided, use this destination grid for identifying ESMF parameters :type dst_grid: :class:`ocgis.spatial.grid.AbstractGrid` """ + #tdk:doc: filemode + #tdk:doc: remove filemode from GridChunker.__init__ documentation in esmf_kwargs assert wgt_path is not None assert src_path is not None assert dst_path is not None @@ -810,6 +813,12 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr from ocgis.regrid.base import create_esmf_field, create_esmf_regrid import ESMF + if filemode is None: + filemode = "BASIC" + else: + filemode = "WITHAUX" + filemode = getattr(ESMF.FileMode, filemode) + if src_grid is None: src_grid = self.src_grid if dst_grid is None: @@ -822,14 +831,18 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr regrid = None # If auxiliary weight file variables are being written, update the ESMF arguments with some additional metadata. - if self.esmf_kwargs.get('filemode', None) == ESMF.FileMode.WITHAUX: - self.esmf_kwargs['src_file'] = src_path - self.esmf_kwargs['dst_file'] = dst_path + if filemode == ESMF.FileMode.WITHAUX: + try: + self.esmf_kwargs['src_file'] = get_file_path(self._src_grid) + self.esmf_kwargs['dst_file'] = get_file_path(self._dst_grid) + except Exception as e: + vm.abort(exc=e) self.esmf_kwargs['src_file_type'] = self.src_grid.driver.get_esmf_fileformat() self.esmf_kwargs['dst_file_type'] = self.dst_grid.driver.get_esmf_fileformat() try: ocgis_lh(msg="creating esmf regrid...", logger=_LOCAL_LOGGER, level=logging.DEBUG) - regrid = create_esmf_regrid(srcfield=srcfield, dstfield=dstfield, filename=wgt_path, **self.esmf_kwargs) + regrid = create_esmf_regrid(srcfield=srcfield, dstfield=dstfield, filename=wgt_path, filemode=filemode, + **self.esmf_kwargs) finally: to_destroy = [regrid, srcgrid, srcfield, dstgrid, dstfield] for t in to_destroy: @@ -904,6 +917,12 @@ def get_grid_object(obj, load=True): return res +def get_file_path(rd): + if not isinstance(rd, RequestDataset): + raise ValueError('must be a RequestDataset') + return rd.uri + + def global_grid_shape(grid): with vm.scoped_by_emptyable('global grid shape', grid): if not vm.is_null: diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 0a568b22a..77cfa3d7b 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -331,16 +331,15 @@ def run_create_merged_weight_file(self, filemode): # Split source and destination grids --------------------------------------------------------------------------- - esmf_kwargs = {"filemode": filemode} gs = GridChunker(src_grid, dst_grid, (2, 2), check_contains=False, allow_masked=True, paths=self.fixture_paths, - genweights=True, esmf_kwargs=esmf_kwargs) + genweights=True) gs.write_chunks() if filemode == "WITHAUX": weightfile = self.get_temporary_file_path('esmf_weights_1.nc') rd = RequestDataset(weightfile, driver='netcdf') field = rd.create_field() - self.assertGreater(len(field.keys()), 3) + self.assertEqual(len(field.keys()), 3) # Merge weight files ------------------------------------------------------------------------------------------- From 7e79370bc05488a884bdda76511403b2463c33ce Mon Sep 17 00:00:00 2001 From: bekozi Date: Tue, 24 Mar 2020 09:38:17 -0500 Subject: [PATCH 07/12] WIP: Grid chunker tests passing --- src/ocgis/ocli.py | 2 +- src/ocgis/spatial/grid_chunker.py | 48 +++++++++++++------ src/ocgis/test/base.py | 2 + .../test_spatial/test_grid_chunker.py | 24 ++++++---- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/ocgis/ocli.py b/src/ocgis/ocli.py index 28d726c31..a29a92c07 100644 --- a/src/ocgis/ocli.py +++ b/src/ocgis/ocli.py @@ -187,7 +187,7 @@ def chunked_rwg(source, destination, weight, nchunks_dst, merge, esmf_src_type, msg = "Writing ESMF weights..." ocgis_lh(msg=msg, level=logging.INFO, logger=CRWG_LOG) handle_weight_file_check(weight) - gs.write_esmf_weights(source, destination, weight) + gs.write_esmf_weights(source, destination, weight, filemode=weightfilemode) # Create the global weight file. This does not apply to spatial subsets because there will always be one weight # file. diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index 7c047df58..b7709d1a7 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -6,7 +6,7 @@ from shapely.geometry import box from ocgis import constants -from ocgis.base import AbstractOcgisObject, grid_abstraction_scope +from ocgis.base import AbstractOcgisObject, grid_abstraction_scope, orphaned from ocgis.collection.field import Field from ocgis.constants import GridChunkerConstants, RegriddingRole, Topology from ocgis.driver.request.core import RequestDataset @@ -100,11 +100,12 @@ class GridChunker(AbstractOcgisObject): memory used. :raises: ValueError """ + #tdk:doc: filemode def __init__(self, source, destination, nchunks_dst=None, paths=None, check_contains=False, allow_masked=True, src_grid_resolution=None, dst_grid_resolution=None, optimized_bbox_subset='auto', iter_dst=None, buffer_value=None, redistribute=False, genweights=False, esmf_kwargs=None, use_spatial_decomp='auto', - eager=True, debug=False): + eager=True, filemode="BASIC", debug=False): self._src_grid = None self._dst_grid = None self._buffer_value = None @@ -116,6 +117,7 @@ def __init__(self, source, destination, nchunks_dst=None, paths=None, check_cont self.source = source self.destination = destination self.eager = eager + self.filemode = filemode self.debug = debug if esmf_kwargs is None: @@ -315,11 +317,15 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): src_global_shape = gidx[ifc.NAME_SRC_GRID_SHAPE] dst_global_shape = gidx[ifc.NAME_DST_GRID_SHAPE] + vc = VariableCollection() + wf_varnames = ['row', 'col', 'S'] + # Get the global weight dimension size. n_s_size = 0 weight_filename = ifile[gidx[ifc.NAME_WEIGHTS_VARIABLE]] wv = weight_filename.join_string_value() split_weight_file_directory = self.paths['wd'] + ctr = 0 for wfn in map(lambda x: os.path.join(split_weight_file_directory, os.path.split(x)[1]), wv): ocgis_lh(msg="current merge weight file target: {}".format(wfn), level=logging.DEBUG, logger=_LOCAL_LOGGER) if not os.path.exists(wfn): @@ -327,20 +333,31 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): raise IOError(wfn) else: continue - curr_dimsize = RequestDataset(wfn, driver='netcdf').get().dimensions['n_s'].size + vc_target = RequestDataset(wfn, driver='netcdf').get() + curr_dimsize = vc_target.dimensions['n_s'].size # ESMF writes the weight file, but it may be empty if there are no generated weights. if curr_dimsize is not None: n_s_size += curr_dimsize + # Copy over auxiliary variables if they are required. + if self.filemode == 'WITHAUX' and ctr == 0: + for var in vc_target.values(): + if var.name not in wf_varnames: + var.load() + with orphaned(var, keep_dimensions=True): + vc.add_variable(var) + # Also copy over global attributes + vc.attrs = vc_target.attrs + ctr += 1 + # Create output weight file. - wf_varnames = ['row', 'col', 'S'] wf_dtypes = [np.int32, np.int32, np.float64] - vc = VariableCollection() dim = Dimension('n_s', n_s_size) for w, wd in zip(wf_varnames, wf_dtypes): var = Variable(name=w, dimensions=dim, dtype=wd) vc.add_variable(var) - vc.write(merged_weight_filename, opened=None) + + vc.write(merged_weight_filename) # Transfer weights to the merged file. sidx = 0 @@ -724,17 +741,22 @@ def write_chunks(self): level=logging.DEBUG) cc += 1 - # Increment the counter outside of the loop to avoid counting empty subsets. - ctr += 1 - # Generate an ESMF weights file if requested and at least one rank has data on it. if self.genweights and len(vm.get_live_ranks_from_object(sub_src)) > 0: vm.barrier() + if (ctr == 1) and (self.filemode == 'WITHAUX'): + filemode = 'WITHAUX' + else: + filemode = 'BASIC' ocgis_lh(logger=_LOCAL_LOGGER, msg='write_chunks:writing esmf weights: {}'.format(wgt_path), level=logging.DEBUG) - self.write_esmf_weights(src_path, dst_path, wgt_path, src_grid=sub_src, dst_grid=sub_dst, filemode=None) + self.write_esmf_weights(src_path, dst_path, wgt_path, src_grid=sub_src, dst_grid=sub_dst, + filemode=filemode) vm.barrier() + # Increment the counter outside of the loop to avoid counting empty subsets. + ctr += 1 + # Global shapes require a VM global scope to collect. src_global_shape = global_grid_shape(self.src_grid) dst_global_shape = global_grid_shape(self.dst_grid) @@ -815,8 +837,6 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr if filemode is None: filemode = "BASIC" - else: - filemode = "WITHAUX" filemode = getattr(ESMF.FileMode, filemode) if src_grid is None: @@ -833,8 +853,8 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr # If auxiliary weight file variables are being written, update the ESMF arguments with some additional metadata. if filemode == ESMF.FileMode.WITHAUX: try: - self.esmf_kwargs['src_file'] = get_file_path(self._src_grid) - self.esmf_kwargs['dst_file'] = get_file_path(self._dst_grid) + self.esmf_kwargs['src_file'] = get_file_path(self.source) + self.esmf_kwargs['dst_file'] = get_file_path(self.destination) except Exception as e: vm.abort(exc=e) self.esmf_kwargs['src_file_type'] = self.src_grid.driver.get_esmf_fileformat() diff --git a/src/ocgis/test/base.py b/src/ocgis/test/base.py index fcb8a61ea..bc45fbd71 100644 --- a/src/ocgis/test/base.py +++ b/src/ocgis/test/base.py @@ -458,6 +458,8 @@ def assertWeightFilesEquivalent(self, src_filename, dst_filename): diffs = np.abs(diffs) self.assertLess(diffs.max(), 1e-14) + self.assertEqual(nwf.attrs, gwf.attrs) + @staticmethod def barrier_print(*args, **kwargs): from ocgis.vmachine.mpi import barrier_print diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 77cfa3d7b..82488776f 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -316,8 +316,6 @@ def test_system_splitting_unstructured_with_weights(self): def run_create_merged_weight_file(self, filemode): import ESMF - if filemode is None: - filemode = "BASIC" esmf_filemode = getattr(ESMF.FileMode, filemode) path_src = self.get_temporary_file_path('src.nc') @@ -331,20 +329,29 @@ def run_create_merged_weight_file(self, filemode): # Split source and destination grids --------------------------------------------------------------------------- - gs = GridChunker(src_grid, dst_grid, (2, 2), check_contains=False, allow_masked=True, paths=self.fixture_paths, - genweights=True) + src_rd = RequestDataset(path_src, driver='netcdf-cf') + dst_rd = RequestDataset(path_dst, driver='netcdf-cf') + gs = GridChunker(src_rd, dst_rd, (2, 2), check_contains=False, allow_masked=True, paths=self.fixture_paths, + genweights=True, filemode=filemode) gs.write_chunks() if filemode == "WITHAUX": weightfile = self.get_temporary_file_path('esmf_weights_1.nc') - rd = RequestDataset(weightfile, driver='netcdf') - field = rd.create_field() - self.assertEqual(len(field.keys()), 3) + vc = RequestDataset(weightfile, driver='netcdf').create_field() + self.assertGreater(len(vc.keys()), 3) + weightfile = self.get_temporary_file_path('esmf_weights_2.nc') + vc = RequestDataset(weightfile, driver='netcdf').get() + self.assertEqual(len(vc.keys()), 3) # Merge weight files ------------------------------------------------------------------------------------------- merged_weight_filename = self.get_temporary_file_path('merged_weights.nc') gs.create_merged_weight_file(merged_weight_filename) + nvars = len(RequestDataset(merged_weight_filename, driver='netcdf').get().keys()) + if filemode == "WITHAUX": + self.assertGreater(nvars, 3) + else: + self.assertEqual(nvars, 3) # Generate a global weight file using ESMF --------------------------------------------------------------------- @@ -365,8 +372,7 @@ def run_create_merged_weight_file(self, filemode): @attr('esmf') def test_create_merged_weight_file(self): - # poss = [None, "WITHAUX"] #tdk:uncomm - poss = ["WITHAUX"] + poss = ["BASIC", "WITHAUX"] for filemode in poss: self.run_create_merged_weight_file(filemode) self.tearDown() From 21e8ede3e915817d8dfc44983a88dc70c1690dfa Mon Sep 17 00:00:00 2001 From: bekozi Date: Tue, 24 Mar 2020 10:13:04 -0500 Subject: [PATCH 08/12] WIP: Tests passing --- .../test/test_ocgis/test_driver/test_nc_esmf_unstruct.py | 4 ++-- src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py b/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py index b291550c9..25b2e3507 100644 --- a/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py +++ b/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py @@ -216,7 +216,8 @@ def test_system_spatial_subsetting(self): @attr('mpi', 'esmf') def test_system_grid_chunking(self): - if vm.size != 4: raise SkipTest('vm.size != 4') + if vm.size != 4: + raise SkipTest('vm.size != 4') from ocgis.spatial.grid_chunker import GridChunker path = self.path_esmf_unstruct @@ -247,7 +248,6 @@ def test_system_grid_chunking(self): d = os.path.join(chunk_wd, 'split_dst_{}.nc'.format(ctr)) sf = Field.read(s, driver=DriverESMFUnstruct) df = Field.read(d, driver=DriverESMFUnstruct) - self.assertLessEqual(sf.grid.shape[0] - df.grid.shape[0], 150) self.assertGreater(sf.grid.shape[0], df.grid.shape[0]) wgt = os.path.join(chunk_wd, 'esmf_weights_{}.nc'.format(ctr)) diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 82488776f..458946588 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -244,6 +244,7 @@ def _create_mRequestDataset_(): @attr('esmf', 'slow') def test_system_negative_values_in_spherical_grid(self): + original_dir = os.getcwd() xcn = np.arange(-10, 350, step=10, dtype=float) xc = np.arange(0, 360, step=10, dtype=float) yc = np.arange(-90, 100, step=10, dtype=float) @@ -253,7 +254,6 @@ def test_system_negative_values_in_spherical_grid(self): yv = Variable("lat", yc, dimensions=["lat"]) gridn = Grid(x=xvn.copy(), y=yv.copy(), crs=Spherical()) - print(gridn.x.v()) gridu = Grid(x=xv.copy(), y=yv.copy(), crs=Spherical()) gridw = create_gridxy_global(5, with_bounds=False, crs=Spherical()) grids = [gridn, gridu, gridw] @@ -294,6 +294,8 @@ def test_system_negative_values_in_spherical_grid(self): dst_filename = os.path.join(griddir, "global-weights.nc") self.assertWeightFilesEquivalent(src_filename, dst_filename) + os.chdir(original_dir) + def test_system_scrip_destination_splitting(self): """Test splitting a SCRIP destination grid.""" From c253213e22c0b48e86941ee746bd57a32a53471b Mon Sep 17 00:00:00 2001 From: bekozi Date: Tue, 24 Mar 2020 11:02:27 -0500 Subject: [PATCH 09/12] WIP: Minor --- src/ocgis/driver/nc.py | 3 ++- src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py | 2 +- src/ocgis/test/test_simple/test_simple.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ocgis/driver/nc.py b/src/ocgis/driver/nc.py index e3f1df9f1..ddceca198 100644 --- a/src/ocgis/driver/nc.py +++ b/src/ocgis/driver/nc.py @@ -8,6 +8,7 @@ import numpy as np import six from netCDF4._netCDF4 import VLType, MFDataset, MFTime + from ocgis import constants, vm from ocgis import env from ocgis.base import orphaned, raise_if_empty @@ -262,7 +263,7 @@ def _open_(uri, mode='r', **kwargs): if kwargs.get('parallel') and kwargs.get('comm') is None: kwargs['comm'] = lvm.comm ret = nc.Dataset(uri, mode=mode, **kwargs) - # tdk:FIX: this should be enabled for MFDataset as well. see https://github.com/Unidata/netcdf4-python/issues/809#issuecomment-435144221 + # tdk:RELEASE:FIX: this should be enabled for MFDataset as well. see https://github.com/Unidata/netcdf4-python/issues/809#issuecomment-435144221 # netcdf4 >= 1.4.0 always returns masked arrays. This is inefficient and is turned off by default by ocgis. if hasattr(ret, 'set_always_mask'): ret.set_always_mask(False) diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 458946588..61917d267 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -457,7 +457,7 @@ def test_nchunks_dst(self): @attr('esmf', 'mpi') def test_write_esmf_weights(self): - #tdk:FIX: before release. masking is source of the problem for the smm assert failures + #tdk:RELEASE:FIX: before release. masking is source of the problem for the smm assert failures raise(SkipTest) # Create source and destination fields. This is the identity test, so the source and destination fields are # equivalent. diff --git a/src/ocgis/test/test_simple/test_simple.py b/src/ocgis/test/test_simple/test_simple.py index d9a736b08..c98a08ecf 100644 --- a/src/ocgis/test/test_simple/test_simple.py +++ b/src/ocgis/test/test_simple/test_simple.py @@ -188,7 +188,7 @@ def test_meta_attrs_eval_function(self): def test_selection_geometry_crs_differs(self): """Test selection is appropriate when CRS of selection geometry differs from source.""" - #tdk:FIX: get test working for release. fails with a newer version of ogr on travis ci + #tdk:RELEASE:FIX: get test working for release. fails with a newer version of ogr on travis ci raise(SkipTest) dataset = self.get_dataset() rd = RequestDataset(**dataset) From 77c5af851cd811d9e8f4a7b44a7a45ccca311037 Mon Sep 17 00:00:00 2001 From: bekozi Date: Tue, 24 Mar 2020 11:19:49 -0500 Subject: [PATCH 10/12] WIP: Added documentation --- src/ocgis/spatial/grid_chunker.py | 9 +++++---- .../test/test_ocgis/test_driver/test_nc_esmf_unstruct.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index b7709d1a7..c5487360f 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -89,7 +89,6 @@ class GridChunker(AbstractOcgisObject): ``'regrid_method'`` ``'CONSERVE'`` ``'CONSERVE'``, ``'BILINEAR'``, ``'PATCH'``, ``'NEAREST_STOD'`` ``'unmapped_action'`` ``'IGNORE'`` ``'IGNORE'``, ``'ERROR'`` ``'ignore_degenerate'`` ``False`` ``True``/``False`` - ``'filemode'`` ``'BASIC'`` ``'BASIC'``, ``'WITHAUX'`` ======================= ============== =============================================================== :param bool use_spatial_decomp: If ``True``, use a spatial decomposition as opposed to an index-based decomposition @@ -98,9 +97,10 @@ class GridChunker(AbstractOcgisObject): :param bool eager: If ``True``, load grid data from disk before chunking. This avoids always loading the data from disk for sourced datasets following a subset. There will be an improvement in performance but an increase in the memory used. + :param str filemode: If ``'BASIC'`` (the default), only write source/destination indices and weight factors to the + output weight file. If ``'WITHAUX'`` also write geometry-related auxiliary variables to the output weight file. :raises: ValueError """ - #tdk:doc: filemode def __init__(self, source, destination, nchunks_dst=None, paths=None, check_contains=False, allow_masked=True, src_grid_resolution=None, dst_grid_resolution=None, optimized_bbox_subset='auto', iter_dst=None, @@ -825,9 +825,10 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr :type src_grid: :class:`ocgis.spatial.grid.AbstractGrid` :param dst_grid: If provided, use this destination grid for identifying ESMF parameters :type dst_grid: :class:`ocgis.spatial.grid.AbstractGrid` + :param str filemode: If ``'BASIC'`` (default when ``None``), only write source/destination indices and weight + factors to the output weight file. If ``'WITHAUX'`` also write geometry-related auxiliary variables to the + output weight file. """ - #tdk:doc: filemode - #tdk:doc: remove filemode from GridChunker.__init__ documentation in esmf_kwargs assert wgt_path is not None assert src_path is not None assert dst_path is not None diff --git a/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py b/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py index 25b2e3507..eeac6ea66 100644 --- a/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py +++ b/src/ocgis/test/test_ocgis/test_driver/test_nc_esmf_unstruct.py @@ -76,7 +76,7 @@ def fixture_esmf_unstruct_field(self): def test_system_converting_state_boundaries_shapefile(self): verbose = False if verbose: ocgis.vm.barrier_print("starting test") - ocgis.env.USE_NETCDF4_MPI = False # tdk:FIX: this hangs in the STATE_FIPS write for asynch might be nc4 bug... + ocgis.env.USE_NETCDF4_MPI = False # tdk:RELEASE:FIX: this hangs in the STATE_FIPS write for asynch might be nc4 bug... keywords = {'transform_to_crs': [None, Spherical], 'use_geometry_iterator': [False, True]} actual_xsums = [] From c9aaac1b8cb204fe908c44af317b7fddcde6dd23 Mon Sep 17 00:00:00 2001 From: bekozi Date: Mon, 30 Mar 2020 09:43:49 -0500 Subject: [PATCH 11/12] WIP: Add user, hostname, and datetime created to weight file attrs --- src/ocgis/spatial/grid_chunker.py | 7 +++++++ src/ocgis/test/base.py | 19 +++++++++++++++++-- .../test_spatial/test_grid_chunker.py | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index c5487360f..8f45755e7 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -348,6 +348,13 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): vc.add_variable(var) # Also copy over global attributes vc.attrs = vc_target.attrs + # Add some additional stuff for record keeping + import getpass + import socket + import datetime + vc.attrs['created_by_user'] = getpass.getuser() + vc.attrs['created_on_hostname'] = socket.getfqdn() + vc.attrs['created_at_datetime'] = str(datetime.datetime.now()) ctr += 1 # Create output weight file. diff --git a/src/ocgis/test/base.py b/src/ocgis/test/base.py index bc45fbd71..0d8e4d22e 100644 --- a/src/ocgis/test/base.py +++ b/src/ocgis/test/base.py @@ -423,7 +423,7 @@ def assertWarns(self, warning, meth): meth() self.assertTrue(any(item.category == warning for item in warning_list)) - def assertWeightFilesEquivalent(self, src_filename, dst_filename): + def assertWeightFilesEquivalent(self, src_filename, dst_filename, remove_created=False): """Assert weight files are equivalent.""" nwf = RequestDataset(dst_filename, driver="netcdf").get() @@ -458,7 +458,22 @@ def assertWeightFilesEquivalent(self, src_filename, dst_filename): diffs = np.abs(diffs) self.assertLess(diffs.max(), 1e-14) - self.assertEqual(nwf.attrs, gwf.attrs) + if remove_created: + actual = nwf.attrs.copy() + desired = gwf.attrs.copy() + to_remove = [] + for k in actual.keys(): + if k.startswith('created_'): + to_remove.append(k) + for target in [actual, desired]: + for t in to_remove: + try: + target.pop(t) + except KeyError: + continue + self.assertEqual(actual, desired) + else: + self.assertEqual(nwf.attrs, gwf.attrs) @staticmethod def barrier_print(*args, **kwargs): diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 61917d267..8af43e170 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -370,7 +370,7 @@ def run_create_merged_weight_file(self, filemode): # Test merged and global weight files are equivalent ----------------------------------------------------------- - self.assertWeightFilesEquivalent(global_weights_filename, merged_weight_filename) + self.assertWeightFilesEquivalent(global_weights_filename, merged_weight_filename, remove_created=True) @attr('esmf') def test_create_merged_weight_file(self): From 70fc0eab6b1a970cbaf4c02abdb7de706b2899d6 Mon Sep 17 00:00:00 2001 From: bekozi Date: Wed, 8 Apr 2020 09:52:41 -0500 Subject: [PATCH 12/12] WIP: Add checks for 'filemode' when calling ESMF API for backwards compatibility --- src/ocgis/ocli.py | 42 ++++++++++++++++++- src/ocgis/spatial/grid_chunker.py | 17 ++++---- src/ocgis/test/base.py | 22 ++++------ src/ocgis/test/test_ocgis/test_ocli.py | 5 ++- .../test_spatial/test_grid_chunker.py | 14 +++---- 5 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/ocgis/ocli.py b/src/ocgis/ocli.py index a29a92c07..b8a6d7ece 100644 --- a/src/ocgis/ocli.py +++ b/src/ocgis/ocli.py @@ -1,11 +1,12 @@ #!/usr/bin/env python - +import datetime import logging import os import shutil import tempfile import click +import netCDF4 as nc from shapely.geometry import box import ocgis @@ -96,6 +97,10 @@ def ocli(): def chunked_rwg(source, destination, weight, nchunks_dst, merge, esmf_src_type, esmf_dst_type, genweights, esmf_regrid_method, spatial_subset, src_resolution, dst_resolution, buffer_distance, wd, persist, eager, ignore_degenerate, data_variables, spatial_subset_path, verbose, loglvl, weightfilemode): + + # Used for creating the history string. + the_locals = locals() + if verbose: ocgis_lh.configure(to_stream=True, level=getattr(logging, loglvl)) ocgis_lh(msg="Starting Chunked Regrid Weight Generation", level=logging.INFO, logger=CRWG_LOG) @@ -208,6 +213,20 @@ def chunked_rwg(source, destination, weight, nchunks_dst, merge, esmf_src_type, ocgis.vm.barrier() + # Append the history string if there is an output weight file. + if weight and ocgis.vm.rank == 0: + if os.path.exists(weight): + # Add some additional stuff for record keeping + import getpass + import socket + import datetime + + with nc.Dataset(weight, 'a') as ds: + ds.setncattr('created_by_user', getpass.getuser()) + ds.setncattr('created_on_hostname', socket.getfqdn()) + ds.setncattr('history', create_history_string(the_locals)) + ocgis.vm.barrier() + # Remove the working directory unless the persist flag is provided. if not persist: if ocgis.vm.rank == 0: @@ -220,6 +239,27 @@ def chunked_rwg(source, destination, weight, nchunks_dst, merge, esmf_src_type, return 0 +def create_history_string(the_locals): + history_parms = {} + for k, v in the_locals.items(): + if v is not None and k != 'history_parms': + if type(v) == bool: + if not v: + history_parms['--no_' + k] = v + else: + history_parms['--' + k] = v + try: + import ESMF + ever = ESMF.__version__ + except ImportError: + ever = None + history = "{}: Created by ocgis (v{}) and ESMF (v{}) with CLI command: ocli chunked-rwg".format( + datetime.datetime.now(), ocgis.__version__, ever) + for k, v in history_parms.items(): + history += " {} {}".format(k, v) + return history + + @ocli.command(help='Apply weights in chunked files with an option to insert the global data.', name='chunked-smm') @click.option('--wd', type=click.Path(exists=True, dir_okay=True), required=False, help="Optional working directory containing destination chunk files. If empty, the current working " diff --git a/src/ocgis/spatial/grid_chunker.py b/src/ocgis/spatial/grid_chunker.py index 8f45755e7..6b3815be7 100644 --- a/src/ocgis/spatial/grid_chunker.py +++ b/src/ocgis/spatial/grid_chunker.py @@ -348,13 +348,6 @@ def create_merged_weight_file(self, merged_weight_filename, strict=False): vc.add_variable(var) # Also copy over global attributes vc.attrs = vc_target.attrs - # Add some additional stuff for record keeping - import getpass - import socket - import datetime - vc.attrs['created_by_user'] = getpass.getuser() - vc.attrs['created_on_hostname'] = socket.getfqdn() - vc.attrs['created_at_datetime'] = str(datetime.datetime.now()) ctr += 1 # Create output weight file. @@ -869,8 +862,14 @@ def write_esmf_weights(self, src_path, dst_path, wgt_path, src_grid=None, dst_gr self.esmf_kwargs['dst_file_type'] = self.dst_grid.driver.get_esmf_fileformat() try: ocgis_lh(msg="creating esmf regrid...", logger=_LOCAL_LOGGER, level=logging.DEBUG) - regrid = create_esmf_regrid(srcfield=srcfield, dstfield=dstfield, filename=wgt_path, filemode=filemode, - **self.esmf_kwargs) + + # Older versions of ESMPy do not support 'filemode'. If it is set to BASIC (the legacy default) then remove + # the filemode argument. + if filemode == ESMF.FileMode.BASIC: + regrid = create_esmf_regrid(srcfield=srcfield, dstfield=dstfield, filename=wgt_path, **self.esmf_kwargs) + else: + regrid = create_esmf_regrid(srcfield=srcfield, dstfield=dstfield, filename=wgt_path, filemode=filemode, + **self.esmf_kwargs) finally: to_destroy = [regrid, srcgrid, srcfield, dstgrid, dstfield] for t in to_destroy: diff --git a/src/ocgis/test/base.py b/src/ocgis/test/base.py index 0d8e4d22e..5a7ee1d74 100644 --- a/src/ocgis/test/base.py +++ b/src/ocgis/test/base.py @@ -423,7 +423,7 @@ def assertWarns(self, warning, meth): meth() self.assertTrue(any(item.category == warning for item in warning_list)) - def assertWeightFilesEquivalent(self, src_filename, dst_filename, remove_created=False): + def assertWeightFilesEquivalent(self, src_filename, dst_filename, special_history=True): """Assert weight files are equivalent.""" nwf = RequestDataset(dst_filename, driver="netcdf").get() @@ -458,22 +458,18 @@ def assertWeightFilesEquivalent(self, src_filename, dst_filename, remove_created diffs = np.abs(diffs) self.assertLess(diffs.max(), 1e-14) - if remove_created: + if special_history: actual = nwf.attrs.copy() desired = gwf.attrs.copy() - to_remove = [] - for k in actual.keys(): - if k.startswith('created_'): - to_remove.append(k) - for target in [actual, desired]: - for t in to_remove: - try: - target.pop(t) - except KeyError: - continue + removes = ['history', 'created_by_user', 'created_on_hostname'] + for r in removes: + actual.pop(r, None) + desired.pop(r, None) self.assertEqual(actual, desired) else: - self.assertEqual(nwf.attrs, gwf.attrs) + actual = nwf.attrs + desired = gwf.attrs + self.assertEqual(actual, desired) @staticmethod def barrier_print(*args, **kwargs): diff --git a/src/ocgis/test/test_ocgis/test_ocli.py b/src/ocgis/test/test_ocgis/test_ocli.py index be15a9f9d..31cc57dfa 100644 --- a/src/ocgis/test/test_ocgis/test_ocli.py +++ b/src/ocgis/test/test_ocgis/test_ocli.py @@ -301,10 +301,13 @@ def test_chunked_rwg_spatial_subset(self): result = runner.invoke(ocli, args=cli_args, catch_exceptions=False) self.assertEqual(result.exit_code, 0) - self.assertTrue(os.path.exists(weight)) actual = RequestDataset(uri=spatial_subset).create_field() actual_ymean = actual.grid.get_value_stacked()[0].mean() actual_xmean = actual.grid.get_value_stacked()[1].mean() self.assertEqual(actual_ymean, 45.) self.assertEqual(actual_xmean, -85.) self.assertEqual(actual.grid.shape, (14, 14)) + + self.assertTrue(os.path.exists(weight)) + actual = RequestDataset(weight, driver='netcdf').create_field() + self.assertIn('history', actual.attrs) diff --git a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py index 8af43e170..611293217 100644 --- a/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py +++ b/src/ocgis/test/test_ocgis/test_spatial/test_grid_chunker.py @@ -370,15 +370,15 @@ def run_create_merged_weight_file(self, filemode): # Test merged and global weight files are equivalent ----------------------------------------------------------- - self.assertWeightFilesEquivalent(global_weights_filename, merged_weight_filename, remove_created=True) + self.assertWeightFilesEquivalent(global_weights_filename, merged_weight_filename) @attr('esmf') - def test_create_merged_weight_file(self): - poss = ["BASIC", "WITHAUX"] - for filemode in poss: - self.run_create_merged_weight_file(filemode) - self.tearDown() - os.mkdir(self.current_dir_output) + def test_create_merged_weight_file_basic(self): + self.run_create_merged_weight_file("BASIC") + + @attr('esmf') + def test_create_merged_weight_file_withaux(self): + self.run_create_merged_weight_file("WITHAUX") @attr('esmf') def test_create_merged_weight_file_unstructured(self):