From 6bbf0f55e912dedd24aaef02b5e7b5b1fa1aaeed Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 22 Oct 2020 18:37:14 +0100 Subject: [PATCH 01/13] Factor out cube.summary dim-naming + remove code for (nonexistent) multi-dim dim-coords. --- lib/iris/cube.py | 46 ++++++++++++++++--------- lib/iris/experimental/representation.py | 25 +++++--------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 964e56c313..95adf24884 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2263,35 +2263,46 @@ def _summary_extra(self, coords, summary, indent): new_summary.append(extra) return new_summary - def summary(self, shorten=False, name_padding=35): + def _summary_dim_name(self, dim): """ - Unicode string summary of the Cube with name, a list of dim coord names - versus length and optionally relevant coordinate information. + Get the dim_coord name that labels a data dimension, + or "--" if there is none (i.e. dimension is anonymous). """ - # Create a set to contain the axis names for each data dimension. - dim_names = [set() for dim in range(len(self.shape))] + dim_coords = self.coords(contains_dimension=dim, dim_coords=True) - # Add the dim_coord names that participate in the associated data - # dimensions. - for dim in range(len(self.shape)): - dim_coords = self.coords(contains_dimension=dim, dim_coords=True) - if dim_coords: - dim_names[dim].add(dim_coords[0].name()) - else: - dim_names[dim].add("-- ") + if len(dim_coords) == 1: + # Only 1 dimension coordinate can map to a dimension. + name = dim_coords[0].name() # N.B. can only be 1 + elif len(dim_coords) == 0: + # Anonymous dimension : NOTE this has an extra space on the end. + name = "-- " + else: + # >1 matching dim coord : This really can't happen ! + names_list = [co.name() for co in dim_coords] + msg = ( + "Cube logic error : " + "multiple dimension coords map to " + "dimension {dim} : {names!r}" + ) + raise ValueError(msg.format(dim=dim, names=names_list)) - # Convert axes sets to lists and sort. - dim_names = [sorted(names, key=sorted_axes) for names in dim_names] + return name + def summary(self, shorten=False, name_padding=35): + """ + Unicode string summary of the Cube with name, a list of dim coord names + versus length and optionally relevant coordinate information. + + """ # Generate textual summary of the cube dimensionality. if self.shape == (): dimension_header = "scalar cube" else: dimension_header = "; ".join( [ - ", ".join(dim_names[dim]) + ": %d" % dim_shape - for dim, dim_shape in enumerate(self.shape) + self._summary_dim_name(dim) + ": %d" % dim_len + for dim, dim_len in enumerate(self.shape) ] ) @@ -2301,6 +2312,7 @@ def summary(self, shorten=False, name_padding=35): cube_header = "{nameunit!s:{length}} ({dimension})".format( length=name_padding, nameunit=nameunit, dimension=dimension_header ) + summary = "" # Generate full cube textual summary. diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py index c33c162d4c..72ec5e3e29 100644 --- a/lib/iris/experimental/representation.py +++ b/lib/iris/experimental/representation.py @@ -117,25 +117,16 @@ def __init__(self, cube): def _get_dim_names(self): """ - Get dimension-describing coordinate names, or '--' if no coordinate] - describes the dimension. - - Note: borrows from `cube.summary`. + Get all the dimension-describing coordinate names, as used by + cube.summary. """ - # Create a set to contain the axis names for each data dimension. - dim_names = list(range(len(self.cube.shape))) - - # Add the dim_coord names that participate in the associated data - # dimensions. - for dim in range(len(self.cube.shape)): - dim_coords = self.cube.coords( - contains_dimension=dim, dim_coords=True - ) - if dim_coords: - dim_names[dim] = dim_coords[0].name() - else: - dim_names[dim] = "--" + # NOTE: use ".strip()" on these, because the name of an anonymous dim + # has an unwanted extra space at the end, i.e. "-- ". + dim_names = [ + self.cube._summary_dim_name(dim).strip() + for dim in range(len(self.cube.shape)) + ] return dim_names def _dim_names(self): From 6deba68be39788dc607edbbd35d261c7d8d26844 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 22 Oct 2020 19:02:15 +0100 Subject: [PATCH 02/13] Import cube.summary rework from #3688, but omitting ucube-specific parts. --- lib/iris/cube.py | 124 +++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 70 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 95adf24884..9589b6f69f 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2376,17 +2376,17 @@ def summary(self, shorten=False, name_padding=35): ) # - # Generate textual summary of cube vector coordinates. + # Generate textual summary of cube vector coordinates, cell measures, ancillary variables and ugrid_mesh. # def vector_summary( - vector_coords, + vector_items, + dim_function, cube_header, max_line_offset, - cell_measures=None, - ancillary_variables=None, + add_extra_lines=False, ): """ - Generates a list of suitably aligned strings containing coord + Generates a list of suitably aligned strings containing item names and dimensions indicated by one or more 'x' symbols. .. note:: @@ -2395,12 +2395,7 @@ def vector_summary( returned with the list of strings. """ - if cell_measures is None: - cell_measures = [] - if ancillary_variables is None: - ancillary_variables = [] vector_summary = [] - vectors = [] # Identify offsets for each dimension text marker. alignment = np.array( @@ -2411,11 +2406,9 @@ def vector_summary( ] ) - # Generate basic textual summary for each vector coordinate + # Generate basic textual summary for each vector item # - WITHOUT dimension markers. - for dim_meta in ( - vector_coords + cell_measures + ancillary_variables - ): + for dim_meta in vector_items: vector_summary.append( "%*s%s" % (indent, " ", iris.util.clip_string(dim_meta.name())) @@ -2423,7 +2416,7 @@ def vector_summary( min_alignment = min(alignment) # Determine whether the cube header requires realignment - # due to one or more longer vector coordinate summaries. + # due to one or more longer vector item summaries. if max_line_offset >= min_alignment: delta = max_line_offset - min_alignment + 5 cube_header = "%-*s (%s)" % ( @@ -2433,59 +2426,38 @@ def vector_summary( ) alignment += delta - if vector_coords: - # Generate full textual summary for each vector coordinate - # - WITH dimension markers. - for index, coord in enumerate(vector_coords): - dims = self.coord_dims(coord) - - for dim in range(len(self.shape)): - width = alignment[dim] - len(vector_summary[index]) - char = "x" if dim in dims else "-" - line = "{pad:{width}}{char}".format( - pad=" ", width=width, char=char - ) - vector_summary[index] += line - vectors = vectors + vector_coords - if cell_measures: - # Generate full textual summary for each vector cell - # measure - WITH dimension markers. - for index, cell_measure in enumerate(cell_measures): - dims = self.cell_measure_dims(cell_measure) - - for dim in range(len(self.shape)): - width = alignment[dim] - len(vector_summary[index]) - char = "x" if dim in dims else "-" - line = "{pad:{width}}{char}".format( - pad=" ", width=width, char=char - ) - vector_summary[index] += line - vectors = vectors + cell_measures - if ancillary_variables: - # Generate full textual summary for each vector ancillary - # variable - WITH dimension markers. - for index, av in enumerate(ancillary_variables): - dims = self.ancillary_variable_dims(av) - - for dim in range(len(self.shape)): - width = alignment[dim] - len(vector_summary[index]) - char = "x" if dim in dims else "-" - line = "{pad:{width}}{char}".format( - pad=" ", width=width, char=char - ) - vector_summary[index] += line - vectors = vectors + ancillary_variables - # Interleave any extra lines that are needed to distinguish - # the coordinates. - vector_summary = self._summary_extra( - vectors, vector_summary, extra_indent - ) + # Generate full textual summary for each vector item + # - WITH dimension markers. + for index, coord in enumerate(vector_items): + dims = dim_function(coord) + + for dim in range(len(self.shape)): + width = alignment[dim] - len(vector_summary[index]) + char = "x" if dim in dims else "-" + line = "{pad:{width}}{char}".format( + pad=" ", width=width, char=char + ) + vector_summary[index] += line + + if add_extra_lines: + # Interleave any extra lines that are needed to distinguish + # the coordinates. + # TODO: This should also be done for cell measures and + # ancillary variables. + vector_summary = self._summary_extra( + vector_items, vector_summary, extra_indent + ) return vector_summary, cube_header # Calculate the maximum line offset. max_line_offset = 0 - for coord in all_coords: + dimension_metadata_to_check = ( + list(all_coords) + + vector_cell_measures + + vector_ancillary_variables + ) + for coord in dimension_metadata_to_check: max_line_offset = max( max_line_offset, len( @@ -2500,7 +2472,11 @@ def vector_summary( if vector_dim_coords: dim_coord_summary, cube_header = vector_summary( - vector_dim_coords, cube_header, max_line_offset + vector_dim_coords, + self.coord_dims, + cube_header, + max_line_offset, + add_extra_lines=True, ) summary += "\n Dimension coordinates:\n" + "\n".join( dim_coord_summary @@ -2508,7 +2484,11 @@ def vector_summary( if vector_aux_coords: aux_coord_summary, cube_header = vector_summary( - vector_aux_coords, cube_header, max_line_offset + vector_aux_coords, + self.coord_dims, + cube_header, + max_line_offset, + add_extra_lines=True, ) summary += "\n Auxiliary coordinates:\n" + "\n".join( aux_coord_summary @@ -2516,7 +2496,11 @@ def vector_summary( if vector_derived_coords: derived_coord_summary, cube_header = vector_summary( - vector_derived_coords, cube_header, max_line_offset + vector_derived_coords, + self.coord_dims, + cube_header, + max_line_offset, + add_extra_lines=True, ) summary += "\n Derived coordinates:\n" + "\n".join( derived_coord_summary @@ -2527,10 +2511,10 @@ def vector_summary( # if vector_cell_measures: cell_measure_summary, cube_header = vector_summary( - [], + vector_cell_measures, + self.cell_measure_dims, cube_header, max_line_offset, - cell_measures=vector_cell_measures, ) summary += "\n Cell measures:\n" summary += "\n".join(cell_measure_summary) @@ -2540,10 +2524,10 @@ def vector_summary( # if vector_ancillary_variables: ancillary_variable_summary, cube_header = vector_summary( - [], + vector_ancillary_variables, + self.ancillary_variable_dims, cube_header, max_line_offset, - ancillary_variables=vector_ancillary_variables, ) summary += "\n Ancillary variables:\n" summary += "\n".join(ancillary_variable_summary) From 40055efe718873db1f6344f3657803afe5a52be9 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 23 Oct 2020 11:51:06 +0100 Subject: [PATCH 03/13] Rename 'vector_summary' result variable of same-named function. --- lib/iris/cube.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 9589b6f69f..aa862023f6 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2395,7 +2395,7 @@ def vector_summary( returned with the list of strings. """ - vector_summary = [] + summary_lines = [] # Identify offsets for each dimension text marker. alignment = np.array( @@ -2409,7 +2409,7 @@ def vector_summary( # Generate basic textual summary for each vector item # - WITHOUT dimension markers. for dim_meta in vector_items: - vector_summary.append( + summary_lines.append( "%*s%s" % (indent, " ", iris.util.clip_string(dim_meta.name())) ) @@ -2432,23 +2432,23 @@ def vector_summary( dims = dim_function(coord) for dim in range(len(self.shape)): - width = alignment[dim] - len(vector_summary[index]) + width = alignment[dim] - len(summary_lines[index]) char = "x" if dim in dims else "-" line = "{pad:{width}}{char}".format( pad=" ", width=width, char=char ) - vector_summary[index] += line + summary_lines[index] += line if add_extra_lines: # Interleave any extra lines that are needed to distinguish # the coordinates. # TODO: This should also be done for cell measures and # ancillary variables. - vector_summary = self._summary_extra( - vector_items, vector_summary, extra_indent + summary_lines = self._summary_extra( + vector_items, summary_lines, extra_indent ) - return vector_summary, cube_header + return summary_lines, cube_header # Calculate the maximum line offset. max_line_offset = 0 From dee113305b683c962a5dcd70e226eefb91d581bb Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 23 Oct 2020 12:33:24 +0100 Subject: [PATCH 04/13] Use generic DimensionalMetadata.cube_dims in place of 'dim_function' arg. --- lib/iris/cube.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index aa862023f6..93a5660c48 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2380,7 +2380,6 @@ def summary(self, shorten=False, name_padding=35): # def vector_summary( vector_items, - dim_function, cube_header, max_line_offset, add_extra_lines=False, @@ -2428,8 +2427,8 @@ def vector_summary( # Generate full textual summary for each vector item # - WITH dimension markers. - for index, coord in enumerate(vector_items): - dims = dim_function(coord) + for index, dim_meta in enumerate(vector_items): + dims = dim_meta.cube_dims(self) for dim in range(len(self.shape)): width = alignment[dim] - len(summary_lines[index]) @@ -2473,7 +2472,6 @@ def vector_summary( if vector_dim_coords: dim_coord_summary, cube_header = vector_summary( vector_dim_coords, - self.coord_dims, cube_header, max_line_offset, add_extra_lines=True, @@ -2485,7 +2483,6 @@ def vector_summary( if vector_aux_coords: aux_coord_summary, cube_header = vector_summary( vector_aux_coords, - self.coord_dims, cube_header, max_line_offset, add_extra_lines=True, @@ -2497,7 +2494,6 @@ def vector_summary( if vector_derived_coords: derived_coord_summary, cube_header = vector_summary( vector_derived_coords, - self.coord_dims, cube_header, max_line_offset, add_extra_lines=True, @@ -2511,10 +2507,7 @@ def vector_summary( # if vector_cell_measures: cell_measure_summary, cube_header = vector_summary( - vector_cell_measures, - self.cell_measure_dims, - cube_header, - max_line_offset, + vector_cell_measures, cube_header, max_line_offset, ) summary += "\n Cell measures:\n" summary += "\n".join(cell_measure_summary) @@ -2524,10 +2517,7 @@ def vector_summary( # if vector_ancillary_variables: ancillary_variable_summary, cube_header = vector_summary( - vector_ancillary_variables, - self.ancillary_variable_dims, - cube_header, - max_line_offset, + vector_ancillary_variables, cube_header, max_line_offset, ) summary += "\n Ancillary variables:\n" summary += "\n".join(ancillary_variable_summary) From 2de90c70d2e2deafce34ed1bf9b34c0b30b1e49d Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 23 Oct 2020 12:52:11 +0100 Subject: [PATCH 05/13] Replace boilerplate calls to 'vector_summary' with generic code controlled by a table. --- lib/iris/cube.py | 68 +++++++++++++----------------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 93a5660c48..724b58bfa8 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2469,58 +2469,26 @@ def vector_summary( ), ) - if vector_dim_coords: - dim_coord_summary, cube_header = vector_summary( - vector_dim_coords, - cube_header, - max_line_offset, - add_extra_lines=True, - ) - summary += "\n Dimension coordinates:\n" + "\n".join( - dim_coord_summary - ) - - if vector_aux_coords: - aux_coord_summary, cube_header = vector_summary( - vector_aux_coords, - cube_header, - max_line_offset, - add_extra_lines=True, - ) - summary += "\n Auxiliary coordinates:\n" + "\n".join( - aux_coord_summary - ) - - if vector_derived_coords: - derived_coord_summary, cube_header = vector_summary( - vector_derived_coords, - cube_header, - max_line_offset, - add_extra_lines=True, - ) - summary += "\n Derived coordinates:\n" + "\n".join( - derived_coord_summary - ) - - # - # Generate summary of cube cell measures attribute # - if vector_cell_measures: - cell_measure_summary, cube_header = vector_summary( - vector_cell_measures, cube_header, max_line_offset, - ) - summary += "\n Cell measures:\n" - summary += "\n".join(cell_measure_summary) - + # Generate textual summary for each type of vector component. # - # Generate summary of cube ancillary variables attribute - # - if vector_ancillary_variables: - ancillary_variable_summary, cube_header = vector_summary( - vector_ancillary_variables, cube_header, max_line_offset, - ) - summary += "\n Ancillary variables:\n" - summary += "\n".join(ancillary_variable_summary) + vector_summary_specs = [ + ("Dimension coordinates", vector_dim_coords, True), + ("Auxiliary coordinates", vector_aux_coords, True), + ("Derived coordinates", vector_derived_coords, True), + ("Cell measures", vector_cell_measures, False), + ("Ancillary variables", vector_ancillary_variables, False), + ] + for title, elements, add_extra_lines in vector_summary_specs: + if elements: + summary_lines, cube_header = vector_summary( + vector_items=elements, + cube_header=cube_header, + max_line_offset=max_line_offset, + add_extra_lines=add_extra_lines, + ) + summary += f"\n {title}:\n" + summary += "\n".join(summary_lines) # # Generate textual summary of cube scalar coordinates. From 988710ca9a50cb161bf40b5ac99931c42ee6632b Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 23 Oct 2020 13:04:20 +0100 Subject: [PATCH 06/13] Define cube summary 'vector sections' via a function, which iris-ugrid can override. --- lib/iris/cube.py | 193 ++++++++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 85 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 724b58bfa8..8f96408fe7 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2289,6 +2289,85 @@ def _summary_dim_name(self, dim): return name + _VectorSectionSpec = namedtuple( + "Spec", ("title", "elements", "add_extra_lines") + ) + + def _summary_vector_sections_info(self): + """ + Return a table describing the order and content of the 'vector' + sections of the cube summary. + + Returns: + + * summary_info (list of cube._VectorSectionSpec) + A list of summary sections info, in summary order. + + The table entries contain arguments to be passed to the + 'vector_summary' inner function, inside cube.summary(). + + Note: provided as a separate function so that derived cube types + (as in iris-ugrid) can override / extend the cube summary. + + """ + # Cache the derived coords so we can rely on consistent + # object IDs. + derived_coords = self.derived_coords + # Determine the cube coordinates that are scalar (single-valued) + # AND non-dimensioned. + dim_coords = self.dim_coords + aux_coords = self.aux_coords + all_coords = dim_coords + aux_coords + derived_coords + scalar_coords = [ + coord + for coord in all_coords + if not self.coord_dims(coord) and coord.shape == (1,) + ] + # Determine the cube coordinates that are not scalar BUT + # dimensioned. + scalar_coord_ids = set(map(id, scalar_coords)) + vector_dim_coords = [ + coord for coord in dim_coords if id(coord) not in scalar_coord_ids + ] + vector_aux_coords = [ + coord for coord in aux_coords if id(coord) not in scalar_coord_ids + ] + vector_derived_coords = [ + coord + for coord in derived_coords + if id(coord) not in scalar_coord_ids + ] + + # cell measures + vector_cell_measures = [ + cm for cm in self.cell_measures() if cm.shape != (1,) + ] + + # Ancillary Variables + vector_ancillary_variables = [av for av in self.ancillary_variables()] + + # Sort scalar coordinates by name. + scalar_coords.sort(key=lambda coord: coord.name()) + # Sort vector coordinates by data dimension and name. + vector_dim_coords.sort( + key=lambda coord: (self.coord_dims(coord), coord.name()) + ) + vector_aux_coords.sort( + key=lambda coord: (self.coord_dims(coord), coord.name()) + ) + vector_derived_coords.sort( + key=lambda coord: (self.coord_dims(coord), coord.name()) + ) + Spec = self._VectorSectionSpec + section_specs = [ + Spec("Dimension coordinates", vector_dim_coords, True), + Spec("Auxiliary coordinates", vector_aux_coords, True), + Spec("Derived coordinates", vector_derived_coords, True), + Spec("Cell measures", vector_cell_measures, False), + Spec("Ancillary variables", vector_ancillary_variables, False), + ] + return section_specs + def summary(self, shorten=False, name_padding=35): """ Unicode string summary of the Cube with name, a list of dim coord names @@ -2320,64 +2399,10 @@ def summary(self, shorten=False, name_padding=35): indent = 10 extra_indent = " " * 13 - # Cache the derived coords so we can rely on consistent - # object IDs. - derived_coords = self.derived_coords - # Determine the cube coordinates that are scalar (single-valued) - # AND non-dimensioned. - dim_coords = self.dim_coords - aux_coords = self.aux_coords - all_coords = dim_coords + aux_coords + derived_coords - scalar_coords = [ - coord - for coord in all_coords - if not self.coord_dims(coord) and coord.shape == (1,) - ] - # Determine the cube coordinates that are not scalar BUT - # dimensioned. - scalar_coord_ids = set(map(id, scalar_coords)) - vector_dim_coords = [ - coord - for coord in dim_coords - if id(coord) not in scalar_coord_ids - ] - vector_aux_coords = [ - coord - for coord in aux_coords - if id(coord) not in scalar_coord_ids - ] - vector_derived_coords = [ - coord - for coord in derived_coords - if id(coord) not in scalar_coord_ids - ] - - # cell measures - vector_cell_measures = [ - cm for cm in self.cell_measures() if cm.shape != (1,) - ] + # Fetch the vector sections information. + vector_summary_info = self._summary_vector_sections_info() - # Ancillary Variables - vector_ancillary_variables = [ - av for av in self.ancillary_variables() - ] - - # Sort scalar coordinates by name. - scalar_coords.sort(key=lambda coord: coord.name()) - # Sort vector coordinates by data dimension and name. - vector_dim_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - vector_aux_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - vector_derived_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - - # - # Generate textual summary of cube vector coordinates, cell measures, ancillary variables and ugrid_mesh. - # + # Support routine to format vector summary sections. def vector_summary( vector_items, cube_header, @@ -2451,35 +2476,24 @@ def vector_summary( # Calculate the maximum line offset. max_line_offset = 0 - dimension_metadata_to_check = ( - list(all_coords) - + vector_cell_measures - + vector_ancillary_variables - ) - for coord in dimension_metadata_to_check: - max_line_offset = max( - max_line_offset, - len( - "%*s%s" - % ( - indent, - " ", - iris.util.clip_string(str(coord.name())), - ) - ), - ) + for spec in vector_summary_info: + for element in spec.elements: + max_line_offset = max( + max_line_offset, + len( + "%*s%s" + % ( + indent, + " ", + iris.util.clip_string(str(element.name())), + ) + ), + ) # - # Generate textual summary for each type of vector component. + # Generate textual summaries of cube vector elements. # - vector_summary_specs = [ - ("Dimension coordinates", vector_dim_coords, True), - ("Auxiliary coordinates", vector_aux_coords, True), - ("Derived coordinates", vector_derived_coords, True), - ("Cell measures", vector_cell_measures, False), - ("Ancillary variables", vector_ancillary_variables, False), - ] - for title, elements, add_extra_lines in vector_summary_specs: + for title, elements, add_extra_lines in vector_summary_info: if elements: summary_lines, cube_header = vector_summary( vector_items=elements, @@ -2493,9 +2507,16 @@ def vector_summary( # # Generate textual summary of cube scalar coordinates. # + scalar_coords = [ + coord + for coord in self.coords() + if not self.coord_dims(coord) and coord.shape == (1,) + ] scalar_summary = [] - if scalar_coords: + scalar_coords = sorted( + scalar_coords, key=lambda coord: coord.name() + ) for coord in scalar_coords: if ( coord.units in ["1", "no_unit", "unknown"] @@ -2554,7 +2575,9 @@ def vector_summary( scalar_summary ) - # cell measures + # + # Generate summary of cube cell measures + # scalar_cell_measures = [ cm for cm in self.cell_measures() if cm.shape == (1,) ] From 60a359eefb70425774b7537dbbc0759a8ed28423 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sat, 24 Oct 2020 12:39:19 +0100 Subject: [PATCH 07/13] Vector section summary titles used by cube.repr_html() are drawn from the cube itself. --- lib/iris/experimental/representation.py | 28 ++++++++++--------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py index 72ec5e3e29..598942ee21 100644 --- a/lib/iris/experimental/representation.py +++ b/lib/iris/experimental/representation.py @@ -85,24 +85,18 @@ def __init__(self, cube): self.cube_id = id(self.cube) self.cube_str = escape(str(self.cube)) - self.str_headings = { - "Dimension coordinates:": None, - "Auxiliary coordinates:": None, - "Derived coordinates:": None, - "Cell measures:": None, - "Ancillary variables:": None, - "Scalar coordinates:": None, - "Scalar cell measures:": None, - "Attributes:": None, - "Cell methods:": None, - } - self.dim_desc_coords = [ - "Dimension coordinates:", - "Auxiliary coordinates:", - "Derived coordinates:", - "Cell measures:", - "Ancillary variables:", + vector_sections = cube._summary_vector_sections_info() + vector_headings = [spec.title for spec in vector_sections] + # TODO: this list of 'scalar summary sections' should probably also be + # defined by the cube itself. + all_headings = vector_headings + [ + "Scalar coordinates", + "Scalar cell measures", + "Attributes", + "Cell methods", ] + self.str_headings = {title + ":": None for title in all_headings} + self.dim_desc_coords = [title + ":" for title in vector_headings] self.two_cell_headers = ["Scalar coordinates:", "Attributes:"] From 48448394ddcce9a64c014bf52fc389e9d745f017 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 27 Oct 2020 15:00:10 +0000 Subject: [PATCH 08/13] Test for Cube._summary_dim_name(). --- lib/iris/tests/unit/cube/test_Cube.py | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 9c03f0f4d4..2593a4f9b4 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2434,5 +2434,39 @@ def test_cell_method_correct_order(self): self.assertTrue(cube1 == cube2) +class Test__summary_dim_name(tests.IrisTest): + def test_standard_named_dim(self): + cube = Cube(np.zeros((5, 3))) + cube.add_dim_coord( + DimCoord(np.arange(5), standard_name="longitude", units="degrees"), + 0, + ) + result = cube._summary_dim_name(0) + self.assertEqual(result, "longitude") + + def test_long_named_dim(self): + cube = Cube(np.zeros((5, 3))) + cube.add_dim_coord( + DimCoord(np.arange(5), long_name="alons", units="degrees"), 0 + ) + result = cube._summary_dim_name(0) + self.assertEqual(result, "alons") + + def test_unmapped_dim(self): + cube = Cube(np.zeros((5, 3))) + cube.add_dim_coord(DimCoord(np.arange(5), long_name="x", units=1), 0) + result = cube._summary_dim_name(1) + self.assertEqual(result, "-- ") + + def test_aux_named_dim(self): + cube = Cube(np.zeros((5, 3))) + cube.add_aux_coord( + DimCoord(np.arange(5), standard_name="longitude", units="degrees"), + 0, + ) + result = cube._summary_dim_name(0) + self.assertEqual(result, "-- ") + + if __name__ == "__main__": tests.main() From 2f7152880618989fd32bf5d5b0dd9ed0177bd163 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 27 Oct 2020 15:24:06 +0000 Subject: [PATCH 09/13] Tests for Cube._summary_vector_sections_info(). --- lib/iris/tests/unit/cube/test_Cube.py | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 2593a4f9b4..6699ecfada 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2468,5 +2468,92 @@ def test_aux_named_dim(self): self.assertEqual(result, "-- ") +class Test__summary_vector_sections_info(tests.IrisTest): + def test_section_titles(self): + cube = Cube([0, 1]) + result = cube._summary_vector_sections_info() + self.assertEqual(len(result), 5) + self.assertEqual( + [sect[0] for sect in result], + [ + "Dimension coordinates", + "Auxiliary coordinates", + "Derived coordinates", + "Cell measures", + "Ancillary variables", + ], + ) + + def test_extra_lines(self): + cube = Cube([0, 1]) + result = cube._summary_vector_sections_info() + self.assertEqual(len(result), 5) + self.assertEqual( + [sect[2] for sect in result], [True, True, True, False, False] + ) + + def test_dim_aux_coords(self): + cube = Cube(np.zeros((3, 5, 4))) + cube.add_dim_coord( + DimCoord(np.arange(3), long_name="lats", units="degrees"), 0 + ) + cube.add_aux_coord( + DimCoord(np.arange(5), standard_name="longitude", units="degrees"), + 1, + ) + cube.add_aux_coord( + AuxCoord(np.zeros((5, 4)), long_name="co2d", units=1), (1, 2), + ) + result = cube._summary_vector_sections_info() + self.assertEqual(len(result), 5) + self.assertEqual( + [[co.name() for co in sect[1]] for sect in result], + [ + ["lats"], + ["longitude", "co2d"], + [], # derived + [], # cell measures + [], # ancils + ], + ) + + def test_cell_measures(self): + cube = Cube(np.zeros((3, 5, 4))) + cube.add_cell_measure( + CellMeasure(np.arange(3), long_name="coeffs", units=1), 0 + ) + result = cube._summary_vector_sections_info() + self.assertEqual(len(result), 5) + self.assertEqual( + [[co.name() for co in sect[1]] for sect in result], + [ + [], # dim coords + [], # aux coords + [], # derived + ["coeffs"], # cell measures + [], # ancils + ], + ) + + def test_ancils(self): + cube = Cube(np.zeros((3, 5, 4))) + cube.add_ancillary_variable( + CellMeasure(np.zeros((3, 4)), long_name="category", units=1), + (0, 2), + ) + result = cube._summary_vector_sections_info() + self.assertEqual(len(result), 5) + self.assertEqual( + [[co.name() for co in sect[1]] for sect in result], + [ + [], # dim coords + [], # aux coords + [], # derived + [], # cell measures + ["category"], # ancils + ], + ) + + if __name__ == "__main__": tests.main() From f40aadc9bb0311291a79f5d1d6f1f7a21ff53da9 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 27 Oct 2020 15:28:13 +0000 Subject: [PATCH 10/13] Tiny docstring fixes. --- lib/iris/cube.py | 2 +- lib/iris/tests/unit/cube/test_Cube.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 8f96408fe7..ad80c48df0 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2266,7 +2266,7 @@ def _summary_extra(self, coords, summary, indent): def _summary_dim_name(self, dim): """ Get the dim_coord name that labels a data dimension, - or "--" if there is none (i.e. dimension is anonymous). + or "-- " if there is none (i.e. dimension is anonymous). """ dim_coords = self.coords(contains_dimension=dim, dim_coords=True) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 6699ecfada..95675b8881 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2458,7 +2458,8 @@ def test_unmapped_dim(self): result = cube._summary_dim_name(1) self.assertEqual(result, "-- ") - def test_aux_named_dim(self): + def test_aux_mapped_dim(self): + # It should *not* treat an aux-coord as the name of a dim. cube = Cube(np.zeros((5, 3))) cube.add_aux_coord( DimCoord(np.arange(5), standard_name="longitude", units="degrees"), @@ -2505,7 +2506,6 @@ def test_dim_aux_coords(self): AuxCoord(np.zeros((5, 4)), long_name="co2d", units=1), (1, 2), ) result = cube._summary_vector_sections_info() - self.assertEqual(len(result), 5) self.assertEqual( [[co.name() for co in sect[1]] for sect in result], [ @@ -2523,7 +2523,6 @@ def test_cell_measures(self): CellMeasure(np.arange(3), long_name="coeffs", units=1), 0 ) result = cube._summary_vector_sections_info() - self.assertEqual(len(result), 5) self.assertEqual( [[co.name() for co in sect[1]] for sect in result], [ @@ -2542,7 +2541,6 @@ def test_ancils(self): (0, 2), ) result = cube._summary_vector_sections_info() - self.assertEqual(len(result), 5) self.assertEqual( [[co.name() for co in sect[1]] for sect in result], [ From 5c9ace49f3080a53bf1eafe691a81c8b96990818 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2020 15:00:43 +0000 Subject: [PATCH 11/13] Review changes. --- lib/iris/cube.py | 25 +++++-------- lib/iris/tests/unit/cube/test_Cube.py | 51 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index ad80c48df0..6861884a33 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2318,14 +2318,13 @@ def _summary_vector_sections_info(self): dim_coords = self.dim_coords aux_coords = self.aux_coords all_coords = dim_coords + aux_coords + derived_coords - scalar_coords = [ - coord + scalar_coord_ids = [ + id(coord) for coord in all_coords if not self.coord_dims(coord) and coord.shape == (1,) ] # Determine the cube coordinates that are not scalar BUT # dimensioned. - scalar_coord_ids = set(map(id, scalar_coords)) vector_dim_coords = [ coord for coord in dim_coords if id(coord) not in scalar_coord_ids ] @@ -2344,20 +2343,14 @@ def _summary_vector_sections_info(self): ] # Ancillary Variables - vector_ancillary_variables = [av for av in self.ancillary_variables()] + vector_ancillary_variables = [ + av for av in self.ancillary_variables() if av.shape != (1,) + ] - # Sort scalar coordinates by name. - scalar_coords.sort(key=lambda coord: coord.name()) - # Sort vector coordinates by data dimension and name. - vector_dim_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - vector_aux_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - vector_derived_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) + # Sort vector coordinates by data dimension. + vector_dim_coords.sort(key=self.coord_dims) + vector_aux_coords.sort(key=self.coord_dims) + vector_derived_coords.sort(key=self.coord_dims) Spec = self._VectorSectionSpec section_specs = [ Spec("Dimension coordinates", vector_dim_coords, True), diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 95675b8881..335c81624f 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2517,6 +2517,30 @@ def test_dim_aux_coords(self): ], ) + def test_aux_coords_ordering(self): + cube = Cube(np.zeros((3, 4))) + cube.add_aux_coord( + AuxCoord(np.zeros((3, 4)), long_name="a12", units=1), (0, 1) + ) + cube.add_aux_coord( + AuxCoord(np.zeros((3, 4)), long_name="b12", units=1), (0, 1) + ) + cube.add_aux_coord(DimCoord(np.arange(3), long_name="a1", units=1), 0) + cube.add_aux_coord(DimCoord(np.arange(3), long_name="b1", units=1), 0) + cube.add_aux_coord(DimCoord(np.arange(4), long_name="b2", units=1), 1) + cube.add_aux_coord(DimCoord(np.arange(4), long_name="a2", units=1), 1) + result = cube._summary_vector_sections_info() + self.assertEqual( + [[co.name() for co in sect[1]] for sect in result], + [ + [], + ["a1", "b1", "a12", "b12", "a2", "b2"], + [], # derived + [], # cell measures + [], # ancils + ], + ) + def test_cell_measures(self): cube = Cube(np.zeros((3, 5, 4))) cube.add_cell_measure( @@ -2552,6 +2576,33 @@ def test_ancils(self): ], ) + def test_derived(self): + # Simple check for presentation of a derived coordinate. + cube = stock.hybrid_height() + result = cube._summary_vector_sections_info() + section = result[2] # just the derived part + self.assertEqual([co.name() for co in section.elements], ["altitude"]) + + def test_length1_coord_types(self): + cube = Cube(np.zeros((1, 2))) + cube.add_dim_coord(DimCoord(0.1, long_name="length1_dim"), 0) + # Note: you can't have an "unmapped" dim-coord, dims are required. + cube.add_aux_coord(DimCoord(0.2, long_name="length1_aux_mapped"), 0) + cube.add_aux_coord(DimCoord(0.3, long_name="length1_aux_unmapped")) + # Note: "length1_aux_unmapped" is a *scalar* coord, + # so it does not appear in the vector section. + result = cube._summary_vector_sections_info() + self.assertEqual( + [[co.name() for co in sect[1]] for sect in result], + [ + ["length1_dim"], # dim coords + ["length1_aux_mapped"], # aux coords + [], # derived + [], # cell measures + [], # ancils + ], + ) + if __name__ == "__main__": tests.main() From 690447eb2630b02065c1091ce5c8fb9aa296d778 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2020 17:23:33 +0000 Subject: [PATCH 12/13] Simpler calculation of vector elements, and handling of scalar elements. --- lib/iris/cube.py | 81 ++++++++++++--------------- lib/iris/tests/unit/cube/test_Cube.py | 72 +++++++++++++++++++++++- 2 files changed, 107 insertions(+), 46 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 6861884a33..9f16b899e8 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2310,47 +2310,25 @@ def _summary_vector_sections_info(self): (as in iris-ugrid) can override / extend the cube summary. """ - # Cache the derived coords so we can rely on consistent - # object IDs. - derived_coords = self.derived_coords - # Determine the cube coordinates that are scalar (single-valued) - # AND non-dimensioned. - dim_coords = self.dim_coords - aux_coords = self.aux_coords - all_coords = dim_coords + aux_coords + derived_coords - scalar_coord_ids = [ - id(coord) - for coord in all_coords - if not self.coord_dims(coord) and coord.shape == (1,) - ] - # Determine the cube coordinates that are not scalar BUT - # dimensioned. - vector_dim_coords = [ - coord for coord in dim_coords if id(coord) not in scalar_coord_ids - ] - vector_aux_coords = [ - coord for coord in aux_coords if id(coord) not in scalar_coord_ids - ] - vector_derived_coords = [ - coord - for coord in derived_coords - if id(coord) not in scalar_coord_ids - ] - - # cell measures - vector_cell_measures = [ - cm for cm in self.cell_measures() if cm.shape != (1,) - ] + # Construct our lists of different types of element. + vector_dim_coords = self.dim_coords # Always cube-dim-mapped. + + def cube_dimensioned(elements): + # Select only non-scalar coords (and cell-measures, ancils). + # These could in fact have shape (1,) : The key question is whether + # they map to a cube dimension(s) or not. + return [elem for elem in elements if elem.cube_dims(self)] + + vector_aux_coords = cube_dimensioned(self.aux_coords) + vector_derived_coords = cube_dimensioned(self.derived_coords) + vector_cell_measures = cube_dimensioned(self.cell_measures()) + vector_ancillary_variables = cube_dimensioned( + self.ancillary_variables() + ) - # Ancillary Variables - vector_ancillary_variables = [ - av for av in self.ancillary_variables() if av.shape != (1,) - ] + # Note: the element lists are in fact all sorted by (cube-dims, name). + # But that is implicit, as the cube access methods ensure it. - # Sort vector coordinates by data dimension. - vector_dim_coords.sort(key=self.coord_dims) - vector_aux_coords.sort(key=self.coord_dims) - vector_derived_coords.sort(key=self.coord_dims) Spec = self._VectorSectionSpec section_specs = [ Spec("Dimension coordinates", vector_dim_coords, True), @@ -2501,9 +2479,7 @@ def vector_summary( # Generate textual summary of cube scalar coordinates. # scalar_coords = [ - coord - for coord in self.coords() - if not self.coord_dims(coord) and coord.shape == (1,) + coord for coord in self.coords() if not self.coord_dims(coord) ] scalar_summary = [] if scalar_coords: @@ -2569,10 +2545,12 @@ def vector_summary( ) # - # Generate summary of cube cell measures + # Generate summary of scalar cube cell measures # scalar_cell_measures = [ - cm for cm in self.cell_measures() if cm.shape == (1,) + cm + for cm in self.cell_measures() + if not self.cell_measure_dims(cm) ] if scalar_cell_measures: summary += "\n Scalar cell measures:\n" @@ -2582,6 +2560,21 @@ def vector_summary( ] summary += "\n".join(scalar_cms) + # + # Generate summary of scalar ancillary variables + # + scalar_ancils = [ + av + for av in self.ancillary_variables() + if not self.ancillary_variable_dims(av) + ] + if scalar_ancils: + summary += "\n Scalar ancillary variables:\n" + scalar_cms = [ + " {}".format(av.name()) for av in scalar_ancils + ] + summary += "\n".join(scalar_cms) + # # Generate summary of cube attributes. # diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 335c81624f..71f8dcd3f6 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -484,6 +484,37 @@ def test_ancillary_variable(self): ) self.assertEqual(cube.summary(), expected_summary) + def test_other_scalar_elements(self): + # Check for scalar cell-measures and ancils, and the distinction + # between 1-D vector elements and scalar ones. + cube = Cube(np.zeros((1, 4))) + cube.add_cell_measure( + CellMeasure(1, long_name="len1_cm_DIMED", units=1), (0,) + ) + cube.add_cell_measure( + CellMeasure(1, long_name="len1_cm_SCALAR", units=1), () + ) + cube.add_ancillary_variable( + AncillaryVariable(2, long_name="len1_av_DIMED", units=1), (0.0) + ) + cube.add_ancillary_variable( + AncillaryVariable(2, long_name="len1_av_SCALAR", units=1), () + ) + + result = cube.summary() + expected = """\ +unknown / (unknown) (-- : 1; -- : 4) + Cell measures: + len1_cm_DIMED x - + Ancillary variables: + len1_av_DIMED x - + Scalar cell measures: + len1_cm_SCALAR + Scalar ancillary variables: + len1_av_SCALAR\ +""" + self.assertEqual(result, expected) + class Test_is_compatible(tests.IrisTest): def setUp(self): @@ -2517,23 +2548,30 @@ def test_dim_aux_coords(self): ], ) - def test_aux_coords_ordering(self): + def test_ordering(self): + # Check that sections are sorted by (cube-dims, name). cube = Cube(np.zeros((3, 4))) + cube.add_dim_coord(DimCoord(np.arange(3), long_name="z", units=1), 0) + cube.add_dim_coord(DimCoord(np.arange(4), long_name="x", units=1), 1) + cube.add_aux_coord( AuxCoord(np.zeros((3, 4)), long_name="a12", units=1), (0, 1) ) cube.add_aux_coord( AuxCoord(np.zeros((3, 4)), long_name="b12", units=1), (0, 1) ) + cube.add_aux_coord(DimCoord(np.arange(3), long_name="a1", units=1), 0) cube.add_aux_coord(DimCoord(np.arange(3), long_name="b1", units=1), 0) + cube.add_aux_coord(DimCoord(np.arange(4), long_name="b2", units=1), 1) cube.add_aux_coord(DimCoord(np.arange(4), long_name="a2", units=1), 1) + result = cube._summary_vector_sections_info() self.assertEqual( [[co.name() for co in sect[1]] for sect in result], [ - [], + ["z", "x"], ["a1", "b1", "a12", "b12", "a2", "b2"], [], # derived [], # cell measures @@ -2603,6 +2641,36 @@ def test_length1_coord_types(self): ], ) + def test_length1_other_elements(self): + # Check for scalar cell-measures and ancils, and the distinction + # between 1-D vector elements and scalar ones. + cube = Cube(np.zeros((1, 4))) + cube.add_cell_measure( + CellMeasure(1, long_name="len1_cm_DIMED", units=1), (0,) + ) + cube.add_ancillary_variable( + AncillaryVariable(2, long_name="len1_av_DIMED", units=1), (0.0) + ) + # These 2 should *not* appear, as they are "scalar"s (no cube dims). + cube.add_cell_measure( + CellMeasure(1, long_name="len1_cm_SCALAR", units=1), () + ) + cube.add_ancillary_variable( + AncillaryVariable(2, long_name="len1_av_SCALAR", units=1), () + ) + + result = cube._summary_vector_sections_info() + self.assertEqual( + [[co.name() for co in sect[1]] for sect in result], + [ + [], # dim coords + [], # aux coords + [], # derived + ["len1_cm_DIMED"], # cell measures + ["len1_av_DIMED"], # ancils + ], + ) + if __name__ == "__main__": tests.main() From 6b96ecb32bafe25ba3c424426417f11fdc8a07f4 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2020 17:43:38 +0000 Subject: [PATCH 13/13] Test fix. --- lib/iris/tests/unit/cube/test_Cube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 71f8dcd3f6..827b0ff4b6 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2599,7 +2599,7 @@ def test_cell_measures(self): def test_ancils(self): cube = Cube(np.zeros((3, 5, 4))) cube.add_ancillary_variable( - CellMeasure(np.zeros((3, 4)), long_name="category", units=1), + AncillaryVariable(np.zeros((3, 4)), long_name="category", units=1), (0, 2), ) result = cube._summary_vector_sections_info()