diff --git a/hatchet/external/console.py b/hatchet/external/console.py index 613b523a..74fb41e1 100644 --- a/hatchet/external/console.py +++ b/hatchet/external/console.py @@ -70,6 +70,10 @@ def render(self, roots, dataframe, **kwargs): self.colormap_annotations = kwargs["colormap_annotations"] self.min_value = kwargs["min_value"] self.max_value = kwargs["max_value"] + try: + self.modeler_config = kwargs["modeler_config"] + except KeyError: + self.modeler_config = None if self.color: self.colors = self.colors_enabled @@ -88,7 +92,8 @@ def render(self, roots, dataframe, **kwargs): elif isinstance(self.colormap_annotations, list): self.colors_annotations.colormap = self.colormap_annotations self.colors_annotations_mapping = sorted( - list(dataframe[self.annotation_column].apply(str).unique()) + list(dataframe[self.modeler_config][self.annotation_column].apply( + str).unique()) ) elif isinstance(self.colormap_annotations, dict): self.colors_annotations_mapping = self.colormap_annotations @@ -113,7 +118,7 @@ def render(self, roots, dataframe, **kwargs): self.primary_metric = self.metric_columns[0] self.second_metric = None - if self.primary_metric not in dataframe.columns: + if self.primary_metric not in dataframe.columns and self.primary_metric not in dataframe[self.modeler_config].columns: raise KeyError( "metric_column={} does not exist in the dataframe, please select a valid column. See a list of the available metrics with GraphFrame.show_metric_columns().".format( self.primary_metric @@ -133,9 +138,13 @@ def render(self, roots, dataframe, **kwargs): # nan values if "rank" in dataframe.index.names: - metric_series = (dataframe.xs(self.rank, level=1))[self.primary_metric] + metric_series = (dataframe.xs(self.rank, level=1))[ + self.primary_metric] else: - metric_series = dataframe[self.primary_metric] + if self.modeler_config is not None and self.primary_metric not in dataframe.columns: + metric_series = dataframe[self.modeler_config][self.primary_metric] + else: + metric_series = dataframe[self.primary_metric] isfinite_mask = np.isfinite(metric_series.values) filtered_series = pd.Series( metric_series.values[isfinite_mask], metric_series.index[isfinite_mask] @@ -153,7 +162,7 @@ def render(self, roots, dataframe, **kwargs): result += self.render_frame(root, dataframe) if self.color is True: - result += self.render_legend() + result += self.render_legend(dataframe) if self.unicode: return result @@ -167,14 +176,15 @@ def render_preamble(self): r" / /_ ____ _/ /______/ /_ ___ / /_", r" / __ \/ __ `/ __/ ___/ __ \/ _ \/ __/", r" / / / / /_/ / /_/ /__/ / / / __/ /_ ", - r"/_/ /_/\__,_/\__/\___/_/ /_/\___/\__/ {:>2}".format("v" + __version__), + r"/_/ /_/\__,_/\__/\___/_/ /_/\___/\__/ {:>2}".format( + "v" + __version__), r"", r"", ] return "\n".join(lines) - def render_legend(self): + def render_legend(self, dataframe): def render_label(index, low, high): metric_range = self.max_metric - self.min_metric @@ -207,7 +217,8 @@ def render_label(index, low, high): legend += render_label(4, 0.1, 0.3) legend += render_label(5, 0.0, 0.1) - legend += "\n" + self._ansi_color_for_name("name") + "name" + self.colors.end + legend += "\n" + \ + self._ansi_color_for_name("name") + "name" + self.colors.end legend += " User code " legend += self.colors.left + self.lr_arrows["◀"] + self.colors.end @@ -216,6 +227,49 @@ def render_label(index, low, high): legend += " Only in right graph\n" if self.annotation_column is not None: + + # extra-p model complexity analysis legend customization + if "_complexity" in self.annotation_column: + + # get unique complexity classes from all models + unique_complexity_classes = self.get_unique_complexity_classes( + dataframe) + + # add color coding for complexity classes to data frame + color_map_dict = self.colormap_for_complexity_classes( + unique_complexity_classes) + + legend += "\n\033[4mLegend Complexity Classes" + \ + self.colors.end + + for complexity_class in unique_complexity_classes: + legend += "\n" + color_map_dict[complexity_class] + u"█ " + \ + self.colors.end + str(complexity_class) + legend += "\n" + + # create a legend for the model parameters + legend += "\n\033[4mLegend Model Parameters" + \ + self.colors.end + if self.modeler_config is None: + column_headers = list(dataframe.columns.values) + else: + column_headers = list( + dataframe[self.modeler_config].columns.values) + column_name = None + for column in column_headers: + if "_extrap-model" in column and "AR2" not in column and "RE" not in column and "RSS" not in column and "SMAPE" not in column and "coefficient" not in column and "complexity" not in column and "growth_rank" not in column: + column_name = column + break + if self.modeler_config is None: + model_wrapper_object = dataframe[column_name].iloc[0] + else: + model_wrapper_object = dataframe[self.modeler_config][column_name].iloc[0] + # Avg time/rank_extrap-model + for i in range(len(model_wrapper_object.parameters)): + legend += "\n" + \ + str(model_wrapper_object.default_param_names[i]) + " -> " + \ + str(model_wrapper_object.parameters[i]) + # temporal pattern legend customization if "_pattern" in self.annotation_column: score_ranges = [0.0, 0.2, 0.4, 0.6, 1.0] @@ -261,137 +315,263 @@ def render_frame(self, node, dataframe, indent="", child_indent=""): else: df_index = node - node_metric = dataframe.loc[df_index, self.primary_metric] - - metric_precision = "{:." + str(self.precision) + "f}" - metric_str = ( - self._ansi_color_for_metric(node_metric) - + metric_precision.format(node_metric) - + self.colors.end - ) - - if self.second_metric is not None: - metric_str += " {c.faint}{second_metric:.{precision}f}{c.end}".format( - second_metric=dataframe.loc[df_index, self.second_metric], - precision=self.precision, - c=self.colors, + try: + if self.modeler_config is not None and self.primary_metric not in dataframe.columns: + node_metric = dataframe[self.modeler_config].loc[df_index, + self.primary_metric] + else: + node_metric = dataframe.loc[df_index, + self.primary_metric] + if self.modeler_config is not None: + node_metric = float(node_metric) + + metric_precision = "{:." + str(self.precision) + "f}" + metric_str = ( + self._ansi_color_for_metric(node_metric) + + metric_precision.format(node_metric) + + self.colors.end ) - if self.annotation_column is not None: - annotation_content = str( - dataframe.loc[df_index, self.annotation_column] - ) + if self.second_metric is not None: + metric_str += " {c.faint}{second_metric:.{precision}f}{c.end}".format( + second_metric=dataframe.loc[df_index, + self.second_metric], + precision=self.precision, + c=self.colors, + ) - # custom visualization for temporal pattern metrics if it is the annotation column - if "_pattern" in self.annotation_column: - self.temporal_symbols = { - "none": "", - "constant": "\U00002192", - "phased": "\U00002933", - "dynamic": "\U000021DD", - "sporadic": "\U0000219D", - } - pattern_metric = dataframe.loc[df_index, self.annotation_column] - annotation_content = self.temporal_symbols[pattern_metric] - if self.colormap_annotations: - self.colors_annotations_mapping = list( - dataframe[self.annotation_column].apply(str).unique() + if self.annotation_column is not None: + if self.modeler_config is None: + annotation_content = str( + dataframe.loc[df_index, self.annotation_column] + ) + else: + annotation_content = str( + dataframe[self.modeler_config].loc[df_index, + self.annotation_column] ) - coloring_content = pattern_metric - if coloring_content != "none": + + # custom visualization for complexity class analysis with extra-p models + if "_complexity" in self.annotation_column: + + # get unique complexity classes from all models + unique_complexity_classes = self.get_unique_complexity_classes( + dataframe) + + # add color coding for complexity classes to data frame + color_map_dict = self.colormap_for_complexity_classes( + unique_complexity_classes) + + metric_str += "{}".format( + color_map_dict[annotation_content]) + + # custom visualization for temporal pattern metrics if it is the annotation column + if "_pattern" in self.annotation_column: + self.temporal_symbols = { + "none": "", + "constant": "\U00002192", + "phased": "\U00002933", + "dynamic": "\U000021DD", + "sporadic": "\U0000219D", + } + pattern_metric = dataframe.loc[df_index, + self.annotation_column] + annotation_content = self.temporal_symbols[pattern_metric] + if self.colormap_annotations: + self.colors_annotations_mapping = list( + dataframe[self.annotation_column].apply( + str).unique() + ) + coloring_content = pattern_metric + if coloring_content != "none": + color_annotation = self.colors_annotations.colormap[ + self.colors_annotations_mapping.index( + coloring_content) + % len(self.colors_annotations.colormap) + ] + metric_str += " {}".format(color_annotation) + metric_str += "{}".format(annotation_content) + metric_str += "{}".format( + self.colors_annotations.end) + else: + metric_str += "{}".format(annotation_content) + else: # no colormap passed in + metric_str += " {}".format(annotation_content) + + # no pattern column + elif self.colormap_annotations: + if isinstance(self.colormap_annotations, dict): + color_annotation = self.colors_annotations_mapping[ + annotation_content + ] + else: color_annotation = self.colors_annotations.colormap[ - self.colors_annotations_mapping.index(coloring_content) + self.colors_annotations_mapping.index( + annotation_content) % len(self.colors_annotations.colormap) ] - metric_str += " {}".format(color_annotation) + metric_str += " [{}".format(color_annotation) + metric_str += "{}".format(annotation_content) + metric_str += "{}]".format("\033[0m") + + else: + if self.colormap_annotations: + if isinstance(self.colormap_annotations, dict): + color_annotation = self.colors_annotations_mapping[ + annotation_content + ] + else: + color_annotation = self.colors_annotations.colormap[ + self.colors_annotations_mapping.index( + annotation_content) + % len(self.colors_annotations.colormap) + ] + metric_str += " [{}".format(color_annotation) metric_str += "{}".format(annotation_content) - metric_str += "{}".format(self.colors_annotations.end) + metric_str += "{}]".format( + self.colors_annotations.end) + else: - metric_str += "{}".format(annotation_content) - else: # no colormap passed in - metric_str += " {}".format(annotation_content) - # no pattern column - elif self.colormap_annotations: - if isinstance(self.colormap_annotations, dict): - color_annotation = self.colors_annotations_mapping[ - annotation_content - ] - else: - color_annotation = self.colors_annotations.colormap[ - self.colors_annotations_mapping.index(annotation_content) - % len(self.colors_annotations.colormap) - ] - metric_str += " [{}".format(color_annotation) - metric_str += "{}".format(annotation_content) - metric_str += "{}]".format(self.colors_annotations.end) + metric_str += " [{}]".format(annotation_content) + + if self.modeler_config is None: + node_name = dataframe.loc[df_index, self.name] else: - metric_str += " [{}]".format(annotation_content) + node_name = dataframe[self.modeler_config].loc[df_index, self.name] + if self.expand is False: + if len(node_name) > 39: + node_name = ( + node_name[:18] + "..." + + node_name[(len(node_name) - 18):] + ) + name_str = ( + self._ansi_color_for_name( + node_name) + node_name + self.colors.end + ) - node_name = dataframe.loc[df_index, self.name] - if self.expand is False: - if len(node_name) > 39: - node_name = ( - node_name[:18] + "..." + node_name[(len(node_name) - 18) :] - ) - name_str = ( - self._ansi_color_for_name(node_name) + node_name + self.colors.end - ) + # 0 is "", 1 is "L", and 2 is "R" + if "_missing_node" in dataframe.columns: + left_or_right = dataframe.loc[df_index, "_missing_node"] + if left_or_right == 0: + lr_decorator = "" + elif left_or_right == 1: + lr_decorator = " {c.left}{decorator}{c.end}".format( + decorator=self.lr_arrows["◀"], c=self.colors + ) + elif left_or_right == 2: + lr_decorator = " {c.right}{decorator}{c.end}".format( + decorator=self.lr_arrows["▶"], c=self.colors + ) - # 0 is "", 1 is "L", and 2 is "R" - if "_missing_node" in dataframe.columns: - left_or_right = dataframe.loc[df_index, "_missing_node"] - if left_or_right == 0: - lr_decorator = "" - elif left_or_right == 1: - lr_decorator = " {c.left}{decorator}{c.end}".format( - decorator=self.lr_arrows["◀"], c=self.colors - ) - elif left_or_right == 2: - lr_decorator = " {c.right}{decorator}{c.end}".format( - decorator=self.lr_arrows["▶"], c=self.colors + result = "{indent}{metric_str} {name_str}".format( + indent=indent, metric_str=metric_str, name_str=name_str + ) + if "_missing_node" in dataframe.columns: + result += lr_decorator + if self.context in dataframe.columns: + result += u" {c.faint}{context}{c.end}\n".format( + context=dataframe.loc[df_index, + self.context], c=self.colors ) + else: + result += "\n" - result = "{indent}{metric_str} {name_str}".format( - indent=indent, metric_str=metric_str, name_str=name_str - ) - if "_missing_node" in dataframe.columns: - result += lr_decorator - if self.context in dataframe.columns: - result += " {c.faint}{context}{c.end}\n".format( - context=dataframe.loc[df_index, self.context], c=self.colors - ) - else: - result += "\n" + if self.unicode: + indents = {"├": "├─ ", "│": "│ ", "└": "└─ ", " ": " "} + else: + indents = {"├": "|- ", "│": "| ", "└": "`- ", " ": " "} + + # ensures that we never revisit nodes in the case of + # large complex graphs + if node not in self.visited: + self.visited.append(node) + sorted_children = sorted( + node.children, key=lambda n: n._hatchet_nid) + if sorted_children: + last_child = sorted_children[-1] + + for child in sorted_children: + if child is not last_child: + c_indent = child_indent + indents["├"] + cc_indent = child_indent + indents["│"] + else: + c_indent = child_indent + indents["└"] + cc_indent = child_indent + indents[" "] + result += self.render_frame( + child, dataframe, indent=c_indent, child_indent=cc_indent + ) + + except KeyError: + result = "" + indents = {"├": "", "│": "", "└": "", " ": ""} - if self.unicode: - indents = {"├": "├─ ", "│": "│ ", "└": "└─ ", " ": " "} - else: - indents = {"├": "|- ", "│": "| ", "└": "`- ", " ": " "} - - # ensures that we never revisit nodes in the case of - # large complex graphs - if node not in self.visited: - self.visited.append(node) - sorted_children = sorted(node.children, key=lambda n: n._hatchet_nid) - if sorted_children: - last_child = sorted_children[-1] - - for child in sorted_children: - if child is not last_child: - c_indent = child_indent + indents["├"] - cc_indent = child_indent + indents["│"] - else: - c_indent = child_indent + indents["└"] - cc_indent = child_indent + indents[" "] - result += self.render_frame( - child, dataframe, indent=c_indent, child_indent=cc_indent - ) else: result = "" indents = {"├": "", "│": "", "└": "", " ": ""} return result + def get_unique_complexity_classes(self, dataframe): + unique_complexity_classes = [] + if self.modeler_config is None: + for i in range(len(dataframe[self.annotation_column])): + if str(dataframe[self.annotation_column].iloc[i]) not in unique_complexity_classes: + unique_complexity_classes.append( + str(dataframe[self.annotation_column].iloc[i])) + else: + for i in range(len(dataframe[self.modeler_config][self.annotation_column])): + if str(dataframe[self.modeler_config][self.annotation_column].iloc[i]) not in unique_complexity_classes: + unique_complexity_classes.append( + str(dataframe[self.modeler_config][self.annotation_column].iloc[i])) + return unique_complexity_classes + + def colormap_for_complexity_classes(self, unique_complexity_classes): + color_map_dict = {} + range_values = np.arange( + 0, 1, 1 / len(unique_complexity_classes)) + import matplotlib + # chose the color map to take the colors from dynamically + if self.colormap_annotations: + if isinstance(self.colormap_annotations, str): + colormap = self.colormap_annotations + else: + if len(unique_complexity_classes) > 20: + colormap = "brg" + else: + colormap = "tab20b" + if colormap != "black": + cmap = matplotlib.cm.get_cmap(colormap) + for i in range(len(range_values)): + red = int(cmap(range_values[i])[0] / (1 / 255)) + green = int(cmap(range_values[i])[1] / (1 / 255)) + blue = int(cmap(range_values[i])[2] / (1 / 255)) + ansi_color_str = ( + "\033[38;2;" + + str(red) + + ";" + + str(green) + + ";" + + str(blue) + + "m" + ) + color_map_dict[unique_complexity_classes[i] + ] = ansi_color_str + else: + for i in range(len(range_values)): + ansi_color_str = ( + "\033[38;2;" + + str(0) + + ";" + + str(0) + + ";" + + str(0) + + "m" + ) + color_map_dict[unique_complexity_classes[i] + ] = ansi_color_str + + return color_map_dict + def _ansi_color_for_metric(self, metric): metric_range = self.max_metric - self.min_metric diff --git a/hatchet/graphframe.py b/hatchet/graphframe.py index 822720a8..784a880c 100644 --- a/hatchet/graphframe.py +++ b/hatchet/graphframe.py @@ -974,6 +974,7 @@ def tree( render_header=True, min_value=None, max_value=None, + modeler_config=None ): """Visualize the Hatchet graphframe as a tree @@ -994,6 +995,7 @@ def tree( render_header (bool, optional): Shows the Preamble. Defaults to True. min_value (int, optional): Overwrites the min value for the coloring legend. Defaults to None. max_value (int, optional): Overwrites the max value for the coloring legend. Defaults to None. + modeler_config (str, optional): Used when using Extra-P modeler configurations in the thicket dataframe to access the correct multi-column index. Returns: str: String representation of the tree, ready to print @@ -1019,26 +1021,50 @@ def tree( elif sys.version_info.major == 3: unicode = True - return ConsoleRenderer(unicode=unicode, color=color).render( - self.graph.roots, - self.dataframe, - metric_column=metric_column, - annotation_column=annotation_column, - precision=precision, - name_column=name_column, - expand_name=expand_name, - context_column=context_column, - rank=rank, - thread=thread, - depth=depth, - highlight_name=highlight_name, - colormap=colormap, - invert_colormap=invert_colormap, - colormap_annotations=colormap_annotations, - render_header=render_header, - min_value=min_value, - max_value=max_value, - ) + if modeler_config is None: + return ConsoleRenderer(unicode=unicode, color=color).render( + self.graph.roots, + self.dataframe, + metric_column=metric_column, + annotation_column=annotation_column, + precision=precision, + name_column=name_column, + expand_name=expand_name, + context_column=context_column, + rank=rank, + thread=thread, + depth=depth, + highlight_name=highlight_name, + colormap=colormap, + invert_colormap=invert_colormap, + colormap_annotations=colormap_annotations, + render_header=render_header, + min_value=min_value, + max_value=max_value, + ) + else: + return ConsoleRenderer(unicode=unicode, color=color).render( + self.graph.roots, + self.dataframe, + metric_column=metric_column, + annotation_column=annotation_column, + precision=precision, + name_column=name_column, + expand_name=expand_name, + context_column=context_column, + rank=rank, + thread=thread, + depth=depth, + highlight_name=highlight_name, + colormap=colormap, + invert_colormap=invert_colormap, + colormap_annotations=colormap_annotations, + render_header=render_header, + min_value=min_value, + max_value=max_value, + modeler_config=modeler_config, + ) + def to_dot(self, metric=None, name="name", rank=0, thread=0, threshold=0.0): """Write the graph in the graphviz dot format: