diff --git a/.gitignore b/.gitignore index 8c91defd7f..d6604997d1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ cf/test/dir/ cf/test/*.nc cf/test/*.nca +cf/test/*.cdl cf/test/*.txt # coverage reports from a running the test coverage script diff --git a/Changelog.rst b/Changelog.rst index 44690e6419..946116f29a 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -3,10 +3,24 @@ version 3.16.0 **2023-??-??** +* Implemented the reading and manipulation of UGRID mesh topologies + (https://github.com/NCAS-CMS/cf-python/issues/696) +* New methods: `cf.Field.cell_connectivity`, + `cf.Field.cell_connectivities` + (https://github.com/NCAS-CMS/cf-python/issues/696) +* New methods: `cf.Field.domain_topology`, + `cf.Field.domain_topologies` + (https://github.com/NCAS-CMS/cf-python/issues/696) +* New method: `cf.Data.masked-values` + (https://github.com/NCAS-CMS/cf-python/issues/696) +* New method: `cf.Data.arctan2` + (https://github.com/NCAS-CMS/cf-python/issues/38) * Fix bug that caused `cf.Field.collapse` to give incorrect results for the "sum", "sum_of_weights" and "sum_of_weights2" methods, only in the case that weights have been requested (https://github.com/NCAS-CMS/cf-python/issues/701) +* Changed dependency: ``1.11.0.0<=cfdm<1.11.1.0`` +* New dependency: ``scipy>=1.10.0`` version 3.15.4 -------------- @@ -283,8 +297,8 @@ version 3.11.0 * Fix for `cf.aggregate` failures when a datum or coordinate conversion parameter has an array value (https://github.com/NCAS-CMS/cf-python/issues/230) -* Allow for regridding using a destination field featuring size 1 dimension(s) - (https://github.com/NCAS-CMS/cf-python/issues/250) +* Allow for regridding using a destination field featuring size 1 + dimension(s) (https://github.com/NCAS-CMS/cf-python/issues/250) * Fix bug that sometimes caused `cf.Field.autocyclic` to fail when setting a construct that is cyclic and has a defined period * Fix bug that sometimes caused a failure when reading PP extra data diff --git a/MANIFEST.in b/MANIFEST.in index b0ad4f679d..b8ba825ad8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,6 @@ recursive-exclude cf *.o *.so *.a .nfs* recursive-exclude cf/data *.rst prune cf/test recursive-include cf/test __init__.py test_*.py -recursive-include cf/test cfa_test.sh run_tests.py setup_create_field.py create_test_files.py individual_tests.sh -recursive-include cf/test test_file.nc test_file[2-4].nc file.nc file[1-9].nc wgdos_packed.pp extra_data.pp file1.pp *.cdl +recursive-include cf/test cfa_test.sh run_tests.py setup_create_field.py create_test_files*.py individual_tests.sh +recursive-include cf/test test_file.nc test_file[2-4].nc file.nc file[1-9].nc ugrid_global_1.nc ugrid_global_2.nc wgdos_packed.pp extra_data.pp file1.pp *.cdl prune cf/test/dir diff --git a/README.md b/README.md index 2c76aa5913..c6425ed0a1 100644 --- a/README.md +++ b/README.md @@ -85,52 +85,34 @@ The `cf` package uses of its array manipulation and can: * read field constructs from netCDF, CDL, PP and UM datasets, - * create new field constructs in memory, - -* write and append field constructs to netCDF datasets on disk, - +* write and append field and domain constructs to netCDF datasets on disk, +* read, create, and manipulate UGRID mesh topologies, * read, write, and create coordinates defined by geometry cells, - * read netCDF and CDL datasets containing hierarchical groups, - * inspect field constructs, - * test whether two field constructs are the same, - * modify field construct metadata and data, - * create subspaces of field constructs, - * write field constructs to netCDF datasets on disk, - * incorporate, and create, metadata stored in external files, - * read, write, and create data that have been compressed by convention (i.e. ragged or gathered arrays, or coordinate arrays compressed by subsampling), whilst presenting a view of the data in its uncompressed form, - * combine field constructs arithmetically, - * manipulate field construct data by arithmetical and trigonometrical operations, - -* perform statistical collapses on field constructs, - +* perform weighted statistical collapses on field constructs, + including those with geometry cells and UGRID mesh topologies, * perform histogram, percentile and binning operations on field constructs, - * regrid field constructs with (multi-)linear, nearest neighbour, first- and second-order conservative and higher order patch recovery - methods, - + methods, to and from structured and unstructured grids, * apply convolution filters to field constructs, - * create running means from field constructs, - * apply differential operators to field constructs, - * create derived quantities (such as relative vorticity). Visualization diff --git a/cf/__init__.py b/cf/__init__.py index 27ae8680af..9c192d0d24 100644 --- a/cf/__init__.py +++ b/cf/__init__.py @@ -73,9 +73,9 @@ """ -__Conventions__ = "CF-1.10" -__date__ = "2023-10-10" -__version__ = "3.15.4" +__Conventions__ = "CF-1.11" +__date__ = "2023-??-??" +__version__ = "3.16.0" _requires = ( "numpy", @@ -86,6 +86,7 @@ "psutil", "dask", "packaging", + "scipy", ) x = ", ".join(_requires) @@ -144,6 +145,11 @@ except ImportError as error1: raise ImportError(_error0 + str(error1)) +try: + import scipy +except ImportError as error1: + raise ImportError(_error0 + str(error1)) + # Check the version of packaging _minimum_vn = "20.0" if Version(packaging.__version__) < Version(_minimum_vn): @@ -193,8 +199,8 @@ ) # Check the version of cfdm -_minimum_vn = "1.10.1.2" -_maximum_vn = "1.10.2.0" +_minimum_vn = "1.11.0.0" +_maximum_vn = "1.11.1.0" _cfdm_version = Version(cfdm.__version__) if not Version(_minimum_vn) <= _cfdm_version < Version(_maximum_vn): raise RuntimeError( @@ -218,6 +224,14 @@ f"or later. Got {platform.python_version()}" ) +# Check the version of scipy +_minimum_vn = "1.10.0" +if Version(scipy.__version__) < Version(_minimum_vn): + raise RuntimeError( + f"Bad scipy version: cf requires scipy>={_minimum_vn}. " + f"Got {scipy.__version__} at {scipy.__file__}" + ) + from .constructs import Constructs from .mixin import Coordinate @@ -248,18 +262,23 @@ from .dimensioncoordinate import DimensionCoordinate from .auxiliarycoordinate import AuxiliaryCoordinate from .coordinatereference import CoordinateReference +from .cellconnectivity import CellConnectivity from .cellmethod import CellMethod from .cellmeasure import CellMeasure from .domainancillary import DomainAncillary from .domainaxis import DomainAxis +from .domaintopology import DomainTopology from .fieldancillary import FieldAncillary from .field import Field from .data import Data from .data.array import ( + BoundsFromNodesArray, + CellConnectivityArray, CFANetCDFArray, FullArray, GatheredArray, NetCDFArray, + PointTopologyArray, RaggedContiguousArray, RaggedIndexedArray, RaggedIndexedContiguousArray, diff --git a/cf/aggregate.py b/cf/aggregate.py index b1da134397..6fe53c2c9f 100644 --- a/cf/aggregate.py +++ b/cf/aggregate.py @@ -224,6 +224,8 @@ class _Meta: "Nd_coordinates", "Cell_measures", "Domain_ancillaries", + "Domain_topologies", + "Cell_connectivities", "Field_ancillaries", ), ) @@ -335,7 +337,11 @@ def __init__( self.key_to_identity = {} self.all_field_anc_identities = set() - self.all_domain_anc_identities = set() + # self.all_domain_anc_identities = set() + self.all_identities = { + "domain_ancillary": set(), + "field_ancillary": set(), + } self.message = "" self.info = info @@ -372,7 +378,7 @@ def __init__( # Parent field or domain # ------------------------------------------------------------ self.field = f - self.has_data = f.has_data() + self.has_field_data = f.has_data() self.identity = f.identity( strict=strict_identities, relaxed=relaxed_identities and not ncvar_identities, @@ -395,7 +401,7 @@ def __init__( return if self.identity is None: - if not allow_no_identity and self.has_data: + if not allow_no_identity and self.has_field_data: if info: self.message = ( "no identity; consider setting relaxed_identities=True" @@ -665,8 +671,11 @@ def __init__( "field_ancillary", todict=True ) for key, field_anc in field_ancs.items(): + if not self.has_data(field_anc): + return + # Find this field ancillary's identity - identity = self.field_ancillary_has_identity_and_data(field_anc) + identity = self.get_identity(field_anc) if identity is None: return @@ -722,11 +731,13 @@ def __init__( if anc is None: continue + if not self.has_data(anc): + return + # Set this domain ancillary's identity - identity = (ref_identity, term) - identity = self.domain_ancillary_has_identity_and_data( - anc, identity - ) + identity = self.get_identity(anc, (ref_identity, term)) + if identity is None: + return # Find the canonical units units = self.canonical_units( @@ -758,6 +769,9 @@ def __init__( if key in ancs_in_refs: continue + if not self.has_data(anc): + return + # Find this domain ancillary's identity identity = self.domain_ancillary_has_identity_and_data(anc) if identity is None: @@ -791,12 +805,11 @@ def __init__( self.msr = {} info_msr = {} for key, msr in f.cell_measures(todict=True).items(): - if not self.cell_measure_has_measure(msr): + if not self.has_measure(msr): return - if ( - not msr.nc_get_external() - and not self.cell_measure_has_data_and_units(msr) + if not msr.nc_get_external() and not ( + self.has_data(msr) and self.has_units(msr) ): return @@ -859,6 +872,111 @@ def __init__( "external": tuple([v["external"] for v in value]), } + # ------------------------------------------------------------ + # Domain topologies + # ------------------------------------------------------------ + self.domain_topology = {} + info_topology = {} + for key, topology in f.domain_topologies(todict=True).items(): + if not (self.has_cell(topology) and self.has_data(topology)): + return + + # Find axes' identities + axes = tuple( + [self.axis_to_id[axis] for axis in construct_axes[key]] + ) + + identity = topology.get_cell() + + # Find the canonical axes + canonical_axes = self.canonical_axes(topology, identity, axes) + canonical_axis = canonical_axes[0] + + if canonical_axis in info_topology: + # Check for ambiguous domain topologies, i.e. those + # which span the same axis. + if info: + self.message = f"duplicate {topology!r}" + + return + else: + info_topology[canonical_axis] = [] + + info_topology[canonical_axes[0]].append( + { + "cell": identity, + "key": key, + "axes": axes, + "canonical_axes": canonical_axes, + } + ) + + # For each domain topology's canonical axis, sort the + # information by axis identities. + for units, value in info_topology.items(): + self.domain_topology[canonical_axis] = { + "cell": tuple([v["cell"] for v in value]), + "keys": tuple([v["key"] for v in value]), + "axes": tuple([v["axes"] for v in value]), + "canonical_axes": tuple([v["canonical_axes"] for v in value]), + } + + # ------------------------------------------------------------ + # Cell connectivities + # ------------------------------------------------------------ + self.cell_connectivity = {} + info_connectivity = {} + for key, connectivity in f.cell_connectivities(todict=True).items(): + if not ( + self.has_connectivity(connectivity) + and self.has_data(connectivity) + ): + return + + # Find axes' identities + axes = tuple( + [self.axis_to_id[axis] for axis in construct_axes[key]] + ) + + identity = connectivity.get_connectivity() + + # Find the canonical axes + canonical_axes = self.canonical_axes(connectivity, identity, axes) + canonical_axis = canonical_axes[0] + + if canonical_axis in info_connectivity: + # Check for ambiguous cell connectivities, i.e. those + # which span the same axis with the same connectivity + # type. + for value in info_connectivity[canonical_axis]: + if identity == value["connectivity"]: + if info: + self.message = f"duplicate {connectivity!r}" + + return + else: + info_connectivity[canonical_axis] = [] + + info_connectivity[canonical_axes[0]].append( + { + "connectivity": identity, + "key": key, + "axes": axes, + "canonical_axes": canonical_axes, + } + ) + + # For each cell connectivity's canonical axis, sort the + # information by cell type. + for axis, value in info_connectivity.items(): + value.sort(key=itemgetter("connectivity")) + self.cell_connectivity[axis] = { + "connectivity": tuple([v["connectivity"] for v in value]), + "keys": tuple([v["key"] for v in value]), + "axes": tuple([v["axes"] for v in value]), + "canonical_axes": tuple([v["canonical_axes"] for v in value]), + } + # ------------------------------------------------------------ # Properties and attributes # ------------------------------------------------------------ @@ -1303,48 +1421,113 @@ def canonical_cell_methods(self, rtol=None, atol=None): return cms - def cell_measure_has_data_and_units(self, msr): - """True only if a cell measure has both data and units. + def has_cell(self, topology): + """Whether a domain topology construct has a connectivity type. + + .. versionadded:: 3.16.0 :Parameters: - msr: `CellMeasure` + topology: `DomainTopology` + The construct to test. :Returns: `bool` + `True` if the construct has a cell type, otherwise + `False`. """ - if not msr.Units: + if topology.get_cell(None) is None: if self.info: - self.message = f"{msr.identity()!r} cell measure has no units" + self.message = ( + f"{topology.identity()!r} " + "domain topology construct has no cell type" + ) return False - if not msr.has_data(): + return True + + def has_connectivity(self, connectivity): + """Whether a cell connectivity construct has a connectivity type. + + .. versionadded:: 3.16.0 + + :Parameters: + + connectivity: `CellConnectivity` + The construct to test. + + :Returns: + + `bool` + `True` if the construct has a connectivity type, + otherwise `False`. + + """ + if connectivity.get_connectivity(None) is None: if self.info: - self.message = f"{msr.identity()!r} cell measure has no data" + self.message = ( + f"{connectivity.identity()!r} " + "cell connectivity construct has no connectivity type" + ) return False return True - def cell_measure_has_measure(self, msr): - """True only if a cell measure has a measure. + def has_measure(self, msr): + """Whether a cell measure construct has a measure. + + .. versionadded:: 3.16.0 :Parameters: msr: `CellMeasure` + The construct to test. :Returns: `bool` + `True` if the construct has a measure, otherwise + `False`. """ - if not msr.get_measure(False): + if msr.get_measure(None) is None: if self.info: self.message = ( - f"{msr.identity()!r} cell measure has no measure" + f"{msr.identity()!r} " + "cell measure construct has no measure" + ) + + return False + + return True + + def has_units(self, construct): + """Whether a construct has units. + + .. versionadded:: 3.16.0 + + :Parameters: + + construct: Construct + The construct to test. + + :Returns: + + `bool` + `True` if the construct has units, otherwise `False`. + + """ + if not construct.Units: + if self.info: + construct_type = construct.construct_type + self.message = ( + f"{construct.identity()!r} " + f"{construct_type.replace('_', ' ')} construct " + "has no units" ) return False @@ -1366,8 +1549,8 @@ def coord_has_identity_and_data(self, coord, axes=None): :Returns: `str` or `None` - The coordinate construct's identity, or `None` if there is - no identity and/or no data. + The coordinate construct's identity, or `None` if + there is no identity and/or no data. """ identity = coord.identity( @@ -1396,47 +1579,33 @@ def coord_has_identity_and_data(self, coord, axes=None): if self.info: self.message = f"{coord!r} has no identity or no data" - def field_ancillary_has_identity_and_data(self, anc): - """Return a field ancillary's identity if it has one and has - data. + def has_data(self, construct): + """Whether a construct has data. + + .. versionadded:: 3.16.0 :Parameters: - coord: `FieldAncillary` + construct: Construct + The construct to test. :Returns: - `str` or `None` - The coordinate construct's identity, or `None` if - there is no identity and/or no data. + `bool` + `True` if the construct has data, otherwise `False`. """ - identity = anc.identity( - strict=self.strict_identities, - relaxed=self.relaxed_identities and not self.ncvar_identities, - nc_only=self.ncvar_identities, - default=None, - ) - - if identity is not None: - all_field_anc_identities = self.all_field_anc_identities - - if identity in all_field_anc_identities: - if self.info: - self.message = f"multiple {identity!r} field ancillaries" - - return + if not construct.has_data(): + if self.info: + construct_type = construct.construct_type + self.message = ( + f"{construct.identity()!r} " + f"{construct_type.replace('_', ' ')} has no data" + ) - if anc.has_data(): - all_field_anc_identities.add(identity) - return identity + return False - # Still here? - if self.info: - self.message = ( - f"{anc.identity()!r} field ancillary has no identity or " - "no data" - ) + return True def coordinate_reference_signatures(self, refs): """List the structural signatures of given coordinate @@ -1477,61 +1646,58 @@ def coordinate_reference_signatures(self, refs): return signatures - def domain_ancillary_has_identity_and_data(self, anc, identity=None): - """Return a domain ancillary's identity if it has one and has - data. + def get_identity(self, construct, identity=None): + """Return a construct's identity if it has one. + + .. versionadded:: 3.16.0 :Parameters: - anc: cf.DomainAncillary + construct: Construct + The construct to test. identity: optional :Returns: `str` or `None` - The domain ancillary identity, or None if there is no - identity and/or no data. + The construct identity, or `None` if there isn't one. """ if identity is not None: - anc_identity = identity + construct_identity = identity else: - anc_identity = anc.identity( + construct_identity = construct.identity( strict=self.strict_identities, relaxed=self.relaxed_identities and not self.ncvar_identities, nc_only=self.ncvar_identities, default=None, ) - if anc_identity is None: + construct_type = construct.construct_type + if construct_identity is None: if self.info: self.message = ( - f"{anc.identity()!r} domain ancillary has no identity" + f"{construct.identity()!r} " + f"{construct_type.replace('_', ' ')} has no identity" ) return - all_domain_anc_identities = self.all_domain_anc_identities - - if anc_identity in all_domain_anc_identities: - if self.info: - self.message = ( - f"multiple {anc.identity()!r} domain ancillaries" - ) - return - - if not anc.has_data(): - if self.info: - self.message = ( - f"{anc.identity()!r} domain ancillary has no data" - ) + all_identities = self.all_identities.get(construct_type) + if all_identities is not None: + if construct_identity in all_identities: + if self.info: + self.message = ( + f"multiple {construct.identity()!r} " + f"{construct_type.replace('_', ' ')} constructs" + ) - return + return - all_domain_anc_identities.add(anc_identity) + all_identities.add(construct_identity) - return anc_identity + return construct_identity @_manage_log_level_via_verbose_attr def print_info(self, signature=True): @@ -1605,16 +1771,11 @@ def structural_signature(self): Units = self.units.units Cell_methods = self.cell_methods - Data = self.has_data - # signature_append = signature.append + Data = self.has_field_data # Properties - # signature_append(('Properties', self.properties)) Properties = self.properties - # standard_error_multiplier - # signature_append(('standard_error_multiplier', - # f.get_property('standard_error_multiplier', None))) standard_error_multiplier = f.get_property( "standard_error_multiplier", None ) @@ -1715,6 +1876,30 @@ def structural_signature(self): ] Domain_ancillaries = tuple(x) + # Domain topologies + topology = self.domain_topology + x = [ + ( + identity, + ("cell", topology[identity]["cell"]), + ("axes", topology[identity]["canonical_axes"]), + ) + for identity in sorted(topology) + ] + Domain_topologies = tuple(x) + + # Cell connectivities + connectivity = self.cell_connectivity + x = [ + ( + identity, + ("connectivity", connectivity[identity]["connectivity"]), + ("axes", connectivity[identity]["canonical_axes"]), + ) + for identity in sorted(connectivity) + ] + Cell_connectivities = tuple(x) + # Field ancillaries field_anc = self.field_anc x = [ @@ -1747,6 +1932,8 @@ def structural_signature(self): Nd_coordinates=Nd_coordinates, Cell_measures=Cell_measures, Domain_ancillaries=Domain_ancillaries, + Domain_topologies=Domain_topologies, + Cell_connectivities=Cell_connectivities, Field_ancillaries=Field_ancillaries, ) @@ -2913,7 +3100,7 @@ def aggregate( # ] # }, # 'auxiliary_coordinate': {}, - # 'cell_interface': {}, + # 'cell_connectivity': {}, # 'cell_measure': {}, # 'domain_ancillary': {}, # 'domain_topology': {}, @@ -2940,8 +3127,8 @@ def aggregate( "cell_measure": {}, "domain_ancillary": {}, "field_ancillary": {}, - "domain_topology": {}, # UGRID - "cell_interface": {}, # UGRID + "domain_topology": {}, + "cell_connectivity": {}, } m0 = m[0].copy() @@ -3365,12 +3552,16 @@ def _create_hash_and_first_values( sort_indices = slice(None, None, -1) else: sort_indices = slice(None) - + elif identity in m.domain_topology: + # ... or which doesn't have a dimension coordinate but + # does have a domain topology ... + sort_indices = slice(None) + needs_sorting = False else: # ... or which doesn't have a dimension coordinate but # does have one or more 1-d auxiliary coordinates aux = m_axis_identity["keys"][0] - # '.data.compute()' is faster than '.array' + # Note: '.data.compute()' is faster than '.array' sort_indices = np.argsort(constructs[aux].data.compute()) m_sort_keys[axis] = aux needs_sorting = True @@ -3541,6 +3732,76 @@ def _create_hash_and_first_values( msr["hash_values"] = hash_values + # ------------------------------------------------------------ + # Domain topologies + # ------------------------------------------------------------ + if donotchecknonaggregatingaxes: + for topology in m.domain_topology.values(): + topology["hash_values"] = [(None,) * len(topology["keys"])] + else: + for topology in m.domain_topology.values(): + hash_values = [] + for key, canonical_axes in zip( + topology["keys"], topology["canonical_axes"] + ): + construct = constructs[key] + sort_indices, needs_sorting = _sort_indices( + m, canonical_axes + ) + + # Get the hash of the data array + h = _get_hfl( + construct, + _no_units, + sort_indices, + needs_sorting, + False, + False, + hfl_cache, + rtol, + atol, + ) + + hash_values.append((h,)) + + topology["hash_values"] = hash_values + + # ------------------------------------------------------------ + # Cell connectivities + # ------------------------------------------------------------ + if donotchecknonaggregatingaxes: + for connectivity in m.cell_connectivity.values(): + connectivity["hash_values"] = [ + (None,) * len(connectivity["keys"]) + ] + else: + for connectivity in m.cell_connectivity.values(): + hash_values = [] + for key, canonical_axes in zip( + connectivity["keys"], connectivity["canonical_axes"] + ): + construct = constructs[key] + sort_indices, needs_sorting = _sort_indices( + m, canonical_axes + ) + + # Get the hash of the data array + h = _get_hfl( + construct, + _no_units, + sort_indices, + needs_sorting, + False, + False, + hfl_cache, + rtol, + atol, + ) + + hash_values.append((h,)) + + connectivity["hash_values"] = hash_values + # ------------------------------------------------------------ # Field ancillaries # ------------------------------------------------------------ @@ -3921,6 +4182,42 @@ def _hash_values(m): groups_of_fields.append([m1]) continue + # Still here? Then check the domain topologies + topology = m0.domain_topology + for axis in topology: + for axes, hash_value0, hash_value1 in zip( + topology[axis]["axes"], + topology[axis]["hash_values"], + m1.domain_topology[axis]["hash_values"], + ): + if a_identity not in axes and hash_value0 != hash_value1: + # There is a matching pair of domain + # topologies that does not span the + # aggregating axis and they have different + # data array values + ok = False + break + + # Still here? Then check the cell connectivities + connectivity = m0.cell_connectivity + for axis in connectivity: + for axes, hash_value0, hash_value1 in zip( + connectivity[axis]["axes"], + connectivity[axis]["hash_values"], + m1.cell_connectivity[axis]["hash_values"], + ): + if a_identity not in axes and hash_value0 != hash_value1: + # There is a matching pair of cell + # connectivities that does not span the + # aggregating axis and they have different + # data array values + ok = False + break + + if not ok: + groups_of_fields.append([m1]) + continue + # Still here? Then set the identity of the aggregating # axis m0.a_identity = a_identity @@ -4137,39 +4434,24 @@ def _ok_coordinate_arrays( f"contiguous={bool(contiguous)} and " f"{m.axis[axis]['ids'][dim_coord_index]!r} " "dimension coordinate cells do not match " - "the cell spacing condition between fields " - f"({data1!r} - {data0!r} = {dim_diff!r}) " + "the cell spacing condition between fields: " + f"{data1!r} - {data0!r} = {dim_diff!r} " f"!= {diff!r}" ) return False else: # ------------------------------------------------------------ - # The aggregating axis does not have a dimension coordinate, - # but it does have at least one 1-d auxiliary coordinate. + # The aggregating axis does not have a dimension coordinate # ------------------------------------------------------------ - # Check for duplicate auxiliary coordinate values - for i, identity in enumerate(meta[0].axis[axis]["ids"]): - set_of_1d_aux_coord_values = set() - number_of_1d_aux_coord_values = 0 - for m in meta: - aux = m.axis[axis]["keys"][i] - # '.data.compute()' is faster than '.array' - array = m.field.constructs[aux].data.compute() - set_of_1d_aux_coord_values.update(array) - number_of_1d_aux_coord_values += array.size - if ( - len(set_of_1d_aux_coord_values) - != number_of_1d_aux_coord_values - ): - if info: - meta[0].message = ( - f"no {identity!r} dimension coordinates and " - f"{identity!r} auxiliary coordinates have " - "duplicate values" - ) + if axis in m.domain_topology or axis in m.cell_connectivity: + if info: + meta[0].message = ( + f"can't aggregate along the {axis!r} mesh topology " + "discrete axis" + ) - return False + return False # ---------------------------------------------------------------- # Still here? Then the aggregating axis does not overlap between @@ -4429,7 +4711,7 @@ def _aggregate_2_fields( # Update the size of the aggregating axis in the output parent # construct # ---------------------------------------------------------------- - if m0.has_data: + if m0.has_field_data: # ---------------------------------------------------------------- # Insert the data array from parent1 into the data array of # parent0 diff --git a/cf/auxiliarycoordinate.py b/cf/auxiliarycoordinate.py index fba4082529..46c28f0879 100644 --- a/cf/auxiliarycoordinate.py +++ b/cf/auxiliarycoordinate.py @@ -36,9 +36,7 @@ class AuxiliaryCoordinate( {{netCDF variable}} - The netCDF variable group structure may be accessed with the - `nc_set_variable`, `nc_get_variable`, `nc_variable_groups`, - `nc_clear_variable_groups` and `nc_set_variable_groups` methods. + {{netCDF UGRID node coordinate}} """ @@ -47,11 +45,3 @@ def __new__(cls, *args, **kwargs): instance = super().__new__(cls) instance._Bounds = Bounds return instance - - def __repr__(self): - """Called by the `repr` built-in function. - - x.__repr__() <==> repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " >>> i.classes() {'AuxiliaryCoordinate': cf.auxiliarycoordinate.AuxiliaryCoordinate, + 'BoundsFromNodesArray': cf.data.array.boundsfromnodesarray.BoundsFromNodesArray, + 'CellConnectivity': cf.cellconnectivity.CellConnectivity, + 'CellConnectivityArray': cf.data.array.cellconnectivityarray.CellConnectivityArray, 'CellMeasure': cf.cellmeasure.CellMeasure, 'CellMethod': cf.cellmethod.CellMethod, 'CFANetCDFArray': cf.data.array.cfanetcdfarray.CFANetCDFArray, @@ -202,6 +215,7 @@ def implementation(): 'Domain': cf.domain.Domain, 'DomainAncillary': cf.domainancillary.DomainAncillary, 'DomainAxis': cf.domainaxis.DomainAxis, + 'DomainTopology': cf.domaintopology.DomainTopology, 'Field': cf.field.Field, 'FieldAncillary': cf.fieldancillary.FieldAncillary, 'Bounds': cf.bounds.Bounds, @@ -217,6 +231,7 @@ def implementation(): 'Data': cf.data.data.Data, 'GatheredArray': cf.data.array.gatheredarray.GatheredArray, 'NetCDFArray': cf.data.array.netcdfarray.NetCDFArray, + 'PointTopologyArray': , 'RaggedContiguousArray': cf.data.array.raggedcontiguousarray.RaggedContiguousArray, 'RaggedIndexedArray': cf.data.array.raggedindexedarray.RaggedIndexedArray, 'RaggedIndexedContiguousArray': cf.data.array.raggedindexedcontiguousarray.RaggedIndexedContiguousArray, diff --git a/cf/constructs.py b/cf/constructs.py index 95d068f18d..29dbbc9dfc 100644 --- a/cf/constructs.py +++ b/cf/constructs.py @@ -47,6 +47,8 @@ class Constructs(cfdm.Constructs): * dimension coordinate constructs * domain ancillary constructs * domain axis constructs + * domain topology constructs + * cell connectivity constructs * cell method constructs * field ancillary constructs @@ -126,9 +128,6 @@ def _flip(self, axes): ] construct.flip(iaxes, inplace=True) - # ---------------------------------------------------------------- - # Methods - # ---------------------------------------------------------------- def close(self): """Close all files referenced by the metadata constructs. diff --git a/cf/count.py b/cf/count.py index c2db119324..090c14b6b3 100644 --- a/cf/count.py +++ b/cf/count.py @@ -4,42 +4,4 @@ class Count(mixin.PropertiesData, cfdm.Count): - """A count variable required to uncompress a ragged array. - - A collection of features stored using a contiguous ragged array - combines all features along a single dimension (the sample - dimension) such that each feature in the collection occupies a - contiguous block. - - The information needed to uncompress the data is stored in a count - variable that gives the size of each block. - - **NetCDF interface** - - The netCDF variable name of the count variable may be accessed - with the `nc_set_variable`, `nc_get_variable`, `nc_del_variable` - and `nc_has_variable` methods. - - The name of the netCDF dimension spanned by the count variable's - data may be accessed with the `nc_set_dimension`, - `nc_get_dimension`, `nc_del_dimension` and `nc_has_dimension` - methods. - - The name of the netCDF sample dimension spanned by the compressed - data (that is stored in the "sample_dimension" netCDF attribute - and which does not correspond to a domain axis construct) may be - accessed with the `nc_set_sample_dimension`, - `nc_get_sample_dimension`, `nc_del_sample_dimension` and - `nc_has_sample_dimension` methods. - - .. versionadded:: 3.0.0 - - """ - - def __repr__(self): - """Called by the `repr` built-in function. - - x.__repr__() <==> repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - .. versionadded:: 3.0.0 - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - .. versionadded:: 3.0.0 - - """ - return super().__repr__().replace("<", " repr(x) - - .. versionadded:: 3.0.0 - - """ - return super().__repr__().replace("<", " repr(x) - - .. versionadded:: 3.0.0 - - """ - return super().__repr__().replace("<", " repr(x) - - .. versionadded:: 3.0.0 - - """ - return super().__repr__().replace("<", "= + masked in the destination grid definition; or b) ``w_ji >= min_weight`` for those masked source grid cells i for which ``w_ji > 0``. **Conservative first-order regridding** Destination grid cell j will only be masked if a) it is - masked in destination grid definition; or b) The sum of - ``w_ji`` for all non-masked source grid cells i is + masked in the destination grid definition; or b) the sum + of ``w_ji`` for all non-masked source grid cells i is strictly less than *min_weight*. :Returns: @@ -168,8 +180,9 @@ def regrid( # are the gathered regridding axes and whose left-hand dimension # represent of all the other dimensions. # ---------------------------------------------------------------- + n_src_axes = len(src_shape) a = a.transpose(axis_order) - non_regrid_shape = a.shape[: a.ndim - len(src_shape)] + non_regrid_shape = a.shape[: a.ndim - n_src_axes] dst_size, src_size = weights.shape a = a.reshape(-1, src_size) a = a.T @@ -200,7 +213,7 @@ def regrid( if variable_mask or (src_mask is None and ref_src_mask.any()): raise ValueError( f"Can't regrid with the {method!r} method when the source " - f"data mask varies over different {len(src_shape)}-d " + f"data mask varies over different {n_src_axes}-d " "regridding slices" ) @@ -279,10 +292,38 @@ def regrid( a = a.T a = a.reshape(non_regrid_shape + tuple(dst_shape)) + n_dst_axes = len(dst_shape) + + if n_src_axes == 1 and n_dst_axes == 2: + # The regridding operation increased the number of data axes + # by 1 => modify 'axis_order' to contain the new axis. + # + # E.g. UGRID -> regular lat-lon could change 'axis_order' from + # [0,2,1] to [0,3,1,2] + raxis = axis_order[-1] + axis_order = [ + i if i <= raxis else i + n_dst_axes - 1 for i in axis_order + ] + axis_order[-1:] = range(raxis, raxis + n_dst_axes) + elif n_src_axes == 2 and n_dst_axes == 1: + # The regridding operation decreased the number of data axes + # by 1 => modify 'axis_order' to remove the removed axis. + # + # E.g. regular lat-lon -> UGRID could change 'axis_order' from + # [0,2,4,5,1,3] to [0,2,3,4,1], or [0,2,4,5,3,1] to + # [0,1,3,4,2] + raxis0, raxis = axis_order[-2:] + axis_order = [i if i <= raxis else i - 1 for i in axis_order[:-1]] + elif n_src_axes != n_dst_axes: + raise ValueError( + f"Can't (yet) regrid from {n_src_axes} dimensions to " + f"{n_dst_axes} dimensions" + ) + d = {k: i for i, k in enumerate(axis_order)} axis_reorder = [i for k, i in sorted(d.items())] - a = a.transpose(axis_reorder) + a = a.transpose(axis_reorder) return a @@ -421,10 +462,10 @@ def _regrid( # destination cell j that intersects with unmasked cells # of the source grid. # - # D_j = 1 - w_i1j - ... - w_iNj + # D_j = w_i1j + ... + w_iNj # - # where w_iXj is the unmasked weight for masked source - # cell i and destination cell j. + # where each w_iXj is the weight for unmasked source cell + # i and destination cell j. dst_size = weights.shape[0] if dst_mask is None: dst_mask = np.zeros((dst_size,), dtype=bool) @@ -451,7 +492,7 @@ def _regrid( continue w = data[i0:i1] - D_j = 1 - w[mask].sum() + D_j = w[~mask].sum() w = w / D_j w[mask] = 0 data[i0:i1] = w diff --git a/cf/data/dask_utils.py b/cf/data/dask_utils.py index fafbf82501..63291a4d96 100644 --- a/cf/data/dask_utils.py +++ b/cf/data/dask_utils.py @@ -9,6 +9,7 @@ import dask.array as da import numpy as np from dask.core import flatten +from scipy.ndimage import convolve1d from ..cfdatetime import dt, dt2rt, rt2dt from ..functions import atol as cf_atol @@ -128,12 +129,6 @@ def cf_contains(a, value): return np.array(value in a).reshape((1,) * a.ndim) -try: - from scipy.ndimage import convolve1d -except ImportError: - pass - - def cf_convolve1d(a, window=None, axis=-1, origin=0): """Calculate a 1-d convolution along the given axis. diff --git a/cf/data/data.py b/cf/data/data.py index 840be8c204..c672620b1e 100644 --- a/cf/data/data.py +++ b/cf/data/data.py @@ -18,6 +18,7 @@ from dask.base import collections_to_dsk, is_dask_collection, tokenize from dask.highlevelgraph import HighLevelGraph from dask.optimization import cull +from scipy.sparse import issparse from ..cfdatetime import dt as cf_dt from ..constants import masked as cf_masked @@ -178,6 +179,7 @@ def __init__( copy=True, dtype=None, mask=None, + mask_value=None, to_memory=False, init_options=None, _use_array=True, @@ -266,6 +268,12 @@ def __init__( .. versionadded:: 3.0.5 + mask_value: scalar array_like + Mask *array* where it is equal to *mask_value*, using + numerically tolerant floating point equality. + + .. versionadded:: 3.16.0 + {{init source: optional}} hardmask: `bool`, optional @@ -392,6 +400,8 @@ def __init__( # No data has been set return + sparse_array = issparse(array) + try: ndim = array.ndim except AttributeError: @@ -482,8 +492,18 @@ def __init__( # Apply a mask if mask is not None: + if sparse_array: + raise ValueError("Can't mask sparse array") + self.where(mask, cf_masked, inplace=True) + # Apply masked values + if mask_value is not None: + if sparse_array: + raise ValueError("Can't mask sparse array") + + self.masked_values(mask_value, inplace=True) + @property def dask_compressed_array(self): """Returns a dask array of the compressed data. @@ -766,14 +786,6 @@ def __bool__(self): return bool(self.to_dask_array()) - def __repr__(self): - """Called by the `repr` built-in function. - - x.__repr__() <==> repr(x) - - """ - return super().__repr__().replace("<", ">> d.compute() array([1., 2., 3.]) + >>> from scipy.sparse import csr_array + >>> d = cf.Data(csr_array((2, 3))) + >>> d.compute() + <2x3 sparse array of type '' + with 0 stored elements in Compressed Sparse Row format> + >>>: d.array + array([[0., 0., 0.], + [0., 0., 0.]]) + >>> d.compute().toarray() + array([[0., 0., 0.], + [0., 0., 0.]]) + """ a = self.to_dask_array().compute() @@ -3734,6 +3760,7 @@ def _regrid( from .dask_regrid import regrid, regrid_weights shape = self.shape + ndim = self.ndim src_shape = tuple(shape[i] for i in regrid_axes) if src_shape != operator.src_shape: raise ValueError( @@ -3753,11 +3780,44 @@ def _regrid( ] dx = dx.rechunk(chunks) - # Define the regridded chunksizes - regridded_chunks = tuple( - (regridded_sizes[i],) if i in regridded_sizes else c - for i, c in enumerate(dx.chunks) - ) + # Define the regridded chunksizes (allowing for the regridded + # data to have more/fewer axes). + regridded_chunks = [] # The 'chunks' parameter to `map_blocks` + drop_axis = [] # The 'drop_axis' parameter to `map_blocks` + new_axis = [] # The 'new_axis' parameter to `map_blocks` + n = 0 + for i, c in enumerate(dx.chunks): + if i in regridded_sizes: + sizes = regridded_sizes[i] + n_sizes = len(sizes) + if not n_sizes: + drop_axis.append(i) + continue + + regridded_chunks.extend(sizes) + if n_sizes > 1: + new_axis.extend(range(n + 1, n + n_sizes)) + n += n_sizes - 1 + else: + regridded_chunks.extend(c) + + n += 1 + + # Update the axis identifiers. + # + # This is necessary when regridding changes the number of data + # dimensions (e.g. as happens when regridding a mesh topology + # axis to/from separate lat and lon axes). + if new_axis: + axes = list(self._axes) + for i in new_axis: + axes.insert(i, new_axis_identifier(tuple(axes))) + + self._axes = axes + elif drop_axis: + axes = self._axes + axes = [axes[i] for i in range(ndim) if i not in drop_axis] + self._axes = axes # Set the output data type if method in ("nearest_dtos", "nearest_stod"): @@ -3765,7 +3825,7 @@ def _regrid( else: dst_dtype = float - non_regrid_axes = [i for i in range(self.ndim) if i not in regrid_axes] + non_regrid_axes = [i for i in range(ndim) if i not in regrid_axes] src_mask = operator.src_mask if src_mask is not None: @@ -3790,6 +3850,8 @@ def _regrid( weights_dst_mask=weights_dst_mask, ref_src_mask=src_mask, chunks=regridded_chunks, + drop_axis=drop_axis, + new_axis=new_axis, meta=np.array((), dtype=dst_dtype), ) @@ -5094,7 +5156,9 @@ def array(self): """ array = self.compute().copy() - if not isinstance(array, np.ndarray): + if issparse(array): + array = array.toarray() + elif not isinstance(array, np.ndarray): array = np.asanyarray(array) return array @@ -5205,7 +5269,6 @@ def mask(self): return mask_data_obj - # `arctan2`, AT2 seealso @_inplace_enabled(default=False) def arctan(self, inplace=False): """Take the trigonometric inverse tangent of the data element- @@ -5215,7 +5278,7 @@ def arctan(self, inplace=False): .. versionadded:: 3.0.7 - .. seealso:: `tan`, `arcsin`, `arccos`, `arctanh` + .. seealso:: `tan`, `arcsin`, `arccos`, `arctanh`, `arctan2` :Parameters: @@ -5254,47 +5317,6 @@ def arctan(self, inplace=False): return d - # AT2 - # - # @classmethod - # def arctan2(cls, y, x): - # '''Take the "two-argument" trigonometric inverse tangent - # element-wise for `y`/`x`. - # - # Explicitly this returns, for all corresponding elements, the angle - # between the positive `x` axis and the line to the point (`x`, `y`), - # where the signs of both `x` and `y` are taken into account to - # determine the quadrant. Such knowledge of the signs of `x` and `y` - # are lost when the quotient is input to the standard "one-argument" - # `arctan` function, such that use of `arctan` leaves the quadrant - # ambiguous. `arctan2` may therefore be preferred. - # - # Units are ignored in the calculation. The result has units of radians. - # - # .. versionadded:: 3.2.0 - # - # .. seealso:: `arctan`, `tan` - # - # :Parameters: - # - # y: `Data` - # The data array to provide the numerator elements, corresponding - # to the `y` coordinates in the `arctan2` definition. - # - # x: `Data` - # The data array to provide the denominator elements, - # corresponding to the `x` coordinates in the `arctan2` - # definition. - # - # :Returns: - # - # `Data` - # - # **Examples** - # - # ''' - # return cls(np.arctan2(y, x), units=_units_radians) - @_inplace_enabled(default=False) def arctanh(self, inplace=False): """Take the inverse hyperbolic tangent of the data element-wise. @@ -7334,6 +7356,96 @@ def asdata(cls, d, dtype=None, copy=False): return data + @classmethod + def arctan2(cls, x1, x2): + """Element-wise arc tangent of ``x1/x2`` with correct quadrant. + + The quadrant (i.e. branch) is chosen so that ``arctan2(y, x)`` + is the signed angle in radians between the ray ending at the + origin and passing through the point ``(1, 0)``, and the ray + ending at the origin and passing through the point ``(x, y)``. + (Note the role reversal: the "y-coordinate" is the first + function parameter, the "x-coordinate" is the second.) By IEEE + convention, this function is defined for ``x = +/-0`` and for + either or both of ``y = +/-inf`` and ``x = +/-inf`` (see Notes + for specific values). + + `arctan2` is identical to the ``atan2`` function of the + underlying C library. The following special values are defined + in the C standard: + + ====== ====== =================== + *x1* *x2* ``arctan2(x1, x2)`` + ====== ====== =================== + +/- 0 +0 +/- 0 + +/- 0 -0 +/- pi + > 0 +/-inf +0 / +pi + < 0 +/-inf -0 / -pi + +/-inf +inf +/- (pi/4) + +/-inf -inf +/- (3*pi/4) + ====== ====== =================== + + Note that ``+0`` and ``-0`` are distinct floating point + numbers, as are ``+inf`` and ``-inf``. + + .. versionadded:: 3.16.0 + + .. seealso:: `arctan`, `tan` + + :Parameters: + + x1: array_like + Y coordinates. + + x2: array_like + X coordinates. *x1* and *x2* must be broadcastable to + a common shape (which becomes the shape of the + output). + + :Returns: + + `Data` + Array of angles in radians, in the range ``(-pi, + pi]``. + + **Examples** + + >>> import numpy as np + >>> x = cf.Data([-1, +1, +1, -1]) + >>> y = cf.Data([-1, -1, +1, +1]) + >>> print((cf.Data.arctan2(y, x) * 180 / np.pi).array) + [-135.0 -45.0 45.0 135.0] + >>> x[1] = cf.masked + >>> y[1] = cf.masked + >>> print((cf.Data.arctan2(y, x) * 180 / np.pi).array) + [-135.0 -- 45.0 135.0] + + >>> print(cf.Data.arctan2([0, 0, np.inf], [+0., -0., np.inf]).array) + [0.0 3.141592653589793 0.7853981633974483] + + >>> print((cf.Data.arctan2([1, -1], [0, 0]) * 180 / np.pi).array) + [90.0 -90.0] + + """ + try: + y = x1.to_dask_array() + except AttributeError: + y = cls.asdata(x1).to_dask_array() + + try: + x = x2.to_dask_array() + except AttributeError: + x = cls.asdata(x2).to_dask_array() + + mask = da.ma.getmaskarray(y) | da.ma.getmaskarray(x) + y = da.ma.filled(y, 1) + x = da.ma.filled(x, 1) + + dx = da.arctan2(y, x) + dx = da.ma.masked_array(dx, mask=mask) + + return cls(dx, units=_units_radians) + @_inplace_enabled(default=False) def compressed(self, inplace=False): """Return all non-masked values in a one dimensional data array. @@ -7761,6 +7873,45 @@ def second(self): """ return YMDhms(self, "second") + @property + def sparse_array(self): + """Return an independent `scipy` sparse array of the data. + + In-place changes to the returned sparse array do not affect + the underlying dask array. + + An `AttributeError` is raised if a sparse array representation + is not available. + + **Performance** + + `sparse_array` causes all delayed operations to be + computed. The returned sparse array is a deep copy of that + returned by created `compute`. + + .. versionadded:: 3.16.0 + + .. seealso:: `array` + + :Returns: + + An independent `scipy` sparse array of the data. + + **Examples** + + >>> from scipy.sparse import issparse + >>> issparse(d.sparse_array) + True + + """ + array = self.compute() + if issparse(array): + return array.copy() + + raise AttributeError( + "A sparse array representation of the data is not available" + ) + @_inplace_enabled(default=False) def uncompress(self, inplace=False): """Uncompress the data. @@ -9871,6 +10022,60 @@ def masked_all( d._set_dask(dx) return d + @_inplace_enabled(default=False) + def masked_values(self, value, rtol=None, atol=None, inplace=False): + """Mask using floating point equality. + + Masks the data where elements are approximately equal to the + given value. For integer types, exact equality is used. + + .. versionadded:: 3.16.0 + + .. seealso:: `mask` + + :Parameters: + + value: number + Masking value. + + {{rtol: number, optional}} + + {{atol: number, optional}} + + {{inplace: `bool`, optional}} + + :Returns: + + `{{class}}` or `None` + The result of masking the data where approximately + equal to *value*, or `None` if the operation was + in-place. + + **Examples** + + >>> d = {{package}}.{{class}}([1, 1.1, 2, 1.1, 3]) + >>> e = d.masked_values(1.1) + >>> print(e.array) + [1.0 -- 2.0 -- 3.0] + + """ + d = _inplace_enabled_define_and_cleanup(self) + + if rtol is None: + rtol = self._rtol + else: + rtol = float(rtol) + + if atol is None: + atol = self._atol + else: + atol = float(atol) + + dx = d.to_dask_array() + dx = da.ma.masked_values(dx, value, rtol=rtol, atol=atol) + d._set_dask(dx) + return d + @_inplace_enabled(default=False) @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") def mid_range( @@ -11168,7 +11373,7 @@ def tanh(self, inplace=False): .. versionadded:: 3.1.0 - .. seealso:: `arctanh`, `sinh`, `cosh`, `tan` + .. seealso:: `arctanh`, `sinh`, `cosh`, `tan`, `arctan2` :Parameters: @@ -11357,7 +11562,6 @@ def squeeze(self, axes=None, inplace=False, i=False): return d - # `arctan2`, AT2 seealso @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) def tan(self, inplace=False, i=False): diff --git a/cf/data/utils.py b/cf/data/utils.py index 82fe619f8d..747e703cfe 100644 --- a/cf/data/utils.py +++ b/cf/data/utils.py @@ -999,3 +999,50 @@ def parse_weights(d, weights, axis=None): # Return the product of the weights components, which will be # broadcastable to d return reduce(mul, w) + + +def normalize_chunks(chunks, shape=None, dtype=None): + """Normalize chunks to tuple of tuples. + + The shape may contain sizes of ``nan``. This could occur when the + underlying data is compressed in a way which makes the shape + impossible to infer without actually uncompressing the data. + + If *shape* contains no ``nan`` sizes then this function is + identical to `dask.array.core.normalize_chunks`. If it does, then + the output chunks for each such axis will be ``(nan,)``. + + .. versionadded 3.16.0 + + :Parameters: + + chunks: tuple, int, dict, or string + The chunks to be normalized. See + `dask.array.core.normalize_chunks` for details. + + shape: `tuple` + The shape of the data. + + dtype: data-type + The data-type for the data. + + :Returns: + + `tuple` + The normalized chunks. + + """ + from math import isnan, nan + + from dask.array.core import normalize_chunks + + if not any(map(isnan, shape)): + return normalize_chunks(chunks, shape=shape, dtype=dtype) + + out = [ + (nan,) + if isnan(size) + else normalize_chunks(chunk, shape=(size,), dtype=dtype)[0] + for chunk, size in zip(chunks, shape) + ] + return tuple(out) diff --git a/cf/dimensioncoordinate.py b/cf/dimensioncoordinate.py index c88fec0f14..347a0d7dd5 100644 --- a/cf/dimensioncoordinate.py +++ b/cf/dimensioncoordinate.py @@ -110,14 +110,6 @@ def __init__( if chars is not None: self._set_component("cell_characteristics", chars, copy=False) - def __repr__(self): - """Called by the `repr` built-in function. - - x.__repr__() <==> repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", ">> h = f._conform_for_data_broadcasting(g) - - """ - - other = self._conform_for_assignment(other, check_coordinates=True) - - # Remove leading size one dimensions - ndiff = other.ndim - self.ndim - if ndiff > 0 and set(other.shape[:ndiff]) == set((1,)): - for i in range(ndiff): - other = other.squeeze(0) - - return other - - @_manage_log_level_via_verbosity - def _equivalent_construct_data( - self, - field1, - key0=None, - key1=None, - s=None, - t=None, - atol=None, - rtol=None, - verbose=None, - axis_map=None, - ): - """True if the field has equivalent construct data to another. - - Two real numbers ``x`` and ``y`` are considered equal if - ``|x-y|<=atol+rtol|y|``, where ``atol`` (the tolerance on absolute - differences) and ``rtol`` (the tolerance on relative differences) - are positive, typically very small numbers. See the *atol* and - *rtol* parameters. - - :Parameters: - - key0: `str` - - key1: `str` - - field1: `Field` - - s: `dict`, optional - - t: `dict`, optional - - atol: `float`, optional - The tolerance on absolute differences between real - numbers. The default value is set by the `atol` function. - - rtol: `float`, optional - The tolerance on relative differences between real - numbers. The default value is set by the `rtol` function. - - {{verbose: `int` or `str` or `None`, optional}} - - """ - item0 = self.constructs[key0] - item1 = field1.constructs[key1] - - if item0.has_data() != item1.has_data(): - if is_log_level_info(logger): - logger.info( - f"{self.__class__.__name__}: Only one item has data" - ) # pragma: no cover - - return False - - if not item0.has_data(): - # Neither field has a data array - return True - - if item0.size != item1.size: - if is_log_level_info(logger): - logger.info( - f"{self.__class__.__name__}: Different metadata construct " - f"data array size: {item0.size} != {item1.size}" - ) # pragma: no cover - - return False - - if item0.ndim != item1.ndim: - if is_log_level_info(logger): - logger.info( - f"{self.__class__.__name__}: Different data array ranks " - f"({item0.ndim}, {item1.ndim})" - ) # pragma: no cover - - return False - - axes0 = self.get_data_axes(key0, default=()) - axes1 = field1.get_data_axes(key1, default=()) - - if s is None: - s = self.analyse_items() - if t is None: - t = field1.analyse_items() - - transpose_axes = [] - if axis_map is None: - for axis0 in axes0: - axis1 = t["id_to_axis"].get(s["axis_to_id"][axis0], None) - if axis1 is None: - if is_log_level_info(logger): - # TODO: improve message here (make user friendly): - logger.info( - "t['id_to_axis'] does not have a key " - f"s['axis_to_id'][axis0] for " - f"{self.__class__.__name__}" - ) # pragma: no cover - - return False - - transpose_axes.append(axes1.index(axis1)) - else: - for axis0 in axes0: - axis1 = axis_map.get(axis0) - if axis1 is None: - if is_log_level_info(logger): - # TODO: improve message here (make user friendly): - logger.info( - f"axis_map[axis0] is None for {self.__class__.__name__}" - ) # pragma: no cover - - return False - - transpose_axes.append(axes1.index(axis1)) - - copy1 = True - - if transpose_axes != list(range(item1.ndim)): - if copy1: - item1 = item1.copy() - copy1 = False - - item1.transpose(transpose_axes, inplace=True) - - if item0.shape != item1.shape: - # add traceback TODO - return False - - flip_axes = [ - i - for i, (axis1, axis0) in enumerate(zip(axes1, axes0)) - if field1.direction(axis1) != self.direction(axis0) - ] - - if flip_axes: - if copy1: - item1 = item1.copy() - copy1 = False - - item1.flip(flip_axes, inplace=True) - - if not item0._equivalent_data( - item1, rtol=rtol, atol=atol, verbose=verbose - ): - # add traceback TODO - return False - - return True - - # ---------------------------------------------------------------- - # Worker functions for weights - # ---------------------------------------------------------------- - def _weights_area_XY( - self, - comp, - weights_axes, - auto=False, - measure=False, - radius=None, - methods=False, - ): - """Calculate area weights from X and Y dimension coordinate - constructs. - - :Parameters: - - measure: `bool` - If True then make sure that the weights represent true - cell sizes. - - methods: `bool`, optional - If True then add a description of the method used to - create the weights to the *comp* dictionary, as opposed to - the actual weights. - - :Returns: - - `bool` or `None` - - """ - xkey, xcoord = self.dimension_coordinate( - "X", item=True, default=(None, None) - ) - ykey, ycoord = self.dimension_coordinate( - "Y", item=True, default=(None, None) - ) - - if xcoord is None or ycoord is None: - if auto: - return - - raise ValueError( - "No unique 'X' and 'Y' dimension coordinate constructs for " - "calculating area weights" - ) - - if xcoord.Units.equivalent( - Units("radians") - ) and ycoord.Units.equivalent(Units("radians")): - pass - elif xcoord.Units.equivalent( - Units("metres") - ) and ycoord.Units.equivalent(Units("metres")): - radius = None - else: - if auto: - return - - raise ValueError( - "Insufficient coordinate constructs for calculating " - "area weights" - ) - - xaxis = self.get_data_axes(xkey)[0] - yaxis = self.get_data_axes(ykey)[0] - - for axis in (xaxis, yaxis): - if axis in weights_axes: - if auto: - return - - raise ValueError( - "Multiple weights specifications for " - f"{self.constructs.domain_axis_identity(axis)!r} axis" - ) - - if measure and radius is not None: - radius = self.radius(default=radius) - - if measure or xcoord.size > 1: - if not xcoord.has_bounds(): - if auto: - return - - raise ValueError( - "Can't create area weights: No bounds for " - f"{xcoord.identity()!r} axis" - ) - - if methods: - comp[(xaxis,)] = "linear " + xcoord.identity() - else: - cells = xcoord.cellsize - if xcoord.Units.equivalent(Units("radians")): - cells.Units = _units_radians - if measure: - cells *= radius - cells.override_units(radius.Units, inplace=True) - else: - cells.Units = Units("metres") - - comp[(xaxis,)] = cells - - weights_axes.add(xaxis) - - if measure or ycoord.size > 1: - if not ycoord.has_bounds(): - if auto: - return - - raise ValueError( - "Can't create area weights: No bounds for " - f"{ycoord.identity()!r} axis" - ) - - if ycoord.Units.equivalent(Units("radians")): - ycoord = ycoord.clip(-90, 90, units=Units("degrees")) - ycoord.sin(inplace=True) - - if methods: - comp[(yaxis,)] = "linear sine " + ycoord.identity() - else: - cells = ycoord.cellsize - if measure: - cells *= radius - - comp[(yaxis,)] = cells - else: - if methods: - comp[(yaxis,)] = "linear " + ycoord.identity() - else: - cells = ycoord.cellsize - comp[(yaxis,)] = cells - - weights_axes.add(yaxis) - - return True - - def _weights_data( - self, - w, - comp, - weights_axes, - axes=None, - data=False, - components=False, - methods=False, - ): - """Creates weights for the field construct's data array. - - :Parameters: - - methods: `bool`, optional - If True then add a description of the method used to - create the weights to the *comp* dictionary, as - opposed to the actual weights. - - """ - # -------------------------------------------------------- - # Data weights - # -------------------------------------------------------- - field_data_axes = self.get_data_axes() - - if axes is not None: - if isinstance(axes, (str, int)): - axes = (axes,) - - if len(axes) != w.ndim: - raise ValueError( - "'axes' parameter must provide an axis identifier " - f"for each weights data dimension. Got {axes!r} for " - f"{w.ndim} dimension(s)." - ) - - iaxes = [ - field_data_axes.index(self.domain_axis(axis, key=True)) - for axis in axes - ] - - if data: - for i in range(self.ndim): - if i not in iaxes: - w = w.insert_dimension(position=i) - iaxes.insert(i, i) - - w = w.transpose(iaxes) - - if w.ndim > 0: - while w.shape[0] == 1: - w = w.squeeze(0) - - else: - iaxes = list(range(self.ndim - w.ndim, self.ndim)) - - if not (components or methods): - if not self._is_broadcastable(w.shape): - raise ValueError( - f"The 'Data' weights (shape {w.shape}) are not " - "broadcastable to the field construct's data " - f"(shape {self.shape})." - ) - - axes0 = field_data_axes[self.ndim - w.ndim :] - else: - axes0 = [field_data_axes[i] for i in iaxes] - - for axis0 in axes0: - if axis0 in weights_axes: - raise ValueError( - "Multiple weights specified for " - f"{self.constructs.domain_axis_identity(axis0)!r} axis" - ) - - if methods: - comp[tuple(axes0)] = "custom data" - else: - comp[tuple(axes0)] = w - - weights_axes.update(axes0) - - return True - - def _weights_field(self, fields, comp, weights_axes, methods=False): - """Creates a weights field.""" - s = self.analyse_items() - - domain_axes = self.domain_axes(todict=True) - # domain_axes_size_1 = self.domain_axes(filter_by_size=(1,), todict=True) - - for w in fields: - t = w.analyse_items() - # TODO CHECK this with org - - if t["undefined_axes"]: - # if set( - # t.domain_axes.filter_by_size(gt(1), view=True) - # ).intersection(t["undefined_axes"]): - w_domain_axes_1 = w.domain_axes( - filter_by_size=(1,), todict=True - ) - if set(w_domain_axes_1).intersection(t["undefined_axes"]): - raise ValueError( - f"Weights field {w} has at least one undefined " - f"domain axes: {w_domain_axes_1}." - ) - - w = w.squeeze() - - w_domain_axes = w.domain_axes(todict=True) - - axis1_to_axis0 = {} - - coordinate_references = self.coordinate_references(todict=True) - w_coordinate_references = w.coordinate_references(todict=True) - - for axis1 in w.get_data_axes(): - identity = t["axis_to_id"].get(axis1, None) - if identity is None: - raise ValueError( - "Weights field has unmatched, size > 1 " - f"{w.constructs.domain_axis_identity(axis1)!r} axis" - ) - - axis0 = s["id_to_axis"].get(identity, None) - if axis0 is None: - raise ValueError( - f"Weights field has unmatched, size > 1 {identity!r} " - "axis" - ) - - w_axis_size = w_domain_axes[axis1].get_size() - self_axis_size = domain_axes[axis0].get_size() - - if w_axis_size != self_axis_size: - raise ValueError( - f"Weights field has incorrectly sized {identity!r} " - f"axis ({w_axis_size} != {self_axis_size})" - ) - - axis1_to_axis0[axis1] = axis0 - - # Check that the defining coordinate data arrays are - # compatible - key0 = s["axis_to_coord"][axis0] - key1 = t["axis_to_coord"][axis1] - - if not self._equivalent_construct_data( - w, key0=key0, key1=key1, s=s, t=t - ): - raise ValueError( - f"Weights field has incompatible {identity!r} " - "coordinates" - ) - - # Still here? Then the defining coordinates have - # equivalent data arrays - - # If the defining coordinates are attached to - # coordinate references then check that those - # coordinate references are equivalent - refs0 = [ - key - for key, ref in coordinate_references.items() - if key0 in ref.coordinates() - ] - refs1 = [ - key - for key, ref in w_coordinate_references.items() - if key1 in ref.coordinates() - ] - - nrefs = len(refs0) - if nrefs > 1 or nrefs != len(refs1): - # The defining coordinate are associated with - # different numbers of coordinate references - equivalent_refs = False - elif not nrefs: - # Neither defining coordinate is associated with a - # coordinate reference - equivalent_refs = True - else: - # Each defining coordinate is associated with - # exactly one coordinate reference - equivalent_refs = self._equivalent_coordinate_references( - w, key0=refs0[0], key1=refs1[0], s=s, t=t - ) - - if not equivalent_refs: - raise ValueError( - "Input weights field has an incompatible " - "coordinate reference" - ) - - axes0 = tuple( - [axis1_to_axis0[axis1] for axis1 in w.get_data_axes()] - ) - - for axis0 in axes0: - if axis0 in weights_axes: - raise ValueError( - "Multiple weights specified for " - f"{self.constructs.domain_axis_identity(axis0)!r} " - "axis" - ) - - comp[tuple(axes0)] = w.data - - weights_axes.update(axes0) - - return True - - def _weights_field_scalar(self, methods=False): - """Return a scalar field of weights with long_name ``'weight'``. - - :Returns: - - `Field` - - """ - data = Data(1.0, "1") - - f = type(self)() - f.set_data(data, copy=False) - f.long_name = "weight" - f.comment = f"Weights for {self!r}" - - return f - - def _weights_geometry_area( - self, - domain_axis, - comp, - weights_axes, - auto=False, - measure=False, - radius=None, - great_circle=False, - return_areas=False, - methods=False, - ): - """Creates area weights for polygon geometry cells. - - .. versionadded:: 3.2.0 - - :Parameters: - - domain_axis : `str` or `None` - - measure: `bool` - If True then make sure that the weights represent true - cell sizes. - - methods: `bool`, optional - If True then add a description of the method used to - create the weights to the *comp* dictionary, as opposed to - the actual weights. - - :Returns: - - `bool` or `Data` - - """ - axis, aux_X, aux_Y, aux_Z = self._weights_yyy( - domain_axis, "polygon", methods=methods, auto=auto - ) - - if axis is None: - if auto: - return False - - if domain_axis is None: - raise ValueError("No polygon geometries") - - raise ValueError( - "No polygon geometries for " - f"{self.constructs.domain_axis_identity(domain_axis)!r} axis" - ) - - if axis in weights_axes: - if auto: - return False - - raise ValueError( - "Multiple weights specifications for " - f"{self.constructs.domain_axis_identity(axis)!r} axis" - ) - - # Check for interior rings - interior_ring_X = aux_X.get_interior_ring(None) - interior_ring_Y = aux_Y.get_interior_ring(None) - if interior_ring_X is None and interior_ring_Y is None: - interior_ring = None - elif interior_ring_X is None: - raise ValueError( - "Can't find weights: X coordinates have missing " - "interior ring variable" - ) - elif interior_ring_Y is None: - raise ValueError( - "Can't find weights: Y coordinates have missing " - "interior ring variable" - ) - elif not interior_ring_X.data.equals(interior_ring_Y.data): - raise ValueError( - "Can't find weights: Interior ring variables for X and Y " - "coordinates have different data values" - ) - else: - interior_ring = interior_ring_X.data - if interior_ring.shape != aux_X.bounds.shape[:-1]: - raise ValueError( - "Can't find weights: Interior ring variables have " - f"incorrect shape. Got {interior_ring.shape}, expected " - f"{aux_X.bounds.shape[:-1]}" - ) - - x = aux_X.bounds.data - y = aux_Y.bounds.data - - if x.Units.equivalent(_units_metres) and y.Units.equivalent( - _units_metres - ): - # ---------------------------------------------------- - # Plane polygons defined by straight lines. - # - # Use the shoelace formula: - # https://en.wikipedia.org/wiki/Shoelace_formula - # - # Do this in preference to weights based on spherical - # polygons, which require the great circle assumption. - # ---------------------------------------------------- - spherical = False - - if methods: - comp[(axis,)] = "area plane polygon geometry" - return True - - y.Units = x.Units - - all_areas = (x[..., :-1] * y[..., 1:]).sum(-1, squeeze=True) - ( - x[..., 1:] * y[..., :-1] - ).sum(-1, squeeze=True) - - for i, (parts_x, parts_y) in enumerate(zip(x, y)): - for j, (nodes_x, nodes_y) in enumerate(zip(parts_x, parts_y)): - nodes_x = nodes_x.compressed() - nodes_y = nodes_y.compressed() - - if (nodes_x.size and nodes_x[0] != nodes_x[-1]) or ( - nodes_y.size and nodes_y[0] != nodes_y[-1] - ): - # First and last nodes of this polygon - # part are different => need to account - # for the "last" edge of the polygon that - # joins the first and last points. - all_areas[i, j] += x[-1] * y[0] - x[0] * y[-1] - - all_areas = all_areas.abs() * 0.5 - - elif x.Units.equivalent(_units_radians) and y.Units.equivalent( - _units_radians - ): - # ---------------------------------------------------- - # Spherical polygons defined by great circles - # - # The area of such a spherical polygon is given by the - # sum of the interior angles minus (N-2)pi, where N is - # the number of sides (Todhunter, - # https://en.wikipedia.org/wiki/Spherical_trigonometry#Spherical_polygons): - # - # Area of N-sided polygon on the unit sphere = - # \left(\sum _{n=1}^{N}A_{n}\right) - (N - 2)\pi - # - # where A_{n} denotes the n-th interior angle. - # ---------------------------------------------------- - spherical = True - - if not great_circle: - raise ValueError( - "Must set great_circle=True to allow the derivation of " - "area weights from spherical polygons composed from " - "great circle segments." - ) - - if methods: - comp[(axis,)] = "area spherical polygon geometry" - return True - - x.Units = _units_radians - y.Units = _units_radians - - interior_angle = self._weights_interior_angle(x, y) - - # Find the number of edges of each polygon (note that - # this number may be one too few, but we'll adjust for - # that later). - N = interior_angle.sample_size(-1, squeeze=True) - - all_areas = interior_angle.sum(-1, squeeze=True) - (N - 2) * np.pi - - for i, (parts_x, parts_y) in enumerate(zip(x, y)): - for j, (nodes_x, nodes_y) in enumerate(zip(parts_x, parts_y)): - nodes_x = nodes_x.compressed() - nodes_y = nodes_y.compressed() - - if (nodes_x.size and nodes_x[0] != nodes_x[-1]) or ( - nodes_y.size and nodes_y[0] != nodes_y[-1] - ): - # First and last nodes of this polygon - # part are different => need to account - # for the "last" edge of the polygon that - # joins the first and last points. - interior_angle = self._weights_interior_angle( - nodes_x[[0, -1]], nodes_y[[0, -1]] - ) - - all_areas[i, j] += interior_angle + np.pi - - area_min = all_areas.min() - if area_min < 0: - raise ValueError( - "A spherical polygon geometry part has negative area" - ) - else: - return False - - # Change the sign of areas for polygons that are interior - # rings - if interior_ring is not None: - all_areas.where(interior_ring, -all_areas, inplace=True) - - # Sum the areas of each part to get the total area of each - # cell - areas = all_areas.sum(-1, squeeze=True) - - if measure and spherical and aux_Z is not None: - # Multiply by radius squared, accounting for any Z - # coordinates, to get the actual area - z = aux_Z.get_data(None, _fill_value=False) - if z is None: - r = radius - else: - if not z.Units.equivalent(_units_metres): - raise ValueError( - "Z coordinates must have units equivalent to " - f"metres for area calculations. Got {z.Units!r}" - ) - - positive = aux_Z.get_property("positive", None) - if positive is None: - raise ValueError( - "Value of Z coordinate 'positive' property is not " - "defined" - ) - - if positive.lower() == "up": - r = radius + z - elif positive.lower() == "down": - r = radius - z - else: - raise ValueError( - "Bad value of Z coordinate 'positive' " - f"property: {positive!r}." - ) - - areas *= r**2 - - if return_areas: - return areas - - comp[(axis,)] = areas - - weights_axes.add(axis) - - return True - - def _weights_geometry_line( - self, - domain_axis, - comp, - weights_axes, - auto=False, - measure=False, - radius=None, - great_circle=False, - methods=False, - ): - """Creates line-length weights for line geometries. - - .. versionadded:: 3.2.0 - - :Parameters: - - measure: `bool` - If True then make sure that the weights represent true - cell sizes. - - methods: `bool`, optional - If True then add a description of the method used to - create the weights to the *comp* dictionary, as opposed to - the actual weights. - - """ - axis, aux_X, aux_Y, aux_Z = self._weights_yyy( - domain_axis, "line", methods=methods, auto=auto - ) - - if axis is None: - if auto: - return False - - if domain_axis is None: - raise ValueError("No line geometries") - - raise ValueError( - "No line geometries for " - f"{self.constructs.domain_axis_identity(domain_axis)!r} axis" - ) - - if axis in weights_axes: - if auto: - return False - - raise ValueError( - "Multiple weights specifications for " - f"{self.constructs.domain_axis_identity(axis)!r} axis" - ) - - x = aux_X.bounds.data - y = aux_Y.bounds.data - - if x.Units.equivalent(_units_metres) and y.Units.equivalent( - _units_metres - ): - # ---------------------------------------------------- - # Plane lines. - # - # Each line segment is the simple cartesian distance - # between two adjacent nodes. - # ---------------------------------------------------- - if methods: - comp[(axis,)] = "linear plane line geometry" - return True - - y.Units = x.Units - - delta_x = x.diff(axis=-1) - delta_y = y.diff(axis=-1) - - all_lengths = (delta_x**2 + delta_y**2) ** 0.5 - all_lengths = all_lengths.sum(-1, squeeze=True) - - elif x.Units.equivalent(_units_radians) and y.Units.equivalent( - _units_radians - ): - # ---------------------------------------------------- - # Spherical lines. - # - # Each line segment is a great circle arc between two - # adjacent nodes. - # - # The length of the great circle arc is the the - # interior angle multiplied by the radius. The - # interior angle is calculated with a special case of - # the Vincenty formula: - # https://en.wikipedia.org/wiki/Great-circle_distance - # ---------------------------------------------------- - if not great_circle: - raise ValueError( - "Must set great_circle=True to allow the derivation " - "of line-length weights from great circle segments." - ) - - if methods: - comp[(axis,)] = "linear spherical line geometry" - return True - - x.Units = _units_radians - y.Units = _units_radians - - interior_angle = self._weights_interior_angle(x, y) - if interior_angle.min() < 0: - raise ValueError( - "A spherical line geometry segment has " - f"negative length: {interior_angle.min() * radius!r}" - ) - - all_lengths = interior_angle.sum(-1, squeeze=True) - - if measure: - all_lengths *= radius - else: - return False - - # Sum the lengths of each part to get the total length of - # each cell - lengths = all_lengths.sum(-1, squeeze=True) - - comp[(axis,)] = lengths - - weights_axes.add(axis) - - return True - - def _weights_geometry_volume( - self, - comp, - weights_axes, - auto=False, - measure=False, - radius=None, - great_circle=False, - methods=False, - ): - """Creates volume weights for polygon geometry cells. - - .. versionadded:: 3.2.0 - - :Parameters: - - measure: `bool` - If True then make sure that the weights represent true - cell sizes. - - methods: `bool`, optional - If True then add a description of the method used to - create the weights to the *comp* dictionary, as opposed to - the actual weights. - - """ - axis, aux_X, aux_Y, aux_Z = self._weights_yyy( - "polygon", methods=methods, auto=auto - ) - - if axis is None and auto: - return False - - if axis in weights_axes: - if auto: - return False - - raise ValueError( - "Multiple weights specifications for " - f"{self.constructs.domain_axis_identity(axis)!r} axis" - ) - - x = aux_X.bounds.data - y = aux_Y.bounds.data - z = aux_Z.bounds.data - - if not z.Units.equivalent(_units_metres): - if auto: - return False - - raise ValueError( - "Z coordinate bounds must have units equivalent to " - f"metres for volume calculations. Got {z.Units!r}." - ) - - if not methods: - # Initialise cell volumes as the cell areas - volumes = self._weights_geometry_area( - comp, - weights_axes, - auto=auto, - measure=measure, - radius=radius, - great_circle=great_circle, - methods=False, - return_areas=True, - ) - - if measure: - delta_z = abs(z[..., 1] - z[..., 0]) - delta_z.squeeze(axis=-1, inplace=True) - - if x.Units.equivalent(_units_metres) and y.Units.equivalent( - _units_metres - ): - # ---------------------------------------------------- - # Plane polygons defined by straight lines. - # - # Do this in preference to weights based on spherical - # polygons, which require the great circle assumption. - # ---------------------------------------------------- - if methods: - comp[(axis,)] = "volume plane polygon geometry" - return True - - if measure: - volumes *= delta_z - - elif x.Units.equivalent(_units_radians) and y.Units.equivalent( - _units_radians - ): - # ---------------------------------------------------- - # Spherical polygons defined by great circles - # - # The area of such a spherical polygon is given by the - # sum of the interior angles minus (N-2)pi, where N is - # the number of sides (Todhunter): - # - # https://en.wikipedia.org/wiki/Spherical_trigonometry#Area_and_spherical_excess - # - # The interior angle of a side is calculated with a - # special case of the Vincenty formula: - # https://en.wikipedia.org/wiki/Great-circle_distance - # ---------------------------------------------------- - if not great_circle: - raise ValueError( - "Must set great_circle=True to allow the derivation " - "of volume weights from spherical polygons composed " - "from great circle segments." - ) - - if methods: - comp[(axis,)] = "volume spherical polygon geometry" - return True - - if measure: - r = radius - - # actual_volume = - # [actual_area/(4*pi*r**2)] - # * (4/3)*pi*[(r+delta_z)**3 - r**3)] - volumes *= ( - delta_z**3 / (3 * r**2) + delta_z**2 / r + delta_z - ) - else: - raise ValueError( - "X and Y coordinate bounds must both have units " - "equivalent to either metres, for plane polygon, or radians, " - "for spherical polygon, volume calculations. Got " - f"{x.Units!r} and {y.Units!r}." - ) - - comp[(axis,)] = volumes - - weights_axes.add(axis) - - return True - - def _weights_interior_angle(self, data_lambda, data_phi): - r"""Find the interior angle between each adjacent pair of - geometry nodes defined on a sphere. - - The interior angle of two points on the sphere is calculated with - a special case of the Vincenty formula - (https://en.wikipedia.org/wiki/Great-circle_distance): - - \Delta \sigma =\arctan { - \frac {\sqrt {\left(\cos \phi _{2}\sin(\Delta \lambda )\right)^{2} + - \left(\cos \phi _{1}\sin \phi _{2} - - \sin \phi _{1}\cos \phi _{2}\cos(\Delta \lambda )\right)^{2} } } - {\sin \phi _{1}\sin \phi _{2} + - \cos \phi _{1}\cos \phi _{2}\cos(\Delta \lambda )} - } - - :Parameters: - - data_lambda: `Data` - Longitudes. Must have units of radians, which is not - checked. - - data_phi: `Data` - Latitudes. Must have units of radians, which is not - checked. - - :Returns: - - `Data` - The interior angles in units of radians. - - """ - delta_lambda = data_lambda.diff(axis=-1) - - cos_phi = data_phi.cos() - sin_phi = data_phi.sin() - - cos_phi_1 = cos_phi[..., :-1] - cos_phi_2 = cos_phi[..., 1:] - - sin_phi_1 = sin_phi[..., :-1] - sin_phi_2 = sin_phi[..., 1:] - - cos_delta_lambda = delta_lambda.cos() - sin_delta_lambda = delta_lambda.sin() - - numerator = ( - (cos_phi_2 * sin_delta_lambda) ** 2 - + ( - cos_phi_1 * sin_phi_2 - - sin_phi_1 * cos_phi_2 * cos_delta_lambda - ) - ** 2 - ) ** 0.5 - - denominator = ( - sin_phi_1 * sin_phi_2 + cos_phi_1 * cos_phi_2 * cos_delta_lambda - ) - - # TODO RuntimeWarning: overflow encountered in true_divide comes from - # numerator/denominator with missing values - - interior_angle = (numerator / denominator).arctan() - - interior_angle.override_units(_units_1, inplace=True) - - return interior_angle - - def _weights_linear( - self, - axis, - comp, - weights_axes, - auto=False, - measure=False, - methods=False, - ): - """1-d linear weights from dimension coordinate constructs. - - :Parameters: - - axis: `str` - - comp: `dict` - - weights_axes: `set` - - auto: `bool` - - measure: `bool` - If True then make sure that the weights represent true - cell sizes. - - methods: `bool`, optional - If True then add a description of the method used to - create the weights to the *comp* dictionary, as opposed to - the actual weights. - - :Returns: - - `bool` - - """ - da_key = self.domain_axis(axis, key=True, default=None) - if da_key is None: - if auto: - return False - - raise ValueError( - "Can't create weights: Can't find domain axis " - f"matching {axis!r}" - ) - - dim = self.dimension_coordinate(filter_by_axis=(da_key,), default=None) - if dim is None: - if auto: - return False - - raise ValueError( - f"Can't create linear weights for {axis!r} axis: Can't find " - "dimension coordinate construct." - ) - - if not measure and dim.size == 1: - return False - - if da_key in weights_axes: - if auto: - return False - - raise ValueError( - f"Can't create linear weights for {axis!r} axis: Multiple " - "axis specifications" - ) - - if not dim.has_bounds(): - # Dimension coordinate has no bounds - if auto: - return False - - raise ValueError( - f"Can't create linear weights for {axis!r} axis: No bounds" - ) - else: - # Bounds exist - if methods: - comp[ - (da_key,) - ] = "linear " + self.constructs.domain_axis_identity(da_key) - else: - comp[(da_key,)] = dim.cellsize - - weights_axes.add(da_key) + return other - return True + def _conform_for_data_broadcasting(self, other): + """Conforms the field with another, ready for data broadcasting. - def _weights_measure( - self, measure, comp, weights_axes, methods=False, auto=False - ): - """Cell measure weights. + Note that the other field, *other*, is not changed in-place. :Parameters: - methods: `bool`, optional - If True then add a description of the method used to - create the weights to the *comp* dictionary, as opposed to - the actual weights. + other: `Field` + The field to conform. :Returns: - `bool` - - """ - m = self.cell_measures(filter_by_measure=(measure,), todict=True) - len_m = len(m) - - if not len_m: - if measure == "area": - return False - - if auto: - return - - raise ValueError( - f"Can't find weights: No {measure!r} cell measure" - ) + `Field` + The conformed version of *other*. - elif len_m > 1: - if auto: - return False + **Examples** - raise ValueError( - f"Can't find weights: Multiple {measure!r} cell measures" - ) + >>> h = f._conform_for_data_broadcasting(g) - key, clm = m.popitem() + """ - clm_axes0 = self.get_data_axes(key) + other = self._conform_for_assignment(other, check_coordinates=True) - clm_axes = tuple( - [axis for axis, n in zip(clm_axes0, clm.data.shape) if n > 1] - ) + # Remove leading size one dimensions + ndiff = other.ndim - self.ndim + if ndiff > 0 and set(other.shape[:ndiff]) == set((1,)): + for i in range(ndiff): + other = other.squeeze(0) - for axis in clm_axes: - if axis in weights_axes: - if auto: - return False + return other - raise ValueError( - "Multiple weights specifications for " - f"{self.constructs.domain_axis_identity(axis)!r} axis" - ) + @_manage_log_level_via_verbosity + def _equivalent_construct_data( + self, + field1, + key0=None, + key1=None, + s=None, + t=None, + atol=None, + rtol=None, + verbose=None, + axis_map=None, + ): + """True if the field has equivalent construct data to another. - clm = clm.get_data(_fill_value=False).copy() - if clm_axes != clm_axes0: - iaxes = [clm_axes0.index(axis) for axis in clm_axes] - clm.squeeze(iaxes, inplace=True) + Two real numbers ``x`` and ``y`` are considered equal if + ``|x-y|<=atol+rtol|y|``, where ``atol`` (the tolerance on absolute + differences) and ``rtol`` (the tolerance on relative differences) + are positive, typically very small numbers. See the *atol* and + *rtol* parameters. - if methods: - comp[tuple(clm_axes)] = measure + " cell measure" - else: - comp[tuple(clm_axes)] = clm + :Parameters: - weights_axes.update(clm_axes) + key0: `str` - return True + key1: `str` - def _weights_scale(self, w, scale): - """Scale the weights so that they are <= scale. + field1: `Field` - :Parameters: + s: `dict`, optional - w: `Data` - The weights to be scaled. + t: `dict`, optional - scale: number or `None` - The maximum value of the scaled weights. If `None` then no - scaling is applied. + atol: `float`, optional + The tolerance on absolute differences between real + numbers. The default value is set by the `atol` function. - :Returns: + rtol: `float`, optional + The tolerance on relative differences between real + numbers. The default value is set by the `rtol` function. - `Data` - The scaled weights. + {{verbose: `int` or `str` or `None`, optional}} """ - if scale is None: - return w - - if scale <= 0: - raise ValueError( - "Can't set 'scale' parameter to a negative number. " - f"Got {scale!r}" - ) - - w = w / w.max() - if scale != 1: - w = w * scale - - return w + item0 = self.constructs[key0] + item1 = field1.constructs[key1] - def _weights_yyy( - self, domain_axis, geometry_type, methods=False, auto=False - ): - """Checks whether weights can be created for given coordinates. + if item0.has_data() != item1.has_data(): + if is_log_level_info(logger): + logger.info( + f"{self.__class__.__name__}: Only one item has data" + ) # pragma: no cover - .. versionadded:: 3.2.0 + return False - :Parameters: + if not item0.has_data(): + # Neither field has a data array + return True - domain_axis : `str` or `None` + if item0.size != item1.size: + if is_log_level_info(logger): + logger.info( + f"{self.__class__.__name__}: Different metadata construct " + f"data array size: {item0.size} != {item1.size}" + ) # pragma: no cover - geometry_type: `str` - Either ``'polygon'`` or ``'line'``. + return False - auto: `bool` + if item0.ndim != item1.ndim: + if is_log_level_info(logger): + logger.info( + f"{self.__class__.__name__}: Different data array ranks " + f"({item0.ndim}, {item1.ndim})" + ) # pragma: no cover - :Returns: + return False - `tuple` + axes0 = self.get_data_axes(key0, default=()) + axes1 = field1.get_data_axes(key1, default=()) - """ - aux_X = None - aux_Y = None - aux_Z = None - x_axis = None - y_axis = None - z_axis = None - - auxiliary_coordinates_1d = self.auxiliary_coordinates( - filter_by_naxes=(1,), todict=True - ) + if s is None: + s = self.analyse_items() + if t is None: + t = field1.analyse_items() - for key, aux in auxiliary_coordinates_1d.items(): - if aux.get_geometry(None) != geometry_type: - continue + transpose_axes = [] + if axis_map is None: + for axis0 in axes0: + axis1 = t["id_to_axis"].get(s["axis_to_id"][axis0], None) + if axis1 is None: + if is_log_level_info(logger): + # TODO: improve message here (make user friendly): + logger.info( + "t['id_to_axis'] does not have a key " + f"s['axis_to_id'][axis0] for " + f"{self.__class__.__name__}" + ) # pragma: no cover - if aux.X: - aux_X = aux.copy() - x_axis = self.get_data_axes(key)[0] - if domain_axis is not None and x_axis != domain_axis: - aux_X = None - continue - elif aux.Y: - aux_Y = aux.copy() - y_axis = self.get_data_axes(key)[0] - if domain_axis is not None and y_axis != domain_axis: - aux_Y = None - continue - elif aux.Z: - aux_Z = aux.copy() - z_axis = self.get_data_axes(key)[0] - if domain_axis is not None and z_axis != domain_axis: - aux_Z = None - continue + return False - if aux_X is None or aux_Y is None: - if auto: - return (None,) * 4 + transpose_axes.append(axes1.index(axis1)) + else: + for axis0 in axes0: + axis1 = axis_map.get(axis0) + if axis1 is None: + if is_log_level_info(logger): + # TODO: improve message here (make user friendly): + logger.info( + f"axis_map[axis0] is None for {self.__class__.__name__}" + ) # pragma: no cover - raise ValueError( - "Can't create weights: Need both X and Y nodes to " - f"calculate {geometry_type} geometry weights" - ) + return False - if x_axis != y_axis: - if auto: - return (None,) * 4 + transpose_axes.append(axes1.index(axis1)) - raise ValueError( - "Can't create weights: X and Y nodes span different " - "domain axes" - ) + copy1 = True - axis = x_axis + if transpose_axes != list(range(item1.ndim)): + if copy1: + item1 = item1.copy() + copy1 = False - if aux_X.get_bounds(None) is None or aux_Y.get_bounds(None) is None: - # Not both X and Y coordinates have bounds - if auto: - return (None,) * 4 + item1.transpose(transpose_axes, inplace=True) - raise ValueError("Not both X and Y coordinates have bounds") + if item0.shape != item1.shape: + if is_log_level_info(logger): + logger.info( + f"{self.__class__.__name__}: Different shapes: " + f"{item0.shape} != {item1.shape}" + ) # pragma: no cover - if aux_X.bounds.shape != aux_Y.bounds.shape: - if auto: - return (None,) * 4 + return False - raise ValueError( - "Can't find weights: X and Y geometry coordinate bounds " - "must have the same shape. " - f"Got {aux_X.bounds.shape} and {aux_Y.bounds.shape}" - ) + flip_axes = [ + i + for i, (axis1, axis0) in enumerate(zip(axes1, axes0)) + if field1.direction(axis1) != self.direction(axis0) + ] - if aux_Z is None: - for key, aux in auxiliary_coordinates_1d.items(): - if aux.Z: - aux_Z = aux.copy() - z_axis = self.get_data_axes(key)[0] + if flip_axes: + if copy1: + item1 = item1.copy() + copy1 = False - # Check Z coordinates - if aux_Z is not None: - if z_axis != x_axis: - if auto: - return (None,) * 4 + item1.flip(flip_axes, inplace=True) - raise ValueError( - "Z coordinates span different domain axis to X and Y " - "geometry coordinates" - ) + if not item0._equivalent_data( + item1, rtol=rtol, atol=atol, verbose=verbose + ): + if is_log_level_info(logger): + logger.info( + f"{self.__class__.__name__}: Non-equivalent data" + ) # pragma: no cover - return axis, aux_X, aux_Y, aux_Z + return False - # ---------------------------------------------------------------- - # End of worker functions for weights - # ---------------------------------------------------------------- + return True - # ---------------------------------------------------------------- - # Attributes - # ---------------------------------------------------------------- @property def DSG(self): """True if the field contains a collection of discrete sampling @@ -4391,17 +3132,18 @@ def weights( ): """Return weights for the data array values. - The weights are those used during a statistical collapse of the - data. For example when computing a area weight average. + The weights are those used during a statistical collapse of + the data. For example when computing an area weight average. Weights for any combination of axes may be returned. Weights are either derived from the field construct's metadata - (such as coordinate cell sizes) or provided explicitly in the form - of other `Field` constructs. In any case, the outer product of - these weights components is returned in a field which is - broadcastable to the original field (see the *components* parameter - for returning the components individually). + (such as coordinate cell sizes) or provided explicitly in the + form of other `Field` constructs. In any case, the outer + product of these weights components is returned in a field + which is broadcastable to the original field (see the + *components* parameter for returning the components + individually). By default null, equal weights are returned. @@ -4420,56 +3162,65 @@ def weights( greater than 1, raising an exception if this is not possible (this is the default).; - * **Type 2** will always succeed in creating weights for - all axes of the field, even if some of those weights are - null. + * **Type 2** will always succeed in creating weights + for all axes of the field, even if some of those + weights are null. * **Type 3** allows particular types of weights to be - defined for particular axes, and an exception will be - raised if it is not possible to the create weights. + defined for particular axes, and an exception will + be raised if it is not possible to create the + weights. .. **Type 1** and **Type 2** come at the expense of not - always being able to control exactly how the weights are - created (although which methods were used can be inspected - with use of the *methods* parameter). + always being able to control exactly how the weights + are created (although which methods were used can be + inspected with use of the *methods* parameter). * **Type 1**: *weights* may be: - ========== ============================================ + ========== ======================================== *weights* Description - ========== ============================================ - `True` This is the default. Weights are created for - all axes (or a subset of them, see the - *axes* parameter). Set the *methods* - parameter to find out how the weights were - actually created. - - The weights components are created for axes - of the field by one or more of the following - methods, in order of preference, + ========== ======================================== + `True` This is the default. Weights are created + for all axes (or a subset of them, see + the *axes* parameter). Set the *methods* + parameter to find out how the weights + were actually created. + + The weights components are created for + axes of the field by one or more of the + following methods, in order of + preference, 1. Volume cell measures 2. Area cell measures - 3. Area calculated from (grid) latitude - and (grid) longitude dimension - coordinate constructs with bounds - 4. Cell sizes of dimension coordinate + 3. Area calculated from X and Y + dimension coordinate constructs + with bounds + 4. Area calculated from 1-d auxiliary + coordinate constructs for geomtries + or a UGRID mesh topology. + 5. Length calculated from 1-d auxiliary + coordinate constructs for geometries + or a UGRID mesh topology. + 6. Cell sizes of dimension coordinate constructs with bounds - 5. Equal weights + 7. Equal weights and the outer product of these weights - components is returned in a field constructs - which is broadcastable to the original field - construct (see the *components* parameter). - ========== ============================================ + components is returned in a field + constructs which is broadcastable to the + original field construct (see the + *components* parameter). + ========== ======================================== * **Type 2**: *weights* may be one of: - ========== ============================================ + ========== ======================================== *weights* Description - ========== ============================================ + ========== ======================================== `None` Equal weights for all axes. `False` Equal weights for all axes. @@ -4479,23 +3230,32 @@ def weights( construct's data, unless the *axes* parameter is also set. - `dict` Explicit weights in a dictionary of the form - that is returned from a call to the + `dict` Explicit weights in a dictionary of the + form that is returned from a call to the `weights` method with ``component=True`` - ========== ============================================ + ========== ======================================== * **Type 3**: *weights* may be one, or a sequence, of: ============ ========================================== *weights* Description ============ ========================================== - ``'area'`` Cell area weights from the field - construct's area cell measure construct - or, if one doesn't exist, from (grid) - latitude and (grid) longitude dimension - coordinate constructs. Set the *methods* - parameter to find out how the weights were - actually created. + ``'area'`` Cell area weights. The weights + components are created for axes of the + field by the following methods, in + order of preference, + + 1. Area cell measures. + 2. X and Y dimension coordinate + constructs with bounds. + 3. X and Y 1-d auxiliary coordinate + constructs for polygon cells + defined by geometries or a UGRID + mesh topology. + + Set the *methods* parameter to find + out how the weights were actually + created. ``'volume'`` Cell volume weights from the field construct's volume cell measure construct. @@ -4510,8 +3270,8 @@ def weights( ============ ========================================== If *weights* is a sequence of any combination of the - above then the returned field contains the outer product - of the weights defined by each element of the + above then the returned field contains the outer + product of the weights defined by each element of the sequence. The ordering of the sequence is irrelevant. *Parameter example:* @@ -4521,15 +3281,15 @@ def weights( height: ``f.weights(['area', 'Z'])``. scale: number, optional - If set to a positive number then scale the weights so that - they are less than or equal to that number. If weights - components have been requested (see the *components* - parameter) then each component is scaled independently of - the others. + If set to a positive number then scale the weights so + that they are less than or equal to that number. If + weights components have been requested (see the + *components* parameter) then each component is scaled + independently of the others. *Parameter example:* - To scale all weights so that they lie between 0 and 1: - ``scale=1``. + To scale all weights so that they lie between 0 and + 1: ``scale=1``. measure: `bool`, optional Create weights that are cell measures, i.e. which @@ -4561,64 +3321,54 @@ def weights( the cell volumes will be calculated using the size of the vertical coordinate cells. - radius: optional - Specify the radius used for calculating the areas of - cells defined in spherical polar coordinates. The - radius is that which would be returned by this call of - the field construct's `~cf.Field.radius` method: - ``f.radius(radius)``. See the `cf.Field.radius` for - details. - - By default *radius* is ``'earth'`` which means that if - and only if the radius can not found from the datums - of any coordinate reference constructs, then the - default radius taken as 6371229 metres. + {{radius: optional}} components: `bool`, optional - If True then a dictionary of orthogonal weights components - is returned instead of a field. Each key is a tuple of - integers representing axis positions in the field - construct's data, with corresponding values of weights in - `Data` objects. The axes of weights match the axes of the - field construct's data array in the order given by their - dictionary keys. + If True then a dictionary of orthogonal weights + components is returned instead of a field. Each key is + a tuple of integers representing axis positions in the + field construct's data, with corresponding values of + weights in `Data` objects. The axes of weights match + the axes of the field construct's data array in the + order given by their dictionary keys. methods: `bool`, optional - If True, then return a dictionary describing methods used - to create the weights. + If True, then return a dictionary describing methods + used to create the weights. data: `bool`, optional - If True then return the weights in a `Data` instance that - is broadcastable to the original data. + If True then return the weights in a `Data` instance + that is broadcastable to the original data. .. versionadded:: 3.1.0 great_circle: `bool`, optional - If True then allow, if required, the derivation of i) area - weights from polygon geometry cells by assuming that each - cell part is a spherical polygon composed of great circle - segments; and ii) and the derivation of line-length - weights from line geometry cells by assuming that each - line part is composed of great circle segments. + If True then allow, if required, the derivation of i) + area weights from polygon cells by assuming that each + cell part is a spherical polygon composed of great + circle segments; and ii) the derivation of + line-length weights line cells by assuming that each + line part is composed of great circle segments. Only + applies to geometry and UGRID cells. .. versionadded:: 3.2.0 axes: (sequence of) `int` or `str`, optional - Modify the behaviour when *weights* is `True` or a `Data` - instance. Ignored for any other value the *weights* - parameter. + Modify the behaviour when *weights* is `True` or a + `Data` instance. Ignored for any other value the + *weights* parameter. - If *weights* is `True` then weights are created only for - the specified axes (as opposed to all + If *weights* is `True` then weights are created only + for the specified axes (as opposed to all axes). I.e. ``weight=True, axes=axes`` is identical to ``weights=axes``. - If *weights* is a `Data` instance then the specified axes - identify each dimension of the given weights. If the - weights do not broadcast to the field construct's data - then setting the *axes* parameter is required so that the - broadcasting can be inferred, otherwise setting the *axes* - is not required. + If *weights* is a `Data` instance then the specified + axes identify each dimension of the given weights. If + the weights do not broadcast to the field construct's + data then setting the *axes* parameter is required so + that the broadcasting can be inferred, otherwise + setting the *axes* is not required. *Parameter example:* ``axes='T'`` @@ -4636,9 +3386,9 @@ def weights( :Returns: `Field` or `Data` or `dict` - The weights field; or if *data* is True, weights data in - broadcastable form; or if *components* is True, orthogonal - weights in a dictionary. + The weights field; or if *data* is True, weights data + in broadcastable form; or if *components* is True, + orthogonal weights in a dictionary. **Examples** @@ -4666,6 +3416,8 @@ def weights( (2,): 'linear longitude'} """ + from .weights import Weights + if isinstance(weights, str) and weights == "auto": _DEPRECATION_ERROR_KWARG_VALUE( self, @@ -4701,7 +3453,7 @@ def weights( return Data(1.0, "1") # Return a field containing a single weight of 1 - return self._weights_field_scalar() + return Weights.field_scalar(self) # Still here? if methods: @@ -4713,9 +3465,6 @@ def weights( # All axes which have weights weights_axes = set() - if radius is not None: - radius = self.radius(default=radius) - if weights is True and axes is not None: # -------------------------------------------------------- # Restrict weights to the specified axes @@ -4727,18 +3476,19 @@ def weights( # Auto-detect all weights # -------------------------------------------------------- # Volume weights - if self._weights_measure( - "volume", comp, weights_axes, methods=methods, auto=True + if Weights.cell_measure( + self, "volume", comp, weights_axes, methods=methods, auto=True ): # Found volume weights from cell measures pass - elif self._weights_measure( - "area", comp, weights_axes, methods=methods, auto=True + elif Weights.cell_measure( + self, "area", comp, weights_axes, methods=methods, auto=True ): # Found area weights from cell measures pass - elif self._weights_area_XY( + elif Weights.area_XY( + self, comp, weights_axes, measure=measure, @@ -4753,7 +3503,8 @@ def weights( domain_axes = self.domain_axes(todict=True) for da_key in domain_axes: - if self._weights_geometry_area( + if Weights.polygon_area( + self, da_key, comp, weights_axes, @@ -4765,7 +3516,8 @@ def weights( ): # Found area weights from polygon geometries pass - elif self._weights_geometry_line( + elif Weights.line_length( + self, da_key, comp, weights_axes, @@ -4777,7 +3529,8 @@ def weights( ): # Found linear weights from line geometries pass - elif self._weights_linear( + elif Weights.linear( + self, da_key, comp, weights_axes, @@ -4840,13 +3593,14 @@ def weights( # -------------------------------------------------------- # Field # -------------------------------------------------------- - self._weights_field([weights], comp, weights_axes) + Weights.field(self, [weights], comp, weights_axes) elif isinstance(weights, Data): # -------------------------------------------------------- # Data # -------------------------------------------------------- - self._weights_data( + Weights.data( + self, weights, comp, weights_axes, @@ -4869,10 +3623,10 @@ def weights( else: axes.append(weights) else: - # In rare edge cases, e.g. if a user sets: - # weights=f[0].cell_area - # when they mean weights=f[0].cell_area(), we reach this - # code but weights is not iterable. So check it is first: + # In rare edge cases (e.g. if a user sets + # `weights=f[0].cell_area` when they really meant + # `weights=f[0].cell_area()`) we reach this code but + # find that weights is not iterable. So check it is. try: weights = iter(weights) except TypeError: @@ -4893,22 +3647,33 @@ def weights( axes.append(w) # Field weights - self._weights_field(fields, comp, weights_axes) + Weights.field(self, fields, comp, weights_axes) # Volume weights if "volume" in cell_measures: - self._weights_measure( - "volume", comp, weights_axes, methods=methods, auto=False + Weights.cell_measure( + self, + "volume", + comp, + weights_axes, + methods=methods, + auto=False, ) # Area weights if "area" in cell_measures: - if self._weights_measure( - "area", comp, weights_axes, methods=methods, auto=True + if Weights.cell_measure( + self, + "area", + comp, + weights_axes, + methods=methods, + auto=True, ): # Found area weights from cell measures pass - elif self._weights_area_XY( + elif Weights.area_XY( + self, comp, weights_axes, measure=measure, @@ -4920,8 +3685,9 @@ def weights( # coordinates pass else: - # Found area weights from polygon geometries - self._weights_geometry_area( + # Found area weights from UGRID/geometry cells + Weights.polygon_area( + self, None, comp, weights_axes, @@ -4940,7 +3706,8 @@ def weights( f"{axis!r}" ) - if self._weights_geometry_area( + if Weights.polygon_area( + self, da_key, comp, weights_axes, @@ -4952,7 +3719,8 @@ def weights( ): # Found area weights from polygon geometries pass - elif self._weights_geometry_line( + elif Weights.line_length( + self, da_key, comp, weights_axes, @@ -4965,7 +3733,8 @@ def weights( # Found linear weights from line geometries pass else: - self._weights_linear( + Weights.linear( + self, da_key, comp, weights_axes, @@ -4983,10 +3752,11 @@ def weights( del comp[(yaxis,)] weights_axes.discard(xaxis) weights_axes.discard(yaxis) - if not self._weights_measure( - "area", comp, weights_axes, methods=methods + if not Weights.cell_measure( + self, "area", comp, weights_axes, methods=methods ): - self._weights_area_XY( + Weights.area_XY( + self, comp, weights_axes, measure=measure, @@ -4996,23 +3766,16 @@ def weights( if not methods: if scale is not None: - # -------------------------------------------------------- + # ---------------------------------------------------- # Scale the weights so that they are <= scale - # -------------------------------------------------------- + # ---------------------------------------------------- for key, w in comp.items(): - comp[key] = self._weights_scale(w, scale) + comp[key] = Weights.scale(w, scale) for w in comp.values(): if not measure: w.override_units("1", inplace=True) - mn = w.minimum() - if mn <= 0: - raise ValueError( - "All weights must be positive. " - f"Got a weight of {mn}" - ) - if components or methods: # -------------------------------------------------------- # Return a dictionary of component weights, which may be @@ -5034,7 +3797,7 @@ def weights( # No component weights have been defined so return an # equal weights field # -------------------------------------------------------- - f = self._weights_field_scalar() + f = Weights.field_scalar(self) if data: return f.data @@ -5055,7 +3818,7 @@ def weights( # -------------------------------------------------------- # Scale the weights so that they are <= scale # -------------------------------------------------------- - wdata = self._weights_scale(wdata, scale) + wdata = Weights.scale(wdata, scale) # ------------------------------------------------------------ # Reorder the data so that its dimensions are in the same @@ -7788,6 +6551,7 @@ def collapse( a = self.domain_axis(x, key=True, default=None) if a is None: raise ValueError(msg.format(x)) + axes2.append(a) all_axes.append(axes2) @@ -7807,8 +6571,6 @@ def collapse( # # ------------------------------------------------------------ domain_axes = f.domain_axes(todict=False, cached=domain_axes) - # auxiliary_coordinates = f.auxiliary_coordinates(view=True) - # dimension_coordinates = f.dimension_coordinates(view=True) for method, axes, within, over, axes_in in zip( all_methods, all_axes, all_within, all_over, input_axes @@ -7818,8 +6580,6 @@ def collapse( raise ValueError(f"Unknown collapse method: {method!r}") method = method2 - - # collapse_axes_all_sizes = domain_axes.filter_by_key(*axes) collapse_axes_all_sizes = f.domain_axes( filter_by_key=axes, todict=False ) @@ -7923,6 +6683,8 @@ def collapse( "simultaneously" ) + axis = [a for a in collapse_axes][0] + # ------------------------------------------------------------ # Grouped collapse: Calculate weights # ------------------------------------------------------------ @@ -7954,12 +6716,17 @@ def collapse( radius=radius, great_circle=great_circle, ) - - if not g_weights: + if g_weights: + # For grouped collapses, bring the weights + # into memory. This is to prevent lazy + # operations being run on the entire weights + # array for every group. + iaxes = (self.get_data_axes().index(axis),) + if iaxes in g_weights: + g_weights[iaxes] = g_weights[iaxes].persist() + else: g_weights = None - axis = [a for a in collapse_axes][0] - f = f._collapse_grouped( method, axis, @@ -8050,7 +6817,6 @@ def collapse( radius=radius, great_circle=great_circle, ) - if d_weights: d_kwargs["weights"] = d_weights @@ -14408,6 +13174,7 @@ def section(self, axes=None, stop=None, min_step=1, **kwargs): @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) + @_manage_log_level_via_verbosity def regrids( self, dst, @@ -14425,6 +13192,7 @@ def regrids( check_coordinates=False, min_weight=None, weights_file=None, + verbose=None, inplace=False, i=False, _compute_field_mass=None, @@ -14465,6 +13233,12 @@ def regrids( the points on either side are together without a gap (as is the case for NEMO model outputs). + **UGRID meshes** + + Data defined on UGRID face or node cells may be regridded to + any other latitude-longitude grid, including other UGRID + meshes. + **Cyclicity of the X axis** The cyclicity of the X (longitude) axes of the source and @@ -14595,7 +13369,11 @@ def regrids( {{weights_file: `str` or `None`, optional}} - .. versionadded:: 3.15.2 + .. versionadded:: 3.15.2 + + {{verbose: `int` or `str` or `None`, optional}} + + .. versionadded:: 3.16.0 {{inplace: `bool`, optional}} @@ -14724,6 +13502,11 @@ def regridc( the field being regridded and the specification of the destination grid given by the *dst* parameter. + **UGRID meshes** + + At present, Cartesian regridding is only available when + neither the source nor destination grid is a UGRID mesh. + {{regrid Masked cells}} {{regrid Implementation}} @@ -14835,7 +13618,7 @@ def regridc( {{weights_file: `str` or `None`, optional}} - .. versionadded:: 3.15.2 + .. versionadded:: 3.15.2 {{inplace: `bool`, optional}} diff --git a/cf/fieldancillary.py b/cf/fieldancillary.py index c9241863d8..ca32eeda3c 100644 --- a/cf/fieldancillary.py +++ b/cf/fieldancillary.py @@ -4,37 +4,4 @@ class FieldAncillary(mixin.PropertiesData, cfdm.FieldAncillary): - """A field ancillary construct of the CF data model. - - The field ancillary construct provides metadata which are - distributed over the same sampling domain as the field itself. For - example, if a data variable holds a variable retrieved from a - satellite instrument, a related ancillary data variable might - provide the uncertainty estimates for those retrievals (varying - over the same spatiotemporal domain). - - The field ancillary construct consists of an array of the - ancillary data, which is zero-dimensional or which depends on one - or more of the domain axes, and properties to describe the - data. It is assumed that the data do not depend on axes of the - domain which are not spanned by the array, along which the values - are implicitly propagated. CF-netCDF ancillary data variables - correspond to field ancillary constructs. Note that a field - ancillary construct is constrained by the domain definition of the - parent field construct but does not contribute to the domain's - definition, unlike, for instance, an auxiliary coordinate - construct or domain ancillary construct. - - **NetCDF interface** - - {{netcdf variable}} - - """ - - def __repr__(self): - """Called by the `repr` built-in function. - - x.__repr__() <==> repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) + + .. versionadded:: 3.16.0 + + """ + return super().__repr__().replace("<", " repr(x) + + .. versionadded:: 3.16.0 + + """ + return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " repr(x) - - """ - return super().__repr__().replace("<", " 1: + mask = da.transpose(mask, axes=axes) return mask @@ -1819,6 +2160,13 @@ def update_coordinates(src, dst, src_grid, dst_grid): Replace the existing coordinate constructs that span the regridding axes with those from the destination grid. + Also, if the source grid is a mesh, remove the existing domain + topology and cell connectivity constructs that span the regridding + axis; and if the destination grid is a mesh copy domain topology + and cell connectivity constructs from the destination grid. + + .. versionadded:: 3.14.0 + :Parameters: src: `Field` @@ -1827,10 +2175,10 @@ def update_coordinates(src, dst, src_grid, dst_grid): dst: `Field` or `Domain` The field or domain containing the destination grid. - src_grid: `Grid` + src_grid: `Grid` or `Mesh` The definition of the source grid. - dst_grid: `Grid` + dst_grid: `Grid` or `Mesh` The definition of the destination grid. :Returns: @@ -1841,44 +2189,77 @@ def update_coordinates(src, dst, src_grid, dst_grid): src_axis_keys = src_grid.axis_keys dst_axis_keys = dst_grid.axis_keys - # Remove the source coordinates of new field - for key in src.coordinates( - filter_by_axis=src_axis_keys, axis_mode="or", todict=True + # Remove the source coordinate, domain topology and cell + # connectivity constructs from regridded field. + for key in src.constructs( + filter_by_type=( + "dimension_coordinate", + "auxiliary_coordinate", + "domain_topology", + "cell_connectivity", + ), + filter_by_axis=src_axis_keys, + axis_mode="or", + todict=True, ): src.del_construct(key) # Domain axes src_domain_axes = src.domain_axes(todict=True) dst_domain_axes = dst.domain_axes(todict=True) - for src_axis, dst_axis in zip(src_axis_keys, dst_axis_keys): - src_domain_axis = src_domain_axes[src_axis] - dst_domain_axis = dst_domain_axes[dst_axis] + if src_grid.n_regrid_axes == dst_grid.n_regrid_axes: + # Change the size of the regridded domain axes + for src_axis, dst_axis in zip(src_axis_keys, dst_axis_keys): + src_domain_axis = src_domain_axes[src_axis] + dst_domain_axis = dst_domain_axes[dst_axis] - src_domain_axis.set_size(dst_domain_axis.size) + src_domain_axis.set_size(dst_domain_axis.size) - ncdim = dst_domain_axis.nc_get_dimension(None) - if ncdim is not None: - src_domain_axis.nc_set_dimension(ncdim) + ncdim = dst_domain_axis.nc_get_dimension(None) + if ncdim is not None: + src_domain_axis.nc_set_dimension(ncdim) + else: + # The regridding has changed the number of data axes (e.g. by + # regridding a source mesh grid to a destination non-mesh + # grid, or vice versa), so insert new domain axis constructs + # for all of the new axes. + src_axis_keys = [ + src.set_construct(dst_domain_axes[dst_axis].copy()) + for dst_axis in dst_axis_keys + ] + src_grid.new_axis_keys = src_axis_keys - # Coordinates axis_map = { dst_axis: src_axis for dst_axis, src_axis in zip(dst_axis_keys, src_axis_keys) } dst_data_axes = dst.constructs.data_axes() - for key, aux in dst.coordinates( + # Copy coordinates constructs from the destination grid + for key, coord in dst.coordinates( filter_by_axis=dst_axis_keys, axis_mode="subset", todict=True ).items(): axes = [axis_map[axis] for axis in dst_data_axes[key]] - src.set_construct(aux, axes=axes) + src.set_construct(coord, axes=axes) + + # Copy domain topology and cell connectivity constructs from the + # destination grid + if dst_grid.is_mesh: + for key, topology in dst.constructs( + filter_by_type=("domain_topology", "cell_connectivity"), + filter_by_axis=dst_axis_keys, + axis_mode="exact", + todict=True, + ).items(): + axes = [axis_map[axis] for axis in dst_data_axes[key]] + src.set_construct(topology, axes=axes) -def update_non_coordinates( - src, dst, regrid_operator, src_grid=None, dst_grid=None -): +def update_non_coordinates(src, dst, src_grid, dst_grid, regrid_operator): """Update the coordinate references of the regridded field. + .. versionadded:: 3.14.0 + :Parameters: src: `Field` @@ -1923,14 +2304,17 @@ def update_non_coordinates( src.del_coordinate_reference(ref_key) # ---------------------------------------------------------------- - # Delete source grid cell measures and field ancillaries that span - # any of the regridding axes + # Delete source grid cell measure and field ancillary constructs + # that span any of the regridding axes. # ---------------------------------------------------------------- for key in src.constructs( - filter_by_type=("cell_measure", "field_ancillary"), todict=True + filter_by_type=("cell_measure", "field_ancillary"), + filter_by_axis=src_axis_keys, + axis_mode="or", + todict=True, ): - if set(data_axes[key]).intersection(src_axis_keys): - src.del_construct(key) + # if set(data_axes[key]).intersection(src_axis_keys): + src.del_construct(key) # ---------------------------------------------------------------- # Regrid any remaining source domain ancillaries that span all of @@ -2000,3 +2384,113 @@ def update_non_coordinates( if axes and set(axes).issubset(dst_axis_keys): src.set_coordinate_reference(ref, parent=dst, strict=True) + + +def update_data(src, regridded_data, src_grid): + """Insert the regridded field data. + + .. versionadded:: 3.16.0 + + .. seealso: `update_coordinates`, `update_non_coordinates` + + :Parameters: + + src`: `Field` + The regridded field construct, that will be updated + in-place. + + regridded_data: `numpy.ndarray` + The regridded array + + src_grid: `Grid` + The definition of the source grid. + + :Returns: + + `None` + + """ + data_axes = src.get_data_axes() + if src_grid.new_axis_keys: + # The regridding has changed the number of data axes (e.g. by + # regridding a source UGRID grid to a destination non-UGRID + # grid, or vice versa) => delete the old, superceded domain + # axis construct and update the list of data axes. + data_axes = list(data_axes) + index = data_axes.index(src_grid.axis_keys[0]) + for axis in src_grid.axis_keys: + data_axes.remove(axis) + src.del_construct(axis) + + data_axes[index:index] = src_grid.new_axis_keys + + src.set_data(regridded_data, axes=data_axes, copy=False) + + +def get_mesh(f): + """Get domain topology mesh information. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` or `Domain` + The construct from which to get the mesh information. + + :Returns: + + 3-`tuple` + If the field or domain has no domain topology construct + then ``(None, None, None)`` is returned. Otherwise the + tuple contains: + + * The domain topology construct + * The mesh location of the domain topology (e.g. ``'face'``) + * The identifier of domain axis construct that is spanned + by the domain topology construct + + """ + key, domain_topology = f.domain_topology(item=True, default=(None, None)) + if domain_topology is None: + return (None, None, None) + + return ( + domain_topology, + domain_topology.get_cell(""), + f.get_data_axes(key)[0], + ) + + +def has_coordinate_arrays(grid): + """Whether all grid coordinates have representative arrays. + + .. versionadded:: 3.16.0 + + :Parameters: + + grid: `Grid` + The definition of the grid. + + :Returns: + + `bool` + True if and only if there are grid coordinates and they + all have representative arrays. + + """ + if not grid.coords: + return False + + for coord in grid.coords: + try: + has_data = coord.has_data() + except AttributeError: + # 'coord' is not a construct, because it doesn't have a + # `has_data` attribute, and so must be something that + # certainly has data (e.g. a numpy array). + has_data = True + + if not has_data: + return False + + return True diff --git a/cf/regrid/regridoperator.py b/cf/regrid/regridoperator.py index 00ca216b6f..a8f783f696 100644 --- a/cf/regrid/regridoperator.py +++ b/cf/regrid/regridoperator.py @@ -40,6 +40,7 @@ def __init__( dst_axes=None, dst=None, weights_file=None, + src_mesh_location=None, ): """**Initialisation** @@ -124,6 +125,13 @@ def __init__( .. versionadded:: 3.15.2 + src_mesh_location: `str`, optional + The UGRID mesh element of the source grid + (e.g. ``'face'``). An empty string should be used for + a non-UGRID source grid. + + .. versionadded:: 3.16.0 + """ super().__init__() @@ -149,6 +157,7 @@ def __init__( self._set_component("dst_axes", dst_axes, copy=False) self._set_component("dst", dst, copy=False) self._set_component("weights_file", weights_file, copy=False) + self._set_component("src_mesh_location", src_mesh_location, copy=False) def __repr__(self): """x.__repr__() <==> repr(x)""" @@ -313,6 +322,15 @@ def src_mask(self): """ return self._get_component("src_mask") + @property + def src_mesh_location(self): + """The UGRID mesh element of the source grid. + + .. versionadded:: 3.16.0 + + """ + return self._get_component("src_mesh_location") + @property def src_shape(self): """The shape of the source grid. @@ -391,6 +409,7 @@ def copy(self): dst_axes=self.dst_axes, dst=self.dst.copy(), weights_file=self.weights_file, + src_mesh_location=self.src_mesh_location, ) @_display_or_return @@ -431,6 +450,7 @@ def dump(self, display=True): "start_index", "src_axes", "dst_axes", + "src_mesh_location", "dst", "weights", "row", @@ -603,11 +623,10 @@ def tosparse(self): # Note: It is much more efficient to access 'weights.indptr' # and 'weights.data' directly, rather than iterating # over rows of 'weights' and using 'weights.getrow'. - count_nonzero = np.count_nonzero indptr = weights.indptr.tolist() data = weights.data for j, (i0, i1) in enumerate(zip(indptr[:-1], indptr[1:])): - if not count_nonzero(data[i0:i1]): + if not data[i0:i1].size: dst_mask[j] = True if not dst_mask.any(): diff --git a/cf/test/create_test_files.npz b/cf/test/create_test_files.npz new file mode 100644 index 0000000000..6febacde48 Binary files /dev/null and b/cf/test/create_test_files.npz differ diff --git a/cf/test/create_test_files.py b/cf/test/create_test_files.py index 0cd4f5dd42..80f3be4a9f 100644 --- a/cf/test/create_test_files.py +++ b/cf/test/create_test_files.py @@ -7,17 +7,26 @@ faulthandler.enable() # to debug seg faults and timeouts +import cfdm import netCDF4 -import cf +VN = cfdm.CF() -VN = cf.CF() +# Load large arrays +filename = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "create_test_files.npz" +) +arrays = np.load(filename) +# -------------------------------------------------------------------- +# DSG files +# -------------------------------------------------------------------- def _make_contiguous_file(filename): + """Make a netCDF file with a contiguous ragged array DSG feature.""" n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" n.createDimension("station", 4) @@ -106,9 +115,10 @@ def _make_contiguous_file(filename): def _make_indexed_file(filename): + """Make a netCDF file with an indexed ragged array DSG feature.""" n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" n.createDimension("station", 4) @@ -238,15 +248,16 @@ def _make_indexed_file(filename): def _make_indexed_contiguous_file(filename): + """Make a netCDF file with an indexed contiguous ragged array.""" n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeriesProfile" # 3 stations n.createDimension("station", 3) # 58 profiles spreadover 4 stations, each at a different time - profile = n.createDimension("profile", 58) + n.createDimension("profile", 58) n.createDimension("obs", None) n.createDimension("name_strlen", 8) n.createDimension("bounds", 2) @@ -606,6 +617,9 @@ def _make_indexed_contiguous_file(filename): return filename +# -------------------------------------------------------------------- +# External variable files +# -------------------------------------------------------------------- def _make_external_files(): """Make netCDF files with external variables.""" @@ -622,7 +636,7 @@ def _pp( nc.createDimension("grid_latitude", 10) nc.createDimension("grid_longitude", 9) - nc.Conventions = "CF-" + VN + nc.Conventions = f"CF-{VN}" if parent: nc.external_variables = "areacella" @@ -715,10 +729,14 @@ def _pp( return parent_file, external_file, combined_file, external_missing_file +# -------------------------------------------------------------------- +# Gathered files +# -------------------------------------------------------------------- def _make_gathered_file(filename): """Make a netCDF file with a gathered array.""" def _jj(shape, list_values): + """Create and return a gathered array.""" array = np.ma.masked_all(shape) for i, (index, x) in enumerate(np.ndenumerate(array)): if i in list_values: @@ -727,7 +745,7 @@ def _jj(shape, list_values): n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" time = n.createDimension("time", 2) height = n.createDimension("height", 3) @@ -848,6 +866,9 @@ def _jj(shape, list_values): return filename +gathered = _make_gathered_file("gathered.nc") + + # -------------------------------------------------------------------- # Geometry files # -------------------------------------------------------------------- @@ -855,11 +876,12 @@ def _make_geometry_1_file(filename): """See n.comment for details.""" n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" n.comment = ( - "Make a netCDF file with 2 node coordinates variables, each of " - "which has a corresponding auxiliary coordinate variable." + "Make a netCDF file with 2 node coordinates variables, " + "each of which has a corresponding auxiliary coordinate " + "variable." ) n.createDimension("time", 4) @@ -931,11 +953,12 @@ def _make_geometry_2_file(filename): """See n.comment for details.""" n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" n.comment = ( - "A netCDF file with 3 node coordinates variables, only two of " - "which have a corresponding auxiliary coordinate variable." + "A netCDF file with 3 node coordinates variables, only " + "two of which have a corresponding auxiliary coordinate " + "variable." ) n.createDimension("time", 4) @@ -1011,16 +1034,18 @@ def _make_geometry_3_file(filename): """See n.comment for details.""" n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" n.comment = ( - "A netCDF file with 3 node coordinates variables, each of which " - "contains only one point, only two of which have a corresponding " - "auxiliary coordinate variables. There is no node count variable." + "A netCDF file with 3 node coordinates variables, each of " + "which contains only one point, only two of which have a " + "corresponding auxiliary coordinate variables. There is no " + "node count variable." ) n.createDimension("time", 4) n.createDimension("instance", 3) + # node = n.createDimension('node' , 3) t = n.createVariable("time", "i4", ("time",)) t.units = "seconds since 2016-11-07 20:00 UTC" @@ -1087,11 +1112,11 @@ def _make_geometry_4_file(filename): """See n.comment for details.""" n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" n.comment = ( - "A netCDF file with 2 node coordinates variables, none of which " - "have a corresponding auxiliary coordinate variable." + "A netCDF file with 2 node coordinates variables, none of " + "which have a corresponding auxiliary coordinate variable." ) n.createDimension("time", 4) @@ -1157,9 +1182,13 @@ def _make_interior_ring_file(filename): n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") # Global attributes - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" - n.comment = "TODO" + n.comment = ( + "A netCDF file with an interior ring variable of geometry " + "coordinates where x and y (but not z) are node " + "coordinates." + ) # Dimensions n.createDimension("time", 4) @@ -1234,7 +1263,7 @@ def _make_interior_ring_file(filename): datum.longitude_of_prime_meridian = 0.0 pr = n.createVariable("pr", "f8", ("instance", "time")) - pr.standard_name = "preciptitation_amount" + pr.standard_name = "precipitation_amount" pr.standard_units = "kg m-2" pr.coordinates = "time lat lon z instance_id" pr.grid_mapping = "datum" @@ -1257,9 +1286,12 @@ def _make_interior_ring_file_2(filename): n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") # Global attributes - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.featureType = "timeSeries" - n.comment = "TODO" + n.comment = ( + "A netCDF file with an interior ring variable of geometry " + "coordinates where x, y and z are node coordinates." + ) # Dimensions n.createDimension("time", 4) @@ -1333,7 +1365,7 @@ def _make_interior_ring_file_2(filename): datum.longitude_of_prime_meridian = 0.0 pr = n.createVariable("pr", "f8", ("instance", "time")) - pr.standard_name = "preciptitation_amount" + pr.standard_name = "precipitation_amount" pr.standard_units = "kg m-2" pr.coordinates = "time lat lon z instance_id" pr.grid_mapping = "datum" @@ -1355,7 +1387,7 @@ def _make_string_char_file(filename): """See n.comment for details.""" n = netCDF4.Dataset(filename, "w", format="NETCDF4") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN}" n.comment = "A netCDF file with variables of string and char data types" n.createDimension("dim1", 1) @@ -1406,12 +1438,14 @@ def _make_string_char_file(filename): c_months1 = n.createVariable("c_months1", "S1", ("dim1", "strlen8")) c_months1.long_name = "char: One month" c_months1[:] = netCDF4.stringtochar(np.array(["December"], dtype="S8")) + c_months0 = n.createVariable("c_months0", "S1", ("strlen3",)) c_months0.long_name = "char: One month (scalar)" c_months0[:] = np.array(list("May")) c_numbers = n.createVariable("c_numbers", "S1", ("lat", "lon", "strlen5")) c_numbers.long_name = "char: Two dimensional" + np.empty((2, 3, 5), dtype="S1") c_numbers[...] = netCDF4.stringtochar(numbers) c_months4m = n.createVariable("c_months4m", "S1", ("time", "strlen7")) @@ -1424,73 +1458,6 @@ def _make_string_char_file(filename): return filename -def _make_broken_bounds_cdl(filename): - with open(filename, mode="w") as f: - f.write( - """netcdf broken_bounds { -dimensions: - lat = 180 ; - bnds = 2 ; - lon = 288 ; - time = UNLIMITED ; // (1825 currently) -variables: - double lat(lat) ; - lat:long_name = "latitude" ; - lat:units = "degrees_north" ; - lat:axis = "Y" ; - lat:bounds = "lat_bnds" ; - lat:standard_name = "latitude" ; - lat:cell_methods = "time: point" ; - double lat_bnds(lat, bnds) ; - lat_bnds:long_name = "latitude bounds" ; - lat_bnds:units = "degrees_north" ; - lat_bnds:axis = "Y" ; - double lon(lon) ; - lon:long_name = "longitude" ; - lon:units = "degrees_east" ; - lon:axis = "X" ; - lon:bounds = "lon_bnds" ; - lon:standard_name = "longitude" ; - lon:cell_methods = "time: point" ; - double lon_bnds(lon, bnds) ; - lon_bnds:long_name = "longitude bounds" ; - lon_bnds:units = "m" ; - lon_bnds:axis = "X" ; - float pr(time, lat, lon) ; - pr:long_name = "Precipitation" ; - pr:units = "kg m-2 s-1" ; - pr:missing_value = 1.e+20f ; - pr:_FillValue = 1.e+20f ; - pr:cell_methods = "area: time: mean" ; - pr:cell_measures = "area: areacella" ; - pr:standard_name = "precipitation_flux" ; - pr:interp_method = "conserve_order1" ; - pr:original_name = "pr" ; - double time(time) ; - time:long_name = "time" ; - time:units = "days since 1850-01-01 00:00:00" ; - time:axis = "T" ; - time:calendar_type = "noleap" ; - time:calendar = "noleap" ; - time:bounds = "time_bnds" ; - time:standard_name = "time" ; - time:description = "Temporal mean" ; - double time_bnds(time, bnds) ; - time_bnds:long_name = "time axis boundaries" ; - time_bnds:units = "days since 1850-01-01 00:00:00" ; - -// global attributes: - :external_variables = "areacella" ; - :Conventions = "CF-""" - + VN - + """" ; - :source = "model" ; - :comment = "Bounds variable has incompatible units to its parent coordinate variable" ; -} -""" - ) - - def _make_subsampled_1(filename): """Lossy compression by coordinate subsampling (1). @@ -1897,3284 +1864,12 @@ def _make_subsampled_2(filename): rec_lon = n.createVariable("rec_lon", "f4", ("track", "scan")) rec_lon.standard_name = "longitude" rec_lon.units = "degrees_east" - rec_lon[...] = np.array( - [ - [ - -63.87722, - -63.894657, - -63.912052, - -63.92941, - -63.946724, - -63.963997, - -63.981228, - -63.99842, - -64.01557, - -64.03268, - -64.04975, - -64.06677, - -64.08376, - -64.10071, - -64.117615, - -64.134476, - -64.1513, - -64.16808, - -64.18483, - -64.20154, - -64.21821, - -64.23484, - -64.251434, - -64.26799, - -64.284515, - -64.300995, - -64.31744, - -64.33384, - -64.350204, - -64.36653, - -64.38283, - -64.39908, - ], - [ - -63.877724, - -63.895164, - -63.912563, - -63.92992, - -63.947235, - -63.96451, - -63.981747, - -63.998943, - -64.0161, - -64.0332, - -64.05028, - -64.06731, - -64.0843, - -64.10124, - -64.11815, - -64.13502, - -64.15183, - -64.168625, - -64.18537, - -64.20208, - -64.21876, - -64.23539, - -64.25199, - -64.26855, - -64.28507, - -64.30155, - -64.318, - -64.334404, - -64.35078, - -64.3671, - -64.3834, - -64.39965, - ], - [ - -63.878216, - -63.895657, - -63.91306, - -63.93042, - -63.94774, - -63.96502, - -63.982254, - -63.99945, - -64.01661, - -64.03372, - -64.0508, - -64.067825, - -64.084816, - -64.10177, - -64.118675, - -64.135544, - -64.15236, - -64.16915, - -64.185905, - -64.20262, - -64.21929, - -64.23593, - -64.25253, - -64.26909, - -64.285614, - -64.3021, - -64.31855, - -64.33496, - -64.351326, - -64.36766, - -64.38396, - -64.400215, - ], - [ - -63.878696, - -63.89614, - -63.913544, - -63.93091, - -63.94823, - -63.96551, - -63.98275, - -63.99995, - -64.017105, - -64.034225, - -64.0513, - -64.06834, - -64.08533, - -64.10228, - -64.119194, - -64.13606, - -64.15288, - -64.16967, - -64.186424, - -64.20314, - -64.21982, - -64.23646, - -64.25306, - -64.26962, - -64.28615, - -64.30264, - -64.31909, - -64.3355, - -64.351875, - -64.36821, - -64.38451, - -64.400764, - ], - [ - -63.879166, - -63.896614, - -63.91402, - -63.931385, - -63.948708, - -63.965992, - -63.983234, - -64.000435, - -64.01759, - -64.03471, - -64.051796, - -64.06883, - -64.08583, - -64.10278, - -64.1197, - -64.136566, - -64.15339, - -64.17018, - -64.186935, - -64.20365, - -64.22034, - -64.23698, - -64.253586, - -64.27015, - -64.286674, - -64.30317, - -64.31962, - -64.33603, - -64.35241, - -64.368744, - -64.38505, - -64.401306, - ], - [ - -63.879623, - -63.89707, - -63.914482, - -63.93185, - -63.949177, - -63.96646, - -63.983707, - -64.00091, - -64.018074, - -64.035194, - -64.05228, - -64.06931, - -64.08631, - -64.10327, - -64.120186, - -64.13706, - -64.15388, - -64.17068, - -64.18744, - -64.204155, - -64.22084, - -64.23749, - -64.25409, - -64.27066, - -64.28719, - -64.30368, - -64.32014, - -64.336555, - -64.35293, - -64.36927, - -64.385574, - -64.40184, - ], - [ - -63.88007, - -63.897522, - -63.914932, - -63.932304, - -63.949635, - -63.966923, - -63.98417, - -64.00137, - -64.01854, - -64.03567, - -64.05275, - -64.069786, - -64.08679, - -64.10375, - -64.12067, - -64.13754, - -64.154366, - -64.171165, - -64.18793, - -64.20465, - -64.22134, - -64.23798, - -64.25459, - -64.271164, - -64.2877, - -64.30419, - -64.32065, - -64.33707, - -64.35345, - -64.36979, - -64.38609, - -64.40236, - ], - [ - -63.880505, - -63.897957, - -63.91537, - -63.932747, - -63.950077, - -63.96737, - -63.98462, - -64.00183, - -64.019, - -64.036125, - -64.05321, - -64.07025, - -64.08726, - -64.10422, - -64.12114, - -64.138016, - -64.15484, - -64.17164, - -64.1884, - -64.20513, - -64.22182, - -64.238464, - -64.25508, - -64.27165, - -64.288185, - -64.30468, - -64.321144, - -64.33756, - -64.35394, - -64.37029, - -64.3866, - -64.40286, - ], - [ - -63.880928, - -63.898384, - -63.915802, - -63.933178, - -63.950512, - -63.967804, - -63.985058, - -64.002266, - -64.01944, - -64.03657, - -64.05366, - -64.0707, - -64.08771, - -64.104675, - -64.1216, - -64.13848, - -64.155304, - -64.172104, - -64.18887, - -64.2056, - -64.22229, - -64.23894, - -64.255554, - -64.27213, - -64.288666, - -64.30517, - -64.321625, - -64.33805, - -64.35444, - -64.37078, - -64.38709, - -64.40336, - ], - [ - -63.881336, - -63.8988, - -63.916218, - -63.933598, - -63.95093, - -63.968227, - -63.985485, - -64.0027, - -64.01987, - -64.037, - -64.05409, - -64.071144, - -64.08815, - -64.10512, - -64.12204, - -64.13892, - -64.155754, - -64.17256, - -64.18932, - -64.206055, - -64.22275, - -64.2394, - -64.25602, - -64.2726, - -64.28914, - -64.30564, - -64.322105, - -64.338524, - -64.35491, - -64.37126, - -64.38757, - -64.40385, - ], - [ - -63.881737, - -63.8992, - -63.916622, - -63.934002, - -63.951344, - -63.96864, - -63.985897, - -64.00311, - -64.02029, - -64.03742, - -64.05452, - -64.07157, - -64.08858, - -64.105545, - -64.122475, - -64.139366, - -64.15619, - -64.173004, - -64.18977, - -64.206505, - -64.2232, - -64.23985, - -64.25648, - -64.273056, - -64.2896, - -64.3061, - -64.32256, - -64.339, - -64.355385, - -64.371735, - -64.38805, - -64.40432, - ], - [ - -63.882126, - -63.899593, - -63.917015, - -63.9344, - -63.95174, - -63.969044, - -63.9863, - -64.00352, - -64.0207, - -64.037834, - -64.05493, - -64.07198, - -64.089, - -64.105965, - -64.1229, - -64.139786, - -64.156624, - -64.17343, - -64.19021, - -64.20694, - -64.22364, - -64.240295, - -64.25692, - -64.2735, - -64.29005, - -64.30655, - -64.32302, - -64.33945, - -64.35584, - -64.37219, - -64.38851, - -64.404785, - ], - [ - -63.882504, - -63.89997, - -63.917397, - -63.934784, - -63.95213, - -63.969433, - -63.986694, - -64.003914, - -64.021095, - -64.03823, - -64.05533, - -64.07239, - -64.0894, - -64.10638, - -64.123314, - -64.140205, - -64.15704, - -64.17385, - -64.19063, - -64.20737, - -64.22407, - -64.24073, - -64.25735, - -64.27393, - -64.29048, - -64.30699, - -64.32346, - -64.3399, - -64.356285, - -64.37264, - -64.38896, - -64.40524, - ], - [ - -63.88287, - -63.900337, - -63.917767, - -63.935158, - -63.952503, - -63.96981, - -63.987076, - -64.004295, - -64.02148, - -64.03862, - -64.055725, - -64.07278, - -64.0898, - -64.10677, - -64.12371, - -64.1406, - -64.15745, - -64.17426, - -64.19104, - -64.20778, - -64.22449, - -64.24115, - -64.257774, - -64.27436, - -64.29091, - -64.30742, - -64.32389, - -64.340324, - -64.35672, - -64.373085, - -64.389404, - -64.405685, - ], - [ - -63.88322, - -63.900696, - -63.91813, - -63.935516, - -63.952866, - -63.970177, - -63.987442, - -64.00467, - -64.02185, - -64.038994, - -64.0561, - -64.07316, - -64.09018, - -64.10716, - -64.1241, - -64.14099, - -64.157845, - -64.17466, - -64.191444, - -64.20818, - -64.22489, - -64.241554, - -64.25819, - -64.27477, - -64.29132, - -64.30784, - -64.32431, - -64.34075, - -64.35715, - -64.37351, - -64.38983, - -64.40611, - ], - [ - -63.883564, - -63.90104, - -63.918476, - -63.935867, - -63.95322, - -63.97053, - -63.9878, - -64.00503, - -64.02222, - -64.03936, - -64.056465, - -64.07353, - -64.09055, - -64.10754, - -64.12447, - -64.14137, - -64.15823, - -64.17505, - -64.19183, - -64.20858, - -64.22529, - -64.24195, - -64.25858, - -64.27518, - -64.29173, - -64.30824, - -64.32472, - -64.34116, - -64.35756, - -64.373924, - -64.39025, - -64.40653, - ], - [ - -63.88726, - -63.90472, - -63.92214, - -63.939514, - -63.956852, - -63.974148, - -63.9914, - -64.00861, - -64.02579, - -64.042915, - -64.060005, - -64.07706, - -64.09406, - -64.11103, - -64.12795, - -64.14484, - -64.16168, - -64.17849, - -64.19527, - -64.212, - -64.2287, - -64.24535, - -64.26197, - -64.27856, - -64.2951, - -64.31161, - -64.32807, - -64.344505, - -64.36089, - -64.37725, - -64.39356, - -64.40984, - ], - [ - -63.887756, - -63.905216, - -63.922638, - -63.940018, - -63.957355, - -63.97465, - -63.99191, - -64.009125, - -64.0263, - -64.043434, - -64.060524, - -64.077576, - -64.09458, - -64.11156, - -64.12848, - -64.14537, - -64.16221, - -64.17902, - -64.1958, - -64.21253, - -64.22923, - -64.245895, - -64.26252, - -64.2791, - -64.29565, - -64.31216, - -64.32863, - -64.34506, - -64.36145, - -64.37781, - -64.39413, - -64.41041, - ], - [ - -63.888237, - -63.9057, - -63.923126, - -63.940506, - -63.957848, - -63.975147, - -63.99241, - -64.00963, - -64.0268, - -64.04394, - -64.06103, - -64.07809, - -64.0951, - -64.11207, - -64.129, - -64.14589, - -64.16273, - -64.17954, - -64.19632, - -64.21306, - -64.22976, - -64.24642, - -64.263054, - -64.27964, - -64.29619, - -64.3127, - -64.32917, - -64.345604, - -64.362, - -64.37836, - -64.39468, - -64.41096, - ], - [ - -63.88871, - -63.906174, - -63.9236, - -63.940987, - -63.95833, - -63.97563, - -63.992893, - -64.01012, - -64.02729, - -64.04443, - -64.06153, - -64.07858, - -64.0956, - -64.11257, - -64.1295, - -64.14639, - -64.16324, - -64.18005, - -64.19683, - -64.21358, - -64.23028, - -64.24695, - -64.26357, - -64.28016, - -64.296715, - -64.313225, - -64.329704, - -64.34614, - -64.36253, - -64.3789, - -64.39522, - -64.4115, - ], - [ - -63.889168, - -63.906635, - -63.924065, - -63.941452, - -63.958797, - -63.976105, - -63.993366, - -64.01059, - -64.02777, - -64.044914, - -64.06201, - -64.07907, - -64.096085, - -64.11306, - -64.13, - -64.14689, - -64.163734, - -64.18055, - -64.197334, - -64.21408, - -64.23078, - -64.24745, - -64.264084, - -64.28068, - -64.297226, - -64.31374, - -64.33022, - -64.34666, - -64.36306, - -64.379425, - -64.395744, - -64.41203, - ], - [ - -63.889614, - -63.90709, - -63.92452, - -63.94191, - -63.959255, - -63.976562, - -63.99383, - -64.011055, - -64.02824, - -64.04538, - -64.062485, - -64.079544, - -64.096565, - -64.11354, - -64.13048, - -64.14738, - -64.164215, - -64.18104, - -64.19782, - -64.21457, - -64.23128, - -64.24795, - -64.26458, - -64.28117, - -64.29773, - -64.31425, - -64.33073, - -64.34717, - -64.36357, - -64.37994, - -64.39626, - -64.41255, - ], - [ - -63.890053, - -63.907528, - -63.92496, - -63.942352, - -63.9597, - -63.977013, - -63.99428, - -64.01151, - -64.028694, - -64.045845, - -64.06294, - -64.08001, - -64.09703, - -64.11401, - -64.13095, - -64.14785, - -64.164696, - -64.18152, - -64.1983, - -64.21506, - -64.231766, - -64.248436, - -64.265076, - -64.28167, - -64.298225, - -64.31474, - -64.33123, - -64.34767, - -64.364075, - -64.38045, - -64.396774, - -64.41306, - ], - [ - -63.890476, - -63.907955, - -63.92539, - -63.942783, - -63.960136, - -63.97745, - -63.99472, - -64.011955, - -64.029144, - -64.04629, - -64.06339, - -64.08046, - -64.09749, - -64.11447, - -64.13141, - -64.148315, - -64.16515, - -64.181984, - -64.19878, - -64.21552, - -64.23224, - -64.24892, - -64.26555, - -64.28215, - -64.298706, - -64.31523, - -64.33172, - -64.34816, - -64.36457, - -64.38094, - -64.39727, - -64.41357, - ], - [ - -63.89089, - -63.908367, - -63.925808, - -63.943203, - -63.96056, - -63.977875, - -63.99515, - -64.01238, - -64.02957, - -64.04672, - -64.063835, - -64.0809, - -64.09793, - -64.114914, - -64.13186, - -64.148766, - -64.16561, - -64.18244, - -64.199234, - -64.21599, - -64.232704, - -64.24938, - -64.266014, - -64.282616, - -64.29918, - -64.315704, - -64.33219, - -64.34864, - -64.36505, - -64.381424, - -64.39776, - -64.414055, - ], - [ - -63.891293, - -63.90877, - -63.926212, - -63.943615, - -63.96097, - -63.97829, - -63.995567, - -64.0128, - -64.03, - -64.04715, - -64.06426, - -64.08133, - -64.09836, - -64.11535, - -64.13229, - -64.1492, - -64.16605, - -64.182884, - -64.19968, - -64.21643, - -64.233154, - -64.24983, - -64.26647, - -64.28308, - -64.299644, - -64.31617, - -64.33266, - -64.349106, - -64.365524, - -64.3819, - -64.39823, - -64.41453, - ], - [ - -63.89168, - -63.909164, - -63.92661, - -63.94401, - -63.961372, - -63.978695, - -63.99597, - -64.01321, - -64.0304, - -64.04756, - -64.064674, - -64.08175, - -64.09878, - -64.11577, - -64.13272, - -64.14963, - -64.16648, - -64.18332, - -64.20011, - -64.21687, - -64.23359, - -64.250275, - -64.26692, - -64.28352, - -64.300095, - -64.31662, - -64.333115, - -64.34956, - -64.36598, - -64.382355, - -64.3987, - -64.41499, - ], - [ - -63.89206, - -63.909546, - -63.926994, - -63.944397, - -63.96176, - -63.979084, - -63.996365, - -64.0136, - -64.03081, - -64.047966, - -64.06508, - -64.08215, - -64.09919, - -64.11618, - -64.13313, - -64.15005, - -64.1669, - -64.18374, - -64.20054, - -64.2173, - -64.234024, - -64.25071, - -64.26736, - -64.28396, - -64.30053, - -64.31706, - -64.33356, - -64.35001, - -64.36643, - -64.382805, - -64.39915, - -64.41545, - ], - [ - -63.892426, - -63.909916, - -63.927364, - -63.94477, - -63.96214, - -63.979465, - -63.996746, - -64.01399, - -64.03119, - -64.048355, - -64.06547, - -64.08255, - -64.09959, - -64.116585, - -64.13354, - -64.15045, - -64.16731, - -64.18415, - -64.20095, - -64.21771, - -64.23444, - -64.25113, - -64.26778, - -64.284386, - -64.30096, - -64.3175, - -64.33399, - -64.35045, - -64.36687, - -64.38325, - -64.39959, - -64.41589, - ], - [ - -63.89278, - -63.910275, - -63.927727, - -63.945133, - -63.962505, - -63.97983, - -63.997116, - -64.014366, - -64.03157, - -64.04873, - -64.06585, - -64.08293, - -64.09997, - -64.11697, - -64.13393, - -64.15084, - -64.16771, - -64.18455, - -64.201355, - -64.218124, - -64.23485, - -64.25154, - -64.26819, - -64.284805, - -64.30138, - -64.31792, - -64.33441, - -64.35087, - -64.367294, - -64.383675, - -64.400024, - -64.41633, - ], - [ - -63.893124, - -63.910618, - -63.928074, - -63.945488, - -63.962856, - -63.980186, - -63.99748, - -64.014725, - -64.03193, - -64.049095, - -64.06622, - -64.083305, - -64.10034, - -64.11735, - -64.13431, - -64.15122, - -64.1681, - -64.184944, - -64.201744, - -64.21851, - -64.235245, - -64.25194, - -64.26859, - -64.28521, - -64.30178, - -64.31832, - -64.33482, - -64.35129, - -64.36771, - -64.384094, - -64.400444, - -64.41675, - ], - [ - -63.893456, - -63.910954, - -63.92841, - -63.945827, - -63.9632, - -63.980534, - -63.997826, - -64.015076, - -64.03228, - -64.04945, - -64.066574, - -64.083664, - -64.10071, - -64.11771, - -64.134674, - -64.15159, - -64.16847, - -64.18532, - -64.202126, - -64.2189, - -64.235634, - -64.25233, - -64.26898, - -64.2856, - -64.30218, - -64.31872, - -64.33522, - -64.351685, - -64.36811, - -64.3845, - -64.40085, - -64.41716, - ], - [ - -63.898563, - -63.91605, - -63.933495, - -63.9509, - -63.968266, - -63.985588, - -64.00287, - -64.02011, - -64.03731, - -64.05447, - -64.07158, - -64.08866, - -64.10569, - -64.12269, - -64.13964, - -64.15655, - -64.173416, - -64.190254, - -64.20705, - -64.22381, - -64.240524, - -64.25721, - -64.27385, - -64.29046, - -64.30702, - -64.323555, - -64.34004, - -64.35649, - -64.37291, - -64.38928, - -64.405624, - -64.42192, - ], - [ - -63.899055, - -63.916546, - -63.933994, - -63.9514, - -63.968765, - -63.98609, - -64.00337, - -64.020615, - -64.03782, - -64.05498, - -64.0721, - -64.08918, - -64.10622, - -64.12321, - -64.14017, - -64.15708, - -64.17394, - -64.19078, - -64.20758, - -64.22434, - -64.241066, - -64.257744, - -64.27439, - -64.291, - -64.30757, - -64.324104, - -64.34059, - -64.35705, - -64.37347, - -64.38985, - -64.40618, - -64.422485, - ], - [ - -63.899536, - -63.917027, - -63.93448, - -63.95189, - -63.969257, - -63.986584, - -64.00387, - -64.02112, - -64.03832, - -64.05548, - -64.07261, - -64.08968, - -64.10673, - -64.123726, - -64.14068, - -64.15759, - -64.17446, - -64.1913, - -64.2081, - -64.22487, - -64.24159, - -64.25828, - -64.274925, - -64.291534, - -64.308105, - -64.32464, - -64.34113, - -64.35759, - -64.37401, - -64.39039, - -64.40674, - -64.42304, - ], - [ - -63.900005, - -63.9175, - -63.93495, - -63.952366, - -63.969738, - -63.98707, - -64.00436, - -64.02161, - -64.03881, - -64.05598, - -64.0731, - -64.09018, - -64.10722, - -64.12423, - -64.14118, - -64.158104, - -64.174965, - -64.19181, - -64.20861, - -64.22538, - -64.2421, - -64.2588, - -64.275444, - -64.29206, - -64.30863, - -64.325165, - -64.34167, - -64.35812, - -64.37455, - -64.39093, - -64.40727, - -64.423584, - ], - [ - -63.900463, - -63.91796, - -63.935417, - -63.95283, - -63.970203, - -63.987537, - -64.00483, - -64.02208, - -64.03929, - -64.05646, - -64.073586, - -64.09067, - -64.10771, - -64.12472, - -64.14168, - -64.1586, - -64.17546, - -64.19231, - -64.209114, - -64.22588, - -64.24261, - -64.2593, - -64.275955, - -64.29257, - -64.30914, - -64.32568, - -64.342186, - -64.35865, - -64.37507, - -64.39146, - -64.40781, - -64.42411, - ], - [ - -63.90091, - -63.918407, - -63.935867, - -63.953285, - -63.97066, - -63.987995, - -64.00529, - -64.022545, - -64.03976, - -64.05692, - -64.07405, - -64.09114, - -64.10819, - -64.1252, - -64.14216, - -64.15908, - -64.17595, - -64.192795, - -64.2096, - -64.22637, - -64.2431, - -64.259796, - -64.27645, - -64.29307, - -64.30965, - -64.32619, - -64.34269, - -64.35915, - -64.37558, - -64.39197, - -64.40832, - -64.42463, - ], - [ - -63.901344, - -63.918846, - -63.936306, - -63.953728, - -63.971107, - -63.988445, - -64.00574, - -64.022995, - -64.04021, - -64.05738, - -64.07452, - -64.091606, - -64.10866, - -64.12566, - -64.14263, - -64.15955, - -64.17642, - -64.19327, - -64.21008, - -64.22685, - -64.24359, - -64.260284, - -64.27694, - -64.293564, - -64.31014, - -64.32668, - -64.34319, - -64.35966, - -64.37608, - -64.39248, - -64.40883, - -64.42514, - ], - [ - -63.901768, - -63.91927, - -63.936733, - -63.95416, - -63.97154, - -63.98888, - -64.00618, - -64.02344, - -64.04066, - -64.05783, - -64.07497, - -64.09206, - -64.10911, - -64.12612, - -64.14309, - -64.16001, - -64.17689, - -64.19373, - -64.21055, - -64.227325, - -64.24406, - -64.26076, - -64.27742, - -64.29404, - -64.31062, - -64.32717, - -64.34367, - -64.360146, - -64.37658, - -64.39297, - -64.409325, - -64.42564, - ], - [ - -63.902176, - -63.919685, - -63.937153, - -63.95458, - -63.971962, - -63.989304, - -64.00661, - -64.023865, - -64.041084, - -64.058266, - -64.0754, - -64.09249, - -64.10955, - -64.126564, - -64.14353, - -64.16046, - -64.17734, - -64.19419, - -64.211006, - -64.22778, - -64.24452, - -64.26122, - -64.277885, - -64.29451, - -64.3111, - -64.327644, - -64.344154, - -64.36063, - -64.37706, - -64.39345, - -64.409805, - -64.426125, - ], - [ - -63.902576, - -63.920086, - -63.937557, - -63.954983, - -63.97237, - -63.989716, - -64.00702, - -64.024284, - -64.041504, - -64.058685, - -64.07582, - -64.092926, - -64.10998, - -64.12699, - -64.14397, - -64.160904, - -64.17777, - -64.19463, - -64.21145, - -64.22823, - -64.24497, - -64.26167, - -64.27834, - -64.29497, - -64.311554, - -64.3281, - -64.34462, - -64.36109, - -64.377525, - -64.39392, - -64.41028, - -64.426605, - ], - [ - -63.902966, - -63.92048, - -63.93795, - -63.95538, - -63.97277, - -63.990116, - -64.00742, - -64.02469, - -64.041916, - -64.0591, - -64.07624, - -64.09334, - -64.1104, - -64.12742, - -64.144394, - -64.16132, - -64.17821, - -64.19506, - -64.21188, - -64.22867, - -64.24541, - -64.262115, - -64.278786, - -64.29541, - -64.312004, - -64.32856, - -64.34507, - -64.36155, - -64.37798, - -64.39439, - -64.41074, - -64.42707, - ], - [ - -63.903343, - -63.920856, - -63.93833, - -63.955765, - -63.973156, - -63.990505, - -64.00781, - -64.025085, - -64.04231, - -64.059494, - -64.07664, - -64.09374, - -64.1108, - -64.12782, - -64.144806, - -64.16174, - -64.17863, - -64.19549, - -64.21231, - -64.229095, - -64.24584, - -64.26255, - -64.27921, - -64.295845, - -64.31244, - -64.328995, - -64.34551, - -64.36199, - -64.37843, - -64.39484, - -64.411194, - -64.42752, - ], - [ - -63.903706, - -63.921227, - -63.9387, - -63.95614, - -63.973534, - -63.990887, - -64.008194, - -64.02547, - -64.042694, - -64.05988, - -64.077034, - -64.09414, - -64.1112, - -64.12822, - -64.1452, - -64.16215, - -64.17903, - -64.19589, - -64.21272, - -64.22951, - -64.246254, - -64.26297, - -64.27964, - -64.29627, - -64.31287, - -64.32942, - -64.34595, - -64.36243, - -64.37887, - -64.39527, - -64.41164, - -64.42796, - ], - [ - -63.90406, - -63.92158, - -63.93906, - -63.956497, - -63.973896, - -63.991253, - -64.00857, - -64.02584, - -64.04307, - -64.06026, - -64.07741, - -64.09452, - -64.11158, - -64.12861, - -64.14559, - -64.16254, - -64.17943, - -64.1963, - -64.21312, - -64.22991, - -64.246666, - -64.263374, - -64.28005, - -64.296684, - -64.313286, - -64.32984, - -64.34637, - -64.36285, - -64.379295, - -64.3957, - -64.41207, - -64.4284, - ], - [ - -63.904404, - -63.921925, - -63.939407, - -63.95685, - -63.974247, - -63.991608, - -64.00893, - -64.0262, - -64.043434, - -64.06062, - -64.077774, - -64.09489, - -64.11195, - -64.12898, - -64.14597, - -64.16292, - -64.17982, - -64.196686, - -64.21351, - -64.2303, - -64.247055, - -64.26377, - -64.28045, - -64.29709, - -64.31369, - -64.33025, - -64.34678, - -64.36326, - -64.37971, - -64.39612, - -64.41248, - -64.42882, - ], - [ - -63.90473, - -63.92226, - -63.939743, - -63.957188, - -63.974586, - -63.991947, - -64.00927, - -64.02654, - -64.043785, - -64.06098, - -64.07813, - -64.095245, - -64.11232, - -64.12935, - -64.14633, - -64.163284, - -64.18019, - -64.19706, - -64.21389, - -64.23069, - -64.247444, - -64.26416, - -64.28084, - -64.29748, - -64.31409, - -64.33065, - -64.347176, - -64.36366, - -64.38011, - -64.39652, - -64.412895, - -64.42923, - ], - ], - dtype="float32", - ) + rec_lon[...] = arrays["rec_lon"] rec_lat = n.createVariable("rec_lat", "f4", ("track", "scan")) rec_lat.standard_name = "latitude" rec_lat.units = "degrees_north" - rec_lat[...] = np.array( - [ - [ - 31.443592, - 31.443207, - 31.44282, - 31.44243, - 31.44204, - 31.441648, - 31.441254, - 31.44086, - 31.440464, - 31.440067, - 31.439669, - 31.439268, - 31.438868, - 31.438465, - 31.43806, - 31.437656, - 31.43725, - 31.436842, - 31.436434, - 31.436024, - 31.435614, - 31.4352, - 31.434788, - 31.434372, - 31.433956, - 31.43354, - 31.43312, - 31.432703, - 31.432281, - 31.43186, - 31.431438, - 31.431015, - ], - [ - 31.458286, - 31.457888, - 31.457487, - 31.457085, - 31.456682, - 31.456278, - 31.455872, - 31.455465, - 31.455057, - 31.454647, - 31.454237, - 31.453825, - 31.453411, - 31.452995, - 31.45258, - 31.452164, - 31.451744, - 31.451324, - 31.450905, - 31.450483, - 31.45006, - 31.449635, - 31.44921, - 31.448784, - 31.448355, - 31.447927, - 31.447496, - 31.447065, - 31.446632, - 31.4462, - 31.445766, - 31.44533, - ], - [ - 31.472979, - 31.472569, - 31.472155, - 31.47174, - 31.471325, - 31.47091, - 31.47049, - 31.470072, - 31.46965, - 31.469229, - 31.468805, - 31.46838, - 31.467955, - 31.467527, - 31.4671, - 31.46667, - 31.46624, - 31.465809, - 31.465376, - 31.464941, - 31.464506, - 31.46407, - 31.463633, - 31.463194, - 31.462755, - 31.462313, - 31.461872, - 31.46143, - 31.460985, - 31.460539, - 31.460093, - 31.459646, - ], - [ - 31.487673, - 31.48725, - 31.486824, - 31.486397, - 31.485968, - 31.485538, - 31.48511, - 31.484676, - 31.484243, - 31.483809, - 31.483374, - 31.482937, - 31.482498, - 31.48206, - 31.481619, - 31.481178, - 31.480736, - 31.480291, - 31.479847, - 31.4794, - 31.478954, - 31.478506, - 31.478056, - 31.477606, - 31.477154, - 31.476702, - 31.476248, - 31.475792, - 31.475336, - 31.47488, - 31.474422, - 31.473963, - ], - [ - 31.502365, - 31.50193, - 31.501492, - 31.501053, - 31.500612, - 31.50017, - 31.499727, - 31.499283, - 31.498837, - 31.49839, - 31.497942, - 31.497494, - 31.497044, - 31.496592, - 31.49614, - 31.495686, - 31.495232, - 31.494776, - 31.494318, - 31.49386, - 31.4934, - 31.49294, - 31.49248, - 31.492016, - 31.491552, - 31.491089, - 31.490623, - 31.490156, - 31.489689, - 31.48922, - 31.48875, - 31.48828, - ], - [ - 31.51706, - 31.516611, - 31.516161, - 31.515709, - 31.515255, - 31.514801, - 31.514345, - 31.51389, - 31.513432, - 31.512972, - 31.512512, - 31.51205, - 31.511587, - 31.511124, - 31.510658, - 31.510193, - 31.509727, - 31.509258, - 31.50879, - 31.50832, - 31.507849, - 31.507376, - 31.506903, - 31.506428, - 31.505953, - 31.505476, - 31.505, - 31.50452, - 31.50404, - 31.503561, - 31.503078, - 31.502596, - ], - [ - 31.531754, - 31.531292, - 31.530828, - 31.530365, - 31.5299, - 31.529432, - 31.528965, - 31.528496, - 31.528025, - 31.527554, - 31.52708, - 31.526608, - 31.526133, - 31.525656, - 31.525179, - 31.524702, - 31.524223, - 31.523743, - 31.523262, - 31.52278, - 31.522297, - 31.521812, - 31.521326, - 31.52084, - 31.520353, - 31.519865, - 31.519375, - 31.518885, - 31.518393, - 31.5179, - 31.517408, - 31.516914, - ], - [ - 31.546448, - 31.545975, - 31.545498, - 31.545021, - 31.544544, - 31.544064, - 31.543585, - 31.543102, - 31.54262, - 31.542135, - 31.54165, - 31.541164, - 31.540678, - 31.54019, - 31.5397, - 31.53921, - 31.53872, - 31.538227, - 31.537733, - 31.53724, - 31.536745, - 31.536247, - 31.535751, - 31.535252, - 31.534754, - 31.534252, - 31.53375, - 31.533249, - 31.532745, - 31.532242, - 31.531736, - 31.53123, - ], - [ - 31.561144, - 31.560656, - 31.560167, - 31.559679, - 31.559189, - 31.558697, - 31.558203, - 31.557709, - 31.557215, - 31.556719, - 31.556221, - 31.555723, - 31.555223, - 31.554722, - 31.55422, - 31.553719, - 31.553215, - 31.552711, - 31.552206, - 31.5517, - 31.551193, - 31.550684, - 31.550175, - 31.549665, - 31.549154, - 31.548641, - 31.548128, - 31.547615, - 31.5471, - 31.546583, - 31.546066, - 31.54555, - ], - [ - 31.575838, - 31.575338, - 31.574839, - 31.574335, - 31.573833, - 31.573328, - 31.572823, - 31.572317, - 31.57181, - 31.5713, - 31.570791, - 31.57028, - 31.569769, - 31.569256, - 31.568743, - 31.568228, - 31.567713, - 31.567196, - 31.566679, - 31.56616, - 31.565641, - 31.56512, - 31.5646, - 31.564077, - 31.563555, - 31.56303, - 31.562506, - 31.56198, - 31.561453, - 31.560925, - 31.560396, - 31.559868, - ], - [ - 31.590534, - 31.590021, - 31.589508, - 31.588993, - 31.588478, - 31.587961, - 31.587444, - 31.586926, - 31.586405, - 31.585884, - 31.585361, - 31.584839, - 31.584314, - 31.58379, - 31.583263, - 31.582737, - 31.58221, - 31.581682, - 31.581152, - 31.580622, - 31.580091, - 31.57956, - 31.579025, - 31.578491, - 31.577955, - 31.57742, - 31.576883, - 31.576345, - 31.575808, - 31.575268, - 31.574726, - 31.574186, - ], - [ - 31.605228, - 31.604704, - 31.60418, - 31.603651, - 31.603125, - 31.602594, - 31.602064, - 31.601534, - 31.601002, - 31.600468, - 31.599934, - 31.599398, - 31.598862, - 31.598324, - 31.597786, - 31.597248, - 31.596708, - 31.596167, - 31.595627, - 31.595083, - 31.59454, - 31.593996, - 31.59345, - 31.592905, - 31.592358, - 31.59181, - 31.59126, - 31.590712, - 31.59016, - 31.58961, - 31.589058, - 31.588505, - ], - [ - 31.619925, - 31.619389, - 31.618849, - 31.61831, - 31.61777, - 31.617228, - 31.616686, - 31.616142, - 31.615597, - 31.615051, - 31.614506, - 31.613956, - 31.613409, - 31.61286, - 31.612309, - 31.611757, - 31.611206, - 31.610653, - 31.6101, - 31.609545, - 31.60899, - 31.608435, - 31.607876, - 31.607319, - 31.60676, - 31.6062, - 31.605639, - 31.605078, - 31.604515, - 31.603952, - 31.60339, - 31.602825, - ], - [ - 31.634623, - 31.634071, - 31.633522, - 31.632969, - 31.632416, - 31.631863, - 31.631308, - 31.63075, - 31.630194, - 31.629637, - 31.629078, - 31.628517, - 31.627956, - 31.627394, - 31.626831, - 31.626268, - 31.625706, - 31.62514, - 31.624575, - 31.624008, - 31.62344, - 31.622871, - 31.622303, - 31.621733, - 31.621162, - 31.62059, - 31.620018, - 31.619446, - 31.618872, - 31.618296, - 31.617722, - 31.617144, - ], - [ - 31.649319, - 31.648756, - 31.648193, - 31.647629, - 31.647062, - 31.646496, - 31.64593, - 31.64536, - 31.64479, - 31.64422, - 31.64365, - 31.643078, - 31.642504, - 31.64193, - 31.641356, - 31.64078, - 31.640203, - 31.639627, - 31.63905, - 31.638472, - 31.637892, - 31.637312, - 31.63673, - 31.636148, - 31.635565, - 31.634981, - 31.634398, - 31.633812, - 31.633226, - 31.63264, - 31.632053, - 31.631464, - ], - [ - 31.664017, - 31.66344, - 31.662865, - 31.662289, - 31.66171, - 31.66113, - 31.660551, - 31.659971, - 31.65939, - 31.658806, - 31.658222, - 31.657639, - 31.657053, - 31.656467, - 31.65588, - 31.655293, - 31.654703, - 31.654114, - 31.653524, - 31.652935, - 31.652342, - 31.65175, - 31.651157, - 31.650564, - 31.64997, - 31.649374, - 31.648777, - 31.64818, - 31.647583, - 31.646984, - 31.646385, - 31.645786, - ], - [ - 31.546421, - 31.546041, - 31.54566, - 31.545277, - 31.544891, - 31.544506, - 31.544119, - 31.54373, - 31.54334, - 31.542948, - 31.542555, - 31.542162, - 31.541765, - 31.541368, - 31.540972, - 31.540571, - 31.54017, - 31.539768, - 31.539366, - 31.53896, - 31.538553, - 31.538145, - 31.537737, - 31.537327, - 31.536917, - 31.536503, - 31.53609, - 31.535675, - 31.53526, - 31.534843, - 31.534424, - 31.534006, - ], - [ - 31.561115, - 31.560722, - 31.560328, - 31.559933, - 31.559536, - 31.559137, - 31.558737, - 31.558336, - 31.557934, - 31.55753, - 31.557125, - 31.556719, - 31.55631, - 31.555902, - 31.555492, - 31.55508, - 31.554667, - 31.554253, - 31.553837, - 31.55342, - 31.553001, - 31.552582, - 31.55216, - 31.551739, - 31.551315, - 31.550892, - 31.550467, - 31.55004, - 31.549612, - 31.549183, - 31.548754, - 31.548323, - ], - [ - 31.57581, - 31.575403, - 31.574997, - 31.574589, - 31.574179, - 31.573769, - 31.573357, - 31.572943, - 31.572529, - 31.572111, - 31.571693, - 31.571276, - 31.570856, - 31.570435, - 31.570011, - 31.569588, - 31.569164, - 31.568737, - 31.568308, - 31.567879, - 31.56745, - 31.567019, - 31.566586, - 31.56615, - 31.565716, - 31.565279, - 31.564842, - 31.564404, - 31.563965, - 31.563524, - 31.563084, - 31.56264, - ], - [ - 31.590504, - 31.590086, - 31.589666, - 31.589245, - 31.588823, - 31.5884, - 31.587976, - 31.58755, - 31.587122, - 31.586693, - 31.586264, - 31.585833, - 31.585402, - 31.584967, - 31.584532, - 31.584097, - 31.58366, - 31.583221, - 31.58278, - 31.58234, - 31.581898, - 31.581453, - 31.581009, - 31.580563, - 31.580116, - 31.579668, - 31.57922, - 31.57877, - 31.578318, - 31.577866, - 31.577412, - 31.576958, - ], - [ - 31.605198, - 31.604767, - 31.604336, - 31.603903, - 31.603468, - 31.603031, - 31.602594, - 31.602156, - 31.601717, - 31.601276, - 31.600834, - 31.600391, - 31.599945, - 31.5995, - 31.599052, - 31.598606, - 31.598156, - 31.597706, - 31.597254, - 31.5968, - 31.596346, - 31.59589, - 31.595434, - 31.594976, - 31.594517, - 31.594057, - 31.593596, - 31.593134, - 31.59267, - 31.592207, - 31.591742, - 31.591276, - ], - [ - 31.619892, - 31.61945, - 31.619005, - 31.618559, - 31.618113, - 31.617664, - 31.617214, - 31.616764, - 31.616312, - 31.615858, - 31.615404, - 31.614948, - 31.61449, - 31.614033, - 31.613575, - 31.613113, - 31.612654, - 31.61219, - 31.611727, - 31.611261, - 31.610794, - 31.610327, - 31.609858, - 31.609388, - 31.608917, - 31.608446, - 31.607973, - 31.607498, - 31.607025, - 31.606548, - 31.606071, - 31.605595, - ], - [ - 31.634588, - 31.634132, - 31.633675, - 31.633217, - 31.632757, - 31.632296, - 31.631834, - 31.63137, - 31.630907, - 31.630442, - 31.629974, - 31.629507, - 31.629038, - 31.628567, - 31.628096, - 31.627623, - 31.62715, - 31.626675, - 31.6262, - 31.625721, - 31.625242, - 31.624763, - 31.624283, - 31.623802, - 31.623318, - 31.622835, - 31.62235, - 31.621864, - 31.621378, - 31.62089, - 31.620401, - 31.619913, - ], - [ - 31.649282, - 31.648815, - 31.648346, - 31.647875, - 31.647402, - 31.646929, - 31.646454, - 31.645979, - 31.645502, - 31.645023, - 31.644545, - 31.644064, - 31.643583, - 31.6431, - 31.642618, - 31.642134, - 31.641647, - 31.641161, - 31.640673, - 31.640182, - 31.639692, - 31.6392, - 31.638708, - 31.638214, - 31.63772, - 31.637224, - 31.636728, - 31.63623, - 31.635733, - 31.635233, - 31.634731, - 31.634232, - ], - [ - 31.663979, - 31.663498, - 31.663015, - 31.662533, - 31.662048, - 31.661562, - 31.661076, - 31.660587, - 31.660097, - 31.659607, - 31.659117, - 31.658623, - 31.65813, - 31.657635, - 31.657139, - 31.656643, - 31.656145, - 31.655645, - 31.655146, - 31.654644, - 31.654142, - 31.653639, - 31.653133, - 31.652628, - 31.652122, - 31.651615, - 31.651106, - 31.650597, - 31.650085, - 31.649574, - 31.649063, - 31.64855, - ], - [ - 31.678675, - 31.67818, - 31.677687, - 31.67719, - 31.676693, - 31.676195, - 31.675695, - 31.675196, - 31.674694, - 31.67419, - 31.673687, - 31.673182, - 31.672676, - 31.672169, - 31.671661, - 31.671152, - 31.670643, - 31.670132, - 31.669619, - 31.669106, - 31.66859, - 31.668076, - 31.667559, - 31.667042, - 31.666523, - 31.666004, - 31.665483, - 31.664963, - 31.66444, - 31.663918, - 31.663395, - 31.66287, - ], - [ - 31.69337, - 31.692865, - 31.692358, - 31.691849, - 31.69134, - 31.690828, - 31.690317, - 31.689804, - 31.689291, - 31.688776, - 31.68826, - 31.687742, - 31.687223, - 31.686705, - 31.686184, - 31.685663, - 31.68514, - 31.684618, - 31.684093, - 31.683569, - 31.683043, - 31.682514, - 31.681986, - 31.681456, - 31.680925, - 31.680395, - 31.679863, - 31.679329, - 31.678797, - 31.67826, - 31.677725, - 31.677189, - ], - [ - 31.708067, - 31.707548, - 31.70703, - 31.706509, - 31.705986, - 31.705463, - 31.704939, - 31.704412, - 31.703886, - 31.70336, - 31.702831, - 31.702301, - 31.70177, - 31.70124, - 31.700708, - 31.700174, - 31.69964, - 31.699104, - 31.698568, - 31.69803, - 31.697493, - 31.696953, - 31.696413, - 31.695871, - 31.69533, - 31.694786, - 31.694242, - 31.693697, - 31.693151, - 31.692604, - 31.692057, - 31.69151, - ], - [ - 31.722765, - 31.722233, - 31.7217, - 31.721167, - 31.720633, - 31.720097, - 31.71956, - 31.719023, - 31.718485, - 31.717945, - 31.717403, - 31.716862, - 31.71632, - 31.715776, - 31.71523, - 31.714685, - 31.71414, - 31.713593, - 31.713043, - 31.712494, - 31.711943, - 31.711391, - 31.71084, - 31.710287, - 31.709732, - 31.709177, - 31.708622, - 31.708065, - 31.707508, - 31.70695, - 31.70639, - 31.70583, - ], - [ - 31.737461, - 31.736917, - 31.736374, - 31.735826, - 31.735281, - 31.734732, - 31.734182, - 31.733633, - 31.733082, - 31.732529, - 31.731977, - 31.731422, - 31.730867, - 31.730312, - 31.729755, - 31.729197, - 31.72864, - 31.728079, - 31.727518, - 31.726957, - 31.726395, - 31.725832, - 31.725267, - 31.7247, - 31.724136, - 31.723568, - 31.723001, - 31.722433, - 31.721863, - 31.721294, - 31.720722, - 31.720152, - ], - [ - 31.75216, - 31.751602, - 31.751045, - 31.750486, - 31.749928, - 31.749367, - 31.748806, - 31.748243, - 31.74768, - 31.747116, - 31.74655, - 31.745983, - 31.745417, - 31.744848, - 31.74428, - 31.74371, - 31.74314, - 31.742567, - 31.741995, - 31.74142, - 31.740847, - 31.74027, - 31.739695, - 31.739119, - 31.73854, - 31.73796, - 31.737381, - 31.736801, - 31.73622, - 31.735638, - 31.735056, - 31.734472, - ], - [ - 31.766857, - 31.766289, - 31.765718, - 31.765148, - 31.764576, - 31.764004, - 31.76343, - 31.762854, - 31.762278, - 31.761702, - 31.761124, - 31.760546, - 31.759966, - 31.759386, - 31.758804, - 31.758223, - 31.757639, - 31.757055, - 31.756472, - 31.755886, - 31.755299, - 31.754711, - 31.754124, - 31.753534, - 31.752945, - 31.752354, - 31.751762, - 31.751171, - 31.750578, - 31.749985, - 31.74939, - 31.748795, - ], - [ - 31.648563, - 31.64819, - 31.647814, - 31.647438, - 31.64706, - 31.64668, - 31.6463, - 31.645916, - 31.645533, - 31.645147, - 31.644762, - 31.644373, - 31.643984, - 31.643593, - 31.643202, - 31.642809, - 31.642414, - 31.642017, - 31.641619, - 31.64122, - 31.64082, - 31.640417, - 31.640015, - 31.63961, - 31.639204, - 31.638798, - 31.63839, - 31.637981, - 31.637571, - 31.63716, - 31.636747, - 31.636333, - ], - [ - 31.663258, - 31.66287, - 31.662483, - 31.662094, - 31.661703, - 31.66131, - 31.660917, - 31.660522, - 31.660126, - 31.659729, - 31.65933, - 31.65893, - 31.658527, - 31.658125, - 31.65772, - 31.657316, - 31.65691, - 31.6565, - 31.65609, - 31.65568, - 31.655266, - 31.654852, - 31.654438, - 31.654022, - 31.653605, - 31.653185, - 31.652765, - 31.652346, - 31.651922, - 31.6515, - 31.651075, - 31.65065, - ], - [ - 31.677952, - 31.677551, - 31.67715, - 31.67675, - 31.676346, - 31.675941, - 31.675537, - 31.675129, - 31.67472, - 31.67431, - 31.673899, - 31.673487, - 31.673073, - 31.672657, - 31.672241, - 31.671824, - 31.671404, - 31.670984, - 31.670563, - 31.67014, - 31.669714, - 31.669289, - 31.668861, - 31.668432, - 31.668003, - 31.667574, - 31.667141, - 31.666708, - 31.666275, - 31.66584, - 31.665403, - 31.664967, - ], - [ - 31.692644, - 31.692234, - 31.69182, - 31.691406, - 31.69099, - 31.690573, - 31.690155, - 31.689735, - 31.689314, - 31.688892, - 31.688469, - 31.688044, - 31.687618, - 31.68719, - 31.686762, - 31.68633, - 31.6859, - 31.685467, - 31.685034, - 31.684599, - 31.684162, - 31.683723, - 31.683285, - 31.682844, - 31.682404, - 31.681961, - 31.681519, - 31.681072, - 31.680628, - 31.680182, - 31.679733, - 31.679285, - ], - [ - 31.70734, - 31.706915, - 31.70649, - 31.706062, - 31.705635, - 31.705206, - 31.704775, - 31.704342, - 31.703909, - 31.703474, - 31.703037, - 31.7026, - 31.702162, - 31.701723, - 31.701283, - 31.70084, - 31.700397, - 31.699951, - 31.699505, - 31.699059, - 31.698608, - 31.69816, - 31.697708, - 31.697256, - 31.696804, - 31.69635, - 31.695894, - 31.695438, - 31.69498, - 31.69452, - 31.694061, - 31.693602, - ], - [ - 31.722034, - 31.721598, - 31.721159, - 31.72072, - 31.72028, - 31.719837, - 31.719393, - 31.718948, - 31.718504, - 31.718056, - 31.717607, - 31.717157, - 31.716707, - 31.716255, - 31.715803, - 31.71535, - 31.714893, - 31.714436, - 31.713978, - 31.713518, - 31.713057, - 31.712595, - 31.712133, - 31.711668, - 31.711205, - 31.710737, - 31.71027, - 31.709803, - 31.709333, - 31.708862, - 31.708391, - 31.707918, - ], - [ - 31.736729, - 31.736279, - 31.735828, - 31.735376, - 31.734924, - 31.734468, - 31.734013, - 31.733557, - 31.733097, - 31.73264, - 31.732178, - 31.731716, - 31.731253, - 31.73079, - 31.730324, - 31.729856, - 31.72939, - 31.72892, - 31.72845, - 31.727978, - 31.727507, - 31.727032, - 31.726557, - 31.726082, - 31.725605, - 31.725126, - 31.724648, - 31.724167, - 31.723686, - 31.723204, - 31.722721, - 31.722237, - ], - [ - 31.751425, - 31.750961, - 31.750498, - 31.750034, - 31.749569, - 31.749102, - 31.748632, - 31.748163, - 31.747692, - 31.747221, - 31.746748, - 31.746273, - 31.745798, - 31.745321, - 31.744844, - 31.744366, - 31.743887, - 31.743404, - 31.742922, - 31.74244, - 31.741955, - 31.741468, - 31.740982, - 31.740494, - 31.740005, - 31.739515, - 31.739025, - 31.738533, - 31.738039, - 31.737545, - 31.737051, - 31.736555, - ], - [ - 31.766119, - 31.765644, - 31.76517, - 31.764692, - 31.764214, - 31.763735, - 31.763254, - 31.762772, - 31.76229, - 31.761805, - 31.761318, - 31.760832, - 31.760345, - 31.759855, - 31.759367, - 31.758875, - 31.758383, - 31.75789, - 31.757395, - 31.7569, - 31.756403, - 31.755905, - 31.755407, - 31.754908, - 31.754406, - 31.753904, - 31.7534, - 31.752897, - 31.752394, - 31.751888, - 31.75138, - 31.750874, - ], - [ - 31.780815, - 31.780329, - 31.779839, - 31.77935, - 31.778858, - 31.778368, - 31.777874, - 31.77738, - 31.776884, - 31.776388, - 31.77589, - 31.77539, - 31.77489, - 31.77439, - 31.773888, - 31.773384, - 31.77288, - 31.772375, - 31.77187, - 31.771362, - 31.770853, - 31.770344, - 31.769833, - 31.76932, - 31.768808, - 31.768293, - 31.767778, - 31.767263, - 31.766747, - 31.76623, - 31.76571, - 31.765192, - ], - [ - 31.795511, - 31.795012, - 31.79451, - 31.794008, - 31.793505, - 31.793001, - 31.792494, - 31.791988, - 31.791481, - 31.790972, - 31.79046, - 31.78995, - 31.789438, - 31.788925, - 31.78841, - 31.787895, - 31.787378, - 31.786861, - 31.786343, - 31.785824, - 31.785303, - 31.78478, - 31.784258, - 31.783733, - 31.783209, - 31.782684, - 31.782158, - 31.78163, - 31.781101, - 31.780573, - 31.780043, - 31.779512, - ], - [ - 31.810207, - 31.809694, - 31.809181, - 31.808666, - 31.808151, - 31.807634, - 31.807116, - 31.806597, - 31.806076, - 31.805555, - 31.805033, - 31.80451, - 31.803986, - 31.80346, - 31.802933, - 31.802406, - 31.801878, - 31.801348, - 31.800817, - 31.800285, - 31.799753, - 31.79922, - 31.798683, - 31.79815, - 31.797611, - 31.797073, - 31.796535, - 31.795998, - 31.795456, - 31.794916, - 31.794374, - 31.79383, - ], - [ - 31.824903, - 31.824379, - 31.823853, - 31.823326, - 31.822798, - 31.822268, - 31.821737, - 31.821205, - 31.820673, - 31.820139, - 31.819605, - 31.819069, - 31.818533, - 31.817995, - 31.817455, - 31.816916, - 31.816376, - 31.815834, - 31.81529, - 31.814747, - 31.814203, - 31.813658, - 31.81311, - 31.812563, - 31.812014, - 31.811464, - 31.810915, - 31.810364, - 31.809813, - 31.80926, - 31.808706, - 31.808151, - ], - [ - 31.839602, - 31.839064, - 31.838526, - 31.837986, - 31.837444, - 31.836903, - 31.836359, - 31.835815, - 31.83527, - 31.834724, - 31.834177, - 31.83363, - 31.83308, - 31.832531, - 31.83198, - 31.831427, - 31.830875, - 31.83032, - 31.829765, - 31.82921, - 31.828653, - 31.828096, - 31.827538, - 31.826979, - 31.826418, - 31.825857, - 31.825294, - 31.824732, - 31.824167, - 31.823603, - 31.823038, - 31.822472, - ], - [ - 31.8543, - 31.853748, - 31.853197, - 31.852646, - 31.852093, - 31.851538, - 31.850983, - 31.850426, - 31.849869, - 31.84931, - 31.848751, - 31.84819, - 31.84763, - 31.847067, - 31.846504, - 31.84594, - 31.845375, - 31.844809, - 31.844242, - 31.843674, - 31.843105, - 31.842535, - 31.841965, - 31.841393, - 31.84082, - 31.840248, - 31.839674, - 31.8391, - 31.838524, - 31.837948, - 31.83737, - 31.836792, - ], - [ - 31.868998, - 31.868435, - 31.86787, - 31.867306, - 31.86674, - 31.866173, - 31.865606, - 31.865036, - 31.864466, - 31.863895, - 31.863323, - 31.862751, - 31.862177, - 31.861603, - 31.861029, - 31.86045, - 31.859875, - 31.859297, - 31.858717, - 31.858137, - 31.857557, - 31.856976, - 31.856392, - 31.855808, - 31.855225, - 31.85464, - 31.854053, - 31.853468, - 31.85288, - 31.852293, - 31.851704, - 31.851114, - ], - ], - dtype="float32", - ) + rec_lat[...] = arrays["rec_lat"] # Interpolation variables tp_interpolation = n.createVariable("tp_interpolation", "i4", ()) @@ -5259,10354 +1954,271 @@ def _make_subsampled_2(filename): r[...] = np.arange(48 * 32).reshape(48, 32) n.close() - return filename -def _make_regrid_file(filename): - n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") +def _make_ugrid_1(filename): + """Create a UGRID file with a 2-d mesh topology.""" + n = netCDF4.Dataset(filename, "w") - n.Conventions = "CF-" + VN + n.Conventions = f"CF-{VN} UGRID-1.0" n.createDimension("time", 2) - n.createDimension("bounds2", 2) - n.createDimension("latitude", 30) - n.createDimension("longitude", 48) - n.createDimension("time_1", 1) - n.createDimension("lat", 73) - n.createDimension("lon", 96) - - latitude = n.createVariable("latitude", "f8", ("latitude",)) - latitude.standard_name = "latitude" - latitude.units = "degrees_north" - latitude.bounds = "latitude_bounds" - latitude[...] = np.arange(-87, 90.0, 6) - - longitude = n.createVariable("longitude", "f8", ("longitude",)) - longitude.standard_name = "longitude" - longitude.units = "degrees_east" - longitude.bounds = "longitude_bounds" - longitude[...] = np.arange(3.75, 360, 7.5) - - lat = n.createVariable("lat", "f8", ("lat",)) - lat.standard_name = "latitude" - lat.units = "degrees_north" - lat.bounds = "lat_bounds" - lat[...] = np.arange(-90, 91.0, 2.5) - - lon = n.createVariable("lon", "f8", ("lon",)) - lon.standard_name = "longitude" - lon.units = "degrees_east" - lon.bounds = "lon_bounds" - lon[...] = np.arange(3.75, 361, 3.75) + n.createDimension("nMesh2_node", 7) + n.createDimension("nMesh2_edge", 9) + n.createDimension("nMesh2_face", 3) + n.createDimension("Two", 2) + n.createDimension("Four", 4) + + Mesh2 = n.createVariable("Mesh2", "i4", ()) + Mesh2.cf_role = "mesh_topology" + Mesh2.topology_dimension = 2 + Mesh2.node_coordinates = "Mesh2_node_x Mesh2_node_y" + Mesh2.face_node_connectivity = "Mesh2_face_nodes" + Mesh2.edge_node_connectivity = "Mesh2_edge_nodes" + Mesh2.edge_dimension = "nMesh2_edge" + Mesh2.edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" + Mesh2.face_coordinates = "Mesh2_face_x Mesh2_face_y" + Mesh2.face_edge_connectivity = "Mesh2_face_edges" + Mesh2.face_face_connectivity = "Mesh2_face_links" + Mesh2.edge_face_connectivity = "Mesh2_edge_face_links" + + Mesh2_face_nodes = n.createVariable( + "Mesh2_face_nodes", "i4", ("nMesh2_face", "Four"), fill_value=-99 + ) + Mesh2_face_nodes.long_name = "Maps every face to its corner nodes" + Mesh2_face_nodes[...] = [[2, 3, 1, 0], [4, 5, 3, 2], [1, 3, 6, -99]] - longitude_bounds = n.createVariable( - "longitude_bounds", "f8", ("longitude", "bounds2") + Mesh2_edge_nodes = n.createVariable( + "Mesh2_edge_nodes", "i4", ("Two", "nMesh2_edge") ) - longitude_bounds[..., 0] = longitude[...] - 3.75 - longitude_bounds[..., 1] = longitude[...] + 3.75 + Mesh2_edge_nodes.long_name = "Maps every edge to its two nodes" + Mesh2_edge_nodes[...] = [ + [1, 3, 3, 0, 2, 2, 2, 5, 3], + [6, 6, 1, 1, 0, 3, 4, 4, 5], + ] - latitude_bounds = n.createVariable( - "latitude_bounds", "f8", ("latitude", "bounds2") + # Optional mesh topology variables + Mesh2_face_edges = n.createVariable( + "Mesh2_face_edges", "i4", ("nMesh2_face", "Four"), fill_value=-99 ) - latitude_bounds[..., 0] = latitude[...] - 3 - latitude_bounds[..., 1] = latitude[...] + 3 + Mesh2_face_edges.long_name = "Maps every face to its edges." - lon_bounds = n.createVariable("lon_bounds", "f8", ("lon", "bounds2")) - lon_bounds[..., 0] = lon[...] - 1.875 - lon_bounds[..., 1] = lon[...] + 1.875 + Mesh2_face_links = n.createVariable( + "Mesh2_face_links", "i4", ("nMesh2_face", "Four"), fill_value=-99 + ) + Mesh2_face_links.long_name = "neighbour faces for faces" + Mesh2_face_links[...] = [ + [1, 2, -99, -99], + [0, -99, -99, -99], + [0, -99, -99, -99], + ] - lat_bounds = n.createVariable("lat_bounds", "f8", ("lat", "bounds2")) - lat_bounds[..., 0] = lat[...] - 1.25 - lat_bounds[..., 1] = lat[...] + 1.25 + Mesh2_edge_face_links = n.createVariable( + "Mesh2_edge_face_links", "i4", ("nMesh2_edge", "Two"), fill_value=-99 + ) + Mesh2_edge_face_links.long_name = "neighbour faces for edges" + + # Mesh node coordinates + Mesh2_node_x = n.createVariable("Mesh2_node_x", "f4", ("nMesh2_node",)) + Mesh2_node_x.standard_name = "longitude" + Mesh2_node_x.units = "degrees_east" + Mesh2_node_x[...] = [-45, -43, -45, -43, -45, -43, -40] + + Mesh2_node_y = n.createVariable("Mesh2_node_y", "f4", ("nMesh2_node",)) + Mesh2_node_y.standard_name = "latitude" + Mesh2_node_y.units = "degrees_north" + Mesh2_node_y[...] = [35, 35, 33, 33, 31, 31, 34] + + # Optional mesh face and edge coordinate variables + Mesh2_face_x = n.createVariable("Mesh2_face_x", "f4", ("nMesh2_face",)) + Mesh2_face_x.standard_name = "longitude" + Mesh2_face_x.units = "degrees_east" + Mesh2_face_x[...] = [-44, -44, -42] + + Mesh2_face_y = n.createVariable("Mesh2_face_y", "f4", ("nMesh2_face",)) + Mesh2_face_y.standard_name = "latitude" + Mesh2_face_y.units = "degrees_north" + Mesh2_face_y[...] = [34, 32, 34] + + Mesh2_edge_x = n.createVariable("Mesh2_edge_x", "f4", ("nMesh2_edge",)) + Mesh2_edge_x.standard_name = "longitude" + Mesh2_edge_x.units = "degrees_east" + Mesh2_edge_x[...] = [-41.5, -41.5, -43, -44, -45, -44, -45, -44, -43] + + Mesh2_edge_y = n.createVariable("Mesh2_edge_y", "f4", ("nMesh2_edge",)) + Mesh2_edge_y.standard_name = "latitude" + Mesh2_edge_y.units = "degrees_north" + Mesh2_edge_y[...] = [34.5, 33.5, 34, 35, 34, 33, 32, 31, 32] + + # Non-mesh coordinates + t = n.createVariable("time", "f8", ("time",)) + t.standard_name = "time" + t.units = "seconds since 2016-01-01 00:00:00" + t.bounds = "time_bounds" + t[...] = [43200, 129600] - time = n.createVariable("time", "f4", ("time",)) - time.standard_name = "time" - time.units = "days since 1860-1-1" - time.calendar = "360_day" - time.axis = "T" - time.bounds = "time_bounds" - time[...] = [15, 45] + t_bounds = n.createVariable("time_bounds", "f8", ("time", "Two")) + t_bounds[...] = [[0, 86400], [86400, 172800]] - time_bounds = n.createVariable("time_bounds", "f4", ("time", "bounds2")) - time_bounds[...] = [ - [ - 0, - 30, - ], - [30, 60], + # Data variables + ta = n.createVariable("ta", "f4", ("time", "nMesh2_face")) + ta.standard_name = "air_temperature" + ta.units = "K" + ta.mesh = "Mesh2" + ta.location = "face" + ta.coordinates = "Mesh2_face_x Mesh2_face_y" + ta[...] = [[282.96, 282.69, 283.21], [281.53, 280.99, 281.23]] + + v = n.createVariable("v", "f4", ("time", "nMesh2_edge")) + v.standard_name = "northward_wind" + v.units = "ms-1" + v.mesh = "Mesh2" + v.location = "edge" + v.coordinates = "Mesh2_edge_x Mesh2_edge_y" + v[...] = [ + [10.2, 10.63, 8.74, 9.05, 8.15, 10.89, 8.44, 10.66, 8.93], + [9.66, 10.74, 9.24, 10.58, 9.79, 10.27, 10.58, 11.68, 11.22], ] - time_1 = n.createVariable("time_1", "f4", ("time_1",)) - time_1.standard_name = "time" - time_1.units = "days since 1860-1-1" - time_1.calendar = "360_day" - time_1.axis = "T" - time_1.bounds = "time_1_bounds" - time_1[...] = 15 - - time_1_bounds = n.createVariable( - "time_1_bounds", "f4", ("time_1", "bounds2") - ) - time_1_bounds[...] = [0, 30] - - height = n.createVariable("height", "f8", ()) - height.units = "m" - height.standard_name = "height" - height.positive = "up" - height.axis = "Z" - height[...] = 2 - - src = n.createVariable("src", "f8", ("time", "latitude", "longitude")) - src.standard_name = "air_temperature" - src.units = "K" - src.coordinates = "height" - src.cell_methods = "time: mean" - - # Don't generate this data randomly - it's useful to see the real - # patterns of global temperature. - src[...] = [ - [ - [ - 243.6, - 242.4, - 241.8, - 241.5, - 241.2, - 240.8, - 240.5, - 240.4, - 240.2, - 240.5, - 241.2, - 241.9, - 242.5, - 243.0, - 243.4, - 244.1, - 245.2, - 246.4, - 247.0, - 247.4, - 248.3, - 250.2, - 251.6, - 252.3, - 253.9, - 255.8, - 257.6, - 258.7, - 258.5, - 257.7, - 256.8, - 255.6, - 254.1, - 252.0, - 251.6, - 252.4, - 254.0, - 255.1, - 254.8, - 254.3, - 253.4, - 252.6, - 251.5, - 249.9, - 248.3, - 246.4, - 244.9, - 244.2, - ], - [ - 243.3, - 241.0, - 239.8, - 238.3, - 237.4, - 237.4, - 238.9, - 240.3, - 241.3, - 241.2, - 241.2, - 240.8, - 241.0, - 242.6, - 244.4, - 245.5, - 246.4, - 248.2, - 249.8, - 251.0, - 254.3, - 260.7, - 265.4, - 266.1, - 265.9, - 265.9, - 266.4, - 266.1, - 265.3, - 264.2, - 262.8, - 261.8, - 261.0, - 258.7, - 256.9, - 257.3, - 259.5, - 262.7, - 264.4, - 264.9, - 265.4, - 264.2, - 264.7, - 263.4, - 258.3, - 251.8, - 247.2, - 245.4, - ], - [ - 248.6, - 245.7, - 245.6, - 244.7, - 243.3, - 243.3, - 244.2, - 245.2, - 249.4, - 251.7, - 248.8, - 245.0, - 243.0, - 244.0, - 245.7, - 245.2, - 244.7, - 246.0, - 246.9, - 248.7, - 252.4, - 257.6, - 266.8, - 269.5, - 269.7, - 270.1, - 270.5, - 270.6, - 270.2, - 269.2, - 266.7, - 266.1, - 266.7, - 267.5, - 267.3, - 266.2, - 266.5, - 268.1, - 267.3, - 267.5, - 271.5, - 271.5, - 271.5, - 271.0, - 267.6, - 261.5, - 255.5, - 252.0, - ], - [ - 270.3, - 269.9, - 269.6, - 268.4, - 266.0, - 264.0, - 258.8, - 256.8, - 262.0, - 265.2, - 263.6, - 257.3, - 254.3, - 253.9, - 255.6, - 256.9, - 255.9, - 253.9, - 254.5, - 261.0, - 263.8, - 267.9, - 270.5, - 271.8, - 272.3, - 272.6, - 273.4, - 273.9, - 273.7, - 273.6, - 273.3, - 273.4, - 273.9, - 273.8, - 273.8, - 273.6, - 274.4, - 274.6, - 271.1, - 268.0, - 271.2, - 271.5, - 271.4, - 271.3, - 271.6, - 271.5, - 270.8, - 270.5, - ], - [ - 274.5, - 274.3, - 274.4, - 274.3, - 274.5, - 274.5, - 273.4, - 273.1, - 273.2, - 273.3, - 273.0, - 271.5, - 271.6, - 272.5, - 272.8, - 272.8, - 272.6, - 272.5, - 272.1, - 272.5, - 273.5, - 273.6, - 274.0, - 274.8, - 274.9, - 275.0, - 275.0, - 275.0, - 274.9, - 274.7, - 274.8, - 274.9, - 275.1, - 275.2, - 275.5, - 275.8, - 276.0, - 276.2, - 276.0, - 273.4, - 272.8, - 273.9, - 274.5, - 274.6, - 274.7, - 274.7, - 274.7, - 274.6, - ], - [ - 275.3, - 275.2, - 275.4, - 275.3, - 275.4, - 275.7, - 275.7, - 275.8, - 275.9, - 275.5, - 274.9, - 274.5, - 274.3, - 274.6, - 274.7, - 275.0, - 275.2, - 275.3, - 275.5, - 275.6, - 276.0, - 276.7, - 277.0, - 276.8, - 276.8, - 277.0, - 277.1, - 276.8, - 276.7, - 277.1, - 277.0, - 277.4, - 277.3, - 277.2, - 277.2, - 277.3, - 277.5, - 278.0, - 278.8, - 279.1, - 278.3, - 278.2, - 277.8, - 277.3, - 277.4, - 276.9, - 276.4, - 275.8, - ], - [ - 278.6, - 278.7, - 278.4, - 278.0, - 277.8, - 278.0, - 278.3, - 278.2, - 278.1, - 278.0, - 278.7, - 279.4, - 279.1, - 279.2, - 279.1, - 278.9, - 279.0, - 279.6, - 279.8, - 280.1, - 280.2, - 280.6, - 281.1, - 280.0, - 280.0, - 280.5, - 281.2, - 281.0, - 280.8, - 281.3, - 281.6, - 281.4, - 281.1, - 280.7, - 280.3, - 280.1, - 280.3, - 280.7, - 280.8, - 283.0, - 281.4, - 280.3, - 279.6, - 279.4, - 279.7, - 279.9, - 279.1, - 278.6, - ], - [ - 283.9, - 283.2, - 282.5, - 281.8, - 281.6, - 282.0, - 282.4, - 282.3, - 282.3, - 283.0, - 284.3, - 284.2, - 284.0, - 283.7, - 283.6, - 283.7, - 284.0, - 284.2, - 284.3, - 285.0, - 285.2, - 285.1, - 285.2, - 286.9, - 287.3, - 286.5, - 286.2, - 285.7, - 285.6, - 285.7, - 285.5, - 285.2, - 285.0, - 284.5, - 284.0, - 283.4, - 283.0, - 283.1, - 283.0, - 288.0, - 285.5, - 285.2, - 284.0, - 283.2, - 283.4, - 284.7, - 284.9, - 284.6, - ], - [ - 289.3, - 288.6, - 288.9, - 289.4, - 288.8, - 289.1, - 289.5, - 289.3, - 289.2, - 289.6, - 289.4, - 288.9, - 288.4, - 288.1, - 288.0, - 288.2, - 288.3, - 288.4, - 289.4, - 290.3, - 291.6, - 290.6, - 290.8, - 291.8, - 291.8, - 290.8, - 290.1, - 290.1, - 290.7, - 290.5, - 290.1, - 289.9, - 289.6, - 289.1, - 288.5, - 287.6, - 286.8, - 286.9, - 287.5, - 294.9, - 293.2, - 291.4, - 289.9, - 289.6, - 290.1, - 290.5, - 290.3, - 289.8, - ], - [ - 292.5, - 292.3, - 293.5, - 292.9, - 296.4, - 295.3, - 294.8, - 294.7, - 294.4, - 293.9, - 293.1, - 292.5, - 291.8, - 291.3, - 291.9, - 293.9, - 292.1, - 293.6, - 297.5, - 295.3, - 295.3, - 295.4, - 295.1, - 294.6, - 294.5, - 294.0, - 294.0, - 294.6, - 295.1, - 295.1, - 295.1, - 295.0, - 294.7, - 294.1, - 293.3, - 292.0, - 290.6, - 289.5, - 293.3, - 301.4, - 299.6, - 296.1, - 294.7, - 294.5, - 294.6, - 294.8, - 294.0, - 293.0, - ], - [ - 293.4, - 293.4, - 295.6, - 293.0, - 297.2, - 299.2, - 298.2, - 297.2, - 296.2, - 295.3, - 294.6, - 294.2, - 294.3, - 294.2, - 295.8, - 298.7, - 297.5, - 299.4, - 300.3, - 298.5, - 297.7, - 297.8, - 297.4, - 297.0, - 296.9, - 296.7, - 297.0, - 297.1, - 297.3, - 297.4, - 297.2, - 296.8, - 296.2, - 295.6, - 294.8, - 293.6, - 292.1, - 290.7, - 290.9, - 297.7, - 300.3, - 297.1, - 298.1, - 296.9, - 295.9, - 295.2, - 294.4, - 293.7, - ], - [ - 295.2, - 295.6, - 295.6, - 295.3, - 298.0, - 300.5, - 298.8, - 298.5, - 297.4, - 296.6, - 296.5, - 296.6, - 296.7, - 296.8, - 298.6, - 301.8, - 301.4, - 299.2, - 299.3, - 299.3, - 300.0, - 299.5, - 299.1, - 299.1, - 298.9, - 298.8, - 299.0, - 299.0, - 298.9, - 298.5, - 297.5, - 296.3, - 295.0, - 294.3, - 293.7, - 292.8, - 292.0, - 292.6, - 290.3, - 292.7, - 299.9, - 296.4, - 297.0, - 297.4, - 296.1, - 295.3, - 294.6, - 294.5, - ], - [ - 297.4, - 296.9, - 294.4, - 294.9, - 296.7, - 300.0, - 299.4, - 299.2, - 298.7, - 298.7, - 299.3, - 299.7, - 299.7, - 299.8, - 300.7, - 301.9, - 301.3, - 300.1, - 301.1, - 301.1, - 301.4, - 301.1, - 300.7, - 300.3, - 300.3, - 300.6, - 300.5, - 300.4, - 300.0, - 299.4, - 298.2, - 296.5, - 294.9, - 293.7, - 292.9, - 292.9, - 293.9, - 293.8, - 289.6, - 297.1, - 298.9, - 296.4, - 296.1, - 298.7, - 297.6, - 296.6, - 296.1, - 296.4, - ], - [ - 300.5, - 299.9, - 295.9, - 295.1, - 295.0, - 299.8, - 300.3, - 300.1, - 300.2, - 300.4, - 300.7, - 300.8, - 301.0, - 301.1, - 301.5, - 301.9, - 302.1, - 302.2, - 302.0, - 300.7, - 301.9, - 301.7, - 301.5, - 301.3, - 301.3, - 301.3, - 301.1, - 300.9, - 300.6, - 300.0, - 299.0, - 297.6, - 296.4, - 295.7, - 295.5, - 296.0, - 297.4, - 295.2, - 298.4, - 300.2, - 298.8, - 298.2, - 297.6, - 299.5, - 299.2, - 298.7, - 298.8, - 299.4, - ], - [ - 301.4, - 299.9, - 298.7, - 296.7, - 294.5, - 299.3, - 300.2, - 300.1, - 300.2, - 300.5, - 300.6, - 300.7, - 301.1, - 300.2, - 301.2, - 300.7, - 301.7, - 302.3, - 300.1, - 300.7, - 300.8, - 300.6, - 300.4, - 300.1, - 299.8, - 299.5, - 299.0, - 298.5, - 298.2, - 297.9, - 297.5, - 297.2, - 297.1, - 297.3, - 297.8, - 298.5, - 299.2, - 297.0, - 300.9, - 300.5, - 300.2, - 299.5, - 299.7, - 300.3, - 300.4, - 300.5, - 300.7, - 301.0, - ], - [ - 301.7, - 298.3, - 298.4, - 297.1, - 296.4, - 299.3, - 299.3, - 299.7, - 300.1, - 300.2, - 300.1, - 300.5, - 301.3, - 299.8, - 301.0, - 299.6, - 301.9, - 301.9, - 301.7, - 301.3, - 300.8, - 300.5, - 300.2, - 300.0, - 299.6, - 299.3, - 299.2, - 298.9, - 298.6, - 298.5, - 298.3, - 298.3, - 298.4, - 298.8, - 299.2, - 299.6, - 299.7, - 297.9, - 298.3, - 299.1, - 300.2, - 299.9, - 299.9, - 300.2, - 300.6, - 301.0, - 301.3, - 301.3, - ], - [ - 296.5, - 296.5, - 298.6, - 298.1, - 297.5, - 291.4, - 294.6, - 298.1, - 298.8, - 299.6, - 297.7, - 299.7, - 300.3, - 299.2, - 300.4, - 301.3, - 299.5, - 300.9, - 300.8, - 300.5, - 300.2, - 299.9, - 299.5, - 299.3, - 299.1, - 299.1, - 299.1, - 299.0, - 299.0, - 299.1, - 299.3, - 299.4, - 299.8, - 299.8, - 299.3, - 299.6, - 299.1, - 298.2, - 298.7, - 300.1, - 299.2, - 299.0, - 298.6, - 298.6, - 299.0, - 299.9, - 298.2, - 297.5, - ], - [ - 296.9, - 296.4, - 296.6, - 295.1, - 297.1, - 293.1, - 290.8, - 295.7, - 296.5, - 297.2, - 293.9, - 297.9, - 297.6, - 293.8, - 295.8, - 298.8, - 298.2, - 298.9, - 298.9, - 299.1, - 298.9, - 298.2, - 297.5, - 296.9, - 296.6, - 296.3, - 296.2, - 296.0, - 296.0, - 296.4, - 296.8, - 297.1, - 297.6, - 298.7, - 296.1, - 294.9, - 296.5, - 298.6, - 298.3, - 298.6, - 298.2, - 297.6, - 297.2, - 296.9, - 296.7, - 297.3, - 297.9, - 297.5, - ], - [ - 290.4, - 289.2, - 288.8, - 289.8, - 293.2, - 290.9, - 288.7, - 291.5, - 295.4, - 292.9, - 289.2, - 292.3, - 289.4, - 284.6, - 287.3, - 293.6, - 294.7, - 293.8, - 294.2, - 295.1, - 295.9, - 296.3, - 296.0, - 295.3, - 294.6, - 294.0, - 293.4, - 293.0, - 292.7, - 292.7, - 292.7, - 292.7, - 293.2, - 293.8, - 284.9, - 292.3, - 296.2, - 295.6, - 296.5, - 297.3, - 296.9, - 296.4, - 295.8, - 295.4, - 295.3, - 294.3, - 292.3, - 292.0, - ], - [ - 287.2, - 286.3, - 287.4, - 286.9, - 287.2, - 284.8, - 288.8, - 285.3, - 285.1, - 284.1, - 282.3, - 276.0, - 275.1, - 274.6, - 277.2, - 277.9, - 286.6, - 288.8, - 289.0, - 289.7, - 290.8, - 291.9, - 292.5, - 292.4, - 292.0, - 291.4, - 290.7, - 290.2, - 289.9, - 290.0, - 290.0, - 289.9, - 289.7, - 284.1, - 279.8, - 286.4, - 287.8, - 291.0, - 293.1, - 293.7, - 294.0, - 294.2, - 294.1, - 293.8, - 293.4, - 293.4, - 289.8, - 287.6, - ], - [ - 280.6, - 285.5, - 288.7, - 288.8, - 286.8, - 281.0, - 278.3, - 276.1, - 275.1, - 274.5, - 259.3, - 252.7, - 252.3, - 264.4, - 271.9, - 273.4, - 278.4, - 279.7, - 279.1, - 284.5, - 286.3, - 287.4, - 287.9, - 288.1, - 287.9, - 287.4, - 287.1, - 287.1, - 287.3, - 287.4, - 287.5, - 286.6, - 280.6, - 274.1, - 274.0, - 274.2, - 274.0, - 280.3, - 288.9, - 290.1, - 290.6, - 290.9, - 291.3, - 291.6, - 291.5, - 291.3, - 289.6, - 281.6, - ], - [ - 283.4, - 283.8, - 281.9, - 278.6, - 274.7, - 270.8, - 275.5, - 275.8, - 273.4, - 263.9, - 261.0, - 262.5, - 259.6, - 261.2, - 263.1, - 263.7, - 257.6, - 263.7, - 270.0, - 273.9, - 278.9, - 279.9, - 281.1, - 282.0, - 282.3, - 282.1, - 282.3, - 283.0, - 283.8, - 284.4, - 284.7, - 279.8, - 267.4, - 263.8, - 267.3, - 266.6, - 266.9, - 269.3, - 279.5, - 284.7, - 286.3, - 286.8, - 287.5, - 288.5, - 289.0, - 288.4, - 285.2, - 278.3, - ], - [ - 274.1, - 268.7, - 266.7, - 267.4, - 275.8, - 272.9, - 272.3, - 266.8, - 265.1, - 263.9, - 260.3, - 255.9, - 254.0, - 254.2, - 254.2, - 251.0, - 246.6, - 247.9, - 260.6, - 268.4, - 272.8, - 274.1, - 275.4, - 275.8, - 276.4, - 277.3, - 278.2, - 279.1, - 280.3, - 281.3, - 282.2, - 276.3, - 266.5, - 261.3, - 260.0, - 260.9, - 266.2, - 262.4, - 264.5, - 274.0, - 276.3, - 278.6, - 281.2, - 283.9, - 285.7, - 285.4, - 284.7, - 279.6, - ], - [ - 272.7, - 265.5, - 263.2, - 260.9, - 259.3, - 258.8, - 258.3, - 257.4, - 256.2, - 256.5, - 257.0, - 255.2, - 252.8, - 252.1, - 249.8, - 244.3, - 241.6, - 242.7, - 251.1, - 267.8, - 266.7, - 266.4, - 271.4, - 272.1, - 273.2, - 274.5, - 275.8, - 276.8, - 278.2, - 279.6, - 278.7, - 268.7, - 262.3, - 257.8, - 255.5, - 256.1, - 257.5, - 260.0, - 258.8, - 260.5, - 268.1, - 275.4, - 278.5, - 280.7, - 282.4, - 283.3, - 281.9, - 275.9, - ], - [ - 276.9, - 267.1, - 268.1, - 255.3, - 249.1, - 247.3, - 247.4, - 249.0, - 248.9, - 249.3, - 249.4, - 250.5, - 250.6, - 247.2, - 243.4, - 239.2, - 236.3, - 236.0, - 243.7, - 256.7, - 256.2, - 249.1, - 261.6, - 264.2, - 268.2, - 271.2, - 270.2, - 272.0, - 273.9, - 273.1, - 266.5, - 262.1, - 260.5, - 256.4, - 251.8, - 250.0, - 252.0, - 257.4, - 252.0, - 254.8, - 270.0, - 275.2, - 278.1, - 280.0, - 281.3, - 282.1, - 282.0, - 275.4, - ], - [ - 274.8, - 263.2, - 258.6, - 246.4, - 242.3, - 240.9, - 240.9, - 240.7, - 238.5, - 238.7, - 239.0, - 239.7, - 238.6, - 235.6, - 233.2, - 230.9, - 229.4, - 231.4, - 234.8, - 238.1, - 238.7, - 241.6, - 244.2, - 247.5, - 257.9, - 264.2, - 254.9, - 256.2, - 256.3, - 254.9, - 255.7, - 253.4, - 251.6, - 249.1, - 245.6, - 245.1, - 247.2, - 248.4, - 248.9, - 257.5, - 270.1, - 259.8, - 265.1, - 275.8, - 277.7, - 274.3, - 278.0, - 278.9, - ], - [ - 273.2, - 268.4, - 256.0, - 247.6, - 250.2, - 244.8, - 239.2, - 234.9, - 234.6, - 230.2, - 229.6, - 230.8, - 231.4, - 229.9, - 228.3, - 227.7, - 228.3, - 230.0, - 230.5, - 231.3, - 231.8, - 235.8, - 238.5, - 239.8, - 242.0, - 246.1, - 248.1, - 244.2, - 246.0, - 247.0, - 246.2, - 243.8, - 242.5, - 241.8, - 242.7, - 241.8, - 241.7, - 242.4, - 245.5, - 254.0, - 263.4, - 250.4, - 244.5, - 248.4, - 255.2, - 261.2, - 267.8, - 270.9, - ], - [ - 268.5, - 269.1, - 268.5, - 265.7, - 262.3, - 243.8, - 236.0, - 236.2, - 235.6, - 235.6, - 234.7, - 229.1, - 228.7, - 227.4, - 228.8, - 233.2, - 233.0, - 235.3, - 235.9, - 236.5, - 236.9, - 237.3, - 237.9, - 238.7, - 239.2, - 239.6, - 241.5, - 239.2, - 239.7, - 240.2, - 241.2, - 240.7, - 240.0, - 240.8, - 242.7, - 240.8, - 239.1, - 245.3, - 251.1, - 258.6, - 247.1, - 239.5, - 237.2, - 235.4, - 241.2, - 246.8, - 247.9, - 258.9, - ], - [ - 259.5, - 258.8, - 258.0, - 253.9, - 243.8, - 239.9, - 238.3, - 237.7, - 235.8, - 233.8, - 233.3, - 233.2, - 234.4, - 232.8, - 234.4, - 236.1, - 236.5, - 237.4, - 237.5, - 237.6, - 237.8, - 236.8, - 236.3, - 236.2, - 236.1, - 235.9, - 235.7, - 235.3, - 234.9, - 234.6, - 235.6, - 236.4, - 236.5, - 236.5, - 237.7, - 239.6, - 235.8, - 232.6, - 234.3, - 238.0, - 236.9, - 235.3, - 233.4, - 235.4, - 238.5, - 241.4, - 243.3, - 252.2, - ], - [ - 235.4, - 235.8, - 236.3, - 235.0, - 234.9, - 236.2, - 235.8, - 235.5, - 236.3, - 236.6, - 235.6, - 235.9, - 236.6, - 236.9, - 237.2, - 237.9, - 238.5, - 238.7, - 238.6, - 238.4, - 238.2, - 238.1, - 237.9, - 237.2, - 236.7, - 236.2, - 235.8, - 235.2, - 234.3, - 233.1, - 232.7, - 233.3, - 233.8, - 233.0, - 232.9, - 233.1, - 234.2, - 233.5, - 233.0, - 234.8, - 234.5, - 234.2, - 234.7, - 236.3, - 237.7, - 238.0, - 236.8, - 236.1, - ], - ], + pa = n.createVariable("pa", "f4", ("time", "nMesh2_node")) + pa.standard_name = "air_pressure" + pa.units = "hPa" + pa.mesh = "Mesh2" + pa.location = "node" + pa.coordinates = "Mesh2_node_x Mesh2_node_y" + pa[...] = [ + [999.67, 1006.45, 999.85, 1006.55, 1006.14, 1005.68, 999.48], [ - [ - 242.9, - 242.6, - 241.9, - 242.3, - 240.2, - 240.4, - 240.3, - 241.3, - 240.0, - 239.5, - 242.1, - 241.0, - 243.0, - 243.7, - 244.3, - 243.8, - 244.9, - 246.1, - 247.5, - 247.8, - 248.7, - 249.7, - 250.9, - 253.1, - 254.7, - 256.3, - 257.2, - 259.0, - 258.2, - 257.7, - 256.6, - 255.9, - 254.4, - 251.2, - 250.6, - 252.5, - 254.6, - 254.5, - 255.0, - 254.9, - 253.3, - 253.3, - 251.8, - 249.6, - 248.8, - 247.2, - 244.7, - 244.3, - ], - [ - 243.6, - 240.8, - 239.4, - 237.9, - 237.0, - 237.5, - 239.4, - 239.8, - 241.0, - 240.8, - 240.8, - 241.1, - 240.8, - 241.7, - 245.4, - 246.1, - 246.6, - 249.1, - 250.7, - 250.8, - 254.7, - 260.1, - 265.0, - 266.1, - 265.4, - 265.8, - 265.6, - 266.5, - 266.2, - 263.3, - 262.6, - 261.4, - 261.5, - 258.0, - 256.7, - 256.6, - 260.0, - 262.8, - 264.3, - 265.7, - 265.2, - 264.2, - 265.5, - 263.1, - 257.6, - 252.6, - 246.3, - 245.4, - ], - [ - 248.9, - 245.5, - 246.5, - 244.3, - 242.8, - 243.0, - 243.2, - 244.5, - 249.7, - 252.2, - 249.1, - 244.7, - 242.8, - 244.1, - 245.0, - 245.6, - 245.6, - 245.5, - 247.5, - 249.4, - 252.2, - 257.6, - 266.6, - 269.0, - 270.0, - 271.0, - 270.0, - 270.2, - 271.0, - 269.2, - 266.4, - 266.2, - 266.8, - 267.2, - 268.0, - 265.9, - 266.2, - 268.4, - 268.2, - 267.6, - 271.8, - 271.2, - 272.1, - 271.8, - 267.5, - 261.8, - 255.2, - 252.6, - ], - [ - 270.4, - 269.6, - 269.1, - 267.5, - 266.9, - 264.2, - 259.1, - 256.5, - 262.1, - 266.1, - 263.7, - 256.9, - 255.1, - 253.5, - 255.6, - 256.0, - 256.6, - 254.5, - 253.5, - 261.3, - 264.7, - 268.4, - 271.0, - 271.3, - 271.5, - 273.1, - 273.2, - 273.8, - 272.9, - 274.1, - 272.8, - 273.2, - 273.5, - 274.4, - 274.7, - 274.6, - 275.0, - 275.3, - 271.3, - 267.3, - 271.9, - 271.2, - 271.7, - 272.3, - 271.2, - 270.5, - 271.0, - 270.8, - ], - [ - 274.9, - 273.8, - 273.7, - 273.6, - 274.8, - 275.0, - 272.5, - 273.9, - 274.2, - 273.1, - 272.3, - 270.9, - 272.0, - 273.3, - 272.1, - 272.1, - 271.9, - 273.3, - 271.6, - 272.1, - 272.6, - 273.1, - 273.3, - 275.7, - 275.6, - 274.0, - 275.1, - 274.8, - 274.0, - 275.1, - 275.1, - 275.7, - 274.1, - 276.1, - 275.1, - 275.3, - 276.5, - 275.4, - 275.2, - 274.4, - 273.3, - 274.3, - 274.7, - 274.3, - 274.9, - 273.7, - 274.4, - 274.1, - ], - [ - 275.7, - 275.4, - 275.7, - 275.7, - 275.1, - 276.0, - 275.5, - 276.2, - 276.8, - 276.4, - 274.5, - 274.0, - 275.1, - 274.5, - 273.7, - 274.0, - 274.7, - 274.8, - 275.1, - 275.3, - 275.5, - 277.2, - 277.0, - 276.6, - 277.7, - 277.5, - 276.2, - 277.7, - 275.8, - 277.9, - 277.5, - 276.7, - 277.8, - 276.8, - 277.9, - 277.7, - 278.0, - 278.0, - 279.4, - 278.9, - 278.5, - 278.0, - 278.3, - 277.3, - 276.7, - 276.7, - 277.3, - 276.1, - ], - [ - 278.9, - 277.7, - 279.3, - 277.1, - 278.2, - 277.6, - 278.7, - 277.7, - 277.1, - 278.5, - 279.2, - 279.3, - 280.0, - 279.7, - 278.1, - 278.9, - 278.1, - 279.8, - 279.1, - 279.4, - 279.6, - 280.6, - 282.1, - 280.3, - 279.7, - 280.1, - 281.1, - 281.5, - 281.2, - 280.9, - 281.0, - 281.9, - 281.7, - 281.3, - 279.7, - 281.1, - 280.6, - 279.9, - 281.6, - 283.4, - 281.9, - 281.2, - 279.1, - 278.9, - 279.7, - 279.8, - 280.1, - 277.7, - ], - [ - 283.2, - 283.7, - 283.3, - 281.8, - 280.9, - 282.2, - 281.4, - 282.6, - 282.6, - 283.4, - 284.8, - 284.4, - 283.7, - 283.1, - 283.2, - 283.2, - 283.5, - 283.5, - 284.9, - 284.3, - 284.3, - 285.9, - 285.9, - 286.5, - 287.0, - 286.6, - 285.7, - 285.0, - 286.4, - 285.3, - 286.5, - 284.7, - 285.6, - 284.4, - 284.4, - 284.4, - 283.2, - 282.3, - 282.4, - 287.8, - 285.9, - 285.2, - 284.4, - 282.4, - 283.1, - 284.1, - 284.7, - 283.6, - ], - [ - 289.1, - 288.0, - 288.6, - 289.2, - 288.4, - 290.0, - 289.4, - 290.2, - 289.0, - 290.2, - 288.5, - 288.4, - 288.8, - 288.6, - 288.0, - 288.2, - 287.6, - 288.3, - 289.4, - 290.0, - 292.5, - 290.1, - 290.6, - 291.5, - 290.9, - 291.0, - 290.4, - 289.9, - 290.0, - 290.9, - 289.8, - 289.7, - 289.9, - 289.9, - 289.0, - 288.4, - 286.7, - 286.6, - 287.3, - 294.3, - 294.1, - 291.8, - 289.5, - 288.8, - 289.5, - 290.4, - 290.2, - 289.5, - ], - [ - 292.9, - 292.7, - 294.2, - 292.1, - 295.9, - 295.1, - 294.9, - 295.3, - 294.4, - 293.0, - 294.0, - 292.5, - 291.2, - 291.9, - 291.3, - 293.6, - 291.4, - 292.9, - 298.4, - 294.4, - 294.8, - 296.3, - 294.1, - 295.1, - 294.3, - 293.7, - 293.0, - 295.5, - 294.5, - 295.8, - 294.5, - 294.1, - 295.2, - 294.6, - 292.5, - 291.9, - 290.4, - 290.1, - 293.1, - 301.1, - 300.5, - 296.7, - 294.3, - 295.1, - 294.9, - 295.8, - 293.6, - 292.4, - ], - [ - 292.9, - 293.7, - 294.7, - 292.5, - 296.6, - 299.8, - 297.2, - 297.8, - 297.0, - 294.5, - 293.6, - 294.1, - 295.3, - 294.8, - 296.5, - 299.1, - 297.0, - 298.8, - 301.1, - 298.9, - 297.6, - 298.1, - 298.1, - 296.9, - 296.0, - 297.2, - 296.4, - 297.4, - 298.0, - 297.6, - 296.7, - 296.0, - 297.0, - 296.5, - 293.8, - 294.4, - 293.0, - 290.0, - 291.8, - 297.6, - 300.3, - 296.8, - 297.4, - 296.3, - 295.4, - 295.0, - 294.4, - 294.4, - ], - [ - 294.2, - 296.5, - 295.0, - 294.8, - 298.9, - 301.2, - 298.0, - 297.8, - 297.7, - 297.2, - 296.2, - 296.7, - 295.8, - 297.6, - 298.9, - 301.0, - 301.3, - 299.2, - 299.8, - 298.9, - 299.2, - 298.7, - 299.5, - 299.7, - 299.2, - 299.6, - 298.3, - 298.5, - 299.4, - 298.3, - 297.4, - 297.2, - 294.1, - 294.2, - 293.2, - 293.0, - 291.2, - 293.2, - 289.3, - 293.7, - 300.1, - 296.6, - 297.9, - 298.4, - 296.4, - 295.5, - 293.9, - 293.7, - ], - [ - 297.7, - 297.4, - 294.5, - 294.2, - 295.9, - 300.2, - 300.0, - 299.2, - 298.5, - 298.0, - 298.8, - 300.5, - 300.2, - 300.3, - 300.8, - 302.5, - 301.6, - 300.5, - 300.4, - 301.9, - 302.1, - 300.4, - 300.7, - 301.3, - 300.3, - 301.6, - 300.2, - 301.3, - 300.6, - 298.6, - 298.8, - 297.1, - 295.0, - 293.9, - 292.3, - 293.1, - 294.7, - 293.0, - 289.4, - 296.3, - 298.8, - 296.1, - 295.2, - 297.8, - 297.1, - 296.7, - 296.4, - 296.2, - ], - [ - 300.9, - 300.6, - 295.8, - 294.8, - 294.3, - 298.8, - 300.8, - 299.5, - 299.8, - 300.7, - 299.8, - 301.8, - 300.1, - 302.0, - 301.2, - 301.7, - 301.6, - 303.0, - 301.8, - 301.0, - 302.5, - 301.1, - 301.4, - 300.5, - 302.0, - 300.5, - 300.9, - 300.5, - 300.0, - 300.5, - 298.4, - 297.2, - 295.7, - 296.2, - 294.8, - 295.3, - 298.1, - 295.8, - 298.0, - 299.8, - 298.9, - 298.9, - 296.7, - 298.8, - 298.5, - 299.0, - 298.4, - 299.5, - ], - [ - 300.4, - 298.9, - 298.5, - 296.9, - 294.5, - 299.1, - 300.3, - 300.9, - 299.8, - 299.7, - 301.0, - 301.5, - 301.9, - 300.9, - 302.0, - 301.2, - 302.0, - 302.0, - 300.1, - 300.1, - 300.2, - 299.8, - 301.2, - 300.2, - 299.4, - 299.9, - 299.0, - 298.7, - 297.6, - 298.2, - 298.4, - 298.0, - 297.5, - 296.6, - 297.5, - 297.6, - 300.0, - 297.6, - 301.7, - 299.9, - 300.8, - 300.3, - 300.6, - 300.6, - 299.8, - 300.4, - 300.1, - 301.7, - ], - [ - 300.8, - 297.9, - 298.4, - 296.2, - 296.4, - 298.5, - 299.0, - 299.9, - 299.2, - 300.2, - 300.1, - 301.3, - 301.3, - 299.8, - 300.0, - 299.7, - 301.7, - 301.9, - 301.5, - 302.0, - 299.9, - 300.8, - 300.8, - 299.0, - 300.0, - 300.0, - 299.1, - 299.4, - 298.5, - 298.9, - 297.4, - 298.9, - 298.3, - 298.2, - 298.7, - 298.7, - 299.5, - 297.3, - 298.7, - 299.8, - 300.7, - 299.8, - 300.5, - 300.8, - 300.7, - 301.3, - 301.1, - 300.3, - ], - [ - 296.6, - 295.8, - 297.6, - 298.2, - 298.2, - 290.9, - 294.3, - 297.5, - 297.9, - 298.7, - 297.3, - 300.1, - 300.3, - 298.3, - 299.8, - 301.3, - 299.0, - 300.6, - 301.3, - 300.6, - 301.1, - 299.9, - 299.2, - 300.3, - 299.1, - 298.7, - 299.0, - 299.7, - 299.8, - 298.2, - 299.5, - 300.3, - 299.6, - 299.6, - 299.9, - 298.9, - 300.0, - 297.6, - 297.8, - 299.6, - 299.2, - 298.7, - 298.9, - 298.2, - 299.4, - 300.4, - 298.8, - 297.0, - ], - [ - 296.9, - 296.6, - 296.4, - 295.2, - 297.7, - 292.7, - 289.9, - 296.6, - 295.7, - 297.8, - 294.4, - 298.8, - 297.4, - 294.0, - 296.1, - 298.2, - 297.8, - 299.4, - 298.7, - 298.4, - 299.4, - 298.7, - 296.9, - 296.5, - 295.9, - 295.3, - 296.4, - 296.5, - 296.9, - 295.6, - 297.0, - 296.2, - 296.8, - 297.9, - 295.1, - 294.6, - 295.8, - 298.3, - 298.7, - 297.9, - 297.6, - 297.2, - 297.4, - 297.9, - 296.4, - 296.4, - 298.9, - 296.7, - ], - [ - 291.3, - 290.2, - 288.0, - 289.4, - 292.3, - 290.7, - 288.5, - 290.8, - 294.6, - 292.4, - 289.6, - 291.6, - 289.7, - 284.3, - 288.0, - 294.4, - 293.9, - 294.1, - 293.4, - 295.5, - 295.5, - 296.2, - 295.2, - 294.9, - 293.9, - 294.2, - 292.7, - 292.6, - 293.6, - 291.7, - 292.2, - 293.6, - 292.9, - 294.3, - 285.4, - 293.1, - 295.9, - 295.9, - 296.1, - 297.0, - 297.1, - 297.1, - 295.0, - 296.2, - 296.1, - 294.4, - 292.3, - 292.1, - ], - [ - 287.1, - 286.7, - 286.8, - 287.3, - 288.0, - 285.4, - 288.1, - 284.9, - 285.0, - 283.9, - 283.1, - 276.6, - 274.9, - 275.4, - 276.8, - 278.7, - 285.8, - 289.5, - 288.4, - 289.9, - 290.2, - 292.1, - 291.7, - 293.1, - 291.6, - 290.8, - 291.0, - 290.7, - 289.5, - 290.8, - 290.5, - 289.8, - 289.7, - 284.3, - 280.6, - 285.4, - 287.7, - 290.5, - 292.7, - 293.9, - 294.3, - 294.3, - 293.7, - 293.2, - 294.4, - 292.7, - 289.9, - 287.8, - ], - [ - 280.2, - 285.6, - 288.6, - 289.1, - 287.3, - 280.4, - 277.4, - 276.9, - 274.4, - 273.7, - 259.0, - 252.1, - 252.8, - 264.0, - 272.6, - 274.0, - 278.0, - 279.8, - 278.2, - 283.8, - 286.2, - 286.4, - 287.5, - 288.6, - 287.6, - 286.8, - 287.2, - 287.6, - 288.1, - 287.8, - 288.5, - 286.2, - 280.6, - 273.6, - 274.5, - 274.8, - 273.3, - 280.8, - 288.7, - 290.5, - 289.6, - 291.6, - 291.4, - 291.5, - 292.3, - 291.7, - 290.0, - 281.8, - ], - [ - 283.8, - 283.9, - 282.7, - 279.1, - 274.5, - 270.1, - 276.5, - 276.1, - 273.0, - 264.4, - 261.2, - 262.8, - 258.6, - 260.2, - 262.5, - 264.3, - 257.8, - 262.8, - 270.9, - 273.5, - 279.0, - 280.7, - 280.7, - 282.3, - 282.4, - 282.9, - 281.4, - 284.0, - 282.9, - 284.8, - 284.4, - 280.3, - 267.5, - 264.5, - 267.8, - 265.6, - 266.5, - 269.4, - 279.7, - 284.7, - 285.4, - 286.4, - 286.7, - 288.1, - 289.2, - 288.4, - 284.5, - 278.6, - ], - [ - 274.9, - 269.1, - 266.5, - 266.7, - 275.0, - 272.5, - 272.6, - 267.4, - 265.5, - 263.8, - 259.4, - 256.3, - 253.8, - 253.2, - 255.1, - 251.7, - 247.0, - 248.7, - 261.2, - 268.9, - 272.9, - 274.4, - 275.9, - 275.2, - 276.7, - 277.9, - 278.2, - 279.5, - 280.6, - 280.5, - 281.7, - 276.6, - 265.7, - 261.9, - 261.0, - 260.2, - 265.8, - 262.9, - 264.8, - 274.3, - 276.4, - 278.3, - 280.9, - 283.7, - 286.2, - 285.0, - 284.4, - 278.7, - ], - [ - 272.0, - 266.1, - 263.0, - 260.3, - 258.5, - 258.8, - 258.1, - 257.8, - 256.0, - 256.2, - 257.7, - 255.5, - 252.9, - 251.7, - 250.8, - 243.5, - 241.2, - 243.1, - 250.8, - 268.8, - 266.8, - 267.2, - 271.3, - 272.6, - 273.9, - 273.7, - 275.7, - 275.9, - 277.4, - 280.2, - 279.5, - 268.3, - 262.4, - 258.0, - 255.4, - 255.9, - 257.5, - 259.5, - 259.1, - 259.6, - 269.1, - 275.4, - 278.9, - 279.8, - 282.9, - 283.3, - 282.8, - 275.7, - ], - [ - 277.0, - 267.4, - 267.9, - 255.7, - 248.9, - 247.9, - 248.2, - 249.4, - 249.1, - 249.9, - 248.7, - 249.5, - 250.6, - 247.9, - 244.2, - 238.8, - 236.1, - 236.8, - 244.3, - 257.5, - 255.3, - 248.7, - 262.3, - 265.0, - 268.4, - 271.1, - 270.5, - 272.9, - 274.2, - 273.9, - 266.9, - 261.4, - 260.6, - 257.2, - 252.1, - 249.6, - 251.7, - 256.5, - 251.5, - 254.6, - 269.6, - 274.4, - 279.0, - 279.3, - 281.8, - 281.5, - 282.5, - 274.8, - ], - [ - 274.1, - 262.4, - 258.9, - 246.6, - 242.9, - 240.3, - 240.1, - 240.9, - 239.4, - 239.1, - 239.1, - 239.6, - 237.9, - 236.6, - 233.5, - 230.9, - 228.4, - 231.3, - 235.3, - 238.9, - 238.5, - 242.5, - 244.2, - 247.1, - 257.4, - 264.3, - 255.1, - 257.1, - 256.5, - 255.2, - 256.2, - 253.6, - 251.6, - 248.7, - 245.2, - 246.0, - 247.4, - 248.1, - 249.6, - 257.7, - 269.3, - 259.9, - 265.2, - 274.8, - 277.1, - 273.9, - 277.3, - 279.2, - ], - [ - 272.7, - 268.2, - 256.6, - 248.0, - 249.9, - 245.1, - 240.2, - 234.0, - 234.0, - 231.0, - 229.3, - 231.7, - 231.6, - 229.1, - 228.7, - 228.7, - 228.6, - 230.4, - 231.1, - 231.5, - 232.6, - 236.6, - 238.2, - 240.7, - 241.6, - 245.2, - 247.1, - 244.1, - 246.0, - 248.0, - 245.9, - 242.9, - 242.4, - 241.6, - 242.4, - 241.2, - 242.2, - 241.6, - 245.7, - 254.9, - 263.2, - 250.5, - 243.6, - 248.7, - 254.6, - 261.3, - 267.8, - 271.1, - ], - [ - 267.5, - 269.3, - 267.7, - 265.7, - 262.4, - 242.9, - 236.2, - 235.2, - 235.7, - 235.3, - 233.8, - 228.7, - 229.6, - 227.4, - 229.6, - 232.4, - 233.1, - 236.1, - 236.5, - 237.0, - 236.0, - 238.1, - 238.4, - 239.0, - 239.5, - 239.2, - 242.1, - 238.3, - 238.9, - 239.6, - 241.2, - 240.3, - 239.0, - 240.0, - 242.4, - 239.9, - 238.9, - 245.3, - 251.6, - 259.2, - 247.7, - 238.6, - 236.8, - 236.2, - 240.5, - 246.3, - 248.2, - 259.4, - ], - [ - 258.7, - 258.8, - 258.6, - 254.5, - 243.2, - 240.2, - 239.2, - 238.7, - 235.0, - 233.4, - 233.7, - 233.9, - 235.1, - 233.6, - 234.7, - 236.3, - 235.6, - 237.5, - 237.8, - 236.8, - 237.7, - 236.3, - 235.8, - 237.0, - 235.2, - 234.9, - 235.7, - 235.3, - 235.5, - 235.0, - 235.9, - 236.9, - 236.8, - 237.2, - 238.5, - 238.8, - 236.7, - 232.0, - 235.2, - 237.8, - 237.1, - 236.2, - 233.4, - 235.0, - 239.1, - 240.4, - 242.8, - 253.1, - ], - [ - 235.7, - 235.5, - 235.4, - 235.1, - 234.4, - 236.7, - 235.6, - 234.8, - 236.0, - 236.9, - 235.7, - 235.4, - 237.2, - 237.8, - 237.3, - 237.0, - 237.5, - 239.1, - 239.4, - 238.7, - 237.5, - 238.9, - 238.8, - 237.6, - 235.8, - 236.8, - 236.4, - 235.0, - 234.6, - 233.8, - 233.6, - 233.5, - 233.2, - 233.9, - 233.4, - 233.1, - 234.8, - 234.0, - 233.3, - 234.8, - 235.3, - 234.6, - 233.7, - 235.7, - 238.6, - 239.0, - 237.2, - 236.8, - ], + 1003.48, + 1006.42, + 1000.83, + 1002.98, + 1008.28, + 1002.97, + 1002.47, ], ] - dst = n.createVariable("dst", "f4", ("time_1", "lat", "lon")) - dst.standard_name = "air_temperature" - dst.units = "K" - dst.cell_methods = "time_1: mean" + n.close() + return filename - # Don't generate this data randomly - it's useful to see the real - # patterns of global temperature. - dst[...] = [ - [ - [ - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - 246.0, - ], - [ - 243.6, - 243.3, - 243.0, - 242.7, - 242.5, - 242.5, - 242.5, - 242.4, - 242.5, - 242.5, - 242.4, - 242.2, - 241.9, - 241.7, - 241.5, - 241.4, - 241.4, - 241.4, - 241.4, - 241.4, - 241.5, - 241.7, - 241.8, - 241.9, - 242.1, - 242.1, - 241.9, - 241.9, - 241.8, - 242.0, - 242.2, - 242.5, - 243.2, - 243.7, - 244.1, - 244.5, - 244.8, - 244.8, - 244.7, - 244.8, - 244.9, - 245.1, - 245.7, - 246.3, - 247.6, - 248.1, - 248.3, - 247.9, - 247.2, - 246.8, - 246.9, - 247.3, - 247.9, - 248.2, - 248.5, - 249.1, - 249.6, - 249.8, - 250.1, - 250.5, - 250.8, - 250.7, - 250.8, - 250.3, - 250.3, - 250.2, - 249.8, - 249.6, - 249.8, - 250.3, - 250.7, - 251.2, - 252.0, - 252.7, - 252.5, - 251.5, - 250.5, - 249.8, - 248.6, - 248.0, - 247.9, - 248.2, - 248.3, - 248.3, - 248.2, - 247.8, - 247.5, - 247.0, - 246.5, - 246.4, - 246.0, - 245.4, - 244.9, - 244.4, - 244.0, - 243.8, - ], - [ - 244.1, - 243.6, - 242.8, - 241.9, - 241.3, - 241.0, - 240.8, - 240.8, - 240.5, - 240.2, - 239.8, - 239.5, - 239.4, - 239.5, - 239.5, - 239.3, - 239.1, - 239.0, - 239.1, - 239.5, - 240.1, - 240.7, - 241.2, - 241.7, - 242.0, - 242.5, - 243.1, - 243.5, - 243.6, - 243.9, - 244.3, - 244.8, - 245.3, - 246.0, - 246.8, - 247.5, - 248.0, - 248.4, - 248.7, - 248.9, - 249.3, - 250.2, - 251.5, - 252.8, - 253.6, - 254.2, - 254.3, - 255.1, - 256.9, - 258.4, - 259.8, - 261.2, - 262.7, - 264.0, - 264.8, - 265.3, - 265.1, - 264.4, - 263.6, - 262.6, - 261.6, - 261.1, - 260.4, - 259.4, - 258.2, - 257.2, - 255.3, - 253.5, - 252.7, - 252.5, - 253.0, - 253.5, - 254.1, - 255.2, - 257.1, - 258.0, - 258.3, - 258.3, - 258.7, - 258.5, - 257.9, - 257.0, - 256.0, - 255.5, - 255.0, - 254.2, - 252.9, - 251.7, - 250.8, - 249.6, - 248.3, - 246.9, - 245.7, - 245.0, - 244.5, - 244.3, - ], - [ - 244.9, - 243.6, - 242.0, - 240.5, - 239.4, - 238.6, - 238.1, - 237.5, - 237.2, - 237.1, - 236.9, - 236.8, - 237.1, - 237.6, - 237.9, - 237.9, - 237.9, - 237.8, - 237.6, - 237.9, - 238.6, - 239.3, - 239.9, - 240.2, - 241.1, - 242.4, - 243.5, - 244.0, - 244.7, - 245.4, - 245.9, - 246.1, - 246.5, - 247.1, - 247.9, - 248.8, - 249.8, - 250.5, - 250.9, - 251.4, - 252.2, - 253.6, - 256.0, - 259.4, - 262.9, - 265.9, - 267.3, - 267.7, - 267.6, - 267.4, - 267.0, - 266.6, - 266.5, - 266.6, - 266.5, - 265.9, - 265.3, - 265.1, - 265.0, - 264.8, - 264.8, - 264.8, - 264.7, - 264.5, - 263.5, - 262.0, - 259.9, - 257.4, - 255.7, - 255.3, - 255.3, - 255.7, - 256.5, - 257.5, - 258.9, - 260.6, - 261.8, - 262.8, - 263.3, - 263.8, - 264.2, - 264.0, - 263.1, - 262.2, - 262.4, - 262.4, - 261.2, - 259.3, - 257.5, - 255.4, - 253.0, - 250.5, - 248.2, - 246.6, - 245.9, - 245.6, - ], - [ - 244.3, - 242.9, - 241.7, - 241.1, - 240.7, - 240.2, - 239.1, - 238.1, - 237.5, - 237.1, - 236.8, - 237.0, - 237.9, - 239.2, - 240.3, - 241.1, - 241.9, - 242.5, - 242.5, - 241.8, - 241.4, - 241.2, - 240.8, - 240.2, - 239.5, - 239.5, - 240.4, - 241.8, - 243.1, - 244.2, - 245.0, - 245.5, - 245.8, - 246.5, - 247.3, - 248.4, - 249.5, - 250.3, - 250.7, - 251.4, - 253.3, - 255.7, - 259.1, - 263.3, - 265.9, - 266.1, - 266.4, - 266.0, - 265.7, - 265.5, - 265.5, - 265.6, - 265.8, - 266.2, - 266.3, - 265.9, - 265.5, - 265.3, - 264.6, - 264.1, - 263.4, - 262.4, - 261.5, - 261.2, - 261.0, - 260.8, - 260.0, - 258.8, - 257.4, - 256.8, - 256.8, - 257.4, - 258.7, - 260.1, - 261.9, - 263.6, - 265.0, - 265.2, - 264.8, - 264.8, - 265.5, - 265.2, - 264.3, - 262.9, - 263.7, - 265.2, - 266.0, - 265.8, - 263.1, - 260.8, - 256.8, - 252.4, - 249.0, - 247.0, - 245.9, - 245.1, - ], - [ - 245.2, - 243.7, - 242.2, - 241.3, - 241.5, - 241.7, - 241.3, - 240.1, - 238.9, - 238.5, - 238.6, - 239.0, - 239.9, - 241.3, - 242.4, - 243.3, - 245.4, - 246.5, - 246.4, - 246.4, - 246.4, - 245.9, - 245.2, - 243.8, - 242.2, - 241.2, - 241.2, - 241.7, - 242.5, - 243.3, - 243.8, - 244.2, - 244.4, - 244.5, - 245.4, - 246.5, - 247.4, - 247.6, - 247.6, - 248.0, - 248.9, - 250.8, - 254.1, - 259.1, - 262.6, - 265.6, - 265.6, - 265.2, - 264.8, - 264.8, - 265.4, - 266.1, - 266.6, - 267.2, - 267.2, - 267.2, - 266.8, - 266.7, - 265.6, - 263.3, - 261.6, - 259.5, - 258.2, - 258.3, - 259.4, - 260.8, - 261.7, - 261.7, - 261.5, - 260.9, - 260.2, - 260.3, - 261.3, - 262.4, - 263.9, - 266.1, - 267.0, - 267.7, - 267.5, - 267.2, - 271.5, - 271.5, - 271.6, - 271.7, - 271.6, - 271.5, - 271.6, - 271.4, - 263.7, - 260.6, - 256.3, - 252.6, - 249.8, - 248.1, - 247.1, - 246.2, - ], - [ - 248.4, - 246.8, - 245.6, - 244.3, - 244.1, - 244.4, - 244.0, - 242.7, - 241.0, - 240.4, - 240.9, - 242.0, - 243.3, - 243.7, - 243.4, - 243.9, - 245.3, - 248.8, - 250.6, - 250.9, - 250.5, - 248.5, - 246.7, - 245.5, - 244.2, - 243.3, - 243.6, - 244.7, - 245.7, - 245.8, - 245.3, - 244.4, - 243.7, - 243.7, - 244.8, - 246.1, - 246.7, - 246.4, - 246.4, - 247.1, - 249.2, - 251.9, - 255.3, - 259.6, - 261.3, - 270.2, - 270.4, - 271.0, - 271.1, - 271.2, - 271.4, - 271.5, - 271.5, - 271.5, - 271.5, - 271.4, - 271.3, - 271.7, - 272.0, - 272.1, - 267.4, - 266.9, - 266.4, - 266.3, - 266.4, - 266.6, - 267.3, - 268.2, - 268.5, - 268.2, - 266.9, - 265.2, - 264.3, - 264.3, - 265.8, - 267.1, - 267.0, - 266.0, - 265.7, - 266.6, - 271.6, - 271.6, - 271.5, - 271.5, - 271.4, - 271.5, - 271.6, - 271.5, - 271.3, - 271.4, - 262.7, - 259.6, - 254.4, - 252.3, - 251.2, - 249.6, - ], - [ - 257.5, - 254.5, - 252.1, - 249.8, - 249.2, - 250.4, - 251.5, - 250.9, - 250.6, - 250.5, - 249.1, - 248.0, - 247.6, - 247.1, - 246.9, - 247.4, - 249.1, - 253.6, - 256.8, - 257.8, - 255.1, - 251.7, - 247.6, - 245.1, - 244.0, - 243.2, - 243.2, - 244.8, - 246.9, - 247.9, - 247.8, - 247.1, - 246.2, - 245.6, - 245.4, - 245.6, - 246.3, - 246.7, - 248.1, - 250.9, - 253.0, - 253.8, - 254.5, - 254.6, - 256.0, - 270.2, - 270.8, - 271.3, - 271.2, - 271.3, - 271.4, - 271.4, - 271.3, - 271.8, - 272.6, - 272.6, - 271.1, - 271.1, - 271.3, - 271.6, - 271.7, - 271.7, - 271.6, - 271.5, - 271.5, - 271.5, - 271.4, - 271.4, - 271.2, - 271.6, - 271.7, - 271.6, - 271.5, - 272.3, - 272.1, - 271.5, - 271.8, - 268.5, - 265.9, - 264.9, - 271.5, - 271.3, - 271.3, - 271.4, - 271.5, - 271.5, - 271.5, - 271.5, - 271.3, - 271.2, - 271.1, - 271.2, - 271.3, - 263.1, - 261.2, - 259.6, - ], - [ - 271.3, - 271.4, - 271.5, - 271.6, - 271.6, - 271.5, - 271.3, - 271.3, - 264.0, - 264.2, - 263.1, - 259.6, - 256.1, - 254.7, - 252.4, - 250.5, - 251.9, - 254.3, - 259.4, - 262.6, - 263.1, - 258.8, - 255.1, - 253.2, - 252.1, - 251.8, - 251.8, - 252.1, - 252.9, - 253.4, - 253.9, - 254.0, - 253.7, - 252.8, - 251.7, - 250.5, - 249.8, - 250.2, - 252.7, - 254.8, - 256.8, - 258.2, - 259.7, - 270.3, - 270.2, - 270.5, - 271.2, - 271.1, - 271.3, - 271.5, - 271.6, - 271.6, - 272.2, - 273.2, - 274.1, - 274.1, - 273.8, - 273.9, - 274.0, - 273.9, - 273.4, - 272.8, - 272.5, - 272.7, - 273.6, - 274.0, - 273.8, - 273.6, - 273.7, - 273.8, - 273.2, - 272.8, - 273.5, - 274.4, - 275.4, - 275.3, - 272.6, - 268.4, - 265.9, - 265.2, - 271.4, - 271.3, - 271.0, - 271.2, - 271.5, - 271.5, - 271.3, - 271.2, - 271.2, - 271.2, - 271.2, - 271.3, - 271.4, - 271.4, - 271.3, - 271.3, - ], - [ - 272.8, - 273.1, - 273.0, - 272.9, - 272.8, - 272.5, - 271.3, - 271.2, - 271.1, - 271.2, - 271.1, - 271.2, - 271.0, - 263.1, - 260.1, - 260.2, - 270.2, - 269.5, - 269.5, - 269.8, - 270.1, - 269.9, - 270.9, - 260.1, - 259.0, - 257.8, - 256.3, - 255.8, - 256.3, - 258.0, - 259.9, - 261.0, - 260.8, - 259.9, - 258.9, - 257.2, - 255.9, - 257.6, - 261.3, - 270.5, - 270.7, - 270.4, - 270.6, - 271.1, - 271.4, - 271.2, - 271.3, - 272.4, - 272.8, - 273.2, - 273.4, - 273.4, - 273.6, - 274.1, - 274.2, - 274.2, - 274.2, - 274.2, - 274.0, - 273.9, - 273.9, - 273.9, - 274.0, - 274.1, - 274.4, - 274.6, - 274.5, - 274.4, - 274.3, - 274.4, - 274.6, - 274.5, - 274.5, - 274.8, - 275.1, - 275.2, - 275.1, - 274.6, - 268.9, - 267.9, - 271.2, - 271.1, - 271.2, - 272.2, - 271.4, - 271.1, - 271.0, - 271.0, - 271.6, - 272.2, - 271.4, - 271.7, - 271.3, - 271.7, - 271.6, - 272.3, - ], - [ - 274.2, - 274.2, - 274.1, - 274.3, - 274.3, - 274.3, - 274.3, - 274.5, - 274.1, - 274.5, - 274.7, - 274.5, - 271.5, - 271.2, - 271.1, - 270.9, - 270.8, - 270.7, - 270.8, - 270.9, - 271.0, - 271.1, - 271.0, - 270.8, - 270.8, - 270.8, - 270.9, - 270.9, - 271.0, - 271.1, - 271.0, - 271.0, - 271.1, - 271.1, - 271.1, - 271.1, - 271.0, - 271.0, - 270.9, - 270.9, - 271.1, - 271.1, - 271.2, - 271.3, - 271.4, - 272.1, - 273.3, - 273.9, - 274.1, - 274.3, - 274.6, - 274.5, - 274.5, - 274.5, - 274.4, - 274.3, - 274.2, - 274.2, - 274.1, - 274.0, - 273.9, - 274.1, - 274.3, - 274.4, - 274.4, - 274.6, - 274.6, - 274.7, - 274.8, - 274.9, - 275.1, - 275.4, - 275.4, - 275.4, - 275.6, - 275.6, - 275.4, - 275.2, - 275.1, - 269.6, - 271.3, - 271.3, - 272.3, - 272.8, - 272.9, - 273.0, - 273.2, - 273.4, - 273.5, - 273.8, - 273.8, - 274.0, - 274.0, - 274.1, - 274.0, - 274.0, - ], - [ - 274.7, - 274.6, - 274.4, - 274.4, - 274.5, - 274.4, - 274.3, - 274.4, - 274.4, - 274.5, - 274.8, - 275.0, - 274.8, - 274.4, - 274.0, - 273.8, - 274.0, - 274.5, - 274.5, - 274.4, - 274.3, - 274.3, - 272.9, - 271.3, - 271.2, - 271.3, - 273.0, - 272.9, - 273.0, - 273.6, - 273.6, - 273.4, - 272.9, - 272.7, - 272.9, - 272.8, - 271.8, - 271.6, - 271.6, - 272.3, - 274.1, - 274.6, - 274.6, - 274.5, - 274.8, - 274.7, - 274.9, - 275.2, - 275.1, - 275.0, - 275.0, - 275.0, - 275.1, - 275.2, - 275.2, - 275.2, - 275.2, - 275.0, - 274.9, - 274.8, - 274.9, - 274.9, - 274.9, - 275.0, - 275.0, - 275.1, - 275.1, - 275.2, - 275.4, - 275.5, - 275.8, - 276.0, - 275.9, - 276.1, - 276.4, - 276.5, - 276.4, - 276.1, - 275.5, - 273.0, - 271.6, - 272.0, - 273.1, - 273.8, - 274.5, - 274.8, - 274.9, - 274.9, - 274.3, - 274.8, - 274.9, - 274.8, - 274.7, - 274.8, - 274.8, - 274.8, - ], - [ - 275.0, - 274.9, - 274.6, - 274.3, - 274.3, - 274.4, - 274.2, - 274.2, - 274.3, - 274.5, - 274.6, - 274.7, - 274.8, - 274.9, - 275.0, - 275.1, - 275.1, - 275.1, - 275.1, - 275.0, - 274.6, - 274.3, - 274.0, - 271.7, - 272.1, - 272.6, - 273.2, - 274.1, - 274.3, - 274.4, - 274.6, - 274.7, - 274.7, - 274.8, - 274.9, - 274.6, - 274.7, - 274.9, - 274.9, - 275.0, - 275.2, - 275.4, - 275.5, - 275.4, - 275.5, - 275.4, - 275.7, - 275.7, - 275.6, - 275.6, - 275.6, - 275.6, - 275.7, - 275.7, - 275.6, - 275.6, - 275.6, - 275.6, - 275.9, - 275.7, - 275.5, - 275.6, - 275.7, - 275.8, - 275.8, - 275.8, - 275.9, - 276.0, - 276.0, - 276.1, - 276.3, - 276.4, - 276.4, - 276.5, - 276.6, - 276.8, - 276.9, - 277.2, - 277.2, - 277.0, - 276.4, - 275.6, - 275.8, - 276.0, - 276.3, - 276.2, - 276.1, - 276.3, - 276.2, - 276.2, - 276.1, - 275.7, - 275.5, - 275.5, - 275.3, - 275.0, - ], - [ - 275.2, - 275.1, - 274.9, - 274.9, - 275.2, - 275.3, - 275.2, - 275.0, - 275.0, - 275.2, - 275.5, - 275.6, - 275.5, - 275.5, - 275.5, - 275.6, - 275.8, - 275.9, - 275.8, - 275.3, - 274.9, - 274.5, - 274.6, - 274.2, - 274.3, - 273.7, - 273.7, - 273.7, - 273.9, - 274.0, - 274.4, - 274.5, - 274.6, - 274.9, - 275.0, - 275.0, - 275.0, - 275.1, - 275.1, - 275.4, - 275.6, - 275.9, - 276.3, - 276.6, - 277.0, - 276.5, - 276.5, - 276.5, - 276.5, - 276.6, - 276.6, - 276.7, - 277.0, - 276.7, - 276.4, - 276.3, - 276.1, - 276.2, - 277.1, - 276.7, - 276.5, - 276.4, - 276.8, - 277.0, - 276.9, - 276.9, - 277.0, - 277.0, - 276.9, - 276.9, - 277.0, - 276.9, - 276.9, - 277.1, - 277.2, - 277.7, - 277.9, - 278.7, - 279.2, - 279.0, - 278.1, - 278.2, - 278.1, - 278.3, - 278.6, - 278.0, - 277.3, - 277.0, - 277.1, - 277.1, - 277.0, - 276.7, - 276.4, - 276.3, - 276.1, - 275.6, - ], - [ - 276.0, - 275.8, - 275.7, - 275.8, - 276.1, - 276.2, - 276.2, - 276.1, - 275.9, - 276.0, - 276.3, - 276.4, - 276.3, - 276.4, - 276.3, - 276.3, - 276.5, - 276.5, - 276.3, - 276.0, - 275.7, - 275.1, - 275.7, - 275.6, - 275.7, - 275.7, - 276.0, - 275.6, - 276.0, - 275.4, - 275.8, - 275.7, - 275.6, - 275.6, - 275.9, - 275.9, - 276.2, - 276.5, - 276.2, - 276.1, - 276.3, - 276.1, - 277.0, - 277.3, - 278.2, - 278.2, - 277.7, - 277.5, - 277.5, - 277.6, - 277.5, - 277.8, - 278.2, - 278.2, - 278.1, - 278.0, - 277.6, - 277.2, - 278.1, - 278.1, - 278.0, - 278.0, - 278.8, - 278.7, - 278.4, - 278.3, - 278.2, - 278.2, - 278.1, - 278.0, - 278.1, - 278.1, - 278.3, - 278.4, - 278.5, - 278.9, - 279.5, - 280.2, - 279.7, - 281.3, - 280.2, - 279.7, - 279.5, - 279.3, - 279.1, - 278.3, - 277.8, - 277.9, - 278.3, - 278.3, - 278.2, - 277.7, - 277.4, - 277.2, - 276.7, - 276.3, - ], - [ - 277.2, - 277.2, - 277.4, - 277.5, - 277.5, - 277.5, - 277.4, - 277.2, - 276.9, - 276.9, - 277.1, - 277.2, - 277.4, - 277.5, - 277.5, - 277.4, - 277.4, - 277.2, - 277.2, - 277.0, - 276.8, - 276.2, - 277.4, - 277.5, - 277.4, - 277.2, - 277.5, - 277.7, - 277.9, - 277.6, - 278.0, - 277.4, - 277.5, - 277.4, - 277.6, - 277.9, - 278.4, - 278.3, - 278.2, - 278.6, - 279.2, - 278.8, - 279.5, - 279.4, - 280.0, - 280.7, - 280.1, - 278.8, - 278.6, - 278.9, - 278.8, - 279.0, - 279.6, - 280.1, - 280.0, - 279.8, - 279.7, - 279.4, - 279.7, - 280.1, - 280.3, - 280.5, - 280.7, - 280.3, - 280.1, - 280.1, - 279.9, - 279.8, - 279.5, - 279.3, - 279.3, - 279.3, - 279.4, - 279.5, - 279.7, - 280.1, - 280.4, - 279.8, - 282.0, - 281.8, - 281.2, - 281.1, - 280.4, - 279.5, - 279.1, - 278.7, - 278.6, - 278.7, - 279.0, - 279.1, - 279.5, - 279.0, - 278.5, - 278.0, - 277.2, - 277.3, - ], - [ - 279.0, - 279.3, - 279.5, - 279.4, - 279.1, - 278.8, - 278.6, - 278.3, - 278.3, - 278.2, - 278.2, - 278.3, - 278.6, - 278.8, - 278.6, - 278.5, - 278.4, - 278.3, - 278.7, - 278.3, - 278.5, - 280.1, - 281.1, - 280.7, - 280.3, - 280.1, - 280.0, - 280.2, - 280.1, - 279.9, - 279.7, - 279.5, - 279.9, - 279.8, - 280.0, - 280.9, - 280.9, - 280.8, - 280.9, - 281.1, - 281.1, - 280.8, - 281.1, - 281.1, - 281.6, - 281.8, - 281.0, - 280.1, - 280.2, - 280.5, - 281.0, - 281.1, - 281.4, - 282.2, - 281.8, - 281.6, - 281.5, - 281.4, - 281.6, - 282.2, - 282.3, - 282.4, - 282.1, - 281.9, - 281.9, - 281.8, - 281.4, - 281.2, - 280.9, - 280.7, - 280.6, - 280.5, - 280.6, - 280.7, - 280.9, - 281.0, - 281.1, - 279.2, - 284.1, - 283.7, - 282.6, - 281.6, - 280.6, - 280.3, - 280.1, - 279.7, - 279.5, - 279.5, - 279.9, - 279.9, - 280.4, - 280.3, - 280.0, - 279.7, - 279.0, - 279.3, - ], - [ - 281.7, - 281.7, - 281.4, - 281.2, - 280.9, - 280.5, - 280.2, - 279.8, - 280.0, - 280.0, - 279.8, - 280.0, - 280.2, - 280.3, - 280.0, - 279.9, - 279.9, - 279.8, - 280.4, - 280.0, - 281.9, - 282.4, - 282.6, - 282.2, - 282.3, - 282.2, - 281.9, - 281.9, - 281.7, - 281.5, - 281.4, - 281.6, - 282.0, - 282.0, - 282.3, - 282.7, - 282.4, - 282.5, - 282.5, - 282.5, - 282.7, - 282.6, - 282.7, - 282.8, - 283.2, - 283.1, - 282.7, - 282.0, - 282.5, - 283.3, - 283.9, - 283.5, - 283.7, - 284.1, - 283.9, - 283.6, - 283.4, - 283.4, - 283.6, - 283.9, - 283.9, - 283.8, - 283.6, - 283.6, - 283.6, - 283.4, - 283.1, - 282.8, - 282.6, - 282.3, - 282.2, - 281.9, - 281.8, - 281.9, - 281.9, - 281.9, - 281.9, - 277.8, - 287.9, - 285.9, - 283.8, - 282.2, - 282.2, - 282.4, - 282.3, - 281.9, - 281.1, - 281.0, - 281.1, - 281.0, - 281.5, - 282.3, - 282.3, - 282.3, - 282.0, - 282.0, - ], - [ - 284.2, - 283.9, - 283.5, - 283.1, - 282.7, - 282.4, - 282.0, - 281.6, - 281.8, - 281.5, - 281.5, - 281.9, - 281.9, - 282.1, - 281.9, - 281.9, - 282.0, - 281.8, - 282.2, - 282.2, - 284.0, - 284.2, - 284.2, - 284.1, - 284.2, - 283.9, - 283.7, - 283.6, - 283.6, - 283.6, - 283.6, - 283.8, - 283.9, - 283.9, - 284.2, - 284.1, - 284.1, - 284.2, - 284.4, - 285.2, - 285.7, - 285.1, - 284.9, - 284.9, - 284.9, - 283.6, - 287.7, - 288.0, - 286.9, - 288.5, - 287.4, - 286.6, - 287.0, - 286.3, - 286.2, - 285.8, - 285.7, - 285.7, - 285.7, - 285.8, - 285.7, - 285.6, - 285.3, - 285.2, - 285.0, - 284.9, - 284.7, - 284.4, - 284.3, - 283.9, - 283.6, - 283.4, - 283.0, - 283.0, - 282.8, - 283.1, - 283.2, - 280.2, - 289.8, - 289.2, - 285.5, - 284.6, - 284.9, - 285.1, - 284.9, - 283.8, - 283.0, - 283.1, - 283.0, - 282.7, - 283.5, - 284.8, - 284.8, - 284.9, - 285.1, - 284.7, - ], - [ - 286.4, - 286.1, - 285.7, - 285.2, - 284.9, - 284.5, - 284.0, - 283.6, - 283.8, - 283.3, - 283.2, - 284.3, - 284.8, - 285.0, - 285.3, - 285.1, - 284.9, - 284.8, - 285.3, - 285.4, - 286.6, - 286.3, - 286.2, - 286.2, - 286.2, - 286.0, - 285.9, - 285.7, - 285.7, - 285.7, - 285.5, - 285.5, - 285.6, - 285.8, - 285.8, - 285.9, - 286.2, - 286.3, - 286.4, - 287.2, - 287.9, - 287.7, - 287.4, - 287.2, - 287.5, - 288.2, - 286.9, - 291.5, - 291.3, - 290.5, - 289.1, - 288.8, - 288.6, - 287.9, - 287.7, - 287.4, - 287.5, - 287.6, - 287.6, - 287.6, - 287.3, - 287.1, - 286.9, - 286.9, - 286.8, - 286.6, - 286.3, - 286.1, - 286.0, - 285.7, - 285.4, - 285.1, - 284.6, - 284.2, - 284.0, - 284.6, - 284.8, - 281.5, - 289.1, - 290.4, - 287.6, - 289.3, - 289.9, - 287.9, - 286.6, - 286.2, - 286.0, - 285.5, - 285.5, - 286.1, - 287.1, - 287.8, - 287.2, - 287.5, - 287.3, - 287.0, - ], - [ - 288.7, - 288.5, - 288.1, - 287.6, - 287.1, - 286.8, - 287.2, - 286.7, - 286.5, - 285.8, - 286.0, - 287.4, - 287.8, - 288.3, - 288.7, - 288.2, - 287.8, - 288.0, - 288.5, - 288.7, - 289.1, - 288.6, - 288.5, - 288.2, - 288.0, - 287.9, - 287.6, - 287.4, - 287.4, - 287.4, - 287.3, - 287.3, - 287.5, - 287.6, - 287.7, - 287.8, - 287.9, - 288.0, - 288.1, - 289.0, - 290.0, - 291.0, - 290.2, - 289.5, - 289.6, - 290.0, - 291.1, - 290.8, - 292.5, - 291.4, - 290.5, - 290.1, - 289.7, - 289.4, - 289.2, - 289.3, - 289.6, - 289.9, - 290.0, - 289.6, - 289.3, - 289.2, - 289.0, - 289.0, - 289.0, - 288.7, - 288.4, - 288.2, - 288.0, - 287.6, - 287.2, - 286.8, - 286.3, - 286.0, - 285.8, - 286.3, - 286.6, - 283.4, - 291.9, - 296.0, - 290.3, - 292.7, - 292.4, - 290.3, - 289.5, - 289.2, - 288.9, - 288.8, - 289.2, - 289.7, - 289.9, - 289.6, - 289.5, - 289.6, - 289.4, - 289.1, - ], - [ - 290.7, - 290.8, - 290.4, - 290.0, - 290.5, - 291.8, - 292.7, - 293.0, - 293.4, - 292.1, - 291.7, - 292.0, - 291.9, - 291.8, - 291.6, - 291.2, - 291.2, - 291.3, - 291.3, - 291.3, - 291.1, - 290.7, - 290.7, - 290.2, - 289.7, - 289.5, - 289.3, - 289.1, - 289.1, - 289.1, - 289.2, - 289.5, - 289.8, - 289.6, - 289.4, - 289.5, - 289.6, - 290.2, - 293.9, - 290.9, - 292.4, - 294.3, - 292.9, - 291.8, - 291.8, - 291.9, - 292.2, - 292.8, - 292.7, - 292.4, - 292.3, - 291.9, - 291.4, - 291.0, - 291.1, - 291.3, - 291.6, - 292.4, - 292.3, - 292.0, - 291.8, - 291.6, - 291.5, - 291.4, - 291.3, - 291.0, - 290.7, - 290.5, - 290.2, - 289.8, - 289.4, - 288.8, - 288.3, - 287.9, - 287.5, - 288.1, - 288.3, - 286.7, - 294.7, - 298.5, - 297.5, - 294.4, - 293.8, - 292.8, - 292.0, - 291.3, - 291.0, - 291.1, - 291.2, - 291.5, - 291.8, - 291.9, - 291.9, - 291.5, - 291.3, - 291.1, - ], - [ - 292.2, - 292.3, - 292.0, - 292.2, - 292.3, - 291.9, - 293.8, - 295.6, - 296.0, - 296.2, - 295.2, - 293.9, - 293.5, - 293.3, - 293.3, - 293.1, - 293.2, - 293.2, - 293.1, - 292.8, - 292.5, - 292.1, - 292.1, - 291.7, - 291.3, - 291.0, - 290.6, - 290.4, - 290.3, - 290.4, - 290.5, - 290.1, - 291.1, - 291.0, - 290.9, - 291.1, - 291.6, - 297.3, - 298.4, - 294.6, - 291.0, - 296.0, - 294.8, - 294.0, - 294.1, - 294.0, - 293.7, - 294.0, - 293.6, - 293.7, - 293.5, - 293.1, - 292.7, - 292.5, - 292.9, - 293.4, - 293.7, - 294.1, - 294.1, - 294.0, - 293.9, - 293.8, - 293.7, - 293.6, - 293.5, - 293.4, - 293.0, - 292.8, - 292.5, - 292.1, - 291.4, - 290.8, - 290.1, - 289.5, - 289.0, - 289.7, - 288.3, - 292.3, - 297.3, - 301.5, - 301.0, - 299.1, - 295.6, - 294.8, - 293.4, - 293.0, - 292.9, - 293.0, - 293.2, - 293.5, - 293.9, - 294.0, - 293.8, - 293.3, - 292.8, - 292.4, - ], - [ - 292.9, - 292.5, - 291.9, - 292.0, - 291.5, - 295.9, - 289.6, - 290.0, - 297.4, - 297.2, - 295.9, - 295.2, - 295.1, - 295.2, - 295.3, - 295.2, - 295.1, - 294.9, - 294.6, - 294.2, - 293.8, - 293.4, - 293.2, - 292.8, - 292.3, - 291.8, - 291.6, - 291.1, - 291.5, - 292.2, - 293.2, - 297.0, - 294.3, - 292.2, - 292.0, - 292.5, - 298.2, - 298.2, - 297.6, - 295.0, - 292.4, - 296.8, - 295.8, - 295.7, - 295.8, - 295.4, - 295.0, - 294.9, - 294.6, - 294.8, - 294.5, - 294.1, - 294.0, - 294.2, - 294.6, - 294.9, - 295.3, - 295.4, - 295.4, - 295.4, - 295.5, - 295.4, - 295.4, - 295.4, - 295.4, - 295.3, - 294.9, - 294.5, - 294.2, - 293.8, - 293.2, - 292.4, - 291.6, - 290.8, - 290.5, - 290.7, - 287.0, - 294.5, - 299.9, - 301.8, - 303.2, - 299.2, - 297.4, - 296.8, - 295.7, - 294.8, - 294.9, - 294.9, - 294.8, - 295.0, - 295.3, - 295.2, - 294.9, - 294.3, - 293.7, - 293.3, - ], - [ - 293.3, - 293.0, - 292.8, - 293.8, - 293.2, - 298.3, - 293.4, - 289.7, - 290.7, - 298.8, - 297.3, - 297.2, - 297.1, - 296.7, - 296.7, - 296.6, - 296.2, - 295.8, - 295.4, - 295.0, - 294.6, - 294.1, - 293.7, - 293.3, - 293.1, - 293.1, - 293.4, - 292.4, - 292.9, - 294.0, - 295.2, - 299.8, - 293.3, - 293.0, - 293.2, - 297.7, - 300.7, - 298.8, - 297.4, - 298.5, - 294.3, - 297.7, - 296.6, - 296.9, - 296.7, - 296.2, - 295.8, - 295.7, - 295.6, - 295.8, - 295.4, - 295.3, - 295.4, - 295.8, - 295.9, - 296.0, - 296.3, - 296.4, - 296.5, - 296.6, - 296.7, - 296.6, - 296.6, - 296.6, - 296.5, - 296.3, - 295.9, - 295.6, - 295.2, - 294.6, - 294.0, - 293.3, - 292.5, - 291.7, - 291.1, - 290.3, - 286.5, - 294.7, - 297.7, - 304.8, - 304.4, - 300.7, - 297.3, - 298.0, - 297.6, - 296.6, - 296.3, - 296.2, - 295.9, - 295.8, - 295.8, - 295.6, - 295.1, - 294.6, - 293.9, - 293.6, - ], - [ - 293.5, - 293.2, - 293.4, - 293.4, - 291.1, - 297.3, - 294.5, - 292.8, - 290.8, - 299.7, - 299.5, - 299.4, - 299.1, - 298.2, - 297.6, - 297.2, - 296.7, - 296.1, - 295.6, - 295.2, - 294.8, - 294.4, - 294.1, - 293.8, - 293.9, - 294.2, - 294.2, - 293.8, - 294.3, - 295.6, - 297.2, - 300.9, - 295.2, - 296.9, - 298.7, - 299.6, - 301.0, - 300.8, - 300.3, - 300.0, - 295.9, - 298.4, - 297.6, - 297.7, - 297.5, - 297.2, - 296.9, - 296.8, - 296.9, - 296.8, - 296.5, - 296.5, - 296.7, - 296.9, - 296.9, - 296.9, - 297.1, - 297.2, - 297.3, - 297.3, - 297.3, - 297.2, - 297.1, - 297.0, - 296.7, - 296.3, - 296.0, - 295.7, - 295.4, - 294.9, - 294.3, - 293.7, - 293.0, - 292.2, - 291.3, - 290.6, - 290.3, - 293.8, - 283.8, - 303.0, - 303.3, - 301.1, - 295.7, - 296.0, - 299.0, - 298.0, - 297.4, - 297.0, - 296.4, - 296.0, - 295.7, - 295.3, - 294.8, - 294.4, - 293.9, - 293.7, - ], - [ - 293.7, - 293.6, - 294.4, - 293.9, - 294.2, - 296.8, - 296.3, - 294.3, - 293.1, - 298.0, - 300.5, - 300.5, - 298.9, - 299.1, - 298.0, - 297.7, - 297.0, - 296.4, - 295.9, - 295.5, - 295.1, - 294.9, - 294.9, - 294.7, - 295.0, - 295.3, - 295.1, - 295.0, - 295.6, - 296.9, - 298.5, - 301.8, - 297.3, - 301.1, - 302.4, - 299.7, - 299.6, - 301.0, - 300.8, - 298.4, - 297.0, - 299.1, - 298.5, - 298.5, - 298.3, - 298.1, - 298.0, - 298.0, - 297.9, - 297.6, - 297.5, - 297.6, - 297.7, - 297.8, - 297.7, - 297.7, - 297.7, - 297.9, - 298.0, - 297.8, - 297.6, - 297.4, - 297.1, - 296.7, - 296.3, - 295.9, - 295.6, - 295.4, - 295.2, - 294.7, - 294.2, - 293.6, - 292.9, - 292.1, - 291.3, - 290.9, - 291.9, - 294.1, - 280.6, - 297.3, - 301.2, - 301.0, - 296.7, - 296.5, - 299.8, - 298.7, - 297.8, - 297.1, - 296.3, - 295.8, - 295.4, - 295.0, - 294.6, - 294.3, - 293.9, - 293.8, - ], - [ - 294.3, - 294.7, - 294.9, - 294.8, - 300.4, - 293.8, - 295.8, - 294.9, - 295.8, - 298.2, - 301.2, - 301.1, - 295.4, - 299.3, - 298.7, - 298.3, - 297.7, - 297.1, - 296.5, - 296.1, - 295.8, - 295.8, - 295.9, - 295.6, - 295.9, - 296.1, - 295.9, - 296.0, - 296.6, - 297.9, - 299.7, - 302.8, - 300.9, - 303.2, - 301.0, - 298.0, - 298.2, - 299.2, - 299.3, - 300.2, - 299.3, - 300.0, - 299.3, - 299.1, - 298.8, - 298.6, - 298.6, - 298.7, - 298.7, - 298.5, - 298.5, - 298.4, - 298.5, - 298.5, - 298.5, - 298.5, - 298.5, - 298.6, - 298.6, - 298.4, - 298.0, - 297.5, - 297.0, - 296.4, - 295.8, - 295.3, - 294.9, - 294.7, - 294.5, - 294.2, - 293.7, - 293.2, - 292.5, - 291.7, - 291.5, - 291.8, - 293.3, - 293.4, - 281.5, - 294.6, - 300.3, - 299.7, - 299.1, - 296.2, - 294.6, - 299.0, - 298.1, - 297.1, - 296.2, - 295.7, - 295.3, - 295.0, - 294.7, - 294.3, - 294.0, - 294.1, - ], - [ - 295.1, - 295.6, - 295.7, - 295.2, - 294.5, - 294.9, - 296.9, - 295.3, - 294.1, - 297.9, - 301.5, - 300.9, - 301.8, - 298.2, - 298.7, - 298.8, - 298.2, - 297.6, - 297.1, - 296.8, - 296.7, - 296.9, - 297.0, - 297.0, - 297.1, - 297.0, - 296.9, - 297.1, - 297.6, - 298.7, - 300.4, - 302.7, - 302.9, - 301.2, - 299.5, - 299.3, - 299.9, - 299.3, - 298.6, - 297.4, - 300.6, - 300.3, - 299.9, - 299.7, - 299.4, - 299.4, - 299.4, - 299.4, - 299.4, - 299.2, - 299.1, - 299.0, - 299.0, - 299.3, - 299.4, - 299.5, - 299.3, - 299.2, - 299.2, - 298.7, - 298.1, - 297.5, - 296.9, - 296.1, - 295.4, - 294.8, - 294.3, - 294.0, - 293.7, - 293.4, - 293.1, - 292.5, - 292.0, - 291.8, - 292.4, - 293.3, - 293.7, - 292.8, - 281.2, - 293.6, - 299.9, - 300.7, - 298.9, - 296.3, - 294.1, - 296.1, - 298.5, - 297.5, - 296.7, - 296.1, - 295.7, - 295.4, - 295.0, - 294.7, - 294.5, - 294.7, - ], - [ - 295.7, - 296.3, - 296.6, - 296.3, - 293.9, - 295.3, - 295.8, - 295.6, - 294.4, - 297.6, - 300.6, - 300.6, - 301.4, - 298.0, - 298.9, - 299.1, - 298.6, - 298.0, - 297.7, - 297.6, - 297.7, - 298.2, - 298.5, - 298.5, - 298.5, - 298.5, - 298.2, - 298.4, - 298.8, - 299.7, - 300.9, - 302.2, - 302.6, - 300.0, - 297.9, - 299.1, - 299.7, - 300.4, - 299.2, - 301.1, - 300.7, - 300.8, - 300.9, - 300.7, - 300.4, - 300.2, - 300.0, - 299.7, - 299.6, - 299.5, - 299.7, - 299.8, - 299.8, - 299.8, - 300.0, - 300.1, - 299.9, - 299.6, - 299.3, - 299.1, - 298.4, - 297.7, - 296.8, - 295.8, - 295.0, - 294.4, - 293.9, - 293.5, - 293.1, - 292.8, - 292.6, - 292.2, - 292.2, - 292.7, - 293.3, - 293.3, - 290.7, - 293.3, - 283.6, - 295.9, - 300.0, - 301.3, - 296.9, - 295.9, - 295.1, - 296.2, - 298.6, - 298.3, - 297.6, - 296.9, - 296.4, - 295.9, - 295.6, - 295.3, - 295.2, - 295.3, - ], - [ - 296.7, - 297.4, - 298.1, - 298.3, - 293.0, - 294.1, - 295.4, - 294.8, - 296.0, - 296.7, - 297.1, - 300.4, - 301.3, - 298.5, - 299.2, - 299.2, - 298.8, - 298.5, - 298.4, - 298.7, - 299.1, - 299.4, - 299.6, - 299.8, - 299.9, - 299.8, - 299.6, - 299.8, - 300.2, - 300.8, - 301.3, - 301.9, - 302.2, - 302.4, - 299.3, - 299.7, - 300.3, - 302.5, - 300.3, - 301.2, - 301.4, - 301.5, - 301.5, - 301.3, - 300.9, - 300.6, - 300.4, - 300.2, - 300.2, - 300.2, - 300.6, - 301.0, - 300.8, - 300.5, - 300.4, - 300.4, - 300.2, - 300.0, - 299.7, - 299.4, - 298.9, - 298.1, - 297.2, - 296.3, - 295.5, - 294.7, - 294.1, - 293.5, - 293.0, - 292.6, - 292.3, - 292.4, - 292.9, - 293.6, - 294.6, - 295.1, - 293.8, - 280.5, - 293.1, - 299.5, - 298.8, - 298.8, - 297.6, - 295.5, - 295.4, - 294.9, - 298.9, - 299.0, - 298.2, - 297.5, - 296.9, - 296.4, - 296.2, - 296.0, - 295.9, - 296.3, - ], - [ - 298.1, - 298.6, - 299.5, - 299.7, - 293.4, - 293.8, - 294.6, - 293.7, - 293.8, - 296.3, - 297.1, - 300.2, - 300.4, - 299.7, - 299.6, - 299.5, - 299.3, - 299.5, - 299.6, - 299.9, - 300.1, - 300.3, - 300.6, - 300.8, - 300.8, - 300.8, - 300.7, - 300.9, - 301.2, - 301.6, - 301.8, - 302.0, - 302.1, - 302.2, - 302.2, - 302.3, - 300.9, - 302.6, - 301.9, - 301.9, - 302.0, - 301.8, - 301.7, - 301.4, - 301.2, - 301.1, - 301.1, - 301.0, - 300.9, - 300.9, - 301.2, - 301.3, - 301.1, - 300.9, - 300.8, - 300.7, - 300.6, - 300.4, - 300.1, - 299.9, - 299.6, - 299.0, - 298.1, - 297.3, - 296.3, - 295.4, - 294.7, - 294.1, - 293.8, - 293.4, - 293.4, - 293.8, - 294.6, - 295.3, - 296.4, - 296.5, - 286.2, - 293.0, - 299.3, - 299.6, - 298.9, - 298.4, - 298.4, - 297.8, - 295.5, - 294.9, - 298.6, - 299.3, - 298.8, - 298.2, - 297.7, - 297.2, - 296.9, - 296.9, - 297.2, - 297.7, - ], - [ - 299.3, - 300.1, - 300.9, - 301.0, - 296.5, - 295.0, - 295.4, - 294.5, - 293.6, - 293.2, - 298.3, - 300.1, - 300.6, - 300.2, - 300.2, - 300.2, - 300.1, - 300.3, - 300.4, - 300.5, - 300.6, - 300.7, - 300.8, - 300.9, - 301.0, - 301.1, - 301.2, - 301.2, - 301.4, - 301.6, - 301.8, - 301.9, - 301.9, - 302.0, - 302.2, - 302.2, - 302.4, - 302.3, - 302.5, - 302.3, - 302.1, - 301.8, - 301.7, - 301.6, - 301.5, - 301.5, - 301.4, - 301.3, - 301.3, - 301.4, - 301.4, - 301.3, - 301.2, - 301.1, - 301.1, - 301.0, - 300.9, - 300.7, - 300.5, - 300.2, - 299.8, - 299.2, - 298.4, - 297.5, - 296.7, - 296.1, - 295.6, - 295.3, - 295.1, - 295.0, - 295.1, - 295.5, - 296.1, - 297.1, - 297.6, - 296.6, - 292.3, - 299.6, - 300.4, - 299.8, - 299.4, - 298.4, - 298.4, - 298.9, - 296.7, - 296.7, - 300.1, - 299.5, - 299.2, - 298.8, - 298.4, - 298.2, - 298.2, - 298.4, - 298.7, - 298.9, - ], - [ - 300.7, - 301.3, - 301.7, - 301.6, - 297.2, - 296.6, - 296.7, - 296.4, - 294.2, - 293.5, - 299.5, - 299.8, - 300.8, - 300.4, - 300.1, - 300.1, - 300.2, - 300.4, - 300.5, - 300.5, - 300.6, - 300.8, - 300.8, - 300.8, - 300.8, - 300.9, - 300.9, - 300.9, - 301.2, - 301.4, - 301.6, - 301.8, - 302.0, - 302.1, - 302.2, - 302.4, - 302.4, - 302.4, - 300.7, - 296.4, - 302.0, - 301.9, - 301.9, - 301.8, - 301.8, - 301.7, - 301.6, - 301.5, - 301.5, - 301.4, - 301.4, - 301.4, - 301.3, - 301.2, - 301.0, - 300.9, - 300.8, - 300.7, - 300.4, - 299.9, - 299.4, - 298.8, - 298.2, - 297.7, - 297.3, - 296.9, - 296.6, - 296.4, - 296.3, - 296.3, - 296.5, - 296.9, - 297.4, - 298.4, - 298.5, - 291.0, - 298.6, - 300.5, - 300.8, - 300.9, - 300.1, - 298.7, - 298.5, - 298.4, - 297.9, - 297.8, - 299.0, - 299.5, - 299.9, - 299.7, - 299.6, - 299.5, - 299.5, - 299.6, - 299.9, - 300.1, - ], - [ - 301.2, - 301.4, - 301.5, - 301.6, - 298.1, - 298.3, - 298.3, - 297.6, - 294.4, - 293.5, - 297.6, - 299.1, - 300.7, - 300.6, - 300.2, - 299.9, - 300.0, - 300.2, - 300.4, - 300.4, - 300.5, - 300.6, - 300.5, - 300.6, - 300.7, - 300.8, - 300.9, - 301.5, - 299.1, - 302.0, - 302.1, - 302.2, - 302.6, - 302.4, - 302.2, - 302.3, - 302.2, - 298.0, - 295.6, - 301.5, - 301.8, - 301.7, - 301.6, - 301.5, - 301.3, - 301.2, - 301.1, - 301.0, - 301.0, - 300.9, - 300.7, - 300.6, - 300.3, - 300.0, - 299.9, - 299.8, - 299.7, - 299.6, - 299.4, - 299.2, - 298.8, - 298.5, - 298.1, - 297.8, - 297.8, - 297.7, - 297.6, - 297.5, - 297.6, - 297.7, - 297.9, - 298.2, - 298.8, - 299.5, - 299.3, - 294.1, - 300.7, - 301.2, - 301.1, - 300.9, - 300.2, - 299.5, - 298.9, - 298.7, - 298.8, - 297.7, - 300.7, - 300.1, - 300.4, - 300.3, - 300.3, - 300.3, - 300.4, - 300.5, - 300.7, - 300.9, - ], - [ - 301.2, - 301.3, - 301.4, - 299.1, - 297.8, - 299.4, - 298.8, - 297.3, - 291.6, - 294.2, - 297.2, - 299.3, - 300.3, - 300.2, - 300.3, - 300.2, - 300.1, - 300.2, - 300.3, - 300.5, - 300.5, - 300.6, - 300.6, - 300.6, - 300.8, - 301.1, - 301.2, - 298.7, - 300.5, - 302.1, - 300.4, - 300.7, - 296.8, - 302.3, - 302.2, - 302.3, - 302.6, - 299.3, - 301.6, - 301.2, - 300.7, - 300.4, - 300.3, - 300.2, - 300.1, - 300.0, - 300.0, - 299.7, - 299.7, - 299.4, - 299.3, - 299.2, - 298.8, - 298.6, - 298.3, - 297.9, - 297.7, - 297.5, - 297.4, - 297.5, - 297.3, - 297.2, - 297.1, - 297.0, - 297.2, - 297.0, - 297.3, - 297.4, - 297.7, - 298.0, - 298.0, - 298.6, - 298.9, - 299.3, - 299.3, - 294.4, - 300.5, - 301.2, - 300.9, - 300.7, - 299.8, - 300.0, - 301.0, - 299.5, - 299.3, - 300.7, - 300.7, - 300.2, - 300.4, - 300.5, - 300.4, - 300.5, - 300.6, - 300.7, - 300.9, - 301.0, - ], - [ - 301.4, - 301.5, - 301.4, - 299.0, - 298.3, - 299.5, - 299.6, - 297.9, - 293.7, - 293.8, - 296.3, - 302.4, - 300.0, - 299.5, - 299.7, - 300.1, - 300.3, - 300.3, - 300.5, - 300.6, - 300.6, - 300.6, - 300.8, - 300.9, - 301.3, - 301.6, - 301.5, - 298.7, - 301.6, - 301.6, - 298.6, - 299.5, - 302.0, - 301.9, - 302.1, - 302.2, - 302.1, - 301.8, - 301.2, - 300.6, - 300.2, - 300.0, - 299.8, - 299.7, - 299.6, - 299.4, - 299.4, - 299.1, - 299.0, - 298.6, - 298.5, - 298.2, - 298.0, - 297.8, - 297.6, - 297.3, - 297.0, - 297.0, - 296.6, - 296.8, - 296.5, - 296.5, - 296.3, - 296.3, - 296.0, - 296.1, - 296.5, - 296.8, - 297.0, - 297.8, - 298.0, - 298.8, - 299.0, - 299.3, - 299.4, - 293.3, - 299.8, - 300.5, - 300.4, - 299.8, - 300.2, - 302.2, - 300.8, - 300.1, - 300.2, - 300.1, - 300.0, - 300.2, - 300.2, - 300.3, - 300.4, - 300.6, - 300.8, - 300.9, - 301.1, - 301.2, - ], - [ - 301.9, - 301.9, - 301.6, - 298.0, - 297.9, - 299.2, - 298.9, - 297.8, - 295.1, - 295.1, - 300.5, - 301.9, - 300.2, - 299.3, - 299.3, - 299.8, - 299.9, - 300.1, - 300.2, - 300.3, - 300.2, - 300.1, - 300.3, - 300.5, - 301.1, - 301.6, - 301.5, - 299.3, - 300.9, - 301.2, - 299.4, - 297.5, - 301.9, - 301.9, - 301.9, - 301.9, - 301.9, - 301.7, - 301.6, - 301.3, - 301.0, - 300.7, - 300.6, - 300.4, - 300.3, - 300.1, - 299.9, - 299.8, - 299.7, - 299.5, - 299.3, - 299.0, - 299.1, - 299.2, - 299.0, - 298.8, - 298.6, - 298.5, - 298.4, - 298.4, - 298.1, - 298.1, - 298.3, - 298.3, - 298.2, - 298.4, - 298.7, - 298.8, - 299.1, - 299.3, - 299.4, - 299.7, - 299.7, - 299.7, - 299.9, - 300.1, - 293.9, - 299.3, - 300.0, - 298.1, - 302.8, - 301.0, - 299.5, - 300.0, - 299.9, - 299.9, - 300.0, - 300.2, - 300.4, - 300.6, - 300.8, - 301.0, - 301.3, - 301.4, - 301.7, - 301.8, - ], - [ - 302.3, - 302.5, - 299.8, - 295.9, - 296.7, - 297.9, - 297.1, - 295.9, - 298.0, - 299.7, - 292.1, - 298.2, - 298.1, - 299.2, - 298.9, - 299.5, - 299.8, - 300.0, - 300.1, - 300.0, - 299.8, - 299.7, - 299.8, - 300.2, - 300.7, - 301.2, - 301.3, - 296.9, - 300.8, - 301.4, - 302.1, - 298.0, - 302.2, - 301.9, - 301.5, - 301.8, - 301.8, - 301.7, - 301.6, - 301.5, - 301.4, - 301.3, - 301.2, - 301.1, - 301.0, - 300.8, - 300.7, - 300.6, - 300.4, - 300.3, - 300.1, - 300.0, - 300.1, - 299.9, - 299.9, - 299.9, - 299.8, - 299.7, - 299.7, - 299.8, - 299.7, - 299.6, - 299.5, - 299.4, - 299.4, - 299.6, - 299.7, - 299.8, - 300.0, - 300.1, - 300.1, - 300.1, - 300.1, - 299.9, - 300.1, - 301.1, - 290.3, - 299.6, - 299.7, - 298.0, - 296.0, - 299.6, - 299.4, - 299.8, - 299.6, - 299.6, - 299.9, - 300.1, - 300.4, - 300.7, - 301.0, - 301.3, - 301.6, - 302.2, - 299.7, - 300.3, - ], - [ - 298.4, - 297.2, - 296.5, - 293.9, - 297.7, - 298.4, - 294.5, - 296.0, - 301.3, - 299.8, - 288.7, - 292.5, - 294.3, - 296.1, - 298.5, - 298.7, - 299.2, - 299.5, - 299.7, - 299.7, - 299.6, - 299.5, - 299.6, - 300.0, - 300.4, - 300.7, - 300.8, - 300.4, - 300.8, - 301.3, - 301.5, - 301.9, - 302.0, - 297.3, - 301.3, - 301.4, - 301.3, - 301.2, - 301.1, - 301.0, - 300.9, - 300.8, - 300.8, - 300.7, - 300.5, - 300.3, - 300.3, - 300.2, - 300.0, - 300.0, - 300.0, - 299.9, - 299.9, - 299.9, - 299.9, - 299.9, - 299.9, - 299.8, - 299.7, - 299.8, - 299.8, - 299.8, - 299.8, - 299.8, - 299.9, - 300.1, - 300.2, - 300.1, - 299.9, - 299.8, - 299.8, - 300.0, - 299.9, - 299.7, - 299.7, - 298.3, - 295.7, - 296.7, - 303.6, - 300.4, - 299.1, - 299.5, - 299.7, - 299.7, - 299.0, - 298.9, - 299.1, - 299.3, - 299.6, - 299.9, - 300.0, - 300.5, - 301.6, - 298.6, - 297.4, - 298.3, - ], - [ - 296.5, - 294.9, - 293.1, - 296.6, - 300.2, - 300.2, - 298.6, - 299.1, - 301.0, - 299.0, - 290.4, - 291.1, - 290.9, - 290.4, - 297.5, - 297.8, - 298.0, - 298.4, - 299.1, - 299.6, - 299.8, - 293.2, - 299.1, - 299.5, - 299.9, - 300.2, - 300.3, - 297.6, - 298.5, - 301.0, - 301.0, - 301.2, - 301.1, - 298.1, - 300.7, - 300.7, - 300.6, - 300.5, - 300.4, - 300.2, - 299.9, - 299.7, - 299.6, - 299.4, - 299.2, - 299.0, - 298.9, - 298.8, - 298.6, - 298.6, - 298.6, - 298.6, - 298.7, - 298.6, - 298.6, - 298.7, - 298.7, - 298.7, - 298.7, - 298.8, - 299.0, - 299.1, - 299.2, - 299.3, - 299.5, - 299.8, - 299.9, - 299.6, - 299.2, - 299.0, - 299.1, - 299.6, - 300.1, - 300.1, - 294.4, - 299.6, - 300.2, - 299.3, - 301.1, - 300.2, - 299.0, - 299.5, - 299.1, - 298.7, - 298.5, - 298.2, - 298.1, - 298.1, - 298.3, - 298.6, - 298.7, - 299.2, - 300.9, - 295.8, - 295.9, - 296.5, - ], - [ - 298.7, - 297.5, - 297.7, - 299.4, - 300.9, - 299.8, - 297.2, - 296.9, - 299.0, - 301.1, - 291.8, - 289.3, - 288.2, - 297.1, - 297.1, - 296.8, - 296.8, - 297.3, - 298.3, - 299.3, - 300.1, - 291.1, - 298.5, - 298.8, - 299.1, - 299.3, - 298.9, - 295.9, - 297.2, - 296.0, - 300.4, - 300.4, - 300.1, - 297.6, - 300.2, - 300.1, - 300.1, - 300.1, - 300.0, - 299.8, - 299.6, - 299.3, - 299.0, - 298.6, - 298.3, - 298.1, - 297.9, - 297.7, - 297.6, - 297.5, - 297.5, - 297.4, - 297.5, - 297.5, - 297.4, - 297.2, - 297.2, - 297.3, - 297.2, - 297.5, - 297.8, - 298.1, - 298.3, - 298.6, - 298.8, - 299.2, - 299.5, - 299.4, - 298.8, - 298.1, - 298.0, - 298.6, - 300.1, - 296.6, - 299.2, - 297.7, - 297.0, - 296.4, - 297.7, - 298.7, - 298.7, - 298.8, - 298.4, - 298.2, - 298.1, - 297.8, - 297.5, - 297.3, - 297.2, - 297.5, - 297.7, - 297.9, - 299.8, - 296.7, - 297.8, - 298.6, - ], - [ - 298.5, - 297.9, - 297.1, - 296.8, - 297.3, - 297.1, - 294.6, - 293.7, - 296.6, - 299.4, - 293.8, - 298.8, - 284.9, - 289.3, - 297.1, - 295.9, - 295.7, - 296.3, - 297.4, - 298.5, - 291.5, - 294.5, - 298.3, - 298.2, - 298.4, - 298.2, - 297.4, - 293.2, - 293.8, - 293.9, - 299.2, - 299.3, - 295.4, - 299.2, - 299.3, - 299.1, - 299.2, - 299.2, - 299.3, - 299.4, - 299.3, - 299.0, - 298.6, - 298.1, - 297.6, - 297.3, - 297.0, - 296.8, - 296.7, - 296.6, - 296.5, - 296.2, - 296.3, - 296.3, - 296.1, - 296.1, - 296.1, - 296.1, - 296.3, - 296.5, - 296.8, - 297.0, - 297.2, - 297.2, - 297.3, - 297.7, - 298.4, - 299.1, - 298.9, - 297.9, - 297.2, - 296.4, - 291.5, - 293.8, - 299.1, - 299.1, - 298.8, - 298.8, - 298.9, - 298.9, - 298.5, - 298.2, - 297.8, - 297.5, - 297.4, - 297.3, - 297.1, - 296.9, - 296.7, - 296.5, - 296.5, - 296.9, - 299.4, - 299.6, - 298.4, - 298.6, - ], - [ - 295.1, - 294.7, - 293.0, - 292.3, - 293.5, - 294.7, - 292.3, - 293.1, - 295.3, - 296.1, - 294.7, - 300.3, - 284.4, - 288.6, - 289.2, - 295.6, - 294.9, - 295.7, - 296.8, - 298.0, - 291.2, - 291.8, - 290.6, - 298.1, - 298.1, - 297.6, - 288.6, - 289.9, - 289.8, - 296.0, - 297.7, - 298.2, - 298.0, - 297.6, - 297.2, - 297.0, - 297.2, - 297.5, - 297.7, - 297.9, - 298.2, - 298.4, - 298.3, - 298.0, - 297.6, - 297.1, - 296.6, - 296.3, - 295.9, - 295.7, - 295.4, - 295.1, - 295.0, - 294.9, - 294.7, - 294.7, - 294.6, - 294.6, - 294.8, - 295.2, - 295.4, - 295.3, - 295.4, - 295.3, - 295.2, - 295.4, - 296.6, - 298.2, - 298.9, - 289.8, - 287.0, - 289.8, - 294.7, - 298.4, - 298.6, - 298.7, - 299.0, - 299.0, - 298.6, - 298.5, - 298.2, - 297.8, - 297.5, - 297.1, - 296.7, - 296.6, - 296.6, - 296.4, - 296.2, - 296.1, - 296.1, - 295.9, - 296.5, - 296.6, - 295.4, - 295.0, - ], - [ - 292.2, - 290.9, - 290.4, - 290.4, - 290.2, - 288.6, - 289.6, - 290.2, - 292.8, - 292.5, - 299.2, - 289.1, - 287.6, - 289.6, - 290.8, - 290.7, - 295.2, - 295.5, - 296.5, - 297.5, - 289.3, - 291.2, - 287.7, - 298.1, - 298.2, - 288.9, - 286.8, - 285.9, - 286.0, - 292.4, - 295.0, - 296.9, - 296.4, - 295.5, - 294.8, - 294.5, - 294.8, - 295.2, - 295.6, - 295.9, - 296.3, - 296.7, - 296.9, - 297.0, - 296.8, - 296.5, - 296.1, - 295.8, - 295.3, - 294.9, - 294.6, - 294.3, - 294.1, - 293.8, - 293.6, - 293.4, - 293.2, - 293.1, - 293.0, - 293.1, - 293.1, - 293.0, - 293.0, - 293.0, - 292.9, - 293.4, - 294.6, - 296.7, - 290.7, - 283.2, - 283.4, - 296.3, - 294.1, - 297.6, - 297.4, - 297.6, - 295.5, - 295.2, - 298.1, - 297.8, - 297.6, - 297.3, - 297.1, - 296.8, - 296.3, - 295.9, - 295.8, - 295.7, - 295.6, - 295.5, - 295.5, - 294.7, - 293.2, - 292.9, - 292.1, - 292.2, - ], - [ - 291.1, - 288.9, - 286.0, - 287.7, - 287.6, - 286.6, - 287.5, - 287.1, - 290.2, - 290.0, - 297.1, - 287.9, - 286.7, - 288.3, - 290.4, - 288.3, - 295.3, - 295.2, - 295.8, - 288.5, - 288.9, - 288.4, - 287.4, - 287.3, - 291.2, - 286.0, - 284.9, - 280.5, - 282.4, - 280.3, - 283.8, - 293.2, - 293.6, - 293.6, - 292.4, - 292.2, - 292.3, - 292.5, - 293.2, - 293.8, - 294.2, - 294.8, - 295.2, - 295.5, - 295.6, - 295.5, - 295.2, - 294.8, - 294.4, - 294.0, - 293.7, - 293.4, - 293.1, - 292.8, - 292.6, - 292.4, - 292.2, - 292.0, - 291.8, - 291.8, - 291.7, - 291.7, - 291.8, - 291.8, - 291.6, - 292.0, - 293.2, - 295.5, - 284.4, - 280.6, - 286.9, - 294.5, - 295.2, - 296.1, - 295.2, - 291.2, - 296.1, - 296.7, - 296.8, - 296.6, - 296.5, - 296.5, - 296.3, - 296.1, - 295.7, - 295.3, - 295.1, - 294.9, - 294.9, - 294.8, - 294.9, - 294.4, - 290.6, - 290.8, - 290.8, - 291.1, - ], - [ - 290.4, - 288.1, - 285.2, - 285.7, - 286.9, - 287.4, - 287.2, - 286.0, - 287.6, - 289.6, - 290.1, - 285.8, - 284.8, - 287.0, - 289.7, - 288.5, - 294.5, - 294.6, - 286.5, - 286.4, - 284.1, - 285.4, - 287.0, - 288.2, - 287.0, - 282.8, - 281.1, - 278.0, - 278.5, - 280.5, - 279.8, - 276.8, - 289.8, - 292.0, - 290.9, - 290.5, - 290.5, - 290.4, - 290.8, - 291.3, - 291.9, - 292.6, - 293.2, - 293.7, - 294.0, - 294.1, - 294.0, - 293.9, - 293.6, - 293.2, - 292.8, - 292.4, - 292.1, - 291.6, - 291.3, - 291.1, - 290.9, - 290.9, - 290.9, - 290.8, - 290.7, - 290.8, - 290.9, - 290.9, - 290.6, - 290.9, - 292.0, - 293.9, - 279.2, - 280.3, - 285.6, - 293.0, - 293.5, - 293.6, - 292.5, - 293.9, - 294.4, - 294.8, - 295.0, - 295.2, - 295.2, - 295.3, - 295.3, - 295.3, - 295.1, - 294.9, - 294.6, - 294.4, - 294.2, - 294.0, - 294.0, - 294.2, - 294.0, - 289.0, - 289.9, - 290.0, - ], - [ - 288.3, - 287.8, - 287.2, - 286.6, - 286.8, - 287.7, - 288.2, - 287.0, - 286.6, - 287.5, - 284.8, - 283.8, - 284.1, - 293.8, - 294.4, - 283.1, - 280.0, - 280.8, - 283.8, - 284.8, - 281.4, - 284.1, - 284.0, - 275.8, - 272.7, - 278.9, - 274.1, - 272.2, - 277.0, - 274.2, - 277.1, - 276.9, - 275.1, - 288.0, - 288.3, - 289.1, - 289.0, - 288.8, - 288.8, - 289.2, - 289.6, - 290.3, - 290.9, - 291.6, - 292.0, - 292.2, - 292.2, - 292.2, - 292.1, - 291.9, - 291.6, - 291.3, - 291.0, - 290.6, - 290.3, - 290.0, - 289.7, - 289.6, - 289.7, - 289.9, - 289.8, - 289.7, - 289.8, - 289.8, - 289.5, - 289.8, - 290.7, - 279.3, - 278.3, - 280.4, - 281.1, - 288.7, - 288.6, - 288.4, - 288.4, - 291.6, - 292.2, - 292.7, - 293.0, - 293.3, - 293.5, - 293.8, - 293.9, - 294.1, - 294.1, - 294.0, - 293.8, - 293.7, - 293.6, - 293.4, - 293.1, - 293.1, - 293.5, - 287.8, - 287.2, - 287.1, - ], - [ - 283.4, - 285.0, - 285.9, - 285.5, - 286.3, - 287.6, - 287.1, - 286.6, - 286.6, - 284.8, - 282.4, - 282.7, - 284.0, - 293.1, - 274.8, - 275.5, - 279.8, - 279.6, - 278.0, - 283.2, - 282.9, - 276.8, - 261.2, - 245.8, - 259.6, - 256.2, - 259.3, - 267.7, - 277.8, - 274.4, - 275.5, - 275.4, - 274.8, - 284.1, - 284.2, - 285.8, - 286.5, - 286.5, - 287.1, - 287.7, - 288.2, - 288.7, - 289.2, - 289.7, - 290.1, - 290.3, - 290.4, - 290.2, - 290.2, - 290.0, - 289.8, - 289.6, - 289.3, - 289.1, - 289.0, - 288.9, - 288.9, - 288.7, - 288.7, - 288.9, - 288.9, - 288.6, - 288.8, - 288.7, - 288.3, - 288.6, - 281.2, - 277.0, - 276.8, - 277.0, - 275.8, - 277.1, - 276.5, - 276.5, - 276.7, - 288.3, - 290.4, - 291.3, - 291.5, - 291.7, - 291.9, - 292.1, - 292.4, - 292.6, - 292.8, - 292.9, - 292.8, - 292.8, - 292.7, - 292.6, - 292.3, - 292.2, - 292.5, - 292.7, - 282.1, - 283.9, - ], - [ - 279.1, - 280.6, - 283.5, - 283.6, - 289.8, - 290.2, - 290.1, - 290.0, - 290.4, - 290.8, - 280.5, - 281.3, - 283.8, - 276.7, - 274.6, - 276.6, - 276.4, - 275.7, - 272.5, - 281.0, - 278.8, - 254.8, - 251.9, - 253.4, - 256.7, - 247.5, - 257.2, - 263.1, - 270.5, - 272.0, - 273.2, - 273.7, - 274.2, - 280.9, - 279.7, - 282.0, - 282.5, - 282.2, - 284.2, - 285.2, - 286.2, - 287.0, - 287.5, - 287.9, - 288.2, - 288.4, - 288.6, - 288.5, - 288.3, - 288.2, - 287.9, - 287.7, - 287.4, - 287.3, - 287.3, - 287.4, - 287.6, - 287.5, - 287.5, - 287.6, - 287.8, - 287.6, - 287.7, - 287.6, - 287.1, - 279.8, - 279.5, - 273.8, - 273.7, - 274.4, - 274.8, - 275.3, - 275.0, - 275.1, - 274.7, - 284.1, - 288.4, - 289.8, - 290.2, - 290.2, - 290.6, - 290.7, - 290.9, - 291.1, - 291.3, - 291.6, - 291.8, - 291.8, - 291.7, - 291.6, - 291.5, - 291.4, - 291.5, - 291.4, - 282.4, - 278.9, - ], - [ - 278.8, - 277.5, - 278.0, - 288.2, - 288.2, - 288.1, - 288.2, - 288.6, - 289.0, - 289.3, - 278.3, - 280.4, - 277.2, - 272.0, - 275.3, - 276.3, - 275.6, - 273.5, - 269.1, - 269.9, - 255.7, - 246.4, - 251.9, - 253.4, - 245.8, - 246.2, - 258.6, - 263.1, - 267.9, - 270.2, - 270.5, - 271.0, - 276.2, - 276.7, - 274.4, - 277.2, - 269.0, - 266.8, - 280.2, - 282.3, - 283.3, - 284.2, - 285.0, - 285.6, - 285.8, - 286.0, - 286.3, - 286.5, - 286.4, - 286.3, - 286.1, - 285.9, - 285.7, - 285.6, - 285.5, - 285.7, - 286.1, - 286.1, - 286.2, - 286.4, - 286.5, - 286.5, - 286.6, - 286.7, - 277.9, - 276.5, - 273.3, - 269.3, - 271.2, - 272.5, - 272.2, - 271.8, - 271.7, - 271.3, - 270.9, - 270.7, - 284.5, - 287.6, - 288.5, - 288.9, - 289.5, - 289.8, - 289.6, - 289.7, - 290.0, - 290.2, - 290.5, - 290.7, - 290.8, - 290.7, - 290.7, - 290.5, - 290.1, - 290.0, - 289.2, - 281.4, - ], - [ - 285.2, - 286.4, - 286.4, - 286.0, - 286.2, - 286.3, - 286.6, - 286.0, - 276.0, - 274.1, - 274.7, - 274.2, - 270.0, - 272.4, - 287.2, - 274.7, - 275.2, - 275.3, - 275.2, - 260.8, - 251.5, - 268.3, - 270.1, - 263.4, - 252.5, - 263.2, - 252.3, - 262.8, - 264.5, - 265.2, - 266.5, - 268.0, - 272.0, - 270.4, - 247.3, - 271.7, - 275.1, - 266.1, - 277.3, - 278.6, - 280.3, - 281.6, - 282.2, - 282.6, - 283.1, - 283.4, - 283.9, - 284.3, - 284.5, - 284.3, - 283.8, - 283.5, - 283.5, - 283.4, - 283.6, - 284.0, - 284.5, - 284.7, - 284.9, - 285.2, - 285.3, - 285.5, - 285.4, - 285.4, - 273.3, - 267.6, - 265.5, - 264.9, - 263.3, - 271.6, - 270.0, - 268.6, - 268.3, - 268.5, - 268.7, - 270.4, - 281.5, - 285.3, - 286.5, - 287.6, - 288.1, - 288.5, - 288.7, - 288.6, - 288.7, - 288.8, - 289.0, - 289.6, - 289.7, - 289.8, - 289.6, - 289.3, - 288.6, - 287.8, - 279.7, - 279.1, - ], - [ - 273.8, - 284.8, - 284.3, - 283.4, - 284.1, - 284.3, - 269.9, - 276.7, - 275.1, - 272.3, - 266.6, - 264.7, - 269.8, - 274.6, - 283.5, - 272.9, - 272.1, - 273.2, - 272.5, - 263.6, - 259.2, - 256.8, - 262.2, - 266.6, - 266.4, - 261.5, - 258.6, - 264.1, - 262.1, - 262.1, - 260.5, - 263.4, - 255.3, - 246.1, - 253.9, - 266.2, - 270.5, - 272.7, - 262.4, - 270.6, - 276.3, - 277.7, - 277.8, - 277.9, - 278.5, - 279.6, - 280.3, - 280.7, - 281.2, - 281.3, - 281.1, - 281.1, - 281.3, - 281.5, - 281.7, - 282.3, - 282.9, - 283.3, - 283.5, - 284.0, - 284.2, - 284.4, - 284.4, - 278.8, - 269.8, - 264.9, - 264.1, - 262.6, - 262.6, - 268.9, - 265.4, - 264.7, - 264.5, - 265.8, - 266.6, - 265.8, - 267.8, - 279.0, - 281.4, - 283.4, - 284.6, - 285.3, - 285.8, - 286.0, - 286.8, - 286.9, - 287.1, - 288.1, - 288.7, - 288.8, - 288.4, - 288.0, - 287.3, - 286.7, - 277.4, - 276.5, - ], - [ - 274.9, - 283.4, - 282.1, - 269.1, - 281.3, - 261.9, - 270.2, - 267.7, - 280.6, - 281.8, - 282.8, - 273.9, - 265.5, - 281.3, - 272.6, - 269.8, - 268.9, - 269.3, - 271.0, - 266.4, - 261.9, - 257.4, - 261.4, - 253.0, - 261.0, - 261.9, - 257.6, - 260.1, - 257.9, - 258.3, - 256.1, - 252.3, - 255.7, - 247.3, - 251.4, - 240.9, - 265.6, - 270.0, - 261.5, - 269.7, - 270.6, - 274.3, - 274.8, - 275.4, - 276.4, - 277.5, - 277.5, - 277.6, - 278.1, - 278.5, - 278.6, - 279.0, - 279.3, - 279.7, - 280.0, - 280.5, - 281.1, - 281.7, - 282.1, - 282.5, - 283.0, - 283.1, - 283.4, - 276.3, - 268.0, - 265.9, - 262.4, - 261.1, - 261.6, - 262.4, - 262.7, - 263.0, - 262.7, - 264.7, - 265.1, - 264.7, - 266.2, - 265.9, - 276.9, - 279.0, - 280.8, - 281.2, - 280.9, - 281.5, - 283.2, - 283.6, - 284.3, - 286.1, - 287.3, - 287.6, - 286.8, - 286.8, - 286.5, - 286.3, - 275.0, - 276.1, - ], - [ - 274.5, - 271.0, - 266.0, - 268.2, - 266.9, - 266.2, - 266.2, - 263.3, - 277.4, - 279.3, - 280.8, - 273.2, - 273.8, - 278.1, - 269.3, - 267.1, - 265.8, - 264.3, - 266.5, - 265.0, - 266.8, - 262.1, - 257.7, - 260.3, - 253.8, - 252.7, - 247.7, - 255.3, - 255.3, - 253.8, - 254.4, - 250.6, - 250.4, - 243.1, - 244.6, - 248.9, - 249.0, - 266.6, - 262.0, - 270.0, - 272.0, - 273.2, - 273.3, - 274.0, - 274.7, - 275.4, - 275.5, - 275.6, - 275.9, - 276.1, - 276.5, - 277.1, - 277.7, - 278.1, - 278.4, - 278.9, - 279.7, - 280.2, - 280.6, - 281.2, - 281.9, - 282.1, - 282.5, - 276.1, - 272.3, - 267.5, - 260.7, - 261.8, - 262.1, - 259.5, - 259.8, - 261.4, - 261.6, - 272.4, - 273.0, - 259.2, - 261.9, - 262.4, - 264.8, - 274.8, - 276.8, - 276.2, - 276.9, - 277.8, - 279.4, - 280.8, - 282.1, - 284.0, - 285.3, - 286.1, - 285.4, - 285.1, - 285.2, - 285.8, - 285.1, - 283.7, - ], - [ - 273.0, - 274.0, - 266.9, - 261.6, - 264.6, - 265.3, - 264.3, - 262.6, - 262.2, - 264.1, - 268.2, - 268.7, - 267.6, - 266.1, - 263.6, - 262.5, - 260.5, - 260.1, - 259.8, - 258.0, - 258.9, - 259.2, - 256.6, - 251.1, - 247.7, - 252.2, - 249.4, - 249.4, - 253.3, - 249.6, - 249.9, - 246.2, - 246.0, - 244.9, - 242.8, - 246.4, - 249.2, - 250.5, - 264.2, - 269.5, - 271.8, - 271.9, - 271.8, - 272.8, - 273.5, - 273.9, - 273.9, - 274.0, - 274.2, - 274.5, - 275.1, - 275.7, - 276.4, - 276.9, - 277.2, - 277.7, - 278.5, - 279.1, - 279.6, - 280.3, - 281.1, - 281.4, - 281.0, - 275.6, - 271.9, - 266.2, - 263.0, - 260.8, - 257.4, - 257.4, - 256.9, - 258.9, - 259.5, - 269.7, - 252.8, - 258.9, - 261.3, - 262.3, - 264.2, - 272.0, - 273.8, - 268.0, - 274.9, - 276.0, - 277.5, - 279.2, - 280.6, - 282.1, - 283.3, - 284.4, - 284.4, - 284.1, - 285.0, - 285.1, - 284.0, - 282.6, - ], - [ - 271.1, - 272.8, - 268.2, - 266.4, - 264.6, - 262.8, - 263.0, - 261.6, - 261.5, - 261.0, - 260.6, - 260.5, - 260.5, - 260.2, - 259.6, - 258.6, - 256.7, - 257.0, - 257.7, - 256.5, - 256.8, - 257.7, - 258.7, - 253.6, - 252.2, - 251.6, - 251.0, - 251.5, - 252.9, - 251.5, - 249.3, - 244.7, - 242.7, - 242.7, - 240.6, - 242.4, - 248.5, - 248.8, - 265.8, - 269.7, - 270.9, - 270.4, - 269.2, - 271.3, - 272.1, - 272.4, - 272.5, - 272.7, - 273.0, - 273.5, - 274.1, - 274.7, - 275.4, - 275.9, - 276.3, - 276.8, - 277.6, - 278.3, - 279.1, - 279.8, - 280.5, - 280.6, - 280.3, - 266.9, - 265.4, - 261.0, - 259.6, - 258.2, - 256.0, - 255.5, - 255.1, - 257.3, - 257.7, - 258.2, - 257.1, - 260.2, - 258.5, - 260.7, - 260.2, - 260.6, - 264.8, - 267.5, - 274.2, - 275.5, - 276.9, - 278.5, - 279.6, - 280.7, - 281.8, - 282.9, - 282.9, - 283.2, - 284.6, - 283.9, - 281.7, - 280.7, - ], - [ - 272.9, - 278.0, - 266.3, - 264.1, - 264.1, - 261.7, - 260.5, - 260.0, - 258.1, - 256.2, - 255.4, - 254.6, - 254.8, - 255.0, - 254.6, - 256.0, - 254.5, - 254.0, - 255.1, - 256.0, - 256.1, - 256.0, - 257.0, - 257.5, - 256.7, - 254.9, - 252.3, - 253.8, - 251.8, - 248.4, - 245.3, - 242.5, - 240.9, - 240.6, - 239.5, - 239.7, - 245.4, - 244.2, - 260.7, - 268.4, - 269.1, - 268.7, - 240.9, - 267.9, - 269.2, - 270.1, - 270.8, - 271.1, - 272.0, - 272.8, - 273.2, - 274.0, - 274.8, - 275.5, - 276.1, - 276.5, - 277.1, - 277.9, - 278.7, - 279.4, - 280.0, - 279.9, - 267.4, - 266.1, - 263.0, - 261.9, - 258.8, - 256.7, - 255.0, - 255.2, - 254.8, - 254.5, - 256.1, - 255.6, - 256.0, - 268.1, - 253.7, - 258.8, - 255.8, - 255.8, - 261.0, - 266.4, - 273.4, - 275.0, - 276.6, - 278.6, - 279.7, - 280.2, - 281.0, - 281.7, - 282.3, - 282.5, - 283.6, - 282.1, - 269.5, - 272.3, - ], - [ - 279.5, - 278.6, - 276.0, - 264.8, - 272.6, - 272.6, - 259.8, - 257.3, - 254.5, - 251.7, - 250.6, - 250.5, - 250.5, - 250.1, - 250.8, - 252.4, - 253.3, - 251.8, - 252.0, - 252.7, - 252.6, - 252.2, - 252.9, - 254.6, - 255.4, - 255.2, - 255.5, - 251.6, - 249.8, - 244.5, - 243.6, - 239.9, - 238.9, - 237.6, - 238.1, - 239.3, - 240.5, - 250.0, - 255.8, - 266.6, - 266.7, - 267.2, - 245.0, - 243.8, - 265.6, - 267.4, - 268.2, - 268.7, - 270.2, - 271.3, - 272.1, - 272.9, - 273.6, - 274.7, - 275.5, - 276.4, - 277.0, - 277.7, - 278.3, - 278.6, - 278.1, - 269.4, - 266.8, - 263.5, - 263.3, - 262.7, - 260.3, - 257.5, - 254.8, - 253.6, - 252.4, - 251.2, - 251.1, - 251.3, - 262.3, - 268.0, - 249.2, - 256.3, - 252.8, - 250.1, - 256.0, - 271.0, - 273.2, - 275.2, - 277.1, - 278.8, - 279.9, - 280.3, - 280.9, - 281.6, - 282.6, - 282.9, - 283.4, - 282.9, - 280.9, - 270.4, - ], - [ - 279.7, - 277.9, - 275.4, - 273.1, - 257.7, - 272.2, - 269.6, - 250.4, - 251.0, - 248.2, - 245.6, - 246.1, - 246.9, - 246.0, - 247.1, - 248.1, - 248.7, - 248.0, - 248.3, - 248.7, - 248.9, - 248.9, - 248.9, - 249.6, - 250.4, - 250.2, - 247.7, - 246.4, - 244.8, - 244.6, - 241.7, - 239.0, - 238.3, - 236.0, - 233.8, - 235.6, - 236.8, - 240.2, - 250.0, - 258.6, - 257.6, - 257.7, - 256.0, - 242.0, - 257.2, - 264.2, - 264.5, - 265.3, - 266.7, - 268.3, - 269.3, - 271.1, - 272.2, - 272.0, - 273.1, - 274.7, - 275.8, - 276.5, - 277.1, - 277.2, - 266.4, - 266.3, - 262.2, - 262.1, - 260.9, - 261.2, - 259.3, - 256.6, - 254.0, - 251.6, - 249.0, - 248.4, - 251.3, - 250.0, - 253.9, - 263.0, - 245.9, - 252.7, - 254.5, - 246.1, - 268.6, - 271.9, - 273.7, - 275.7, - 277.1, - 278.2, - 279.5, - 280.0, - 280.7, - 281.3, - 281.8, - 281.8, - 281.7, - 282.2, - 281.6, - 265.3, - ], - [ - 279.8, - 277.7, - 255.1, - 258.3, - 255.5, - 270.2, - 266.8, - 246.2, - 247.0, - 245.2, - 242.0, - 243.4, - 243.2, - 243.2, - 244.7, - 245.2, - 244.9, - 243.2, - 243.9, - 244.2, - 244.7, - 244.7, - 244.2, - 244.3, - 245.4, - 243.4, - 240.8, - 239.1, - 238.9, - 238.3, - 236.7, - 236.1, - 234.8, - 233.7, - 232.2, - 231.0, - 229.6, - 237.8, - 243.5, - 244.1, - 242.9, - 240.6, - 250.7, - 250.6, - 239.3, - 252.1, - 250.7, - 250.8, - 256.8, - 263.2, - 267.1, - 268.8, - 269.6, - 250.4, - 257.9, - 258.2, - 259.1, - 262.1, - 261.1, - 259.5, - 259.4, - 261.1, - 259.4, - 257.8, - 257.6, - 257.5, - 255.0, - 254.4, - 251.8, - 248.8, - 245.5, - 249.3, - 250.1, - 249.3, - 250.3, - 252.9, - 246.2, - 249.3, - 255.3, - 261.8, - 268.1, - 271.6, - 273.4, - 274.7, - 275.4, - 276.2, - 278.5, - 279.4, - 280.3, - 280.9, - 281.3, - 281.2, - 281.0, - 281.4, - 281.4, - 280.7, - ], - [ - 278.7, - 277.8, - 264.9, - 264.0, - 254.2, - 267.9, - 249.4, - 242.7, - 244.0, - 242.0, - 240.6, - 241.8, - 241.4, - 241.1, - 241.8, - 241.7, - 240.0, - 238.2, - 238.6, - 239.1, - 239.4, - 239.5, - 239.9, - 240.3, - 240.7, - 238.7, - 236.5, - 235.7, - 234.5, - 233.6, - 232.0, - 229.2, - 230.4, - 227.2, - 226.2, - 229.9, - 232.8, - 235.1, - 237.5, - 239.0, - 239.8, - 240.1, - 240.2, - 241.7, - 245.4, - 245.5, - 246.0, - 244.4, - 255.6, - 260.1, - 262.5, - 266.9, - 267.4, - 250.2, - 259.4, - 258.6, - 258.3, - 257.9, - 256.5, - 254.3, - 254.3, - 256.5, - 256.6, - 254.8, - 251.6, - 252.2, - 251.5, - 249.3, - 246.8, - 245.4, - 244.0, - 242.9, - 248.6, - 248.7, - 248.8, - 248.2, - 249.5, - 252.3, - 246.9, - 258.9, - 267.3, - 270.3, - 272.2, - 248.1, - 253.5, - 273.7, - 276.3, - 277.8, - 279.1, - 279.8, - 281.3, - 281.1, - 280.7, - 280.8, - 280.4, - 279.8, - ], - [ - 276.4, - 276.5, - 275.5, - 275.0, - 250.6, - 252.0, - 256.1, - 241.6, - 241.3, - 241.2, - 239.5, - 239.0, - 238.2, - 239.2, - 238.2, - 237.3, - 236.0, - 234.8, - 234.5, - 234.7, - 235.0, - 234.5, - 234.9, - 236.3, - 236.9, - 235.5, - 234.5, - 233.6, - 231.4, - 229.8, - 228.6, - 228.8, - 227.9, - 227.4, - 232.3, - 234.1, - 234.0, - 232.7, - 232.6, - 234.1, - 233.7, - 233.4, - 235.4, - 237.4, - 237.3, - 240.4, - 241.4, - 242.2, - 251.4, - 252.7, - 255.0, - 261.0, - 261.3, - 245.5, - 252.5, - 252.0, - 250.5, - 251.8, - 252.1, - 252.0, - 251.6, - 252.7, - 252.8, - 248.1, - 247.8, - 248.3, - 247.9, - 245.9, - 244.3, - 243.9, - 243.3, - 243.7, - 243.5, - 242.5, - 247.1, - 246.3, - 247.3, - 246.6, - 245.6, - 252.1, - 266.7, - 269.9, - 271.9, - 249.9, - 252.0, - 243.4, - 273.0, - 270.7, - 270.1, - 271.5, - 278.6, - 255.6, - 257.5, - 277.9, - 277.3, - 276.7, - ], - [ - 274.7, - 274.7, - 274.0, - 273.4, - 240.9, - 248.1, - 247.8, - 246.6, - 243.0, - 240.4, - 237.7, - 244.3, - 241.4, - 241.3, - 232.1, - 232.7, - 233.6, - 232.8, - 231.5, - 231.0, - 230.8, - 230.1, - 230.8, - 232.7, - 234.3, - 232.2, - 231.2, - 230.9, - 229.7, - 228.1, - 227.9, - 227.8, - 227.4, - 228.2, - 229.6, - 229.1, - 229.2, - 230.8, - 231.3, - 231.9, - 231.8, - 231.5, - 231.8, - 234.2, - 236.6, - 238.2, - 240.1, - 241.3, - 240.1, - 238.5, - 239.6, - 243.8, - 255.4, - 242.5, - 250.6, - 249.5, - 245.8, - 245.5, - 244.6, - 244.6, - 244.8, - 244.0, - 244.3, - 245.4, - 245.0, - 244.5, - 243.8, - 243.1, - 242.6, - 242.6, - 241.2, - 242.2, - 244.2, - 243.3, - 240.8, - 245.5, - 246.3, - 245.5, - 243.6, - 247.8, - 266.3, - 269.5, - 247.1, - 253.6, - 249.5, - 247.9, - 247.0, - 250.8, - 266.3, - 261.0, - 263.1, - 270.9, - 274.4, - 275.1, - 274.2, - 273.9, - ], - [ - 272.2, - 272.7, - 272.0, - 271.5, - 270.3, - 270.5, - 242.2, - 243.8, - 247.3, - 263.6, - 258.9, - 246.2, - 240.0, - 240.6, - 239.1, - 235.5, - 236.5, - 238.7, - 229.9, - 227.0, - 227.7, - 226.9, - 226.5, - 229.8, - 232.2, - 230.6, - 230.0, - 229.6, - 229.5, - 228.5, - 227.8, - 227.6, - 227.5, - 228.6, - 229.5, - 229.6, - 229.0, - 228.5, - 228.8, - 229.7, - 230.1, - 230.4, - 231.9, - 238.6, - 238.6, - 238.3, - 238.2, - 237.5, - 239.9, - 244.9, - 246.5, - 245.2, - 248.3, - 255.0, - 234.9, - 238.8, - 242.9, - 248.6, - 248.2, - 250.8, - 250.9, - 247.1, - 248.3, - 241.2, - 241.0, - 240.8, - 240.6, - 240.3, - 239.9, - 244.3, - 242.7, - 240.5, - 241.9, - 241.4, - 237.7, - 237.7, - 244.5, - 244.5, - 247.1, - 252.4, - 266.4, - 269.8, - 249.9, - 253.8, - 245.5, - 238.3, - 238.4, - 242.4, - 247.6, - 249.2, - 245.4, - 255.7, - 264.5, - 264.9, - 266.0, - 269.0, - ], - [ - 269.0, - 269.4, - 269.4, - 269.8, - 271.1, - 270.8, - 268.2, - 266.5, - 265.6, - 264.8, - 261.3, - 246.1, - 235.9, - 237.4, - 238.2, - 235.0, - 235.9, - 237.4, - 237.4, - 228.2, - 237.7, - 235.9, - 223.6, - 225.8, - 227.0, - 228.1, - 227.4, - 226.5, - 226.1, - 226.3, - 226.9, - 228.1, - 227.3, - 226.7, - 227.5, - 235.2, - 235.5, - 235.8, - 235.5, - 235.1, - 235.1, - 235.6, - 236.4, - 238.5, - 239.4, - 239.6, - 240.6, - 240.2, - 239.7, - 241.4, - 243.6, - 241.9, - 241.5, - 251.9, - 244.3, - 240.9, - 241.6, - 242.6, - 242.3, - 243.6, - 243.1, - 242.9, - 245.9, - 238.0, - 238.6, - 238.3, - 238.3, - 240.3, - 241.8, - 244.4, - 243.1, - 238.5, - 239.1, - 239.7, - 239.8, - 242.1, - 247.8, - 245.0, - 251.2, - 264.4, - 268.3, - 269.9, - 232.1, - 250.7, - 244.8, - 242.9, - 241.2, - 237.9, - 239.4, - 246.4, - 245.0, - 249.2, - 248.3, - 247.0, - 251.1, - 263.6, - ], - [ - 267.8, - 268.5, - 268.3, - 269.1, - 269.9, - 268.6, - 266.7, - 265.9, - 264.3, - 262.9, - 255.5, - 237.1, - 230.7, - 236.7, - 238.3, - 235.6, - 234.2, - 235.0, - 234.6, - 237.0, - 240.1, - 234.9, - 234.3, - 231.2, - 225.1, - 227.0, - 228.0, - 227.5, - 227.0, - 226.7, - 228.7, - 237.0, - 234.4, - 235.6, - 236.8, - 236.5, - 235.3, - 235.5, - 236.0, - 236.8, - 237.3, - 237.7, - 237.5, - 237.0, - 237.2, - 237.3, - 237.9, - 238.6, - 238.3, - 238.3, - 239.5, - 239.2, - 238.4, - 240.3, - 238.0, - 237.5, - 238.4, - 239.1, - 239.2, - 239.7, - 240.3, - 241.0, - 242.1, - 241.8, - 241.6, - 240.7, - 240.6, - 241.3, - 242.6, - 243.3, - 242.9, - 242.6, - 236.4, - 238.7, - 241.0, - 250.4, - 251.0, - 253.4, - 262.5, - 268.7, - 268.8, - 231.8, - 243.0, - 241.4, - 239.4, - 238.4, - 234.8, - 235.0, - 237.2, - 241.8, - 242.2, - 247.0, - 246.7, - 247.1, - 248.8, - 257.9, - ], - [ - 266.3, - 268.4, - 267.8, - 267.9, - 267.3, - 266.4, - 265.5, - 264.8, - 262.8, - 262.1, - 259.4, - 245.0, - 231.9, - 235.1, - 237.9, - 236.7, - 235.8, - 235.2, - 235.2, - 236.9, - 236.9, - 234.1, - 232.7, - 231.0, - 231.8, - 233.7, - 234.0, - 225.9, - 227.3, - 236.4, - 237.8, - 239.8, - 236.5, - 236.2, - 236.3, - 236.5, - 236.3, - 237.0, - 237.4, - 237.8, - 238.1, - 237.6, - 236.9, - 236.0, - 235.8, - 236.1, - 236.8, - 237.4, - 237.5, - 237.2, - 237.3, - 237.1, - 237.1, - 237.9, - 237.9, - 238.0, - 238.1, - 237.9, - 237.4, - 237.1, - 237.5, - 238.2, - 238.8, - 239.8, - 240.2, - 240.6, - 240.6, - 240.6, - 240.6, - 241.0, - 242.2, - 243.4, - 236.8, - 239.2, - 240.8, - 241.8, - 249.6, - 254.1, - 240.6, - 239.5, - 239.6, - 236.0, - 232.0, - 229.8, - 229.2, - 228.7, - 227.9, - 229.8, - 234.4, - 238.3, - 241.0, - 250.1, - 247.6, - 247.8, - 249.5, - 254.9, - ], - [ - 266.7, - 266.9, - 265.6, - 265.9, - 265.2, - 264.6, - 264.6, - 262.5, - 250.8, - 241.4, - 240.2, - 239.9, - 236.9, - 238.9, - 240.3, - 238.4, - 236.9, - 236.5, - 234.4, - 233.6, - 234.0, - 234.2, - 233.2, - 233.4, - 235.1, - 233.9, - 231.8, - 232.0, - 232.3, - 234.2, - 233.9, - 235.5, - 235.1, - 235.7, - 236.7, - 237.4, - 237.2, - 237.1, - 237.0, - 236.9, - 237.4, - 237.5, - 237.4, - 236.3, - 235.6, - 235.6, - 235.7, - 235.6, - 235.7, - 235.7, - 235.7, - 235.6, - 235.7, - 235.7, - 235.5, - 235.5, - 235.5, - 235.4, - 235.2, - 235.1, - 235.5, - 236.5, - 237.0, - 237.1, - 237.2, - 237.3, - 237.3, - 237.2, - 237.6, - 239.0, - 241.1, - 246.3, - 232.2, - 234.5, - 234.2, - 231.2, - 231.1, - 233.8, - 236.8, - 238.4, - 236.6, - 236.1, - 234.8, - 235.3, - 233.6, - 233.4, - 235.0, - 237.4, - 239.0, - 239.4, - 240.2, - 241.5, - 242.2, - 244.1, - 247.4, - 257.0, - ], - [ - 247.2, - 247.8, - 250.1, - 246.0, - 246.8, - 247.5, - 245.6, - 243.0, - 238.0, - 235.9, - 237.4, - 238.7, - 238.8, - 239.1, - 240.2, - 236.9, - 235.1, - 236.5, - 234.2, - 231.7, - 231.0, - 231.8, - 232.2, - 232.4, - 235.5, - 235.5, - 236.2, - 235.3, - 234.9, - 235.0, - 235.4, - 236.6, - 236.9, - 237.6, - 238.0, - 238.2, - 238.0, - 238.2, - 238.3, - 238.1, - 238.4, - 238.2, - 238.0, - 237.4, - 236.9, - 237.3, - 236.8, - 236.2, - 236.2, - 236.3, - 236.1, - 235.6, - 235.2, - 234.8, - 234.3, - 233.9, - 233.5, - 233.2, - 232.8, - 232.6, - 233.1, - 234.0, - 234.2, - 234.4, - 234.2, - 234.0, - 233.9, - 233.9, - 233.8, - 234.4, - 235.7, - 238.1, - 235.7, - 235.8, - 240.5, - 224.1, - 225.3, - 227.2, - 232.5, - 240.2, - 239.2, - 238.6, - 239.2, - 239.4, - 235.6, - 235.0, - 233.9, - 235.2, - 235.9, - 236.5, - 239.9, - 238.9, - 238.5, - 239.7, - 242.2, - 245.1, - ], - [ - 237.3, - 237.3, - 238.2, - 237.7, - 238.5, - 239.3, - 238.2, - 236.9, - 235.5, - 236.1, - 237.4, - 237.9, - 237.4, - 237.0, - 237.1, - 236.0, - 235.7, - 236.5, - 236.7, - 236.4, - 235.3, - 234.4, - 234.8, - 234.9, - 235.4, - 235.8, - 236.0, - 235.9, - 236.1, - 236.4, - 236.9, - 237.4, - 237.9, - 238.3, - 238.7, - 238.8, - 238.8, - 238.6, - 238.6, - 238.6, - 238.5, - 238.4, - 238.4, - 238.2, - 238.0, - 238.1, - 237.6, - 236.9, - 236.6, - 236.4, - 236.1, - 235.7, - 235.5, - 235.2, - 234.8, - 234.4, - 234.1, - 233.5, - 232.7, - 231.8, - 231.6, - 232.0, - 232.2, - 232.9, - 233.0, - 232.7, - 232.3, - 232.0, - 231.9, - 232.1, - 232.4, - 233.0, - 233.1, - 234.2, - 236.6, - 232.8, - 232.1, - 232.7, - 233.6, - 236.6, - 235.3, - 235.1, - 236.3, - 234.4, - 234.0, - 234.8, - 235.8, - 237.1, - 238.1, - 239.5, - 240.2, - 240.6, - 239.4, - 238.3, - 238.6, - 238.1, - ], - [ - 231.9, - 231.5, - 231.7, - 231.8, - 232.1, - 232.1, - 231.8, - 231.6, - 231.6, - 232.3, - 233.4, - 233.9, - 233.7, - 233.5, - 233.6, - 234.0, - 235.2, - 236.4, - 237.1, - 237.5, - 237.2, - 237.2, - 237.4, - 237.6, - 237.9, - 238.2, - 238.5, - 238.7, - 238.8, - 238.8, - 238.9, - 239.0, - 239.1, - 239.1, - 239.1, - 239.0, - 238.9, - 239.0, - 238.5, - 238.3, - 238.1, - 238.1, - 238.2, - 238.2, - 238.1, - 238.0, - 237.9, - 237.8, - 237.6, - 237.3, - 237.1, - 237.2, - 237.1, - 236.9, - 236.6, - 236.4, - 236.2, - 235.6, - 235.0, - 234.7, - 234.3, - 233.6, - 233.3, - 233.8, - 235.0, - 235.9, - 235.3, - 234.1, - 234.0, - 234.1, - 233.5, - 233.1, - 233.1, - 233.7, - 234.0, - 232.8, - 232.8, - 233.1, - 233.4, - 233.7, - 233.0, - 232.6, - 232.8, - 233.1, - 233.6, - 234.3, - 234.8, - 235.2, - 235.4, - 235.3, - 235.0, - 234.6, - 234.0, - 233.5, - 233.5, - 232.7, - ], - [ - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - 235.4, - ], - ] - ] +def _make_ugrid_2(filename): + """Create a UGRID file with a 2-d mesh topology.""" + n = netCDF4.Dataset(filename, "w") -def _make_cfa_file(filename): - n = netCDF4.Dataset(filename, "w", format="NETCDF4") + n.Conventions = f"CF-{VN}" - n.Conventions = f"CF-{VN} CFA-0.6.2" - n.comment = ( - "A CFA-netCDF file with non-standarised aggregation instructions" + n.createDimension("time", 2) + n.createDimension("nMesh2_node", 7) + n.createDimension("nMesh2_edge", 9) + n.createDimension("nMesh2_face", 3) + n.createDimension("Two", 2) + n.createDimension("Four", 4) + + Mesh2 = n.createVariable("Mesh2", "i4", ()) + Mesh2.cf_role = "mesh_topology" + Mesh2.topology_dimension = 2 + Mesh2.node_coordinates = "Mesh2_node_x Mesh2_node_y" + Mesh2.face_node_connectivity = "Mesh2_face_nodes" + Mesh2.edge_node_connectivity = "Mesh2_edge_nodes" + Mesh2.face_dimension = "nMesh2_face" + Mesh2.edge_dimension = "nMesh2_edge" + Mesh2.face_face_connectivity = "Mesh2_face_links" + + Mesh2_face_nodes = n.createVariable( + "Mesh2_face_nodes", "i4", ("Four", "nMesh2_face"), fill_value=-99 ) + Mesh2_face_nodes.long_name = "Maps every face to its corner nodes" + Mesh2_face_nodes[...] = [[2, 4, 1], [3, 5, 3], [1, 3, 6], [0, 2, -99]] - n.createDimension("time", 12) - level = n.createDimension("level", 1) - lat = n.createDimension("lat", 73) - lon = n.createDimension("lon", 144) - n.createDimension("f_time", 2) - n.createDimension("f_level", 1) - n.createDimension("f_lat", 1) - n.createDimension("f_lon", 1) - n.createDimension("i", 4) - n.createDimension("j", 2) - - lon = n.createVariable("lon", "f4", ("lon",)) - lon.standard_name = "longitude" - lon.units = "degrees_east" - - lat = n.createVariable("lat", "f4", ("lat",)) - lat.standard_name = "latitude" - lat.units = "degrees_north" - - time = n.createVariable("time", "f4", ("time",)) - time.standard_name = "time" - time.units = "days since 2000-01-01" - - level = n.createVariable("level", "f4", ("level",)) + Mesh2_edge_nodes = n.createVariable( + "Mesh2_edge_nodes", "i4", ("nMesh2_edge", "Two") + ) + Mesh2_edge_nodes.long_name = "Maps every edge to its two nodes" + Mesh2_edge_nodes[...] = [ + [1, 6], + [3, 6], + [3, 1], + [0, 1], + [2, 0], + [2, 3], + [2, 4], + [5, 4], + [3, 5], + ] - tas = n.createVariable("tas", "f4", ()) - tas.standard_name = "air_temperature" - tas.units = "K" - tas.aggregated_dimensions = "time level lat lon" - tas.aggregated_data = "location: aggregation_location file: aggregation_file format: aggregation_format address: aggregation_address tracking_id: aggregation_tracking_id" + # Mesh node coordinates + Mesh2_node_x = n.createVariable("Mesh2_node_x", "f4", ("nMesh2_node",)) + Mesh2_node_x.standard_name = "longitude" + Mesh2_node_x.units = "degrees_east" + Mesh2_node_x[...] = [-45, -43, -45, -43, -45, -43, -40] - loc = n.createVariable("aggregation_location", "i4", ("i", "j")) - loc[0, :] = 6 - loc[1, 0] = level.size - loc[2, 0] = lat.size - loc[3, 0] = lon.size + Mesh2_node_y = n.createVariable("Mesh2_node_y", "f4", ("nMesh2_node",)) + Mesh2_node_y.standard_name = "latitude" + Mesh2_node_y.units = "degrees_north" + Mesh2_node_y[...] = [35, 35, 33, 33, 31, 31, 34] - fil = n.createVariable( - "aggregation_file", str, ("f_time", "f_level", "f_lat", "f_lon") + # Optional mesh topology variables + Mesh2_face_links = n.createVariable( + "Mesh2_face_links", "i4", ("Four", "nMesh2_face"), fill_value=-99 ) - fil[0, 0, 0, 0] = "January-June.nc" - fil[1, 0, 0, 0] = "July-December.nc" + Mesh2_face_links.long_name = "neighbour faces for faces" + Mesh2_face_links[...] = [ + [1, 0, 0], + [2, -99, -99], + [-99, -99, -99], + [-99, -99, -99], + ] - add = n.createVariable( - "aggregation_address", str, ("f_time", "f_level", "f_lat", "f_lon") - ) - add[0, 0, 0, 0] = "tas0" - add[1, 0, 0, 0] = "tas1" + # Non-mesh coordinates + t = n.createVariable("time", "f8", ("time",)) + t.standard_name = "time" + t.units = "seconds since 2016-01-01 00:00:00" + t.bounds = "time_bounds" + t[...] = [43200, 129600] - fmt = n.createVariable("aggregation_format", str, ()) - fmt[()] = "nc" + t_bounds = n.createVariable("time_bounds", "f8", ("time", "Two")) + t_bounds[...] = [[0, 86400], [86400, 172800]] - tid = n.createVariable( - "aggregation_tracking_id", str, ("f_time", "f_level", "f_lat", "f_lon") - ) - tid[0, 0, 0, 0] = "tracking_id0" - tid[1, 0, 0, 0] = "tracking_id1" + # Data variables + ta = n.createVariable("ta", "f4", ("time", "nMesh2_face")) + ta.standard_name = "air_temperature" + ta.units = "K" + ta.mesh = "Mesh2" + ta.location = "face" + ta[...] = [[282.96, 282.69, 283.21], [281.53, 280.99, 281.23]] + + v = n.createVariable("v", "f4", ("time", "nMesh2_edge")) + v.standard_name = "northward_wind" + v.units = "ms-1" + v.mesh = "Mesh2" + v.location = "edge" + v[...] = [ + [10.2, 10.63, 8.74, 9.05, 8.15, 10.89, 8.44, 10.66, 8.93], + [9.66, 10.74, 9.24, 10.58, 9.79, 10.27, 10.58, 11.68, 11.22], + ] - n.close() + pa = n.createVariable("pa", "f4", ("time", "nMesh2_node")) + pa.standard_name = "air_pressure" + pa.units = "hPa" + pa.mesh = "Mesh2" + pa.location = "node" + pa[...] = [ + [999.67, 1006.45, 999.85, 1006.55, 1006.14, 1005.68, 999.48], + [ + 1003.48, + 1006.42, + 1000.83, + 1002.98, + 1008.28, + 1002.97, + 1002.47, + ], + ] + n.close() return filename @@ -15632,21 +2244,16 @@ def _make_cfa_file(filename): "geometry_interior_ring_2.nc" ) -gathered = _make_gathered_file("gathered.nc") - string_char_file = _make_string_char_file("string_char.nc") -broken_bounds_file = _make_broken_bounds_cdl("broken_bounds.cdl") - subsampled_file_1 = _make_subsampled_1("subsampled_1.nc") subsampled_file_1 = _make_subsampled_2("subsampled_2.nc") -regrid_file = _make_regrid_file("regrid.nc") - -cfa_file = _make_cfa_file("cfa.nc") +ugrid_1 = _make_ugrid_1("ugrid_1.nc") +ugrid_2 = _make_ugrid_2("ugrid_2.nc") if __name__ == "__main__": print("Run date:", datetime.datetime.now()) - cf.environment() + cfdm.environment() print() unittest.main(verbosity=2) diff --git a/cf/test/create_test_files_2.npz b/cf/test/create_test_files_2.npz new file mode 100644 index 0000000000..86adb54b23 Binary files /dev/null and b/cf/test/create_test_files_2.npz differ diff --git a/cf/test/create_test_files_2.py b/cf/test/create_test_files_2.py new file mode 100644 index 0000000000..1433e6c6ab --- /dev/null +++ b/cf/test/create_test_files_2.py @@ -0,0 +1,285 @@ +import datetime +import faulthandler +import os +import unittest + +import numpy as np + +faulthandler.enable() # to debug seg faults and timeouts + +import netCDF4 + +import cf + +VN = cf.CF() + +# Load large arrays +filename = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "create_test_files_2.npz" +) +arrays = np.load(filename) + + +def _make_broken_bounds_cdl(filename): + with open(filename, mode="w") as f: + f.write( + """netcdf broken_bounds { +dimensions: + lat = 180 ; + bnds = 2 ; + lon = 288 ; + time = UNLIMITED ; // (1825 currently) +variables: + double lat(lat) ; + lat:long_name = "latitude" ; + lat:units = "degrees_north" ; + lat:axis = "Y" ; + lat:bounds = "lat_bnds" ; + lat:standard_name = "latitude" ; + lat:cell_methods = "time: point" ; + double lat_bnds(lat, bnds) ; + lat_bnds:long_name = "latitude bounds" ; + lat_bnds:units = "degrees_north" ; + lat_bnds:axis = "Y" ; + double lon(lon) ; + lon:long_name = "longitude" ; + lon:units = "degrees_east" ; + lon:axis = "X" ; + lon:bounds = "lon_bnds" ; + lon:standard_name = "longitude" ; + lon:cell_methods = "time: point" ; + double lon_bnds(lon, bnds) ; + lon_bnds:long_name = "longitude bounds" ; + lon_bnds:units = "m" ; + lon_bnds:axis = "X" ; + float pr(time, lat, lon) ; + pr:long_name = "Precipitation" ; + pr:units = "kg m-2 s-1" ; + pr:missing_value = 1.e+20f ; + pr:_FillValue = 1.e+20f ; + pr:cell_methods = "area: time: mean" ; + pr:cell_measures = "area: areacella" ; + pr:standard_name = "precipitation_flux" ; + pr:interp_method = "conserve_order1" ; + pr:original_name = "pr" ; + double time(time) ; + time:long_name = "time" ; + time:units = "days since 1850-01-01 00:00:00" ; + time:axis = "T" ; + time:calendar_type = "noleap" ; + time:calendar = "noleap" ; + time:bounds = "time_bnds" ; + time:standard_name = "time" ; + time:description = "Temporal mean" ; + double time_bnds(time, bnds) ; + time_bnds:long_name = "time axis boundaries" ; + time_bnds:units = "days since 1850-01-01 00:00:00" ; + +// global attributes: + :external_variables = "areacella" ; + :Conventions = "CF-""" + + VN + + """" ; + :source = "model" ; + :comment = "Bounds variable has incompatible units to its parent coordinate variable" ; +} +""" + ) + + +def _make_regrid_file(filename): + n = netCDF4.Dataset(filename, "w", format="NETCDF3_CLASSIC") + + n.Conventions = "CF-" + VN + + n.createDimension("time", 2) + n.createDimension("bounds2", 2) + n.createDimension("latitude", 30) + n.createDimension("longitude", 48) + n.createDimension("time_1", 1) + n.createDimension("lat", 73) + n.createDimension("lon", 96) + + latitude = n.createVariable("latitude", "f8", ("latitude",)) + latitude.standard_name = "latitude" + latitude.units = "degrees_north" + latitude.bounds = "latitude_bounds" + latitude[...] = np.arange(-87, 90.0, 6) + + longitude = n.createVariable("longitude", "f8", ("longitude",)) + longitude.standard_name = "longitude" + longitude.units = "degrees_east" + longitude.bounds = "longitude_bounds" + longitude[...] = np.arange(3.75, 360, 7.5) + + lat = n.createVariable("lat", "f8", ("lat",)) + lat.standard_name = "latitude" + lat.units = "degrees_north" + lat.bounds = "lat_bounds" + lat[...] = np.arange(-90, 91.0, 2.5) + + lon = n.createVariable("lon", "f8", ("lon",)) + lon.standard_name = "longitude" + lon.units = "degrees_east" + lon.bounds = "lon_bounds" + lon[...] = np.arange(3.75, 361, 3.75) + + longitude_bounds = n.createVariable( + "longitude_bounds", "f8", ("longitude", "bounds2") + ) + longitude_bounds[..., 0] = longitude[...] - 3.75 + longitude_bounds[..., 1] = longitude[...] + 3.75 + + latitude_bounds = n.createVariable( + "latitude_bounds", "f8", ("latitude", "bounds2") + ) + latitude_bounds[..., 0] = latitude[...] - 3 + latitude_bounds[..., 1] = latitude[...] + 3 + + lon_bounds = n.createVariable("lon_bounds", "f8", ("lon", "bounds2")) + lon_bounds[..., 0] = lon[...] - 1.875 + lon_bounds[..., 1] = lon[...] + 1.875 + + lat_bounds = n.createVariable("lat_bounds", "f8", ("lat", "bounds2")) + lat_bounds[..., 0] = lat[...] - 1.25 + lat_bounds[..., 1] = lat[...] + 1.25 + + time = n.createVariable("time", "f4", ("time",)) + time.standard_name = "time" + time.units = "days since 1860-1-1" + time.calendar = "360_day" + time.axis = "T" + time.bounds = "time_bounds" + time[...] = [15, 45] + + time_bounds = n.createVariable("time_bounds", "f4", ("time", "bounds2")) + time_bounds[...] = [ + [ + 0, + 30, + ], + [30, 60], + ] + + time_1 = n.createVariable("time_1", "f4", ("time_1",)) + time_1.standard_name = "time" + time_1.units = "days since 1860-1-1" + time_1.calendar = "360_day" + time_1.axis = "T" + time_1.bounds = "time_1_bounds" + time_1[...] = 15 + + time_1_bounds = n.createVariable( + "time_1_bounds", "f4", ("time_1", "bounds2") + ) + time_1_bounds[...] = [0, 30] + + height = n.createVariable("height", "f8", ()) + height.units = "m" + height.standard_name = "height" + height.positive = "up" + height.axis = "Z" + height[...] = 2 + + src = n.createVariable("src", "f8", ("time", "latitude", "longitude")) + src.standard_name = "air_temperature" + src.units = "K" + src.coordinates = "height" + src.cell_methods = "time: mean" + + # Don't generate this data randomly - it's useful to see the real + # patterns of global temperature. + src[...] = arrays["src"] + + dst = n.createVariable("dst", "f4", ("time_1", "lat", "lon")) + dst.standard_name = "air_temperature" + dst.units = "K" + dst.cell_methods = "time_1: mean" + + # Don't generate this data randomly - it's useful to see the real + # patterns of global temperature. + dst[...] = arrays["dst"] + + +def _make_cfa_file(filename): + n = netCDF4.Dataset(filename, "w", format="NETCDF4") + + n.Conventions = f"CF-{VN} CFA-0.6.2" + n.comment = ( + "A CFA-netCDF file with non-standarised aggregation instructions" + ) + + n.createDimension("time", 12) + level = n.createDimension("level", 1) + lat = n.createDimension("lat", 73) + lon = n.createDimension("lon", 144) + n.createDimension("f_time", 2) + n.createDimension("f_level", 1) + n.createDimension("f_lat", 1) + n.createDimension("f_lon", 1) + n.createDimension("i", 4) + n.createDimension("j", 2) + + lon = n.createVariable("lon", "f4", ("lon",)) + lon.standard_name = "longitude" + lon.units = "degrees_east" + + lat = n.createVariable("lat", "f4", ("lat",)) + lat.standard_name = "latitude" + lat.units = "degrees_north" + + time = n.createVariable("time", "f4", ("time",)) + time.standard_name = "time" + time.units = "days since 2000-01-01" + + level = n.createVariable("level", "f4", ("level",)) + + tas = n.createVariable("tas", "f4", ()) + tas.standard_name = "air_temperature" + tas.units = "K" + tas.aggregated_dimensions = "time level lat lon" + tas.aggregated_data = "location: aggregation_location file: aggregation_file format: aggregation_format address: aggregation_address tracking_id: aggregation_tracking_id" + + loc = n.createVariable("aggregation_location", "i4", ("i", "j")) + loc[0, :] = 6 + loc[1, 0] = level.size + loc[2, 0] = lat.size + loc[3, 0] = lon.size + + fil = n.createVariable( + "aggregation_file", str, ("f_time", "f_level", "f_lat", "f_lon") + ) + fil[0, 0, 0, 0] = "January-June.nc" + fil[1, 0, 0, 0] = "July-December.nc" + + add = n.createVariable( + "aggregation_address", str, ("f_time", "f_level", "f_lat", "f_lon") + ) + add[0, 0, 0, 0] = "tas0" + add[1, 0, 0, 0] = "tas1" + + fmt = n.createVariable("aggregation_format", str, ()) + fmt[()] = "nc" + + tid = n.createVariable( + "aggregation_tracking_id", str, ("f_time", "f_level", "f_lat", "f_lon") + ) + tid[0, 0, 0, 0] = "tracking_id0" + tid[1, 0, 0, 0] = "tracking_id1" + + n.close() + + return filename + + +broken_bounds_file = _make_broken_bounds_cdl("broken_bounds.cdl") + +regrid_file = _make_regrid_file("regrid.nc") + +cfa_file = _make_cfa_file("cfa.nc") + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print() + unittest.main(verbosity=2) diff --git a/cf/test/individual_tests.sh b/cf/test/individual_tests.sh index f02a941197..fea95ef58b 100755 --- a/cf/test/individual_tests.sh +++ b/cf/test/individual_tests.sh @@ -1,12 +1,14 @@ #!/bin/bash -file=create_test_files.py -echo "Running $file" -python $file -rc=$? -if [[ $rc != 0 ]]; then - exit $rc -fi +for file in create_test_files*.py +do + echo "Running $file" + python $file + rc=$? + if [[ $rc != 0 ]]; then + exit $rc + fi +done file=setup_create_field.py echo "Running $file" diff --git a/cf/test/run_tests.py b/cf/test/run_tests.py index c154fa0317..6026c4e123 100644 --- a/cf/test/run_tests.py +++ b/cf/test/run_tests.py @@ -64,6 +64,10 @@ def add_doctests(test_suite): test_loader().discover(test_dir, pattern="create_test_files.py") ) +testsuite_setup_0b = unittest.TestSuite() +testsuite_setup_0b.addTests( + test_loader().discover(test_dir, pattern="create_test_files_2.py") +) # Build the test suite from the tests found in the test files. testsuite_setup_1 = unittest.TestSuite() testsuite_setup_1.addTests( @@ -83,6 +87,7 @@ def add_doctests(test_suite): def run_test_suite_setup_0(verbosity=2): runner = unittest.TextTestRunner(verbosity=verbosity) runner.run(testsuite_setup_0) + runner.run(testsuite_setup_0b) def run_doctests_only(verbosity=2): diff --git a/cf/test/test_CellConnectivity.py b/cf/test/test_CellConnectivity.py new file mode 100644 index 0000000000..61515b9605 --- /dev/null +++ b/cf/test/test_CellConnectivity.py @@ -0,0 +1,104 @@ +import datetime +import faulthandler +import unittest + +faulthandler.enable() # to debug seg faults and timeouts + +import cf + +# Create testcell connectivity object +c = cf.CellConnectivity() +c.set_properties({"long_name": "neighbour faces for faces"}) +c.nc_set_variable("Mesh2_face_links") +data = cf.Data( + [ + [0, 1, 2, -99, -99], + [1, 0, -99, -99, -99], + [2, 0, -99, -99, -99], + ], + dtype="i4", +) +data.masked_values(-99, inplace=True) +c.set_data(data) +c.set_connectivity("edge") + + +class CellConnectivityTest(unittest.TestCase): + """Unit test for the CellConnectivity class.""" + + c = c + + def setUp(self): + """Preparations called immediately before each test method.""" + # Disable log messages to silence expected warnings + cf.log_level("DISABLE") + # Note: to enable all messages for given methods, lines or + # calls (those without a 'verbose' option to do the same) + # e.g. to debug them, wrap them (for methods, start-to-end + # internally) as follows: + # + # cf.LOG_LEVEL('DEBUG') + # < ... test code ... > + # cf.log_level('DISABLE') + + def test_CellConnectivity__repr__str__dump(self): + """Test all means of CellConnectivity inspection.""" + c = self.c + self.assertEqual( + repr(c), "" + ) + self.assertEqual(str(c), "connectivity:edge(3, 5) ") + self.assertEqual( + c.dump(display=False), + """Cell Connectivity: connectivity:edge + long_name = 'neighbour faces for faces' + Data(3, 5) = [[0, ..., --]]""", + ) + + def test_CellConnectivity_copy(self): + """Test the copy of CellConnectivity.""" + c = self.c + self.assertTrue(c.equals(c.copy())) + + def test_CellConnectivity_data(self): + """Test the data of CellConnectivity.""" + c = self.c + self.assertEqual(c.ndim, 1) + + def test_CellConnectivity_connectivity(self): + """Test the 'connectivity' methods of CellConnectivity.""" + c = self.c.copy() + self.assertTrue(c.has_connectivity()) + self.assertEqual(c.get_connectivity(), "edge") + self.assertEqual(c.del_connectivity(), "edge") + self.assertFalse(c.has_connectivity()) + self.assertIsNone(c.get_connectivity(None)) + self.assertIsNone(c.del_connectivity(None)) + + with self.assertRaises(ValueError): + c.get_connectivity() + + with self.assertRaises(ValueError): + c.del_connectivity() + + self.assertIsNone(c.set_connectivity("edge")) + self.assertTrue(c.has_connectivity()) + self.assertEqual(c.get_connectivity(), "edge") + + def test_CellConnectivity_transpose(self): + """Test the 'transpose' method of CellConnectivity.""" + c = self.c.copy() + d = c.transpose() + self.assertTrue(c.equals(d)) + self.assertIsNone(c.transpose(inplace=True)) + + for axes in ([1], [1, 0], [3]): + with self.assertRaises(ValueError): + c.transpose(axes) + + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print() + unittest.main(verbosity=2) diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py index bf269a789c..2d439ba695 100644 --- a/cf/test/test_Data.py +++ b/cf/test/test_Data.py @@ -13,15 +13,7 @@ import dask.array as da import numpy as np - -SCIPY_AVAILABLE = False -try: - from scipy.ndimage import convolve1d - - SCIPY_AVAILABLE = True -# not 'except ImportError' as that can hide nested errors, catch anything: -except Exception: - pass # test with this dependency will then be skipped by unittest +from scipy.ndimage import convolve1d faulthandler.enable() # to debug seg faults and timeouts @@ -638,9 +630,6 @@ def test_Data_apply_masking(self): def test_Data_convolution_filter(self): """Test the `convolution_filter` Data method.""" # raise unittest.SkipTest("GSASL has no PLAIN support") - if not SCIPY_AVAILABLE: - raise unittest.SkipTest("SciPy must be installed for this test.") - d = cf.Data(self.ma, units="m", chunks=(2, 4, 5, 3)) window = [0.1, 0.15, 0.5, 0.15, 0.1] @@ -867,17 +856,6 @@ def test_Data_stats(self): }, ) - # NaN values aren't 'equal' to e/o, so check call works and that some - # representative values are as expected, in this case - s5 = cf.Data([[-2, -1, 0], [1, 2, 3]]).stats(all=True, weights=0) - - self.assertEqual(len(s5), 16) - self.assertEqual(s5["minimum"], -2) - self.assertEqual(s5["sum"], 0) - self.assertEqual(s5["sample_size"], 6) - self.assertTrue(np.isnan(s5["mean"])) - self.assertTrue(np.isnan(s5["variance"])) # needs all=True to show up - def test_Data__init__dtype_mask(self): """Test `__init__` for Data with `dtype` and `mask` keywords.""" for m in (1, 20, True): @@ -2868,7 +2846,6 @@ def test_Data_trigonometric_hyperbolic(self): (d.mask.array == c.mask).all(), "{}, {}, {}, {}".format(method, units, d.array, c), ) - # --- End: for # Also test masking behaviour: masking of invalid data occurs for # np.ma module by default but we don't want that so there is logic @@ -2898,25 +2875,22 @@ def test_Data_trigonometric_hyperbolic(self): self.assertTrue(np.isposinf(g[2])) self.assertIs(g[3], cf.masked) - # AT2 - # - # # Treat arctan2 separately (as is a class method & takes two inputs) - # for x in (1, -1): - # a1 = 0.9 * x * self.ma - # a2 = 0.5 * x * self.a - # # Transform data for 'a' into range more appropriate for inverse: - # a1 = np.sin(a1.data) - # a2 = np.cos(a2.data) - - # c = np.ma.arctan2(a1, a2) - # for units in (None, '', '1', 'radians', 'K'): - # d1 = cf.Data(a1, units=units) - # d2 = cf.Data(a2, units=units) - # e = cf.Data.arctan2(d1, d2) - # # Note: no inplace arg for arctan2 (operates on 2 arrays) - # self.assertEqual(d1.shape, c.shape) - # self.assertTrue((e.array == c).all()) - # self.assertTrue((d1.mask.array == c.mask).all()) + # Treat arctan2 separately (class method taking two inputs) + for x in (1, -1): + a1 = 0.9 * x * self.ma + a2 = 0.5 * x * self.a + # Transform data into range more appropriate for inverse + a1 = np.sin(a1.data) + a2 = np.cos(a2.data) + + c = np.ma.arctan2(a1, a2) + for units in (None, "", "1", "radians", "K"): + d1 = cf.Data(a1, units=units) + d2 = cf.Data(a2, units=units) + e = cf.Data.arctan2(d1, d2) + self.assertEqual(d1.shape, c.shape) + self.assertTrue((e.array == c).all()) + self.assertTrue((d1.mask.array == c.mask).all()) def test_Data_filled(self): """Test the `filled` Data method.""" @@ -4718,6 +4692,45 @@ def test_Data_todict(self): self.assertIn((key, 0), x) self.assertIn((key, 1), x) + def test_Data_masked_values(self): + """Test Data.masked_values.""" + array = np.array([[1, 1.1, 2, 1.1, 3]]) + d = cf.Data(array) + e = d.masked_values(1.1) + ea = e.array + a = np.ma.masked_values(array, 1.1, rtol=cf.rtol(), atol=cf.atol()) + self.assertTrue(np.isclose(ea, a).all()) + self.assertTrue((ea.mask == a.mask).all()) + self.assertIsNone(d.masked_values(1.1, inplace=True)) + self.assertTrue(d.equals(e)) + + array = np.array([[1, 1.1, 2, 1.1, 3]]) + d = cf.Data(array, mask_value=1.1) + da = e.array + self.assertTrue(np.isclose(da, a).all()) + self.assertTrue((da.mask == a.mask).all()) + + def test_Data_sparse_array(self): + """Test Data based on sparse arrays.""" + from scipy.sparse import csr_array + + indptr = np.array([0, 2, 3, 6]) + indices = np.array([0, 2, 2, 0, 1, 2]) + data = np.array([1, 2, 3, 4, 5, 6]) + s = csr_array((data, indices, indptr), shape=(3, 3)) + + d = cf.Data(s) + self.assertFalse((d.sparse_array != s).toarray().any()) + self.assertTrue((d.array == s.toarray()).all()) + + d = cf.Data(s, dtype=float) + self.assertEqual(d.sparse_array.dtype, float) + + # Can't mask sparse array during __init__ + mask = [[0, 0, 1], [0, 0, 0], [0, 0, 0]] + with self.assertRaises(ValueError): + cf.Data(s, mask=mask) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_DomainTopology.py b/cf/test/test_DomainTopology.py new file mode 100644 index 0000000000..d7eb533759 --- /dev/null +++ b/cf/test/test_DomainTopology.py @@ -0,0 +1,98 @@ +import datetime +import faulthandler +import unittest + +faulthandler.enable() # to debug seg faults and timeouts + +import cf + +# Create test domain topology object +c = cf.DomainTopology() +c.set_properties({"long_name": "Maps every face to its corner nodes"}) +c.nc_set_variable("Mesh2_face_nodes") +data = cf.Data( + [[2, 3, 1, 0], [6, 7, 3, 2], [1, 3, 8, -99]], + dtype="i4", +) +data.masked_values(-99, inplace=True) +c.set_data(data) +c.set_cell("face") + + +class DomainTopologyTest(unittest.TestCase): + """Unit test for the DomainTopology class.""" + + d = c + + def setUp(self): + """Preparations called immediately before each test method.""" + # Disable log messages to silence expected warnings + cf.log_level("DISABLE") + # Note: to enable all messages for given methods, lines or + # calls (those without a 'verbose' option to do the same) + # e.g. to debug them, wrap them (for methods, start-to-end + # internally) as follows: + # + # cf.LOG_LEVEL('DEBUG') + # < ... test code ... > + # cf.log_level('DISABLE') + + def test_DomainTopology__repr__str__dump(self): + """Test all means of DomainTopology inspection.""" + d = self.d + self.assertEqual(repr(d), "") + self.assertEqual(str(d), "cell:face(3, 4) ") + self.assertEqual( + d.dump(display=False), + """Domain Topology: cell:face + long_name = 'Maps every face to its corner nodes' + Data(3, 4) = [[2, ..., --]]""", + ) + + def test_DomainTopology_copy(self): + """Test the copy of DomainTopology.""" + d = self.d + self.assertTrue(d.equals(d.copy())) + + def test_DomainTopology_data(self): + """Test the data of DomainTopology.""" + d = self.d + self.assertEqual(d.ndim, 1) + + def test_DomainTopology_cell(self): + """Test the 'cell' methods of DomainTopology.""" + d = self.d.copy() + self.assertTrue(d.has_cell()) + self.assertEqual(d.get_cell(), "face") + self.assertEqual(d.del_cell(), "face") + self.assertFalse(d.has_cell()) + self.assertIsNone(d.get_cell(None)) + self.assertIsNone(d.del_cell(None)) + + with self.assertRaises(ValueError): + d.get_cell() + + with self.assertRaises(ValueError): + d.set_cell("bad value") + + self.assertIsNone(d.set_cell("face")) + self.assertTrue(d.has_cell()) + self.assertEqual(d.get_cell(), "face") + + def test_DomainTopology_transpose(self): + """Test the 'transpose' method of DomainTopology.""" + d = self.d.copy() + e = d.transpose() + self.assertTrue(d.equals(e)) + self.assertIsNone(d.transpose(inplace=True)) + + for axes in ([1], [1, 0], [3]): + with self.assertRaises(ValueError): + d.transpose(axes) + + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print() + unittest.main(verbosity=2) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 945b119277..fb410ae9e8 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -9,21 +9,7 @@ import numpy import numpy as np - -SCIPY_AVAILABLE = False -try: - from scipy.ndimage import convolve1d - - # In some cases we don't need SciPy directly, since it is required by code - # here which uses 'convolve1d' under-the-hood. Without it installed, get: - # - # NameError: name 'convolve1d' is not defined. Did you - # mean: 'cf_convolve1d'? - - SCIPY_AVAILABLE = True -# not 'except ImportError' as that can hide nested errors, catch anything: -except Exception: - pass # test with this dependency will then be skipped by unittest +from scipy.ndimage import convolve1d faulthandler.enable() # to debug seg faults and timeouts @@ -78,6 +64,10 @@ class FieldTest(unittest.TestCase): os.path.dirname(os.path.abspath(__file__)), "DSG_timeSeriesProfile_indexed_contiguous.nc", ) + ugrid_global = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "ugrid_global_1.nc", + ) chunk_sizes = (100000, 300, 34, 17) original_chunksize = cf.chunksize() @@ -1696,9 +1686,6 @@ def test_Field_autocyclic(self): def test_Field_construct_key(self): self.f.construct_key("grid_longitude") - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_Field_convolution_filter(self): f = cf.read(self.filename1)[0] @@ -1731,9 +1718,6 @@ def test_Field_convolution_filter(self): (gx[:, 1] == [135, 180, 225, 270, 315, 360, 360, 360]).all() ) - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_Field_moving_window(self): weights = cf.Data([1, 2, 3, 10, 5, 6, 7, 8]) / 2 @@ -1874,9 +1858,6 @@ def test_Field_moving_window(self): self.assertEqual(len(g.cell_methods()), len(f.cell_methods()) + 1) - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_Field_derivative(self): f = cf.example_field(0) f[...] = np.arange(9)[1:] * 45 @@ -2336,9 +2317,6 @@ def test_Field_percentile(self): # TODO: add loop to check get same shape and close enough data # for every possible axis combo (see also test_Data_percentile). - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_Field_grad_xy(self): f = cf.example_field(0) @@ -2424,9 +2402,6 @@ def test_Field_grad_xy(self): y.dimension_coordinate("X").standard_name, "longitude" ) - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_Field_laplacian_xy(self): f = cf.example_field(0) @@ -2642,6 +2617,18 @@ def test_Field_auxiliary_to_dimension_to_auxiliary(self): with self.assertRaises(ValueError): f.auxiliary_to_dimension("latitude") + def test_Field_subspace_ugrid(self): + f = cf.read(self.ugrid_global)[0] + + with self.assertRaises(ValueError): + # Can't specify 2 conditions for 1 axis + g = f.subspace(X=cf.wi(40, 70), Y=cf.wi(-20, 30)) + + g = f.subspace(X=cf.wi(40, 70)) + g = g.subspace(Y=cf.wi(-20, 30)) + self.assertTrue(g.aux("X").data.range() < 30) + self.assertTrue(g.aux("Y").data.range() < 50) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_Maths.py b/cf/test/test_Maths.py index 342b1ec0b6..0900692aeb 100644 --- a/cf/test/test_Maths.py +++ b/cf/test/test_Maths.py @@ -2,30 +2,12 @@ import faulthandler import unittest - -SCIPY_AVAILABLE = False -try: - # We don't need SciPy directly in this test, it is only required by code - # here which uses 'convolve1d' under-the-hood. Without it installed, get: - # - # NameError: name 'convolve1d' is not defined. Did you - # mean: 'cf_convolve1d'? - import scipy - - SCIPY_AVAILABLE = True -# not 'except ImportError' as that can hide nested errors, catch anything: -except Exception: - pass # test with this dependency will then be skipped by unittest - faulthandler.enable() # to debug seg faults and timeouts import cf class MathTest(unittest.TestCase): - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_curl_xy(self): f = cf.example_field(0) @@ -115,9 +97,6 @@ def test_curl_xy(self): c.dimension_coordinate("X").standard_name, "longitude" ) - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_div_xy(self): f = cf.example_field(0) @@ -206,9 +185,6 @@ def test_div_xy(self): d.dimension_coordinate("X").standard_name, "longitude" ) - @unittest.skipIf( - not SCIPY_AVAILABLE, "scipy must be installed for this test." - ) def test_differential_operators(self): f = cf.example_field(0) diff --git a/cf/test/test_RegridOperator.py b/cf/test/test_RegridOperator.py index 2c1542240f..b20cb0cfcf 100644 --- a/cf/test/test_RegridOperator.py +++ b/cf/test/test_RegridOperator.py @@ -29,6 +29,8 @@ def test_RegridOperator_attributes(self): self.assertEqual(self.r.weights.ndim, 2) self.assertIsNone(self.r.row) self.assertIsNone(self.r.col) + self.assertIsNone(self.r.weights_file) + self.assertEqual(self.r.src_mesh_location, "") def test_RegridOperator_copy(self): self.assertIsInstance(self.r.copy(), self.r.__class__) diff --git a/cf/test/test_UGRID.py b/cf/test/test_UGRID.py new file mode 100644 index 0000000000..7d80ba3166 --- /dev/null +++ b/cf/test/test_UGRID.py @@ -0,0 +1,189 @@ +import atexit +import datetime +import faulthandler +import os +import tempfile +import unittest + +import numpy as np + +faulthandler.enable() # to debug seg faults and timeouts + +import cf + +warnings = False + +# Set up temporary files +n_tmpfiles = 1 +tmpfiles = [ + tempfile.mkstemp("_test_read_write.nc", dir=os.getcwd())[1] + for i in range(n_tmpfiles) +] +[tmpfile1] = tmpfiles + + +def _remove_tmpfiles(): + """Remove temporary files created during tests.""" + for f in tmpfiles: + try: + os.remove(f) + except OSError: + pass + + +atexit.register(_remove_tmpfiles) + + +class UGRIDTest(unittest.TestCase): + """Test UGRID field constructs.""" + + filename1 = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "ugrid_1.nc" + ) + + filename2 = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "ugrid_2.nc" + ) + + def setUp(self): + """Preparations called immediately before each test method.""" + # Disable log messages to silence expected warnings + cf.LOG_LEVEL("DISABLE") + # Note: to enable all messages for given methods, lines or + # calls (those without a 'verbose' option to do the same) + # e.g. to debug them, wrap them (for methods, start-to-end + # internally) as follows: cf.LOG_LEVEL('DEBUG') + # + # < ... test code ... > + # cf.log_level('DISABLE') + + def test_UGRID_read(self): + """Test reading of UGRID files.""" + f1 = cf.read(self.filename1) + + self.assertEqual(len(f1), 3) + for g in f1: + self.assertEqual(len(g.domain_topologies()), 1) + self.assertEqual(len(g.auxiliary_coordinates()), 2) + self.assertEqual(len(g.dimension_coordinates()), 1) + + for aux in g.auxiliary_coordinates().values(): + self.assertTrue(aux.has_data()) + + if g.domain_topology().get_cell() == "face": + self.assertEqual(len(g.cell_connectivities()), 1) + self.assertEqual( + g.cell_connectivity().get_connectivity(), "edge" + ) + + # Check that all fields have the same mesh id + mesh_ids1 = set(g.get_mesh_id() for g in f1) + self.assertEqual(len(mesh_ids1), 1) + + f2 = cf.read(self.filename2) + self.assertEqual(len(f2), 3) + for g in f2: + self.assertEqual(len(g.domain_topologies()), 1) + self.assertEqual(len(g.auxiliary_coordinates()), 2) + self.assertEqual(len(g.dimension_coordinates()), 1) + + cell = g.domain_topology().get_cell() + if cell in ("edge", "face"): + for aux in g.auxiliary_coordinates().values(): + self.assertFalse(aux.has_data()) + + if cell == "face": + self.assertEqual(len(g.cell_connectivities()), 1) + self.assertEqual( + g.cell_connectivity().get_connectivity(), "edge" + ) + + # Check that all fields have the same mesh id + mesh_ids2 = set(g.get_mesh_id() for g in f2) + self.assertEqual(len(mesh_ids2), 1) + + # Check that the different files have different mesh ids + self.assertNotEqual(mesh_ids1, mesh_ids2) + + def test_UGRID_data(self): + """Test reading of UGRID data.""" + node1, face1, edge1 = cf.read(self.filename1) + node2, face2, edge2 = cf.read(self.filename2) + + # Domain topology arrays + domain_topology1 = face1.domain_topology() + self.assertTrue( + ( + domain_topology1.array + == np.array([[2, 3, 1, 0], [4, 5, 3, 2], [1, 3, 6, -99]]) + ).all() + ) + self.assertTrue(domain_topology1.equals(face2.domain_topology())) + + domain_topology1 = edge1.domain_topology() + self.assertTrue( + ( + domain_topology1.array + == np.array( + [ + [1, 6], + [3, 6], + [3, 1], + [0, 1], + [2, 0], + [2, 3], + [2, 4], + [5, 4], + [3, 5], + ] + ) + ).all() + ) + self.assertTrue(domain_topology1.equals(edge2.domain_topology())) + + # Cell connectivity arrays + cell_connectivity1 = face1.cell_connectivity() + self.assertTrue( + ( + cell_connectivity1.array + == np.array( + [ + [0, 1, 2, -99, -99], + [1, 0, -99, -99, -99], + [2, 0, -99, -99, -99], + ] + ) + ).all() + ) + self.assertTrue(cell_connectivity1.equals(face2.cell_connectivity())) + + def test_read_UGRID_domain(self): + """Test reading of UGRID files into domains.""" + d1 = cf.read(self.filename1, domain=True) + + self.assertEqual(len(d1), 3) + for g in d1: + self.assertIsInstance(g, cf.Domain) + self.assertEqual(len(g.domain_topologies()), 1) + self.assertEqual(len(g.auxiliary_coordinates()), 2) + self.assertEqual(len(g.dimension_coordinates()), 0) + + for aux in g.auxiliary_coordinates().values(): + self.assertTrue(aux.has_data()) + + if g.domain_topology().get_cell() == "face": + self.assertEqual(len(g.cell_connectivities()), 1) + self.assertEqual( + g.cell_connectivity().get_connectivity(), "edge" + ) + + # Check that all domains have the same mesh id + mesh_ids1 = set(g.get_mesh_id() for g in d1) + self.assertEqual(len(mesh_ids1), 1) + + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print("") + unittest.main(verbosity=2) diff --git a/cf/test/test_aggregate.py b/cf/test/test_aggregate.py index cd95ad72eb..784e9e5fe1 100644 --- a/cf/test/test_aggregate.py +++ b/cf/test/test_aggregate.py @@ -57,7 +57,7 @@ def test_basic_aggregate(self): g.equals(g0, verbose=2), "g != itself after aggregation" ) - self.assertTrue(h[0].equals(f, verbose=-1), "h[0] != f") + self.assertTrue(h[0].equals(f, verbose=2), "h[0] != f") with warnings.catch_warnings(): warnings.simplefilter("ignore", category=FutureWarning) @@ -610,6 +610,45 @@ def test_climatology_cells(self): condition = cells["T"][0]["cellsize"] self.assertTrue(condition.equals(cf.Y())) + def test_aggregate_ugrid(self): + """Test ugrid aggregation""" + f = cf.example_field(8) + + # Test that aggregation over a non-ugrid axis (time, in this + # case) works. + g = f.copy() + t = g.dim("T") + cf.bounds_combination_mode("OR") + t += 72000 + a = cf.aggregate([f, g]) + self.assertEqual(len(a), 1) + a = a[0] + self.assertEqual(len(a.domain_topologies()), 1) + self.assertEqual(len(a.cell_connectivities()), 1) + + # Test that aggregation over a non-ugrid axis doesn't work + # when the domain topology constructs are different + h = g.copy() + d = h.domain_topology() + d = d.data + d += 1 + self.assertEqual(len(cf.aggregate([f, h])), 2) + + # Test that aggregation over a non-ugrid axis doesn't work + # when the cell connnectivty constructs are different + h = g.copy() + c = h.cell_connectivity() + d = c.data + d += 1 + self.assertEqual(len(cf.aggregate([f, h])), 2) + + # Test that aggregation over a ugrid axis doesn't work + g = f.copy() + x = g.aux("X") + d = x.data + d += 0.1 + self.assertEqual(len(cf.aggregate([f, g])), 2) + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_collapse.py b/cf/test/test_collapse.py index 6faada8cd5..0525aecddb 100644 --- a/cf/test/test_collapse.py +++ b/cf/test/test_collapse.py @@ -665,16 +665,17 @@ def test_Field_collapse_GROUPS(self): def test_Field_collapse_sum(self): f = cf.example_field(0) - w = f.weights("area", measure=True) + w = f.weights("area", measure=True).persist() a = f.array wa = w.array ws = a * wa + ws_sum = ws.sum() g = f.collapse("area: sum") self.assertTrue((g.array == a.sum()).all()) g = f.collapse("area: sum", weights=w) - self.assertTrue((g.array == ws.sum()).all()) + self.assertTrue((g.array == ws_sum).all()) self.assertEqual(g.Units, cf.Units("1")) g = f.collapse("area: sum", weights=w, scale=1) @@ -682,7 +683,7 @@ def test_Field_collapse_sum(self): self.assertEqual(g.Units, cf.Units("1")) g = f.collapse("area: sum", weights=w) - self.assertTrue((g.array == ws.sum()).all()) + self.assertTrue((g.array == ws_sum).all()) self.assertEqual(g.Units, cf.Units("1")) # Can't set measure=True for 'sum' collapses @@ -691,13 +692,12 @@ def test_Field_collapse_sum(self): def test_Field_collapse_integral(self): f = cf.example_field(0) - w = f.weights("area", measure=True) + w = f.weights("area", measure=True).persist() a = f.array wa = w.array - ws = a * wa g = f.collapse("area: integral", weights=w, measure=True) - self.assertTrue((g.array == ws.sum()).all()) + self.assertTrue((g.array == (a * wa).sum()).all()) self.assertEqual(g.Units, cf.Units("m2")) # Must set the 'weights' parameter for 'integral' collapses @@ -714,7 +714,7 @@ def test_Field_collapse_integral(self): def test_Field_collapse_sum_weights(self): f = cf.example_field(0) - w = f.weights("area", measure=True) + w = f.weights("area", measure=True).persist() wa = w.array g = f.collapse("area: sum_of_weights") @@ -735,25 +735,45 @@ def test_Field_collapse_sum_weights(self): def test_Field_collapse_sum_weights2(self): f = cf.example_field(0) - w = f.weights("area", measure=True) + w = f.weights("area", measure=True).persist() wa = w.array**2 + wa_sum = wa.sum() g = f.collapse("area: sum_of_weights2") self.assertTrue((g.array == 40).all()) self.assertEqual(g.Units, cf.Units()) g = f.collapse("area: sum_of_weights2", weights=w) - self.assertTrue((g.array == wa.sum()).all()) + self.assertTrue((g.array == wa_sum).all()) self.assertEqual(g.Units, cf.Units("1")) g = f.collapse("area: sum_of_weights2", weights=w, measure=True) - self.assertTrue((g.array == wa.sum()).all()) + self.assertTrue((g.array == wa_sum).all()) self.assertEqual(g.Units, cf.Units("m4")) g = f.collapse("area: sum_of_weights2", weights=w, scale=1) self.assertTrue((g.array == (wa / wa.max()).sum()).all()) self.assertEqual(g.Units, cf.Units("1")) + def test_Field_collapse_non_positive_weights(self): + f = cf.example_field(0) + w = f.weights("area").persist() + + for method in ( + "mean", + "sum", + "root_mean_square", + "variance", + "sum_of_weights", + ): + for x in (0, -3.14): + w[0, 0] = x + g = f.collapse(axes="area", method=method, weights=w) + with self.assertRaises(ValueError): + # The check for non-positive weights occurs at + # compute time + g.array + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_external.py b/cf/test/test_external.py index dabd4d01a7..8d7489f78e 100644 --- a/cf/test/test_external.py +++ b/cf/test/test_external.py @@ -173,7 +173,7 @@ def test_EXTERNAL_AGGREGATE(self): # cell measure. f_lon_thirds = [f[:, :3], f[:, 3:6], f[:, 6:]] - g = cf.aggregate(f_lon_thirds) + g = cf.aggregate(f_lon_thirds, verbose=2) self.assertEqual(len(g), 1) diff --git a/cf/test/test_gathering.py b/cf/test/test_gathering.py index a26350dd18..bafa8efeda 100644 --- a/cf/test/test_gathering.py +++ b/cf/test/test_gathering.py @@ -31,7 +31,7 @@ def _remove_tmpfiles(): atexit.register(_remove_tmpfiles) -class DSGTest(unittest.TestCase): +class GatheringTest(unittest.TestCase): gathered = os.path.join( os.path.dirname(os.path.abspath(__file__)), "gathered.nc" ) diff --git a/cf/test/test_read_write.py b/cf/test/test_read_write.py index e4da245eab..0eefa1b2ac 100644 --- a/cf/test/test_read_write.py +++ b/cf/test/test_read_write.py @@ -336,6 +336,12 @@ def test_write_netcdf_mode(self): if fmt == "NETCDF4_CLASSIC" and ex_field_n in (6, 7): continue + print( + "TODOUGRID: excluding example fields 8, 9, 10 until writing UGRID is enabled" + ) + if ex_field_n in (8, 9, 10): + continue + cf.write(ex_field, tmpfile, fmt=fmt, mode="a") f = cf.read(tmpfile) @@ -404,7 +410,10 @@ def test_write_netcdf_mode(self): # Now do the same test, but appending all of the example fields in # one operation rather than one at a time, to check that it works. cf.write(g, tmpfile, fmt=fmt, mode="w") # 1. overwrite to wipe - append_ex_fields = cf.example_fields() + print( + "TODOUGRID: excluding example fields 8, 9, 10 until writing UGRID is enabled" + ) + append_ex_fields = cf.example_fields(0, 1, 2, 3, 4, 5, 6, 7) del append_ex_fields[1] # note: can remove after Issue #141 closed if fmt in "NETCDF4_CLASSIC": # Remove n=6 and =7 for reasons as given above (del => minus 1) @@ -663,6 +672,7 @@ def test_read_CDL(self): geometry_1_file = os.path.join( os.path.dirname(os.path.abspath(__file__)), "geometry_1.nc" ) + subprocess.run( " ".join(["ncdump", "-h", geometry_1_file, ">", tmpfileh2]), shell=True, diff --git a/cf/test/test_regrid_mesh.py b/cf/test/test_regrid_mesh.py new file mode 100644 index 0000000000..3095640135 --- /dev/null +++ b/cf/test/test_regrid_mesh.py @@ -0,0 +1,294 @@ +import datetime +import faulthandler +import os +import unittest + +faulthandler.enable() # to debug seg faults and timeouts + +import numpy as np + +import cf + +# ESMF renamed its Python module to `esmpy` at ESMF version 8.4.0. Allow +# either for now for backwards compatibility. +esmpy_imported = False +try: + import esmpy + + esmpy_imported = True +except ImportError: + try: + # Take the new name to use in preference to the old one. + import ESMF as esmpy + + esmpy_imported = True + except ImportError: + pass + +all_methods = ( + "linear", + "conservative", + "conservative_2nd", + "nearest_dtos", + "nearest_stod", + "patch", +) + + +# Set numerical comparison tolerances +atol = 2e-12 +rtol = 0 + +meshloc = { + "face": esmpy.MeshLoc.ELEMENT, + "node": esmpy.MeshLoc.NODE, +} + + +def esmpy_regrid(coord_sys, method, src, dst, **kwargs): + """Helper function that regrids one dimension of Field data using + pure esmpy. + + Used to verify `cf.Field.regridc` + + :Returns: + + Regridded numpy masked array. + + """ + esmpy_regrid = cf.regrid.regrid( + coord_sys, + src, + dst, + method, + return_esmpy_regrid_operator=True, + **kwargs + ) + + src_meshloc = None + dst_meshloc = None + + domain_topology = src.domain_topology(default=None) + if domain_topology is not None: + src_meshloc = meshloc[domain_topology.get_cell()] + + domain_topology = dst.domain_topology(default=None) + if domain_topology is not None: + dst_meshloc = meshloc[domain_topology.get_cell()] + + src_field = esmpy.Field( + esmpy_regrid.srcfield.grid, meshloc=src_meshloc, name="src" + ) + dst_field = esmpy.Field( + esmpy_regrid.dstfield.grid, meshloc=dst_meshloc, name="dst" + ) + + fill_value = 1e20 + array = np.squeeze(src.array) + if array.shape != src_field.data.shape: + array = array.transpose() + + src_field.data[...] = np.ma.MaskedArray(array, copy=False).filled( + fill_value + ) + dst_field.data[...] = fill_value + + esmpy_regrid(src_field, dst_field, zero_region=esmpy.Region.SELECT) + + out = dst_field.data + + return np.ma.MaskedArray(out.copy(), mask=(out == fill_value)) + + +class RegridMeshTest(unittest.TestCase): + # Get the test source and destination fields + src_mesh_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "ugrid_global_1.nc" + ) + dst_mesh_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "ugrid_global_2.nc" + ) + grid_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "regrid.nc" + ) + + src_mesh = cf.read(src_mesh_file)[0] + dst_mesh = cf.read(dst_mesh_file)[0] + grid = cf.read(grid_file)[0] + + def setUp(self): + """Preparations called immediately before each test method.""" + # Disable log messages to silence expected warnings + cf.log_level("DISABLE") + # Note: to enable all messages for given methods, lines or calls (those + # without a 'verbose' option to do the same) e.g. to debug them, wrap + # them (for methods, start-to-end internally) as follows: + # cfdm.log_level('DEBUG') + # < ... test code ... > + # cfdm.log_level('DISABLE') + + @unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.") + def test_Field_regrid_mesh_to_mesh(self): + self.assertFalse(cf.regrid_logging()) + + dst = self.dst_mesh.copy() + src = self.src_mesh.copy() + + # Mask some destination grid points + dst[0, 2:35] = cf.masked + + coord_sys = "spherical" + + for src_masked in (False, True): + if src_masked: + src = src.copy() + src[100:200] = cf.masked + + # Loop over whether or not to use the destination grid + # masked points + for use_dst_mask in (False, True): + for method in all_methods: + x = src.regrids( + dst, method=method, use_dst_mask=use_dst_mask + ) + a = x.array + + y = esmpy_regrid( + coord_sys, + method, + src, + dst, + use_dst_mask=use_dst_mask, + ) + + self.assertTrue(np.allclose(y, a, atol=atol, rtol=rtol)) + + if isinstance(a, np.ma.MaskedArray): + self.assertTrue((y.mask == a.mask).all()) + else: + self.assertFalse(y.mask.any()) + + @unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.") + def test_Field_regrid_mesh_to_grid(self): + self.assertFalse(cf.regrid_logging()) + + dst = self.grid.copy() + src = self.src_mesh.copy() + + # Mask some destination grid points + dst[0, 30, 2:35] = cf.masked + + coord_sys = "spherical" + + for src_masked in (False, True): + if src_masked: + src = src.copy() + src[100:200] = cf.masked + + # Loop over whether or not to use the destination grid + # masked points + for use_dst_mask in (False, True): + for method in all_methods: + if method == "nearest_dtos": + continue + + x = src.regrids( + dst, method=method, use_dst_mask=use_dst_mask + ) + a = x.array + a = a.transpose() + + y = esmpy_regrid( + coord_sys, + method, + src, + dst, + use_dst_mask=use_dst_mask, + ) + + self.assertTrue(np.allclose(y, a, atol=atol, rtol=rtol)) + + if isinstance(a, np.ma.MaskedArray): + self.assertTrue((y.mask == a.mask).all()) + else: + self.assertFalse(y.mask.any()) + + # nearest_dtos doesn't work at the moment + with self.assertRaises(ValueError): + src.regrids(dst, method="nearest_dtos") + + @unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.") + def test_Field_regrid_grid_to_mesh(self): + self.assertFalse(cf.regrid_logging()) + + src = self.grid.copy() + dst = self.src_mesh.copy() + + # Mask some destination grid points + dst[100:300] = cf.masked + + coord_sys = "spherical" + + for src_masked in (False, True): + if src_masked: + src = src.copy() + src[0, 30:34, 10:80] = cf.masked + + # Loop over whether or not to use the destination grid + # masked points + for use_dst_mask in (False, True): + for method in all_methods: + if method == "nearest_dtos": + continue + + x = src.regrids( + dst, method=method, use_dst_mask=use_dst_mask + ) + a = x.array + a = np.squeeze(a) + + y = esmpy_regrid( + coord_sys, + method, + src, + dst, + use_dst_mask=use_dst_mask, + ) + + self.assertTrue(np.allclose(y, a, atol=atol, rtol=rtol)) + + if isinstance(a, np.ma.MaskedArray): + self.assertTrue((y.mask == a.mask).all()) + else: + self.assertFalse(y.mask.any()) + + # nearest_dtos doesn't work at the moment + with self.assertRaises(ValueError): + src.regrids(dst, method="nearest_dtos") + + @unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.") + def test_Field_regrid_mesh_cartesian(self): + self.assertFalse(cf.regrid_logging()) + + # Cartesian regridding involving meshes is not currently + # supported + src = self.src_mesh + dst = self.dst_mesh + with self.assertRaises(ValueError): + src.regridc(dst, method="linear") + + dst = self.grid + with self.assertRaises(ValueError): + src.regridc(dst, method="linear") + + src = self.grid + dst = self.dst_mesh + with self.assertRaises(ValueError): + src.regridc(dst, method="linear") + + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print("") + unittest.main(verbosity=2) diff --git a/cf/test/test_weights.py b/cf/test/test_weights.py new file mode 100644 index 0000000000..7f3bbb5cdf --- /dev/null +++ b/cf/test/test_weights.py @@ -0,0 +1,299 @@ +import datetime +import unittest + +import numpy as np + +import cf + +# A radius greater than 1. Used since weights based on the unit +# sphere and non-spheres are tested separately. +r = 2 +radius = cf.Data(r, "m") + +# -------------------------------------------------------------------- +# Spherical polygon geometry with duplicated first/last node. +# +# The cells have areas pi/2 and pi +# The cells have line lengths 3pi/2 and 5pi/2 +# -------------------------------------------------------------------- +gps = cf.example_field(6) +gps.del_construct("auxiliarycoordinate3") +gps.del_construct("grid_mapping_name:latitude_longitude") + +lon = cf.AuxiliaryCoordinate() +lon.standard_name = "longitude" +bounds = cf.Data( + [[315, 45, 45, 315, 999, 999, 999], [90, 90, 0, 45, 45, 135, 90]], + "degrees_east", + mask_value=999, +).reshape(2, 1, 7) +lon.set_bounds(cf.Bounds(data=bounds)) +lon.set_geometry("polygon") + +lat = cf.AuxiliaryCoordinate() +lat.standard_name = "latitude" +bounds = cf.Data( + [[0, 0, 90, 0, 999, 999, 999], [0, 90, 0, 0, -90, 0, 0]], + "degrees_north", + mask_value=999, +).reshape(2, 1, 7) +lat.set_bounds(cf.Bounds(data=bounds)) +lat.set_geometry("polygon") + +gps.del_construct("longitude") +gps.del_construct("latitude") +gps.set_construct(lon, axes="domainaxis0", copy=False) +gps.set_construct(lat, axes="domainaxis0", copy=False) + +# -------------------------------------------------------------------- +# Plane polygon geometry with interior ring and without duplicated +# first/last node +# +# The cells have areas 3 and 8 +# The cells have line lengths 9 and 13 +# -------------------------------------------------------------------- +gppi = gps.copy() +lon = gppi.auxiliary_coordinate("X") +lat = gppi.auxiliary_coordinate("Y") + +lon.override_units("m", inplace=True) +lon.standard_name = "projection_x_coordinate" +bounds = cf.Data( + [ + [ + [2, 2, 0, 0, 999, 999, 999, 999], + [0.5, 1.5, 1.5, 0.5, 999, 999, 999, 999], + ], + [ + [2, 2, 0, 0, 1, 1, 3, 3], + [999, 999, 999, 999, 999, 999, 999, 999], + ], + ], + "m", + mask_value=999, +).reshape(2, 2, 8) +lon.set_bounds(cf.Bounds(data=bounds)) +lon.set_interior_ring(cf.InteriorRing(data=[[0, 1], [0, 0]])) + +lat.override_units("m", inplace=True) +lat.standard_name = "projection_y_coordinate" +bounds = cf.Data( + [ + [ + [-1, 1, 1, -1, 999, 999, 999, 999], + [0.5, 0.5, -0.5, -0.5, 999, 999, 999, 999], + ], + [ + [-1, 1, 1, -1, -1, -3, -3, -1], + [999, 999, 999, 999, 999, 999, 999, 999], + ], + ], + "m", + mask_value=999, +).reshape(2, 2, 8) +lat.set_bounds(cf.Bounds(data=bounds)) +lat.set_interior_ring(cf.InteriorRing(data=[[0, 1], [0, 0]])) + + +class WeightsTest(unittest.TestCase): + def test_weights_polygon_area_geometry(self): + # Spherical polygon geometry weights with duplicated first/last + # node + f = gps.copy() + lon = f.auxiliary_coordinate("X") + lat = f.auxiliary_coordinate("Y") + + # Surface area of unit sphere + sphere_area = 4 * np.pi + correct_weights = np.array([sphere_area / 8, sphere_area / 4]) + + # Spherical polygon geometry weights with duplicated first/last + # node + w = gps.weights("X", great_circle=True) + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("1")) + + w = gps.weights("area", great_circle=True, measure=True, radius=radius) + self.assertTrue((w.array == (r**2) * correct_weights).all()) + self.assertEqual(w.Units, cf.Units("m2")) + + # Spherical polygon geometry weights without duplicated + # first/last node + bounds = cf.Data( + [[315, 45, 45, 999, 999, 999], [90, 90, 0, 45, 45, 135]], + "degrees_east", + mask_value=999, + ).reshape(2, 1, 6) + lon.set_bounds(cf.Bounds(data=bounds)) + + bounds = cf.Data( + [[0, 0, 90, 999, 999, 999], [0, 90, 0, 0, -90, 0]], + "degrees_north", + mask_value=999, + ).reshape(2, 1, 6) + lat.set_bounds(cf.Bounds(data=bounds)) + + w = f.weights("X", great_circle=True) + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("1")) + + w = f.weights("area", great_circle=True, measure=True, radius=radius) + self.assertTrue((w.array == (r**2) * correct_weights).all()) + self.assertEqual(w.Units, cf.Units("m2")) + + # Plane polygon geometry with no duplicated first/last nodes, + # and an interior ring + correct_weights = np.array([3, 8]) + w = gppi.weights("area") + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("1")) + + w = gppi.weights("area", measure=True) + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("m2")) + + def test_weights_polygon_area_ugrid(self): + f = cf.example_field(8) + f = f[..., [0, 2]] + + # Surface area of unit sphere + sphere_area = 4 * np.pi + correct_weights = np.array([sphere_area / 8, sphere_area / 4]) + + # Spherical polygon weights + lon = f.auxiliary_coordinate("X") + lon.del_data() + bounds = cf.Data( + [[315, 45, 45, 999, 999, 999], [90, 90, 0, 45, 45, 135]], + "degrees_east", + mask_value=999, + ).reshape(2, 6) + lon.set_bounds(cf.Bounds(data=bounds)) + + lat = f.auxiliary_coordinate("Y") + bounds = cf.Data( + [[0, 0, 90, 999, 999, 999], [0, 90, 0, 0, -90, 0]], + "degrees_north", + mask_value=999, + ).reshape(2, 6) + lat.set_bounds(cf.Bounds(data=bounds)) + + w = f.weights("X", great_circle=True) + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("1")) + + w = f.weights("area", great_circle=True, measure=True, radius=radius) + self.assertTrue((w.array == (r**2) * correct_weights).all()) + self.assertEqual(w.Units, cf.Units("m2")) + + # Plane polygon weights + lon.override_units("m", inplace=True) + lon.standard_name = "projection_x_coordinate" + bounds = cf.Data( + [[2, 2, 0, 0, 999, 999, 999, 999], [2, 2, 0, 0, 1, 1, 3, 3]], + "m", + mask_value=999, + ).reshape(2, 8) + lon.set_bounds(cf.Bounds(data=bounds)) + + lat.override_units("m", inplace=True) + lat.standard_name = "projection_y_coordinate" + bounds = cf.Data( + [ + [-1, 1, 1, -1, 999, 999, 999, 999], + [-1, 1, 1, -1, -1, -3, -3, -1], + ], + "m", + mask_value=999, + ).reshape(2, 8) + lat.set_bounds(cf.Bounds(data=bounds)) + + correct_weights = np.array([4, 8]) + w = f.weights("area") + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("1")) + + w = f.weights("area", measure=True) + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("m2")) + + def test_weights_line_length_geometry(self): + # Spherical line geometry + gls = gps.copy() + lon = gls.auxiliary_coordinate("X") + lat = gls.auxiliary_coordinate("Y") + lon.set_geometry("line") + lat.set_geometry("line") + + # Circumference of unit sphere + cirumference = 2 * np.pi + correct_weights = np.array( + [3 * cirumference / 4, 5 * cirumference / 4] + ) + + w = gls.weights("X", great_circle=True) + self.assertTrue((w.array == correct_weights).all()) + self.assertEqual(w.Units, cf.Units("1")) + + w = gls.weights("X", great_circle=True, measure=True, radius=radius) + self.assertTrue((w.array == r * correct_weights).all()) + self.assertEqual(w.Units, cf.Units("m")) + + # Plane line geometry with multiple parts + gppm = gppi.copy() + lon = gppm.auxiliary_coordinate("X") + lat = gppm.auxiliary_coordinate("Y") + lon.set_geometry("line") + lat.set_geometry("line") + lon.del_interior_ring() + lat.del_interior_ring() + + correct_weights = np.array([9, 13]) + w = gppm.weights("X") + self.assertTrue((w.array == correct_weights).all()) + + def test_weights_line_area_ugrid(self): + f = cf.example_field(9) + f = f[..., 0:3] + lon = f.auxiliary_coordinate("X") + lat = f.auxiliary_coordinate("Y") + + lon.del_data() + bounds = cf.Data( + [[315, 45], [45, 45], [45, 315]], + "degrees_east", + mask_value=999, + ) + lon.set_bounds(cf.Bounds(data=bounds)) + + lat.del_data() + bounds = cf.Data( + [[0, 0], [0, 90], [90, 0]], + "degrees_north", + mask_value=999, + ) + lat.set_bounds(cf.Bounds(data=bounds)) + + # Circumference of unit sphere + cirumference = 2 * np.pi + correct_weights = np.array([cirumference / 4] * 3) + + # Spherical line weights + w = f.weights("X", great_circle=True) + self.assertTrue( + np.isclose(w, correct_weights, rtol=0, atol=9e-15).all() + ) + self.assertEqual(w.Units, cf.Units("1")) + + w = f.weights("X", great_circle=True, measure=True, radius=radius) + self.assertTrue( + np.isclose(w, r * correct_weights, rtol=0, atol=9e-15).all() + ) + self.assertEqual(w.Units, cf.Units("m")) + + +if __name__ == "__main__": + print("Run date:", datetime.datetime.now()) + cf.environment() + print("") + unittest.main(verbosity=2) diff --git a/cf/test/ugrid_global_1.nc b/cf/test/ugrid_global_1.nc new file mode 100644 index 0000000000..25bc2afef5 Binary files /dev/null and b/cf/test/ugrid_global_1.nc differ diff --git a/cf/test/ugrid_global_2.nc b/cf/test/ugrid_global_2.nc new file mode 100644 index 0000000000..4a262f835a Binary files /dev/null and b/cf/test/ugrid_global_2.nc differ diff --git a/cf/tiepointindex.py b/cf/tiepointindex.py index d1a26b30f3..adec79d05f 100644 --- a/cf/tiepointindex.py +++ b/cf/tiepointindex.py @@ -4,55 +4,4 @@ class TiePointIndex(mixin.PropertiesData, cfdm.TiePointIndex): - """A tie point index variable with properties. - - Space may be saved by storing a subsample of the coordinates. The - uncompressed coordinates can be reconstituted by interpolation - from the subsampled coordinate values, also called either "tie - points" or "bounds tie points". - - For each interpolated dimension, the locations of the (bounds) tie - points are defined by a corresponding tie point index variable, - which also indicates the extent of each continuous area. - - **NetCDF interface** - - {{netCDF variable}} - - The netCDF subsampled dimension name may be accessed with the - `nc_set_subsampled_dimension`, `nc_get_subsampled_dimension`, - `nc_del_subsampled_dimension` and `nc_has_subsampled_dimension` - methods. - - The netCDF subsampled dimension group structure may be accessed - with the `nc_set_subsampled_dimension`, - `nc_get_subsampled_dimension`, `nc_subsampled_dimension_groups`, - `nc_clear_subsampled_dimension_groups`, and - `nc_set_subsampled_dimension_groups` methods. - - The netCDF interpolation subarea dimension name may be accessed - with the `nc_set_interpolation_subarea_dimension`, - `nc_get_interpolation_subarea_dimension`, - `nc_del_interpolation_subarea_dimension`, and - `nc_has_interpolation_subarea_dimension` methods. - - The netCDF interpolation subarea dimension group structure may be - accessed with the `nc_set_interpolation_subarea_dimension`, - `nc_get_interpolation_subarea_dimension`, - `nc_interpolation_subarea_dimension_groups`, - `nc_clear_interpolation_subarea_dimension_groups` and - `nc_set_interpolation_subarea_dimension_groups` methods. - - .. versionadded:: 3.14 - - .. seealso:: `InterpolationParameter` - - """ - - def __repr__(self): - """Called by the `repr` built-in function. - - x.__repr__() <==> repr(x) - - """ - return super().__repr__().replace("<", " 1: + if not xcoord.has_bounds(): + if auto: + return False + + raise ValueError( + "Can't create area weights: No bounds for " + f"{xcoord.identity()!r} axis" + ) + + if methods: + weights[(xaxis,)] = f"linear {xcoord.identity()}" + else: + cells = xcoord.cellsize + if xcoord.Units.equivalent(radians): + cells.Units = radians + if measure: + cells = cells * radius + cells.override_units(radius.Units, inplace=True) + else: + cells.Units = metres + + weights[(xaxis,)] = cells + + weights_axes.add(xaxis) + + if measure or ycoord.size > 1: + if not ycoord.has_bounds(): + if auto: + return False + + raise ValueError( + "Can't create area weights: No bounds for " + f"{ycoord.identity()!r} axis" + ) + + if ycoord.Units.equivalent(radians): + ycoord = ycoord.clip(-90, 90, units=Units("degrees")) + ycoord.sin(inplace=True) + + if methods: + weights[(yaxis,)] = f"linear sine {ycoord.identity()}" + else: + cells = ycoord.cellsize + if measure: + cells = cells * radius + + weights[(yaxis,)] = cells + else: + if methods: + weights[(yaxis,)] = f"linear {ycoord.identity()}" + else: + cells = ycoord.cellsize + weights[(yaxis,)] = cells + + weights_axes.add(yaxis) + + return True + + @classmethod + def data( + cls, + f, + w, + weights, + weights_axes, + axes=None, + data=False, + components=False, + methods=False, + ): + """Create weights from `Data`. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + w: `Data` + Create weights from this data, that must be + broadcastable to the data of *f*, unless the *axes* + parameter is also set. + + axes: (sequence of) `int` or `str`, optional + If not `None` then weights are created only for the + specified axes. + + {{weights weights: `dict`}} + + {{weights weights_axes: `set`}} + + data: `bool`, optional + If True then create weights in a `Data` instance that + is broadcastable to the original data. + + components: `bool`, optional + If True then the *weights* dictionary is updated with + orthogonal weights components. + + {{weights methods: `bool`, optional}} + + :Returns: + + `bool` + `True` if weights were created, otherwise `False`. + + """ + # ------------------------------------------------------------ + # Data weights + # ------------------------------------------------------------ + field_data_axes = f.get_data_axes() + + if axes is not None: + if isinstance(axes, (str, int)): + axes = (axes,) + + if len(axes) != w.ndim: + raise ValueError( + "'axes' parameter must provide an axis identifier " + f"for each weights data dimension. Got {axes!r} for " + f"{w.ndim} dimension(s)." + ) + + iaxes = [ + field_data_axes.index(f.domain_axis(axis, key=True)) + for axis in axes + ] + + if data: + for i in range(f.ndim): + if i not in iaxes: + w = w.insert_dimension(position=i) + iaxes.insert(i, i) + + w = w.transpose(iaxes) + + if w.ndim > 0: + while w.shape[0] == 1: + w = w.squeeze(0) + + else: + iaxes = list(range(f.ndim - w.ndim, f.ndim)) + + if not (components or methods): + if not f._is_broadcastable(w.shape): + raise ValueError( + f"The 'Data' weights (shape {w.shape}) are not " + "broadcastable to the field construct's data " + f"(shape {f.shape})." + ) + + axes0 = field_data_axes[f.ndim - w.ndim :] + else: + axes0 = [field_data_axes[i] for i in iaxes] + + for axis0 in axes0: + if axis0 in weights_axes: + raise ValueError( + "Multiple weights specified for " + f"{f.constructs.domain_axis_identity(axis0)!r} axis" + ) + + if methods: + weights[tuple(axes0)] = "custom data" + else: + weights[tuple(axes0)] = w + + weights_axes.update(axes0) + return True + + @classmethod + def field(cls, f, field_weights, weights, weights_axes): + """Create weights from other field constructs. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + field_weights: sequence of `Field` + The field constructs from which to create weights. + + {{weights weights: `dict`}} + + {{weights weights_axes: `set`}} + + :Returns: + + `bool` + `True` if weights were created, otherwise `False`. + + """ + s = f.analyse_items() + + domain_axes = f.domain_axes(todict=True) + for w in field_weights: + t = w.analyse_items() + + if t["undefined_axes"]: + w_domain_axes_1 = w.domain_axes( + filter_by_size=(1,), todict=True + ) + if set(w_domain_axes_1).intersection(t["undefined_axes"]): + raise ValueError( + f"Weights field {w} has at least one undefined " + f"domain axes: {w_domain_axes_1}." + ) + + w = w.squeeze() + + w_domain_axes = w.domain_axes(todict=True) + + axis1_to_axis0 = {} + + coordinate_references = f.coordinate_references(todict=True) + w_coordinate_references = w.coordinate_references(todict=True) + + for axis1 in w.get_data_axes(): + identity = t["axis_to_id"].get(axis1, None) + if identity is None: + raise ValueError( + "Weights field has unmatched, size > 1 " + f"{w.constructs.domain_axis_identity(axis1)!r} axis" + ) + + axis0 = s["id_to_axis"].get(identity, None) + if axis0 is None: + raise ValueError( + f"Weights field has unmatched, size > 1 {identity!r} " + "axis" + ) + + w_axis_size = w_domain_axes[axis1].get_size() + f_axis_size = domain_axes[axis0].get_size() + + if w_axis_size != f_axis_size: + raise ValueError( + f"Weights field has incorrectly sized {identity!r} " + f"axis ({w_axis_size} != {f_axis_size})" + ) + + axis1_to_axis0[axis1] = axis0 + + # Check that the defining coordinate data arrays are + # compatible + key0 = s["axis_to_coord"][axis0] + key1 = t["axis_to_coord"][axis1] + + if not f._equivalent_construct_data( + w, key0=key0, key1=key1, s=s, t=t + ): + raise ValueError( + f"Weights field has incompatible {identity!r} " + "coordinates" + ) + + # Still here? Then the defining coordinates have + # equivalent data arrays + + # If the defining coordinates are attached to + # coordinate references then check that those + # coordinate references are equivalent + refs0 = [ + key + for key, ref in coordinate_references.items() + if key0 in ref.coordinates() + ] + refs1 = [ + key + for key, ref in w_coordinate_references.items() + if key1 in ref.coordinates() + ] + + nrefs = len(refs0) + if nrefs > 1 or nrefs != len(refs1): + # The defining coordinates are associated with + # different numbers of coordinate references + equivalent_refs = False + elif not nrefs: + # Neither defining coordinate is associated with a + # coordinate reference + equivalent_refs = True + else: + # Each defining coordinate is associated with + # exactly one coordinate reference + equivalent_refs = f._equivalent_coordinate_references( + w, key0=refs0[0], key1=refs1[0], s=s, t=t + ) + + if not equivalent_refs: + raise ValueError( + "Input weights field has an incompatible " + "coordinate reference" + ) + + axes0 = tuple( + [axis1_to_axis0[axis1] for axis1 in w.get_data_axes()] + ) + + for axis in axes0: + if axis in weights_axes: + raise ValueError( + "Multiple weights specified for " + f"{f.constructs.domain_axis_identity(axis)!r} " + "axis" + ) + + weights[tuple(axes0)] = w.data + weights_axes.update(axes0) + + return True + + @classmethod + def field_scalar(cls, f): + """Return a scalar field of weights with long name ``'weight'``. + + .. versionadded:: 3.16.0 + + :Parameter: + + f: `Field` + The field for which the weights are being created. + + :Returns: + + `Field` + The scalar weights field, with a single array value of + ``1``. + + **Examples** + + >>> s = w.field_scalar(f) + >> print(s) + Field: long_name=weight + ----------------------- + Data : long_name=weight 1 + >>> print(s.array) + 1.0 + + """ + data = f._Data(1.0, "1") + + f = type(f)() + f.set_data(data, copy=False) + f.long_name = "weight" + f.comment = f"Weights for {f!r}" + return f + + @classmethod + def polygon_area( + cls, + f, + domain_axis, + weights, + weights_axes, + auto=False, + measure=False, + radius=None, + great_circle=False, + return_areas=False, + methods=False, + ): + """Creates area weights for polygon geometry cells. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + domain_axis: `str` or `None` + If set to a domain axis identifier + (e.g. ``'domainaxis1'``) then only accept cells that + recognise the given axis. If `None` then the cells may + span any axis. + + {{weights weights: `dict`}} + + {{weights weights_axes: `set`}} + + {{weights auto: `bool`, optional}} + + {{weights measure: `bool`, optional}} + + {{radius: optional}} + + great_circle: `bool`, optional + If True then assume that the edges of spherical + polygons are great circles. + + {{weights methods: `bool`, optional}} + + :Returns: + + `bool` or `Data` + `True` if weights were created, otherwise `False`. If + *return_areas* is True and weights were created, then + the weights are returned. + + """ + from .units import Units + + axis, aux_X, aux_Y, aux_Z, ugrid = cls._geometry_ugrid_cells( + f, domain_axis, "polygon", auto=auto + ) + + if axis is None: + if auto: + return False + + if domain_axis is None: + raise ValueError("No polygon cells") + + raise ValueError( + "No polygon cells for " + f"{f.constructs.domain_axis_identity(domain_axis)!r} axis" + ) + + if axis in weights_axes: + if auto: + return False + + raise ValueError( + "Multiple weights specifications for " + f"{f.constructs.domain_axis_identity(axis)!r} axis" + ) + + x = aux_X.bounds.data + y = aux_Y.bounds.data + + radians = Units("radians") + metres = Units("metres") + + if x.Units.equivalent(radians) and y.Units.equivalent(radians): + if not great_circle: + raise ValueError( + "Must set great_circle=True to allow the derivation of " + "area weights from spherical polygons composed from " + "great circle segments." + ) + + if methods: + weights[(axis,)] = "area spherical polygon" + return True + + spherical = True + x.Units = radians + elif x.Units.equivalent(metres) and y.Units.equivalent(metres): + if methods: + weights[(axis,)] = "area plane polygon" + return True + + spherical = False + else: + return False + + y.Units = x.Units + x = x.persist() + y = y.persist() + + # Find the number of nodes per polygon + n_nodes = x.count(axis=-1, keepdims=False).array + if (y.count(axis=-1, keepdims=False) != n_nodes).any(): + raise ValueError( + "Can't create area weights for " + f"{f.constructs.domain_axis_identity(axis)!r} axis: " + f"{aux_X!r} and {aux_Y!r} have inconsistent bounds " + "specifications" + ) + + if ugrid: + areas = cls._polygon_area_ugrid(f, x, y, n_nodes, spherical) + else: + areas = cls._polygon_area_geometry( + f, x, y, aux_X, aux_Y, n_nodes, spherical + ) + + del x, y, n_nodes + + if not measure: + areas.override_units(Units("1"), inplace=True) + elif spherical: + areas = cls._spherical_area_measure(f, areas, aux_Z, radius) + + if return_areas: + return areas + + weights[(axis,)] = areas + weights_axes.add(axis) + return True + + @classmethod + def _polygon_area_geometry(cls, f, x, y, aux_X, aux_Y, n_nodes, spherical): + """Creates area weights for polygon geometry cells. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + x: `Data` + The X coordinates of the polygon nodes, with units of + radians or equivalent to metres. + + y: `Data` + The Y coordinates of the polygon nodes, with the same + units as *x*. + + aux_X: `AuxiliaryCoordinate` + The X coordinate construct of *f*. + + aux_Y: `AuxiliaryCoordinate` + The Y coordinate construct of *f*. + + n_nodes: `numpy.ndarray` + The number of nodes per polygon, equal to the number + of non-missing values in the trailing dimension of + *x*. + + spherical: `bool` + True for spherical lines. + + :Returns: + + `Data` + The area of the geometry polygon cells, in units of + square metres. + + """ + import numpy as np + + x_units = x.Units + y_units = y.Units + + # Check for interior rings + interior_ring_X = aux_X.get_interior_ring(None) + interior_ring_Y = aux_Y.get_interior_ring(None) + if interior_ring_X is None and interior_ring_Y is None: + interior_rings = None + elif interior_ring_X is None: + raise ValueError( + "Can't find weights: X coordinates have missing " + "interior ring variable" + ) + elif interior_ring_Y is None: + raise ValueError( + "Can't find weights: Y coordinates have missing " + "interior ring variable" + ) + elif not interior_ring_X.data.equals(interior_ring_Y.data): + raise ValueError( + "Can't find weights: Interior ring variables for X and Y " + "coordinates have different data values" + ) + else: + interior_rings = interior_ring_X.data + if interior_rings.shape != aux_X.bounds.shape[:-1]: + raise ValueError( + "Can't find weights: Interior ring variables have " + f"incorrect shape. Got {interior_rings.shape}, " + f"expected {aux_X.bounds.shape[:-1]}" + ) + + rows = np.arange(x.shape[0]) + n_nodes_m1 = n_nodes - 1 + + duplicate = x[0, 0, 0].isclose(x[0, 0, n_nodes_m1[0, 0]]) & y[ + 0, 0, 0 + ].isclose(y[0, 0, n_nodes_m1[0, 0]]) + duplicate = duplicate.array + + y = y.array + + # Pad out the boundary vertex array for each cell with first + # and last bounds neighbours + empty_col_x = np.ma.masked_all(x.shape[:-1] + (1,), dtype=x.dtype) + empty_col_y = np.ma.masked_all(y.shape[:-1] + (1,), dtype=y.dtype) + + if not duplicate.any(): + # The first and last boundary vertices are different in + # all polygons, i.e. No. nodes = No. edges. + # + # Insert two new Y columns that duplicate the first and + # last nodes. + # + # E.g. for triangle cells: + # [[[4, 5, 6]]] -> [[[6, 4, 5, 6, 4]]] + # [[[4, 5, 6, --]]] -> [[[6, 4, 5, 6, 4, --]]] + n_nodes_p1 = n_nodes + 1 + y = np.ma.concatenate((empty_col_y, y, empty_col_y), axis=-1) + for i in range(x.shape[1]): + y[:, i, 0] = y[rows, i, n_nodes[:, i]] + y[rows, i, n_nodes_p1[:, i]] = y[rows, i, 1] + + if spherical: + # Spherical polygons defined by great circles: Also + # insert the columns into X. + x = x.array + x = np.ma.concatenate((empty_col_x, x, empty_col_x), axis=-1) + for i in range(x.shape[1]): + x[:, i, 0] = x[rows, i, n_nodes[:, i]] + x[rows, i, n_nodes_p1[:, i]] = x[rows, i, 1] + + # The number of edges in each polygon + N = n_nodes + + del n_nodes_p1 + elif duplicate.all(): + # The first and last boundary vertices coincide in all + # cells, i.e. No. nodes = No. edges + 1. + # + # E.g. for triangle cells: + # [[[4, 5, 6, 4]]] -> [[[6, 4, 5, 6, 4]]] + # [[[4, 5, 6, 4, --]]] -> [[[6, 4, 5, 6, 4, --]]] + if not spherical: + raise ValueError( + "Can't (yet) calculate weights for plane geometry " + "polygons with identical first and last nodes" + ) + + y = np.ma.concatenate((empty_col_y, y), axis=-1) + for i in range(x.shape[1]): + y[:, i, 0] = y[rows, i, n_nodes_m1[:, i]] + + x = x.array + x = np.ma.concatenate((empty_col_x, x), axis=-1) + for i in range(x.shape[1]): + x[:, i, 0] = x[rows, i, n_nodes_m1[:, i]] + + # The number of edges in each polygon + N = n_nodes_m1 + else: + raise ValueError( + "Can't calculate spherical geometry polygon cell areas " + "when the first and last boundary vertices coincide in " + "some cells but not all" + ) + + del duplicate, rows, n_nodes_m1 + + y = f._Data(y, units=y_units) + if spherical: + x = f._Data(x, units=x_units) + + if spherical: + # Spherical polygons defined by great circles + all_areas = cls._spherical_polygon_areas( + f, x, y, N, interior_rings + ) + else: + # Plane polygons defined by straight lines. + all_areas = cls._plane_polygon_areas(x, y) + + # Sum the areas of each geometry polygon part to get the total + # area of each cell + areas = all_areas.sum(-1, squeeze=True) + return areas + + @classmethod + def _polygon_area_ugrid(cls, f, x, y, n_nodes, spherical): + """Creates area weights for UGRID face cells. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + x: `Data` + The X coordinates of the polygon nodes, with units of + radians or equivalent to metres. + + y: `Data` + The Y coordinates of the polygon nodes, with the same + units as *x*. + + n_nodes: `numpy.ndarray` + The number of nodes per polygon, equal to the number + of non-missing values in the trailing dimension of + *x*. + + spherical: `bool` + True for spherical polygons. + + :Returns: + + `Data` + The area of the UGRID face cells, in units of square + metres. + + """ + import numpy as np + + y_units = y.Units + + n_nodes0 = n_nodes.item(0) + if (n_nodes == n_nodes0).all(): + # All cells have the same number of nodes, so we can use + # an integer and a slice in place of two integer arrays, + # which is much faster. + n_nodes = n_nodes0 + rows = slice(None) + else: + rows = np.arange(x.shape[0]) + + n_nodes_p1 = n_nodes + 1 + + # The first and last boundary vertices are different in + # all polygons, i.e. No. nodes = No. edges. + # + # Insert two new Y columns that duplicate the first and last + # nodes. + # + # E.g. for triangle cells: + # [[4, 5, 6]] -> [[6, 4, 5, 6, 4]] + # [[4, 5, 6, --]] -> [[6, 4, 5, 6, 4, --]] + y = y.array + empty_col_y = np.ma.masked_all((y.shape[0], 1), dtype=y.dtype) + y = np.ma.concatenate((empty_col_y, y, empty_col_y), axis=1) + y[:, 0] = y[rows, n_nodes] + y[rows, n_nodes_p1] = y[rows, 1] + y = f._Data(y, units=y_units) + + if spherical: + # Spherical polygons defined by great circles: Also insert + # the columns into X. + x_units = x.Units + x = x.array + empty_col_x = np.ma.masked_all((x.shape[0], 1), dtype=x.dtype) + x = np.ma.concatenate((empty_col_x, x, empty_col_x), axis=1) + x[:, 0] = x[rows, n_nodes] + x[rows, n_nodes_p1] = x[rows, 1] + x = f._Data(x, units=x_units) + + areas = cls._spherical_polygon_areas(f, x, y, n_nodes) + else: + # Plane polygons defined by straight lines + areas = cls._plane_polygon_areas(x, y) + + return areas + + @classmethod + def line_length( + cls, + f, + domain_axis, + weights, + weights_axes, + auto=False, + measure=False, + radius=None, + great_circle=False, + methods=False, + ): + """Creates line-length weights for line geometries. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + domain_axis: `str` or `None` + If set to a domain axis identifier + (e.g. ``'domainaxis1'``) then only recognise cells + that span the given axis. If `None` then the cells may + span any axis. + + {{weights weights: `dict`}} + + {{weights weights_axes: `set`}} + + {{weights auto: `bool`, optional}} + + {{weights measure: `bool`, optional}} + + {{radius: optional}} + + great_circle: `bool`, optional + If True then assume that the spherical lines are great + circles. + + {{weights methods: `bool`, optional}} + + :Returns: + + `bool` + `True` if weights were created, otherwise `False`. + + """ + from .units import Units + + axis, aux_X, aux_Y, aux_Z, ugrid = cls._geometry_ugrid_cells( + f, domain_axis, "line", auto=auto + ) + + if axis is None: + if auto: + return False + + if domain_axis is None: + raise ValueError("No line cells") + + raise ValueError( + "No line cells for " + f"{f.constructs.domain_axis_identity(domain_axis)!r} axis" + ) + + if axis in weights_axes: + if auto: + return False + + raise ValueError( + "Multiple weights specifications for " + f"{f.constructs.domain_axis_identity(axis)!r} axis" + ) + + radians = Units("radians") + metres = Units("metres") + + x = aux_X.bounds.data + y = aux_Y.bounds.data + + if x.Units.equivalent(radians) and y.Units.equivalent(radians): + if not great_circle: + raise ValueError( + "Must set great_circle=True to allow the derivation of " + "area weights from spherical polygons composed from " + "great circle segments." + ) + + if methods: + weights[(axis,)] = "linear spherical line" + return True + + spherical = True + x.Units = radians + elif x.Units.equivalent(metres) and y.Units.equivalent(metres): + if methods: + weights[(axis,)] = "linear plane line" + return True + + spherical = False + else: + return False + + y.Units = x.Units + + if ugrid: + lengths = cls._line_length_ugrid(f, x, y, spherical) + else: + lengths = cls._line_length_geometry(f, x, y, spherical) + + if not measure: + lengths.override_units(Units("1"), inplace=True) + elif spherical: + r = f.radius(default=radius) + r = r.override_units(Units("1")) + lengths = lengths * r + + weights[(axis,)] = lengths + weights_axes.add(axis) + return True + + @classmethod + def _line_length_geometry(cls, f, x, y, spherical): + """Creates line-length weights for line geometries. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + x: `Data` + The X coordinates of the line nodes. + + y: `Data` + The Y coordinates of the line nodes. + + spherical: `bool` + True for spherical lines. + + :Returns: + + `Data` + The length of the geometry line cells, in units of + metres. + + """ + from .units import Units + + if spherical: + all_lengths = cls._central_angles(f, x, y) + all_lengths.override_units(Units("m"), inplace=True) + else: + delta_x = x.diff(axis=-1) + delta_y = y.diff(axis=-1) + all_lengths = (delta_x**2 + delta_y**2) ** 0.5 + + # Sum the lengths of each part to get the total length of each + # cell + lengths = all_lengths.sum(axes=(-2, -1), squeeze=True) + return lengths + + @classmethod + def _line_length_ugrid(cls, f, x, y, spherical): + """Creates line-length weights for UGRID edges. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + x: `Data` + The X coordinates of the line nodes. + + y: `Data` + The Y coordinates of the line nodes. + + spherical: `bool` + True for spherical lines. + + :Returns: + + `Data` + The length of the UGRID edge cells, in units of + metres. + + """ + from .units import Units + + if spherical: + lengths = cls._central_angles(f, x, y) + lengths.override_units(Units("m"), inplace=True) + else: + delta_x = x.diff(axis=-1) + delta_y = y.diff(axis=-1) + lengths = (delta_x**2 + delta_y**2) ** 0.5 + + lengths.squeeze(axes=-1, inplace=True) + return lengths + + @classmethod + def geometry_volume( + cls, + f, + domain_axis, + weights, + weights_axes, + auto=False, + measure=False, + radius=None, + great_circle=False, + methods=False, + ): + """Creates volume weights for polygon geometry cells. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + domain_axis: `str` or `None` + If set to a domain axis identifier + (e.g. ``'domainaxis1'``) then only recognise cells + that span the given axis. If `None` then the cells may + span any axis. + + {{weights weights: `dict`}} + + {{weights weights_axes: `set`}} + + {{weights auto: `bool`, optional}} + + {{weights measure: `bool`, optional}} + + {{radius: optional}} + + great_circle: `bool`, optional + If True then assume that the edges of spherical + polygons are great circles. + + {{weights methods: `bool`, optional}} + + :Returns: + + `bool` + `True` if weights were created, otherwise `False`. + + """ + from .units import Units + + axis, aux_X, aux_Y, aux_Z, ugrid = cls._geometry_ugrid_cells( + f, domain_axis, "polygon", auto=auto + ) + + if axis is None and auto: + return False + + if axis in weights_axes: + if auto: + return False + + raise ValueError( + "Multiple weights specifications for " + f"{f.constructs.domain_axis_identity(axis)!r} axis" + ) + + x = aux_X.bounds.data + y = aux_Y.bounds.data + z = aux_Z.bounds.data + + radians = Units("radians") + metres = Units("metres") + + if not z.Units.equivalent(metres): + if auto: + return False + + raise ValueError( + "Z coordinate bounds must have units equivalent to " + f"metres for volume calculations. Got {z.Units!r}." + ) + + if not methods: + # Initialise cell volumes as the cell areas + areas = cls.polygon_area( + f, + weights, + weights_axes, + auto=auto, + measure=measure, + radius=radius, + great_circle=great_circle, + methods=False, + return_areas=True, + ) + if measure: + delta_z = abs(z[..., 1] - z[..., 0]) + delta_z.squeeze(axis=-1, inplace=True) + + if x.Units.equivalent(metres) and y.Units.equivalent(metres): + # -------------------------------------------------------- + # Plane polygons defined by straight lines. + # -------------------------------------------------------- + if methods: + weights[(axis,)] = "volume plane polygon geometry" + return True + + if measure: + volumes = areas * delta_z + else: + volumes = areas + + elif x.Units.equivalent(radians) and y.Units.equivalent(radians): + # -------------------------------------------------------- + # Spherical polygons defined by great circles + # -------------------------------------------------------- + if not great_circle: + raise ValueError( + "Must set great_circle=True to allow the derivation " + "of volume weights from spherical polygons composed " + "from great circle segments." + ) + + if methods: + weights[(axis,)] = "volume spherical polygon geometry" + return True + + if measure: + r = f.radius(default=radius) + + # actual_volume = + # [actual_area/(4*pi*r**2)] + # * (4/3)*pi*[(r+delta_z)**3 - r**3)] + volumes = areas * ( + delta_z**3 / (3 * r**2) + delta_z**2 / r + delta_z + ) + else: + volumes = areas + else: + raise ValueError( + "X and Y coordinate bounds must both have units " + "equivalent to either metres (for plane polygon) or " + "radians (for spherical polygon) volume calculations. Got " + f"{x.Units!r} and {y.Units!r}." + ) + + weights[(axis,)] = volumes + weights_axes.add(axis) + return True + + @classmethod + def _interior_angles(cls, f, lon, lat, interior_rings=None): + """Find the interior angles at spherical polygon vertices. + + The interior angle at a vertex is that formed by two adjacent + sides. Each interior angle is therefore a function of three + vertices - the vertex at which the interior angle is required and + the two adjacent vertices either side of it. + + **Method** + + Bevis, M., Cambareri, G. Computing the area of a spherical polygon + of arbitrary shape. Math Geol 19, 335–346 (1987). + + http://bemlar.ism.ac.jp/zhuang/Refs/Refs/bevis1987mathgeol.pdf + + Abstract: An algorithm for determining the area of a spherical + polygon of arbitrary shape is presented. The kernel of the + problem is to compute the interior angle at each vertex of the + spherical polygon; a well-known relationship between the area of + a spherical polygon and the sum of its interior angles then may + be exploited. The algorithm has been implemented as a FORTRAN + subroutine and a listing is provided. + + .. versionadded:: 3.16.0 + + .. seealso:: `_central_angles` + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + lon: `Data` + Longitudes. Must have units of radians, which is not + checked. + + lat: `Data` + Latitudes. Must have units of radians, which is not + checked. + + interior_ring: `Data`, optional + The interior ring indicators for parts of polygon + geometry cells. If set must have shape + ``lon.shape[:-1]``. + + :Returns: + + `Data` + The interior angles of each spherical polygon, in + units of radians. + + """ + import numpy as np + + from .query import lt + + # P denotes a vertex at which the interior angle is required, A + # denotes the adjacent point clockwise from P, and B denotes the + # adjacent point anticlockwise from P. + lon_A = lon[..., :-2] + lon_P = lon[..., 1:-1] + lon_B = lon[..., 2:] + + lon_A_minus_lon_P = lon_A - lon_P + lon_B_minus_lon_P = lon_B - lon_P + + cos_lat = lat.cos() + cos_lat_A = cos_lat[..., :-2] + cos_lat_P = cos_lat[..., 1:-1] + cos_lat_B = cos_lat[..., 2:] + + sin_lat = lat.sin() + sin_lat_A = sin_lat[..., :-2] + sin_lat_P = sin_lat[..., 1:-1] + sin_lat_B = sin_lat[..., 2:] + + y = lon_A_minus_lon_P.sin() * cos_lat_A + x = ( + sin_lat_A * cos_lat_P + - cos_lat_A * sin_lat_P * lon_A_minus_lon_P.cos() + ) + lat_A_primed = f._Data.arctan2(y, x) + + y = lon_B_minus_lon_P.sin() * cos_lat_B + x = ( + sin_lat_B * cos_lat_P + - cos_lat_B * sin_lat_P * lon_B_minus_lon_P.cos() + ) + lat_B_primed = f._Data.arctan2(y, x) + + # The CF vertices here are, in general, given in anticlockwise + # order, so we do "alpha_P = lat_B_primed - lat_A_primed", + # rather than the "alpha_P = lat_A_primed - lat_B_primed" + # given in Bevis and Cambareri, which assumes clockwise order. + alpha_P = lat_B_primed - lat_A_primed + + if interior_rings is not None: + # However, interior rings *are* given in clockwise order in + # CF, so we need to negate alpha_P in these cases. + alpha_P.where(interior_rings, -alpha_P, inplace=True) + + # Add 2*pi to negative values + alpha_P = alpha_P.where(lt(0), alpha_P + 2 * np.pi, alpha_P) + return alpha_P + + @classmethod + def _central_angles(cls, f, lon, lat): + r"""Find the central angles for spherical great circle line segments. + + The central angle of two points on the sphere is the angle + subtended from the centre of the sphere by the two points on its + surface. It is calculated with a special case of the Vincenty + formula that is accurate for all spherical distances: + + **Method** + + \Delta \sigma =\arctan {\frac {\sqrt {\left(\cos \phi + _{2}\sin(\Delta \lambda )\right)^{2}+\left(\cos \phi _{1}\sin + \phi _{2}-\sin \phi _{1}\cos \phi _{2}\cos(\Delta \lambda + )\right)^{2}}}{\sin \phi _{1}\sin \phi _{2}+\cos \phi _{1}\cos + \phi _{2}\cos(\Delta \lambda )}} + + The quadrant for \Delta \sigma should be governed by the signs of + the numerator and denominator of the right hand side using the + atan2 function. + + (https://en.wikipedia.org/wiki/Great-circle_distance) + + .. versionadded:: 3.16.0 + + .. seealso:: `_interior_angles` + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + lon: `Data` + Longitudes with units of radians. + + lat: `Data` + Latitudes with units of radians. + + :Returns: + + `Data` + The central angles in units of radians. + + """ + # A and B denote adjacent vertices that define each line segment + delta_lon = lon.diff(axis=-1) + + cos_lat = lat.cos() + sin_lat = lat.sin() + + cos_lat_A = cos_lat[..., :-1] + cos_lat_B = cos_lat[..., 1:] + + sin_lat_A = sin_lat[..., :-1] + sin_lat_B = sin_lat[..., 1:] + + cos_delta_lon = delta_lon.cos() + sin_delta_lon = delta_lon.sin() + + y = ( + (cos_lat_B * sin_delta_lon) ** 2 + + (cos_lat_A * sin_lat_B - sin_lat_A * cos_lat_B * cos_delta_lon) + ** 2 + ) ** 0.5 + x = sin_lat_A * sin_lat_B + cos_lat_A * cos_lat_B * cos_delta_lon + + delta_sigma = f._Data.arctan2(y, x) + return delta_sigma + + @classmethod + def linear( + cls, + f, + axis, + weights, + weights_axes, + auto=False, + measure=False, + methods=False, + ): + """1-d linear weights from dimension coordinate constructs. + + .. versionadded:: 3.16.0 + + :Parameters: + + axis: `str` + The identity of the domain axis over which to find the + weights. + + {{weights weights: `dict`}} + + {{weights weights_axes: `set`}} + + {{weights auto: `bool`, optional}} + + {{weights measure: `bool`, optional}} + + {{weights methods: `bool`, optional}} + + :Returns: + + `bool` + `True` if weights were created, otherwise `False`. + + """ + da_key = f.domain_axis(axis, key=True, default=None) + if da_key is None: + if auto: + return False + + raise ValueError( + "Can't create weights: Can't find domain axis matching " + f"{axis!r}" + ) + + dim = f.dimension_coordinate(filter_by_axis=(da_key,), default=None) + if dim is None: + if auto: + return False + + raise ValueError( + f"Can't create linear weights for {axis!r} axis: Can't find " + "dimension coordinate construct." + ) + + if not measure and dim.size == 1: + return False + + if da_key in weights_axes: + if auto: + return False + + raise ValueError( + f"Can't create linear weights for {axis!r} axis: Multiple " + "axis specifications" + ) + + if not dim.has_bounds(): + # Dimension coordinate has no bounds + if auto: + return False + + raise ValueError( + f"Can't create linear weights for {axis!r} axis: No bounds" + ) + else: + # Bounds exist + if methods: + weights[ + (da_key,) + ] = f"linear {f.constructs.domain_axis_identity(da_key)}" + else: + weights[(da_key,)] = dim.cellsize + + weights_axes.add(da_key) + return True + + @classmethod + def cell_measure( + cls, f, measure, weights, weights_axes, methods=False, auto=False + ): + """Cell measure weights. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + {{weights weights: `dict`}} + + {{weights weights_axes: `set`}} + + {{weights methods: `bool`, optional}} + + {{weights auto: `bool`, optional}} + + :Returns: + + `bool` + `True` if weights were created, otherwise `False`. + + """ + m = f.cell_measures(filter_by_measure=(measure,), todict=True) + len_m = len(m) + + if not len_m: + if measure == "area": + return False + + if auto: + return False + + raise ValueError( + f"Can't find weights: No {measure!r} cell measure" + ) + + elif len_m > 1: + if auto: + return False + + raise ValueError( + f"Can't find weights: Multiple {measure!r} cell measures" + ) + + key, clm = m.popitem() + + clm_axes0 = f.get_data_axes(key) + + clm_axes = tuple( + [axis for axis, n in zip(clm_axes0, clm.data.shape) if n > 1] + ) + + for axis in clm_axes: + if axis in weights_axes: + if auto: + return False + + raise ValueError( + "Multiple weights specifications for " + f"{f.constructs.domain_axis_identity(axis)!r} axis" + ) + + clm = clm.get_data(_fill_value=False).copy() + if clm_axes != clm_axes0: + iaxes = [clm_axes0.index(axis) for axis in clm_axes] + clm.squeeze(iaxes, inplace=True) + + if methods: + weights[tuple(clm_axes)] = measure + " cell measure" + else: + weights[tuple(clm_axes)] = clm + + weights_axes.update(clm_axes) + return True + + @classmethod + def scale(cls, w, scale): + """Scale the weights so that they are <= scale. + + .. versionadded:: 3.16.0 + + :Parameters: + + w: `Data` + The weights to be scaled. + + scale: number or `None` + The maximum value of the scaled weights. If `None` + then no scaling is applied. + + :Returns: + + `Data` + The scaled weights. + + """ + if scale is None: + return w + + if scale < 0: + raise ValueError( + "Can't set 'scale' parameter to a negatve number. Got " + f"{scale!r}" + ) + + w = w / w.max() + if scale != 1: + w = w * scale + + return w + + @classmethod + def _geometry_ugrid_cells(cls, f, domain_axis, cell_type, auto=False): + """Checks whether weights for geometry or UGRID cells can be created. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + domain_axis: `str` or `None` + If set to a domain axis identifier + (e.g. ``'domainaxis1'``) then only recognise cells + that span the given axis. If `None` then the cells may + span any axis. + + cell_type: `str` + Either ``'polygon'`` or ``'line'``. + + {{weights auto: `bool`, optional}} + + :Returns: + + 5-`tuple` + If no appropriate geometry/UGRID cells were found then + a `tuple` of all `None` is returned. Otherwise the + `tuple` comprises: + + * The domain axis identifier of the cells, or `None`. + * The X coordinate construct of *f*, or `None`. + * The Y coordinate construct of *f*, or `None`. + * The Z coordinate construct of *f*, or `None`. + * `True` if the cells are a UGRID mesh, `False` if + they are geometry cells, or `None`. + + """ + n_return = 5 + aux_X = None + aux_Y = None + aux_Z = None + x_axis = None + y_axis = None + z_axis = None + ugrid = None + + if cell_type == "polygon": + ugrid_cell_type = "face" + else: + ugrid_cell_type = "edge" + + auxiliary_coordinates_1d = f.auxiliary_coordinates( + filter_by_naxes=(1,), todict=True + ) + + for key, aux in auxiliary_coordinates_1d.items(): + aux_axis = f.get_data_axes(key)[0] + + ugrid = f.domain_topology(default=None, filter_by_axis=(aux_axis,)) + if ugrid is not None: + cell = ugrid.get_cell(None) + else: + cell = None + + if not ( + cell == ugrid_cell_type or aux.get_geometry(None) == cell_type + ): + continue + + # Still here? Then this auxiliary coordinate has either UGRID + # or geometry cells. + if aux.X: + aux_X = aux.copy() + x_axis = aux_axis + if domain_axis is not None and x_axis != domain_axis: + aux_X = None + continue + elif aux.Y: + aux_Y = aux.copy() + y_axis = aux_axis + if domain_axis is not None and y_axis != domain_axis: + aux_Y = None + continue + elif aux.Z: + aux_Z = aux.copy() + z_axis = aux_axis + if domain_axis is not None and z_axis != domain_axis: + aux_Z = None + continue + + if aux_X is None or aux_Y is None: + if auto: + return (None,) * n_return + + raise ValueError( + "Can't create weights: Need both X and Y nodes to " + f"calculate {cell_type} cell weights" + ) + + if x_axis != y_axis: + if auto: + return (None,) * n_return + + raise ValueError( + "Can't create weights: X and Y cells span different domain " + "axes" + ) + + axis = x_axis + + if aux_X.get_bounds(None) is None or aux_Y.get_bounds(None) is None: + # Not both X and Y coordinates have bounds + if auto: + return (None,) * n_return + + raise ValueError( + "Can't create weights: Not both X and Y coordinates have " + "bounds" + ) + + if aux_X.bounds.shape != aux_Y.bounds.shape: + if auto: + return (None,) * n_return + + raise ValueError( + "Can't create weights: UGRID/geometry X and Y coordinate " + "bounds must have the same shape. " + f"Got {aux_X.bounds.shape} and {aux_Y.bounds.shape}" + ) + + if aux_Z is None: + for key, aux in auxiliary_coordinates_1d.items(): + if aux.Z: + aux_Z = aux.copy() + z_axis = f.get_data_axes(key)[0] + + # Check Z coordinates + if aux_Z is not None: + if z_axis != x_axis: + if auto: + return (None,) * n_return + + raise ValueError( + "Z coordinates span different domain axis to X and Y " + "geometry coordinates" + ) + + return axis, aux_X, aux_Y, aux_Z, bool(ugrid) + + @classmethod + def _spherical_area_measure(cls, f, areas, aux_Z, radius=None): + """Convert spherical polygon weights to cell measures. + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + areas: `Data` + The area of the polygon cells on the unit sphere, in + units of square metres. + + aux_Z: `AuxiliaryCoordinate` + The Z coordinate construct of *f*. + + {{radius: optional}} + + :Returns: + + `Data` + The area of each polygon on the surface of the defined + sphere, in units of square metres. + + """ + # Multiply by radius squared, accounting for any Z + # coordinates, to get the actual area + from .units import Units + + radius = f.radius(default=radius) + if aux_Z is None: + # No Z coordinates + r = radius + else: + z = aux_Z.get_data(None, _fill_value=False) + if z is None: + # No Z coordinates + r = radius + else: + if not z.Units.equivalent(Units("metres")): + raise ValueError( + "Z coordinates must have units equivalent to " + f"metres for area calculations. Got {z.Units!r}" + ) + + positive = aux_Z.get_property("positive", None) + if positive is None: + raise ValueError( + "Z coordinate 'positive' property is not defined" + ) + + if positive.lower() == "up": + r = radius + z + elif positive.lower() == "down": + r = radius - z + else: + raise ValueError( + "Bad value of Z coordinate 'positive' property: " + f"{positive!r}." + ) + + r = r.override_units(Units("1")) + areas = areas * r**2 + return areas + + @classmethod + def _plane_polygon_areas(cls, x, y): + r"""Calculate the areas of plane polygons. + + The area, A, of a plane polygon is given by the shoelace + formula: + + A={\frac {1}{2}}\sum _{i=1}^{n}x_{i}(y_{i+1}-y_{i-1})} + + (https://en.wikipedia.org/wiki/Shoelace_formula). + + The formula gives a positive area for polygon nodes stored in + anticlockwise order as viewed from above, and a negative area + for polygon nodes stored in clockwise order. Note that + interior ring polygons are stored in clockwise order. + + .. versionadded:: 3.16.0 + + :Parameters: + + x: `Data` + The X coordinates of the polygon nodes, with no + duplication of the first and last nodes (i.e. the + polygons are represented by ``N`` values, where + ``N`` is the number of edges). + + y: `Data` + The Y coordinates of the polygon nodes, with + wrap-around duplication of the first and last nodes + (i.e. the polygons are represented by ``N + 2`` + values, where ``N`` is the number of edges). + + :Returns: + + `Data` + The area of each plane polygon defined by the trailing + dimensions of *x* and *y*, in units of square metres. + + """ + areas = 0.5 * (x * (y[..., 2:] - y[..., :-2])).sum(-1, squeeze=True) + return areas + + @classmethod + def _spherical_polygon_areas(cls, f, x, y, N, interior_rings=None): + r"""Calculate the areas of polygons on the unit sphere. + + The area, A, of a polygon on the unit sphere, whose sides are + great circles, is given by (Todhunter): + + A=\left(\sum _{n=1}^{N}A_{n}\right)-(N-2)\pi + + where A_{n} is the n-th interior angle, and N is the number of + sides (https://en.wikipedia.org/wiki/Spherical_trigonometry). + + .. versionadded:: 3.16.0 + + :Parameters: + + f: `Field` + The field for which the weights are being created. + + x: array_like + The X coordinates of the polygon nodes, with + wrap-around duplication of the first and last nodes + (i.e. the polygons are represented by ``N + 2`` + values, where ``N`` is the number of edges). + + y: array_like + The Y coordinates of the polygon nodes, with + wrap-around duplication of the first and last nodes + (i.e. the polygons are represented by ``N + 2`` + values, where ``N`` is the number of edges). + + N: array_like + The number of edges in each polygon. + + interior_rings: array_like, optional + The interior ring indicators for parts of polygon + geometry cells. If set must have shape + ``x.shape[:-1]``. + + :Returns: + + `Data` + The area on the unit sphere of each polygon defined by + the trailing dimensions of *x* and *y*, in units of + square metres. + + """ + from numpy import pi + + from .units import Units + + interior_angles = cls._interior_angles(f, x, y, interior_rings) + areas = interior_angles.sum(-1, squeeze=True) - (N - 2) * pi + areas.override_units(Units("m2"), inplace=True) + return areas diff --git a/docs/source/class/cf.AuxiliaryCoordinate.rst b/docs/source/class/cf.AuxiliaryCoordinate.rst index f8e390faf5..4cc7dae7e0 100644 --- a/docs/source/class/cf.AuxiliaryCoordinate.rst +++ b/docs/source/class/cf.AuxiliaryCoordinate.rst @@ -492,7 +492,14 @@ NetCDF ~cf.AuxiliaryCoordinate.nc_del_variable ~cf.AuxiliaryCoordinate.nc_get_variable ~cf.AuxiliaryCoordinate.nc_has_variable - ~cf.AuxiliaryCoordinate.nc_set_variable + ~cf.AuxiliaryCoordinate.nc_set_variable + ~cf.AuxiliaryCoordinate.nc_del_node_coordinate_variable + ~cf.AuxiliaryCoordinate.nc_get_node_coordinate_variable + ~cf.AuxiliaryCoordinate.nc_has_node_coordinate_variable + ~cf.AuxiliaryCoordinate.nc_node_coordinate_variable_groups + ~cf.AuxiliaryCoordinate.nc_set_node_coordinate_variable + ~cf.AuxiliaryCoordinate.nc_set_node_coordinate_variable_groups + ~cf.AuxiliaryCoordinate.nc_clear_node_coordinate_variable_groups Groups ^^^^^^ diff --git a/docs/source/class/cf.Constructs.rst b/docs/source/class/cf.Constructs.rst index 2473606dad..05caf74fc8 100644 --- a/docs/source/class/cf.Constructs.rst +++ b/docs/source/class/cf.Constructs.rst @@ -32,6 +32,8 @@ Filtering ~cf.Constructs.filter_by_key ~cf.Constructs.filter_by_ncdim ~cf.Constructs.filter_by_ncvar + ~cf.Constructs.filter_by_cell + ~cf.Constructs.filter_by_connectivity ~cf.Constructs.filters_applied ~cf.Constructs.clear_filters_applied ~cf.Constructs.inverse_filter diff --git a/docs/source/class/cf.Data.rst b/docs/source/class/cf.Data.rst index ae6c6098b6..72e099bde8 100644 --- a/docs/source/class/cf.Data.rst +++ b/docs/source/class/cf.Data.rst @@ -19,7 +19,6 @@ Inspection :template: attribute.rst ~cf.Data.array - ~cf.Data.varray ~cf.Data.dtype ~cf.Data.ndim ~cf.Data.shape @@ -28,6 +27,8 @@ Inspection ~cf.Data.dump ~cf.Data.inspect ~cf.Data.isscalar + ~cf.Data.sparse_array + Units ----- @@ -324,6 +325,7 @@ Mask support ~cf.Data.filled ~cf.Data.harden_mask ~cf.Data.masked_invalid + ~cf.Data.masked_values ~cf.Data.del_fill_value ~cf.Data.get_fill_value ~cf.Data.has_fill_value @@ -360,7 +362,7 @@ Trigonometric functions ~cf.Data.arcsin ~cf.Data.arccos ~cf.Data.arctan -.. ~cf.Data.arctan2 [AT2] + ~cf.Data.arctan2 Hyperbolic functions ^^^^^^^^^^^^^^^^^^^^ @@ -877,3 +879,5 @@ Deprecated :template: attribute.rst ~cf.Data.ispartitioned + ~cf.Data.varray + diff --git a/docs/source/class/cf.Domain.rst b/docs/source/class/cf.Domain.rst index 65ea957db8..526700e480 100644 --- a/docs/source/class/cf.Domain.rst +++ b/docs/source/class/cf.Domain.rst @@ -119,12 +119,23 @@ Metadata constructs :template: method.rst ~cf.Domain.auxiliary_coordinates + ~cf.Domain.auxiliary_coordinate + ~cf.Domain.cell_connectivities + ~cf.Domain.cell_connectivity ~cf.Domain.cell_measures + ~cf.Domain.cell_measure ~cf.Domain.coordinates + ~cf.Domain.coordinate ~cf.Domain.coordinate_references + ~cf.Domain.coordinate_reference ~cf.Domain.dimension_coordinates + ~cf.Domain.dimension_coordinate ~cf.Domain.domain_ancillaries + ~cf.Domain.domain_ancillary ~cf.Domain.domain_axes + ~cf.Domain.domain_axis + ~cf.Domain.domain_topologies + ~cf.Domain.domain_topology ~cf.Domain.construct ~cf.Domain.construct_item ~cf.Domain.construct_key @@ -137,19 +148,12 @@ Metadata constructs ~cf.Domain.get_data_axes ~cf.Domain.has_data_axes ~cf.Domain.set_data_axes - ~cf.Domain.auxiliary_coordinate ~cf.Domain.auxiliary_to_dimension - ~cf.Domain.cell_measure - ~cf.Domain.coordinate - ~cf.Domain.coordinate_reference + ~cf.Domain.dimension_to_auxiliary ~cf.Domain.coordinate_reference_domain_axes ~cf.Domain.get_coordinate_reference ~cf.Domain.set_coordinate_reference ~cf.Domain.del_coordinate_reference - ~cf.Domain.dimension_coordinate - ~cf.Domain.dimension_to_auxiliary - ~cf.Domain.domain_ancillary - ~cf.Domain.domain_axis ~cf.Domain.domain_axis_key ~cf.Domain.del_domain_axis ~cf.Domain.climatological_time_axes diff --git a/docs/source/class/cf.Field.rst b/docs/source/class/cf.Field.rst index 76f616f30e..30bf221f5b 100644 --- a/docs/source/class/cf.Field.rst +++ b/docs/source/class/cf.Field.rst @@ -272,14 +272,27 @@ Metadata constructs :template: method.rst ~cf.Field.auxiliary_coordinates + ~cf.Field.auxiliary_coordinate + ~cf.Field.cell_connectivities + ~cf.Field.cell_connectivity ~cf.Field.cell_measures + ~cf.Field.cell_measure ~cf.Field.cell_methods + ~cf.Field.cell_method ~cf.Field.coordinates + ~cf.Field.coordinate ~cf.Field.coordinate_references + ~cf.Field.coordinate_reference ~cf.Field.dimension_coordinates + ~cf.Field.dimension_coordinate ~cf.Field.domain_ancillaries + ~cf.Field.domain_ancillary ~cf.Field.domain_axes + ~cf.Field.domain_axis + ~cf.Field.domain_topologies + ~cf.Field.domain_topology ~cf.Field.field_ancillaries + ~cf.Field.field_ancillary ~cf.Field.construct ~cf.Field.construct_item ~cf.Field.construct_key @@ -292,25 +305,16 @@ Metadata constructs ~cf.Field.get_data_axes ~cf.Field.has_data_axes ~cf.Field.set_data_axes - ~cf.Field.auxiliary_coordinate ~cf.Field.auxiliary_to_dimension - ~cf.Field.cell_measure - ~cf.Field.cell_method - ~cf.Field.coordinate - ~cf.Field.coordinate_reference + ~cf.Field.dimension_to_auxiliary ~cf.Field.coordinate_reference_domain_axes ~cf.Field.get_coordinate_reference ~cf.Field.set_coordinate_reference ~cf.Field.del_coordinate_reference - ~cf.Field.dimension_coordinate - ~cf.Field.dimension_to_auxiliary - ~cf.Field.domain_ancillary - ~cf.Field.domain_axis ~cf.Field.domain_axis_key ~cf.Field.domain_axis_position ~cf.Field.domain_mask ~cf.Field.del_domain_axis - ~cf.Field.field_ancillary ~cf.Field.map_axes ~cf.Field.climatological_time_axes diff --git a/docs/source/class/cf.GatheredArray.rst b/docs/source/class/cf.GatheredArray.rst index 61b55b93af..f6693a2a33 100644 --- a/docs/source/class/cf.GatheredArray.rst +++ b/docs/source/class/cf.GatheredArray.rst @@ -33,6 +33,7 @@ cf.GatheredArray ~cf.GatheredArray.source ~cf.GatheredArray.subarray_shapes ~cf.GatheredArray.subarrays + ~cf.GatheredArray.subarray_parameters ~cf.GatheredArray.to_dask_array ~cf.GatheredArray.to_memory diff --git a/docs/source/class/cf.RaggedContiguousArray.rst b/docs/source/class/cf.RaggedContiguousArray.rst index 4c3dd71c07..e1f6fd8dc1 100644 --- a/docs/source/class/cf.RaggedContiguousArray.rst +++ b/docs/source/class/cf.RaggedContiguousArray.rst @@ -35,6 +35,7 @@ cf.RaggedContiguousArray ~cf.RaggedContiguousArray.source ~cf.RaggedContiguousArray.subarray_shapes ~cf.RaggedContiguousArray.subarrays + ~cf.RaggedContiguousArray.subarray_parameters ~cf.RaggedContiguousArray.to_dask_array ~cf.RaggedContiguousArray.to_memory diff --git a/docs/source/class/cf.RaggedIndexedArray.rst b/docs/source/class/cf.RaggedIndexedArray.rst index b1853cfc11..abc4edc74a 100644 --- a/docs/source/class/cf.RaggedIndexedArray.rst +++ b/docs/source/class/cf.RaggedIndexedArray.rst @@ -34,6 +34,7 @@ cf.RaggedIndexedArray ~cf.RaggedIndexedArray.source ~cf.RaggedIndexedArray.subarray_shapes ~cf.RaggedIndexedArray.subarrays + ~cf.RaggedIndexedArray.subarray_parameters ~cf.RaggedIndexedArray.to_dask_array ~cf.RaggedIndexedArray.to_memory diff --git a/docs/source/class/cf.RaggedIndexedContiguousArray.rst b/docs/source/class/cf.RaggedIndexedContiguousArray.rst index 4d913bc082..af27e95277 100644 --- a/docs/source/class/cf.RaggedIndexedContiguousArray.rst +++ b/docs/source/class/cf.RaggedIndexedContiguousArray.rst @@ -34,6 +34,7 @@ cf.RaggedIndexedContiguousArray ~cf.RaggedIndexedContiguousArray.source ~cf.RaggedIndexedContiguousArray.subarray_shapes ~cf.RaggedIndexedContiguousArray.subarrays + ~cf.RaggedIndexedContiguousArray.subarray_parameters ~cf.RaggedIndexedContiguousArray.to_dask_array ~cf.RaggedIndexedContiguousArray.to_memory diff --git a/docs/source/class/cf.RegridOperator.rst b/docs/source/class/cf.RegridOperator.rst index d8361bbd98..dfe5b47d56 100644 --- a/docs/source/class/cf.RegridOperator.rst +++ b/docs/source/class/cf.RegridOperator.rst @@ -44,6 +44,7 @@ cf.RegridOperator ~cf.RegridOperator.src_cyclic ~cf.RegridOperator.src_mask ~cf.RegridOperator.src_shape + ~cf.RegridOperator.src_mesh_location ~cf.RegridOperator.start_index ~cf.RegridOperator.weights ~cf.RegridOperator.weights_file diff --git a/docs/source/field_analysis.rst b/docs/source/field_analysis.rst index e29f4baebb..c3ea848470 100644 --- a/docs/source/field_analysis.rst +++ b/docs/source/field_analysis.rst @@ -1357,18 +1357,27 @@ Spherical source domain Spherical destination domain `Latitude-longitude`_ `Rotated latitude-longitude`_ `Latitude-longitude`_ `Plane projection`_ `Latitude-longitude`_ `Tripolar`_ +`Latitude-longitude`_ `UGRID mesh`_ `Rotated latitude-longitude`_ `Latitude-longitude`_ `Rotated latitude-longitude`_ `Rotated latitude-longitude`_ `Rotated latitude-longitude`_ `Plane projection`_ `Rotated latitude-longitude`_ `Tripolar`_ +`Rotated latitude-longitude`_ `UGRID mesh`_ `Plane projection`_ `Latitude-longitude`_ `Plane projection`_ `Rotated latitude-longitude`_ `Plane projection`_ `Plane projection`_ `Plane projection`_ `Tripolar`_ +`Plane projection`_ `UGRID mesh`_ `Tripolar`_ `Latitude-longitude`_ `Tripolar`_ `Rotated latitude-longitude`_ `Tripolar`_ `Plane projection`_ `Tripolar`_ `Tripolar`_ +`Tripolar`_ `UGRID mesh`_ +`UGRID mesh`_ `Latitude-longitude`_ +`UGRID mesh`_ `Rotated latitude-longitude`_ +`UGRID mesh`_ `Plane projection`_ +`UGRID mesh`_ `Tripolar`_ +`UGRID mesh`_ `UGRID mesh`_ ============================== ============================== The most convenient usage is when the destination domain exists diff --git a/docs/source/installation.rst b/docs/source/installation.rst index e6a335077b..e744fd23ca 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -201,8 +201,10 @@ Required * `cftime `_, version 1.6.2 or newer (note that this package may be installed with netCDF4). -* `cfdm `_, version 1.10.1.2 or up to, - but not including, 1.10.2.0. +* `scipy `_, version 1.10.0 or newer. + +* `cfdm `_, version 1.11.0.0 or up to, + but not including, 1.11.1.0. * `cfunits `_, version 3.3.6 or newer. @@ -253,10 +255,6 @@ environments for which these features are not required. or may be installed from source. -.. rubric:: Convolution filters, derivatives and relative vorticity - -* `scipy `_, version 1.10.0 or newer. - .. rubric:: Subspacing based on N-dimensional construct cells (N > 1) containing a given value diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index b3a715296a..a1941fbc8d 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -120,7 +120,8 @@ elements. The following file types can be read: * All formats of netCDF3 and netCDF4 files can be read, containing - datasets for any version of CF up to and including CF-|version|. + datasets for any version of CF up to and including CF-|version|, + including :ref:`UGRID ` datasets. .. @@ -2671,6 +2672,45 @@ CF-netCDF geometry container variable is automatically created, and the cells encoded with the :ref:`compression ` techniques defined in the CF conventions. +---- + +.. _UGRID-mesh-topologies: + +**UGRID mesh topologies** +------------------------- + +A `UGRID_` mesh topology defines the geospatial topology of cells +arranged in two or three dimensions in real space but indexed by a +single dimension. It explicitly describes the topological +relationships between cells, i.e. spatial relationships which do not +depend on the cell locations, via a mesh of connected nodes. See the +`domain topology construct`_ and `cell connectivity construct`_ +descriptions in the CF data model (from CF-1.11) for more details, +including on how the mesh relates to the cells of the domain. + +.. code-block:: python + :caption: *Inspect a dataset containing a UGRID mesh topology.* + + >>> f = cf.example_field(8) + >>> print(f) + Field: air_temperature (ncvar%ta) + --------------------------------- + Data : air_temperature(time(2), ncdim%nMesh2_face(3)) K + Cell methods : time(2): point (interval: 3600 s) + Dimension coords: time(2) = [2016-01-02 01:00:00, 2016-01-02 11:00:00] gregorian + Auxiliary coords: longitude(ncdim%nMesh2_face(3)) = [-44.0, -44.0, -42.0] degrees_east + : latitude(ncdim%nMesh2_face(3)) = [34.0, 32.0, 34.0] degrees_north + Domain Topology : cell:face(ncdim%nMesh2_face(3), 4) = [[2, ..., --]] + Cell connects : connectivity:edge(ncdim%nMesh2_face(3), 5) = [[0, ..., --]] + +.. + COMMENTED OUT UNTIL THIS WORKS! When a field construct containing a + UGRID mesh topology is written to disk, a CF-netCDF UGRID mesh + topology variable is automatically created which will be shared by + any data variables that can make use of the same mesh. + +---- + .. _Domain-ancillaries: Domain ancillaries @@ -6992,3 +7032,4 @@ if any, are filtered out. .. _geometries: http://cfconventions.org/cf-conventions/cf-conventions.html#geometries .. _Hierarchical groups: http://cfconventions.org/cf-conventions/cf-conventions.html#groups .. _Lossy compression by coordinate subsampling: http://cfconventions.org/cf-conventions/cf-conventions.html#compression-by-coordinate-subsampling +.. _UGRID: https://cfconventions.org/cf-conventions/cf-conventions.html#ugrid-conventions diff --git a/requirements.txt b/requirements.txt index 3c5ef46ec4..a10ba9f71a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ netCDF4>=1.5.4 cftime>=1.6.2 numpy>=1.22 -cfdm>=1.10.1.2, <1.10.2.0 +cfdm>=1.11.0.0, <1.11.1.0 psutil>=0.6.0 cfunits>=3.3.6 dask>=2022.12.1 packaging>=20.0 +scipy>=1.10.0